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.
- 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 +128 -924
- 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
|
|
|
@@ -188,7 +66,7 @@ const props = withDefaults(
|
|
|
188
66
|
{
|
|
189
67
|
readonly: false,
|
|
190
68
|
placeholder: undefined,
|
|
191
|
-
height:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
125
|
+
const toolbarConfig = computed<Partial<IToolbarConfig>>(() => ({
|
|
126
|
+
excludeKeys: uploadEnabled.value ? [] : ['uploadImage', 'uploadVideo'],
|
|
127
|
+
}))
|
|
467
128
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
147
|
+
function handleCreated(editor: IDomEditor) {
|
|
148
|
+
editorRef.value = editor
|
|
149
|
+
if (props.disabled) editor.disable()
|
|
150
|
+
else editor.enable()
|
|
151
|
+
}
|
|
734
152
|
|
|
735
|
-
watch(
|
|
736
|
-
|
|
153
|
+
watch(html, (v) => {
|
|
154
|
+
if (syncing.value || readOnlyMode.value) return
|
|
155
|
+
emit('update:modelValue', normalizeHtml(String(v || '')))
|
|
737
156
|
})
|
|
738
157
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
-
|
|
748
|
-
|
|
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
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
780
|
-
|
|
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:
|
|
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
|
-
|
|
845
|
-
.vm-richtext__color-btn {
|
|
210
|
+
.vm-richtext__toolbar {
|
|
846
211
|
position: relative;
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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-
|
|
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
|
-
|
|
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) {
|
|
960
|
-
|
|
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
|
-
|
|
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;
|
|
277
|
+
:deep(.w-e-toolbar) {
|
|
278
|
+
border: none !important;
|
|
279
|
+
background: transparent !important;
|
|
989
280
|
}
|
|
990
281
|
|
|
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;
|
|
282
|
+
:deep(.w-e-text-container) {
|
|
283
|
+
border: none !important;
|
|
284
|
+
background: var(--card) !important;
|
|
285
|
+
}
|
|
1003
286
|
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
287
|
+
:deep(.w-e-scroll) {
|
|
288
|
+
height: var(--vm-richtext-height, 400px) !important;
|
|
289
|
+
min-height: 400px !important;
|
|
1007
290
|
}
|
|
1008
291
|
|
|
1009
|
-
.
|
|
1010
|
-
min-height:
|
|
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
|
-
|
|
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); }
|
|
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>
|