From 6328f178d333fa85f5b15e06bdba299cde11f3f8 Mon Sep 17 00:00:00 2001 From: shenmo Date: Mon, 13 Jul 2026 22:57:54 +0800 Subject: [PATCH] =?UTF-8?q?refactor(home):=20=E9=87=8D=E6=9E=84=E9=A6=96?= =?UTF-8?q?=E9=A1=B5=E4=B8=8E=E4=BE=A7=E8=BE=B9=E6=A0=8F=EF=BC=8C=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E6=97=A7HomeList=E7=B1=BB=E5=9E=8B=E5=B9=B6=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E9=A6=96=E9=A1=B5=E5=88=97=E8=A1=A8=E4=BE=A7=E8=BE=B9?= =?UTF-8?q?=E6=A0=8F=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 移除废弃的HomeList类型定义,新增homeList侧边栏类型 2. 重构AppGrid组件,添加showOrigin属性支持自定义显示来源标记 3. 简化HomeView组件,移除旧的列表展示逻辑与相关代码 4. 新增侧边栏首页列表入口加载与应用数据加载逻辑 5. 优化侧边栏计数与应用预加载逻辑,适配新的首页列表类型 --- src/App.vue | 338 ++++++++++++++++++++++++----------- src/components/AppGrid.vue | 12 +- src/components/HomeView.vue | 50 +----- src/global/typedefinition.ts | 7 +- 4 files changed, 251 insertions(+), 156 deletions(-) diff --git a/src/App.vue b/src/App.vue index 36c86790..e73ad5bf 100644 --- a/src/App.vue +++ b/src/App.vue @@ -86,14 +86,14 @@ @open-detail="openDetail" /> @@ -103,7 +103,8 @@ :loading="loading" :scroll-key="activeTab + '-' + selectedCategory" :store-filter="storeFilter" - @open-detail="openDetail" + :show-origin="storeFilter === 'both' && !isHomeListTab" + @open-detail="handleAppCardOpenDetail" /> @@ -388,7 +389,6 @@ import type { ChannelPayload, CategoryInfo, HomeLink, - HomeList, FlarumLoginPayload, SidebarEntry, UpdateCenterItem, @@ -448,6 +448,8 @@ const tabCategories: Ref>> = ref( {}, ); const tabApps: Ref> = ref({}); +// 首页推荐列表入口对应的各来源 jsonUrl:{ [entryId]: { spark?, apm? } } +const homeListUrls = ref>({}); const activeTab = ref("home"); type MainView = "default" | "favorites"; const currentView = ref("default"); @@ -601,9 +603,14 @@ const entryCounts = computed(() => { sidebarEntries.value.forEach((entry) => { if (entry.type === "category" && entry.value) { - counts[entry.id] = allApps.filter( - (app) => app.category === entry.value, - ).length; + // 优先使用已加载的入口应用总数;未加载时回退到全局分类计数 + const tabLen = tabApps.value[entry.id]?.length; + counts[entry.id] = + tabLen !== undefined + ? tabLen + : allApps.filter((app) => app.category === entry.value).length; + } else if (entry.type === "homeList") { + counts[entry.id] = tabApps.value[entry.id]?.length || 0; } else { counts[entry.id] = 0; } @@ -614,6 +621,25 @@ const entryCounts = computed(() => { const currentDisplayApp = computed(() => getDisplayApp(currentApp.value)); +// 当前激活的侧栏入口是否为首页推荐列表类型 +const isHomeListTab = computed( + () => + activeTab.value !== "home" && + activeTab.value !== "all" && + sidebarEntries.value.some( + (e) => e.id === activeTab.value && e.type === "homeList", + ), +); + +// 应用卡片点击:首页推荐列表复用首页逻辑,标记 _fromHomeView 以便从双仓库获取完整信息 +const handleAppCardOpenDetail = (app: App) => { + if (isHomeListTab.value) { + openDetail({ ...app, _fromHomeView: true }); + } else { + openDetail(app); + } +}; + const clientArch = computed(() => window.apm_store.arch || "amd64"); const currentReviewAppKey = computed(() => { @@ -731,17 +757,15 @@ const selectTab = (tab: string) => { selectedCategory.value = "all"; isSidebarOpen.value = false; window.scrollTo({ top: 0, behavior: "smooth" }); - if ( - tab === "home" && - homeLinks.value.length === 0 && - homeLists.value.length === 0 - ) { + if (tab === "home" && homeLinks.value.length === 0) { loadHome(); } if (tab !== "home" && tab !== "all") { const entry = sidebarEntries.value.find((e) => e.id === tab); if (entry && entry.type === "category") { loadTabApps(tab); + } else if (entry && entry.type === "homeList") { + loadHomeListApps(tab); } } }; @@ -1036,7 +1060,6 @@ const closeScreenPreview = () => { // Home data const homeLinks = ref([]); -const homeLists = ref([]); const homeLoading = ref(false); const homeError = ref(""); @@ -1044,7 +1067,6 @@ const loadHome = async () => { homeLoading.value = true; homeError.value = ""; homeLinks.value = []; - homeLists.value = []; try { const arch = window.apm_store.arch || "amd64"; const modes: Array<"spark" | "apm"> = @@ -1068,65 +1090,6 @@ const loadHome = async () => { } catch (e) { console.warn(`Failed to load ${mode} homelinks.json`, e); } - - // homelist.json - try { - const res2 = await fetch(`${base}/homelist.json`); - if (res2.ok) { - const lists = await res2.json(); - for (const item of lists) { - if (item.type === "appList" && item.jsonUrl) { - try { - const url = `${APM_STORE_BASE_URL}/${finalArch}${item.jsonUrl}`; - const r = await fetch(url); - if (r.ok) { - const appsJson = await r.json(); - const rawApps = appsJson || []; - const apps = await Promise.all( - rawApps.map(async (a: Record) => { - const baseApp = { - name: a.Name || a.name || a.Pkgname || a.PkgName || "", - pkgname: a.Pkgname || a.pkgname || "", - category: a.Category || a.category || "unknown", - more: a.More || a.more || "", - version: a.Version || "", - filename: a.Filename || a.filename || "", - origin: mode as "spark" | "apm", - }; - - try { - const realAppUrl = `${APM_STORE_BASE_URL}/${finalArch}/${baseApp.category}/${baseApp.pkgname}/app.json`; - const realRes = await fetch(realAppUrl); - if (realRes.ok) { - const realApp = await realRes.json(); - if (realApp.Filename) - baseApp.filename = realApp.Filename; - if (realApp.More) baseApp.more = realApp.More; - if (realApp.Name) baseApp.name = realApp.Name; - } - } catch (e) { - console.warn( - `Failed to fetch real app.json for ${baseApp.pkgname}`, - e, - ); - } - return baseApp; - }), - ); - homeLists.value.push({ - title: `${item.name || "推荐"} (${mode === "spark" ? "星火" : "APM"})`, - apps, - }); - } - } catch (e) { - console.warn("Failed to load home list", item, e); - } - } - } - } - } catch (e) { - console.warn(`Failed to load ${mode} homelist.json`, e); - } } } catch (error: unknown) { homeError.value = (error as Error)?.message || "加载首页失败"; @@ -1135,6 +1098,166 @@ const loadHome = async () => { } }; +// 加载首页推荐列表为侧边栏入口(按名称合并 spark/apm,置于分类入口上方) +const loadHomeListEntries = async () => { + try { + const arch = window.apm_store.arch || "amd64"; + const modes: Array<"spark" | "apm"> = + storeFilter.value === "both" ? ["spark", "apm"] : [storeFilter.value]; + + // 按列表名称合并各来源的 jsonUrl + const byName = new Map< + string, + { name: string; urls: { spark?: string; apm?: string } } + >(); + + for (const mode of modes) { + const finalArch = mode === "spark" ? `${arch}-store` : `${arch}-apm`; + const base = `${APM_STORE_BASE_URL}/${finalArch}/home`; + + try { + const res = await fetch(`${base}/homelist.json`); + if (res.ok) { + const lists = await res.json(); + lists.forEach( + (item: { name?: string; type?: string; jsonUrl?: string }) => { + if (item.type === "appList" && item.jsonUrl) { + const name = item.name || "推荐"; + const existing = byName.get(name); + if (existing) { + existing.urls[mode] = item.jsonUrl; + } else { + byName.set(name, { + name, + urls: { [mode]: item.jsonUrl } as { + spark?: string; + apm?: string; + }, + }); + } + } + }, + ); + } + } catch (e) { + console.warn(`Failed to load ${mode} homelist.json`, e); + } + } + + const entries: SidebarEntry[] = []; + const urlsMap: Record = {}; + + byName.forEach((info, name) => { + const id = `home-list-${name}`; + entries.push({ + id, + name, + icon: "fas fa-star", + type: "homeList", + }); + urlsMap[id] = info.urls; + }); + + if (entries.length > 0) { + // 首页推荐入口置于分类入口上方 + sidebarEntries.value = [...entries, ...sidebarEntries.value]; + homeListUrls.value = { ...homeListUrls.value, ...urlsMap }; + logger.info(`已加载 ${entries.length} 个首页推荐列表入口`); + } + } catch (error) { + logger.warn(`加载首页推荐列表入口失败: ${error}`); + } +}; + +// 加载首页推荐列表的应用数据(复用首页逻辑,合并展示 spark+apm) +const loadHomeListApps = async (entryId: string) => { + if (tabApps.value[entryId]) return; + + const urls = homeListUrls.value[entryId]; + if (!urls) return; + + const arch = window.apm_store.arch || "amd64"; + const loadedApps: App[] = []; + + // 同时加载各来源的推荐列表应用 + 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`; + + try { + const path = `/${finalArch}${jsonUrl}`; + const rawApps = + (await fetchWithRetry[]>(path)) || []; + const apps = await Promise.all( + rawApps.map(async (a) => { + const category = a.Category || a.category || "unknown"; + const pkgname = a.Pkgname || a.pkgname || ""; + + // 复用首页逻辑:从仓库获取完整应用信息 + try { + const realAppUrl = `/${finalArch}/${category}/${pkgname}/app.json`; + const realApp = await fetchWithRetry(realAppUrl); + return normalizeAppJson(realApp, category, mode); + } catch (e) { + console.warn(`Failed to fetch app.json for ${pkgname}`, e); + } + + // 回退:使用列表中的基本信息构建 App 对象 + return { + name: a.Name || a.name || pkgname || "", + pkgname, + version: a.Version || "", + filename: a.Filename || a.filename || "", + category, + more: a.More || a.more || "", + torrent_address: "", + author: "", + contributor: "", + website: "", + update: "", + size: "", + tags: "", + img_urls: [], + icons: "", + origin: mode, + currentStatus: "not-installed" as const, + } as App; + }), + ); + loadedApps.push(...apps); + } catch (e) { + logger.warn(`加载首页列表 ${entryId} (${mode}) 失败: ${e}`); + } + }), + ); + + tabApps.value = { ...tabApps.value, [entryId]: loadedApps }; + logger.info(`首页列表 "${entryId}" 加载完成,共 ${loadedApps.length} 个应用`); +}; + +// 并行预加载所有侧边栏入口的应用数据,避免点击时才加载导致缓慢 +const preloadSidebarTabApps = (): Promise => { + const tasks: Promise[] = []; + for (const entry of sidebarEntries.value) { + if (entry.type === "category") { + tasks.push( + loadTabApps(entry.id).catch((e: unknown) => + logger.warn(`预加载入口 ${entry.id} 失败: ${e}`), + ), + ); + } else if (entry.type === "homeList") { + tasks.push( + loadHomeListApps(entry.id).catch((e: unknown) => + logger.warn(`预加载首页列表 ${entry.id} 失败: ${e}`), + ), + ); + } + } + return Promise.all(tasks).then(() => undefined); +}; + const prevScreen = () => { if (currentScreenIndex.value > 0) { currentScreenIndex.value--; @@ -2449,9 +2572,11 @@ const loadTabApps = async (entryId: string) => { const modes: Array<"spark" | "apm"> = storeFilter.value === "both" ? ["spark", "apm"] : [storeFilter.value]; const folderName = entry.value || entry.id; - const loadedApps: App[] = []; const subCats = tabCategories.value[entryId]; + // 收集所有需要发起的请求任务(mode × 子分类),然后全并发加载 + const tasks: Promise[] = []; + for (const mode of modes) { const finalArch = mode === "spark" ? `${arch}-store` : `${arch}-apm`; @@ -2464,37 +2589,44 @@ const loadTabApps = async (entryId: string) => { ) continue; - try { - const path = `/${finalArch}/${folderName}/${subCat}/applist.json`; - logger.info(`加载入口子分类: ${entryId}/${subCat} (来源: ${mode})`); - const categoryApps = await fetchWithRetry(path); - loadedApps.push( - ...(categoryApps || []).map((aj) => - normalizeAppJson(aj, subCat, mode), - ), - ); - } catch (e) { - logger.warn( - `加载入口子分类 ${entryId}/${subCat} (${mode}) 失败: ${e}`, - ); - } + const path = `/${finalArch}/${folderName}/${subCat}/applist.json`; + logger.info(`加载入口子分类: ${entryId}/${subCat} (来源: ${mode})`); + tasks.push( + fetchWithRetry(path) + .then((categoryApps) => + (categoryApps || []).map((aj) => + normalizeAppJson(aj, subCat, mode), + ), + ) + .catch((e: unknown) => { + logger.warn( + `加载入口子分类 ${entryId}/${subCat} (${mode}) 失败: ${e}`, + ); + return [] as App[]; + }), + ); } } else { - try { - const path = `/${finalArch}/${folderName}/applist.json`; - logger.info(`加载入口目录: ${entryId} (来源: ${mode})`); - const categoryApps = await fetchWithRetry(path); - loadedApps.push( - ...(categoryApps || []).map((aj) => - normalizeAppJson(aj, folderName, mode), - ), - ); - } catch (e) { - logger.warn(`加载入口目录 ${entryId} (${mode}) 失败: ${e}`); - } + const path = `/${finalArch}/${folderName}/applist.json`; + logger.info(`加载入口目录: ${entryId} (来源: ${mode})`); + tasks.push( + fetchWithRetry(path) + .then((categoryApps) => + (categoryApps || []).map((aj) => + normalizeAppJson(aj, folderName, mode), + ), + ) + .catch((e: unknown) => { + logger.warn(`加载入口目录 ${entryId} (${mode}) 失败: ${e}`); + return [] as App[]; + }), + ); } } + const results = await Promise.all(tasks); + const loadedApps = results.flat(); + tabApps.value = { ...tabApps.value, [entryId]: loadedApps }; logger.info(`入口 "${entryId}" 加载完成,共 ${loadedApps.length} 个应用`); }; @@ -2608,6 +2740,8 @@ onMounted(async () => { await loadSidebarConfig(); + await loadHomeListEntries(); + await loadTabCategories(); // 分类目录加载后,并行加载主页数据和所有应用列表 @@ -2624,6 +2758,8 @@ onMounted(async () => { resolve(); }); }), + // 并行预加载所有侧边栏入口的应用数据 + preloadSidebarTabApps(), ]).then(() => { // 所有数据加载完成后的回调(可选) logger.info("所有应用数据加载完成"); diff --git a/src/components/AppGrid.vue b/src/components/AppGrid.vue index 18de0ca8..32307932 100644 --- a/src/components/AppGrid.vue +++ b/src/components/AppGrid.vue @@ -26,7 +26,7 @@ v-for="(app, index) in apps" :key="index" :app="app" - :show-origin="storeFilter === 'both'" + :show-origin="effectiveShowOrigin" @open-detail="$emit('open-detail', app)" /> @@ -46,7 +46,7 @@ v-for="app in item.apps" :key="app.pkgname" :app="app" - :show-origin="storeFilter === 'both'" + :show-origin="effectiveShowOrigin" @open-detail="$emit('open-detail', app)" /> @@ -93,8 +93,16 @@ const props = defineProps<{ loading: boolean; storeFilter?: "spark" | "apm" | "both"; scrollKey?: string; + showOrigin?: boolean; }>(); +// 显式传入 showOrigin 时优先使用;否则回退到根据 storeFilter 推断 +const effectiveShowOrigin = computed(() => + props.showOrigin !== undefined + ? props.showOrigin + : props.storeFilter === "both", +); + defineEmits<{ (e: "open-detail", app: App): void; }>(); diff --git a/src/components/HomeView.vue b/src/components/HomeView.vue index 25402b2e..f850a6ee 100644 --- a/src/components/HomeView.vue +++ b/src/components/HomeView.vue @@ -2,7 +2,7 @@
@@ -16,7 +16,7 @@
- - -
-
-
-

- {{ section.title }} -

-
-
- -
-
-