@@ -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(86 0px, 94 vw)" :loading="detailLoading" :show-footer="false" >
< AppDrawer v-model = "detailOpen" title="操作日志详情" size="min(92 0px, 96 vw)" :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 : 16 px ; margin - bottom : 18 px ; padding - bottom : 14 px ; border - bottom : 1 px solid var ( -- cc - border ) ; } . panel - title h2 { margin : 0 ; color : # 344054 ; font - size : 16 px ; } . panel - title p { margin : 5 px 0 0 ; color : var ( -- cc - muted ) ; font - size : 11 px ; } . panel - title > svg { color : var ( -- cc - primary ) ; } . audit - query { margin - bottom : 16 px ; } . module - empty { display : flex ; min - height : 260 px ; align - items : center ; justify - content : center ; flex - direction : column ; gap : 7 px ; color : # 98 a2b3 ; text - align : center ; } . module - empty strong { color : # 475467 ; font - size : 13 px ; } . module - empty p { margin : 0 ; color : var ( -- cc - muted ) ; font - size : 12 px ; } . snapshot - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; gap : 16 px ; margin - top : 18 px ; } . snapshot - grid section { min - width : 0 ; } . snapshot - grid h3 { margin : 0 0 8 px ; color : # 475467 ; font - size : 14 px ; } . snapshot - grid pre { min - height : 160 px ; margin : 0 ; padding : 14 px ; overflow : auto ; border : 1 px solid var ( -- cc - border ) ; border - radius : 6 px ; background : # f8fafc ; color : # 344054 ; font : 12 px / 1.6 ui - monospace , SFMono - Regular , Consolas , monospace ; white - space : pre - wrap ; overflow - wrap : anywhere ; } @ media ( max - width : 720 px ) { . snapshot - grid { grid - template - columns : 1 fr } }
. customer - audit - panel { min - width : 0 ; } . panel - title { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 16 px ; margin - bottom : 18 px ; padding - bottom : 14 px ; border - bottom : 1 px solid var ( -- cc - border ) ; } . panel - title h2 { margin : 0 ; color : # 344054 ; font - size : 16 px ; } . panel - title p { margin : 5 px 0 0 ; color : var ( -- cc - muted ) ; font - size : 11 px ; } . panel - title > svg { color : var ( -- cc - primary ) ; } . audit - query { margin - bottom : 16 px ; } . module - empty { display : flex ; min - height : 260 px ; align - items : center ; justify - content : center ; flex - direction : column ; gap : 7 px ; color : # 98 a2b3 ; text - align : center ; } . module - empty strong { color : # 475467 ; font - size : 13 px ; } . module - empty p { margin : 0 ; color : var ( -- cc - muted ) ; font - size : 12 px ; }
. 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 : 3 px 9 px ; overflow : hidden ; border : 1 px solid # dbeafe ; border - radius : 999 px ; background : # eff6ff ; color : # 1 d4ed8 ; font - size : 12 px ; 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 : 13 px ; padding : 16 px 18 px ; border : 1 px solid # d1fadf ; border - radius : 10 px ; background : # f6fef9 ; } . audit - detail - hero . is - failed { border - color : # fee4e2 ; background : # fff7f6 ; } . result - icon { display : grid ; width : 38 px ; height : 38 px ; 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 : 12 px ; } . hero - copy h3 { margin : 3 px 0 4 px ; color : # 101828 ; font - size : 17 px ; } . hero - copy p { margin : 0 ; color : # 475467 ; font - size : 13 px ; line - height : 1.55 ; }
. audit - detail - grid { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ; margin - top : 16 px ; overflow : hidden ; border : 1 px solid # e4e7ec ; border - radius : 10 px ; background : # fff ; } . audit - detail - grid > div { display : flex ; min - width : 0 ; min - height : 82 px ; flex - direction : column ; justify - content : center ; gap : 5 px ; padding : 13 px 16 px ; border - right : 1 px solid # eaecf0 ; border - bottom : 1 px solid # eaecf0 ; } . audit - detail - grid > div : nth - child ( 3 n ) { 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 : 12 px ; } . audit - detail - grid strong { overflow - wrap : anywhere ; color : # 1 d2939 ; font - size : 14 px ; font - weight : 600 ; } . audit - detail - grid small { overflow - wrap : anywhere ; color : # 98 a2b3 ; font - size : 11 px ; } . request - cell > div { display : flex ; min - width : 0 ; align - items : center ; gap : 9 px ; } . request - cell code { min - width : 0 ; overflow - wrap : anywhere ; color : # 344054 ; font : 12 px / 1.55 ui - monospace , SFMono - Regular , Consolas , monospace ; } . failure - alert { margin - top : 16 px ; }
. snapshot - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; gap : 16 px ; margin - top : 18 px ; } . snapshot - grid section { min - width : 0 ; padding : 15 px ; border : 1 px solid # e4e7ec ; border - radius : 10 px ; background : # fff ; } . snapshot - grid h3 { margin : 0 ; color : # 344054 ; font - size : 14 px ; } . snapshot - grid p { margin : 4 px 0 10 px ; color : # 98 a2b3 ; font - size : 11 px ; } . snapshot - grid pre { min - height : 160 px ; max - height : 360 px ; margin : 0 ; padding : 14 px ; overflow : auto ; border : 1 px solid # eaecf0 ; border - radius : 8 px ; background : # f8fafc ; color : # 344054 ; font : 12 px / 1.65 ui - monospace , SFMono - Regular , Consolas , monospace ; white - space : pre - wrap ; overflow - wrap : anywhere ; }
@ media ( max - width : 720 px ) { . audit - detail - grid { grid - template - columns : 1 fr } . audit - detail - grid > div , . audit - detail - grid > div : nth - child ( 3 n ) { border - right : 0 } . audit - detail - grid > div . wide { grid - column : auto } . snapshot - grid { grid - template - columns : 1 fr } . audit - detail - hero { align - items : flex - start } . audit - detail - hero > . el - tag { flex : 0 0 auto } }
< / style >