From 24d55d09974863eea4d9d578ddf6ca2d6dda29e6 Mon Sep 17 00:00:00 2001 From: shenmo Date: Sat, 30 May 2026 20:41:59 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=BB=93=E6=9E=84=E5=B9=B6=E6=96=B0=E5=A2=9E=E5=A4=9A?= =?UTF-8?q?=E6=A0=87=E7=AD=BE=E5=88=86=E7=B1=BB=E5=BA=94=E7=94=A8=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 重构下载重试超时列表为多行格式提升可读性 2. 合并环境变量配置行简化代码 3. 新增多标签分类页面的应用加载能力,包括: - 添加displayCategories和displayApps计算属性 - 实现loadTabCategories和loadTabApps加载子分类数据 - 抽离normalizeAppJson复用应用数据格式化逻辑 - 优化侧边栏分类切换的应用过滤逻辑 --- electron/main/backend/install-manager.ts | 4 +- electron/main/backend/shared-installer.ts | 4 +- src/App.vue | 194 ++++++++++++++++++---- src/global/storeConfig.ts | 3 +- 4 files changed, 173 insertions(+), 32 deletions(-) diff --git a/electron/main/backend/install-manager.ts b/electron/main/backend/install-manager.ts index 433c9dc2..21ee42ca 100644 --- a/electron/main/backend/install-manager.ts +++ b/electron/main/backend/install-manager.ts @@ -407,7 +407,9 @@ async function processNextInQueue() { sendStatus("downloading"); // 下载重试逻辑:共10次,5次3秒,3次5秒,2次10秒 - const timeoutList = [3000, 3000, 3000, 3000, 3000, 5000, 5000, 5000, 10000, 10000]; + const timeoutList = [ + 3000, 3000, 3000, 3000, 3000, 5000, 5000, 5000, 10000, 10000, + ]; let retryCount = 0; let downloadSuccess = false; diff --git a/electron/main/backend/shared-installer.ts b/electron/main/backend/shared-installer.ts index e3542e84..83f1f59c 100644 --- a/electron/main/backend/shared-installer.ts +++ b/electron/main/backend/shared-installer.ts @@ -106,7 +106,9 @@ export const downloadPackage = async ({ onStatus?.("downloading"); // 下载重试逻辑:共10次,5次3秒,3次5秒,2次10秒 - const timeoutList = [3000, 3000, 3000, 3000, 3000, 5000, 5000, 5000, 10000, 10000]; + const timeoutList = [ + 3000, 3000, 3000, 3000, 3000, 5000, 5000, 5000, 10000, 10000, + ]; let retryCount = 0; let downloadSuccess = false; diff --git a/src/App.vue b/src/App.vue index bd882524..91f9a57e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -61,9 +61,9 @@ v-if=" currentView === 'default' && activeTab !== 'home' && - Object.keys(categories).length > 0 + Object.keys(displayCategories).length > 0 " - :categories="categories" + :categories="displayCategories" :selected-category="selectedCategory" :category-counts="categoryCounts" @select-category="selectSubCategory" @@ -436,6 +436,10 @@ const isDarkTheme = computed(() => { const categories: Ref> = ref({}); const apps: Ref = ref([]); +const tabCategories: Ref>> = ref( + {}, +); +const tabApps: Ref> = ref({}); const activeTab = ref("home"); type MainView = "default" | "favorites"; const currentView = ref("default"); @@ -543,8 +547,18 @@ const baseApps = computed(() => { return result; }); +const displayCategories = computed(() => { + if (activeTab.value === "all") return categories.value; + return tabCategories.value[activeTab.value] || {}; +}); + +const displayApps = computed(() => { + if (activeTab.value === "all") return baseApps.value; + return tabApps.value[activeTab.value] || []; +}); + const filteredApps = computed(() => { - let result = [...baseApps.value]; + let result = [...displayApps.value]; const effectiveCategory = getEffectiveCategory(); if (effectiveCategory && effectiveCategory !== "all") { @@ -559,12 +573,14 @@ const filteredApps = computed(() => { }); const categoryCounts = computed(() => { + const sourceApps = displayApps.value; + if (searchQuery.value.trim()) { - return countSearchMatchesByCategory(baseApps.value, searchQuery.value); + return countSearchMatchesByCategory(sourceApps, searchQuery.value); } const counts: Record = { all: apps.value.length }; - apps.value.forEach((app) => { + sourceApps.forEach((app) => { if (!counts[app.category]) counts[app.category] = 0; counts[app.category]++; }); @@ -714,6 +730,12 @@ const selectTab = (tab: string) => { ) { loadHome(); } + if (tab !== "home" && tab !== "all") { + const entry = sidebarEntries.value.find((e) => e.id === tab); + if (entry && entry.type === "category") { + loadTabApps(tab); + } + } }; const selectSubCategory = (category: string) => { @@ -728,7 +750,7 @@ const getEffectiveCategory = (): string => { const entry = sidebarEntries.value.find((e) => e.id === activeTab.value); if (entry) { - if (entry.type === "category" && entry.value) return entry.value; + if (entry.type === "category") return selectedCategory.value; if (entry.type === "search") { searchQuery.value = entry.value || ""; return selectedCategory.value; @@ -2323,6 +2345,139 @@ const loadSidebarConfig = async () => { } }; +const normalizeAppJson = ( + appJson: AppJson, + category: string, + origin: "spark" | "apm", +): App => ({ + name: appJson.Name, + pkgname: appJson.Pkgname, + version: appJson.Version, + filename: appJson.Filename, + torrent_address: appJson.Torrent_address, + author: appJson.Author, + contributor: appJson.Contributor, + website: appJson.Website, + update: appJson.Update, + size: appJson.Size, + more: appJson.More, + tags: appJson.Tags, + img_urls: + typeof appJson.img_urls === "string" + ? (JSON.parse(appJson.img_urls) as string[]) + : appJson.img_urls, + icons: appJson.icons, + category: category, + origin: origin, + currentStatus: "not-installed" as const, +}); + +const loadTabCategories = async () => { + const arch = window.apm_store.arch || "amd64"; + const modes: Array<"spark" | "apm"> = + storeFilter.value === "both" ? ["spark", "apm"] : [storeFilter.value]; + const newTabCategories: Record> = {}; + + for (const entry of sidebarEntries.value) { + if (entry.type !== "category") continue; + const folderName = entry.value || entry.id; + const catData: Record = {}; + + for (const mode of modes) { + const finalArch = mode === "spark" ? `${arch}-store` : `${arch}-apm`; + const path = `/${finalArch}/${folderName}/categories.json`; + + try { + const response = await axiosInstance.get(path); + const data = response.data; + Object.keys(data).forEach((key) => { + if (catData[key]) { + if (!catData[key].origins.includes(mode)) { + catData[key].origins.push(mode); + } + } else { + catData[key] = { + zh: data[key].zh || data[key], + origins: [mode], + }; + } + }); + } catch { + // 该入口没有子分类,静默忽略 + } + } + + if (Object.keys(catData).length > 0) { + newTabCategories[entry.id] = catData; + logger.info( + `入口 "${entry.id}" 加载到 ${Object.keys(catData).length} 个子分类`, + ); + } + } + + tabCategories.value = newTabCategories; +}; + +const loadTabApps = async (entryId: string) => { + if (tabApps.value[entryId]) return; + + const entry = sidebarEntries.value.find((e) => e.id === entryId); + if (!entry || entry.type !== "category") return; + + const arch = window.apm_store.arch || "amd64"; + 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]; + + for (const mode of modes) { + const finalArch = mode === "spark" ? `${arch}-store` : `${arch}-apm`; + + if (subCats && Object.keys(subCats).length > 0) { + for (const [subCat, catInfo] of Object.entries(subCats)) { + if ( + catInfo.origins && + catInfo.origins.length > 0 && + !catInfo.origins.includes(mode) + ) + 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}`, + ); + } + } + } 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}`); + } + } + } + + tabApps.value = { ...tabApps.value, [entryId]: loadedApps }; + logger.info(`入口 "${entryId}" 加载完成,共 ${loadedApps.length} 个应用`); +}; + const loadApps = async (onFirstBatch?: () => void) => { try { logger.info("开始加载应用数据(全并发带重试)..."); @@ -2350,28 +2505,9 @@ const loadApps = async (onFirstBatch?: () => void) => { logger.info(`加载分类: ${category} (来源: ${mode})`); const categoryApps = await fetchWithRetry(path); - const normalizedApps = (categoryApps || []).map((appJson) => ({ - name: appJson.Name, - pkgname: appJson.Pkgname, - version: appJson.Version, - filename: appJson.Filename, - torrent_address: appJson.Torrent_address, - author: appJson.Author, - contributor: appJson.Contributor, - website: appJson.Website, - update: appJson.Update, - size: appJson.Size, - more: appJson.More, - tags: appJson.Tags, - img_urls: - typeof appJson.img_urls === "string" - ? (JSON.parse(appJson.img_urls) as string[]) - : appJson.img_urls, - icons: appJson.icons, - category: category, - origin: mode as "spark" | "apm", - currentStatus: "not-installed" as const, - })); + const normalizedApps = (categoryApps || []).map((appJson) => + normalizeAppJson(appJson, category, mode as "spark" | "apm"), + ); // 增量式更新,让用户尽快看到部分数据 apps.value.push(...normalizedApps); @@ -2444,6 +2580,8 @@ onMounted(async () => { await loadSidebarConfig(); + await loadTabCategories(); + // 分类目录加载后,并行加载主页数据和所有应用列表 // 使用非阻塞方式加载,让UI先展示出来 loading.value = true; diff --git a/src/global/storeConfig.ts b/src/global/storeConfig.ts index a0221791..5d8b7134 100644 --- a/src/global/storeConfig.ts +++ b/src/global/storeConfig.ts @@ -10,8 +10,7 @@ export const APM_STORE_STATS_BASE_URL: string = export const DEFAULT_SPARK_BACKEND_BASE_URL = "http://127.0.0.1:8000"; export const SPARK_BACKEND_BASE_URL: string = - import.meta.env.VITE_SPARK_BACKEND_BASE_URL || - DEFAULT_SPARK_BACKEND_BASE_URL; + import.meta.env.VITE_SPARK_BACKEND_BASE_URL || DEFAULT_SPARK_BACKEND_BASE_URL; export const SPARK_ACCOUNT_CENTER_URL: string = import.meta.env.VITE_SPARK_ACCOUNT_CENTER_URL ||