一、背景与问题
系统中的数据都是非常重要的,但是如有人不小心修改了数据,异或有意而为之等等,这样都会对系统造成很大的影响,甚至对于公司可能也会造成一些影响。所以对于一个个重要的数据但凡谁去改动,都应该有详细的记录变更,就好比大家熟悉的git一样,任何变动都有对应的记录。
那么具体需要记录哪些呢?
- 时间:什么时候修改的
- 用户:具体谁修改的
- 设备:在哪个设备、ip等
- 修改前:修改之前的数据
- 修改后:修改之后的数据
二、架构与思想
具体后端的架构设计请看后端数据变动记录设计 ;
对于前端而言我们分析下需求:
- 应该抽成一个组件,应该很多需要用到
- 需要有数据对比,准备采用 git diff 对比,用
diff
,diff2html
库
三、具体使用
3.1、DataTracer 组件
在src/components/support
中,有DataTracer
组件,在使用的时候可以直接引用。DataTracer
组件有两个参数:
js
let props = defineProps({
// 数据id
dataId: {
type: Number,
},
// 数据 类型
type: {
type: Number,
},
});
3.2、添加DataTracer类型
在前端:src/constants/support/data-tracer-const.js
中 找到(添加)对应的类型
js
// 业务类型
export const DATA_TRACER_TYPE_ENUM = {
GOODS: {
value: 1,
desc: '商品',
},
OA_NOTICE: {
value: 2,
desc: 'OA-通知公告',
},
OA_ENTERPRISE: {
value: 3,
desc: 'OA-企业信息',
},
};
3.3、引入组件
引入组件,传入 常量参数 :
js
<a-tab-pane key="dataTracer" tab="变更记录">
<!--数据变更组件--->
<DataTracer :dataId="enterpriseId" :type="DATA_TRACER_TYPE_ENUM.OA_ENTERPRISE.value" />
</a-tab-pane>
import DataTracer from '/@/components/support/data-tracer/index.vue';
import { DATA_TRACER_TYPE_ENUM } from '/@/constants/support/data-tracer-const';
四、实现原理
4.1、抽成组件
根据需求,我们清晰的知道,数据变更DataTracer
各个系统都会用到,属于支撑Support属性
,所以在 需要将组件定义在src/components/support
中。
代码:src/components/support/datatracer/index.vue
:
js
<!--
* 数据变动记录 表格 组件
*
* @Author: 1024创新实验室-主任:卓大
* @lastUpdated: 2023-08-12 21:01:52
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
*
-->
<template>
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="关键字" class="smart-query-form-item">
<a-input style="width: 300px" v-model:value="queryForm.keywords" placeholder="变更内容" />
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="onReload">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-card size="small" :bordered="false">
<a-table size="small" :dataSource="tableData" :columns="columns" rowKey="dataTracerId" :pagination="false" bordered>
<template #bodyCell="{ record, index, column }">
<template v-if="column.dataIndex === 'dataTracerId'">
<div>{{ index + 1 }}</div>
</template>
<template v-if="column.dataIndex === 'userName'">
<div>{{record.userName}} ({{ $smartEnumPlugin.getDescByValue('USER_TYPE_ENUM', record.userType) }})</div>
</template>
<template v-if="column.dataIndex === 'userAgent'">
<div>{{ record.browser }} / {{ record.os }} / {{ record.device }}</div>
</template>
<template v-if="column.dataIndex === 'content'">
<div class="operate-content" v-html="record.content"></div>
</template>
<template v-else-if="column.dataIndex === 'action'">
<a-button v-if="record.diffOld || record.diffNew" @click="showDetail(record)" type="link">详情 </a-button>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="onSearch"
@showSizeChange="onSearch"
:show-total="(total) => `共${total}条`"
/>
</div>
<a-modal v-model:visible="visibleDiff" width="90%" title="数据比对" :footer="null">
<div v-html="prettyHtml"></div>
</a-modal>
</a-card>
</template>
<script setup>
import * as Diff from 'diff';
import * as Diff2Html from 'diff2html';
import 'diff2html/bundles/css/diff2html.min.css';
import uaparser from 'ua-parser-js';
import { nextTick, reactive, ref, watch } from 'vue';
import { dataTracerApi } from '/@/api/support/data-tracer/data-tracer-api';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { smartSentry } from '/@/lib/smart-sentry';
let props = defineProps({
// 数据id
dataId: {
type: Number,
},
// 数据 类型
type: {
type: Number,
},
});
const columns = reactive([
{
title: '序号',
dataIndex: 'dataTracerId',
width: 50,
},
{
title: '操作时间',
dataIndex: 'createTime',
width: 150,
},
{
title: '操作人',
dataIndex: 'userName',
width: 100,
ellipsis: true,
},
{
title: 'IP',
dataIndex: 'ip',
ellipsis: true,
width: 100,
},
{
title: '客户端',
dataIndex: 'userAgent',
ellipsis: true,
width: 150,
},
{
title: '操作内容',
dataIndex: 'content',
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 80,
},
]);
// --------------- 查询表单、查询方法 ---------------
const queryFormState = {
pageNum: 1,
pageSize: PAGE_SIZE,
searchCount: true,
keywords: undefined,
};
const queryForm = reactive({ ...queryFormState });
const tableLoading = ref(false);
const tableData = ref([]);
const total = ref(0);
function onReload() {
Object.assign(queryForm, queryFormState);
onSearch();
}
async function onSearch() {
try {
tableLoading.value = true;
let responseModel = await dataTracerApi.queryList(Object.assign({}, queryForm, { dataId: props.dataId, type: props.type }));
for (const e of responseModel.data.list) {
if (!e.userAgent) {
continue;
}
let ua = uaparser(e.userAgent);
e.browser = ua.browser.name;
e.os = ua.os.name;
e.device = ua.device.vendor ? ua.device.vendor + ua.device.model : '';
}
const list = responseModel.data.list;
total.value = responseModel.data.total;
tableData.value = list;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
// ========= 定义 watch 监听 ===============
watch(
() => props.dataId,
(e) => {
if (e) {
queryForm.dataId = e;
onSearch();
}
},
{ immediate: true }
);
// --------------- diff 特效 ---------------
// diff
const visibleDiff = ref(false);
let prettyHtml = ref('');
function showDetail(record) {
visibleDiff.value = true;
let diffOld = record.diffOld.replaceAll('<br/>','\r\n');
let diffNew = record.diffNew.replaceAll('<br/>','\r\n');
console.log(diffOld)
console.log(diffNew)
const args = ['', diffOld, diffNew, '变更前', '变更后'];
let diffPatch = Diff.createPatch(...args);
let html = Diff2Html.html(diffPatch, {
drawFileList: false,
matching: 'words',
diffMaxChanges: 1000,
outputFormat: 'side-by-side',
});
prettyHtml.value = html;
nextTick(() => {
let diffDiv = document.querySelectorAll('.d2h-file-side-diff');
if (diffDiv.length > 0) {
let left = diffDiv[0],
right = diffDiv[1];
left.addEventListener('scroll', function (e) {
if (left.scrollLeft != right.scrollLeft) {
right.scrollLeft = left.scrollLeft;
}
});
right.addEventListener('scroll', function (e) {
if (left.scrollLeft != right.scrollLeft) {
left.scrollLeft = right.scrollLeft;
}
});
}
});
}
</script>
<style scoped lang="less">
.operate-content {
line-height: 20px;
margin: 5px 0px;
}
</style>
4.2、git diff 特效
效果:
具体用户、IP、设备、变更项 | 基于git diff的变更查看 |
代码:
js
// --------------- diff 特效 ---------------
// diff
const visibleDiff = ref(false);
let prettyHtml = ref('');
function showDetail(record) {
visibleDiff.value = true;
let diffOld = record.diffOld.replaceAll('<br/>','\r\n');
let diffNew = record.diffNew.replaceAll('<br/>','\r\n');
console.log(diffOld)
console.log(diffNew)
const args = ['', diffOld, diffNew, '变更前', '变更后'];
let diffPatch = Diff.createPatch(...args);
let html = Diff2Html.html(diffPatch, {
drawFileList: false,
matching: 'words',
diffMaxChanges: 1000,
outputFormat: 'side-by-side',
});
prettyHtml.value = html;
nextTick(() => {
let diffDiv = document.querySelectorAll('.d2h-file-side-diff');
if (diffDiv.length > 0) {
let left = diffDiv[0],
right = diffDiv[1];
left.addEventListener('scroll', function (e) {
if (left.scrollLeft != right.scrollLeft) {
right.scrollLeft = left.scrollLeft;
}
});
right.addEventListener('scroll', function (e) {
if (left.scrollLeft != right.scrollLeft) {
left.scrollLeft = right.scrollLeft;
}
});
}
});
}
联系我们
1024创新实验室-主任:卓大,混迹于各个技术圈,研究过计算机,熟悉点 java,略懂点前端。
1024创新实验室(河南·洛阳) 致力于成为中原领先、国内一流的技术团队,以技术创新为驱动,合作各类项目(软件外包、技术顾问、培训等等)。
加微信: 卓大 拉你入群,一起学习 | 公众号 :六边形工程师 分享:赚钱、代码、生活 | 请 “1024创新实验室” “烩面里加肉” “ 咖啡配胡辣汤,提神又饱腹” | 抖音 : 六边形工程师 直播:赚钱、代码、中医 |