From 750e1d89e4ea95c6c3a5db0a00b12d9b4a652c7b Mon Sep 17 00:00:00 2001 From: shenmo Date: Tue, 14 Jul 2026 13:39:20 +0800 Subject: [PATCH] feat: add SPK share link support and optimize category count calculation - add SPK share button in app detail pages and support parsing SPK links in search box - fix category total count to exclude the all entry - remove redundant count badge from sidebar tabs - rewrite category count calculation with proper filtering of all entry - update search category count logic to use base apps for all tab count --- src/App.vue | 26 +++++++++++++++++++++++++- src/components/AppDetailModal.vue | 30 ++++++++++++++++++++++++++++++ src/components/AppDetailPage.vue | 29 +++++++++++++++++++++++++++++ src/components/AppHeader.vue | 15 ++++++++++++++- src/components/AppSidebar.vue | 5 ----- src/components/CategoryBar.vue | 5 +++-- 6 files changed, 101 insertions(+), 9 deletions(-) diff --git a/src/App.vue b/src/App.vue index e73ad5bf..9c2c6f69 100644 --- a/src/App.vue +++ b/src/App.vue @@ -58,6 +58,7 @@ @open-install-settings="handleOpenInstallSettings" @open-about="openAboutModal" @toggle-sidebar="isSidebarOpen = !isSidebarOpen" + @spk-link="handleSpkLink" /> { const sourceApps = displayApps.value; if (searchQuery.value.trim()) { - return countSearchMatchesByCategory(sourceApps, searchQuery.value); + // all 统计所有应用中的搜索匹配数(不随 tab 变化) + const allCounts = countSearchMatchesByCategory( + baseApps.value, + searchQuery.value, + ); + // 各分类统计当前 tab 中的搜索匹配数 + const tabCounts = countSearchMatchesByCategory( + sourceApps, + searchQuery.value, + ); + return { ...tabCounts, all: allCounts.all }; } const counts: Record = { all: apps.value.length }; @@ -2694,6 +2705,19 @@ const handleSearchInput = (value: string) => { searchQuery.value = value; }; +const handleSpkLink = (pkgname: string) => { + currentView.value = "default"; + activeTab.value = "all"; + // 尝试从已加载的应用中查找 + const target = apps.value.find((a) => a.pkgname === pkgname); + if (target) { + openDetail({ ...target, _fromDeepLink: true }); + } else { + // 找不到时回退到搜索 + searchQuery.value = pkgname; + } +}; + const handleSearchFocus = () => { currentView.value = "default"; if (activeTab.value === "home") activeTab.value = "all"; diff --git a/src/components/AppDetailModal.vue b/src/components/AppDetailModal.vue index c38feb82..5f437b2a 100644 --- a/src/components/AppDetailModal.vue +++ b/src/components/AppDetailModal.vue @@ -188,6 +188,17 @@ {{ favoriteButtonText }} + @@ -733,6 +744,25 @@ const hideImage = (e: Event) => { (e.target as HTMLElement).style.display = "none"; }; +const spkCopied = ref(false); +let spkCopiedTimer: ReturnType | null = null; + +const copySpkLink = async () => { + if (!displayApp.value?.pkgname) return; + const spkLink = `spk://store/${displayApp.value.category || "unknown"}/${displayApp.value.pkgname}`; + try { + await navigator.clipboard.writeText(spkLink); + spkCopied.value = true; + if (spkCopiedTimer) clearTimeout(spkCopiedTimer); + spkCopiedTimer = setTimeout(() => { + spkCopied.value = false; + }, 2000); + } catch { + // 降级方案:使用 prompt + prompt("请手动复制 SPK 分享链接:", spkLink); + } +}; + const onOverlayWheel = (e: WheelEvent) => { const target = e.target as HTMLElement; if (target.closest(".overflow-y-auto, .overflow-auto")) return; diff --git a/src/components/AppDetailPage.vue b/src/components/AppDetailPage.vue index d9ad1807..408efd3b 100644 --- a/src/components/AppDetailPage.vue +++ b/src/components/AppDetailPage.vue @@ -125,6 +125,17 @@ {{ favoriteButtonText }} +
{ const hideImage = (event: Event) => { (event.target as HTMLElement).style.display = "none"; }; + +const spkCopied = ref(false); +let spkCopiedTimer: ReturnType | null = null; + +const copySpkLink = async () => { + if (!displayApp.value?.pkgname) return; + const spkLink = `spk://store/${displayApp.value.category || "unknown"}/${displayApp.value.pkgname}`; + try { + await navigator.clipboard.writeText(spkLink); + spkCopied.value = true; + if (spkCopiedTimer) clearTimeout(spkCopiedTimer); + spkCopiedTimer = setTimeout(() => { + spkCopied.value = false; + }, 2000); + } catch { + prompt("请手动复制 SPK 分享链接:", spkLink); + } +}; diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue index a345e6ab..5c6e0b39 100644 --- a/src/components/AppHeader.vue +++ b/src/components/AppHeader.vue @@ -19,7 +19,7 @@ id="searchBox" v-model="localSearchQuery" class="w-full rounded-2xl border border-slate-200/70 bg-white/80 py-3 pl-12 pr-20 text-sm text-slate-700 shadow-sm outline-none transition placeholder:text-slate-400 focus:border-brand/50 focus:ring-4 focus:ring-brand/10 dark:border-slate-800/70 dark:bg-slate-900/60 dark:text-slate-200" - placeholder="搜索应用名 / 包名 / 标签" + placeholder="搜索应用名 / 包名 / 标签 / 粘贴 SPK 分享链接" @focus="handleSearchFocus" @input="handleInput" /> @@ -68,6 +68,7 @@ const emit = defineEmits<{ (e: "open-install-settings"): void; (e: "open-about"): void; (e: "toggle-sidebar"): void; + (e: "spk-link", pkgname: string): void; }>(); const localSearchQuery = ref(props.searchQuery || ""); @@ -77,6 +78,18 @@ const handleSearchFocus = () => { }; const handleInput = () => { + const value = localSearchQuery.value.trim(); + // 检测 SPK 分享链接: spk://store/{category}/{pkgname} + const spkMatch = value.match(/^spk:\/\/search\/(.+)$/i) || + value.match(/^spk:\/\/store\/[^/]+\/(.+)$/i); + if (spkMatch) { + const pkgname = spkMatch[1].trim(); + if (pkgname) { + localSearchQuery.value = ""; + emit("spk-link", pkgname); + return; + } + } emit("update-search", localSearchQuery.value); }; diff --git a/src/components/AppSidebar.vue b/src/components/AppSidebar.vue index c1e9674b..bcd95679 100644 --- a/src/components/AppSidebar.vue +++ b/src/components/AppSidebar.vue @@ -96,11 +96,6 @@ {{ entry.name }} - {{ entryCounts[entry.id] }} diff --git a/src/components/CategoryBar.vue b/src/components/CategoryBar.vue index 613ccc30..1aa90f14 100644 --- a/src/components/CategoryBar.vue +++ b/src/components/CategoryBar.vue @@ -45,9 +45,10 @@ const emit = defineEmits<{ const totalCount = computed(() => { let total = 0; - Object.values(props.categoryCounts).forEach((v) => { + for (const [key, v] of Object.entries(props.categoryCounts)) { + if (key === "all") continue; // all 是总计,不是分类 if (typeof v === "number") total += v; - }); + } return total; });