倒计时没有覆盖复机停机的问题

This commit is contained in:
2026-06-25 10:30:49 +08:00
parent cfb6b52cc2
commit f604791c69
6 changed files with 144 additions and 17 deletions

View File

@@ -165,9 +165,8 @@ export class AssetService extends BaseService {
* @param identifier 资产标识符ICCID 或 VirtualNo * @param identifier 资产标识符ICCID 或 VirtualNo
*/ */
static stopAsset(identifier: string): Promise<BaseResponse<DeviceStopResponse | void>> { static stopAsset(identifier: string): Promise<BaseResponse<DeviceStopResponse | void>> {
return this.post<BaseResponse<DeviceStopResponse | void>>( return runRateLimitedAssetAction('stop', identifier, () =>
`/api/admin/assets/${identifier}/stop`, this.post<BaseResponse<DeviceStopResponse | void>>(`/api/admin/assets/${identifier}/stop`, {})
{}
) )
} }

View File

@@ -1,7 +1,7 @@
const FIVE_MINUTES_MS = 5 * 60 * 1000 const FIVE_MINUTES_MS = 5 * 60 * 1000
const STORAGE_KEY_PREFIX = 'asset-action-rate-limit' const STORAGE_KEY_PREFIX = 'asset-action-rate-limit'
export type AssetRateLimitedAction = 'refresh' | 'start' export type AssetRateLimitedAction = 'refresh' | 'start' | 'stop'
export class FrontendRateLimitError extends Error { export class FrontendRateLimitError extends Error {
readonly remainingMs: number readonly remainingMs: number

View File

@@ -239,20 +239,22 @@
type="success" type="success"
size="small" size="small"
link link
:disabled="isBindingStartDisabled(scope.row)"
@click="handleEnableBindingCard(scope.row)" @click="handleEnableBindingCard(scope.row)"
v-permission="'iot_info:start'" v-permission="'iot_info:start'"
> >
启用此卡 {{ getBindingStartButtonText(scope.row) }}
</ElButton> </ElButton>
<ElButton <ElButton
v-if="scope.row.network_status === 1" v-if="scope.row.network_status === 1"
type="warning" type="warning"
size="small" size="small"
link link
:disabled="isBindingStopDisabled(scope.row)"
@click="handleDisableBindingCard(scope.row)" @click="handleDisableBindingCard(scope.row)"
v-permission="'iot_info:stop'" v-permission="'iot_info:stop'"
> >
停用此卡 {{ getBindingStopButtonText(scope.row) }}
</ElButton> </ElButton>
<ElButton <ElButton
type="primary" type="primary"
@@ -397,17 +399,19 @@
v-if="cardInfo?.network_status === 0" v-if="cardInfo?.network_status === 0"
v-permission="'iot_info:start'" v-permission="'iot_info:start'"
type="success" type="success"
:disabled="startDisabled"
@click="handleEnableCard" @click="handleEnableCard"
> >
启用此卡 {{ startRemainingText ? `启用此卡(${startRemainingText})` : '启用此卡' }}
</ElButton> </ElButton>
<ElButton <ElButton
v-if="cardInfo?.network_status === 1" v-if="cardInfo?.network_status === 1"
v-permission="'iot_info:stop'" v-permission="'iot_info:stop'"
type="warning" type="warning"
:disabled="stopDisabled"
@click="handleDisableCard" @click="handleDisableCard"
> >
停用此卡 {{ stopRemainingText ? `停用此卡(${stopRemainingText})` : '停用此卡' }}
</ElButton> </ElButton>
<!--<ElButton type="danger" v-permission="'iot_info:handler_stop'" @click="handleManualDeactivate"> 手动停用 </ElButton>--> <!--<ElButton type="danger" v-permission="'iot_info:handler_stop'" @click="handleManualDeactivate"> 手动停用 </ElButton>-->
<!-- 手动停用暂时不开放 --> <!-- 手动停用暂时不开放 -->
@@ -580,10 +584,22 @@
deviceRealtime: DeviceRealtimeInfo | null deviceRealtime: DeviceRealtimeInfo | null
pollingEnabled: boolean pollingEnabled: boolean
realtimeLoading?: 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<Props>(), { const props = withDefaults(defineProps<Props>(), {
realtimeLoading: false realtimeLoading: false,
startDisabled: false,
stopDisabled: false,
startRemainingText: '',
stopRemainingText: ''
}) })
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
@@ -717,17 +733,15 @@
// 卡操作 // 卡操作
const handleEnableCard = () => { const handleEnableCard = () => {
if (props.startDisabled) return
emit('enableCard') emit('enableCard')
} }
const handleDisableCard = () => { const handleDisableCard = () => {
if (props.stopDisabled) return
emit('disableCard') emit('disableCard')
} }
const handleManualDeactivate = () => {
emit('manualDeactivate')
}
// 设备操作 // 设备操作
const handleRebootDevice = () => { const handleRebootDevice = () => {
emit('rebootDevice') 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) => { const handleEnableBindingCard = (card: BindingCard) => {
if (isBindingStartDisabled(card)) return
ElMessageBox.confirm(`确定要启用此卡(${card.iccid})吗?`, '启用确认', { ElMessageBox.confirm(`确定要启用此卡(${card.iccid})吗?`, '启用确认', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
@@ -771,6 +806,7 @@
} }
const handleDisableBindingCard = (card: BindingCard) => { const handleDisableBindingCard = (card: BindingCard) => {
if (isBindingStopDisabled(card)) return
ElMessageBox.confirm(`确定要停用此卡(${card.iccid})吗?`, '停用确认', { ElMessageBox.confirm(`确定要停用此卡(${card.iccid})吗?`, '停用确认', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',

View File

@@ -69,6 +69,10 @@ export function useAssetOperations(cardInfo: Ref<any>, refreshAssetFn?: () => Pr
} }
} catch (error: any) { } catch (error: any) {
if (error !== 'cancel') { if (error !== 'cancel') {
if (error instanceof FrontendRateLimitError) {
ElMessage.warning(`操作过于频繁,请${formatRemainingTime(error.remainingMs)}后再试`)
return
}
console.error('停用失败:', error) console.error('停用失败:', error)
} }
} finally { } finally {

View File

@@ -20,6 +20,14 @@
:device-realtime="deviceRealtime" :device-realtime="deviceRealtime"
v-model:polling-enabled="pollingEnabled" v-model:polling-enabled="pollingEnabled"
:realtime-loading="realtimeLoading" :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" @enable-card="handleEnableCard"
@disable-card="handleDisableCard" @disable-card="handleDisableCard"
@manual-deactivate="handleManualDeactivate" @manual-deactivate="handleManualDeactivate"
@@ -176,7 +184,8 @@
import { import {
formatRemainingTime, formatRemainingTime,
FrontendRateLimitError, FrontendRateLimitError,
getAssetActionRateLimitState getAssetActionRateLimitState,
type AssetRateLimitedAction
} from '@/utils/business/apiRateLimit' } from '@/utils/business/apiRateLimit'
// 引入子组件 // 引入子组件
@@ -248,6 +257,8 @@
// ========== 资产操作方法(使用 composable ========== // ========== 资产操作方法(使用 composable ==========
const { const {
enableCardLoading,
disableCardLoading,
enableCard: enableCardOp, enableCard: enableCardOp,
disableCard: disableCardOp, disableCard: disableCardOp,
manualDeactivateCard: manualDeactivateCardOp, 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 = () => { const updateRateLimitNow = () => {
rateLimitNow.value = Date.now() rateLimitNow.value = Date.now()
} }
@@ -399,14 +463,24 @@
* IoT卡操作 - 启用卡 * IoT卡操作 - 启用卡
*/ */
const handleEnableCard = async () => { const handleEnableCard = async () => {
if (startDisabled.value) return
try {
await enableCardOp() await enableCardOp()
} finally {
updateRateLimitNow()
}
} }
/** /**
* IoT卡操作 - 停用卡 * IoT卡操作 - 停用卡
*/ */
const handleDisableCard = async () => { const handleDisableCard = async () => {
if (stopDisabled.value) return
try {
await disableCardOp() await disableCardOp()
} finally {
updateRateLimitNow()
}
} }
/** /**
@@ -550,6 +624,7 @@
*/ */
const handleEnableBindingCard = async (payload: { card: any }) => { const handleEnableBindingCard = async (payload: { card: any }) => {
const { card } = payload const { card } = payload
if (isStartActionDisabled(card.iccid)) return
try { try {
const { AssetService } = await import('@/api/modules') const { AssetService } = await import('@/api/modules')
await AssetService.startAsset(card.iccid) await AssetService.startAsset(card.iccid)
@@ -562,6 +637,8 @@
return return
} }
console.error('启用绑定卡失败:', error) console.error('启用绑定卡失败:', error)
} finally {
updateRateLimitNow()
} }
} }
@@ -570,6 +647,7 @@
*/ */
const handleDisableBindingCard = async (payload: { card: any }) => { const handleDisableBindingCard = async (payload: { card: any }) => {
const { card } = payload const { card } = payload
if (isStopActionDisabled(card.iccid)) return
try { try {
const { AssetService } = await import('@/api/modules') const { AssetService } = await import('@/api/modules')
await AssetService.stopAsset(card.iccid) await AssetService.stopAsset(card.iccid)
@@ -577,7 +655,13 @@
if (cardInfo.value) if (cardInfo.value)
await loadRealtimeStatus(cardInfo.value.identifier, cardInfo.value.asset_type) await loadRealtimeStatus(cardInfo.value.identifier, cardInfo.value.asset_type)
} catch (error: any) { } catch (error: any) {
if (error instanceof FrontendRateLimitError) {
ElMessage.warning(`操作过于频繁,请${formatRemainingTime(error.remainingMs)}后再试`)
return
}
console.error('停用绑定卡失败:', error) console.error('停用绑定卡失败:', error)
} finally {
updateRateLimitNow()
} }
} }

View File

@@ -2920,6 +2920,10 @@
await getTableData() await getTableData()
} }
} catch (error: any) { } catch (error: any) {
if (error instanceof FrontendRateLimitError) {
ElMessage.warning(`操作过于频繁,请${formatRemainingTime(error.remainingMs)}后再试`)
return
}
console.error('停用此卡失败:', error) console.error('停用此卡失败:', error)
} }
}) })