Commit b862acf8 by pangchong

feat(editor): 实现表格列和跨列规格节点删除功能

- 支持删除 COLSPEC 节点,自动更新表格列数及列索引属性
- 支持删除对应列的所有 ENTRY 单元格,并调整跨列单元格的起止位置
- 支持删除 SPANSPEC 节点,并还原相关 ENTRY 节的跨列属性
- 优化删除逻辑,优先处理特殊节点 COLSPEC 和 SPANSPEC,剩余节点统一删除
- 实现表格列宽拖拽调整功能,支持动态更新 COLSPEC 宽度属性
- 添加预览打印功能,生成干净无编辑样式的打印版 HTML
- 支持将预览内容导出为独立 HTML 文件下载
- 在打印预览及导出 HTML 时排除编辑辅助元素及相关样式
- 修改节点渲染组件,隐藏重复中英文段落,减少重复内容展示
- 编辑界面增加隐藏的打印预览容器,用以生成纯净打印内容
- 编辑工具栏新增预览和下载 HTML 按钮,提升用户操作便利性
parent 4dafd6c7
...@@ -56,6 +56,67 @@ export function useDocNodeRenderer(props: { node: XmlNode; parent?: XmlNode | nu ...@@ -56,6 +56,67 @@ 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)
}) })
...@@ -375,6 +436,7 @@ export function useDocNodeRenderer(props: { node: XmlNode; parent?: XmlNode | nu ...@@ -375,6 +436,7 @@ export function useDocNodeRenderer(props: { node: XmlNode; parent?: XmlNode | nu
isHeaderTag, isHeaderTag,
isInsideAlert, isInsideAlert,
isChineseContext, isChineseContext,
shouldHideDuplicate,
formatEff, formatEff,
getListBullet, getListBullet,
getTopicSeqNum, getTopicSeqNum,
......
<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"
class="doc-node-wrapper relative transition-all" class="doc-node-wrapper relative transition-all"
...@@ -499,7 +500,7 @@ ...@@ -499,7 +500,7 @@
<!-- 16. CBLST (电路断路器列表) 特殊处理 (仿照 PDF 样式表格) --> <!-- 16. CBLST (电路断路器列表) 特殊处理 (仿照 PDF 样式表格) -->
<template v-else-if="node.tagName === 'CBLST'"> <template v-else-if="node.tagName === 'CBLST'">
<div class="my-4 overflow-x-auto select-none border border-divider rounded-lg p-3 bg-card shadow-sm"> <div class="my-4 overflow-x-auto select-none border border-divider rounded-lg p-3 bg-card shadow-sm">
<div class="text-xs font-bold text-color3 mb-2 flex items-center space-x-1"> <div class="text-xs font-bold text-color3 mb-2 flex items-center space-x-1 print-hide">
<n-icon color="var(--primary-color)"><grid-outline /></n-icon> <n-icon color="var(--primary-color)"><grid-outline /></n-icon>
<span> <span>
电路断路器清单 (CBLST) - 行动: 电路断路器清单 (CBLST) - 行动:
...@@ -618,7 +619,7 @@ ...@@ -618,7 +619,7 @@
<!-- 18. CALS TABLE (表格) 可视化编辑器集成 --> <!-- 18. CALS TABLE (表格) 可视化编辑器集成 -->
<template v-else-if="node.tagName === 'TABLE'"> <template v-else-if="node.tagName === 'TABLE'">
<div class="my-4 border border-divider rounded-lg overflow-hidden bg-card p-3 shadow-sm"> <div class="my-4 border border-divider rounded-lg overflow-hidden bg-card p-3 shadow-sm">
<div class="flex items-center space-x-2 mb-2 pb-2 border-b border-divider"> <div class="flex items-center space-x-2 mb-2 pb-2 border-b border-divider print-hide">
<n-icon color="var(--primary-color)"><grid-outline /></n-icon> <n-icon color="var(--primary-color)"><grid-outline /></n-icon>
<span class="text-xs font-bold text-color2">表格编辑区域</span> <span class="text-xs font-bold text-color2">表格编辑区域</span>
</div> </div>
...@@ -629,7 +630,7 @@ ...@@ -629,7 +630,7 @@
<!-- 19. GRAPHIC (图片) 可视化处理 --> <!-- 19. GRAPHIC (图片) 可视化处理 -->
<template v-else-if="node.tagName === 'GRAPHIC'"> <template v-else-if="node.tagName === 'GRAPHIC'">
<div class="my-4 border border-divider rounded-lg overflow-hidden bg-fill-2 p-4 flex flex-col items-center"> <div class="my-4 border border-divider rounded-lg overflow-hidden bg-fill-2 p-4 flex flex-col items-center">
<div class="w-full flex items-center justify-between pb-2 border-b border-divider mb-3"> <div class="w-full flex items-center justify-between pb-2 border-b border-divider mb-3 print-hide">
<span class="text-xs font-bold text-color2 flex items-center space-x-1"> <span class="text-xs font-bold text-color2 flex items-center space-x-1">
<n-icon><image-outline /></n-icon> <n-icon><image-outline /></n-icon>
<span>工卡附图 [GNBR: {{ node.children.find((c) => c.tagName === 'SHEET')?.attributes.GNBR || '无' }}]</span> <span>工卡附图 [GNBR: {{ node.children.find((c) => c.tagName === 'SHEET')?.attributes.GNBR || '无' }}]</span>
...@@ -699,7 +700,7 @@ ...@@ -699,7 +700,7 @@
<!-- 22. SELECTION (选项组) 处理 --> <!-- 22. SELECTION (选项组) 处理 -->
<template v-else-if="node.tagName === 'SELECTION'"> <template v-else-if="node.tagName === 'SELECTION'">
<div class="my-3 p-3 bg-fill-3 rounded-lg border border-divider space-y-2"> <div class="my-3 p-3 bg-fill-3 rounded-lg border border-divider space-y-2">
<div class="text-xs font-bold text-color3 mb-1 select-none">选项组配置:</div> <div class="text-xs font-bold text-color3 mb-1 select-none print-hide">选项组配置:</div>
<div class="flex flex-wrap gap-4"> <div class="flex flex-wrap gap-4">
<div <div
v-for="item in node.children" v-for="item in node.children"
...@@ -787,7 +788,7 @@ ...@@ -787,7 +788,7 @@
<!-- 25. ZONELST 隐藏处理 (只在被选中时渲染灰色提示框) --> <!-- 25. ZONELST 隐藏处理 (只在被选中时渲染灰色提示框) -->
<template v-else-if="node.tagName === 'ZONELST'"> <template v-else-if="node.tagName === 'ZONELST'">
<div v-if="isSelected" class="p-2 border border-dashed border-divider rounded bg-fill-2 text-center text-xs text-color3 select-none"> <div v-if="isSelected" class="p-2 border border-dashed border-divider rounded bg-fill-2 text-center text-xs text-color3 select-none print-hide">
[ 区域列表 &lt;ZONELST&gt; PDF 渲染中已设为隐藏,不予展示内容 ] [ 区域列表 &lt;ZONELST&gt; PDF 渲染中已设为隐藏,不予展示内容 ]
</div> </div>
</template> </template>
...@@ -1013,6 +1014,7 @@ const { ...@@ -1013,6 +1014,7 @@ const {
isHeaderTag, isHeaderTag,
isInsideAlert, isInsideAlert,
isChineseContext, isChineseContext,
shouldHideDuplicate,
formatEff, formatEff,
getListBullet, getListBullet,
getTopicSeqNum, getTopicSeqNum,
......
...@@ -93,12 +93,18 @@ ...@@ -93,12 +93,18 @@
导出 XML 导出 XML
</CommonButton> </CommonButton>
<!-- <CommonButton size="small" type="primary" class="preview-btn flex-shrink-0" @click="emit('preview')"> <CommonButton size="small" type="primary" class="preview-btn flex-shrink-0" @click="emit('download-html')">
<template #icon>
<n-icon><eye-outline /></n-icon>
</template>
下载html
</CommonButton>
<CommonButton size="small" type="primary" class="preview-btn flex-shrink-0" @click="emit('preview')">
<template #icon> <template #icon>
<n-icon><eye-outline /></n-icon> <n-icon><eye-outline /></n-icon>
</template> </template>
预览工卡 预览工卡
</CommonButton> --> </CommonButton>
<n-divider vertical class="!mx-0 flex-shrink-0" /> <n-divider vertical class="!mx-0 flex-shrink-0" />
...@@ -196,7 +202,7 @@ import BatchTranslateModal from './components/BatchTranslateModal/index.vue' ...@@ -196,7 +202,7 @@ import BatchTranslateModal from './components/BatchTranslateModal/index.vue'
import ExtractTranslateModal from './components/ExtractTranslateModal/index.vue' import ExtractTranslateModal from './components/ExtractTranslateModal/index.vue'
import SearchTranslateModal from './components/SearchTranslateModal/index.vue' import SearchTranslateModal from './components/SearchTranslateModal/index.vue'
const emit = defineEmits(['save', 'validate', 'export', 'preview']) const emit = defineEmits(['save', 'validate', 'export', 'preview', 'download-html'])
const { const {
editorStore, editorStore,
......
...@@ -57,6 +57,8 @@ export function useTableEditor(props: { node: XmlNode }) { ...@@ -57,6 +57,8 @@ export function useTableEditor(props: { node: XmlNode }) {
}) })
const selectedCellIds = ref<string[]>([]) const selectedCellIds = ref<string[]>([])
const resizedWidths = ref<string[] | null>(null)
const preventCellClick = ref(false)
const insertedRowIds = ref<Set<string>>(new Set()) const insertedRowIds = ref<Set<string>>(new Set())
const insertedCellIds = ref<Set<string>>(new Set()) const insertedCellIds = ref<Set<string>>(new Set())
...@@ -216,6 +218,24 @@ export function useTableEditor(props: { node: XmlNode }) { ...@@ -216,6 +218,24 @@ export function useTableEditor(props: { node: XmlNode }) {
text: getDeepText(pNode), text: getDeepText(pNode),
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,
...@@ -1127,10 +1147,12 @@ export function useTableEditor(props: { node: XmlNode }) { ...@@ -1127,10 +1147,12 @@ export function useTableEditor(props: { node: XmlNode }) {
} }
const handleCellClick = (cell: TableCellModel, e: MouseEvent) => { const handleCellClick = (cell: TableCellModel, e: MouseEvent) => {
if (preventCellClick.value) return
selectCellLogic(cell, e) selectCellLogic(cell, e)
} }
const handleParaClick = (para: any, cell: TableCellModel, e: MouseEvent) => { const handleParaClick = (para: any, cell: TableCellModel, e: MouseEvent) => {
if (preventCellClick.value) return
store.setSelectedNodeId(para.id) store.setSelectedNodeId(para.id)
selectCellLogic(cell, e) selectCellLogic(cell, e)
} }
...@@ -1550,6 +1572,9 @@ export function useTableEditor(props: { node: XmlNode }) { ...@@ -1550,6 +1572,9 @@ export function useTableEditor(props: { node: XmlNode }) {
/** 计算各列的宽度样式 */ /** 计算各列的宽度样式 */
const colWidthStyles = computed(() => { const colWidthStyles = computed(() => {
if (resizedWidths.value) {
return resizedWidths.value
}
const specs = structure.value.colSpecs || [] const specs = structure.value.colSpecs || []
const colsCount = structure.value.cols const colsCount = structure.value.cols
...@@ -1704,6 +1729,100 @@ export function useTableEditor(props: { node: XmlNode }) { ...@@ -1704,6 +1729,100 @@ export function useTableEditor(props: { node: XmlNode }) {
return rowNodeObj.node.children.filter((c) => c.tagName !== 'ENTRY') return rowNodeObj.node.children.filter((c) => c.tagName !== 'ENTRY')
} }
const handleResizeStart = (cell: TableCellModel, e: MouseEvent) => {
const colIdx = (cell.colIdx ?? 0) + (cell.colspan ?? 1) - 1
if (colIdx < 0 || colIdx >= structure.value.cols) return
const tableEl = (e.target as HTMLElement).closest('table')
if (!tableEl) return
const colElements = tableEl.querySelectorAll('colgroup col')
if (colElements.length < structure.value.cols + 2) return
const startWidths: number[] = []
for (let i = 0; i < structure.value.cols; i++) {
const colEl = colElements[i + 1] as HTMLElement
const computedWidth = parseFloat(window.getComputedStyle(colEl).width)
startWidths.push(isNaN(computedWidth) ? 80 : computedWidth)
}
const startX = e.clientX
let justResized = false
const handleMouseMove = (moveEvent: MouseEvent) => {
const dx = moveEvent.clientX - startX
if (Math.abs(dx) > 2) {
justResized = true
}
const newWidths = [...startWidths]
const minWidth = 40
if (colIdx < structure.value.cols - 1) {
let newWidthI = startWidths[colIdx] + dx
let newWidthNext = startWidths[colIdx + 1] - dx
if (newWidthI < minWidth) {
const diff = minWidth - newWidthI
newWidthI = minWidth
newWidthNext = newWidthNext - diff
}
if (newWidthNext < minWidth) {
const diff = minWidth - newWidthNext
newWidthNext = minWidth
newWidthI = newWidthI - diff
}
newWidths[colIdx] = newWidthI
newWidths[colIdx + 1] = newWidthNext
} else {
let newWidthI = startWidths[colIdx] + dx
if (newWidthI < minWidth) {
newWidthI = minWidth
}
newWidths[colIdx] = newWidthI
}
resizedWidths.value = newWidths.map((w) => `${w}px`)
}
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
if (justResized) {
preventCellClick.value = true
setTimeout(() => {
preventCellClick.value = false
}, 50)
}
if (resizedWidths.value) {
store.saveSnapshot()
const tgroup = findTgroup(props.node)
if (tgroup) {
const colSpecs = tgroup.children.filter((c) => c.tagName === 'COLSPEC')
colSpecs.forEach((spec, idx) => {
if (resizedWidths.value) {
const finalWidth = parseFloat(resizedWidths.value[idx])
const originalWidth = spec.attributes.COLWIDTH || spec.attributes.colwidth || ''
if (originalWidth.includes('*') || !originalWidth) {
spec.attributes.COLWIDTH = `${Math.round(finalWidth)}*`
} else {
const unit = originalWidth.replace(/[0-9.]/g, '').trim() || 'px'
spec.attributes.COLWIDTH = `${Math.round(finalWidth)}${unit}`
}
}
})
}
resizedWidths.value = null
store.rebuildNodeMap()
}
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
}
return { return {
structure, structure,
selectedCellIds, selectedCellIds,
...@@ -1748,7 +1867,8 @@ export function useTableEditor(props: { node: XmlNode }) { ...@@ -1748,7 +1867,8 @@ export function useTableEditor(props: { node: XmlNode }) {
// 样式计算 // 样式计算
colWidthStyles, colWidthStyles,
tableFrameClass, tableFrameClass,
getCellStyle getCellStyle,
handleResizeStart
} }
} }
......
...@@ -94,6 +94,11 @@ ...@@ -94,6 +94,11 @@
v-text="para.text" v-text="para.text"
></div> ></div>
</template> </template>
<!-- 拖拽调整列宽手柄 -->
<div
class="absolute -right-1 top-0 bottom-0 w-2 cursor-col-resize hover:bg-primary/40 active:bg-primary/60 transition-colors z-20 select-none"
@mousedown.stop.prevent="handleResizeStart(cell, $event)"
></div>
</th> </th>
<!-- 表头操作栏 --> <!-- 表头操作栏 -->
<th class="p-2 border border-divider bg-fill-4 text-center"> <th class="p-2 border border-divider bg-fill-4 text-center">
...@@ -180,6 +185,11 @@ ...@@ -180,6 +185,11 @@
v-text="para.text" v-text="para.text"
></div> ></div>
</template> </template>
<!-- 拖拽调整列宽手柄 -->
<div
class="absolute -right-1 top-0 bottom-0 w-2 cursor-col-resize hover:bg-primary/40 active:bg-primary/60 transition-colors z-20 select-none"
@mousedown.stop.prevent="handleResizeStart(cell, $event)"
></div>
</td> </td>
<!-- 行删除按钮 --> <!-- 行删除按钮 -->
<td class="p-2 border border-divider text-center"> <td class="p-2 border border-divider text-center">
...@@ -273,7 +283,8 @@ const { ...@@ -273,7 +283,8 @@ const {
getRowNonEntryChildren, getRowNonEntryChildren,
colWidthStyles, colWidthStyles,
tableFrameClass, tableFrameClass,
getCellStyle getCellStyle,
handleResizeStart
} = useTableEditor(props) } = useTableEditor(props)
/** TableBatchModal 组件实例引用 */ /** TableBatchModal 组件实例引用 */
......
...@@ -106,11 +106,276 @@ export function useEditor() { ...@@ -106,11 +106,276 @@ export function useEditor() {
} }
} }
const prepareCleanedHtml = () => {
const container = document.getElementById('print-preview-container')
if (!container) return null
// 克隆 HTML 树以进行离线清理
const clone = container.cloneNode(true) as HTMLElement
clone.style.display = 'block'
// 0. 移除所有带有 print-hide 类名的编辑辅助/提示元素
clone.querySelectorAll('.print-hide').forEach((el) => {
el.remove()
})
// 1. 去除 contenteditable 属性,防止打印时显示光标或被编辑
clone.querySelectorAll('[contenteditable]').forEach((el) => {
el.removeAttribute('contenteditable')
})
// 2. 清理全部与编辑、高亮、聚焦相关的样式类名
clone.querySelectorAll('*').forEach((el) => {
const classes = Array.from(el.classList)
classes.forEach((cls) => {
if (
cls.startsWith('ring-') ||
cls.startsWith('outline-') ||
cls === 'outline' ||
cls.startsWith('bg-primary') ||
cls.includes('focus:') ||
cls.includes('hover:') ||
cls === 'cursor-pointer' ||
cls === 'transition-all' ||
cls === 'transition-colors'
) {
el.classList.remove(cls)
}
})
})
// 3. 移除表格的调整列宽拖拽手柄
clone.querySelectorAll('.cursor-col-resize').forEach((el) => {
el.remove()
})
// 4. 移除表格的辅助列(行号列、操作删除列)与删除快捷行
clone.querySelectorAll('table').forEach((table) => {
// 如果没有 colgroup,说明不是 CALS 数据表格,可能是签字点表格,跳过辅助列裁剪
if (!table.querySelector('colgroup')) {
return
}
// 标记为真实 CALS 数据表格,以便应用 100% 宽度等定制打印样式
table.classList.add('cals-table')
// 移除列删除快捷按钮行(即 tbody 中的最后一行 tr)
table.querySelectorAll('tbody').forEach((tbody) => {
const rows = Array.from(tbody.children).filter((el) => el.tagName === 'TR')
if (rows.length > 0) {
rows[rows.length - 1].remove()
}
})
// 移除 colgroup 的首尾 col
table.querySelectorAll('colgroup').forEach((cg) => {
const cols = cg.querySelectorAll('col')
if (cols.length >= 2) {
cols[0].remove()
cols[cols.length - 1].remove()
}
})
// 移除每一行的首尾单元格
table.querySelectorAll('tr').forEach((tr) => {
const cells = Array.from(tr.children)
if (cells.length >= 2) {
cells[0].remove()
cells[cells.length - 1].remove()
}
})
// 移除外层的灰色卡片背景及阴影边框样式,使表格呈现扁平文档打印风格
const wrapper = table.parentElement
if (wrapper) {
wrapper.className = ''
const grandparent = wrapper.parentElement
if (grandparent) {
grandparent.className = ''
}
}
})
// 移除图片 (GRAPHIC) 外层的边框与背景色类名
clone.querySelectorAll('.flex.flex-col.items-center').forEach((el) => {
if (el.classList.contains('border-divider')) {
el.className = 'flex flex-col items-center'
}
})
// 5. 移除表格顶部的快捷操作工具栏
clone.querySelectorAll('.flex.items-center.pb-2.border-b').forEach((el) => {
el.remove()
})
// 6. 获取当前页面的所有 CSS 样式(含 Tailwind 和 Naive UI 样式)
const styles = Array.from(document.querySelectorAll('link[rel="stylesheet"], style'))
.map((el) => el.outerHTML)
.join('\n')
return {
innerHTML: clone.innerHTML,
styles
}
}
const printStyles = `
body {
background-color: #ffffff !important;
color: #000000 !important;
padding: 40px !important;
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
/* 移除非表格元素的虚线边框、段落分隔线以及容器装饰边框 */
.border-b.border-divider\\/30,
.border-b.border-divider,
.border-l.border-dashed {
border: none !important;
border-bottom: none !important;
border-left: none !important;
padding-left: 0 !important;
padding-bottom: 0 !important;
padding-top: 0 !important;
margin-left: 0 !important;
}
/* 移除外层卡片及编辑容器的圆角边框、背景及阴影 */
.border.border-divider.rounded-lg,
.rounded-lg.shadow-inner.bg-fill-2,
.bg-card.p-3 {
border: none !important;
box-shadow: none !important;
background: transparent !important;
background-color: transparent !important;
padding: 0 !important;
margin: 0 !important;
}
/* 隐藏编辑器辅助标记元素 */
.print-hide {
display: none !important;
}
/* 避免打印时因祖先容器的 overflow 限制造成表格右侧截断 */
.print-preview-root,
.print-preview-root *,
table.cals-table {
overflow: visible !important;
}
/* 强置数据表格黑白打印边界与 100% 宽度,并覆写编辑态的 min-width 限制以自适应收缩 */
table.cals-table {
border-collapse: collapse !important;
width: 100% !important;
min-width: 0 !important;
margin-top: 10px !important;
margin-bottom: 20px !important;
}
table.cals-table th, table.cals-table td {
border: 1px solid #000000 !important;
padding: 8px 10px !important;
}
@media print {
body {
padding: 0 !important;
}
/* 打印优化:避免表格行或图片在中间被截断分页 */
table, tr, img {
page-break-inside: avoid !important;
}
}
`
const handlePreview = () => {
const cleaned = prepareCleanedHtml()
if (!cleaned) return
// 7. 创建隐藏的 iframe 以启动原生打印,避免弹出新窗口/标签页
const iframe = document.createElement('iframe')
iframe.style.position = 'fixed'
iframe.style.right = '0'
iframe.style.bottom = '0'
iframe.style.width = '0'
iframe.style.height = '0'
iframe.style.border = '0'
document.body.appendChild(iframe)
const iframeDoc = iframe.contentWindow?.document || iframe.contentDocument
if (!iframeDoc) {
window.$message.error('无法创建打印通道')
document.body.removeChild(iframe)
return
}
iframeDoc.write(`
<!DOCTYPE html>
<html>
<head>
<title>工卡预览与打印</title>
${cleaned.styles}
<style>
${printStyles}
</style>
</head>
<body>
<div class="print-preview-root">
${cleaned.innerHTML}
</div>
<script>
window.onload = function() {
setTimeout(() => {
window.print();
}, 300);
};
<\/script>
</body>
</html>
`)
iframeDoc.close()
// 打印完成后(或取消后)销毁 iframe 释放内存
const handleAfterPrint = () => {
document.body.removeChild(iframe)
}
if (iframe.contentWindow) {
iframe.contentWindow.addEventListener('afterprint', handleAfterPrint)
} else {
// 安全回退,防备老旧浏览器不支持 afterprint 事件
setTimeout(() => {
if (iframe.parentNode) {
document.body.removeChild(iframe)
}
}, 5000)
}
}
const handleDownloadHtml = () => {
const cleaned = prepareCleanedHtml()
if (!cleaned) return
const htmlContent = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>工卡预览与打印</title>
${cleaned.styles}
<style>
${printStyles}
</style>
</head>
<body>
<div class="print-preview-root">
${cleaned.innerHTML}
</div>
</body>
</html>`
const blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8;' })
openDownloadModal({
fileName: 'jobcard_flow_preview.html',
title: '下载 HTML 预览包',
localBlob: blob
})
}
return { return {
initialize, initialize,
save, save,
exportXml, exportXml,
getAllNodeKeys, getAllNodeKeys,
validate validate,
handlePreview,
handleDownloadHtml
} }
} }
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
@export="handleExport" @export="handleExport"
@expand-all="handleExpandAll" @expand-all="handleExpandAll"
@collapse-all="handleCollapseAll" @collapse-all="handleCollapseAll"
@preview="handlePreview"
@download-html="handleDownloadHtml"
/> />
<!-- 主体布局:自定义可拖拽分割面板 --> <!-- 主体布局:自定义可拖拽分割面板 -->
...@@ -28,6 +30,11 @@ ...@@ -28,6 +30,11 @@
<EditorPanel /> <EditorPanel />
</div> </div>
</div> </div>
<!-- 用于打印/预览的隐藏容器,不使用虚拟滚动以加载完整文档 -->
<div id="print-preview-container" style="display: none;">
<DocNodeRenderer v-if="editorStore.xmlTree" :node="editorStore.xmlTree" :parent="null" />
</div>
</div> </div>
</template> </template>
...@@ -38,6 +45,7 @@ import EditorToolbar from './components/EditorToolbar/index.vue' ...@@ -38,6 +45,7 @@ import EditorToolbar from './components/EditorToolbar/index.vue'
import NodeTree from './components/NodeTree/index.vue' import NodeTree from './components/NodeTree/index.vue'
import EditorPanel from './components/EditorPanel/index.vue' import EditorPanel from './components/EditorPanel/index.vue'
import Splitter from './components/Splitter/index.vue' import Splitter from './components/Splitter/index.vue'
import DocNodeRenderer from './components/DocNodeRenderer/index.vue'
const themeVars = useThemeVars() const themeVars = useThemeVars()
const editorStore = useEditorStore() const editorStore = useEditorStore()
...@@ -51,7 +59,7 @@ const leftWidthPx = ref(560) ...@@ -51,7 +59,7 @@ const leftWidthPx = ref(560)
const isCollapsed = ref(false) const isCollapsed = ref(false)
// 实例化业务逻辑 Hook // 实例化业务逻辑 Hook
const { initialize, save, exportXml, getAllNodeKeys, validate } = useEditor() const { initialize, save, exportXml, getAllNodeKeys, validate, handlePreview, handleDownloadHtml } = useEditor()
onMounted(() => { onMounted(() => {
initialize() initialize()
......
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