结果图

背景
某天要给管理后台的数据配置添加一个审核功能,为了更好的审核,就需要看到数据差异,于是就要开发一款对比差异的页面
调研
- 调研有没有现成的组件
- 评估自研成本(有AI的加入,成本大幅度降低)
现成的组件感觉有点一般,于是我打算自研,用的是diff
的diffChars
功能,因为要精确到字符级 ,如果是行级,可以用diffLines
,更简单
用diffChars
需要自己还原原来的数据,最重要的还是他的精度高,可以做字符级的高亮,他的数据结构参考
json
[
{
"count": 17,
"added": false,
"removed": false,
"value": "{\n \"name\": \"Alic"
},
....
]
代码
技术栈:vue3、unocss
子组件--Diff.vue
html
<script lang="ts" setup>
import type { Change } from 'diff'
import { diffChars } from 'diff'
const props = defineProps<{
oldData: unknown
newData: unknown
}>()
const oldStr = computed(() => `${JSON.stringify(props.oldData, null, 2)}\n`)
const newStr = computed(() => `${JSON.stringify(props.newData, null, 2)}\n`)
const diffCharList = computed<Change[]>(() => diffChars(oldStr.value, newStr.value))
interface Segment {
text: string
added?: boolean
removed?: boolean
}
function buildLines() {
const oldLines: { segments: Segment[]; changed: boolean }[] = []
const newLines: { segments: Segment[]; changed: boolean }[] = []
const oldCurrent: Segment[] = []
const newCurrent: Segment[] = []
const pushLine = () => {
oldLines.push({
segments: [...oldCurrent],
changed: oldCurrent.some((s) => s.removed),
})
newLines.push({
segments: [...newCurrent],
changed: newCurrent.some((s) => s.added),
})
oldCurrent.length = 0
newCurrent.length = 0
}
diffCharList.value.forEach((part) => {
const lines = part.value.split(/(?<=\n)/)
lines.forEach((line, i) => {
const isNewLine = line.endsWith('\n')
if (part.added) {
newCurrent.push({ text: line, added: true })
if (isNewLine) pushLine()
} else if (part.removed) {
oldCurrent.push({ text: line, removed: true })
if (isNewLine) pushLine()
} else {
oldCurrent.push({ text: line })
newCurrent.push({ text: line })
if (isNewLine) pushLine()
}
})
})
if (oldCurrent.length || newCurrent.length) pushLine()
return { oldLines, newLines }
}
const { oldLines, newLines } = computed(() => buildLines()).value
</script>
<template>
<div class="font-mono text-14 lh-32 flex overflow-auto b b-gray-300 bg-white">
<!-- 旧数据 -->
<div class="flex-1 p-4 b-r b-gray-300 overflow-x-auto">
<div
v-for="(line, index) in oldLines"
:key="'old-' + index"
:class="{
'bg-red-100': line.changed,
}"
class="whitespace-pre-wrap"
>
<template v-for="(segment, i) in line.segments" :key="i">
<span
:class="{
'bg-red-300 line-through': segment.removed,
}"
>
{{ segment.text }}
</span>
</template>
</div>
</div>
<!-- 新数据 -->
<div class="flex-1 p-4 overflow-x-auto">
<div
v-for="(line, index) in newLines"
:key="'new-' + index"
:class="{
'bg-green-100': line.changed,
}"
class="whitespace-pre-wrap"
>
<template v-for="(segment, i) in line.segments" :key="i">
<span :class="segment.added ? 'bg-green-300' : ''">
{{ segment.text }}
</span>
</template>
</div>
</div>
</div>
</template>
<style lang="scss" scoped></style>
父组件
html
<Diff :old-data="diffObj.oldData" :new-data="diffObj.newData" />
diffObj.value.oldData = {
name: 'Alice',
age: 25,
address: {
city: 'New York',
zip: '10001',
},
tags: ['developer', 'frontend'],
text: 'hello world fsdkafjs撒放假就疯狂拉萨大家疯狂了发啥地方撒的',
text2: 'hello world fsdkafjs撒放假就疯狂拉萨大家疯狂的发啥地方撒的',
}
diffObj.value.newData = {
name: 'Alicia', // 修改
age: 26, // 修改
address: {
city: 'San Francisco', // 修改
zip: '94105', // 修改
},
tags: ['developer', 'fullstack'], // 修改数组项
text: 'hello world fsdkafjs撒放假就疯狂拉萨大家疯狂了发啥地方撒的',
text2: 'hello world fsdkafjs撒放假就疯狂拉萨大家疯狂了发啥地方撒的',
active: true, // 新增字段
}