Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
I
Ifar-Xml-Edtor
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
pangchong
Ifar-Xml-Edtor
Commits
5af0d0fb
Commit
5af0d0fb
authored
Jul 28, 2026
by
pangchong
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 完善对比工卡逻辑
parent
d278bc3b
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
1035 additions
and
140 deletions
+1035
-140
src/components/CommonXmlEditor.vue
+57
-11
src/types/xmlNode.ts
+4
-0
src/views/editor/components/DocNodeRenderer/index.vue
+47
-0
src/views/editor/components/EditorToolbar/components/CompareModal/constants/index.ts
+1
-1
src/views/editor/components/EditorToolbar/components/CompareModal/functionals/index.ts
+473
-48
src/views/editor/components/EditorToolbar/components/CompareModal/index.vue
+403
-80
src/views/editor/components/TableEditor/index.vue
+50
-0
No files found.
src/components/CommonXmlEditor.vue
View file @
5af0d0fb
...
@@ -34,6 +34,13 @@ import { history, defaultKeymap, historyKeymap } from '@codemirror/commands'
...
@@ -34,6 +34,13 @@ import { history, defaultKeymap, historyKeymap } from '@codemirror/commands'
import
{
searchKeymap
,
highlightSelectionMatches
,
openSearchPanel
,
search
,
SearchQuery
,
getSearchQuery
}
from
'@codemirror/search'
import
{
searchKeymap
,
highlightSelectionMatches
,
openSearchPanel
,
search
,
SearchQuery
,
getSearchQuery
}
from
'@codemirror/search'
import
{
lintKeymap
}
from
'@codemirror/lint'
import
{
lintKeymap
}
from
'@codemirror/lint'
interface
InlineDiff
{
line
:
number
from
:
number
to
:
number
class
:
string
}
interface
Props
{
interface
Props
{
modelValue
:
string
modelValue
:
string
disabled
?:
boolean
disabled
?:
boolean
...
@@ -42,6 +49,7 @@ interface Props {
...
@@ -42,6 +49,7 @@ interface Props {
minHeight
?:
string
|
number
minHeight
?:
string
|
number
lineClasses
?:
string
[]
lineClasses
?:
string
[]
lineNumbers
?:
(
string
|
number
)[]
lineNumbers
?:
(
string
|
number
)[]
inlineDiffs
?:
InlineDiff
[]
}
}
const
props
=
withDefaults
(
defineProps
<
Props
>
(),
{
const
props
=
withDefaults
(
defineProps
<
Props
>
(),
{
...
@@ -49,19 +57,33 @@ const props = withDefaults(defineProps<Props>(), {
...
@@ -49,19 +57,33 @@ const props = withDefaults(defineProps<Props>(), {
disabled
:
false
,
disabled
:
false
,
minHeight
:
200
,
minHeight
:
200
,
lineClasses
:
()
=>
[],
lineClasses
:
()
=>
[],
lineNumbers
:
()
=>
[]
lineNumbers
:
()
=>
[],
inlineDiffs
:
()
=>
[]
})
})
// ── 差异高亮与自定义行号的 CodeMirror 6 拓展实现 ──
// ── 差异高亮与自定义行号的 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
>
()
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
++
)
{
for
(
let
i
=
1
;
i
<=
doc
.
lines
;
i
++
)
{
const
cls
=
lineClasses
[
i
-
1
]
const
cls
=
lineClasses
[
i
-
1
]
if
(
cls
)
{
const
line
=
doc
.
line
(
i
)
const
line
=
doc
.
line
(
i
)
if
(
cls
)
{
builder
.
add
(
builder
.
add
(
line
.
from
,
line
.
from
,
line
.
from
,
line
.
from
,
...
@@ -70,19 +92,37 @@ function getLineDecorations(lineClasses: string[], doc: any) {
...
@@ -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
()
return
builder
.
finish
()
}
}
const
lineClassesField
=
StateField
.
define
<
DecorationSet
>
({
const
lineClassesField
=
StateField
.
define
<
DecorationSet
>
({
create
(
state
)
{
create
(
state
)
{
return
getLineDecorations
(
props
.
lineClasses
||
[],
state
.
doc
)
return
getLineDecorations
(
props
.
lineClasses
||
[],
props
.
inlineDiffs
||
[],
state
.
doc
)
},
},
update
(
deco
,
tr
)
{
update
(
deco
,
tr
)
{
deco
=
deco
.
map
(
tr
.
changes
)
deco
=
deco
.
map
(
tr
.
changes
)
for
(
const
effect
of
tr
.
effects
)
{
for
(
const
effect
of
tr
.
effects
)
{
if
(
effect
.
is
(
setLineClassesEffect
))
{
if
(
effect
.
is
(
setLineClassesEffect
))
{
deco
=
getLineDecorations
(
effect
.
value
,
tr
.
state
.
doc
)
deco
=
getLineDecorations
(
effect
.
value
.
lineClasses
,
effect
.
value
.
inlineDiffs
,
tr
.
state
.
doc
)
}
}
}
}
return
deco
return
deco
...
@@ -675,7 +715,10 @@ watch(
...
@@ -675,7 +715,10 @@ watch(
})
})
if
(
props
.
lineClasses
&&
props
.
lineClasses
.
length
>
0
)
{
if
(
props
.
lineClasses
&&
props
.
lineClasses
.
length
>
0
)
{
editorView
.
dispatch
({
editorView
.
dispatch
({
effects
:
setLineClassesEffect
.
of
(
props
.
lineClasses
)
effects
:
setLineClassesEffect
.
of
({
lineClasses
:
props
.
lineClasses
,
inlineDiffs
:
props
.
inlineDiffs
||
[]
})
})
})
}
}
}
}
...
@@ -693,11 +736,14 @@ watch(
...
@@ -693,11 +736,14 @@ watch(
)
)
watch
(
watch
(
()
=>
props
.
lineClasses
,
[()
=>
props
.
lineClasses
,
()
=>
props
.
inlineDiffs
]
,
(
newClasses
)
=>
{
(
[
newClasses
,
newDiffs
]
)
=>
{
if
(
editorView
&&
newClasses
)
{
if
(
editorView
)
{
editorView
.
dispatch
({
editorView
.
dispatch
({
effects
:
setLineClassesEffect
.
of
(
newClasses
)
effects
:
setLineClassesEffect
.
of
({
lineClasses
:
newClasses
||
[],
inlineDiffs
:
newDiffs
||
[]
})
})
})
}
}
},
},
...
...
src/types/xmlNode.ts
View file @
5af0d0fb
...
@@ -19,6 +19,10 @@ export interface XmlNode {
...
@@ -19,6 +19,10 @@ export interface XmlNode {
parentId
:
string
|
null
parentId
:
string
|
null
/** 比对差异状态 */
/** 比对差异状态 */
diffStatus
?:
'added'
|
'removed'
|
'modified'
|
'none'
diffStatus
?:
'added'
|
'removed'
|
'modified'
|
'none'
/** 是否包含属性变更 */
attrModified
?:
boolean
/** 具体变更的属性列表 */
attrDiffs
?:
{
key
:
string
;
oldVal
:
string
;
newVal
:
string
}[]
}
}
/**
/**
...
...
src/views/editor/components/DocNodeRenderer/index.vue
View file @
5af0d0fb
...
@@ -4,6 +4,7 @@
...
@@ -4,6 +4,7 @@
:data-node-id=
"node.id"
:data-node-id=
"node.id"
:data-tag-name=
"node.tagName"
:data-tag-name=
"node.tagName"
:data-diff-status=
"node.diffStatus"
:data-diff-status=
"node.diffStatus"
:data-attr-modified=
"node.attrModified ? 'true' : undefined"
class=
"doc-node-wrapper relative transition-all"
class=
"doc-node-wrapper relative transition-all"
:class=
"[
:class=
"[
renderInline ? 'inline align-baseline mx-0.5' : 'block my-1',
renderInline ? 'inline align-baseline mx-0.5' : 'block my-1',
...
@@ -27,6 +28,29 @@
...
@@ -27,6 +28,29 @@
</div>
</div>
</div>
</div>
</Transition>
</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>
🏷️
<
{{ node.tagName }}
>
属性变更明细
</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 节点特殊处理 (不展示) -->
<!-- 0. 各类 HEADER 节点特殊处理 (不展示) -->
<
template
v-if=
"isHeaderTag"
></
template
>
<
template
v-if=
"isHeaderTag"
></
template
>
...
@@ -1326,6 +1350,29 @@
...
@@ -1326,6 +1350,29 @@
v
-
text
=
"node.textContent || node.tagName"
v
-
text
=
"node.textContent || node.tagName"
><
/div
>
><
/div
>
<
/template
>
<
/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
>
<
/component
>
<
/template
>
<
/template
>
...
...
src/views/editor/components/EditorToolbar/components/CompareModal/constants/index.ts
View file @
5af0d0fb
...
@@ -15,6 +15,6 @@ export interface RenderDiffHunk {
...
@@ -15,6 +15,6 @@ export interface RenderDiffHunk {
// XML源码比对差异段样式模型
// XML源码比对差异段样式模型
export
interface
XmlDiffHunk
{
export
interface
XmlDiffHunk
{
type
:
'added'
|
'removed'
type
:
'added'
|
'removed'
|
'modified'
style
:
Record
<
string
,
string
>
style
:
Record
<
string
,
string
>
}
}
src/views/editor/components/EditorToolbar/components/CompareModal/functionals/index.ts
View file @
5af0d0fb
...
@@ -42,6 +42,7 @@ function cloneWithDiff(node: XmlNode, status: DiffStatusType = 'none'): XmlNode
...
@@ -42,6 +42,7 @@ function cloneWithDiff(node: XmlNode, status: DiffStatusType = 'none'): XmlNode
...
node
,
...
node
,
id
:
node
.
id
+
'_'
+
status
,
// 避免 id 冲突
id
:
node
.
id
+
'_'
+
status
,
// 避免 id 冲突
diffStatus
:
status
as
any
,
diffStatus
:
status
as
any
,
attrModified
:
false
,
children
,
children
,
mixedContent
mixedContent
}
}
...
@@ -121,10 +122,14 @@ function diffXmlTrees(oldNode: XmlNode | null, newNode: XmlNode | null): { oldDi
...
@@ -121,10 +122,14 @@ function diffXmlTrees(oldNode: XmlNode | null, newNode: XmlNode | null): { oldDi
const
newAttrs
=
{
...
newNode
!
.
attributes
}
const
newAttrs
=
{
...
newNode
!
.
attributes
}
let
attrsChanged
=
false
let
attrsChanged
=
false
const
attrDiffs
:
{
key
:
string
;
oldVal
:
string
;
newVal
:
string
}[]
=
[]
const
allAttrKeys
=
new
Set
([...
Object
.
keys
(
oldAttrs
),
...
Object
.
keys
(
newAttrs
)])
const
allAttrKeys
=
new
Set
([...
Object
.
keys
(
oldAttrs
),
...
Object
.
keys
(
newAttrs
)])
for
(
const
key
of
allAttrKeys
)
{
for
(
const
key
of
allAttrKeys
)
{
if
(
oldAttrs
[
key
]
!==
newAttrs
[
key
])
{
const
oVal
=
oldAttrs
[
key
]
||
''
const
nVal
=
newAttrs
[
key
]
||
''
if
(
oVal
!==
nVal
)
{
attrsChanged
=
true
attrsChanged
=
true
attrDiffs
.
push
({
key
,
oldVal
:
oVal
,
newVal
:
nVal
})
}
}
}
}
...
@@ -315,6 +320,8 @@ function diffXmlTrees(oldNode: XmlNode | null, newNode: XmlNode | null): { oldDi
...
@@ -315,6 +320,8 @@ function diffXmlTrees(oldNode: XmlNode | null, newNode: XmlNode | null): { oldDi
...
oldNode
!
,
...
oldNode
!
,
id
:
oldNode
!
.
id
+
'_diff'
,
id
:
oldNode
!
.
id
+
'_diff'
,
diffStatus
:
diffStatus
as
any
,
diffStatus
:
diffStatus
as
any
,
attrModified
:
attrsChanged
,
attrDiffs
,
children
:
oldChildrenDiff
,
children
:
oldChildrenDiff
,
mixedContent
:
oldMixedContent
,
mixedContent
:
oldMixedContent
,
attributes
:
oldAttrs
,
attributes
:
oldAttrs
,
...
@@ -325,6 +332,8 @@ function diffXmlTrees(oldNode: XmlNode | null, newNode: XmlNode | null): { oldDi
...
@@ -325,6 +332,8 @@ function diffXmlTrees(oldNode: XmlNode | null, newNode: XmlNode | null): { oldDi
...
newNode
!
,
...
newNode
!
,
id
:
newNode
!
.
id
+
'_diff'
,
id
:
newNode
!
.
id
+
'_diff'
,
diffStatus
:
diffStatus
as
any
,
diffStatus
:
diffStatus
as
any
,
attrModified
:
attrsChanged
,
attrDiffs
,
children
:
newChildrenDiff
,
children
:
newChildrenDiff
,
mixedContent
:
newMixedContent
,
mixedContent
:
newMixedContent
,
attributes
:
newAttrs
,
attributes
:
newAttrs
,
...
@@ -350,6 +359,10 @@ export function useCompareModal() {
...
@@ -350,6 +359,10 @@ export function useCompareModal() {
const
leftLineClasses
=
ref
<
string
[]
>
([])
const
leftLineClasses
=
ref
<
string
[]
>
([])
const
rightLineClasses
=
ref
<
string
[]
>
([])
const
rightLineClasses
=
ref
<
string
[]
>
([])
// XML 内部单词/字符级的细粒度 Diff 标记
const
leftInlineDiffs
=
ref
<
{
line
:
number
;
from
:
number
;
to
:
number
;
class
:
string
}[]
>
([])
const
rightInlineDiffs
=
ref
<
{
line
:
number
;
from
:
number
;
to
:
number
;
class
:
string
}[]
>
([])
// XML 的行号数组
// XML 的行号数组
const
leftLineNumbers
=
ref
<
(
string
|
number
)[]
>
([])
const
leftLineNumbers
=
ref
<
(
string
|
number
)[]
>
([])
const
rightLineNumbers
=
ref
<
(
string
|
number
)[]
>
([])
const
rightLineNumbers
=
ref
<
(
string
|
number
)[]
>
([])
...
@@ -453,6 +466,127 @@ export function useCompareModal() {
...
@@ -453,6 +466,127 @@ export function useCompareModal() {
return
{
added
,
removed
,
modified
}
return
{
added
,
removed
,
modified
}
})
})
// 按类型收集渲染区中所有 diff 节点的 ID 列表,用于顺序导航
const
diffNodeIds
=
computed
(()
=>
{
const
added
:
string
[]
=
[]
const
removed
:
string
[]
=
[]
const
modified
:
string
[]
=
[]
const
visitOld
=
(
node
:
XmlNode
)
=>
{
if
(
!
node
)
return
if
(
node
.
diffStatus
===
'removed'
)
{
removed
.
push
(
node
.
id
)
return
}
if
(
node
.
diffStatus
===
'modified'
)
modified
.
push
(
node
.
id
)
if
(
node
.
children
)
node
.
children
.
forEach
(
visitOld
)
}
const
visitNew
=
(
node
:
XmlNode
)
=>
{
if
(
!
node
)
return
if
(
node
.
diffStatus
===
'added'
)
{
added
.
push
(
node
.
id
)
return
}
if
(
node
.
children
)
node
.
children
.
forEach
(
visitNew
)
}
if
(
leftRenderTree
.
value
)
visitOld
(
leftRenderTree
.
value
)
if
(
rightRenderTree
.
value
)
visitNew
(
rightRenderTree
.
value
)
return
{
added
,
removed
,
modified
}
})
// 各类型当前导航索引
const
navIndex
=
reactive
({
added
:
0
,
removed
:
0
,
modified
:
0
})
/**
* 滚动渲染区到指定 node.id 对应的 DOM 元素
* side: 'left' 滚动左侧(删除、变更),'right' 滚动右侧(新增、变更)
*/
const
scrollRenderToNode
=
(
nodeId
:
string
,
side
:
'left'
|
'right'
)
=>
{
const
container
=
side
===
'left'
?
leftRenderContainer
.
value
:
rightRenderContainer
.
value
if
(
!
container
)
return
const
el
=
container
.
querySelector
(
`[data-node-id="
${
nodeId
}
"]`
)
as
HTMLElement
|
null
if
(
!
el
)
return
el
.
scrollIntoView
({
behavior
:
'smooth'
,
block
:
'center'
})
// 短暂闪烁高亮
const
prev
=
el
.
style
.
outline
el
.
style
.
outline
=
'2px solid #f59e0b'
el
.
style
.
borderRadius
=
'2px'
setTimeout
(()
=>
{
el
.
style
.
outline
=
prev
},
1400
)
}
/** 按类型导航到上一个或下一个 diff 节点 */
const
navigateDiff
=
(
type
:
'added'
|
'removed'
|
'modified'
,
direction
:
'prev'
|
'next'
)
=>
{
const
ids
=
diffNodeIds
.
value
[
type
]
if
(
!
ids
||
ids
.
length
===
0
)
return
if
(
direction
===
'next'
)
{
navIndex
[
type
]
=
(
navIndex
[
type
]
+
1
)
%
ids
.
length
}
else
{
navIndex
[
type
]
=
(
navIndex
[
type
]
-
1
+
ids
.
length
)
%
ids
.
length
}
const
nodeId
=
ids
[
navIndex
[
type
]]
// 1. 滚动渲染区
const
scrollRender
=
(
side
:
'left'
|
'right'
)
=>
scrollRenderToNode
(
nodeId
,
side
)
if
(
type
===
'added'
)
{
scrollRender
(
'right'
)
}
else
if
(
type
===
'removed'
)
{
scrollRender
(
'left'
)
}
else
{
scrollRender
(
'left'
)
scrollRender
(
'right'
)
}
// 2. 同步高亮定位 XML 编辑器(复用点击渲染区时的高亮逻辑)
const
highlightXmlNode
=
(
rawNodeId
:
string
)
=>
{
const
cleanId
=
rawNodeId
.
replace
(
/_
(
diff|added|removed|unchanged|modified
)
/g
,
''
).
replace
(
/-txt-
\d
+/g
,
''
)
// 左侧 XML
const
leftRange
=
leftNodeRanges
.
value
.
get
(
cleanId
)
if
(
leftRange
)
{
const
leftDisplayLine
=
leftOriginalToEditor
.
value
.
get
(
leftRange
.
startLine
)
if
(
leftDisplayLine
)
{
leftActiveLine
.
value
=
leftDisplayLine
leftEditorRef
.
value
?.
highlightAndScrollToLine
(
leftDisplayLine
)
}
}
// 右侧 XML
const
rightRange
=
rightNodeRanges
.
value
.
get
(
cleanId
)
if
(
rightRange
)
{
const
rightDisplayLine
=
rightOriginalToEditor
.
value
.
get
(
rightRange
.
startLine
)
if
(
rightDisplayLine
)
{
rightActiveLine
.
value
=
rightDisplayLine
rightEditorRef
.
value
?.
highlightAndScrollToLine
(
rightDisplayLine
)
}
}
}
highlightXmlNode
(
nodeId
)
}
const
navigateToNextDiff
=
(
type
:
'added'
|
'removed'
|
'modified'
)
=>
navigateDiff
(
type
,
'next'
)
const
navigateToPrevDiff
=
(
type
:
'added'
|
'removed'
|
'modified'
)
=>
navigateDiff
(
type
,
'prev'
)
// 格式化展示导航进度(如 "1/2"、"2/2" 或 "0")
const
navCurrentDisplay
=
computed
(()
=>
{
const
addedCount
=
diffNodeIds
.
value
.
added
.
length
const
removedCount
=
diffNodeIds
.
value
.
removed
.
length
const
modifiedCount
=
diffNodeIds
.
value
.
modified
.
length
return
{
added
:
addedCount
>
0
?
`
${(
navIndex
.
added
%
addedCount
)
+
1
}
/
${
addedCount
}
`
:
'0'
,
removed
:
removedCount
>
0
?
`
${(
navIndex
.
removed
%
removedCount
)
+
1
}
/
${
removedCount
}
`
:
'0'
,
modified
:
modifiedCount
>
0
?
`
${(
navIndex
.
modified
%
modifiedCount
)
+
1
}
/
${
modifiedCount
}
`
:
'0'
}
})
const
showXmlCompare
=
ref
(
true
)
const
showXmlCompare
=
ref
(
true
)
const
showRenderCompare
=
ref
(
true
)
const
showRenderCompare
=
ref
(
true
)
...
@@ -487,6 +621,8 @@ export function useCompareModal() {
...
@@ -487,6 +621,8 @@ export function useCompareModal() {
rightDisplayXml
.
value
=
''
rightDisplayXml
.
value
=
''
leftLineClasses
.
value
=
[]
leftLineClasses
.
value
=
[]
rightLineClasses
.
value
=
[]
rightLineClasses
.
value
=
[]
leftInlineDiffs
.
value
=
[]
rightInlineDiffs
.
value
=
[]
leftLineNumbers
.
value
=
[]
leftLineNumbers
.
value
=
[]
rightLineNumbers
.
value
=
[]
rightLineNumbers
.
value
=
[]
leftRenderTree
.
value
=
null
leftRenderTree
.
value
=
null
...
@@ -573,12 +709,124 @@ export function useCompareModal() {
...
@@ -573,12 +709,124 @@ export function useCompareModal() {
const
rClasses
:
string
[]
=
[]
const
rClasses
:
string
[]
=
[]
const
lNums
:
(
string
|
number
)[]
=
[]
const
lNums
:
(
string
|
number
)[]
=
[]
const
rNums
:
(
string
|
number
)[]
=
[]
const
rNums
:
(
string
|
number
)[]
=
[]
const
lInlineDiffs
:
{
line
:
number
;
from
:
number
;
to
:
number
;
class
:
string
}[]
=
[]
const
rInlineDiffs
:
{
line
:
number
;
from
:
number
;
to
:
number
;
class
:
string
}[]
=
[]
let
leftRealLine
=
1
let
leftRealLine
=
1
let
rightRealLine
=
1
let
rightRealLine
=
1
let
displayLineIndex
=
1
let
displayLineIndex
=
1
for
(
const
change
of
diffResults
)
{
let
idx
=
0
while
(
idx
<
diffResults
.
length
)
{
const
current
=
diffResults
[
idx
]
const
next
=
diffResults
[
idx
+
1
]
// 检查是否为成对的「修改行」(前面删、后面增,或前面增、后面删)
if
(
next
&&
((
current
.
removed
&&
next
.
added
)
||
(
current
.
added
&&
next
.
removed
)))
{
const
removedChange
=
current
.
removed
?
current
:
next
const
addedChange
=
current
.
added
?
current
:
next
const
removedLines
=
removedChange
.
value
.
split
(
'
\
n'
)
if
(
removedLines
.
length
>
1
&&
removedLines
[
removedLines
.
length
-
1
]
===
''
)
{
removedLines
.
pop
()
}
const
addedLines
=
addedChange
.
value
.
split
(
'
\
n'
)
if
(
addedLines
.
length
>
1
&&
addedLines
[
addedLines
.
length
-
1
]
===
''
)
{
addedLines
.
pop
()
}
const
maxPair
=
Math
.
max
(
removedLines
.
length
,
addedLines
.
length
)
for
(
let
p
=
0
;
p
<
maxPair
;
p
++
)
{
const
rLine
=
removedLines
[
p
]
const
aLine
=
addedLines
[
p
]
if
(
rLine
!==
undefined
&&
aLine
!==
undefined
)
{
// 同一行都有内容:说明是同一行的改动,直接同行侧重对比!
lLines
.
push
(
rLine
)
lClasses
.
push
(
'diff-modified-left'
)
lNums
.
push
(
leftRealLine
)
rLines
.
push
(
aLine
)
rClasses
.
push
(
'diff-modified-right'
)
rNums
.
push
(
rightRealLine
)
// 详细计算该行内部单词/字符级别的 Inline Word Diff
const
wordDiffs
=
Diff
.
diffWords
(
rLine
,
aLine
)
let
leftCharOffset
=
0
let
rightCharOffset
=
0
for
(
const
w
of
wordDiffs
)
{
const
len
=
w
.
value
.
length
if
(
w
.
removed
)
{
lInlineDiffs
.
push
({
line
:
displayLineIndex
,
from
:
leftCharOffset
,
to
:
leftCharOffset
+
len
,
class
:
'cm-diff-inline-removed'
})
leftCharOffset
+=
len
}
else
if
(
w
.
added
)
{
rInlineDiffs
.
push
({
line
:
displayLineIndex
,
from
:
rightCharOffset
,
to
:
rightCharOffset
+
len
,
class
:
'cm-diff-inline-added'
})
rightCharOffset
+=
len
}
else
{
leftCharOffset
+=
len
rightCharOffset
+=
len
}
}
leftEditorToOriginal
.
value
.
set
(
displayLineIndex
,
leftRealLine
)
leftOriginalToEditor
.
value
.
set
(
leftRealLine
,
displayLineIndex
)
rightEditorToOriginal
.
value
.
set
(
displayLineIndex
,
rightRealLine
)
rightOriginalToEditor
.
value
.
set
(
rightRealLine
,
displayLineIndex
)
leftRealLine
++
rightRealLine
++
displayLineIndex
++
}
else
if
(
rLine
!==
undefined
)
{
// 多出来的旧行
lLines
.
push
(
rLine
)
lClasses
.
push
(
'diff-removed'
)
lNums
.
push
(
leftRealLine
)
rLines
.
push
(
''
)
rClasses
.
push
(
'diff-removed-placeholder'
)
rNums
.
push
(
''
)
leftEditorToOriginal
.
value
.
set
(
displayLineIndex
,
leftRealLine
)
leftOriginalToEditor
.
value
.
set
(
leftRealLine
,
displayLineIndex
)
leftRealLine
++
displayLineIndex
++
}
else
if
(
aLine
!==
undefined
)
{
// 多出来的增行
lLines
.
push
(
''
)
lClasses
.
push
(
'diff-added-placeholder'
)
lNums
.
push
(
''
)
rLines
.
push
(
aLine
)
rClasses
.
push
(
'diff-added'
)
rNums
.
push
(
rightRealLine
)
rightEditorToOriginal
.
value
.
set
(
displayLineIndex
,
rightRealLine
)
rightOriginalToEditor
.
value
.
set
(
rightRealLine
,
displayLineIndex
)
rightRealLine
++
displayLineIndex
++
}
}
idx
+=
2
continue
}
// 普通单块处理
const
change
=
current
const
lines
=
change
.
value
.
split
(
'
\
n'
)
const
lines
=
change
.
value
.
split
(
'
\
n'
)
if
(
lines
.
length
>
1
&&
lines
[
lines
.
length
-
1
]
===
''
)
{
if
(
lines
.
length
>
1
&&
lines
[
lines
.
length
-
1
]
===
''
)
{
lines
.
pop
()
lines
.
pop
()
...
@@ -594,7 +842,6 @@ export function useCompareModal() {
...
@@ -594,7 +842,6 @@ export function useCompareModal() {
rClasses
.
push
(
'diff-added'
)
rClasses
.
push
(
'diff-added'
)
rNums
.
push
(
rightRealLine
)
rNums
.
push
(
rightRealLine
)
// 记录右侧编辑器到原始格式化行号的对应关系
rightEditorToOriginal
.
value
.
set
(
displayLineIndex
,
rightRealLine
)
rightEditorToOriginal
.
value
.
set
(
displayLineIndex
,
rightRealLine
)
rightOriginalToEditor
.
value
.
set
(
rightRealLine
,
displayLineIndex
)
rightOriginalToEditor
.
value
.
set
(
rightRealLine
,
displayLineIndex
)
...
@@ -611,7 +858,6 @@ export function useCompareModal() {
...
@@ -611,7 +858,6 @@ export function useCompareModal() {
rClasses
.
push
(
'diff-removed-placeholder'
)
rClasses
.
push
(
'diff-removed-placeholder'
)
rNums
.
push
(
''
)
rNums
.
push
(
''
)
// 记录左侧编辑器到原始格式化行号的对应关系
leftEditorToOriginal
.
value
.
set
(
displayLineIndex
,
leftRealLine
)
leftEditorToOriginal
.
value
.
set
(
displayLineIndex
,
leftRealLine
)
leftOriginalToEditor
.
value
.
set
(
leftRealLine
,
displayLineIndex
)
leftOriginalToEditor
.
value
.
set
(
leftRealLine
,
displayLineIndex
)
...
@@ -628,7 +874,6 @@ export function useCompareModal() {
...
@@ -628,7 +874,6 @@ export function useCompareModal() {
rClasses
.
push
(
''
)
rClasses
.
push
(
''
)
rNums
.
push
(
rightRealLine
)
rNums
.
push
(
rightRealLine
)
// 记录两侧编辑器到原始格式化行号的对应关系
leftEditorToOriginal
.
value
.
set
(
displayLineIndex
,
leftRealLine
)
leftEditorToOriginal
.
value
.
set
(
displayLineIndex
,
leftRealLine
)
leftOriginalToEditor
.
value
.
set
(
leftRealLine
,
displayLineIndex
)
leftOriginalToEditor
.
value
.
set
(
leftRealLine
,
displayLineIndex
)
...
@@ -640,12 +885,16 @@ export function useCompareModal() {
...
@@ -640,12 +885,16 @@ export function useCompareModal() {
displayLineIndex
++
displayLineIndex
++
}
}
}
}
idx
++
}
}
leftDisplayXml
.
value
=
lLines
.
join
(
'
\
n'
)
leftDisplayXml
.
value
=
lLines
.
join
(
'
\
n'
)
rightDisplayXml
.
value
=
rLines
.
join
(
'
\
n'
)
rightDisplayXml
.
value
=
rLines
.
join
(
'
\
n'
)
leftLineClasses
.
value
=
lClasses
leftLineClasses
.
value
=
lClasses
rightLineClasses
.
value
=
rClasses
rightLineClasses
.
value
=
rClasses
leftInlineDiffs
.
value
=
lInlineDiffs
rightInlineDiffs
.
value
=
rInlineDiffs
leftLineNumbers
.
value
=
lNums
leftLineNumbers
.
value
=
lNums
rightLineNumbers
.
value
=
rNums
rightLineNumbers
.
value
=
rNums
...
@@ -661,9 +910,11 @@ export function useCompareModal() {
...
@@ -661,9 +910,11 @@ export function useCompareModal() {
compared
.
value
=
true
compared
.
value
=
true
window
.
$message
?.
success
(
'比对成功'
)
window
.
$message
?.
success
(
'比对成功'
)
// 比对成功后同步滚动绑定
// 比对成功后同步滚动绑定
,并分两轮补齐左右渲染节点高度差
nextTick
(()
=>
{
nextTick
(()
=>
{
bindScrollSync
()
bindScrollSync
()
setTimeout
(
syncRenderHeights
,
300
)
setTimeout
(
syncRenderHeights
,
800
)
})
})
}
catch
(
err
:
any
)
{
}
catch
(
err
:
any
)
{
window
.
$message
?.
error
(
`比对发生错误:
${
err
.
message
||
err
}
`
)
window
.
$message
?.
error
(
`比对发生错误:
${
err
.
message
||
err
}
`
)
...
@@ -672,6 +923,67 @@ export function useCompareModal() {
...
@@ -672,6 +923,67 @@ export function useCompareModal() {
}
}
}
}
/**
* 高度对齐补偿(并行遍历两棵虚拟数据树):
* ⚠️ doc-node-wrapper 有 transition-all,paddingBottom 变化会触发 150ms 动画。
* 直接读 offsetHeight 会读到动画中间值,导致父节点重复叠加补偿。
* 修复:测量前临时关闭所有节点的 transition,补偿完成后恢复。
*/
const
syncRenderHeights
=
()
=>
{
const
lc
=
leftRenderContainer
.
value
const
rc
=
rightRenderContainer
.
value
if
(
!
lc
||
!
rc
)
return
if
(
!
leftRenderTree
.
value
||
!
rightRenderTree
.
value
)
return
// 1. 收集所有块级 doc-node-wrapper
const
leftAll
=
Array
.
from
(
lc
.
querySelectorAll
<
HTMLElement
>
(
'.doc-node-wrapper:not(.inline)'
))
const
rightAll
=
Array
.
from
(
rc
.
querySelectorAll
<
HTMLElement
>
(
'.doc-node-wrapper:not(.inline)'
))
// 2. 临时关闭 transition(避免 paddingBottom 动画导致 offsetHeight 读到中间值)
const
allNodes
=
[...
leftAll
,
...
rightAll
]
allNodes
.
forEach
((
el
)
=>
{
el
.
style
.
transition
=
'none'
})
// 强制 reflow,使 transition:none 立即生效
void
lc
.
offsetHeight
// 3. 清除所有旧的 paddingBottom
leftAll
.
forEach
((
el
)
=>
{
el
.
style
.
paddingBottom
=
''
})
rightAll
.
forEach
((
el
)
=>
{
el
.
style
.
paddingBottom
=
''
})
// 再次强制 reflow,确保清除立即生效
void
lc
.
offsetHeight
// 4. 后序并行遍历两棵虚拟树,用各自节点 id 在各自容器中找 DOM 元素
const
walk
=
(
leftNode
:
XmlNode
,
rightNode
:
XmlNode
)
=>
{
const
lc_children
=
leftNode
.
children
||
[]
const
rc_children
=
rightNode
.
children
||
[]
const
len
=
Math
.
min
(
lc_children
.
length
,
rc_children
.
length
)
for
(
let
i
=
0
;
i
<
len
;
i
++
)
{
walk
(
lc_children
[
i
],
rc_children
[
i
])
}
const
leftEl
=
lc
.
querySelector
<
HTMLElement
>
(
`[data-node-id="
${
leftNode
.
id
}
"]`
)
const
rightEl
=
rc
.
querySelector
<
HTMLElement
>
(
`[data-node-id="
${
rightNode
.
id
}
"]`
)
if
(
!
leftEl
||
!
rightEl
)
return
if
(
leftEl
.
classList
.
contains
(
'inline'
)
||
rightEl
.
classList
.
contains
(
'inline'
))
return
const
hL
=
leftEl
.
offsetHeight
const
hR
=
rightEl
.
offsetHeight
const
diff
=
hR
-
hL
if
(
diff
>
2
)
{
leftEl
.
style
.
paddingBottom
=
`
${
diff
}
px`
}
else
if
(
diff
<
-
2
)
{
rightEl
.
style
.
paddingBottom
=
`
${
Math
.
abs
(
diff
)}
px`
}
}
walk
(
leftRenderTree
.
value
,
rightRenderTree
.
value
)
// 5. 补偿完成后在下一帧恢复 transition
requestAnimationFrame
(()
=>
{
allNodes
.
forEach
((
el
)
=>
{
el
.
style
.
transition
=
''
})
})
}
// 同步 XML 编辑器滚动
// 同步 XML 编辑器滚动
// 用位置相等判断阻断回路:设置 other.scrollTop 后 other 的 scroll 事件触发时两侧已相等 → 直接返回
// 用位置相等判断阻断回路:设置 other.scrollTop 后 other 的 scroll 事件触发时两侧已相等 → 直接返回
// 不使用锁标志,确保每次用户滚动事件都能即时同步对侧,不产生位置滞后
// 不使用锁标志,确保每次用户滚动事件都能即时同步对侧,不产生位置滞后
...
@@ -917,8 +1229,8 @@ export function useCompareModal() {
...
@@ -917,8 +1229,8 @@ export function useCompareModal() {
}
}
})
})
// 将左侧 lineClasses 拆解为纵向标记段
// 将左侧 lineClasses 拆解为纵向
小地图
标记段
const
d
iffHunks
=
computed
<
XmlDiffHunk
[]
>
(()
=>
{
const
leftD
iffHunks
=
computed
<
XmlDiffHunk
[]
>
(()
=>
{
const
cls
=
leftLineClasses
.
value
const
cls
=
leftLineClasses
.
value
const
n
=
cls
.
length
const
n
=
cls
.
length
if
(
n
===
0
)
return
[]
if
(
n
===
0
)
return
[]
...
@@ -941,6 +1253,14 @@ export function useCompareModal() {
...
@@ -941,6 +1253,14 @@ export function useCompareModal() {
type
:
'added'
,
type
:
'added'
,
style
:
{
top
:
`
${(
start
/
n
)
*
100
}
%`
,
height
:
`
${
Math
.
max
(
0.5
,
(
count
/
n
)
*
100
)}
%`
,
minHeight
:
'3px'
}
style
:
{
top
:
`
${(
start
/
n
)
*
100
}
%`
,
height
:
`
${
Math
.
max
(
0.5
,
(
count
/
n
)
*
100
)}
%`
,
minHeight
:
'3px'
}
})
})
}
else
if
(
cls
[
i
]
===
'diff-modified-left'
)
{
const
start
=
i
while
(
i
<
n
&&
cls
[
i
]
===
'diff-modified-left'
)
i
++
const
count
=
i
-
start
result
.
push
({
type
:
'modified'
,
style
:
{
top
:
`
${(
start
/
n
)
*
100
}
%`
,
height
:
`
${
Math
.
max
(
0.5
,
(
count
/
n
)
*
100
)}
%`
,
minHeight
:
'3px'
}
})
}
else
{
}
else
{
i
++
i
++
}
}
...
@@ -948,6 +1268,47 @@ export function useCompareModal() {
...
@@ -948,6 +1268,47 @@ export function useCompareModal() {
return
result
return
result
})
})
// 将右侧 lineClasses 拆解为纵向小地图标记段
const
rightDiffHunks
=
computed
<
XmlDiffHunk
[]
>
(()
=>
{
const
cls
=
rightLineClasses
.
value
const
n
=
cls
.
length
if
(
n
===
0
)
return
[]
const
result
:
XmlDiffHunk
[]
=
[]
let
i
=
0
while
(
i
<
n
)
{
if
(
cls
[
i
]
===
'diff-added'
)
{
const
start
=
i
while
(
i
<
n
&&
cls
[
i
]
===
'diff-added'
)
i
++
const
count
=
i
-
start
result
.
push
({
type
:
'added'
,
style
:
{
top
:
`
${(
start
/
n
)
*
100
}
%`
,
height
:
`
${
Math
.
max
(
0.5
,
(
count
/
n
)
*
100
)}
%`
,
minHeight
:
'3px'
}
})
}
else
if
(
cls
[
i
]
===
'diff-removed-placeholder'
)
{
const
start
=
i
while
(
i
<
n
&&
cls
[
i
]
===
'diff-removed-placeholder'
)
i
++
const
count
=
i
-
start
result
.
push
({
type
:
'removed'
,
style
:
{
top
:
`
${(
start
/
n
)
*
100
}
%`
,
height
:
`
${
Math
.
max
(
0.5
,
(
count
/
n
)
*
100
)}
%`
,
minHeight
:
'3px'
}
})
}
else
if
(
cls
[
i
]
===
'diff-modified-right'
)
{
const
start
=
i
while
(
i
<
n
&&
cls
[
i
]
===
'diff-modified-right'
)
i
++
const
count
=
i
-
start
result
.
push
({
type
:
'modified'
,
style
:
{
top
:
`
${(
start
/
n
)
*
100
}
%`
,
height
:
`
${
Math
.
max
(
0.5
,
(
count
/
n
)
*
100
)}
%`
,
minHeight
:
'3px'
}
})
}
else
{
i
++
}
}
return
result
})
const
diffHunks
=
leftDiffHunks
const
updateGutterScroll
=
()
=>
{
const
updateGutterScroll
=
()
=>
{
if
(
leftScrollerEl
)
{
if
(
leftScrollerEl
)
{
gutterScrollTop
.
value
=
leftScrollerEl
.
scrollTop
gutterScrollTop
.
value
=
leftScrollerEl
.
scrollTop
...
@@ -1250,13 +1611,6 @@ export function useCompareModal() {
...
@@ -1250,13 +1611,6 @@ export function useCompareModal() {
}
}
}
}
console
.
log
(
'[DEBUG Compare] handleXmlLineSelect:'
,
{
side
,
origLine
,
targetNodeId
,
minSpan
})
if
(
!
targetNodeId
)
return
if
(
!
targetNodeId
)
return
activeNodeId
.
value
=
targetNodeId
activeNodeId
.
value
=
targetNodeId
...
@@ -1265,44 +1619,44 @@ export function useCompareModal() {
...
@@ -1265,44 +1619,44 @@ export function useCompareModal() {
el
.
classList
.
remove
(
'render-active-highlight'
)
el
.
classList
.
remove
(
'render-active-highlight'
)
})
})
// 查找并高亮左侧与右侧排版区域中匹配的元素
// 查找并高亮左侧与右侧排版区域中匹配的元素
(精确匹配 nodeId,防止 node_1 误选 node_10)
const
targetLeft
=
leftRenderContainer
.
value
?.
querySelector
(
`[data-node-id^="
${
targetNodeId
}
"]`
)
as
HTMLElement
const
findRenderNodeElement
=
(
container
:
HTMLElement
|
null
,
nodeId
:
string
):
HTMLElement
|
null
=>
{
console
.
log
(
if
(
!
container
||
!
nodeId
)
return
null
'[DEBUG Compare] targetLeft element:'
,
const
els
=
container
.
querySelectorAll
(
'[data-node-id]'
)
targetLeft
for
(
let
i
=
0
;
i
<
els
.
length
;
i
++
)
{
?
{
const
el
=
els
[
i
]
as
HTMLElement
tagName
:
targetLeft
.
tagName
,
const
idAttr
=
el
.
getAttribute
(
'data-node-id'
)
||
''
nodeIdAttr
:
targetLeft
.
getAttribute
(
'data-node-id'
),
if
(
idAttr
===
nodeId
||
idAttr
.
startsWith
(
`
${
nodeId
}
_`
))
{
outerHTML
:
targetLeft
.
outerHTML
.
slice
(
0
,
200
)
return
el
}
}
:
'null'
}
)
return
null
}
const
targetLeft
=
findRenderNodeElement
(
leftRenderContainer
.
value
,
targetNodeId
)
const
targetRight
=
findRenderNodeElement
(
rightRenderContainer
.
value
,
targetNodeId
)
if
(
targetLeft
)
{
if
(
targetLeft
)
{
targetLeft
.
classList
.
add
(
'render-active-highlight'
)
targetLeft
.
classList
.
add
(
'render-active-highlight'
)
}
}
const
targetRight
=
rightRenderContainer
.
value
?.
querySelector
(
`[data-node-id^="
${
targetNodeId
}
"]`
)
as
HTMLElement
console
.
log
(
'[DEBUG Compare] targetRight element:'
,
targetRight
?
{
tagName
:
targetRight
.
tagName
,
nodeIdAttr
:
targetRight
.
getAttribute
(
'data-node-id'
),
outerHTML
:
targetRight
.
outerHTML
.
slice
(
0
,
200
)
}
:
'null'
)
if
(
targetRight
)
{
if
(
targetRight
)
{
targetRight
.
classList
.
add
(
'render-active-highlight'
)
targetRight
.
classList
.
add
(
'render-active-highlight'
)
}
}
// 智能定位:大尺寸容器(如 JOBCARD, CHP)滚动至顶部 start,小尺寸节点滚动至中央 center
const
scrollToElement
=
(
targetEl
:
HTMLElement
,
containerEl
:
HTMLElement
|
null
)
=>
{
if
(
!
targetEl
||
!
containerEl
)
return
const
containerHeight
=
containerEl
.
clientHeight
||
400
const
targetHeight
=
targetEl
.
clientHeight
||
0
const
blockAlign
=
targetHeight
>
containerHeight
*
0.35
?
'start'
:
'center'
targetEl
.
scrollIntoView
({
behavior
:
'smooth'
,
block
:
blockAlign
})
}
// 根据触发源,让对应的渲染区域容器进行滚动定位
// 根据触发源,让对应的渲染区域容器进行滚动定位
if
(
side
===
'left'
&&
targetLeft
)
{
if
(
side
===
'left'
&&
targetLeft
)
{
targetLeft
.
scrollIntoView
({
behavior
:
'smooth'
,
block
:
'center'
}
)
scrollToElement
(
targetLeft
,
leftRenderContainer
.
value
)
}
else
if
(
side
===
'right'
&&
targetRight
)
{
}
else
if
(
side
===
'right'
&&
targetRight
)
{
targetRight
.
scrollIntoView
({
behavior
:
'smooth'
,
block
:
'center'
}
)
scrollToElement
(
targetRight
,
rightRenderContainer
.
value
)
}
}
}
}
...
@@ -1321,7 +1675,7 @@ export function useCompareModal() {
...
@@ -1321,7 +1675,7 @@ export function useCompareModal() {
document
.
querySelectorAll
(
'.render-active-highlight'
).
forEach
((
el
)
=>
{
document
.
querySelectorAll
(
'.render-active-highlight'
).
forEach
((
el
)
=>
{
el
.
classList
.
remove
(
'render-active-highlight'
)
el
.
classList
.
remove
(
'render-active-highlight'
)
})
})
document
.
querySelectorAll
(
`[data-node-id
^="
${
nodeId
}
"]`
).
forEach
((
el
)
=>
{
document
.
querySelectorAll
(
`[data-node-id
="
${
nodeId
}
"], [data-node-id^="
${
nodeId
}
_
"]`
).
forEach
((
el
)
=>
{
el
.
classList
.
add
(
'render-active-highlight'
)
el
.
classList
.
add
(
'render-active-highlight'
)
})
})
...
@@ -1329,13 +1683,10 @@ export function useCompareModal() {
...
@@ -1329,13 +1683,10 @@ export function useCompareModal() {
leftActiveLine
.
value
=
null
leftActiveLine
.
value
=
null
rightActiveLine
.
value
=
null
rightActiveLine
.
value
=
null
// 高亮定位 XML 编辑器
// 高亮定位 XML 编辑器
console
.
log
(
'[CompareModal] Clicked side:'
,
side
,
'nodeId:'
,
nodeId
)
const
leftRange
=
leftNodeRanges
.
value
.
get
(
nodeId
)
const
leftRange
=
leftNodeRanges
.
value
.
get
(
nodeId
)
console
.
log
(
'[CompareModal] leftRange:'
,
leftRange
)
if
(
leftRange
)
{
if
(
leftRange
)
{
const
leftDisplayLine
=
leftOriginalToEditor
.
value
.
get
(
leftRange
.
startLine
)
const
leftDisplayLine
=
leftOriginalToEditor
.
value
.
get
(
leftRange
.
startLine
)
console
.
log
(
'[CompareModal] leftDisplayLine:'
,
leftDisplayLine
)
if
(
leftDisplayLine
)
{
if
(
leftDisplayLine
)
{
leftActiveLine
.
value
=
leftDisplayLine
leftActiveLine
.
value
=
leftDisplayLine
leftEditorRef
.
value
?.
highlightAndScrollToLine
(
leftDisplayLine
)
leftEditorRef
.
value
?.
highlightAndScrollToLine
(
leftDisplayLine
)
...
@@ -1343,10 +1694,8 @@ export function useCompareModal() {
...
@@ -1343,10 +1694,8 @@ export function useCompareModal() {
}
}
const
rightRange
=
rightNodeRanges
.
value
.
get
(
nodeId
)
const
rightRange
=
rightNodeRanges
.
value
.
get
(
nodeId
)
console
.
log
(
'[CompareModal] rightRange:'
,
rightRange
)
if
(
rightRange
)
{
if
(
rightRange
)
{
const
rightDisplayLine
=
rightOriginalToEditor
.
value
.
get
(
rightRange
.
startLine
)
const
rightDisplayLine
=
rightOriginalToEditor
.
value
.
get
(
rightRange
.
startLine
)
console
.
log
(
'[CompareModal] rightDisplayLine:'
,
rightDisplayLine
)
if
(
rightDisplayLine
)
{
if
(
rightDisplayLine
)
{
rightActiveLine
.
value
=
rightDisplayLine
rightActiveLine
.
value
=
rightDisplayLine
rightEditorRef
.
value
?.
highlightAndScrollToLine
(
rightDisplayLine
)
rightEditorRef
.
value
?.
highlightAndScrollToLine
(
rightDisplayLine
)
...
@@ -1424,6 +1773,67 @@ export function useCompareModal() {
...
@@ -1424,6 +1773,67 @@ export function useCompareModal() {
}
}
})
})
// ── 拖拽调整区域高度 (Vertical Splitter) ──────────────────
const
mainContainerRef
=
ref
<
HTMLElement
|
null
>
(
null
)
const
textZoneHeight
=
ref
<
number
|
null
>
(
null
)
const
isResizingVertical
=
ref
(
false
)
// 上方 XML 对比区域动态样式
const
textZoneStyle
=
computed
(()
=>
{
if
(
!
showXmlCompare
.
value
)
return
{}
if
(
!
showRenderCompare
.
value
)
return
{
flex
:
'1 1 0% !important'
}
if
(
textZoneHeight
.
value
!==
null
)
{
return
{
flex
:
`0 0
${
textZoneHeight
.
value
}
px !important`
,
minHeight
:
'80px !important'
}
}
return
{
flex
:
'0 0 40% !important'
,
minHeight
:
'80px !important'
}
})
// 下方渲染对比区域动态样式
const
renderZoneStyle
=
computed
(()
=>
{
if
(
!
showRenderCompare
.
value
)
return
{}
if
(
!
showXmlCompare
.
value
)
return
{
flex
:
'1 1 0% !important'
}
return
{
flex
:
'1 1 0% !important'
,
minHeight
:
'100px !important'
}
})
// 1. 上下高度拖拽 (Vertical Drag)
const
startVerticalDrag
=
(
e
:
PointerEvent
)
=>
{
e
.
preventDefault
()
isResizingVertical
.
value
=
true
const
startY
=
e
.
clientY
const
containerEl
=
mainContainerRef
.
value
if
(
!
containerEl
)
return
const
textZoneEl
=
containerEl
.
querySelector
(
'.compare-text-zone'
)
as
HTMLElement
|
null
const
startHeight
=
textZoneEl
?
textZoneEl
.
getBoundingClientRect
().
height
:
textZoneHeight
.
value
||
280
const
containerHeight
=
containerEl
.
getBoundingClientRect
().
height
const
onPointerMove
=
(
moveEv
:
PointerEvent
)
=>
{
if
(
!
isResizingVertical
.
value
)
return
const
deltaY
=
moveEv
.
clientY
-
startY
const
newHeight
=
Math
.
max
(
80
,
Math
.
min
(
containerHeight
-
120
,
startHeight
+
deltaY
))
textZoneHeight
.
value
=
newHeight
}
const
onPointerUp
=
()
=>
{
isResizingVertical
.
value
=
false
window
.
removeEventListener
(
'pointermove'
,
onPointerMove
)
window
.
removeEventListener
(
'pointerup'
,
onPointerUp
)
}
window
.
addEventListener
(
'pointermove'
,
onPointerMove
)
window
.
addEventListener
(
'pointerup'
,
onPointerUp
)
}
return
{
return
{
visible
,
visible
,
leftXml
,
leftXml
,
...
@@ -1432,6 +1842,8 @@ export function useCompareModal() {
...
@@ -1432,6 +1842,8 @@ export function useCompareModal() {
rightDisplayXml
,
rightDisplayXml
,
leftLineClasses
,
leftLineClasses
,
rightLineClasses
,
rightLineClasses
,
leftInlineDiffs
,
rightInlineDiffs
,
leftLineNumbers
,
leftLineNumbers
,
rightLineNumbers
,
rightLineNumbers
,
leftRenderTree
,
leftRenderTree
,
...
@@ -1445,6 +1857,9 @@ export function useCompareModal() {
...
@@ -1445,6 +1857,9 @@ export function useCompareModal() {
compared
,
compared
,
comparing
,
comparing
,
diffCounts
,
diffCounts
,
navCurrentDisplay
,
navigateToNextDiff
,
navigateToPrevDiff
,
showXmlCompare
,
showXmlCompare
,
showRenderCompare
,
showRenderCompare
,
leftRenderContainer
,
leftRenderContainer
,
...
@@ -1452,6 +1867,7 @@ export function useCompareModal() {
...
@@ -1452,6 +1867,7 @@ export function useCompareModal() {
open
,
open
,
handleUseCurrent
,
handleUseCurrent
,
handleCompare
,
handleCompare
,
syncRenderHeights
,
unbindScrollSync
,
unbindScrollSync
,
// 自定义横向滚动
// 自定义横向滚动
isLeftHorizontalScrollable
,
isLeftHorizontalScrollable
,
...
@@ -1468,6 +1884,8 @@ export function useCompareModal() {
...
@@ -1468,6 +1884,8 @@ export function useCompareModal() {
isDragging
,
isDragging
,
viewportStyle
,
viewportStyle
,
diffHunks
,
diffHunks
,
leftDiffHunks
,
rightDiffHunks
,
handleThumbPointerdown
,
handleThumbPointerdown
,
handleScrollbarClick
,
handleScrollbarClick
,
renderScrollerBound
,
renderScrollerBound
,
...
@@ -1489,6 +1907,13 @@ export function useCompareModal() {
...
@@ -1489,6 +1907,13 @@ export function useCompareModal() {
handleXmlLineSelect
,
handleXmlLineSelect
,
handleRenderClick
,
handleRenderClick
,
handleTextExpandedChange
,
handleTextExpandedChange
,
handleRenderExpandedChange
handleRenderExpandedChange
,
// 上下拖拽调节
mainContainerRef
,
textZoneHeight
,
isResizingVertical
,
textZoneStyle
,
renderZoneStyle
,
startVerticalDrag
}
}
}
}
src/views/editor/components/EditorToolbar/components/CompareModal/index.vue
View file @
5af0d0fb
...
@@ -22,12 +22,27 @@
...
@@ -22,12 +22,27 @@
+
+
</span>
</span>
<span
class=
"text-success font-medium"
>
新增内容
</span>
<span
class=
"text-success font-medium"
>
新增内容
</span>
<template
v-if=
"compared"
>
<button
class=
"flex items-center justify-center w-4 h-4 rounded text-success hover:bg-success/20 transition-colors cursor-pointer select-none text-[11px] font-bold"
title=
"上一处新增"
@
click=
"navigateToPrevDiff('added')"
>
‹
</button>
<span
<span
v-if=
"compared"
class=
"px-1.5 py-0.5 text-[10px] font-bold rounded bg-success/10 border border-success/30 text-success leading-none select-none min-w-[18px] text-center"
class=
"px-1.5 py-0.5 text-[10px] font-bold rounded bg-success/10 border border-success/30 text-success leading-none"
>
>
{{
diffCounts
.
added
}}
{{
navCurrentDisplay
.
added
}}
</span>
</span>
<button
class=
"flex items-center justify-center w-4 h-4 rounded text-success hover:bg-success/20 transition-colors cursor-pointer select-none text-[11px] font-bold"
title=
"下一处新增"
@
click=
"navigateToNextDiff('added')"
>
›
</button>
</
template
>
</div>
</div>
<div
class=
"flex items-center gap-1.5"
>
<div
class=
"flex items-center gap-1.5"
>
<span
<span
...
@@ -36,12 +51,27 @@
...
@@ -36,12 +51,27 @@
-
-
</span>
</span>
<span
class=
"text-error font-medium line-through"
>
删除内容
</span>
<span
class=
"text-error font-medium line-through"
>
删除内容
</span>
<
template
v-if=
"compared"
>
<button
class=
"flex items-center justify-center w-4 h-4 rounded text-error hover:bg-error/20 transition-colors cursor-pointer select-none text-[11px] font-bold"
title=
"上一处删除"
@
click=
"navigateToPrevDiff('removed')"
>
‹
</button>
<span
<span
v-if=
"compared"
class=
"px-1.5 py-0.5 text-[10px] font-bold rounded bg-error/10 border border-error/30 text-error leading-none select-none min-w-[18px] text-center"
class=
"px-1.5 py-0.5 text-[10px] font-bold rounded bg-error/10 border border-error/30 text-error leading-none"
>
>
{{
diffCounts
.
removed
}}
{{
navCurrentDisplay
.
removed
}}
</span>
</span>
<button
class=
"flex items-center justify-center w-4 h-4 rounded text-error hover:bg-error/20 transition-colors cursor-pointer select-none text-[11px] font-bold"
title=
"下一处删除"
@
click=
"navigateToNextDiff('removed')"
>
›
</button>
</
template
>
</div>
</div>
<div
class=
"flex items-center gap-1.5"
>
<div
class=
"flex items-center gap-1.5"
>
<span
<span
...
@@ -50,12 +80,27 @@
...
@@ -50,12 +80,27 @@
~
~
</span>
</span>
<span
class=
"text-warning font-medium"
>
属性/文本变更
</span>
<span
class=
"text-warning font-medium"
>
属性/文本变更
</span>
<
template
v-if=
"compared"
>
<button
class=
"flex items-center justify-center w-4 h-4 rounded text-warning hover:bg-warning/20 transition-colors cursor-pointer select-none text-[11px] font-bold"
title=
"上一处属性/文本变更"
@
click=
"navigateToPrevDiff('modified')"
>
‹
</button>
<span
<span
v-if=
"compared"
class=
"px-1.5 py-0.5 text-[10px] font-bold rounded bg-warning/10 border border-warning/30 text-warning leading-none select-none min-w-[18px] text-center"
class=
"px-1.5 py-0.5 text-[10px] font-bold rounded bg-warning/10 border border-warning/30 text-warning leading-none"
>
>
{{
diffCounts
.
modified
}}
{{
navCurrentDisplay
.
modified
}}
</span>
</span>
<button
class=
"flex items-center justify-center w-4 h-4 rounded text-warning hover:bg-warning/20 transition-colors cursor-pointer select-none text-[11px] font-bold"
title=
"下一处属性/文本变更"
@
click=
"navigateToNextDiff('modified')"
>
›
</button>
</
template
>
</div>
</div>
<div
class=
"flex items-center gap-1.5"
>
<div
class=
"flex items-center gap-1.5"
>
<span
class=
"w-3.5 h-3.5 rounded bg-fill-3 border border-divider/60 repeating-stripes"
></span>
<span
class=
"w-3.5 h-3.5 rounded bg-fill-3 border border-divider/60 repeating-stripes"
></span>
...
@@ -89,12 +134,16 @@
...
@@ -89,12 +134,16 @@
</div>
</div>
<!-- 主内容区域:双栏展示 -->
<!-- 主内容区域:双栏展示 -->
<div
class=
"flex-1 flex flex-col min-h-0 overflow-hidden bg-fill-2/30 p-4 gap-4 relative"
>
<div
ref=
"mainContainerRef"
class=
"flex-1 flex flex-col min-h-0 overflow-hidden bg-fill-2/30 p-4 gap-2 relative"
:class=
"{ 'select-none': isResizingVertical }"
>
<!-- 加载遮罩(比对进行中) -->
<!-- 加载遮罩(比对进行中) -->
<transition
name=
"fade"
>
<transition
name=
"fade"
>
<div
v-if=
"comparing"
class=
"
absolute inset-0 z-20 flex flex-col items-center justify-center bg-card/80 backdrop-blur-sm
"
>
<div
v-if=
"comparing"
class=
"
compare-loading-overlay absolute inset-0 z-20 flex flex-col items-center justify-center gap-3
"
>
<
n-spin
size=
"large"
/
>
<
div
class=
"compare-spinner"
></div
>
<span
class=
"
mt-3 text-sm text-color2 font-medium
"
>
正在分析对比,请稍候...
</span>
<span
class=
"
compare-loading-text
"
>
正在分析对比,请稍候...
</span>
</div>
</div>
</transition>
</transition>
<!-- 上方:XML 源代码对比区域 -->
<!-- 上方:XML 源代码对比区域 -->
...
@@ -102,10 +151,12 @@
...
@@ -102,10 +151,12 @@
class=
"flex flex-col compare-text-zone shrink-0 gap-1.5"
class=
"flex flex-col compare-text-zone shrink-0 gap-1.5"
:class=
"{
:class=
"{
'is-collapsed': !showXmlCompare,
'is-collapsed': !showXmlCompare,
'expanded-full': !showRenderCompare
'expanded-full': !showRenderCompare,
'is-resizing': isResizingVertical
}"
}"
:style=
"textZoneStyle"
>
>
<div
class=
"flex items-center justify-between text-xs font-bold text-color2 px-1"
>
<div
class=
"flex items-center justify-between text-xs font-bold text-color2 px-1
shrink-0
"
>
<div
class=
"flex items-center gap-2"
>
<div
class=
"flex items-center gap-2"
>
<span>
源工卡 XML (旧版本)
</span>
<span>
源工卡 XML (旧版本)
</span>
<CommonButton
size=
"tiny"
quaternary
type=
"info"
@
click=
"handleUseCurrent"
>
使用当前打开的工卡
</CommonButton>
<CommonButton
size=
"tiny"
quaternary
type=
"info"
@
click=
"handleUseCurrent"
>
使用当前打开的工卡
</CommonButton>
...
@@ -127,6 +178,7 @@
...
@@ -127,6 +178,7 @@
min-height=
"100%"
min-height=
"100%"
:line-classes=
"computedLeftLineClasses"
:line-classes=
"computedLeftLineClasses"
:line-numbers=
"leftLineNumbers"
:line-numbers=
"leftLineNumbers"
:inline-diffs=
"leftInlineDiffs"
@
line-select=
"handleXmlLineSelect($event, 'left')"
@
line-select=
"handleXmlLineSelect($event, 'left')"
/>
/>
</div>
</div>
...
@@ -138,12 +190,19 @@
...
@@ -138,12 +190,19 @@
<!-- diff 差异标记(仅对比后显示) -->
<!-- diff 差异标记(仅对比后显示) -->
<
template
v-if=
"compared"
>
<
template
v-if=
"compared"
>
<div
<div
v-for=
"(hunk, i) in
d
iffHunks"
v-for=
"(hunk, i) in
leftD
iffHunks"
:key=
"i"
:key=
"i"
class=
"absolute left-0.5 right-0.5 rounded-sm"
class=
"absolute left-0.5 right-0.5 rounded-sm"
:style=
"[
:style=
"[
hunk.style,
hunk.style,
{ background: hunk.type === 'removed' ? 'rgba(239,83,80,0.8)' : 'rgba(102,187,106,0.8)' }
{
background:
hunk.type === 'removed'
? 'rgba(239,83,80,0.85)'
: hunk.type === 'added'
? 'rgba(102,187,106,0.85)'
: 'rgba(245,158,11,0.9)'
}
]"
]"
/>
/>
</
template
>
</
template
>
...
@@ -183,6 +242,7 @@
...
@@ -183,6 +242,7 @@
min-height=
"100%"
min-height=
"100%"
:line-classes=
"computedRightLineClasses"
:line-classes=
"computedRightLineClasses"
:line-numbers=
"rightLineNumbers"
:line-numbers=
"rightLineNumbers"
:inline-diffs=
"rightInlineDiffs"
@
line-select=
"handleXmlLineSelect($event, 'right')"
@
line-select=
"handleXmlLineSelect($event, 'right')"
/>
/>
</div>
</div>
...
@@ -194,12 +254,19 @@
...
@@ -194,12 +254,19 @@
<!-- diff 差异标记(仅对比后显示) -->
<!-- diff 差异标记(仅对比后显示) -->
<
template
v-if=
"compared"
>
<
template
v-if=
"compared"
>
<div
<div
v-for=
"(hunk, i) in
d
iffHunks"
v-for=
"(hunk, i) in
rightD
iffHunks"
:key=
"i"
:key=
"i"
class=
"absolute left-0.5 right-0.5 rounded-sm"
class=
"absolute left-0.5 right-0.5 rounded-sm"
:style=
"[
:style=
"[
hunk.style,
hunk.style,
{ background: hunk.type === 'removed' ? 'rgba(239,83,80,0.8)' : 'rgba(102,187,106,0.8)' }
{
background:
hunk.type === 'removed'
? 'rgba(239,83,80,0.85)'
: hunk.type === 'added'
? 'rgba(102,187,106,0.85)'
: 'rgba(245,158,11,0.9)'
}
]"
]"
/>
/>
</
template
>
</
template
>
...
@@ -233,19 +300,39 @@
...
@@ -233,19 +300,39 @@
</div>
</div>
</div>
</div>
<!-- 上下拖拽分割条 (Resizer) -->
<div
v-if=
"showXmlCompare && showRenderCompare"
class=
"compare-split-divider group shrink-0 relative flex items-center justify-center cursor-row-resize select-none z-10 -my-0.5 h-3 rounded"
:class=
"{ 'is-dragging': isResizingVertical }"
@
pointerdown=
"startVerticalDrag"
>
<!-- 可视指示线 -->
<div
class=
"compare-divider-line"
></div>
<!-- 手柄圆点组 -->
<div
class=
"compare-divider-handle"
>
<div
class=
"handle-dot"
></div>
<div
class=
"handle-dot"
></div>
<div
class=
"handle-dot"
></div>
</div>
</div>
<!-- 下方:排版样式渲染对比区域 -->
<!-- 下方:排版样式渲染对比区域 -->
<div
<div
class=
"flex-grow flex flex-col compare-render-zone min-h-0 gap-1.5"
class=
"flex-grow flex flex-col compare-render-zone min-h-0 gap-1.5"
:class=
"{
:class=
"{
'is-collapsed': !showRenderCompare
'is-collapsed': !showRenderCompare,
'is-resizing': isResizingVertical
}"
}"
:style=
"renderZoneStyle"
>
>
<div
class=
"flex items-center justify-between text-xs font-bold text-color2 px-1 shrink-0"
>
<div
class=
"flex items-center justify-between text-xs font-bold text-color2 px-1 shrink-0"
>
<span>
源工卡 渲染 (旧版本)
</span>
<span>
源工卡 渲染 (旧版本)
</span>
<span>
目标工卡 渲染 (新版本)
</span>
<span>
目标工卡 渲染 (新版本)
</span>
</div>
</div>
<div
class=
"flex-grow flex gap-
4
min-h-0"
>
<div
class=
"flex-grow flex gap-
2
min-h-0"
>
<!-- 左:源工卡渲染 -->
<!-- 左:源工卡渲染 -->
<div
class=
"flex-1 min-w-0 h-full flex border border-divider rounded-xl bg-card overflow-hidden"
>
<div
class=
"flex-1 min-w-0 h-full flex border border-divider rounded-xl bg-card overflow-hidden"
>
<div
<div
...
@@ -371,6 +458,8 @@ const {
...
@@ -371,6 +458,8 @@ const {
rightDisplayXml
,
rightDisplayXml
,
leftLineClasses
,
leftLineClasses
,
rightLineClasses
,
rightLineClasses
,
leftInlineDiffs
,
rightInlineDiffs
,
leftLineNumbers
,
leftLineNumbers
,
rightLineNumbers
,
rightLineNumbers
,
leftRenderTree
,
leftRenderTree
,
...
@@ -384,6 +473,9 @@ const {
...
@@ -384,6 +473,9 @@ const {
compared
,
compared
,
comparing
,
comparing
,
diffCounts
,
diffCounts
,
navCurrentDisplay
,
navigateToNextDiff
,
navigateToPrevDiff
,
showXmlCompare
,
showXmlCompare
,
showRenderCompare
,
showRenderCompare
,
leftRenderContainer
,
leftRenderContainer
,
...
@@ -407,6 +499,8 @@ const {
...
@@ -407,6 +499,8 @@ const {
isDragging
,
isDragging
,
viewportStyle
,
viewportStyle
,
diffHunks
,
diffHunks
,
leftDiffHunks
,
rightDiffHunks
,
handleThumbPointerdown
,
handleThumbPointerdown
,
handleScrollbarClick
,
handleScrollbarClick
,
renderScrollerBound
,
renderScrollerBound
,
...
@@ -425,7 +519,14 @@ const {
...
@@ -425,7 +519,14 @@ const {
handleXmlLineSelect
,
handleXmlLineSelect
,
handleRenderClick
,
handleRenderClick
,
handleTextExpandedChange
,
handleTextExpandedChange
,
handleRenderExpandedChange
handleRenderExpandedChange
,
// 拖拽控制面板与状态
mainContainerRef
,
textZoneHeight
,
isResizingVertical
,
textZoneStyle
,
renderZoneStyle
,
startVerticalDrag
}
=
useCompareModal
()
}
=
useCompareModal
()
// 注入只读对比模式状态
// 注入只读对比模式状态
...
@@ -549,73 +650,192 @@ defineExpose({
...
@@ -549,73 +650,192 @@ defineExpose({
)
!important
;
)
!important
;
}
}
//
排版渲染结果比对样式
//
排版渲染结果比对样式
(完全不侵入和改变原工卡的任何
Padding
、
Margin
和缩进样式)
:deep
([
data-diff-status
=
'added'
])
{
:deep
([
data-diff-status
=
'added'
])
{
background-color
:
rgba
(
102
,
187
,
106
,
0.
08
)
;
background-color
:
rgba
(
102
,
187
,
106
,
0.
1
)
!important
;
outline
:
1.5px
dashed
rgba
(
102
,
187
,
106
,
0.
4
5
)
!important
;
outline
:
1.5px
dashed
rgba
(
102
,
187
,
106
,
0.5
)
!important
;
outline-offset
:
1px
;
outline-offset
:
1px
;
position
:
relative
;
position
:
relative
;
border-radius
:
4px
;
border-radius
:
4px
;
&::before
{
&::before
{
content
:
'+'
;
content
:
'+'
;
position
:
absolute
;
position
:
absolute
;
left
:
2
px
;
left
:
-18
px
;
top
:
2px
;
top
:
2px
;
color
:
#2e7d32
;
color
:
#2e7d32
;
font-weight
:
bold
;
font-weight
:
bold
;
font-size
:
1
1
px
;
font-size
:
1
2
px
;
line-height
:
1
;
line-height
:
1
;
z-index
:
10
;
z-index
:
10
;
background-color
:
rgba
(
102
,
187
,
106
,
0.2
);
background-color
:
rgba
(
102
,
187
,
106
,
0.2
5
);
padding
:
1px
3
px
;
padding
:
1px
4
px
;
border-radius
:
2
px
;
border-radius
:
3
px
;
}
}
}
}
:deep
([
data-diff-status
=
'removed'
])
{
:deep
([
data-diff-status
=
'removed'
])
{
background-color
:
rgba
(
239
,
83
,
80
,
0.
08
)
;
background-color
:
rgba
(
239
,
83
,
80
,
0.
1
)
!important
;
outline
:
1.5px
dashed
rgba
(
239
,
83
,
80
,
0.
4
5
)
!important
;
outline
:
1.5px
dashed
rgba
(
239
,
83
,
80
,
0.5
)
!important
;
outline-offset
:
1px
;
outline-offset
:
1px
;
text-decoration
:
line-through
;
text-decoration
:
line-through
!important
;
opacity
:
0.7
2
;
opacity
:
0.7
5
;
position
:
relative
;
position
:
relative
;
border-radius
:
4px
;
border-radius
:
4px
;
&::before
{
&::before
{
content
:
'-'
;
content
:
'-'
;
position
:
absolute
;
position
:
absolute
;
left
:
2
px
;
left
:
-18
px
;
top
:
2px
;
top
:
2px
;
color
:
#c62828
;
color
:
#c62828
;
font-weight
:
bold
;
font-weight
:
bold
;
font-size
:
1
1
px
;
font-size
:
1
2
px
;
line-height
:
1
;
line-height
:
1
;
z-index
:
10
;
z-index
:
10
;
background-color
:
rgba
(
239
,
83
,
80
,
0.2
);
background-color
:
rgba
(
239
,
83
,
80
,
0.2
5
);
padding
:
1px
4px
;
padding
:
1px
4px
;
border-radius
:
2
px
;
border-radius
:
3
px
;
}
}
}
}
:deep
([
data-diff-status
=
'modified'
])
{
:deep
([
data-diff-status
=
'modified'
])
{
background-color
:
rgba
(
255
,
193
,
7
,
0.0
6
)
;
background-color
:
rgba
(
255
,
193
,
7
,
0.0
8
)
!important
;
outline
:
1.5px
dashed
rgba
(
255
,
193
,
7
,
0.4
)
!important
;
outline
:
1.5px
dashed
rgba
(
255
,
193
,
7
,
0.4
5
)
!important
;
outline-offset
:
1px
;
outline-offset
:
1px
;
border-radius
:
4px
;
border-radius
:
4px
;
}
}
//
隐藏空白对齐占位节点
:deep
([
data-attr-modified
=
'true'
])
{
:deep
([
data-diff-status
=
'added-placeholder'
]),
/* 属性具体变更点由 DocNodeRenderer 内部 attr-diff-badge 明确呈现 */
}
//
XML
源码编辑区同一行属性
/
内容修改侧重高亮
:deep
(
.diff-modified-left
)
{
background-color
:
rgba
(
255
,
193
,
7
,
0.14
)
!important
;
}
:deep
(
.diff-modified-right
)
{
background-color
:
rgba
(
255
,
193
,
7
,
0.18
)
!important
;
}
//
XML
源码编辑区行内细粒度
(
Word-level
)
变动字符强醒目标记
:deep
(
.cm-diff-inline-removed
)
{
background-color
:
rgba
(
239
,
68
,
68
,
0.22
)
!important
;
color
:
#b91c1c
!important
;
font-weight
:
700
!important
;
border-radius
:
3px
!important
;
padding
:
0
3px
!important
;
outline
:
1px
solid
rgba
(
220
,
38
,
38
,
0.5
)
!important
;
}
:deep
(
.cm-diff-inline-added
)
{
background-color
:
rgba
(
34
,
197
,
94
,
0.22
)
!important
;
color
:
#15803d
!important
;
font-weight
:
700
!important
;
border-radius
:
3px
!important
;
padding
:
0
3px
!important
;
outline
:
1px
solid
rgba
(
22
,
163
,
74
,
0.5
)
!important
;
}
//
──
暗色主题
(
Dark
Mode
)
文本与高亮适应性优化
──
:deep
(
.dark
)
.cm-diff-inline-removed
,
html
.dark
:deep
(
.cm-diff-inline-removed
)
{
background-color
:
rgba
(
239
,
68
,
68
,
0.35
)
!important
;
color
:
#fca5a5
!important
;
outline-color
:
rgba
(
248
,
113
,
113
,
0.6
)
!important
;
}
:deep
(
.dark
)
.cm-diff-inline-added
,
html
.dark
:deep
(
.cm-diff-inline-added
)
{
background-color
:
rgba
(
34
,
197
,
94
,
0.35
)
!important
;
color
:
#86efac
!important
;
outline-color
:
rgba
(
74
,
222
,
128
,
0.6
)
!important
;
}
:deep
(
.dark
)
[
data-diff-status
=
'added'
]
::before
,
html
.dark
:deep
([
data-diff-status
=
'added'
]
::before
)
{
color
:
#86efac
!important
;
background-color
:
rgba
(
34
,
197
,
94
,
0.35
)
!important
;
}
:deep
(
.dark
)
[
data-diff-status
=
'removed'
]
::before
,
html
.dark
:deep
([
data-diff-status
=
'removed'
]
::before
)
{
color
:
#fca5a5
!important
;
background-color
:
rgba
(
239
,
68
,
68
,
0.35
)
!important
;
}
//
占位排版比对结果(保持原工卡缩进与同行
1-to-1
完全对齐)
:deep
([
data-diff-status
=
'removed-placeholder'
])
{
:deep
([
data-diff-status
=
'removed-placeholder'
])
{
opacity
:
0
!important
;
background-color
:
rgba
(
239
,
83
,
80
,
0.05
)
!important
;
visibility
:
hidden
!important
;
background-image
:
repeating-linear-gradient
(
pointer-events
:
none
!important
;
-45deg
,
*
{
transparent
,
opacity
:
0
!important
;
transparent
5px
,
visibility
:
hidden
!important
;
rgba
(
239
,
83
,
80
,
0.08
)
5px
,
pointer-events
:
none
!important
;
rgba
(
239
,
83
,
80
,
0.08
)
10px
)
!important
;
outline
:
1.5px
dashed
rgba
(
239
,
83
,
80
,
0.4
)
!important
;
outline-offset
:
1px
;
text-decoration
:
line-through
!important
;
opacity
:
0.72
;
position
:
relative
;
border-radius
:
4px
;
&::before
{
content
:
'-'
;
position
:
absolute
;
left
:
-18px
;
top
:
2px
;
color
:
#c62828
;
font-weight
:
bold
;
font-size
:
12px
;
line-height
:
1
;
z-index
:
10
;
background-color
:
rgba
(
239
,
83
,
80
,
0.25
);
padding
:
1px
4px
;
border-radius
:
3px
;
}
}
}
}
:deep
([
data-diff-status
=
'added-placeholder'
])
{
background-color
:
rgba
(
102
,
187
,
106
,
0.05
)
!important
;
background-image
:
repeating-linear-gradient
(
-45deg
,
transparent
,
transparent
5px
,
rgba
(
102
,
187
,
106
,
0.08
)
5px
,
rgba
(
102
,
187
,
106
,
0.08
)
10px
)
!important
;
outline
:
1.5px
dashed
rgba
(
102
,
187
,
106
,
0.4
)
!important
;
outline-offset
:
1px
;
opacity
:
0.72
;
position
:
relative
;
border-radius
:
4px
;
&::before
{
content
:
'+'
;
position
:
absolute
;
left
:
-18px
;
top
:
2px
;
color
:
#2e7d32
;
font-weight
:
bold
;
font-size
:
12px
;
line-height
:
1
;
z-index
:
10
;
background-color
:
rgba
(
102
,
187
,
106
,
0.25
);
padding
:
1px
4px
;
border-radius
:
3px
;
}
}
:deep
(
.dark
)
[
data-diff-status
=
'removed-placeholder'
]
::before
,
html
.dark
:deep
([
data-diff-status
=
'removed-placeholder'
]
::before
)
{
color
:
#fca5a5
!important
;
background-color
:
rgba
(
239
,
68
,
68
,
0.3
)
!important
;
}
:deep
(
.dark
)
[
data-diff-status
=
'added-placeholder'
]
::before
,
html
.dark
:deep
([
data-diff-status
=
'added-placeholder'
]
::before
)
{
color
:
#86efac
!important
;
background-color
:
rgba
(
34
,
197
,
94
,
0.3
)
!important
;
}
//
单词级高亮显示
//
单词级高亮显示
:deep
(
.text-success
)
{
:deep
(
.text-success
)
{
color
:
#2e7d32
!important
;
color
:
#2e7d32
!important
;
...
@@ -633,6 +853,17 @@ defineExpose({
...
@@ -633,6 +853,17 @@ defineExpose({
border-radius
:
2px
;
border-radius
:
2px
;
}
}
:deep
(
.dark
)
.text-success
,
html
.dark
:deep
(
.text-success
)
{
color
:
#4ade80
!important
;
background-color
:
rgba
(
34
,
197
,
94
,
0.25
)
!important
;
}
:deep
(
.dark
)
.text-error
,
html
.dark
:deep
(
.text-error
)
{
color
:
#f87171
!important
;
background-color
:
rgba
(
239
,
68
,
68
,
0.25
)
!important
;
}
//
选中排版节点双向联动高亮样式
//
选中排版节点双向联动高亮样式
:deep
(
.render-active-highlight
)
{
:deep
(
.render-active-highlight
)
{
outline
:
2px
dashed
var
(
--primary-color
,
#165dff
)
!important
;
outline
:
2px
dashed
var
(
--primary-color
,
#165dff
)
!important
;
...
@@ -662,64 +893,156 @@ defineExpose({
...
@@ -662,64 +893,156 @@ defineExpose({
/* XML 文本区默认样式与展开/收起过渡 */
/* XML 文本区默认样式与展开/收起过渡 */
.compare-text-zone
{
.compare-text-zone
{
flex
:
1
1
0%
!important
;
flex
:
0
0
40%
;
min-height
:
180px
!important
;
min-height
:
80px
;
max-height
:
320px
!important
;
margin-bottom
:
0px
;
margin-bottom
:
0px
!important
;
opacity
:
1
;
opacity
:
1
!important
;
overflow
:
hidden
;
overflow
:
hidden
;
/* 展开过渡:弹性分配比和高度立即变化,不透明度延迟 0.15s 淡入 */
transition
:
transition
:
flex
0.4s
cubic-bezier
(
0.25
,
0.8
,
0.25
,
1
)
0s
,
flex
0.35s
cubic-bezier
(
0.25
,
0.8
,
0.25
,
1
),
max-height
0.4s
cubic-bezier
(
0.25
,
0.8
,
0.25
,
1
)
0s
,
min-height
0.35s
cubic-bezier
(
0.25
,
0.8
,
0.25
,
1
),
min-height
0.4s
cubic-bezier
(
0.25
,
0.8
,
0.25
,
1
)
0s
,
opacity
0.25s
linear
;
opacity
0.2s
linear
0.15s
!important
;
}
/* 拖拽过程中禁用 CSS 过渡,保证 60FPS 实时极速响应 */
.compare-text-zone.is-resizing
,
.compare-render-zone.is-resizing
{
transition
:
none
!important
;
}
}
/* 当渲染区收起时,文本区独占全部剩余高度 */
/* 当渲染区收起时,文本区独占全部剩余高度 */
.compare-text-zone.expanded-full
{
.compare-text-zone.expanded-full
{
flex
:
1
1
0%
!important
;
flex
:
1
1
0%
!important
;
min-height
:
0
!important
;
min-height
:
0
!important
;
max-height
:
100%
!important
;
}
}
/* 当文本区收起时
:不透明度立即淡出,尺寸延迟 0.08s 收折
*/
/* 当文本区收起时 */
.compare-text-zone.is-collapsed
{
.compare-text-zone.is-collapsed
{
flex
:
0
0
0%
!important
;
flex
:
0
0
0%
!important
;
max-height
:
0
!important
;
min-height
:
0px
!important
;
min-height
:
0
!important
;
opacity
:
0
!important
;
opacity
:
0
!important
;
pointer-events
:
none
;
pointer-events
:
none
;
transition
:
transition
:
opacity
0.15s
linear
0s
,
opacity
0.2s
linear
,
flex
0.4s
cubic-bezier
(
0.25
,
0.8
,
0.25
,
1
)
0.08s
,
flex
0.35s
cubic-bezier
(
0.25
,
0.8
,
0.25
,
1
),
max-height
0.4s
cubic-bezier
(
0.25
,
0.8
,
0.25
,
1
)
0.08s
,
min-height
0.35s
cubic-bezier
(
0.25
,
0.8
,
0.25
,
1
)
!important
;
min-height
0.4s
cubic-bezier
(
0.25
,
0.8
,
0.25
,
1
)
0.08s
!important
;
}
}
/* 渲染区默认样式与展开/收起过渡 */
/* 渲染区默认样式与展开/收起过渡 */
.compare-render-zone
{
.compare-render-zone
{
flex
:
2
1
0%
!important
;
flex
:
1
1
0%
;
min-height
:
0
!important
;
min-height
:
100px
;
max-height
:
2000px
!important
;
opacity
:
1
;
opacity
:
1
!important
;
overflow
:
hidden
;
overflow
:
hidden
;
/* 展开过渡:弹性大小和高度立即变化,不透明度延迟 0.15s 淡入 */
transition
:
transition
:
flex
0.
4s
cubic-bezier
(
0.25
,
0.8
,
0.25
,
1
)
0s
,
flex
0.
35s
cubic-bezier
(
0.25
,
0.8
,
0.25
,
1
)
,
m
ax-height
0.4s
cubic-bezier
(
0.25
,
0.8
,
0.25
,
1
)
0s
,
m
in-height
0.35s
cubic-bezier
(
0.25
,
0.8
,
0.25
,
1
)
,
opacity
0.2
s
linear
0.15s
!important
;
opacity
0.2
5s
linear
;
}
}
/* 当渲染区收起时:
不透明度立即淡出,弹性大小延迟 0.08s 收折
*/
/* 当渲染区收起时:
纯靠 flex 平滑收缩到 0% + opacity 淡出
*/
.compare-render-zone.is-collapsed
{
.compare-render-zone.is-collapsed
{
flex
:
0
0
0%
!important
;
flex
:
0
0
0%
!important
;
m
ax-height
:
0
!important
;
m
in-height
:
0px
!important
;
opacity
:
0
!important
;
opacity
:
0
!important
;
pointer-events
:
none
;
pointer-events
:
none
;
transition
:
transition
:
opacity
0.15s
linear
0s
,
opacity
0.2s
linear
,
flex
0.4s
cubic-bezier
(
0.25
,
0.8
,
0.25
,
1
)
0.08s
,
flex
0.35s
cubic-bezier
(
0.25
,
0.8
,
0.25
,
1
),
max-height
0.4s
cubic-bezier
(
0.25
,
0.8
,
0.25
,
1
)
0.08s
!important
;
min-height
0.35s
cubic-bezier
(
0.25
,
0.8
,
0.25
,
1
)
!important
;
}
/* ── 上下拖拽分割条(完全对齐主框架 Splitter 组件视觉风格) ── */
.compare-split-divider
{
transition
:
background-color
0.2s
;
&:hover,
&.is-dragging
{
background-color
:
color-mix
(
in
srgb
,
var
(
--primary-color
,
#165dff
)
10%
,
transparent
);
}
}
.compare-divider-line
{
position
:
absolute
;
left
:
0
;
right
:
0
;
top
:
50%
;
height
:
1px
;
transform
:
translateY
(
-50%
);
background-color
:
var
(
--divider-color
,
rgba
(
0
,
0
,
0
,
0.08
));
transition
:
background-color
0.2s
,
height
0.2s
;
}
.compare-split-divider
:hover
.compare-divider-line
,
.compare-split-divider.is-dragging
.compare-divider-line
{
background-color
:
var
(
--primary-color
,
#165dff
);
height
:
2px
;
}
.compare-divider-handle
{
position
:
relative
;
z-index
:
1
;
display
:
flex
;
flex-direction
:
row
;
align-items
:
center
;
gap
:
3px
;
padding
:
3px
6px
;
border-radius
:
8px
;
background
:
var
(
--colorFill2
,
rgba
(
0
,
0
,
0
,
0.04
));
border
:
1px
solid
var
(
--divider-color
,
rgba
(
0
,
0
,
0
,
0.08
));
opacity
:
0
;
transform
:
scaleX
(
0.8
);
transition
:
opacity
0.2s
,
transform
0.2s
,
background
0.2s
;
}
.compare-split-divider
:hover
.compare-divider-handle
,
.compare-split-divider.is-dragging
.compare-divider-handle
{
opacity
:
1
;
transform
:
scaleX
(
1
);
background
:
var
(
--primary-color
,
#165dff
);
border-color
:
var
(
--primary-color
,
#165dff
);
}
.handle-dot
{
width
:
4px
;
height
:
4px
;
border-radius
:
50%
;
background-color
:
var
(
--textColor3
,
rgba
(
0
,
0
,
0
,
0.25
));
transition
:
background-color
0.2s
;
}
.compare-split-divider
:hover
.handle-dot
,
.compare-split-divider.is-dragging
.handle-dot
{
background-color
:
white
;
}
/* ── 加载遮罩:纯 GPU 加速动画,无 backdrop-filter ── */
.compare-loading-overlay
{
background-color
:
color-mix
(
in
srgb
,
var
(
--card-color
,
#fff
)
92%
,
transparent
);
}
.compare-spinner
{
width
:
36px
;
height
:
36px
;
border-radius
:
50%
;
border
:
2.5px
solid
color-mix
(
in
srgb
,
var
(
--primary-color
,
#7c5cfc
)
20%
,
transparent
);
border-top-color
:
var
(
--primary-color
,
#7c5cfc
);
will-change
:
transform
;
animation
:
compare-spin
0.9s
linear
infinite
;
}
.compare-loading-text
{
font-size
:
12px
;
font-weight
:
500
;
}
@keyframes
compare-spin
{
to
{
transform
:
rotate
(
360deg
);
}
}
}
</
style
>
</
style
>
src/views/editor/components/TableEditor/index.vue
View file @
5af0d0fb
...
@@ -78,6 +78,8 @@
...
@@ -78,6 +78,8 @@
:rowspan=
"cell.rowspan"
:rowspan=
"cell.rowspan"
:data-node-id=
"cell.id"
:data-node-id=
"cell.id"
data-tag-name=
"ENTRY"
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=
"p-2 text-left font-bold bg-fill-4 border border-divider text-color1 transition-colors relative"
:class=
"[
:class=
"[
isDiffMode ? 'cursor-default' : 'cursor-pointer',
isDiffMode ? 'cursor-default' : 'cursor-pointer',
...
@@ -91,6 +93,29 @@
...
@@ -91,6 +93,29 @@
@
click
.
stop=
"isDiffMode ? null : handleCellClick(cell, $event)"
@
click
.
stop=
"isDiffMode ? null : handleCellClick(cell, $event)"
@
contextmenu
.
prevent=
"isDiffMode ? null : handleCellContextMenu(cell, row.id, $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>
🏷️
<
{{ cell.rawNode.tagName }}
>
属性变更明细
</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"
>
<
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"
/>
<DocNodeRenderer
v-for=
"child in cell.rawNode!.children"
:key=
"child.id"
:node=
"child"
:parent=
"cell.rawNode"
/>
</
template
>
</
template
>
...
@@ -190,6 +215,8 @@
...
@@ -190,6 +215,8 @@
:rowspan=
"cell.rowspan"
:rowspan=
"cell.rowspan"
:data-node-id=
"cell.id"
:data-node-id=
"cell.id"
data-tag-name=
"ENTRY"
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=
"p-2 border border-divider text-color2 transition-colors relative"
:class=
"[
:class=
"[
isDiffMode ? 'cursor-default' : 'cursor-pointer',
isDiffMode ? 'cursor-default' : 'cursor-pointer',
...
@@ -203,6 +230,29 @@
...
@@ -203,6 +230,29 @@
@
click
.
stop=
"isDiffMode ? null : handleCellClick(cell, $event)"
@
click
.
stop=
"isDiffMode ? null : handleCellClick(cell, $event)"
@
contextmenu
.
prevent=
"isDiffMode ? null : handleCellContextMenu(cell, row.id, $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>
🏷️
<
{{ cell.rawNode.tagName }}
>
属性变更明细
</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"
>
<
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"
/>
<DocNodeRenderer
v-for=
"child in cell.rawNode!.children"
:key=
"child.id"
:node=
"child"
:parent=
"cell.rawNode"
/>
</
template
>
</
template
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment