富文本问题

This commit is contained in:
2026-07-14 12:58:25 +08:00
parent c36003db2f
commit 3ded1868a3
4 changed files with 664 additions and 150 deletions
@@ -8,6 +8,7 @@
ok-text="确认采集"
cancel-text="取消"
@ok="handleSubmit"
centered
>
<div class="collect-body">
<a-descriptions :column="2" size="small" bordered class="contract-brief">
@@ -1354,7 +1354,7 @@ onBeforeUnmount(() => {
.contract-template-layout {
display: grid;
grid-template-columns: 460px minmax(0, 1fr);
grid-template-columns: 560px minmax(0, 1fr);
height: 100%;
min-height: 0;
}
@@ -1,29 +1,111 @@
<template>
<div class="template-rich-editor" :class="{ 'is-disabled': disabled }">
<Toolbar
class="editor-toolbar"
:editor="editorRef"
:default-config="toolbarConfig"
:mode="mode"
/>
<Editor
class="editor-content"
:model-value="modelValue"
:default-config="editorConfig"
:mode="mode"
@onCreated="handleCreated"
@update:modelValue="handleChange"
/>
<div class="editor-tip">
支持普通文字列表和表格复制带合并单元格的表格时会自动规整结构建议粘贴后结合右侧预览确认最终 Word 效果
<div class="editor-toolbar">
<div class="toolbar-groups">
<div v-for="(group, groupIndex) in toolbarGroups" :key="groupIndex" class="toolbar-group">
<button
v-for="action in group"
:key="action.key"
type="button"
class="toolbar-button"
:disabled="disabled"
@mousedown.prevent
@click="handleToolbarAction(action)"
>
{{ action.label }}
</button>
</div>
</div>
<button
type="button"
class="toolbar-button toolbar-button-primary"
@mousedown.prevent
@click="openFullscreen"
>
{{ disabled ? '宽屏查看' : '宽屏编辑' }}
</button>
</div>
<div class="editor-content-shell">
<div class="editor-scroll">
<div
ref="inlineEditorRef"
class="editor-surface"
:class="{ 'has-table': containsTable }"
:contenteditable="String(!disabled)"
:data-placeholder="placeholder"
@focus="setActiveSurface('inline')"
@mouseup="captureSelection('inline')"
@keyup="captureSelection('inline')"
@input="handleEditorInput('inline')"
@paste="handlePaste($event, 'inline')"
></div>
</div>
</div>
<div class="editor-tip">
复制 Word 表格时会优先保留原始合并结构表格较宽或包含合并单元格时使用宽屏编辑可避免内容被压缩成竖排
</div>
<a-modal
:open="isFullscreen"
:title="disabled ? '富文本宽屏查看' : '富文本宽屏编辑'"
width="96vw"
:footer="null"
:mask-closable="false"
:body-style="{ padding: '0', height: '82vh', overflow: 'hidden' }"
@update:open="handleFullscreenVisibleChange"
>
<div class="fullscreen-layout">
<div class="editor-toolbar fullscreen-toolbar">
<div class="toolbar-groups">
<div v-for="(group, groupIndex) in toolbarGroups" :key="`fullscreen-${groupIndex}`" class="toolbar-group">
<button
v-for="action in group"
:key="`fullscreen-${action.key}`"
type="button"
class="toolbar-button"
:disabled="disabled"
@mousedown.prevent
@click="handleToolbarAction(action, 'fullscreen')"
>
{{ action.label }}
</button>
</div>
</div>
<button
type="button"
class="toolbar-button toolbar-button-primary"
@mousedown.prevent
@click="isFullscreen = false"
>
完成
</button>
</div>
<div class="fullscreen-body">
<div class="editor-scroll fullscreen-scroll">
<div
ref="fullscreenEditorRef"
class="editor-surface fullscreen-surface"
:class="{ 'has-table': containsTable }"
:contenteditable="String(!disabled)"
:data-placeholder="placeholder"
@focus="setActiveSurface('fullscreen')"
@mouseup="captureSelection('fullscreen')"
@keyup="captureSelection('fullscreen')"
@input="handleEditorInput('fullscreen')"
@paste="handlePaste($event, 'fullscreen')"
></div>
</div>
</div>
</div>
</a-modal>
</div>
</template>
<script setup>
import { computed, onBeforeUnmount, shallowRef } from 'vue'
import '@wangeditor/editor/dist/css/style.css'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { computed, nextTick, ref, watch } from 'vue'
const props = defineProps({
modelValue: {
@@ -42,8 +124,12 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue'])
const mode = 'default'
const editorRef = shallowRef(null)
const inlineEditorRef = ref(null)
const fullscreenEditorRef = ref(null)
const isFullscreen = ref(false)
const activeSurface = ref('inline')
const currentHtml = ref('')
const TABLE_STYLE_PROPS = [
'width',
'min-width',
@@ -56,97 +142,327 @@ const TABLE_STYLE_PROPS = [
'font-style',
'text-decoration',
'white-space',
'border',
'border-top',
'border-right',
'border-bottom',
'border-left',
'border-collapse',
'table-layout',
]
const toolbarConfig = {
toolbarKeys: [
'headerSelect',
'|',
'bold',
'italic',
'underline',
'through',
'|',
'color',
'bgColor',
'|',
'bulletedList',
'numberedList',
'|',
'justifyLeft',
'justifyCenter',
'justifyRight',
'|',
'insertTable',
'deleteTable',
'insertTableRow',
'deleteTableRow',
'insertTableCol',
'deleteTableCol',
'tableHeader',
'|',
'undo',
'redo',
const INLINE_STYLE_PROPS = [
'text-align',
'background-color',
'color',
'font-weight',
'font-style',
'text-decoration',
'white-space',
]
const toolbarGroups = [
[
{ key: 'bold', label: '加粗', command: 'bold' },
{ key: 'italic', label: '斜体', command: 'italic' },
{ key: 'underline', label: '下划线', command: 'underline' },
{ key: 'removeFormat', label: '清除格式', command: 'removeFormat' },
],
excludeKeys: ['fullScreen'],
[
{ key: 'justifyLeft', label: '左对齐', command: 'justifyLeft' },
{ key: 'justifyCenter', label: '居中', command: 'justifyCenter' },
{ key: 'justifyRight', label: '右对齐', command: 'justifyRight' },
],
[
{ key: 'insertUnorderedList', label: '无序列表', command: 'insertUnorderedList' },
{ key: 'insertOrderedList', label: '有序列表', command: 'insertOrderedList' },
],
[
{ key: 'insertTable', label: '插入表格', type: 'insertTable' },
{ key: 'undo', label: '撤销', command: 'undo' },
{ key: 'redo', label: '重做', command: 'redo' },
],
]
const savedRanges = {
inline: null,
fullscreen: null,
}
const editorConfig = computed(() => ({
placeholder: props.placeholder,
readOnly: props.disabled,
autoFocus: false,
scroll: true,
customPaste: handleCustomPaste,
hoverbarKeys: {
table: {
menuKeys: [
'insertTableRow',
'deleteTableRow',
'insertTableCol',
'deleteTableCol',
'tableHeader',
'deleteTable',
],
},
const containsTable = computed(() => /<table[\s>]/i.test(currentHtml.value))
watch(
() => props.modelValue,
(value) => {
const nextHtml = String(value || '')
if (nextHtml === currentHtml.value) {
syncEditorHtml()
return
}
currentHtml.value = nextHtml
syncEditorHtml()
},
MENU_CONF: {
insertTable: {
rows: 3,
cols: 4,
},
insertImage: { base64LimitSize: 0 },
uploadImage: { server: '' },
uploadVideo: { server: '' },
},
}))
{ immediate: true },
)
function handleCreated(editor) {
editorRef.value = editor
watch(isFullscreen, async (value) => {
if (!value) {
activeSurface.value = 'inline'
await nextTick()
syncEditorHtml()
return
}
await nextTick()
syncEditorHtml()
if (!props.disabled) {
focusSurface('fullscreen', true)
}
})
function getEditorElement(surface = activeSurface.value) {
return surface === 'fullscreen' ? fullscreenEditorRef.value : inlineEditorRef.value
}
function handleChange(value) {
emit('update:modelValue', value || '')
function syncEditorHtml(skipSurface = '') {
syncSurfaceHtml('inline', skipSurface)
syncSurfaceHtml('fullscreen', skipSurface)
}
function handleCustomPaste(editor, event) {
const html = event?.clipboardData?.getData('text/html') || ''
if (!html || !/<table[\s>]/i.test(html)) {
function syncSurfaceHtml(surface, skipSurface = '') {
if (surface === skipSurface) {
return
}
const editor = getEditorElement(surface)
if (!editor) {
return
}
const html = currentHtml.value || ''
if (editor.innerHTML !== html) {
editor.innerHTML = html
}
}
function setActiveSurface(surface) {
activeSurface.value = surface
}
function focusSurface(surface, placeAtEnd = false) {
const editor = getEditorElement(surface)
if (!editor) {
return
}
editor.focus()
if (placeAtEnd) {
placeCaretAtEnd(editor)
captureSelection(surface)
}
}
function placeCaretAtEnd(editor) {
if (!editor) {
return
}
const selection = window.getSelection()
if (!selection) {
return
}
const range = document.createRange()
range.selectNodeContents(editor)
range.collapse(false)
selection.removeAllRanges()
selection.addRange(range)
}
function captureSelection(surface = activeSurface.value) {
const editor = getEditorElement(surface)
const selection = window.getSelection()
if (!editor || !selection || !selection.rangeCount) {
return
}
const range = selection.getRangeAt(0)
if (!editor.contains(range.commonAncestorContainer)) {
return
}
savedRanges[surface] = range.cloneRange()
}
function restoreSelection(surface = activeSurface.value) {
const editor = getEditorElement(surface)
if (!editor) {
return false
}
editor.focus()
const selection = window.getSelection()
if (!selection) {
return false
}
const range = savedRanges[surface]
selection.removeAllRanges()
if (range && editor.contains(range.commonAncestorContainer)) {
selection.addRange(range)
return true
}
const normalizedHtml = normalizePastedHtml(html)
if (!normalizedHtml) {
placeCaretAtEnd(editor)
return true
}
function handleEditorInput(surface) {
const editor = getEditorElement(surface)
if (!editor) {
return
}
activeSurface.value = surface
currentHtml.value = normalizeEditableHtml(editor.innerHTML)
syncEditorHtml(surface)
emit('update:modelValue', currentHtml.value)
captureSelection(surface)
}
function normalizeEditableHtml(html) {
const normalized = String(html || '')
.replace(/<div><br\s*\/?><\/div>/gi, '')
.replace(/<p><br\s*\/?><\/p>/gi, '')
.trim()
return hasMeaningfulHtml(normalized) ? normalized : ''
}
function hasMeaningfulHtml(html) {
if (!html) {
return false
}
if (/<(table|img|hr|video|audio)[\s>]/i.test(html)) {
return true
}
return normalizeTextContent(html) !== ''
}
function normalizeTextContent(html) {
return String(html || '')
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
.replace(/<[^>]+>/g, ' ')
.replace(/&nbsp;/gi, ' ')
.replace(/\s+/g, ' ')
.trim()
}
function handleToolbarAction(action, targetSurface = '') {
if (!action) {
return
}
if (action.type === 'insertTable') {
insertDefaultTable(targetSurface)
return
}
runDocumentCommand(action.command, action.value, targetSurface)
}
function resolveTargetSurface(targetSurface = '') {
if (targetSurface) {
return targetSurface
}
return isFullscreen.value ? 'fullscreen' : activeSurface.value || 'inline'
}
function runDocumentCommand(command, value = null, targetSurface = '') {
if (props.disabled) {
return
}
const surface = resolveTargetSurface(targetSurface)
activeSurface.value = surface
restoreSelection(surface)
document.execCommand(command, false, value)
captureSelection(surface)
handleEditorInput(surface)
}
function insertDefaultTable(targetSurface = '') {
if (props.disabled) {
return
}
const html = buildDefaultTableHtml(4, 4)
insertHtmlAtCursor(html, targetSurface)
}
function insertHtmlAtCursor(html, targetSurface = '') {
const surface = resolveTargetSurface(targetSurface)
activeSurface.value = surface
restoreSelection(surface)
const inserted = document.execCommand('insertHTML', false, html)
if (!inserted) {
const editor = getEditorElement(surface)
insertHtmlByRange(editor, html)
}
captureSelection(surface)
handleEditorInput(surface)
}
function insertHtmlByRange(editor, html) {
if (!editor) {
return
}
const selection = window.getSelection()
if (!selection || !selection.rangeCount) {
editor.insertAdjacentHTML('beforeend', html)
placeCaretAtEnd(editor)
return
}
const range = selection.getRangeAt(0)
if (!editor.contains(range.commonAncestorContainer)) {
editor.insertAdjacentHTML('beforeend', html)
placeCaretAtEnd(editor)
return
}
range.deleteContents()
const fragment = range.createContextualFragment(html)
const lastNode = fragment.lastChild
range.insertNode(fragment)
if (lastNode) {
const nextRange = document.createRange()
nextRange.setStartAfter(lastNode)
nextRange.collapse(true)
selection.removeAllRanges()
selection.addRange(nextRange)
}
}
function buildDefaultTableHtml(rows = 4, cols = 4) {
const minWidth = Math.max(640, cols * 140)
const rowHtml = Array.from({ length: rows }, () => {
const cellHtml = Array.from({ length: cols }, () => '<td><br></td>').join('')
return `<tr>${cellHtml}</tr>`
}).join('')
return `<table style="width:100%;min-width:${minWidth}px;border-collapse:collapse;table-layout:auto;margin:8px 0;">${rowHtml}</table><p><br></p>`
}
function handlePaste(event, surface) {
if (props.disabled) {
return
}
const html = event?.clipboardData?.getData('text/html') || ''
const plainText = event?.clipboardData?.getData('text/plain') || ''
const normalizedHtml = html ? normalizePastedHtml(html) : ''
const fallbackTableHtml = !normalizedHtml ? buildPlainTextTableHtml(plainText) : ''
if (!normalizedHtml && !fallbackTableHtml) {
return
}
event.preventDefault()
editor.dangerouslyInsertHtml(normalizedHtml)
return false
activeSurface.value = surface
insertHtmlAtCursor(normalizedHtml || fallbackTableHtml, surface)
}
function normalizePastedHtml(html) {
const parser = new window.DOMParser()
const doc = parser.parseFromString(html, 'text/html')
const doc = parser.parseFromString(`<html><body>${html}</body></html>`, 'text/html')
const body = doc.body
if (!body) {
return ''
@@ -161,11 +477,16 @@ function normalizePastedHtml(html) {
function normalizePastedTable(table, doc) {
const normalizedTable = doc.createElement('table')
normalizedTable.style.width = normalizeTableWidth(table) || '100%'
const columnCount = estimateTableColumnCount(table)
const tableWidth = normalizeTableWidth(table) || '100%'
normalizedTable.style.width = tableWidth
normalizedTable.style.minWidth = `${Math.max(640, columnCount * 140)}px`
normalizedTable.style.borderCollapse = 'collapse'
normalizedTable.style.tableLayout = 'fixed'
normalizedTable.style.tableLayout = 'auto'
normalizedTable.style.margin = '8px 0'
cloneNormalizedColGroups(table, normalizedTable, doc)
extractPastedTableRows(table).forEach((row) => {
const normalizedRow = doc.createElement('tr')
Array.from(row.children || []).forEach((cell) => {
@@ -183,6 +504,36 @@ function normalizePastedTable(table, doc) {
table.replaceWith(normalizedTable)
}
function cloneNormalizedColGroups(sourceTable, targetTable, doc) {
Array.from(sourceTable.querySelectorAll(':scope > colgroup')).forEach((group) => {
const normalizedGroup = doc.createElement('colgroup')
Array.from(group.children || []).forEach((child) => {
if (String(child.tagName || '').toLowerCase() !== 'col') {
return
}
const normalizedCol = doc.createElement('col')
const width = normalizeSizeValue(child.style?.width || child.getAttribute('width') || '')
if (width) {
normalizedCol.style.width = width
}
normalizedGroup.appendChild(normalizedCol)
})
if (normalizedGroup.children.length) {
targetTable.appendChild(normalizedGroup)
}
})
}
function estimateTableColumnCount(table) {
return extractPastedTableRows(table).reduce((maxCount, row) => {
const columnCount = Array.from(row.children || []).reduce((sum, cell) => {
const colspan = Number.parseInt(String(cell.getAttribute('colspan') || '').trim(), 10)
return sum + (Number.isFinite(colspan) && colspan > 1 ? colspan : 1)
}, 0)
return Math.max(maxCount, columnCount)
}, 0)
}
function extractPastedTableRows(table) {
const rows = Array.from(table.querySelectorAll(':scope > tr'))
if (rows.length) {
@@ -201,9 +552,13 @@ function extractPastedTableRows(table) {
function normalizeTableWidth(table) {
const inlineWidth = table.style?.width || table.getAttribute('width') || ''
const normalized = String(inlineWidth || '').trim()
return normalizeSizeValue(inlineWidth) || '100%'
}
function normalizeSizeValue(value) {
const normalized = String(value || '').trim()
if (!normalized) {
return '100%'
return ''
}
if (/^\d+(\.\d+)?$/.test(normalized)) {
return `${normalized}px`
@@ -223,13 +578,15 @@ function applyNormalizedCellStyle(sourceCell, targetCell) {
const textAlign = sourceCell.style?.textAlign || sourceCell.getAttribute('align') || ''
const verticalAlign = sourceCell.style?.verticalAlign || sourceCell.getAttribute('valign') || ''
const backgroundColor = sourceCell.style?.backgroundColor || sourceCell.getAttribute('bgcolor') || ''
const width = sourceCell.style?.width || sourceCell.getAttribute('width') || ''
const width = normalizeSizeValue(sourceCell.style?.width || sourceCell.getAttribute('width') || '')
targetCell.style.border = '1px solid #1f2937'
targetCell.style.padding = '6px 8px'
targetCell.style.padding = '6px 10px'
targetCell.style.verticalAlign = normalizeVerticalAlign(verticalAlign) || 'middle'
targetCell.style.wordBreak = 'break-word'
targetCell.style.wordBreak = 'keep-all'
targetCell.style.overflowWrap = 'break-word'
targetCell.style.whiteSpace = 'pre-wrap'
targetCell.style.minWidth = '120px'
if (textAlign) {
targetCell.style.textAlign = textAlign
@@ -238,7 +595,7 @@ function applyNormalizedCellStyle(sourceCell, targetCell) {
targetCell.style.backgroundColor = backgroundColor
}
if (width) {
targetCell.style.width = /^\d+(\.\d+)?$/.test(width) ? `${width}px` : width
targetCell.style.width = width
}
if (String(sourceCell.tagName || '').toLowerCase() === 'th') {
@@ -274,22 +631,15 @@ function cleanupPastedNode(node) {
node.removeAttribute('lang')
node.removeAttribute('dir')
node.removeAttribute('data-mce-style')
node.removeAttribute('data-mce-selected')
const tag = String(node.tagName || '').toLowerCase()
if (tag === 'table' || tag === 'td' || tag === 'th' || tag === 'tr') {
if (tag === 'table' || tag === 'td' || tag === 'th' || tag === 'tr' || tag === 'col' || tag === 'colgroup') {
retainInlineStyle(node, TABLE_STYLE_PROPS)
return
}
retainInlineStyle(node, [
'text-align',
'background-color',
'color',
'font-weight',
'font-style',
'text-decoration',
'white-space',
])
retainInlineStyle(node, INLINE_STYLE_PROPS)
}
function retainInlineStyle(node, allowedProperties = []) {
@@ -307,6 +657,44 @@ function retainInlineStyle(node, allowedProperties = []) {
}
}
function buildPlainTextTableHtml(text) {
const normalizedText = String(text || '').replace(/\r/g, '')
if (!normalizedText.includes('\t')) {
return ''
}
const rows = normalizedText
.split('\n')
.map((line) => line.split('\t'))
.filter((cells) => cells.some((cell) => String(cell || '').trim() !== ''))
const columnCount = rows.reduce((maxCount, row) => Math.max(maxCount, row.length), 0)
if (rows.length < 2 || columnCount < 2) {
return ''
}
const minWidth = Math.max(640, columnCount * 140)
const rowHtml = rows
.map((cells) => {
const cellHtml = Array.from({ length: columnCount }, (_, index) => {
const value = escapeHtml(String(cells[index] || '').trim())
return `<td>${value || '<br>'}</td>`
}).join('')
return `<tr>${cellHtml}</tr>`
})
.join('')
return `<table style="width:100%;min-width:${minWidth}px;border-collapse:collapse;table-layout:auto;margin:8px 0;">${rowHtml}</table><p><br></p>`
}
function openFullscreen() {
isFullscreen.value = true
}
function handleFullscreenVisibleChange(value) {
isFullscreen.value = !!value
}
function normalizeVerticalAlign(value) {
const normalized = String(value || '').trim().toLowerCase()
if (['top', 'middle', 'center', 'bottom'].includes(normalized)) {
@@ -315,12 +703,14 @@ function normalizeVerticalAlign(value) {
return ''
}
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor) {
editor.destroy()
}
})
function escapeHtml(text) {
return String(text || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
</script>
<style scoped>
@@ -332,60 +722,135 @@ onBeforeUnmount(() => {
}
.template-rich-editor.is-disabled {
opacity: 0.9;
opacity: 0.92;
}
.editor-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
border-bottom: 1px solid #edf2f7;
background: linear-gradient(180deg, #fcfdff 0%, #f8fbff 100%);
}
.editor-content {
min-height: 260px;
.toolbar-groups {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.editor-content :deep(.w-e-text-container) {
min-height: 240px !important;
.toolbar-group {
display: flex;
align-items: center;
gap: 8px;
padding-right: 10px;
border-right: 1px solid #e5edf7;
}
.editor-content :deep(.w-e-scroll) {
padding: 8px 0;
.toolbar-group:last-child {
padding-right: 0;
border-right: none;
}
.editor-content :deep(.w-e-text-placeholder) {
top: 14px;
}
.editor-content :deep(.w-e-text-container [data-slate-editor]) {
padding: 0 14px 12px;
}
.editor-content :deep(table) {
width: 100%;
max-width: 100%;
margin: 8px 0;
border-collapse: collapse;
table-layout: fixed;
.toolbar-button {
appearance: none;
border: 1px solid #d5deea;
background: #fff;
color: #334155;
font-size: 12px;
line-height: 1;
padding: 8px 10px;
border-radius: 8px;
cursor: pointer;
transition: all 0.18s ease;
}
.editor-content :deep(table td),
.editor-content :deep(table th) {
min-width: 84px;
border: 1px solid #1f2937 !important;
padding: 6px 8px;
vertical-align: middle;
word-break: break-word;
white-space: pre-wrap;
.toolbar-button:hover:not(:disabled) {
border-color: #93c5fd;
color: #1d4ed8;
background: #eff6ff;
}
.editor-content :deep(table th) {
font-weight: 700;
text-align: center;
.toolbar-button:disabled {
cursor: not-allowed;
opacity: 0.48;
}
.toolbar-button-primary {
border-color: #3b82f6;
color: #1d4ed8;
background: #eff6ff;
white-space: nowrap;
}
.editor-content-shell {
background: #f8fafc;
}
.editor-content :deep(table p) {
margin: 0;
.editor-scroll {
overflow: auto;
padding: 12px;
}
.editor-surface {
min-height: 260px;
min-width: 100%;
width: max-content;
outline: none;
padding: 14px 16px 18px;
border-radius: 10px;
background: #fff;
line-height: 1.7;
color: #0f172a;
box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.18);
}
.editor-surface.has-table {
min-width: 760px;
}
.editor-surface[contenteditable='false'] {
background: #fcfdff;
}
.editor-surface:empty::before {
content: attr(data-placeholder);
color: #94a3b8;
}
.editor-surface p,
.editor-surface div,
.editor-surface li {
margin: 0 0 6px;
}
.editor-surface table {
width: 100%;
max-width: none;
margin: 8px 0;
border-collapse: collapse;
table-layout: auto;
background: #fff;
}
.editor-surface td,
.editor-surface th {
min-width: 120px;
border: 1px solid #1f2937 !important;
padding: 6px 10px;
vertical-align: middle;
word-break: keep-all;
overflow-wrap: break-word;
white-space: pre-wrap;
}
.editor-surface th {
font-weight: 700;
text-align: center;
background: #f8fafc;
}
.editor-tip {
@@ -396,4 +861,52 @@ onBeforeUnmount(() => {
border-top: 1px solid #edf2f7;
line-height: 1.6;
}
.fullscreen-layout {
height: 100%;
display: flex;
flex-direction: column;
min-height: 0;
background: #f4f7fb;
}
.fullscreen-toolbar {
border-bottom-color: rgba(148, 163, 184, 0.16);
}
.fullscreen-body {
flex: 1;
min-height: 0;
}
.fullscreen-scroll {
height: 100%;
padding: 16px;
}
.fullscreen-surface {
min-height: 100%;
min-width: 960px;
}
@media (max-width: 900px) {
.editor-toolbar,
.fullscreen-toolbar {
flex-direction: column;
align-items: stretch;
}
.toolbar-groups {
width: 100%;
}
.toolbar-button-primary {
width: 100%;
}
.editor-surface.has-table,
.fullscreen-surface {
min-width: 720px;
}
}
</style>
@@ -602,7 +602,7 @@ function buildWordTableCellXml(
]
if (cellEntry?.kind === 'continuation') {
tcProps.push('<w:vMerge/>')
tcProps.push('<w:vMerge w:val="continue"/>')
return `<w:tc><w:tcPr>${tcProps.filter(Boolean).join('')}</w:tcPr>${buildParagraphXml('', { align: defaultCellAlign })}</w:tc>`
}