Skip to content

一、背景与问题

系统中的数据都是非常重要的,但是如有人不小心修改了数据,异或有意而为之等等,这样都会对系统造成很大的影响,甚至对于公司可能也会造成一些影响。所以对于一个个重要的数据但凡谁去改动,都应该有详细的记录变更,就好比大家熟悉的git一样,任何变动都有对应的记录。
那么具体需要记录哪些呢?

  • 时间:什么时候修改的
  • 用户:具体谁修改的
  • 设备:在哪个设备、ip等
  • 修改前:修改之前的数据
  • 修改后:修改之后的数据

二、架构与思想

具体后端的架构设计请看后端数据变动记录设计 ;
对于前端而言我们分析下需求:

  • 应该抽成一个组件,应该很多需要用到
  • 需要有数据对比,准备采用 git diff 对比,用diffdiff2html

三、具体使用

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创新实验室”
“烩面里加肉”
“ 咖啡配胡辣汤,提神又饱腹”
抖音 : 六边形工程师
直播:赚钱、代码、中医