动态父组件
<template>
<el-button
type="primary"
size="small"
plain
@click="showDiffDialog(subItem)"
>查看修改内容</el-button
>
<TextDiffDialog
v-model:visible="diffDialogVisible"
:before="currentDiffItem?.before?.[currentDiffItem?.fieldName] || ''"
:after="currentDiffItem?.after?.[currentDiffItem?.fieldName] || ''"
/>
</el-scrollbar>
</template>
<script>
import TextDiffDialog from './TextDiffDialog.vue';
export default {
name: 'logIndex',
components: { TextDiffDialog },
data() {
return {
diffDialogVisible: false,
currentDiffItem: null,
};
},
props: {
formId: String,
},
methods: {
showDiffDialog(item) {
this.currentDiffItem = item;
this.diffDialogVisible = true;
},
},
created() {},
mounted() {
},
};
</script>
<style scoped lang="scss">
.el-button--small {
padding: 0px 6px;
height: 26px;
line-height: 26px;
border: 0;
}
</style>
npm install diff
//子组件
<template>
<el-dialog
v-model="dialogVisible"
title="变更对比"
width="60%"
:before-close="handleClose"
class="text-diff-dialog"
>
<template #header>
<div class="diff-header">
<div class="diff-title">
<i class="iconfont icon-shejibiangeng"></i>变更对比
</div>
<div class="diff-stats">
<el-switch v-model="showDiff" active-text="显示修改标注" />
<el-tooltip
effect="light"
content="<p><span style='background: #52bd94;color: #172890;padding: 2px;border-radius: 2px;'>绿色背景</span> 表示新增</p><p><span style='background:#f4b6b6;color: #172890;padding: 2px;border-radius: 2px;'>红色背景</span> 表示删除</p>"
raw-content
>
<i style="color: #a5adba" class="el-icon-question"></i>
</el-tooltip>
</div>
</div>
</template>
<div class="diff-container">
<div class="diff-content" ref="diffContent">
<div class="diff-panel before">
<div class="panel-header">修改前</div>
<div class="panel-content">
<div
v-for="(line, index) in beforeLines"
:key="'before-' + index"
class="diff-line"
>
<span class="line-number">{{ line.lineNumber }}</span>
<span class="line-content" v-html="line.content"></span>
</div>
</div>
</div>
<div class="diff-panel after">
<div
class="panel-header"
style="display: flex; justify-content: space-between"
>
修改后 <span class="total">有 {{ totalChanges }} 处修改</span>
</div>
<div class="panel-content">
<div
v-for="(line, index) in afterLines"
:key="'after-' + index"
class="diff-line"
>
<span class="line-number">{{ line.lineNumber }}</span>
<span class="line-content" v-html="line.content"></span>
</div>
</div>
</div>
</div>
</div>
</el-dialog>
</template>
<script>
import { diffChars } from 'diff';
export default {
name: 'TextDiffDialog',
props: {
visible: {
type: Boolean,
default: false,
},
before: {
type: String,
default: '',
},
after: {
type: String,
default: '',
},
},
data() {
return {
dialogVisible: false,
beforeLines: [],
afterLines: [],
showDiff: false,
totalChanges: 0,
};
},
watch: {
visible(val) {
this.dialogVisible = val;
if (val) {
this.$nextTick(() => {
this.generateDiff();
});
}
},
dialogVisible(val) {
if (!val) {
this.showDiff = false;
this.$emit('update:visible', false);
}
},
showDiff() {
this.generateDiff();
},
},
methods: {
handleClose() {
this.dialogVisible = false;
},
generateDiff() {
const beforeText = this.before || '';
const afterText = this.after || '';
// 将文本分割成行
const beforeLines = beforeText.split('\n');
const afterLines = afterText.split('\n');
this.beforeLines = beforeLines.map((line, index) => ({
lineNumber: index + 1,
content: this.escapeHtml(line),
}));
// 使用diffChars进行字符级别的差异比较
const changes = diffChars(beforeText, afterText);
let currentLine = '';
let lineNumber = 1;
this.afterLines = [];
this.totalChanges = 0;
changes.forEach((change) => {
if (change.added) {
if (this.showDiff) {
currentLine += `<span class="diff-added">${this.escapeHtml(change.value)}</span>`;
} else {
currentLine += this.escapeHtml(change.value);
}
this.totalChanges++;
} else if (change.removed) {
if (this.showDiff) {
currentLine += `<span class="diff-deleted">${this.escapeHtml(change.value)}</span>`;
}
this.totalChanges++;
} else {
currentLine += this.escapeHtml(change.value);
}
// 处理换行
if (change.value.includes('\n')) {
const lines = currentLine.split('\n');
lines.forEach((line, index) => {
if (index < lines.length - 1) {
this.afterLines.push({
lineNumber: lineNumber++,
content: line,
});
}
});
currentLine = lines[lines.length - 1];
}
});
// 添加最后一行
if (currentLine) {
this.afterLines.push({
lineNumber: lineNumber,
content: currentLine,
});
}
},
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
},
},
};
</script>
<style lang="scss" scoped>
.text-diff-dialog {
:deep(.el-dialog__body) {
padding: 0;
}
}
.diff-container {
height: 600px;
display: flex;
flex-direction: column;
width: 100%;
}
.diff-header {
padding: 0 6px 6px 6px;
border-bottom: 1px solid #ebeef5;
display: flex;
justify-content: space-between;
align-items: center;
}
.diff-stats {
display: flex;
align-items: center;
gap: 6px;
margin-right: 20px;
}
.total {
color: #6b778c !important;
font-size: 13px;
}
.diff-content {
flex: 1;
display: flex;
overflow: hidden;
}
.diff-panel {
flex: 1;
display: flex;
flex-direction: column;
border-right: 1px solid #ebeef5;
&:last-child {
border-right: none;
}
.panel-header {
padding: 12px 16px;
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
font-weight: 500;
color: #172b4d;
font-size: 16px;
}
.panel-content {
flex: 1;
overflow-y: auto;
padding: 16px;
font-family: monospace;
white-space: pre-wrap;
word-break: break-all;
}
}
.diff-line {
display: flex;
line-height: 1.5;
.line-number {
color: #909399;
text-align: right;
padding-right: 8px;
user-select: none;
}
.line-content {
color: #172b4d;
font-size: 14px;
flex: 1;
:deep(.diff-added) {
background-color: #52bd94;
border-radius: 2px;
padding: 2px;
margin-left: 2px;
}
:deep(.diff-deleted) {
background-color: #f4b6b6;
text-decoration: line-through;
border-radius: 2px;
padding: 2px;
margin-left: 2px;
}
}
}
.diff-title {
color: #172b4d;
font-size: 16px;
font-weight: 500;
line-height: 24px; /* 150% */
}
</style>