feat: 资产同步状态与同步轨迹入口
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m43s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m43s
This commit is contained in:
@@ -40,7 +40,7 @@
|
||||
:icon="Refresh"
|
||||
:disabled="refreshDisabled"
|
||||
>
|
||||
{{ refreshButtonText }}
|
||||
同步
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,7 +48,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElCard, ElInput, ElButton } from 'element-plus'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
@@ -63,19 +63,13 @@
|
||||
interface Props {
|
||||
hasCardInfo?: boolean
|
||||
refreshDisabled?: boolean
|
||||
refreshRemainingText?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
withDefaults(defineProps<Props>(), {
|
||||
hasCardInfo: false,
|
||||
refreshDisabled: false,
|
||||
refreshRemainingText: ''
|
||||
refreshDisabled: false
|
||||
})
|
||||
|
||||
const refreshButtonText = computed(() =>
|
||||
props.refreshRemainingText ? `同步(${props.refreshRemainingText})` : '同步'
|
||||
)
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
search: [payload: { identifier: string }]
|
||||
|
||||
@@ -178,6 +178,42 @@
|
||||
</template>
|
||||
</ElDescriptions>
|
||||
|
||||
<ElDivider content-position="left">同步状态</ElDivider>
|
||||
<ElDescriptions :column="descriptionsColumn" border>
|
||||
<ElDescriptionsItem label="轮询状态">
|
||||
<ElTag
|
||||
v-if="cardInfo?.polling"
|
||||
:type="cardInfo.polling.enabled ? 'success' : 'info'"
|
||||
size="small"
|
||||
>
|
||||
{{ cardInfo.polling.enabled ? '已启用' : '未启用' }}
|
||||
</ElTag>
|
||||
<span v-else>-</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="活跃级别">
|
||||
{{ cardInfo?.polling?.activity_level || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="最后活跃时间">
|
||||
{{ formatDateTime(cardInfo?.polling?.last_activity_at) || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="最后活跃场景">
|
||||
{{ cardInfo?.polling?.last_activity_scene || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="下次轮询时间">
|
||||
{{ formatDateTime(cardInfo?.polling?.next_poll_at) || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="同步轨迹">
|
||||
<ElButton
|
||||
v-permission="'asset_info:view_sync_trail'"
|
||||
type="primary"
|
||||
link
|
||||
@click="emit('viewSyncTrail')"
|
||||
>
|
||||
查看同步轨迹
|
||||
</ElButton>
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<!-- 设备绑定卡列表 -->
|
||||
<template v-if="cardInfo?.asset_type === 'device'">
|
||||
<ElDivider content-position="left">绑定卡列表</ElDivider>
|
||||
@@ -496,6 +532,7 @@
|
||||
ElMessage
|
||||
} from 'element-plus'
|
||||
import { CopyDocument } from '@element-plus/icons-vue'
|
||||
import type { AssetPollingStatus } from '@/types/api'
|
||||
import { useAssetFormatters } from '../composables/useAssetFormatters'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
@@ -551,6 +588,7 @@
|
||||
last_data_check_at?: string | null
|
||||
last_real_name_check_at?: string | null
|
||||
last_card_status_check_at?: string | null
|
||||
polling?: AssetPollingStatus | null
|
||||
bound_device_id?: number
|
||||
bound_device_no?: string
|
||||
bound_device_name?: string
|
||||
@@ -685,6 +723,7 @@
|
||||
(e: 'updateBindingCardRealnameStatus', payload: { card: BindingCard }): void
|
||||
(e: 'navigateToCard', iccid: string): void
|
||||
(e: 'navigateToDevice', deviceNo: string): void
|
||||
(e: 'viewSyncTrail'): void
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { AssetService } from '@/api/modules'
|
||||
import { formatRemainingTime, FrontendRateLimitError } from '@/utils/business/apiRateLimit'
|
||||
import { FrontendRateLimitError } from '@/utils/business/apiRateLimit'
|
||||
import type {
|
||||
AssetBoundCard,
|
||||
AssetWalletResponse,
|
||||
@@ -123,6 +123,7 @@ export function useAssetInfo() {
|
||||
updated_at: data.updated_at,
|
||||
accumulated_recharge: data.accumulated_recharge,
|
||||
first_commission_paid: data.first_commission_paid,
|
||||
polling: data.polling ?? null,
|
||||
|
||||
// 卡专属字段 (asset_type === 'card')
|
||||
iccid: data.iccid || '',
|
||||
@@ -430,11 +431,11 @@ export function useAssetInfo() {
|
||||
return true
|
||||
} catch (error: any) {
|
||||
if (error instanceof FrontendRateLimitError) {
|
||||
ElMessage.warning(`操作过于频繁,请${formatRemainingTime(error.remainingMs)}后再试`)
|
||||
ElMessage.warning('操作过于频繁,请稍后重试')
|
||||
return false
|
||||
}
|
||||
if (error?.response?.status === 429) {
|
||||
ElMessage.warning('操作过于频繁,请5分钟后再试')
|
||||
ElMessage.warning('操作过于频繁,请稍后重试')
|
||||
return false
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
ref="assetSearchCardRef"
|
||||
:has-card-info="!!cardInfo"
|
||||
:refresh-disabled="refreshDisabled"
|
||||
:refresh-remaining-text="refreshRemainingText"
|
||||
@search="handleSearch"
|
||||
@refresh="handleRefresh"
|
||||
/>
|
||||
@@ -43,6 +42,7 @@
|
||||
@update-binding-card-realname-status="handleUpdateBindingCardRealnameStatus"
|
||||
@navigate-to-card="handleNavigateToCard"
|
||||
@navigate-to-device="handleNavigateToDevice"
|
||||
@view-sync-trail="handleViewSyncTrail"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -332,26 +332,7 @@
|
||||
const refreshSubmitting = ref(false)
|
||||
let rateLimitTimer: ReturnType<typeof setInterval> | undefined
|
||||
|
||||
const refreshRateLimitState = computed(() => {
|
||||
const identifier = cardInfo.value?.identifier
|
||||
if (!identifier) {
|
||||
return {
|
||||
limited: false,
|
||||
remainingMs: 0,
|
||||
retryAt: 0
|
||||
}
|
||||
}
|
||||
return getAssetActionRateLimitState('refresh', identifier, rateLimitNow.value)
|
||||
})
|
||||
|
||||
const refreshDisabled = computed(
|
||||
() => refreshSubmitting.value || refreshRateLimitState.value.limited
|
||||
)
|
||||
const refreshRemainingText = computed(() =>
|
||||
refreshRateLimitState.value.limited
|
||||
? formatRemainingTime(refreshRateLimitState.value.remainingMs)
|
||||
: ''
|
||||
)
|
||||
const refreshDisabled = computed(() => refreshSubmitting.value)
|
||||
|
||||
const startRateLimitState = computed(() => {
|
||||
const identifier = cardInfo.value?.iccid || cardInfo.value?.identifier
|
||||
@@ -459,6 +440,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewSyncTrail = () => {
|
||||
if (!cardInfo.value) return
|
||||
|
||||
router.push({
|
||||
path: '/audit/integrations',
|
||||
query: {
|
||||
resource_type: cardInfo.value.asset_type,
|
||||
resource_key: cardInfo.value.identifier
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* IoT卡操作 - 启用卡
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* 资产信息页面类型定义
|
||||
*/
|
||||
|
||||
import type { AssetPollingStatus } from '@/types/api'
|
||||
|
||||
// 资产类型枚举
|
||||
export type AssetType = 'card' | 'device'
|
||||
|
||||
@@ -37,6 +39,7 @@ export interface AssetInfo {
|
||||
last_data_check_at?: string | null
|
||||
last_real_name_check_at?: string | null
|
||||
last_card_status_check_at?: string | null
|
||||
polling?: AssetPollingStatus | null
|
||||
}
|
||||
|
||||
// 设备绑定卡信息
|
||||
|
||||
Reference in New Issue
Block a user