From f37c2dc0beedde2c8a8376f3e6452b1dc27c6a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Sat, 13 Jun 2026 11:07:06 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D=20Edge=20Andr?= =?UTF-8?q?oid=20=E7=A7=BB=E5=8A=A8=E7=AB=AF=20popup=20=E9=80=82=E9=85=8D?= =?UTF-8?q?=E9=97=AE=E9=A2=98=20(#686)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两处移动端问题,根因不同分别修复: 1. popup 右侧留白:popup.html 把 html/body 宽度写死 320px。桌面端 popup 视口宽度恒等于 body 宽度(320px),移动端被强制撑满设备宽度(≥360px),固定 320px 导致右侧空白。改用媒体查询 @media (min-width:340px) 在移动端切换为 width:100%、max-height:none,桌面端不命中、行为零变化。 2. 点击「设置」等打不开内部页:window.open 在 Edge Android 上无法打开 chrome-extension:// 内部页(外部 https 网址正常)。将 6 处打开 /src/*.html 的 window.open 改为 openInCurrentTab(chrome.tabs.create):popup 的设置/新建 脚本、ScriptMenuList 的编辑/用户配置、MainLayout 拖拽导入、Tools 数据导入。 openInCurrentTab 增加返回创建出来的标签(chrome.tabs.Tab|undefined),供 MainLayout 判断是否成功打开;同步补充单元测试,并让 chrome-extension-mock 的 tabs.create 返回 Promise 以贴近真实 MV3。 --- packages/chrome-extension-mock/tab.ts | 6 ++--- src/pages/components/ScriptMenuList/index.tsx | 11 +++++--- src/pages/components/layout/MainLayout.tsx | 8 +++--- src/pages/options/routes/Tools.tsx | 6 ++--- src/pages/popup.html | 10 +++++++ src/pages/popup/App.tsx | 9 ++++--- src/pkg/utils/utils.test.ts | 27 +++++++++++++++++++ src/pkg/utils/utils.ts | 9 +++---- 8 files changed, 63 insertions(+), 23 deletions(-) diff --git a/packages/chrome-extension-mock/tab.ts b/packages/chrome-extension-mock/tab.ts index c747c55c6..8c4cb8f5a 100644 --- a/packages/chrome-extension-mock/tab.ts +++ b/packages/chrome-extension-mock/tab.ts @@ -26,9 +26,9 @@ export default class MockTab { create(createProperties: chrome.tabs.CreateProperties, callback?: (tab: chrome.tabs.Tab) => void) { this.hook.emit("create", createProperties); - callback?.({ - id: 1, - } as chrome.tabs.Tab); + const tab = { id: 1 } as chrome.tabs.Tab; + callback?.(tab); + return Promise.resolve(tab); } remove(tabId: number) { diff --git a/src/pages/components/ScriptMenuList/index.tsx b/src/pages/components/ScriptMenuList/index.tsx index 27448deb7..ea6b926ff 100644 --- a/src/pages/components/ScriptMenuList/index.tsx +++ b/src/pages/components/ScriptMenuList/index.tsx @@ -33,6 +33,7 @@ import type { ScriptMenuItemOption, } from "@App/app/service/service_worker/types"; import { popupClient, runtimeClient, scriptClient } from "@App/pages/store/features/script"; +import { openInCurrentTab } from "@App/pkg/utils/utils"; import { i18nName } from "@App/locales/locales"; // 用于读取 metadata @@ -245,8 +246,9 @@ const ListMenuItem = React.memo( className="tw-text-left" type="secondary" icon={} - onClick={() => { - window.open(`/src/options.html#/script/editor/${item.uuid}`, "_blank"); + onClick={async () => { + // 经由扩展 API 打开,兼容 Edge Android(移动端 window.open 打不开内部页,#686) + await openInCurrentTab(`/src/options.html#/script/editor/${item.uuid}`); window.close(); }} > @@ -297,8 +299,9 @@ const ListMenuItem = React.memo( key="config" type="secondary" icon={} - onClick={() => { - window.open(`/src/options.html#/?userConfig=${item.uuid}`, "_blank"); + onClick={async () => { + // 经由扩展 API 打开,兼容 Edge Android(移动端 window.open 打不开内部页,#686) + await openInCurrentTab(`/src/options.html#/?userConfig=${item.uuid}`); window.close(); }} > diff --git a/src/pages/components/layout/MainLayout.tsx b/src/pages/components/layout/MainLayout.tsx index 34c92742d..d022825b5 100644 --- a/src/pages/components/layout/MainLayout.tsx +++ b/src/pages/components/layout/MainLayout.tsx @@ -35,7 +35,7 @@ import "./index.css"; import { arcoLocale } from "@App/locales/arco"; import { prepareScriptByCode } from "@App/pkg/utils/script"; import { saveHandle } from "@App/pkg/utils/filehandle-db"; -import { makeBlobURL } from "@App/pkg/utils/utils"; +import { makeBlobURL, openInCurrentTab } from "@App/pkg/utils/utils"; import ScrollBoundary from "@App/pages/components/layout/ScrollBoundary"; // --- 工具函数移出组件外,避免每次 Render 重新定义 --- @@ -244,9 +244,9 @@ const MainLayout: React.FC<{ } const fid = checkOk[1].value; await saveHandle(fid, fileHandle); // fileHandle以DB方式传送至安装页面 - // 打开安装页面 - const installWindow = window.open(`/src/install.html?file=${fid}`, "_blank"); - if (!installWindow) { + // 打开安装页面(经由扩展 API,兼容 Edge Android —— 移动端 window.open 打不开内部页,#686) + const installTab = await openInCurrentTab(`/src/install.html?file=${fid}`); + if (!installTab) { throw new Error(t("install_page_open_failed")); } stat.success++; diff --git a/src/pages/options/routes/Tools.tsx b/src/pages/options/routes/Tools.tsx index b55d0265a..074b584d2 100644 --- a/src/pages/options/routes/Tools.tsx +++ b/src/pages/options/routes/Tools.tsx @@ -28,7 +28,7 @@ import { useSystemConfig } from "./utils"; import { uuidv4 } from "@App/pkg/utils/uuid"; import { cacheInstance } from "@App/app/cache"; import { CACHE_KEY_IMPORT_FILE } from "@App/app/cache_key"; -import { makeBlobURL } from "@App/pkg/utils/utils"; +import { makeBlobURL, openInCurrentTab } from "@App/pkg/utils/utils"; const openImportWindow = async (filename: string, file: Blob) => { // 打开导入窗口,用cache实现数据交互 @@ -39,8 +39,8 @@ const openImportWindow = async (filename: string, file: Blob) => { filename: filename, url: url, }); - // 打开导入窗口,用cache实现数据交互 - window.open(chrome.runtime.getURL(`/src/import.html?uuid=${uuid}`), "_blank"); + // 打开导入窗口,用cache实现数据交互(经由扩展 API,兼容 Edge Android) + await openInCurrentTab(`/src/import.html?uuid=${uuid}`); }; function Tools() { diff --git a/src/pages/popup.html b/src/pages/popup.html index ad12fe28c..f87a81dd9 100644 --- a/src/pages/popup.html +++ b/src/pages/popup.html @@ -15,6 +15,16 @@ min-height: 150px; max-height: 500px; } + /* 桌面端 popup 的视口宽度恒等于 body 宽度(320px),永远不会命中此查询,行为不变; + 移动端(如 Edge Android)popup 被强制撑满设备宽度(≥360px),命中后填满外层容器, + 消除右侧留白(#686)。阈值取 340px:高于桌面 320px、低于最小手机宽度。 */ + @media (min-width: 340px) { + html, + body { + width: 100%; + max-height: none; + } + } diff --git a/src/pages/popup/App.tsx b/src/pages/popup/App.tsx index 33ffde740..26d4baa86 100644 --- a/src/pages/popup/App.tsx +++ b/src/pages/popup/App.tsx @@ -22,7 +22,7 @@ import { popupClient, requestOpenBatchUpdatePage } from "@App/pages/store/featur import type { ScriptMenu, TPopupScript } from "@App/app/service/service_worker/types"; import { systemConfig } from "@App/pages/store/global"; import { isChineseUser, localePath } from "@App/locales/locales"; -import { getCurrentTab } from "@App/pkg/utils/utils"; +import { getCurrentTab, openInCurrentTab } from "@App/pkg/utils/utils"; import { subscribeMessage } from "@App/pages/store/global"; import type { TDeleteScript, TEnableScript, TScriptRunStatus } from "@App/app/service/queue"; import { SCRIPT_RUN_STATUS_RUNNING } from "@App/app/repo/scripts"; @@ -309,8 +309,9 @@ function App() { systemConfig.setEnableScript(val); }, handleSettingsClick: () => { - // 使用 window.open 而非 连结:避免 Vivaldi 等浏览器偶发崩溃 - window.open("/src/options.html", "_blank"); + // 经由扩展 API 打开(而非 window.open / ):既避免 Vivaldi 偶发崩溃, + // 也兼容 Edge Android —— 移动端 window.open 打不开 chrome-extension:// 内部页(#686) + openInCurrentTab("/src/options.html"); }, handleNotificationClick: () => { setShowAlert((prev) => !prev); @@ -341,7 +342,7 @@ function App() { await chrome.storage.local.set({ activeTabUrl: { url: currentUrl }, }); - window.open("/src/options.html#/script/editor?target=initial", "_blank"); + await openInCurrentTab("/src/options.html#/script/editor?target=initial"); break; case "checkUpdate": requestOpenBatchUpdatePage(getUrlDomain(currentUrl)); diff --git a/src/pkg/utils/utils.test.ts b/src/pkg/utils/utils.test.ts index 80cd69f57..e148c91fe 100644 --- a/src/pkg/utils/utils.test.ts +++ b/src/pkg/utils/utils.test.ts @@ -5,6 +5,7 @@ import { cleanFileName, formatBytes, normalizeResponseHeaders, + openInCurrentTab, stringMatching, stripUndefined, toCamelCase, @@ -704,3 +705,29 @@ describe("stripUndefined", () => { expect(result).toEqual({ a: [1, 2, 3] }); }); }); + +describe("openInCurrentTab", () => { + // 在 Edge Android 等移动端,window.open 打不开 chrome-extension:// 内部页, + // 内部页必须经由扩展 API(chrome.tabs.create)打开(见 #686)。 + it("应通过 chrome.tabs.create 在当前标签页之后打开内部页", async () => { + let created: chrome.tabs.CreateProperties | undefined; + const onCreate = (props: chrome.tabs.CreateProperties) => { + created = props; + }; + (chrome.tabs as any).hook.on("create", onCreate); + try { + await openInCurrentTab("/src/options.html"); + } finally { + (chrome.tabs as any).hook.removeListener("create", onCreate); + } + expect(created?.url).toBe("/src/options.html"); + // getCurrentTab 返回 index:0 的标签,新标签应排在其后 + expect(created?.index).toBe(1); + }); + + // MainLayout 拖拽导入据返回值判断是否成功打开安装页,因此必须回传创建出来的标签 + it("应返回创建出来的标签供调用方判断是否成功打开", async () => { + const tab = await openInCurrentTab("/src/install.html?file=abc"); + expect(tab?.id).toBe(1); + }); +}); diff --git a/src/pkg/utils/utils.ts b/src/pkg/utils/utils.ts index a64fd14ec..b86781901 100644 --- a/src/pkg/utils/utils.ts +++ b/src/pkg/utils/utils.ts @@ -111,7 +111,7 @@ export async function getTab(tabId: number) { } // 在当前页后打开一个新页面,如果指定tabId则在该tab后打开 -export async function openInCurrentTab(url: string, tabId?: number) { +export async function openInCurrentTab(url: string, tabId?: number): Promise { const tab = await (tabId ? getTab(tabId) : getCurrentTab()); const createProperties: chrome.tabs.CreateProperties = { url }; if (tab) { @@ -128,8 +128,7 @@ export async function openInCurrentTab(url: string, tabId?: number) { } // 先尝试以 openerTabId 和 windowId 打开 try { - await chrome.tabs.create(createProperties); - return; + return await chrome.tabs.create(createProperties); } catch (e: any) { console.error("Error opening tab:", e); } @@ -137,11 +136,11 @@ export async function openInCurrentTab(url: string, tabId?: number) { delete createProperties.openerTabId; delete createProperties.windowId; try { - await chrome.tabs.create(createProperties); - return; + return await chrome.tabs.create(createProperties); } catch (e: any) { console.error("Retry opeing tab error:", e); } + return undefined; } // 检查订阅规则是否改变,是否能够静默更新