From cbfaf9d2692ef95b2e63f2055b4e8e097750bde3 Mon Sep 17 00:00:00 2001 From: zhaojisen <1301338853@qq.com> Date: Wed, 5 Aug 2026 18:39:14 +0800 Subject: [PATCH] fix(asset): Prevent repeated asset pagination after menu switches Asset requests return through global Tauri events, allowing stale responses to be consumed by a newly selected menu. Correlate each request with a unique request ID and keep display filtering separate from pagination state. --- src-tauri/src/commands/asset_actions.rs | 18 ++++++-- ui/composables/useAssetFetcher.ts | 61 +++++++++++++++++++------ 2 files changed, 61 insertions(+), 18 deletions(-) diff --git a/src-tauri/src/commands/asset_actions.rs b/src-tauri/src/commands/asset_actions.rs index da5076dc..9313a73c 100644 --- a/src-tauri/src/commands/asset_actions.rs +++ b/src-tauri/src/commands/asset_actions.rs @@ -40,12 +40,17 @@ pub async fn get_assets( session: State<'_, ApiSessionStore>, mut query: AssetQuery, favorite: Option, + request_id: String, ) -> Result<(), String> { + // 资产结果通过全局事件广播,所有成功和失败分支都必须原样回传该请求 ID。 let (context, asset_service) = match load_asset_service(&app, &session).await { Ok(result) => result, Err(error) => { error!("load asset service failed: {}", error); - let _ = app.emit("get-asset-failure", json!({ "status": 401 })); + let _ = app.emit( + "get-asset-failure", + json!({ "status": 401, "request_id": request_id }), + ); return Ok(()); } }; @@ -58,7 +63,10 @@ pub async fn get_assets( if !assets_data.success { error!("获取 Asset 数据失败"); - let _ = app.emit("get-asset-failure", json!({ "status": assets_data.status })); + let _ = app.emit( + "get-asset-failure", + json!({ "status": assets_data.status, "request_id": request_id }), + ); return Ok(()); } @@ -69,12 +77,16 @@ pub async fn get_assets( json!({ "status": assets_data.status, "data": data, + "request_id": request_id, }), ); } Err(error) => { error!("解析资产列表 JSON 失败: {}", error); - let _ = app.emit("get-asset-failure", json!({ "status": assets_data.status })); + let _ = app.emit( + "get-asset-failure", + json!({ "status": assets_data.status, "request_id": request_id }), + ); return Ok(()); } } diff --git a/ui/composables/useAssetFetcher.ts b/ui/composables/useAssetFetcher.ts index 91c90791..c55f9130 100644 --- a/ui/composables/useAssetFetcher.ts +++ b/ui/composables/useAssetFetcher.ts @@ -4,6 +4,13 @@ import type { AssetPageType, AssetsResponse, PermedAccount, PermedProtocol, RawA import { useUserInfoStore } from "~/store/modules/userInfo"; const LIMIT = 20; +let assetRequestSequence = 0; + +// get_assets 通过全局事件返回结果,请求 ID 用于区分不同菜单及不同分页请求。 +const createAssetRequestId = (assetType: AssetPageType) => { + assetRequestSequence += 1; + return `${assetType}-${Date.now()}-${assetRequestSequence}`; +}; export const useAssetFetcher = (assetType: AssetPageType, scrollRef?: Ref) => { const { t } = useI18n(); @@ -23,6 +30,7 @@ export const useAssetFetcher = (assetType: AssetPageType, scrollRef?: Ref([]); const lastDetailAssetId = ref(null); + const activeRequestId = ref(null); const subscribeGetAssetsEvent = ref(null); const subscribeGetAssetFailedEvent = ref(null); const subscribeGetFavoriteAssetsEvent = ref(null); @@ -243,16 +251,19 @@ export const useAssetFetcher = (assetType: AssetPageType, scrollRef?: Ref { + const appendPageData = (pageData: RawAssetData[], fetchedCount: number, count?: number | null) => { // 如果大于 20 条那么只显示 20 条 if (pageData.length > LIMIT) pageData = pageData.slice(0, LIMIT); rawAssetsList.value.push(...pageData); - offset.value += pageData.length; - totalCount.value = (count ?? rawAssetsList.value.length) as number; - hasMore.value = rawAssetsList.value.length < totalCount.value; + // pageData 是过滤后的展示数据;分页游标必须按服务端原始页条数推进,保持与 count 同一口径。 + offset.value += fetchedCount; + totalCount.value = count ?? offset.value; + // 原始页为空代表分页没有进展,即使 count 异常偏大也要停止,避免重复请求同一个 offset。 + hasMore.value = fetchedCount > 0 && offset.value < totalCount.value; }; /** @@ -285,10 +296,13 @@ export const useAssetFetcher = (assetType: AssetPageType, scrollRef?: Ref { - endLoading(); - }); + appendPageData(filtered, pageResults.length, resp.data.count); + endLoading(); }); subscribeGetAssetFailedEvent.value = await useTauriEventListen("get-asset-failure", (event) => { interface eventPayload { status: number + request_id: string } + const payload = event.payload as eventPayload; + if (!isLoading.value) return; if (!isActiveForCurrentRoute()) return; + // 过期请求失败不能结束新请求的 loading,也不能修改 hasMore 或登录状态。 + if (payload.request_id !== activeRequestId.value) return; - const payload = event.payload as eventPayload; + activeRequestId.value = null; const status = payload.status; hasMore.value = false; @@ -379,9 +406,7 @@ export const useAssetFetcher = (assetType: AssetPageType, scrollRef?: Ref { - endLoading(); - }); + endLoading(); }); subscribeGetFavoriteAssetsEvent.value = await useTauriEventListen("get-favorite-assets-success", async (event) => { @@ -408,6 +433,11 @@ export const useAssetFetcher = (assetType: AssetPageType, scrollRef?: Ref { subscribeGetAssetsEvent.value?.(); subscribeGetAssetFailedEvent.value?.(); + subscribeGetFavoriteAssetsEvent.value?.(); + + subscribeGetAssetsEvent.value = null; + subscribeGetAssetFailedEvent.value = null; + subscribeGetFavoriteAssetsEvent.value = null; }; let unsubscribeSearch: (() => void) | null = null; @@ -531,6 +561,7 @@ export const useAssetFetcher = (assetType: AssetPageType, scrollRef?: Ref { + activeRequestId.value = null; unListenTauriEvent(); unListenEventBusEvent(); stopScrollListener?.();