Commit 233926ec by pangchong

feat: 节点操作新增顺序调整

parent 158ac826
......@@ -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
}
}
......@@ -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') {
......
......@@ -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
}
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment