256 lines
4.7 KiB
Vue
256 lines
4.7 KiB
Vue
<template>
|
||
<el-dialog
|
||
v-model="visible"
|
||
title="变更详情"
|
||
width="920px"
|
||
@close="onClose"
|
||
>
|
||
<div class="timeline" v-loading="loading">
|
||
<div
|
||
v-for="(item, index) in logList"
|
||
:key="index"
|
||
class="timeline-item"
|
||
>
|
||
<!-- 左侧圆点 + 竖线 -->
|
||
<div class="timeline-left">
|
||
<div class="dot" :class="dotClass(item.action)"></div>
|
||
<div
|
||
class="line"
|
||
v-if="index < logList.length - 1"
|
||
></div>
|
||
</div>
|
||
|
||
<!-- 右侧内容 -->
|
||
<div class="timeline-right">
|
||
<!-- 摘要行 -->
|
||
<div class="summary">
|
||
<span class="time">{{ formatTime(item.operateTime) }}</span>
|
||
<el-tag :type="tagType(item.action)" size="small">
|
||
{{ item.action }}
|
||
</el-tag>
|
||
<span class="operator">{{ item.operator }}</span>
|
||
<span class="table">{{ item.table }}</span>
|
||
<span class="row-id">ID: {{ item.rowId }}</span>
|
||
</div>
|
||
|
||
<!-- 展开/折叠 diff -->
|
||
<div class="diff-wrapper">
|
||
<AuditLogDetail :data="item" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<el-empty
|
||
v-if="!loading && logList.length === 0"
|
||
description="暂无变更记录"
|
||
/>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, watch } from "vue";
|
||
import { ElMessage } from "element-plus";
|
||
import axios from "@/utils/axios";
|
||
import AuditLogDetail from "./AuditLogDetail.vue";
|
||
|
||
const props = defineProps({
|
||
modelValue: Boolean,
|
||
bizNo: String, // ✅ 业务单据号(loggroupid)
|
||
});
|
||
|
||
const emit = defineEmits(["update:modelValue"]);
|
||
|
||
const visible = ref(false);
|
||
const loading = ref(false);
|
||
const logList = ref([]);
|
||
|
||
watch(
|
||
() => props.modelValue,
|
||
(val) => {
|
||
visible.value = val;
|
||
if (val) {
|
||
loadLog();
|
||
}
|
||
}
|
||
);
|
||
|
||
watch(visible, (val) => {
|
||
emit("update:modelValue", val);
|
||
});
|
||
|
||
const loadLog = async () => {
|
||
if (!props.bizNo) {
|
||
ElMessage.warning("bizNo 不能为空");
|
||
return;
|
||
}
|
||
|
||
loading.value = true;
|
||
try {
|
||
const response = await axios.get("/logdetail", {
|
||
params: {
|
||
bizNo: props.bizNo,
|
||
},
|
||
});
|
||
|
||
const result = response.data;
|
||
if (!result?.data || result.data.length === 0) {
|
||
logList.value = [];
|
||
return;
|
||
}
|
||
|
||
const parsedList = [];
|
||
|
||
for (const raw of result.data) {
|
||
try {
|
||
const changeData =
|
||
typeof raw.changeData === "string"
|
||
? JSON.parse(raw.changeData)
|
||
: raw.changeData;
|
||
|
||
changeData.table = changeData.table || raw.tableName;
|
||
changeData.operator = changeData.operator || raw.operator;
|
||
changeData.operateTime =
|
||
changeData.operateTime || raw.operateTime;
|
||
|
||
parsedList.push(changeData);
|
||
} catch (e) {
|
||
console.error("changeData 解析失败", e);
|
||
}
|
||
}
|
||
|
||
logList.value = parsedList;
|
||
} catch (e) {
|
||
console.error(e);
|
||
ElMessage.error("加载变更日志失败");
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
const onClose = () => {
|
||
logList.value = [];
|
||
};
|
||
|
||
const tagType = (action) => {
|
||
switch (action) {
|
||
case "INSERT":
|
||
return "success";
|
||
case "UPDATE":
|
||
return "primary";
|
||
case "DELETE":
|
||
return "danger";
|
||
default:
|
||
return "info";
|
||
}
|
||
};
|
||
|
||
const dotClass = (action) => {
|
||
switch (action) {
|
||
case "INSERT":
|
||
return "dot-green";
|
||
case "UPDATE":
|
||
return "dot-blue";
|
||
case "DELETE":
|
||
return "dot-red";
|
||
default:
|
||
return "dot-gray";
|
||
}
|
||
};
|
||
|
||
const formatTime = (time) => {
|
||
if (!time) return "";
|
||
if (typeof time === "string" && time.includes("T")) {
|
||
return time.replace("T", " ").split(".")[0];
|
||
}
|
||
return time;
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.timeline {
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.timeline-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.timeline-left {
|
||
width: 24px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.dot {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
margin-top: 6px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.dot-green {
|
||
background-color: #67c23a;
|
||
}
|
||
|
||
.dot-blue {
|
||
background-color: #409eff;
|
||
}
|
||
|
||
.dot-red {
|
||
background-color: #f56c6c;
|
||
}
|
||
|
||
.dot-gray {
|
||
background-color: #909399;
|
||
}
|
||
|
||
.line {
|
||
width: 2px;
|
||
flex: 1;
|
||
background-color: #e4e7ed;
|
||
margin-top: 4px;
|
||
min-height: 24px;
|
||
}
|
||
|
||
.timeline-right {
|
||
flex: 1;
|
||
padding-bottom: 16px;
|
||
}
|
||
|
||
.summary {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
font-size: 13px;
|
||
color: #606266;
|
||
}
|
||
|
||
.summary .time {
|
||
font-weight: bold;
|
||
color: #303133;
|
||
}
|
||
|
||
.summary .operator {
|
||
color: #409eff;
|
||
}
|
||
|
||
.summary .table {
|
||
font-weight: bold;
|
||
}
|
||
|
||
.summary .row-id {
|
||
color: #909399;
|
||
}
|
||
|
||
.diff-wrapper {
|
||
background-color: #fafafa;
|
||
border-radius: 4px;
|
||
padding: 8px 12px;
|
||
margin-top: 6px;
|
||
}
|
||
</style> |