Commit 0a57dc1b by pangchong

style: 优化REFBLOCK子项的分隔符,隐藏重复的ALL适用性标签并优化HTML清理逻辑

parent 082136f7
...@@ -70,7 +70,10 @@ export const INLINE_ELEMENTS = [ ...@@ -70,7 +70,10 @@ export const INLINE_ELEMENTS = [
'REVEND', 'REVEND',
'EXPD', 'EXPD',
'EXPDNAME', 'EXPDNAME',
'ITEMNBR' 'ITEMNBR',
'RECORD',
'RECORD-ITEMS',
'RECORD-ITEM'
] ]
// 段落类节点标签(PARA 为英文段落,PARAC 为中文段落) // 段落类节点标签(PARA 为英文段落,PARAC 为中文段落)
......
<template> <template>
<!-- 触发按钮 --> <!-- 触发按钮 -->
<slot name="trigger" :open="() => appStore.settingsOpen = true">
<CommonButton quaternary circle @click="appStore.settingsOpen = true"> <CommonButton quaternary circle @click="appStore.settingsOpen = true">
<template #icon> <template #icon>
<n-icon><settings-outline /></n-icon> <n-icon><settings-outline /></n-icon>
</template> </template>
</CommonButton> </CommonButton>
</slot>
<!-- 偏好设置抽屉 --> <!-- 偏好设置抽屉 -->
<n-drawer <n-drawer
......
...@@ -284,8 +284,34 @@ const createDefaultNodeStructure = (tagName: string): XmlNode => { ...@@ -284,8 +284,34 @@ const createDefaultNodeStructure = (tagName: string): XmlNode => {
} }
} }
if (tagName === 'RECORD-LINE') {
const recordId = crypto.randomUUID()
return {
id,
tagName: 'RECORD-LINE',
attributes: { ...attributes },
children: [
{
id: recordId,
tagName: 'RECORD',
attributes: {
MULTI: 'N',
MANDATORY: 'Y',
'MES-TYPE': 'TEXT'
},
children: [],
textContent: '',
mixedContent: [],
parentId: id
}
],
textContent: '',
mixedContent: [],
parentId: null
}
}
let defaultText = '' let defaultText = ''
if (tagName === 'RECORD-LINE') defaultText = '记录项:__________________'
if (tagName === 'DATE') defaultText = new Date().toISOString().split('T')[0] if (tagName === 'DATE') defaultText = new Date().toISOString().split('T')[0]
if (tagName === 'UNIT-RECORD') defaultText = '测量值:____ 毫米' if (tagName === 'UNIT-RECORD') defaultText = '测量值:____ 毫米'
......
...@@ -56,64 +56,6 @@ export function useDocNodeRenderer(props: { node: XmlNode; parent?: XmlNode | nu ...@@ -56,64 +56,6 @@ export function useDocNodeRenderer(props: { node: XmlNode; parent?: XmlNode | nu
provide('insideChinese', true) provide('insideChinese', true)
} }
const shouldHideDuplicate = computed(() => {
const node = props.node
const parent = props.parent
if (!parent) return false
const isParaPair = node.tagName === 'PARA' || node.tagName === 'PARAC'
const isTitlePair = node.tagName === 'TITLE' || node.tagName === 'TITLEC'
if (!isParaPair && !isTitlePair) return false
const siblingTagName = node.tagName === 'PARA' ? 'PARAC' : node.tagName === 'PARAC' ? 'PARA' : node.tagName === 'TITLE' ? 'TITLEC' : 'TITLE'
const siblings = parent.children.filter((c) => c.tagName === siblingTagName)
const getEffectiveText = (n: XmlNode): string => {
if (n.textContent) return n.textContent.trim()
if (n.mixedContent && n.mixedContent.length > 0) {
return n.mixedContent
.filter((item) => item.type === 'text')
.map((item) => item.text || '')
.join('')
.trim()
}
if (n.children && n.children.length > 0) {
const getDeep = (nn: XmlNode): string => {
if (nn.textContent) return nn.textContent.trim()
if (nn.mixedContent && nn.mixedContent.length > 0) {
return nn.mixedContent
.filter((item) => item.type === 'text')
.map((item) => item.text || '')
.join('')
.trim()
}
if (nn.children && nn.children.length > 0) {
return nn.children.map(getDeep).join('').trim()
}
return ''
}
return getDeep(n)
}
return ''
}
const currentEffectiveText = getEffectiveText(node)
if (!currentEffectiveText) return false
const hasDuplicateSibling = siblings.some((sib) => {
return getEffectiveText(sib) === currentEffectiveText
})
if (hasDuplicateSibling) {
// Hide the English tag (PARA or TITLE) and keep the Chinese one (PARAC or TITLEC)
if (node.tagName === 'PARA' || node.tagName === 'TITLE') {
return true
}
}
return false
})
const renderInline = computed(() => { const renderInline = computed(() => {
return props.isInline || INLINE_ELEMENTS_SET.has(props.node.tagName) return props.isInline || INLINE_ELEMENTS_SET.has(props.node.tagName)
...@@ -158,12 +100,65 @@ export function useDocNodeRenderer(props: { node: XmlNode; parent?: XmlNode | nu ...@@ -158,12 +100,65 @@ export function useDocNodeRenderer(props: { node: XmlNode; parent?: XmlNode | nu
if (!eff) return 'ALL' if (!eff) return 'ALL'
const cleaned = eff.replace(/\s+/g, '') const cleaned = eff.replace(/\s+/g, '')
if (cleaned === '001999') return 'ALL' if (cleaned === '001999') return 'ALL'
// 如果是纯数字,且长度是6的倍数
if (/^\d+$/.test(cleaned) && cleaned.length % 6 === 0) {
const parts: string[] = []
for (let i = 0; i < cleaned.length; i += 6) {
const block = cleaned.substring(i, i + 6)
if (block === '001999') {
parts.push('ALL')
continue
}
const start = block.substring(0, 3)
const end = block.substring(3)
if (start === end) {
parts.push(start)
} else {
parts.push(`${start}-${end}`)
}
}
return parts.join(', ')
}
// 如果是纯数字且长度是3位(单个飞机)
if (/^\d{3}$/.test(cleaned)) {
return cleaned
}
// 回退逻辑:如果长度为6
if (cleaned.length === 6) { if (cleaned.length === 6) {
return `${cleaned.substring(0, 3)}-${cleaned.substring(3)}` return `${cleaned.substring(0, 3)}-${cleaned.substring(3)}`
} }
return cleaned return cleaned
} }
// 判断是否需要显示参考/引用前缀,若内容本身以引用词汇开头则不显示前缀
const shouldShowPrefix = (text: string | undefined): boolean => {
if (!text) return true
const trimmed = text.trim().toLowerCase()
if (
trimmed.startsWith('refer') ||
trimmed.startsWith('ref') ||
trimmed.startsWith('see') ||
trimmed.startsWith('fig') ||
trimmed.startsWith('view')
) {
return false
}
if (
trimmed.startsWith('参考') ||
trimmed.startsWith('参阅') ||
trimmed.startsWith('参见') ||
trimmed.startsWith('见') ||
trimmed.startsWith('图')
) {
return false
}
return true
}
// 罗马数字转换辅助 // 罗马数字转换辅助
const romanize = (num: number): string => { const romanize = (num: number): string => {
let roman = '' let roman = ''
...@@ -434,8 +429,8 @@ export function useDocNodeRenderer(props: { node: XmlNode; parent?: XmlNode | nu ...@@ -434,8 +429,8 @@ export function useDocNodeRenderer(props: { node: XmlNode; parent?: XmlNode | nu
isHeaderTag, isHeaderTag,
isInsideAlert, isInsideAlert,
isChineseContext, isChineseContext,
shouldHideDuplicate,
formatEff, formatEff,
shouldShowPrefix,
getListBullet, getListBullet,
getTopicSeqNum, getTopicSeqNum,
getTopicTitleNodes, getTopicTitleNodes,
......
<template> <template>
<component <component
v-if="!shouldHideDuplicate"
:is="renderInline ? 'span' : 'div'" :is="renderInline ? 'span' : 'div'"
:data-node-id="node.id" :data-node-id="node.id"
:data-tag-name="node.tagName"
class="doc-node-wrapper relative transition-all" class="doc-node-wrapper relative transition-all"
:class="[ :class="[
renderInline ? 'inline align-baseline mx-0.5' : 'block my-1', renderInline ? 'inline align-baseline mx-0.5' : 'block my-1',
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
}" }"
> >
<span <span
class="absolute left-0 top-0 font-bold underline select-none" class="absolute left-0 top-0 pt-1 leading-relaxed font-bold underline select-none"
:style="{ :style="{
color: node.tagName === 'WARNING' ? 'red' : node.tagName === 'CAUTION' ? '#ff6a00' : 'blue' color: node.tagName === 'WARNING' ? 'red' : node.tagName === 'CAUTION' ? '#ff6a00' : 'blue'
}" }"
...@@ -45,6 +45,7 @@ ...@@ -45,6 +45,7 @@
<!-- 2. EFFECT / CONEFFECT (适用性) 特殊处理 --> <!-- 2. EFFECT / CONEFFECT (适用性) 特殊处理 -->
<template v-else-if="node.tagName === 'EFFECT' || node.tagName === 'CONEFFECT'"> <template v-else-if="node.tagName === 'EFFECT' || node.tagName === 'CONEFFECT'">
<template v-if="formatEff(node.attributes.EFFRG || node.textContent) !== 'ALL' || isSelected">
<span v-if="renderInline" class="font-bold text-xs select-none py-1 uppercase mx-1 italic" style="color: red"> <span v-if="renderInline" class="font-bold text-xs select-none py-1 uppercase mx-1 italic" style="color: red">
** {{ node.tagName === 'EFFECT' ? 'ON A/C' : 'CONF' }}: {{ formatEff(node.attributes.EFFRG || node.textContent) }} ** {{ node.tagName === 'EFFECT' ? 'ON A/C' : 'CONF' }}: {{ formatEff(node.attributes.EFFRG || node.textContent) }}
</span> </span>
...@@ -56,6 +57,7 @@ ...@@ -56,6 +57,7 @@
** {{ node.tagName === 'EFFECT' ? 'ON A/C' : 'CONF' }}: {{ formatEff(node.attributes.EFFRG || node.textContent) }} ** {{ node.tagName === 'EFFECT' ? 'ON A/C' : 'CONF' }}: {{ formatEff(node.attributes.EFFRG || node.textContent) }}
</div> </div>
</template> </template>
</template>
<!-- 3. SBEFF / SBEFFC (服务通告适用性) 处理 --> <!-- 3. SBEFF / SBEFFC (服务通告适用性) 处理 -->
<template v-else-if="node.tagName === 'SBEFF' || node.tagName === 'SBEFFC'"> <template v-else-if="node.tagName === 'SBEFF' || node.tagName === 'SBEFFC'">
...@@ -95,9 +97,13 @@ ...@@ -95,9 +97,13 @@
<!-- 6. PARA / PARAC 段落文本处理 --> <!-- 6. PARA / PARAC 段落文本处理 -->
<template v-else-if="PARA_SET.has(node.tagName)"> <template v-else-if="PARA_SET.has(node.tagName)">
<div <component
:is="renderInline ? 'span' : 'div'"
class="text-sm leading-relaxed py-1 flex flex-wrap items-center" class="text-sm leading-relaxed py-1 flex flex-wrap items-center"
:class="[isInsideAlert ? 'text-inherit' : 'text-color2']" :class="[
isInsideAlert ? 'text-inherit' : 'text-color2',
renderInline ? 'inline mx-0.5' : 'flex'
]"
:style="{ justifyContent: 'var(--cell-align, flex-start)' }" :style="{ justifyContent: 'var(--cell-align, flex-start)' }"
> >
<template v-if="node.mixedContent && node.mixedContent.length > 0"> <template v-if="node.mixedContent && node.mixedContent.length > 0">
...@@ -138,23 +144,26 @@ ...@@ -138,23 +144,26 @@
</template> </template>
<!-- 否则,如果是一个纯文本的段落,直接展示和编辑文本 --> <!-- 否则,如果是一个纯文本的段落,直接展示和编辑文本 -->
<template v-else> <template v-else>
<div <component
:is="renderInline ? 'span' : 'div'"
contenteditable="true" contenteditable="true"
class="w-full focus:outline-none focus:bg-fill-3 px-1 rounded transition-colors" class="focus:outline-none focus:bg-fill-3 px-1 rounded transition-colors"
:class="[renderInline ? 'inline mx-0.5' : 'w-full']"
@blur="handleTextBlur" @blur="handleTextBlur"
v-text="node.textContent" v-text="node.textContent"
></div> ></component>
</template> </template>
</div> </component>
</template> </template>
<!-- 7. REFBLOCK 特殊处理 --> <!-- 7. REFBLOCK 特殊处理 -->
<template v-else-if="node.tagName === 'REFBLOCK'"> <template v-else-if="node.tagName === 'REFBLOCK'">
<span class="inline-ref-block font-medium" style="color: blue"> <span class="inline-ref-block font-medium" style="color: blue">
{{ isChineseContext ? '(参考: ' : '(Ref: ' }}
<template v-if="node.mixedContent && node.mixedContent.length > 0"> <template v-if="node.mixedContent && node.mixedContent.length > 0">
<template v-for="(item, index) in node.mixedContent" :key="index"> <template v-for="(item, index) in node.mixedContent" :key="index">
<span <span
v-if="item.type === 'text'" v-if="item.type === 'text' && item.text && item.text.trim()"
:data-node-id="`${node.id}-txt-${index}`" :data-node-id="`${node.id}-txt-${index}`"
contenteditable="true" contenteditable="true"
class="focus:outline-none px-0.5 rounded inline transition-all" class="focus:outline-none px-0.5 rounded inline transition-all"
...@@ -187,6 +196,7 @@ ...@@ -187,6 +196,7 @@
v-text="node.textContent" v-text="node.textContent"
></span> ></span>
</template> </template>
{{ ')' }}
</span> </span>
</template> </template>
...@@ -198,7 +208,7 @@ ...@@ -198,7 +208,7 @@
@click.stop="editorStore.setSelectedNodeId(node.id)" @click.stop="editorStore.setSelectedNodeId(node.id)"
> >
<template v-if="!insideRefBlock"> <template v-if="!insideRefBlock">
{{ isChineseContext ? '(参考: ' : '(Ref: ' }} {{ shouldShowPrefix(node.textContent) ? (isChineseContext ? '(参考: ' : '(Ref: ') : '(' }}
<template v-if="node.mixedContent && node.mixedContent.length > 0"> <template v-if="node.mixedContent && node.mixedContent.length > 0">
<template v-for="(item, index) in node.mixedContent" :key="index"> <template v-for="(item, index) in node.mixedContent" :key="index">
<span <span
...@@ -288,7 +298,7 @@ ...@@ -288,7 +298,7 @@
@click.stop="editorStore.setSelectedNodeId(node.id)" @click.stop="editorStore.setSelectedNodeId(node.id)"
> >
<template v-if="!insideRefBlock"> <template v-if="!insideRefBlock">
{{ isChineseContext ? '(参考: ' : '(Ref: ' }} {{ shouldShowPrefix(node.textContent || node.attributes.REFLOC) ? (isChineseContext ? '(参考: ' : '(Ref: ') : '(' }}
<span <span
contenteditable="true" contenteditable="true"
class="focus:outline-none focus:bg-fill-3 px-0.5 rounded inline" class="focus:outline-none focus:bg-fill-3 px-0.5 rounded inline"
...@@ -316,7 +326,7 @@ ...@@ -316,7 +326,7 @@
@click.stop="editorStore.setSelectedNodeId(node.id)" @click.stop="editorStore.setSelectedNodeId(node.id)"
> >
<template v-if="!insideRefBlock"> <template v-if="!insideRefBlock">
{{ isChineseContext ? '(参考: ' : '(Ref: ' }} {{ shouldShowPrefix(node.textContent) ? (isChineseContext ? '(参考: ' : '(Ref: ') : '(' }}
<template v-if="node.mixedContent && node.mixedContent.length > 0"> <template v-if="node.mixedContent && node.mixedContent.length > 0">
<template v-for="(item, index) in node.mixedContent" :key="index"> <template v-for="(item, index) in node.mixedContent" :key="index">
<span <span
...@@ -667,18 +677,44 @@ ...@@ -667,18 +677,44 @@
<!-- 20. RECORD-LINE (记录项) 处理 --> <!-- 20. RECORD-LINE (记录项) 处理 -->
<template v-else-if="node.tagName === 'RECORD-LINE'"> <template v-else-if="node.tagName === 'RECORD-LINE'">
<div class="my-2 p-2 bg-fill-3 rounded border border-divider flex items-center space-x-2"> <div class="my-1 py-1">
<CommonTag type="info" size="small" round>记录项</CommonTag> <div class="flex flex-wrap items-center gap-1.5 text-sm text-color2">
<div <template v-if="node.children && node.children.length > 0">
<DocNodeRenderer
v-for="child in node.children"
:key="child.id"
:node="child"
:parent="node"
is-inline
/>
</template>
<template v-else-if="node.textContent && node.textContent.trim()">
<span
contenteditable="true" contenteditable="true"
class="flex-1 text-sm text-color2 font-medium focus:outline-none focus:bg-fill-2 px-1 rounded" class="focus:outline-none focus:bg-fill-3 px-0.5 rounded"
@blur="handleTextBlur" @blur="handleTextBlur"
v-text="node.textContent" v-text="node.textContent"
></div> ></span>
<div class="text-xs text-color3 select-none">[已预留输入线]</div> </template>
<span v-else class="text-xs text-color3 select-none">[空记录项,请从左侧树或右键菜单插入子节点]</span>
</div>
</div> </div>
</template> </template>
<!-- 20.5 RECORD (记录输入框) 处理 -->
<template v-else-if="node.tagName === 'RECORD'">
<span
contenteditable="true"
class="inline-block border-b border-color3 hover:border-primary focus:border-primary min-w-[80px] text-center focus:outline-none focus:bg-fill-2 px-1 mx-1 text-sm font-mono text-primary font-bold transition-all"
:style="{
width: node.attributes.WIDTH ? (node.attributes.WIDTH.includes('px') || node.attributes.WIDTH.includes('%') ? node.attributes.WIDTH : `${node.attributes.WIDTH}px`) : 'auto',
minWidth: node.attributes.WIDTH ? '0' : '80px'
}"
@blur="handleTextBlur"
v-text="node.textContent"
></span>
</template>
<!-- 21. UNIT-RECORD (单位记录项) 处理 --> <!-- 21. UNIT-RECORD (单位记录项) 处理 -->
<template v-else-if="node.tagName === 'UNIT-RECORD'"> <template v-else-if="node.tagName === 'UNIT-RECORD'">
<div class="my-2 p-2 bg-fill-3 rounded border border-divider flex items-center justify-between"> <div class="my-2 p-2 bg-fill-3 rounded border border-divider flex items-center justify-between">
...@@ -907,7 +943,7 @@ ...@@ -907,7 +943,7 @@
<!-- 结构上等同于 NOTE,但含义不同,用不同的标签文字区分 --> <!-- 结构上等同于 NOTE,但含义不同,用不同的标签文字区分 -->
<template v-else-if="NOTE_VARIANT_SET.has(node.tagName)"> <template v-else-if="NOTE_VARIANT_SET.has(node.tagName)">
<div class="relative my-3 text-sm" style="padding-left: 80px; color: blue"> <div class="relative my-3 text-sm" style="padding-left: 80px; color: blue">
<span class="absolute left-0 top-0 font-bold underline select-none" style="color: blue"> <span class="absolute left-0 top-0 pt-1 leading-relaxed font-bold underline select-none" style="color: blue">
{{ {{
node.tagName === 'HNANOTE' node.tagName === 'HNANOTE'
? '操作注意 HNANOTE:' ? '操作注意 HNANOTE:'
...@@ -1017,8 +1053,8 @@ const { ...@@ -1017,8 +1053,8 @@ const {
isHeaderTag, isHeaderTag,
isInsideAlert, isInsideAlert,
isChineseContext, isChineseContext,
shouldHideDuplicate,
formatEff, formatEff,
shouldShowPrefix,
getListBullet, getListBullet,
getTopicSeqNum, getTopicSeqNum,
getTopicTitleNodes, getTopicTitleNodes,
...@@ -1039,4 +1075,8 @@ const { ...@@ -1039,4 +1075,8 @@ const {
.doc-node-wrapper:hover { .doc-node-wrapper:hover {
background-color: var(--n-color-hover); background-color: var(--n-color-hover);
} }
.inline-ref-block > *:not(:first-child)::before {
content: ", ";
color: inherit;
}
</style> </style>
import { ImageOutline, GridOutline, DocumentTextOutline, CreateOutline } from '@vicons/ionicons5' import { ImageOutline, GridOutline, DocumentTextOutline, CreateOutline, RemoveOutline } from '@vicons/ionicons5'
/** /**
* EditorToolbar 组件级静态常量 * EditorToolbar 组件级静态常量
...@@ -9,5 +9,6 @@ export const GREEN_BUTTONS: any[] = [ ...@@ -9,5 +9,6 @@ export const GREEN_BUTTONS: any[] = [
// { label: '插入图片', tag: 'GRAPHIC', icon: ImageOutline }, // { label: '插入图片', tag: 'GRAPHIC', icon: ImageOutline },
{ label: '插入表格', tag: 'TABLE', icon: GridOutline }, { label: '插入表格', tag: 'TABLE', icon: GridOutline },
// { label: '插入模板', tag: 'PRETOPIC', icon: DocumentTextOutline }, // { label: '插入模板', tag: 'PRETOPIC', icon: DocumentTextOutline },
{ label: '插入签字点', tag: 'SIGNOFF', icon: CreateOutline } { label: '插入签字点', tag: 'SIGNOFF', icon: CreateOutline },
{ label: '插入下划线', tag: 'RECORD-LINE', icon: RemoveOutline }
] ]
<template> <template>
<div <div
class="editor-toolbar flex flex-nowrap items-center px-4 py-2 border-b border-divider bg-card select-none w-full overflow-x-auto overflow-y-hidden h-[54px]" class="editor-toolbar flex flex-nowrap items-center px-3 border-b border-divider bg-card select-none w-full overflow-x-auto overflow-y-hidden h-[42px] gap-1.5"
> >
<!-- 左侧:内容插入操作区 (强制不收缩) --> <!-- 组 1:插入位置模式切换 (仿富文本编辑器对齐选择器) -->
<div class="flex items-center gap-2 flex-shrink-0"> <div class="flex items-center gap-1.5 flex-shrink-0">
<!-- 插入位置设置 --> <span class="text-xs text-color3 font-medium">插入模式</span>
<div class="flex items-center gap-1.5 bg-fill-3 px-2.5 py-1 rounded-md border border-divider text-xs flex-shrink-0"> <div class="flex items-center bg-fill-2 p-0.5 rounded border border-divider">
<span class="font-medium text-color3">插入</span> <button
<span class="font-semibold text-color2 w-8 text-center" :class="insertBelow ? 'text-primary' : 'text-color2'"> type="button"
{{ insertBelow ? '下方' : '内部' }} class="px-2.5 h-6 text-xs font-semibold rounded transition-all select-none focus:outline-none"
</span> :class="!insertBelow ? 'bg-card text-primary shadow-[0_1px_2px_rgba(0,0,0,0.06)]' : 'text-color2 hover:bg-fill-3'"
<n-switch v-model:value="insertBelow" size="small" /> @click="insertBelow = false"
>
内部
</button>
<button
type="button"
class="px-2.5 h-6 text-xs font-semibold rounded transition-all select-none focus:outline-none"
:class="insertBelow ? 'bg-card text-primary shadow-[0_1px_2px_rgba(0,0,0,0.06)]' : 'text-color2 hover:bg-fill-3'"
@click="insertBelow = true"
>
下方
</button>
</div>
</div> </div>
<n-divider vertical class="!mx-0 flex-shrink-0" /> <div class="w-[1px] h-4 bg-divider mx-1 flex-shrink-0"></div>
<!-- 插入元素按钮组 (这里同为 flex-shrink-0) --> <!-- 组 2:插入元素按钮组 -->
<div class="flex items-center gap-1 flex-shrink-0"> <div class="flex items-center gap-0.5 flex-shrink-0">
<CommonButton <button
v-for="btn in GREEN_BUTTONS" v-for="btn in GREEN_BUTTONS"
:key="btn.tag" :key="btn.tag"
secondary type="button"
size="small" class="toolbar-btn flex items-center gap-1.5 px-2.5 h-7 rounded text-color2 hover:bg-fill-2 active:bg-fill-3 transition-colors select-none focus:outline-none flex-shrink-0"
class="insert-btn flex-shrink-0"
@click="handleInsert(btn.tag)" @click="handleInsert(btn.tag)"
> >
<template #icon> <n-icon class="text-base"><component :is="btn.icon" /></n-icon>
<n-icon><component :is="btn.icon" /></n-icon> <span class="text-xs font-medium">{{ btn.label.replace('插入', '') }}</span>
</template> </button>
{{ btn.label.replace('插入', '') }}
</CommonButton> <!-- 插入 XML 片段 -->
<button
<!-- 新增:插入 XML 片段按钮 --> type="button"
<CommonButton secondary size="small" class="insert-btn flex-shrink-0" @click="insertFragmentModalRef?.open(insertBelow)"> class="toolbar-btn flex items-center gap-1.5 px-2.5 h-7 rounded text-color2 hover:bg-fill-2 active:bg-fill-3 transition-colors select-none focus:outline-none flex-shrink-0"
<template #icon> @click="insertFragmentModalRef?.open(insertBelow)"
<n-icon><code-working-outline /></n-icon> >
</template> <n-icon class="text-base"><code-working-outline /></n-icon>
XML片段 <span class="text-xs font-medium">XML片段</span>
</CommonButton> </button>
</div>
</div> </div>
<!-- 右侧:全局工具与管理操作区 (固定宽度,ml-auto 靠右,强制不收缩) --> <div class="w-[1px] h-4 bg-divider mx-1 flex-shrink-0"></div>
<div class="flex items-center gap-1.5 flex-shrink-0 ml-auto">
<!-- 翻译辅助组 --> <!-- 组 3:翻译辅助组 -->
<div class="flex items-center gap-1 bg-fill-3 px-1 py-0.5 rounded-md border border-divider flex-shrink-0"> <div class="flex items-center gap-0.5 flex-shrink-0">
<CommonButton <button
size="small" type="button"
quaternary class="toolbar-btn flex items-center gap-1.5 px-2.5 h-7 rounded transition-colors select-none focus:outline-none flex-shrink-0"
class="util-btn" :class="[
:type="batchTranslateModalRef?.completed ? 'success' : batchTranslateModalRef?.loading ? 'primary' : 'default'" batchTranslateModalRef?.completed
? 'text-success bg-success/10 hover:bg-success/15'
: batchTranslateModalRef?.loading
? 'text-primary bg-primary/10 hover:bg-primary/15'
: 'text-color2 hover:bg-fill-2 active:bg-fill-3'
]"
@click="handleTranslate('batch')" @click="handleTranslate('batch')"
> >
<template #icon> <n-icon class="text-base">
<n-icon>
<sync-outline v-if="batchTranslateModalRef?.loading" class="animate-spin text-primary" /> <sync-outline v-if="batchTranslateModalRef?.loading" class="animate-spin text-primary" />
<checkmark-circle-outline v-else-if="batchTranslateModalRef?.completed" class="text-success" /> <checkmark-circle-outline v-else-if="batchTranslateModalRef?.completed" class="text-success" />
<language-outline v-else /> <language-outline v-else />
</n-icon> </n-icon>
</template> <span class="text-xs font-medium">
<span v-if="batchTranslateModalRef?.loading">批量翻译 (进行中...)</span> {{ batchTranslateModalRef?.loading ? '翻译中…' : batchTranslateModalRef?.completed ? '翻译完成' : '批量翻译' }}
<span v-else-if="batchTranslateModalRef?.completed">批量翻译 (已完成)</span> </span>
<span v-else>批量翻译</span> </button>
</CommonButton> <button
<CommonButton size="small" quaternary class="util-btn" @click="handleTranslate('extract')"> type="button"
<template #icon> class="toolbar-btn flex items-center gap-1.5 px-2.5 h-7 rounded text-color2 hover:bg-fill-2 active:bg-fill-3 transition-colors select-none focus:outline-none flex-shrink-0"
<n-icon><download-outline /></n-icon> @click="handleTranslate('extract')"
</template> >
提取翻译 <n-icon class="text-base"><download-outline /></n-icon>
</CommonButton> <span class="text-xs font-medium">提取翻译</span>
<CommonButton size="small" quaternary class="util-btn" @click="handleTranslate('search')"> </button>
<template #icon> <button
<n-icon><search-outline /></n-icon> type="button"
</template> class="toolbar-btn flex items-center gap-1.5 px-2.5 h-7 rounded text-color2 hover:bg-fill-2 active:bg-fill-3 transition-colors select-none focus:outline-none flex-shrink-0"
搜索翻译 @click="handleTranslate('search')"
</CommonButton> >
<n-icon class="text-base"><search-outline /></n-icon>
<span class="text-xs font-medium">搜索翻译</span>
</button>
</div> </div>
<n-divider vertical class="!mx-0 flex-shrink-0" /> <div class="w-[1px] h-4 bg-divider mx-1 flex-shrink-0"></div>
<CommonButton size="small" secondary class="xml-btn flex-shrink-0" :disabled="isUploading" :loading="isUploading" @click="triggerUpload">
<template #icon>
<n-icon><cloud-upload-outline /></n-icon>
</template>
{{ isUploading ? '解析中…' : '导入 XML' }}
</CommonButton>
<CommonButton size="small" secondary class="xml-btn flex-shrink-0" @click="emit('export')"> <!-- 组 4:文件管理与导入导出 -->
<template #icon> <div class="flex items-center gap-0.5 flex-shrink-0">
<n-icon><cloud-download-outline /></n-icon> <button
</template> type="button"
导出 XML class="toolbar-btn flex items-center gap-1.5 px-2.5 h-7 rounded text-color2 hover:bg-fill-2 active:bg-fill-3 transition-colors select-none focus:outline-none flex-shrink-0 disabled:opacity-50"
</CommonButton> :disabled="isUploading"
@click="triggerUpload"
<CommonButton size="small" type="primary" class="preview-btn flex-shrink-0" @click="emit('download-html')"> >
<template #icon> <n-icon class="text-base">
<n-icon><eye-outline /></n-icon> <sync-outline v-if="isUploading" class="animate-spin" />
</template> <cloud-upload-outline v-else />
下载html </n-icon>
</CommonButton> <span class="text-xs font-medium">{{ isUploading ? '导入中…' : '导入 XML' }}</span>
<CommonButton size="small" type="primary" class="preview-btn flex-shrink-0" @click="emit('preview')"> </button>
<template #icon> <button
<n-icon><eye-outline /></n-icon> type="button"
</template> class="toolbar-btn flex items-center gap-1.5 px-2.5 h-7 rounded text-color2 hover:bg-fill-2 active:bg-fill-3 transition-colors select-none focus:outline-none flex-shrink-0"
预览工卡 @click="emit('export')"
</CommonButton> >
<n-icon class="text-base"><cloud-download-outline /></n-icon>
<span class="text-xs font-medium">导出 XML</span>
</button>
<button
type="button"
class="toolbar-btn flex items-center gap-1.5 px-2.5 h-7 rounded text-color2 hover:bg-fill-2 active:bg-fill-3 transition-colors select-none focus:outline-none flex-shrink-0"
@click="emit('download-html')"
>
<n-icon class="text-base"><eye-outline /></n-icon>
<span class="text-xs font-medium">下载html</span>
</button>
<button
type="button"
class="flex items-center gap-1 px-3 h-7 rounded bg-primary text-white hover:bg-primary-hover active:bg-primary-pressed transition-colors select-none focus:outline-none flex-shrink-0 shadow-[0_1px_2px_rgba(0,0,0,0.08)]"
@click="emit('preview')"
>
<n-icon class="text-base"><eye-outline /></n-icon>
<span class="text-xs font-semibold">预览工卡</span>
</button>
</div>
<n-divider vertical class="!mx-0 flex-shrink-0" /> <div class="w-[1px] h-4 bg-divider mx-1 flex-shrink-0"></div>
<!-- 撤销 / 重做 --> <!-- 组 5:历史操作 (撤销 / 重做) -->
<n-tooltip trigger="hover"> <div class="flex items-center gap-0.5 flex-shrink-0">
<n-tooltip trigger="hover" :show-delay="600">
<template #trigger> <template #trigger>
<CommonButton size="small" quaternary :disabled="!canUndo" @click="editorStore.undo()" class="flex-shrink-0"> <button
<template #icon> type="button"
<n-icon><arrow-undo-outline /></n-icon> class="toolbar-btn flex items-center justify-center w-7 h-7 rounded text-color2 hover:bg-fill-2 active:bg-fill-3 disabled:opacity-40 disabled:hover:bg-transparent transition-colors select-none focus:outline-none flex-shrink-0"
</template> :disabled="!canUndo"
后退 @click="editorStore.undo()"
</CommonButton> >
<n-icon class="text-base"><arrow-undo-outline /></n-icon>
</button>
</template> </template>
撤销 (Ctrl+Z) 撤销 (Ctrl+Z)
</n-tooltip> </n-tooltip>
<n-tooltip trigger="hover"> <n-tooltip trigger="hover" :show-delay="600">
<template #trigger> <template #trigger>
<CommonButton size="small" quaternary :disabled="!canRedo" @click="editorStore.redo()" class="flex-shrink-0"> <button
<template #icon> type="button"
<n-icon><arrow-redo-outline /></n-icon> class="toolbar-btn flex items-center justify-center w-7 h-7 rounded text-color2 hover:bg-fill-2 active:bg-fill-3 disabled:opacity-40 disabled:hover:bg-transparent transition-colors select-none focus:outline-none flex-shrink-0"
</template> :disabled="!canRedo"
前进 @click="editorStore.redo()"
</CommonButton> >
<n-icon class="text-base"><arrow-redo-outline /></n-icon>
</button>
</template> </template>
重做 (Ctrl+Y) 重做 (Ctrl+Y)
</n-tooltip> </n-tooltip>
</div>
<n-divider vertical class="!mx-0 flex-shrink-0" /> <!-- 组 6:系统工具与偏好设置 (靠右) -->
<div class="flex items-center gap-1.5 flex-shrink-0 ml-auto">
<div class="w-[1px] h-4 bg-divider mx-0.5 flex-shrink-0"></div>
<!-- 主题切换 --> <n-tooltip trigger="hover" :show-delay="600">
<n-tooltip trigger="hover">
<template #trigger> <template #trigger>
<CommonButton size="small" quaternary circle @click="appStore.isDark = !appStore.isDark" class="flex-shrink-0"> <button
<template #icon> type="button"
<n-icon> class="toolbar-btn flex items-center justify-center w-7 h-7 rounded text-color2 hover:bg-fill-2 active:bg-fill-3 transition-colors select-none focus:outline-none flex-shrink-0"
@click="appStore.isDark = !appStore.isDark"
>
<n-icon class="text-base">
<sunny-outline v-if="appStore.isDark" /> <sunny-outline v-if="appStore.isDark" />
<moon-outline v-else /> <moon-outline v-else />
</n-icon> </n-icon>
</template> </button>
</CommonButton>
</template> </template>
{{ appStore.isDark ? '切换亮色主题' : '切换暗色主题' }} {{ appStore.isDark ? '切换亮色主题' : '切换暗色主题' }}
</n-tooltip> </n-tooltip>
<!-- 偏好设置 --> <SettingsDrawer>
<div class="flex-shrink-0 flex items-center"> <template #trigger="{ open }">
<SettingsDrawer /> <n-tooltip trigger="hover" :show-delay="600">
</div> <template #trigger>
<button
type="button"
class="toolbar-btn flex items-center justify-center w-7 h-7 rounded text-color2 hover:bg-fill-2 active:bg-fill-3 transition-colors select-none focus:outline-none flex-shrink-0"
@click="open"
>
<n-icon class="text-base"><settings-outline /></n-icon>
</button>
</template>
偏好设置
</n-tooltip>
</template>
</SettingsDrawer>
</div> </div>
<!-- 插入 XML 片段弹窗 --> <!-- 插入 XML 片段弹窗 -->
...@@ -190,7 +241,8 @@ import { ...@@ -190,7 +241,8 @@ import {
MoonOutline, MoonOutline,
CodeWorkingOutline, CodeWorkingOutline,
SyncOutline, SyncOutline,
CheckmarkCircleOutline CheckmarkCircleOutline,
SettingsOutline
} from '@vicons/ionicons5' } from '@vicons/ionicons5'
import { useEditorToolbar } from './functionals' import { useEditorToolbar } from './functionals'
import { GREEN_BUTTONS } from './constants' import { GREEN_BUTTONS } from './constants'
...@@ -228,7 +280,6 @@ const insertFragmentModalRef = ref<any>(null) ...@@ -228,7 +280,6 @@ const insertFragmentModalRef = ref<any>(null)
</script> </script>
<style scoped> <style scoped>
/* 消除工具栏容器本身的 focus outline */
.editor-toolbar { .editor-toolbar {
outline: none; outline: none;
/* 允许横向滚动 */ /* 允许横向滚动 */
...@@ -236,57 +287,47 @@ const insertFragmentModalRef = ref<any>(null) ...@@ -236,57 +287,47 @@ const insertFragmentModalRef = ref<any>(null)
scrollbar-color: var(--n-scrollbar-color, rgba(0, 0, 0, 0.2)) transparent; scrollbar-color: var(--n-scrollbar-color, rgba(0, 0, 0, 0.2)) transparent;
} }
/* 整个工具栏滚动条自定义:极其纤细,显眼且好看 */ /* 整个工具栏滚动条自定义 */
.editor-toolbar::-webkit-scrollbar { .editor-toolbar::-webkit-scrollbar {
height: 5px; height: 4px;
} }
.editor-toolbar::-webkit-scrollbar-track { .editor-toolbar::-webkit-scrollbar-track {
background: color-mix(in srgb, var(--divider-color) 40%, transparent); background: transparent;
border-radius: 2.5px;
} }
.editor-toolbar::-webkit-scrollbar-thumb { .editor-toolbar::-webkit-scrollbar-thumb {
background: var(--n-scrollbar-color, rgba(0, 0, 0, 0.2)); background: var(--n-scrollbar-color, rgba(0, 0, 0, 0.15));
border-radius: 2.5px; border-radius: 2px;
} }
.editor-toolbar::-webkit-scrollbar-thumb:hover { .editor-toolbar::-webkit-scrollbar-thumb:hover {
background: var(--n-scrollbar-color-hover, rgba(0, 0, 0, 0.4)); background: var(--n-scrollbar-color-hover, rgba(0, 0, 0, 0.35));
}
/* 插入按钮:深色文字 + 更紧凑的圆角 */
.editor-toolbar :deep(.insert-btn.n-button) {
--n-border-radius: 6px;
font-weight: 600;
font-size: 12px;
}
/* 翻译辅助按钮:去掉 margin,更紧凑 */
.editor-toolbar :deep(.util-btn.n-button) {
--n-border-radius: 6px;
font-size: 12px;
} }
/* XML 管理按钮 */ /* 消除工具栏中按钮聚焦或点击时由浏览器产生的原生高亮边框/外轮廓 */
.editor-toolbar :deep(.xml-btn.n-button) { .editor-toolbar button:focus,
--n-border-radius: 6px; .editor-toolbar button:focus-visible,
font-size: 12px; .editor-toolbar button:active {
font-weight: 600; outline: none !important;
border: none !important;
box-shadow: none !important;
} }
/* 预览工卡按钮:突出主色 */ /* 富文本工具栏按钮通用样式 */
.editor-toolbar :deep(.preview-btn.n-button) { .toolbar-btn {
--n-border-radius: 6px; display: inline-flex;
font-size: 12px; align-items: center;
font-weight: 700; justify-content: center;
background: transparent;
border: none;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
} }
/* 全局清除 n-button 间的默认 margin-right */ .toolbar-btn:hover:not(:disabled) {
.editor-toolbar :deep(.n-button + .n-button) { background-color: var(--colorFill2);
margin-right: 0 !important; color: var(--colorText1);
} }
/* n-divider vertical 高度统一 */ .toolbar-btn:active:not(:disabled) {
.editor-toolbar :deep(.n-divider--vertical) { background-color: var(--colorFill3);
height: 20px;
margin: 0 4px;
} }
</style> </style>
...@@ -6,13 +6,6 @@ ...@@ -6,13 +6,6 @@
class="rounded-xl overflow-hidden border border-divider shadow-lg relative bg-fill-4" class="rounded-xl overflow-hidden border border-divider shadow-lg relative bg-fill-4"
style="background-image: radial-gradient(circle, rgba(0, 0, 0, 0.02) 1px, transparent 1px); background-size: 16px 16px" style="background-image: radial-gradient(circle, rgba(0, 0, 0, 0.02) 1px, transparent 1px); background-size: 16px 16px"
> >
<!-- 磨砂玻璃质感的顶部装饰条 -->
<div class="h-8 bg-fill-3 border-b border-divider flex items-center px-4 space-x-1.5 select-none shrink-0">
<div class="w-3 h-3 rounded-full bg-danger/80"></div>
<div class="w-3 h-3 rounded-full bg-warning/80"></div>
<div class="w-3 h-3 rounded-full bg-success/80"></div>
</div>
<div class="p-6 font-mono text-sm leading-relaxed overflow-x-auto max-h-[500px] scrollbar-thin select-all"> <div class="p-6 font-mono text-sm leading-relaxed overflow-x-auto max-h-[500px] scrollbar-thin select-all">
<pre class="text-color1"><code class="xml-content-pre">{{ viewXmlContent }}</code></pre> <pre class="text-color1"><code class="xml-content-pre">{{ viewXmlContent }}</code></pre>
</div> </div>
......
...@@ -210,7 +210,7 @@ export function useTableEditor(props: { node: XmlNode }) { ...@@ -210,7 +210,7 @@ export function useTableEditor(props: { node: XmlNode }) {
}) })
// 解析文字 // 解析文字
let paragraphs: CellParagraphModel[] = entryNode.children const paragraphs: CellParagraphModel[] = entryNode.children
.filter((c) => PARA_TAGS.includes(c.tagName)) .filter((c) => PARA_TAGS.includes(c.tagName))
.map((pNode) => ({ .map((pNode) => ({
id: pNode.id, id: pNode.id,
...@@ -219,23 +219,6 @@ export function useTableEditor(props: { node: XmlNode }) { ...@@ -219,23 +219,6 @@ export function useTableEditor(props: { node: XmlNode }) {
attributes: pNode.attributes attributes: pNode.attributes
})) }))
const filteredParagraphs: CellParagraphModel[] = []
for (let i = 0; i < paragraphs.length; i++) {
const current = paragraphs[i]
const isParaPair = current.tagName === 'PARA' || current.tagName === 'PARAC'
if (isParaPair) {
const siblingTagName = current.tagName === 'PARA' ? 'PARAC' : 'PARA'
const hasDuplicate = paragraphs.some((p) => {
return p.tagName === siblingTagName && p.text.trim() === current.text.trim()
})
if (hasDuplicate && current.tagName === 'PARA') {
continue
}
}
filteredParagraphs.push(current)
}
paragraphs = filteredParagraphs
if (paragraphs.length === 0 && !hasComplexChildren) { if (paragraphs.length === 0 && !hasComplexChildren) {
paragraphs.push({ paragraphs.push({
id: entryNode.id, id: entryNode.id,
......
...@@ -78,7 +78,7 @@ ...@@ -78,7 +78,7 @@
@click.stop="handleCellClick(cell, $event)" @click.stop="handleCellClick(cell, $event)"
@contextmenu.prevent="handleCellContextMenu(cell, row.id, $event)" @contextmenu.prevent="handleCellContextMenu(cell, row.id, $event)"
> >
<template v-if="cell.hasComplexChildren && cell.rawNode"> <template v-if="cell.rawNode && cell.rawNode.children && cell.rawNode.children.length > 0">
<DocNodeRenderer v-for="child in cell.rawNode!.children" :key="child.id" :node="child" :parent="cell.rawNode" /> <DocNodeRenderer v-for="child in cell.rawNode!.children" :key="child.id" :node="child" :parent="cell.rawNode" />
</template> </template>
<template v-else> <template v-else>
...@@ -169,7 +169,7 @@ ...@@ -169,7 +169,7 @@
@click.stop="handleCellClick(cell, $event)" @click.stop="handleCellClick(cell, $event)"
@contextmenu.prevent="handleCellContextMenu(cell, row.id, $event)" @contextmenu.prevent="handleCellContextMenu(cell, row.id, $event)"
> >
<template v-if="cell.hasComplexChildren && cell.rawNode"> <template v-if="cell.rawNode && cell.rawNode.children && cell.rawNode.children.length > 0">
<DocNodeRenderer v-for="child in cell.rawNode!.children" :key="child.id" :node="child" :parent="cell.rawNode" /> <DocNodeRenderer v-for="child in cell.rawNode!.children" :key="child.id" :node="child" :parent="cell.rawNode" />
</template> </template>
<template v-else> <template v-else>
......
...@@ -119,6 +119,13 @@ export function useEditor() { ...@@ -119,6 +119,13 @@ export function useEditor() {
el.remove() el.remove()
}) })
// 0.5. 移除所有评估为 'ALL' 且非必填显示的 applicability 标签 (避免在导出的 HTML 中重复出现大量的 ** ON A/C: ALL)
clone.querySelectorAll('[data-tag-name="EFFECT"], [data-tag-name="CONEFFECT"]').forEach((el) => {
if (el.textContent?.includes('ALL')) {
el.remove()
}
})
// 1. 去除 contenteditable 属性,防止打印时显示光标或被编辑 // 1. 去除 contenteditable 属性,防止打印时显示光标或被编辑
clone.querySelectorAll('[contenteditable]').forEach((el) => { clone.querySelectorAll('[contenteditable]').forEach((el) => {
el.removeAttribute('contenteditable') el.removeAttribute('contenteditable')
...@@ -198,6 +205,11 @@ export function useEditor() { ...@@ -198,6 +205,11 @@ export function useEditor() {
} }
}) })
// 移除 RECORD-LINE / UNIT-RECORD 外层的边框与背景色类名
clone.querySelectorAll('[data-tag-name="RECORD-LINE"] > div, [data-tag-name="UNIT-RECORD"] > div').forEach((el) => {
el.classList.remove('bg-fill-3', 'border', 'border-divider', 'p-2', 'rounded')
})
// 5. 移除表格顶部的快捷操作工具栏 // 5. 移除表格顶部的快捷操作工具栏
clone.querySelectorAll('.flex.items-center.pb-2.border-b').forEach((el) => { clone.querySelectorAll('.flex.items-center.pb-2.border-b').forEach((el) => {
el.remove() el.remove()
......
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