From bf3c38a460e11edbb9123e86373f7d01760983c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Sun, 21 Dec 2025 00:35:05 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E5=A4=84=E7=90=86popup=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/components/ScriptMenuList/index.tsx | 17 +++++------ src/pages/popup/App.tsx | 28 ++++--------------- 2 files changed, 13 insertions(+), 32 deletions(-) diff --git a/src/pages/components/ScriptMenuList/index.tsx b/src/pages/components/ScriptMenuList/index.tsx index 934b62c29..4d7257caa 100644 --- a/src/pages/components/ScriptMenuList/index.tsx +++ b/src/pages/components/ScriptMenuList/index.tsx @@ -329,8 +329,6 @@ type ScriptMenuEntry = ScriptMenu & { metadata: SCMetadata; }; -let scriptDataAsyncCounter = 0; - // Popup 页面使用的脚本/选单清单元件:只负责渲染与互动,状态与持久化交由外部 client 处理。 const ScriptMenuList = React.memo( ({ @@ -350,7 +348,6 @@ const ScriptMenuList = React.memo( const [extraData, setExtraData] = useState< | { uuids: string; - lang: string; metadata: Record; } | undefined @@ -419,12 +416,11 @@ const ScriptMenuList = React.memo( // string memo 避免 uuids 以外的改变影响 const uuids = useMemo(() => script.map((item) => item.uuid).join("\n"), [script]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const lang = useMemo(() => i18nLang(), [t]); // 当 t 改变时,重新检查当前页面语言 // 以 异步方式 取得 metadata 放入 extraData // script 或 extraData 的更新时都会再次执行 useEffect(() => { + let isMounted = true; if (extraData && extraData.uuids === uuids && extraData.lang === lang) { // extraData 已取得 // 把 getPopupData() 的 scriptMenuList 和 异步结果 的 metadata 合并至 scriptMenuList @@ -433,10 +429,8 @@ const ScriptMenuList = React.memo( updateScriptMenuList(newScriptMenuList); } else { // 取得 extraData - scriptDataAsyncCounter = (scriptDataAsyncCounter % 255) + 1; // 轮出 1 ~ 255 - const lastCounter = scriptDataAsyncCounter; scriptDAO.gets(uuids.split("\n")).then((res) => { - if (lastCounter !== scriptDataAsyncCounter) { + if (!isMounted) { // 由于 state 改变,在结果取得前 useEffect 再次执行,因此需要忽略上次结果 return; } @@ -454,11 +448,14 @@ const ScriptMenuList = React.memo( } satisfies SCMetadata; } } - setExtraData({ uuids, lang, metadata: metadataRecord }); + setExtraData({ uuids, metadata: metadataRecord }); // 再次触发 useEffect }); } - }, [script, uuids, lang, extraData]); + return () => { + isMounted = false; + }; + }, [script, uuids, extraData]); useEffect(() => { // 注册菜单快速键(accessKey):以各分组第一个项目的 accessKey 作为触发条件。 diff --git a/src/pages/popup/App.tsx b/src/pages/popup/App.tsx index 039c34eb8..901ded12e 100644 --- a/src/pages/popup/App.tsx +++ b/src/pages/popup/App.tsx @@ -59,39 +59,23 @@ function App() { const { t } = useTranslation(); const pageTabIdRef = useRef(0); - // 只随 script 数量和启动状态而改变的state - const normalEnables = useMemo(() => { - // 返回字串让 React 比对 state 有否改动 - return scriptList.map((script) => (script.enable ? 1 : 0)).join(","); - }, [scriptList]); - - // 只随 script 数量和启动状态而改变的state - const backEnables = useMemo(() => { - // 返回字串让 React 比对 state 有否改动 - return backScriptList.map((script) => (script.enable ? 1 : 0)).join(","); - }, [backScriptList]); - const normalScriptCounts = useMemo(() => { - // 拆回array - const enables = normalEnables.split(","); // 计算已开启了的数量 - const running = enables.reduce((p, c) => p + (+c ? 1 : 0), 0); + const running = scriptList.reduce((p, c) => p + (c.enable ? 1 : 0), 0); return { running, - total: enables.length, // 总数 + total: scriptList.length, // 总数 }; - }, [normalEnables]); + }, [scriptList]); const backScriptCounts = useMemo(() => { - // 拆回array - const enables = backEnables.split(","); // 计算已开启了的数量 - const running = enables.reduce((p, c) => p + (+c ? 1 : 0), 0); + const running = backScriptList.reduce((p, c) => p + (c.enable ? 1 : 0), 0); return { running, - total: enables.length, // 总数 + total: backScriptList.length, // 总数 }; - }, [backEnables]); + }, [backScriptList]); const urlHost = useMemo(() => { let url: URL | undefined; From 5eb46834ae27a526f0b169820fb36bacc5b0c278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Sun, 21 Dec 2025 01:16:07 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E8=B0=83=E6=95=B4=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/repo/repo.ts | 27 ++++++++ src/pages/components/ScriptMenuList/index.tsx | 62 ++++++------------- 2 files changed, 47 insertions(+), 42 deletions(-) diff --git a/src/app/repo/repo.ts b/src/app/repo/repo.ts index 114da65b3..53ca314aa 100644 --- a/src/app/repo/repo.ts +++ b/src/app/repo/repo.ts @@ -204,6 +204,33 @@ export abstract class Repo { }); } + public getRecord(keys: string[]): Promise>> { + keys = keys.map((key) => this.joinKey(key)); + if (this.useCache) { + return loadCache().then((cache) => { + const record: Partial> = {}; + for (const key of keys) { + if (cache[key]) { + record[key] = Object.assign({}, cache[key]); + } else { + record[key] = cache[key]; + } + } + return record; + }); + } + return new Promise((resolve) => { + chrome.storage.local.get(keys, (result) => { + const lastError = chrome.runtime.lastError; + if (lastError) { + console.error("chrome.runtime.lastError in chrome.storage.local.get:", lastError); + // 无视storage API错误,继续执行 + } + resolve(result as Partial>); + }); + }); + } + private filter(data: { [key: string]: T }, filters?: (key: string, value: T) => boolean): T[] { const ret: T[] = []; for (const key in data) { diff --git a/src/pages/components/ScriptMenuList/index.tsx b/src/pages/components/ScriptMenuList/index.tsx index 4d7257caa..237434f8b 100644 --- a/src/pages/components/ScriptMenuList/index.tsx +++ b/src/pages/components/ScriptMenuList/index.tsx @@ -33,7 +33,7 @@ import type { ScriptMenuItemOption, } from "@App/app/service/service_worker/types"; import { popupClient, runtimeClient, scriptClient } from "@App/pages/store/features/script"; -import { i18nLang, i18nName } from "@App/locales/locales"; +import { i18nName } from "@App/locales/locales"; // 用于读取 metadata const scriptDAO = new ScriptDAO(); @@ -344,14 +344,6 @@ const ScriptMenuList = React.memo( currentUrl: string; menuExpandNum: number; }) => { - // extraData 为 undefined 时先等待异步加载完成,避免重复渲染 - const [extraData, setExtraData] = useState< - | { - uuids: string; - metadata: Record; - } - | undefined - >(undefined); const [scriptMenuList, setScriptMenuList] = useState([]); const { t } = useTranslation(); @@ -414,48 +406,34 @@ const ScriptMenuList = React.memo( return url; }, [currentUrl]); - // string memo 避免 uuids 以外的改变影响 - const uuids = useMemo(() => script.map((item) => item.uuid).join("\n"), [script]); - + const cache = useMemo(() => new Map(), []); // 以 异步方式 取得 metadata 放入 extraData // script 或 extraData 的更新时都会再次执行 useEffect(() => { let isMounted = true; - if (extraData && extraData.uuids === uuids && extraData.lang === lang) { - // extraData 已取得 - // 把 getPopupData() 的 scriptMenuList 和 异步结果 的 metadata 合并至 scriptMenuList - const metadata = extraData.metadata; - const newScriptMenuList = script.map((item) => ({ ...item, metadata: metadata[item.uuid] || {} })); - updateScriptMenuList(newScriptMenuList); - } else { - // 取得 extraData - scriptDAO.gets(uuids.split("\n")).then((res) => { - if (!isMounted) { - // 由于 state 改变,在结果取得前 useEffect 再次执行,因此需要忽略上次结果 - return; - } - const metadataRecord = {} as Record; - const nameKey = `name:${lang}`; - for (const entry of res) { - if (entry) { - const m = entry.metadata; - const [icon] = m.icon || m.iconurl || m.icon64 || m.icon64url || []; - // metadataRecord 的储存量不影响 storage.session 但影响页面的记忆体 - // 按需要可以增加其他 metadata, 例如 @match @include @exclude - metadataRecord[entry.uuid] = { - icon: [icon], // 只储存单个 icon - [nameKey]: [i18nName(entry)], // 只储存 i18n 的 name - } satisfies SCMetadata; + // 先从 cache 读取,避免重复请求相同 uuid 的 metadata + Promise.all( + script.map(async (item) => { + let metadata = cache.get(item.uuid); + if (!metadata) { + const script = await scriptDAO.get(item.uuid); + if (script) { + metadata = script.metadata || {}; } + cache.set(item.uuid, metadata); } - setExtraData({ uuids, metadata: metadataRecord }); - // 再次触发 useEffect - }); - } + return { ...item, metadata: metadata || {} }; + }) + ).then((newScriptMenuList) => { + if (!isMounted) { + return; + } + updateScriptMenuList(newScriptMenuList); + }); return () => { isMounted = false; }; - }, [script, uuids, extraData]); + }, [cache, script]); useEffect(() => { // 注册菜单快速键(accessKey):以各分组第一个项目的 accessKey 作为触发条件。 From 32324cea7d04a911f1f9959bc769123b1c331969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Sun, 21 Dec 2025 18:11:21 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E9=80=9A=E8=BF=87=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/pages/popup/App.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/pages/popup/App.test.tsx b/tests/pages/popup/App.test.tsx index 348e41f66..9cfe7b175 100644 --- a/tests/pages/popup/App.test.tsx +++ b/tests/pages/popup/App.test.tsx @@ -169,8 +169,8 @@ describe("Popup App Component", () => { await waitFor( () => { // 检查是否存在折叠面板结构 - expect(screen.getByText("current_page_scripts (0/1)")).toBeInTheDocument(); - expect(screen.getByText("enabled_background_scripts (0/1)")).toBeInTheDocument(); + expect(screen.getByText("current_page_scripts (0/0)")).toBeInTheDocument(); + expect(screen.getByText("enabled_background_scripts (0/0)")).toBeInTheDocument(); }, { timeout: 3000 } ); @@ -191,8 +191,8 @@ describe("Popup App Component", () => { await waitFor( () => { expect(screen.getByText("ScriptCat")).toBeInTheDocument(); - expect(screen.getByText("current_page_scripts (0/1)")).toBeInTheDocument(); - expect(screen.getByText("enabled_background_scripts (0/1)")).toBeInTheDocument(); + expect(screen.getByText("current_page_scripts (0/0)")).toBeInTheDocument(); + expect(screen.getByText("enabled_background_scripts (0/0)")).toBeInTheDocument(); expect(screen.getByText("v" + ExtVersion)).toBeInTheDocument(); }, { timeout: 3000 }