From d8bea4ac23331a0bb6cd10584cc5d61a4edeab05 Mon Sep 17 00:00:00 2001 From: xiyidaiwa Date: Thu, 13 Aug 2026 21:54:08 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=BA=94=E7=94=A8=E6=88=AA=E5=9B=BE?= =?UTF-8?q?=E6=8C=89=E5=AE=9E=E9=99=85=E5=AD=98=E5=9C=A8=E6=95=B0=E9=87=8F?= =?UTF-8?q?=E9=A2=84=E8=A7=88=EF=BC=8C=E4=B8=8D=E5=86=8D=E5=9B=BA=E5=AE=9A?= =?UTF-8?q?=205=20=E5=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useAppDetail.loadScreenshots 改为异步探测:并发 HEAD 请求 screen_1~5.png, 仅把返回 2xx 的真实截图加入 screenshots,不足 5 张时预览计数与按钮边界自动跟随 - ScreenPreview 已有的 disabled 边界 (currentScreenIndex === screenshots.length-1) 因此生效,切换到最后一张后禁止继续向后,消除空白页 - 新增 screenshotAbortController:打开新详情/关闭详情时取消旧探测,避免结果覆盖 - 探测异常不影响详情页弹窗,截图区域在探测完成后自动刷新/显示暂无应用截图 - debian/changelog 测试版本号 5.2.1.3-test - 验证:vue-tsc 0 / eslint 0 / dpkg-buildpackage 打包成功 --- debian/changelog | 2 +- src/composables/useAppDetail.ts | 56 ++++++++++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/debian/changelog b/debian/changelog index c945eae2..378ea83c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -spark-store (5.2.1.2-test) UNRELEASED; urgency=medium +spark-store (5.2.1.3-test) UNRELEASED; urgency=medium * Initial release. (Closes: #nnnn) diff --git a/src/composables/useAppDetail.ts b/src/composables/useAppDetail.ts index 0c38f0a6..28e94884 100644 --- a/src/composables/useAppDetail.ts +++ b/src/composables/useAppDetail.ts @@ -34,6 +34,7 @@ import { buildReviewTags, } from "../modules/appIdentity"; import { loadFavoriteMetadataForDetail } from "./useFavorites"; +import { axiosInstance } from "./useHttp"; import type { ReviewTags } from "../global/typedefinition"; import type { Ref } from "vue"; @@ -54,6 +55,9 @@ const currentReviewTags = computed(() => { }); }); +// 截图探测请求的中断控制器:每次打开新详情前取消旧探测,避免结果覆盖当前应用。 +let screenshotAbortController: AbortController | null = null; + // 从仓库获取应用详细信息的辅助函数 const fetchAppFromStore = async ( pkgname: string, @@ -329,7 +333,10 @@ const openDetail = async (app: App | OpenDetailInput) => { currentApp.value = finalApp; currentScreenIndex.value = 0; - loadScreenshots(displayAppForScreenshots); + // 截图异步探测,详情页先弹出不阻塞;探测完成后截图区域自动刷新 + loadScreenshots(displayAppForScreenshots).catch(() => { + // 异常(含取消)已在 loadScreenshots 内处理,此处仅避免未捕获 promise + }); showModal.value = true; currentAppSparkInstalled.value = false; @@ -386,19 +393,60 @@ const checkAppInstalled = (app: App) => { } }; -const loadScreenshots = (app: App) => { +// 探测单张截图是否真实存在:用 HEAD 请求,超时 3s,仅当返回 2xx 时视为有效。 +// 使用 axiosInstance 以便复用同源配置,但 HEAD PNG 不会触发 .json 缓存穿透拦截器。 +const checkScreenshotExists = async ( + url: string, + signal?: AbortSignal, +): Promise => { + try { + await axiosInstance.head(url, { timeout: 3000, signal }); + return true; + } catch { + return false; + } +}; + +const loadScreenshots = async (app: App) => { + // 取消上一应用的截图探测,避免结果覆盖到当前应用 + if (screenshotAbortController) { + screenshotAbortController.abort(); + } + const controller = new AbortController(); + screenshotAbortController = controller; + screenshots.value = []; + if (!app.category || app.category === "unknown") return; const arch = window.apm_store.arch || "amd64"; const finalArch = app.origin === "spark" ? `${arch}-store` : `${arch}-apm`; + + const candidates: string[] = []; for (let i = 1; i <= 5; i++) { - const screenshotUrl = `${APM_STORE_BASE_URL}/${finalArch}/${app.category}/${app.pkgname}/screen_${i}.png`; - screenshots.value.push(screenshotUrl); + candidates.push( + `${APM_STORE_BASE_URL}/${finalArch}/${app.category}/${app.pkgname}/screen_${i}.png`, + ); + } + + try { + // 并发探测 5 张候选截图,只保留真实存在的;失败视为无该图,不阻塞详情页打开。 + const results = await Promise.all( + candidates.map((url) => checkScreenshotExists(url, controller.signal)), + ); + const valid = candidates.filter((_, index) => results[index]); + screenshots.value = valid; + } catch { + // 当前轮次被新详情页取消或异常:不写入旧结果 } }; const closeDetail = () => { showModal.value = false; currentApp.value = null; + // 关闭详情时取消正在进行的截图探测,避免结果写入已关闭的旧应用 + if (screenshotAbortController) { + screenshotAbortController.abort(); + screenshotAbortController = null; + } }; const openScreenPreview = (index: number) => {