From 387be920b1384293197e4e09b3a4ecb79607bbd1 Mon Sep 17 00:00:00 2001 From: shenmo Date: Thu, 16 Jul 2026 10:27:18 +0800 Subject: [PATCH] refactor(apps): optimize home app list loading and grid layout 1. refactor home app list loading logic: use seen set instead of map, handle deduplication with spark priority 2. add vertical scrolling for small app grid when not using virtual scroll 3. remove unnecessary overflow-y-auto from main container --- src/App.vue | 123 +++++++++++++++++++------------------ src/components/AppGrid.vue | 25 +++++--- 2 files changed, 80 insertions(+), 68 deletions(-) diff --git a/src/App.vue b/src/App.vue index 45a52024..5ef24d27 100644 --- a/src/App.vue +++ b/src/App.vue @@ -45,7 +45,7 @@ /> -
+
@@ -1196,7 +1196,7 @@ const loadHomeListEntries = async () => { } }; -// 加载首页推荐列表的应用数据(复用首页逻辑,合并展示 spark+apm) +// 加载首页推荐列表的应用数据(合并展示 spark+apm,按 pkgname 去重,spark 优先) const loadHomeListApps = async (entryId: string) => { if (tabApps.value[entryId]) return; // 防止重复加载:如果正在加载中则跳过 @@ -1209,74 +1209,79 @@ const loadHomeListApps = async (entryId: string) => { loadingTabs.value = new Set(loadingTabs.value).add(entryId); const arch = window.apm_store.arch || "amd64"; - // 按 pkgname 去重,spark 优先(spark 在 modes 数组最前面,先占据 key) - const appMap = new Map(); + const loadedApps: App[] = []; + const seenPkgnames = new Set(); - // 同时加载各来源的推荐列表应用 - await Promise.all( - (Object.keys(urls) as Array<"spark" | "apm">).map(async (mode) => { - const jsonUrl = urls[mode]; - if (!jsonUrl) return; - const finalArch = mode === "spark" ? `${arch}-store` : `${arch}-apm`; + const parseAppList = ( + rawApps: Record[], + mode: "spark" | "apm", + ): App[] => + rawApps.map((a) => { + const category = a.Category || a.category || "unknown"; - try { - const path = `/${finalArch}${jsonUrl}`; - const rawApps = - (await fetchWithRetry[]>(path)) || []; - // 直接使用列表数据构建 App 对象,避免为每个应用单独请求 app.json(N+1 问题) - // 应用详情会在用户点击时通过 fetchAppFromStore 按需获取 - for (const a of rawApps) { - const pkgname = a.Pkgname || a.pkgname || ""; - if (!pkgname || appMap.has(pkgname)) continue; // 已由更高优先级来源占据 - - const category = a.Category || a.category || "unknown"; - - let img_urls: string[] = []; - const rawImgUrls = a.img_urls; - if (typeof rawImgUrls === "string") { - try { - img_urls = JSON.parse(rawImgUrls); - } catch { - img_urls = []; - } - } else if (Array.isArray(rawImgUrls)) { - img_urls = rawImgUrls; - } - - appMap.set(pkgname, { - name: a.Name || a.name || pkgname, - pkgname, - version: a.Version || "", - filename: a.Filename || a.filename || "", - torrent_address: a.Torrent_address || "", - author: a.Author || "", - contributor: a.Contributor || "", - website: a.Website || "", - update: a.Update || "", - size: a.Size || "", - more: a.More || a.more || "", - tags: a.Tags || "", - img_urls, - icons: a.icons || "", - category, - origin: mode, - currentStatus: "not-installed" as const, - } as App); + let img_urls: string[] = []; + const rawImgUrls = a.img_urls; + if (typeof rawImgUrls === "string") { + try { + img_urls = JSON.parse(rawImgUrls); + } catch { + img_urls = []; } - } catch (e) { - logger.warn(`加载首页列表 ${entryId} (${mode}) 失败: ${e}`); + } else if (Array.isArray(rawImgUrls)) { + img_urls = rawImgUrls; } - }), - ); - tabApps.value = { ...tabApps.value, [entryId]: [...appMap.values()] }; + return { + name: a.Name || a.name || a.Pkgname || a.pkgname || "", + pkgname: a.Pkgname || a.pkgname || "", + version: a.Version || "", + filename: a.Filename || a.filename || "", + torrent_address: a.Torrent_address || "", + author: a.Author || "", + contributor: a.Contributor || "", + website: a.Website || "", + update: a.Update || "", + size: a.Size || "", + more: a.More || a.more || "", + tags: a.Tags || "", + img_urls, + icons: a.icons || "", + category, + origin: mode, + currentStatus: "not-installed" as const, + } as App; + }); + + // 按优先级顺序加载:spark 优先,apm 中与 spark 同名的跳过 + const modes: Array<"spark" | "apm"> = ["spark", "apm"]; + for (const mode of modes) { + const jsonUrl = urls[mode]; + if (!jsonUrl) continue; + const finalArch = mode === "spark" ? `${arch}-store` : `${arch}-apm`; + + try { + const path = `/${finalArch}${jsonUrl}`; + const rawApps = + (await fetchWithRetry[]>(path)) || []; + const apps = parseAppList(rawApps, mode); + for (const app of apps) { + if (!app.pkgname || seenPkgnames.has(app.pkgname)) continue; + seenPkgnames.add(app.pkgname); + loadedApps.push(app); + } + } catch (e) { + logger.warn(`加载首页列表 ${entryId} (${mode}) 失败: ${e}`); + } + } + + tabApps.value = { ...tabApps.value, [entryId]: loadedApps }; // 移除加载标记 const next = new Set(loadingTabs.value); next.delete(entryId); loadingTabs.value = next; - logger.info(`首页列表 "${entryId}" 加载完成,共 ${appMap.size} 个应用`); + logger.info(`首页列表 "${entryId}" 加载完成,共 ${loadedApps.length} 个应用`); }; // 并行预加载所有侧边栏入口的应用数据,避免点击时才加载导致缓慢 diff --git a/src/components/AppGrid.vue b/src/components/AppGrid.vue index 32307932..e995e0c5 100644 --- a/src/components/AppGrid.vue +++ b/src/components/AppGrid.vue @@ -17,18 +17,20 @@

- +
- +
+ +
@@ -180,6 +182,11 @@ const gridRows = computed(() => { margin: -24px -16px; /* 抵消父容器的 px-4 py-6 */ } +.non-virtual-scroller { + height: calc(100vh - 140px); + overflow-y: auto; +} + @media (min-width: 1024px) { .scroller { margin: -24px -40px; /* 抵消父容器的 lg:px-10 */