This commit is contained in:
2026-08-18 09:46:34 +08:00
parent dd87d4d916
commit 834382e04b
4 changed files with 179 additions and 26 deletions
@@ -29,6 +29,17 @@ describe('CustomerAuditLogsPanel', () => {
expect(wrapper.text()).toContain('127.0.0.1')
expect(wrapper.text()).toContain('编辑客户档案')
expect(wrapper.text()).not.toContain('PLATFORM_TENANT_UPDATE')
expect(wrapper.text()).toContain('平台管理员')
expect(wrapper.text()).toContain('客户与权限服务')
})
it('shows inbound audit events in Chinese without wrapping technical codes into the table', async () => {
vi.mocked(listPlatformAuditLogs).mockResolvedValue({ records: [{ ...row, eventType: 'INBOUND_MEASURE_APPROVE', description: '测量审核通过', serviceName: 'firstleg-service' }], page: 1, size: 20, total: 1, pages: 1 })
const wrapper = mountPanel(['platform:audit:list']); await flushPromises()
expect(wrapper.text()).toContain('入库测量审核通过')
expect(wrapper.text()).toContain('头程履约服务')
expect(wrapper.text()).not.toContain('INBOUND_MEASURE_APPROVE')
})
it('shows the server total instead of the current page row count', async () => {
@@ -80,12 +91,17 @@ describe('CustomerAuditLogsPanel', () => {
})
it('loads detail and formats json snapshots', async () => {
vi.mocked(getPlatformAuditLog).mockResolvedValue({ ...row, httpMethod: 'PUT', requestUri: '/tenants/8', clientIp: '127.0.0.1', durationMs: 12, beforeData: '{"name":"旧名称"}', afterData: '{"name":"新名称"}' })
vi.mocked(getPlatformAuditLog).mockResolvedValue({ ...row, eventType: 'INBOUND_MEASURE_APPROVE', resourceType: 'INBOUND_MEASUREMENT', serviceName: 'firstleg-service', httpMethod: 'PUT', requestUri: '/tenants/8', clientIp: '127.0.0.1', durationMs: 12, beforeData: '{"name":"旧名称"}', afterData: '{"name":"新名称"}' })
const wrapper = mountPanel(['platform:audit:list']); await flushPromises()
await wrapper.get('button[aria-label="查看操作日志1详情"]').trigger('click'); await flushPromises()
expect(getPlatformAuditLog).toHaveBeenCalledWith('1')
expect(wrapper.text()).toContain('旧名称')
expect(wrapper.text()).toContain('新名称')
expect(wrapper.text()).toContain('入库测量审核通过')
expect(wrapper.text()).toContain('入库测量')
expect(wrapper.text()).toContain('头程履约服务')
expect(wrapper.text()).toContain('更新')
expect(wrapper.find('.audit-detail-grid').exists()).toBe(true)
})
it('loads the selected audit page', async () => {
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { Eye, ScrollText } from '@lucide/vue'
import { CircleCheck, CircleX, Eye, ScrollText } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
@@ -9,7 +9,7 @@ import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.v
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { getPlatformAuditLog, listPlatformAuditLogs, type PlatformAuditLogDetail, type PlatformAuditLogListItem, type PlatformAuditLogQuery } from '../api/platformAudit'
import { auditEventLabel } from '../presentation'
import { auditDescriptionLabel, auditEventLabel, auditHttpMethodLabel, auditOperatorLabel, auditResourceLabel, auditServiceLabel } from '../presentation'
const props = defineProps<{ customerId: string; customerName: string }>()
const access = useAccessPreviewStore()
@@ -36,11 +36,11 @@ const queryFields: QueryField[] = [
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 92, fixed: 'left', slotName: 'actions' },
{ key: 'description', label: '操作内容', minWidth: 190, slotName: 'description' },
{ key: 'description', label: '操作内容', minWidth: 220, slotName: 'description' },
{ key: 'clientIp', label: 'IP 地址', width: 140, slotName: 'clientIp' },
{ key: 'eventType', label: '事件类型', minWidth: 220, slotName: 'eventType' },
{ key: 'username', label: '操作人', width: 140, slotName: 'username' },
{ key: 'serviceName', label: '来源服务', width: 150, slotName: 'serviceName' },
{ key: 'eventType', label: '事件类型', minWidth: 180, slotName: 'eventType' },
{ key: 'username', label: '操作人', width: 130, slotName: 'username' },
{ key: 'serviceName', label: '来源服务', width: 145, slotName: 'serviceName' },
{ key: 'success', label: '结果', width: 90, slotName: 'success' },
{ key: 'occurredAt', label: '发生时间', width: 170, slotName: 'occurredAt' },
]
@@ -133,34 +133,49 @@ watch([page, pageSize], ([nextPage, nextSize], [previousPage, previousSize]) =>
<LoadErrorState v-if="loadError" title="客户操作日志加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
<DataTable v-else v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" row-key="id" aria-label="客户操作日志列表" empty-text="当前客户暂无操作日志" preference-key="customer-audit-logs" border stripe>
<template #actions="{ row }"><el-button link type="primary" :aria-label="`查看操作日志${(row as PlatformAuditLogListItem).id}详情`" @click="openDetail(row as PlatformAuditLogListItem)"><Eye :size="15" />详情</el-button></template>
<template #description="{ row }">{{ display((row as PlatformAuditLogListItem).description) }}</template>
<template #description="{ row }"><el-tooltip :content="auditDescriptionLabel((row as PlatformAuditLogListItem).description)" placement="top" :show-after="400"><span class="cell-clamp">{{ auditDescriptionLabel((row as PlatformAuditLogListItem).description) }}</span></el-tooltip></template>
<template #clientIp="{ row }">{{ display((row as PlatformAuditLogListItem).clientIp) }}</template>
<template #eventType="{ row }">{{ auditEventLabel((row as PlatformAuditLogListItem).eventType) }}</template>
<template #username="{ row }">{{ display((row as PlatformAuditLogListItem).username) }}</template>
<template #serviceName="{ row }">{{ display((row as PlatformAuditLogListItem).serviceName) }}</template>
<template #eventType="{ row }"><el-tooltip :content="(row as PlatformAuditLogListItem).eventType" placement="top" :show-after="500"><span class="event-label">{{ auditEventLabel((row as PlatformAuditLogListItem).eventType) }}</span></el-tooltip></template>
<template #username="{ row }">{{ auditOperatorLabel((row as PlatformAuditLogListItem).username) }}</template>
<template #serviceName="{ row }"><el-tooltip :content="display((row as PlatformAuditLogListItem).serviceName)" placement="top" :show-after="500"><span class="service-label">{{ auditServiceLabel((row as PlatformAuditLogListItem).serviceName) }}</span></el-tooltip></template>
<template #success="{ row }"><el-tag :type="(row as PlatformAuditLogListItem).success ? 'success' : 'danger'">{{ (row as PlatformAuditLogListItem).success ? '成功' : '失败' }}</el-tag></template>
<template #occurredAt="{ row }">{{ formatDate((row as PlatformAuditLogListItem).occurredAt) }}</template>
<template #pagination-left>共 {{ total }} 条客户操作记录</template>
</DataTable>
</template>
<AppDrawer v-model="detailOpen" title="操作日志详情" size="min(860px, 94vw)" :loading="detailLoading" :show-footer="false">
<AppDrawer v-model="detailOpen" title="操作日志详情" size="min(920px, 96vw)" :loading="detailLoading" :show-footer="false">
<LoadErrorState v-if="detailError" title="详情加载失败" :message="detailError.message" :loading="detailLoading"><template #details><ApiErrorMeta :details="detailError" /></template></LoadErrorState>
<template v-else-if="detail">
<el-descriptions :column="2" border>
<el-descriptions-item label="操作内容">{{ display(detail.description) }}</el-descriptions-item><el-descriptions-item label="结果">{{ detail.success ? '成功' : '失败' }}</el-descriptions-item>
<el-descriptions-item label="操作人">{{ display(detail.username) }}</el-descriptions-item><el-descriptions-item label="发生时间">{{ formatDate(detail.occurredAt) }}</el-descriptions-item>
<el-descriptions-item label="事件类型">{{ auditEventLabel(detail.eventType) }}</el-descriptions-item><el-descriptions-item label="资源">{{ detail.resourceType }} / {{ display(detail.resourceId) }}</el-descriptions-item>
<el-descriptions-item label="请求">{{ display(detail.httpMethod) }} {{ display(detail.requestUri) }}</el-descriptions-item><el-descriptions-item label="耗时">{{ detail.durationMs == null ? '—' : `${detail.durationMs} ms` }}</el-descriptions-item>
<el-descriptions-item label="来源服务">{{ display(detail.serviceName) }}</el-descriptions-item><el-descriptions-item label="请求标识">{{ display(detail.requestId) }}</el-descriptions-item>
<el-descriptions-item v-if="!detail.success" label="失败原因" :span="2">{{ display(detail.errorMessage || detail.errorCode) }}</el-descriptions-item>
</el-descriptions>
<div class="snapshot-grid"><section><h3>操作前快照</h3><pre>{{ formatSnapshot(detail.beforeData) }}</pre></section><section><h3>操作结果快照</h3><pre>{{ formatSnapshot(detail.afterData) }}</pre></section></div>
<section class="audit-detail-hero" :class="detail.success ? 'is-success' : 'is-failed'">
<span class="result-icon" aria-hidden="true"><CircleCheck v-if="detail.success" :size="22" /><CircleX v-else :size="22" /></span>
<div class="hero-copy"><span>事件类型</span><h3>{{ auditEventLabel(detail.eventType) }}</h3><p>{{ auditDescriptionLabel(detail.description) }}</p></div>
<el-tag :type="detail.success ? 'success' : 'danger'" effect="light">{{ detail.success ? '操作成功' : '操作失败' }}</el-tag>
</section>
<section class="audit-detail-grid" aria-label="操作日志基础信息">
<div><span>操作人</span><strong>{{ auditOperatorLabel(detail.username) }}</strong></div>
<div><span>发生时间</span><strong>{{ formatDate(detail.occurredAt) }}</strong></div>
<div><span>来源服务</span><strong>{{ auditServiceLabel(detail.serviceName) }}</strong><small>{{ display(detail.serviceName) }}</small></div>
<div><span>客户端 IP</span><strong>{{ display(detail.clientIp) }}</strong></div>
<div><span>业务资源</span><strong>{{ auditResourceLabel(detail.resourceType) }}</strong><small>{{ display(detail.resourceId) }}</small></div>
<div><span>处理耗时</span><strong>{{ detail.durationMs == null ? '—' : `${detail.durationMs} ms` }}</strong></div>
<div class="wide request-cell"><span>请求地址</span><div><el-tag size="small" effect="plain">{{ auditHttpMethodLabel(detail.httpMethod) }}</el-tag><code>{{ display(detail.requestUri) }}</code></div></div>
<div class="wide identity-cell"><span>请求标识</span><strong>{{ display(detail.requestId) }}</strong><small v-if="detail.traceId">链路标识:{{ detail.traceId }}</small></div>
</section>
<el-alert v-if="!detail.success" class="failure-alert" type="error" :closable="false" show-icon :title="display(detail.errorMessage || detail.errorCode)" />
<div class="snapshot-grid"><section><h3>操作前快照</h3><p>操作执行前保留的业务数据</p><pre>{{ formatSnapshot(detail.beforeData) }}</pre></section><section><h3>操作结果快照</h3><p>操作执行后返回或保存的业务数据</p><pre>{{ formatSnapshot(detail.afterData) }}</pre></section></div>
</template>
</AppDrawer>
</section>
</template>
<style scoped>
.customer-audit-panel { min-width:0; }.panel-title { display:flex; align-items:flex-start; justify-content:space-between; gap:16px; margin-bottom:18px; padding-bottom:14px; border-bottom:1px solid var(--cc-border); }.panel-title h2 { margin:0; color:#344054; font-size:16px; }.panel-title p { margin:5px 0 0; color:var(--cc-muted); font-size:11px; }.panel-title > svg { color:var(--cc-primary); }.audit-query { margin-bottom:16px; }.module-empty { display:flex; min-height:260px; align-items:center; justify-content:center; flex-direction:column; gap:7px; color:#98a2b3; text-align:center; }.module-empty strong { color:#475467; font-size:13px; }.module-empty p { margin:0; color:var(--cc-muted); font-size:12px; }.snapshot-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:16px; margin-top:18px; }.snapshot-grid section { min-width:0; }.snapshot-grid h3 { margin:0 0 8px; color:#475467; font-size:14px; }.snapshot-grid pre { min-height:160px; margin:0; padding:14px; overflow:auto; border:1px solid var(--cc-border); border-radius:6px; background:#f8fafc; color:#344054; font:12px/1.6 ui-monospace,SFMono-Regular,Consolas,monospace; white-space:pre-wrap; overflow-wrap:anywhere; }@media(max-width:720px){.snapshot-grid{grid-template-columns:1fr}}
.customer-audit-panel { min-width:0; }.panel-title { display:flex; align-items:flex-start; justify-content:space-between; gap:16px; margin-bottom:18px; padding-bottom:14px; border-bottom:1px solid var(--cc-border); }.panel-title h2 { margin:0; color:#344054; font-size:16px; }.panel-title p { margin:5px 0 0; color:var(--cc-muted); font-size:11px; }.panel-title > svg { color:var(--cc-primary); }.audit-query { margin-bottom:16px; }.module-empty { display:flex; min-height:260px; align-items:center; justify-content:center; flex-direction:column; gap:7px; color:#98a2b3; text-align:center; }.module-empty strong { color:#475467; font-size:13px; }.module-empty p { margin:0; color:var(--cc-muted); font-size:12px; }
.cell-clamp { display:-webkit-box; overflow:hidden; -webkit-box-orient:vertical; -webkit-line-clamp:2; line-height:1.45; overflow-wrap:anywhere; }.event-label { display:inline-flex; max-width:100%; padding:3px 9px; overflow:hidden; border:1px solid #dbeafe; border-radius:999px; background:#eff6ff; color:#1d4ed8; font-size:12px; line-height:1.35; text-overflow:ellipsis; white-space:nowrap; }.service-label { display:block; overflow:hidden; color:#475467; text-overflow:ellipsis; white-space:nowrap; }
.audit-detail-hero { display:flex; align-items:flex-start; gap:13px; padding:16px 18px; border:1px solid #d1fadf; border-radius:10px; background:#f6fef9; }.audit-detail-hero.is-failed { border-color:#fee4e2; background:#fff7f6; }.result-icon { display:grid; width:38px; height:38px; flex:0 0 auto; place-items:center; border-radius:50%; background:#dcfae6; color:#079455; }.is-failed .result-icon { background:#fee4e2; color:#d92d20; }.hero-copy { min-width:0; flex:1; }.hero-copy > span { color:#667085; font-size:12px; }.hero-copy h3 { margin:3px 0 4px; color:#101828; font-size:17px; }.hero-copy p { margin:0; color:#475467; font-size:13px; line-height:1.55; }
.audit-detail-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); margin-top:16px; overflow:hidden; border:1px solid #e4e7ec; border-radius:10px; background:#fff; }.audit-detail-grid > div { display:flex; min-width:0; min-height:82px; flex-direction:column; justify-content:center; gap:5px; padding:13px 16px; border-right:1px solid #eaecf0; border-bottom:1px solid #eaecf0; }.audit-detail-grid > div:nth-child(3n) { border-right:0; }.audit-detail-grid > div.wide { grid-column:1/-1; border-right:0; }.audit-detail-grid > div:last-child { border-bottom:0; }.audit-detail-grid span { color:#667085; font-size:12px; }.audit-detail-grid strong { overflow-wrap:anywhere; color:#1d2939; font-size:14px; font-weight:600; }.audit-detail-grid small { overflow-wrap:anywhere; color:#98a2b3; font-size:11px; }.request-cell > div { display:flex; min-width:0; align-items:center; gap:9px; }.request-cell code { min-width:0; overflow-wrap:anywhere; color:#344054; font:12px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace; }.failure-alert { margin-top:16px; }
.snapshot-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:16px; margin-top:18px; }.snapshot-grid section { min-width:0; padding:15px; border:1px solid #e4e7ec; border-radius:10px; background:#fff; }.snapshot-grid h3 { margin:0; color:#344054; font-size:14px; }.snapshot-grid p { margin:4px 0 10px; color:#98a2b3; font-size:11px; }.snapshot-grid pre { min-height:160px; max-height:360px; margin:0; padding:14px; overflow:auto; border:1px solid #eaecf0; border-radius:8px; background:#f8fafc; color:#344054; font:12px/1.65 ui-monospace,SFMono-Regular,Consolas,monospace; white-space:pre-wrap; overflow-wrap:anywhere; }
@media(max-width:720px){.audit-detail-grid{grid-template-columns:1fr}.audit-detail-grid>div,.audit-detail-grid>div:nth-child(3n){border-right:0}.audit-detail-grid>div.wide{grid-column:auto}.snapshot-grid{grid-template-columns:1fr}.audit-detail-hero{align-items:flex-start}.audit-detail-hero>.el-tag{flex:0 0 auto}}
</style>
+18 -3
View File
@@ -1,14 +1,29 @@
import { describe, expect, it } from 'vitest'
import { auditEventLabel } from './presentation'
import { auditDescriptionLabel, auditEventLabel, auditHttpMethodLabel, auditOperatorLabel, auditResourceLabel, auditServiceLabel } from './presentation'
describe('audit event presentation', () => {
it('uses customer-facing labels for known event codes', () => {
expect(auditEventLabel('PLATFORM_TENANT_UPDATE')).toBe('编辑客户档案')
expect(auditEventLabel('TENANT_REVIEW')).toBe('客户入驻审核')
expect(auditEventLabel('INBOUND_MEASURE_APPROVE')).toBe('入库测量审核通过')
expect(auditEventLabel('INBOUND_WEIGHT_OCR_CONFIRM')).toBe('确认 OCR 重量')
})
it('keeps unknown event codes visible for diagnosis', () => {
expect(auditEventLabel('FUTURE_CUSTOMER_EVENT')).toBe('FUTURE_CUSTOMER_EVENT')
it('uses a Chinese fallback instead of exposing an unknown technical code', () => {
expect(auditEventLabel('INBOUND_CARTON_CREATE')).toBe('入库箱码创建')
expect(auditEventLabel('FUTURE_CUSTOMER_EVENT')).toBe('客户')
expect(auditEventLabel('')).toBe('—')
})
it('localizes service, resource, operator and request method values', () => {
expect(auditServiceLabel('firstleg-service')).toBe('头程履约服务')
expect(auditResourceLabel('INBOUND_MEASUREMENT')).toBe('入库测量')
expect(auditOperatorLabel('platform_admin')).toBe('平台管理员')
expect(auditHttpMethodLabel('POST')).toBe('提交')
})
it('prefers the Chinese half of a bilingual audit description', () => {
expect(auditDescriptionLabel('Approve order / 审核通过')).toBe('审核通过')
expect(auditDescriptionLabel('测量审核通过')).toBe('测量审核通过')
})
})
+108 -1
View File
@@ -13,10 +13,117 @@ const auditEventLabels: Record<string, string> = {
PLATFORM_TENANT_QUALIFICATION_UPLOAD: '平台上传企业资质',
TENANT_REQUIRED_QUALIFICATION_PACKAGE_UPLOAD: '上传必填资质包',
PLATFORM_REQUIRED_QUALIFICATION_PACKAGE_UPLOAD: '平台上传必填资质包',
INBOUND_MEASURE_APPROVE: '入库测量审核通过',
INBOUND_MEASURE_REJECT: '驳回入库测量',
INBOUND_MEASURE_EXCEPTION_RELEASE: '解除测量异常',
INBOUND_CARTON_SCAN: '扫描入库箱码',
INBOUND_CARTON_MANUAL_ENTRY: '手工录入箱码',
INBOUND_CARTON_MEASUREMENT_COUNT_CONFIRM: '确认测量箱数',
INBOUND_MEASUREMENT_DRAFT_SAVE: '保存入库测量草稿',
INBOUND_MEASUREMENT_SUBMIT: '提交入库测量审核',
INBOUND_WEIGHT_IMAGE_DELETE: '删除称重图片',
INBOUND_WEIGHT_OCR_RECORD: '登记称重图片 OCR 原值',
INBOUND_WEIGHT_OCR_CORRECT: '修正 OCR 重量',
INBOUND_WEIGHT_OCR_CONFIRM: '确认 OCR 重量',
INBOUND_SMART_SPLIT: '执行智能分柜',
INBOUND_START_PICKING: '开始拣货',
INBOUND_COMPLETE_PICKING: '完成拣货',
INBOUND_COMPLETE_LOADING: '完成装柜',
INBOUND_LOADING_EVIDENCE_UPSERT: '保存装柜证据',
INBOUND_LOADING_EVIDENCE_VOID: '作废装柜证据',
ORDER_CREATE: '创建运单',
ORDER_UPDATE: '编辑运单',
ORDER_SUBMIT: '提交运单',
ORDER_APPROVE: '审核通过运单',
ORDER_REJECT: '驳回运单',
ORDER_CANCEL: '取消运单',
ORDER_ACCEPT: '受理运单',
ORDER_START: '开始处理运单',
ORDER_COMPLETE: '完成运单',
OCEAN_BOOKING_CREATE: '创建海运订舱',
OCEAN_BOOKING_RELEASE: '确认放舱',
OCEAN_BOOKING_CANCEL: '取消海运订舱',
CONTAINER_CREATE: '创建柜次',
CONTAINER_UPDATE: '编辑柜次',
CONTAINER_TRANSPORT_UPDATE: '更新柜次运输信息',
OUTBOUND_CONFIRM_DISPATCH: '确认国内出仓',
OUTBOUND_REVERSE_DISPATCH: '撤销国内出仓',
PDA_COMPLETE_LOADING: 'PDA 完成装柜',
}
const serviceLabels: Record<string, string> = {
'iam-service': '客户与权限服务',
'auth-service': '认证服务',
'order-service': '订单服务',
'firstleg-service': '头程履约服务',
'finance-service': '财务服务',
'warehouse-service': '仓储服务',
'file-service': '文件服务',
'audit-service': '审计服务',
'integration-service': '外部集成服务',
'event-bridge-service': '事件桥接服务',
gateway: '网关服务',
}
const resourceLabels: Record<string, string> = {
TENANT: '客户',
CUSTOMER: '客户',
FIRST_LEG_ORDER: '头程运单',
INBOUND: '入库单',
INBOUND_MEASUREMENT: '入库测量',
CONTAINER: '柜次',
OCEAN_BOOKING: '海运订舱',
LOADING_PLAN: '装柜计划',
OUTBOUND_DISPATCH: '国内出仓',
QUALIFICATION: '企业资质',
MEMBER: '企业成员',
}
const eventTokenLabels: Record<string, string> = {
TENANT: '客户', CUSTOMER: '客户', ORDER: '运单', FIRSTLEG: '头程履约', INBOUND: '入库',
MEASURE: '测量', MEASUREMENT: '测量', WEIGHT: '重量', OCR: 'OCR', CARTON: '箱码',
CONTAINER: '柜次', OCEAN: '海运', BOOKING: '订舱', OUTBOUND: '出仓', LOADING: '装柜',
CREATE: '创建', UPDATE: '更新', SUBMIT: '提交', APPROVE: '审核通过', REJECT: '驳回',
CANCEL: '取消', CONFIRM: '确认', COMPLETE: '完成', START: '开始', SAVE: '保存',
DELETE: '删除', UPLOAD: '上传', ATTACH: '关联', RELEASE: '放行', RECORD: '记录',
CORRECT: '修正', DRAFT: '草稿', SCAN: '扫描', MANUAL: '手工', ENTRY: '录入',
}
export function auditEventLabel(value?: string | null) {
const code = value?.trim()
if (!code) return '—'
return auditEventLabels[code] ?? code
if (auditEventLabels[code]) return auditEventLabels[code]
const translated = code.split('_').map((token) => eventTokenLabels[token]).filter(Boolean).join('')
return translated || '其他业务操作'
}
export function auditDescriptionLabel(value?: string | null) {
const text = value?.trim()
if (!text) return '—'
const chinesePart = text.split(/\s*\/\s*/).find((part) => /[\u3400-\u9fff]/.test(part))
return chinesePart?.trim() || text
}
export function auditServiceLabel(value?: string | null) {
const code = value?.trim()
if (!code) return '—'
return serviceLabels[code] ?? code.replace(/-service$/i, '服务')
}
export function auditResourceLabel(value?: string | null) {
const code = value?.trim()
if (!code) return '—'
return resourceLabels[code] ?? code
}
export function auditOperatorLabel(value?: string | null) {
const code = value?.trim()
if (!code) return '—'
return code === 'platform_admin' ? '平台管理员' : code
}
export function auditHttpMethodLabel(value?: string | null) {
const code = value?.trim().toUpperCase()
if (!code) return '—'
return ({ GET: '查询', POST: '提交', PUT: '更新', PATCH: '变更', DELETE: '删除' } as Record<string, string>)[code] ?? code
}