fix: 记录查询列表显示 ICCID、设备号、套餐名称、运营商名称与操作人,不再显示内部 ID
Some checks failed
构建并部署前端到测试环境 / deploy (push) Has been cancelled

- 卡 / 设备列优先显示 ICCID、设备号(IMEI),后端未返回时才退回内部 ID
- 运营商账户筛选改为按名称搜索
This commit is contained in:
邹进
2026-11-07 17:07:31 +09:00
parent 984db9889c
commit 3411b66c71
9 changed files with 163 additions and 43 deletions

View File

@@ -110,3 +110,37 @@ test('退款后处理状态与权限码常量', () => {
assert.equal(constants.labelOf(constants.TRAFFIC_DECISION_LABELS, 'new_value'), 'new_value')
assert.equal(constants.labelOf(constants.TRAFFIC_DECISION_LABELS, ''), '-')
})
test('卡 / 设备展示优先用 ICCID、设备号,没有时才显示内部 ID', () => {
assert.equal(
utils.assetDisplay({ iot_card_id: 367218, iccid: '89860624660030813727' }),
'89860624660030813727'
)
assert.equal(
utils.assetDisplay({
device_id: 49323,
device_virtual_no: '60003081372',
device_imei: '869555083099518'
}),
'60003081372(IMEI 869555083099518)'
)
assert.equal(
utils.assetDisplay({
device_id: 1,
device_virtual_no: '862639078983683',
device_imei: '862639078983683'
}),
'862639078983683'
)
assert.equal(
utils.assetDisplay({ device_id: 1, device_imei: '869555083099518' }),
'869555083099518'
)
assert.equal(utils.assetDisplay({ iot_card_id: 367218 }), '卡ID 367218')
assert.equal(utils.assetDisplay({ device_id: 49323 }), '设备ID 49323')
assert.equal(utils.assetDisplay({}), '-')
assert.equal(utils.nameOrId('移动账户A', 3), '移动账户A')
assert.equal(utils.nameOrId('', 3), 'ID 3')
assert.equal(utils.nameOrId('', 7, '账号ID'), '账号ID 7')
assert.equal(utils.nameOrId(undefined, 0), '-')
})

View File

@@ -23,7 +23,9 @@
<ElTableColumn label="ICCID / 卡 ID" min-width="190">
<template #default="{ row }">{{ row.iccid || row.iot_card_id }}</template>
</ElTableColumn>
<ElTableColumn label="运营商账户" prop="carrier_id" width="100" />
<ElTableColumn label="运营商账户" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ nameOrId(row.carrier_name, row.carrier_id) }}</template>
</ElTableColumn>
<ElTableColumn label="当日用量" min-width="110">
<template #default="{ row }">{{ formatMb(row.settled_mb) }}</template>
</ElTableColumn>
@@ -47,10 +49,10 @@
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import { computed, reactive } from 'vue'
import { storeToRefs } from 'pinia'
import { TrafficRecordsService } from '@/api/modules/trafficRecords'
import { useRecordList } from '@/composables/useRecordList'
import { useCarrierFilterItem, useRecordList } from '@/composables/useRecordList'
import { useUserStore } from '@/store/modules/user'
import type { SearchFormItem } from '@/types'
import type { CardDailyUsage } from '@/types/api/trafficRecords'
@@ -58,7 +60,7 @@
import {
compactQuery,
formatMb,
optionalPositiveInt,
nameOrId,
recentDateRange,
requireIdentifierForNonPlatform,
toDateQuery
@@ -68,7 +70,7 @@
const { info: userInfo } = storeToRefs(useUserStore())
const emptyFilters = () => ({
identifier: '',
carrier_id: '',
carrier_id: undefined as number | undefined,
min_usage_mb: '',
range: recentDateRange(7) as [string, string] | null
})
@@ -87,13 +89,14 @@
...page,
...toDateQuery(filters.range),
identifier,
carrier_id: optionalPositiveInt(filters.carrier_id, '运营商账户 ID'),
carrier_id: filters.carrier_id,
min_usage_mb: minUsage
})
)
})
const searchItems: SearchFormItem[] = [
const { item: carrierItem } = useCarrierFilterItem()
const searchItems = computed<SearchFormItem[]>(() => [
{
label: '资产标识',
prop: 'identifier',
@@ -113,14 +116,14 @@
clearable: false
}
},
{ label: '运营商账户ID', prop: 'carrier_id', type: 'input', config: { clearable: true } },
{
label: '当日用量≥(MB)',
prop: 'min_usage_mb',
type: 'input',
config: { clearable: true, placeholder: '排查异常大流量' }
}
]
},
carrierItem.value
])
function reset() {
Object.assign(filters, emptyFilters())

View File

@@ -31,23 +31,23 @@
<ElTableColumn label="冲正时间" min-width="170">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
</ElTableColumn>
<ElTableColumn
v-if="!isFixed"
label="ICCID / 设备号"
min-width="200"
show-overflow-tooltip
>
<template #default="{ row }">{{ assetDisplay(row) }}</template>
</ElTableColumn>
<ElTableColumn v-if="!isFixed" label="套餐" min-width="180" show-overflow-tooltip>
<template #default="{ row }">{{ row.package_name || '-' }}</template>
</ElTableColumn>
<ElTableColumn
v-if="!isFixed"
label="套餐使用记录ID"
prop="package_usage_id"
min-width="130"
/>
<ElTableColumn v-if="!isFixed" label="卡ID / 设备ID" min-width="120">
<template #default="{ row }">
{{
row.iot_card_id
? `卡 ${row.iot_card_id}`
: row.device_id
? `设备 ${row.device_id}`
: '-'
}}
</template>
</ElTableColumn>
<ElTableColumn label="调整量" min-width="110">
<template #default="{ row }">
<span :class="row.delta_mb > 0 ? 'delta-deduct' : 'delta-refund'">
@@ -66,7 +66,11 @@
</template>
</ElTableColumn>
<ElTableColumn label="原因" prop="reason" min-width="200" show-overflow-tooltip />
<ElTableColumn label="操作人ID" prop="operator_id" width="100" />
<ElTableColumn label="操作人" min-width="120">
<template #default="{ row }">{{
nameOrId(row.operator_name, row.operator_id, '账号ID')
}}</template>
</ElTableColumn>
<ElTableColumn
label="关联采集 ID"
prop="ref_observation_id"
@@ -98,7 +102,9 @@
import { ADJUSTMENT_DIRECTION_OPTIONS } from '@/config/constants/trafficRecords'
import {
compactQuery,
assetDisplay,
formatMb,
nameOrId,
optionalPositiveInt,
recentTimeRange,
requireIdentifierForNonPlatform,

View File

@@ -21,13 +21,15 @@
>
<ElTable v-loading="loading" :data="rows" row-key="id" border empty-text="暂无套餐日流量">
<ElTableColumn label="日期" prop="date" width="120" />
<ElTableColumn label="套餐使用记录ID" prop="package_usage_id" min-width="130" />
<ElTableColumn label="套餐ID" prop="package_id" width="100" />
<ElTableColumn label="卡ID / 设备ID" min-width="130">
<template #default="{ row }">
{{ row.iot_card_id ? `卡 ${row.iot_card_id}` : `设备 ${row.device_id}` }}
</template>
<ElTableColumn label="ICCID / 设备号" min-width="200" show-overflow-tooltip>
<template #default="{ row }">{{ assetDisplay(row) }}</template>
</ElTableColumn>
<ElTableColumn label="套餐" min-width="200" show-overflow-tooltip>
<template #default="{ row }">{{
nameOrId(row.package_name, row.package_id, '套餐ID')
}}</template>
</ElTableColumn>
<ElTableColumn label="套餐使用记录ID" prop="package_usage_id" min-width="130" />
<ElTableColumn label="当日流量" min-width="110">
<template #default="{ row }">{{ formatMb(row.daily_usage_mb) }}</template>
</ElTableColumn>
@@ -44,17 +46,19 @@
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import { computed, reactive } from 'vue'
import { storeToRefs } from 'pinia'
import { TrafficRecordsService } from '@/api/modules/trafficRecords'
import { useRecordList } from '@/composables/useRecordList'
import { useCarrierFilterItem, useRecordList } from '@/composables/useRecordList'
import { useUserStore } from '@/store/modules/user'
import type { SearchFormItem } from '@/types'
import type { PackageUsageDailyRecord } from '@/types/api/trafficRecords'
import { formatDateTime } from '@/utils/business/format'
import {
compactQuery,
assetDisplay,
formatMb,
nameOrId,
optionalPositiveInt,
recentDateRange,
requireIdentifierForNonPlatform,
@@ -67,7 +71,7 @@
identifier: '',
package_usage_id: '',
package_id: '',
carrier_id: '',
carrier_id: undefined as number | undefined,
range: recentDateRange(7) as [string, string] | null
})
const filters = reactive(emptyFilters())
@@ -86,12 +90,13 @@
identifier,
package_usage_id: packageUsageId,
package_id: optionalPositiveInt(filters.package_id, '套餐 ID'),
carrier_id: optionalPositiveInt(filters.carrier_id, '运营商账户 ID')
carrier_id: filters.carrier_id
})
)
})
const searchItems: SearchFormItem[] = [
const { item: carrierItem } = useCarrierFilterItem()
const searchItems = computed<SearchFormItem[]>(() => [
{
label: '资产标识',
prop: 'identifier',
@@ -118,8 +123,8 @@
config: { clearable: true }
},
{ label: '套餐ID', prop: 'package_id', type: 'input', config: { clearable: true } },
{ label: '运营商账户ID', prop: 'carrier_id', type: 'input', config: { clearable: true } }
]
carrierItem.value
])
function reset() {
Object.assign(filters, emptyFilters())

View File

@@ -37,7 +37,15 @@
<ElTableColumn label="资产" min-width="200">
<template #default="{ row }">
{{ row.asset_type === 'device' ? '设备' : '单卡' }}
{{ row.iccid || row.asset_id || '' }}
{{
row.asset_type === 'device'
? assetDisplay({
device_id: row.asset_id,
device_virtual_no: row.device_virtual_no,
device_imei: row.device_imei
})
: assetDisplay({ iot_card_id: row.asset_id, iccid: row.iccid })
}}
</template>
</ElTableColumn>
<ElTableColumn label="第几次" prop="attempt" width="80" />
@@ -94,6 +102,7 @@
} from '@/config/constants/trafficRecords'
import {
compactQuery,
assetDisplay,
isPlatformAccount,
optionalPositiveInt,
recentTimeRange,

View File

@@ -46,7 +46,9 @@
<ElTableColumn v-if="!isAsset" label="ICCID / 卡 ID" min-width="190">
<template #default="{ row }">{{ row.iccid || row.iot_card_id || '-' }}</template>
</ElTableColumn>
<ElTableColumn v-if="!isAsset" label="运营商账户" prop="carrier_id" width="100" />
<ElTableColumn v-if="!isAsset" label="运营商账户" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ nameOrId(row.carrier_name, row.carrier_id) }}</template>
</ElTableColumn>
<ElTableColumn label="来源" width="90">
<template #default="{ row }">{{ labelOf(TRAFFIC_SOURCE_LABELS, row.source) }}</template>
</ElTableColumn>
@@ -135,7 +137,7 @@
import { computed, reactive, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { TrafficRecordsService } from '@/api/modules/trafficRecords'
import { useRecordList } from '@/composables/useRecordList'
import { useCarrierFilterItem, useRecordList } from '@/composables/useRecordList'
import { useUserStore } from '@/store/modules/user'
import type { SearchFormItem } from '@/types'
import type { RawDataBlock, TrafficCollectionRecord } from '@/types/api/trafficRecords'
@@ -155,8 +157,8 @@
import {
compactQuery,
formatMb,
nameOrId,
optionalBoolean,
optionalPositiveInt,
recentTimeRange,
requireIdentifierForNonPlatform,
toTimeQuery
@@ -174,6 +176,7 @@
)
const isAsset = computed(() => props.mode === 'asset')
const { item: carrierItem } = useCarrierFilterItem(props.mode !== 'asset')
const { info: userInfo } = storeToRefs(useUserStore())
const pageSizes = computed(() => (isAsset.value ? [50, 100] : [20, 50, 100]))
const emptyText = computed(() => (props.mode === 'failure' ? '暂无采集失败记录' : '暂无采集记录'))
@@ -185,7 +188,7 @@
const emptyFilters = () => ({
identifier: '',
carrier_id: '' as string | number,
carrier_id: undefined as number | undefined,
decision: '',
source: '',
scene: '',
@@ -222,7 +225,7 @@
...page,
...time,
identifier,
carrier_id: optionalPositiveInt(filters.carrier_id, '运营商账户 ID'),
carrier_id: filters.carrier_id,
source: filters.source,
scene: filters.scene.trim()
}
@@ -288,7 +291,7 @@
const common = [
input('资产标识', 'identifier', 'ICCID / 虚拟号 / MSISDN / IMEI / SN'),
rangeItem,
input('运营商账户ID', 'carrier_id'),
carrierItem.value,
select('来源', 'source', TRAFFIC_SOURCE_OPTIONS),
input('场景', 'scene')
]

View File

@@ -1,4 +1,6 @@
import { onBeforeUnmount, reactive, ref, shallowRef } from 'vue'
import { computed, onBeforeUnmount, reactive, ref, shallowRef } from 'vue'
import { useGatewayCarriers } from '@/composables/useGateway'
import type { SearchFormItem } from '@/types'
import type { BaseResponse } from '@/types/api/common'
import type { RecordPage } from '@/types/api/trafficRecords'
import { errorMessage } from '@/utils/business/trafficRecords'
@@ -66,3 +68,24 @@ export function useRecordList<T>(
return { rows, total, loading, error, pagination, load, search, changePage, changeSize, clear }
}
/** 运营商账户筛选:按名称远程搜索,提交的是账户 ID(复用网关页面的运营商候选)。 */
export function useCarrierFilterItem(loadOnMount = true) {
const carriers = useGatewayCarriers(loadOnMount)
const item = computed<SearchFormItem>(() => ({
label: '运营商账户',
prop: 'carrier_id',
type: 'select',
options: carriers.options.value,
onChange: ({ val }: { val: unknown }) => carriers.select(val),
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: carriers.search,
loading: carriers.loading.value,
placeholder: '输入运营商名称搜索'
}
}))
return { item, carriers }
}

View File

@@ -62,6 +62,7 @@ export interface TrafficCollectionRecord {
iot_card_id?: number
iccid?: string
carrier_id?: number
carrier_name?: string
source?: string
scene?: string
result?: string
@@ -99,6 +100,7 @@ export interface CardDailyUsage {
iot_card_id: number
iccid?: string
carrier_id?: number
carrier_name?: string
settled_mb: number
month_used_mb: number
vendor_used_mb: number
@@ -119,8 +121,12 @@ export interface PackageUsageDailyRecord {
date: string
package_usage_id: number
package_id: number
package_name?: string
iot_card_id: number
iccid?: string
device_id: number
device_virtual_no?: string
device_imei?: string
daily_usage_mb: number
cumulative_usage_mb: number
updated_at?: string
@@ -137,8 +143,12 @@ export interface PackageTrafficAdjustment {
id: number
created_at: string
package_usage_id: number
package_name?: string
iot_card_id?: number
iccid?: string
device_id?: number
device_virtual_no?: string
device_imei?: string
delta_mb: number
before_usage_mb: number
after_usage_mb: number
@@ -146,6 +156,7 @@ export interface PackageTrafficAdjustment {
after_status?: number
reason: string
operator_id?: number
operator_name?: string
ref_event_id?: string
ref_observation_id?: string
}
@@ -175,6 +186,8 @@ export interface RefundPostProcessAttempt {
asset_type?: string
asset_id?: number
iccid?: string
device_virtual_no?: string
device_imei?: string
attempt: number
step: string
result: string

View File

@@ -125,6 +125,30 @@ export function formatMb(value: unknown): string {
return `${Number.isInteger(number) ? number : number.toFixed(2)} MB`
}
/** 卡 / 设备展示:优先 ICCID、设备号(IMEI),都没有时才显示内部 ID */
export function assetDisplay(row: {
iot_card_id?: number | null
iccid?: string
device_id?: number | null
device_virtual_no?: string
device_imei?: string
}): string {
if (row.iccid) return row.iccid
if (row.device_virtual_no || row.device_imei) {
const no = row.device_virtual_no || row.device_imei
return row.device_imei && row.device_imei !== no
? `${no}(IMEI ${row.device_imei})`
: String(no)
}
if (row.iot_card_id) return `卡ID ${row.iot_card_id}`
if (row.device_id) return `设备ID ${row.device_id}`
return '-'
}
/** 名称为空时退回显示 ID */
export const nameOrId = (name?: string, id?: number | null, idLabel = 'ID'): string =>
name || (id ? `${idLabel} ${id}` : '-')
export function errorMessage(error: unknown): string {
const data = (error as { response?: { data?: { msg?: string; message?: string } } })?.response
?.data