@@ -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' ,
'|' ,
const INLINE _STYLE _PROPS = [
'text-align' ,
'background-color ' ,
'color' ,
'bgColor ' ,
'| ' ,
'bulletedList ' ,
'numberedList ' ,
'|' ,
'justifyLeft' ,
'justifyCenter' ,
'justifyRight' ,
'|' ,
'insertTable' ,
'deleteTable' ,
'insertTableRow' ,
'deleteTableRow' ,
'insertTableCol' ,
'deleteTableCol' ,
'tableHeader' ,
'|' ,
'undo' ,
'redo' ,
'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' ] ,
}
const editorConfig = computed ( ( ) => ( {
placeholder : props . placeholder ,
readOnly : props . disabled ,
autoFocus : false ,
scroll : true ,
customPaste : handleCustomPaste ,
hoverbarKeys : {
table : {
menuKeys : [
'insertTableRow' ,
'deleteTableRow' ,
'insertTableCol' ,
'deleteTableCol' ,
'tableHeader' ,
'deleteTable' ,
[
{ key : 'justifyLeft' , label : '左对齐' , command : 'justifyLeft' } ,
{ key : 'justifyCenter' , label : '居中' , command : 'justifyCenter' } ,
{ key : 'justifyRight' , label : '右对齐' , command : 'justifyRight' } ,
] ,
} ,
} ,
MENU _CONF : {
insertTable : {
rows : 3 ,
cols : 4 ,
} ,
insertImage : { base64LimitSize : 0 } ,
uploadImage : { server : '' } ,
uploadVideo : { server : '' } ,
} ,
} ) )
[
{ 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' } ,
] ,
]
function handleCreated ( editor ) {
editorRef . value = editor
const savedRanges = {
inline : null ,
fullscreen : null ,
}
function handleChange ( value ) {
emit ( 'update:modelValue' , value || '' )
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 ( )
} ,
{ immediate : true } ,
)
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 syncEditorHtml ( skipSurface = '' ) {
syncSurfaceHtml ( 'inline' , skipSurface )
syncSurfaceHtml ( 'fullscreen' , skipSurface )
}
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
}
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 ( / /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
}
function handleCustomPaste ( editor , event ) {
const html = event ? . clipboardData ? . getData ( 'text/html' ) || ''
if ( ! html || ! /<table[\s>]/i . test ( html ) ) {
return true
}
const plainText = event ? . clipboardData ? . getData ( 'text/plain' ) || ''
const normalizedHtml = html ? normalizePastedHtml ( html ) : ''
const fallbackTableHtml = ! normalizedHtml ? buildPlainTextTableHtml ( plainText ) : ''
const normalizedHtml = normalizePastedHtml ( html )
if ( ! normalizedHtml ) {
return true
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 8 px'
targetCell . style . padding = '6px 10 px'
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 , '&' )
. replace ( /</g , '<' )
. replace ( />/g , '>' )
. replace ( /"/g , '"' )
. replace ( /'/g , ''' )
}
} )
< / 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 : 12 px ;
padding : 10 px 12 px ;
border - bottom : 1 px solid # edf2f7 ;
background : linear - gradient ( 180 deg , # fcfdff 0 % , # f8fbff 100 % ) ;
}
. editor - content {
min - height : 260 px ;
. toolbar - groups {
display : flex ;
align - items : center ;
gap : 10 px ;
flex - wrap : wrap ;
}
. editor - content : deep ( . w - e - text - container ) {
min - height : 240 px ! important ;
. toolbar - group {
display : flex ;
align - items : center ;
gap : 8 px ;
padding - right : 10 px ;
border - right : 1 px solid # e5edf7 ;
}
. editor - content : deep ( . w - e - scroll ) {
padding : 8 px 0 ;
. toolbar - group : last - child {
padding - right : 0 ;
border - right : none ;
}
. editor - content : deep ( . w - e - text - placeholder ) {
top : 14 px ;
}
. editor - content : deep ( . w - e - text - container [ data - slate - editor ] ) {
padding : 0 14 px 12 px ;
}
. editor - content : deep ( table ) {
width : 100 % ;
max - width : 100 % ;
margin : 8 px 0 ;
border - collapse : collapse ;
table - layout : fixed ;
. toolbar - button {
appearance : none ;
border : 1 px solid # d5deea ;
background : # fff ;
color : # 334155 ;
font - size : 12 px ;
line - height : 1 ;
padding : 8 px 10 px ;
border - radius : 8 px ;
cursor : pointer ;
transition : all 0.18 s ease ;
}
. editor - content : deep ( table td ) ,
. editor - content : deep ( table th ) {
min - width : 84 px ;
border : 1 px solid # 1 f2937 ! important ;
padding : 6 px 8 px ;
vertical - align : middle ;
word - break : break - word ;
white - space : pre - wrap ;
. toolbar - button : hover : not ( : disabled ) {
border - color : # 93 c5fd ;
color : # 1 d4ed8 ;
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 : # 3 b82f6 ;
color : # 1 d4ed8 ;
background : # eff6ff ;
white - space : nowrap ;
}
. editor - content - shell {
background : # f8fafc ;
}
. editor - content : deep ( table p ) {
margin : 0 ;
. editor - scroll {
overflow : auto ;
padding : 12 px ;
}
. editor - surface {
min - height : 260 px ;
min - width : 100 % ;
width : max - content ;
outline : none ;
padding : 14 px 16 px 18 px ;
border - radius : 10 px ;
background : # fff ;
line - height : 1.7 ;
color : # 0 f172a ;
box - shadow : inset 0 0 0 1 px rgba ( 148 , 163 , 184 , 0.18 ) ;
}
. editor - surface . has - table {
min - width : 760 px ;
}
. editor - surface [ contenteditable = 'false' ] {
background : # fcfdff ;
}
. editor - surface : empty : : before {
content : attr ( data - placeholder ) ;
color : # 94 a3b8 ;
}
. editor - surface p ,
. editor - surface div ,
. editor - surface li {
margin : 0 0 6 px ;
}
. editor - surface table {
width : 100 % ;
max - width : none ;
margin : 8 px 0 ;
border - collapse : collapse ;
table - layout : auto ;
background : # fff ;
}
. editor - surface td ,
. editor - surface th {
min - width : 120 px ;
border : 1 px solid # 1 f2937 ! important ;
padding : 6 px 10 px ;
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 : 1 px 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 : 16 px ;
}
. fullscreen - surface {
min - height : 100 % ;
min - width : 960 px ;
}
@ media ( max - width : 900 px ) {
. 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 : 720 px ;
}
}
< / style >