Commit 13c42f53 by pangchong

feat(editor): 实现节点 MERGED 属性快速标记与高亮功能

- 新增 toggleNodeMerged 方法,支持快速切换节点 MERGED 属性状态
- 编辑器中渲染带 MERGED="TRUE" 节点时显示黄色背景
- 白名单标签(SIGNOFF、CBLST、TABLE)避免外层重复添加黄色背景
- 节点树、文档大纲中添加合并标记显示与样式高亮提示
- 在编辑器工具栏、选择工具条及右键菜单添加标记合并按钮与操作
- CBLST 表格新增 MERGED 状态下的黑色边框及相关样式调整
- 多处组件响应树版本变化,实现节点合并状态的动态更新与渲染
- FAQ 文档新增合并标记操作说明及使用指南
parent 2fd93a4b
......@@ -225,3 +225,10 @@ export const TRANSLATE_TARGET_TAGS = ['PARAC', 'TITLEC']
// 虚拟辅助排版标签集(不参与 DTD 规则约束,如打印换页符)
export const VIRTUAL_LAYOUT_TAGS = ['PAGEBREAK']
/**
* 排除在通用外层容器(.doc-node-wrapper)施加 MERGED 黄色背景的标签列表
* 这些复合结构或特殊组件(如签字表 SIGNOFF、断路器列表 CBLST、CALS 表格 TABLE 等)
* 拥有自己的内部高亮与边框排版逻辑,避免在外层包装节点上重复添加黄色背景导致外边距漏底或打印多余边框。
*/
export const EXCLUDE_WRAPPER_MERGED_TAGS = ['SIGNOFF', 'CBLST', 'TABLE']
......@@ -610,6 +610,43 @@ export const FAQ_DATABASE: FaqItem[] = [
isHot: true
},
{
id: 'faq_node_merged_attr',
category: 'tree',
categoryName: '节点树与大纲',
title: '如何快速标记或取消节点的合并(MERGED)黄底高亮?',
keywords: [
'merged',
'合并',
'黄底',
'黄色背景',
'标记合并',
'取消合并',
'快捷标记',
'高亮合并',
'hb',
'hebing',
'hd',
'huangdi',
'bjhb'
],
synonyms: ['怎么给节点加黄底', 'MERGED属性怎么改', '为什么有黄色背景', '如何去掉黄色标记', '标记合并在哪里', '怎么标记MERGED'],
answer: '在工卡中,带有 MERGED="TRUE" 属性的节点会在编辑器中自动呈现黄色高亮背景。系统提供了一键式极速切换操作:',
steps: [
'【顶部主工具栏】:选中任意节点后,直接点击顶部主工具栏【标记合并 / 已标记合并】按钮(带 GitMerge 图标),一键赋予或移除该节点的 MERGED="TRUE" 属性。',
'【正文浮动工具条】:在正文中双击节点或选取文本弹出悬浮气泡条时,点击【标记/取消合并】图标按钮即可就地快速切换。',
'【右键上下文菜单】:在正文任意节点上点击右键,在菜单第二项直接选择【标记为合并】或【取消合并标记 (MERGED)】。',
'【安全撤销与重做】:所有 MERGED 标记切换操作均已自动接入历史记录,支持通过 Ctrl+Z 撤销或 Ctrl+Y 重做。'
],
highlights: [
'标记后编辑器即时呈现黄色背景,左侧节点树与 Word 大纲同步呈现黄底高亮、琥珀色指示边框与【合并】徽标,断路器表(CBLST)等复杂表格也会同步高亮内部行。',
'特殊复合组件(如签字表 SIGNOFF、断路器表 CBLST、CALS 表格 TABLE 等)已配置白名单,避免在外层包装节点上重复添加黄色背景导致外边距漏底或打印多余边框。'
],
actions: [{ label: '撤销上一步', type: 'event', payload: 'trigger_undo' }],
relatedQuestions: ['faq_node_tree_ops', 'faq_insert_mode', 'faq_keyboard_shortcuts'],
contextTags: ['PARA', 'PARAC', 'WARNING', 'CAUTION', 'NOTE', 'CBLST', 'TABLE', 'TITLE'],
isHot: true
},
{
id: 'faq_doc_outline_view',
category: 'tree',
categoryName: '节点树与大纲',
......
......@@ -373,7 +373,8 @@ export const useEditorStore = defineStore('editor', {
forceScrollSignal: 0,
editorZoom: 100,
expandedKeys: [],
skipExpandOnSelect: false
skipExpandOnSelect: false,
treeVersion: 0
}),
getters: {
......@@ -432,6 +433,7 @@ export const useEditorStore = defineStore('editor', {
traverse(this.xmlTree, null)
}
this.nodeMap = markRaw(map)
this.treeVersion++
},
insertNode(tagName: string, insertBelow: boolean, customNode?: XmlNode, skipDtdCheck?: boolean) {
......@@ -909,6 +911,32 @@ export const useEditorStore = defineStore('editor', {
},
/**
* 快速切换指定(或当前选中)节点的 MERGED 属性状态
* @param nodeId 可选节点 ID,默认使用当前 selectedNodeId
* @returns 切换后的状态:true 为已标记 MERGED,false 为已取消 MERGED,null 为无有效节点
*/
toggleNodeMerged(nodeId?: string): boolean | null {
const id = nodeId || this.selectedNodeId
if (!id) return null
const target = this.nodeMap.get(id)?.node
if (!target) return null
const currentAttrs = { ...(target.attributes || {}) }
const isMerged = currentAttrs.MERGED === 'TRUE'
this.saveSnapshot()
if (isMerged) {
delete currentAttrs.MERGED
} else {
currentAttrs.MERGED = 'TRUE'
}
target.attributes = currentAttrs
this.rebuildNodeMap()
return !isMerged
},
/**
* 更新当前选中节点的文本内容
*/
updateSelectedNodeText(text: string) {
......
......@@ -17,4 +17,5 @@ export interface EditorState {
editorZoom: number // 编辑器文字字号缩放比例 (80 - 200)
expandedKeys: string[] // 全局的节点展开状态
skipExpandOnSelect: boolean // 在选中时是否跳过祖先展开逻辑
treeVersion: number // 树数据响应式版本计数器
}
export { LIST_ITEM_TAGS, HEADER_TAGS, ROMAN_LOOKUP, ORDERED_LIST_ITEM_TAGS } from '@/configs/xmlTags'
export { LIST_ITEM_TAGS, HEADER_TAGS, ROMAN_LOOKUP, ORDERED_LIST_ITEM_TAGS, EXCLUDE_WRAPPER_MERGED_TAGS } from '@/configs/xmlTags'
......@@ -11,7 +11,7 @@
isSelected ? 'ring-2 ring-primary ring-offset-1 rounded-sm bg-primary/5' : '',
!renderInline && isContainer ? 'py-1 px-1' : ''
]"
:style="node.attributes?.MERGED === 'TRUE' && node.tagName !== 'SIGNOFF' ? { backgroundColor: 'yellow' } : {}"
:style="node.attributes?.MERGED === 'TRUE' && !EXCLUDE_WRAPPER_MERGED_SET.has(node.tagName) ? { backgroundColor: 'yellow' } : {}"
@click.stop="handleNodeClick($event)"
>
<!-- 智能翻译加载遮罩 -->
......@@ -682,36 +682,58 @@
<!-- 16. CBLST (电路断路器列表) 特殊处理 (仿照 PDF 样式表格) -->
<template v-else-if="node.tagName === 'CBLST'">
<div class="my-4 overflow-x-auto select-none border border-divider rounded-lg p-3 bg-card shadow-sm">
<div
class="cblst-container my-4 overflow-x-auto select-none border border-divider rounded-lg p-3 bg-card shadow-sm transition-colors"
:class="[isSelected ? 'ring-2 ring-primary ring-offset-1' : '']"
>
<div class="text-xs font-bold text-color3 mb-2 flex items-center space-x-1 print-hide">
<n-icon color="var(--primary-color)"><grid-outline /></n-icon>
<span>
电路断路器清单 (CBLST) - 行动:
{{ node.attributes.ACTION === 'verif-close' ? '确认关闭' : node.attributes.ACTION === 'open' ? '断开' : '操作' }}
</span>
<span
v-if="node.attributes?.MERGED === 'TRUE'"
class="ml-2 px-1.5 py-0.5 rounded text-[10px] font-bold font-mono bg-warning/20 text-warning border border-warning/30 select-none"
>
MERGED
</span>
</div>
<table class="w-full border-collapse border border-divider text-xs">
<table
class="w-full border-collapse border text-xs"
:class="node.attributes?.MERGED === 'TRUE' ? 'merged-cblst border-black' : 'border-divider'"
:style="node.attributes?.MERGED === 'TRUE' ? 'background-color: yellow; color: #000000;' : ''"
>
<thead>
<tr class="bg-fill-2 text-color1 font-bold h-8">
<th class="border border-divider px-2 text-center w-1/4">面板 PANEL</th>
<th class="border border-divider px-2 text-center w-5/12">说明 DESIGNATION</th>
<th class="border border-divider px-2 text-center w-1/6">功能号 FIN</th>
<th class="border border-divider px-2 text-center w-1/6">位置 LOCATION</th>
<tr
class="font-bold h-8"
:class="node.attributes?.MERGED === 'TRUE' ? 'bg-black/10 text-black' : 'bg-fill-2 text-color1'"
>
<th class="border px-2 text-center w-1/4" :class="node.attributes?.MERGED === 'TRUE' ? 'border-black' : 'border-divider'">面板 PANEL</th>
<th class="border px-2 text-center w-5/12" :class="node.attributes?.MERGED === 'TRUE' ? 'border-black' : 'border-divider'">说明 DESIGNATION</th>
<th class="border px-2 text-center w-1/6" :class="node.attributes?.MERGED === 'TRUE' ? 'border-black' : 'border-divider'">功能号 FIN</th>
<th class="border px-2 text-center w-1/6" :class="node.attributes?.MERGED === 'TRUE' ? 'border-black' : 'border-divider'">位置 LOCATION</th>
</tr>
</thead>
<tbody>
<template v-for="subList in node.children" :key="subList.id">
<!-- 如果 CBSUBLST 下有除了 CBDATA 以外的子节点,比如说明文本,可以单独渲染一行 -->
<tr v-if="hasNonCbDataChildren(subList)" class="bg-fill-1 text-color2">
<td colspan="4" class="border border-divider px-2 py-1 font-semibold">
<tr
v-if="hasNonCbDataChildren(subList)"
:class="node.attributes?.MERGED === 'TRUE' || subList.attributes?.MERGED === 'TRUE' ? 'bg-black/5 text-black' : 'bg-fill-1 text-color2'"
>
<td colspan="4" class="border px-2 py-1 font-semibold" :class="node.attributes?.MERGED === 'TRUE' ? 'border-black' : 'border-divider'">
<DocNodeRenderer v-for="child in getNonCbDataChildren(subList)" :key="child.id" :node="child" :parent="subList" />
</td>
</tr>
<!-- 遍历 CBDATA 行 -->
<template v-for="cbData in subList.children.filter((c) => c.tagName === 'CBDATA')" :key="cbData.id">
<!-- 如果 CBDATA 带有 EFFECT,按 PDF 样式可以单独渲染一行作为适用性提示 -->
<tr v-if="cbData.children.some((c) => c.tagName === 'EFFECT')" class="bg-fill-1">
<td colspan="4" class="border border-divider px-2 py-0.5 text-danger font-bold text-[10px]">
<tr
v-if="cbData.children.some((c) => c.tagName === 'EFFECT')"
:class="node.attributes?.MERGED === 'TRUE' ? 'bg-black/5' : 'bg-fill-1'"
>
<td colspan="4" class="border px-2 py-0.5 text-danger font-bold text-[10px]" :class="node.attributes?.MERGED === 'TRUE' ? 'border-black' : 'border-divider'">
<DocNodeRenderer
v-for="eff in cbData.children.filter((c) => c.tagName === 'EFFECT')"
:key="eff.id"
......@@ -723,25 +745,44 @@
</tr>
<tr
:data-node-id="cbData.id"
class="h-8 hover:bg-fill-3 transition-colors cursor-pointer"
class="h-8 transition-colors cursor-pointer"
:class="[
node.attributes?.MERGED === 'TRUE'
? 'hover:bg-black/10'
: cbData.attributes?.MERGED === 'TRUE' || subList.attributes?.MERGED === 'TRUE'
? 'hover:bg-black/10'
: 'hover:bg-fill-3',
cbData.attributes?.MERGED === 'TRUE' || subList.attributes?.MERGED === 'TRUE'
? 'merged-cbdata'
: '',
editorStore.selectedNodeId === cbData.id ||
cbData.children.some(
(c) => c.id === editorStore.selectedNodeId && COMPOSITE_CONTAINER_MAP['CBDATA']?.includes(c.tagName)
)
? 'bg-primary/5 font-bold'
? node.attributes?.MERGED === 'TRUE'
? 'bg-black/15 font-bold'
: 'bg-primary/5 font-bold'
: ''
]"
:style="
node.attributes?.MERGED !== 'TRUE' && (cbData.attributes?.MERGED === 'TRUE' || subList.attributes?.MERGED === 'TRUE')
? 'background-color: yellow; color: #000000;'
: ''
"
@click.stop="selectNode(cbData.id)"
>
<td
v-for="colTag in COMPOSITE_CONTAINER_MAP['CBDATA']"
:key="colTag"
class="border border-divider px-2"
:class="{
'text-center font-mono': colTag === 'PAN' || colTag === 'CB' || colTag === 'CBLOC',
'font-bold text-color1': colTag === 'CB'
}"
class="border px-2"
:class="[
node.attributes?.MERGED === 'TRUE' ? 'border-black' : 'border-divider',
{
'text-center font-mono': colTag === 'PAN' || colTag === 'CB' || colTag === 'CBLOC',
'font-bold': colTag === 'CB',
'text-color1': colTag === 'CB' && node.attributes?.MERGED !== 'TRUE' && cbData.attributes?.MERGED !== 'TRUE' && subList.attributes?.MERGED !== 'TRUE'
}
]"
>
<DocNodeRenderer
v-if="cbData.children.find((c) => c.tagName === colTag)"
......@@ -1593,7 +1634,8 @@ import {
COMPOSITE_CONTAINER_MAP,
EMPTY_MARKER_TAGS,
NOTE_VARIANT_TAGS,
HIDDEN_STRUCTURAL_TAGS
HIDDEN_STRUCTURAL_TAGS,
EXCLUDE_WRAPPER_MERGED_TAGS
} from '@/configs/xmlTags'
// 动态载入各个定制环境下的 SignoffRenderer.vue
......@@ -1616,6 +1658,7 @@ const SUPER_SUB_SET = new Set(SUPER_SUB_TAGS)
const EMPTY_MARKER_SET = new Set(EMPTY_MARKER_TAGS)
const NOTE_VARIANT_SET = new Set(NOTE_VARIANT_TAGS)
const HIDDEN_STRUCTURAL_SET = new Set(HIDDEN_STRUCTURAL_TAGS)
const EXCLUDE_WRAPPER_MERGED_SET = new Set(EXCLUDE_WRAPPER_MERGED_TAGS)
const props = withDefaults(
defineProps<{
......@@ -1728,7 +1771,7 @@ const handleNodeClick = (e: MouseEvent) => {
:deep(.n-image img) {
width: 100%;
}
/* 打印时隐藏分页符的视觉标签,仅保留分页效果 */
/* 打印时隐藏分页符的视觉标签与卡片容器边框,仅保留分页效果与纯净表格 */
@media print {
.pagebreak-marker {
height: 0 !important;
......@@ -1740,11 +1783,25 @@ const handleNodeClick = (e: MouseEvent) => {
.pagebreak-marker > * {
display: none !important;
}
.cblst-container {
border: none !important;
box-shadow: none !important;
background: transparent !important;
background-color: transparent !important;
padding: 0 !important;
margin: 0 !important;
}
}
/* 当处于 MERGED (黄色背景) 时,强制表格外边框及所有内部单元格边框为黑色实线,防止暗色模式下 dark:border-white/40 导致边框在黄色背景上隐形消失 */
.merged-signoff,
.merged-signoff td {
.merged-signoff td,
.merged-cblst,
.merged-cblst th,
.merged-cblst td,
.merged-cbdata,
.merged-cbdata td {
border-color: #000000 !important;
color: #000000 !important;
}
</style>
......@@ -16,7 +16,8 @@ import {
AddCircleOutline,
ArrowUpOutline,
ArrowDownOutline,
LayersOutline
LayersOutline,
GitMergeOutline
} from '@vicons/ionicons5'
import { useEditorStore } from '@/store/editor'
import type { XmlNode } from '@/types/xmlNode'
......@@ -247,6 +248,11 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
return activeNodeId.value.includes('-txt-') || activeNode.value?.tagName === '#text'
})
// 当前节点是否已标记为合并 (MERGED="TRUE")
const isCurrentMerged = computed(() => {
return activeNode.value?.attributes?.MERGED === 'TRUE'
})
// 是否是虚拟排版节点(如分页符)
const isVirtualLayoutNode = computed(() => {
return activeNode.value ? VIRTUAL_LAYOUT_TAGS.includes(activeNode.value.tagName) : false
......@@ -390,6 +396,16 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
addNodeVisible.value = true
}
// 快速标记/取消合并 (MERGED)
else if (actionName === 'toggleMerged') {
const nextState = editorStore.toggleNodeMerged(realId)
if (nextState !== null) {
window.$message.success(
nextState ? `已将 <${mapped.node.tagName}> 标记为合并 (MERGED="TRUE")` : `已取消 <${mapped.node.tagName}> 的合并标记`
)
}
}
// 2. 复制节点
else if (actionName === 'copyNode') {
if (isVirtual) {
......@@ -964,6 +980,15 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
icon: icon(CreateOutline)
})
// 快捷标记/取消合并
if (!isTextNode.value) {
options.push({
label: isCurrentMerged.value ? '取消合并标记' : '标记为合并',
key: 'toggleMerged',
icon: icon(GitMergeOutline)
})
}
// 2. 结构调整 (子菜单)
options.push({
label: '结构调整',
......
......@@ -423,6 +423,22 @@ export function useSelectionToolbar() {
visible.value = false
}
const isCurrentMerged = computed(() => {
return activeNode.value?.attributes?.MERGED === 'TRUE'
})
const handleToggleMerged = () => {
if (!activeNode.value) return
const realId = isVirtualText.value ? activeNodeId.value?.split('-txt-')[0] : activeNode.value.id
if (!realId) return
const res = store.toggleNodeMerged(realId)
if (res === true) {
window.$message?.success(`已将节点 <${activeNode.value.tagName}> 标记为合并`)
} else if (res === false) {
window.$message?.info(`已取消节点 <${activeNode.value.tagName}> 的合并标记`)
}
}
// 监听双击事件:双击任意 data-node-id 元素时,即使是 select-none 元素,也直接弹起工具栏并选中该节点
const handleDblclick = (e: MouseEvent) => {
if (mouseupTimeoutId) {
......@@ -499,6 +515,9 @@ export function useSelectionToolbar() {
handleAddChild,
handleInsertBefore,
handleInsertAfter,
handleDeleteNode
handleDeleteNode,
isCurrentMerged,
handleToggleMerged
}
}
......@@ -26,6 +26,25 @@
编辑节点属性
</n-tooltip>
<!-- 标记/取消合并 (MERGED) -->
<n-tooltip trigger="hover">
<template #trigger>
<CommonButton
size="tiny"
quaternary
circle
:type="isCurrentMerged ? 'warning' : 'default'"
:disabled="!hasActiveNode || isVirtualText"
@click="handleToggleMerged"
>
<template #icon>
<n-icon><GitMergeOutline /></n-icon>
</template>
</CommonButton>
</template>
{{ isCurrentMerged ? '取消合并标记' : '标记为合并' }}
</n-tooltip>
<!-- 查看规则 -->
<n-tooltip trigger="hover">
<template #trigger>
......@@ -132,7 +151,8 @@ import {
ArrowDownOutline,
TrashOutline,
LanguageOutline,
SyncOutline
SyncOutline,
GitMergeOutline
} from '@vicons/ionicons5'
import { useSelectionToolbar } from './functionals/index'
......@@ -154,7 +174,9 @@ const {
handleAddChild,
handleInsertBefore,
handleInsertAfter,
handleDeleteNode
handleDeleteNode,
isCurrentMerged,
handleToggleMerged
} = useSelectionToolbar()
</script>
......
......@@ -249,6 +249,25 @@ export function useEditorToolbar(emit: any) {
}
})
const isCurrentMerged = computed(() => {
return editorStore.selectedNode?.attributes?.MERGED === 'TRUE'
})
const handleToggleMerged = () => {
const node = editorStore.selectedNode
if (!node) {
window.$message.warning('请先在树中或编辑器中选择一个目标节点!')
return
}
const res = editorStore.toggleNodeMerged(node.id)
if (res === true) {
window.$message.success(`已将节点 <${node.tagName}> 标记为合并`)
} else if (res === false) {
window.$message.info(`已取消节点 <${node.tagName}> 的合并标记`)
}
}
return {
editorStore,
appStore,
......@@ -272,6 +291,8 @@ export function useEditorToolbar(emit: any) {
extractTranslateModalRef,
searchTranslateModalRef,
xmlSizeStr,
templateSelectModalRef
templateSelectModalRef,
isCurrentMerged,
handleToggleMerged
}
}
......@@ -287,6 +287,35 @@
</n-popover>
</div>
<div class="w-px h-5 flex-shrink-0 mx-2" style="background: #d0d0d0"></div>
<!-- 组 2.5:节点快速标记操作 -->
<div class="flex items-center gap-0.5 flex-shrink-0">
<n-tooltip trigger="hover" :show-delay="300">
<template #trigger>
<button
type="button"
class="toolbar-btn flex items-center gap-1.5 px-2.5 h-7 rounded transition-all select-none focus:outline-none flex-shrink-0 disabled:opacity-40 disabled:hover:bg-transparent"
:class="[
isCurrentMerged
? 'bg-warning/20 text-warning font-semibold border border-warning/40 shadow-sm'
: 'text-color2 hover:bg-fill-2 active:bg-fill-3'
]"
:disabled="!editorStore.selectedNode"
@click="handleToggleMerged"
>
<n-icon class="text-base"><git-merge-outline /></n-icon>
<span class="text-xs font-medium">{{ isCurrentMerged ? '已标记合并' : '标记合并' }}</span>
</button>
</template>
<div v-if="editorStore.selectedNode" class="text-xs space-y-0.5">
<div>当前节点: &lt;{{ editorStore.selectedNode.tagName }}&gt;</div>
<div>{{ isCurrentMerged ? '点击取消该节点的合并 (MERGED) 标记' : '快捷标记为合并,高亮显示黄色背景' }}</div>
</div>
<span v-else class="text-xs">请先选择节点</span>
</n-tooltip>
</div>
<div class="w-px h-5 flex-shrink-0 mx-3" style="background: #d0d0d0"></div>
<div class="flex items-center gap-0.5 flex-shrink-0">
......@@ -525,7 +554,8 @@ import {
CheckmarkCircleOutline,
SettingsOutline,
GitCompareOutline,
ArchiveOutline
ArchiveOutline,
GitMergeOutline
} from '@vicons/ionicons5'
import { useEditorToolbar } from './functionals'
import { GREEN_BUTTONS } from './constants'
......@@ -569,7 +599,9 @@ const {
extractTranslateModalRef,
searchTranslateModalRef,
xmlSizeStr,
templateSelectModalRef
templateSelectModalRef,
isCurrentMerged,
handleToggleMerged
} = useEditorToolbar(emit)
const insertFragmentModalRef = ref<any>(null)
......
......@@ -6,6 +6,7 @@ export interface OutlineItem {
text: string
fullText: string
level: number
isMerged?: boolean
}
// 文档大纲组件 Props 定义
......
......@@ -21,6 +21,7 @@ export function useDocOutlineView(props: DocOutlineProps) {
const outlineList = computed<OutlineItem[]>(() => {
try {
if (!editorStore.xmlTree) return []
const _ = editorStore.treeVersion
const outline: OutlineItem[] = []
const getPreviewText = (n: any): string => {
......@@ -231,13 +232,15 @@ export function useDocOutlineView(props: DocOutlineProps) {
}
if (isOutline) {
const isMerged = node.attributes?.MERGED === 'TRUE'
outline.push({
id: node.id,
tagName: node.tagName,
seqNum,
text: text,
fullText: `${seqNum}${text}`,
level
level,
isMerged
})
}
......
......@@ -16,11 +16,13 @@
:class="[
activeOutlineId === item.id
? 'bg-primary text-white font-semibold shadow-sm'
: item.level === 0
? 'bg-primary/10 dark:bg-primary/20 border border-primary/30 text-color1 dark:text-white hover:bg-primary/15'
: item.level === 1
? 'hover:bg-fill-3 text-color1 font-bold'
: 'hover:bg-fill-3 text-color2 font-normal'
: item.isMerged
? 'bg-amber-100/90 text-amber-950 dark:bg-amber-500/25 dark:text-amber-200 border-l-2 border-amber-500 font-medium'
: item.level === 0
? 'bg-primary/10 dark:bg-primary/20 border border-primary/30 text-color1 dark:text-white hover:bg-primary/15'
: item.level === 1
? 'hover:bg-fill-3 text-color1 font-bold'
: 'hover:bg-fill-3 text-color2 font-normal'
]"
@click="handleOutlineClick(item.id)"
>
......@@ -80,6 +82,20 @@
>
{{ item.text }}
</span>
<!-- 合并标记 Badge -->
<span
v-if="item.isMerged"
class="text-[10px] font-semibold px-1.5 py-0.2 rounded shrink-0 leading-tight border flex items-center gap-0.5"
:class="[
activeOutlineId === item.id
? 'bg-amber-300 text-amber-950 border-amber-400 font-bold shadow-sm'
: 'bg-amber-200 text-amber-900 border-amber-400 dark:bg-amber-500/30 dark:text-amber-200 dark:border-amber-500/40'
]"
title="此节点已标记为合并 (MERGED='TRUE')"
>
<n-icon size="10"><GitMergeOutline /></n-icon>
<span>合并</span>
</span>
</div>
<!-- 定位指示 / 悬浮跳转图标 -->
......@@ -131,7 +147,8 @@ import {
GridOutline,
WarningOutline,
InformationCircleOutline,
NavigateOutline
NavigateOutline,
GitMergeOutline
} from '@vicons/ionicons5'
import type { DocOutlineProps } from './constants'
import { useDocOutlineView } from './functionals'
......
......@@ -31,10 +31,13 @@
? 'bg-primary text-white tree-node-selected'
: (showDropdown || isAnyModalVisible) && contextNodeId === item.id
? 'tree-node-context-active'
: hasCustomColor(item)
? 'hover:bg-fill-3'
: 'hover:bg-fill-3 text-color2',
VIRTUAL_LAYOUT_TAGS.includes(item.tagName) ? 'tree-node-pagebreak' : ''
: item.isMerged
? 'bg-amber-100/90 text-amber-950 dark:bg-amber-500/25 dark:text-amber-200 border-l-2 border-amber-500 hover:bg-amber-200/90 dark:hover:bg-amber-500/35 font-medium'
: hasCustomColor(item)
? 'hover:bg-fill-3'
: 'hover:bg-fill-3 text-color2',
VIRTUAL_LAYOUT_TAGS.includes(item.tagName) ? 'tree-node-pagebreak' : '',
item.isMerged ? 'tree-node-merged' : ''
]"
:style="[
{
......@@ -137,6 +140,20 @@
>
<!-- 节点名称高亮 -->
<span class="font-bold text-sm flex-shrink-0" v-html="highlightText(item.tagName, pattern)"></span>
<!-- 合并标记 Badge (黄底醒目标识) -->
<span
v-if="item.isMerged"
class="text-[10px] font-semibold px-1.5 py-0.5 rounded shrink-0 leading-none flex items-center gap-0.5 border"
:class="[
effectiveSelectedId === item.id
? 'bg-amber-300 text-amber-950 border-amber-400 font-bold shadow-sm'
: 'bg-amber-200/90 text-amber-900 border-amber-400/80 dark:bg-amber-500/35 dark:text-amber-100 dark:border-amber-400/40'
]"
title="此节点已标记为合并 (MERGED='TRUE')"
>
<n-icon size="10"><GitMergeOutline /></n-icon>
<span>合并</span>
</span>
<!-- 属性摘要 Badge (包含淡色背景与框线,与文本预览彻底区分) -->
<span
v-if="item.attrSummary"
......@@ -194,7 +211,7 @@
</template>
<script setup lang="ts">
import { SyncOutline, NavigateOutline } from '@vicons/ionicons5'
import { SyncOutline, NavigateOutline, GitMergeOutline } from '@vicons/ionicons5'
import { VIRTUAL_LAYOUT_TAGS } from '@/configs/xmlTags'
import type { FlatNode, XmlTreeViewProps } from './constants'
import { useXmlTreeView } from './functionals'
......@@ -516,4 +533,9 @@ defineExpose({
color: #ffffff !important;
border: 1px dashed color-mix(in srgb, var(--card-color, #ffffff) 40%, transparent) !important;
}
/* ── 合并标记节点(MERGED="TRUE")在树中的黄底凸显样式 ── */
.tree-node-merged:not(.tree-node-selected) {
box-shadow: inset 0 0 0 1px rgba(245, 158, 11, 0.4);
}
</style>
......@@ -34,6 +34,7 @@ export interface FlatNode {
hasChildren: boolean
isExpanded: boolean
rawNode: XmlNode
isMerged?: boolean
}
// DTD 批量删除被拦截节点的明细定义
......
......@@ -14,7 +14,8 @@ import {
DocumentTextOutline,
CodeWorkingOutline,
LanguageOutline,
ArchiveOutline
ArchiveOutline,
GitMergeOutline
} from '@vicons/ionicons5'
import { useEditorStore } from '@/store/editor'
import { useAppStore } from '@/store/app'
......@@ -229,6 +230,7 @@ export function useNodeTree(
}
}
const isExpanded = expandedKeys.value.has(node.id)
const isMerged = node.attributes?.MERGED === 'TRUE'
let attrSummary = ''
let subtitle = ''
......@@ -466,7 +468,8 @@ export function useNodeTree(
depth,
hasChildren,
isExpanded,
rawNode: node
rawNode: node,
isMerged
})
// 如果处于搜索状态,强制展开展示搜索结果;否则根据 isExpanded 展开
......@@ -521,6 +524,7 @@ export function useNodeTree(
// 扁平列表数据
const flatList = computed(() => {
if (!editorStore.xmlTree) return []
const _ = editorStore.treeVersion
return buildFlatList(editorStore.xmlTree, 0, pattern.value)
})
......@@ -1017,6 +1021,16 @@ export function useNodeTree(
icon: icon(CreateOutline)
})
// 快速标记/取消合并 (MERGED)
if (!isVirtual) {
const isMerged = node.attributes?.MERGED === 'TRUE'
options.push({
label: isMerged ? '取消合并标记' : '标记为合并',
key: 'toggleMerged',
icon: icon(GitMergeOutline)
})
}
// 9. 编辑结构
const structureChildren: DropdownOption[] = []
......@@ -1179,6 +1193,17 @@ export function useNodeTree(
break
}
// ── 快速切换合并标记 ──
case 'toggleMerged': {
const res = editorStore.toggleNodeMerged(node.id)
if (res === true) {
window.$message?.success(`已将节点 <${node.tagName}> 标记为合并`)
} else if (res === false) {
window.$message?.info(`已取消节点 <${node.tagName}> 的合并标记`)
}
break
}
// ── 智能翻译节点 ──
case 'translateNode': {
const enSourceNode = getEnglishSourceNode(node, parent)
......@@ -1595,6 +1620,7 @@ export function useNodeTree(
}
const hasCustomColor = (item: FlatNode): boolean => {
if (item.isMerged) return true
const tag = item.tagName.toUpperCase()
return COLORED_TAGS.map((t) => t.toUpperCase()).includes(tag)
}
......
......@@ -459,6 +459,7 @@ export function useEditor() {
margin-left: 0 !important;
}
/* 移除外层卡片及编辑容器的圆角边框、背景及阴影 */
.cblst-container,
.border.border-divider.rounded-lg,
.rounded-lg.shadow-inner.bg-fill-2,
.bg-card.p-3 {
......
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