From e7fb8e689ae9ba9c847e29824607c544b6ef201c Mon Sep 17 00:00:00 2001 From: shenmo Date: Sun, 29 Mar 2026 17:21:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=BA=94=E7=94=A8=E8=AF=A6=E6=83=85):=20?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E5=BA=94=E7=94=A8=E8=AF=A6=E6=83=85=E9=A1=B5?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构应用详情页逻辑,支持从首页和深度链接直接打开应用时自动获取完整信息 优化应用卡片来源标识显示,支持同时显示多个来源 统一代码格式,修复多行字符串和模板字符串的换行问题 --- electron/main/backend/install-manager.ts | 16 +- src/App.vue | 196 ++++++++++++++++++----- src/components/AppCard.vue | 58 ++++--- src/components/AppDetailModal.vue | 150 +++++++++++++---- src/components/AppGrid.vue | 5 +- src/components/HomeView.vue | 49 ++++-- src/modules/processInstall.ts | 7 +- 7 files changed, 360 insertions(+), 121 deletions(-) diff --git a/electron/main/backend/install-manager.ts b/electron/main/backend/install-manager.ts index 5e7fe784..66312021 100644 --- a/electron/main/backend/install-manager.ts +++ b/electron/main/backend/install-manager.ts @@ -480,9 +480,7 @@ async function processNextInQueue() { ) { clearInterval(timeoutChecker); child.kill(); - reject( - new Error(`下载卡在0%超过 ${currentTimeout / 1000} 秒`), - ); + reject(new Error(`下载卡在0%超过 ${currentTimeout / 1000} 秒`)); } }, progressCheckInterval); @@ -527,7 +525,9 @@ async function processNextInQueue() { } catch (err) { retryCount++; if (retryCount >= timeoutList.length) { - throw new Error(`下载失败,已重试 ${timeoutList.length} 次: ${err}`); + throw new Error( + `下载失败,已重试 ${timeoutList.length} 次: ${err}`, + ); } sendLog(`下载失败,准备重试 (${retryCount}/${timeoutList.length})`); // 等待2秒后重试 @@ -860,7 +860,9 @@ ipcMain.handle("list-installed", async () => { if (hasEntries) { try { const desktopFiles = fs.readdirSync(entriesPath); - logger.debug(`Found desktop files for ${pkgname}: ${desktopFiles.join(", ")}`); + logger.debug( + `Found desktop files for ${pkgname}: ${desktopFiles.join(", ")}`, + ); for (const file of desktopFiles) { if (file.endsWith(".desktop")) { const desktopPath = path.join(entriesPath, file); @@ -870,7 +872,9 @@ ipcMain.handle("list-installed", async () => { const iconMatch = content.match(/^Icon=(.+)$/m); if (nameMatch) appName = nameMatch[1].trim(); if (iconMatch) icon = iconMatch[1].trim(); - logger.debug(`Parsed desktop file for ${pkgname}: name=${appName}, icon=${icon}`); + logger.debug( + `Parsed desktop file for ${pkgname}: name=${appName}, icon=${icon}`, + ); break; } } diff --git a/src/App.vue b/src/App.vue index 019b9236..58386271 100644 --- a/src/App.vue +++ b/src/App.vue @@ -389,84 +389,191 @@ const selectCategory = (category: string) => { } }; +// 从仓库获取应用详细信息的辅助函数 +const fetchAppFromStore = async ( + pkgname: string, + category: string, + origin: "spark" | "apm", +): Promise => { + try { + const arch = window.apm_store.arch || "amd64"; + const finalArch = origin === "spark" ? `${arch}-store` : `${arch}-apm`; + const appJsonUrl = `${APM_STORE_BASE_URL}/${finalArch}/${category}/${pkgname}/app.json`; + const response = await fetch(cacheBuster(appJsonUrl)); + if (!response.ok) return null; + const appJson = await response.json(); + return { + name: appJson.Name || "", + pkgname: appJson.Pkgname || 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", + }; + } catch (e) { + console.warn(`Failed to fetch ${origin} app info for ${pkgname}`, e); + return null; + } +}; + const openDetail = async (app: App | Record) => { - // 提取 pkgname(必须存在) + // 提取 pkgname 和 category(必须存在) const pkgname = (app as Record).pkgname as string; + const category = + ((app as Record).category as string) || "unknown"; + // 检查是否来自 HomeView 或 DeepLink(需要重新获取完整信息) + const fromHomeView = (app as Record)._fromHomeView === true; + const fromDeepLink = (app as Record)._fromDeepLink === true; + const needFetchFromStore = fromHomeView || fromDeepLink; if (!pkgname) { console.warn("openDetail: 缺少 pkgname", app); return; } - // 首先尝试从当前已经处理好(合并/筛选)的 filteredApps 中查找,以便获取 isMerged 状态等 + // 首先尝试从当前已经处理好(合并/筛选)的 filteredApps 中查找 let fullApp = filteredApps.value.find((a) => a.pkgname === pkgname); - // 如果没找到(可能是从已安装列表之类的其他入口打开的),回退到全局 apps 中查找完整 App + // 如果没找到,回退到全局 apps 中查找 if (!fullApp) { fullApp = apps.value.find((a) => a.pkgname === pkgname); } - if (!fullApp) { - // 构造一个最小可用的 App 对象 - fullApp = { - name: ((app as Record).name as string) || "", - pkgname: pkgname, - version: ((app as Record).version as string) || "", - filename: ((app as Record).filename as string) || "", - category: - ((app as Record).category as string) || "unknown", - torrent_address: "", - author: "", - contributor: "", - website: "", - update: "", - size: "", - more: ((app as Record).more as string) || "", - tags: "", - img_urls: [], - icons: "", - origin: - ((app as Record).origin as "spark" | "apm") || "apm", - currentStatus: "not-installed", - } as App; + + let finalApp: App; + + // 来自 HomeView 或 DeepLink 的应用需要重新从仓库获取完整信息 + if (needFetchFromStore) { + // 从 Spark 和 APM 仓库获取完整的应用信息 + const [sparkApp, apmApp] = await Promise.all([ + storeFilter.value !== "apm" + ? fetchAppFromStore(pkgname, category, "spark") + : Promise.resolve(null), + storeFilter.value !== "spark" + ? fetchAppFromStore(pkgname, category, "apm") + : Promise.resolve(null), + ]); + + // 构建合并的应用对象 + if (sparkApp || apmApp) { + // 如果两个仓库都有这个应用,创建合并对象 + if (sparkApp && apmApp) { + finalApp = { + ...sparkApp, // 默认使用 Spark 的信息作为主显示 + isMerged: true, + sparkApp: sparkApp, + apmApp: apmApp, + viewingOrigin: "spark", // 默认查看 Spark 版本 + }; + } else if (sparkApp) { + finalApp = sparkApp; + } else { + finalApp = apmApp!; + } + } else if (fullApp) { + finalApp = fullApp; + } else { + // 两个仓库都没有找到,且本地也没有,构造一个最小可用的 App 对象 + finalApp = { + name: ((app as Record).name as string) || "", + pkgname: pkgname, + version: ((app as Record).version as string) || "", + filename: ((app as Record).filename as string) || "", + category: category, + torrent_address: "", + author: "", + contributor: "", + website: "", + update: "", + size: "", + more: ((app as Record).more as string) || "", + tags: "", + img_urls: [], + icons: "", + origin: + ((app as Record).origin as "spark" | "apm") || "apm", + currentStatus: "not-installed", + } as App; + } + } else { + // 非 HomeView 来源,使用原来的逻辑 + if (fullApp) { + finalApp = fullApp; + } else { + // 构造一个最小可用的 App 对象 + finalApp = { + name: ((app as Record).name as string) || "", + pkgname: pkgname, + version: ((app as Record).version as string) || "", + filename: ((app as Record).filename as string) || "", + category: category, + torrent_address: "", + author: "", + contributor: "", + website: "", + update: "", + size: "", + more: ((app as Record).more as string) || "", + tags: "", + img_urls: [], + icons: "", + origin: + ((app as Record).origin as "spark" | "apm") || "apm", + currentStatus: "not-installed", + } as App; + } } - // 合并应用:先检查 Spark/APM 安装状态,已安装的版本优先展示 - if (fullApp.isMerged && (fullApp.sparkApp || fullApp.apmApp)) { + // 检查 Spark/APM 安装状态,已安装的版本优先展示 + if (finalApp.isMerged && (finalApp.sparkApp || finalApp.apmApp)) { const [sparkInstalled, apmInstalled] = await Promise.all([ - fullApp.sparkApp + finalApp.sparkApp ? (window.ipcRenderer.invoke("check-installed", { - pkgname: fullApp.sparkApp.pkgname, + pkgname: finalApp.sparkApp.pkgname, origin: "spark", }) as Promise) : Promise.resolve(false), - fullApp.apmApp + finalApp.apmApp ? (window.ipcRenderer.invoke("check-installed", { - pkgname: fullApp.apmApp.pkgname, + pkgname: finalApp.apmApp.pkgname, origin: "apm", }) as Promise) : Promise.resolve(false), ]); if (sparkInstalled && !apmInstalled) { - fullApp.viewingOrigin = "spark"; + finalApp.viewingOrigin = "spark"; } else if (apmInstalled && !sparkInstalled) { - fullApp.viewingOrigin = "apm"; + finalApp.viewingOrigin = "apm"; } - // 若都安装或都未安装,不设置 viewingOrigin,由模态框默认展示 spark + // 若都安装或都未安装,默认展示 spark } const displayAppForScreenshots = - fullApp.viewingOrigin !== undefined && fullApp.isMerged - ? ((fullApp.viewingOrigin === "spark" - ? fullApp.sparkApp - : fullApp.apmApp) ?? fullApp) - : fullApp; + finalApp.viewingOrigin !== undefined && finalApp.isMerged + ? ((finalApp.viewingOrigin === "spark" + ? finalApp.sparkApp + : finalApp.apmApp) ?? finalApp) + : finalApp; - currentApp.value = fullApp; + currentApp.value = finalApp; currentScreenIndex.value = 0; loadScreenshots(displayAppForScreenshots); showModal.value = true; currentAppSparkInstalled.value = false; currentAppApmInstalled.value = false; - checkAppInstalled(fullApp); + checkAppInstalled(finalApp); nextTick(() => { const modal = document.querySelector( @@ -1181,9 +1288,12 @@ onMounted(async () => { (_event: IpcRendererEvent, data: { pkgname: string }) => { // 根据包名直接打开应用详情 const tryOpen = () => { + // 先切换到"全部应用"分类 + activeCategory.value = "all"; + // 使用类似 HomeView 的方式打开应用,从两个仓库获取完整信息 const target = apps.value.find((a) => a.pkgname === data.pkgname); if (target) { - openDetail(target); + openDetail({ ...target, _fromDeepLink: true }); } else { // 如果找不到应用,回退到搜索模式 searchQuery.value = data.pkgname; diff --git a/src/components/AppCard.vue b/src/components/AppCard.vue index b76297be..ed0e3b35 100644 --- a/src/components/AppCard.vue +++ b/src/components/AppCard.vue @@ -21,30 +21,29 @@ > {{ app.name || "" }} - - {{ - app.isMerged - ? "Spark/APM" - : app.origin === "spark" - ? "Spark" - : "APM" - }} - + +
+ + Spark + + + APM + +
{{ app.pkgname || "" }} · {{ app.version || "" }}
-
- {{ description || '\u00A0' }} +
+ {{ description || "\u00A0" }}
@@ -57,6 +56,9 @@ import type { App } from "../global/typedefinition"; const props = defineProps<{ app: App; + // 从外部传入的 Spark/APM 可用性信息 + sparkAvailable?: boolean; + apmAvailable?: boolean; }>(); const emit = defineEmits<{ @@ -69,6 +71,22 @@ const loadedIcon = ref( 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect fill="%23f0f0f0" width="100" height="100"/%3E%3C/svg%3E', ); +// 是否显示 Spark 标识 +const showSparkBadge = computed(() => { + // 如果明确指定了 sparkAvailable,使用它 + if (props.sparkAvailable !== undefined) return props.sparkAvailable; + // 否则根据 app 的 origin 或 isMerged 判断 + return props.app.origin === "spark" || props.app.isMerged === true; +}); + +// 是否显示 APM 标识 +const showApmBadge = computed(() => { + // 如果明确指定了 apmAvailable,使用它 + if (props.apmAvailable !== undefined) return props.apmAvailable; + // 否则根据 app 的 origin 或 isMerged 判断 + return props.app.origin === "apm" || props.app.isMerged === true; +}); + const iconPath = computed(() => { const arch = window.apm_store.arch || "amd64"; const finalArch = diff --git a/src/components/AppDetailModal.vue b/src/components/AppDetailModal.vue index 8483936e..e3878f42 100644 --- a/src/components/AppDetailModal.vue +++ b/src/components/AppDetailModal.vue @@ -57,14 +57,25 @@
-
- 版本 - {{ displayApp?.version || "-" }} +
+ 版本 + {{ displayApp?.version || "-" }}
-
- 来源 +
+ 来源
-
- 下载量 - {{ downloadCount }} +
+ 下载量 + {{ downloadCount }}
@@ -163,55 +182,79 @@
-
+
分类 - {{ displayApp.category }} + {{ displayApp.category }}
作者 - {{ displayApp.author }} + {{ displayApp.author }}
贡献者 - {{ displayApp.contributor }} + {{ displayApp.contributor }}
大小 - {{ displayApp.size }} + {{ displayApp.size }}
更新 - {{ displayApp.update }} + {{ displayApp.update }}
网站 - {{ displayApp.website }} + {{ displayApp.website }}
标签 - {{ displayApp.tags }} + {{ displayApp.tags }}
@@ -223,7 +266,9 @@ v-if="displayApp?.more && displayApp.more.trim() !== ''" class="rounded-2xl border border-slate-200/60 bg-slate-50/50 p-5 dark:border-slate-800/60 dark:bg-slate-800/30" > -

+

应用详情

@@ -241,7 +286,9 @@
-

+

应用截图

@@ -295,53 +342,92 @@ > -

+

应用信息

-
+
应用名称 - {{ displayApp.name }} + {{ displayApp.name }}
包名 - {{ displayApp.pkgname }} + {{ displayApp.pkgname }}
版本 - {{ displayApp.version }} + {{ displayApp.version }}
分类 - {{ displayApp.category }} + {{ displayApp.category }}
作者 - {{ displayApp.author }} + {{ displayApp.author }}
贡献者 - {{ displayApp.contributor }} + {{ displayApp.contributor }}
大小 - {{ displayApp.size }} + {{ displayApp.size }}
更新时间 - {{ displayApp.update }} + {{ displayApp.update }}
标签 - {{ displayApp.tags }} + {{ displayApp.tags }}
来源 - {{ displayApp.origin === 'spark' ? 'Spark' : 'APM' }} + {{ displayApp.origin === "spark" ? "Spark" : "APM" }}
@@ -525,5 +611,3 @@ const hideImage = (e: Event) => { (e.target as HTMLElement).style.display = "none"; }; - - diff --git a/src/components/AppGrid.vue b/src/components/AppGrid.vue index 522e0b39..28638727 100644 --- a/src/components/AppGrid.vue +++ b/src/components/AppGrid.vue @@ -10,10 +10,7 @@ @open-detail="$emit('open-detail', app)" />
-
+

- {{ storeFilter === 'apm' ? '欢迎来到星火应用商店 (Amber PM)' : '欢迎来到星火应用商店' }} + {{ + storeFilter === "apm" + ? "欢迎来到星火应用商店 (Amber PM)" + : "欢迎来到星火应用商店" + }}

- {{ storeFilter === 'apm' ? '探索丰富的应用,发现更多精彩内容' : '探索丰富的应用,发现更多精彩内容' }} + {{ + storeFilter === "apm" + ? "探索丰富的应用,发现更多精彩内容" + : "探索丰富的应用,发现更多精彩内容" + }}

@@ -43,7 +51,11 @@

- {{ storeFilter === 'apm' ? '欢迎来到星火应用商店 (Amber PM)' : '欢迎来到星火应用商店' }} + {{ + storeFilter === "apm" + ? "欢迎来到星火应用商店 (Amber PM)" + : "欢迎来到星火应用商店" + }}

探索丰富的应用,发现更多精彩内容 @@ -61,29 +73,41 @@ :title="link.more as string" > -

+
-
+
-
+
{{ link.name }}
-
+
{{ link.more }}
@@ -103,7 +127,7 @@ v-for="app in section.apps" :key="app.pkgname" :app="app" - @open-detail="$emit('open-detail', $event)" + @open-detail="handleOpenDetail(app)" />
@@ -126,10 +150,15 @@ defineProps<{ storeFilter?: "spark" | "apm" | "both"; }>(); -defineEmits<{ +const emit = defineEmits<{ (e: "open-detail", app: App | Record): void; }>(); +// 处理应用卡片点击,添加来自首页的标记 +const handleOpenDetail = (app: App) => { + emit("open-detail", { ...app, _fromHomeView: true }); +}; + // 图片加载状态跟踪 const imageLoaded = reactive>({}); diff --git a/src/modules/processInstall.ts b/src/modules/processInstall.ts index 7d60b1a6..4bfc0cae 100644 --- a/src/modules/processInstall.ts +++ b/src/modules/processInstall.ts @@ -27,8 +27,7 @@ export const handleInstall = (appObj?: App) => { if ( downloads.value.find( - (d) => - d.pkgname === targetApp.pkgname && d.origin === targetApp.origin, + (d) => d.pkgname === targetApp.pkgname && d.origin === targetApp.origin, ) ) { logger.info( @@ -107,9 +106,7 @@ export const handleUpgrade = (app: App) => { (d) => d.pkgname === app.pkgname && d.origin === app.origin, ) ) { - logger.info( - `任务已存在,忽略重复添加: ${app.pkgname} (${app.origin})`, - ); + logger.info(`任务已存在,忽略重复添加: ${app.pkgname} (${app.origin})`); return; }