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;
}
// 检查订阅规则是否改变,是否能够静默更新