Commit 7260b8a1 by pangchong

feat: 优化表格渲染样式

parent 16e14a6e
...@@ -27,6 +27,7 @@ export interface AttrDef { ...@@ -27,6 +27,7 @@ export interface AttrDef {
export interface FlatNode { export interface FlatNode {
id: string id: string
tagName: string tagName: string
attrSummary?: string
subtitle: string subtitle: string
fullSubtitle?: string fullSubtitle?: string
depth: number depth: number
......
...@@ -213,7 +213,7 @@ export function useNodeTree( ...@@ -213,7 +213,7 @@ export function useNodeTree(
} }
// 递归构建扁平列表 // 递归构建扁平列表
const buildFlatList = (node: XmlNode, depth = 0, search = ''): FlatNode[] => { const buildFlatList = (node: XmlNode, depth = 0, search = '', parent: XmlNode | null = null): FlatNode[] => {
if (search && !matchNode(node, search)) { if (search && !matchNode(node, search)) {
return [] return []
} }
...@@ -232,6 +232,7 @@ export function useNodeTree( ...@@ -232,6 +232,7 @@ export function useNodeTree(
} }
const isExpanded = expandedKeys.value.has(node.id) const isExpanded = expandedKeys.value.has(node.id)
let attrSummary = ''
let subtitle = '' let subtitle = ''
let fullSubtitle = '' let fullSubtitle = ''
const formatEff = (eff: string | undefined): string => { const formatEff = (eff: string | undefined): string => {
...@@ -245,25 +246,25 @@ export function useNodeTree( ...@@ -245,25 +246,25 @@ export function useNodeTree(
} }
if (node.tagName === 'EFFECT') { if (node.tagName === 'EFFECT') {
subtitle = `ON A/C: ${formatEff(node.attributes.EFFRG || node.textContent)}` attrSummary = `ON A/C: ${formatEff(node.attributes.EFFRG || node.textContent)}`
} else if (node.tagName === 'CONEFFECT') { } else if (node.tagName === 'CONEFFECT') {
subtitle = `CONF: ${formatEff(node.attributes.EFFRG || node.textContent)}` attrSummary = `CONF: ${formatEff(node.attributes.EFFRG || node.textContent)}`
} else if (node.tagName === 'SBEFF' || node.tagName === 'SBEFFC') { } else if (node.tagName === 'SBEFF' || node.tagName === 'SBEFFC') {
subtitle = `SB: ${node.attributes.SBCOND || ''} SB ${node.attributes.SBNBR || ''} for A/C ${formatEff(node.attributes.EFFRG)}` attrSummary = `SB: ${node.attributes.SBCOND || ''} SB ${node.attributes.SBNBR || ''} for A/C ${formatEff(node.attributes.EFFRG)}`
} else if (node.tagName === 'CBLST') { } else if (node.tagName === 'CBLST') {
const action = node.attributes.ACTION === 'verif-close' ? '确认关闭' : node.attributes.ACTION === 'open' ? '断开' : '操作' const action = node.attributes.ACTION === 'verif-close' ? '确认关闭' : node.attributes.ACTION === 'open' ? '断开' : '操作'
subtitle = `行动: ${action}` attrSummary = `行动: ${action}`
} else if (node.tagName === 'CEP' || node.tagName === 'SUBTASK') { } else if (node.tagName === 'CEP' || node.tagName === 'SUBTASK') {
const { CHAPPREF, CHAPNBR, SECTNBR, SUBJNBR, FUNC, SEQ, CONFLTR } = node.attributes || {} const { CHAPPREF, CHAPNBR, SECTNBR, SUBJNBR, FUNC, SEQ, CONFLTR } = node.attributes || {}
const parts = [CHAPPREF, CHAPNBR, SECTNBR, SUBJNBR, FUNC, SEQ, CONFLTR].filter(Boolean) const parts = [CHAPPREF, CHAPNBR, SECTNBR, SUBJNBR, FUNC, SEQ, CONFLTR].filter(Boolean)
subtitle = parts.join('-') attrSummary = parts.join('-')
} else if (node.tagName === 'UNIT-RECORD') { } else if (node.tagName === 'UNIT-RECORD') {
const text = node.textContent ? node.textContent.trim() : '' attrSummary = `单位: ${node.attributes.UNIT || 'mm'}`
subtitle = `${text} (单位: ${node.attributes.UNIT || 'mm'})` subtitle = node.textContent ? node.textContent.trim() : ''
} else if (node.tagName === 'SIGNOFF') { } else if (node.tagName === 'SIGNOFF') {
const tag = node.attributes.TAG || '签字' const tag = node.attributes.TAG || '签字'
const level = node.attributes['CK-LEVEL'] const level = node.attributes['CK-LEVEL']
subtitle = level ? `Tag: ${tag} (${level}级)` : `Tag: ${tag}` attrSummary = level ? `${tag} (${level}级)` : `${tag}`
} else if (node.tagName === 'TOR') { } else if (node.tagName === 'TOR') {
const formatTorValueStr = (valNode: XmlNode): string => { const formatTorValueStr = (valNode: XmlNode): string => {
const min = (valNode.attributes.MIN || '').trim() const min = (valNode.attributes.MIN || '').trim()
...@@ -275,7 +276,7 @@ export function useNodeTree( ...@@ -275,7 +276,7 @@ export function useNodeTree(
return `${min || max} ${unit}` return `${min || max} ${unit}`
} }
const torvalues = node.children.filter((c) => c.tagName === 'TORVALUE') const torvalues = node.children.filter((c) => c.tagName === 'TORVALUE')
subtitle = torvalues attrSummary = torvalues
.map((c, idx) => { .map((c, idx) => {
const str = formatTorValueStr(c) const str = formatTorValueStr(c)
return idx > 0 ? `(${str})` : str return idx > 0 ? `(${str})` : str
...@@ -286,20 +287,118 @@ export function useNodeTree( ...@@ -286,20 +287,118 @@ export function useNodeTree(
const max = (node.attributes.MAX || '').trim() const max = (node.attributes.MAX || '').trim()
const unit = (node.attributes.UNIT || '').trim() const unit = (node.attributes.UNIT || '').trim()
if (min && max && min !== max) { if (min && max && min !== max) {
subtitle = `${min} - ${max} ${unit}` attrSummary = `${min} - ${max} ${unit}`
} else { } else {
subtitle = `${min || max} ${unit}` attrSummary = `${min || max} ${unit}`
} }
} else if (node.tagName === 'SHEET') { } else if (node.tagName === 'SHEET') {
const sheetNbr = node.attributes.SHEETNBR || '' const sheetNbr = node.attributes.SHEETNBR || ''
const gnbr = node.attributes.GNBR || '' const gnbr = node.attributes.GNBR || ''
subtitle = sheetNbr ? `Sheet ${sheetNbr} [GNBR: ${gnbr}]` : `GNBR: ${gnbr}` attrSummary = sheetNbr ? `Sheet ${sheetNbr} [GNBR: ${gnbr}]` : `GNBR: ${gnbr}`
} else if (node.tagName === 'TABLE') {
const frame = node.attributes.FRAME || ''
const cols = node.attributes.COLS || ''
const align = node.attributes.ALIGN || ''
const valign = node.attributes.VALIGN || ''
const parts = []
if (frame) parts.push(`FRAME: ${frame}`)
if (cols) parts.push(`${cols}列`)
if (align) parts.push(`对齐: ${align}`)
if (valign) parts.push(`垂直: ${valign}`)
attrSummary = parts.join(' | ')
subtitle = getNodeTextPreview(node, false)
} else if (node.tagName === 'TGROUP') {
const cols = node.attributes.COLS || ''
const align = node.attributes.ALIGN || ''
const parts = []
if (cols) parts.push(`${cols}列`)
if (align) parts.push(`对齐: ${align}`)
attrSummary = parts.join(' | ')
} else if (node.tagName === 'COLSPEC') {
const colName = node.attributes.COLNAME || node.attributes.COLNUM || ''
const width = node.attributes.COLWIDTH || ''
const align = node.attributes.ALIGN || ''
const parts = []
if (colName) parts.push(`列 ${colName}`)
if (width) parts.push(`宽度: ${width}`)
if (align) parts.push(`对齐: ${align}`)
attrSummary = parts.join(' | ')
} else if (node.tagName === 'SPANSPEC') {
const spanName = node.attributes.SPANNAME || ''
const namest = node.attributes.NAMEST || ''
const nameend = node.attributes.NAMEEND || ''
const align = node.attributes.ALIGN || ''
const parts = []
if (spanName) parts.push(`跨列 ${spanName} (${namest} ~ ${nameend})`)
else if (namest || nameend) parts.push(`跨列 (${namest} ~ ${nameend})`)
if (align) parts.push(`对齐: ${align}`)
attrSummary = parts.join(' | ')
} else if (node.tagName === 'THEAD') {
const valign = node.attributes.VALIGN || ''
attrSummary = valign ? `表头 | 垂直: ${valign}` : '表头'
} else if (node.tagName === 'TBODY') {
const valign = node.attributes.VALIGN || ''
attrSummary = valign ? `表主体 | 垂直: ${valign}` : '表主体'
} else if (node.tagName === 'TFOOT') {
const valign = node.attributes.VALIGN || ''
attrSummary = valign ? `表尾 | 垂直: ${valign}` : '表尾'
} else if (node.tagName === 'ROW') {
const rowSiblings = parent ? (parent.children || []).filter((c) => c.tagName === 'ROW') : []
const rowIdx = rowSiblings.findIndex((c) => c.id === node.id)
const rowLabel = rowIdx !== -1 ? `第 ${rowIdx + 1} 行` : ''
const entryCount = (node.children || []).filter((c) => c.tagName === 'ENTRY').length
const rowsep = node.attributes.ROWSEP
const valign = node.attributes.VALIGN || ''
const parts = []
if (rowLabel) parts.push(rowLabel)
if (valign) parts.push(`垂直: ${valign}`)
if (rowsep) parts.push(`ROWSEP: ${rowsep}`)
attrSummary = parts.join(' | ')
// 收集行内各单元格文本摘要作为预览
const entryTexts = (node.children || [])
.filter((c) => c.tagName === 'ENTRY')
.map((c) => getNodeTextPreview(c, false))
.filter(Boolean)
if (entryTexts.length > 0) {
subtitle = entryTexts.slice(0, 3).join(' | ') + (entryTexts.length > 3 ? ' ...' : '')
}
} else if (node.tagName === 'ENTRY') {
const attrParts = []
const entrySiblings = parent ? (parent.children || []).filter((c) => c.tagName === 'ENTRY') : []
const entryIdx = entrySiblings.findIndex((c) => c.id === node.id)
if (node.attributes.SPANNAME) {
attrParts.push(`跨列: ${node.attributes.SPANNAME}`)
} else if (node.attributes.NAMEST || node.attributes.NAMEEND) {
attrParts.push(`${node.attributes.NAMEST || ''} ~ ${node.attributes.NAMEEND || ''}`)
} else if (node.attributes.COLNAME) {
attrParts.push(`列: ${node.attributes.COLNAME}`)
} else if (entryIdx !== -1) {
attrParts.push(`第 ${entryIdx + 1} 列`)
}
if (node.attributes.MOREROWS) {
attrParts.push(`跨 ${Number(node.attributes.MOREROWS) + 1} 行`)
}
if (node.attributes.ALIGN) {
attrParts.push(`对齐: ${node.attributes.ALIGN}`)
}
if (node.attributes.VALIGN) {
attrParts.push(`垂直: ${node.attributes.VALIGN}`)
}
attrSummary = attrParts.join(' | ')
subtitle = getNodeTextPreview(node, false)
} else if (node.attributes.ID) { } else if (node.attributes.ID) {
subtitle = node.attributes.ID attrSummary = `ID: ${node.attributes.ID}`
subtitle = getNodeTextPreview(node, false)
} else if (node.attributes.EFFRG) { } else if (node.attributes.EFFRG) {
subtitle = `A/C: ${formatEff(node.attributes.EFFRG)}` attrSummary = `A/C: ${formatEff(node.attributes.EFFRG)}`
subtitle = getNodeTextPreview(node, false)
} else if (node.attributes.EFFECT) { } else if (node.attributes.EFFECT) {
subtitle = node.attributes.EFFECT attrSummary = node.attributes.EFFECT
subtitle = getNodeTextPreview(node, false)
} else { } else {
subtitle = getNodeTextPreview(node, false) subtitle = getNodeTextPreview(node, false)
fullSubtitle = getNodeTextPreview(node, true) fullSubtitle = getNodeTextPreview(node, true)
...@@ -312,6 +411,7 @@ export function useNodeTree( ...@@ -312,6 +411,7 @@ export function useNodeTree(
list.push({ list.push({
id: node.id, id: node.id,
tagName: node.tagName, tagName: node.tagName,
attrSummary,
subtitle, subtitle,
fullSubtitle, fullSubtitle,
depth, depth,
...@@ -328,7 +428,7 @@ export function useNodeTree( ...@@ -328,7 +428,7 @@ export function useNodeTree(
if (item.type === 'element' && item.nodeId) { if (item.type === 'element' && item.nodeId) {
const child = node.children.find((c) => c.id === item.nodeId) const child = node.children.find((c) => c.id === item.nodeId)
if (child) { if (child) {
list.push(...buildFlatList(child, depth + 1, search)) list.push(...buildFlatList(child, depth + 1, search, node))
} }
} else if (item.type === 'text') { } else if (item.type === 'text') {
// 仅当文本项目数量大于 1 时,才生成虚拟的 #text 节点展示 // 仅当文本项目数量大于 1 时,才生成虚拟的 #text 节点展示
...@@ -362,7 +462,7 @@ export function useNodeTree( ...@@ -362,7 +462,7 @@ export function useNodeTree(
}) })
} else { } else {
for (const child of node.children) { for (const child of node.children) {
list.push(...buildFlatList(child, depth + 1, search)) list.push(...buildFlatList(child, depth + 1, search, node))
} }
} }
} }
......
...@@ -139,17 +139,32 @@ ...@@ -139,17 +139,32 @@
</n-icon> </n-icon>
</div> </div>
<!-- Label & Subtitle --> <!-- Label & Subtitle & AttrSummary -->
<div <div
class="flex-1 min-w-0 flex items-center space-x-2 z-10" class="flex-1 min-w-0 flex items-center space-x-1.5 z-10"
:title="item.fullSubtitle || item.subtitle ? `${item.tagName} ${item.fullSubtitle || item.subtitle}` : item.tagName" :title="
item.fullSubtitle || item.subtitle || item.attrSummary
? `${item.tagName} ${item.attrSummary ? '[' + item.attrSummary + '] ' : ''}${item.fullSubtitle || item.subtitle}`
: item.tagName
"
> >
<!-- 节点名称高亮 --> <!-- 节点名称高亮 -->
<span class="font-bold text-sm flex-shrink-0" v-html="highlightText(item.tagName, pattern)"></span> <span class="font-bold text-sm flex-shrink-0" v-html="highlightText(item.tagName, pattern)"></span>
<!-- 子标题高亮 --> <!-- 属性摘要 Badge (包含淡色背景与框线,与文本预览彻底区分) -->
<span
v-if="item.attrSummary"
class="text-[10px] font-mono px-1.5 py-0.5 rounded shrink-0 leading-none font-medium border"
:class="[
effectiveSelectedId === item.id
? 'bg-white/20 text-white border-white/30'
: 'bg-primary/10 text-primary border-primary/20 dark:bg-primary/20 dark:border-primary/30'
]"
v-html="highlightText(item.attrSummary, pattern)"
></span>
<!-- 节点文本内容预览高亮 -->
<span <span
v-if="item.subtitle" v-if="item.subtitle"
class="text-xs truncate" class="text-xs truncate font-normal"
:title="item.fullSubtitle || item.subtitle" :title="item.fullSubtitle || item.subtitle"
:class="[ :class="[
effectiveSelectedId === item.id effectiveSelectedId === item.id
......
...@@ -1753,6 +1753,41 @@ export function useTableEditor(props: { node: XmlNode }) { ...@@ -1753,6 +1753,41 @@ export function useTableEditor(props: { node: XmlNode }) {
} }
} }
// 3. ENTRY 节点 MERGED 属性:单元格显示黄色背景(与 PDF 规范一致)
if ((cell.attributes?.MERGED || cell.rawNode?.attributes?.MERGED) === 'TRUE') {
styles['background-color'] = 'yellow'
}
// 4. COLSPEC MERGED:该列的列规范被标记为合并,对应列的所有单元格显示黄色
if (!styles['background-color']) {
const tgroupNode = store.nodeMap.get(structure.value.tgroupId)?.node
if (tgroupNode) {
const colSpecs = tgroupNode.children.filter((c) => c.tagName === 'COLSPEC')
const colSpec = colSpecs[cell.colIdx ?? 0]
if (colSpec?.attributes?.MERGED === 'TRUE') {
styles['background-color'] = 'yellow'
}
}
}
// 5. SPANSPEC MERGED:单元格引用的跨列规范被标记为合并,显示黄色
if (!styles['background-color']) {
const spanName = (cell.attributes?.SPANNAME || cell.rawNode?.attributes?.SPANNAME || '').toLowerCase()
if (spanName) {
const tgroupNode = store.nodeMap.get(structure.value.tgroupId)?.node
if (tgroupNode) {
const spanSpec = tgroupNode.children
.filter((c) => c.tagName === 'SPANSPEC')
.find((c) =>
(c.attributes?.SPANNAME || c.attributes?.spanname || '').toLowerCase() === spanName
)
if (spanSpec?.attributes?.MERGED === 'TRUE') {
styles['background-color'] = 'yellow'
}
}
}
}
return styles return styles
} }
......
...@@ -6,15 +6,27 @@ ...@@ -6,15 +6,27 @@
<span class="text-xs text-color3">{{ CELL_EDIT_TIP }}</span> <span class="text-xs text-color3">{{ CELL_EDIT_TIP }}</span>
<span v-if="!isDiffMode" class="text-xs text-color3 select-none">· 右键单元格可快速操作</span> <span v-if="!isDiffMode" class="text-xs text-color3 select-none">· 右键单元格可快速操作</span>
<span v-else class="text-xs text-color3 select-none">· 比对只读模式</span> <span v-else class="text-xs text-color3 select-none">· 比对只读模式</span>
<!-- TABLE 节点 MERGED 状态提示 -->
<span
v-if="node.attributes?.MERGED === 'TRUE'"
class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-bold select-none"
style="background: yellow; color: #78350f; border: 1px solid #ca8a04"
>
MERGED
</span>
</div> </div>
<!-- 可视化二维表格视图 --> <!-- 可视化二维表格视图 -->
<div class="flex-1 overflow-auto border border-divider rounded-lg shadow-inner bg-fill-2 p-2"> <div
class="flex-1 overflow-auto border rounded-lg shadow-inner p-2"
:style="node.attributes?.MERGED === 'TRUE' ? { backgroundColor: 'yellow', borderColor: '#ca8a04' } : {}"
>
<table <table
class="w-full border-collapse text-sm table-fixed min-w-[600px] transition-all" class="w-full border-collapse text-sm table-fixed min-w-[600px] transition-all"
:data-node-id="structure.tgroupId" :data-node-id="structure.tgroupId"
data-tag-name="TGROUP" data-tag-name="TGROUP"
:class="[isNodeSelected(structure.tgroupId) ? 'ring-2 ring-primary ring-offset-2 rounded' : '', tableFrameClass]" :class="[isNodeSelected(structure.tgroupId) ? 'ring-2 ring-primary ring-offset-2 rounded' : '', tableFrameClass]"
:style="editorStore.nodeMap.get(structure.tgroupId)?.node.attributes?.MERGED === 'TRUE' ? { backgroundColor: 'yellow' } : {}"
> >
<colgroup> <colgroup>
<col class="w-12" /> <col class="w-12" />
...@@ -30,6 +42,7 @@ ...@@ -30,6 +42,7 @@
@click.stop="editorStore.setSelectedNodeId(structure.theadId)" @click.stop="editorStore.setSelectedNodeId(structure.theadId)"
class="transition-all" class="transition-all"
:class="[isNodeSelected(structure.theadId) ? 'outline outline-2 outline-primary outline-offset-[-2px] bg-primary/5' : '']" :class="[isNodeSelected(structure.theadId) ? 'outline outline-2 outline-primary outline-offset-[-2px] bg-primary/5' : '']"
:style="editorStore.nodeMap.get(structure.theadId)?.node.attributes?.MERGED === 'TRUE' ? { backgroundColor: 'yellow' } : {}"
> >
<template v-for="(row, rIdx) in structure.theadRows" :key="row.id"> <template v-for="(row, rIdx) in structure.theadRows" :key="row.id">
<!-- 如果 ROW 拥有非 ENTRY 子节点 (如 EFFECT, CONEFFECT, REVST, REVEND) --> <!-- 如果 ROW 拥有非 ENTRY 子节点 (如 EFFECT, CONEFFECT, REVST, REVEND) -->
...@@ -56,6 +69,7 @@ ...@@ -56,6 +69,7 @@
isNodeSelected(row.id) ? 'outline outline-2 outline-primary outline-offset-[-2px] bg-primary/10' : '', isNodeSelected(row.id) ? 'outline outline-2 outline-primary outline-offset-[-2px] bg-primary/10' : '',
insertedRowIds.has(row.id) ? 'inserted-row-highlight' : '' insertedRowIds.has(row.id) ? 'inserted-row-highlight' : ''
]" ]"
:style="editorStore.nodeMap.get(row.id)?.node.attributes?.MERGED === 'TRUE' ? { backgroundColor: 'yellow' } : {}"
@click.stop="editorStore.setSelectedNodeId(row.id)" @click.stop="editorStore.setSelectedNodeId(row.id)"
> >
<!-- 表头行选择号 --> <!-- 表头行选择号 -->
...@@ -174,6 +188,7 @@ ...@@ -174,6 +188,7 @@
@click.stop="editorStore.setSelectedNodeId(structure.tbodyId)" @click.stop="editorStore.setSelectedNodeId(structure.tbodyId)"
class="transition-all" class="transition-all"
:class="[isNodeSelected(structure.tbodyId) ? 'outline outline-2 outline-primary outline-offset-[-2px] bg-primary/5' : '']" :class="[isNodeSelected(structure.tbodyId) ? 'outline outline-2 outline-primary outline-offset-[-2px] bg-primary/5' : '']"
:style="editorStore.nodeMap.get(structure.tbodyId)?.node.attributes?.MERGED === 'TRUE' ? { backgroundColor: 'yellow' } : {}"
> >
<template v-for="(row, rIdx) in structure.tbodyRows" :key="row.id"> <template v-for="(row, rIdx) in structure.tbodyRows" :key="row.id">
<!-- 如果 ROW 拥有非 ENTRY 子节点 (如 EFFECT, CONEFFECT, REVST, REVEND) --> <!-- 如果 ROW 拥有非 ENTRY 子节点 (如 EFFECT, CONEFFECT, REVST, REVEND) -->
...@@ -200,6 +215,7 @@ ...@@ -200,6 +215,7 @@
isNodeSelected(row.id) ? 'outline outline-2 outline-primary outline-offset-[-2px] bg-primary/10' : '', isNodeSelected(row.id) ? 'outline outline-2 outline-primary outline-offset-[-2px] bg-primary/10' : '',
insertedRowIds.has(row.id) ? 'inserted-row-highlight' : '' insertedRowIds.has(row.id) ? 'inserted-row-highlight' : ''
]" ]"
:style="editorStore.nodeMap.get(row.id)?.node.attributes?.MERGED === 'TRUE' ? { backgroundColor: 'yellow' } : {}"
@click.stop="editorStore.setSelectedNodeId(row.id)" @click.stop="editorStore.setSelectedNodeId(row.id)"
> >
<!-- 行号选择单元格 --> <!-- 行号选择单元格 -->
......
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