Commit a43e8977 by pangchong

feat(editor): 支持本地暂存多种插入方式及效果预览优化

- 完善本地暂存功能,支持暂存整卡或选中片段的多样插入模式(上方、下方、内部、替换、追加根节点)
- 增加对XML节点插入时父节点mixedContent的同步维护,保证编辑状态一致
- 引入插入操作的可用性动态计算与理由提示,禁止不合规插入并提供明确反馈
- 实现暂存片段的效果预览功能,支持60FPS流式渲染大文档,提升加载体验与响应速度
- 优化签字表格及渲染组件样式,确保暗色模式下边框清晰可见
- 更新本地暂存帮助文档,增加操作步骤及预览功能说明,加强用户引导
- 修复XML节点操作中replace模式的DTD计数逻辑错误,提升数据一致性
- 调整编辑器内部相关类型和工具函数,支持文档块扁平化及虚拟化渲染需求
parent c19e8786
<template> <template>
<div class="my-3 flex overflow-x-auto select-none" :class="containerClass"> <div class="my-3 flex overflow-x-auto select-none" :class="containerClass">
<table class="border-collapse border border-black text-xs bg-white text-black"> <table
class="border-collapse border border-black text-xs text-black"
:class="node.attributes?.MERGED === 'TRUE' ? '' : 'bg-white'"
:style="node.attributes?.MERGED === 'TRUE' ? 'background-color: yellow; color: #000000;' : ''"
>
<tbody> <tbody>
<!-- 第一行:表头与签字人 --> <!-- 第一行:表头与签字人 -->
<tr class="h-10"> <tr class="h-10">
......
...@@ -1009,16 +1009,37 @@ export const FAQ_DATABASE: FaqItem[] = [ ...@@ -1009,16 +1009,37 @@ export const FAQ_DATABASE: FaqItem[] = [
id: 'faq_stash_manager', id: 'faq_stash_manager',
category: 'file', category: 'file',
categoryName: '文件与导出', categoryName: '文件与导出',
title: '什么是“本地暂存(Stash)”?如何防丢失?', title: '什么是“本地暂存(Stash)”?如何防丢失与效果预览?',
keywords: ['暂存', '本地暂存', 'stash', '草稿', '恢复', '防丢失', '版本', '历史暂存', '备份', 'zc', 'zancun', 'cg', 'caogao'], keywords: [
synonyms: ['怎么存草稿', '电脑关了数据还在吗', '怎么备份当前工卡', '恢复暂存', '暂存版本'], '暂存',
answer: '本地暂存功能类似于 Git Stash,能够将您当前编辑的工卡快照保存在本地浏览器中,防止意外刷新或关闭导致数据丢失。', '本地暂存',
steps: [ 'stash',
'在顶部工具栏点击【本地暂存】按钮。', '草稿',
'点击“新建暂存”,输入备注名称(如“发动机大修第一版”),系统立即保存当前完整状态。', '恢复',
'随时可在暂存列表中查看历史快照、对比变动或一键【恢复】到对应版本。' '防丢失',
'版本',
'历史暂存',
'备份',
'预览效果',
'zc',
'zancun',
'cg',
'caogao',
'ylxg'
],
synonyms: ['怎么存草稿', '电脑关了数据还在吗', '怎么备份当前工卡', '恢复暂存', '暂存版本', '暂存预览'],
answer: '本地暂存类似于 Git Stash 与历史快照库,可将正在编辑的完整 XML 或局部片段保存在浏览器本地持久化缓存中,防止误操作或页面刷新丢失数据。',
steps: [
'在顶部工具栏点击【本地暂存】按钮,进入管理面板。',
'【暂存整卡/片段】:支持一键【暂存整个 XML】或针对当前光标所选节点【暂存选中片段】。',
'【详情与代码预览】:点击左侧列表项可查看格式化 XML 代码与文件体积,并支持一键复制 XML 文本或导出为文件。',
'【排版效果预览】:点击列表项右侧的眼睛图标【预览效果】,可通过 60FPS 流式平滑加载直接查看渲染后的真实文档排版。',
'【灵活应用恢复】:完整文档支持一键覆盖恢复当前编辑区;片段支持在当前目标节点的“上方插入”、“下方插入”、“插入子节点”、“替换当前节点”或“追加到末尾”。'
],
highlights: [
'暂存数据保存在本地持久化存储中,重启电脑或关闭页面均不丢失。',
'对于超大 XML 文档,效果预览内置时间切片流式渲染,加载动效丝滑无卡顿。'
], ],
highlights: ['暂存数据存储在本地持久化存储中,即使关闭浏览器或重启电脑也能随时找回。'],
actions: [{ label: '打开本地暂存', type: 'event', payload: 'trigger_stash' }], actions: [{ label: '打开本地暂存', type: 'event', payload: 'trigger_stash' }],
relatedQuestions: ['faq_export_xml', 'faq_compare_card'], relatedQuestions: ['faq_export_xml', 'faq_compare_card'],
isHot: true isHot: true
......
...@@ -516,8 +516,8 @@ export const useEditorStore = defineStore('editor', { ...@@ -516,8 +516,8 @@ export const useEditorStore = defineStore('editor', {
}, },
insertXmlFragment(xmlString: string, mode: 'above' | 'below' | 'inside', targetNodeId?: string) { insertXmlFragment(xmlString: string, mode: 'above' | 'below' | 'inside', targetNodeId?: string) {
const targetId = targetNodeId || this.selectedNodeId const rawTargetId = targetNodeId || this.selectedNodeId
if (!this.xmlTree || !targetId) { if (!this.xmlTree || !rawTargetId) {
throw new Error('请先选择一个目标节点!') throw new Error('请先选择一个目标节点!')
} }
...@@ -526,6 +526,7 @@ export const useEditorStore = defineStore('editor', { ...@@ -526,6 +526,7 @@ export const useEditorStore = defineStore('editor', {
this.skipExpandOnSelect = true this.skipExpandOnSelect = true
} }
const targetId = rawTargetId.includes('-txt-') ? rawTargetId.split('-txt-')[0] : rawTargetId
const item = this.nodeMap.get(targetId) const item = this.nodeMap.get(targetId)
if (!item) { if (!item) {
throw new Error('当前的目标节点无效!') throw new Error('当前的目标节点无效!')
...@@ -598,7 +599,28 @@ export const useEditorStore = defineStore('editor', { ...@@ -598,7 +599,28 @@ export const useEditorStore = defineStore('editor', {
destParentNode.children.push(...newNodes) destParentNode.children.push(...newNodes)
} }
this.selectedNodeId = newNodes[newNodes.length - 1].id // 同步维护父节点的 mixedContent (若存在)
if (destParentNode.mixedContent && destParentNode.mixedContent.length > 0) {
const mixedIdx = destParentNode.mixedContent.findIndex((m) => m.type === 'element' && m.nodeId === targetId)
const newMixedItems = newNodes.map((n) => ({ type: 'element' as const, nodeId: n.id }))
if (mode === 'above') {
if (mixedIdx !== -1) {
destParentNode.mixedContent.splice(mixedIdx, 0, ...newMixedItems)
} else {
destParentNode.mixedContent.unshift(...newMixedItems)
}
} else if (mode === 'below') {
if (mixedIdx !== -1) {
destParentNode.mixedContent.splice(mixedIdx + 1, 0, ...newMixedItems)
} else {
destParentNode.mixedContent.push(...newMixedItems)
}
} else {
destParentNode.mixedContent.push(...newMixedItems)
}
}
this.setSelectedNodeId(newNodes[newNodes.length - 1].id, true)
newNodes.forEach((node) => this.handleNodeInsertion(node)) newNodes.forEach((node) => this.handleNodeInsertion(node))
this.rebuildNodeMap() this.rebuildNodeMap()
...@@ -610,11 +632,25 @@ export const useEditorStore = defineStore('editor', { ...@@ -610,11 +632,25 @@ export const useEditorStore = defineStore('editor', {
return newNodes.length return newNodes.length
}, },
insertXmlNodeFragment(fragment: XmlNode, mode: 'above' | 'below' | 'inside' | 'replace', targetNodeId?: string) { insertXmlNodeFragment(
const targetId = targetNodeId || this.selectedNodeId fragment: XmlNode,
if (!this.xmlTree || !targetId) { mode: 'above' | 'below' | 'inside' | 'replace' | 'append-root',
targetNodeId?: string
) {
if (!this.xmlTree) {
throw new Error('当前编辑器未加载任何 XML 文档!')
}
let targetId: string
if (mode === 'append-root') {
targetId = this.xmlTree.id
} else {
const rawTargetId = targetNodeId || this.selectedNodeId
if (!rawTargetId) {
throw new Error('请先选择一个目标节点!') throw new Error('请先选择一个目标节点!')
} }
targetId = rawTargetId.includes('-txt-') ? rawTargetId.split('-txt-')[0] : rawTargetId
}
const appStore = useAppStore() const appStore = useAppStore()
if (!appStore.autoExpandOnInsert) { if (!appStore.autoExpandOnInsert) {
...@@ -654,9 +690,11 @@ export const useEditorStore = defineStore('editor', { ...@@ -654,9 +690,11 @@ export const useEditorStore = defineStore('editor', {
// 确定被插入的目标父节点 // 确定被插入的目标父节点
let destParentNode: XmlNode let destParentNode: XmlNode
if (mode === 'above' || mode === 'below' || mode === 'replace') { if (mode === 'append-root') {
destParentNode = this.xmlTree
} else if (mode === 'above' || mode === 'below' || mode === 'replace') {
if (!parentNode) { if (!parentNode) {
throw new Error('无法操作根节点') throw new Error('根节点无法执行同级插入或替换操作!')
} }
destParentNode = parentNode destParentNode = parentNode
} else { } else {
...@@ -665,7 +703,11 @@ export const useEditorStore = defineStore('editor', { ...@@ -665,7 +703,11 @@ export const useEditorStore = defineStore('editor', {
// 严格校验 DTD 规则约束 // 严格校验 DTD 规则约束
const tagName = fragment.tagName const tagName = fragment.tagName
const existingCount = destParentNode.children.filter((c) => c.tagName === tagName).length let existingCount = destParentNode.children.filter((c) => c.tagName === tagName).length
// 修复 replace 模式下同名替换时被替换节点应被扣除计数的 Bug
if (mode === 'replace' && targetNode.tagName === tagName) {
existingCount = Math.max(0, existingCount - 1)
}
if (!canAddChild(destParentNode.tagName, tagName, existingCount)) { if (!canAddChild(destParentNode.tagName, tagName, existingCount)) {
throw new Error(`DTD 校验失败: 节点 <${destParentNode.tagName}> 无法接受子元素 <${tagName}>`) throw new Error(`DTD 校验失败: 节点 <${destParentNode.tagName}> 无法接受子元素 <${tagName}>`)
} }
...@@ -696,11 +738,38 @@ export const useEditorStore = defineStore('editor', { ...@@ -696,11 +738,38 @@ export const useEditorStore = defineStore('editor', {
destParentNode.children.splice(index, 1, clonedNode) destParentNode.children.splice(index, 1, clonedNode)
} }
} else { } else {
// inside // inside or append-root
destParentNode.children.push(clonedNode) destParentNode.children.push(clonedNode)
} }
this.selectedNodeId = clonedNode.id // 同步维护父节点的 mixedContent (若存在)
if (destParentNode.mixedContent && destParentNode.mixedContent.length > 0) {
const mixedIdx = destParentNode.mixedContent.findIndex((m) => m.type === 'element' && m.nodeId === targetId)
if (mode === 'replace') {
if (mixedIdx !== -1) {
destParentNode.mixedContent.splice(mixedIdx, 1, { type: 'element', nodeId: clonedNode.id })
} else {
destParentNode.mixedContent.push({ type: 'element', nodeId: clonedNode.id })
}
} else if (mode === 'above') {
if (mixedIdx !== -1) {
destParentNode.mixedContent.splice(mixedIdx, 0, { type: 'element', nodeId: clonedNode.id })
} else {
destParentNode.mixedContent.unshift({ type: 'element', nodeId: clonedNode.id })
}
} else if (mode === 'below') {
if (mixedIdx !== -1) {
destParentNode.mixedContent.splice(mixedIdx + 1, 0, { type: 'element', nodeId: clonedNode.id })
} else {
destParentNode.mixedContent.push({ type: 'element', nodeId: clonedNode.id })
}
} else {
// inside or append-root
destParentNode.mixedContent.push({ type: 'element', nodeId: clonedNode.id })
}
}
this.setSelectedNodeId(clonedNode.id, true)
this.handleNodeInsertion(clonedNode) this.handleNodeInsertion(clonedNode)
this.rebuildNodeMap() this.rebuildNodeMap()
...@@ -709,6 +778,7 @@ export const useEditorStore = defineStore('editor', { ...@@ -709,6 +778,7 @@ export const useEditorStore = defineStore('editor', {
this.skipExpandOnSelect = false this.skipExpandOnSelect = false
}) })
} }
return clonedNode
}, },
setXmlTree(tree: XmlNode) { setXmlTree(tree: XmlNode) {
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
isSelected ? 'ring-2 ring-primary ring-offset-1 rounded-sm bg-primary/5' : '', isSelected ? 'ring-2 ring-primary ring-offset-1 rounded-sm bg-primary/5' : '',
!renderInline && isContainer ? 'py-1 px-1' : '' !renderInline && isContainer ? 'py-1 px-1' : ''
]" ]"
:style="node.attributes?.MERGED === 'TRUE' ? { backgroundColor: 'yellow' } : {}" :style="node.attributes?.MERGED === 'TRUE' && node.tagName !== 'SIGNOFF' ? { backgroundColor: 'yellow' } : {}"
@click.stop="handleNodeClick($event)" @click.stop="handleNodeClick($event)"
> >
<!-- 智能翻译加载遮罩 --> <!-- 智能翻译加载遮罩 -->
...@@ -1171,23 +1171,36 @@ ...@@ -1171,23 +1171,36 @@
<template v-else-if="node.tagName === 'SIGNOFF'"> <template v-else-if="node.tagName === 'SIGNOFF'">
<component v-if="CustomSignoff" :is="CustomSignoff" :node="node" /> <component v-if="CustomSignoff" :is="CustomSignoff" :node="node" />
<div v-else class="my-3 overflow-x-auto select-none"> <div v-else class="my-3 overflow-x-auto select-none">
<table class="min-w-[400px] border-collapse border border-black dark:border-white/40 text-xs bg-card text-color1"> <table
class="min-w-[400px] border-collapse border border-black dark:border-white/40 text-xs text-color1"
:class="node.attributes?.MERGED === 'TRUE' ? 'merged-signoff' : 'bg-card'"
:style="node.attributes?.MERGED === 'TRUE' ? 'background-color: yellow; color: #000000;' : ''"
>
<tbody> <tbody>
<tr class="h-8"> <tr class="h-8">
<!-- 标签 --> <!-- 标签 -->
<td class="px-2 border border-black dark:border-white/40 font-bold text-center bg-fill-2 min-w-[60px]"> <td
class="px-2 border border-black dark:border-white/40 font-bold text-center min-w-[60px]"
:class="node.attributes?.MERGED === 'TRUE' ? '' : 'bg-fill-2'"
>
{{ node.attributes.TAG || '签字' }} {{ node.attributes.TAG || '签字' }}
</td> </td>
<!-- Mech 栏 --> <!-- Mech 栏 -->
<td class="px-2 border border-black dark:border-white/40 font-bold text-center bg-fill-2/50 w-20">操作人 MECH</td> <td
class="px-2 border border-black dark:border-white/40 font-bold text-center w-20"
:class="node.attributes?.MERGED === 'TRUE' ? '' : 'bg-fill-2/50'"
>操作人 MECH</td>
<td class="px-2 border border-black dark:border-white/40 text-center w-28 font-mono"> <td class="px-2 border border-black dark:border-white/40 text-center w-28 font-mono">
{{ node.attributes.mech ? `${node.attributes.mech} ${node.attributes.mechName || ''}` : '——' }} {{ node.attributes.mech ? `${node.attributes.mech} ${node.attributes.mechName || ''}` : '——' }}
</td> </td>
<!-- Insp (若有) --> <!-- Insp (若有) -->
<template v-if="['B', 'D', 'E', 'C'].includes(node.attributes['CK-LEVEL'] || 'B')"> <template v-if="['B', 'D', 'E', 'C'].includes(node.attributes['CK-LEVEL'] || 'B')">
<td class="px-2 border border-black dark:border-white/40 font-bold text-center bg-fill-2/50 w-20"> <td
class="px-2 border border-black dark:border-white/40 font-bold text-center w-20"
:class="node.attributes?.MERGED === 'TRUE' ? '' : 'bg-fill-2/50'"
>
{{ node.attributes['CK-LEVEL'] === 'C' ? '确认人 VERF' : '检验员 INSP' }} {{ node.attributes['CK-LEVEL'] === 'C' ? '确认人 VERF' : '检验员 INSP' }}
</td> </td>
<td class="px-2 border border-black dark:border-white/40 text-center w-28 font-mono"> <td class="px-2 border border-black dark:border-white/40 text-center w-28 font-mono">
...@@ -1726,4 +1739,10 @@ const handleNodeClick = (e: MouseEvent) => { ...@@ -1726,4 +1739,10 @@ const handleNodeClick = (e: MouseEvent) => {
display: none !important; display: none !important;
} }
} }
/* 当处于 MERGED (黄色背景) 时,强制表格外边框及所有内部单元格边框为黑色实线,防止暗色模式下 dark:border-white/40 导致边框在黄色背景上隐形消失 */
.merged-signoff,
.merged-signoff td {
border-color: #000000 !important;
}
</style> </style>
...@@ -82,3 +82,52 @@ export interface BlockPosition { ...@@ -82,3 +82,52 @@ export interface BlockPosition {
bottom: number bottom: number
height: number height: number
} }
/**
* 递归构建扁平化文档块列表
* JOBCARD / CEP / TASK 为透明容器,穿透递归其子节点
*/
export const getEditorBlocks = (node: XmlNode): EditorBlock[] => {
const blocks: EditorBlock[] = []
const walk = (n: XmlNode) => {
if (!n) return
if (n.tagName === 'CEP') {
const virtualHeaderNode: XmlNode = {
id: n.id,
tagName: 'CEP-HEADER',
attributes: n.attributes || {},
textContent: '',
children: [],
mixedContent: [],
parentId: n.parentId
}
blocks.push({ id: virtualHeaderNode.id, tagName: 'CEP-HEADER', rawNode: virtualHeaderNode })
;(n.children || []).forEach(walk)
} else if (n.tagName === 'TASK') {
// TASK 节点:创建虚拟 TASK-HEADER 展示任务编号,然后穿透子节点
const virtualTaskHeaderNode: XmlNode = {
id: n.id,
tagName: 'TASK-HEADER',
attributes: n.attributes || {},
textContent: '',
children: [],
mixedContent: [],
parentId: n.parentId
}
blocks.push({ id: virtualTaskHeaderNode.id, tagName: 'TASK-HEADER', rawNode: virtualTaskHeaderNode })
;(n.children || []).forEach(walk)
} else if (TRANSPARENT_TAGS_SET.has(n.tagName)) {
;(n.children || []).forEach(walk)
} else if (n.tagName === 'TOPIC') {
// 特殊拆分:TOPIC 节点自身仅作为标题 Block,其内容子节点(SUBTASK 等)则作为平级 Block 扁平化,以实现细粒度虚拟化
blocks.push({ id: n.id, tagName: 'TOPIC', rawNode: n })
const contentNodes = (n.children || []).filter((c) => c.tagName !== 'TITLE' && c.tagName !== 'TITLEC')
contentNodes.forEach(walk)
} else {
blocks.push({ id: n.id, tagName: n.tagName, rawNode: n })
}
}
walk(node)
return blocks
}
import type { EditorBlock } from '@/views/editor/components/EditorPanel/constants'
export interface ViewEffectState { export interface ViewEffectState {
visible: boolean visible: boolean
title: string title: string
xmlTree: any loading: boolean
blocks: EditorBlock[]
} }
import type { XmlNode } from '@/types/xmlNode' import type { XmlNode } from '@/types/xmlNode'
import { getEditorBlocks, type EditorBlock } from '@/views/editor/components/EditorPanel/constants'
export function useViewEffectModal() { export function useViewEffectModal() {
const visible = ref(false) const visible = ref(false)
const title = ref('') const title = ref('')
const xmlTree = ref<XmlNode | null>(null) const loading = ref(false)
const blocks = ref<EditorBlock[]>([])
let rafId: number | null = null
const open = (modalTitle: string, xmlContent: string) => { const cancelRender = () => {
if (rafId !== null) {
cancelAnimationFrame(rafId)
rafId = null
}
}
const open = async (modalTitle: string, source: string | XmlNode) => {
cancelRender()
title.value = modalTitle title.value = modalTitle
try { blocks.value = []
// 解析 XML 片段 // 遵循规则 12:先开窗后加载
xmlTree.value = parseXmlToTree(xmlContent)
visible.value = true visible.value = true
loading.value = true
const startTime = Date.now()
try {
// 给浏览器 1 帧绘制时间,确保弹窗以原生 60FPS 顺畅展开,骨架屏和 loading 动画平滑启动
await nextTick()
await new Promise((resolve) => requestAnimationFrame(resolve))
let rootNode: XmlNode
if (typeof source === 'string') {
rootNode = await parseXmlToTreeAsync(source)
} else {
rootNode = JSON.parse(JSON.stringify(source))
}
const allBlocks = getEditorBlocks(rootNode)
if (allBlocks.length === 0) {
loading.value = false
return
}
// 预留合理的最小感知时长(400ms),确保大文件加载动效清晰可见、平滑过渡,避免一闪而过无感知
const MIN_LOADING_TIME = 400
const elapsed = Date.now() - startTime
if (elapsed < MIN_LOADING_TIME) {
await new Promise((resolve) => setTimeout(resolve, MIN_LOADING_TIME - elapsed))
}
if (!visible.value) return
// 首屏仅渲染第一批(6 个块,足以填满弹窗可视高度),挂载耗时 < 10ms,绝不卡顿主线程动画
const BATCH_SIZE = 6
blocks.value = allBlocks.slice(0, BATCH_SIZE)
// 等待首屏 DOM 挂载完成
await nextTick()
// 首屏立即可见,立刻关闭 loading,让用户平滑过渡到文档效果
loading.value = false
// 后续块采用时间切片(Time Slicing),在后续空闲帧每帧追加一批,零掉帧无感流式加载
if (allBlocks.length > BATCH_SIZE) {
let currentIndex = BATCH_SIZE
const renderNextBatch = () => {
if (!visible.value || currentIndex >= allBlocks.length) {
rafId = null
return
}
const nextIndex = Math.min(currentIndex + 8, allBlocks.length)
blocks.value.push(...allBlocks.slice(currentIndex, nextIndex))
currentIndex = nextIndex
if (currentIndex < allBlocks.length) {
rafId = requestAnimationFrame(renderNextBatch)
} else {
rafId = null
}
}
rafId = requestAnimationFrame(renderNextBatch)
}
} catch (err: any) { } catch (err: any) {
window.$message?.error(`解析 XML 效果失败: ${err.message || err}`) window.$message?.error(`解析 XML 效果失败: ${err.message || err}`)
loading.value = false
}
} }
watch(visible, (val) => {
if (!val) {
cancelRender()
blocks.value = []
} }
})
onBeforeUnmount(() => {
cancelRender()
})
return { return {
visible, visible,
title, title,
xmlTree, loading,
blocks,
open open
} }
} }
<template> <template>
<CommonModal v-model="visible" :title="title" :width="900" :show-confirm="false" cancel-text="关闭"> <CommonModal
<DocNodeRenderer v-if="xmlTree" :node="xmlTree" :parent="null" /> v-model="visible"
:title="title"
:width="900"
:loading="loading"
:show-confirm="false"
cancel-text="关闭"
>
<div class="min-h-[400px] p-2">
<!-- 渲染文档树效果(时间切片分批流式渲染,保持60FPS高刷与瞬时首屏) -->
<div v-if="blocks.length > 0" class="space-y-1">
<div v-for="block in blocks" :key="block.id" class="w-full py-0.5">
<DocNodeRenderer :node="block.rawNode" :parent="null" />
</div>
</div>
<!-- 加载中的文档骨架占位效果(首屏加载前呈现,60FPS 平滑脉冲) -->
<div v-else-if="loading" class="flex flex-col gap-4 py-8 px-6 animate-pulse select-none">
<div class="flex items-center justify-between">
<div class="h-6 bg-fill-3 rounded-md w-1/3"></div>
<div class="h-5 bg-fill-2 rounded-md w-24"></div>
</div>
<div class="h-4 bg-fill-3 rounded w-full"></div>
<div class="h-4 bg-fill-3 rounded w-5/6"></div>
<div class="h-4 bg-fill-3 rounded w-4/5"></div>
<div class="my-2 p-4 bg-fill-2 border border-divider/60 rounded-lg flex flex-col gap-3">
<div class="h-4 bg-fill-3 rounded w-1/4"></div>
<div class="h-3 bg-fill-3 rounded w-3/4"></div>
<div class="h-3 bg-fill-3 rounded w-2/3"></div>
</div>
<div class="h-4 bg-fill-3 rounded w-11/12"></div>
<div class="h-4 bg-fill-3 rounded w-3/4"></div>
<div class="h-4 bg-fill-3 rounded w-4/6"></div>
</div>
<!-- 空状态或解析无内容 -->
<div v-else class="flex flex-col items-center justify-center py-16 text-color3 select-none">
<n-icon size="40" class="opacity-30 mb-2">
<DocumentTextOutline />
</n-icon>
<div class="text-xs">暂无内容或预览数据为空</div>
</div>
</div>
</CommonModal> </CommonModal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import DocNodeRenderer from '../../../../../DocNodeRenderer/index.vue' import DocNodeRenderer from '../../../../../DocNodeRenderer/index.vue'
import { DocumentTextOutline } from '@vicons/ionicons5'
import { useViewEffectModal } from './functionals' import { useViewEffectModal } from './functionals'
const { visible, title, xmlTree, open } = useViewEffectModal() const { visible, title, loading, blocks, open } = useViewEffectModal()
// 注入只读模式的对比状态,禁用一切编辑行为及组件内部编辑事件 // 注入只读模式的对比状态,禁用一切编辑行为及组件内部编辑事件
provide('diffMode', true) provide('diffMode', true)
...@@ -19,3 +61,5 @@ defineExpose({ ...@@ -19,3 +61,5 @@ defineExpose({
</script> </script>
<style scoped></style> <style scoped></style>
...@@ -55,7 +55,10 @@ export function useTemplateSelectModal() { ...@@ -55,7 +55,10 @@ export function useTemplateSelectModal() {
} }
const handleInsert = () => { const handleInsert = () => {
if (!selectedTemplate.value) return if (!selectedTemplate.value) {
window.$message?.warning('请从列表中选择要插入的模板')
return
}
try { try {
const mode = insertBelowSetting.value ? 'below' : 'inside' const mode = insertBelowSetting.value ? 'below' : 'inside'
......
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
:width="750" :width="750"
:loading="loading" :loading="loading"
confirm-text="确认插入" confirm-text="确认插入"
:confirm-disabled="!selectedTemplate"
@confirm="handleInsert" @confirm="handleInsert"
> >
<div class="flex flex-col space-y-4"> <div class="flex flex-col space-y-4">
......
<template> <template>
<div class="my-3 flex overflow-x-auto select-none" :class="containerClass"> <div class="my-3 flex overflow-x-auto select-none" :class="containerClass">
<table class="border-collapse border border-black dark:border-white/40 text-xs bg-card text-color1"> <table
class="border-collapse border border-black dark:border-white/40 text-xs text-color1"
:class="node.attributes?.MERGED === 'TRUE' ? 'merged-signoff' : 'bg-card'"
:style="node.attributes?.MERGED === 'TRUE' ? 'background-color: yellow; color: #000000;' : ''"
>
<tbody> <tbody>
<!-- 第一行:表头与签字人 --> <!-- 第一行:表头与签字人 -->
<tr class="h-10"> <tr class="h-10">
<!-- 工作者 Per.By --> <!-- 工作者 Per.By -->
<td class="px-2 border border-black dark:border-white/40 font-bold text-center w-24 bg-fill-2/50"> <td
class="px-2 border border-black dark:border-white/40 font-bold text-center w-24"
:class="node.attributes?.MERGED === 'TRUE' ? '' : 'bg-fill-2/50'"
>
工作者 工作者
<br /> <br />
Per.By Per.By
...@@ -17,7 +24,10 @@ ...@@ -17,7 +24,10 @@
<!-- 检查者 Insp.By (CK-LEVEL B, 且支持 D/E 兼容) --> <!-- 检查者 Insp.By (CK-LEVEL B, 且支持 D/E 兼容) -->
<template v-if="['B', 'D', 'E'].includes(node.attributes['CK-LEVEL'])"> <template v-if="['B', 'D', 'E'].includes(node.attributes['CK-LEVEL'])">
<td class="px-2 border border-black dark:border-white/40 font-bold text-center w-24 bg-fill-2/50"> <td
class="px-2 border border-black dark:border-white/40 font-bold text-center w-24"
:class="node.attributes?.MERGED === 'TRUE' ? '' : 'bg-fill-2/50'"
>
检查者 检查者
<br /> <br />
Insp.By Insp.By
...@@ -30,7 +40,10 @@ ...@@ -30,7 +40,10 @@
<!-- 必检 RII.By (CK-LEVEL C) --> <!-- 必检 RII.By (CK-LEVEL C) -->
<template v-else-if="node.attributes['CK-LEVEL'] === 'C'"> <template v-else-if="node.attributes['CK-LEVEL'] === 'C'">
<td class="px-2 border border-black dark:border-white/40 font-bold text-center w-24 bg-fill-2/50"> <td
class="px-2 border border-black dark:border-white/40 font-bold text-center w-24"
:class="node.attributes?.MERGED === 'TRUE' ? '' : 'bg-fill-2/50'"
>
必检 必检
<br /> <br />
RII.By RII.By
...@@ -90,3 +103,11 @@ const containerClass = computed(() => { ...@@ -90,3 +103,11 @@ const containerClass = computed(() => {
return location === 'LEFT' ? 'justify-start' : 'justify-end' return location === 'LEFT' ? 'justify-start' : 'justify-end'
}) })
</script> </script>
<style scoped>
/* 当处于 MERGED (黄色背景) 时,强制表格外边框及所有内部单元格边框为黑色实线,防止暗色模式下 dark:border-white/40 导致边框在黄色背景上隐形消失 */
.merged-signoff,
.merged-signoff td {
border-color: #000000 !important;
}
</style>
...@@ -68,7 +68,7 @@ export default defineConfig(({ mode }) => { ...@@ -68,7 +68,7 @@ export default defineConfig(({ mode }) => {
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
headers: { headers: {
Cookie: '_udid=7927a0ca-972a-4e44-948a-c2edfd510fdb; JSESSIONID=C4A682632B2A7670516350D782474347; _amro_sk=31047732-4a3e-4681-b99a-16e99ee22810' Cookie: '_udid=2189d6e1-8312-4c16-a133-837951dfe125; JSESSIONID=3545B5297E4782E7963868A0CB522864; _amro_sk=8547dbec-a248-46ec-870a-f5b99c15d716'
} }
}, },
'/mnt': { '/mnt': {
......
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