vome-core 0.3.42 → 0.3.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/admin/components/ui/avatar/index.js +1 -1
  2. package/dist/admin/components/ui/checkbox/index.js +1 -1
  3. package/dist/admin/components/ui/dialog/index.js +1 -1
  4. package/dist/admin/components/ui/dropdown-menu/index.js +1 -1
  5. package/dist/admin/components/ui/input/index.js +1 -1
  6. package/dist/admin/components/ui/label/index.js +1 -1
  7. package/dist/admin/components/ui/scroll-area/index.js +1 -1
  8. package/dist/admin/components/ui/select/index.js +1 -1
  9. package/dist/admin/components/ui/separator/index.js +1 -1
  10. package/dist/admin/components/ui/sheet/index.js +1 -1
  11. package/dist/admin/components/ui/sonner/index.js +1 -1
  12. package/dist/admin/components/ui/table/index.js +1 -1
  13. package/dist/admin/components/ui/tabs/index.js +1 -1
  14. package/dist/admin/components/ui/tooltip/index.js +1 -1
  15. package/dist/admin/crud/auto-fields.js +1 -1
  16. package/dist/admin/crud/components/vm-richtext.vue +128 -924
  17. package/dist/admin/crud/config.js +1 -1
  18. package/dist/admin/crud/confirm.js +1 -1
  19. package/dist/admin/crud/crud-page-request.js +1 -1
  20. package/dist/admin/crud/data-i18n.js +1 -1
  21. package/dist/admin/crud/dict.js +1 -1
  22. package/dist/admin/crud/export-fetch.js +1 -1
  23. package/dist/admin/crud/key.js +1 -1
  24. package/dist/admin/crud/plugins.js +1 -1
  25. package/dist/admin/crud/span.js +1 -1
  26. package/dist/admin/crud/style.js +1 -1
  27. package/dist/admin/crud/validate.js +1 -1
  28. package/dist/admin/directives/perm.js +1 -1
  29. package/dist/admin/hooks/useUpload.js +1 -1
  30. package/dist/admin/lib/browser.js +1 -1
  31. package/dist/admin/lib/cn.js +1 -1
  32. package/dist/admin/lib/dialog-float.js +1 -1
  33. package/dist/admin/lib/export-excel.js +1 -1
  34. package/dist/admin/lib/import-excel.js +1 -1
  35. package/dist/admin/lib/json.js +1 -1
  36. package/dist/admin/lib/menu.js +1 -1
  37. package/dist/admin/lib/micro-locale.js +1 -1
  38. package/dist/admin/lib/micro-theme.js +1 -1
  39. package/dist/admin/lib/shell.js +1 -1
  40. package/dist/admin/lib/theme-color-options.js +1 -1
  41. package/dist/admin/lib/tree.js +1 -1
  42. package/dist/admin/lib/upload.js +1 -1
  43. package/dist/admin/lib/video-frame.js +1 -1
  44. package/dist/admin/router/create.js +1 -1
  45. package/dist/admin/router/guards.js +1 -1
  46. package/dist/ai/index.js +1 -1
  47. package/dist/client/ai-model-registry.js +1 -1
  48. package/dist/client/base.js +1 -1
  49. package/dist/client/configure.js +1 -1
  50. package/dist/client/dict.js +1 -1
  51. package/dist/client/eps-payload.js +1 -1
  52. package/dist/client/eps.js +1 -1
  53. package/dist/client/index.js +1 -1
  54. package/dist/client/locale-change.js +1 -1
  55. package/dist/client/locale-store.js +1 -1
  56. package/dist/client/service.js +1 -1
  57. package/dist/client/vite-admin.js +1 -1
  58. package/dist/client/vite-auto-import.js +1 -1
  59. package/dist/client/vite-micro-proxy.js +1 -1
  60. package/dist/client/vite-plugin-eps.js +1 -1
  61. package/dist/client/vite-resolve.js +1 -1
  62. package/dist/config/index.js +1 -1
  63. package/dist/index.js +1 -1
  64. package/dist/server/index.js +1 -1
  65. package/dist/shared/datetime-picker.js +1 -1
  66. package/dist/shared/decimal.js +1 -1
  67. package/dist/shared/excel.js +1 -1
  68. package/dist/shared/index.js +1 -1
  69. package/dist/shared/locale-json.js +1 -1
  70. package/dist/shared/tree.js +1 -1
  71. package/package.json +3 -19
  72. package/dist/admin/crud/components/vm-richtext-extensions.d.ts +0 -28
  73. package/dist/admin/crud/components/vm-richtext-extensions.js +0 -1
@@ -17,153 +17,31 @@
17
17
  v-else
18
18
  class="vm-richtext__shell"
19
19
  >
20
- <div
21
- v-if="editor"
20
+ <Toolbar
22
21
  class="vm-richtext__toolbar"
23
- >
24
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('bold') }" :title="crudLabel('rtBold')" @click="editor.chain().focus().toggleBold().run()"><i class="ri-bold" /></button>
25
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('italic') }" :title="crudLabel('rtItalic')" @click="editor.chain().focus().toggleItalic().run()"><i class="ri-italic" /></button>
26
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('underline') }" :title="crudLabel('rtUnderline')" @click="editor.chain().focus().toggleUnderline().run()"><i class="ri-underline" /></button>
27
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('strike') }" :title="crudLabel('rtStrike')" @click="editor.chain().focus().toggleStrike().run()"><i class="ri-strikethrough" /></button>
28
- <span
29
- class="vm-richtext__btn vm-richtext__color-btn"
30
- :class="{ 'is-active': editor.isActive('highlight'), 'is-disabled': disabled }"
31
- :title="crudLabel('rtBgColor')"
32
- role="button"
33
- @pointerdown="rememberSelection"
34
- >
35
- <i class="ri-mark-pen-line" aria-hidden="true" />
36
- <span class="vm-richtext__color-bar" :style="{ background: highlightColor }" />
37
- <vm-color-picker
38
- class="vm-richtext__color-hit"
39
- size="sm"
40
- swatch-only
41
- :enable-theme="false"
42
- :disabled="disabled"
43
- :model-value="highlightColor"
44
- @update:model-value="applyHighlightColor"
45
- />
46
- </span>
47
- <span
48
- class="vm-richtext__btn vm-richtext__color-btn"
49
- :class="{ 'is-active': Boolean(textColor && textColor !== '#111827'), 'is-disabled': disabled }"
50
- :title="crudLabel('rtTextColor')"
51
- role="button"
52
- @pointerdown="rememberSelection"
53
- >
54
- <i class="ri-font-color" aria-hidden="true" />
55
- <span class="vm-richtext__color-bar" :style="{ background: textColor }" />
56
- <vm-color-picker
57
- class="vm-richtext__color-hit"
58
- size="sm"
59
- swatch-only
60
- :enable-theme="false"
61
- :disabled="disabled"
62
- :model-value="textColor"
63
- @update:model-value="applyTextColor"
64
- />
65
- </span>
66
- <span class="vm-richtext__sep" />
67
- <select
68
- class="vm-richtext__select"
69
- :title="crudLabel('rtFontSize')"
70
- :value="fontSize"
71
- :disabled="disabled"
72
- @change="onFontSizeChange"
73
- >
74
- <option value="">{{ crudLabel('rtFontSizeDefault') }}</option>
75
- <option v-for="s in FONT_SIZES" :key="s" :value="`${s}px`">{{ s }}</option>
76
- </select>
77
- <span class="vm-richtext__sep" />
78
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive({ textAlign: 'left' }) }" :title="crudLabel('rtAlignLeft')" @click="editor.chain().focus().setTextAlign('left').run()"><i class="ri-align-left" /></button>
79
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive({ textAlign: 'center' }) }" :title="crudLabel('rtAlignCenter')" @click="editor.chain().focus().setTextAlign('center').run()"><i class="ri-align-center" /></button>
80
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive({ textAlign: 'right' }) }" :title="crudLabel('rtAlignRight')" @click="editor.chain().focus().setTextAlign('right').run()"><i class="ri-align-right" /></button>
81
- <span class="vm-richtext__sep" />
82
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('bulletList') }" :title="crudLabel('rtBulletList')" @click="editor.chain().focus().toggleBulletList().run()"><i class="ri-list-unordered" /></button>
83
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('orderedList') }" :title="crudLabel('rtOrderedList')" @click="editor.chain().focus().toggleOrderedList().run()"><i class="ri-list-ordered" /></button>
84
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('taskList') }" :title="crudLabel('rtTaskList')" @click="editor.chain().focus().toggleTaskList().run()"><i class="ri-checkbox-line" /></button>
85
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('blockquote') }" :title="crudLabel('rtBlockquote')" @click="editor.chain().focus().toggleBlockquote().run()"><i class="ri-double-quotes-l" /></button>
86
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('codeBlock') }" :title="crudLabel('rtCodeBlock')" @click="editor.chain().focus().toggleCodeBlock().run()"><i class="ri-code-box-line" /></button>
87
- <button type="button" class="vm-richtext__btn" :title="crudLabel('rtHr')" @click="editor.chain().focus().setHorizontalRule().run()"><i class="ri-separator" /></button>
88
- <span class="vm-richtext__sep" />
89
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('link') }" :title="crudLabel('rtLink')" @click="toggleLink"><i class="ri-link" /></button>
90
- <button v-if="uploadEnabled" type="button" class="vm-richtext__btn" :title="crudLabel('rtImage')" @click="pickImage"><i class="ri-image-line" /></button>
91
- <button v-if="uploadEnabled" type="button" class="vm-richtext__btn" :title="crudLabel('rtVideo')" @click="pickVideo"><i class="ri-video-line" /></button>
92
- <button
93
- ref="emojiBtnRef"
94
- type="button"
95
- class="vm-richtext__btn"
96
- :class="{ 'is-active': emojiOpen }"
97
- :title="crudLabel('rtEmoji')"
98
- @click="toggleEmoji"
99
- >
100
- <i class="ri-emotion-line" />
101
- </button>
102
- <Teleport to="body">
103
- <div
104
- v-if="emojiOpen"
105
- ref="emojiPanelRef"
106
- class="vm-richtext__emoji-panel vm-dialog-float"
107
- :style="emojiPanelStyle"
108
- @pointerdown.stop
109
- @mousedown.stop
110
- >
111
- <button
112
- v-for="(e, i) in VM_RICHTEXT_EMOJIS"
113
- :key="i"
114
- type="button"
115
- class="vm-richtext__emoji-item"
116
- @click.stop="insertEmoji(e)"
117
- >
118
- {{ e }}
119
- </button>
120
- </div>
121
- </Teleport>
122
- <span class="vm-richtext__sep" />
123
- <button type="button" class="vm-richtext__btn" :title="crudLabel('rtTable')" @click="insertTable"><i class="ri-table-line" /></button>
124
- <button type="button" class="vm-richtext__btn" :title="crudLabel('rtUndo')" :disabled="!editor.can().undo()" @click="editor.chain().focus().undo().run()"><i class="ri-arrow-go-back-line" /></button>
125
- <button type="button" class="vm-richtext__btn" :title="crudLabel('rtRedo')" :disabled="!editor.can().redo()" @click="editor.chain().focus().redo().run()"><i class="ri-arrow-go-forward-line" /></button>
126
- </div>
127
-
128
- <EditorContent class="vm-richtext__content" :editor="editor" />
129
- <vm-context-menu
130
- ref="tableCtxRef"
131
- :items="tableCtxItems"
132
- :z-index="10050"
133
- @select="onTableCtxSelect"
22
+ :editor="editorRef"
23
+ :default-config="toolbarConfig"
24
+ mode="default"
25
+ />
26
+ <Editor
27
+ class="vm-richtext__editor"
28
+ v-model="html"
29
+ :default-config="editorConfig"
30
+ mode="default"
31
+ @onCreated="handleCreated"
134
32
  />
135
33
  </div>
136
34
  </div>
137
35
  </template>
138
36
 
139
37
  <script setup lang="ts">
38
+ import '@wangeditor/editor/dist/css/style.css'
140
39
  import { crudLabel } from '../style'
141
- import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
40
+ import { computed, onBeforeUnmount, shallowRef, watch } from 'vue'
142
41
  import DOMPurify from 'dompurify'
143
- import { EditorContent, useEditor } from '@tiptap/vue-3'
144
- import StarterKit from '@tiptap/starter-kit'
145
- import Image from '@tiptap/extension-image'
146
- import Link from '@tiptap/extension-link'
147
- import Placeholder from '@tiptap/extension-placeholder'
148
- import Underline from '@tiptap/extension-underline'
149
- import TextAlign from '@tiptap/extension-text-align'
150
- import Highlight from '@tiptap/extension-highlight'
151
- import TextStyle from '@tiptap/extension-text-style'
152
- import Color from '@tiptap/extension-color'
153
- import TaskList from '@tiptap/extension-task-list'
154
- import TaskItem from '@tiptap/extension-task-item'
155
- import Table from '@tiptap/extension-table'
156
- import TableRow from '@tiptap/extension-table-row'
157
- import TableHeader from '@tiptap/extension-table-header'
158
- import TableCell from '@tiptap/extension-table-cell'
42
+ import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
43
+ import type { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'
159
44
  import { useUpload } from '../../hooks/useUpload'
160
- import { FONT_SIZES, FontSize, VM_RICHTEXT_EMOJIS, VmRichtextVideo } from './vm-richtext-extensions'
161
- import VmColorPicker from './vm-color-picker.vue'
162
- import VmContextMenu from './vm-context-menu.vue'
163
- import { CellSelection, cellAround } from '@tiptap/pm/tables'
164
- import type { Editor } from '@tiptap/core'
165
- import type { EditorView } from '@tiptap/pm/view'
166
- import type { CtxMenuItem } from '../../../../typings/admin/comm/crud'
167
45
 
168
46
  defineOptions({ name: 'vm-richtext' })
169
47
 
@@ -188,7 +66,7 @@ const props = withDefaults(
188
66
  {
189
67
  readonly: false,
190
68
  placeholder: undefined,
191
- height: 320,
69
+ height: 400,
192
70
  uploadPrefixPath: 'app/public/richtext',
193
71
  autoUpload: true,
194
72
  },
@@ -197,225 +75,24 @@ const props = withDefaults(
197
75
  const emit = defineEmits<{ 'update:modelValue': [v: string] }>()
198
76
 
199
77
  const { toUpload } = useUpload()
78
+ const editorRef = shallowRef<IDomEditor>()
200
79
  const syncing = shallowRef(false)
201
- const uploading = shallowRef(false)
202
- const emojiOpen = shallowRef(false)
203
- const emojiBtnRef = shallowRef<HTMLElement>()
204
- const emojiPanelRef = shallowRef<HTMLElement>()
205
- const emojiPanelStyle = shallowRef<Record<string, string>>({})
206
- const textColor = ref('#111827')
207
- const highlightColor = ref('#fef08a')
208
- /** 当前选区字号;空串 = 默认(编辑区基线 14px) */
209
- const fontSize = ref('')
210
- /** 点开取色器前保存选区,避免焦点离开编辑器后丢标记范围 */
211
- let savedSelection: { from: number; to: number } | null = null
212
-
213
- const tableCtxRef = ref<{ open: (ev: MouseEvent) => void; close: () => void } | null>(null)
214
- const tableCtxItems = ref<CtxMenuItem[]>([])
215
- /** 右键按下时缓存多选,防止 mousedown 先把 CellSelection 清掉 */
216
- let pendingCellSel: { anchor: number; head: number } | null = null
217
-
218
- type TableCtxKey =
219
- | 'addColumnBefore'
220
- | 'addColumnAfter'
221
- | 'addRowBefore'
222
- | 'addRowAfter'
223
- | 'deleteColumn'
224
- | 'deleteRow'
225
- | 'mergeCells'
226
- | 'splitCell'
227
-
228
- function cellPosInSelection(sel: CellSelection, cellPos: number) {
229
- let hit = false
230
- sel.forEachCell((_node, pos) => {
231
- if (pos === cellPos) hit = true
232
- })
233
- return hit
234
- }
235
-
236
- function restorePendingCellSelection(view: EditorView) {
237
- if (!pendingCellSel) return false
238
- try {
239
- const sel = CellSelection.create(
240
- view.state.doc,
241
- pendingCellSel.anchor,
242
- pendingCellSel.head,
243
- )
244
- view.dispatch(view.state.tr.setSelection(sel))
245
- return true
246
- } catch {
247
- return false
248
- }
249
- }
250
-
251
- /**
252
- * 右键 mousedown:表格插件会先清掉多选。
253
- * 若点在已有 CellSelection 内 → preventDefault 并记下选区,供 contextmenu 恢复。
254
- */
255
- function onTableMouseDown(view: EditorView, event: MouseEvent) {
256
- if (event.button !== 2) {
257
- pendingCellSel = null
258
- return false
259
- }
260
- const target = event.target as HTMLElement | null
261
- if (!target?.closest?.('td, th')) {
262
- pendingCellSel = null
263
- return false
264
- }
265
-
266
- const { selection } = view.state
267
- if (!(selection instanceof CellSelection)) {
268
- pendingCellSel = null
269
- return false
270
- }
271
-
272
- pendingCellSel = {
273
- anchor: selection.$anchorCell.pos,
274
- head: selection.$headCell.pos,
275
- }
276
-
277
- const coords = view.posAtCoords({ left: event.clientX, top: event.clientY })
278
- if (!coords) return false
279
- const $cell = cellAround(view.state.doc.resolve(coords.pos))
280
- if (!$cell) return false
281
-
282
- if (cellPosInSelection(selection, $cell.pos)) {
283
- event.preventDefault()
284
- return true
285
- }
286
- return false
287
- }
288
-
289
- /** 右键落点:已在多选内则保留;否则选中该单元格作为参照 */
290
- function selectTableCellFromEvent(ed: Editor, event: MouseEvent) {
291
- const view = ed.view
292
- const coords = view.posAtCoords({ left: event.clientX, top: event.clientY })
293
- if (!coords) return false
294
- const $cell = cellAround(view.state.doc.resolve(coords.pos))
295
- if (!$cell) return false
296
-
297
- const { selection } = view.state
298
- if (selection instanceof CellSelection && cellPosInSelection(selection, $cell.pos)) {
299
- return true
300
- }
301
-
302
- // mousedown 已清多选时,用缓存恢复(且右键仍落在原多选内)
303
- if (pendingCellSel) {
304
- const probe = CellSelection.create(
305
- view.state.doc,
306
- pendingCellSel.anchor,
307
- pendingCellSel.head,
308
- )
309
- if (cellPosInSelection(probe, $cell.pos)) {
310
- return restorePendingCellSelection(view)
311
- }
312
- }
313
-
314
- const tr = view.state.tr.setSelection(CellSelection.create(view.state.doc, $cell.pos))
315
- view.dispatch(tr)
316
- return true
317
- }
318
-
319
- function buildTableCtxItems(ed: Editor): CtxMenuItem[] {
320
- const can = ed.can()
321
- return [
322
- {
323
- key: 'addColumnBefore',
324
- label: crudLabel('rtTableColBefore'),
325
- disabled: !can.addColumnBefore(),
326
- },
327
- {
328
- key: 'addColumnAfter',
329
- label: crudLabel('rtTableColAfter'),
330
- disabled: !can.addColumnAfter(),
331
- },
332
- {
333
- key: 'addRowBefore',
334
- label: crudLabel('rtTableRowBefore'),
335
- disabled: !can.addRowBefore(),
336
- },
337
- {
338
- key: 'addRowAfter',
339
- label: crudLabel('rtTableRowAfter'),
340
- disabled: !can.addRowAfter(),
341
- },
342
- {
343
- key: 'deleteColumn',
344
- label: crudLabel('rtTableDeleteCol'),
345
- disabled: !can.deleteColumn(),
346
- },
347
- {
348
- key: 'deleteRow',
349
- label: crudLabel('rtTableDeleteRow'),
350
- disabled: !can.deleteRow(),
351
- },
352
- {
353
- key: 'mergeCells',
354
- label: crudLabel('rtTableMerge'),
355
- disabled: !can.mergeCells(),
356
- },
357
- {
358
- key: 'splitCell',
359
- label: crudLabel('rtTableSplit'),
360
- disabled: !can.splitCell(),
361
- },
362
- ]
363
- }
364
-
365
- function onTableContextMenu(view: EditorView, event: MouseEvent) {
366
- const ed = editor.value
367
- if (!ed || ed.isDestroyed || props.disabled) return false
368
- const target = event.target as HTMLElement | null
369
- if (!target?.closest?.('td, th')) return false
370
- if (!selectTableCellFromEvent(ed, event)) return false
371
- event.preventDefault()
372
- tableCtxItems.value = buildTableCtxItems(ed)
373
- tableCtxRef.value?.open(event)
374
- pendingCellSel = null
375
- return true
376
- }
377
-
378
- function onTableCtxSelect(key: string) {
379
- const ed = editor.value
380
- if (!ed) return
381
- const k = key as TableCtxKey
382
- const chain = ed.chain().focus()
383
- switch (k) {
384
- case 'addColumnBefore':
385
- chain.addColumnBefore().run()
386
- break
387
- case 'addColumnAfter':
388
- chain.addColumnAfter().run()
389
- break
390
- case 'addRowBefore':
391
- chain.addRowBefore().run()
392
- break
393
- case 'addRowAfter':
394
- chain.addRowAfter().run()
395
- break
396
- case 'deleteColumn':
397
- chain.deleteColumn().run()
398
- break
399
- case 'deleteRow':
400
- chain.deleteRow().run()
401
- break
402
- case 'mergeCells':
403
- chain.mergeCells().run()
404
- break
405
- case 'splitCell':
406
- chain.splitCell().run()
407
- break
408
- default:
409
- break
410
- }
411
- }
412
80
 
413
81
  const readOnlyMode = computed(() => props.readonly)
414
82
  const uploadEnabled = computed(() => Boolean(props.uploadImage) || props.autoUpload !== false)
415
83
 
84
+ /** 编辑区高度至少 400px,保证工具栏浮层(表情等)有空间;弹窗不够时由 upsert body 滚动 */
85
+ function resolveEditorHeight(): string {
86
+ const h = props.height
87
+ if (typeof h === 'number') return `${Math.max(h, 400)}px`
88
+ const s = String(h ?? '400').trim()
89
+ if (/^\d+(\.\d+)?$/.test(s)) return `${Math.max(Number(s), 400)}px`
90
+ if (/^\d+(\.\d+)?px$/i.test(s)) return `${Math.max(parseFloat(s), 400)}px`
91
+ return s || '400px'
92
+ }
93
+
416
94
  const shellStyle = computed(() => {
417
- const h = typeof props.height === 'number' ? `${props.height}px` : String(props.height || '320px')
418
- return { '--vm-richtext-height': h } as Record<string, string>
95
+ return { '--vm-richtext-height': resolveEditorHeight() } as Record<string, string>
419
96
  })
420
97
 
421
98
  function sanitize(html: string) {
@@ -436,6 +113,8 @@ function normalizeHtml(html: string) {
436
113
 
437
114
  const safeHtml = computed(() => sanitize(String(props.modelValue || '')))
438
115
 
116
+ const html = shallowRef(normalizeHtml(String(props.modelValue || '')) || '<p><br></p>')
117
+
439
118
  async function resolveUpload(file: File): Promise<string> {
440
119
  if (props.uploadImage) return props.uploadImage(file)
441
120
  if (props.autoUpload === false) throw new Error(crudLabel('noUploadImage'))
@@ -443,321 +122,67 @@ async function resolveUpload(file: File): Promise<string> {
443
122
  return res.url
444
123
  }
445
124
 
446
- async function insertImage(file: File) {
447
- if (!editor.value || uploading.value) return
448
- uploading.value = true
449
- try {
450
- const url = await resolveUpload(file)
451
- editor.value.chain().focus().setImage({ src: url, alt: file.name || 'image' }).run()
452
- } finally {
453
- uploading.value = false
454
- }
455
- }
456
-
457
- async function insertVideo(file: File) {
458
- if (!editor.value || uploading.value) return
459
- uploading.value = true
460
- try {
461
- const url = await resolveUpload(file)
462
- editor.value.chain().focus().setVideo({ src: url }).run()
463
- } finally {
464
- uploading.value = false
465
- }
466
- }
125
+ const toolbarConfig = computed<Partial<IToolbarConfig>>(() => ({
126
+ excludeKeys: uploadEnabled.value ? [] : ['uploadImage', 'uploadVideo'],
127
+ }))
467
128
 
468
- function pickFile(accept: string, handler: (file: File) => void) {
469
- const input = document.createElement('input')
470
- input.type = 'file'
471
- input.accept = accept
472
- input.onchange = () => {
473
- const file = input.files?.[0]
474
- if (file) handler(file)
475
- }
476
- input.click()
477
- }
478
-
479
- function pickImage() {
480
- pickFile('image/*', (file) => void insertImage(file))
481
- }
482
-
483
- function pickVideo() {
484
- pickFile('video/*', (file) => void insertVideo(file))
485
- }
486
-
487
- function placeEmojiPanel() {
488
- const btn = emojiBtnRef.value
489
- if (!btn) return
490
- const rect = btn.getBoundingClientRect()
491
- // 10×28 + 9×2 gap + 16 padding
492
- const panelW = 314
493
- const panelH = 280
494
- const gap = 6
495
- const pad = 8
496
- // 右缘对齐按钮(往左展开,避免溢出大弹窗右侧)
497
- let left = rect.right - panelW
498
- let top = rect.bottom + gap
499
-
500
- const host =
501
- (btn.closest('[data-slot="dialog-content"]') as HTMLElement | null) ||
502
- (btn.closest('.vm-dialog-shell-crud') as HTMLElement | null)
503
- const bound = host?.getBoundingClientRect() ?? {
504
- left: pad,
505
- right: window.innerWidth - pad,
506
- top: pad,
507
- bottom: window.innerHeight - pad,
508
- }
509
-
510
- left = Math.min(left, bound.right - panelW - pad)
511
- left = Math.max(left, bound.left + pad)
512
- if (top + panelH > bound.bottom - pad) {
513
- top = Math.max(bound.top + pad, rect.top - panelH - gap)
514
- }
515
-
516
- emojiPanelStyle.value = {
517
- position: 'fixed',
518
- left: `${Math.round(left)}px`,
519
- top: `${Math.round(top)}px`,
520
- width: `${panelW}px`,
521
- zIndex: '10050',
522
- }
523
- }
524
-
525
- async function toggleEmoji() {
526
- emojiOpen.value = !emojiOpen.value
527
- if (emojiOpen.value) {
528
- await nextTick()
529
- placeEmojiPanel()
530
- }
531
- }
532
-
533
- function insertEmoji(emoji: string) {
534
- editor.value?.chain().focus().insertContent(emoji).run()
535
- emojiOpen.value = false
536
- }
537
-
538
- function rememberSelection() {
539
- const ed = editor.value
540
- if (!ed) return
541
- const { from, to } = ed.state.selection
542
- savedSelection = { from, to }
543
- }
544
-
545
- function syncColorModels() {
546
- const ed = editor.value
547
- if (!ed) return
548
- const fg = ed.getAttributes('textStyle').color as string | undefined
549
- textColor.value = fg?.trim() || '#111827'
550
- const bg = ed.getAttributes('highlight').color as string | undefined
551
- highlightColor.value = bg?.trim() || '#fef08a'
552
- const size = ed.getAttributes('textStyle').fontSize as string | undefined
553
- fontSize.value = size?.trim() || ''
554
- }
555
-
556
- function onFontSizeChange(e: Event) {
557
- const ed = editor.value
558
- if (!ed) return
559
- const v = String((e.target as HTMLSelectElement).value || '').trim()
560
- fontSize.value = v
561
- if (!v) {
562
- ed.chain().focus().unsetFontSize().run()
563
- return
564
- }
565
- ed.chain().focus().setFontSize(v).run()
566
- }
567
-
568
- function withSavedSelection(run: (chain: {
569
- unsetColor: () => { run: () => boolean }
570
- setColor: (c: string) => { run: () => boolean }
571
- unsetHighlight: () => { run: () => boolean }
572
- setHighlight: (attrs: { color: string }) => { run: () => boolean }
573
- }) => void) {
574
- const ed = editor.value
575
- if (!ed) return
576
- const chain = ed.chain().focus()
577
- if (savedSelection) {
578
- chain.setTextSelection(savedSelection)
579
- }
580
- run(chain as never)
581
- savedSelection = {
582
- from: ed.state.selection.from,
583
- to: ed.state.selection.to,
584
- }
585
- syncColorModels()
586
- }
587
-
588
- function applyTextColor(v: string) {
589
- const color = String(v || '').trim()
590
- withSavedSelection((chain) => {
591
- if (!color) {
592
- chain.unsetColor().run()
593
- return
594
- }
595
- chain.setColor(color).run()
596
- })
597
- }
598
-
599
- function applyHighlightColor(v: string) {
600
- const color = String(v || '').trim()
601
- withSavedSelection((chain) => {
602
- if (!color) {
603
- chain.unsetHighlight().run()
604
- return
605
- }
606
- chain.setHighlight({ color }).run()
607
- })
608
- }
609
-
610
- function insertTable() {
611
- editor.value
612
- ?.chain()
613
- .focus()
614
- .insertTable({ rows: 3, cols: 3, withHeaderRow: true })
615
- .run()
616
- }
617
-
618
- function toggleLink() {
619
- const ed = editor.value
620
- if (!ed) return
621
- const prev = ed.getAttributes('link').href as string | undefined
622
- if (ed.isActive('link')) {
623
- ed.chain().focus().unsetLink().run()
624
- return
625
- }
626
- const url = window.prompt(crudLabel('linkUrlPrompt'), prev || 'https://')
627
- if (url == null) return
628
- const href = url.trim()
629
- if (!href) {
630
- ed.chain().focus().unsetLink().run()
631
- return
632
- }
633
- ed.chain().focus().extendMarkRange('link').setLink({ href, target: '_blank', rel: 'noopener noreferrer' }).run()
634
- }
635
-
636
- function handleMediaPaste(event: ClipboardEvent) {
637
- if (!uploadEnabled.value) return false
638
- const items = Array.from(event.clipboardData?.items || [])
639
- const imageItem = items.find((item) => item.type.startsWith('image/'))
640
- if (imageItem) {
641
- const file = imageItem.getAsFile()
642
- if (!file) return false
643
- event.preventDefault()
644
- void insertImage(file)
645
- return true
646
- }
647
- const videoItem = items.find((item) => item.type.startsWith('video/'))
648
- if (videoItem) {
649
- const file = videoItem.getAsFile()
650
- if (!file) return false
651
- event.preventDefault()
652
- void insertVideo(file)
653
- return true
654
- }
655
- return false
656
- }
657
-
658
- function handleMediaDrop(event: DragEvent, moved: boolean) {
659
- if (moved || !uploadEnabled.value) return false
660
- const files = Array.from(event.dataTransfer?.files || [])
661
- const image = files.find((f) => f.type.startsWith('image/'))
662
- if (image) {
663
- event.preventDefault()
664
- void insertImage(image)
665
- return true
666
- }
667
- const video = files.find((f) => f.type.startsWith('video/'))
668
- if (video) {
669
- event.preventDefault()
670
- void insertVideo(video)
671
- return true
672
- }
673
- return false
674
- }
675
-
676
- const editor = useEditor({
677
- content: normalizeHtml(String(props.modelValue || '')),
678
- editable: !props.disabled,
679
- extensions: [
680
- StarterKit.configure({ heading: false }),
681
- Underline,
682
- TextStyle,
683
- Color,
684
- FontSize,
685
- Highlight.configure({ multicolor: true }),
686
- TextAlign.configure({ types: ['paragraph'] }),
687
- TaskList,
688
- TaskItem.configure({ nested: true }),
689
- Link.configure({
690
- openOnClick: false,
691
- HTMLAttributes: { rel: 'noopener noreferrer', target: '_blank' },
692
- }),
693
- Image.configure({ allowBase64: false }),
694
- VmRichtextVideo,
695
- Table.configure({ resizable: true }),
696
- TableRow,
697
- TableHeader,
698
- TableCell,
699
- Placeholder.configure({
700
- placeholder: props.placeholder ?? crudLabel('richtextPlaceholder'),
701
- }),
702
- ],
703
- editorProps: {
704
- attributes: { class: 'vm-richtext__prose' },
705
- handlePaste: (_view, event) => handleMediaPaste(event),
706
- handleDrop: (_view, event, _slice, moved) => handleMediaDrop(event, moved),
707
- handleDOMEvents: {
708
- mousedown: (view, event) => onTableMouseDown(view, event),
709
- contextmenu: (view, event) => onTableContextMenu(view, event),
129
+ const editorConfig = computed<Partial<IEditorConfig>>(() => ({
130
+ placeholder: props.placeholder ?? crudLabel('richtextPlaceholder'),
131
+ MENU_CONF: {
132
+ uploadImage: {
133
+ async customUpload(file: File, insertFn: (url: string, alt: string, href: string) => void) {
134
+ const url = await resolveUpload(file)
135
+ insertFn(url, file.name || 'image', url)
136
+ },
137
+ },
138
+ uploadVideo: {
139
+ async customUpload(file: File, insertFn: (url: string, poster: string) => void) {
140
+ const url = await resolveUpload(file)
141
+ insertFn(url, '')
142
+ },
710
143
  },
711
144
  },
712
- onUpdate: ({ editor: ed }) => {
713
- if (syncing.value) return
714
- emit('update:modelValue', normalizeHtml(ed.getHTML()))
715
- },
716
- onSelectionUpdate: () => {
717
- syncColorModels()
718
- },
719
- onTransaction: () => {
720
- syncColorModels()
721
- },
722
- })
145
+ }))
723
146
 
724
- watch(() => props.modelValue, (next) => {
725
- const ed = editor.value
726
- if (!ed || readOnlyMode.value) return
727
- const html = normalizeHtml(String(next || ''))
728
- const current = normalizeHtml(ed.getHTML())
729
- if (current === html) return
730
- syncing.value = true
731
- ed.commands.setContent(html || '<p></p>', false)
732
- syncing.value = false
733
- })
147
+ function handleCreated(editor: IDomEditor) {
148
+ editorRef.value = editor
149
+ if (props.disabled) editor.disable()
150
+ else editor.enable()
151
+ }
734
152
 
735
- watch(() => props.disabled, (v) => {
736
- editor.value?.setEditable(!v)
153
+ watch(html, (v) => {
154
+ if (syncing.value || readOnlyMode.value) return
155
+ emit('update:modelValue', normalizeHtml(String(v || '')))
737
156
  })
738
157
 
739
- function onDocPointer(e: Event) {
740
- if (!emojiOpen.value) return
741
- const t = e.target as Node
742
- if (emojiBtnRef.value?.contains(t)) return
743
- if (emojiPanelRef.value?.contains(t)) return
744
- emojiOpen.value = false
745
- }
158
+ watch(
159
+ () => props.modelValue,
160
+ (next) => {
161
+ if (readOnlyMode.value) return
162
+ const n = normalizeHtml(String(next || ''))
163
+ const cur = normalizeHtml(String(html.value || ''))
164
+ if (n === cur) return
165
+ syncing.value = true
166
+ html.value = n || '<p><br></p>'
167
+ syncing.value = false
168
+ },
169
+ )
746
170
 
747
- function onWinReposition() {
748
- if (emojiOpen.value) placeEmojiPanel()
749
- }
171
+ watch(
172
+ () => props.disabled,
173
+ (v) => {
174
+ const ed = editorRef.value
175
+ if (!ed) return
176
+ if (v) ed.disable()
177
+ else ed.enable()
178
+ },
179
+ )
750
180
 
751
- onMounted(() => {
752
- document.addEventListener('pointerdown', onDocPointer, true)
753
- window.addEventListener('resize', onWinReposition)
754
- window.addEventListener('scroll', onWinReposition, true)
755
- })
756
181
  onBeforeUnmount(() => {
757
- document.removeEventListener('pointerdown', onDocPointer, true)
758
- window.removeEventListener('resize', onWinReposition)
759
- window.removeEventListener('scroll', onWinReposition, true)
760
- editor.value?.destroy()
182
+ const ed = editorRef.value
183
+ if (!ed) return
184
+ ed.destroy()
185
+ editorRef.value = undefined
761
186
  })
762
187
  </script>
763
188
 
@@ -765,7 +190,6 @@ onBeforeUnmount(() => {
765
190
  .vm-richtext {
766
191
  width: 100%;
767
192
  min-width: 0;
768
- box-sizing: border-box;
769
193
  }
770
194
 
771
195
  .vm-richtext.is-disabled {
@@ -776,136 +200,28 @@ onBeforeUnmount(() => {
776
200
  .vm-richtext__shell {
777
201
  display: flex;
778
202
  flex-direction: column;
779
- width: 100%;
780
- min-width: 0;
781
- border: 1px solid var(--input);
782
- border-radius: var(--vm-control-radius);
783
- background: var(--card);
784
- overflow: hidden;
785
- }
786
-
787
- .vm-richtext__toolbar {
788
- display: flex;
789
- flex-wrap: wrap;
790
- align-items: center;
791
- gap: 2px;
792
- padding: 6px 8px;
793
- border-bottom: 1px solid var(--border);
794
- background: var(--muted);
795
- }
796
-
797
- .vm-richtext__btn {
798
- display: inline-flex;
799
- align-items: center;
800
- justify-content: center;
801
- min-width: 28px;
802
- height: 28px;
803
- padding: 0 6px;
804
- border: none;
805
- border-radius: 6px;
806
- background: transparent;
807
- color: var(--foreground);
808
- font-size: 12px;
809
- font-weight: 600;
810
- line-height: 1;
811
- cursor: pointer;
812
- transition: background 0.15s ease, color 0.15s ease;
813
-
814
- i { font-size: 15px; line-height: 1; }
815
-
816
- &:hover:not(:disabled) {
817
- background: color-mix(in srgb, var(--brand) 10%, transparent);
818
- color: var(--brand);
819
- }
820
-
821
- &.is-active {
822
- background: color-mix(in srgb, var(--brand) 16%, transparent);
823
- color: var(--brand);
824
- }
825
-
826
- &:disabled { opacity: 0.35; cursor: not-allowed; }
827
- }
828
-
829
- .vm-richtext__select {
830
- height: 28px;
831
- min-width: 64px;
832
- padding: 0 6px;
203
+ /* visible:表情/下拉挂在工具栏下,勿被 shell 裁切;弹窗不够高时靠 upsert body 滚动 */
204
+ overflow: visible;
833
205
  border: 1px solid var(--border);
834
- border-radius: 6px;
206
+ border-radius: var(--radius, 8px);
835
207
  background: var(--card);
836
- color: var(--foreground);
837
- font-size: 12px;
838
- line-height: 1;
839
- cursor: pointer;
840
-
841
- &:disabled { opacity: 0.35; cursor: not-allowed; }
842
208
  }
843
209
 
844
- /** 保留原图标 + 底部色条;透明覆盖层承接 vm-color-picker 点击 */
845
- .vm-richtext__color-btn {
210
+ .vm-richtext__toolbar {
846
211
  position: relative;
847
- display: inline-flex;
848
- flex-direction: column;
849
- align-items: center;
850
- justify-content: center;
851
- gap: 1px;
852
- padding: 0 5px 3px;
853
-
854
- &.is-disabled {
855
- opacity: 0.35;
856
- pointer-events: none;
857
- }
858
-
859
- i {
860
- position: relative;
861
- z-index: 1;
862
- pointer-events: none;
863
- }
212
+ z-index: 2;
213
+ overflow: visible;
214
+ border-bottom: 1px solid var(--border) !important;
215
+ background: var(--muted) !important;
864
216
  }
865
217
 
866
- .vm-richtext__color-bar {
218
+ .vm-richtext__editor {
867
219
  position: relative;
868
220
  z-index: 1;
869
- display: block;
870
- width: 14px;
871
- height: 3px;
872
- border-radius: 1px;
873
- pointer-events: none;
874
- box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--border) 70%, transparent);
875
- }
876
-
877
- .vm-richtext__color-hit {
878
- position: absolute !important;
879
- inset: 0;
880
- z-index: 2;
881
- width: auto !important;
882
- opacity: 0;
883
-
884
- :deep(.vm-color-picker__swatch) {
885
- width: 100%;
886
- height: 100%;
887
- border: none;
888
- border-radius: 6px;
889
- background: transparent;
890
- }
891
-
892
- :deep(.vm-color-picker__swatch-fill) {
893
- display: none;
894
- }
895
- }
896
-
897
- .vm-richtext__sep {
898
- width: 1px;
899
- height: 18px;
900
- margin: 0 4px;
901
- background: var(--border);
902
- }
903
-
904
- .vm-richtext__content {
905
- height: var(--vm-richtext-height, 320px) !important;
906
- min-height: var(--vm-richtext-height, 320px) !important;
907
221
  flex-shrink: 0;
908
- overflow-y: auto;
222
+ height: var(--vm-richtext-height, 400px) !important;
223
+ min-height: 400px !important;
224
+ overflow-y: hidden;
909
225
  background: var(--card);
910
226
  }
911
227
 
@@ -918,7 +234,6 @@ onBeforeUnmount(() => {
918
234
 
919
235
  :deep(p) { margin: 0.5em 0; }
920
236
  :deep(strong), :deep(b) { font-weight: 800; }
921
- /* Tailwind preflight 会清掉 list-style,只读展示也要补回 */
922
237
  :deep(ul) {
923
238
  margin: 0.5em 0;
924
239
  padding-left: 1.5em;
@@ -936,28 +251,21 @@ onBeforeUnmount(() => {
936
251
  :deep(ol ol) { list-style-type: lower-alpha; }
937
252
  :deep(ol ol ol) { list-style-type: lower-roman; }
938
253
  :deep(li) { display: list-item; }
939
- :deep(ul[data-type='taskList']) {
940
- list-style: none;
941
- padding-left: 0.25em;
942
- }
943
- :deep(ul[data-type='taskList'] li) {
944
- display: flex;
945
- gap: 8px;
946
- align-items: center;
947
- }
948
- :deep(ul[data-type='taskList'] li > label) {
949
- display: flex;
950
- align-items: center;
951
- flex-shrink: 0;
952
- margin: 0;
953
- }
954
- :deep(ul[data-type='taskList'] li > div > p) { margin: 0; }
955
254
  :deep(a) { color: var(--brand); text-decoration: underline; }
956
255
  :deep(img), :deep(video) { max-width: 100%; height: auto; margin: 8px 0; border-radius: 8px; }
957
256
  :deep(table) { width: 100%; border-collapse: collapse; margin: 8px 0; }
958
257
  :deep(th), :deep(td) { border: 1px solid var(--border); padding: 6px 8px; }
959
- :deep(blockquote) { margin: 0.5em 0; padding-left: 12px; border-left: 3px solid var(--border); color: var(--muted-foreground); }
960
- :deep(h1), :deep(h2), :deep(h3), :deep(h4) { margin: 0.65em 0 0.35em; font-weight: 650; line-height: 1.35; }
258
+ :deep(blockquote) {
259
+ margin: 0.5em 0;
260
+ padding-left: 12px;
261
+ border-left: 3px solid var(--border);
262
+ color: var(--muted-foreground);
263
+ }
264
+ :deep(h1), :deep(h2), :deep(h3), :deep(h4) {
265
+ margin: 0.65em 0 0.35em;
266
+ font-weight: 650;
267
+ line-height: 1.35;
268
+ }
961
269
  }
962
270
 
963
271
  .vm-richtext.is-readonly .vm-richtext__html {
@@ -965,145 +273,41 @@ onBeforeUnmount(() => {
965
273
  border: none;
966
274
  background: transparent;
967
275
  }
968
- </style>
969
276
 
970
- <style lang="scss">
971
- /* Teleport 到 body:Dialog modal 会给 body pointer-events:none,须 vm-dialog-float 恢复 */
972
- .vm-richtext__emoji-panel {
973
- box-sizing: border-box;
974
- display: grid;
975
- grid-template-columns: repeat(10, 28px);
976
- gap: 2px;
977
- width: 314px;
978
- max-height: 280px;
979
- overflow-x: hidden;
980
- overflow-y: auto;
981
- overscroll-behavior: contain;
982
- padding: 8px;
983
- border: 1px solid var(--border);
984
- border-radius: 10px;
985
- background: var(--card);
986
- box-shadow: var(--shadow-soft, 0 8px 24px rgba(0, 0, 0, 0.12));
987
- scrollbar-width: thin;
988
- pointer-events: auto;
277
+ :deep(.w-e-toolbar) {
278
+ border: none !important;
279
+ background: transparent !important;
989
280
  }
990
281
 
991
- .vm-richtext__emoji-item {
992
- display: flex;
993
- align-items: center;
994
- justify-content: center;
995
- width: 28px;
996
- height: 28px;
997
- border: none;
998
- border-radius: 6px;
999
- background: transparent;
1000
- font-size: 18px;
1001
- line-height: 1;
1002
- cursor: pointer;
282
+ :deep(.w-e-text-container) {
283
+ border: none !important;
284
+ background: var(--card) !important;
285
+ }
1003
286
 
1004
- &:hover {
1005
- background: var(--muted);
1006
- }
287
+ :deep(.w-e-scroll) {
288
+ height: var(--vm-richtext-height, 400px) !important;
289
+ min-height: 400px !important;
1007
290
  }
1008
291
 
1009
- .vm-richtext__content .tiptap.vm-richtext__prose {
1010
- min-height: 100%;
1011
- padding: 12px 14px;
1012
- outline: none;
292
+ :deep(.w-e-text-container [data-slate-editor]) {
293
+ min-height: calc(var(--vm-richtext-height, 400px) - 16px) !important;
1013
294
  color: var(--foreground);
1014
295
  font-size: 14px;
1015
296
  line-height: 1.65;
1016
297
 
1017
- p { margin: 0.45em 0; }
1018
298
  strong, b { font-weight: 800; }
299
+ }
300
+ </style>
1019
301
 
1020
- p.is-editor-empty:first-child::before {
1021
- content: attr(data-placeholder);
1022
- float: left;
1023
- height: 0;
1024
- pointer-events: none;
1025
- color: var(--muted-foreground);
1026
- }
1027
-
1028
- h2, h3 { margin: 0.75em 0 0.35em; font-weight: 650; line-height: 1.35; }
1029
- /* Tailwind / 全局 reset 会 list-style:none,必须显式恢复,否则只见缩进不见符号 */
1030
- ul {
1031
- margin: 0.45em 0;
1032
- padding-left: 1.5em;
1033
- list-style-type: disc;
1034
- list-style-position: outside;
1035
- }
1036
- ul ul { list-style-type: circle; }
1037
- ul ul ul { list-style-type: square; }
1038
- ol {
1039
- margin: 0.45em 0;
1040
- padding-left: 1.5em;
1041
- list-style-type: decimal;
1042
- list-style-position: outside;
1043
- }
1044
- ol ol { list-style-type: lower-alpha; }
1045
- ol ol ol { list-style-type: lower-roman; }
1046
- li {
1047
- display: list-item;
1048
- }
1049
- ul[data-type='taskList'] {
1050
- list-style: none;
1051
- padding-left: 0.25em;
1052
- }
1053
- ul[data-type='taskList'] li {
1054
- display: flex;
1055
- gap: 8px;
1056
- align-items: center;
1057
- }
1058
- ul[data-type='taskList'] li > label {
1059
- display: flex;
1060
- align-items: center;
1061
- flex-shrink: 0;
1062
- margin: 0;
1063
- }
1064
- ul[data-type='taskList'] li > div > p { margin: 0; }
1065
- blockquote { margin: 0.45em 0; padding-left: 12px; border-left: 3px solid var(--border); color: var(--muted-foreground); }
1066
- a { color: var(--brand); text-decoration: underline; cursor: pointer; }
1067
- img, video { display: block; max-width: 100%; height: auto; margin: 8px 0; border-radius: 8px; }
1068
- mark {
1069
- border-radius: 2px;
1070
- padding: 0 2px;
1071
- }
1072
- mark:not([style*='background']) {
1073
- background: color-mix(in srgb, var(--brand) 22%, transparent);
1074
- }
1075
- code { padding: 0.1em 0.35em; border-radius: 4px; background: var(--muted); font-size: 0.92em; }
1076
- pre { margin: 0.5em 0; padding: 10px 12px; border-radius: 8px; background: var(--muted); overflow-x: auto;
1077
- code { padding: 0; background: transparent; }
1078
- }
1079
- table { width: 100%; border-collapse: collapse; margin: 8px 0; table-layout: fixed; }
1080
- th, td {
1081
- position: relative;
1082
- min-width: 48px;
1083
- padding: 6px 8px;
1084
- border: 1px solid var(--border);
1085
- vertical-align: top;
1086
- }
1087
- th { background: var(--muted); font-weight: 600; }
1088
- .selectedCell {
1089
- background: color-mix(in srgb, var(--brand) 16%, transparent);
1090
- }
1091
- .selectedCell::after {
1092
- content: '';
1093
- position: absolute;
1094
- inset: 0;
1095
- pointer-events: none;
1096
- background: color-mix(in srgb, var(--brand) 10%, transparent);
1097
- }
1098
- .column-resize-handle {
1099
- position: absolute;
1100
- top: 0;
1101
- right: -2px;
1102
- bottom: 0;
1103
- width: 4px;
1104
- background: var(--brand);
1105
- pointer-events: none;
1106
- }
1107
- hr { margin: 12px 0; border: none; border-top: 1px solid var(--border); }
302
+ <style lang="scss">
303
+ /* wangEditor 浮层挂到 body,需高于 Dialog */
304
+ .w-e-bar-item-menus-container,
305
+ .w-e-select-list,
306
+ .w-e-drop-panel,
307
+ .w-e-modal,
308
+ .w-e-panel-content-emotion,
309
+ .w-e-panel-content-color,
310
+ .w-e-panel-content-table {
311
+ z-index: 10050 !important;
1108
312
  }
1109
313
  </style>