From b3eb1d01d9ab7a3dad88791624585b1e77209134 Mon Sep 17 00:00:00 2001 From: xiyidaiwa Date: Fri, 7 Aug 2026 18:21:39 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=A0=87=E7=AD=BE=E4=BC=98=E5=85=88?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E7=AD=96=E7=95=A5=20auto=20=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E6=8C=89=E6=9C=8D=E5=8A=A1=E7=AB=AF=E4=BC=98=E5=85=88?= =?UTF-8?q?=E7=BA=A7=E9=85=8D=E7=BD=AE=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - auto 模式用合并应用自身(含 sparkApp/apmApp)匹配 priority-config.json, 不再仅取单一子版,避免漏匹配。 - openDetail 区分「已安装页显式来源强制」与「其他页面按策略」; 非强制分支清除 forceViewingOrigin 粘性标志,避免跨入口状态泄漏。 - AppDetailModal 按 TagPriorityStrategy 响应式重算默认来源; 手动切标签仅本次会话预览,不持久。 - storeConfig.loadPriorityConfig 改用 axios(与渲染进程一致), 修复 Electron 下全局 fetch 失败导致配置为空、auto 全部回退 APM 的问题。 - 首页推荐列表 parseAppList 从 jsonUrl 提取分类,避免 category 误为 unknown。 --- electron/main/index.ts | 18 +-- src/App.vue | 57 +++++---- src/components/AppDetailModal.vue | 52 ++++++-- src/global/storeConfig.ts | 206 ++++++++++++++---------------- src/global/tagPriority.ts | 3 + src/global/typedefinition.ts | 1 + 6 files changed, 188 insertions(+), 149 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index 9a37e241..8a30bbf4 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -308,21 +308,17 @@ function getWindowStatePath(): string { // 校验保存的窗口位置是否至少部分落在某个显示器可见区域内,避免窗口跑到屏幕外 function isVisible(bounds: WindowState): boolean { - if ( - bounds.width === undefined || - bounds.height === undefined || - bounds.x === undefined || - bounds.y === undefined - ) { + // 解构为局部常量后,控制流收窄(const 不可变)可穿透到下方嵌套闭包, + // 消除 x/y/width/height 的 “可能为未定义” 告警 + const { x, y, width, height } = bounds; + if (x === undefined || y === undefined || width === undefined || height === undefined) { return false; } const displays = screen.getAllDisplays(); return displays.some((display) => { - const { x, y, width, height } = display.workArea; - const horizontally = - bounds.x < x + width && bounds.x + bounds.width > x; - const vertically = - bounds.y < y + height && bounds.y + bounds.height > y; + const w = display.workArea; + const horizontally = x < w.x + w.width && x + width > w.x; + const vertically = y < w.y + w.height && y + height > w.y; return horizontally && vertically; }); } diff --git a/src/App.vue b/src/App.vue index b98d945b..13129809 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1095,38 +1095,46 @@ const openDetail = async (app: App | OpenDetailInput) => { }) as Promise) : Promise.resolve(false), ]); - // 优先遵从点击来源 origin(未安装时、已安装时都应以用户点击的版本为准) - let preferred = (app as Record).origin as - | "spark" - | "apm" - | undefined; + // 来源默认展示规则: + // 1) 已安装页打开:按安装类型打开。 + // - 仅一个来源已安装 → 强制展示该已装来源(无需策略覆盖) + // - 多个来源已安装 → 按「设置的优先标签」打开(策略) + // 2) 其他页面:一律按「设置的优先标签」打开(策略 > 服务端混合默认),不强制。 + // 注:forceViewingOrigin 仅用于「已安装页 + 唯一安装来源」这一显式安装类型场景, + // 其余情况都不强制,交由详情页按用户标签策略重算。 + let forceOrigin: "spark" | "apm" | undefined = undefined; if (fromInstalled) { - // 已安装页:依据实际安装来源决定默认展示 const installedOrigins = (app as Record).origins as | Array<"spark" | "apm"> | undefined; - if (installedOrigins && installedOrigins.length > 0) { - if ( - installedOrigins.includes("spark") && - installedOrigins.includes("apm") - ) { - // 两种类型都已安装:保持所有应用页的自动显示模式(不强制指定来源) - preferred = undefined; - } else { - // 仅安装一种类型:优先显示已安装的类型(另一类型仍可在来源切换中查看) - preferred = installedOrigins[0]; - } + if (installedOrigins && installedOrigins.length === 1) { + // 单来源安装:按安装类型强制 + forceOrigin = installedOrigins[0]; + } else if (installedOrigins && installedOrigins.length > 1) { + // 多来源安装:交由策略(按设置优先标签),不强制 + forceOrigin = undefined; + } else { + // origins 未随事件携带时,用 IPC 检测结果兜底判断安装类型 + if (sparkInstalled && !apmInstalled) forceOrigin = "spark"; + else if (apmInstalled && !sparkInstalled) forceOrigin = "apm"; } } - if (preferred && (preferred === "spark" ? finalApp.sparkApp : finalApp.apmApp)) { - finalApp.viewingOrigin = preferred; + // 非强制分支:清除可能由「已安装页」入口遗留的 forceViewingOrigin 粘性标志, + // 避免同一应用从其他页面再次打开时被锁死在旧来源(表现为设置切换不生效)。 + finalApp.forceViewingOrigin = false; + if (forceOrigin && (forceOrigin === "spark" ? finalApp.sparkApp : finalApp.apmApp)) { + // 已安装页 + 唯一安装来源 → 强制展示该安装类型,优先级高于用户标签策略 + finalApp.viewingOrigin = forceOrigin; + finalApp.forceViewingOrigin = true; } else if (sparkInstalled && !apmInstalled) { - // 无点击来源时:仅一个仓库已安装则优先展示已安装版本 + // 仅 Spark 安装(其他页面):默认回退展示已装版本,不强制(仍受用户策略覆盖) finalApp.viewingOrigin = "spark"; } else if (apmInstalled && !sparkInstalled) { finalApp.viewingOrigin = "apm"; } else { - // 都安装/都未安装且未指定来源:按优先级配置决定默认展示 + // 都安装/都未安装且未指定来源:交由「标签优先显示策略」决定默认展示。 + // 此处仅写入混合默认作为回退(供 appIdentity / 截图等下游使用), + // 但不置 forceViewingOrigin,详情页会按用户策略重算。 finalApp.viewingOrigin = getHybridDefaultOrigin( finalApp.sparkApp || finalApp, ); @@ -1496,7 +1504,12 @@ const loadHomeListApps = async (entryId: string) => { mode: "spark" | "apm", ): App[] => rawApps.map((a) => { - const category = a.Category || a.category || "unknown"; + // 首页推荐列表的 jsonUrl 形如 /home/lists/xxx.json;服务端原始数据不含 category 字段, + // 这里提取 URL 首段目录作为分类(无真实分类时回退 "unknown"),避免污染后续优先级匹配。 + const urlCategory = (urls[mode] || "") + .split("/") + .filter(Boolean)[0] || "unknown"; + const category = a.Category || a.category || urlCategory; let img_urls: string[] = []; const rawImgUrls = a.img_urls; diff --git a/src/components/AppDetailModal.vue b/src/components/AppDetailModal.vue index 4c334e8b..5a6499e5 100644 --- a/src/components/AppDetailModal.vue +++ b/src/components/AppDetailModal.vue @@ -578,7 +578,8 @@ import { getHybridDefaultOrigin, } from "../global/storeConfig"; import { - getTagPriorityStrategy, + tagPriorityStrategyRef, + initTagPriorityStrategy, type TagPriorityStrategy, } from "../global/tagPriority"; // 评论功能暂时关闭 @@ -641,8 +642,9 @@ const computeDefaultViewingOrigin = ( app: App, strategy: TagPriorityStrategy, ): "spark" | "apm" => { - // 父组件已显式指定展示来源(如从已安装应用页按特定来源打开),优先级最高 - if (app.viewingOrigin) return app.viewingOrigin; + // 父组件显式指定展示来源(如从已安装应用页按特定来源打开)时优先级最高, + // 此时强制覆盖用户标签策略;否则交由下方策略逻辑决定。 + if (app.forceViewingOrigin && app.viewingOrigin) return app.viewingOrigin; // 非合并应用只有一个来源标签,直接展示该标签 if (!app.isMerged) return app.origin; @@ -661,19 +663,49 @@ const computeDefaultViewingOrigin = ( return preferred; } - // 回退到应用配置的优先级策略(getHybridDefaultOrigin),并约束在可用标签范围内 - const auto = getHybridDefaultOrigin(app.sparkApp ?? app.apmApp ?? app); + // 回退到应用配置的优先级策略(getHybridDefaultOrigin),使用合并应用自身的 + // 规范标识(pkgname/category/tags)匹配服务器 priority-config.json,并约束在可用标签范围内 + const auto = getHybridDefaultOrigin(app); + console.log( + `[PriorityConfig] auto 决策 pkgname=${app.pkgname} category=${app.category} sparkTags=${app.sparkApp?.tags ?? ""} apmTags=${app.apmApp?.tags ?? ""} -> ${auto}`, + ); return available.includes(auto) ? auto : available[0]; }; +// 进入详情页时按「标签优先显示策略」计算默认来源标签。 +// 同时监听 props.app 与共享策略 ref: +// - props.app 变化(重开/切换应用)→ 始终按当前策略重算(手动切标签仅本次会话预览,不持久) +// - 策略变化(在设置页实时切换)→ 已打开且未手动切标签的详情页立即更新默认标签 +// 手动点标签页(selectOrigin)只是临时预览:置 manualOverride 标记,本次会话内保持, +// 既不被策略变化强行拉回,也不跨重开持久(重开后会按设置重算)。 +initTagPriorityStrategy(); +const manualOverride = ref(false); // 用户是否手动切换过标签(本次会话) +let lastPkgname: string | null = null; // 上一次计算对应的应用 pkgname,用于识别"新打开/重开" watch( - () => props.app, - (newApp: App | null) => { + [() => props.app, tagPriorityStrategyRef], + ([newApp]: [App | null, TagPriorityStrategy]) => { isIconLoaded.value = false; - if (newApp) { + if (!newApp) { + // 详情页关闭:清空会话状态,确保下次打开按最新策略重算 + lastPkgname = null; + manualOverride.value = false; + return; + } + if (newApp.pkgname !== lastPkgname) { + // 新应用(含重开后再次打开同一应用):重置手动标记,按当前策略计算 + lastPkgname = newApp.pkgname; + manualOverride.value = false; viewingOrigin.value = computeDefaultViewingOrigin( newApp, - getTagPriorityStrategy(), + tagPriorityStrategyRef.value, + ); + } else if (manualOverride.value || newApp.forceViewingOrigin) { + // 同一应用且用户正在手动预览 / 父组件强制来源:保留当前标签,不重算 + } else { + // 同一应用、无手动预览、策略变化 → 实时按新策略更新默认标签 + viewingOrigin.value = computeDefaultViewingOrigin( + newApp, + tagPriorityStrategyRef.value, ); } }, @@ -845,6 +877,8 @@ const confirmOpen = (origin: "spark" | "apm") => { // }; const selectOrigin = (origin: "spark" | "apm") => { + // 手动切换标签:仅本次会话预览,不写回策略、不跨重开持久 + manualOverride.value = true; viewingOrigin.value = origin; emit("select-origin", origin); }; diff --git a/src/global/storeConfig.ts b/src/global/storeConfig.ts index 5d8b7134..9527259c 100644 --- a/src/global/storeConfig.ts +++ b/src/global/storeConfig.ts @@ -1,9 +1,17 @@ import { ref } from "vue"; +import axios from "axios"; import type { App, StoreMode } from "./typedefinition"; export const APM_STORE_BASE_URL: string = import.meta.env.VITE_APM_STORE_BASE_URL || ""; +// 加载优先级配置专用客户端:与渲染进程其余请求共用同一套 HTTP 客户端, +// 避免 Electron 渲染进程中全局 fetch 失败导致配置始终为空、auto 模式全部回退 APM。 +const priorityConfigAxios = axios.create({ + baseURL: APM_STORE_BASE_URL, + timeout: 10000, +}); + export const APM_STORE_STATS_BASE_URL: string = import.meta.env.VITE_APM_STORE_STATS_BASE_URL || ""; @@ -77,59 +85,54 @@ export let hasPriorityConfigFile = false; */ export async function loadPriorityConfig(arch: string): Promise { try { - const configUrl = `${APM_STORE_BASE_URL}/${arch}-store/priority-config.json`; - const response = await fetch(configUrl, { - method: "GET", - cache: "no-cache", - }); - - if (response.ok) { - const config = await response.json(); - hasPriorityConfigFile = true; - // 支持新旧两种配置格式 - if (config.sparkPriority || config.apmPriority) { - // 新格式:双向配置 - dynamicPriorityConfig = { - sparkPriority: { - pkgnames: config.sparkPriority?.pkgnames || [], - categories: config.sparkPriority?.categories || [], - tags: config.sparkPriority?.tags || [], - }, - apmPriority: { - pkgnames: config.apmPriority?.pkgnames || [], - categories: config.apmPriority?.categories || [], - tags: config.apmPriority?.tags || [], - }, - }; - } else { - // 旧格式:只配置 sparkPriority(兼容旧配置) - dynamicPriorityConfig = { - sparkPriority: { - pkgnames: config.pkgnames || [], - categories: config.categories || [], - tags: config.tags || [], - }, - apmPriority: { - pkgnames: [], - categories: [], - tags: [], - }, - }; - } - isPriorityConfigLoaded = true; - console.log( - "[PriorityConfig] 已从服务器加载优先级配置:", - dynamicPriorityConfig, - ); + const configPath = `/${arch}-store/priority-config.json`; + console.log( + `[PriorityConfig] 开始加载优先级配置: ${APM_STORE_BASE_URL}${configPath}`, + ); + const response = await priorityConfigAxios.get(configPath); + const config = response.data; + hasPriorityConfigFile = true; + // 支持新旧两种配置格式 + if (config.sparkPriority || config.apmPriority) { + // 新格式:双向配置 + dynamicPriorityConfig = { + sparkPriority: { + pkgnames: config.sparkPriority?.pkgnames || [], + categories: config.sparkPriority?.categories || [], + tags: config.sparkPriority?.tags || [], + }, + apmPriority: { + pkgnames: config.apmPriority?.pkgnames || [], + categories: config.apmPriority?.categories || [], + tags: config.apmPriority?.tags || [], + }, + }; } else { - // 配置文件不存在,默认优先 Spark - console.log("[PriorityConfig] 服务器无配置文件,使用默认 Spark 优先"); - hasPriorityConfigFile = false; - resetPriorityConfig(); + // 旧格式:只配置 sparkPriority(兼容旧配置) + dynamicPriorityConfig = { + sparkPriority: { + pkgnames: config.pkgnames || [], + categories: config.categories || [], + tags: config.tags || [], + }, + apmPriority: { + pkgnames: [], + categories: [], + tags: [], + }, + }; } + isPriorityConfigLoaded = true; + console.log( + "[PriorityConfig] 已从服务器加载优先级配置:", + JSON.stringify(dynamicPriorityConfig), + ); } catch (error) { - // 获取失败,默认优先 Spark - console.warn("[PriorityConfig] 加载配置失败,使用默认 Spark 优先:", error); + // 获取失败(含 404:服务器无配置文件),默认优先 APM + console.warn( + `[PriorityConfig] 加载配置失败(${APM_STORE_BASE_URL}/${arch}-store/priority-config.json),使用默认 APM 优先:`, + error, + ); hasPriorityConfigFile = false; resetPriorityConfig(); } @@ -155,69 +158,58 @@ function resetPriorityConfig(): void { } /** - * 获取混合模式下应用的默认优先来源 - * 判断优先级(从高到低): - * 1. apmPriority.pkgnames - 强制优先 APM - * 2. sparkPriority.pkgnames - 强制优先 Spark - * 3. apmPriority.categories - 该分类优先 APM - * 4. sparkPriority.categories - 该分类优先 Spark - * 5. apmPriority.tags - 包含标签优先 APM - * 6. sparkPriority.tags - 包含标签优先 Spark - * 7. 默认行为: - * - 没有配置文件 → 优先 Spark - * - 有配置文件但规则为空 → 优先 APM + * 按优先级配置规则判断单个应用标识是否命中「优先来源」。 + * 命中则返回 "apm" / "spark",未命中返回 null。 + * 规则优先级(从高到低):apmPriority.pkgnames > sparkPriority.pkgnames > + * apmPriority.categories > sparkPriority.categories > apmPriority.tags > sparkPriority.tags + */ +function matchPriority(app: App): "apm" | "spark" | null { + const { sparkPriority, apmPriority } = dynamicPriorityConfig; + + if (apmPriority.pkgnames.includes(app.pkgname)) return "apm"; + if (sparkPriority.pkgnames.includes(app.pkgname)) return "spark"; + if (apmPriority.categories.includes(app.category)) return "apm"; + if (sparkPriority.categories.includes(app.category)) return "spark"; + + if (app.tags && apmPriority.tags.length > 0) { + const appTags = app.tags.split(";").map((t) => t.trim().toLowerCase()); + for (const ruleTag of apmPriority.tags) { + if (appTags.includes(ruleTag.toLowerCase())) return "apm"; + } + } + if (app.tags && sparkPriority.tags.length > 0) { + const appTags = app.tags.split(";").map((t) => t.trim().toLowerCase()); + for (const ruleTag of sparkPriority.tags) { + if (appTags.includes(ruleTag.toLowerCase())) return "spark"; + } + } + return null; +} + +/** + * 获取混合模式下应用的默认优先来源(即「应用配置优先级」)。 + * 对合并应用,依次用顶层标识、sparkApp、apmApp 去匹配优先级配置, + * 任一来源命中即生效(避免顶层 pkgname 取自某一子版而漏掉另一子版的规则)。 + * 均不命中时回退到 HYBRID_DEFAULT_PRIORITY(默认优先 APM)。 * @param app 应用信息 * @returns "apm" 或 "spark" */ export function getHybridDefaultOrigin(app: App): "apm" | "spark" { - const { sparkPriority, apmPriority } = dynamicPriorityConfig; + const result = matchPriority(app); + if (result) return result; - // 1. 检查 APM 优先的包名(最高优先级) - if (apmPriority.pkgnames.includes(app.pkgname)) { - return "apm"; - } - - // 2. 检查 Spark 优先的包名 - if (sparkPriority.pkgnames.includes(app.pkgname)) { - return "spark"; - } - - // 3. 检查 APM 优先的分类 - if (apmPriority.categories.includes(app.category)) { - return "apm"; - } - - // 4. 检查 Spark 优先的分类 - if (sparkPriority.categories.includes(app.category)) { - return "spark"; - } - - // 5. 检查 APM 优先的标签 - if (app.tags && apmPriority.tags.length > 0) { - const appTags = app.tags.split(";").map((t) => t.trim().toLowerCase()); - for (const ruleTag of apmPriority.tags) { - if (appTags.includes(ruleTag.toLowerCase())) { - return "apm"; - } + // 合并应用:两个子版分别匹配,任一命中即采用 + if (app.isMerged) { + if (app.sparkApp) { + const r = matchPriority(app.sparkApp); + if (r) return r; + } + if (app.apmApp) { + const r = matchPriority(app.apmApp); + if (r) return r; } } - // 6. 检查 Spark 优先的标签 - if (app.tags && sparkPriority.tags.length > 0) { - const appTags = app.tags.split(";").map((t) => t.trim().toLowerCase()); - for (const ruleTag of sparkPriority.tags) { - if (appTags.includes(ruleTag.toLowerCase())) { - return "spark"; - } - } - } - - // 7. 默认行为 - if (!hasPriorityConfigFile) { - // 没有配置文件 → 默认优先 Spark - return "spark"; - } else { - // 有配置文件但规则为空 → 默认优先 APM - return "apm"; - } + // 默认行为:与 HYBRID_DEFAULT_PRIORITY 保持一致(默认优先 APM) + return HYBRID_DEFAULT_PRIORITY; } diff --git a/src/global/tagPriority.ts b/src/global/tagPriority.ts index eeed429e..e4b23f92 100644 --- a/src/global/tagPriority.ts +++ b/src/global/tagPriority.ts @@ -21,6 +21,9 @@ const VALID_STRATEGIES: TagPriorityStrategy[] = ["auto", "spark", "apm"]; // 共享响应式引用:设置页写入、详情页读取,保证跨组件同步 const strategy = ref("auto"); +// 导出共享 ref,供详情页以响应式方式 watch(设置变化实时生效) +export const tagPriorityStrategyRef = strategy; + let loaded = false; const readFromStorage = (): TagPriorityStrategy => { diff --git a/src/global/typedefinition.ts b/src/global/typedefinition.ts index 64ac8a5f..cf3dacb1 100644 --- a/src/global/typedefinition.ts +++ b/src/global/typedefinition.ts @@ -115,6 +115,7 @@ export interface App { sparkApp?: App; // Optional reference to the spark version apmApp?: App; // Optional reference to the apm version viewingOrigin?: "spark" | "apm"; // Currently viewed origin inside the app modal + forceViewingOrigin?: boolean; // true 表示 viewingOrigin 为父组件显式指定(如从已安装页按特定来源打开),优先级高于用户标签策略;未设置或 false 时由标签策略决定默认展示 origins?: Array<"spark" | "apm">; // 实际安装来源集合(同时以 APM 与 Spark 安装时含两项) downloadCount?: number; // 下载量(用于下载排行,由 download-times.txt 解析) }