Commit 5af0d0fb by pangchong

feat: 完善对比工卡逻辑

parent d278bc3b
......@@ -34,6 +34,13 @@ import { history, defaultKeymap, historyKeymap } from '@codemirror/commands'
import { searchKeymap, highlightSelectionMatches, openSearchPanel, search, SearchQuery, getSearchQuery } from '@codemirror/search'
import { lintKeymap } from '@codemirror/lint'
interface InlineDiff {
line: number
from: number
to: number
class: string
}
interface Props {
modelValue: string
disabled?: boolean
......@@ -42,6 +49,7 @@ interface Props {
minHeight?: string | number
lineClasses?: string[]
lineNumbers?: (string | number)[]
inlineDiffs?: InlineDiff[]
}
const props = withDefaults(defineProps<Props>(), {
......@@ -49,19 +57,33 @@ const props = withDefaults(defineProps<Props>(), {
disabled: false,
minHeight: 200,
lineClasses: () => [],
lineNumbers: () => []
lineNumbers: () => [],
inlineDiffs: () => []
})
// ── 差异高亮与自定义行号的 CodeMirror 6 拓展实现 ──
const setLineClassesEffect = StateEffect.define<string[]>()
const setLineClassesEffect = StateEffect.define<{ lineClasses: string[]; inlineDiffs: InlineDiff[] }>()
function getLineDecorations(lineClasses: string[], doc: any) {
function getLineDecorations(lineClasses: string[], inlineDiffs: InlineDiff[], doc: any) {
const builder = new RangeSetBuilder<Decoration>()
// 将 inlineDiffs 按 line 分组
const diffsByLine = new Map<number, InlineDiff[]>()
if (inlineDiffs && inlineDiffs.length > 0) {
for (const item of inlineDiffs) {
if (!diffsByLine.has(item.line)) {
diffsByLine.set(item.line, [])
}
diffsByLine.get(item.line)!.push(item)
}
}
for (let i = 1; i <= doc.lines; i++) {
const cls = lineClasses[i - 1]
if (cls) {
const line = doc.line(i)
if (cls) {
builder.add(
line.from,
line.from,
......@@ -70,19 +92,37 @@ function getLineDecorations(lineClasses: string[], doc: any) {
})
)
}
const lineDiffs = diffsByLine.get(i)
if (lineDiffs && lineDiffs.length > 0) {
lineDiffs.sort((a, b) => a.from - b.from)
for (const diff of lineDiffs) {
const startPos = Math.min(line.from + diff.from, line.to)
const endPos = Math.min(line.from + diff.to, line.to)
if (startPos < endPos) {
builder.add(
startPos,
endPos,
Decoration.mark({
class: diff.class
})
)
}
}
}
}
return builder.finish()
}
const lineClassesField = StateField.define<DecorationSet>({
create(state) {
return getLineDecorations(props.lineClasses || [], state.doc)
return getLineDecorations(props.lineClasses || [], props.inlineDiffs || [], state.doc)
},
update(deco, tr) {
deco = deco.map(tr.changes)
for (const effect of tr.effects) {
if (effect.is(setLineClassesEffect)) {
deco = getLineDecorations(effect.value, tr.state.doc)
deco = getLineDecorations(effect.value.lineClasses, effect.value.inlineDiffs, tr.state.doc)
}
}
return deco
......@@ -675,7 +715,10 @@ watch(
})
if (props.lineClasses && props.lineClasses.length > 0) {
editorView.dispatch({
effects: setLineClassesEffect.of(props.lineClasses)
effects: setLineClassesEffect.of({
lineClasses: props.lineClasses,
inlineDiffs: props.inlineDiffs || []
})
})
}
}
......@@ -693,11 +736,14 @@ watch(
)
watch(
() => props.lineClasses,
(newClasses) => {
if (editorView && newClasses) {
[() => props.lineClasses, () => props.inlineDiffs],
([newClasses, newDiffs]) => {
if (editorView) {
editorView.dispatch({
effects: setLineClassesEffect.of(newClasses)
effects: setLineClassesEffect.of({
lineClasses: newClasses || [],
inlineDiffs: newDiffs || []
})
})
}
},
......
......@@ -19,6 +19,10 @@ export interface XmlNode {
parentId: string | null
/** 比对差异状态 */
diffStatus?: 'added' | 'removed' | 'modified' | 'none'
/** 是否包含属性变更 */
attrModified?: boolean
/** 具体变更的属性列表 */
attrDiffs?: { key: string; oldVal: string; newVal: string }[]
}
/**
......
......@@ -4,6 +4,7 @@
:data-node-id="node.id"
:data-tag-name="node.tagName"
:data-diff-status="node.diffStatus"
:data-attr-modified="node.attrModified ? 'true' : undefined"
class="doc-node-wrapper relative transition-all"
:class="[
renderInline ? 'inline align-baseline mx-0.5' : 'block my-1',
......@@ -27,6 +28,29 @@
</div>
</div>
</Transition>
<!-- 属性变更徽标:仅块级节点用 absolute 定位,行内节点已由黄色背景/diff边框标示 -->
<n-popover v-if="!renderInline && node.attrDiffs && node.attrDiffs.length > 0" trigger="hover" placement="top-end" style="max-width: 380px">
<template #trigger>
<span
class="attr-diff-badge absolute -top-2 right-0 inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[10px] font-bold font-mono cursor-pointer select-none shadow-md z-20 pointer-events-auto transition-all hover:opacity-90"
style="background: #f59e0b; color: #fff; line-height: 1.2"
@click.stop
>
🏷️ {{ node.tagName }}
</span>
</template>
<div class="p-2 space-y-1.5 text-xs font-mono select-none" @click.stop>
<div class="font-bold border-b border-divider pb-1 flex items-center gap-1">
<span>🏷️ &lt;{{ node.tagName }}&gt; 属性变更明细</span>
</div>
<div v-for="diff in node.attrDiffs" :key="diff.key" class="flex items-center gap-1.5 flex-wrap">
<span class="font-semibold">{{ diff.key }}:</span>
<span class="line-through text-red-500 px-1 bg-red-500/10 rounded">{{ diff.oldVal || '(空)' }}</span>
<span class="text-amber-500 font-bold"></span>
<span class="text-emerald-600 dark:text-emerald-400 font-bold px-1 bg-emerald-500/10 rounded">{{ diff.newVal || '(空)' }}</span>
</div>
</div>
</n-popover>
<!-- 0. 各类 HEADER 节点特殊处理 (不展示) -->
<template v-if="isHeaderTag"></template>
......@@ -1326,6 +1350,29 @@
v-text="node.textContent || node.tagName"
></div>
</template>
<!-- 行内节点属性变更上标徽标:渲染在内容之后,不遮挡任何文字 -->
<n-popover v-if="renderInline && node.attrDiffs && node.attrDiffs.length > 0" trigger="hover" placement="top" style="max-width: 380px">
<template #trigger>
<sup
class="inline-flex items-center justify-center cursor-pointer select-none rounded-full shadow-sm transition-transform hover:scale-125 ml-0.5"
style="background: #f59e0b; color: #fff; font-size: 8px; width: 14px; height: 14px; vertical-align: super; line-height: 1"
@click.stop
>
🏷
</sup>
</template>
<div class="p-2 space-y-1.5 text-xs font-mono select-none" @click.stop>
<div class="font-bold border-b border-divider pb-1 flex items-center gap-1">
<span>🏷️ &lt;{{ node.tagName }}&gt; 属性变更明细</span>
</div>
<div v-for="diff in node.attrDiffs" :key="diff.key" class="flex items-center gap-1.5 flex-wrap">
<span class="font-semibold">{{ diff.key }}:</span>
<span class="line-through text-red-500 px-1 bg-red-500/10 rounded">{{ diff.oldVal || '(空)' }}</span>
<span class="text-amber-500 font-bold"></span>
<span class="text-emerald-600 dark:text-emerald-400 font-bold px-1 bg-emerald-500/10 rounded">{{ diff.newVal || '(空)' }}</span>
</div>
</div>
</n-popover>
</component>
</template>
......
......@@ -15,6 +15,6 @@ export interface RenderDiffHunk {
// XML源码比对差异段样式模型
export interface XmlDiffHunk {
type: 'added' | 'removed'
type: 'added' | 'removed' | 'modified'
style: Record<string, string>
}
......@@ -78,6 +78,8 @@
:rowspan="cell.rowspan"
:data-node-id="cell.id"
data-tag-name="ENTRY"
:data-diff-status="cell.rawNode?.diffStatus"
:data-attr-modified="cell.rawNode?.attrModified ? 'true' : undefined"
class="p-2 text-left font-bold bg-fill-4 border border-divider text-color1 transition-colors relative"
:class="[
isDiffMode ? 'cursor-default' : 'cursor-pointer',
......@@ -91,6 +93,29 @@
@click.stop="isDiffMode ? null : handleCellClick(cell, $event)"
@contextmenu.prevent="isDiffMode ? null : handleCellContextMenu(cell, row.id, $event)"
>
<!-- 属性变更徽标:absolute 定位在单元格右上角,不侵入内容流 -->
<n-popover v-if="cell.rawNode?.attrDiffs && cell.rawNode.attrDiffs.length > 0" trigger="hover" placement="top-end" style="max-width: 380px">
<template #trigger>
<span
class="attr-diff-badge absolute -top-2 right-0 inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[10px] font-bold font-mono cursor-pointer select-none shadow-md z-20 pointer-events-auto transition-all hover:opacity-90"
style="background: #f59e0b; color: #fff; line-height: 1.2;"
@click.stop
>
🏷️ {{ cell.rawNode.tagName }}
</span>
</template>
<div class="p-2 space-y-1.5 text-xs font-mono select-none" @click.stop>
<div class="font-bold border-b border-divider pb-1 flex items-center gap-1">
<span>🏷️ &lt;{{ cell.rawNode.tagName }}&gt; 属性变更明细</span>
</div>
<div v-for="diff in cell.rawNode.attrDiffs" :key="diff.key" class="flex items-center gap-1.5 flex-wrap">
<span class="font-semibold">{{ diff.key }}:</span>
<span class="line-through text-red-500 px-1 bg-red-500/10 rounded">{{ diff.oldVal || '(空)' }}</span>
<span class="text-amber-500 font-bold"></span>
<span class="text-emerald-600 dark:text-emerald-400 font-bold px-1 bg-emerald-500/10 rounded">{{ diff.newVal || '(空)' }}</span>
</div>
</div>
</n-popover>
<template v-if="cell.rawNode && cell.rawNode.children && cell.rawNode.children.length > 0">
<DocNodeRenderer v-for="child in cell.rawNode!.children" :key="child.id" :node="child" :parent="cell.rawNode" />
</template>
......@@ -190,6 +215,8 @@
:rowspan="cell.rowspan"
:data-node-id="cell.id"
data-tag-name="ENTRY"
:data-diff-status="cell.rawNode?.diffStatus"
:data-attr-modified="cell.rawNode?.attrModified ? 'true' : undefined"
class="p-2 border border-divider text-color2 transition-colors relative"
:class="[
isDiffMode ? 'cursor-default' : 'cursor-pointer',
......@@ -203,6 +230,29 @@
@click.stop="isDiffMode ? null : handleCellClick(cell, $event)"
@contextmenu.prevent="isDiffMode ? null : handleCellContextMenu(cell, row.id, $event)"
>
<!-- 属性变更徽标:absolute 定位在单元格右上角,不侵入内容流 -->
<n-popover v-if="cell.rawNode?.attrDiffs && cell.rawNode.attrDiffs.length > 0" trigger="hover" placement="top-end" style="max-width: 380px">
<template #trigger>
<span
class="attr-diff-badge absolute -top-2 right-0 inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[10px] font-bold font-mono cursor-pointer select-none shadow-md z-20 pointer-events-auto transition-all hover:opacity-90"
style="background: #f59e0b; color: #fff; line-height: 1.2;"
@click.stop
>
🏷️ {{ cell.rawNode.tagName }}
</span>
</template>
<div class="p-2 space-y-1.5 text-xs font-mono select-none" @click.stop>
<div class="font-bold border-b border-divider pb-1 flex items-center gap-1">
<span>🏷️ &lt;{{ cell.rawNode.tagName }}&gt; 属性变更明细</span>
</div>
<div v-for="diff in cell.rawNode.attrDiffs" :key="diff.key" class="flex items-center gap-1.5 flex-wrap">
<span class="font-semibold">{{ diff.key }}:</span>
<span class="line-through text-red-500 px-1 bg-red-500/10 rounded">{{ diff.oldVal || '(空)' }}</span>
<span class="text-amber-500 font-bold"></span>
<span class="text-emerald-600 dark:text-emerald-400 font-bold px-1 bg-emerald-500/10 rounded">{{ diff.newVal || '(空)' }}</span>
</div>
</div>
</n-popover>
<template v-if="cell.rawNode && cell.rawNode.children && cell.rawNode.children.length > 0">
<DocNodeRenderer v-for="child in cell.rawNode!.children" :key="child.id" :node="child" :parent="cell.rawNode" />
</template>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment