diff --git a/src/App.vue b/src/App.vue index 447e24f0..17b544e7 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1464,6 +1464,16 @@ const clearRestoreState = () => { showRestoreModal.value = false; }; +const nextFavoriteRequestGeneration = (): number => { + favoriteRequestGeneration.value += 1; + return favoriteRequestGeneration.value; +}; + +const nextDownloadedRequestGeneration = (): number => { + downloadedRequestGeneration.value += 1; + return downloadedRequestGeneration.value; +}; + const isCurrentFavoriteRequest = (generation: number): boolean => favoriteRequestGeneration.value === generation && isLoggedIn.value; @@ -1522,7 +1532,7 @@ const loadDownloadedHistory = async (): Promise => { if (!requireLogin("请登录后查看和管理账号信息。")) return; const userId = currentUser.value?.id; if (userId === undefined) return; - const generation = downloadedRequestGeneration.value; + const generation = nextDownloadedRequestGeneration(); downloadedLoading.value = true; downloadedError.value = ""; @@ -1741,7 +1751,7 @@ const loadActiveFavoriteItems = async ( }; const refreshFavorites = async (): Promise => { - const generation = favoriteRequestGeneration.value; + const generation = nextFavoriteRequestGeneration(); favoriteLoading.value = true; favoriteError.value = ""; try { @@ -1806,7 +1816,7 @@ const openFavoriteManagement = async () => { }; const selectFavoriteFolder = async (folderId: number) => { - const generation = favoriteRequestGeneration.value; + const generation = nextFavoriteRequestGeneration(); activeFavoriteFolderId.value = folderId; favoriteLoading.value = true; favoriteError.value = ""; diff --git a/src/__tests__/unit/App.account-placeholders.test.ts b/src/__tests__/unit/App.account-placeholders.test.ts index 5314f517..caadeb39 100644 --- a/src/__tests__/unit/App.account-placeholders.test.ts +++ b/src/__tests__/unit/App.account-placeholders.test.ts @@ -462,6 +462,108 @@ describe("App account placeholders", () => { expect(screen.getByText("暂无下载记录。")).toBeTruthy(); }); + it("ignores older downloaded history refreshes for the same user", async () => { + const firstHistory = createDeferred(); + const secondHistory = createDeferred(); + vi.mocked(listDownloadedApps) + .mockReturnValueOnce(firstHistory.promise) + .mockReturnValueOnce(secondHistory.promise); + render(App); + + await fireEvent.click(await screen.findByRole("button", { name: /Momen/ })); + await fireEvent.click(screen.getByText("用户管理")); + await fireEvent.click(screen.getByRole("button", { name: "刷新" })); + + secondHistory.resolve( + downloadedList([ + { + id: 88, + appKey: "app:office:new-app", + pkgname: "new-app", + name: "新下载应用", + category: "office", + selectedOrigin: "apm", + version: "2.0.0", + packageArch: "amd64", + downloadedAt: "2026-05-18T00:00:00Z", + }, + ]), + ); + expect(await screen.findByText("新下载应用")).toBeTruthy(); + + firstHistory.resolve( + downloadedList([ + { + id: 77, + appKey: "app:office:old-app", + pkgname: "old-app", + name: "旧下载应用", + category: "office", + selectedOrigin: "apm", + version: "1.0.0", + packageArch: "amd64", + downloadedAt: "2026-05-18T00:00:00Z", + }, + ]), + ); + await firstHistory.promise; + await Promise.resolve(); + await Promise.resolve(); + + expect(screen.queryByText("旧下载应用")).toBeNull(); + expect(screen.getByText("新下载应用")).toBeTruthy(); + }); + + it("ignores older favorite folder refreshes for the same user", async () => { + const firstFolders = createDeferred(); + const secondFolders = createDeferred(); + vi.mocked(listFavoriteFolders) + .mockReturnValueOnce(firstFolders.promise) + .mockReturnValueOnce(secondFolders.promise); + render(App); + + await fireEvent.click( + await screen.findByRole("button", { name: /^Momen$/ }), + ); + await fireEvent.click(screen.getByRole("button", { name: "我的收藏" })); + await fireEvent.click( + await screen.findByRole("button", { name: /^Momen$/ }), + ); + if (!screen.queryByRole("button", { name: "我的收藏" })) { + await fireEvent.click( + await screen.findByRole("button", { name: /^Momen$/ }), + ); + } + await fireEvent.click(screen.getByRole("button", { name: "我的收藏" })); + + secondFolders.resolve([ + { + id: 42, + name: "新收藏夹", + itemCount: 0, + createdAt: "2026-05-18T00:00:00Z", + updatedAt: "2026-05-18T00:00:00Z", + }, + ]); + expect(await screen.findByText("新收藏夹 (0)")).toBeTruthy(); + + firstFolders.resolve([ + { + id: 41, + name: "旧收藏夹", + itemCount: 0, + createdAt: "2026-05-18T00:00:00Z", + updatedAt: "2026-05-18T00:00:00Z", + }, + ]); + await firstFolders.promise; + await Promise.resolve(); + await Promise.resolve(); + + expect(screen.queryByText("旧收藏夹 (0)")).toBeNull(); + expect(screen.getByText("新收藏夹 (0)")).toBeTruthy(); + }); + it("restores cloud apps by origin and package when category changed", async () => { vi.mocked(fetchSyncedAppList).mockResolvedValueOnce({ snapshotName: "默认列表", diff --git a/src/__tests__/unit/AppDetailPage.test.ts b/src/__tests__/unit/AppDetailPage.test.ts index 36505fbd..f52f5fa7 100644 --- a/src/__tests__/unit/AppDetailPage.test.ts +++ b/src/__tests__/unit/AppDetailPage.test.ts @@ -91,6 +91,28 @@ describe("AppDetailPage", () => { ); }); + it("gates reviews for anonymous users", async () => { + const rendered = render(AppDetailPage, { + props: { + app, + screenshots: [], + sparkInstalled: false, + apmInstalled: false, + loggedIn: false, + reviewAppKey: "apm:amd64-apm:office:wps", + reviewTags: sparkTags, + }, + }); + + expect(screen.queryByTestId("reviews-panel")).toBeNull(); + await fireEvent.click( + screen.getByRole("button", { name: "登录后查看评价" }), + ); + expect(rendered.emitted("request-login")?.[0]?.[0]).toBe( + "登录后查看和发表评论。", + ); + }); + it("updates review identity when switching a merged app origin", async () => { render(AppDetailPage, { props: { @@ -98,7 +120,7 @@ describe("AppDetailPage", () => { screenshots: [], sparkInstalled: false, apmInstalled: false, - loggedIn: false, + loggedIn: true, reviewAppKey: "spark:amd64-store:office:wps", reviewTags: sparkTags, }, diff --git a/src/__tests__/unit/ReviewsPanel.test.ts b/src/__tests__/unit/ReviewsPanel.test.ts index fd6ef4d7..ca7a7bf3 100644 --- a/src/__tests__/unit/ReviewsPanel.test.ts +++ b/src/__tests__/unit/ReviewsPanel.test.ts @@ -52,6 +52,8 @@ describe("ReviewsPanel", () => { expect(screen.getByText("登录后发表评论")).toBeTruthy(); expect(screen.getByText("1.0.0")).toBeTruthy(); expect(screen.getByText("deepin 25")).toBeTruthy(); + expect(fetchRatingSummary).not.toHaveBeenCalled(); + expect(fetchReviews).not.toHaveBeenCalled(); }); it("ignores stale review responses after app key changes", async () => { @@ -84,10 +86,10 @@ describe("ReviewsPanel", () => { ); const rendered = render(ReviewsPanel, { - props: { appKey: "first", tags, loggedIn: false }, + props: { appKey: "first", tags, loggedIn: true }, }); - await rendered.rerender({ appKey: "second", tags, loggedIn: false }); + await rendered.rerender({ appKey: "second", tags, loggedIn: true }); resolveSecondSummary({ averageRating: 5, reviewCount: 1, starCounts: {} }); resolveSecondReviews([ diff --git a/src/components/AppDetailPage.vue b/src/components/AppDetailPage.vue index e3b26ca4..7b1c4bd8 100644 --- a/src/components/AppDetailPage.vue +++ b/src/components/AppDetailPage.vue @@ -196,12 +196,31 @@ +
+

+ + 应用评价 +

+

+ 登录星火账号后可查看评价并发表评论。 +

+ +
diff --git a/src/components/ReviewsPanel.vue b/src/components/ReviewsPanel.vue index eefc2382..b6ae6049 100644 --- a/src/components/ReviewsPanel.vue +++ b/src/components/ReviewsPanel.vue @@ -152,8 +152,19 @@ const ratingText = computed(() => { return `${summary.value.averageRating.toFixed(1)} / 5 (${summary.value.reviewCount})`; }); +const clearReviewState = () => { + loadGeneration.value += 1; + reviews.value = []; + summary.value = null; + loading.value = false; + error.value = ""; +}; + const loadReviews = async () => { - if (!props.appKey) return; + if (!props.loggedIn || !props.appKey) { + clearReviewState(); + return; + } const generation = loadGeneration.value + 1; loadGeneration.value = generation; const appKey = props.appKey; @@ -202,5 +213,5 @@ const submit = async () => { }; onMounted(loadReviews); -watch(() => props.appKey, loadReviews); +watch(() => [props.appKey, props.loggedIn], loadReviews);