diff --git a/src-tauri/src/commands/asset_actions.rs b/src-tauri/src/commands/asset_actions.rs index 9313a73ca..1714a4335 100644 --- a/src-tauri/src/commands/asset_actions.rs +++ b/src-tauri/src/commands/asset_actions.rs @@ -123,9 +123,13 @@ pub async fn get_asset_detail( let (_, asset_service) = match load_asset_service(&app, &session).await { Ok(result) => result, Err(error) => { + error!( + "get asset detail failed: asset_id={}, error={}", + asset_id, error + ); let _ = app.emit( "get-asset-detail-failure", - json!({ "status": 401, "error": error }), + json!({ "status": 401, "error": error, "asset_id": asset_id }), ); return Ok(()); } @@ -134,9 +138,17 @@ pub async fn get_asset_detail( let asset_detail = asset_service.get_asset_detail(&asset_id).await; if !asset_detail.success { + error!( + "get asset detail failed: asset_id={}, status={}", + asset_id, asset_detail.status + ); let _ = app.emit( "get-asset-detail-failure", - json!({ "status": asset_detail.status }), + json!({ + "status": asset_detail.status, + "error": asset_detail.data, + "asset_id": asset_id + }), ); return Ok(()); } diff --git a/ui/components/ConnectionEditor/connectionEditor.vue b/ui/components/ConnectionEditor/connectionEditor.vue index 3ddf5ffde..4bbb86d1f 100644 --- a/ui/components/ConnectionEditor/connectionEditor.vue +++ b/ui/components/ConnectionEditor/connectionEditor.vue @@ -7,7 +7,9 @@ const props = defineProps<{ }>(); const { t, locale } = useI18n(); +const toast = useToast(); const { getAssetDetail } = useAssetAction(); +const ASSET_DETAIL_TIMEOUT_MS = 15000; const open = ref(false); const currentAsset = ref(null); @@ -163,10 +165,30 @@ async function ensureDetails(asset: AssetItem) { if (!noAccounts && !noProtocols) return asset; - const detailsReady = new Promise((resolve) => { - const unsubscribe = useEventBus().once( + const bus = useEventBus(); + + return await new Promise((resolve, reject) => { + let settled = false; + let timer: ReturnType | undefined; + let unsubscribeUpdated: (() => void) | undefined; + let unsubscribeFailed: (() => void) | undefined; + + const cleanup = () => { + if (timer) clearTimeout(timer); + unsubscribeUpdated?.(); + unsubscribeFailed?.(); + }; + + const finish = (next: () => void) => { + if (settled) return; + settled = true; + cleanup(); + next(); + }; + + unsubscribeUpdated = bus.on( "assetDetailUpdated", - (payload: { assetId: string, permedAccounts: PermedAccount[], permedProtocols: PermedProtocol[] }) => { + (payload) => { if (payload.assetId !== asset.id) return; currentAsset.value = { @@ -175,16 +197,26 @@ async function ensureDetails(asset: AssetItem) { permedProtocols: payload.permedProtocols || [] } as AssetItem; - resolve(currentAsset.value!); - } + finish(() => resolve(currentAsset.value!)); + }, + false ); - void unsubscribe; - }); + unsubscribeFailed = bus.on( + "assetDetailFailed", + (payload) => { + if (payload.assetId !== asset.id) return; + finish(() => reject(new Error("get asset detail failed"))); + }, + false + ); - await getAssetDetail(asset.id); - const updated = await detailsReady; - return updated; + timer = setTimeout(() => { + finish(() => reject(new Error("get asset detail timeout"))); + }, ASSET_DETAIL_TIMEOUT_MS); + + getAssetDetail(asset.id); + }); } /** @@ -193,7 +225,26 @@ async function ensureDetails(asset: AssetItem) { */ async function openModal(asset: AssetItem, preferredProtocol?: string): Promise { currentAsset.value = asset; - await ensureDetails(asset); + + try { + await ensureDetails(asset); + } catch (error) { + const timedOut = error instanceof Error && error.message.includes("timeout"); + + if (timedOut) { + toast.add({ + title: t("Asset.GetAssetFailed"), + description: t("ConnectError.ConnectFailed"), + color: "error", + icon: "line-md:close-circle", + progress: true, + duration: 4000 + }); + } + + throw error; + } + initDraft(currentAsset.value!, preferredProtocol); open.value = true; diff --git a/ui/composables/useAssetAction.ts b/ui/composables/useAssetAction.ts index 7cd38e5d2..a806b2e8e 100644 --- a/ui/composables/useAssetAction.ts +++ b/ui/composables/useAssetAction.ts @@ -578,11 +578,33 @@ export const useAssetAction = () => { } }); - // TODO 提示 - unlistenGetAssetDetailFailed = await useTauriEventListen("get-asset-detail-failure", () => { - // interface eventPayload { - // status: string - // } + unlistenGetAssetDetailFailed = await useTauriEventListen("get-asset-detail-failure", (event) => { + interface eventPayload { + status?: number | string + error?: string + asset_id?: string + } + + const payload = event.payload as eventPayload; + const status = Number(payload.status); + + toast.add({ + title: status === 401 ? t("Login.LoginAuthenticationExpired") : t("Asset.GetAssetFailed"), + description: status === 401 + ? t("Login.LoginAuthenticationExpiredDescription") + : t("ConnectError.ConnectFailed"), + color: "error", + icon: "line-md:close-circle", + progress: true, + duration: 4000 + }); + + if (payload.asset_id) { + useEventBus().emit("assetDetailFailed", { + assetId: payload.asset_id, + status: Number.isFinite(status) ? status : undefined + }); + } }); unlistenRenameSuccess = await useTauriEventListen("rename-success", (event) => { diff --git a/ui/composables/useEventBus.ts b/ui/composables/useEventBus.ts index d5f9819a9..ea3f6cee3 100644 --- a/ui/composables/useEventBus.ts +++ b/ui/composables/useEventBus.ts @@ -27,6 +27,10 @@ type BusEvents = { permedAccounts: PermedAccount[] permedProtocols: PermedProtocol[] } + assetDetailFailed: { + assetId: string + status?: number + } } & Record; const emitter: Emitter = mitt();