feat(web): 配置历史对比改为左右分栏,支持修改行识别
- 对比视图从上下布局改为左右分栏(当前生效 vs 历史版本) - 新增 modified 行类型,相邻的删除+新增行配对标记为已修改(黄色高亮) - 移除 diff npm 依赖,改用内置 LCS 算法实现行级对比 - 弹窗宽度调整为 1200px 适配双栏布局
This commit is contained in:
+5
-5
@@ -9,14 +9,14 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.6.0",
|
||||||
|
"element-ui": "^2.15.14",
|
||||||
"vue": "^2.7.16",
|
"vue": "^2.7.16",
|
||||||
"vue-router": "^3.6.5",
|
"vue-router": "^3.6.5",
|
||||||
"vuex": "^3.6.2",
|
"vuex": "^3.6.2"
|
||||||
"element-ui": "^2.15.14",
|
|
||||||
"axios": "^1.6.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^5.0.0",
|
"@vitejs/plugin-vue2": "^2.3.1",
|
||||||
"@vitejs/plugin-vue2": "^2.3.1"
|
"vite": "^5.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,8 @@
|
|||||||
<el-table-column prop="version" label="版本" width="100"></el-table-column>
|
<el-table-column prop="version" label="版本" width="100"></el-table-column>
|
||||||
<el-table-column label="操作" width="200">
|
<el-table-column label="操作" width="200">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
|
<el-button type="text" @click="showHistory(scope.row)">变更历史</el-button>
|
||||||
<el-button type="text" @click="$router.push(`/config/edit/${scope.row.id}`)">编辑</el-button>
|
<el-button type="text" @click="$router.push(`/config/edit/${scope.row.id}`)">编辑</el-button>
|
||||||
<el-button type="text" @click="showHistory(scope.row)">历史</el-button>
|
|
||||||
<el-button type="text" style="color: #F56C6C" @click="handleDelete(scope.row)">删除</el-button>
|
<el-button type="text" style="color: #F56C6C" @click="handleDelete(scope.row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
layout="total, prev, pager, next"
|
layout="total, prev, pager, next"
|
||||||
></el-pagination>
|
></el-pagination>
|
||||||
|
|
||||||
<el-dialog title="配置历史" :visible.sync="historyVisible" width="900px">
|
<el-dialog title="变更历史" :visible.sync="historyVisible" width="900px">
|
||||||
<el-table :data="historyList" border stripe>
|
<el-table :data="historyList" border stripe>
|
||||||
<el-table-column prop="id" label="ID" width="60"></el-table-column>
|
<el-table-column prop="id" label="ID" width="60"></el-table-column>
|
||||||
<el-table-column prop="createdBy" label="操作人" width="100"></el-table-column>
|
<el-table-column prop="createdBy" label="操作人" width="100"></el-table-column>
|
||||||
@@ -57,27 +57,135 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog title="历史版本内容" :visible.sync="contentVisible" width="800px" append-to-body>
|
<el-dialog title="历史版本对比" :visible.sync="contentVisible" width="1200px" append-to-body>
|
||||||
<div style="margin-bottom: 10px">
|
<div style="margin-bottom: 10px">
|
||||||
<strong>配置名称:</strong>{{ currentHistory.name }} |
|
<strong>配置名称:</strong>{{ currentHistory.name }} |
|
||||||
<strong>版本:</strong>{{ currentHistory.version }} |
|
<strong>版本:</strong>{{ currentHistory.version }} |
|
||||||
<strong>类型:</strong>{{ getTypeName(currentHistory.type) }}
|
<strong>类型:</strong>{{ getTypeName(currentHistory.type) }}
|
||||||
</div>
|
</div>
|
||||||
<el-input
|
<div style="margin-bottom: 8px; font-size: 13px; color: #666;">
|
||||||
v-model="formattedContent"
|
<span style="background: #ffeef0; padding: 2px 6px; margin-right: 10px;">红色 = 已删除</span>
|
||||||
type="textarea"
|
<span style="background: #e6ffed; padding: 2px 6px; margin-right: 10px;">绿色 = 新增</span>
|
||||||
:rows="25"
|
<span style="background: #fff8e1; padding: 2px 6px;">黄色 = 已修改</span>
|
||||||
readonly
|
</div>
|
||||||
style="font-family: monospace"
|
<div class="diff-container">
|
||||||
></el-input>
|
<div class="diff-panel">
|
||||||
|
<div class="diff-panel-header">当前生效配置</div>
|
||||||
|
<div class="diff-panel-body">
|
||||||
|
<div v-for="(line, index) in leftLines" :key="'l'+index" :class="['diff-line', line.type]">
|
||||||
|
<span class="line-number">{{ line.lineNum }}</span>
|
||||||
|
<span class="line-content">{{ line.text }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="diff-panel">
|
||||||
|
<div class="diff-panel-header">历史版本</div>
|
||||||
|
<div class="diff-panel-body">
|
||||||
|
<div v-for="(line, index) in rightLines" :key="'r'+index" :class="['diff-line', line.type]">
|
||||||
|
<span class="line-number">{{ line.lineNum }}</span>
|
||||||
|
<span class="line-content">{{ line.text }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</el-card>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getConfigList, deleteConfig, getConfigHistory, rollbackConfig } from '../../api/config'
|
import { getConfigList, deleteConfig, getConfigHistory, rollbackConfig, getConfig } from '../../api/config'
|
||||||
import request from '../../api/request'
|
import request from '../../api/request'
|
||||||
|
|
||||||
|
function computeDiff(oldText, newText) {
|
||||||
|
const oldLines = oldText.split('\n')
|
||||||
|
const newLines = newText.split('\n')
|
||||||
|
const m = oldLines.length
|
||||||
|
const n = newLines.length
|
||||||
|
|
||||||
|
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0))
|
||||||
|
for (let i = 1; i <= m; i++) {
|
||||||
|
for (let j = 1; j <= n; j++) {
|
||||||
|
if (oldLines[i - 1] === newLines[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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ops = []
|
||||||
|
let i = m, j = n
|
||||||
|
while (i > 0 || j > 0) {
|
||||||
|
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
||||||
|
ops.push({ type: 'equal', oldIdx: i - 1, newIdx: j - 1 })
|
||||||
|
i--; j--
|
||||||
|
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
||||||
|
ops.push({ type: 'added', newIdx: j - 1 })
|
||||||
|
j--
|
||||||
|
} else {
|
||||||
|
ops.push({ type: 'removed', oldIdx: i - 1 })
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ops.reverse()
|
||||||
|
|
||||||
|
// 配对相邻 removed+added 为 modified
|
||||||
|
const paired = []
|
||||||
|
let k = 0
|
||||||
|
while (k < ops.length) {
|
||||||
|
if (ops[k].type === 'removed') {
|
||||||
|
const removedGroup = []
|
||||||
|
while (k < ops.length && ops[k].type === 'removed') {
|
||||||
|
removedGroup.push(ops[k])
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
const addedGroup = []
|
||||||
|
while (k < ops.length && ops[k].type === 'added') {
|
||||||
|
addedGroup.push(ops[k])
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
const pairCount = Math.min(removedGroup.length, addedGroup.length)
|
||||||
|
for (let p = 0; p < pairCount; p++) {
|
||||||
|
paired.push({ type: 'modified', oldIdx: removedGroup[p].oldIdx, newIdx: addedGroup[p].newIdx })
|
||||||
|
}
|
||||||
|
for (let p = pairCount; p < removedGroup.length; p++) {
|
||||||
|
paired.push(removedGroup[p])
|
||||||
|
}
|
||||||
|
for (let p = pairCount; p < addedGroup.length; p++) {
|
||||||
|
paired.push(addedGroup[p])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
paired.push(ops[k])
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成左右面板行
|
||||||
|
const left = []
|
||||||
|
const right = []
|
||||||
|
let leftNum = 0, rightNum = 0
|
||||||
|
for (const op of paired) {
|
||||||
|
if (op.type === 'equal') {
|
||||||
|
leftNum++; rightNum++
|
||||||
|
left.push({ text: oldLines[op.oldIdx], type: 'unchanged', lineNum: leftNum })
|
||||||
|
right.push({ text: newLines[op.newIdx], type: 'unchanged', lineNum: rightNum })
|
||||||
|
} else if (op.type === 'modified') {
|
||||||
|
leftNum++; rightNum++
|
||||||
|
left.push({ text: oldLines[op.oldIdx], type: 'modified', lineNum: leftNum })
|
||||||
|
right.push({ text: newLines[op.newIdx], type: 'modified', lineNum: rightNum })
|
||||||
|
} else if (op.type === 'removed') {
|
||||||
|
leftNum++
|
||||||
|
left.push({ text: oldLines[op.oldIdx], type: 'removed', lineNum: leftNum })
|
||||||
|
right.push({ text: '', type: 'empty', lineNum: '' })
|
||||||
|
} else if (op.type === 'added') {
|
||||||
|
rightNum++
|
||||||
|
left.push({ text: '', type: 'empty', lineNum: '' })
|
||||||
|
right.push({ text: newLines[op.newIdx], type: 'added', lineNum: rightNum })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { left, right }
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -91,7 +199,8 @@ export default {
|
|||||||
currentConfig: null,
|
currentConfig: null,
|
||||||
contentVisible: false,
|
contentVisible: false,
|
||||||
currentHistory: {},
|
currentHistory: {},
|
||||||
formattedContent: ''
|
leftLines: [],
|
||||||
|
rightLines: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -135,17 +244,23 @@ export default {
|
|||||||
this.historyVisible = true
|
this.historyVisible = true
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
},
|
},
|
||||||
viewHistoryContent(row) {
|
async viewHistoryContent(row) {
|
||||||
this.currentHistory = row
|
this.currentHistory = row
|
||||||
const content = row.contents || ''
|
const historyContent = row.contents || ''
|
||||||
if (row.type === 3) {
|
const formatted = row.type === 3 ? (() => { try { return JSON.stringify(JSON.parse(historyContent), null, 2) } catch (e) { return historyContent } })() : historyContent
|
||||||
|
|
||||||
|
this.leftLines = []
|
||||||
|
this.rightLines = []
|
||||||
try {
|
try {
|
||||||
this.formattedContent = JSON.stringify(JSON.parse(content), null, 2)
|
const res = await getConfig(row.originId)
|
||||||
} catch (e) {
|
if (res && res.data && res.data.contents) {
|
||||||
this.formattedContent = content
|
const currentRaw = res.data.type === 3 ? (() => { try { return JSON.stringify(JSON.parse(res.data.contents), null, 2) } catch (e) { return res.data.contents } })() : res.data.contents
|
||||||
|
const result = computeDiff(currentRaw, formatted)
|
||||||
|
this.leftLines = result.left
|
||||||
|
this.rightLines = result.right
|
||||||
}
|
}
|
||||||
} else {
|
} catch (e) {
|
||||||
this.formattedContent = content
|
this.rightLines = formatted.split('\n').map((line, i) => ({ lineNum: i + 1, text: line, type: 'unchanged' }))
|
||||||
}
|
}
|
||||||
this.contentVisible = true
|
this.contentVisible = true
|
||||||
},
|
},
|
||||||
@@ -173,3 +288,78 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
::v-deep .diff-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .diff-panel {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .diff-panel-header {
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #606266;
|
||||||
|
border-bottom: 1px solid #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .diff-panel-body {
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .diff-line {
|
||||||
|
display: flex;
|
||||||
|
padding: 0 10px;
|
||||||
|
line-height: 22px;
|
||||||
|
white-space: pre;
|
||||||
|
min-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .diff-line .line-number {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 40px;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 10px;
|
||||||
|
color: #909399;
|
||||||
|
border-right: 1px solid #ebeef5;
|
||||||
|
margin-right: 10px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .diff-line .line-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .diff-line.added {
|
||||||
|
background: #e6ffed;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .diff-line.removed {
|
||||||
|
background: #ffeef0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .diff-line.modified {
|
||||||
|
background: #fff8e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .diff-line.unchanged {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .diff-line.empty {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user