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
233926ec
Commit
233926ec
authored
Jul 30, 2026
by
pangchong
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 节点操作新增顺序调整
parent
158ac826
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
471 additions
and
41 deletions
+471
-41
src/utils/dtdManager.ts
+255
-1
src/views/editor/components/EditorPanel/components/EditAreaContextMenuModal/functionals/index.ts
+83
-0
src/views/editor/components/EditorPanel/components/EditAreaContextMenuModal/index.vue
+15
-1
src/views/editor/components/NodeTree/functionals/index.ts
+118
-39
No files found.
src/utils/dtdManager.ts
View file @
233926ec
import
type
{
DtdSchema
,
DtdElement
,
DtdAttribute
,
DtdContentModel
}
from
'@/types/xmlNode'
import
type
{
DtdSchema
,
DtdElement
,
DtdAttribute
,
DtdContentModel
,
XmlNode
}
from
'@/types/xmlNode'
import
{
VIRTUAL_LAYOUT_TAGS
}
from
'@/configs/xmlTags'
/**
* DTD 规则管理器
...
...
@@ -322,6 +323,135 @@ export function getDtdDefinedOrder(tagName: string): string[] {
}
/**
* 从 DTD 内容模型中提取 sequence 顺序约束(内部辅助函数)。
*
* 规则:
* - 只对同一 sequence 组中处于不同位置(i < j)的元素对生成约束
* - choice 组内部的兄弟元素之间不产生约束(可任意顺序、交叉出现)
* - 递归处理嵌套结构(choice 内部也可能有嵌套 sequence)
*
* 返回值 [(a, b)] 表示"在该父节点下,a 必须整体早于 b 出现"
*/
function
buildSequenceConstraints
(
model
:
DtdContentModel
):
Array
<
[
string
,
string
]
>
{
const
constraints
:
Array
<
[
string
,
string
]
>
=
[]
/** 递归收集一个子模型中所有可能出现的元素名(去重) */
const
getLeafNames
=
(
m
:
DtdContentModel
):
string
[]
=>
{
const
names
:
string
[]
=
[]
const
collect
=
(
n
:
DtdContentModel
)
=>
{
if
(
n
.
type
===
'elementRef'
&&
n
.
name
&&
!
names
.
includes
(
n
.
name
))
{
names
.
push
(
n
.
name
)
}
if
(
n
.
children
)
n
.
children
.
forEach
(
collect
)
}
collect
(
m
)
return
names
}
const
process
=
(
m
:
DtdContentModel
)
=>
{
if
(
m
.
type
===
'sequence'
&&
m
.
children
&&
m
.
children
.
length
>
1
)
{
// 同一 sequence 中:位置 i 的元素集合必须早于位置 j (j>i) 的元素集合
for
(
let
i
=
0
;
i
<
m
.
children
.
length
;
i
++
)
{
const
before
=
getLeafNames
(
m
.
children
[
i
])
for
(
let
j
=
i
+
1
;
j
<
m
.
children
.
length
;
j
++
)
{
const
after
=
getLeafNames
(
m
.
children
[
j
])
for
(
const
a
of
before
)
{
for
(
const
b
of
after
)
{
if
(
a
!==
b
&&
!
constraints
.
some
(([
x
,
y
])
=>
x
===
a
&&
y
===
b
))
{
constraints
.
push
([
a
,
b
])
}
}
}
}
}
}
// 递归处理子节点(包括 choice 内部可能存在的嵌套 sequence)
if
(
m
.
children
)
m
.
children
.
forEach
(
process
)
}
process
(
model
)
return
constraints
}
/**
* 判断某个子节点在父节点中能否上移 / 下移(DTD 顺序约束校验)
*
* 核心思路:
* 1. 通过 buildSequenceConstraints 提取 DTD sequence 约束对 [(a→b)]
* - choice 组内部元素(如 PARA、PARAC、WARNING)之间无约束,可自由移动
* - sequence 不同位置间的元素有严格顺序约束
* 2. 模拟交换后,对每个约束 (a→b) 验证:
* "a 的最后一次出现" 不得晚于 "b 的第一次出现"
* → 违反则拒绝移动
*
* 旧实现用 getDtdDefinedOrder + 位置单调检查,对 choice 组误判(
* 例如 [PARAC, PARA, PARAC] 被错误标记为无效,导致两个方向全灰)。
*
* @param parentTagName 父节点标签名
* @param childTags 当前所有子节点的标签名列表(按显示顺序)
* @param childIndex 要移动的子节点在 childTags 中的索引
* @returns { canMoveUp, canMoveDown }
*/
export
function
canMoveNode
(
parentTagName
:
string
,
childTags
:
string
[],
childIndex
:
number
):
{
canMoveUp
:
boolean
;
canMoveDown
:
boolean
}
{
const
atTop
=
childIndex
<=
0
const
atBottom
=
childIndex
>=
childTags
.
length
-
1
const
rule
=
getElementRule
(
parentTagName
)
// 没有 DTD 规则:仅做边界判断
if
(
!
rule
||
!
rule
.
contentModel
||
!
rule
.
contentModel
.
parsed
)
{
return
{
canMoveUp
:
!
atTop
,
canMoveDown
:
!
atBottom
}
}
const
model
=
rule
.
contentModel
.
parsed
// 混合内容 / 纯文本:DTD 不约束子节点顺序
if
(
model
.
type
===
'mixed'
||
model
.
type
===
'pcdata'
)
{
return
{
canMoveUp
:
!
atTop
,
canMoveDown
:
!
atBottom
}
}
// 提取 sequence 顺序约束(choice 组内无约束)
const
constraints
=
buildSequenceConstraints
(
model
)
// 没有顺序约束(如纯 choice* 模型):仅做边界判断
if
(
constraints
.
length
===
0
)
{
return
{
canMoveUp
:
!
atTop
,
canMoveDown
:
!
atBottom
}
}
/**
* 验证给定标签列表是否满足所有顺序约束。
* 对约束 (a→b):"a 的最后出现索引" 必须 ≤ "b 的最早出现索引"。
* 即:不允许存在任何 b 出现在 a 之前的情况。
*/
const
isOrderValid
=
(
tags
:
string
[]):
boolean
=>
{
for
(
const
[
a
,
b
]
of
constraints
)
{
const
lastA
=
tags
.
lastIndexOf
(
a
)
const
firstB
=
tags
.
indexOf
(
b
)
if
(
lastA
!==
-
1
&&
firstB
!==
-
1
&&
lastA
>
firstB
)
{
return
false
}
}
return
true
}
const
swapCheck
=
(
i
:
number
,
j
:
number
):
boolean
=>
{
const
copy
=
[...
childTags
]
;[
copy
[
i
],
copy
[
j
]]
=
[
copy
[
j
],
copy
[
i
]]
return
isOrderValid
(
copy
)
}
return
{
canMoveUp
:
!
atTop
&&
swapCheck
(
childIndex
,
childIndex
-
1
),
canMoveDown
:
!
atBottom
&&
swapCheck
(
childIndex
,
childIndex
+
1
)
}
}
/**
* 根据父节点 DTD 定义的子节点顺序进行排序
*/
export
function
sortChildrenByDtd
(
parentTagName
:
string
,
childTags
:
string
[]):
string
[]
{
...
...
@@ -336,3 +466,127 @@ export function sortChildrenByDtd(parentTagName: string, childTags: string[]): s
return
idxA
-
idxB
})
}
/**
* 综合判断某个节点在父节点中能否上移 / 下移
* 优先处理 mixedContent 混合内容,其次处理纯元素子节点 DTD sequence 约束
*/
export
function
checkCanMoveNode
(
parent
:
XmlNode
|
null
|
undefined
,
nodeId
:
string
,
isVirtual
:
boolean
=
false
):
{
canMoveUp
:
boolean
;
canMoveDown
:
boolean
}
{
if
(
!
parent
)
return
{
canMoveUp
:
false
,
canMoveDown
:
false
}
// 1. 如果父节点包含 mixedContent 且内容不为空,按 mixedContent 排布列表判断
if
(
parent
.
mixedContent
&&
parent
.
mixedContent
.
length
>
0
)
{
let
mcIdx
=
-
1
if
(
isVirtual
||
nodeId
.
includes
(
'-txt-'
))
{
mcIdx
=
parseInt
(
nodeId
.
split
(
'-txt-'
)[
1
],
10
)
}
else
{
mcIdx
=
parent
.
mixedContent
.
findIndex
((
m
)
=>
m
.
nodeId
===
nodeId
)
}
if
(
mcIdx
!==
-
1
)
{
return
{
canMoveUp
:
mcIdx
>
0
,
canMoveDown
:
mcIdx
<
parent
.
mixedContent
.
length
-
1
}
}
}
// 2. 虚拟文本节点但在 mixedContent 未找到,不允许移动
if
(
isVirtual
||
nodeId
.
includes
(
'-txt-'
))
{
return
{
canMoveUp
:
false
,
canMoveDown
:
false
}
}
// 3. 规范结构元素:使用过滤后的真实 children,结合 DTD 进行 sequence 约束校验
if
(
VIRTUAL_LAYOUT_TAGS
.
includes
(
parent
.
tagName
))
{
return
{
canMoveUp
:
false
,
canMoveDown
:
false
}
}
const
realSiblings
=
parent
.
children
?
parent
.
children
.
filter
((
c
)
=>
!
VIRTUAL_LAYOUT_TAGS
.
includes
(
c
.
tagName
))
:
[]
const
realIdx
=
realSiblings
.
findIndex
((
c
)
=>
c
.
id
===
nodeId
)
if
(
realIdx
===
-
1
)
{
return
{
canMoveUp
:
false
,
canMoveDown
:
false
}
}
return
canMoveNode
(
parent
.
tagName
,
realSiblings
.
map
((
c
)
=>
c
.
tagName
),
realIdx
)
}
/**
* 通用节点移动执行函数(支持 mixedContent 数组与结构化 children 数组同步)
*/
export
function
moveNodeInParent
(
parent
:
XmlNode
|
null
|
undefined
,
nodeId
:
string
,
direction
:
'up'
|
'down'
,
isVirtual
:
boolean
=
false
):
boolean
{
if
(
!
parent
)
return
false
// 1. 如果父节点存在 mixedContent,优先在 mixedContent 中执行交换
if
(
parent
.
mixedContent
&&
parent
.
mixedContent
.
length
>
0
)
{
let
mcIdx
=
-
1
if
(
isVirtual
||
nodeId
.
includes
(
'-txt-'
))
{
mcIdx
=
parseInt
(
nodeId
.
split
(
'-txt-'
)[
1
],
10
)
}
else
{
mcIdx
=
parent
.
mixedContent
.
findIndex
((
m
)
=>
m
.
nodeId
===
nodeId
)
}
if
(
mcIdx
!==
-
1
)
{
const
targetIdx
=
direction
===
'up'
?
mcIdx
-
1
:
mcIdx
+
1
if
(
targetIdx
<
0
||
targetIdx
>=
parent
.
mixedContent
.
length
)
{
return
false
}
// 交换 mixedContent
const
mc
=
parent
.
mixedContent
;[
mc
[
mcIdx
],
mc
[
targetIdx
]]
=
[
mc
[
targetIdx
],
mc
[
mcIdx
]]
// 同步刷新 parent.children 顺序,与 mixedContent 中出现元素的先后顺序一致
if
(
parent
.
children
&&
parent
.
children
.
length
>
0
)
{
const
newChildrenOrder
:
XmlNode
[]
=
[]
for
(
const
item
of
mc
)
{
if
(
item
.
type
===
'element'
&&
item
.
nodeId
)
{
const
childNode
=
parent
.
children
.
find
((
c
)
=>
c
.
id
===
item
.
nodeId
)
if
(
childNode
&&
!
newChildrenOrder
.
some
((
c
)
=>
c
.
id
===
childNode
.
id
))
{
newChildrenOrder
.
push
(
childNode
)
}
}
}
for
(
const
childNode
of
parent
.
children
)
{
if
(
!
newChildrenOrder
.
some
((
c
)
=>
c
.
id
===
childNode
.
id
))
{
newChildrenOrder
.
push
(
childNode
)
}
}
parent
.
children
=
newChildrenOrder
}
return
true
}
}
// 2. 规范结构元素:在 children 中按物理索引做相邻交换
if
(
!
parent
.
children
)
return
false
const
allChildren
=
parent
.
children
const
realSiblings
=
allChildren
.
filter
((
c
)
=>
!
VIRTUAL_LAYOUT_TAGS
.
includes
(
c
.
tagName
))
const
realIdx
=
realSiblings
.
findIndex
((
c
)
=>
c
.
id
===
nodeId
)
if
(
realIdx
===
-
1
)
return
false
const
targetRealIdx
=
direction
===
'up'
?
realIdx
-
1
:
realIdx
+
1
if
(
targetRealIdx
<
0
||
targetRealIdx
>=
realSiblings
.
length
)
return
false
const
currentReal
=
realSiblings
[
realIdx
]
const
targetReal
=
realSiblings
[
targetRealIdx
]
const
fullIdx1
=
allChildren
.
findIndex
((
c
)
=>
c
.
id
===
currentReal
.
id
)
const
fullIdx2
=
allChildren
.
findIndex
((
c
)
=>
c
.
id
===
targetReal
.
id
)
if
(
fullIdx1
!==
-
1
&&
fullIdx2
!==
-
1
)
{
;[
allChildren
[
fullIdx1
],
allChildren
[
fullIdx2
]]
=
[
allChildren
[
fullIdx2
],
allChildren
[
fullIdx1
]]
return
true
}
return
false
}
src/views/editor/components/EditorPanel/components/EditAreaContextMenuModal/functionals/index.ts
View file @
233926ec
...
...
@@ -269,6 +269,31 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
return
canDeleteChild
(
mapped
.
parent
.
tagName
,
mapped
.
node
.
tagName
,
existingTags
)
})
// 是否允许上移(DTD 顺序约束)
const
canMoveUpActive
=
computed
(()
=>
{
const
nodeId
=
activeNodeId
.
value
if
(
!
nodeId
)
return
false
const
isVirtual
=
nodeId
.
includes
(
'-txt-'
)
const
realId
=
isVirtual
?
nodeId
.
split
(
'-txt-'
)[
0
]
:
nodeId
const
mapped
=
editorStore
.
nodeMap
.
get
(
realId
)
if
(
!
mapped
||
!
mapped
.
parent
)
return
false
const
parent
=
isVirtual
?
mapped
.
node
:
mapped
.
parent
return
checkCanMoveNode
(
parent
,
nodeId
,
isVirtual
).
canMoveUp
})
// 是否允许下移(DTD 顺序约束)
const
canMoveDownActive
=
computed
(()
=>
{
const
nodeId
=
activeNodeId
.
value
if
(
!
nodeId
)
return
false
const
isVirtual
=
nodeId
.
includes
(
'-txt-'
)
const
realId
=
isVirtual
?
nodeId
.
split
(
'-txt-'
)[
0
]
:
nodeId
const
mapped
=
editorStore
.
nodeMap
.
get
(
realId
)
if
(
!
mapped
||
!
mapped
.
parent
)
return
false
const
parent
=
isVirtual
?
mapped
.
node
:
mapped
.
parent
return
checkCanMoveNode
(
parent
,
nodeId
,
isVirtual
).
canMoveDown
})
// 根据 DTD 获取允许添加的子标签列表
const
allowedChildrenList
=
computed
<
string
[]
>
(()
=>
{
const
node
=
activeNode
.
value
...
...
@@ -509,6 +534,56 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
addNodeAllowedTags
.
value
=
insertableParentList
.
value
addNodeVisible
.
value
=
true
}
// 11. 上移节点
else
if
(
actionName
===
'moveUp'
)
{
const
mapped2
=
editorStore
.
nodeMap
.
get
(
realId
)
if
(
!
mapped2
||
!
mapped2
.
parent
)
return
const
parent
=
isVirtual
?
mapped2
.
node
:
mapped2
.
parent
const
nodeIdToMove
=
activeNodeId
.
value
||
realId
const
{
canMoveUp
}
=
checkCanMoveNode
(
parent
,
nodeIdToMove
,
isVirtual
)
if
(
!
canMoveUp
)
{
window
.
$message
.
warning
(
'已在最顶部或 DTD 顺序不支持上移'
)
return
}
editorStore
.
saveSnapshot
()
const
success
=
moveNodeInParent
(
parent
,
nodeIdToMove
,
'up'
,
isVirtual
)
if
(
success
)
{
editorStore
.
rebuildNodeMap
()
if
(
!
isVirtual
)
{
editorStore
.
setSelectedNodeId
(
realId
)
}
window
.
$message
.
success
(
'上移成功'
)
}
else
{
window
.
$message
.
warning
(
'上移失败'
)
}
}
// 12. 下移节点
else
if
(
actionName
===
'moveDown'
)
{
const
mapped2
=
editorStore
.
nodeMap
.
get
(
realId
)
if
(
!
mapped2
||
!
mapped2
.
parent
)
return
const
parent
=
isVirtual
?
mapped2
.
node
:
mapped2
.
parent
const
nodeIdToMove
=
activeNodeId
.
value
||
realId
const
{
canMoveDown
}
=
checkCanMoveNode
(
parent
,
nodeIdToMove
,
isVirtual
)
if
(
!
canMoveDown
)
{
window
.
$message
.
warning
(
'已在最底部或 DTD 顺序不支持下移'
)
return
}
editorStore
.
saveSnapshot
()
const
success
=
moveNodeInParent
(
parent
,
nodeIdToMove
,
'down'
,
isVirtual
)
if
(
success
)
{
editorStore
.
rebuildNodeMap
()
if
(
!
isVirtual
)
{
editorStore
.
setSelectedNodeId
(
realId
)
}
window
.
$message
.
success
(
'下移成功'
)
}
else
{
window
.
$message
.
warning
(
'下移失败'
)
}
}
}
const
pasteXmlOptions
=
computed
(()
=>
[
...
...
@@ -523,6 +598,11 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
{
label
:
'插入到下方'
,
key
:
'insertAfter'
,
disabled
:
!
insertableParentList
.
value
.
length
}
])
const
editOrderOptions
=
computed
(()
=>
[
{
label
:
'上移'
,
key
:
'moveUp'
,
disabled
:
!
canMoveUpActive
.
value
},
{
label
:
'下移'
,
key
:
'moveDown'
,
disabled
:
!
canMoveDownActive
.
value
}
])
const
pasteNodeOptions
=
computed
(()
=>
[
{
label
:
'粘贴到上方'
,
key
:
'pasteNodeAbove'
,
disabled
:
!
insertableParentList
.
value
.
length
},
{
label
:
'粘贴到下方'
,
key
:
'pasteNodeBelow'
,
disabled
:
!
insertableParentList
.
value
.
length
},
...
...
@@ -538,6 +618,8 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
isVirtualLayoutNode
,
canTranslateActive
,
canDeleteActive
,
canMoveUpActive
,
canMoveDownActive
,
allowedChildrenList
,
insertableParentList
,
hasCopyCache
,
...
...
@@ -545,6 +627,7 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
runAction
,
pasteXmlOptions
,
editStructureOptions
,
editOrderOptions
,
pasteNodeOptions
}
}
src/views/editor/components/EditorPanel/components/EditAreaContextMenuModal/index.vue
View file @
233926ec
...
...
@@ -74,7 +74,7 @@
</n-dropdown>
</div>
<div
class=
"grid grid-cols-
3
gap-2"
>
<div
class=
"grid grid-cols-
4
gap-2"
>
<!-- 4. 删除节点 -->
<CommonButton
type=
"error"
secondary
block
:disabled=
"!canDeleteActive"
@
click=
"runAction('deleteNode')"
>
<
template
#
icon
>
...
...
@@ -93,6 +93,16 @@
</CommonButton>
</n-dropdown>
<!-- 5b. 编辑顺序 -->
<n-dropdown
trigger=
"click"
:options=
"editOrderOptions"
:disabled=
"isTextNode || (!canMoveUpActive && !canMoveDownActive)"
@
select=
"runAction"
>
<CommonButton
type=
"warning"
secondary
block
:disabled=
"isTextNode || (!canMoveUpActive && !canMoveDownActive)"
>
<
template
#
icon
>
<n-icon><swap-vertical-outline
/></n-icon>
</
template
>
编辑顺序
</CommonButton>
</n-dropdown>
<!-- 6. 粘贴节点 (DTD) -->
<n-dropdown
trigger=
"click"
:options=
"pasteNodeOptions"
:disabled=
"!hasCopyCache"
@
select=
"runAction"
>
<CommonButton
type=
"warning"
secondary
block
:disabled=
"!hasCopyCache"
>
...
...
@@ -160,6 +170,7 @@ import {
ClipboardOutline
,
TrashOutline
,
BuildOutline
,
SwapVerticalOutline
,
LanguageOutline
,
CodeWorkingOutline
,
EyeOutline
,
...
...
@@ -187,11 +198,14 @@ const {
isVirtualLayoutNode
,
canTranslateActive
,
canDeleteActive
,
canMoveUpActive
,
canMoveDownActive
,
hasCopyCache
,
isTranslating
,
runAction
,
pasteXmlOptions
,
editStructureOptions
,
editOrderOptions
,
pasteNodeOptions
}
=
useEditAreaContextMenu
(
props
,
(
event
,
val
)
=>
{
if
(
event
===
'update:visible'
)
{
...
...
src/views/editor/components/NodeTree/functionals/index.ts
View file @
233926ec
...
...
@@ -874,14 +874,15 @@ export function useNodeTree(
return
options
}
// 查看XML
// ================= 组 1:查看与辅助 =================
// 1. 查看XML
options
.
push
({
label
:
'查看XML'
,
key
:
'viewXml'
,
icon
:
icon
(
CodeWorkingOutline
)
})
// 查看规则(虚拟文本节点不需要)
//
2.
查看规则(虚拟文本节点不需要)
if
(
!
isVirtual
)
{
options
.
push
({
label
:
'查看规则'
,
...
...
@@ -890,14 +891,7 @@ export function useNodeTree(
})
}
// 编辑节点(属性/内容)
options
.
push
({
label
:
isVirtual
?
'编辑文本'
:
'编辑节点'
,
key
:
'editNode'
,
icon
:
icon
(
CreateOutline
)
})
// 智能翻译
// 3. 智能翻译
const
enSourceNode
=
getEnglishSourceNode
(
node
,
parent
)
if
(
enSourceNode
&&
!
isVirtual
&&
hasTranslatableText
(
enSourceNode
))
{
options
.
push
({
...
...
@@ -907,21 +901,17 @@ export function useNodeTree(
})
}
// 复制节点
// ================= 组 2:复制 / 粘贴 / 暂存 =================
options
.
push
({
type
:
'divider'
,
key
:
'd1'
})
// 4. 复制节点
options
.
push
({
label
:
'复制节点'
,
key
:
'copyNode'
,
icon
:
icon
(
CopyOutline
)
})
// 本地暂存
options
.
push
({
label
:
'本地暂存'
,
key
:
'localStash'
,
icon
:
icon
(
ArchiveOutline
)
})
// 粘贴节点(有缓存时显示)
// 5. 粘贴节点(有缓存时显示)
if
(
copyNodeCache
.
value
)
{
const
pasteChildren
:
DropdownOption
[]
=
isVirtual
?
[
...
...
@@ -941,7 +931,7 @@ export function useNodeTree(
})
}
// 粘贴XML
//
6.
粘贴XML
const
pasteFragmentChildren
:
DropdownOption
[]
=
isVirtual
?
[
{
label
:
'粘贴到上方'
,
key
:
'pasteFragmentAbove'
,
icon
:
icon
(
ClipboardOutline
),
disabled
:
!
parent
},
...
...
@@ -959,28 +949,24 @@ export function useNodeTree(
children
:
pasteFragmentChildren
})
// 删除节点(受 DTD 约束,文本节点和分页符节点永远允许删除)
if
(
parent
)
{
const
deletable
=
isVirtual
||
VIRTUAL_LAYOUT_TAGS
.
includes
(
node
.
tagName
)
?
true
:
(()
=>
{
const
existingTags
=
parent
.
children
.
filter
((
c
)
=>
!
VIRTUAL_LAYOUT_TAGS
.
includes
(
c
.
tagName
)).
map
((
c
)
=>
c
.
tagName
)
// 位置特许规则:父级中最后一个子节点(按位置)允许删除
if
(
existingTags
.
length
>
0
&&
existingTags
[
existingTags
.
length
-
1
]
===
node
.
tagName
)
{
return
true
}
return
canDeleteChild
(
parent
.
tagName
,
node
.
tagName
,
existingTags
)
})()
// 7. 本地暂存
options
.
push
({
label
:
'删除节点'
,
key
:
'deleteNode'
,
icon
:
icon
(
TrashOutline
),
disabled
:
!
deletable
label
:
'本地暂存'
,
key
:
'localStash'
,
icon
:
icon
(
ArchiveOutline
)
})
// ================= 组 3:节点编辑与结构调整 =================
options
.
push
({
type
:
'divider'
,
key
:
'd2'
})
// 8. 编辑节点(属性/内容)
options
.
push
({
label
:
isVirtual
?
'编辑文本'
:
'编辑节点'
,
key
:
'editNode'
,
icon
:
icon
(
CreateOutline
)
})
}
//
编辑结构子菜单
//
9. 编辑结构
const
structureChildren
:
DropdownOption
[]
=
[]
// 添加子节点(文本节点不能拥有子节点)
...
...
@@ -1024,6 +1010,33 @@ export function useNodeTree(
})
}
// 10. 编辑顺序(上移 / 下移)
if
(
parent
)
{
const
{
canMoveUp
,
canMoveDown
}
=
checkCanMoveNode
(
parent
,
nodeId
,
isVirtual
)
options
.
push
({
label
:
'编辑顺序'
,
key
:
'editOrder'
,
icon
:
icon
(
ArrowUpOutline
),
children
:
[
{
label
:
'上移'
,
key
:
'moveUp'
,
icon
:
icon
(
ArrowUpOutline
),
disabled
:
!
canMoveUp
},
{
label
:
'下移'
,
key
:
'moveDown'
,
icon
:
icon
(
ArrowDownOutline
),
disabled
:
!
canMoveDown
}
]
})
}
// ================= 组 4:更多与删除 =================
options
.
push
({
type
:
'divider'
,
key
:
'd3'
})
// 保存为模板
options
.
push
({
label
:
'保存为模板'
,
...
...
@@ -1031,6 +1044,27 @@ export function useNodeTree(
icon
:
icon
(
SaveOutline
)
})
// 删除节点(受 DTD 约束,文本节点和分页符节点永远允许删除)
if
(
parent
)
{
const
deletable
=
isVirtual
||
VIRTUAL_LAYOUT_TAGS
.
includes
(
node
.
tagName
)
?
true
:
(()
=>
{
const
existingTags
=
parent
.
children
.
filter
((
c
)
=>
!
VIRTUAL_LAYOUT_TAGS
.
includes
(
c
.
tagName
)).
map
((
c
)
=>
c
.
tagName
)
// 位置特许规则:父级中最后一个子节点(按位置)允许删除
if
(
existingTags
.
length
>
0
&&
existingTags
[
existingTags
.
length
-
1
]
===
node
.
tagName
)
{
return
true
}
return
canDeleteChild
(
parent
.
tagName
,
node
.
tagName
,
existingTags
)
})()
options
.
push
({
label
:
'删除节点'
,
key
:
'deleteNode'
,
icon
:
icon
(
TrashOutline
),
disabled
:
!
deletable
})
}
return
options
}
...
...
@@ -1406,6 +1440,51 @@ export function useNodeTree(
}
break
}
// ── 上移节点 ──
case
'moveUp'
:
{
if
(
!
parent
)
break
const
{
canMoveUp
}
=
checkCanMoveNode
(
parent
,
nodeId
,
isVirtual
)
if
(
!
canMoveUp
)
{
window
.
$message
?.
warning
(
'已在最顶部或 DTD 顺序不支持上移'
)
break
}
editorStore
.
saveSnapshot
()
const
success
=
moveNodeInParent
(
parent
,
nodeId
,
'up'
,
isVirtual
)
if
(
success
)
{
editorStore
.
rebuildNodeMap
()
if
(
!
isVirtual
)
{
editorStore
.
setSelectedNodeId
(
realNodeId
)
}
window
.
$message
?.
success
(
'上移成功'
)
}
else
{
window
.
$message
?.
warning
(
'上移失败'
)
}
break
}
// ── 下移节点 ──
case
'moveDown'
:
{
if
(
!
parent
)
break
const
{
canMoveDown
}
=
checkCanMoveNode
(
parent
,
nodeId
,
isVirtual
)
if
(
!
canMoveDown
)
{
window
.
$message
?.
warning
(
'已在最底部或 DTD 顺序不支持下移'
)
break
}
editorStore
.
saveSnapshot
()
const
success
=
moveNodeInParent
(
parent
,
nodeId
,
'down'
,
isVirtual
)
if
(
success
)
{
editorStore
.
rebuildNodeMap
()
if
(
!
isVirtual
)
{
editorStore
.
setSelectedNodeId
(
realNodeId
)
}
window
.
$message
?.
success
(
'下移成功'
)
}
else
{
window
.
$message
?.
warning
(
'下移失败'
)
}
break
}
}
}
...
...
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