Files
nl-admin-view/apps/web-antd/src/views/system/database/components/ChangeLog.vue
李琦 e8f30a1275
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
AI代码生成工具
2026-08-10 15:51:04 +08:00

386 lines
9.2 KiB
Vue

<script setup lang="ts">
/**
* 数据库变更记录:筛选 + 列表 + 展开详情
* 使用 antd 组件与主题变量,避免原生 input 在暗色下不可读
*/
import { h, onMounted, ref } from 'vue';
import {
Button,
DatePicker,
Input,
Select,
Table,
Tag,
message,
} from 'ant-design-vue';
import { Icon } from '#/components/icon';
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: undefined as string | undefined,
start_time: '',
end_time: '',
});
const operationTypeOptions = [
{ label: '结构变更', value: 'structure_change' },
{ label: '数据变更', value: 'data_change' },
{ label: 'SQL查询', value: 'sql_query' },
];
const expandedRowKeys = ref<(string | number)[]>([]);
/**
* 加载变更记录
*/
const loadChangeLog = async (page = 1, pageSize = 20) => {
loading.value = true;
try {
const params: Record<string, 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 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 displayTime = (value: unknown) => {
if (value == null || value === '' || value === 0) return '-';
if (typeof value === 'string' && Number.isNaN(Number(value))) {
return value;
}
const num = Number(value);
if (!Number.isNaN(num) && num > 1_000_000_000) {
return new Date(num * 1000).toLocaleString('zh-CN');
}
return String(value);
};
const startDate = ref<string | undefined>(undefined);
const endDate = ref<string | undefined>(undefined);
const handleSearch = () => {
filters.value.start_time = startDate.value || '';
filters.value.end_time = endDate.value || '';
loadChangeLog(1, pagination.value.pageSize);
};
const handleReset = () => {
filters.value = {
table_name: '',
operation_type: undefined,
start_time: '',
end_time: '',
};
startDate.value = undefined;
endDate.value = undefined;
loadChangeLog(1, pagination.value.pageSize);
};
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 72, fixed: 'left' as const },
{
title: '表名',
dataIndex: 'table_name',
key: 'table_name',
width: 180,
ellipsis: true,
},
{
title: '操作类型',
dataIndex: 'operation_type',
key: 'operation_type',
width: 110,
customRender: ({ text }: any) =>
h(Tag, { color: getOperationTypeColor(text) }, () =>
getOperationTypeText(text),
),
},
{
title: '操作详情',
dataIndex: 'operation_detail',
key: 'operation_detail',
width: 140,
ellipsis: true,
customRender: ({ record }: any) => {
const detail = record.operation_detail || {};
return getActionText(detail.action || '');
},
},
{
title: '操作用户',
dataIndex: 'user_id',
key: 'user_id',
width: 100,
},
{
title: '操作时间',
dataIndex: 'created_at',
key: 'created_at',
width: 180,
customRender: ({ text }: any) => displayTime(text),
},
];
onMounted(() => {
loadChangeLog();
});
</script>
<template>
<div class="change-log">
<div class="change-log__filters">
<Input
v-model:value="filters.table_name"
allow-clear
placeholder="表名"
class="change-log__field"
@press-enter="handleSearch"
>
<template #prefix>
<Icon icon="lucide:table-2" :size="14" />
</template>
</Input>
<Select
v-model:value="filters.operation_type"
allow-clear
placeholder="操作类型"
class="change-log__field"
:options="operationTypeOptions"
/>
<DatePicker
v-model:value="startDate"
class="change-log__field"
placeholder="开始日期"
value-format="YYYY-MM-DD"
/>
<DatePicker
v-model:value="endDate"
class="change-log__field"
placeholder="结束日期"
value-format="YYYY-MM-DD"
/>
<div class="change-log__actions">
<Button type="primary" @click="handleSearch">
<Icon icon="lucide:search" :size="14" class="mr-1" />
搜索
</Button>
<Button @click="handleReset">重置</Button>
</div>
</div>
<Table
class="change-log__table"
row-key="id"
size="middle"
:columns="columns"
:data-source="changeLogList"
:loading="loading"
:scroll="{ x: 900 }"
:pagination="{
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total,
showSizeChanger: true,
showTotal: (total: number) => `共 ${total} 条`,
}"
v-model:expandedRowKeys="expandedRowKeys"
@change="(p: any) => loadChangeLog(p.current, p.pageSize)"
>
<template #expandedRowRender="{ record }">
<div class="change-log__detail">
<div v-if="record.sql_statement" class="change-log__block">
<div class="change-log__label">SQL 语句</div>
<pre class="change-log__pre">{{ record.sql_statement }}</pre>
</div>
<div v-if="record.operation_detail" class="change-log__block">
<div class="change-log__label">操作详情</div>
<pre class="change-log__pre">{{
JSON.stringify(record.operation_detail, null, 2)
}}</pre>
</div>
<div
v-if="record.before_data || record.after_data"
class="change-log__diff"
>
<div class="change-log__block">
<div class="change-log__label">变更前</div>
<pre class="change-log__pre change-log__pre--scroll">{{
JSON.stringify(record.before_data ?? null, null, 2)
}}</pre>
</div>
<div class="change-log__block">
<div class="change-log__label">变更后</div>
<pre class="change-log__pre change-log__pre--scroll">{{
JSON.stringify(record.after_data ?? null, null, 2)
}}</pre>
</div>
</div>
</div>
</template>
</Table>
</div>
</template>
<style scoped>
.change-log__filters {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
margin-bottom: 16px;
padding: 14px 16px;
border-radius: 12px;
border: 1px solid hsl(var(--border));
background: hsl(var(--card, var(--background)));
}
.change-log__field {
width: 180px;
}
.change-log__actions {
display: flex;
gap: 8px;
margin-left: auto;
}
.change-log__table {
border-radius: 12px;
overflow: hidden;
}
.change-log__detail {
display: flex;
flex-direction: column;
gap: 12px;
padding: 4px 8px 8px;
}
.change-log__diff {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.change-log__label {
margin-bottom: 6px;
font-size: 12px;
font-weight: 600;
color: hsl(var(--muted-foreground));
}
.change-log__pre {
margin: 0;
padding: 12px;
border-radius: 10px;
border: 1px solid hsl(var(--border));
background: hsl(var(--muted) / 0.35);
color: hsl(var(--foreground));
font-size: 12px;
line-height: 1.55;
white-space: pre-wrap;
word-break: break-all;
}
.change-log__pre--scroll {
max-height: 260px;
overflow: auto;
}
@media (max-width: 900px) {
.change-log__diff {
grid-template-columns: 1fr;
}
.change-log__field {
width: 100%;
min-width: 140px;
flex: 1 1 160px;
}
.change-log__actions {
margin-left: 0;
width: 100%;
}
}
</style>