Commit 16d381f4 by pangchong

refactor(xml): 优化节点深拷贝和UUID重新生成逻辑

- 重构cloneNode函数,使用Map跟踪id映射,实现子节点和mixedContent中元素引用的ID更新
- 修改编辑器中递归更新UUID逻辑,确保mixedContent内元素的nodeId同步更新
- 在NodeTree中粘贴操作前添加DTD校验,防止非法子节点添加,提升数据一致性
- 调整deepCloneWithNewIds实现,统一ID映射管理,简化子节点和mixedContent的复制流程
parent a9fea765
...@@ -224,20 +224,26 @@ export function getNodePath(root: XmlNode, nodeId: string): XmlNode[] { ...@@ -224,20 +224,26 @@ export function getNodePath(root: XmlNode, nodeId: string): XmlNode[] {
* 深拷贝节点(生成新 ID) * 深拷贝节点(生成新 ID)
*/ */
export function cloneNode(node: XmlNode, newParentId: string | null = null): XmlNode { export function cloneNode(node: XmlNode, newParentId: string | null = null): XmlNode {
const idMap = new Map<string, string>()
const clone = (n: XmlNode, parentId: string | null): XmlNode => {
const newId = generateId() const newId = generateId()
idMap.set(n.id, newId)
const children = n.children.map((c) => clone(c, newId))
const mixedContent = n.mixedContent.map((item) => {
if (item.type === 'text') return { ...item }
return { ...item, nodeId: idMap.get(item.nodeId || '') || item.nodeId }
})
return { return {
id: newId, id: newId,
tagName: node.tagName, tagName: n.tagName,
attributes: { ...node.attributes }, attributes: { ...n.attributes },
children: node.children.map((c) => cloneNode(c, newId)), children,
textContent: node.textContent, textContent: n.textContent,
mixedContent: node.mixedContent.map((item) => { mixedContent,
if (item.type === 'text') return { ...item } parentId
// 元素引用需要更新 nodeId,但这里只做浅复制标记 }
return { ...item }
}),
parentId: newParentId
} }
return clone(node, newParentId)
} }
/** /**
......
...@@ -446,10 +446,19 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e ...@@ -446,10 +446,19 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
const cloneNode = JSON.parse(JSON.stringify(copyNodeCache.value)) const cloneNode = JSON.parse(JSON.stringify(copyNodeCache.value))
// 递归生成全新 UUID // 递归生成全新 UUID
const idMap = new Map<string, string>()
const regenerateIds = (n: XmlNode, pid: string | null) => { const regenerateIds = (n: XmlNode, pid: string | null) => {
n.id = crypto.randomUUID() const newId = crypto.randomUUID()
idMap.set(n.id, newId)
n.id = newId
n.parentId = pid n.parentId = pid
n.children.forEach((c) => regenerateIds(c, n.id)) n.children.forEach((c) => regenerateIds(c, newId))
n.mixedContent = n.mixedContent.map((m) => {
if (m.type === 'element' && m.nodeId) {
return { ...m, nodeId: idMap.get(m.nodeId) || m.nodeId }
}
return { ...m }
})
} }
regenerateIds(cloneNode, null) regenerateIds(cloneNode, null)
......
...@@ -987,6 +987,13 @@ export function useNodeTree( ...@@ -987,6 +987,13 @@ export function useNodeTree(
// ── 粘贴到上方 ── // ── 粘贴到上方 ──
case 'pasteAbove': { case 'pasteAbove': {
if (!copyNodeCache.value || !parent) break if (!copyNodeCache.value || !parent) break
if (copyNodeCache.value.tagName !== '#text') {
const existingCount = parent.children.filter((c) => c.tagName === copyNodeCache.value!.tagName).length
if (!canAddChild(parent.tagName, copyNodeCache.value.tagName, existingCount)) {
window.$message?.warning(`DTD 校验失败: 节点 <${parent.tagName}> 无法接受子元素 <${copyNodeCache.value.tagName}>`)
break
}
}
const cloned = deepCloneWithNewIds(copyNodeCache.value, parent.id) const cloned = deepCloneWithNewIds(copyNodeCache.value, parent.id)
editorStore.saveSnapshot() editorStore.saveSnapshot()
...@@ -1046,6 +1053,13 @@ export function useNodeTree( ...@@ -1046,6 +1053,13 @@ export function useNodeTree(
// ── 粘贴到下方 ── // ── 粘贴到下方 ──
case 'pasteBelow': { case 'pasteBelow': {
if (!copyNodeCache.value || !parent) break if (!copyNodeCache.value || !parent) break
if (copyNodeCache.value.tagName !== '#text') {
const existingCount = parent.children.filter((c) => c.tagName === copyNodeCache.value!.tagName).length
if (!canAddChild(parent.tagName, copyNodeCache.value.tagName, existingCount)) {
window.$message?.warning(`DTD 校验失败: 节点 <${parent.tagName}> 无法接受子元素 <${copyNodeCache.value.tagName}>`)
break
}
}
const cloned = deepCloneWithNewIds(copyNodeCache.value, parent.id) const cloned = deepCloneWithNewIds(copyNodeCache.value, parent.id)
editorStore.saveSnapshot() editorStore.saveSnapshot()
...@@ -1105,6 +1119,13 @@ export function useNodeTree( ...@@ -1105,6 +1119,13 @@ export function useNodeTree(
// ── 粘贴到内部 ── // ── 粘贴到内部 ──
case 'pasteInside': { case 'pasteInside': {
if (!copyNodeCache.value) break if (!copyNodeCache.value) break
if (copyNodeCache.value.tagName !== '#text') {
const existingCount = node.children.filter((c) => c.tagName === copyNodeCache.value!.tagName).length
if (!canAddChild(node.tagName, copyNodeCache.value.tagName, existingCount)) {
window.$message?.warning(`DTD 校验失败: 节点 <${node.tagName}> 无法接受子元素 <${copyNodeCache.value.tagName}>`)
break
}
}
const cloned = deepCloneWithNewIds(copyNodeCache.value, nodeId) const cloned = deepCloneWithNewIds(copyNodeCache.value, nodeId)
editorStore.saveSnapshot() editorStore.saveSnapshot()
...@@ -1234,18 +1255,30 @@ export function useNodeTree( ...@@ -1234,18 +1255,30 @@ export function useNodeTree(
} }
const deepCloneWithNewIds = (node: XmlNode, newParentId: string | null): XmlNode => { const deepCloneWithNewIds = (node: XmlNode, newParentId: string | null): XmlNode => {
const idMap = new Map<string, string>()
const clone = (n: XmlNode, parentId: string | null): XmlNode => {
const newId = crypto.randomUUID() const newId = crypto.randomUUID()
const cloned: XmlNode = { idMap.set(n.id, newId)
...node, const children = n.children.map((child) => clone(child, newId))
const mixedContent = n.mixedContent.map((m) => {
if (m.type === 'element' && m.nodeId) {
return { ...m, nodeId: idMap.get(m.nodeId) || m.nodeId }
}
return { ...m }
})
return {
...n,
id: newId, id: newId,
parentId: newParentId, parentId,
attributes: { ...node.attributes }, attributes: { ...n.attributes },
mixedContent: node.mixedContent.map((m) => ({ ...m })), mixedContent,
children: node.children.map((child) => deepCloneWithNewIds(child, newId)) children
}
} }
return cloned return clone(node, newParentId)
} }
// 右键下拉菜单数据 // 右键下拉菜单数据
const dropdownOptions = computed<DropdownOption[]>(() => { const dropdownOptions = computed<DropdownOption[]>(() => {
if (!contextNodeId.value) return [] if (!contextNodeId.value) return []
......
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