Files
nl-admin-view/apps/web-antd/src/views/system/database/components/ChangeLog.vue

266 lines
7.7 KiB
Vue
Raw Normal View History

2026-01-23 17:13:48 +08:00
<script setup lang="ts">
import { h, onMounted, ref } from 'vue';
import { Button, Card, message, Table, Tag } from 'ant-design-vue';
import { getChangeLogApi } from '../api';
defineOptions({
name: 'ChangeLog',
});
const loading = ref(false);
const changeLogList = ref<any[]>([]);
const pagination = ref({
current: 1,
pageSize: 20,
total: 0,
});
const filters = ref({
table_name: '',
operation_type: '',
start_time: '',
end_time: '',
});
// 操作类型选项
const operationTypeOptions = [
{ label: '全部', value: '' },
{ label: '结构变更', value: 'structure_change' },
{ label: '数据变更', value: 'data_change' },
{ label: 'SQL查询', value: 'sql_query' },
];
// 表列表从筛选条件获取实际应该从API获取
const tableList = ref<any[]>([]);
// 加载变更记录
const loadChangeLog = async (page = 1, pageSize = 20) => {
loading.value = true;
try {
const params: any = {
page,
page_size: pageSize,
};
if (filters.value.table_name) {
params.table_name = filters.value.table_name;
}
if (filters.value.operation_type) {
params.operation_type = filters.value.operation_type;
}
if (filters.value.start_time) {
params.start_time = filters.value.start_time;
}
if (filters.value.end_time) {
params.end_time = filters.value.end_time;
}
const res = await getChangeLogApi(params);
changeLogList.value = res.items || [];
pagination.value = {
current: res.page || 1,
pageSize: res.size || 20,
total: res.total || 0,
};
} catch (error: any) {
message.error(error.message || '加载变更记录失败');
changeLogList.value = [];
} finally {
loading.value = false;
}
};
// 操作类型标签颜色
const getOperationTypeColor = (type: string) => {
const colorMap: Record<string, string> = {
structure_change: 'blue',
data_change: 'green',
sql_query: 'orange',
};
return colorMap[type] || 'default';
};
// 操作类型标签文本
const getOperationTypeText = (type: string) => {
const textMap: Record<string, string> = {
structure_change: '结构变更',
data_change: '数据变更',
sql_query: 'SQL查询',
};
return textMap[type] || type;
};
// 格式化时间
const formatTime = (timestamp: number) => {
if (!timestamp) return '-';
return new Date(timestamp * 1000).toLocaleString('zh-CN');
};
// 搜索
const handleSearch = () => {
loadChangeLog(1, pagination.value.pageSize);
};
// 重置
const handleReset = () => {
filters.value = {
table_name: '',
operation_type: '',
start_time: '',
end_time: '',
};
loadChangeLog(1, pagination.value.pageSize);
};
// 展开详情
const expandedRowKeys = ref<number[]>([]);
const getActionText = (action: string) => {
const actionText: Record<string, string> = {
update_table_comment: '更新表注释',
update_column_comment: '更新字段注释',
add_index: '添加索引',
drop_index: '删除索引',
add_column: '添加字段',
modify_column: '修改字段',
drop_column: '删除字段',
insert: '插入数据',
update: '更新数据',
delete: '删除数据',
batch_delete: '批量删除',
select_query: 'SELECT查询',
};
return actionText[action] || action;
};
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
{ title: '表名', dataIndex: 'table_name', key: 'table_name', width: 150 },
{
title: '操作类型',
dataIndex: 'operation_type',
key: 'operation_type',
width: 120,
customRender: ({ text }: any) =>
h(Tag, { color: getOperationTypeColor(text) }, () => getOperationTypeText(text)),
},
{
title: '操作详情',
dataIndex: 'operation_detail',
key: 'operation_detail',
customRender: ({ record }: any) => {
const detail = record.operation_detail || {};
const action = detail.action || '';
return getActionText(action);
},
},
{ title: '操作用户', dataIndex: 'user_id', key: 'user_id', width: 100 },
{
title: '操作时间',
dataIndex: 'created_at',
key: 'created_at',
width: 180,
customRender: ({ text }: any) => formatTime(text),
},
];
onMounted(() => {
loadChangeLog();
});
</script>
<template>
<div class="change-log">
<Card title="变更记录" class="mb-4">
<div class="mb-4 flex flex-wrap items-center gap-4">
<div class="flex items-center gap-2">
<span class="text-sm">表名</span>
<input
v-model="filters.table_name"
type="text"
placeholder="请输入表名"
class="w-40 rounded border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800"
/>
</div>
<div class="flex items-center gap-2">
<span class="text-sm">操作类型</span>
<select
v-model="filters.operation_type"
class="w-32 rounded border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800"
>
<option v-for="option in operationTypeOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
<div class="flex items-center gap-2">
<span class="text-sm">开始时间</span>
<input
v-model="filters.start_time"
type="date"
class="w-40 rounded border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800"
/>
</div>
<div class="flex items-center gap-2">
<span class="text-sm">结束时间</span>
<input
v-model="filters.end_time"
type="date"
class="w-40 rounded border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800"
/>
</div>
<div class="flex items-center gap-2">
<Button type="primary" @click="handleSearch">搜索</Button>
<Button @click="handleReset">重置</Button>
</div>
</div>
</Card>
<Table
:columns="columns"
:data-source="changeLogList"
:loading="loading"
:pagination="{
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total,
showSizeChanger: true,
showTotal: (total) => `${total}`,
}"
:expanded-row-keys="expandedRowKeys"
@change="(p: any) => loadChangeLog(p.current, p.pageSize)"
>
<template #expandedRowRender="{ record }">
<div class="p-4 space-y-4">
<div v-if="record.sql_statement">
<div class="mb-2 font-semibold">SQL语句</div>
<pre class="rounded bg-gray-100 p-3 text-sm dark:bg-gray-800">{{ record.sql_statement }}</pre>
</div>
<div v-if="record.operation_detail">
<div class="mb-2 font-semibold">操作详情</div>
<pre class="rounded bg-gray-100 p-3 text-sm dark:bg-gray-800">{{ JSON.stringify(record.operation_detail, null, 2) }}</pre>
</div>
<div v-if="record.before_data" class="grid grid-cols-2 gap-4">
<div>
<div class="mb-2 font-semibold">变更前</div>
<pre class="max-h-64 overflow-auto rounded bg-gray-100 p-3 text-sm dark:bg-gray-800">{{ JSON.stringify(record.before_data, null, 2) }}</pre>
</div>
<div>
<div class="mb-2 font-semibold">变更后</div>
<pre class="max-h-64 overflow-auto rounded bg-gray-100 p-3 text-sm dark:bg-gray-800">{{ JSON.stringify(record.after_data, null, 2) }}</pre>
</div>
</div>
</div>
</template>
</Table>
</div>
</template>
<style scoped>
.space-y-4 > * + * {
margin-top: 1rem;
}
</style>