diff --git a/src/App.vue b/src/App.vue index 62aed9cb..887f5cba 100644 --- a/src/App.vue +++ b/src/App.vue @@ -402,6 +402,7 @@ const showFavoriteSelector = ref(false); const favoriteTargetApp = ref(null); const favoriteLoading = ref(false); const favoriteError = ref(""); +const favoriteRequestGeneration = ref(0); /** 启动参数 --no-apm => 仅 Spark;--no-spark => 仅 APM;由主进程 IPC 提供 */ const storeFilter = ref<"spark" | "apm" | "both">("both"); @@ -1319,6 +1320,7 @@ const openLoginFromPrompt = () => { }; const clearFavoriteState = () => { + favoriteRequestGeneration.value += 1; favoriteFolders.value = []; activeFavoriteFolderId.value = null; favoriteItems.value = []; @@ -1328,6 +1330,9 @@ const clearFavoriteState = () => { favoriteError.value = ""; }; +const isCurrentFavoriteRequest = (generation: number): boolean => + favoriteRequestGeneration.value === generation && isLoggedIn.value; + const handleLogout = () => { logout(); clearFavoriteState(); @@ -1368,50 +1373,73 @@ const openUserManagement = () => { showLoginPrompt.value = false; }; -const loadFavoriteFolders = async (): Promise => { - favoriteFolders.value = await listFavoriteFolders(); - const activeFolderExists = favoriteFolders.value.some( +const loadFavoriteFolders = async ( + generation = favoriteRequestGeneration.value, +): Promise => { + const folders = await listFavoriteFolders(); + if (!isCurrentFavoriteRequest(generation)) return false; + + favoriteFolders.value = folders; + const activeFolderExists = folders.some( (folder) => folder.id === activeFavoriteFolderId.value, ); if (!activeFolderExists) { - activeFavoriteFolderId.value = favoriteFolders.value[0]?.id ?? null; + activeFavoriteFolderId.value = folders[0]?.id ?? null; } + return true; }; -const loadActiveFavoriteItems = async (): Promise => { +const loadActiveFavoriteItems = async ( + generation = favoriteRequestGeneration.value, +): Promise => { if (!activeFavoriteFolderId.value) { + if (!isCurrentFavoriteRequest(generation)) return false; favoriteItems.value = []; - return; + return true; } - favoriteItems.value = await listFavoriteItems(activeFavoriteFolderId.value); + const items = await listFavoriteItems(activeFavoriteFolderId.value); + if (!isCurrentFavoriteRequest(generation)) return false; + + favoriteItems.value = items; + return true; }; const refreshFavorites = async (): Promise => { + const generation = favoriteRequestGeneration.value; favoriteLoading.value = true; favoriteError.value = ""; try { - await Promise.all([refreshFavoriteInstalledApps(), loadFavoriteFolders()]); - await loadActiveFavoriteItems(); + await Promise.all([ + refreshFavoriteInstalledApps(), + loadFavoriteFolders(generation), + ]); + if (!isCurrentFavoriteRequest(generation)) return; + await loadActiveFavoriteItems(generation); } catch (error: unknown) { + if (!isCurrentFavoriteRequest(generation)) return; favoriteError.value = (error as Error)?.message || "读取收藏夹失败"; } finally { - favoriteLoading.value = false; + if (isCurrentFavoriteRequest(generation)) favoriteLoading.value = false; } }; const openFavoriteSelector = async (app: App) => { if (!requireLogin("收藏应用需要登录星火账号。")) return; + const generation = favoriteRequestGeneration.value; favoriteTargetApp.value = app; favoriteError.value = ""; try { - await loadFavoriteFolders(); + const foldersLoaded = await loadFavoriteFolders(generation); + if (!foldersLoaded || !isCurrentFavoriteRequest(generation)) return; showFavoriteSelector.value = true; } catch (error: unknown) { + if (!isCurrentFavoriteRequest(generation)) return; favoriteError.value = (error as Error)?.message || "读取收藏夹失败"; } }; const addCurrentFavoriteToFolder = async (folderId: number | "default") => { + const generation = favoriteRequestGeneration.value; const app = favoriteTargetApp.value; if (!app) return; try { @@ -1422,10 +1450,12 @@ const addCurrentFavoriteToFolder = async (folderId: number | "default") => { category: app.category, iconUrl: app.icons, }); + if (!isCurrentFavoriteRequest(generation)) return; showFavoriteSelector.value = false; favoriteTargetApp.value = null; await refreshFavorites(); } catch (error: unknown) { + if (!isCurrentFavoriteRequest(generation)) return; favoriteError.value = (error as Error)?.message || "添加收藏失败"; } }; @@ -1440,15 +1470,17 @@ const openFavoriteManagement = async () => { }; const selectFavoriteFolder = async (folderId: number) => { + const generation = favoriteRequestGeneration.value; activeFavoriteFolderId.value = folderId; favoriteLoading.value = true; favoriteError.value = ""; try { - await loadActiveFavoriteItems(); + await loadActiveFavoriteItems(generation); } catch (error: unknown) { + if (!isCurrentFavoriteRequest(generation)) return; favoriteError.value = (error as Error)?.message || "读取收藏应用失败"; } finally { - favoriteLoading.value = false; + if (isCurrentFavoriteRequest(generation)) favoriteLoading.value = false; } }; @@ -1456,30 +1488,36 @@ const createFavoriteFolderFromPrompt = async () => { const name = window.prompt("请输入收藏夹名称"); const folderName = name?.trim(); if (!folderName) return; + const generation = favoriteRequestGeneration.value; favoriteLoading.value = true; favoriteError.value = ""; try { const folder = await createFavoriteFolder(folderName); + if (!isCurrentFavoriteRequest(generation)) return; activeFavoriteFolderId.value = folder.id; await refreshFavorites(); } catch (error: unknown) { + if (!isCurrentFavoriteRequest(generation)) return; favoriteError.value = (error as Error)?.message || "创建收藏夹失败"; } finally { - favoriteLoading.value = false; + if (isCurrentFavoriteRequest(generation)) favoriteLoading.value = false; } }; const removeSelectedFavorites = async (ids: number[]) => { if (!activeFavoriteFolderId.value || ids.length === 0) return; + const generation = favoriteRequestGeneration.value; favoriteLoading.value = true; favoriteError.value = ""; try { await bulkDeleteFavoriteItems(activeFavoriteFolderId.value, ids); + if (!isCurrentFavoriteRequest(generation)) return; await refreshFavorites(); } catch (error: unknown) { + if (!isCurrentFavoriteRequest(generation)) return; favoriteError.value = (error as Error)?.message || "移除收藏失败"; } finally { - favoriteLoading.value = false; + if (isCurrentFavoriteRequest(generation)) favoriteLoading.value = false; } }; diff --git a/src/__tests__/unit/App.account-placeholders.test.ts b/src/__tests__/unit/App.account-placeholders.test.ts index df9debbe..45c89532 100644 --- a/src/__tests__/unit/App.account-placeholders.test.ts +++ b/src/__tests__/unit/App.account-placeholders.test.ts @@ -2,6 +2,7 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/vue"; import { beforeEach, describe, expect, it, vi } from "vitest"; import App from "@/App.vue"; +import { listFavoriteFolders } from "@/modules/backendApi"; import { setAuthSession } from "@/global/authState"; import type { FavoriteFolder, FavoriteItem } from "@/global/typedefinition"; @@ -29,6 +30,15 @@ const favoriteItems: FavoriteItem[] = [ }, ]; +const createDeferred = () => { + let resolve!: (value: T) => void; + const promise = new Promise((promiseResolve) => { + resolve = promiseResolve; + }); + + return { promise, resolve }; +}; + vi.mock("axios", () => { const get = vi.fn(async (url: string) => { if (url.includes("categories.json")) { @@ -146,6 +156,13 @@ describe("App account placeholders", () => { removeEventListener: vi.fn(), })), ); + vi.stubGlobal("scrollTo", vi.fn()); + class MockIntersectionObserver { + observe = vi.fn(); + disconnect = vi.fn(); + unobserve = vi.fn(); + } + vi.stubGlobal("IntersectionObserver", MockIntersectionObserver); }); it("shows the user management placeholder from the logged-in quick menu", async () => { @@ -291,4 +308,47 @@ describe("App account placeholders", () => { expect(screen.queryByText("wps · office")).toBeNull(); expect(screen.queryByRole("heading", { name: "我的收藏" })).toBeNull(); }); + + it("does not reopen the favorite selector when folder loading resolves after logout", async () => { + const slowFolders = createDeferred(); + vi.mocked(listFavoriteFolders).mockReturnValueOnce(slowFolders.promise); + render(App); + + await fireEvent.click(await screen.findByText("全部应用")); + await fireEvent.click(await screen.findByText("wps · 1.0.0")); + expect(await screen.findByRole("heading", { name: "WPS" })).toBeTruthy(); + await fireEvent.click(screen.getByRole("button", { name: "收藏" })); + + await fireEvent.click( + await screen.findByRole("button", { name: /^Momen$/ }), + ); + if (!screen.queryByText("退出登录")) { + await fireEvent.click( + await screen.findByRole("button", { name: /^Momen$/ }), + ); + } + await fireEvent.click(screen.getByText("退出登录")); + + await waitFor(() => { + expect(screen.getByRole("button", { name: "登录 / 注册" })).toBeTruthy(); + }); + + slowFolders.resolve([ + { + id: 42, + name: "旧账号收藏夹", + itemCount: 1, + createdAt: "2026-05-18T00:00:00Z", + updatedAt: "2026-05-18T00:00:00Z", + }, + ]); + await slowFolders.promise; + await Promise.resolve(); + await Promise.resolve(); + + expect(screen.queryByText("旧账号收藏夹 (1)")).toBeNull(); + expect(screen.queryByText("wps · office")).toBeNull(); + expect(screen.queryByText("旧账号收藏夹")).toBeNull(); + expect(screen.queryByRole("dialog", { name: "选择收藏夹" })).toBeNull(); + }); });