Commit 318eff57 by pangchong

feat(table-editor): 添加合并单元格保留内容功能

- 新增 TableMergeModal 组件,实现合并单元格时选择保留内容
- 在 TableEditor 中集成 TableMergeModal,实现弹框操作和确认合并逻辑
- 增加 useTableEditor hook 中的 executeMergeAction 方法处理合并并保留内容
- 修改合并单元格逻辑,支持根据用户选择保留指定内容合并至左上角单元格
- 支持快捷预设操作,如仅保留主单元格、保留非空内容、全选/全不选、反选等
- 提供合并后预览功能,展示合并后单元格内内容顺序拼接效果
- 优化单元格调整列宽手柄显示条件,避免超出列范围调整
- 重构相关工具函数,包含节点克隆、表格结构解析、节点查找等辅助功能
parent 9b6e3bc1
import type { XmlNode } from '@/types/xmlNode'
import type { TableStructureModel } from '@/views/editor/components/TableEditor/constants'
export interface MergeContentItem {
id: string
cellId: string
rowIdx: number
colIdx: number
tagName: string
text: string
xmlNode?: XmlNode
selected: boolean
}
export interface MergeCellGroup {
cellId: string
rowIdx: number
colIdx: number
isTopLeft: boolean
items: MergeContentItem[]
}
export interface TableMergeModalOpenParams {
tableNode: XmlNode
cellIds: string[]
minCol: number
maxCol: number
minRow: number
maxRow: number
isThead: boolean
structure: TableStructureModel
}
export type ConfirmMergeEmit = (event: 'confirm', retainedItems: MergeContentItem[], params: TableMergeModalOpenParams) => void
export interface MergePresetAction {
key: string
label: string | (() => string)
type?: 'default' | 'primary' | 'info' | 'success' | 'warning' | 'error'
handler: () => void
}
<template>
<CommonModal v-model="show" title="合并单元格 - 选择保留内容" :width="960" confirm-text="确认合并" @confirm="handleConfirm">
<div class="flex flex-col gap-3 py-1">
<!-- 说明提示 -->
<div class="text-xs text-color3 bg-fill-2 p-2.5 rounded border border-divider">
<span>请勾选需要保留的单元格内容,每条内容下方展示实际渲染效果,保留的内容将按顺序合并至左上角主单元格中。</span>
</div>
<!-- 左右双栏布局 -->
<div class="flex gap-4 items-stretch" style="min-height: 420px">
<!-- 左栏:单元格内容勾选列表 -->
<div class="flex-1 flex flex-col gap-2.5 min-w-0">
<!-- 快捷预设按钮栏 -->
<div class="flex items-center gap-2 shrink-0 overflow-x-auto">
<span class="text-xs text-color3 font-bold shrink-0">快捷选项:</span>
<CommonButton
v-for="action in presetActions"
:key="action.key"
size="tiny"
secondary
:type="action.type ?? 'default'"
@click="action.handler()"
>
{{ typeof action.label === 'function' ? action.label() : action.label }}
</CommonButton>
</div>
<!-- 单元格内容分组列表 -->
<div class="flex-1 max-h-[400px] overflow-y-auto space-y-3 pr-1.5">
<div
v-for="group in cellGroups"
:key="group.cellId"
class="border rounded-lg overflow-hidden transition-all"
:class="group.isTopLeft ? 'border-primary/50' : 'border-divider'"
>
<!-- 单元格组头部 -->
<div class="flex items-center justify-between px-3 py-2" :class="group.isTopLeft ? 'bg-primary/8' : 'bg-fill-2'">
<CommonCheckboxSingle
:checked="isGroupSelected(group)"
:indeterminate="isGroupIndeterminate(group)"
@update:checked="(val: boolean) => toggleGroup(group, val)"
>
<span class="font-bold text-xs text-color1">{{ group.rowIdx + 1 }} 行 · 第 {{ group.colIdx + 1 }}</span>
</CommonCheckboxSingle>
<CommonTag v-if="group.isTopLeft" type="primary" size="small">主单元格</CommonTag>
<CommonTag v-else type="default" size="small">合并单元格</CommonTag>
</div>
<!-- 段落/节点列表,每条内嵌 DocNodeRenderer 预览 -->
<div v-if="group.items.length > 0" class="divide-y divide-divider">
<div
v-for="item in group.items"
:key="item.id"
class="flex items-start gap-3 px-3 py-2 transition-all cursor-pointer"
:class="item.selected ? 'bg-fill-1' : 'bg-fill-3 opacity-60'"
@click.stop="item.selected = !item.selected"
>
<!-- 勾选框 -->
<CommonCheckboxSingle v-model:checked="item.selected" class="mt-0.5 shrink-0" @click.stop />
<!-- 内容预览区 -->
<div class="flex-1 min-w-0">
<!-- 若有 xmlNode,直接用 DocNodeRenderer 渲染实际效果 -->
<template v-if="item.xmlNode">
<div
class="p-2 rounded border text-sm leading-relaxed transition-all overflow-hidden"
:class="item.selected ? 'border-divider bg-card' : 'border-divider/40 bg-fill-3'"
style="max-height: 120px; overflow-y: auto"
>
<DocNodeRenderer :node="item.xmlNode" :parent="null" />
</div>
</template>
<!-- 无 xmlNode(纯文本段落),展示文本内容 -->
<template v-else>
<div
class="p-2 rounded border text-sm leading-relaxed transition-all"
:class="[
item.selected ? 'border-divider bg-card' : 'border-divider/40 bg-fill-3',
!item.selected ? 'line-through text-color3' : 'text-color1'
]"
>
{{ item.text || '(空内容)' }}
</div>
</template>
<!-- 标签名角标 -->
<span
class="inline-block mt-1 text-[10px] font-mono px-1.5 py-0.5 rounded"
:class="item.tagName === 'PARAC' ? 'bg-warning/15 text-warning' : 'bg-fill-3 text-color3'"
>
&lt;{{ item.tagName }}&gt;
</span>
</div>
</div>
</div>
<div v-else class="px-3 py-2 text-xs text-color3 italic bg-fill-1">(空单元格)</div>
</div>
</div>
</div>
<!-- 右栏:合并后效果预览面板 -->
<div class="w-[360px] shrink-0 bg-fill-2 p-3.5 rounded-lg border border-divider flex flex-col gap-2">
<div class="text-xs font-bold text-color3 flex items-center justify-between pb-2 border-b border-divider shrink-0">
<span>合并后效果预览</span>
<CommonTag size="tiny" type="primary">按顺序拼接</CommonTag>
</div>
<div class="flex-1 max-h-[400px] min-h-[300px] overflow-y-auto p-3 bg-card rounded border border-divider">
<template v-if="previewCellNode && previewCellNode.children.length > 0">
<DocNodeRenderer v-for="child in previewCellNode.children" :key="child.id" :node="child" :parent="previewCellNode" />
</template>
<div v-else class="h-full flex items-center justify-center text-xs text-color3 italic py-8 text-center">
(未选择任何内容,
<br />
合并后单元格将为空)
</div>
</div>
</div>
</div>
</div>
</CommonModal>
</template>
<script setup lang="ts">
import { useTableMergeModal } from './functionals'
import type { TableMergeModalOpenParams, MergeContentItem } from './constants'
import CommonCheckboxSingle from '@/components/CommonCheckboxSingle.vue'
import DocNodeRenderer from '@/views/editor/components/DocNodeRenderer/index.vue'
// 注入只读模式,禁用内部编辑事件
provide('diffMode', true)
const emit = defineEmits<{
(e: 'confirm', retainedItems: MergeContentItem[], params: TableMergeModalOpenParams): void
}>()
const {
show,
cellGroups,
open,
presetActions,
isGroupSelected,
isGroupIndeterminate,
toggleGroup,
previewCellNode,
handleConfirm
} = useTableMergeModal(emit)
defineExpose({ open })
</script>
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
<tr <tr
:data-node-id="row.id" :data-node-id="row.id"
data-tag-name="ROW" data-tag-name="ROW"
class="border-b border-divider group transition-colors transition-all" class="group transition-colors transition-all"
:class="[ :class="[
!isDiffMode ? 'hover:bg-fill-3 cursor-pointer' : 'cursor-default', !isDiffMode ? 'hover:bg-fill-3 cursor-pointer' : 'cursor-default',
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' : '',
...@@ -164,7 +164,7 @@ ...@@ -164,7 +164,7 @@
</template> </template>
<!-- 拖拽调整列宽手柄 --> <!-- 拖拽调整列宽手柄 -->
<div <div
v-if="!isDiffMode" v-if="!isDiffMode && ((cell.colIdx ?? 0) + (cell.colspan ?? 1)) < structure.cols"
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" 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)" @mousedown.stop.prevent="handleResizeStart(cell, $event)"
></div> ></div>
...@@ -209,7 +209,7 @@ ...@@ -209,7 +209,7 @@
<tr <tr
:data-node-id="row.id" :data-node-id="row.id"
data-tag-name="ROW" data-tag-name="ROW"
class="border-b border-divider group transition-colors transition-all" class="group transition-colors transition-all"
:class="[ :class="[
!isDiffMode ? 'hover:bg-fill-3 cursor-pointer' : 'cursor-default', !isDiffMode ? 'hover:bg-fill-3 cursor-pointer' : 'cursor-default',
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' : '',
...@@ -310,7 +310,7 @@ ...@@ -310,7 +310,7 @@
</template> </template>
<!-- 拖拽调整列宽手柄 --> <!-- 拖拽调整列宽手柄 -->
<div <div
v-if="!isDiffMode" v-if="!isDiffMode && ((cell.colIdx ?? 0) + (cell.colspan ?? 1)) < structure.cols"
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" 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)" @mousedown.stop.prevent="handleResizeStart(cell, $event)"
></div> ></div>
...@@ -365,6 +365,8 @@ ...@@ -365,6 +365,8 @@
<!-- 批量操作弹框 --> <!-- 批量操作弹框 -->
<TableBatchModal ref="batchModalRef" @confirm="executeBatchAction" /> <TableBatchModal ref="batchModalRef" @confirm="executeBatchAction" />
<!-- 合并单元格保留内容弹框 -->
<TableMergeModal ref="mergeModalRef" @confirm="executeMergeAction" />
</div> </div>
</template> </template>
...@@ -377,6 +379,7 @@ import { CELL_EDIT_TIP } from './constants' ...@@ -377,6 +379,7 @@ import { CELL_EDIT_TIP } from './constants'
import { useEditorStore } from '@/store/editor' import { useEditorStore } from '@/store/editor'
import DocNodeRenderer from '../DocNodeRenderer/index.vue' import DocNodeRenderer from '../DocNodeRenderer/index.vue'
import TableBatchModal from './components/TableBatchModal/index.vue' import TableBatchModal from './components/TableBatchModal/index.vue'
import TableMergeModal from './components/TableMergeModal/index.vue'
const props = defineProps<{ const props = defineProps<{
node: XmlNode node: XmlNode
...@@ -386,6 +389,10 @@ const editorStore = useEditorStore() ...@@ -386,6 +389,10 @@ const editorStore = useEditorStore()
const injectedDiffMode = inject<boolean | Ref<boolean>>('diffMode', false) const injectedDiffMode = inject<boolean | Ref<boolean>>('diffMode', false)
const isDiffMode = computed(() => (typeof injectedDiffMode === 'boolean' ? injectedDiffMode : injectedDiffMode.value)) const isDiffMode = computed(() => (typeof injectedDiffMode === 'boolean' ? injectedDiffMode : injectedDiffMode.value))
/** TableMergeModal 与 TableBatchModal 组件实例引用 */
const mergeModalRef = ref<InstanceType<typeof TableMergeModal>>()
const batchModalRef = ref<InstanceType<typeof TableBatchModal>>()
const { const {
structure, structure,
selectedCellIds, selectedCellIds,
...@@ -414,8 +421,9 @@ const { ...@@ -414,8 +421,9 @@ const {
colWidthStyles, colWidthStyles,
tableFrameClass, tableFrameClass,
getCellStyle, getCellStyle,
handleResizeStart handleResizeStart,
} = useTableEditor(props) executeMergeAction
} = useTableEditor(props, { mergeModalRef })
const isNodeSelected = (id: string) => { const isNodeSelected = (id: string) => {
if (isDiffMode.value) return false if (isDiffMode.value) return false
...@@ -427,9 +435,6 @@ const isCellSelected = (cell: any, colIdx: number) => { ...@@ -427,9 +435,6 @@ const isCellSelected = (cell: any, colIdx: number) => {
return rawIsCellSelected(cell, colIdx) return rawIsCellSelected(cell, colIdx)
} }
/** TableBatchModal 组件实例引用 */
const batchModalRef = ref<InstanceType<typeof TableBatchModal>>()
const { onContextMenuSelect, executeBatchAction } = useTableBatchActions({ const { onContextMenuSelect, executeBatchAction } = useTableBatchActions({
batchModalRef, batchModalRef,
handleContextMenuSelect, handleContextMenuSelect,
......
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