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
386 lines
9.2 KiB
Vue
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>
|