Commit 9bd93c5a by pangchong

feat(editor): 支持多级列表自动创建条目子节点及编辑功能

- 在配置文件中定义列表容器与条目标签映射关系及相关接口类型
- 实现根据列表容器标签自动生成对应条目子节点的默认节点结构
- 在节点树新增节点时,自动为列表容器节点添加对应条目子节点
- 优化编辑器视图中列表容器及条目节点的渲染逻辑
- 新增多级列表编辑器功能,支持列表项新增、删除与排序
- 完善相关工具函数,增强复合结构容器的子节点处理能力
- 增加多级列表相关的常量、转换方法及国际化问答文档内容
- 调整样式及行为,确保列表子节点文本编辑的正确展示和交互
- 在节点树操作完成后自动展开新插入的含子节点列表容器节点
parent 67d011bf
...@@ -155,8 +155,19 @@ export const HIDDEN_STRUCTURAL_TAGS = [ ...@@ -155,8 +155,19 @@ export const HIDDEN_STRUCTURAL_TAGS = [
'SPSKILL' 'SPSKILL'
] ]
// 复合结构特殊容器接口类型定义
export interface CompositeContainerMap {
CBDATA: string[]
TED: string[]
CON: string[]
GRAPHIC: string[]
EINDATA: string[]
EXPD: string[]
[key: string]: string[] | undefined
}
// 复合结构特殊容器及其子节点关系映射表(键为直接父级节点标签名,值为其下需要特殊高亮/选中定位的子节点标签名) // 复合结构特殊容器及其子节点关系映射表(键为直接父级节点标签名,值为其下需要特殊高亮/选中定位的子节点标签名)
export const COMPOSITE_CONTAINER_MAP: Record<string, string[]> = { export const COMPOSITE_CONTAINER_MAP: CompositeContainerMap = {
CBDATA: ['PAN', 'CBNAME', 'CB', 'CBLOC'], CBDATA: ['PAN', 'CBNAME', 'CB', 'CBLOC'],
TED: ['TOOLNAME', 'TOOLNBR'], TED: ['TOOLNAME', 'TOOLNBR'],
CON: ['CONNAME', 'CONNBR'], CON: ['CONNAME', 'CONNBR'],
...@@ -185,6 +196,26 @@ export const DOCUMENT_LIKE_TAGS = ['PARA', 'PARAC', 'TITLE', 'TITLEC', 'REGULATI ...@@ -185,6 +196,26 @@ export const DOCUMENT_LIKE_TAGS = ['PARA', 'PARAC', 'TITLE', 'TITLEC', 'REGULATI
// 所有列表容器标签(有序 LIST1-7 + 无序 UNLIST/NUMLIST) // 所有列表容器标签(有序 LIST1-7 + 无序 UNLIST/NUMLIST)
export const ALL_LIST_TAGS = [...ORDERED_LIST_TAGS, 'UNLIST', 'NUMLIST'] export const ALL_LIST_TAGS = [...ORDERED_LIST_TAGS, 'UNLIST', 'NUMLIST']
// 列表容器与其对应的子节点条目标签映射关系表
export const LIST_CONTAINER_TO_ITEM_MAP: Record<string, string> = {
LIST1: 'L1ITEM',
LIST2: 'L2ITEM',
LIST3: 'L3ITEM',
LIST4: 'L4ITEM',
LIST5: 'L5ITEM',
LIST6: 'L6ITEM',
LIST7: 'L7ITEM',
UNLIST: 'UNLITEM',
NUMLIST: 'NUMLITEM'
}
/**
* 根据列表容器标签获取其对应的条目标签
*/
export const getListItemTag = (containerTag: string): string | undefined => {
return LIST_CONTAINER_TO_ITEM_MAP[containerTag]
}
// 警示及适用性关联标签(WARNING_LIKE + CAUTION + NOTE 及其变体,在列表拆分等场景用作前置拦截) // 警示及适用性关联标签(WARNING_LIKE + CAUTION + NOTE 及其变体,在列表拆分等场景用作前置拦截)
export const ALERT_AND_EFF_TAGS = [...WARNING_LIKE_TAGS, 'CAUTION', 'NOTE', 'HNANOTE', 'ACPANOTE', 'APAPNOTE', 'EAPPNOTE', 'INTNOTE', 'SPECNOTE'] export const ALERT_AND_EFF_TAGS = [...WARNING_LIKE_TAGS, 'CAUTION', 'NOTE', 'HNANOTE', 'ACPANOTE', 'APAPNOTE', 'EAPPNOTE', 'INTNOTE', 'SPECNOTE']
......
...@@ -232,6 +232,62 @@ export const FAQ_DATABASE: FaqItem[] = [ ...@@ -232,6 +232,62 @@ export const FAQ_DATABASE: FaqItem[] = [
contextTags: ['PAGEBREAK'] contextTags: ['PAGEBREAK']
}, },
{ {
id: 'faq_insert_list',
category: 'insert',
categoryName: '元素与排版',
title: '如何插入与编辑多级列表(LIST1-7 / UNLIST / NUMLIST)?',
keywords: [
'列表',
'list',
'有序列表',
'无序列表',
'list1',
'list2',
'list3',
'unlist',
'numlist',
'l1item',
'l2item',
'lb',
'liebiao',
'yxliebiao',
'wxliebiao'
],
synonyms: ['怎么加列表', '添加有序列表', '添加无序列表', '列表项怎么加', '列表怎么编辑', '插入列表'],
answer: '系统支持标准有序列表(<LIST1> 至 <LIST7>、<NUMLIST>)与无序列表(<UNLIST>),插入时会自动生成对应的规范条目子节点(如 L1ITEM、UNLITEM 等)与段落结构。',
steps: [
'在左侧节点树或中间编辑区,选中目标父节点(如 PARA、STEP 或 TASK)。',
'右键选择【插入子节点】或【插入同级节点】,标签选择列表容器(如 LIST1、LIST2 或 UNLIST)。',
'系统在插入列表容器后会自动创建专属条目子节点(如 L1ITEM)并自动展开呈现。',
'在右侧【列表结构编辑器】中,可进行新增列表项、删除项、上下移动调序及文本就地实时编辑。'
],
highlights: [
'100% 符合 DTD 规范:列表容器与条目标签强制自动匹配(LIST1 配 L1ITEM,UNLIST 配 UNLITEM 等)。',
'插入时根据系统“插入节点自动展开”偏好设置自动展开新节点及其子节点,方便直观查看和编辑。'
],
relatedQuestions: ['faq_insert_template', 'faq_node_tree_ops'],
contextTags: [
'LIST1',
'LIST2',
'LIST3',
'LIST4',
'LIST5',
'LIST6',
'LIST7',
'UNLIST',
'NUMLIST',
'L1ITEM',
'L2ITEM',
'L3ITEM',
'L4ITEM',
'L5ITEM',
'L6ITEM',
'L7ITEM',
'UNLITEM',
'NUMLITEM'
]
},
{
id: 'faq_insert_fragment', id: 'faq_insert_fragment',
category: 'insert', category: 'insert',
categoryName: '元素与排版', categoryName: '元素与排版',
......
import type { XmlNode } from '@/types/xmlNode' import type { XmlNode } from '@/types/xmlNode'
import type { EditorState } from './types' import type { EditorState } from './types'
import { useAppStore } from '@/store/app' import { useAppStore } from '@/store/app'
import { CHINESE_FIRST_PARA_TAGS, VIRTUAL_LAYOUT_TAGS } from '@/configs/xmlTags' import { CHINESE_FIRST_PARA_TAGS, VIRTUAL_LAYOUT_TAGS, LIST_CONTAINER_TO_ITEM_MAP } from '@/configs/xmlTags'
export const nodeSelectedRefs = new Set<string>() export const nodeSelectedRefs = new Set<string>()
...@@ -160,7 +160,7 @@ export const createTableStructure = (rows: number, cols: number, cellChildTags: ...@@ -160,7 +160,7 @@ export const createTableStructure = (rows: number, cols: number, cellChildTags:
} }
} }
const createDefaultNodeStructure = (tagName: string): XmlNode => { export const createDefaultNodeStructure = (tagName: string): XmlNode => {
const id = crypto.randomUUID() const id = crypto.randomUUID()
const attributes = createDefaultAttributes(tagName) const attributes = createDefaultAttributes(tagName)
...@@ -168,6 +168,30 @@ const createDefaultNodeStructure = (tagName: string): XmlNode => { ...@@ -168,6 +168,30 @@ const createDefaultNodeStructure = (tagName: string): XmlNode => {
return createTableStructure(3, 3, CHINESE_FIRST_PARA_TAGS) return createTableStructure(3, 3, CHINESE_FIRST_PARA_TAGS)
} }
const itemTag = LIST_CONTAINER_TO_ITEM_MAP[tagName]
if (itemTag) {
const itemId = crypto.randomUUID()
return {
id,
tagName,
attributes: { ...attributes },
children: [
{
id: itemId,
tagName: itemTag,
attributes: createDefaultAttributes(itemTag),
children: [],
textContent: '',
mixedContent: [],
parentId: id
}
],
textContent: '',
mixedContent: [],
parentId: null
}
}
if (tagName === 'GRAPHIC') { if (tagName === 'GRAPHIC') {
const sheetId = crypto.randomUUID() const sheetId = crypto.randomUUID()
return { return {
......
export { LIST_ITEM_TAGS, HEADER_TAGS, ROMAN_LOOKUP, ORDERED_LIST_ITEM_TAGS, EXCLUDE_WRAPPER_MERGED_TAGS } from '@/configs/xmlTags' export {
LIST_ITEM_TAGS,
HEADER_TAGS,
ROMAN_LOOKUP,
ORDERED_LIST_ITEM_TAGS,
EXCLUDE_WRAPPER_MERGED_TAGS,
ALL_LIST_TAGS,
STRUCTURAL_TAGS,
EMPTY_MARKER_TAGS,
HIDDEN_STRUCTURAL_TAGS,
COMPOSITE_CONTAINER_MAP,
ALERT_BLOCK_TAGS,
PARA_TAGS,
CB_COMPONENT_TAGS,
SUPER_SUB_TAGS,
NOTE_VARIANT_TAGS
} from '@/configs/xmlTags'
export type { CompositeContainerMap } from '@/configs/xmlTags'
import type { XmlNode } from '@/types/xmlNode' import type { XmlNode } from '@/types/xmlNode'
import { useEditorStore, nodeSelectedRefs } from '@/store/editor' import { useEditorStore, nodeSelectedRefs } from '@/store/editor'
import { LIST_ITEM_TAGS, HEADER_TAGS, ROMAN_LOOKUP, ORDERED_LIST_ITEM_TAGS } from '../constants' import {
import { ALERT_AND_EFF_TAGS, ALERT_BLOCK_TAGS, COMPOSITE_CONTAINER_MAP, INLINE_ELEMENTS } from '@/configs/xmlTags' LIST_ITEM_TAGS,
HEADER_TAGS,
ROMAN_LOOKUP,
ORDERED_LIST_ITEM_TAGS,
ALL_LIST_TAGS,
STRUCTURAL_TAGS,
EMPTY_MARKER_TAGS,
HIDDEN_STRUCTURAL_TAGS,
COMPOSITE_CONTAINER_MAP
} from '../constants'
import { ALERT_AND_EFF_TAGS, ALERT_BLOCK_TAGS, INLINE_ELEMENTS } from '@/configs/xmlTags'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { SHOW_SINGLE_CHILD_INDEX } from '@/configs' import { SHOW_SINGLE_CHILD_INDEX } from '@/configs'
import { getElementRule } from '@/utils/dtdManager'
// 警示块级节点集合(WARNING / CAUTION / NOTE),用于注入 insideAlert 上下文 // 警示块级节点集合(WARNING / CAUTION / NOTE),用于注入 insideAlert 上下文
const ALERT_ROOT_TAGS = new Set(ALERT_BLOCK_TAGS) const ALERT_ROOT_TAGS = new Set(ALERT_BLOCK_TAGS)
const INLINE_ELEMENTS_SET = new Set(INLINE_ELEMENTS) const INLINE_ELEMENTS_SET = new Set(INLINE_ELEMENTS)
const STRUCTURAL_TAG_SET = new Set(STRUCTURAL_TAGS)
const ALL_LIST_TAG_SET = new Set(ALL_LIST_TAGS)
const EMPTY_MARKER_SET = new Set(EMPTY_MARKER_TAGS)
const HIDDEN_STRUCTURAL_SET = new Set(HIDDEN_STRUCTURAL_TAGS)
export function useDocNodeRenderer(props: { export function useDocNodeRenderer(props: {
node: XmlNode node: XmlNode
...@@ -83,21 +98,48 @@ export function useDocNodeRenderer(props: { ...@@ -83,21 +98,48 @@ export function useDocNodeRenderer(props: {
return props.isInline || INLINE_ELEMENTS_SET.has(props.node.tagName) return props.isInline || INLINE_ELEMENTS_SET.has(props.node.tagName)
}) })
// 判断是否是列表容器 (需要排除表格、主题、选项以及通过复合 Map 特殊渲染的各种复合父节点/容器) // 判断是否是容器节点 (排除表格、主题、选项等独立渲染组件,包含列表容器与其它结构型容器)
const isContainer = computed(() => { const isContainer = computed(() => {
return ( if (renderInline.value) return false
!renderInline.value && const tag = props.node.tagName
props.node.children && if (
props.node.children.length > 0 && tag === 'TABLE' ||
props.node.tagName !== 'TABLE' && tag === 'SELECTION' ||
props.node.tagName !== 'SELECTION' && tag === 'SELECTION-ITEM' ||
props.node.tagName !== 'SELECTION-ITEM' && tag === 'TOPIC' ||
props.node.tagName !== 'TOPIC' && tag === 'PRETOPIC' ||
props.node.tagName !== 'PRETOPIC' && tag === 'CBLST' ||
props.node.tagName !== 'CBLST' && tag === 'EINLST' ||
props.node.tagName !== 'EINLST' && COMPOSITE_CONTAINER_MAP[tag] ||
!COMPOSITE_CONTAINER_MAP[props.node.tagName] LIST_ITEM_TAGS.includes(tag)
) ) {
return false
}
// 属于列表容器(LIST1-7, UNLIST, NUMLIST)或结构型容器,不论子节点是否为空,都必须作为容器节点处理
if (ALL_LIST_TAG_SET.has(tag) || STRUCTURAL_TAG_SET.has(tag)) {
return true
}
return Boolean(props.node.children && props.node.children.length > 0)
})
// 判断是否为非文本的结构型/容器型节点(无论如何都不能作为普通文本进行编辑)
const isNonTextStructuralTag = computed(() => {
const tag = props.node.tagName
if (
ALL_LIST_TAG_SET.has(tag) ||
STRUCTURAL_TAG_SET.has(tag) ||
LIST_ITEM_TAGS.includes(tag) ||
EMPTY_MARKER_SET.has(tag) ||
HIDDEN_STRUCTURAL_SET.has(tag) ||
Boolean(COMPOSITE_CONTAINER_MAP[tag])
) {
return true
}
const rule = getElementRule(tag)
if (rule?.contentModel?.raw) {
return !rule.contentModel.raw.includes('#PCDATA')
}
return false
}) })
// 判断是否是列表项目项 // 判断是否是列表项目项
...@@ -489,10 +531,22 @@ export function useDocNodeRenderer(props: { ...@@ -489,10 +531,22 @@ export function useDocNodeRenderer(props: {
return `${minVal || maxVal} ${unitVal}` return `${minVal || maxVal} ${unitVal}`
} }
const cbDataColumns = COMPOSITE_CONTAINER_MAP.CBDATA || []
const graphicTitleTags = COMPOSITE_CONTAINER_MAP.GRAPHIC || []
const isCbDataSelected = (cbData: XmlNode): boolean => {
const validTags = COMPOSITE_CONTAINER_MAP.CBDATA || []
return (
editorStore.selectedNodeId === cbData.id ||
cbData.children.some((c) => c.id === editorStore.selectedNodeId && validTags.includes(c.tagName))
)
}
return { return {
editorStore, editorStore,
isSelected, isSelected,
isContainer, isContainer,
isNonTextStructuralTag,
formatTorvalue, formatTorvalue,
renderInline, renderInline,
isListItem, isListItem,
...@@ -517,10 +571,22 @@ export function useDocNodeRenderer(props: { ...@@ -517,10 +571,22 @@ export function useDocNodeRenderer(props: {
hasNonCbDataChildren, hasNonCbDataChildren,
getNonCbDataChildren, getNonCbDataChildren,
getCbValue, getCbValue,
getHeaderValue getHeaderValue,
cbDataColumns,
graphicTitleTags,
isCbDataSelected
} }
} }
export const getEinDataSubChildren = (eindata: XmlNode): XmlNode[] => {
const validTags = COMPOSITE_CONTAINER_MAP.EINDATA || []
return eindata.children.filter((c) => validTags.includes(c.tagName))
}
export const getCompositeChildTags = (tagName: string): string[] => {
return COMPOSITE_CONTAINER_MAP[tagName] || []
}
export const getSplitListChildren = (children?: XmlNode[]) => { export const getSplitListChildren = (children?: XmlNode[]) => {
if (!children) return { alerts: [], normals: [] } if (!children) return { alerts: [], normals: [] }
const alertTagsSet = new Set(ALERT_AND_EFF_TAGS) const alertTagsSet = new Set(ALERT_AND_EFF_TAGS)
......
...@@ -638,7 +638,7 @@ ...@@ -638,7 +638,7 @@
<!-- 13. TED / CON (工具/消耗品数据组合) 统一配置化渲染 --> <!-- 13. TED / CON (工具/消耗品数据组合) 统一配置化渲染 -->
<template v-else-if="node.tagName === 'TED' || node.tagName === 'CON'"> <template v-else-if="node.tagName === 'TED' || node.tagName === 'CON'">
<span class="text-color1 cursor-pointer select-none inline-flex items-center space-x-1" @click.stop="selectNode(node.id)"> <span class="text-color1 cursor-pointer select-none inline-flex items-center space-x-1" @click.stop="selectNode(node.id)">
<template v-for="childTag in COMPOSITE_CONTAINER_MAP[node.tagName]" :key="childTag"> <template v-for="childTag in getCompositeChildTags(node.tagName)" :key="childTag">
<template v-if="node.children.find((c) => c.tagName === childTag)"> <template v-if="node.children.find((c) => c.tagName === childTag)">
<span v-if="childTag.endsWith('NBR')" class="text-color3 select-none mx-0.5"> <span v-if="childTag.endsWith('NBR')" class="text-color3 select-none mx-0.5">
{{ childTag === 'CONNBR' ? '(Material Ref. ' : '(' }} {{ childTag === 'CONNBR' ? '(Material Ref. ' : '(' }}
...@@ -654,7 +654,7 @@ ...@@ -654,7 +654,7 @@
<template v-else-if="node.tagName === 'EXPD'"> <template v-else-if="node.tagName === 'EXPD'">
<span class="text-color1 cursor-pointer select-none inline-flex items-center space-x-1" @click.stop="selectNode(node.id)"> <span class="text-color1 cursor-pointer select-none inline-flex items-center space-x-1" @click.stop="selectNode(node.id)">
<template v-if="node.children && node.children.length > 0"> <template v-if="node.children && node.children.length > 0">
<template v-for="childTag in COMPOSITE_CONTAINER_MAP[node.tagName]" :key="childTag"> <template v-for="childTag in getCompositeChildTags(node.tagName)" :key="childTag">
<template v-if="node.children.find((c) => c.tagName === childTag)"> <template v-if="node.children.find((c) => c.tagName === childTag)">
<span v-if="childTag === 'CSN'" class="text-color3 select-none mx-0.5">(Fin:</span> <span v-if="childTag === 'CSN'" class="text-color3 select-none mx-0.5">(Fin:</span>
<span v-else-if="childTag === 'ITEMNBR'" class="text-color3 select-none mx-0.5">(Nbr:</span> <span v-else-if="childTag === 'ITEMNBR'" class="text-color3 select-none mx-0.5">(Nbr:</span>
...@@ -773,10 +773,7 @@ ...@@ -773,10 +773,7 @@
? 'hover:bg-black/10' ? 'hover:bg-black/10'
: 'hover:bg-fill-3', : 'hover:bg-fill-3',
cbData.attributes?.MERGED === 'TRUE' || subList.attributes?.MERGED === 'TRUE' ? 'merged-cbdata' : '', cbData.attributes?.MERGED === 'TRUE' || subList.attributes?.MERGED === 'TRUE' ? 'merged-cbdata' : '',
editorStore.selectedNodeId === cbData.id || isCbDataSelected(cbData)
cbData.children.some(
(c) => c.id === editorStore.selectedNodeId && COMPOSITE_CONTAINER_MAP['CBDATA']?.includes(c.tagName)
)
? node.attributes?.MERGED === 'TRUE' ? node.attributes?.MERGED === 'TRUE'
? 'bg-black/15 font-bold' ? 'bg-black/15 font-bold'
: 'bg-primary/5 font-bold' : 'bg-primary/5 font-bold'
...@@ -791,7 +788,7 @@ ...@@ -791,7 +788,7 @@
@click.stop="selectNode(cbData.id)" @click.stop="selectNode(cbData.id)"
> >
<td <td
v-for="colTag in COMPOSITE_CONTAINER_MAP['CBDATA']" v-for="colTag in cbDataColumns"
:key="colTag" :key="colTag"
class="border px-2" class="border px-2"
:class="[ :class="[
...@@ -855,11 +852,8 @@ ...@@ -855,11 +852,8 @@
</template> </template>
<template v-else> <template v-else>
<div <div
contenteditable="true" class="text-sm leading-relaxed px-1 whitespace-pre-wrap min-h-[1.5em]"
class="text-sm leading-relaxed focus:outline-none focus:bg-fill-3 px-1 rounded whitespace-pre-wrap"
:class="[isInsideAlert ? 'text-inherit' : 'text-color2']" :class="[isInsideAlert ? 'text-inherit' : 'text-color2']"
@blur="handleTextBlur"
@keydown.enter="handleEnterKey"
v-text="node.textContent" v-text="node.textContent"
></div> ></div>
</template> </template>
...@@ -962,7 +956,7 @@ ...@@ -962,7 +956,7 @@
<!-- 图片标题编辑 --> <!-- 图片标题编辑 -->
<div class="w-full mt-3 text-center select-none flex items-center justify-center"> <div class="w-full mt-3 text-center select-none flex items-center justify-center">
<span class="text-xs text-color3 italic mr-1">图标题:</span> <span class="text-xs text-color3 italic mr-1">图标题:</span>
<template v-for="tag in COMPOSITE_CONTAINER_MAP['GRAPHIC']" :key="tag"> <template v-for="tag in graphicTitleTags" :key="tag">
<DocNodeRenderer <DocNodeRenderer
v-if="node.children.find((c) => c.tagName === tag)" v-if="node.children.find((c) => c.tagName === tag)"
:node="node.children.find((c) => c.tagName === tag)!" :node="node.children.find((c) => c.tagName === tag)!"
...@@ -1331,7 +1325,7 @@ ...@@ -1331,7 +1325,7 @@
<span class="text-color2 font-medium">FIN:</span> <span class="text-color2 font-medium">FIN:</span>
<template v-for="eindata in node.children" :key="eindata.id"> <template v-for="eindata in node.children" :key="eindata.id">
<template <template
v-for="child in eindata.children.filter((c) => COMPOSITE_CONTAINER_MAP['EINDATA']?.includes(c.tagName))" v-for="child in getEinDataSubChildren(eindata)"
:key="child.id" :key="child.id"
> >
<span <span
...@@ -1364,7 +1358,7 @@ ...@@ -1364,7 +1358,7 @@
/> />
<div class="pl-2"> <div class="pl-2">
<template <template
v-for="child in eindata.children.filter((c) => COMPOSITE_CONTAINER_MAP['EINDATA']?.includes(c.tagName))" v-for="child in getEinDataSubChildren(eindata)"
:key="child.id" :key="child.id"
> >
<span <span
...@@ -1505,6 +1499,9 @@ ...@@ -1505,6 +1499,9 @@
</div> </div>
</template> </template>
<!-- E4. 结构型/容器型非文本节点(静默处理,不渲染文本也不允许编辑) -->
<template v-else-if="isNonTextStructuralTag" />
<!-- 27. 兜底未匹配的普通文本标签 --> <!-- 27. 兜底未匹配的普通文本标签 -->
<template v-else> <template v-else>
<span <span
...@@ -1513,7 +1510,7 @@ ...@@ -1513,7 +1510,7 @@
class="focus:outline-none focus:bg-fill-3 px-0.5 rounded" class="focus:outline-none focus:bg-fill-3 px-0.5 rounded"
@blur="handleTextBlur" @blur="handleTextBlur"
@keydown.enter.prevent @keydown.enter.prevent
v-text="node.textContent || node.tagName" v-text="node.textContent"
></span> ></span>
<div <div
v-else v-else
...@@ -1521,7 +1518,7 @@ ...@@ -1521,7 +1518,7 @@
class="text-sm text-color2 leading-relaxed focus:outline-none focus:bg-fill-3 px-1 rounded" class="text-sm text-color2 leading-relaxed focus:outline-none focus:bg-fill-3 px-1 rounded"
@blur="handleTextBlur" @blur="handleTextBlur"
@keydown.enter.prevent @keydown.enter.prevent
v-text="node.textContent || node.tagName" v-text="node.textContent"
></div> ></div>
</template> </template>
<!-- 行内节点属性变更上标徽标:渲染在内容之后,不遮挡任何文字 --> <!-- 行内节点属性变更上标徽标:渲染在内容之后,不遮挡任何文字 -->
...@@ -1551,13 +1548,32 @@ ...@@ -1551,13 +1548,32 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Ref } from 'vue'
import DocNodeRenderer from './index.vue' import DocNodeRenderer from './index.vue'
import { ImageOutline, GridOutline, SyncOutline, LinkOutline, NavigateOutline } from '@vicons/ionicons5' import { ImageOutline, GridOutline, SyncOutline, LinkOutline, NavigateOutline } from '@vicons/ionicons5'
import type { XmlNode } from '@/types/xmlNode' import type { XmlNode } from '@/types/xmlNode'
import TableEditor from '../TableEditor/index.vue' import TableEditor from '../TableEditor/index.vue'
import { useDocNodeRenderer, getSplitListChildren, getCepTaskNumber, isAllEinDataSameEffect, getDiffWordClass } from './functionals' import {
useDocNodeRenderer,
getSplitListChildren,
getCepTaskNumber,
isAllEinDataSameEffect,
getDiffWordClass,
getEinDataSubChildren,
getCompositeChildTags
} from './functionals'
import { translatingNodeId } from '@/views/editor/components/NodeTree/functionals' import { translatingNodeId } from '@/views/editor/components/NodeTree/functionals'
import { currentJobCardMainData } from '@/views/editor/constants' import { currentJobCardMainData } from '@/views/editor/constants'
import {
ALERT_BLOCK_TAGS,
PARA_TAGS,
CB_COMPONENT_TAGS,
SUPER_SUB_TAGS,
EMPTY_MARKER_TAGS,
NOTE_VARIANT_TAGS,
HIDDEN_STRUCTURAL_TAGS,
EXCLUDE_WRAPPER_MERGED_TAGS
} from './constants'
const getGraphicKey = (node: XmlNode): string => { const getGraphicKey = (node: XmlNode): string => {
let key = node.attributes?.KEY || node.attributes?.GRAPHICKEY || node.attributes?.ID || '' let key = node.attributes?.KEY || node.attributes?.GRAPHICKEY || node.attributes?.ID || ''
...@@ -1649,17 +1665,6 @@ const getInheritedTitle = () => { ...@@ -1649,17 +1665,6 @@ const getInheritedTitle = () => {
const sheetNbr = props.node.attributes.SHEETNBR || '' const sheetNbr = props.node.attributes.SHEETNBR || ''
return sheetNbr ? `Figure - Sheet ${sheetNbr}` : '' return sheetNbr ? `Figure - Sheet ${sheetNbr}` : ''
} }
import {
ALERT_BLOCK_TAGS,
PARA_TAGS,
CB_COMPONENT_TAGS,
SUPER_SUB_TAGS,
COMPOSITE_CONTAINER_MAP,
EMPTY_MARKER_TAGS,
NOTE_VARIANT_TAGS,
HIDDEN_STRUCTURAL_TAGS,
EXCLUDE_WRAPPER_MERGED_TAGS
} from '@/configs/xmlTags'
// 动态载入各个定制环境下的 SignoffRenderer.vue // 动态载入各个定制环境下的 SignoffRenderer.vue
const customCode = import.meta.env.VITE_CUSTOM_CODE const customCode = import.meta.env.VITE_CUSTOM_CODE
...@@ -1699,7 +1704,6 @@ const props = withDefaults( ...@@ -1699,7 +1704,6 @@ const props = withDefaults(
} }
) )
import type { Ref } from 'vue'
const injectedDiffMode = inject<boolean | Ref<boolean>>('diffMode', false) const injectedDiffMode = inject<boolean | Ref<boolean>>('diffMode', false)
const injectedDiffModeValue = computed(() => (typeof injectedDiffMode === 'boolean' ? injectedDiffMode : injectedDiffMode.value)) const injectedDiffModeValue = computed(() => (typeof injectedDiffMode === 'boolean' ? injectedDiffMode : injectedDiffMode.value))
const isDiffMode = computed(() => props.diffMode || injectedDiffModeValue.value) const isDiffMode = computed(() => props.diffMode || injectedDiffModeValue.value)
...@@ -1709,6 +1713,7 @@ const { ...@@ -1709,6 +1713,7 @@ const {
editorStore, editorStore,
isSelected, isSelected,
isContainer, isContainer,
isNonTextStructuralTag,
formatTorvalue, formatTorvalue,
renderInline, renderInline,
isListItem, isListItem,
...@@ -1726,7 +1731,10 @@ const { ...@@ -1726,7 +1731,10 @@ const {
handleEnterKey, handleEnterKey,
handleMixedTextBlur, handleMixedTextBlur,
hasNonCbDataChildren, hasNonCbDataChildren,
getNonCbDataChildren getNonCbDataChildren,
cbDataColumns,
graphicTitleTags,
isCbDataSelected
} = useDocNodeRenderer(props) } = useDocNodeRenderer(props)
const selectNode = (id: string, forceScroll = false) => { const selectNode = (id: string, forceScroll = false) => {
......
...@@ -5,5 +5,5 @@ export interface ListItemModel { ...@@ -5,5 +5,5 @@ export interface ListItemModel {
text: string text: string
} }
// 静态有序列表标签定义 // 静态有序列表标签及段落标签定义
export { ORDERED_LIST_TAGS, ALL_LIST_TAGS } from '@/configs/xmlTags' export { ORDERED_LIST_TAGS, ALL_LIST_TAGS, LIST_CONTAINER_TO_ITEM_MAP, getListItemTag, PARA_TAGS } from '@/configs/xmlTags'
import { useEditorStore } from '@/store/editor' import { useEditorStore } from '@/store/editor'
import type { XmlNode } from '@/types/xmlNode' import type { XmlNode } from '@/types/xmlNode'
import type { ListItemModel } from '../constants' import type { ListItemModel } from '../constants'
import { ALL_LIST_TAGS } from '../constants' import { ALL_LIST_TAGS, getListItemTag, PARA_TAGS } from '../constants'
import { PARA_TAGS } from '@/configs/xmlTags'
/** /**
* 列表结构编辑器 (ListEditor) 组件专用 Hook 逻辑 * 列表结构编辑器 (ListEditor) 组件专用 Hook 逻辑
...@@ -18,18 +17,7 @@ export function useListEditor() { ...@@ -18,18 +17,7 @@ export function useListEditor() {
} }
const getItemTagName = (containerTag: string): string => { const getItemTagName = (containerTag: string): string => {
switch (containerTag) { return getListItemTag(containerTag) || 'L1ITEM'
case 'LIST1':
return 'L1ITEM'
case 'LIST2':
return 'L2ITEM'
case 'LIST3':
return 'L3ITEM'
case 'UNLIST':
return 'UNLITEM'
default:
return 'L1ITEM'
}
} }
const parseListItems = (node: XmlNode): ListItemModel[] => { const parseListItems = (node: XmlNode): ListItemModel[] => {
...@@ -76,7 +64,7 @@ export function useListEditor() { ...@@ -76,7 +64,7 @@ export function useListEditor() {
const newItem: XmlNode = { const newItem: XmlNode = {
id: itemId, id: itemId,
tagName: itemTag, tagName: itemTag,
attributes: {}, attributes: createDefaultAttributes(itemTag),
children: [], children: [],
textContent: '', textContent: '',
mixedContent: [], mixedContent: [],
...@@ -87,7 +75,7 @@ export function useListEditor() { ...@@ -87,7 +75,7 @@ export function useListEditor() {
const newPara: XmlNode = { const newPara: XmlNode = {
id: paraId, id: paraId,
tagName: 'PARA', tagName: 'PARA',
attributes: {}, attributes: createDefaultAttributes('PARA'),
children: [], children: [],
textContent: '新列表项', textContent: '新列表项',
mixedContent: [], mixedContent: [],
...@@ -97,6 +85,7 @@ export function useListEditor() { ...@@ -97,6 +85,7 @@ export function useListEditor() {
newItem.children.push(newPara) newItem.children.push(newPara)
node.children.push(newItem) node.children.push(newItem)
store.rebuildNodeMap()
store.triggerSync() store.triggerSync()
} }
...@@ -104,26 +93,28 @@ export function useListEditor() { ...@@ -104,26 +93,28 @@ export function useListEditor() {
const idx = node.children.findIndex((c) => c.id === itemId) const idx = node.children.findIndex((c) => c.id === itemId)
if (idx !== -1) { if (idx !== -1) {
node.children.splice(idx, 1) node.children.splice(idx, 1)
store.rebuildNodeMap()
store.triggerSync() store.triggerSync()
} }
} }
const moveItem = (node: XmlNode, itemId: string, direction: 'up' | 'down'): void => { const moveItem = (node: XmlNode, itemId: string, direction: 'up' | 'down'): void => {
const idx = node.children.findIndex((c) => c.id === itemId) const idx = node.children.findIndex((c) => c.id === itemId)
if (idx === -1) return if (idx !== -1) {
const target = direction === 'up' ? idx - 1 : idx + 1 const target = direction === 'up' ? idx - 1 : idx + 1
if (target < 0 || target >= node.children.length) return if (target >= 0 && target < node.children.length) {
const itemTag = getItemTagName(node.tagName) const itemTag = getItemTagName(node.tagName)
if (node.children[target].tagName !== itemTag) return if (node.children[target].tagName === itemTag) {
const temp = node.children[idx] const temp = node.children[idx]
node.children[idx] = node.children[target] node.children[idx] = node.children[target]
node.children[target] = temp node.children[target] = temp
store.rebuildNodeMap()
store.triggerSync() store.triggerSync()
} }
}
}
}
return { return {
parseListItems, parseListItems,
......
...@@ -4,6 +4,7 @@ import { useAppStore } from '@/store/app' ...@@ -4,6 +4,7 @@ import { useAppStore } from '@/store/app'
import type { XmlNode, DtdAttribute } from '@/types/xmlNode' import type { XmlNode, DtdAttribute } from '@/types/xmlNode'
import type { AttrDef } from '../../../constants' import type { AttrDef } from '../../../constants'
import { addNodeVisible, addNodeMode, addNodeTargetId, addNodeAllowedTags } from '../../../functionals' import { addNodeVisible, addNodeMode, addNodeTargetId, addNodeAllowedTags } from '../../../functionals'
import { LIST_CONTAINER_TO_ITEM_MAP } from '@/configs/xmlTags'
/** /**
* AddNodeModal 逻辑 Hook * AddNodeModal 逻辑 Hook
...@@ -313,6 +314,21 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) { ...@@ -313,6 +314,21 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) {
parentId: null parentId: null
} }
// 如果创建的是列表容器节点(ALL_LIST_TAGS: LIST1-7, UNLIST, NUMLIST),同时创建对应的子节点 ITEM
const itemTag = LIST_CONTAINER_TO_ITEM_MAP[newNode.tagName]
if (itemTag) {
const itemId = crypto.randomUUID()
newNode.children.push({
id: itemId,
tagName: itemTag,
attributes: createDefaultAttributes(itemTag),
children: [],
textContent: '',
mixedContent: [],
parentId: newId
})
}
if (addNodeMode.value === 'child') { if (addNodeMode.value === 'child') {
newNode.parentId = targetNode.id newNode.parentId = targetNode.id
targetNode.children.push(newNode) targetNode.children.push(newNode)
...@@ -367,6 +383,15 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) { ...@@ -367,6 +383,15 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) {
window.$message?.success(`成功插入节点 <${form.tagName}>`) window.$message?.success(`成功插入节点 <${form.tagName}>`)
addNodeVisible.value = false addNodeVisible.value = false
// 若新节点有子节点(如 LIST*/TABLE 等容器类节点),需在 nextTick 之后再次确保其展开状态。
// 原因:syncTreeSelection 的异步 watcher 会通过 emit('update:expandedKeys') 整体替换
// store.expandedKeys,可能将 handleNodeInsertion 刚写入的 newId 展开键覆盖掉。
if (appStore.autoExpandOnInsert && newNode.children && newNode.children.length > 0) {
nextTick(() => {
store.expandNodes([newId])
})
}
if (!appStore.autoExpandOnInsert) { if (!appStore.autoExpandOnInsert) {
nextTick(() => { nextTick(() => {
store.skipExpandOnSelect = false store.skipExpandOnSelect = false
......
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