feat(audit-ui): 重新应用前端审计详情弹窗渲染逻辑,支持高级对比结构
This commit is contained in:
@ -84,12 +84,15 @@
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog v-model="detailDialogVisible" title="操作详情" width="700px" destroy-on-close>
|
||||
<el-descriptions :column="2" border>
|
||||
<!-- ★ 重写的详情弹窗:支持三种高级结构 ★ -->
|
||||
<el-dialog v-model="detailDialogVisible" title="操作详情" width="750px" destroy-on-close :close-on-click-modal="false">
|
||||
<!-- 基本信息 -->
|
||||
<el-descriptions :column="2" border class="base-info">
|
||||
<el-descriptions-item label="ID">{{ currentLog.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作人">{{ currentLog.username }} ({{ currentLog.display_name }})</el-descriptions-item>
|
||||
<el-descriptions-item label="模块">{{ currentLog.module }}</el-descriptions-item>
|
||||
<el-descriptions-item label="模块">
|
||||
<el-tag>{{ currentLog.module }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作类型">
|
||||
<el-tag :type="getActionType(currentLog.action)">{{ currentLog.action }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
@ -97,23 +100,92 @@
|
||||
<el-descriptions-item label="IP地址">{{ currentLog.ip_address }}</el-descriptions-item>
|
||||
<el-descriptions-item label="请求方式">{{ currentLog.method }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作时间" :span="2">{{ currentLog.created_at }}</el-descriptions-item>
|
||||
<el-descriptions-item label="请求URL" :span="2">
|
||||
<el-text size="small">{{ currentLog.url }}</el-text>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div v-if="currentLog.details" class="details-box">
|
||||
<div class="details-title">变更内容 (JSON)</div>
|
||||
<pre class="json-content">{{ formatDetails(currentLog.details) }}</pre>
|
||||
<!-- ★ 变更明细区域 ★ -->
|
||||
<div class="details-section">
|
||||
|
||||
<!-- 情况1:UPDATE - 变更对比表 -->
|
||||
<div v-if="detailType === 'changes'" class="changes-box">
|
||||
<div class="section-title">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
字段变更详情(共 {{ changesList.length }} 处变更)
|
||||
</div>
|
||||
<el-table :data="changesList" border stripe size="small" max-height="350">
|
||||
<el-table-column prop="field" label="字段名" width="150">
|
||||
<template #default="{ row }">
|
||||
<span class="field-name">{{ row.field }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="修改前" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<span class="old-value">{{ row.old ?? '空' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="修改后" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<span class="new-value">{{ row.new ?? '空' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 情况2:DELETE - 删除快照 -->
|
||||
<div v-else-if="detailType === 'deleted_snapshot'" class="snapshot-box">
|
||||
<div class="section-title">
|
||||
<el-icon><Delete /></el-icon>
|
||||
数据删除快照
|
||||
</div>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item
|
||||
v-for="(value, key) in deletedSnapshot"
|
||||
:key="String(key)"
|
||||
:label="String(key)"
|
||||
:span="isLongValue(value) ? 2 : 1"
|
||||
>
|
||||
<span class="snapshot-value">{{ formatValue(value) }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 情况3:CREATE - 新增详情 -->
|
||||
<div v-else-if="detailType === 'created'" class="snapshot-box">
|
||||
<div class="section-title">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增数据详情
|
||||
</div>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item
|
||||
v-for="(value, key) in createdData"
|
||||
:key="String(key)"
|
||||
:label="String(key)"
|
||||
:span="isLongValue(value) ? 2 : 1"
|
||||
>
|
||||
<span class="snapshot-value">{{ formatValue(value) }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 兜底:原始 JSON -->
|
||||
<div v-else class="raw-json-box">
|
||||
<div class="section-title">
|
||||
<el-icon><Document /></el-icon>
|
||||
原始数据
|
||||
</div>
|
||||
<pre class="raw-json">{{ rawJson }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
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'
|
||||
|
||||
// 表格数据
|
||||
@ -144,7 +216,71 @@ const actionOptions = ref<string[]>(['create', 'update', 'delete', 'export', 'im
|
||||
const detailDialogVisible = ref(false)
|
||||
const currentLog = ref<any>({})
|
||||
|
||||
// 获取操作类型对应的标签样式
|
||||
// ============================================================
|
||||
// 详情解析逻辑
|
||||
// ============================================================
|
||||
|
||||
// 解析 detail type
|
||||
const detailType = computed(() => {
|
||||
const details = currentLog.value.details
|
||||
if (!details) return 'raw'
|
||||
|
||||
if (details.changes && typeof details.changes === 'object') return 'changes'
|
||||
if (details.deleted_snapshot && typeof details.deleted_snapshot === 'object') return 'deleted_snapshot'
|
||||
if (details.created && typeof details.created === 'object') return 'created'
|
||||
|
||||
return 'raw'
|
||||
})
|
||||
|
||||
// 解析 changes 为表格数据
|
||||
const changesList = computed(() => {
|
||||
const details = currentLog.value.details
|
||||
if (!details?.changes) return []
|
||||
|
||||
return Object.entries(details.changes).map(([field, values]: [string, any]) => ({
|
||||
field,
|
||||
old: values?.old,
|
||||
new: values?.new
|
||||
}))
|
||||
})
|
||||
|
||||
// 解析 deleted_snapshot
|
||||
const deletedSnapshot = computed(() => {
|
||||
const details = currentLog.value.details
|
||||
return details?.deleted_snapshot || {}
|
||||
})
|
||||
|
||||
// 解析 created
|
||||
const createdData = computed(() => {
|
||||
const details = currentLog.value.details
|
||||
return details?.created || {}
|
||||
})
|
||||
|
||||
// 原始 JSON
|
||||
const rawJson = computed(() => {
|
||||
const details = currentLog.value.details
|
||||
if (!details) return ''
|
||||
return JSON.stringify(details, null, 2)
|
||||
})
|
||||
|
||||
// 辅助函数:格式化值
|
||||
const formatValue = (value: any): string => {
|
||||
if (value === null || value === undefined) return '-'
|
||||
if (typeof value === 'object') return JSON.stringify(value)
|
||||
return String(value)
|
||||
}
|
||||
|
||||
// 辅助函数:判断是否是长值
|
||||
const isLongValue = (value: any): boolean => {
|
||||
if (value === null || value === undefined) return false
|
||||
const str = typeof value === 'object' ? JSON.stringify(value) : String(value)
|
||||
return str.length > 50
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 其他方法
|
||||
// ============================================================
|
||||
|
||||
const getActionType = (action: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
'create': 'success',
|
||||
@ -158,11 +294,9 @@ const getActionType = (action: string) => {
|
||||
return typeMap[action?.toLowerCase()] || 'info'
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const getList = async () => {
|
||||
tableLoading.value = true
|
||||
try {
|
||||
// 处理日期范围
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
queryParams.start_date = dateRange.value[0]
|
||||
queryParams.end_date = dateRange.value[1]
|
||||
@ -175,7 +309,6 @@ const getList = async () => {
|
||||
if (res.code === 200) {
|
||||
tableData.value = res.data.list
|
||||
total.value = res.data.total
|
||||
// 更新选项
|
||||
if (res.data.modules) {
|
||||
moduleOptions.value = res.data.modules
|
||||
}
|
||||
@ -190,13 +323,11 @@ const getList = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleQuery = () => {
|
||||
queryParams.page = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
queryParams.page = 1
|
||||
queryParams.username = ''
|
||||
@ -209,29 +340,13 @@ const handleReset = () => {
|
||||
getList()
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetails = (row: any) => {
|
||||
currentLog.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 格式化详情 JSON
|
||||
const formatDetails = (details: any) => {
|
||||
if (!details) return ''
|
||||
if (typeof details === 'string') {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(details), null, 2)
|
||||
} catch {
|
||||
return details
|
||||
}
|
||||
}
|
||||
return JSON.stringify(details, null, 2)
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
getList()
|
||||
// 获取可选模块
|
||||
getAuditModules().then(res => {
|
||||
if (res.code === 200 && res.data) {
|
||||
moduleOptions.value = res.data
|
||||
@ -251,26 +366,79 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.text-gray {
|
||||
color: #999;
|
||||
.base-info {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.details-box {
|
||||
margin-top: 20px;
|
||||
.details-section {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.details-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.json-content {
|
||||
background-color: #f5f7fa;
|
||||
padding: 15px;
|
||||
/* 变更表格样式 */
|
||||
.changes-box {
|
||||
background: #fef0f0;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #fbc4c4;
|
||||
}
|
||||
|
||||
.field-name {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.old-value {
|
||||
color: #f56c6c;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.new-value {
|
||||
color: #67c23a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 快照样式 */
|
||||
.snapshot-box {
|
||||
background: #f5f7fa;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.snapshot-value {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* 原始 JSON */
|
||||
.raw-json-box {
|
||||
background: #f5f7fa;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.raw-json {
|
||||
background: #2d2d2d;
|
||||
color: #67c23a;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user