refactor(audit): 审计中文映射收敛为后端唯一来源
问题:同一套「字段名 → 中文」映射存了三份手工同步的副本 ——
前端 AuditLog.vue 的 fieldMap(约 95 个字段)、后端 api/v1/audit.py 的
ACTION_ALIASES,另有若干内联的 status 映射散落在 service 层。
改一边漏一边就会漂移,页面上冒出英文列名或对不上的中文。
改动:
· 新增 app/utils/audit_labels.py 作为**唯一来源**,统一提供
操作类型归一化(canon_action)、字段名中文化(field_label)、
码值中文化(enum_label / bool_label / person_name_label)、
ID 指向登记(id_ref_of)。
· api/v1/audit.py 改为从该模块 import,删掉本地副本。
· 新增 GET /audit/labels 下发映射(静态标签,免权限码;审计页本身
已由 system_audit 把关)。
· 前端 AuditLog.vue 删除本地 95 行 fieldMap,改从接口拉取;
拉取失败时未命中项原样显示,是可控降级。
★ 码值必须**按模块**翻译:同名 status 在出库/借还/报废里含义完全不同
(出库的 3 是「已出库」,报废的 3 是「已执行(已报废)」),
不区分模块会把报废单显示成已出库。借还模块下还混着字符串状态
(borrowed/returned),因其两张表共用 status 列名。
故映射按 (模块, 字段) 匹配,未登记再退回字段名。
★ ID 字段同理:parent_id 在「系统管理」里是菜单的上级菜单,在「BOM管理」
里是物料节点 —— 指向完全不同的表。
验证:前端 vue-tsc 改前改后均为 565 个错误、去除行号后**报错集合完全相同**,
未引入新错误;GET /audit/labels 实测返回 200。
This commit is contained in:
@ -24,3 +24,12 @@ export function getAuditModules() {
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取操作类型 / 字段名的中文映射
|
||||
// ★ 后端为唯一来源,前端不再自带副本(见 views/system/AuditLog.vue 的说明)
|
||||
export function getAuditLabels() {
|
||||
return request({
|
||||
url: '/audit/labels',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
@ -155,12 +155,12 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="修改前" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<span class="old-value">{{ row.old ?? '空' }}</span>
|
||||
<span class="old-value">{{ valueLabel(row.field, row.old) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="修改后" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<span class="new-value">{{ row.new ?? '空' }}</span>
|
||||
<span class="new-value">{{ valueLabel(row.field, row.new) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@ -179,7 +179,7 @@
|
||||
:label="fieldLabel(key)"
|
||||
:span="isLongValue(value) ? 2 : 1"
|
||||
>
|
||||
<span class="snapshot-value">{{ formatValue(value) }}</span>
|
||||
<span class="snapshot-value">{{ formatValue(value, String(key)) }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
@ -197,7 +197,7 @@
|
||||
:label="fieldLabel(key)"
|
||||
:span="isLongValue(value) ? 2 : 1"
|
||||
>
|
||||
<span class="snapshot-value">{{ formatValue(value) }}</span>
|
||||
<span class="snapshot-value">{{ formatValue(value, String(key)) }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
@ -222,7 +222,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { Search, Refresh, EditPen, Delete, Plus, Document } from '@element-plus/icons-vue'
|
||||
import { getAuditLogs, getAuditModules } from '@/api/audit'
|
||||
import { getAuditLogs, getAuditModules, getAuditLabels } from '@/api/audit'
|
||||
|
||||
// 表格数据
|
||||
const tableLoading = ref(false)
|
||||
@ -251,139 +251,68 @@ const moduleOptions = ref<string[]>([])
|
||||
const actionOptions = ref<string[]>(['create', 'update', 'delete', 'export', 'import'])
|
||||
|
||||
// ============================================================
|
||||
// 中文化映射
|
||||
// ============================================================
|
||||
|
||||
// 操作类型中文化映射
|
||||
// 同时兼容两种大小写:当前监听器写入 UPDATE/CREATE/DELETE(大写),
|
||||
// 历史数据中存在小写与零星中文(如 '修改' / '批量删除'),均需可读。
|
||||
const actionMap: Record<string, string> = {
|
||||
'UPDATE': '修改', 'update': '修改',
|
||||
'CREATE': '新增', 'create': '新增', 'INSERT': '新增', 'insert': '新增',
|
||||
'DELETE': '删除', 'delete': '删除',
|
||||
'LOGIN': '登录', 'login': '登录',
|
||||
'LOGOUT': '登出', 'logout': '登出'
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 字段名中文化映射
|
||||
// 中文化映射 —— ★ 后端为唯一来源,前端不再自带副本
|
||||
//
|
||||
// ★ 覆盖范围:审批单 / 流水 / 库存 / 主数据 / 系统管理 五类核心业务表。
|
||||
// 未命中的字段由 fieldLabel() 兜底为「原字段名」,不会再出现大面积英文列名。
|
||||
// ★ 用法:变更对比、新增详情、删除快照三个区块统一经 fieldLabel() 取值。
|
||||
// (改造前仅"变更对比"区用了 fieldMap,另外两区直接渲染原始 key,
|
||||
// 这是"详情里一堆英文列名"的直接原因。)
|
||||
// 改造前这里硬编码了 actionMap 与 fieldMap 两份映射,后端 api/v1/audit.py
|
||||
// 里又有一份 ACTION_ALIASES。三份手工同步的副本必然漂移:改一边漏一边,
|
||||
// 页面上就会冒出英文列名、或出现对不上的中文 —— 这是已经踩过的坑。
|
||||
//
|
||||
// 现在统一由 GET /audit/labels 下发(后端 app/utils/audit_labels.py 是唯一来源)。
|
||||
// 拉取失败时两个 map 为空,fieldLabel() / actionMap 对未命中项**原样返回**,
|
||||
// 页面退化成显示原始字段名,功能不受影响 —— 可控降级,不是崩溃。
|
||||
// ============================================================
|
||||
const fieldMap: Record<string, string> = {
|
||||
// --- 通用 ---
|
||||
'id': 'ID',
|
||||
'name': '名称',
|
||||
'title': '标题',
|
||||
'remark': '备注',
|
||||
'reason': '原因',
|
||||
'status': '状态',
|
||||
'created_at': '创建时间',
|
||||
'updated_at': '更新时间',
|
||||
'is_active': '是否启用',
|
||||
'is_enabled': '是否启用',
|
||||
'company_name': '所属公司',
|
||||
'operator_name': '操作人',
|
||||
'operator': '操作人',
|
||||
const actionMap = ref<Record<string, string>>({})
|
||||
|
||||
// --- 物料主数据 ---
|
||||
'material_name': '物料名称',
|
||||
'spec_model': '规格型号',
|
||||
'category': '类别',
|
||||
'material_type': '物料类型',
|
||||
'unit': '单位',
|
||||
'base_id': '物料ID',
|
||||
'sku': 'SKU',
|
||||
'batch_number': '批次号',
|
||||
'serial_number': '序列号',
|
||||
'barcode': '条码',
|
||||
'warehouse_location': '库位',
|
||||
'warehouse_loc': '库位',
|
||||
'reference_price': '参考价格',
|
||||
'is_approval_required': '是否需审批',
|
||||
'is_inspection_required': '是否需质检',
|
||||
// 字段名中文化映射 —— 与 actionMap 同源,同样由后端下发
|
||||
const fieldMap = ref<Record<string, string>>({})
|
||||
|
||||
// --- 库存数量 ---
|
||||
'in_quantity': '入库数量',
|
||||
'stock_quantity': '总库存',
|
||||
'available_quantity': '可用库存',
|
||||
'out_quantity': '出库数量',
|
||||
'quantity': '数量',
|
||||
'pre_tax_unit_price': '不含税单价',
|
||||
'post_tax_unit_price': '含税单价',
|
||||
'unit_price': '单价',
|
||||
'total_price': '总价',
|
||||
'tax_rate': '税率',
|
||||
'supplier_name': '供应商',
|
||||
'buyer_name': '采购员',
|
||||
|
||||
// --- 审批单 ---
|
||||
'request_no': '申请单号',
|
||||
'applicant_id': '申请人ID',
|
||||
'allowed_approvers': '允许审批人',
|
||||
'actual_approver_id': '实际审批人ID',
|
||||
'approved_at': '审批时间',
|
||||
'reject_reason': '驳回原因',
|
||||
'items_json': '物料明细',
|
||||
'outbound_type': '出库类型',
|
||||
'consumer_name': '领用人/客户',
|
||||
'borrower_name': '借用人',
|
||||
'executed_at': '执行时间',
|
||||
'executor_name': '执行人',
|
||||
|
||||
// --- 流水 ---
|
||||
'outbound_no': '出库单号',
|
||||
'outbound_time': '出库时间',
|
||||
'borrow_no': '借出单号',
|
||||
'borrow_time': '借出时间',
|
||||
'expected_return_time': '预计归还时间',
|
||||
'return_time': '归还时间',
|
||||
'return_operator': '归还操作人',
|
||||
'is_returned': '是否已归还',
|
||||
'scrap_request_no': '报废申请单号',
|
||||
'operation_time': '操作时间',
|
||||
'source_table': '来源表',
|
||||
'stock_id': '库存ID',
|
||||
'cost_at_scrap': '报废成本',
|
||||
'total_loss': '损失金额',
|
||||
'signature_path': '签名',
|
||||
|
||||
// --- 采购 / BOM ---
|
||||
'purchase_date': '采购日期',
|
||||
'requester_id': '申请人ID',
|
||||
'approver_id': '审批人ID',
|
||||
'bom_no': 'BOM编号',
|
||||
'bom_version': 'BOM版本',
|
||||
'parent_id': '父级ID',
|
||||
'child_id': '子级ID',
|
||||
|
||||
// --- 系统管理 ---
|
||||
'username': '用户名',
|
||||
'display_name': '显示名',
|
||||
'email': '邮箱',
|
||||
'role': '角色',
|
||||
'department': '部门',
|
||||
'password_hash': '密码哈希',
|
||||
'code': '编码',
|
||||
'path': '路径',
|
||||
'sort_order': '排序',
|
||||
'is_visible': '是否可见',
|
||||
'menu_code': '菜单编码',
|
||||
'element_type': '元素类型',
|
||||
'role_code': '角色编码',
|
||||
'target_code': '目标编码',
|
||||
'user_agent': '浏览器标识',
|
||||
'ip_address': 'IP地址'
|
||||
};
|
||||
// 码值中文化映射。
|
||||
// ★ status 必须**按模块**查表:同名 status 在出库/借还/报废里含义不同
|
||||
// (出库的 3 是「已出库」,报废的 3 是「已执行(已报废)」),
|
||||
// 不区分模块就会把报废单显示成已出库。
|
||||
const statusMapByModule = ref<Record<string, Record<string, string>>>({})
|
||||
const defaultStatusMap = ref<Record<string, string>>({})
|
||||
const enumFields = ref<string[]>(['status'])
|
||||
const booleanFields = ref<string[]>([])
|
||||
|
||||
// 字段中文名解析:命中映射取中文,否则原样返回
|
||||
const fieldLabel = (key: string | number): string => {
|
||||
const k = String(key)
|
||||
return fieldMap[k] || k
|
||||
return fieldMap.value[k] || k
|
||||
}
|
||||
|
||||
// 变更值中文化:布尔 → 是/否;枚举(status)→ 按模块查表;其余原样返回。
|
||||
// 查不到就显示原值 —— 宁可看到裸数字,也不要显示一个可能错的中文。
|
||||
const valueLabel = (field: string, value: any): string => {
|
||||
if (value === null || value === undefined || value === '') return '空'
|
||||
if (typeof value === 'object') return JSON.stringify(value)
|
||||
|
||||
if (booleanFields.value.includes(field)) return value ? '是' : '否'
|
||||
|
||||
if (enumFields.value.includes(field)) {
|
||||
const mod = String(currentLog.value?.module || '')
|
||||
const map = statusMapByModule.value[mod] || defaultStatusMap.value || {}
|
||||
const hit = map[String(value)]
|
||||
if (hit) return hit
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
// 拉取中文映射(后端唯一来源)。失败不阻断页面 —— 退化为显示原始字段名与原值。
|
||||
const loadLabels = async () => {
|
||||
try {
|
||||
const res = await getAuditLabels()
|
||||
if (res.code === 200 && res.data) {
|
||||
actionMap.value = res.data.action || {}
|
||||
fieldMap.value = res.data.field || {}
|
||||
statusMapByModule.value = res.data.status || {}
|
||||
defaultStatusMap.value = res.data.defaultStatus || {}
|
||||
enumFields.value = res.data.enumFields || ['status']
|
||||
booleanFields.value = res.data.booleanFields || []
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[AuditLog] 审计中文映射拉取失败,字段名与码值将原样显示', e)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@ -521,10 +450,11 @@ const rawJson = computed(() => {
|
||||
})
|
||||
|
||||
// 辅助函数:格式化值
|
||||
const formatValue = (value: any): string => {
|
||||
// field 传入时走码值中文化(快照里的 status 同样是裸码值)
|
||||
const formatValue = (value: any, field?: string): string => {
|
||||
if (value === null || value === undefined) return '-'
|
||||
if (typeof value === 'object') return JSON.stringify(value)
|
||||
return String(value)
|
||||
return field ? valueLabel(field, value) : String(value)
|
||||
}
|
||||
|
||||
// 辅助函数:判断是否是长值
|
||||
@ -606,6 +536,7 @@ const handleViewDetails = (row: any) => {
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
loadLabels()
|
||||
getAuditModules().then(res => {
|
||||
if (res.code === 200 && res.data) {
|
||||
moduleOptions.value = res.data
|
||||
|
||||
Reference in New Issue
Block a user