From 78a41513bd01913f352119b4aa5045b2d08be346 Mon Sep 17 00:00:00 2001 From: sexygoat <1538832180@qq.com> Date: Thu, 30 Apr 2026 15:16:15 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AD=97=E6=AE=B5=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E5=92=8C=E8=AE=A2=E5=8D=95=E8=AF=A6=E6=83=85=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business/OperationLogsDialog.vue | 2 +- src/types/api/asset.ts | 2 +- .../components/OperationLogsCard.vue | 396 ++++++++++++++---- .../task-management/device-task/index.vue | 9 +- 4 files changed, 313 insertions(+), 96 deletions(-) diff --git a/src/components/business/OperationLogsDialog.vue b/src/components/business/OperationLogsDialog.vue index af7c7a7..eaa10c2 100644 --- a/src/components/business/OperationLogsDialog.vue +++ b/src/components/business/OperationLogsDialog.vue @@ -3,7 +3,7 @@ v-model="dialogVisible" title="操作审计日志" direction="rtl" - size="60%" + size="70%" :before-close="handleClose" > diff --git a/src/types/api/asset.ts b/src/types/api/asset.ts index ba5c34b..89bd50a 100644 --- a/src/types/api/asset.ts +++ b/src/types/api/asset.ts @@ -73,7 +73,7 @@ export interface AssetResolveResponse { first_commission_paid?: boolean // 一次性佣金是否已发放 // ===== 卡专属字段 (asset_type === 'card' 时) ===== - iccid?: string // 卡 ICCID + iccid?: string // ICCID bound_device_id?: number // 绑定设备 ID bound_device_no?: string // 绑定设备虚拟号 bound_device_name?: string // 绑定设备名称 diff --git a/src/views/asset-management/asset-information/components/OperationLogsCard.vue b/src/views/asset-management/asset-information/components/OperationLogsCard.vue index 52ade71..e216641 100644 --- a/src/views/asset-management/asset-information/components/OperationLogsCard.vue +++ b/src/views/asset-management/asset-information/components/OperationLogsCard.vue @@ -11,22 +11,12 @@ style="width: 180px" @change="handleFilterChange" > - - - - - - - - - - - - - - - - + @@ -77,23 +67,25 @@ - + @@ -135,16 +127,154 @@ assetType?: string } + type TagType = 'primary' | 'success' | 'warning' | 'info' | 'danger' + type ResultStatus = '' | 'success' | 'failed' | 'denied' + + type OperationLogRow = AssetOperationLogItem & { + operator_type_code?: string + error_code?: string + error_msg?: string + request_id?: string + request_path?: string + request_method?: string + ip_address?: string + user_agent?: string + } + interface ChangeItem { + type: 'change' key: string fieldName: string + beforeValue?: any afterValue?: any } + interface MessageItem { + type: 'message' + key: string + fieldName: string + message: string + level: 'info' | 'error' + } + + type DisplayItem = ChangeItem | MessageItem + + const operationTypeOptions = [ + { label: '设备WiFi设置', value: 'device_set_wifi' }, + { label: '设备切卡', value: 'device_switch_card' }, + { label: '设备切卡模式', value: 'device_switch_mode' }, + { label: '设备停机', value: 'device_stop' }, + { label: '设备复机', value: 'device_start' }, + { label: '设备重启', value: 'device_reboot' }, + { label: '设备重置', value: 'device_reset' }, + { label: '设备分配', value: 'device_allocate' }, + { label: '设备回收', value: 'device_recall' }, + { label: '卡分配', value: 'card_allocate' }, + { label: '卡回收', value: 'card_recall' }, + { label: '卡停机', value: 'card_stop' }, + { label: '卡复机', value: 'card_start' }, + { label: '手动停卡', value: 'card_manual_stop' }, + { label: '手动复机', value: 'card_manual_start' }, + { label: '自动停卡', value: 'card_auto_stop' }, + { label: '自动复机', value: 'card_auto_start' }, + { label: '卡轮询状态', value: 'card_polling_status' }, + { label: '资产轮询状态', value: 'asset_polling_status' }, + { label: '实名策略', value: 'asset_realname_policy' }, + { label: '实名状态', value: 'asset_realname_status' }, + { label: '轮询开关', value: 'asset_polling' }, + { label: 'IoT卡导入任务', value: 'iot_card_import_task_create' } + ] as const + + const operationTypeTagMap: Record = { + device_set_wifi: 'primary', + device_switch_card: 'success', + device_switch_mode: 'success', + device_stop: 'danger', + device_start: 'success', + device_reboot: 'warning', + device_reset: 'warning', + device_allocate: 'primary', + device_recall: 'warning', + card_allocate: 'primary', + card_recall: 'warning', + card_stop: 'danger', + card_start: 'success', + card_manual_stop: 'danger', + card_manual_start: 'success', + card_auto_stop: 'danger', + card_auto_start: 'success', + card_polling_status: 'info', + asset_realname_policy: 'info', + asset_realname_status: 'info', + asset_polling: 'info', + asset_polling_status: 'info', + iot_card_import_task_create: 'primary' + } + + const operatorTypeMap: Record = { + system: '系统', + admin_user: '平台用户', + agent_user: '代理账号', + enterprise_user: '企业用户', + api_user: 'API用户' + } + + const fieldNameMap: Record = { + asset_type: '资产类型', + batch_no: '批次号', + card_category: '卡类型', + enable_polling: '轮询开关', + file_key: '文件存储键', + first_realname_at: '首次实名时间', + iccid: 'ICCID', + network_status: '网络状态', + real_name_status: '实名状态', + realname_policy: '实名认证策略', + source_service: '来源服务', + status: '资产状态', + stop_reason: '停机原因', + switch_mode: '切卡模式' + } + + const realnamePolicyMap: Record = { + none: '无需实名', + before_order: '先实名后充值/购买', + after_order: '先充值/购买后实名' + } + + const stopReasonMap: Record = { + '': '无', + no_package: '无可用套餐', + carrier_stopped: '运营商停机', + not_realname: '未实名' + } + + const assetTypeMap: Record = { + iot_card: 'IoT卡', + device: '设备' + } + + const cardCategoryMap: Record = { + normal: '普通卡', + industry: '行业卡' + } + + const assetStatusMap: Record = { + '1': '在库', + '2': '已分销', + '3': '已激活', + '4': '已停用' + } + + const newStatusMap: Record = { + allocated: '已分配', + recalled: '已回收' + } + const props = defineProps() const loading = ref(false) - const logList = ref([]) + const logList = ref([]) const total = ref(0) const pagination = reactive({ page: 1, @@ -153,7 +283,7 @@ const filterForm = reactive({ operation_type: '', - result_status: '' as '' | 'success' | 'failed' | 'denied' + result_status: '' as ResultStatus }) const loadLogs = async () => { @@ -204,30 +334,12 @@ loadLogs() } - const getOperationTypeTag = (type: string) => { - const tagMap: Record = { - device_set_wifi: 'primary', - device_switch_card: 'success', - device_switch_mode: 'success', - device_stop: 'danger', - device_start: 'success', - device_reboot: 'warning', - device_reset: 'warning', - device_allocate: 'primary', - device_recall: 'warning', - card_allocate: 'primary', - card_recall: 'warning', - card_stop: 'danger', - card_start: 'success', - asset_realname_policy: 'info', - asset_realname_status: 'info', - asset_polling: 'info' - } - return tagMap[type] || 'info' + const getOperationTypeTag = (type: string): TagType => { + return operationTypeTagMap[type] || 'info' } - const getResultStatusTag = (status: string) => { - const tagMap: Record = { + const getResultStatusTag = (status: string): TagType => { + const tagMap: Record = { success: 'success', failed: 'danger', denied: 'warning' @@ -244,17 +356,18 @@ return textMap[status] || status } - const getOperatorTypeText = (type: string) => { - const textMap: Record = { - admin_user: '后台用户', - enterprise_user: '企业用户', - api_user: 'API用户' + const getOperatorTypeText = (row: OperationLogRow) => { + if (row.operator_type_code && operatorTypeMap[row.operator_type_code]) { + return operatorTypeMap[row.operator_type_code] } - return textMap[type] || type + if (row.operator_type && operatorTypeMap[row.operator_type]) { + return operatorTypeMap[row.operator_type] + } + return row.operator_type || row.operator_type_code || '-' } const formatValue = (value: any): string => { - if (value === null || value === undefined) return '未知' + if (value === null || value === undefined) return '空' if (typeof value === 'boolean') return value ? '是' : '否' if (typeof value === 'number') return String(value) if (typeof value === 'string') return value || '空' @@ -263,29 +376,32 @@ } const formatFieldValue = (key: string, value: any): string => { + if (typeof value === 'string' && (key.endsWith('_at') || key.endsWith('_time'))) { + return formatDateTime(value) + } if (key === 'enable_polling') return value ? '开启' : '关闭' - if (key === 'realname_policy') { - const map: Record = { - none: '无需实名', - before_order: '先实名后充值', - after_order: '先充值后实名' - } - return map[value] || formatValue(value) - } + if (key === 'realname_policy') return realnamePolicyMap[value] || formatValue(value) if (key === 'real_name_status') return value === 1 ? '已实名' : '未实名' - if (key === 'switch_mode') return value === 0 ? '自动' : '手动' - if (key === 'new_status') { - const map: Record = { - allocated: '已分配', - recalled: '已回收' - } - return map[value] || formatValue(value) + if (key === 'switch_mode') { + return String(value) === '0' ? '自动' : String(value) === '1' ? '手动' : formatValue(value) } + if (key === 'new_status') return newStatusMap[value] || formatValue(value) + if (key === 'network_status') { + return Number(value) === 1 ? '正常' : Number(value) === 0 ? '停机' : formatValue(value) + } + if (key === 'stop_reason') return stopReasonMap[String(value ?? '')] ?? formatValue(value) + if (key === 'asset_type') return assetTypeMap[String(value)] || formatValue(value) + if (key === 'card_category') return cardCategoryMap[String(value)] || formatValue(value) + if (key === 'status') return assetStatusMap[String(value)] || formatValue(value) + if (key === 'source_service') return value === 'asset_polling' ? '资产轮询' : formatValue(value) return formatValue(value) } const sanitizeFieldName = (fieldName: string): string => { - return fieldName.trim().replace(/([^)]*)/g, '').trim() + return fieldName + .trim() + .replace(/([^)]*)/g, '') + .trim() } const shouldHideIdField = (key: string, fieldName: string): boolean => { @@ -304,36 +420,127 @@ return false } - return ( + if ( normalizedKeyLower === 'id' || + normalizedFieldName === '绑定记录ID' || + normalizedFieldName === 'ID' + ) { + return true + } + + return ( normalizedKeyLower.endsWith('_id') || /^[a-z][a-z0-9]*Id$/.test(normalizedKey) || - normalizedFieldName === '绑定记录ID' || - normalizedFieldName === 'ID' || normalizedFieldName.endsWith('ID') ) } - const getChangeContent = (row: AssetOperationLogItem): ChangeItem[] => { - const after = row.operation_content_after - const desc = row.operation_fields_desc || {} + const extractOperationContent = ( + row: OperationLogRow, + type: 'before' | 'after' + ): Record => { + const directContent = + type === 'before' ? row.operation_content_before : row.operation_content_after + if (directContent && Object.keys(directContent).length > 0) { + return directContent + } - if (!after || Object.keys(after).length === 0) return [] + const fallbackContainer = type === 'before' ? row.before_data : row.after_data + const nestedContent = fallbackContainer?.operation_content + if (nestedContent && Object.keys(nestedContent).length > 0) { + return nestedContent + } + + return fallbackContainer || {} + } + + const isSameValue = (beforeValue: any, afterValue: any) => { + if (beforeValue === afterValue) return true + return JSON.stringify(beforeValue) === JSON.stringify(afterValue) + } + + const getChangeContent = (row: OperationLogRow): ChangeItem[] => { + const before = extractOperationContent(row, 'before') + const after = extractOperationContent(row, 'after') + const desc = row.operation_fields_desc || {} + const keys = new Set([...Object.keys(before), ...Object.keys(after)]) const changes: ChangeItem[] = [] - for (const key of Object.keys(after)) { - const fieldName = sanitizeFieldName(desc[key] || key) + for (const key of keys) { + const fieldName = sanitizeFieldName(desc[key] || fieldNameMap[key] || key) if (shouldHideIdField(key, fieldName)) continue + const beforeValue = before[key] + const afterValue = after[key] + if (isSameValue(beforeValue, afterValue)) continue + changes.push({ + type: 'change', key, fieldName, - afterValue: after[key] + beforeValue, + afterValue }) } + return changes } + const getMessageItems = (row: OperationLogRow): MessageItem[] => { + const items: MessageItem[] = [] + const seen = new Set() + + const pushMessage = ( + key: string, + fieldName: string, + message?: string, + level: 'info' | 'error' = 'info' + ) => { + const normalizedMessage = message?.trim() + if (!normalizedMessage) return + + const duplicateKey = `${fieldName}:${normalizedMessage}` + if (seen.has(duplicateKey)) return + seen.add(duplicateKey) + + items.push({ + type: 'message', + key, + fieldName, + message: normalizedMessage, + level + }) + } + + pushMessage('error_msg', '失败原因', row.error_msg, 'error') + pushMessage('reason', '原因说明', row.reason, row.result_status === 'failed' ? 'error' : 'info') + + if (row.fail_count > 0) { + pushMessage('fail_count', '批量结果', `失败 ${row.fail_count} 项`, 'error') + } + + row.failed_items?.slice(0, 3).forEach((item, index) => { + const detail = item?.iccid + ? `${item.iccid}:${item.reason || '未知原因'}` + : item?.reason || '' + pushMessage(`failed_items_${index}`, '失败明细', detail, 'error') + }) + + if (!items.length && row.result_status === 'failed') { + pushMessage('result_status', '执行结果', '执行失败', 'error') + } + + if (!items.length && row.result_status === 'denied') { + pushMessage('result_status', '执行结果', '操作被拒绝', 'info') + } + + return items + } + + const getDisplayContent = (row: OperationLogRow): DisplayItem[] => { + return [...getChangeContent(row), ...getMessageItems(row)] + } + watch( () => props.assetIdentifier, (newVal) => { @@ -382,27 +589,36 @@ .change-item { display: flex; - align-items: center; + align-items: flex-start; gap: 4px; font-size: 12px; .field-name { color: var(--el-text-color-secondary); - min-width: 70px; + min-width: 72px; } .field-value { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 4px; + word-break: break-all; + .new-value { color: var(--el-color-success); } + + .message-value { + color: var(--el-text-color-regular); + + &.is-error { + color: var(--el-color-danger); + } + } } } } - - .fail-reason { - color: var(--el-color-danger); - font-size: 12px; - } } } } diff --git a/src/views/asset-management/task-management/device-task/index.vue b/src/views/asset-management/task-management/device-task/index.vue index 86867ef..794c86d 100644 --- a/src/views/asset-management/task-management/device-task/index.vue +++ b/src/views/asset-management/task-management/device-task/index.vue @@ -151,7 +151,7 @@ import { useAuth } from '@/composables/useAuth' import { formatDateTime } from '@/utils/business/format' import { StorageService } from '@/api/modules/storage' - import type { DeviceImportTask, DeviceImportTaskStatus } from '@/types/api/device' + import type { DeviceImportTask, DeviceImportTaskStatus, RealnamePolicy } from '@/types/api/device' import { RoutesAlias } from '@/router/routesAlias' import { generatePackageCode } from '@/utils/codeGenerator' @@ -408,7 +408,9 @@ const res = await DeviceService.getImportTasks(params) if (res.code === 0) { - taskList.value = res.data.items || [] + const taskItems = (res.data as typeof res.data & { items?: DeviceImportTask[] | null }) + .items + taskList.value = taskItems || [] pagination.total = res.data.total || 0 } } catch (error) { @@ -503,7 +505,6 @@ importForm.realname_policy = '' importDialogVisible.value = false } - // 提交上传 const submitUpload = async () => { if (!fileList.value.length) { @@ -540,7 +541,7 @@ const importRes = await DeviceService.importDevices({ file_key, batch_no: importForm.batch_no || undefined, - realname_policy: importForm.realname_policy || undefined + realname_policy: (importForm.realname_policy || undefined) as RealnamePolicy | undefined }) if (importRes.code !== 0) {