This commit is contained in:
289
scripts/tests/assetBindingCardRender.test.mjs
Normal file
289
scripts/tests/assetBindingCardRender.test.mjs
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import ts from 'typescript'
|
||||||
|
import * as vue from 'vue'
|
||||||
|
import { compileScript, parse } from '@vue/compiler-sfc'
|
||||||
|
|
||||||
|
function evaluate(source, imports) {
|
||||||
|
const compiled = ts.transpileModule(source, {
|
||||||
|
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
|
||||||
|
}).outputText
|
||||||
|
const module = { exports: {} }
|
||||||
|
new Function('require', 'module', 'exports', compiled)(
|
||||||
|
(name) => {
|
||||||
|
assert.ok(imports[name], `Unexpected runtime dependency: ${name}`)
|
||||||
|
return imports[name]
|
||||||
|
},
|
||||||
|
module,
|
||||||
|
module.exports
|
||||||
|
)
|
||||||
|
return module.exports
|
||||||
|
}
|
||||||
|
|
||||||
|
const basePath = 'src/views/asset-management/asset-information'
|
||||||
|
const { descriptor } = parse(readFileSync(`${basePath}/components/BasicInfoCard.vue`, 'utf8'))
|
||||||
|
const componentSource = compileScript(descriptor, {
|
||||||
|
id: 'binding-card-test',
|
||||||
|
inlineTemplate: true
|
||||||
|
}).content
|
||||||
|
const formatters = evaluate(readFileSync(`${basePath}/composables/useAssetFormatters.ts`, 'utf8'), {
|
||||||
|
'@/config/constants/carrierTypes': {
|
||||||
|
getCarrierTypeLabel: (code) => ({ CTCC: '中国电信', CMCC: '中国移动' })[code] || code
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function createHostNode(tag, text = '') {
|
||||||
|
return {
|
||||||
|
tag,
|
||||||
|
text,
|
||||||
|
props: {},
|
||||||
|
children: [],
|
||||||
|
parentNode: null,
|
||||||
|
removeChild(child) {
|
||||||
|
const index = this.children.indexOf(child)
|
||||||
|
assert.notEqual(index, -1)
|
||||||
|
this.children.splice(index, 1)
|
||||||
|
child.parentNode = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountBasicInfo(t, userType, permissions = []) {
|
||||||
|
const user = vue.reactive({ info: { id: userType, user_type: userType }, permissions })
|
||||||
|
const hasAuth = (permission) =>
|
||||||
|
Number(user.info.user_type) === 1 || user.permissions.includes(permission)
|
||||||
|
user.hasPermission = hasAuth
|
||||||
|
user.hasButton = hasAuth
|
||||||
|
const elements = Object.fromEntries(
|
||||||
|
[
|
||||||
|
'ElCard',
|
||||||
|
'ElDescriptions',
|
||||||
|
'ElDescriptionsItem',
|
||||||
|
'ElTag',
|
||||||
|
'ElTooltip',
|
||||||
|
'ElSwitch',
|
||||||
|
'ElDivider',
|
||||||
|
'ElButton',
|
||||||
|
'ElAlert',
|
||||||
|
'ElEmpty'
|
||||||
|
].map((name) => [
|
||||||
|
name,
|
||||||
|
vue.defineComponent({
|
||||||
|
name,
|
||||||
|
setup:
|
||||||
|
(_, { slots, attrs }) =>
|
||||||
|
() =>
|
||||||
|
vue.h(name, attrs, [slots.header?.(), slots.default?.()])
|
||||||
|
})
|
||||||
|
])
|
||||||
|
)
|
||||||
|
const component = evaluate(componentSource, {
|
||||||
|
vue,
|
||||||
|
'@vueuse/core': { useWindowSize: () => ({ width: vue.ref(1200) }) },
|
||||||
|
'element-plus': { ...elements, ElMessageBox: {}, ElMessage: {} },
|
||||||
|
'@element-plus/icons-vue': {
|
||||||
|
CopyDocument: vue.defineComponent(() => () => vue.h('copy-icon'))
|
||||||
|
},
|
||||||
|
'../composables/useAssetFormatters': formatters,
|
||||||
|
'@/composables/useAuth': { useAuth: () => ({ hasAuth }) },
|
||||||
|
'@/store/modules/user': { useUserStore: () => user },
|
||||||
|
'@/config/constants': {
|
||||||
|
JULY_PERMISSIONS: { speedTier: { view: 'speed:view', update: 'speed:update' } }
|
||||||
|
},
|
||||||
|
'@/utils/business/format': { formatDateTime: (value) => String(value || '') },
|
||||||
|
'@/utils/business/realNameTime': { formatRealNameTime: (value) => String(value || '-') },
|
||||||
|
'@/utils/business/expiryEstimate': {
|
||||||
|
getExpiryEstimateText: () => '-',
|
||||||
|
getExpiryEstimateTooltip: () => '',
|
||||||
|
getExpiryEstimateClass: () => ''
|
||||||
|
},
|
||||||
|
'@/config/constants/audit': { AUDIT_PERMISSIONS: {} },
|
||||||
|
'@/utils/business/auditNavigation': { resolveAuditResourceTarget: () => null },
|
||||||
|
'@/components/business/audit/investigationController': { openAuditInvestigation: () => {} }
|
||||||
|
}).default
|
||||||
|
const renderer = vue.createRenderer({
|
||||||
|
createElement: (tag) => createHostNode(tag),
|
||||||
|
createText: (text) => createHostNode('#text', text),
|
||||||
|
createComment: (text) => createHostNode('#comment', text),
|
||||||
|
setText: (node, text) => {
|
||||||
|
node.text = text
|
||||||
|
},
|
||||||
|
setElementText: (node, text) => {
|
||||||
|
node.text = text
|
||||||
|
node.children = []
|
||||||
|
},
|
||||||
|
parentNode: (node) => node.parentNode,
|
||||||
|
nextSibling: (node) => {
|
||||||
|
const siblings = node.parentNode?.children || []
|
||||||
|
return siblings[siblings.indexOf(node) + 1] || null
|
||||||
|
},
|
||||||
|
patchProp: (node, key, _, value) => {
|
||||||
|
node.props[key] = value
|
||||||
|
},
|
||||||
|
insert: (node, parent, anchor) => {
|
||||||
|
node.parentNode?.removeChild(node)
|
||||||
|
const index = anchor ? parent.children.indexOf(anchor) : parent.children.length
|
||||||
|
assert.notEqual(index, -1, 'Vue tried to insert beside a node removed outside its renderer')
|
||||||
|
parent.children.splice(index, 0, node)
|
||||||
|
node.parentNode = parent
|
||||||
|
},
|
||||||
|
remove: (node) => node.parentNode?.removeChild(node)
|
||||||
|
})
|
||||||
|
const asset = vue.reactive({
|
||||||
|
asset_type: 'device',
|
||||||
|
asset_id: 101,
|
||||||
|
identifier: 'DEVICE-A',
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
iccid: 'CARD-1',
|
||||||
|
msisdn: '13800000001',
|
||||||
|
carrier_name: '平台运营商账户',
|
||||||
|
carrier_type: 'CTCC',
|
||||||
|
slot_position: 1,
|
||||||
|
is_current: true,
|
||||||
|
network_status: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iccid: 'CARD-2',
|
||||||
|
msisdn: '13800000002',
|
||||||
|
carrier_name: '第二运营商账户',
|
||||||
|
carrier_type: 'CMCC',
|
||||||
|
slot_position: 2,
|
||||||
|
is_current: false,
|
||||||
|
network_status: 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
const root = createHostNode('root')
|
||||||
|
const app = renderer.createApp({
|
||||||
|
render: () => vue.h(component, { cardInfo: asset, deviceRealtime: null, pollingEnabled: false })
|
||||||
|
})
|
||||||
|
const directives = evaluate(readFileSync('src/directives/permission.ts', 'utf8'), {
|
||||||
|
'@/store/modules/user': { useUserStore: () => user }
|
||||||
|
})
|
||||||
|
directives.setupPermissionDirective(app)
|
||||||
|
app.directive('loading', {})
|
||||||
|
app.component(
|
||||||
|
'el-icon',
|
||||||
|
vue.defineComponent({
|
||||||
|
setup:
|
||||||
|
(_, { slots }) =>
|
||||||
|
() =>
|
||||||
|
vue.h('icon', slots.default?.())
|
||||||
|
})
|
||||||
|
)
|
||||||
|
app.mount(root)
|
||||||
|
t.after(() => app.unmount())
|
||||||
|
return { root, asset, user }
|
||||||
|
}
|
||||||
|
|
||||||
|
const textOf = (node) =>
|
||||||
|
node.tag === '#comment' ? '' : node.text + node.children.map(textOf).join('')
|
||||||
|
const nodesWithClass = (node, className) => [
|
||||||
|
...(String(node.props.class || '')
|
||||||
|
.split(' ')
|
||||||
|
.includes(className)
|
||||||
|
? [node]
|
||||||
|
: []),
|
||||||
|
...node.children.flatMap((child) => nodesWithClass(child, className))
|
||||||
|
]
|
||||||
|
|
||||||
|
test('all account types render both binding cards; only the established carrier label rule differs', async (t) => {
|
||||||
|
for (const userType of [1, 2, 3, 4]) {
|
||||||
|
const { root } = mountBasicInfo(t, userType)
|
||||||
|
await vue.nextTick()
|
||||||
|
const cards = nodesWithClass(root, 'binding-card')
|
||||||
|
assert.equal(cards.length, 2)
|
||||||
|
assert.match(textOf(cards[0]), /CARD-1/)
|
||||||
|
assert.match(textOf(cards[1]), /CARD-2/)
|
||||||
|
assert.match(textOf(cards[0]), /13800000001/)
|
||||||
|
assert.match(textOf(cards[0]), userType < 3 ? /平台运营商账户/ : /中国电信/)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('permission changes and current-card/status updates preserve card data without external DOM removal', async (t) => {
|
||||||
|
const { root, asset, user } = mountBasicInfo(t, 3, ['iot_info:stop'])
|
||||||
|
await vue.nextTick()
|
||||||
|
assert.match(textOf(nodesWithClass(root, 'binding-card-actions')[0]), /停用此卡/)
|
||||||
|
user.permissions = ['iot_info:start', 'bound_card:update_realname_status']
|
||||||
|
asset.cards[0].network_status = 0
|
||||||
|
asset.cards[0].is_current = false
|
||||||
|
asset.cards[1].is_current = true
|
||||||
|
await vue.nextTick()
|
||||||
|
const cards = nodesWithClass(root, 'binding-card')
|
||||||
|
assert.equal(cards.length, 2)
|
||||||
|
assert.match(textOf(cards[0]), /CARD-2/)
|
||||||
|
assert.match(textOf(cards[1]), /CARD-1/)
|
||||||
|
assert.match(textOf(nodesWithClass(cards[1], 'binding-card-actions')[0]), /启用此卡/)
|
||||||
|
assert.match(textOf(nodesWithClass(cards[1], 'binding-card-actions')[0]), /更新实名状态/)
|
||||||
|
assert.doesNotMatch(textOf(nodesWithClass(cards[1], 'binding-card-actions')[0]), /停用此卡/)
|
||||||
|
user.permissions = []
|
||||||
|
await vue.nextTick()
|
||||||
|
assert.equal(nodesWithClass(root, 'binding-card').length, 2)
|
||||||
|
for (const actions of nodesWithClass(root, 'binding-card-actions'))
|
||||||
|
assert.equal(textOf(actions).trim(), '')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('missing network status renders unknown and exposes neither start nor stop', async (t) => {
|
||||||
|
const { root, asset } = mountBasicInfo(t, 3, ['iot_info:start', 'iot_info:stop'])
|
||||||
|
delete asset.cards[0].network_status
|
||||||
|
await vue.nextTick()
|
||||||
|
const card = nodesWithClass(root, 'binding-card')[0]
|
||||||
|
assert.match(textOf(card), /网络状态未知/)
|
||||||
|
assert.doesNotMatch(textOf(nodesWithClass(card, 'binding-card-actions')[0]), /启用此卡|停用此卡/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('read-only accounts render realtime start/stop transitions without losing binding-card nodes', async (t) => {
|
||||||
|
for (const userType of [2, 3, 4]) {
|
||||||
|
const { root, asset } = mountBasicInfo(t, userType)
|
||||||
|
asset.cards[0].network_status = 0
|
||||||
|
await vue.nextTick()
|
||||||
|
assert.equal(nodesWithClass(root, 'binding-card').length, 2)
|
||||||
|
assert.match(textOf(nodesWithClass(root, 'binding-card')[0]), /网络状态停机/)
|
||||||
|
asset.cards[0].network_status = 1
|
||||||
|
await vue.nextTick()
|
||||||
|
assert.match(textOf(nodesWithClass(root, 'binding-card')[0]), /网络状态正常/)
|
||||||
|
for (const actions of nodesWithClass(root, 'binding-card-actions')) {
|
||||||
|
assert.equal(textOf(actions).trim(), '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('current-first sorting does not mutate source cards; zero, one, and multiple bindings render stably', async (t) => {
|
||||||
|
const { root, asset } = mountBasicInfo(t, 2)
|
||||||
|
const originalOrder = asset.cards.map((card) => card.iccid)
|
||||||
|
asset.cards[0].is_current = false
|
||||||
|
asset.cards[1].is_current = true
|
||||||
|
await vue.nextTick()
|
||||||
|
assert.deepEqual(
|
||||||
|
asset.cards.map((card) => card.iccid),
|
||||||
|
originalOrder
|
||||||
|
)
|
||||||
|
assert.match(textOf(nodesWithClass(root, 'binding-card')[0]), /CARD-2/)
|
||||||
|
asset.cards = [asset.cards[1]]
|
||||||
|
await vue.nextTick()
|
||||||
|
assert.equal(nodesWithClass(root, 'binding-card').length, 1)
|
||||||
|
asset.cards = []
|
||||||
|
await vue.nextTick()
|
||||||
|
assert.equal(nodesWithClass(root, 'binding-card').length, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('device/card type switches and denied actions do not leave removed DOM anchors', async (t) => {
|
||||||
|
const { root, asset, user } = mountBasicInfo(t, 3)
|
||||||
|
asset.asset_type = 'card'
|
||||||
|
asset.iccid = 'CARD-1'
|
||||||
|
asset.network_status = 1
|
||||||
|
await vue.nextTick()
|
||||||
|
asset.network_status = 0
|
||||||
|
await vue.nextTick()
|
||||||
|
assert.equal(textOf(nodesWithClass(root, 'card-operations')[0]).trim(), '')
|
||||||
|
user.permissions = ['iot_info:start']
|
||||||
|
await vue.nextTick()
|
||||||
|
assert.match(textOf(nodesWithClass(root, 'card-operations')[0]), /启用此卡/)
|
||||||
|
asset.asset_type = 'device'
|
||||||
|
user.permissions = ['device_info:reboot']
|
||||||
|
await vue.nextTick()
|
||||||
|
assert.equal(nodesWithClass(root, 'binding-card').length, 2)
|
||||||
|
assert.match(textOf(nodesWithClass(root, 'device-operations')[0]), /重启设备/)
|
||||||
|
})
|
||||||
744
scripts/tests/assetInfoState.test.mjs
Normal file
744
scripts/tests/assetInfoState.test.mjs
Normal file
@@ -0,0 +1,744 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import ts from 'typescript'
|
||||||
|
import * as vue from 'vue'
|
||||||
|
import { parse } from '@vue/compiler-sfc'
|
||||||
|
|
||||||
|
const sourcePath = 'src/views/asset-management/asset-information/composables/useAssetInfo.ts'
|
||||||
|
const compiledSource = ts.transpileModule(readFileSync(sourcePath, 'utf8'), {
|
||||||
|
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
|
||||||
|
}).outputText
|
||||||
|
|
||||||
|
const response = (data) => ({ code: 0, data })
|
||||||
|
const deferred = () => {
|
||||||
|
let resolve, reject
|
||||||
|
const promise = new Promise((resolvePromise, rejectPromise) => {
|
||||||
|
resolve = resolvePromise
|
||||||
|
reject = rejectPromise
|
||||||
|
})
|
||||||
|
return { promise, resolve, reject }
|
||||||
|
}
|
||||||
|
|
||||||
|
const boundCards = () => [
|
||||||
|
{
|
||||||
|
card_id: 11,
|
||||||
|
iccid: 'CARD-1',
|
||||||
|
msisdn: '13800000001',
|
||||||
|
carrier_name: '运营商账户',
|
||||||
|
carrier_type: 'CTCC',
|
||||||
|
slot_position: 1,
|
||||||
|
is_current: true,
|
||||||
|
network_status: 1,
|
||||||
|
real_name_status: 1,
|
||||||
|
real_name_at: '2026-10-01T01:00:00Z',
|
||||||
|
realname_policy: 'before_order',
|
||||||
|
gateway_extend: 'old reason'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
card_id: 12,
|
||||||
|
iccid: 'CARD-2',
|
||||||
|
msisdn: '13800000002',
|
||||||
|
carrier_name: '另一运营商账户',
|
||||||
|
carrier_type: 'CMCC',
|
||||||
|
slot_position: 2,
|
||||||
|
is_current: false,
|
||||||
|
network_status: 0,
|
||||||
|
real_name_status: 0,
|
||||||
|
real_name_at: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const detail = (identifier = 'DEVICE-A', cards = boundCards()) => ({
|
||||||
|
asset_type: 'device',
|
||||||
|
asset_id: identifier === 'DEVICE-A' ? 101 : 202,
|
||||||
|
virtual_no: identifier,
|
||||||
|
bound_card_count: cards.length,
|
||||||
|
cards,
|
||||||
|
real_total_mb: 1024,
|
||||||
|
real_used_mb: 100
|
||||||
|
})
|
||||||
|
|
||||||
|
function harness(t, overrides = {}) {
|
||||||
|
const user = vue.reactive({ info: { id: 1, user_type: 1 }, isLogin: true })
|
||||||
|
const messages = []
|
||||||
|
const api = {
|
||||||
|
resolveAsset: async (identifier) => response(detail(identifier)),
|
||||||
|
getRealtimeStatus: async () => response(null),
|
||||||
|
getCurrentPackage: async () => response(null),
|
||||||
|
getAssetWallet: async () => response(null),
|
||||||
|
getAssetPackages: async () => response({ items: [], total: 0 }),
|
||||||
|
refreshAsset: async () => response(null),
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
const imports = {
|
||||||
|
vue,
|
||||||
|
'element-plus': {
|
||||||
|
ElMessage: Object.fromEntries(
|
||||||
|
['error', 'success', 'warning'].map((type) => [
|
||||||
|
type,
|
||||||
|
(message) => messages.push({ type, message })
|
||||||
|
])
|
||||||
|
)
|
||||||
|
},
|
||||||
|
'@/api/modules': { AssetService: api },
|
||||||
|
'@/utils/business/apiRateLimit': { FrontendRateLimitError: class extends Error {} },
|
||||||
|
'@/utils/business/apiError': {
|
||||||
|
normalizeApiError: (error) => ({ message: error?.message || 'unknown error' })
|
||||||
|
},
|
||||||
|
'@/store/modules/user': { useUserStore: () => user }
|
||||||
|
}
|
||||||
|
const module = { exports: {} }
|
||||||
|
new Function('require', 'module', 'exports', compiledSource)(
|
||||||
|
(name) => {
|
||||||
|
assert.ok(imports[name], `Unexpected runtime dependency: ${name}`)
|
||||||
|
return imports[name]
|
||||||
|
},
|
||||||
|
module,
|
||||||
|
module.exports
|
||||||
|
)
|
||||||
|
const scope = vue.effectScope()
|
||||||
|
const state = scope.run(() => module.exports.useAssetInfo())
|
||||||
|
t.after(() => scope.stop())
|
||||||
|
return { state, api, user, messages, scope }
|
||||||
|
}
|
||||||
|
|
||||||
|
test('partial realtime responses retain the complete resolved binding-card roster', async (t) => {
|
||||||
|
const { state } = harness(t, {
|
||||||
|
getRealtimeStatus: async () =>
|
||||||
|
response({
|
||||||
|
asset_type: 'device',
|
||||||
|
asset_id: 101,
|
||||||
|
cards: [{ iccid: 'CARD-1', network_status: 0 }]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
assert.deepEqual(
|
||||||
|
state.cardInfo.value.cards.map((card) => card.iccid),
|
||||||
|
['CARD-1', 'CARD-2']
|
||||||
|
)
|
||||||
|
assert.equal(state.cardInfo.value.cards[0].network_status, 0)
|
||||||
|
assert.equal(state.cardInfo.value.cards[1].msisdn, '13800000002')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('realtime fields cannot erase resolved card identity, carrier, slot, policy, or real-name time', async (t) => {
|
||||||
|
const { state } = harness(t, {
|
||||||
|
getRealtimeStatus: async () =>
|
||||||
|
response({
|
||||||
|
asset_type: 'device',
|
||||||
|
asset_id: 101,
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
iccid: 'CARD-1',
|
||||||
|
card_id: 0,
|
||||||
|
msisdn: '',
|
||||||
|
carrier_name: '',
|
||||||
|
carrier_type: '',
|
||||||
|
slot_position: 0,
|
||||||
|
realname_policy: '',
|
||||||
|
real_name_at: 'untrusted realtime time',
|
||||||
|
network_status: 0,
|
||||||
|
real_name_status: 0,
|
||||||
|
is_current: false,
|
||||||
|
gateway_extend: ''
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
assert.deepEqual(state.cardInfo.value.cards[0], {
|
||||||
|
...boundCards()[0],
|
||||||
|
network_status: 0,
|
||||||
|
real_name_status: 0,
|
||||||
|
is_current: false,
|
||||||
|
gateway_extend: ''
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('empty or omitted realtime cards preserve the detail roster; detail reload can remove bindings', async (t) => {
|
||||||
|
const { state, api } = harness(t)
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
for (const cards of [[], undefined, null]) {
|
||||||
|
api.getRealtimeStatus = async () => response({ asset_type: 'device', asset_id: 101, cards })
|
||||||
|
await state.loadRealtimeStatus('DEVICE-A', 'device')
|
||||||
|
assert.equal(state.cardInfo.value.cards.length, 2)
|
||||||
|
}
|
||||||
|
api.resolveAsset = async () => response(detail('DEVICE-A', []))
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
assert.deepEqual(state.cardInfo.value.cards, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unknown realtime cards do not introduce bindings absent from the authorized detail response', async (t) => {
|
||||||
|
const { state } = harness(t, {
|
||||||
|
getRealtimeStatus: async () =>
|
||||||
|
response({ asset_type: 'device', asset_id: 101, cards: [{ iccid: 'UNRESOLVED-CARD' }] })
|
||||||
|
})
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
assert.deepEqual(
|
||||||
|
state.cardInfo.value.cards.map((card) => card.iccid),
|
||||||
|
['CARD-1', 'CARD-2']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('realtime card order does not change the resolved roster; explicit nullable timestamps can clear', async (t) => {
|
||||||
|
const cards = boundCards()
|
||||||
|
cards[0].last_data_check_at = 'old timestamp'
|
||||||
|
const { state } = harness(t, {
|
||||||
|
resolveAsset: async () => response(detail('DEVICE-A', cards)),
|
||||||
|
getRealtimeStatus: async () =>
|
||||||
|
response({
|
||||||
|
asset_type: 'device',
|
||||||
|
asset_id: 101,
|
||||||
|
cards: [
|
||||||
|
{ iccid: 'CARD-2' },
|
||||||
|
{ iccid: 'CARD-1', last_data_check_at: null, real_name_status: null }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
assert.deepEqual(
|
||||||
|
state.cardInfo.value.cards.map((card) => card.iccid),
|
||||||
|
['CARD-1', 'CARD-2']
|
||||||
|
)
|
||||||
|
assert.equal(state.cardInfo.value.cards[0].last_data_check_at, null)
|
||||||
|
assert.equal(state.cardInfo.value.cards[0].real_name_status, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('a late resolve response cannot replace a newer search', async (t) => {
|
||||||
|
const oldDetail = deferred()
|
||||||
|
const { state } = harness(t, {
|
||||||
|
resolveAsset: (identifier) =>
|
||||||
|
identifier === 'DEVICE-A' ? oldDetail.promise : Promise.resolve(response(detail(identifier)))
|
||||||
|
})
|
||||||
|
const oldSearch = state.fetchAssetDetail('DEVICE-A')
|
||||||
|
await state.fetchAssetDetail('DEVICE-B')
|
||||||
|
oldDetail.resolve(response(detail('DEVICE-A')))
|
||||||
|
await oldSearch
|
||||||
|
assert.equal(state.cardInfo.value.identifier, 'DEVICE-B')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('late realtime responses cannot write old card data into a different asset', async (t) => {
|
||||||
|
const { state, api } = harness(t)
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
const oldRealtime = deferred()
|
||||||
|
api.getRealtimeStatus = () => oldRealtime.promise
|
||||||
|
const oldLoad = state.loadRealtimeStatus('DEVICE-A', 'device')
|
||||||
|
api.getRealtimeStatus = async () => response(null)
|
||||||
|
await state.fetchAssetDetail('DEVICE-B')
|
||||||
|
oldRealtime.resolve(
|
||||||
|
response({ asset_type: 'device', asset_id: 101, cards: [{ iccid: 'OLD-CARD' }] })
|
||||||
|
)
|
||||||
|
await oldLoad
|
||||||
|
assert.equal(state.cardInfo.value.identifier, 'DEVICE-B')
|
||||||
|
assert.deepEqual(
|
||||||
|
state.cardInfo.value.cards.map((card) => card.iccid),
|
||||||
|
['CARD-1', 'CARD-2']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('the newest same-asset realtime request wins and stale completion cannot end its loading state', async (t) => {
|
||||||
|
const { state, api } = harness(t)
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
const older = deferred()
|
||||||
|
const newer = deferred()
|
||||||
|
api.getRealtimeStatus = () => older.promise
|
||||||
|
const oldLoad = state.loadRealtimeStatus('DEVICE-A', 'device')
|
||||||
|
api.getRealtimeStatus = () => newer.promise
|
||||||
|
const newLoad = state.loadRealtimeStatus('DEVICE-A', 'device')
|
||||||
|
older.resolve(
|
||||||
|
response({
|
||||||
|
asset_type: 'device',
|
||||||
|
asset_id: 101,
|
||||||
|
cards: [{ iccid: 'CARD-1', network_status: 0 }]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
await oldLoad
|
||||||
|
assert.equal(state.realtimeLoading.value, true)
|
||||||
|
assert.equal(state.cardInfo.value.cards[0].network_status, 1)
|
||||||
|
newer.resolve(
|
||||||
|
response({
|
||||||
|
asset_type: 'device',
|
||||||
|
asset_id: 101,
|
||||||
|
cards: [{ iccid: 'CARD-1', network_status: 1 }]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
await newLoad
|
||||||
|
assert.equal(state.realtimeLoading.value, false)
|
||||||
|
assert.equal(state.cardInfo.value.cards[0].network_status, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('realtime responses identifying a different asset are ignored', async (t) => {
|
||||||
|
const { state, api } = harness(t)
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
for (const identity of [
|
||||||
|
{ asset_type: 'device', asset_id: 202 },
|
||||||
|
{ asset_type: 'card', asset_id: 101 }
|
||||||
|
]) {
|
||||||
|
api.getRealtimeStatus = async () =>
|
||||||
|
response({ ...identity, cards: [{ iccid: 'CARD-1', network_status: 0 }] })
|
||||||
|
await state.loadRealtimeStatus('DEVICE-A', 'device')
|
||||||
|
assert.equal(state.cardInfo.value.cards[0].network_status, 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('starting a new search clears previous device, package, wallet, and binding data immediately', async (t) => {
|
||||||
|
const { state, api } = harness(t)
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
state.deviceRealtime.value = { current_iccid: 'CARD-1' }
|
||||||
|
state.currentPackage.value = { package_name: 'old package' }
|
||||||
|
state.packageList.value = [{ id: 1 }]
|
||||||
|
state.walletInfo.value = { total_balance: 123 }
|
||||||
|
const newDetail = deferred()
|
||||||
|
api.resolveAsset = () => newDetail.promise
|
||||||
|
const nextSearch = state.fetchAssetDetail('DEVICE-B')
|
||||||
|
assert.equal(state.cardInfo.value, null)
|
||||||
|
assert.equal(state.deviceRealtime.value, null)
|
||||||
|
assert.equal(state.currentPackage.value, null)
|
||||||
|
assert.equal(state.walletInfo.value, null)
|
||||||
|
assert.deepEqual(state.packageList.value, [])
|
||||||
|
newDetail.resolve({ code: 403, msg: 'forbidden' })
|
||||||
|
await nextSearch
|
||||||
|
assert.equal(state.cardInfo.value, null)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('changing account invalidates pending requests even when searching the same identifier', async (t) => {
|
||||||
|
const oldDetail = deferred()
|
||||||
|
const { state, user, api } = harness(t, { resolveAsset: () => oldDetail.promise })
|
||||||
|
const oldSearch = state.fetchAssetDetail('DEVICE-A')
|
||||||
|
user.info = { id: 2, user_type: 3 }
|
||||||
|
api.resolveAsset = async () => response(detail('DEVICE-A', [boundCards()[1]]))
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
oldDetail.resolve(response(detail('DEVICE-A')))
|
||||||
|
await oldSearch
|
||||||
|
assert.deepEqual(
|
||||||
|
state.cardInfo.value.cards.map((card) => card.iccid),
|
||||||
|
['CARD-2']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('logout clears visible data and invalidates pending realtime responses', async (t) => {
|
||||||
|
const { state, user, api } = harness(t)
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
const pending = deferred()
|
||||||
|
api.getRealtimeStatus = () => pending.promise
|
||||||
|
const load = state.loadRealtimeStatus('DEVICE-A', 'device')
|
||||||
|
user.isLogin = false
|
||||||
|
assert.equal(state.cardInfo.value, null)
|
||||||
|
pending.resolve(response({ asset_type: 'device', asset_id: 101, cards: boundCards() }))
|
||||||
|
await load
|
||||||
|
assert.equal(state.cardInfo.value, null)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('a late failure of an old search does not clear the current successful asset', async (t) => {
|
||||||
|
const oldDetail = deferred()
|
||||||
|
const { state } = harness(t, {
|
||||||
|
resolveAsset: (identifier) =>
|
||||||
|
identifier === 'DEVICE-A' ? oldDetail.promise : Promise.resolve(response(detail(identifier)))
|
||||||
|
})
|
||||||
|
const oldSearch = state.fetchAssetDetail('DEVICE-A')
|
||||||
|
await state.fetchAssetDetail('DEVICE-B')
|
||||||
|
oldDetail.reject(new Error('old request failed'))
|
||||||
|
await oldSearch
|
||||||
|
assert.equal(state.cardInfo.value?.identifier, 'DEVICE-B')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('account types 1/2/3/4 retain the same resolved bindings despite sparse realtime fields', async (t) => {
|
||||||
|
for (const userType of [1, 2, 3, 4]) {
|
||||||
|
const { state, user } = harness(t, {
|
||||||
|
getRealtimeStatus: async () =>
|
||||||
|
response({
|
||||||
|
asset_type: 'device',
|
||||||
|
asset_id: 101,
|
||||||
|
cards: [{ iccid: 'CARD-1', carrier_name: '' }]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
user.info = { id: userType, user_type: userType }
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
assert.deepEqual(state.cardInfo.value.cards, boundCards())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('a current-card change clears the previous current flag even for a partial realtime list', async (t) => {
|
||||||
|
const { state } = harness(t, {
|
||||||
|
getRealtimeStatus: async () =>
|
||||||
|
response({
|
||||||
|
asset_type: 'device',
|
||||||
|
asset_id: 101,
|
||||||
|
cards: [{ iccid: 'CARD-2', is_current: true }]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
assert.deepEqual(
|
||||||
|
state.cardInfo.value.cards.map((card) => card.is_current),
|
||||||
|
[false, true]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('same-identifier re-search never takes a real-name timestamp from a previous search or realtime', async (t) => {
|
||||||
|
const { state, api } = harness(t)
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
api.resolveAsset = async () =>
|
||||||
|
response(detail('DEVICE-A', [{ ...boundCards()[0], real_name_at: null }]))
|
||||||
|
api.getRealtimeStatus = async () =>
|
||||||
|
response({
|
||||||
|
asset_type: 'device',
|
||||||
|
asset_id: 101,
|
||||||
|
cards: [{ iccid: 'CARD-1', real_name_at: 'stale time' }]
|
||||||
|
})
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
assert.equal(state.cardInfo.value.cards[0].real_name_at, null)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('manual refresh resolves new binding relations and preserves the existing package page', async (t) => {
|
||||||
|
let resolveCalls = 0
|
||||||
|
const { state } = harness(t, {
|
||||||
|
resolveAsset: async () => response(detail('DEVICE-A', ++resolveCalls === 1 ? boundCards() : []))
|
||||||
|
})
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
state.packageList.value = [{ id: 88, children: [{ id: 89 }] }]
|
||||||
|
assert.equal(await state.refreshAsset('DEVICE-A', 'device'), true)
|
||||||
|
assert.equal(resolveCalls, 2)
|
||||||
|
assert.deepEqual(state.cardInfo.value.cards, [])
|
||||||
|
assert.deepEqual(state.packageList.value, [{ id: 88, children: [{ id: 89 }] }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('a pending refresh does not re-search or overwrite a newer asset', async (t) => {
|
||||||
|
const pending = deferred()
|
||||||
|
const calls = []
|
||||||
|
const { state } = harness(t, {
|
||||||
|
refreshAsset: () => pending.promise,
|
||||||
|
resolveAsset: async (identifier) => {
|
||||||
|
calls.push(identifier)
|
||||||
|
return response(detail(identifier))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
const refresh = state.refreshAsset('DEVICE-A', 'device')
|
||||||
|
await state.fetchAssetDetail('DEVICE-B')
|
||||||
|
pending.resolve(response(null))
|
||||||
|
assert.equal(await refresh, false)
|
||||||
|
assert.equal(state.cardInfo.value.identifier, 'DEVICE-B')
|
||||||
|
assert.deepEqual(calls, ['DEVICE-A', 'DEVICE-B'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('late current-package, wallet, and package-list responses do not contaminate a new asset', async (t) => {
|
||||||
|
const { state, api } = harness(t)
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
const pending = { package: deferred(), wallet: deferred(), list: deferred() }
|
||||||
|
api.getCurrentPackage = () => pending.package.promise
|
||||||
|
api.getAssetWallet = () => pending.wallet.promise
|
||||||
|
api.getAssetPackages = () => pending.list.promise
|
||||||
|
const oldLoads = [
|
||||||
|
state.loadCurrentPackage('DEVICE-A'),
|
||||||
|
state.loadAssetWallet('DEVICE-A'),
|
||||||
|
state.loadPackageList('DEVICE-A')
|
||||||
|
]
|
||||||
|
api.getCurrentPackage = async () => response(null)
|
||||||
|
api.getAssetWallet = async () => response(null)
|
||||||
|
await state.fetchAssetDetail('DEVICE-B')
|
||||||
|
pending.package.resolve(response({ package_name: 'old package' }))
|
||||||
|
pending.wallet.resolve(response({ total_balance: 123 }))
|
||||||
|
pending.list.resolve(response({ items: [{ id: 1 }], total: 1 }))
|
||||||
|
const results = await Promise.all(oldLoads)
|
||||||
|
assert.equal(state.currentPackage.value, null)
|
||||||
|
assert.equal(state.walletInfo.value, null)
|
||||||
|
assert.deepEqual(state.packageList.value, [])
|
||||||
|
assert.equal(results[2], null)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('a newer same-asset package page wins even if the older page returns last', async (t) => {
|
||||||
|
const { state, api } = harness(t)
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
const older = deferred()
|
||||||
|
api.getAssetPackages = () => older.promise
|
||||||
|
const firstPage = state.loadPackageList('DEVICE-A', 1)
|
||||||
|
api.getAssetPackages = async () =>
|
||||||
|
response({ items: [{ id: 2, children: [{ id: 3 }] }], total: 9, page: 2 })
|
||||||
|
await state.loadPackageList('DEVICE-A', 2)
|
||||||
|
older.resolve(response({ items: [{ id: 1 }], total: 1, page: 1 }))
|
||||||
|
assert.equal(await firstPage, null)
|
||||||
|
assert.deepEqual(state.packageList.value, [{ id: 2, children: [{ id: 3 }] }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unmount invalidates outstanding resolve requests', async (t) => {
|
||||||
|
const pending = deferred()
|
||||||
|
const { state, scope } = harness(t, { resolveAsset: () => pending.promise })
|
||||||
|
const search = state.fetchAssetDetail('DEVICE-A')
|
||||||
|
scope.stop()
|
||||||
|
pending.resolve(response(detail('DEVICE-A')))
|
||||||
|
assert.equal(await search, null)
|
||||||
|
assert.equal(state.cardInfo.value, null)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('failed realtime loading preserves resolved bindings and a sparse identified request can update status', async (t) => {
|
||||||
|
t.mock.method(console, 'error', () => {})
|
||||||
|
const { state, api } = harness(t, {
|
||||||
|
getRealtimeStatus: async () => {
|
||||||
|
throw new Error('realtime unavailable')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
assert.deepEqual(state.cardInfo.value.cards, boundCards())
|
||||||
|
assert.equal(state.realtimeLoading.value, false)
|
||||||
|
api.getRealtimeStatus = async () => response({ cards: [{ iccid: 'CARD-1', network_status: 0 }] })
|
||||||
|
await state.loadRealtimeStatus('DEVICE-A', 'device')
|
||||||
|
assert.equal(state.cardInfo.value.cards[0].network_status, 0)
|
||||||
|
assert.equal(state.cardInfo.value.cards.length, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('updating user metadata without changing the account or data scope does not clear asset data', async (t) => {
|
||||||
|
const { state, user } = harness(t)
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
const asset = state.cardInfo.value
|
||||||
|
user.info = { ...user.info, username: 'updated display name' }
|
||||||
|
await vue.nextTick()
|
||||||
|
assert.equal(state.cardInfo.value, asset)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('a disposed asset composable cannot restart loads after an operation finishes late', async (t) => {
|
||||||
|
let calls = 0
|
||||||
|
const { state, scope } = harness(t, {
|
||||||
|
resolveAsset: async () => {
|
||||||
|
calls += 1
|
||||||
|
return response(detail('DEVICE-A'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
scope.stop()
|
||||||
|
assert.equal(await state.fetchAssetDetail('DEVICE-A'), null)
|
||||||
|
assert.equal(calls, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('standalone card realtime status still updates while the resolved gateway IMEI remains authoritative', async (t) => {
|
||||||
|
const { state } = harness(t, {
|
||||||
|
resolveAsset: async () =>
|
||||||
|
response({
|
||||||
|
...detail('CARD-1', []),
|
||||||
|
asset_type: 'card',
|
||||||
|
iccid: 'CARD-1',
|
||||||
|
gateway_card_imei: 'resolved-imei'
|
||||||
|
}),
|
||||||
|
getRealtimeStatus: async () =>
|
||||||
|
response({
|
||||||
|
asset_type: 'card',
|
||||||
|
asset_id: 202,
|
||||||
|
network_status: 0,
|
||||||
|
real_name_status: 1,
|
||||||
|
gateway_card_imei: 'realtime-imei'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await state.fetchAssetDetail('CARD-1')
|
||||||
|
assert.equal(state.cardInfo.value.network_status, 0)
|
||||||
|
assert.equal(state.cardInfo.value.real_name_status, 1)
|
||||||
|
assert.equal(state.cardInfo.value.gateway_card_imei, 'resolved-imei')
|
||||||
|
})
|
||||||
|
|
||||||
|
function pageHandlers(state) {
|
||||||
|
const { descriptor } = parse(
|
||||||
|
readFileSync('src/views/asset-management/asset-information/index.vue', 'utf8')
|
||||||
|
)
|
||||||
|
const source = ts.createSourceFile(
|
||||||
|
'asset-information.ts',
|
||||||
|
descriptor.scriptSetup.content,
|
||||||
|
ts.ScriptTarget.Latest,
|
||||||
|
true,
|
||||||
|
ts.ScriptKind.TS
|
||||||
|
)
|
||||||
|
const wanted = new Set([
|
||||||
|
'handleSearch',
|
||||||
|
'handleRefresh',
|
||||||
|
'handleSpeedTierConfirm',
|
||||||
|
'showUpdateRealnameStatusDialog',
|
||||||
|
'handleUpdateRealnameStatusSuccess',
|
||||||
|
'handleUpdateBindingCardRealnameStatus',
|
||||||
|
'handleBindingCardRealnameStatusSuccess'
|
||||||
|
])
|
||||||
|
const declarations = []
|
||||||
|
function visit(node) {
|
||||||
|
if (ts.isVariableDeclaration(node) && wanted.has(node.name.getText(source)))
|
||||||
|
declarations.push(node.getText(source))
|
||||||
|
ts.forEachChild(node, visit)
|
||||||
|
}
|
||||||
|
visit(source)
|
||||||
|
assert.equal(declarations.length, wanted.size)
|
||||||
|
const bindings = {
|
||||||
|
cardInfo: state.cardInfo,
|
||||||
|
fetchAssetDetail: state.fetchAssetDetail,
|
||||||
|
loadPackageList: state.loadPackageList,
|
||||||
|
refreshAsset: state.refreshAsset,
|
||||||
|
refreshSubmitting: vue.ref(false),
|
||||||
|
refreshDisabled: vue.ref(false),
|
||||||
|
updateRateLimitNow: () => {},
|
||||||
|
loadRealtimeStatus: state.loadRealtimeStatus,
|
||||||
|
hasAuth: () => true,
|
||||||
|
JULY_PERMISSIONS: { speedTier: { update: 'speed:update' } },
|
||||||
|
CardService: {
|
||||||
|
setSpeedTier: async () => response({ speed_tier_name: 'test tier' })
|
||||||
|
},
|
||||||
|
ElMessage: { success: () => {}, error: () => {} },
|
||||||
|
normalizeApiError: (error) => ({ message: error?.message || 'unknown error' }),
|
||||||
|
speedLimitDialogVisible: vue.ref(false),
|
||||||
|
speedTierSubmitting: vue.ref(false),
|
||||||
|
assetSearchCardRef: vue.ref(null),
|
||||||
|
startFailureMessage: vue.ref(''),
|
||||||
|
packagePagination: vue.ref({ page: 1, total: 0, pageSize: 5, status: undefined }),
|
||||||
|
walletTransactionSearchToken: vue.ref(0),
|
||||||
|
pollingEnabled: vue.ref(false),
|
||||||
|
updateRealnameStatusDialogVisible: vue.ref(false),
|
||||||
|
bindingCardRealnameStatusDialogVisible: vue.ref(false),
|
||||||
|
bindingCardRealnameStatusIccid: vue.ref(''),
|
||||||
|
bindingCardRealnameStatusValue: vue.ref(0),
|
||||||
|
nextTick: vue.nextTick
|
||||||
|
}
|
||||||
|
const compiled = ts.transpileModule(
|
||||||
|
`let searchRequestId = 0, pollingInitializing = false, realnameStatusAsset = null, bindingCardRealnameStatusAsset = null\n${declarations.map((declaration) => `const ${declaration}`).join('\n')}\nreturn { ${[...wanted].join(', ')} }`,
|
||||||
|
{ compilerOptions: { target: ts.ScriptTarget.ES2022 } }
|
||||||
|
).outputText
|
||||||
|
return {
|
||||||
|
...new Function(...Object.keys(bindings), compiled)(...Object.values(bindings)),
|
||||||
|
...bindings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('page search resolves a device only once and uses its canonical identifier for related requests', async (t) => {
|
||||||
|
const calls = []
|
||||||
|
const { state } = harness(t, {
|
||||||
|
resolveAsset: async (identifier) => {
|
||||||
|
calls.push(['resolve', identifier])
|
||||||
|
return response(detail('DEVICE-A'))
|
||||||
|
},
|
||||||
|
getRealtimeStatus: async (identifier) => {
|
||||||
|
calls.push(['realtime', identifier])
|
||||||
|
return response(null)
|
||||||
|
},
|
||||||
|
getAssetPackages: async (identifier) => {
|
||||||
|
calls.push(['packages', identifier])
|
||||||
|
return response({ items: [], total: 0 })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await pageHandlers(state).handleSearch({ identifier: 'IMEI-ALIAS' })
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
['resolve', 'IMEI-ALIAS'],
|
||||||
|
['realtime', 'DEVICE-A'],
|
||||||
|
['packages', 'DEVICE-A']
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('active gateway synchronization is called only by the sync button, never by searches or data reloads', async (t) => {
|
||||||
|
const syncCalls = []
|
||||||
|
const { state } = harness(t, {
|
||||||
|
refreshAsset: async (identifier) => {
|
||||||
|
syncCalls.push(identifier)
|
||||||
|
return response(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const page = pageHandlers(state)
|
||||||
|
await page.handleRefresh()
|
||||||
|
await page.handleSearch({ identifier: 'DEVICE-A' })
|
||||||
|
await page.handleSearch({ identifier: 'DEVICE-A' })
|
||||||
|
await state.loadRealtimeStatus('DEVICE-A', 'device')
|
||||||
|
page.handleUpdateBindingCardRealnameStatus({ card: boundCards()[0] })
|
||||||
|
await page.handleBindingCardRealnameStatusSuccess()
|
||||||
|
page.showUpdateRealnameStatusDialog()
|
||||||
|
await page.handleUpdateRealnameStatusSuccess()
|
||||||
|
assert.deepEqual(syncCalls, [])
|
||||||
|
|
||||||
|
await page.handleRefresh()
|
||||||
|
assert.deepEqual(syncCalls, ['DEVICE-A'])
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
page.refreshDisabled.value = true
|
||||||
|
await page.handleRefresh()
|
||||||
|
assert.deepEqual(syncCalls, ['DEVICE-A'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('successful speed-tier updates reload saved status without calling active gateway synchronization', async (t) => {
|
||||||
|
const syncCalls = []
|
||||||
|
const realtimeCalls = []
|
||||||
|
const { state } = harness(t, {
|
||||||
|
resolveAsset: async () =>
|
||||||
|
response({ ...detail('CARD-1', []), asset_type: 'card', iccid: 'CARD-1' }),
|
||||||
|
refreshAsset: async (identifier) => {
|
||||||
|
syncCalls.push(identifier)
|
||||||
|
return response(null)
|
||||||
|
},
|
||||||
|
getRealtimeStatus: async (identifier) => {
|
||||||
|
realtimeCalls.push(identifier)
|
||||||
|
return response(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await state.fetchAssetDetail('CARD-1')
|
||||||
|
realtimeCalls.length = 0
|
||||||
|
await pageHandlers(state).handleSpeedTierConfirm({ code: 1 })
|
||||||
|
assert.deepEqual(syncCalls, [])
|
||||||
|
assert.deepEqual(realtimeCalls, ['CARD-1'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('main retains the legacy 19-digit ICCID fallback without triggering synchronization', async (t) => {
|
||||||
|
const identifiers = []
|
||||||
|
let syncCalls = 0
|
||||||
|
const { state } = harness(t, {
|
||||||
|
resolveAsset: async (identifier) => {
|
||||||
|
identifiers.push(identifier)
|
||||||
|
if (identifier.length === 19) return { code: 1006, msg: 'not found' }
|
||||||
|
return response({ ...detail('CARD-1', []), asset_type: 'card', iccid: identifier })
|
||||||
|
},
|
||||||
|
refreshAsset: async () => {
|
||||||
|
syncCalls += 1
|
||||||
|
return response(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await state.fetchAssetDetail('8986000000000000000')
|
||||||
|
assert.equal(identifiers.length, 2)
|
||||||
|
assert.equal(identifiers[0].length, 19)
|
||||||
|
assert.equal(identifiers[1].length, 20)
|
||||||
|
assert.equal(state.cardInfo.value.identifier, identifiers[1])
|
||||||
|
assert.equal(syncCalls, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('a stale page search cannot start additional loads or reset the newer search pagination', async (t) => {
|
||||||
|
const pending = deferred()
|
||||||
|
const { state } = harness(t, {
|
||||||
|
resolveAsset: (identifier) =>
|
||||||
|
identifier === 'DEVICE-A' ? pending.promise : Promise.resolve(response(detail(identifier))),
|
||||||
|
getAssetPackages: async () => response({ items: [{ id: 2 }], total: 22 })
|
||||||
|
})
|
||||||
|
const page = pageHandlers(state)
|
||||||
|
const older = page.handleSearch({ identifier: 'DEVICE-A' })
|
||||||
|
await page.handleSearch({ identifier: 'DEVICE-B' })
|
||||||
|
pending.resolve(response(detail('DEVICE-A')))
|
||||||
|
await older
|
||||||
|
assert.equal(page.packagePagination.value.total, 22)
|
||||||
|
assert.equal(page.walletTransactionSearchToken.value, 1)
|
||||||
|
assert.equal(state.cardInfo.value.identifier, 'DEVICE-B')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('successful bound-card real-name update reloads detail fields but an old dialog cannot reload a new asset', async (t) => {
|
||||||
|
let resolveCalls = 0
|
||||||
|
const { state, api } = harness(t, {
|
||||||
|
resolveAsset: async (identifier) => {
|
||||||
|
resolveCalls += 1
|
||||||
|
return response(detail(identifier))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await state.fetchAssetDetail('DEVICE-A')
|
||||||
|
const page = pageHandlers(state)
|
||||||
|
page.handleUpdateBindingCardRealnameStatus({ card: boundCards()[0] })
|
||||||
|
api.resolveAsset = async () => {
|
||||||
|
resolveCalls += 1
|
||||||
|
return response(
|
||||||
|
detail('DEVICE-A', [{ ...boundCards()[0], real_name_status: 0, real_name_at: null }])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await page.handleBindingCardRealnameStatusSuccess()
|
||||||
|
assert.equal(state.cardInfo.value.cards[0].real_name_status, 0)
|
||||||
|
assert.equal(state.cardInfo.value.cards[0].real_name_at, null)
|
||||||
|
assert.equal(resolveCalls, 2)
|
||||||
|
api.resolveAsset = async (identifier) => {
|
||||||
|
resolveCalls += 1
|
||||||
|
return response(detail(identifier))
|
||||||
|
}
|
||||||
|
page.handleUpdateBindingCardRealnameStatus({ card: boundCards()[0] })
|
||||||
|
await state.fetchAssetDetail('DEVICE-B')
|
||||||
|
await page.handleBindingCardRealnameStatusSuccess()
|
||||||
|
assert.equal(state.cardInfo.value.identifier, 'DEVICE-B')
|
||||||
|
assert.equal(resolveCalls, 3)
|
||||||
|
})
|
||||||
@@ -441,8 +441,8 @@
|
|||||||
<div class="binding-card-field">
|
<div class="binding-card-field">
|
||||||
<span class="binding-card-field-label">网络状态</span>
|
<span class="binding-card-field-label">网络状态</span>
|
||||||
<span class="binding-card-field-value">
|
<span class="binding-card-field-value">
|
||||||
<ElTag :type="bindingCard.network_status === 1 ? 'success' : 'danger'" size="small">
|
<ElTag :type="getNetworkStatusType(bindingCard.network_status)" size="small">
|
||||||
{{ bindingCard.network_status === 1 ? '正常' : '停机' }}
|
{{ getNetworkStatusName(bindingCard.network_status) }}
|
||||||
</ElTag>
|
</ElTag>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -474,30 +474,28 @@
|
|||||||
|
|
||||||
<div class="binding-card-actions">
|
<div class="binding-card-actions">
|
||||||
<ElButton
|
<ElButton
|
||||||
v-if="bindingCard.network_status === 0"
|
v-if="bindingCard.network_status === 0 && hasAuth('iot_info:start')"
|
||||||
type="success"
|
type="success"
|
||||||
size="default"
|
size="default"
|
||||||
:disabled="isBindingStartDisabled(bindingCard)"
|
:disabled="isBindingStartDisabled(bindingCard)"
|
||||||
@click="handleEnableBindingCard(bindingCard)"
|
@click="handleEnableBindingCard(bindingCard)"
|
||||||
v-permission="'iot_info:start'"
|
|
||||||
>
|
>
|
||||||
{{ getBindingStartButtonText(bindingCard) }}
|
{{ getBindingStartButtonText(bindingCard) }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
v-if="bindingCard.network_status === 1"
|
v-if="bindingCard.network_status === 1 && hasAuth('iot_info:stop')"
|
||||||
type="warning"
|
type="warning"
|
||||||
size="default"
|
size="default"
|
||||||
:disabled="isBindingStopDisabled(bindingCard)"
|
:disabled="isBindingStopDisabled(bindingCard)"
|
||||||
@click="handleDisableBindingCard(bindingCard)"
|
@click="handleDisableBindingCard(bindingCard)"
|
||||||
v-permission="'iot_info:stop'"
|
|
||||||
>
|
>
|
||||||
{{ getBindingStopButtonText(bindingCard) }}
|
{{ getBindingStopButtonText(bindingCard) }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
|
v-if="hasAuth('bound_card:update_realname_status')"
|
||||||
type="primary"
|
type="primary"
|
||||||
size="default"
|
size="default"
|
||||||
@click="handleUpdateBindingCardRealnameStatus(bindingCard)"
|
@click="handleUpdateBindingCardRealnameStatus(bindingCard)"
|
||||||
v-permission="'bound_card:update_realname_status'"
|
|
||||||
>
|
>
|
||||||
更新实名状态
|
更新实名状态
|
||||||
</ElButton>
|
</ElButton>
|
||||||
@@ -562,8 +560,7 @@
|
|||||||
style="margin-top: 16px; text-align: right"
|
style="margin-top: 16px; text-align: right"
|
||||||
>
|
>
|
||||||
<ElButton
|
<ElButton
|
||||||
v-if="cardInfo?.network_status === 0"
|
v-if="cardInfo?.network_status === 0 && hasAuth('iot_info:start')"
|
||||||
v-permission="'iot_info:start'"
|
|
||||||
type="success"
|
type="success"
|
||||||
:disabled="startDisabled"
|
:disabled="startDisabled"
|
||||||
@click="handleEnableCard"
|
@click="handleEnableCard"
|
||||||
@@ -571,8 +568,7 @@
|
|||||||
{{ startRemainingText ? `启用此卡(${startRemainingText})` : '启用此卡' }}
|
{{ startRemainingText ? `启用此卡(${startRemainingText})` : '启用此卡' }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
v-if="cardInfo?.network_status === 1"
|
v-if="cardInfo?.network_status === 1 && hasAuth('iot_info:stop')"
|
||||||
v-permission="'iot_info:stop'"
|
|
||||||
type="warning"
|
type="warning"
|
||||||
:disabled="stopDisabled"
|
:disabled="stopDisabled"
|
||||||
@click="handleDisableCard"
|
@click="handleDisableCard"
|
||||||
@@ -583,15 +579,14 @@
|
|||||||
<!-- 手动停用暂时不开放 -->
|
<!-- 手动停用暂时不开放 -->
|
||||||
<!--suppress VueUnrecognizedDirective -->
|
<!--suppress VueUnrecognizedDirective -->
|
||||||
<ElButton
|
<ElButton
|
||||||
v-if="canShowRealnamePolicy"
|
v-if="canShowRealnamePolicy && hasAuth('asset:card_realname_policy')"
|
||||||
v-permission="'asset:card_realname_policy'"
|
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleShowRealnamePolicy"
|
@click="handleShowRealnamePolicy"
|
||||||
>
|
>
|
||||||
实名认证策略
|
实名认证策略
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
v-permission="'iot_info:update_realname_status'"
|
v-if="hasAuth('iot_info:update_realname_status')"
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleShowUpdateRealnameStatus"
|
@click="handleShowUpdateRealnameStatus"
|
||||||
>
|
>
|
||||||
@@ -601,28 +596,27 @@
|
|||||||
|
|
||||||
<!-- 设备操作按钮 -->
|
<!-- 设备操作按钮 -->
|
||||||
<div v-else class="device-operations" style="margin-top: 16px; text-align: right">
|
<div v-else class="device-operations" style="margin-top: 16px; text-align: right">
|
||||||
<ElButton v-permission="'device_info:switch_mode'" type="info" @click="handleShowSwitchMode"
|
<ElButton v-if="hasAuth('device_info:switch_mode')" type="info" @click="handleShowSwitchMode"
|
||||||
>设置切卡模式</ElButton
|
>设置切卡模式</ElButton
|
||||||
>
|
>
|
||||||
<ElButton v-permission="'device_info:reboot'" type="primary" @click="handleRebootDevice">
|
<ElButton v-if="hasAuth('device_info:reboot')" type="primary" @click="handleRebootDevice">
|
||||||
重启设备
|
重启设备
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton v-permission="'device_info:reset'" type="warning" @click="handleResetDevice">
|
<ElButton v-if="hasAuth('device_info:reset')" type="warning" @click="handleResetDevice">
|
||||||
恢复出厂
|
恢复出厂
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
v-permission="'device_info:switch_card'"
|
v-if="hasAuth('device_info:switch_card')"
|
||||||
type="success"
|
type="success"
|
||||||
@click="handleShowSwitchCard"
|
@click="handleShowSwitchCard"
|
||||||
>
|
>
|
||||||
切换SIM卡
|
切换SIM卡
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton v-permission="'device_info:set_wifi'" type="primary" @click="handleShowSetWiFi">
|
<ElButton v-if="hasAuth('device_info:set_wifi')" type="primary" @click="handleShowSetWiFi">
|
||||||
设置WiFi
|
设置WiFi
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
v-if="canShowRealnamePolicy"
|
v-if="canShowRealnamePolicy && hasAuth('asset_info:device_realname_policy')"
|
||||||
v-permission="'asset_info:device_realname_policy'"
|
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleShowRealnamePolicy"
|
@click="handleShowRealnamePolicy"
|
||||||
>
|
>
|
||||||
@@ -926,6 +920,8 @@
|
|||||||
formatDuration,
|
formatDuration,
|
||||||
getRealNameStatusName,
|
getRealNameStatusName,
|
||||||
getRealNameStatusType,
|
getRealNameStatusType,
|
||||||
|
getNetworkStatusName,
|
||||||
|
getNetworkStatusType,
|
||||||
getRealnamePolicyName,
|
getRealnamePolicyName,
|
||||||
getCarrierTypeName,
|
getCarrierTypeName,
|
||||||
getAssetStatusName,
|
getAssetStatusName,
|
||||||
|
|||||||
@@ -68,6 +68,19 @@ export function useAssetFormatters() {
|
|||||||
return map[status] || 'info'
|
return map[status] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取网络状态名称和标签类型,缺失状态不能误报为停机。
|
||||||
|
const getNetworkStatusName = (status?: number) => {
|
||||||
|
if (status === 1) return '正常'
|
||||||
|
if (status === 0) return '停机'
|
||||||
|
return '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNetworkStatusType = (status?: number): TagType => {
|
||||||
|
if (status === 1) return 'success'
|
||||||
|
if (status === 0) return 'danger'
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
|
||||||
// 获取实名认证策略名称
|
// 获取实名认证策略名称
|
||||||
const getRealnamePolicyName = (policy?: string) => {
|
const getRealnamePolicyName = (policy?: string) => {
|
||||||
if (!policy || policy === '') return '未知'
|
if (!policy || policy === '') return '未知'
|
||||||
@@ -261,6 +274,8 @@ export function useAssetFormatters() {
|
|||||||
formatIccidWithDashes,
|
formatIccidWithDashes,
|
||||||
getRealNameStatusName,
|
getRealNameStatusName,
|
||||||
getRealNameStatusType,
|
getRealNameStatusType,
|
||||||
|
getNetworkStatusName,
|
||||||
|
getNetworkStatusType,
|
||||||
getRealnamePolicyName,
|
getRealnamePolicyName,
|
||||||
getCarrierTypeName,
|
getCarrierTypeName,
|
||||||
getAssetStatusName,
|
getAssetStatusName,
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
* 负责资产详情数据的加载和状态管理
|
* 负责资产详情数据的加载和状态管理
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ref } from 'vue'
|
import { getCurrentScope, onScopeDispose, ref, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { AssetService } from '@/api/modules'
|
import { AssetService } from '@/api/modules'
|
||||||
|
import { useUserStore } from '@/store/modules/user'
|
||||||
import { FrontendRateLimitError } from '@/utils/business/apiRateLimit'
|
import { FrontendRateLimitError } from '@/utils/business/apiRateLimit'
|
||||||
import { normalizeApiError } from '@/utils/business/apiError'
|
import { normalizeApiError } from '@/utils/business/apiError'
|
||||||
import type {
|
import type {
|
||||||
@@ -26,13 +27,19 @@ const normalizeOptionalText = (value?: string | null) =>
|
|||||||
typeof value === 'string' ? value.trim() : ''
|
typeof value === 'string' ? value.trim() : ''
|
||||||
|
|
||||||
const NULLABLE_REALTIME_CARD_FIELDS = new Set([
|
const NULLABLE_REALTIME_CARD_FIELDS = new Set([
|
||||||
|
'gateway_extend',
|
||||||
'last_data_check_at',
|
'last_data_check_at',
|
||||||
'last_real_name_check_at',
|
'last_real_name_check_at',
|
||||||
'last_card_status_check_at'
|
'last_card_status_check_at'
|
||||||
])
|
])
|
||||||
|
|
||||||
// 详情接口是绑定卡实名时间的唯一来源;实时状态接口不应覆盖或保留旧值。
|
// 绑定关系和静态信息以详情接口为准,实时接口只补充同一张卡的状态。
|
||||||
const DETAIL_ONLY_BOUND_CARD_FIELDS = new Set(['real_name_at'])
|
const REALTIME_BOUND_CARD_FIELDS = new Set([
|
||||||
|
'network_status',
|
||||||
|
'real_name_status',
|
||||||
|
'is_current',
|
||||||
|
...NULLABLE_REALTIME_CARD_FIELDS
|
||||||
|
])
|
||||||
const ASSET_NOT_FOUND_CODE = 1006
|
const ASSET_NOT_FOUND_CODE = 1006
|
||||||
|
|
||||||
const isLegacyNineteenDigitIdentifier = (identifier: string) => /^89\d{17}$/.test(identifier)
|
const isLegacyNineteenDigitIdentifier = (identifier: string) => /^89\d{17}$/.test(identifier)
|
||||||
@@ -108,22 +115,24 @@ const getDisplayedUsagePercentage = (traffic?: TrafficDisplaySource | null) => {
|
|||||||
const mergeAssetBoundCards = (
|
const mergeAssetBoundCards = (
|
||||||
existingCards: AssetBoundCard[] = [],
|
existingCards: AssetBoundCard[] = [],
|
||||||
realtimeCards: AssetBoundCard[] = []
|
realtimeCards: AssetBoundCard[] = []
|
||||||
): AssetBoundCard[] =>
|
): AssetBoundCard[] => {
|
||||||
realtimeCards.map((realtimeCard) => {
|
const realtimeByIccid = new Map(realtimeCards.map((card) => [card.iccid, card]))
|
||||||
const existingCard = existingCards.find((card) => card.iccid === realtimeCard.iccid)
|
const currentCard = existingCards.find(
|
||||||
if (!existingCard) {
|
(card) => realtimeByIccid.get(card.iccid)?.is_current === true
|
||||||
return { ...realtimeCard, real_name_at: null }
|
)
|
||||||
}
|
|
||||||
|
|
||||||
|
return existingCards.map((existingCard) => {
|
||||||
const mergedCard = { ...existingCard } as unknown as Record<string, unknown>
|
const mergedCard = { ...existingCard } as unknown as Record<string, unknown>
|
||||||
for (const [key, value] of Object.entries(realtimeCard)) {
|
for (const [key, value] of Object.entries(realtimeByIccid.get(existingCard.iccid) || {})) {
|
||||||
if (DETAIL_ONLY_BOUND_CARD_FIELDS.has(key)) continue
|
if (!REALTIME_BOUND_CARD_FIELDS.has(key)) continue
|
||||||
if (value !== undefined && (value !== null || NULLABLE_REALTIME_CARD_FIELDS.has(key))) {
|
if (value !== undefined && (value !== null || NULLABLE_REALTIME_CARD_FIELDS.has(key))) {
|
||||||
mergedCard[key] = value
|
mergedCard[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (currentCard) mergedCard.is_current = existingCard.iccid === currentCard.iccid
|
||||||
return mergedCard as unknown as AssetBoundCard
|
return mergedCard as unknown as AssetBoundCard
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function useAssetInfo() {
|
export function useAssetInfo() {
|
||||||
// 状态管理
|
// 状态管理
|
||||||
@@ -138,23 +147,94 @@ export function useAssetInfo() {
|
|||||||
const walletLoading = ref(false)
|
const walletLoading = ref(false)
|
||||||
const packageLoading = ref(false)
|
const packageLoading = ref(false)
|
||||||
const realtimeLoading = ref(false)
|
const realtimeLoading = ref(false)
|
||||||
|
let assetVersion = 0
|
||||||
|
let packageRequestId = 0
|
||||||
|
let currentPackageRequestId = 0
|
||||||
|
let realtimeRequestId = 0
|
||||||
|
let walletRequestId = 0
|
||||||
|
let disposed = false
|
||||||
|
|
||||||
|
const resetAssetInfo = () => {
|
||||||
|
assetVersion += 1
|
||||||
|
cardInfo.value = null
|
||||||
|
deviceRealtime.value = null
|
||||||
|
currentPackage.value = null
|
||||||
|
currentPackageErrorMsg.value = ''
|
||||||
|
packageList.value = []
|
||||||
|
walletInfo.value = null
|
||||||
|
loading.value = false
|
||||||
|
currentPackageLoading.value = false
|
||||||
|
packageLoading.value = false
|
||||||
|
realtimeLoading.value = false
|
||||||
|
walletLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同一标识符也可能属于不同搜索或登录会话,不能仅比较 identifier。
|
||||||
|
const captureAssetRequest = (identifier: string) => {
|
||||||
|
const version = assetVersion
|
||||||
|
const asset = cardInfo.value
|
||||||
|
return () =>
|
||||||
|
!disposed &&
|
||||||
|
version === assetVersion &&
|
||||||
|
asset !== null &&
|
||||||
|
cardInfo.value === asset &&
|
||||||
|
asset.identifier === identifier
|
||||||
|
}
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
watch(
|
||||||
|
[
|
||||||
|
() => userStore.info.id,
|
||||||
|
() => userStore.info.user_type,
|
||||||
|
() => userStore.info.shop_id,
|
||||||
|
() => userStore.info.enterprise_id,
|
||||||
|
() => userStore.isLogin
|
||||||
|
],
|
||||||
|
resetAssetInfo,
|
||||||
|
{ flush: 'sync' }
|
||||||
|
)
|
||||||
|
if (getCurrentScope()) {
|
||||||
|
onScopeDispose(() => {
|
||||||
|
disposed = true
|
||||||
|
resetAssetInfo()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取资产详情
|
* 获取资产详情
|
||||||
* @param identifier - 资产标识符(ICCID、IMEI、SN、虚拟号或MSISDN)
|
* @param identifier - 资产标识符(ICCID、IMEI、SN、虚拟号或MSISDN)
|
||||||
*/
|
*/
|
||||||
const fetchAssetDetail = async (identifier: string) => {
|
const fetchAssetDetail = async (
|
||||||
|
identifier: string,
|
||||||
|
{ preservePackageList = false }: { preservePackageList?: boolean } = {}
|
||||||
|
) => {
|
||||||
|
if (disposed) return null
|
||||||
|
const previousAsset = cardInfo.value
|
||||||
|
const previousPackages =
|
||||||
|
preservePackageList && cardInfo.value?.identifier === identifier ? packageList.value : []
|
||||||
|
resetAssetInfo()
|
||||||
|
packageList.value = previousPackages
|
||||||
|
const version = assetVersion
|
||||||
const shouldShowSilentRequestError = isLegacyNineteenDigitIdentifier(identifier)
|
const shouldShowSilentRequestError = isLegacyNineteenDigitIdentifier(identifier)
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const response = await resolveAssetForAssetInfo(identifier)
|
const response = await resolveAssetForAssetInfo(identifier)
|
||||||
|
if (version !== assetVersion) return null
|
||||||
|
|
||||||
if (response.code === 0 && response.data) {
|
if (response.code === 0 && response.data) {
|
||||||
const data = response.data
|
const data = response.data
|
||||||
const resolvedGatewayCardImei = normalizeOptionalText(data.gateway_card_imei)
|
const resolvedGatewayCardImei = normalizeOptionalText(data.gateway_card_imei)
|
||||||
|
|
||||||
// 获取资产标识符
|
// 获取资产标识符
|
||||||
const assetIdentifier = data.asset_type === 'card' ? data.iccid : data.virtual_no
|
const assetIdentifier =
|
||||||
|
(data.asset_type === 'card' ? data.iccid : data.virtual_no) || identifier
|
||||||
|
if (
|
||||||
|
previousAsset?.asset_type !== data.asset_type ||
|
||||||
|
previousAsset?.asset_id !== data.asset_id ||
|
||||||
|
previousAsset?.identifier !== assetIdentifier
|
||||||
|
) {
|
||||||
|
packageList.value = []
|
||||||
|
}
|
||||||
|
|
||||||
// 映射API数据到页面显示格式
|
// 映射API数据到页面显示格式
|
||||||
cardInfo.value = {
|
cardInfo.value = {
|
||||||
@@ -216,7 +296,7 @@ export function useAssetInfo() {
|
|||||||
sn: data.sn || '',
|
sn: data.sn || '',
|
||||||
max_sim_slots: data.max_sim_slots || 0,
|
max_sim_slots: data.max_sim_slots || 0,
|
||||||
bound_card_count: data.bound_card_count || 0,
|
bound_card_count: data.bound_card_count || 0,
|
||||||
cards: data.cards || [],
|
cards: (data.cards || []).map((card) => ({ ...card })),
|
||||||
device_protect_status: data.device_protect_status,
|
device_protect_status: data.device_protect_status,
|
||||||
online_status: data.online_status,
|
online_status: data.online_status,
|
||||||
last_online_time: data.last_online_time,
|
last_online_time: data.last_online_time,
|
||||||
@@ -244,19 +324,22 @@ export function useAssetInfo() {
|
|||||||
loadAssetWallet(assetIdentifier)
|
loadAssetWallet(assetIdentifier)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
return version === assetVersion ? cardInfo.value : null
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(response.msg || '查询失败')
|
ElMessage.error(response.msg || '查询失败')
|
||||||
cardInfo.value = null
|
cardInfo.value = null
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
cardInfo.value = null
|
if (version !== assetVersion) return null
|
||||||
|
resetAssetInfo()
|
||||||
if (shouldShowSilentRequestError && !isUnauthorized(error)) {
|
if (shouldShowSilentRequestError && !isUnauthorized(error)) {
|
||||||
const backendMessage = error?.response?.data?.msg
|
const backendMessage = error?.response?.data?.msg
|
||||||
ElMessage.error(backendMessage || normalizeApiError(error).message)
|
ElMessage.error(backendMessage || normalizeApiError(error).message)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
if (version === assetVersion) loading.value = false
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -266,12 +349,17 @@ export function useAssetInfo() {
|
|||||||
* @param pageSize - 每页条数
|
* @param pageSize - 每页条数
|
||||||
*/
|
*/
|
||||||
const loadPackageList = async (identifier: string, page: number = 1, pageSize: number = 10) => {
|
const loadPackageList = async (identifier: string, page: number = 1, pageSize: number = 10) => {
|
||||||
|
const isCurrentAsset = captureAssetRequest(identifier)
|
||||||
|
if (!isCurrentAsset()) return null
|
||||||
|
const requestId = ++packageRequestId
|
||||||
|
const isCurrent = () => isCurrentAsset() && requestId === packageRequestId
|
||||||
try {
|
try {
|
||||||
packageLoading.value = true
|
packageLoading.value = true
|
||||||
const response = await AssetService.getAssetPackages(identifier, {
|
const response = await AssetService.getAssetPackages(identifier, {
|
||||||
page,
|
page,
|
||||||
page_size: pageSize
|
page_size: pageSize
|
||||||
})
|
})
|
||||||
|
if (!isCurrent()) return null
|
||||||
if (response.code === 0 && response.data) {
|
if (response.code === 0 && response.data) {
|
||||||
// 使用分页响应中的 items 数组
|
// 使用分页响应中的 items 数组
|
||||||
packageList.value = response.data.items || []
|
packageList.value = response.data.items || []
|
||||||
@@ -283,12 +371,15 @@ export function useAssetInfo() {
|
|||||||
pageSize: response.data.page_size || pageSize
|
pageSize: response.data.page_size || pageSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
packageList.value = []
|
||||||
return { items: [], total: 0, page: 1, pageSize }
|
return { items: [], total: 0, page: 1, pageSize }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (!isCurrent()) return null
|
||||||
console.error('获取套餐列表失败:', error)
|
console.error('获取套餐列表失败:', error)
|
||||||
|
packageList.value = []
|
||||||
return { items: [], total: 0, page: 1, pageSize }
|
return { items: [], total: 0, page: 1, pageSize }
|
||||||
} finally {
|
} finally {
|
||||||
packageLoading.value = false
|
if (isCurrent()) packageLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,11 +388,16 @@ export function useAssetInfo() {
|
|||||||
* @param identifier - 资产标识符
|
* @param identifier - 资产标识符
|
||||||
*/
|
*/
|
||||||
const loadCurrentPackage = async (identifier: string) => {
|
const loadCurrentPackage = async (identifier: string) => {
|
||||||
|
const isCurrentAsset = captureAssetRequest(identifier)
|
||||||
|
if (!isCurrentAsset()) return
|
||||||
|
const requestId = ++currentPackageRequestId
|
||||||
|
const isCurrent = () => isCurrentAsset() && requestId === currentPackageRequestId
|
||||||
try {
|
try {
|
||||||
currentPackageLoading.value = true
|
currentPackageLoading.value = true
|
||||||
currentPackageErrorMsg.value = ''
|
currentPackageErrorMsg.value = ''
|
||||||
|
|
||||||
const response = await AssetService.getCurrentPackage(identifier)
|
const response = await AssetService.getCurrentPackage(identifier)
|
||||||
|
if (!isCurrent()) return
|
||||||
|
|
||||||
if (response.code === 0) {
|
if (response.code === 0) {
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
@@ -350,6 +446,7 @@ export function useAssetInfo() {
|
|||||||
currentPackageErrorMsg.value = response.msg || '获取当前套餐失败'
|
currentPackageErrorMsg.value = response.msg || '获取当前套餐失败'
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
if (!isCurrent()) return
|
||||||
// 404表示无当前生效套餐,这是正常情况
|
// 404表示无当前生效套餐,这是正常情况
|
||||||
currentPackage.value = null
|
currentPackage.value = null
|
||||||
if (error?.response?.status === 404) {
|
if (error?.response?.status === 404) {
|
||||||
@@ -359,7 +456,7 @@ export function useAssetInfo() {
|
|||||||
currentPackageErrorMsg.value = error?.message || '获取当前套餐失败'
|
currentPackageErrorMsg.value = error?.message || '获取当前套餐失败'
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
currentPackageLoading.value = false
|
if (isCurrent()) currentPackageLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,11 +466,22 @@ export function useAssetInfo() {
|
|||||||
* @param assetType - 资产类型(card/device)
|
* @param assetType - 资产类型(card/device)
|
||||||
*/
|
*/
|
||||||
const loadRealtimeStatus = async (identifier: string, assetType: string) => {
|
const loadRealtimeStatus = async (identifier: string, assetType: string) => {
|
||||||
|
const isCurrentAsset = captureAssetRequest(identifier)
|
||||||
|
if (!isCurrentAsset() || cardInfo.value.asset_type !== assetType) return
|
||||||
|
const requestId = ++realtimeRequestId
|
||||||
|
const isCurrent = () => isCurrentAsset() && requestId === realtimeRequestId
|
||||||
try {
|
try {
|
||||||
realtimeLoading.value = true
|
realtimeLoading.value = true
|
||||||
const response = await AssetService.getRealtimeStatus(identifier)
|
const response = await AssetService.getRealtimeStatus(identifier)
|
||||||
|
if (!isCurrent()) return
|
||||||
if (response.code === 0 && response.data) {
|
if (response.code === 0 && response.data) {
|
||||||
const data = response.data
|
const data = response.data
|
||||||
|
if (
|
||||||
|
(data.asset_type !== undefined && data.asset_type !== assetType) ||
|
||||||
|
(data.asset_id !== undefined && String(data.asset_id) !== String(cardInfo.value.asset_id))
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
// 更新实时状态数据
|
// 更新实时状态数据
|
||||||
if (assetType === 'card') {
|
if (assetType === 'card') {
|
||||||
// 卡的实时状态
|
// 卡的实时状态
|
||||||
@@ -414,8 +522,7 @@ export function useAssetInfo() {
|
|||||||
cardInfo.value.device_protect_status = data.device_protect_status
|
cardInfo.value.device_protect_status = data.device_protect_status
|
||||||
}
|
}
|
||||||
if (data.cards && data.cards.length > 0) {
|
if (data.cards && data.cards.length > 0) {
|
||||||
// 合并实时卡数据与原始卡数据,保留 real_name_at 等静态字段。
|
// 实时 cards 可能缺少部分卡或静态字段,不能替换详情里的完整绑定列表。
|
||||||
// 设备详情里的实名状态、实名时间依赖选中的绑定卡,因此不能在实时刷新时丢掉这些字段。
|
|
||||||
cardInfo.value.cards = mergeAssetBoundCards(cardInfo.value.cards || [], data.cards)
|
cardInfo.value.cards = mergeAssetBoundCards(cardInfo.value.cards || [], data.cards)
|
||||||
}
|
}
|
||||||
if (data.last_data_check_at !== undefined) {
|
if (data.last_data_check_at !== undefined) {
|
||||||
@@ -449,9 +556,9 @@ export function useAssetInfo() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取实时状态失败:', error)
|
if (isCurrent()) console.error('获取实时状态失败:', error)
|
||||||
} finally {
|
} finally {
|
||||||
realtimeLoading.value = false
|
if (isCurrent()) realtimeLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,13 +567,22 @@ export function useAssetInfo() {
|
|||||||
* @param identifier - 资产标识符
|
* @param identifier - 资产标识符
|
||||||
*/
|
*/
|
||||||
const loadAssetWallet = async (identifier: string) => {
|
const loadAssetWallet = async (identifier: string) => {
|
||||||
|
const isCurrentAsset = captureAssetRequest(identifier)
|
||||||
|
if (!isCurrentAsset()) return
|
||||||
|
const requestId = ++walletRequestId
|
||||||
|
const isCurrent = () => isCurrentAsset() && requestId === walletRequestId
|
||||||
try {
|
try {
|
||||||
walletLoading.value = true
|
walletLoading.value = true
|
||||||
const response = await AssetService.getAssetWallet(identifier)
|
const response = await AssetService.getAssetWallet(identifier)
|
||||||
|
if (!isCurrent()) return
|
||||||
if (response.code === 0 && response.data) {
|
if (response.code === 0 && response.data) {
|
||||||
walletInfo.value = response.data
|
walletInfo.value = response.data
|
||||||
|
} else {
|
||||||
|
walletInfo.value = null
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
if (!isCurrent()) return
|
||||||
|
walletInfo.value = null
|
||||||
if (error?.response?.status === 403) {
|
if (error?.response?.status === 403) {
|
||||||
// 企业账号禁止查看钱包信息,不显示错误
|
// 企业账号禁止查看钱包信息,不显示错误
|
||||||
walletInfo.value = null
|
walletInfo.value = null
|
||||||
@@ -474,7 +590,7 @@ export function useAssetInfo() {
|
|||||||
console.error('获取钱包概况失败:', error)
|
console.error('获取钱包概况失败:', error)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
walletLoading.value = false
|
if (isCurrent()) walletLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,16 +601,18 @@ export function useAssetInfo() {
|
|||||||
* @returns true if refresh was successful, false if rate limited
|
* @returns true if refresh was successful, false if rate limited
|
||||||
*/
|
*/
|
||||||
const refreshAsset = async (identifier: string, assetType: string): Promise<boolean> => {
|
const refreshAsset = async (identifier: string, assetType: string): Promise<boolean> => {
|
||||||
|
const isCurrent = captureAssetRequest(identifier)
|
||||||
|
if (!isCurrent() || cardInfo.value.asset_type !== assetType) return false
|
||||||
try {
|
try {
|
||||||
|
// 主动同步网关的接口仅允许由右上角“同步”按钮触发,搜索和普通数据重载不能调用。
|
||||||
await AssetService.refreshAsset(identifier)
|
await AssetService.refreshAsset(identifier)
|
||||||
|
if (!isCurrent()) return false
|
||||||
ElMessage.success('刷新成功')
|
ElMessage.success('刷新成功')
|
||||||
|
|
||||||
// 刷新成功后重新加载实时状态数据
|
// 绑定/解绑和实名时间由详情接口确认,不能只刷新实时状态而保留过期绑定关系。
|
||||||
if (identifier) {
|
return Boolean(await fetchAssetDetail(identifier, { preservePackageList: true }))
|
||||||
await loadRealtimeStatus(identifier, assetType)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
if (!isCurrent()) return false
|
||||||
if (error instanceof FrontendRateLimitError) {
|
if (error instanceof FrontendRateLimitError) {
|
||||||
ElMessage.warning('操作过于频繁,请稍后重试')
|
ElMessage.warning('操作过于频繁,请稍后重试')
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<!-- 第一层:资产概览与当前生效套餐,按信息高度纵向展开 -->
|
<!-- 第一层:资产概览与当前生效套餐,按信息高度纵向展开 -->
|
||||||
<div class="row overview-primary">
|
<div class="row overview-primary">
|
||||||
<BasicInfoCard
|
<BasicInfoCard
|
||||||
|
:key="`${cardInfo.asset_type}:${cardInfo.asset_id}`"
|
||||||
:card-info="cardInfo"
|
:card-info="cardInfo"
|
||||||
:device-realtime="deviceRealtime"
|
:device-realtime="deviceRealtime"
|
||||||
v-model:polling-enabled="pollingEnabled"
|
v-model:polling-enabled="pollingEnabled"
|
||||||
@@ -296,6 +297,8 @@
|
|||||||
const speedTierSubmitting = ref(false)
|
const speedTierSubmitting = ref(false)
|
||||||
const bindingCardRealnameStatusIccid = ref('')
|
const bindingCardRealnameStatusIccid = ref('')
|
||||||
const bindingCardRealnameStatusValue = ref<number>(0)
|
const bindingCardRealnameStatusValue = ref<number>(0)
|
||||||
|
let realnameStatusAsset: any = null
|
||||||
|
let bindingCardRealnameStatusAsset: any = null
|
||||||
|
|
||||||
const switchModeCards = computed<SwitchModeCardOption[]>(() =>
|
const switchModeCards = computed<SwitchModeCardOption[]>(() =>
|
||||||
(cardInfo.value?.cards || [])
|
(cardInfo.value?.cards || [])
|
||||||
@@ -402,37 +405,32 @@
|
|||||||
/**
|
/**
|
||||||
* 处理资产搜索
|
* 处理资产搜索
|
||||||
*/
|
*/
|
||||||
|
let searchRequestId = 0
|
||||||
const handleSearch = async ({ identifier }: { identifier: string }) => {
|
const handleSearch = async ({ identifier }: { identifier: string }) => {
|
||||||
|
const requestId = ++searchRequestId
|
||||||
// 更新搜索框的值
|
// 更新搜索框的值
|
||||||
assetSearchCardRef.value?.updateSearchValue(identifier)
|
assetSearchCardRef.value?.updateSearchValue(identifier)
|
||||||
startFailureMessage.value = ''
|
startFailureMessage.value = ''
|
||||||
|
packagePagination.value.page = 1
|
||||||
|
packagePagination.value.total = 0
|
||||||
|
|
||||||
// fetchAssetDetail 内部已经会并行加载所有相关数据,不需要重复调用
|
// resolve 已返回完整详情;设备不再重复搜索,避免多轮响应相互覆盖。
|
||||||
await fetchAssetDetail(identifier)
|
const asset = await fetchAssetDetail(identifier)
|
||||||
if (!cardInfo.value) return
|
if (requestId !== searchRequestId || !asset || cardInfo.value !== asset) return
|
||||||
|
walletTransactionSearchToken.value += 1
|
||||||
// 如果是设备类型,需要再次调用搜索接口获取完整数据
|
|
||||||
if (cardInfo.value?.asset_type === 'device') {
|
|
||||||
const deviceIdentifier = cardInfo.value.virtual_no || cardInfo.value.iccid || identifier
|
|
||||||
await fetchAssetDetail(deviceIdentifier)
|
|
||||||
}
|
|
||||||
if (!cardInfo.value) return
|
|
||||||
|
|
||||||
if (cardInfo.value) {
|
|
||||||
walletTransactionSearchToken.value += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从接口数据初始化轮询开关,不触发更新接口
|
// 从接口数据初始化轮询开关,不触发更新接口
|
||||||
pollingInitializing = true
|
pollingInitializing = true
|
||||||
pollingEnabled.value = cardInfo.value?.enable_polling ?? false
|
pollingEnabled.value = asset.enable_polling ?? false
|
||||||
await nextTick()
|
await nextTick()
|
||||||
pollingInitializing = false
|
pollingInitializing = false
|
||||||
|
if (requestId !== searchRequestId || cardInfo.value !== asset) return
|
||||||
|
|
||||||
// 重置分页并加载套餐列表
|
// 套餐也使用解析后的规范标识符,支持通过 ICCID、IMEI、SN 等入口查同一设备。
|
||||||
packagePagination.value.page = 1
|
const result = await loadPackageList(asset.identifier, 1, packagePagination.value.pageSize)
|
||||||
const resolvedIdentifier = cardInfo.value.identifier || identifier
|
if (result && requestId === searchRequestId && cardInfo.value === asset) {
|
||||||
const result = await loadPackageList(resolvedIdentifier, 1, packagePagination.value.pageSize)
|
packagePagination.value.total = result.total
|
||||||
packagePagination.value.total = result.total
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -458,7 +456,7 @@
|
|||||||
try {
|
try {
|
||||||
const started = await enableCardOp()
|
const started = await enableCardOp()
|
||||||
if (started && cardInfo.value) {
|
if (started && cardInfo.value) {
|
||||||
await fetchAssetDetail(cardInfo.value.identifier)
|
await fetchAssetDetail(cardInfo.value.identifier, { preservePackageList: true })
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
updateRateLimitNow()
|
updateRateLimitNow()
|
||||||
@@ -571,18 +569,24 @@
|
|||||||
* 显示更新实名状态对话框
|
* 显示更新实名状态对话框
|
||||||
*/
|
*/
|
||||||
const showUpdateRealnameStatusDialog = () => {
|
const showUpdateRealnameStatusDialog = () => {
|
||||||
|
realnameStatusAsset = cardInfo.value
|
||||||
updateRealnameStatusDialogVisible.value = true
|
updateRealnameStatusDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新实名状态成功
|
* 更新实名状态成功
|
||||||
*/
|
*/
|
||||||
const handleUpdateRealnameStatusSuccess = async () => {}
|
const handleUpdateRealnameStatusSuccess = async () => {
|
||||||
|
if (realnameStatusAsset && cardInfo.value === realnameStatusAsset) {
|
||||||
|
await fetchAssetDetail(realnameStatusAsset.identifier, { preservePackageList: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 显示绑定卡更新实名状态对话框
|
* 显示绑定卡更新实名状态对话框
|
||||||
*/
|
*/
|
||||||
const handleUpdateBindingCardRealnameStatus = (payload: { card: any }) => {
|
const handleUpdateBindingCardRealnameStatus = (payload: { card: any }) => {
|
||||||
|
bindingCardRealnameStatusAsset = cardInfo.value
|
||||||
bindingCardRealnameStatusIccid.value = payload.card.iccid
|
bindingCardRealnameStatusIccid.value = payload.card.iccid
|
||||||
bindingCardRealnameStatusValue.value = payload.card.real_name_status ?? 0
|
bindingCardRealnameStatusValue.value = payload.card.real_name_status ?? 0
|
||||||
bindingCardRealnameStatusDialogVisible.value = true
|
bindingCardRealnameStatusDialogVisible.value = true
|
||||||
@@ -591,7 +595,11 @@
|
|||||||
/**
|
/**
|
||||||
* 绑定卡更新实名状态成功
|
* 绑定卡更新实名状态成功
|
||||||
*/
|
*/
|
||||||
const handleBindingCardRealnameStatusSuccess = async () => {}
|
const handleBindingCardRealnameStatusSuccess = async () => {
|
||||||
|
if (bindingCardRealnameStatusAsset && cardInfo.value === bindingCardRealnameStatusAsset) {
|
||||||
|
await fetchAssetDetail(bindingCardRealnameStatusAsset.identifier, { preservePackageList: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 确认实名认证策略
|
* 确认实名认证策略
|
||||||
@@ -679,6 +687,7 @@
|
|||||||
packagePagination.value.page,
|
packagePagination.value.page,
|
||||||
packagePagination.value.pageSize
|
packagePagination.value.pageSize
|
||||||
)
|
)
|
||||||
|
if (!result) return
|
||||||
packagePagination.value.total = result.total
|
packagePagination.value.total = result.total
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadCurrentPackage(cardInfo.value.identifier),
|
loadCurrentPackage(cardInfo.value.identifier),
|
||||||
@@ -699,7 +708,7 @@
|
|||||||
page,
|
page,
|
||||||
packagePagination.value.pageSize
|
packagePagination.value.pageSize
|
||||||
)
|
)
|
||||||
packagePagination.value.total = result.total
|
if (result) packagePagination.value.total = result.total
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -710,7 +719,7 @@
|
|||||||
packagePagination.value.pageSize = size
|
packagePagination.value.pageSize = size
|
||||||
packagePagination.value.page = 1 // 重置到第一页
|
packagePagination.value.page = 1 // 重置到第一页
|
||||||
const result = await loadPackageList(cardInfo.value.identifier, 1, size)
|
const result = await loadPackageList(cardInfo.value.identifier, 1, size)
|
||||||
packagePagination.value.total = result.total
|
if (result) packagePagination.value.total = result.total
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -723,7 +732,7 @@
|
|||||||
packagePagination.value.page,
|
packagePagination.value.page,
|
||||||
packagePagination.value.pageSize
|
packagePagination.value.pageSize
|
||||||
)
|
)
|
||||||
packagePagination.value.total = result.total
|
if (result) packagePagination.value.total = result.total
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -834,7 +843,10 @@
|
|||||||
speedLimitDialogVisible.value = false
|
speedLimitDialogVisible.value = false
|
||||||
await nextTick()
|
await nextTick()
|
||||||
ElMessage.success(`限速设置成功:${response.data.speed_tier_name}`)
|
ElMessage.success(`限速设置成功:${response.data.speed_tier_name}`)
|
||||||
await handleRefresh()
|
// 限速设置后只读取已保存状态,不触发右上角按钮的主动同步接口。
|
||||||
|
if (cardInfo.value?.iccid === iccid) {
|
||||||
|
await loadRealtimeStatus(cardInfo.value.identifier, cardInfo.value.asset_type)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('设置限速失败:', error)
|
console.error('设置限速失败:', error)
|
||||||
speedLimitDialogVisible.value = false
|
speedLimitDialogVisible.value = false
|
||||||
|
|||||||
@@ -1665,6 +1665,7 @@
|
|||||||
{ label: '设备号', prop: 'virtual_no' },
|
{ label: '设备号', prop: 'virtual_no' },
|
||||||
{ label: 'IMEI', prop: 'imei' },
|
{ label: 'IMEI', prop: 'imei' },
|
||||||
{ label: '设备名称', prop: 'device_name' },
|
{ label: '设备名称', prop: 'device_name' },
|
||||||
|
{ label: '授权企业', prop: 'authorized_enterprise_name' },
|
||||||
{ label: '店铺名称', prop: 'shop_name' },
|
{ label: '店铺名称', prop: 'shop_name' },
|
||||||
{ label: '套餐系列', prop: 'series_name' },
|
{ label: '套餐系列', prop: 'series_name' },
|
||||||
{ label: '预计套餐到期时间', prop: 'estimated_final_expires_at' },
|
{ label: '预计套餐到期时间', prop: 'estimated_final_expires_at' },
|
||||||
@@ -1941,6 +1942,13 @@
|
|||||||
showOverflowTooltip: true,
|
showOverflowTooltip: true,
|
||||||
minWidth: 150
|
minWidth: 150
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prop: 'authorized_enterprise_name',
|
||||||
|
label: '授权企业',
|
||||||
|
minWidth: 160,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
formatter: (row: Device) => row.authorized_enterprise_name || '-'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: 'shop_name',
|
prop: 'shop_name',
|
||||||
label: '店铺名称',
|
label: '店铺名称',
|
||||||
|
|||||||
@@ -1996,6 +1996,7 @@
|
|||||||
{ label: 'MSISDN', prop: 'msisdn' },
|
{ label: 'MSISDN', prop: 'msisdn' },
|
||||||
{ label: '卡虚拟号', prop: 'virtual_no' },
|
{ label: '卡虚拟号', prop: 'virtual_no' },
|
||||||
{ label: '运营商', prop: 'carrier_name' },
|
{ label: '运营商', prop: 'carrier_name' },
|
||||||
|
{ label: '授权企业', prop: 'authorized_enterprise_name' },
|
||||||
{ label: '卡业务类型', prop: 'card_category' },
|
{ label: '卡业务类型', prop: 'card_category' },
|
||||||
{ label: '状态', prop: 'status' },
|
{ label: '状态', prop: 'status' },
|
||||||
{ label: '资产世代', prop: 'generation' },
|
{ label: '资产世代', prop: 'generation' },
|
||||||
@@ -2166,6 +2167,13 @@
|
|||||||
width: 150,
|
width: 150,
|
||||||
showOverflowTooltip: true
|
showOverflowTooltip: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prop: 'authorized_enterprise_name',
|
||||||
|
label: '授权企业',
|
||||||
|
minWidth: 160,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
formatter: (row: StandaloneIotCard) => row.authorized_enterprise_name || '-'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: 'shop_name',
|
prop: 'shop_name',
|
||||||
label: '店铺名称',
|
label: '店铺名称',
|
||||||
|
|||||||
Reference in New Issue
Block a user