UNIVPLMDataIntegration/Web/src/views/system/log/logdiff/index.vue
2025-04-05 15:26:07 +08:00

404 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="syslogdiff-container">
<el-card shadow="hover" :body-style="{ padding: '5px 5px 0 5px', display: 'flex', width: '100%', height: '100%', alignItems: 'start' }">
<el-form :model="state.queryParams" ref="queryForm" :show-message="false" :inlineMessage="true" label-width="auto" style="flex: 1 1 0%">
<el-row :gutter="10">
<el-col class="mb5" :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="开始时间" prop="name">
<el-date-picker
v-model="state.queryParams.startTime"
type="datetime"
placeholder="开始时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
:shortcuts="shortcuts"
class="w100"
/>
</el-form-item>
</el-col>
<el-col class="mb5" :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="结束时间" prop="code">
<el-date-picker
v-model="state.queryParams.endTime"
type="datetime"
placeholder="结束时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
:shortcuts="shortcuts"
class="w100"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-divider style="height: calc(100% - 5px); margin: 0 10px" direction="vertical" />
<el-row>
<el-col>
<el-button-group>
<el-button type="primary" icon="ele-Search" @click="handleQuery(true)" v-auth="'sysLogDiff/page'" :loading="options.loading"> 查询 </el-button>
<el-button icon="ele-Refresh" @click="resetQuery" :loading="options.loading"> 重置 </el-button>
</el-button-group>
</el-col>
</el-row>
</el-card>
<el-card class="full-table" shadow="hover" style="margin-top: 5px">
<vxe-grid ref="xGrid" class="xGrid-style" v-bind="options" v-on="gridEvents">
<template #toolbar_buttons> </template>
<template #toolbar_tools>
<vxe-button circle icon="vxe-icon-upload" name="导入" code="showImport" class="mr12" />
</template>
<template #empty>
<el-empty :image-size="200" />
</template>
<template #row_content="{ row }">
<el-card header="差异数据" style="width: 100%; margin: 5px">
<el-table :data="item.columns" v-for="item in row.diffData" :key="item.tableName" :span-method="(data: any) => diffTableSpanMethod(data, item)" border style="width: 100%">
<el-table-column label="表名" width="200">
<template #default>
{{ item.tableName }}
<br />
{{ item.tableDescription }}
</template>
</el-table-column>
<el-table-column prop="columnName" label="字段描述" width="300" :formatter="(row: any) => `${row.columnName} - ${row.columnDescription}`" />
<el-table-column prop="beforeValue" label="修改前" show-overflow-tooltip>
<template #default="columnScope">
<pre v-html="markDiff(columnScope.row.beforeValue, columnScope.row.afterValue, true)" />
</template>
</el-table-column>
<el-table-column prop="afterValue" label="修改后" show-overflow-tooltip>
<template #default="columnScope">
<pre v-html="markDiff(columnScope.row.beforeValue, columnScope.row.afterValue, false)" />
</template>
</el-table-column>
</el-table>
<el-table :data="[{ sql: row.sql }]" border style="width: 100%">
<el-table-column prop="sql" label="SQL语句">
<template #default>
<pre class="sql" v-html="formatSql(row.sql)"></pre>
</template>
</el-table-column>
</el-table>
<el-table :data="row.parameters" border style="width: 100%">
<el-table-column prop="parameterName" label="参数名" width="200" />
<el-table-column prop="typeName" label="类型" width="100" />
<el-table-column prop="value" label="值" />
</el-table>
</el-card>
</template>
<!-- <template #row_buttons="{ row }">
<el-button icon="ele-InfoFilled" text type="primary" @click="handleView({ row })">详情</el-button>
</template> -->
</vxe-grid>
</el-card>
<!-- <el-dialog v-model="state.visible" draggable overflow destroy-on-close>
<template #header>
<div style="color: #fff">
<el-icon size="16" style="margin-right: 3px; display: inline; vertical-align: middle"> <ele-Document /> </el-icon>
<span> 日志详情 </span>
</div>
</template>
<el-tabs v-model="state.activeTab">
<el-tab-pane label="日志消息" name="message">
<el-scrollbar height="calc(100vh - 250px)">
<pre>{{ state.detail.message }}</pre>
</el-scrollbar>
</el-tab-pane>
<el-tab-pane label="SQL" name="sql" :lazy="true">
<el-scrollbar height="calc(100vh - 250px)">
<vue-json-pretty :data="state.detail.sql" showLength showIcon showLineNumber showSelectController />
</el-scrollbar>
</el-tab-pane>
<el-tab-pane label="参数" name="parameters" :lazy="true">
<el-scrollbar height="calc(100vh - 250px)">
<vue-json-pretty :data="state.detail.parameters" showLength showIcon showLineNumber showSelectController />
</el-scrollbar>
</el-tab-pane>
<el-tab-pane label="操作前记录" name="beforeData" :lazy="true">
<el-scrollbar height="calc(100vh - 250px)">
<vue-json-pretty :data="state.detail.beforeData" showLength showIcon showLineNumber showSelectController />
</el-scrollbar>
</el-tab-pane>
<el-tab-pane label="操作后记录" name="afterData" :lazy="true">
<el-scrollbar height="calc(100vh - 250px)">
<vue-json-pretty :data="state.detail.afterData" showLength showIcon showLineNumber showSelectController />
</el-scrollbar>
</el-tab-pane>
</el-tabs>
</el-dialog> -->
</div>
</template>
<script lang="ts" setup name="sysLogDiff">
import { onMounted, reactive, ref } from 'vue';
import { useDateTimeShortCust } from '/@/hooks/dateTimeShortCust';
import { VxeGridInstance, VxeGridListeners, VxeGridPropTypes } from 'vxe-table';
import { useVxeTable } from '/@/hooks/useVxeTableOptionsHook';
import { Local } from '/@/utils/storage';
// import VueJsonPretty from 'vue-json-pretty';
// import 'vue-json-pretty/lib/styles.css';
// import { StringToObj } from '/@/utils/json-utils';
import { getAPI } from '/@/utils/axios-utils';
import { SysLogDiffApi } from '/@/api-services/api';
import { SysLogDiff, PageLogInput } from '/@/api-services/models';
const xGrid = ref<VxeGridInstance>();
const shortcuts = useDateTimeShortCust();
const state = reactive({
queryParams: {
startTime: undefined,
endTime: undefined,
},
localPageParam: {
pageSize: 50 as number,
defaultSort: { field: 'id', order: 'desc', descStr: 'desc' },
},
visible: false,
detail: {
message: '' as string | null | undefined,
sql: undefined,
parameters: undefined,
afterData: undefined,
beforeData: undefined,
},
activeTab: 'message',
});
// 本地存储参数
const localPageParamKey = 'localPageParam:sysDiffLog';
// 表格参数配置
const options = useVxeTable<SysLogDiff>(
{
id: 'sysDiffLog',
name: '差异日志',
columns: [
// { type: 'checkbox', width: 40 },
{ field: 'seq', type: 'seq', title: '序号', width: 60, fixed: 'left' },
{ field: 'expand', type: 'expand', width: 40, slots: { content: 'row_content' } },
{ field: 'createTime', title: '操作时间', minWidth: 100, showOverflow: 'tooltip' },
{ field: 'diffType', title: '差异操作', minWidth: 150, showOverflow: 'tooltip' },
// { field: 'sql', title: 'Sql语句', minWidth: 150, showOverflow: 'tooltip' },
// { field: 'parameters', title: '参数', minWidth: 150, showOverflow: 'tooltip' },
{ field: 'elapsed', title: '耗时(ms)', minWidth: 150, showOverflow: 'tooltip' },
{ field: 'message', title: '日志消息', minWidth: 150, showOverflow: 'tooltip' },
// { field: 'beforeData', title: '操作前记录', minWidth: 150, showOverflow: 'tooltip' },
// { field: 'afterData', title: '操作后记录', minWidth: 150, showOverflow: 'tooltip' },
{ field: 'businessData', title: '业务对象', minWidth: 150, showOverflow: 'tooltip' },
// { title: '操作', fixed: 'right', width: 100, showOverflow: true, slots: { default: 'row_buttons' } },
],
},
// vxeGrid配置参数(此处可覆写任何参数)参考vxe-table官方文档
{
// 代理配置
proxyConfig: { autoLoad: true, ajax: { query: ({ page, sort }) => handleQueryApi(page, sort) } },
// 排序配置
sortConfig: { defaultSort: Local.get(localPageParamKey)?.defaultSort || state.localPageParam.defaultSort },
// 分页配置
pagerConfig: { pageSize: Local.get(localPageParamKey)?.pageSize || state.localPageParam.pageSize },
// 工具栏配置
toolbarConfig: { export: false },
}
);
// 页面初始化
onMounted(() => {
state.localPageParam = Local.get(localPageParamKey) || state.localPageParam;
});
// 查询api
const handleQueryApi = async (page: VxeGridPropTypes.ProxyAjaxQueryPageParams, sort: VxeGridPropTypes.ProxyAjaxQuerySortCheckedParams) => {
const params = Object.assign(state.queryParams, { page: page.currentPage, pageSize: page.pageSize, field: sort.field, order: sort.order, descStr: 'desc' }) as PageLogInput;
return getAPI(SysLogDiffApi).apiSysLogDiffPagePost(params);
};
// 查询操作
const handleQuery = async (reset = false) => {
options.loading = true;
reset ? await xGrid.value?.commitProxy('reload') : await xGrid.value?.commitProxy('query');
options.loading = false;
};
// 重置操作
const resetQuery = async () => {
state.queryParams.startTime = undefined;
state.queryParams.endTime = undefined;
await xGrid.value?.commitProxy('reload');
};
// 表格事件
const gridEvents: VxeGridListeners<SysLogDiff> = {
// 只对 pager-config 配置时有效,分页发生改变时会触发该事件
async pageChange({ pageSize }) {
state.localPageParam.pageSize = pageSize;
Local.set(localPageParamKey, state.localPageParam);
},
// 当排序条件发生变化时会触发该事件
async sortChange({ field, order }) {
state.localPageParam.defaultSort = { field: field, order: order!, descStr: 'desc' };
Local.set(localPageParamKey, state.localPageParam);
},
};
// // 查看详情
// const handleView = async ({ row }: any) => {
// const { data } = await getAPI(SysLogDiffApi).apiSysLogDiffDetailIdGet(row.id);
// state.activeTab = 'message';
// state.detail.message = data?.result?.diffType;
// // 如果请求参数是JSON字符串则尝试转为JSON对象
// state.detail.sql = StringToObj(data?.result?.sql);
// state.detail.parameters = StringToObj(data?.result?.parameters);
// state.detail.afterData = StringToObj(data?.result?.afterData);
// state.detail.beforeData = StringToObj(data?.result?.beforeData);
// state.visible = true;
// };
// 合并差异表格表名列
const diffTableSpanMethod = ({ columnIndex, rowIndex }: any, itme: any) => {
if (columnIndex === 0) {
if (rowIndex === 0) {
return {
rowspan: itme.columns.length,
colspan: 1,
};
} else {
return {
rowspan: 0,
colspan: 0,
};
}
}
};
const formatSql = (sql: string) => {
// 移除多余的空格
let formatted = sql.replace(/\s+/g, ' ').trim();
// 替换反引号包裹的字段
formatted = formatted.replace(/`([^`]+)`/g, '<span class="sql-backtick">`$1`</span>');
// 替换@参数
formatted = formatted.replace(/(@\w+)/g, '<span class="sql-param">$1</span>');
// 替换SQL关键字
formatted = formatted.replace(
/\b(INSERT|DELETE|UPDATE|SELECT|FROM|SET|JOIN|ON|AND|OR|IN|NOT|IS|NULL|WHERE|TRUE|FALSE|LIKE|ORDER BY|GROUP BY|HAVING|LIMIT|AS|WITH|CASE|WHEN|THEN|ELSE|END)\b/g,
'<span class="sql-keyword">$1</span>'
);
// 智能换行
// 在SET和VALUES后面添加换行
formatted = formatted.replace(/(SET|VALUES)(?=\s)/g, '$1\n ');
// 在逗号后面添加换行,除非是最后一个逗号
formatted = formatted.replace(/,(?![^]*?,\s*$)(?=[^\s])/g, ',\n ');
// 在WHERE前添加换行如果WHERE前面不是逗号
formatted = formatted.replace(/([\s\S]+)(WHERE)/g, '$1\n$2');
// 移除由于换行添加的多余空格
formatted = formatted.replace(/\n\s*\n/g, '\n');
return formatted;
};
function lcs(s1: string, s2: string): number[][] {
const m = s1.length;
const n = s2.length;
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (s1[i - 1] === s2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp;
}
function markDiff(oldData: any, newData: any, returnOld: boolean): string {
if (typeof oldData !== 'string' || typeof newData !== 'string') {
return `<span class="diff-${returnOld ? 'delete' : 'add'}">${returnOld ? oldData : newData}</span>`;
}
const dp = lcs(oldData, newData);
const m = oldData.length;
const n = newData.length;
let oldIndex = m,
newIndex = n;
const diffResult: { type: string; content: string }[] = [];
while (oldIndex > 0 || newIndex > 0) {
if (oldIndex > 0 && newIndex > 0 && oldData[oldIndex - 1] === newData[newIndex - 1]) {
diffResult.push({ type: 'unchanged', content: oldData[oldIndex - 1] });
oldIndex--;
newIndex--;
} else if (newIndex > 0 && (oldIndex === 0 || dp[oldIndex][newIndex - 1] >= dp[oldIndex - 1][newIndex])) {
diffResult.push({ type: 'add', content: newData[newIndex - 1] });
newIndex--;
} else {
diffResult.push({ type: 'delete', content: oldData[oldIndex - 1] });
oldIndex--;
}
}
const result = diffResult
.reverse()
.map((chunk) => {
switch (chunk.type) {
case 'add':
return `<span class="diff-add">${chunk.content}</span>`;
case 'delete':
return `<span class="diff-delete">${chunk.content}</span>`;
default:
return chunk.content;
}
})
.join('');
return result.replace(returnOld ? /<span class="diff-add">(.*?)<\/span>/g : /<span class="diff-delete">(.*?)<\/span>/g, '');
}
</script>
<style lang="scss" scoped>
.el-popper {
max-width: 60%;
}
:deep(pre.sql) {
white-space: pre-wrap;
.sql-param {
color: green;
}
.sql-keyword {
color: blue;
}
.sql-backtick {
color: blueviolet;
}
span.diff-unchanged {
color: inherit;
}
span.diff-delete {
color: red;
}
span.diff-add {
color: green;
}
}
:deep(pre) {
span.diff-delete {
color: red;
}
span.diff-add {
color: green;
}
}
</style>