vome-core 0.3.42 → 0.3.43
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.
- package/dist/admin/components/ui/avatar/index.js +1 -1
- package/dist/admin/components/ui/checkbox/index.js +1 -1
- package/dist/admin/components/ui/dialog/index.js +1 -1
- package/dist/admin/components/ui/dropdown-menu/index.js +1 -1
- package/dist/admin/components/ui/input/index.js +1 -1
- package/dist/admin/components/ui/label/index.js +1 -1
- package/dist/admin/components/ui/scroll-area/index.js +1 -1
- package/dist/admin/components/ui/select/index.js +1 -1
- package/dist/admin/components/ui/separator/index.js +1 -1
- package/dist/admin/components/ui/sheet/index.js +1 -1
- package/dist/admin/components/ui/sonner/index.js +1 -1
- package/dist/admin/components/ui/table/index.js +1 -1
- package/dist/admin/components/ui/tabs/index.js +1 -1
- package/dist/admin/components/ui/tooltip/index.js +1 -1
- package/dist/admin/crud/auto-fields.js +1 -1
- package/dist/admin/crud/components/vm-richtext.vue +110 -923
- package/dist/admin/crud/config.js +1 -1
- package/dist/admin/crud/confirm.js +1 -1
- package/dist/admin/crud/crud-page-request.js +1 -1
- package/dist/admin/crud/data-i18n.js +1 -1
- package/dist/admin/crud/dict.js +1 -1
- package/dist/admin/crud/export-fetch.js +1 -1
- package/dist/admin/crud/key.js +1 -1
- package/dist/admin/crud/plugins.js +1 -1
- package/dist/admin/crud/span.js +1 -1
- package/dist/admin/crud/style.js +1 -1
- package/dist/admin/crud/validate.js +1 -1
- package/dist/admin/directives/perm.js +1 -1
- package/dist/admin/hooks/useUpload.js +1 -1
- package/dist/admin/lib/browser.js +1 -1
- package/dist/admin/lib/cn.js +1 -1
- package/dist/admin/lib/dialog-float.js +1 -1
- package/dist/admin/lib/export-excel.js +1 -1
- package/dist/admin/lib/import-excel.js +1 -1
- package/dist/admin/lib/json.js +1 -1
- package/dist/admin/lib/menu.js +1 -1
- package/dist/admin/lib/micro-locale.js +1 -1
- package/dist/admin/lib/micro-theme.js +1 -1
- package/dist/admin/lib/shell.js +1 -1
- package/dist/admin/lib/theme-color-options.js +1 -1
- package/dist/admin/lib/tree.js +1 -1
- package/dist/admin/lib/upload.js +1 -1
- package/dist/admin/lib/video-frame.js +1 -1
- package/dist/admin/router/create.js +1 -1
- package/dist/admin/router/guards.js +1 -1
- package/dist/ai/index.js +1 -1
- package/dist/client/ai-model-registry.js +1 -1
- package/dist/client/base.js +1 -1
- package/dist/client/configure.js +1 -1
- package/dist/client/dict.js +1 -1
- package/dist/client/eps-payload.js +1 -1
- package/dist/client/eps.js +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/locale-change.js +1 -1
- package/dist/client/locale-store.js +1 -1
- package/dist/client/service.js +1 -1
- package/dist/client/vite-admin.js +1 -1
- package/dist/client/vite-auto-import.js +1 -1
- package/dist/client/vite-micro-proxy.js +1 -1
- package/dist/client/vite-plugin-eps.js +1 -1
- package/dist/client/vite-resolve.js +1 -1
- package/dist/config/index.js +1 -1
- package/dist/index.js +1 -1
- package/dist/server/index.js +1 -1
- package/dist/shared/datetime-picker.js +1 -1
- package/dist/shared/decimal.js +1 -1
- package/dist/shared/excel.js +1 -1
- package/dist/shared/index.js +1 -1
- package/dist/shared/locale-json.js +1 -1
- package/dist/shared/tree.js +1 -1
- package/package.json +3 -19
- package/dist/admin/crud/components/vm-richtext-extensions.d.ts +0 -28
- 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
|
-
<
|
|
21
|
-
v-if="editor"
|
|
20
|
+
<Toolbar
|
|
22
21
|
class="vm-richtext__toolbar"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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,
|
|
40
|
+
import { computed, onBeforeUnmount, shallowRef, watch } from 'vue'
|
|
142
41
|
import DOMPurify from 'dompurify'
|
|
143
|
-
import {
|
|
144
|
-
import
|
|
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
|
|
|
@@ -197,218 +75,8 @@ 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)
|
|
@@ -436,6 +104,8 @@ function normalizeHtml(html: string) {
|
|
|
436
104
|
|
|
437
105
|
const safeHtml = computed(() => sanitize(String(props.modelValue || '')))
|
|
438
106
|
|
|
107
|
+
const html = shallowRef(normalizeHtml(String(props.modelValue || '')) || '<p><br></p>')
|
|
108
|
+
|
|
439
109
|
async function resolveUpload(file: File): Promise<string> {
|
|
440
110
|
if (props.uploadImage) return props.uploadImage(file)
|
|
441
111
|
if (props.autoUpload === false) throw new Error(crudLabel('noUploadImage'))
|
|
@@ -443,321 +113,67 @@ async function resolveUpload(file: File): Promise<string> {
|
|
|
443
113
|
return res.url
|
|
444
114
|
}
|
|
445
115
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
}
|
|
467
|
-
|
|
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
|
-
}
|
|
116
|
+
const toolbarConfig = computed<Partial<IToolbarConfig>>(() => ({
|
|
117
|
+
excludeKeys: uploadEnabled.value ? [] : ['uploadImage', 'uploadVideo'],
|
|
118
|
+
}))
|
|
675
119
|
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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),
|
|
120
|
+
const editorConfig = computed<Partial<IEditorConfig>>(() => ({
|
|
121
|
+
placeholder: props.placeholder ?? crudLabel('richtextPlaceholder'),
|
|
122
|
+
MENU_CONF: {
|
|
123
|
+
uploadImage: {
|
|
124
|
+
async customUpload(file: File, insertFn: (url: string, alt: string, href: string) => void) {
|
|
125
|
+
const url = await resolveUpload(file)
|
|
126
|
+
insertFn(url, file.name || 'image', url)
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
uploadVideo: {
|
|
130
|
+
async customUpload(file: File, insertFn: (url: string, poster: string) => void) {
|
|
131
|
+
const url = await resolveUpload(file)
|
|
132
|
+
insertFn(url, '')
|
|
133
|
+
},
|
|
710
134
|
},
|
|
711
135
|
},
|
|
712
|
-
|
|
713
|
-
if (syncing.value) return
|
|
714
|
-
emit('update:modelValue', normalizeHtml(ed.getHTML()))
|
|
715
|
-
},
|
|
716
|
-
onSelectionUpdate: () => {
|
|
717
|
-
syncColorModels()
|
|
718
|
-
},
|
|
719
|
-
onTransaction: () => {
|
|
720
|
-
syncColorModels()
|
|
721
|
-
},
|
|
722
|
-
})
|
|
136
|
+
}))
|
|
723
137
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
if (
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
if (current === html) return
|
|
730
|
-
syncing.value = true
|
|
731
|
-
ed.commands.setContent(html || '<p></p>', false)
|
|
732
|
-
syncing.value = false
|
|
733
|
-
})
|
|
138
|
+
function handleCreated(editor: IDomEditor) {
|
|
139
|
+
editorRef.value = editor
|
|
140
|
+
if (props.disabled) editor.disable()
|
|
141
|
+
else editor.enable()
|
|
142
|
+
}
|
|
734
143
|
|
|
735
|
-
watch(
|
|
736
|
-
|
|
144
|
+
watch(html, (v) => {
|
|
145
|
+
if (syncing.value || readOnlyMode.value) return
|
|
146
|
+
emit('update:modelValue', normalizeHtml(String(v || '')))
|
|
737
147
|
})
|
|
738
148
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
149
|
+
watch(
|
|
150
|
+
() => props.modelValue,
|
|
151
|
+
(next) => {
|
|
152
|
+
if (readOnlyMode.value) return
|
|
153
|
+
const n = normalizeHtml(String(next || ''))
|
|
154
|
+
const cur = normalizeHtml(String(html.value || ''))
|
|
155
|
+
if (n === cur) return
|
|
156
|
+
syncing.value = true
|
|
157
|
+
html.value = n || '<p><br></p>'
|
|
158
|
+
syncing.value = false
|
|
159
|
+
},
|
|
160
|
+
)
|
|
746
161
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
162
|
+
watch(
|
|
163
|
+
() => props.disabled,
|
|
164
|
+
(v) => {
|
|
165
|
+
const ed = editorRef.value
|
|
166
|
+
if (!ed) return
|
|
167
|
+
if (v) ed.disable()
|
|
168
|
+
else ed.enable()
|
|
169
|
+
},
|
|
170
|
+
)
|
|
750
171
|
|
|
751
|
-
onMounted(() => {
|
|
752
|
-
document.addEventListener('pointerdown', onDocPointer, true)
|
|
753
|
-
window.addEventListener('resize', onWinReposition)
|
|
754
|
-
window.addEventListener('scroll', onWinReposition, true)
|
|
755
|
-
})
|
|
756
172
|
onBeforeUnmount(() => {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
173
|
+
const ed = editorRef.value
|
|
174
|
+
if (!ed) return
|
|
175
|
+
ed.destroy()
|
|
176
|
+
editorRef.value = undefined
|
|
761
177
|
})
|
|
762
178
|
</script>
|
|
763
179
|
|
|
@@ -765,7 +181,6 @@ onBeforeUnmount(() => {
|
|
|
765
181
|
.vm-richtext {
|
|
766
182
|
width: 100%;
|
|
767
183
|
min-width: 0;
|
|
768
|
-
box-sizing: border-box;
|
|
769
184
|
}
|
|
770
185
|
|
|
771
186
|
.vm-richtext.is-disabled {
|
|
@@ -776,136 +191,21 @@ onBeforeUnmount(() => {
|
|
|
776
191
|
.vm-richtext__shell {
|
|
777
192
|
display: flex;
|
|
778
193
|
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
194
|
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;
|
|
833
195
|
border: 1px solid var(--border);
|
|
834
|
-
border-radius:
|
|
196
|
+
border-radius: var(--radius, 8px);
|
|
835
197
|
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
|
-
}
|
|
843
|
-
|
|
844
|
-
/** 保留原图标 + 底部色条;透明覆盖层承接 vm-color-picker 点击 */
|
|
845
|
-
.vm-richtext__color-btn {
|
|
846
|
-
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
|
-
}
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
.vm-richtext__color-bar {
|
|
867
|
-
position: relative;
|
|
868
|
-
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
198
|
}
|
|
876
199
|
|
|
877
|
-
.vm-
|
|
878
|
-
|
|
879
|
-
|
|
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);
|
|
200
|
+
.vm-richtext__toolbar {
|
|
201
|
+
border-bottom: 1px solid var(--border) !important;
|
|
202
|
+
background: var(--muted) !important;
|
|
902
203
|
}
|
|
903
204
|
|
|
904
|
-
.vm-
|
|
905
|
-
height: var(--vm-richtext-height, 320px) !important;
|
|
906
|
-
min-height: var(--vm-richtext-height, 320px) !important;
|
|
205
|
+
.vm-richtext__editor {
|
|
907
206
|
flex-shrink: 0;
|
|
908
|
-
|
|
207
|
+
height: var(--vm-richtext-height, 320px) !important;
|
|
208
|
+
overflow-y: hidden;
|
|
909
209
|
background: var(--card);
|
|
910
210
|
}
|
|
911
211
|
|
|
@@ -918,7 +218,6 @@ onBeforeUnmount(() => {
|
|
|
918
218
|
|
|
919
219
|
:deep(p) { margin: 0.5em 0; }
|
|
920
220
|
:deep(strong), :deep(b) { font-weight: 800; }
|
|
921
|
-
/* Tailwind preflight 会清掉 list-style,只读展示也要补回 */
|
|
922
221
|
:deep(ul) {
|
|
923
222
|
margin: 0.5em 0;
|
|
924
223
|
padding-left: 1.5em;
|
|
@@ -936,28 +235,21 @@ onBeforeUnmount(() => {
|
|
|
936
235
|
:deep(ol ol) { list-style-type: lower-alpha; }
|
|
937
236
|
:deep(ol ol ol) { list-style-type: lower-roman; }
|
|
938
237
|
: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
238
|
:deep(a) { color: var(--brand); text-decoration: underline; }
|
|
956
239
|
:deep(img), :deep(video) { max-width: 100%; height: auto; margin: 8px 0; border-radius: 8px; }
|
|
957
240
|
:deep(table) { width: 100%; border-collapse: collapse; margin: 8px 0; }
|
|
958
241
|
:deep(th), :deep(td) { border: 1px solid var(--border); padding: 6px 8px; }
|
|
959
|
-
:deep(blockquote) {
|
|
960
|
-
|
|
242
|
+
:deep(blockquote) {
|
|
243
|
+
margin: 0.5em 0;
|
|
244
|
+
padding-left: 12px;
|
|
245
|
+
border-left: 3px solid var(--border);
|
|
246
|
+
color: var(--muted-foreground);
|
|
247
|
+
}
|
|
248
|
+
:deep(h1), :deep(h2), :deep(h3), :deep(h4) {
|
|
249
|
+
margin: 0.65em 0 0.35em;
|
|
250
|
+
font-weight: 650;
|
|
251
|
+
line-height: 1.35;
|
|
252
|
+
}
|
|
961
253
|
}
|
|
962
254
|
|
|
963
255
|
.vm-richtext.is-readonly .vm-richtext__html {
|
|
@@ -965,145 +257,40 @@ onBeforeUnmount(() => {
|
|
|
965
257
|
border: none;
|
|
966
258
|
background: transparent;
|
|
967
259
|
}
|
|
968
|
-
</style>
|
|
969
260
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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;
|
|
261
|
+
:deep(.w-e-toolbar) {
|
|
262
|
+
border: none !important;
|
|
263
|
+
background: transparent !important;
|
|
989
264
|
}
|
|
990
265
|
|
|
991
|
-
.
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
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;
|
|
266
|
+
:deep(.w-e-text-container) {
|
|
267
|
+
border: none !important;
|
|
268
|
+
background: var(--card) !important;
|
|
269
|
+
}
|
|
1003
270
|
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
}
|
|
271
|
+
:deep(.w-e-scroll) {
|
|
272
|
+
height: var(--vm-richtext-height, 320px) !important;
|
|
1007
273
|
}
|
|
1008
274
|
|
|
1009
|
-
.
|
|
1010
|
-
min-height:
|
|
1011
|
-
padding: 12px 14px;
|
|
1012
|
-
outline: none;
|
|
275
|
+
:deep(.w-e-text-container [data-slate-editor]) {
|
|
276
|
+
min-height: calc(var(--vm-richtext-height, 320px) - 16px) !important;
|
|
1013
277
|
color: var(--foreground);
|
|
1014
278
|
font-size: 14px;
|
|
1015
279
|
line-height: 1.65;
|
|
1016
280
|
|
|
1017
|
-
p { margin: 0.45em 0; }
|
|
1018
281
|
strong, b { font-weight: 800; }
|
|
282
|
+
}
|
|
283
|
+
</style>
|
|
1019
284
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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); }
|
|
285
|
+
<style lang="scss">
|
|
286
|
+
/* wangEditor 浮层挂到 body,需高于 Dialog */
|
|
287
|
+
.w-e-bar-item-menus-container,
|
|
288
|
+
.w-e-select-list,
|
|
289
|
+
.w-e-drop-panel,
|
|
290
|
+
.w-e-modal,
|
|
291
|
+
.w-e-panel-content-emotion,
|
|
292
|
+
.w-e-panel-content-color,
|
|
293
|
+
.w-e-panel-content-table {
|
|
294
|
+
z-index: 10050 !important;
|
|
1108
295
|
}
|
|
1109
296
|
</style>
|