From 0d580c4d47129d1f29a762c4db1a023eb0323f64 Mon Sep 17 00:00:00 2001 From: xiyidaiwa Date: Thu, 6 Aug 2026 20:51:47 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20before-quit=20?= =?UTF-8?q?=E4=B8=AD=20mainWindow=20=E6=9C=AA=E5=AE=9A=E4=B9=89=E6=8A=A5?= =?UTF-8?q?=E9=94=99=EF=BC=8C=E5=B9=B6=E4=BC=98=E5=8C=96=E6=8E=92=E8=A1=8C?= =?UTF-8?q?=E6=A6=9C=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - electron/main/index.ts: flushSaveBounds 改用模块级 win 变量, 修复关闭应用时 mainWindow is not defined 的 ReferenceError - src/App.vue: loadRanking 并发数降至 15;loadHome 多来源请求并行化 - src/modules/ranking.ts: parseContributors 增强异常格式边界处理 --- electron/main/index.ts | 15 ++++++++ src/App.vue | 81 +++++++++++++++++++++++------------------- src/modules/ranking.ts | 5 +-- 3 files changed, 63 insertions(+), 38 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index 9a37e241..6159b592 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -361,6 +361,16 @@ function saveWindowState(state: WindowState): void { } let saveBoundsTimer: NodeJS.Timeout | null = null; +function flushSaveBounds(): void { + if (saveBoundsTimer) { + clearTimeout(saveBoundsTimer); + saveBoundsTimer = null; + } + if (win && !win.isDestroyed()) { + const { width, height, x, y } = win.getBounds(); + saveWindowState({ width, height, x, y, maximized: win.isMaximized() }); + } +} function scheduleSaveBounds(winInstance: BrowserWindow): void { if (saveBoundsTimer) clearTimeout(saveBoundsTimer); saveBoundsTimer = setTimeout(() => { @@ -376,6 +386,11 @@ function scheduleSaveBounds(winInstance: BrowserWindow): void { }, 400); } +// 应用退出前立即持久化(防抖 400ms 可能在快速关闭时丢失最后一次状态) +app.on("before-quit", () => { + flushSaveBounds(); +}); + async function createWindow() { const saved = loadWindowState(); // 恢复时:若上次窗口过大(>1600x900,通常为全屏/最大化),回退到默认尺寸并居中, diff --git a/src/App.vue b/src/App.vue index b98d945b..29a55f44 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1316,7 +1316,7 @@ const loadRanking = async () => { const gen = ++rankingGeneration; rankingLoading.value = true; const all = apps.value.slice(); - const CONCURRENCY = 64; + const CONCURRENCY = 15; const results: App[] = []; for (let i = 0; i < all.length; i += CONCURRENCY) { if (gen !== rankingGeneration) { @@ -1359,42 +1359,51 @@ const loadHome = async () => { // 按名称去重,spark 优先:同名链接 spark 覆盖 apm const seenNames = new Set(); - for (const mode of modes) { - const finalArch = mode === "spark" ? `${arch}-store` : `${arch}-apm`; - const base = `${APM_STORE_BASE_URL}/${finalArch}/home`; - - // homelinks.json - try { - const res = await fetch(`${base}/homelinks.json`); - if (res.ok) { - const raw = await res.json(); - // 校验 links 为数组,且每项均为对象(避免后端返回异常结构导致运行时错误) - const links = Array.isArray(raw) ? (raw.filter((x) => x && typeof x === "object") as Record[]) : []; - for (const l of links) { - const name = (l.Name as string) || (l.name as string) || ""; - if (!name) continue; // 跳过空名称,避免空字符串污染 seenNames 与去重逻辑 - if (seenNames.has(name)) continue; // 已由更高优先级来源(spark)占据 - // 仅校验 url 必需;远程 homelinks.json 不含 icon 字段(图片由 imgUrl 提供), - // 故 icon 不作为硬性校验,缺省为空串以兼容 HomeLink 类型。 - const url = (l.Url as string) || (l.url as string) || ""; - if (!url) continue; - const icon = (l.Icon as string) || (l.icon as string) || ""; - seenNames.add(name); - // 显式提取已知字段构造,避免通过展开运算符 { ...l } 把远程不可信数据中的未知属性注入响应式状态 - const safeLink: HomeLink = { - name, - url, - icon, - more: (l.more as string) || undefined, - imgUrl: (l.imgUrl as string) || undefined, - type: (l.type as string) || undefined, - origin: mode, - }; - homeLinks.value.push(safeLink); - } + // 并行请求各来源的 homelinks.json,缩短首页加载耗时 + const modeResults = await Promise.all( + modes.map(async (mode) => { + const finalArch = mode === "spark" ? `${arch}-store` : `${arch}-apm`; + const base = `${APM_STORE_BASE_URL}/${finalArch}/home`; + try { + const res = await fetch(`${base}/homelinks.json`); + if (res.ok) return { mode, raw: (await res.json()) as unknown }; + } catch (e) { + console.warn(`Failed to load ${mode} homelinks.json`, e); } - } catch (e) { - console.warn(`Failed to load ${mode} homelinks.json`, e); + return { mode, raw: undefined }; + }), + ); + + for (const { mode, raw } of modeResults) { + if (!raw) continue; + // 校验 links 为数组,且每项均为对象(避免后端返回异常结构导致运行时错误) + const links = Array.isArray(raw) + ? (raw.filter((x) => x && typeof x === "object") as Record< + string, + unknown + >[]) + : []; + for (const l of links) { + const name = (l.Name as string) || (l.name as string) || ""; + if (!name) continue; // 跳过空名称,避免空字符串污染 seenNames 与去重逻辑 + if (seenNames.has(name)) continue; // 已由更高优先级来源(spark)占据 + // 仅校验 url 必需;远程 homelinks.json 不含 icon 字段(图片由 imgUrl 提供), + // 故 icon 不作为硬性校验,缺省为空串以兼容 HomeLink 类型。 + const url = (l.Url as string) || (l.url as string) || ""; + if (!url) continue; + const icon = (l.Icon as string) || (l.icon as string) || ""; + seenNames.add(name); + // 显式提取已知字段构造,避免通过展开运算符 { ...l } 把远程不可信数据中的未知属性注入响应式状态 + const safeLink: HomeLink = { + name, + url, + icon, + more: (l.more as string) || undefined, + imgUrl: (l.imgUrl as string) || undefined, + type: (l.type as string) || undefined, + origin: mode, + }; + homeLinks.value.push(safeLink); } } } catch (error: unknown) { diff --git a/src/modules/ranking.ts b/src/modules/ranking.ts index 0b8cb658..fbac423e 100644 --- a/src/modules/ranking.ts +++ b/src/modules/ranking.ts @@ -24,13 +24,14 @@ export interface ContributorRank { * 聚合与展示统一去除 `<...>` 取显示名。 */ export function parseContributors(raw: string | undefined): string[] { - if (!raw) return []; + if (!raw || typeof raw !== "string") return []; const names: string[] = []; for (const part of raw.split(/[;;]/)) { const trimmed = part.trim(); if (!trimmed) continue; + // 去除 <...> 包裹的邮箱,取显示名;长度 1~50 过滤空字符串/异常超长 const name = trimmed.replace(/<[^>]*>/g, "").trim(); - if (name) names.push(name); + if (name.length > 0 && name.length <= 50) names.push(name); } return names; }