mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-09-20 21:50:11 +08:00
fix: 修复 before-quit 中 mainWindow 未定义报错,并优化排行榜加载
- electron/main/index.ts: flushSaveBounds 改用模块级 win 变量, 修复关闭应用时 mainWindow is not defined 的 ReferenceError - src/App.vue: loadRanking 并发数降至 15;loadHome 多来源请求并行化 - src/modules/ranking.ts: parseContributors 增强异常格式边界处理
This commit is contained in:
@@ -361,6 +361,16 @@ function saveWindowState(state: WindowState): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let saveBoundsTimer: NodeJS.Timeout | null = null;
|
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 {
|
function scheduleSaveBounds(winInstance: BrowserWindow): void {
|
||||||
if (saveBoundsTimer) clearTimeout(saveBoundsTimer);
|
if (saveBoundsTimer) clearTimeout(saveBoundsTimer);
|
||||||
saveBoundsTimer = setTimeout(() => {
|
saveBoundsTimer = setTimeout(() => {
|
||||||
@@ -376,6 +386,11 @@ function scheduleSaveBounds(winInstance: BrowserWindow): void {
|
|||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 应用退出前立即持久化(防抖 400ms 可能在快速关闭时丢失最后一次状态)
|
||||||
|
app.on("before-quit", () => {
|
||||||
|
flushSaveBounds();
|
||||||
|
});
|
||||||
|
|
||||||
async function createWindow() {
|
async function createWindow() {
|
||||||
const saved = loadWindowState();
|
const saved = loadWindowState();
|
||||||
// 恢复时:若上次窗口过大(>1600x900,通常为全屏/最大化),回退到默认尺寸并居中,
|
// 恢复时:若上次窗口过大(>1600x900,通常为全屏/最大化),回退到默认尺寸并居中,
|
||||||
|
|||||||
+45
-36
@@ -1316,7 +1316,7 @@ const loadRanking = async () => {
|
|||||||
const gen = ++rankingGeneration;
|
const gen = ++rankingGeneration;
|
||||||
rankingLoading.value = true;
|
rankingLoading.value = true;
|
||||||
const all = apps.value.slice();
|
const all = apps.value.slice();
|
||||||
const CONCURRENCY = 64;
|
const CONCURRENCY = 15;
|
||||||
const results: App[] = [];
|
const results: App[] = [];
|
||||||
for (let i = 0; i < all.length; i += CONCURRENCY) {
|
for (let i = 0; i < all.length; i += CONCURRENCY) {
|
||||||
if (gen !== rankingGeneration) {
|
if (gen !== rankingGeneration) {
|
||||||
@@ -1359,42 +1359,51 @@ const loadHome = async () => {
|
|||||||
// 按名称去重,spark 优先:同名链接 spark 覆盖 apm
|
// 按名称去重,spark 优先:同名链接 spark 覆盖 apm
|
||||||
const seenNames = new Set<string>();
|
const seenNames = new Set<string>();
|
||||||
|
|
||||||
for (const mode of modes) {
|
// 并行请求各来源的 homelinks.json,缩短首页加载耗时
|
||||||
const finalArch = mode === "spark" ? `${arch}-store` : `${arch}-apm`;
|
const modeResults = await Promise.all(
|
||||||
const base = `${APM_STORE_BASE_URL}/${finalArch}/home`;
|
modes.map(async (mode) => {
|
||||||
|
const finalArch = mode === "spark" ? `${arch}-store` : `${arch}-apm`;
|
||||||
// homelinks.json
|
const base = `${APM_STORE_BASE_URL}/${finalArch}/home`;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${base}/homelinks.json`);
|
const res = await fetch(`${base}/homelinks.json`);
|
||||||
if (res.ok) {
|
if (res.ok) return { mode, raw: (await res.json()) as unknown };
|
||||||
const raw = await res.json();
|
} catch (e) {
|
||||||
// 校验 links 为数组,且每项均为对象(避免后端返回异常结构导致运行时错误)
|
console.warn(`Failed to load ${mode} homelinks.json`, e);
|
||||||
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 (e) {
|
return { mode, raw: undefined };
|
||||||
console.warn(`Failed to load ${mode} homelinks.json`, e);
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
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) {
|
} catch (error: unknown) {
|
||||||
|
|||||||
@@ -24,13 +24,14 @@ export interface ContributorRank {
|
|||||||
* 聚合与展示统一去除 `<...>` 取显示名。
|
* 聚合与展示统一去除 `<...>` 取显示名。
|
||||||
*/
|
*/
|
||||||
export function parseContributors(raw: string | undefined): string[] {
|
export function parseContributors(raw: string | undefined): string[] {
|
||||||
if (!raw) return [];
|
if (!raw || typeof raw !== "string") return [];
|
||||||
const names: string[] = [];
|
const names: string[] = [];
|
||||||
for (const part of raw.split(/[;;]/)) {
|
for (const part of raw.split(/[;;]/)) {
|
||||||
const trimmed = part.trim();
|
const trimmed = part.trim();
|
||||||
if (!trimmed) continue;
|
if (!trimmed) continue;
|
||||||
|
// 去除 <...> 包裹的邮箱,取显示名;长度 1~50 过滤空字符串/异常超长
|
||||||
const name = trimmed.replace(/<[^>]*>/g, "").trim();
|
const name = trimmed.replace(/<[^>]*>/g, "").trim();
|
||||||
if (name) names.push(name);
|
if (name.length > 0 && name.length <= 50) names.push(name);
|
||||||
}
|
}
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user