fix(favorites): ignore stale account requests

This commit is contained in:
2026-05-19 00:18:01 +08:00
parent 3a4aa7807a
commit 8da044495a
2 changed files with 113 additions and 15 deletions
+53 -15
View File
@@ -402,6 +402,7 @@ const showFavoriteSelector = ref(false);
const favoriteTargetApp = ref<App | null>(null); const favoriteTargetApp = ref<App | null>(null);
const favoriteLoading = ref(false); const favoriteLoading = ref(false);
const favoriteError = ref(""); const favoriteError = ref("");
const favoriteRequestGeneration = ref(0);
/** 启动参数 --no-apm => 仅 Spark--no-spark => 仅 APM;由主进程 IPC 提供 */ /** 启动参数 --no-apm => 仅 Spark--no-spark => 仅 APM;由主进程 IPC 提供 */
const storeFilter = ref<"spark" | "apm" | "both">("both"); const storeFilter = ref<"spark" | "apm" | "both">("both");
@@ -1319,6 +1320,7 @@ const openLoginFromPrompt = () => {
}; };
const clearFavoriteState = () => { const clearFavoriteState = () => {
favoriteRequestGeneration.value += 1;
favoriteFolders.value = []; favoriteFolders.value = [];
activeFavoriteFolderId.value = null; activeFavoriteFolderId.value = null;
favoriteItems.value = []; favoriteItems.value = [];
@@ -1328,6 +1330,9 @@ const clearFavoriteState = () => {
favoriteError.value = ""; favoriteError.value = "";
}; };
const isCurrentFavoriteRequest = (generation: number): boolean =>
favoriteRequestGeneration.value === generation && isLoggedIn.value;
const handleLogout = () => { const handleLogout = () => {
logout(); logout();
clearFavoriteState(); clearFavoriteState();
@@ -1368,50 +1373,73 @@ const openUserManagement = () => {
showLoginPrompt.value = false; showLoginPrompt.value = false;
}; };
const loadFavoriteFolders = async (): Promise<void> => { const loadFavoriteFolders = async (
favoriteFolders.value = await listFavoriteFolders(); generation = favoriteRequestGeneration.value,
const activeFolderExists = favoriteFolders.value.some( ): Promise<boolean> => {
const folders = await listFavoriteFolders();
if (!isCurrentFavoriteRequest(generation)) return false;
favoriteFolders.value = folders;
const activeFolderExists = folders.some(
(folder) => folder.id === activeFavoriteFolderId.value, (folder) => folder.id === activeFavoriteFolderId.value,
); );
if (!activeFolderExists) { if (!activeFolderExists) {
activeFavoriteFolderId.value = favoriteFolders.value[0]?.id ?? null; activeFavoriteFolderId.value = folders[0]?.id ?? null;
} }
return true;
}; };
const loadActiveFavoriteItems = async (): Promise<void> => { const loadActiveFavoriteItems = async (
generation = favoriteRequestGeneration.value,
): Promise<boolean> => {
if (!activeFavoriteFolderId.value) { if (!activeFavoriteFolderId.value) {
if (!isCurrentFavoriteRequest(generation)) return false;
favoriteItems.value = []; 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<void> => { const refreshFavorites = async (): Promise<void> => {
const generation = favoriteRequestGeneration.value;
favoriteLoading.value = true; favoriteLoading.value = true;
favoriteError.value = ""; favoriteError.value = "";
try { try {
await Promise.all([refreshFavoriteInstalledApps(), loadFavoriteFolders()]); await Promise.all([
await loadActiveFavoriteItems(); refreshFavoriteInstalledApps(),
loadFavoriteFolders(generation),
]);
if (!isCurrentFavoriteRequest(generation)) return;
await loadActiveFavoriteItems(generation);
} catch (error: unknown) { } catch (error: unknown) {
if (!isCurrentFavoriteRequest(generation)) return;
favoriteError.value = (error as Error)?.message || "读取收藏夹失败"; favoriteError.value = (error as Error)?.message || "读取收藏夹失败";
} finally { } finally {
favoriteLoading.value = false; if (isCurrentFavoriteRequest(generation)) favoriteLoading.value = false;
} }
}; };
const openFavoriteSelector = async (app: App) => { const openFavoriteSelector = async (app: App) => {
if (!requireLogin("收藏应用需要登录星火账号。")) return; if (!requireLogin("收藏应用需要登录星火账号。")) return;
const generation = favoriteRequestGeneration.value;
favoriteTargetApp.value = app; favoriteTargetApp.value = app;
favoriteError.value = ""; favoriteError.value = "";
try { try {
await loadFavoriteFolders(); const foldersLoaded = await loadFavoriteFolders(generation);
if (!foldersLoaded || !isCurrentFavoriteRequest(generation)) return;
showFavoriteSelector.value = true; showFavoriteSelector.value = true;
} catch (error: unknown) { } catch (error: unknown) {
if (!isCurrentFavoriteRequest(generation)) return;
favoriteError.value = (error as Error)?.message || "读取收藏夹失败"; favoriteError.value = (error as Error)?.message || "读取收藏夹失败";
} }
}; };
const addCurrentFavoriteToFolder = async (folderId: number | "default") => { const addCurrentFavoriteToFolder = async (folderId: number | "default") => {
const generation = favoriteRequestGeneration.value;
const app = favoriteTargetApp.value; const app = favoriteTargetApp.value;
if (!app) return; if (!app) return;
try { try {
@@ -1422,10 +1450,12 @@ const addCurrentFavoriteToFolder = async (folderId: number | "default") => {
category: app.category, category: app.category,
iconUrl: app.icons, iconUrl: app.icons,
}); });
if (!isCurrentFavoriteRequest(generation)) return;
showFavoriteSelector.value = false; showFavoriteSelector.value = false;
favoriteTargetApp.value = null; favoriteTargetApp.value = null;
await refreshFavorites(); await refreshFavorites();
} catch (error: unknown) { } catch (error: unknown) {
if (!isCurrentFavoriteRequest(generation)) return;
favoriteError.value = (error as Error)?.message || "添加收藏失败"; favoriteError.value = (error as Error)?.message || "添加收藏失败";
} }
}; };
@@ -1440,15 +1470,17 @@ const openFavoriteManagement = async () => {
}; };
const selectFavoriteFolder = async (folderId: number) => { const selectFavoriteFolder = async (folderId: number) => {
const generation = favoriteRequestGeneration.value;
activeFavoriteFolderId.value = folderId; activeFavoriteFolderId.value = folderId;
favoriteLoading.value = true; favoriteLoading.value = true;
favoriteError.value = ""; favoriteError.value = "";
try { try {
await loadActiveFavoriteItems(); await loadActiveFavoriteItems(generation);
} catch (error: unknown) { } catch (error: unknown) {
if (!isCurrentFavoriteRequest(generation)) return;
favoriteError.value = (error as Error)?.message || "读取收藏应用失败"; favoriteError.value = (error as Error)?.message || "读取收藏应用失败";
} finally { } finally {
favoriteLoading.value = false; if (isCurrentFavoriteRequest(generation)) favoriteLoading.value = false;
} }
}; };
@@ -1456,30 +1488,36 @@ const createFavoriteFolderFromPrompt = async () => {
const name = window.prompt("请输入收藏夹名称"); const name = window.prompt("请输入收藏夹名称");
const folderName = name?.trim(); const folderName = name?.trim();
if (!folderName) return; if (!folderName) return;
const generation = favoriteRequestGeneration.value;
favoriteLoading.value = true; favoriteLoading.value = true;
favoriteError.value = ""; favoriteError.value = "";
try { try {
const folder = await createFavoriteFolder(folderName); const folder = await createFavoriteFolder(folderName);
if (!isCurrentFavoriteRequest(generation)) return;
activeFavoriteFolderId.value = folder.id; activeFavoriteFolderId.value = folder.id;
await refreshFavorites(); await refreshFavorites();
} catch (error: unknown) { } catch (error: unknown) {
if (!isCurrentFavoriteRequest(generation)) return;
favoriteError.value = (error as Error)?.message || "创建收藏夹失败"; favoriteError.value = (error as Error)?.message || "创建收藏夹失败";
} finally { } finally {
favoriteLoading.value = false; if (isCurrentFavoriteRequest(generation)) favoriteLoading.value = false;
} }
}; };
const removeSelectedFavorites = async (ids: number[]) => { const removeSelectedFavorites = async (ids: number[]) => {
if (!activeFavoriteFolderId.value || ids.length === 0) return; if (!activeFavoriteFolderId.value || ids.length === 0) return;
const generation = favoriteRequestGeneration.value;
favoriteLoading.value = true; favoriteLoading.value = true;
favoriteError.value = ""; favoriteError.value = "";
try { try {
await bulkDeleteFavoriteItems(activeFavoriteFolderId.value, ids); await bulkDeleteFavoriteItems(activeFavoriteFolderId.value, ids);
if (!isCurrentFavoriteRequest(generation)) return;
await refreshFavorites(); await refreshFavorites();
} catch (error: unknown) { } catch (error: unknown) {
if (!isCurrentFavoriteRequest(generation)) return;
favoriteError.value = (error as Error)?.message || "移除收藏失败"; favoriteError.value = (error as Error)?.message || "移除收藏失败";
} finally { } finally {
favoriteLoading.value = false; if (isCurrentFavoriteRequest(generation)) favoriteLoading.value = false;
} }
}; };
@@ -2,6 +2,7 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/vue";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import App from "@/App.vue"; import App from "@/App.vue";
import { listFavoriteFolders } from "@/modules/backendApi";
import { setAuthSession } from "@/global/authState"; import { setAuthSession } from "@/global/authState";
import type { FavoriteFolder, FavoriteItem } from "@/global/typedefinition"; import type { FavoriteFolder, FavoriteItem } from "@/global/typedefinition";
@@ -29,6 +30,15 @@ const favoriteItems: FavoriteItem[] = [
}, },
]; ];
const createDeferred = <T>() => {
let resolve!: (value: T) => void;
const promise = new Promise<T>((promiseResolve) => {
resolve = promiseResolve;
});
return { promise, resolve };
};
vi.mock("axios", () => { vi.mock("axios", () => {
const get = vi.fn(async (url: string) => { const get = vi.fn(async (url: string) => {
if (url.includes("categories.json")) { if (url.includes("categories.json")) {
@@ -146,6 +156,13 @@ describe("App account placeholders", () => {
removeEventListener: vi.fn(), 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 () => { 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.queryByText("wps · office")).toBeNull();
expect(screen.queryByRole("heading", { name: "我的收藏" })).toBeNull(); expect(screen.queryByRole("heading", { name: "我的收藏" })).toBeNull();
}); });
it("does not reopen the favorite selector when folder loading resolves after logout", async () => {
const slowFolders = createDeferred<FavoriteFolder[]>();
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();
});
}); });