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:
32
openspec/changes/update-asset-sync-status-trail/proposal.md
Normal file
32
openspec/changes/update-asset-sync-status-trail/proposal.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Change: 资产同步状态与同步轨迹入口
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
当前资产信息页虽保留了手动刷新能力和部分同步时间展示,但运营无法直观看到资产当前轮询策略、最近活跃情况及下次轮询时间,也没有可直接进入该资产同步轨迹的入口。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 在资产信息页的卡和设备详情中新增同步状态区域,展示轮询启用状态、活跃级别、最后活跃时间、最后活跃场景和下次轮询时间。
|
||||||
|
- 复用 `GET /api/admin/assets/resolve/{identifier}` 的新增 `polling` 响应对象;无下次轮询时间时展示 `-`。
|
||||||
|
- 保留现有手动刷新按钮及其接口契约,不新增第二个同步或刷新按钮。
|
||||||
|
- 新增“查看同步轨迹”入口,跳转全局审计外部集成页并带入当前资产的筛选参数。
|
||||||
|
- 同步轨迹由目标页面按立即、3分钟、5分钟连续结果展示;本提案不改造全局审计外部集成页面本身。
|
||||||
|
- 对 `rate_limited` 手动刷新结果仅展示本次失败,不展示自动退避倒计时。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: `asset-information`
|
||||||
|
- Related active changes: `update-asset-realtime-sync-timestamps-display`(共享资产同步展示区域)
|
||||||
|
- Affected code:
|
||||||
|
- `src/types/api/asset.ts`
|
||||||
|
- `src/api/modules/asset.ts`
|
||||||
|
- `src/views/asset-management/asset-information/**`
|
||||||
|
- 资产信息页到全局审计外部集成页的路由跳转参数
|
||||||
|
- API contracts:
|
||||||
|
- `GET /api/admin/assets/resolve/{identifier}`
|
||||||
|
- `POST /api/admin/assets/{identifier}/refresh`(保持既有契约)
|
||||||
|
- `GET /api/admin/audit/integrations`(复用 `resource_type`、`resource_key`、`correlation_id` 查询参数)
|
||||||
|
- Out of scope:
|
||||||
|
- 全局审计外部集成页面自身的开发或改造
|
||||||
|
- 新增第二个同步或刷新按钮
|
||||||
|
- `rate_limited` 的自动退避倒计时展示
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Asset Polling Status Display
|
||||||
|
|
||||||
|
The asset information page SHALL display the current polling status for both card and device assets from the `polling` object returned by `GET /api/admin/assets/resolve/{identifier}`.
|
||||||
|
|
||||||
|
#### Scenario: Display complete polling status for a card asset
|
||||||
|
|
||||||
|
- **GIVEN** 用户正在查看卡资产信息页
|
||||||
|
- **WHEN** 资产解析响应包含 `polling.enabled`、`activity_level`、`last_activity_at`、`last_activity_scene` 和 `next_poll_at`
|
||||||
|
- **THEN** 页面 MUST 展示轮询是否启用、活跃级别、最后活跃时间、最后活跃场景和下次轮询时间
|
||||||
|
- **AND** 时间字段 MUST 使用资产信息页统一的时间格式
|
||||||
|
|
||||||
|
#### Scenario: Display complete polling status for a device asset
|
||||||
|
|
||||||
|
- **GIVEN** 用户正在查看设备资产信息页
|
||||||
|
- **WHEN** 资产解析响应包含 `polling` 对象
|
||||||
|
- **THEN** 页面 MUST 展示轮询是否启用、活跃级别、最后活跃时间、最后活跃场景和下次轮询时间
|
||||||
|
|
||||||
|
#### Scenario: Display placeholders for unavailable polling values
|
||||||
|
|
||||||
|
- **GIVEN** 用户正在查看卡或设备资产信息页
|
||||||
|
- **WHEN** `last_activity_at`、`last_activity_scene` 或 `next_poll_at` 为 `null`、空字符串或未返回
|
||||||
|
- **THEN** 页面 MUST 保留对应字段标签
|
||||||
|
- **AND** `next_poll_at` MUST 显示 `-`
|
||||||
|
- **AND** 其他不可用字段 MUST 显示稳定占位内容
|
||||||
|
|
||||||
|
### Requirement: Asset Manual Refresh Remains the Only Refresh Entry
|
||||||
|
|
||||||
|
The asset information page SHALL preserve the existing manual refresh button and interface contract as the only asset refresh entry.
|
||||||
|
|
||||||
|
#### Scenario: Preserve existing manual refresh behavior
|
||||||
|
|
||||||
|
- **WHEN** 用户在资产信息页发起手动刷新
|
||||||
|
- **THEN** 系统 MUST 调用现有手动刷新接口
|
||||||
|
- **AND** 页面 MUST NOT 新增第二个同步或刷新按钮
|
||||||
|
|
||||||
|
#### Scenario: Show rate limited result without countdown
|
||||||
|
|
||||||
|
- **WHEN** 手动刷新请求返回 `rate_limited` 或等价限流失败结果
|
||||||
|
- **THEN** 系统 MUST 仅展示本次请求失败信息
|
||||||
|
- **AND** 系统 MUST NOT 展示自动退避倒计时
|
||||||
|
|
||||||
|
### Requirement: Asset Sync Trail Navigation
|
||||||
|
|
||||||
|
The asset information page SHALL provide a `查看同步轨迹` entry that navigates to the global audit integration page with filters for the current asset. The target query SHALL support `resource_type`, `resource_key`, and `correlation_id` so the audit page can display the immediate, 3-minute, and 5-minute consecutive sync results for the asset.
|
||||||
|
|
||||||
|
#### Scenario: Navigate to filtered sync trail from a card asset
|
||||||
|
|
||||||
|
- **GIVEN** 用户正在查看卡资产信息页
|
||||||
|
- **WHEN** 用户点击“查看同步轨迹”
|
||||||
|
- **THEN** 系统 MUST 跳转到全局审计外部集成页
|
||||||
|
- **AND** 跳转参数 MUST 带入当前卡资产对应的 `resource_type`、`resource_key` 或 `correlation_id` 筛选信息
|
||||||
|
|
||||||
|
#### Scenario: Navigate to filtered sync trail from a device asset
|
||||||
|
|
||||||
|
- **GIVEN** 用户正在查看设备资产信息页
|
||||||
|
- **WHEN** 用户点击“查看同步轨迹”
|
||||||
|
- **THEN** 系统 MUST 跳转到全局审计外部集成页
|
||||||
|
- **AND** 跳转参数 MUST 带入当前设备资产对应的 `resource_type`、`resource_key` 或 `correlation_id` 筛选信息
|
||||||
|
|
||||||
|
#### Scenario: Sync trail includes consecutive polling results
|
||||||
|
|
||||||
|
- **GIVEN** 用户已通过资产信息页进入带筛选的同步轨迹
|
||||||
|
- **WHEN** 全局审计外部集成页查询该资产轨迹
|
||||||
|
- **THEN** 查询结果 MUST 支持按立即、3分钟、5分钟连续同步结果展示
|
||||||
|
|
||||||
|
#### Scenario: Audit integration page implementation is out of scope
|
||||||
|
|
||||||
|
- **WHEN** 本次变更落地
|
||||||
|
- **THEN** 资产信息页的同步轨迹跳转入口 MUST 可用
|
||||||
|
- **AND** 全局审计外部集成页面自身 MUST NOT 被要求在本提案中改造
|
||||||
24
openspec/changes/update-asset-sync-status-trail/tasks.md
Normal file
24
openspec/changes/update-asset-sync-status-trail/tasks.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
## 1. API Contract
|
||||||
|
|
||||||
|
- [x] 1.1 更新资产解析响应类型,支持 `polling.enabled`、`activity_level`、`last_activity_at`、`last_activity_scene`、`next_poll_at`。
|
||||||
|
- [x] 1.2 保持现有资产手动刷新接口与调用方式不变,不新增同步接口或按钮。
|
||||||
|
- [x] 1.3 定义同步轨迹跳转参数,复用 `resource_type`、`resource_key`、`correlation_id` 查询契约。
|
||||||
|
|
||||||
|
## 2. Asset Sync Status UI
|
||||||
|
|
||||||
|
- [x] 2.1 在卡和设备资产信息页新增同步状态区域,展示轮询是否启用、活跃级别、最后活跃时间、最后活跃场景和下次轮询时间。
|
||||||
|
- [x] 2.2 为空的 `last_activity_at`、`last_activity_scene`、`next_poll_at` 提供稳定占位;无下次轮询时间显示 `-`。
|
||||||
|
- [x] 2.3 保留现有手动刷新按钮;`rate_limited` 时仅展示本次失败结果,不显示自动退避倒计时。
|
||||||
|
|
||||||
|
## 3. Sync Trail Navigation
|
||||||
|
|
||||||
|
- [x] 3.1 增加受 `asset_info:view_sync_trail` 按钮权限控制的“查看同步轨迹”入口,跳转全局审计外部集成页并带入当前资产筛选。
|
||||||
|
- [ ] 3.2 确保跳转筛选可支持立即、3分钟、5分钟连续同步结果的轨迹查询(当前仓库未提供全局审计外部集成页,待目标页接入后联调)。
|
||||||
|
- [ ] 3.3 不改造全局审计外部集成页面本身。
|
||||||
|
|
||||||
|
## 4. Verification
|
||||||
|
|
||||||
|
- [ ] 4.1 验证卡和设备均可展示完整、部分为空和全部为空的同步状态。
|
||||||
|
- [x] 4.2 验证保留单一手动刷新入口,且 `rate_limited` 不展示倒计时。
|
||||||
|
- [ ] 4.3 验证“查看同步轨迹”跳转携带当前资产筛选并能查看连续同步结果(待全局审计外部集成页提供后联调)。
|
||||||
|
- [x] 4.4 运行相关前端校验,并执行 `openspec validate update-asset-sync-status-trail --strict`。
|
||||||
@@ -34,14 +34,13 @@ import type {
|
|||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
|
|
||||||
const runRateLimitedAssetAction = async <T>(
|
const runRateLimitedAssetAction = async <T>(
|
||||||
action: AssetRateLimitedAction,
|
action: Exclude<AssetRateLimitedAction, 'refresh'>,
|
||||||
identifier: string,
|
identifier: string,
|
||||||
request: () => Promise<T>
|
request: () => Promise<T>
|
||||||
): Promise<T> => {
|
): Promise<T> => {
|
||||||
assertAssetActionAllowed(action, identifier)
|
assertAssetActionAllowed(action, identifier)
|
||||||
markAssetActionCalled(action, identifier)
|
markAssetActionCalled(action, identifier)
|
||||||
const response = await request()
|
return request()
|
||||||
return response
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AssetService extends BaseService {
|
export class AssetService extends BaseService {
|
||||||
@@ -84,17 +83,14 @@ export class AssetService extends BaseService {
|
|||||||
/**
|
/**
|
||||||
* 主动调网关拉取最新数据后返回
|
* 主动调网关拉取最新数据后返回
|
||||||
* POST /api/admin/assets/:identifier/refresh
|
* POST /api/admin/assets/:identifier/refresh
|
||||||
* 前端按资产标识限制 5 分钟内只能调用一次
|
|
||||||
* @param identifier 资产标识符(ICCID 或 VirtualNo)
|
* @param identifier 资产标识符(ICCID 或 VirtualNo)
|
||||||
*/
|
*/
|
||||||
static refreshAsset(identifier: string): Promise<BaseResponse<AssetRefreshResponse>> {
|
static refreshAsset(identifier: string): Promise<BaseResponse<AssetRefreshResponse>> {
|
||||||
return runRateLimitedAssetAction('refresh', identifier, () =>
|
return this.post<BaseResponse<AssetRefreshResponse>>(
|
||||||
this.post<BaseResponse<AssetRefreshResponse>>(
|
|
||||||
`/api/admin/assets/${identifier}/refresh`,
|
`/api/admin/assets/${identifier}/refresh`,
|
||||||
{},
|
{},
|
||||||
{ timeout: 60000 }
|
{ timeout: 60000 }
|
||||||
)
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -46,6 +46,15 @@ export interface AssetResolveParams {
|
|||||||
include_usage_summary?: boolean // 是否返回当前世代流量汇总字段
|
include_usage_summary?: boolean // 是否返回当前世代流量汇总字段
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 资产轮询状态
|
||||||
|
export interface AssetPollingStatus {
|
||||||
|
enabled: boolean // 是否启用轮询
|
||||||
|
activity_level: string // 活跃级别
|
||||||
|
last_activity_at?: string | null // 最后活跃时间
|
||||||
|
last_activity_scene?: string | null // 最后活跃场景
|
||||||
|
next_poll_at?: string | null // 下次轮询时间
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 资产解析/详情响应
|
* 资产解析/详情响应
|
||||||
* 对应接口:GET /api/admin/assets/resolve/:identifier
|
* 对应接口:GET /api/admin/assets/resolve/:identifier
|
||||||
@@ -80,6 +89,7 @@ export interface AssetResolveResponse {
|
|||||||
updated_at: string // 更新时间
|
updated_at: string // 更新时间
|
||||||
accumulated_recharge?: number // 累计充值金额(分)
|
accumulated_recharge?: number // 累计充值金额(分)
|
||||||
first_commission_paid?: boolean // 一次性佣金是否已发放
|
first_commission_paid?: boolean // 一次性佣金是否已发放
|
||||||
|
polling?: AssetPollingStatus | null // 资产轮询状态
|
||||||
|
|
||||||
// ===== 卡专属字段 (asset_type === 'card' 时) =====
|
// ===== 卡专属字段 (asset_type === 'card' 时) =====
|
||||||
iccid?: string // ICCID
|
iccid?: string // ICCID
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
:icon="Refresh"
|
:icon="Refresh"
|
||||||
:disabled="refreshDisabled"
|
:disabled="refreshDisabled"
|
||||||
>
|
>
|
||||||
{{ refreshButtonText }}
|
同步
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { ElCard, ElInput, ElButton } from 'element-plus'
|
import { ElCard, ElInput, ElButton } from 'element-plus'
|
||||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||||
@@ -63,19 +63,13 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
hasCardInfo?: boolean
|
hasCardInfo?: boolean
|
||||||
refreshDisabled?: boolean
|
refreshDisabled?: boolean
|
||||||
refreshRemainingText?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
hasCardInfo: false,
|
hasCardInfo: false,
|
||||||
refreshDisabled: false,
|
refreshDisabled: false
|
||||||
refreshRemainingText: ''
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const refreshButtonText = computed(() =>
|
|
||||||
props.refreshRemainingText ? `同步(${props.refreshRemainingText})` : '同步'
|
|
||||||
)
|
|
||||||
|
|
||||||
// Emits
|
// Emits
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
search: [payload: { identifier: string }]
|
search: [payload: { identifier: string }]
|
||||||
|
|||||||
@@ -178,6 +178,42 @@
|
|||||||
</template>
|
</template>
|
||||||
</ElDescriptions>
|
</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'">
|
<template v-if="cardInfo?.asset_type === 'device'">
|
||||||
<ElDivider content-position="left">绑定卡列表</ElDivider>
|
<ElDivider content-position="left">绑定卡列表</ElDivider>
|
||||||
@@ -496,6 +532,7 @@
|
|||||||
ElMessage
|
ElMessage
|
||||||
} from 'element-plus'
|
} from 'element-plus'
|
||||||
import { CopyDocument } from '@element-plus/icons-vue'
|
import { CopyDocument } from '@element-plus/icons-vue'
|
||||||
|
import type { AssetPollingStatus } from '@/types/api'
|
||||||
import { useAssetFormatters } from '../composables/useAssetFormatters'
|
import { useAssetFormatters } from '../composables/useAssetFormatters'
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
@@ -551,6 +588,7 @@
|
|||||||
last_data_check_at?: string | null
|
last_data_check_at?: string | null
|
||||||
last_real_name_check_at?: string | null
|
last_real_name_check_at?: string | null
|
||||||
last_card_status_check_at?: string | null
|
last_card_status_check_at?: string | null
|
||||||
|
polling?: AssetPollingStatus | null
|
||||||
bound_device_id?: number
|
bound_device_id?: number
|
||||||
bound_device_no?: string
|
bound_device_no?: string
|
||||||
bound_device_name?: string
|
bound_device_name?: string
|
||||||
@@ -685,6 +723,7 @@
|
|||||||
(e: 'updateBindingCardRealnameStatus', payload: { card: BindingCard }): void
|
(e: 'updateBindingCardRealnameStatus', payload: { card: BindingCard }): void
|
||||||
(e: 'navigateToCard', iccid: string): void
|
(e: 'navigateToCard', iccid: string): void
|
||||||
(e: 'navigateToDevice', deviceNo: string): void
|
(e: 'navigateToDevice', deviceNo: string): void
|
||||||
|
(e: 'viewSyncTrail'): void
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits<Emits>()
|
const emit = defineEmits<Emits>()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { AssetService } from '@/api/modules'
|
import { AssetService } from '@/api/modules'
|
||||||
import { formatRemainingTime, FrontendRateLimitError } from '@/utils/business/apiRateLimit'
|
import { FrontendRateLimitError } from '@/utils/business/apiRateLimit'
|
||||||
import type {
|
import type {
|
||||||
AssetBoundCard,
|
AssetBoundCard,
|
||||||
AssetWalletResponse,
|
AssetWalletResponse,
|
||||||
@@ -123,6 +123,7 @@ export function useAssetInfo() {
|
|||||||
updated_at: data.updated_at,
|
updated_at: data.updated_at,
|
||||||
accumulated_recharge: data.accumulated_recharge,
|
accumulated_recharge: data.accumulated_recharge,
|
||||||
first_commission_paid: data.first_commission_paid,
|
first_commission_paid: data.first_commission_paid,
|
||||||
|
polling: data.polling ?? null,
|
||||||
|
|
||||||
// 卡专属字段 (asset_type === 'card')
|
// 卡专属字段 (asset_type === 'card')
|
||||||
iccid: data.iccid || '',
|
iccid: data.iccid || '',
|
||||||
@@ -430,11 +431,11 @@ export function useAssetInfo() {
|
|||||||
return true
|
return true
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error instanceof FrontendRateLimitError) {
|
if (error instanceof FrontendRateLimitError) {
|
||||||
ElMessage.warning(`操作过于频繁,请${formatRemainingTime(error.remainingMs)}后再试`)
|
ElMessage.warning('操作过于频繁,请稍后重试')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (error?.response?.status === 429) {
|
if (error?.response?.status === 429) {
|
||||||
ElMessage.warning('操作过于频繁,请5分钟后再试')
|
ElMessage.warning('操作过于频繁,请稍后重试')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
ref="assetSearchCardRef"
|
ref="assetSearchCardRef"
|
||||||
:has-card-info="!!cardInfo"
|
:has-card-info="!!cardInfo"
|
||||||
:refresh-disabled="refreshDisabled"
|
:refresh-disabled="refreshDisabled"
|
||||||
:refresh-remaining-text="refreshRemainingText"
|
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@refresh="handleRefresh"
|
@refresh="handleRefresh"
|
||||||
/>
|
/>
|
||||||
@@ -43,6 +42,7 @@
|
|||||||
@update-binding-card-realname-status="handleUpdateBindingCardRealnameStatus"
|
@update-binding-card-realname-status="handleUpdateBindingCardRealnameStatus"
|
||||||
@navigate-to-card="handleNavigateToCard"
|
@navigate-to-card="handleNavigateToCard"
|
||||||
@navigate-to-device="handleNavigateToDevice"
|
@navigate-to-device="handleNavigateToDevice"
|
||||||
|
@view-sync-trail="handleViewSyncTrail"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -332,26 +332,7 @@
|
|||||||
const refreshSubmitting = ref(false)
|
const refreshSubmitting = ref(false)
|
||||||
let rateLimitTimer: ReturnType<typeof setInterval> | undefined
|
let rateLimitTimer: ReturnType<typeof setInterval> | undefined
|
||||||
|
|
||||||
const refreshRateLimitState = computed(() => {
|
const refreshDisabled = computed(() => refreshSubmitting.value)
|
||||||
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 startRateLimitState = computed(() => {
|
const startRateLimitState = computed(() => {
|
||||||
const identifier = cardInfo.value?.iccid || cardInfo.value?.identifier
|
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卡操作 - 启用卡
|
* IoT卡操作 - 启用卡
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
* 资产信息页面类型定义
|
* 资产信息页面类型定义
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { AssetPollingStatus } from '@/types/api'
|
||||||
|
|
||||||
// 资产类型枚举
|
// 资产类型枚举
|
||||||
export type AssetType = 'card' | 'device'
|
export type AssetType = 'card' | 'device'
|
||||||
|
|
||||||
@@ -37,6 +39,7 @@ export interface AssetInfo {
|
|||||||
last_data_check_at?: string | null
|
last_data_check_at?: string | null
|
||||||
last_real_name_check_at?: string | null
|
last_real_name_check_at?: string | null
|
||||||
last_card_status_check_at?: string | null
|
last_card_status_check_at?: string | null
|
||||||
|
polling?: AssetPollingStatus | null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设备绑定卡信息
|
// 设备绑定卡信息
|
||||||
|
|||||||
Reference in New Issue
Block a user