From f604791c69216d09a36fb3340a69ba9c5bb6904d Mon Sep 17 00:00:00 2001 From: break Date: Thu, 25 Jun 2026 10:30:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=80=92=E8=AE=A1=E6=97=B6=E6=B2=A1=E6=9C=89?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E5=A4=8D=E6=9C=BA=E5=81=9C=E6=9C=BA=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/modules/asset.ts | 5 +- src/utils/business/apiRateLimit.ts | 2 +- .../components/BasicInfoCard.vue | 56 +++++++++--- .../composables/useAssetOperations.ts | 4 + .../asset-information/index.vue | 90 ++++++++++++++++++- .../iot-card-management/index.vue | 4 + 6 files changed, 144 insertions(+), 17 deletions(-) diff --git a/src/api/modules/asset.ts b/src/api/modules/asset.ts index 541adc7..39a6960 100644 --- a/src/api/modules/asset.ts +++ b/src/api/modules/asset.ts @@ -165,9 +165,8 @@ export class AssetService extends BaseService { * @param identifier 资产标识符(ICCID 或 VirtualNo) */ static stopAsset(identifier: string): Promise> { - return this.post>( - `/api/admin/assets/${identifier}/stop`, - {} + return runRateLimitedAssetAction('stop', identifier, () => + this.post>(`/api/admin/assets/${identifier}/stop`, {}) ) } diff --git a/src/utils/business/apiRateLimit.ts b/src/utils/business/apiRateLimit.ts index 9603e5d..80c2926 100644 --- a/src/utils/business/apiRateLimit.ts +++ b/src/utils/business/apiRateLimit.ts @@ -1,7 +1,7 @@ const FIVE_MINUTES_MS = 5 * 60 * 1000 const STORAGE_KEY_PREFIX = 'asset-action-rate-limit' -export type AssetRateLimitedAction = 'refresh' | 'start' +export type AssetRateLimitedAction = 'refresh' | 'start' | 'stop' export class FrontendRateLimitError extends Error { readonly remainingMs: number diff --git a/src/views/asset-management/asset-information/components/BasicInfoCard.vue b/src/views/asset-management/asset-information/components/BasicInfoCard.vue index 0c22c8e..55da2be 100644 --- a/src/views/asset-management/asset-information/components/BasicInfoCard.vue +++ b/src/views/asset-management/asset-information/components/BasicInfoCard.vue @@ -184,7 +184,7 @@ @@ -239,20 +239,22 @@ type="success" size="small" link + :disabled="isBindingStartDisabled(scope.row)" @click="handleEnableBindingCard(scope.row)" v-permission="'iot_info:start'" > - 启用此卡 + {{ getBindingStartButtonText(scope.row) }} - 停用此卡 + {{ getBindingStopButtonText(scope.row) }} - 启用此卡 + {{ startRemainingText ? `启用此卡(${startRemainingText})` : '启用此卡' }} - 停用此卡 + {{ stopRemainingText ? `停用此卡(${stopRemainingText})` : '停用此卡' }} @@ -580,10 +584,22 @@ deviceRealtime: DeviceRealtimeInfo | null pollingEnabled: boolean realtimeLoading?: boolean + startDisabled?: boolean + stopDisabled?: boolean + startRemainingText?: string + stopRemainingText?: string + getStartRemainingText?: (identifier: string) => string + getStopRemainingText?: (identifier: string) => string + isStartDisabled?: (identifier: string) => boolean + isStopDisabled?: (identifier: string) => boolean } const props = withDefaults(defineProps(), { - realtimeLoading: false + realtimeLoading: false, + startDisabled: false, + stopDisabled: false, + startRemainingText: '', + stopRemainingText: '' }) const { hasAuth } = useAuth() @@ -717,17 +733,15 @@ // 卡操作 const handleEnableCard = () => { + if (props.startDisabled) return emit('enableCard') } const handleDisableCard = () => { + if (props.stopDisabled) return emit('disableCard') } - const handleManualDeactivate = () => { - emit('manualDeactivate') - } - // 设备操作 const handleRebootDevice = () => { emit('rebootDevice') @@ -758,7 +772,28 @@ } // 绑定卡操作 + const getBindingStartRemainingText = (card: BindingCard) => + props.getStartRemainingText?.(card.iccid) || '' + + const getBindingStopRemainingText = (card: BindingCard) => + props.getStopRemainingText?.(card.iccid) || '' + + const isBindingStartDisabled = (card: BindingCard) => props.isStartDisabled?.(card.iccid) || false + + const isBindingStopDisabled = (card: BindingCard) => props.isStopDisabled?.(card.iccid) || false + + const getBindingStartButtonText = (card: BindingCard) => { + const remainingText = getBindingStartRemainingText(card) + return remainingText ? `启用此卡(${remainingText})` : '启用此卡' + } + + const getBindingStopButtonText = (card: BindingCard) => { + const remainingText = getBindingStopRemainingText(card) + return remainingText ? `停用此卡(${remainingText})` : '停用此卡' + } + const handleEnableBindingCard = (card: BindingCard) => { + if (isBindingStartDisabled(card)) return ElMessageBox.confirm(`确定要启用此卡(${card.iccid})吗?`, '启用确认', { confirmButtonText: '确定', cancelButtonText: '取消', @@ -771,6 +806,7 @@ } const handleDisableBindingCard = (card: BindingCard) => { + if (isBindingStopDisabled(card)) return ElMessageBox.confirm(`确定要停用此卡(${card.iccid})吗?`, '停用确认', { confirmButtonText: '确定', cancelButtonText: '取消', diff --git a/src/views/asset-management/asset-information/composables/useAssetOperations.ts b/src/views/asset-management/asset-information/composables/useAssetOperations.ts index e5a5d57..45d8ccb 100644 --- a/src/views/asset-management/asset-information/composables/useAssetOperations.ts +++ b/src/views/asset-management/asset-information/composables/useAssetOperations.ts @@ -69,6 +69,10 @@ export function useAssetOperations(cardInfo: Ref, refreshAssetFn?: () => Pr } } catch (error: any) { if (error !== 'cancel') { + if (error instanceof FrontendRateLimitError) { + ElMessage.warning(`操作过于频繁,请${formatRemainingTime(error.remainingMs)}后再试`) + return + } console.error('停用失败:', error) } } finally { diff --git a/src/views/asset-management/asset-information/index.vue b/src/views/asset-management/asset-information/index.vue index 1022052..bb87404 100644 --- a/src/views/asset-management/asset-information/index.vue +++ b/src/views/asset-management/asset-information/index.vue @@ -20,6 +20,14 @@ :device-realtime="deviceRealtime" v-model:polling-enabled="pollingEnabled" :realtime-loading="realtimeLoading" + :start-disabled="startDisabled" + :stop-disabled="stopDisabled" + :start-remaining-text="startRemainingText" + :stop-remaining-text="stopRemainingText" + :get-start-remaining-text="getStartRemainingText" + :get-stop-remaining-text="getStopRemainingText" + :is-start-disabled="isStartActionDisabled" + :is-stop-disabled="isStopActionDisabled" @enable-card="handleEnableCard" @disable-card="handleDisableCard" @manual-deactivate="handleManualDeactivate" @@ -176,7 +184,8 @@ import { formatRemainingTime, FrontendRateLimitError, - getAssetActionRateLimitState + getAssetActionRateLimitState, + type AssetRateLimitedAction } from '@/utils/business/apiRateLimit' // 引入子组件 @@ -248,6 +257,8 @@ // ========== 资产操作方法(使用 composable) ========== const { + enableCardLoading, + disableCardLoading, enableCard: enableCardOp, disableCard: disableCardOp, manualDeactivateCard: manualDeactivateCardOp, @@ -342,6 +353,59 @@ : '' ) + const startRateLimitState = computed(() => { + const identifier = cardInfo.value?.iccid || cardInfo.value?.identifier + if (!identifier) { + return { + limited: false, + remainingMs: 0, + retryAt: 0 + } + } + return getAssetActionRateLimitState('start', identifier, rateLimitNow.value) + }) + + const stopRateLimitState = computed(() => { + const identifier = cardInfo.value?.iccid || cardInfo.value?.identifier + if (!identifier) { + return { + limited: false, + remainingMs: 0, + retryAt: 0 + } + } + return getAssetActionRateLimitState('stop', identifier, rateLimitNow.value) + }) + + const startDisabled = computed(() => enableCardLoading.value || startRateLimitState.value.limited) + const stopDisabled = computed(() => disableCardLoading.value || stopRateLimitState.value.limited) + const startRemainingText = computed(() => + startRateLimitState.value.limited + ? formatRemainingTime(startRateLimitState.value.remainingMs) + : '' + ) + const stopRemainingText = computed(() => + stopRateLimitState.value.limited + ? formatRemainingTime(stopRateLimitState.value.remainingMs) + : '' + ) + + const getActionRemainingText = (action: AssetRateLimitedAction, identifier?: string) => { + if (!identifier) return '' + const state = getAssetActionRateLimitState(action, identifier, rateLimitNow.value) + return state.limited ? formatRemainingTime(state.remainingMs) : '' + } + + const isActionDisabled = (action: AssetRateLimitedAction, identifier?: string) => { + if (!identifier) return false + return getAssetActionRateLimitState(action, identifier, rateLimitNow.value).limited + } + + const getStartRemainingText = (identifier: string) => getActionRemainingText('start', identifier) + const getStopRemainingText = (identifier: string) => getActionRemainingText('stop', identifier) + const isStartActionDisabled = (identifier: string) => isActionDisabled('start', identifier) + const isStopActionDisabled = (identifier: string) => isActionDisabled('stop', identifier) + const updateRateLimitNow = () => { rateLimitNow.value = Date.now() } @@ -399,14 +463,24 @@ * IoT卡操作 - 启用卡 */ const handleEnableCard = async () => { - await enableCardOp() + if (startDisabled.value) return + try { + await enableCardOp() + } finally { + updateRateLimitNow() + } } /** * IoT卡操作 - 停用卡 */ const handleDisableCard = async () => { - await disableCardOp() + if (stopDisabled.value) return + try { + await disableCardOp() + } finally { + updateRateLimitNow() + } } /** @@ -550,6 +624,7 @@ */ const handleEnableBindingCard = async (payload: { card: any }) => { const { card } = payload + if (isStartActionDisabled(card.iccid)) return try { const { AssetService } = await import('@/api/modules') await AssetService.startAsset(card.iccid) @@ -562,6 +637,8 @@ return } console.error('启用绑定卡失败:', error) + } finally { + updateRateLimitNow() } } @@ -570,6 +647,7 @@ */ const handleDisableBindingCard = async (payload: { card: any }) => { const { card } = payload + if (isStopActionDisabled(card.iccid)) return try { const { AssetService } = await import('@/api/modules') await AssetService.stopAsset(card.iccid) @@ -577,7 +655,13 @@ if (cardInfo.value) await loadRealtimeStatus(cardInfo.value.identifier, cardInfo.value.asset_type) } catch (error: any) { + if (error instanceof FrontendRateLimitError) { + ElMessage.warning(`操作过于频繁,请${formatRemainingTime(error.remainingMs)}后再试`) + return + } console.error('停用绑定卡失败:', error) + } finally { + updateRateLimitNow() } } diff --git a/src/views/asset-management/iot-card-management/index.vue b/src/views/asset-management/iot-card-management/index.vue index 21f91e5..ce76fbe 100644 --- a/src/views/asset-management/iot-card-management/index.vue +++ b/src/views/asset-management/iot-card-management/index.vue @@ -2920,6 +2920,10 @@ await getTableData() } } catch (error: any) { + if (error instanceof FrontendRateLimitError) { + ElMessage.warning(`操作过于频繁,请${formatRemainingTime(error.remainingMs)}后再试`) + return + } console.error('停用此卡失败:', error) } })