vome-core 0.3.41 → 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 +113 -867
- 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 -14
- package/dist/admin/crud/components/vm-richtext-extensions.js +0 -1
|
@@ -17,146 +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
|
-
<button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }" :title="crudLabel('rtH2')" @click="editor.chain().focus().toggleHeading({ level: 2 }).run()">H2</button>
|
|
68
|
-
<button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('heading', { level: 3 }) }" :title="crudLabel('rtH3')" @click="editor.chain().focus().toggleHeading({ level: 3 }).run()">H3</button>
|
|
69
|
-
<button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('paragraph') }" :title="crudLabel('rtParagraph')" @click="editor.chain().focus().setParagraph().run()">P</button>
|
|
70
|
-
<span class="vm-richtext__sep" />
|
|
71
|
-
<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>
|
|
72
|
-
<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>
|
|
73
|
-
<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>
|
|
74
|
-
<span class="vm-richtext__sep" />
|
|
75
|
-
<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>
|
|
76
|
-
<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>
|
|
77
|
-
<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>
|
|
78
|
-
<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>
|
|
79
|
-
<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>
|
|
80
|
-
<button type="button" class="vm-richtext__btn" :title="crudLabel('rtHr')" @click="editor.chain().focus().setHorizontalRule().run()"><i class="ri-separator" /></button>
|
|
81
|
-
<span class="vm-richtext__sep" />
|
|
82
|
-
<button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('link') }" :title="crudLabel('rtLink')" @click="toggleLink"><i class="ri-link" /></button>
|
|
83
|
-
<button v-if="uploadEnabled" type="button" class="vm-richtext__btn" :title="crudLabel('rtImage')" @click="pickImage"><i class="ri-image-line" /></button>
|
|
84
|
-
<button v-if="uploadEnabled" type="button" class="vm-richtext__btn" :title="crudLabel('rtVideo')" @click="pickVideo"><i class="ri-video-line" /></button>
|
|
85
|
-
<button
|
|
86
|
-
ref="emojiBtnRef"
|
|
87
|
-
type="button"
|
|
88
|
-
class="vm-richtext__btn"
|
|
89
|
-
:class="{ 'is-active': emojiOpen }"
|
|
90
|
-
:title="crudLabel('rtEmoji')"
|
|
91
|
-
@click="toggleEmoji"
|
|
92
|
-
>
|
|
93
|
-
<i class="ri-emotion-line" />
|
|
94
|
-
</button>
|
|
95
|
-
<Teleport to="body">
|
|
96
|
-
<div
|
|
97
|
-
v-if="emojiOpen"
|
|
98
|
-
ref="emojiPanelRef"
|
|
99
|
-
class="vm-richtext__emoji-panel vm-dialog-float"
|
|
100
|
-
:style="emojiPanelStyle"
|
|
101
|
-
@pointerdown.stop
|
|
102
|
-
@mousedown.stop
|
|
103
|
-
>
|
|
104
|
-
<button
|
|
105
|
-
v-for="(e, i) in VM_RICHTEXT_EMOJIS"
|
|
106
|
-
:key="i"
|
|
107
|
-
type="button"
|
|
108
|
-
class="vm-richtext__emoji-item"
|
|
109
|
-
@click.stop="insertEmoji(e)"
|
|
110
|
-
>
|
|
111
|
-
{{ e }}
|
|
112
|
-
</button>
|
|
113
|
-
</div>
|
|
114
|
-
</Teleport>
|
|
115
|
-
<span class="vm-richtext__sep" />
|
|
116
|
-
<button type="button" class="vm-richtext__btn" :title="crudLabel('rtTable')" @click="insertTable"><i class="ri-table-line" /></button>
|
|
117
|
-
<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>
|
|
118
|
-
<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>
|
|
119
|
-
</div>
|
|
120
|
-
|
|
121
|
-
<EditorContent class="vm-richtext__content" :editor="editor" />
|
|
122
|
-
<vm-context-menu
|
|
123
|
-
ref="tableCtxRef"
|
|
124
|
-
:items="tableCtxItems"
|
|
125
|
-
:z-index="10050"
|
|
126
|
-
@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"
|
|
127
32
|
/>
|
|
128
33
|
</div>
|
|
129
34
|
</div>
|
|
130
35
|
</template>
|
|
131
36
|
|
|
132
37
|
<script setup lang="ts">
|
|
38
|
+
import '@wangeditor/editor/dist/css/style.css'
|
|
133
39
|
import { crudLabel } from '../style'
|
|
134
|
-
import { computed,
|
|
40
|
+
import { computed, onBeforeUnmount, shallowRef, watch } from 'vue'
|
|
135
41
|
import DOMPurify from 'dompurify'
|
|
136
|
-
import {
|
|
137
|
-
import
|
|
138
|
-
import Image from '@tiptap/extension-image'
|
|
139
|
-
import Link from '@tiptap/extension-link'
|
|
140
|
-
import Placeholder from '@tiptap/extension-placeholder'
|
|
141
|
-
import Underline from '@tiptap/extension-underline'
|
|
142
|
-
import TextAlign from '@tiptap/extension-text-align'
|
|
143
|
-
import Highlight from '@tiptap/extension-highlight'
|
|
144
|
-
import TextStyle from '@tiptap/extension-text-style'
|
|
145
|
-
import Color from '@tiptap/extension-color'
|
|
146
|
-
import TaskList from '@tiptap/extension-task-list'
|
|
147
|
-
import TaskItem from '@tiptap/extension-task-item'
|
|
148
|
-
import Table from '@tiptap/extension-table'
|
|
149
|
-
import TableRow from '@tiptap/extension-table-row'
|
|
150
|
-
import TableHeader from '@tiptap/extension-table-header'
|
|
151
|
-
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'
|
|
152
44
|
import { useUpload } from '../../hooks/useUpload'
|
|
153
|
-
import { VM_RICHTEXT_EMOJIS, VmRichtextVideo } from './vm-richtext-extensions'
|
|
154
|
-
import VmColorPicker from './vm-color-picker.vue'
|
|
155
|
-
import VmContextMenu from './vm-context-menu.vue'
|
|
156
|
-
import { CellSelection, cellAround } from '@tiptap/pm/tables'
|
|
157
|
-
import type { Editor } from '@tiptap/core'
|
|
158
|
-
import type { EditorView } from '@tiptap/pm/view'
|
|
159
|
-
import type { CtxMenuItem } from '../../../../typings/admin/comm/crud'
|
|
160
45
|
|
|
161
46
|
defineOptions({ name: 'vm-richtext' })
|
|
162
47
|
|
|
@@ -190,216 +75,8 @@ const props = withDefaults(
|
|
|
190
75
|
const emit = defineEmits<{ 'update:modelValue': [v: string] }>()
|
|
191
76
|
|
|
192
77
|
const { toUpload } = useUpload()
|
|
78
|
+
const editorRef = shallowRef<IDomEditor>()
|
|
193
79
|
const syncing = shallowRef(false)
|
|
194
|
-
const uploading = shallowRef(false)
|
|
195
|
-
const emojiOpen = shallowRef(false)
|
|
196
|
-
const emojiBtnRef = shallowRef<HTMLElement>()
|
|
197
|
-
const emojiPanelRef = shallowRef<HTMLElement>()
|
|
198
|
-
const emojiPanelStyle = shallowRef<Record<string, string>>({})
|
|
199
|
-
const textColor = ref('#111827')
|
|
200
|
-
const highlightColor = ref('#fef08a')
|
|
201
|
-
/** 点开取色器前保存选区,避免焦点离开编辑器后丢标记范围 */
|
|
202
|
-
let savedSelection: { from: number; to: number } | null = null
|
|
203
|
-
|
|
204
|
-
const tableCtxRef = ref<{ open: (ev: MouseEvent) => void; close: () => void } | null>(null)
|
|
205
|
-
const tableCtxItems = ref<CtxMenuItem[]>([])
|
|
206
|
-
/** 右键按下时缓存多选,防止 mousedown 先把 CellSelection 清掉 */
|
|
207
|
-
let pendingCellSel: { anchor: number; head: number } | null = null
|
|
208
|
-
|
|
209
|
-
type TableCtxKey =
|
|
210
|
-
| 'addColumnBefore'
|
|
211
|
-
| 'addColumnAfter'
|
|
212
|
-
| 'addRowBefore'
|
|
213
|
-
| 'addRowAfter'
|
|
214
|
-
| 'deleteColumn'
|
|
215
|
-
| 'deleteRow'
|
|
216
|
-
| 'mergeCells'
|
|
217
|
-
| 'splitCell'
|
|
218
|
-
|
|
219
|
-
function cellPosInSelection(sel: CellSelection, cellPos: number) {
|
|
220
|
-
let hit = false
|
|
221
|
-
sel.forEachCell((_node, pos) => {
|
|
222
|
-
if (pos === cellPos) hit = true
|
|
223
|
-
})
|
|
224
|
-
return hit
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function restorePendingCellSelection(view: EditorView) {
|
|
228
|
-
if (!pendingCellSel) return false
|
|
229
|
-
try {
|
|
230
|
-
const sel = CellSelection.create(
|
|
231
|
-
view.state.doc,
|
|
232
|
-
pendingCellSel.anchor,
|
|
233
|
-
pendingCellSel.head,
|
|
234
|
-
)
|
|
235
|
-
view.dispatch(view.state.tr.setSelection(sel))
|
|
236
|
-
return true
|
|
237
|
-
} catch {
|
|
238
|
-
return false
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* 右键 mousedown:表格插件会先清掉多选。
|
|
244
|
-
* 若点在已有 CellSelection 内 → preventDefault 并记下选区,供 contextmenu 恢复。
|
|
245
|
-
*/
|
|
246
|
-
function onTableMouseDown(view: EditorView, event: MouseEvent) {
|
|
247
|
-
if (event.button !== 2) {
|
|
248
|
-
pendingCellSel = null
|
|
249
|
-
return false
|
|
250
|
-
}
|
|
251
|
-
const target = event.target as HTMLElement | null
|
|
252
|
-
if (!target?.closest?.('td, th')) {
|
|
253
|
-
pendingCellSel = null
|
|
254
|
-
return false
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const { selection } = view.state
|
|
258
|
-
if (!(selection instanceof CellSelection)) {
|
|
259
|
-
pendingCellSel = null
|
|
260
|
-
return false
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
pendingCellSel = {
|
|
264
|
-
anchor: selection.$anchorCell.pos,
|
|
265
|
-
head: selection.$headCell.pos,
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const coords = view.posAtCoords({ left: event.clientX, top: event.clientY })
|
|
269
|
-
if (!coords) return false
|
|
270
|
-
const $cell = cellAround(view.state.doc.resolve(coords.pos))
|
|
271
|
-
if (!$cell) return false
|
|
272
|
-
|
|
273
|
-
if (cellPosInSelection(selection, $cell.pos)) {
|
|
274
|
-
event.preventDefault()
|
|
275
|
-
return true
|
|
276
|
-
}
|
|
277
|
-
return false
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/** 右键落点:已在多选内则保留;否则选中该单元格作为参照 */
|
|
281
|
-
function selectTableCellFromEvent(ed: Editor, event: MouseEvent) {
|
|
282
|
-
const view = ed.view
|
|
283
|
-
const coords = view.posAtCoords({ left: event.clientX, top: event.clientY })
|
|
284
|
-
if (!coords) return false
|
|
285
|
-
const $cell = cellAround(view.state.doc.resolve(coords.pos))
|
|
286
|
-
if (!$cell) return false
|
|
287
|
-
|
|
288
|
-
const { selection } = view.state
|
|
289
|
-
if (selection instanceof CellSelection && cellPosInSelection(selection, $cell.pos)) {
|
|
290
|
-
return true
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// mousedown 已清多选时,用缓存恢复(且右键仍落在原多选内)
|
|
294
|
-
if (pendingCellSel) {
|
|
295
|
-
const probe = CellSelection.create(
|
|
296
|
-
view.state.doc,
|
|
297
|
-
pendingCellSel.anchor,
|
|
298
|
-
pendingCellSel.head,
|
|
299
|
-
)
|
|
300
|
-
if (cellPosInSelection(probe, $cell.pos)) {
|
|
301
|
-
return restorePendingCellSelection(view)
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const tr = view.state.tr.setSelection(CellSelection.create(view.state.doc, $cell.pos))
|
|
306
|
-
view.dispatch(tr)
|
|
307
|
-
return true
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function buildTableCtxItems(ed: Editor): CtxMenuItem[] {
|
|
311
|
-
const can = ed.can()
|
|
312
|
-
return [
|
|
313
|
-
{
|
|
314
|
-
key: 'addColumnBefore',
|
|
315
|
-
label: crudLabel('rtTableColBefore'),
|
|
316
|
-
disabled: !can.addColumnBefore(),
|
|
317
|
-
},
|
|
318
|
-
{
|
|
319
|
-
key: 'addColumnAfter',
|
|
320
|
-
label: crudLabel('rtTableColAfter'),
|
|
321
|
-
disabled: !can.addColumnAfter(),
|
|
322
|
-
},
|
|
323
|
-
{
|
|
324
|
-
key: 'addRowBefore',
|
|
325
|
-
label: crudLabel('rtTableRowBefore'),
|
|
326
|
-
disabled: !can.addRowBefore(),
|
|
327
|
-
},
|
|
328
|
-
{
|
|
329
|
-
key: 'addRowAfter',
|
|
330
|
-
label: crudLabel('rtTableRowAfter'),
|
|
331
|
-
disabled: !can.addRowAfter(),
|
|
332
|
-
},
|
|
333
|
-
{
|
|
334
|
-
key: 'deleteColumn',
|
|
335
|
-
label: crudLabel('rtTableDeleteCol'),
|
|
336
|
-
disabled: !can.deleteColumn(),
|
|
337
|
-
},
|
|
338
|
-
{
|
|
339
|
-
key: 'deleteRow',
|
|
340
|
-
label: crudLabel('rtTableDeleteRow'),
|
|
341
|
-
disabled: !can.deleteRow(),
|
|
342
|
-
},
|
|
343
|
-
{
|
|
344
|
-
key: 'mergeCells',
|
|
345
|
-
label: crudLabel('rtTableMerge'),
|
|
346
|
-
disabled: !can.mergeCells(),
|
|
347
|
-
},
|
|
348
|
-
{
|
|
349
|
-
key: 'splitCell',
|
|
350
|
-
label: crudLabel('rtTableSplit'),
|
|
351
|
-
disabled: !can.splitCell(),
|
|
352
|
-
},
|
|
353
|
-
]
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function onTableContextMenu(view: EditorView, event: MouseEvent) {
|
|
357
|
-
const ed = editor.value
|
|
358
|
-
if (!ed || ed.isDestroyed || props.disabled) return false
|
|
359
|
-
const target = event.target as HTMLElement | null
|
|
360
|
-
if (!target?.closest?.('td, th')) return false
|
|
361
|
-
if (!selectTableCellFromEvent(ed, event)) return false
|
|
362
|
-
event.preventDefault()
|
|
363
|
-
tableCtxItems.value = buildTableCtxItems(ed)
|
|
364
|
-
tableCtxRef.value?.open(event)
|
|
365
|
-
pendingCellSel = null
|
|
366
|
-
return true
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function onTableCtxSelect(key: string) {
|
|
370
|
-
const ed = editor.value
|
|
371
|
-
if (!ed) return
|
|
372
|
-
const k = key as TableCtxKey
|
|
373
|
-
const chain = ed.chain().focus()
|
|
374
|
-
switch (k) {
|
|
375
|
-
case 'addColumnBefore':
|
|
376
|
-
chain.addColumnBefore().run()
|
|
377
|
-
break
|
|
378
|
-
case 'addColumnAfter':
|
|
379
|
-
chain.addColumnAfter().run()
|
|
380
|
-
break
|
|
381
|
-
case 'addRowBefore':
|
|
382
|
-
chain.addRowBefore().run()
|
|
383
|
-
break
|
|
384
|
-
case 'addRowAfter':
|
|
385
|
-
chain.addRowAfter().run()
|
|
386
|
-
break
|
|
387
|
-
case 'deleteColumn':
|
|
388
|
-
chain.deleteColumn().run()
|
|
389
|
-
break
|
|
390
|
-
case 'deleteRow':
|
|
391
|
-
chain.deleteRow().run()
|
|
392
|
-
break
|
|
393
|
-
case 'mergeCells':
|
|
394
|
-
chain.mergeCells().run()
|
|
395
|
-
break
|
|
396
|
-
case 'splitCell':
|
|
397
|
-
chain.splitCell().run()
|
|
398
|
-
break
|
|
399
|
-
default:
|
|
400
|
-
break
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
80
|
|
|
404
81
|
const readOnlyMode = computed(() => props.readonly)
|
|
405
82
|
const uploadEnabled = computed(() => Boolean(props.uploadImage) || props.autoUpload !== false)
|
|
@@ -427,6 +104,8 @@ function normalizeHtml(html: string) {
|
|
|
427
104
|
|
|
428
105
|
const safeHtml = computed(() => sanitize(String(props.modelValue || '')))
|
|
429
106
|
|
|
107
|
+
const html = shallowRef(normalizeHtml(String(props.modelValue || '')) || '<p><br></p>')
|
|
108
|
+
|
|
430
109
|
async function resolveUpload(file: File): Promise<string> {
|
|
431
110
|
if (props.uploadImage) return props.uploadImage(file)
|
|
432
111
|
if (props.autoUpload === false) throw new Error(crudLabel('noUploadImage'))
|
|
@@ -434,306 +113,67 @@ async function resolveUpload(file: File): Promise<string> {
|
|
|
434
113
|
return res.url
|
|
435
114
|
}
|
|
436
115
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
try {
|
|
441
|
-
const url = await resolveUpload(file)
|
|
442
|
-
editor.value.chain().focus().setImage({ src: url, alt: file.name || 'image' }).run()
|
|
443
|
-
} finally {
|
|
444
|
-
uploading.value = false
|
|
445
|
-
}
|
|
446
|
-
}
|
|
116
|
+
const toolbarConfig = computed<Partial<IToolbarConfig>>(() => ({
|
|
117
|
+
excludeKeys: uploadEnabled.value ? [] : ['uploadImage', 'uploadVideo'],
|
|
118
|
+
}))
|
|
447
119
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
input.accept = accept
|
|
463
|
-
input.onchange = () => {
|
|
464
|
-
const file = input.files?.[0]
|
|
465
|
-
if (file) handler(file)
|
|
466
|
-
}
|
|
467
|
-
input.click()
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
function pickImage() {
|
|
471
|
-
pickFile('image/*', (file) => void insertImage(file))
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function pickVideo() {
|
|
475
|
-
pickFile('video/*', (file) => void insertVideo(file))
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function placeEmojiPanel() {
|
|
479
|
-
const btn = emojiBtnRef.value
|
|
480
|
-
if (!btn) return
|
|
481
|
-
const rect = btn.getBoundingClientRect()
|
|
482
|
-
// 10×28 + 9×2 gap + 16 padding
|
|
483
|
-
const panelW = 314
|
|
484
|
-
const panelH = 280
|
|
485
|
-
const gap = 6
|
|
486
|
-
const pad = 8
|
|
487
|
-
// 右缘对齐按钮(往左展开,避免溢出大弹窗右侧)
|
|
488
|
-
let left = rect.right - panelW
|
|
489
|
-
let top = rect.bottom + gap
|
|
490
|
-
|
|
491
|
-
const host =
|
|
492
|
-
(btn.closest('[data-slot="dialog-content"]') as HTMLElement | null) ||
|
|
493
|
-
(btn.closest('.vm-dialog-shell-crud') as HTMLElement | null)
|
|
494
|
-
const bound = host?.getBoundingClientRect() ?? {
|
|
495
|
-
left: pad,
|
|
496
|
-
right: window.innerWidth - pad,
|
|
497
|
-
top: pad,
|
|
498
|
-
bottom: window.innerHeight - pad,
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
left = Math.min(left, bound.right - panelW - pad)
|
|
502
|
-
left = Math.max(left, bound.left + pad)
|
|
503
|
-
if (top + panelH > bound.bottom - pad) {
|
|
504
|
-
top = Math.max(bound.top + pad, rect.top - panelH - gap)
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
emojiPanelStyle.value = {
|
|
508
|
-
position: 'fixed',
|
|
509
|
-
left: `${Math.round(left)}px`,
|
|
510
|
-
top: `${Math.round(top)}px`,
|
|
511
|
-
width: `${panelW}px`,
|
|
512
|
-
zIndex: '10050',
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
async function toggleEmoji() {
|
|
517
|
-
emojiOpen.value = !emojiOpen.value
|
|
518
|
-
if (emojiOpen.value) {
|
|
519
|
-
await nextTick()
|
|
520
|
-
placeEmojiPanel()
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function insertEmoji(emoji: string) {
|
|
525
|
-
editor.value?.chain().focus().insertContent(emoji).run()
|
|
526
|
-
emojiOpen.value = false
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
function rememberSelection() {
|
|
530
|
-
const ed = editor.value
|
|
531
|
-
if (!ed) return
|
|
532
|
-
const { from, to } = ed.state.selection
|
|
533
|
-
savedSelection = { from, to }
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function syncColorModels() {
|
|
537
|
-
const ed = editor.value
|
|
538
|
-
if (!ed) return
|
|
539
|
-
const fg = ed.getAttributes('textStyle').color as string | undefined
|
|
540
|
-
textColor.value = fg?.trim() || '#111827'
|
|
541
|
-
const bg = ed.getAttributes('highlight').color as string | undefined
|
|
542
|
-
highlightColor.value = bg?.trim() || '#fef08a'
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
function withSavedSelection(run: (chain: {
|
|
546
|
-
unsetColor: () => { run: () => boolean }
|
|
547
|
-
setColor: (c: string) => { run: () => boolean }
|
|
548
|
-
unsetHighlight: () => { run: () => boolean }
|
|
549
|
-
setHighlight: (attrs: { color: string }) => { run: () => boolean }
|
|
550
|
-
}) => void) {
|
|
551
|
-
const ed = editor.value
|
|
552
|
-
if (!ed) return
|
|
553
|
-
const chain = ed.chain().focus()
|
|
554
|
-
if (savedSelection) {
|
|
555
|
-
chain.setTextSelection(savedSelection)
|
|
556
|
-
}
|
|
557
|
-
run(chain as never)
|
|
558
|
-
savedSelection = {
|
|
559
|
-
from: ed.state.selection.from,
|
|
560
|
-
to: ed.state.selection.to,
|
|
561
|
-
}
|
|
562
|
-
syncColorModels()
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
function applyTextColor(v: string) {
|
|
566
|
-
const color = String(v || '').trim()
|
|
567
|
-
withSavedSelection((chain) => {
|
|
568
|
-
if (!color) {
|
|
569
|
-
chain.unsetColor().run()
|
|
570
|
-
return
|
|
571
|
-
}
|
|
572
|
-
chain.setColor(color).run()
|
|
573
|
-
})
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function applyHighlightColor(v: string) {
|
|
577
|
-
const color = String(v || '').trim()
|
|
578
|
-
withSavedSelection((chain) => {
|
|
579
|
-
if (!color) {
|
|
580
|
-
chain.unsetHighlight().run()
|
|
581
|
-
return
|
|
582
|
-
}
|
|
583
|
-
chain.setHighlight({ color }).run()
|
|
584
|
-
})
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
function insertTable() {
|
|
588
|
-
editor.value
|
|
589
|
-
?.chain()
|
|
590
|
-
.focus()
|
|
591
|
-
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
|
592
|
-
.run()
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
function toggleLink() {
|
|
596
|
-
const ed = editor.value
|
|
597
|
-
if (!ed) return
|
|
598
|
-
const prev = ed.getAttributes('link').href as string | undefined
|
|
599
|
-
if (ed.isActive('link')) {
|
|
600
|
-
ed.chain().focus().unsetLink().run()
|
|
601
|
-
return
|
|
602
|
-
}
|
|
603
|
-
const url = window.prompt(crudLabel('linkUrlPrompt'), prev || 'https://')
|
|
604
|
-
if (url == null) return
|
|
605
|
-
const href = url.trim()
|
|
606
|
-
if (!href) {
|
|
607
|
-
ed.chain().focus().unsetLink().run()
|
|
608
|
-
return
|
|
609
|
-
}
|
|
610
|
-
ed.chain().focus().extendMarkRange('link').setLink({ href, target: '_blank', rel: 'noopener noreferrer' }).run()
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
function handleMediaPaste(event: ClipboardEvent) {
|
|
614
|
-
if (!uploadEnabled.value) return false
|
|
615
|
-
const items = Array.from(event.clipboardData?.items || [])
|
|
616
|
-
const imageItem = items.find((item) => item.type.startsWith('image/'))
|
|
617
|
-
if (imageItem) {
|
|
618
|
-
const file = imageItem.getAsFile()
|
|
619
|
-
if (!file) return false
|
|
620
|
-
event.preventDefault()
|
|
621
|
-
void insertImage(file)
|
|
622
|
-
return true
|
|
623
|
-
}
|
|
624
|
-
const videoItem = items.find((item) => item.type.startsWith('video/'))
|
|
625
|
-
if (videoItem) {
|
|
626
|
-
const file = videoItem.getAsFile()
|
|
627
|
-
if (!file) return false
|
|
628
|
-
event.preventDefault()
|
|
629
|
-
void insertVideo(file)
|
|
630
|
-
return true
|
|
631
|
-
}
|
|
632
|
-
return false
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function handleMediaDrop(event: DragEvent, moved: boolean) {
|
|
636
|
-
if (moved || !uploadEnabled.value) return false
|
|
637
|
-
const files = Array.from(event.dataTransfer?.files || [])
|
|
638
|
-
const image = files.find((f) => f.type.startsWith('image/'))
|
|
639
|
-
if (image) {
|
|
640
|
-
event.preventDefault()
|
|
641
|
-
void insertImage(image)
|
|
642
|
-
return true
|
|
643
|
-
}
|
|
644
|
-
const video = files.find((f) => f.type.startsWith('video/'))
|
|
645
|
-
if (video) {
|
|
646
|
-
event.preventDefault()
|
|
647
|
-
void insertVideo(video)
|
|
648
|
-
return true
|
|
649
|
-
}
|
|
650
|
-
return false
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
const editor = useEditor({
|
|
654
|
-
content: normalizeHtml(String(props.modelValue || '')),
|
|
655
|
-
editable: !props.disabled,
|
|
656
|
-
extensions: [
|
|
657
|
-
StarterKit.configure({ heading: { levels: [2, 3] } }),
|
|
658
|
-
Underline,
|
|
659
|
-
TextStyle,
|
|
660
|
-
Color,
|
|
661
|
-
Highlight.configure({ multicolor: true }),
|
|
662
|
-
TextAlign.configure({ types: ['heading', 'paragraph'] }),
|
|
663
|
-
TaskList,
|
|
664
|
-
TaskItem.configure({ nested: true }),
|
|
665
|
-
Link.configure({
|
|
666
|
-
openOnClick: false,
|
|
667
|
-
HTMLAttributes: { rel: 'noopener noreferrer', target: '_blank' },
|
|
668
|
-
}),
|
|
669
|
-
Image.configure({ allowBase64: false }),
|
|
670
|
-
VmRichtextVideo,
|
|
671
|
-
Table.configure({ resizable: true }),
|
|
672
|
-
TableRow,
|
|
673
|
-
TableHeader,
|
|
674
|
-
TableCell,
|
|
675
|
-
Placeholder.configure({
|
|
676
|
-
placeholder: props.placeholder ?? crudLabel('richtextPlaceholder'),
|
|
677
|
-
}),
|
|
678
|
-
],
|
|
679
|
-
editorProps: {
|
|
680
|
-
attributes: { class: 'vm-richtext__prose' },
|
|
681
|
-
handlePaste: (_view, event) => handleMediaPaste(event),
|
|
682
|
-
handleDrop: (_view, event, _slice, moved) => handleMediaDrop(event, moved),
|
|
683
|
-
handleDOMEvents: {
|
|
684
|
-
mousedown: (view, event) => onTableMouseDown(view, event),
|
|
685
|
-
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
|
+
},
|
|
686
134
|
},
|
|
687
135
|
},
|
|
688
|
-
|
|
689
|
-
if (syncing.value) return
|
|
690
|
-
emit('update:modelValue', normalizeHtml(ed.getHTML()))
|
|
691
|
-
},
|
|
692
|
-
onSelectionUpdate: () => {
|
|
693
|
-
syncColorModels()
|
|
694
|
-
},
|
|
695
|
-
onTransaction: () => {
|
|
696
|
-
syncColorModels()
|
|
697
|
-
},
|
|
698
|
-
})
|
|
136
|
+
}))
|
|
699
137
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
if (current === html) return
|
|
706
|
-
syncing.value = true
|
|
707
|
-
ed.commands.setContent(html || '<p></p>', false)
|
|
708
|
-
syncing.value = false
|
|
709
|
-
})
|
|
138
|
+
function handleCreated(editor: IDomEditor) {
|
|
139
|
+
editorRef.value = editor
|
|
140
|
+
if (props.disabled) editor.disable()
|
|
141
|
+
else editor.enable()
|
|
142
|
+
}
|
|
710
143
|
|
|
711
|
-
watch(
|
|
712
|
-
|
|
144
|
+
watch(html, (v) => {
|
|
145
|
+
if (syncing.value || readOnlyMode.value) return
|
|
146
|
+
emit('update:modelValue', normalizeHtml(String(v || '')))
|
|
713
147
|
})
|
|
714
148
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
+
)
|
|
722
161
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
+
)
|
|
726
171
|
|
|
727
|
-
onMounted(() => {
|
|
728
|
-
document.addEventListener('pointerdown', onDocPointer, true)
|
|
729
|
-
window.addEventListener('resize', onWinReposition)
|
|
730
|
-
window.addEventListener('scroll', onWinReposition, true)
|
|
731
|
-
})
|
|
732
172
|
onBeforeUnmount(() => {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
173
|
+
const ed = editorRef.value
|
|
174
|
+
if (!ed) return
|
|
175
|
+
ed.destroy()
|
|
176
|
+
editorRef.value = undefined
|
|
737
177
|
})
|
|
738
178
|
</script>
|
|
739
179
|
|
|
@@ -741,7 +181,6 @@ onBeforeUnmount(() => {
|
|
|
741
181
|
.vm-richtext {
|
|
742
182
|
width: 100%;
|
|
743
183
|
min-width: 0;
|
|
744
|
-
box-sizing: border-box;
|
|
745
184
|
}
|
|
746
185
|
|
|
747
186
|
.vm-richtext.is-disabled {
|
|
@@ -752,121 +191,21 @@ onBeforeUnmount(() => {
|
|
|
752
191
|
.vm-richtext__shell {
|
|
753
192
|
display: flex;
|
|
754
193
|
flex-direction: column;
|
|
755
|
-
width: 100%;
|
|
756
|
-
min-width: 0;
|
|
757
|
-
border: 1px solid var(--input);
|
|
758
|
-
border-radius: var(--vm-control-radius);
|
|
759
|
-
background: var(--card);
|
|
760
194
|
overflow: hidden;
|
|
195
|
+
border: 1px solid var(--border);
|
|
196
|
+
border-radius: var(--radius, 8px);
|
|
197
|
+
background: var(--card);
|
|
761
198
|
}
|
|
762
199
|
|
|
763
200
|
.vm-richtext__toolbar {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
align-items: center;
|
|
767
|
-
gap: 2px;
|
|
768
|
-
padding: 6px 8px;
|
|
769
|
-
border-bottom: 1px solid var(--border);
|
|
770
|
-
background: var(--muted);
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
.vm-richtext__btn {
|
|
774
|
-
display: inline-flex;
|
|
775
|
-
align-items: center;
|
|
776
|
-
justify-content: center;
|
|
777
|
-
min-width: 28px;
|
|
778
|
-
height: 28px;
|
|
779
|
-
padding: 0 6px;
|
|
780
|
-
border: none;
|
|
781
|
-
border-radius: 6px;
|
|
782
|
-
background: transparent;
|
|
783
|
-
color: var(--foreground);
|
|
784
|
-
font-size: 12px;
|
|
785
|
-
font-weight: 600;
|
|
786
|
-
line-height: 1;
|
|
787
|
-
cursor: pointer;
|
|
788
|
-
transition: background 0.15s ease, color 0.15s ease;
|
|
789
|
-
|
|
790
|
-
i { font-size: 15px; line-height: 1; }
|
|
791
|
-
|
|
792
|
-
&:hover:not(:disabled) {
|
|
793
|
-
background: color-mix(in srgb, var(--brand) 10%, transparent);
|
|
794
|
-
color: var(--brand);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
&.is-active {
|
|
798
|
-
background: color-mix(in srgb, var(--brand) 16%, transparent);
|
|
799
|
-
color: var(--brand);
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
&:disabled { opacity: 0.35; cursor: not-allowed; }
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
/** 保留原图标 + 底部色条;透明覆盖层承接 vm-color-picker 点击 */
|
|
806
|
-
.vm-richtext__color-btn {
|
|
807
|
-
position: relative;
|
|
808
|
-
display: inline-flex;
|
|
809
|
-
flex-direction: column;
|
|
810
|
-
align-items: center;
|
|
811
|
-
justify-content: center;
|
|
812
|
-
gap: 1px;
|
|
813
|
-
padding: 0 5px 3px;
|
|
814
|
-
|
|
815
|
-
&.is-disabled {
|
|
816
|
-
opacity: 0.35;
|
|
817
|
-
pointer-events: none;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
i {
|
|
821
|
-
position: relative;
|
|
822
|
-
z-index: 1;
|
|
823
|
-
pointer-events: none;
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
.vm-richtext__color-bar {
|
|
828
|
-
position: relative;
|
|
829
|
-
z-index: 1;
|
|
830
|
-
display: block;
|
|
831
|
-
width: 14px;
|
|
832
|
-
height: 3px;
|
|
833
|
-
border-radius: 1px;
|
|
834
|
-
pointer-events: none;
|
|
835
|
-
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--border) 70%, transparent);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
.vm-richtext__color-hit {
|
|
839
|
-
position: absolute !important;
|
|
840
|
-
inset: 0;
|
|
841
|
-
z-index: 2;
|
|
842
|
-
width: auto !important;
|
|
843
|
-
opacity: 0;
|
|
844
|
-
|
|
845
|
-
:deep(.vm-color-picker__swatch) {
|
|
846
|
-
width: 100%;
|
|
847
|
-
height: 100%;
|
|
848
|
-
border: none;
|
|
849
|
-
border-radius: 6px;
|
|
850
|
-
background: transparent;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
:deep(.vm-color-picker__swatch-fill) {
|
|
854
|
-
display: none;
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
.vm-richtext__sep {
|
|
859
|
-
width: 1px;
|
|
860
|
-
height: 18px;
|
|
861
|
-
margin: 0 4px;
|
|
862
|
-
background: var(--border);
|
|
201
|
+
border-bottom: 1px solid var(--border) !important;
|
|
202
|
+
background: var(--muted) !important;
|
|
863
203
|
}
|
|
864
204
|
|
|
865
|
-
.vm-
|
|
866
|
-
height: var(--vm-richtext-height, 320px) !important;
|
|
867
|
-
min-height: var(--vm-richtext-height, 320px) !important;
|
|
205
|
+
.vm-richtext__editor {
|
|
868
206
|
flex-shrink: 0;
|
|
869
|
-
|
|
207
|
+
height: var(--vm-richtext-height, 320px) !important;
|
|
208
|
+
overflow-y: hidden;
|
|
870
209
|
background: var(--card);
|
|
871
210
|
}
|
|
872
211
|
|
|
@@ -878,7 +217,7 @@ onBeforeUnmount(() => {
|
|
|
878
217
|
word-break: break-word;
|
|
879
218
|
|
|
880
219
|
:deep(p) { margin: 0.5em 0; }
|
|
881
|
-
|
|
220
|
+
:deep(strong), :deep(b) { font-weight: 800; }
|
|
882
221
|
:deep(ul) {
|
|
883
222
|
margin: 0.5em 0;
|
|
884
223
|
padding-left: 1.5em;
|
|
@@ -896,16 +235,21 @@ onBeforeUnmount(() => {
|
|
|
896
235
|
:deep(ol ol) { list-style-type: lower-alpha; }
|
|
897
236
|
:deep(ol ol ol) { list-style-type: lower-roman; }
|
|
898
237
|
:deep(li) { display: list-item; }
|
|
899
|
-
:deep(ul[data-type='taskList']) {
|
|
900
|
-
list-style: none;
|
|
901
|
-
padding-left: 0.25em;
|
|
902
|
-
}
|
|
903
238
|
:deep(a) { color: var(--brand); text-decoration: underline; }
|
|
904
239
|
:deep(img), :deep(video) { max-width: 100%; height: auto; margin: 8px 0; border-radius: 8px; }
|
|
905
240
|
:deep(table) { width: 100%; border-collapse: collapse; margin: 8px 0; }
|
|
906
241
|
:deep(th), :deep(td) { border: 1px solid var(--border); padding: 6px 8px; }
|
|
907
|
-
:deep(blockquote) {
|
|
908
|
-
|
|
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
|
+
}
|
|
909
253
|
}
|
|
910
254
|
|
|
911
255
|
.vm-richtext.is-readonly .vm-richtext__html {
|
|
@@ -913,138 +257,40 @@ onBeforeUnmount(() => {
|
|
|
913
257
|
border: none;
|
|
914
258
|
background: transparent;
|
|
915
259
|
}
|
|
916
|
-
</style>
|
|
917
260
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
box-sizing: border-box;
|
|
922
|
-
display: grid;
|
|
923
|
-
grid-template-columns: repeat(10, 28px);
|
|
924
|
-
gap: 2px;
|
|
925
|
-
width: 314px;
|
|
926
|
-
max-height: 280px;
|
|
927
|
-
overflow-x: hidden;
|
|
928
|
-
overflow-y: auto;
|
|
929
|
-
overscroll-behavior: contain;
|
|
930
|
-
padding: 8px;
|
|
931
|
-
border: 1px solid var(--border);
|
|
932
|
-
border-radius: 10px;
|
|
933
|
-
background: var(--card);
|
|
934
|
-
box-shadow: var(--shadow-soft, 0 8px 24px rgba(0, 0, 0, 0.12));
|
|
935
|
-
scrollbar-width: thin;
|
|
936
|
-
pointer-events: auto;
|
|
261
|
+
:deep(.w-e-toolbar) {
|
|
262
|
+
border: none !important;
|
|
263
|
+
background: transparent !important;
|
|
937
264
|
}
|
|
938
265
|
|
|
939
|
-
.
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
width: 28px;
|
|
944
|
-
height: 28px;
|
|
945
|
-
border: none;
|
|
946
|
-
border-radius: 6px;
|
|
947
|
-
background: transparent;
|
|
948
|
-
font-size: 18px;
|
|
949
|
-
line-height: 1;
|
|
950
|
-
cursor: pointer;
|
|
266
|
+
:deep(.w-e-text-container) {
|
|
267
|
+
border: none !important;
|
|
268
|
+
background: var(--card) !important;
|
|
269
|
+
}
|
|
951
270
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
}
|
|
271
|
+
:deep(.w-e-scroll) {
|
|
272
|
+
height: var(--vm-richtext-height, 320px) !important;
|
|
955
273
|
}
|
|
956
274
|
|
|
957
|
-
.
|
|
958
|
-
min-height:
|
|
959
|
-
padding: 12px 14px;
|
|
960
|
-
outline: none;
|
|
275
|
+
:deep(.w-e-text-container [data-slate-editor]) {
|
|
276
|
+
min-height: calc(var(--vm-richtext-height, 320px) - 16px) !important;
|
|
961
277
|
color: var(--foreground);
|
|
962
278
|
font-size: 14px;
|
|
963
279
|
line-height: 1.65;
|
|
964
280
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
content: attr(data-placeholder);
|
|
969
|
-
float: left;
|
|
970
|
-
height: 0;
|
|
971
|
-
pointer-events: none;
|
|
972
|
-
color: var(--muted-foreground);
|
|
973
|
-
}
|
|
281
|
+
strong, b { font-weight: 800; }
|
|
282
|
+
}
|
|
283
|
+
</style>
|
|
974
284
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
ol {
|
|
986
|
-
margin: 0.45em 0;
|
|
987
|
-
padding-left: 1.5em;
|
|
988
|
-
list-style-type: decimal;
|
|
989
|
-
list-style-position: outside;
|
|
990
|
-
}
|
|
991
|
-
ol ol { list-style-type: lower-alpha; }
|
|
992
|
-
ol ol ol { list-style-type: lower-roman; }
|
|
993
|
-
li {
|
|
994
|
-
display: list-item;
|
|
995
|
-
}
|
|
996
|
-
ul[data-type='taskList'] {
|
|
997
|
-
list-style: none;
|
|
998
|
-
padding-left: 0.25em;
|
|
999
|
-
}
|
|
1000
|
-
ul[data-type='taskList'] li {
|
|
1001
|
-
display: flex;
|
|
1002
|
-
gap: 8px;
|
|
1003
|
-
align-items: flex-start;
|
|
1004
|
-
}
|
|
1005
|
-
ul[data-type='taskList'] li label { margin-top: 2px; }
|
|
1006
|
-
blockquote { margin: 0.45em 0; padding-left: 12px; border-left: 3px solid var(--border); color: var(--muted-foreground); }
|
|
1007
|
-
a { color: var(--brand); text-decoration: underline; cursor: pointer; }
|
|
1008
|
-
img, video { display: block; max-width: 100%; height: auto; margin: 8px 0; border-radius: 8px; }
|
|
1009
|
-
mark {
|
|
1010
|
-
border-radius: 2px;
|
|
1011
|
-
padding: 0 2px;
|
|
1012
|
-
}
|
|
1013
|
-
mark:not([style*='background']) {
|
|
1014
|
-
background: color-mix(in srgb, var(--brand) 22%, transparent);
|
|
1015
|
-
}
|
|
1016
|
-
code { padding: 0.1em 0.35em; border-radius: 4px; background: var(--muted); font-size: 0.92em; }
|
|
1017
|
-
pre { margin: 0.5em 0; padding: 10px 12px; border-radius: 8px; background: var(--muted); overflow-x: auto;
|
|
1018
|
-
code { padding: 0; background: transparent; }
|
|
1019
|
-
}
|
|
1020
|
-
table { width: 100%; border-collapse: collapse; margin: 8px 0; table-layout: fixed; }
|
|
1021
|
-
th, td {
|
|
1022
|
-
position: relative;
|
|
1023
|
-
min-width: 48px;
|
|
1024
|
-
padding: 6px 8px;
|
|
1025
|
-
border: 1px solid var(--border);
|
|
1026
|
-
vertical-align: top;
|
|
1027
|
-
}
|
|
1028
|
-
th { background: var(--muted); font-weight: 600; }
|
|
1029
|
-
.selectedCell {
|
|
1030
|
-
background: color-mix(in srgb, var(--brand) 16%, transparent);
|
|
1031
|
-
}
|
|
1032
|
-
.selectedCell::after {
|
|
1033
|
-
content: '';
|
|
1034
|
-
position: absolute;
|
|
1035
|
-
inset: 0;
|
|
1036
|
-
pointer-events: none;
|
|
1037
|
-
background: color-mix(in srgb, var(--brand) 10%, transparent);
|
|
1038
|
-
}
|
|
1039
|
-
.column-resize-handle {
|
|
1040
|
-
position: absolute;
|
|
1041
|
-
top: 0;
|
|
1042
|
-
right: -2px;
|
|
1043
|
-
bottom: 0;
|
|
1044
|
-
width: 4px;
|
|
1045
|
-
background: var(--brand);
|
|
1046
|
-
pointer-events: none;
|
|
1047
|
-
}
|
|
1048
|
-
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;
|
|
1049
295
|
}
|
|
1050
296
|
</style>
|