feat(detail): 应用详情默认展开已安装版本

This commit is contained in:
2026-08-15 16:46:32 +08:00
parent b4fbd44ed9
commit f3728eae00
4 changed files with 112 additions and 73 deletions
+2
View File
@@ -15,6 +15,8 @@
- `loong64` 架构下尊重 `--no-spark` 启动参数,不再硬编码覆盖。 - `loong64` 架构下尊重 `--no-spark` 启动参数,不再硬编码覆盖。
- `openDownloadedApp` 的 IPC 调用补充 `.catch` 错误处理。 - `openDownloadedApp` 的 IPC 调用补充 `.catch` 错误处理。
7. **应用详情默认展开已安装版本**:打开应用详情时,若本地只安装了 Spark / APM 中的某一版本,则默认展开该已装来源(例如 WeChat 默认 APM,但本机只装 Spark 版时默认展开 Spark);若两版都已安装,则按优先级/标签策略展示。
相关提交:`1d9d25d6``c75938aa``39a358bb``51473cac``cf0e72ff``b0d60737``d3705ed4` 相关提交:`1d9d25d6``c75938aa``39a358bb``51473cac``cf0e72ff``b0d60737``d3705ed4`
## [1.1.1](https://github.com/elysia-best/apm-app-store/compare/v1.1.0...v1.1.1) (2026-02-17) ## [1.1.1](https://github.com/elysia-best/apm-app-store/compare/v1.1.0...v1.1.1) (2026-02-17)
+91 -66
View File
@@ -501,69 +501,69 @@
</div> </div>
</div> </div>
</div> </div>
</Transition>
<!-- 双来源安装打开时选择 APM 还是 Spark --> <!-- 双来源安装打开时选择 APM 还是 Spark -->
<Transition <Transition
enter-active-class="duration-200 ease-out" enter-active-class="duration-200 ease-out"
enter-from-class="opacity-0 scale-95" enter-from-class="opacity-0 scale-95"
enter-to-class="opacity-100 scale-100" enter-to-class="opacity-100 scale-100"
leave-active-class="duration-150 ease-in" leave-active-class="duration-150 ease-in"
leave-from-class="opacity-100 scale-100" leave-from-class="opacity-100 scale-100"
leave-to-class="opacity-0 scale-95" leave-to-class="opacity-0 scale-95"
>
<div
v-if="openChoiceVisible"
class="fixed inset-0 z-[85] flex items-center justify-center bg-slate-900/70 p-4"
@click.self="openChoiceVisible = false"
> >
<div <div
v-if="openChoiceVisible" class="relative w-full max-w-sm overflow-hidden rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
class="fixed inset-0 z-[85] flex items-center justify-center bg-slate-900/70 p-4"
@click.self="openChoiceVisible = false"
> >
<div <div class="mb-5 flex items-start gap-3">
class="relative w-full max-w-sm overflow-hidden rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900" <div
> class="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-gradient-to-br from-sky-100 to-indigo-50 shadow-inner dark:from-sky-900/30 dark:to-indigo-800/20"
<div class="mb-5 flex items-start gap-3"> >
<div <i class="fas fa-external-link-alt text-xl text-sky-500"></i>
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-gradient-to-br from-sky-100 to-indigo-50 shadow-inner dark:from-sky-900/30 dark:to-indigo-800/20"
>
<i class="fas fa-external-link-alt text-xl text-sky-500"></i>
</div>
<div>
<h3 class="text-lg font-bold text-slate-900 dark:text-white">
打开应用
</h3>
<p class="mt-1 text-sm text-slate-500 dark:text-slate-400">
该应用同时通过 APM Spark 安装请选择要打开的来源
</p>
</div>
</div> </div>
<div class="flex gap-3"> <div>
<button <h3 class="text-lg font-bold text-slate-900 dark:text-white">
type="button" 打开应用
class="inline-flex flex-1 items-center justify-center gap-2 rounded-xl border border-amber-300/70 px-4 py-2.5 text-sm font-semibold text-amber-700 transition hover:bg-amber-50 dark:border-amber-500/40 dark:text-amber-300 dark:hover:bg-amber-500/10" </h3>
@click="confirmOpen('apm')" <p class="mt-1 text-sm text-slate-500 dark:text-slate-400">
> 该应用同时通过 APM Spark 安装请选择要打开的来源
<i class="fas fa-box-open"></i> </p>
打开 APM
</button>
<button
type="button"
class="inline-flex flex-1 items-center justify-center gap-2 rounded-xl border border-sky-300/70 px-4 py-2.5 text-sm font-semibold text-sky-700 transition hover:bg-sky-50 dark:border-sky-500/40 dark:text-sky-300 dark:hover:bg-sky-500/10"
@click="confirmOpen('spark')"
>
<i class="fas fa-bolt"></i>
打开 Spark
</button>
</div>
<div class="mt-4 flex justify-end">
<button
type="button"
class="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-600 transition hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
@click="openChoiceVisible = false"
>
取消
</button>
</div> </div>
</div> </div>
<div class="flex gap-3">
<button
type="button"
class="inline-flex flex-1 items-center justify-center gap-2 rounded-xl border border-amber-300/70 px-4 py-2.5 text-sm font-semibold text-amber-700 transition hover:bg-amber-50 dark:border-amber-500/40 dark:text-amber-300 dark:hover:bg-amber-500/10"
@click="confirmOpen('apm')"
>
<i class="fas fa-box-open"></i>
打开 APM
</button>
<button
type="button"
class="inline-flex flex-1 items-center justify-center gap-2 rounded-xl border border-sky-300/70 px-4 py-2.5 text-sm font-semibold text-sky-700 transition hover:bg-sky-50 dark:border-sky-500/40 dark:text-sky-300 dark:hover:bg-sky-500/10"
@click="confirmOpen('spark')"
>
<i class="fas fa-bolt"></i>
打开 Spark
</button>
</div>
<div class="mt-4 flex justify-end">
<button
type="button"
class="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-600 transition hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
@click="openChoiceVisible = false"
>
取消
</button>
</div>
</div> </div>
</Transition> </div>
</Transition> </Transition>
</template> </template>
@@ -656,13 +656,15 @@ const openWebsite = (url: string) => {
} }
}; };
// 根据「标签优先显示策略」与应用的可用来源标签,计算默认展示的来源 // 根据「标签优先显示策略」、本地安装状态与应用的可用来源标签,计算默认展示的来源
const computeDefaultViewingOrigin = ( const computeDefaultViewingOrigin = (
app: App, app: App,
strategy: TagPriorityStrategy, strategy: TagPriorityStrategy,
sparkInstalled: boolean,
apmInstalled: boolean,
): "spark" | "apm" => { ): "spark" | "apm" => {
// 父组件显式指定展示来源(如从已安装应用页按特定来源打开)时优先级最高, // 父组件显式指定展示来源(如从已安装应用页按特定来源打开)时优先级最高,
// 此时强制覆盖用户标签策略;否则交由下方策略逻辑决定。 // 此时强制覆盖用户标签策略与本地安装状态;否则交由下方策略逻辑决定。
// 注意:被强制的应用(forceViewingOrigin=true)在详情页保持打开期间, // 注意:被强制的应用(forceViewingOrigin=true)在详情页保持打开期间,
// 即使设置页切换标签策略也不会实时变更其展示来源(已安装来源高于策略,属设计行为)。 // 即使设置页切换标签策略也不会实时变更其展示来源(已安装来源高于策略,属设计行为)。
if (app.forceViewingOrigin && app.viewingOrigin) return app.viewingOrigin; if (app.forceViewingOrigin && app.viewingOrigin) return app.viewingOrigin;
@@ -677,7 +679,15 @@ const computeDefaultViewingOrigin = (
if (available.length === 0) return app.origin; // 兜底 if (available.length === 0) return app.origin; // 兜底
if (available.length === 1) return available[0]; // 仅有一个标签时默认展示该标签 if (available.length === 1) return available[0]; // 仅有一个标签时默认展示该标签
// 按策略优先选择对应标签 // 本地只安装了某一个版本时,优先展开已安装的版本,保证打开应用时能直接使用
// 已安装来源(例如 WeChat 默认 APM,但本机只装了 Spark 版时默认展开 Spark)。
const installed: Array<"spark" | "apm"> = [];
if (sparkInstalled && app.sparkApp) installed.push("spark");
if (apmInstalled && app.apmApp) installed.push("apm");
if (installed.length === 1) return installed[0];
// 都安装 / 都未安装时,继续按用户策略或应用配置的优先级展示:
// - 按策略优先选择对应标签
const preferred: "spark" | "apm" | null = const preferred: "spark" | "apm" | null =
strategy === "spark" ? "spark" : strategy === "apm" ? "apm" : null; strategy === "spark" ? "spark" : strategy === "apm" ? "apm" : null;
if (preferred && available.includes(preferred)) { if (preferred && available.includes(preferred)) {
@@ -690,18 +700,29 @@ const computeDefaultViewingOrigin = (
return available.includes(auto) ? auto : available[0]; return available.includes(auto) ? auto : available[0];
}; };
// 进入详情页时按「标签优先显示策略」计算默认来源标签。 // 进入详情页时按「标签优先显示策略」与本地安装状态计算默认来源标签。
// 同时监听 props.app共享策略 ref // 同时监听 props.app共享策略 ref 与安装状态
// - props.app 变化(重开/切换应用)→ 始终按当前策略重算(手动切标签仅本次会话预览,不持久) // - props.app 变化(重开/切换应用)→ 始终按当前策略重算(手动切标签仅本次会话预览,不持久)
// - 策略变化(在设置页实时切换)→ 已打开且未手动切标签的详情页立即更新默认标签 // - 策略变化(在设置页实时切换)→ 已打开且未手动切标签的详情页立即更新默认标签
// - 安装状态异步返回(如仅 Spark 已安装)→ 已打开且未手动切标签时自动切换到已安装版本
// 手动点标签页(selectOrigin)只是临时预览:置 manualOverride 标记,本次会话内保持, // 手动点标签页(selectOrigin)只是临时预览:置 manualOverride 标记,本次会话内保持,
// 既不被策略变化强行拉回,也不跨重开持久(重开后会按设置重算)。 // 既不被策略变化强行拉回,也不跨重开持久(重开后会按设置重算)。
// 注:initTagPriorityStrategy() 已在 App.vue 启动时统一调用,此处不再重复。 // 注:initTagPriorityStrategy() 已在 App.vue 启动时统一调用,此处不再重复。
const manualOverride = ref(false); // 用户是否手动切换过标签(本次会话) const manualOverride = ref(false); // 用户是否手动切换过标签(本次会话)
let lastPkgname: string | null = null; // 上一次计算对应的应用 pkgname,用于识别"新打开/重开" let lastPkgname: string | null = null; // 上一次计算对应的应用 pkgname,用于识别"新打开/重开"
watch( watch(
[() => props.app, tagPriorityStrategyRef], [
([newApp]: [App | null, TagPriorityStrategy]) => { () => props.app,
tagPriorityStrategyRef,
() => props.sparkInstalled,
() => props.apmInstalled,
],
([newApp, strategy, sparkInstalled, apmInstalled]: [
App | null,
TagPriorityStrategy,
boolean,
boolean,
]) => {
isIconLoaded.value = false; isIconLoaded.value = false;
if (!newApp) { if (!newApp) {
// 详情页关闭:清空会话状态,确保下次打开按最新策略重算 // 详情页关闭:清空会话状态,确保下次打开按最新策略重算
@@ -710,20 +731,24 @@ watch(
return; return;
} }
if (newApp.pkgname !== lastPkgname) { if (newApp.pkgname !== lastPkgname) {
// 新应用(含重开后再次打开同一应用):重置手动标记,按当前策略计算 // 新应用(含重开后再次打开同一应用):重置手动标记,按当前策略/安装状态计算
lastPkgname = newApp.pkgname; lastPkgname = newApp.pkgname;
manualOverride.value = false; manualOverride.value = false;
viewingOrigin.value = computeDefaultViewingOrigin( viewingOrigin.value = computeDefaultViewingOrigin(
newApp, newApp,
tagPriorityStrategyRef.value, strategy,
sparkInstalled,
apmInstalled,
); );
} else if (manualOverride.value || newApp.forceViewingOrigin) { } else if (manualOverride.value || newApp.forceViewingOrigin) {
// 同一应用且用户正在手动预览 / 父组件强制来源:保留当前标签,不重算 // 同一应用且用户正在手动预览 / 父组件强制来源:保留当前标签,不重算
} else { } else {
// 同一应用、无手动预览、策略变化 → 实时按新策略更新默认标签 // 同一应用、无手动预览、策略/安装状态变化 → 实时按新状态更新默认标签
viewingOrigin.value = computeDefaultViewingOrigin( viewingOrigin.value = computeDefaultViewingOrigin(
newApp, newApp,
tagPriorityStrategyRef.value, strategy,
sparkInstalled,
apmInstalled,
); );
} }
}, },
+18 -7
View File
@@ -260,6 +260,7 @@ const openDetail = async (app: App | OpenDetailInput) => {
} }
// 检查 Spark/APM 安装状态,已安装的版本优先展示 // 检查 Spark/APM 安装状态,已安装的版本优先展示
let installedStateKnown = false;
if (finalApp.isMerged && (finalApp.sparkApp || finalApp.apmApp)) { if (finalApp.isMerged && (finalApp.sparkApp || finalApp.apmApp)) {
const [sparkInstalled, apmInstalled] = await Promise.all([ const [sparkInstalled, apmInstalled] = await Promise.all([
finalApp.sparkApp finalApp.sparkApp
@@ -275,13 +276,17 @@ const openDetail = async (app: App | OpenDetailInput) => {
}) as Promise<boolean>) }) as Promise<boolean>)
: Promise.resolve(false), : Promise.resolve(false),
]); ]);
installedStateKnown = true;
// 来源默认展示规则: // 来源默认展示规则:
// 1) 已安装页打开:按安装类型打开。 // 1) 已安装页打开:按安装类型打开。
// - 仅一个来源已安装 → 强制展示该已装来源(无需策略覆盖) // - 仅一个来源已安装 → 强制展示该已装来源(无需策略覆盖)
// - 多个来源已安装 → 按「设置的优先标签」打开(策略) // - 多个来源已安装 → 按「设置的优先标签/应用优先级」打开(策略)
// 2) 其他页面:一律按「设置的优先标签」打开(策略 > 服务端混合默认),不强制。 // 2) 其他页面:
// - 仅一个来源已安装 → 默认展示已装版本(详情页按安装状态优先,保证可直接打开)
// - 都安装/都未安装 → 按「标签优先显示策略/应用优先级」打开
// 注:forceViewingOrigin 仅用于「已安装页 + 唯一安装来源」这一显式安装类型场景, // 注:forceViewingOrigin 仅用于「已安装页 + 唯一安装来源」这一显式安装类型场景,
// 其余情况都不强制,交由详情页按用户标签策略重算。 // 其余情况都不强制,交由详情页按安装状态/用户标签策略重算。
let forceOrigin: "spark" | "apm" | undefined = undefined; let forceOrigin: "spark" | "apm" | undefined = undefined;
if (fromInstalled) { if (fromInstalled) {
const installedOrigins = (app as Record<string, unknown>).origins as const installedOrigins = (app as Record<string, unknown>).origins as
@@ -310,7 +315,7 @@ const openDetail = async (app: App | OpenDetailInput) => {
finalApp.viewingOrigin = forceOrigin; finalApp.viewingOrigin = forceOrigin;
finalApp.forceViewingOrigin = true; finalApp.forceViewingOrigin = true;
} else if (sparkInstalled && !apmInstalled) { } else if (sparkInstalled && !apmInstalled) {
// 仅 Spark 安装(其他页面):默认回退展示已装版本,不强制(仍受用户策略覆盖 // 仅 Spark 安装(其他页面):默认回退展示已装版本,不强制(详情页会按安装状态优先
finalApp.viewingOrigin = "spark"; finalApp.viewingOrigin = "spark";
} else if (apmInstalled && !sparkInstalled) { } else if (apmInstalled && !sparkInstalled) {
finalApp.viewingOrigin = "apm"; finalApp.viewingOrigin = "apm";
@@ -322,6 +327,10 @@ const openDetail = async (app: App | OpenDetailInput) => {
finalApp.sparkApp || finalApp, finalApp.sparkApp || finalApp,
); );
} }
// 已拿到安装状态时直接写入全局状态,避免详情页先按“都未安装”闪一下再切到已装版本
currentAppSparkInstalled.value = sparkInstalled;
currentAppApmInstalled.value = apmInstalled;
} }
const displayAppForScreenshots = const displayAppForScreenshots =
@@ -345,9 +354,11 @@ const openDetail = async (app: App | OpenDetailInput) => {
}); });
showModal.value = true; showModal.value = true;
currentAppSparkInstalled.value = false; if (!installedStateKnown) {
currentAppApmInstalled.value = false; currentAppSparkInstalled.value = false;
checkAppInstalled(finalApp); currentAppApmInstalled.value = false;
checkAppInstalled(finalApp);
}
if ( if (
isLoggedIn.value && isLoggedIn.value &&
favoriteFolders.value.length === 0 && favoriteFolders.value.length === 0 &&
+1
View File
@@ -9,6 +9,7 @@
// - "spark" : Spark 优先 —— 默认选中 Spark 标签;若无 Spark 标签则回退优先级策略 // - "spark" : Spark 优先 —— 默认选中 Spark 标签;若无 Spark 标签则回退优先级策略
// - "apm" : APM 优先 —— 默认选中 APM 标签;若无 APM 标签则回退优先级策略 // - "apm" : APM 优先 —— 默认选中 APM 标签;若无 APM 标签则回退优先级策略
// 注:当应用仅有一个来源标签时,无论策略如何都默认展示该标签。 // 注:当应用仅有一个来源标签时,无论策略如何都默认展示该标签。
// 注:若本地仅安装了某一个来源版本,详情页会优先展开已安装版本,此策略在都安装/都未安装时生效。
import { ref } from "vue"; import { ref } from "vue";