Commit 233926ec by pangchong

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

parent 158ac826
...@@ -269,6 +269,31 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e ...@@ -269,6 +269,31 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
return canDeleteChild(mapped.parent.tagName, mapped.node.tagName, existingTags) 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 获取允许添加的子标签列表 // 根据 DTD 获取允许添加的子标签列表
const allowedChildrenList = computed<string[]>(() => { const allowedChildrenList = computed<string[]>(() => {
const node = activeNode.value const node = activeNode.value
...@@ -509,6 +534,56 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e ...@@ -509,6 +534,56 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
addNodeAllowedTags.value = insertableParentList.value addNodeAllowedTags.value = insertableParentList.value
addNodeVisible.value = true 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(() => [ const pasteXmlOptions = computed(() => [
...@@ -523,6 +598,11 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e ...@@ -523,6 +598,11 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
{ label: '插入到下方', key: 'insertAfter', disabled: !insertableParentList.value.length } { 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(() => [ const pasteNodeOptions = computed(() => [
{ label: '粘贴到上方', key: 'pasteNodeAbove', disabled: !insertableParentList.value.length }, { label: '粘贴到上方', key: 'pasteNodeAbove', disabled: !insertableParentList.value.length },
{ label: '粘贴到下方', key: 'pasteNodeBelow', disabled: !insertableParentList.value.length }, { label: '粘贴到下方', key: 'pasteNodeBelow', disabled: !insertableParentList.value.length },
...@@ -538,6 +618,8 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e ...@@ -538,6 +618,8 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
isVirtualLayoutNode, isVirtualLayoutNode,
canTranslateActive, canTranslateActive,
canDeleteActive, canDeleteActive,
canMoveUpActive,
canMoveDownActive,
allowedChildrenList, allowedChildrenList,
insertableParentList, insertableParentList,
hasCopyCache, hasCopyCache,
...@@ -545,6 +627,7 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e ...@@ -545,6 +627,7 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
runAction, runAction,
pasteXmlOptions, pasteXmlOptions,
editStructureOptions, editStructureOptions,
editOrderOptions,
pasteNodeOptions pasteNodeOptions
} }
} }
...@@ -74,7 +74,7 @@ ...@@ -74,7 +74,7 @@
</n-dropdown> </n-dropdown>
</div> </div>
<div class="grid grid-cols-3 gap-2"> <div class="grid grid-cols-4 gap-2">
<!-- 4. 删除节点 --> <!-- 4. 删除节点 -->
<CommonButton type="error" secondary block :disabled="!canDeleteActive" @click="runAction('deleteNode')"> <CommonButton type="error" secondary block :disabled="!canDeleteActive" @click="runAction('deleteNode')">
<template #icon> <template #icon>
...@@ -93,6 +93,16 @@ ...@@ -93,6 +93,16 @@
</CommonButton> </CommonButton>
</n-dropdown> </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) --> <!-- 6. 粘贴节点 (DTD) -->
<n-dropdown trigger="click" :options="pasteNodeOptions" :disabled="!hasCopyCache" @select="runAction"> <n-dropdown trigger="click" :options="pasteNodeOptions" :disabled="!hasCopyCache" @select="runAction">
<CommonButton type="warning" secondary block :disabled="!hasCopyCache"> <CommonButton type="warning" secondary block :disabled="!hasCopyCache">
...@@ -160,6 +170,7 @@ import { ...@@ -160,6 +170,7 @@ import {
ClipboardOutline, ClipboardOutline,
TrashOutline, TrashOutline,
BuildOutline, BuildOutline,
SwapVerticalOutline,
LanguageOutline, LanguageOutline,
CodeWorkingOutline, CodeWorkingOutline,
EyeOutline, EyeOutline,
...@@ -187,11 +198,14 @@ const { ...@@ -187,11 +198,14 @@ const {
isVirtualLayoutNode, isVirtualLayoutNode,
canTranslateActive, canTranslateActive,
canDeleteActive, canDeleteActive,
canMoveUpActive,
canMoveDownActive,
hasCopyCache, hasCopyCache,
isTranslating, isTranslating,
runAction, runAction,
pasteXmlOptions, pasteXmlOptions,
editStructureOptions, editStructureOptions,
editOrderOptions,
pasteNodeOptions pasteNodeOptions
} = useEditAreaContextMenu(props, (event, val) => { } = useEditAreaContextMenu(props, (event, val) => {
if (event === 'update:visible') { if (event === 'update:visible') {
......
...@@ -874,14 +874,15 @@ export function useNodeTree( ...@@ -874,14 +874,15 @@ export function useNodeTree(
return options return options
} }
// 查看XML // ================= 组 1:查看与辅助 =================
// 1. 查看XML
options.push({ options.push({
label: '查看XML', label: '查看XML',
key: 'viewXml', key: 'viewXml',
icon: icon(CodeWorkingOutline) icon: icon(CodeWorkingOutline)
}) })
// 查看规则(虚拟文本节点不需要) // 2. 查看规则(虚拟文本节点不需要)
if (!isVirtual) { if (!isVirtual) {
options.push({ options.push({
label: '查看规则', label: '查看规则',
...@@ -890,14 +891,7 @@ export function useNodeTree( ...@@ -890,14 +891,7 @@ export function useNodeTree(
}) })
} }
// 编辑节点(属性/内容) // 3. 智能翻译
options.push({
label: isVirtual ? '编辑文本' : '编辑节点',
key: 'editNode',
icon: icon(CreateOutline)
})
// 智能翻译
const enSourceNode = getEnglishSourceNode(node, parent) const enSourceNode = getEnglishSourceNode(node, parent)
if (enSourceNode && !isVirtual && hasTranslatableText(enSourceNode)) { if (enSourceNode && !isVirtual && hasTranslatableText(enSourceNode)) {
options.push({ options.push({
...@@ -907,21 +901,17 @@ export function useNodeTree( ...@@ -907,21 +901,17 @@ export function useNodeTree(
}) })
} }
// 复制节点 // ================= 组 2:复制 / 粘贴 / 暂存 =================
options.push({ type: 'divider', key: 'd1' })
// 4. 复制节点
options.push({ options.push({
label: '复制节点', label: '复制节点',
key: 'copyNode', key: 'copyNode',
icon: icon(CopyOutline) icon: icon(CopyOutline)
}) })
// 本地暂存 // 5. 粘贴节点(有缓存时显示)
options.push({
label: '本地暂存',
key: 'localStash',
icon: icon(ArchiveOutline)
})
// 粘贴节点(有缓存时显示)
if (copyNodeCache.value) { if (copyNodeCache.value) {
const pasteChildren: DropdownOption[] = isVirtual const pasteChildren: DropdownOption[] = isVirtual
? [ ? [
...@@ -941,7 +931,7 @@ export function useNodeTree( ...@@ -941,7 +931,7 @@ export function useNodeTree(
}) })
} }
// 粘贴XML // 6. 粘贴XML
const pasteFragmentChildren: DropdownOption[] = isVirtual const pasteFragmentChildren: DropdownOption[] = isVirtual
? [ ? [
{ label: '粘贴到上方', key: 'pasteFragmentAbove', icon: icon(ClipboardOutline), disabled: !parent }, { label: '粘贴到上方', key: 'pasteFragmentAbove', icon: icon(ClipboardOutline), disabled: !parent },
...@@ -959,28 +949,24 @@ export function useNodeTree( ...@@ -959,28 +949,24 @@ export function useNodeTree(
children: pasteFragmentChildren children: pasteFragmentChildren
}) })
// 删除节点(受 DTD 约束,文本节点和分页符节点永远允许删除) // 7. 本地暂存
if (parent) { options.push({
const deletable = label: '本地暂存',
isVirtual || VIRTUAL_LAYOUT_TAGS.includes(node.tagName) key: 'localStash',
? true icon: icon(ArchiveOutline)
: (() => { })
const existingTags = parent.children.filter((c) => !VIRTUAL_LAYOUT_TAGS.includes(c.tagName)).map((c) => c.tagName)
// 位置特许规则:父级中最后一个子节点(按位置)允许删除 // ================= 组 3:节点编辑与结构调整 =================
if (existingTags.length > 0 && existingTags[existingTags.length - 1] === node.tagName) { options.push({ type: 'divider', key: 'd2' })
return true
}
return canDeleteChild(parent.tagName, node.tagName, existingTags)
})()
options.push({
label: '删除节点',
key: 'deleteNode',
icon: icon(TrashOutline),
disabled: !deletable
})
}
// 编辑结构子菜单 // 8. 编辑节点(属性/内容)
options.push({
label: isVirtual ? '编辑文本' : '编辑节点',
key: 'editNode',
icon: icon(CreateOutline)
})
// 9. 编辑结构
const structureChildren: DropdownOption[] = [] const structureChildren: DropdownOption[] = []
// 添加子节点(文本节点不能拥有子节点) // 添加子节点(文本节点不能拥有子节点)
...@@ -1024,6 +1010,33 @@ export function useNodeTree( ...@@ -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({ options.push({
label: '保存为模板', label: '保存为模板',
...@@ -1031,6 +1044,27 @@ export function useNodeTree( ...@@ -1031,6 +1044,27 @@ export function useNodeTree(
icon: icon(SaveOutline) 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 return options
} }
...@@ -1406,6 +1440,51 @@ export function useNodeTree( ...@@ -1406,6 +1440,51 @@ export function useNodeTree(
} }
break 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