提交 | 用户 | age
|
c0e4c9
|
1 |
<template> |
3ad1a4
|
2 |
<div :class="prefixCls" :style="{ width: containerWidth }"> |
128809
|
3 |
<textarea |
H |
4 |
:id="tinymceId" |
|
5 |
ref="elRef" |
|
6 |
:style="{ visibility: 'hidden' }" |
|
7 |
v-if="!initOptions.inline" |
|
8 |
></textarea> |
8e0137
|
9 |
<slot v-else></slot> |
170f4d
|
10 |
<div class="p-2 tox-statusbar"> |
db42d0
|
11 |
<ImgUpload |
H |
12 |
ref="fileRef" |
|
13 |
:fullscreen="fullscreen" |
|
14 |
:fileListTemp="fileListTemp" |
|
15 |
@uploading="handleImageUploading" |
|
16 |
@done="handleDone" |
|
17 |
@fileListChange="fileListChange" |
|
18 |
v-if="isImg" |
|
19 |
v-show="editorRef" |
|
20 |
:title="'附件'" |
|
21 |
:type="'file'" |
|
22 |
:disabled="disabled" |
|
23 |
/> |
|
24 |
<div class="my-upload-list"> |
128809
|
25 |
<div style="display: flex"> |
H |
26 |
<ImgUpload |
|
27 |
:fullscreen="fullscreen" |
|
28 |
@uploading="handleImageUploading" |
|
29 |
@done="handleDone" |
67287b
|
30 |
v-if="isImg" |
128809
|
31 |
v-show="editorRef" |
H |
32 |
:title="'图片'" |
db42d0
|
33 |
:type="'png'" |
128809
|
34 |
:disabled="disabled" |
H |
35 |
:accept="'.jpg,.jpeg,.gif,.png,.webp'" |
|
36 |
/> |
67287b
|
37 |
<a-button v-if="isText" type="text" size="small"> |
128809
|
38 |
<SnippetsOutlined /> |
H |
39 |
快速文本</a-button |
|
40 |
> |
|
41 |
</div> |
|
42 |
</div> |
170f4d
|
43 |
</div> |
c0e4c9
|
44 |
</div> |
J |
45 |
</template> |
|
46 |
|
bab28a
|
47 |
<script lang="ts" setup> |
128809
|
48 |
import type { Editor, RawEditorSettings } from 'tinymce'; |
H |
49 |
import { PaperClipOutlined, UploadOutlined, SnippetsOutlined } from '@ant-design/icons-vue'; |
db42d0
|
50 |
import { useGlobSetting } from '@/hooks/setting'; |
H |
51 |
|
128809
|
52 |
import tinymce from 'tinymce/tinymce'; |
H |
53 |
import 'tinymce/themes/silver'; |
|
54 |
import 'tinymce/icons/default/icons'; |
|
55 |
import 'tinymce/plugins/advlist'; |
|
56 |
import 'tinymce/plugins/anchor'; |
|
57 |
import 'tinymce/plugins/autolink'; |
|
58 |
import 'tinymce/plugins/autosave'; |
|
59 |
import 'tinymce/plugins/code'; |
|
60 |
import 'tinymce/plugins/codesample'; |
|
61 |
import 'tinymce/plugins/directionality'; |
|
62 |
import 'tinymce/plugins/fullscreen'; |
|
63 |
import 'tinymce/plugins/hr'; |
|
64 |
import 'tinymce/plugins/insertdatetime'; |
|
65 |
import 'tinymce/plugins/link'; |
|
66 |
import 'tinymce/plugins/lists'; |
|
67 |
import 'tinymce/plugins/media'; |
|
68 |
import 'tinymce/plugins/nonbreaking'; |
|
69 |
import 'tinymce/plugins/noneditable'; |
|
70 |
import 'tinymce/plugins/pagebreak'; |
|
71 |
import 'tinymce/plugins/paste'; |
|
72 |
import 'tinymce/plugins/preview'; |
|
73 |
import 'tinymce/plugins/print'; |
|
74 |
import 'tinymce/plugins/save'; |
|
75 |
import 'tinymce/plugins/searchreplace'; |
|
76 |
import 'tinymce/plugins/spellchecker'; |
|
77 |
import 'tinymce/plugins/tabfocus'; |
|
78 |
// import 'tinymce/plugins/table'; |
|
79 |
import 'tinymce/plugins/template'; |
|
80 |
import 'tinymce/plugins/textpattern'; |
|
81 |
import 'tinymce/plugins/visualblocks'; |
|
82 |
import 'tinymce/plugins/visualchars'; |
|
83 |
import 'tinymce/plugins/wordcount'; |
a81268
|
84 |
|
128809
|
85 |
import 'tinymce/plugins/image'; |
H |
86 |
import 'tinymce/plugins/table'; |
|
87 |
import 'tinymce/plugins/charmap'; |
|
88 |
import 'tinymce/plugins/imagetools'; |
|
89 |
import 'tinymce/plugins/help'; |
|
90 |
import 'tinymce/plugins/emoticons'; |
|
91 |
import 'tinymce/plugins/emoticons/js/emojis'; |
|
92 |
// import 'tinymce/plugins/bdmap'; |
|
93 |
// import 'tinymce/plugins/indent2em'; |
|
94 |
import 'tinymce/plugins/autoresize'; |
|
95 |
// import 'tinymce/plugins/formatpainter'; |
|
96 |
// import 'tinymce/plugins/axupimgs'; |
28c484
|
97 |
|
128809
|
98 |
// import 'tinymce/plugins/powerpaste'; |
H |
99 |
// import 'tinymce/plugins/casechange'; |
|
100 |
import 'tinymce/plugins/importcss'; |
|
101 |
// import 'tinymce/plugins/tinyddrive'; |
|
102 |
// import 'tinymce/plugins/advcode'; |
|
103 |
// import 'tinymce/plugins/mediaembed'; |
|
104 |
import 'tinymce/plugins/toc'; |
|
105 |
// import 'tinymce/plugins/checklist'; |
|
106 |
// import 'tinymce/plugins/tinycespellchecker'; |
|
107 |
// import 'tinymce/plugins/a11ychecker'; |
|
108 |
// import 'tinymce/plugins/permanentpen'; |
|
109 |
// import 'tinymce/plugins/pageembed'; |
|
110 |
// import 'tinymce/plugins/tinycomments'; |
|
111 |
// import 'tinymce/plugins/mentions'; |
|
112 |
import 'tinymce/plugins/quickbars'; |
|
113 |
// import 'tinymce/plugins/linkchecker'; |
|
114 |
// import 'tinymce/plugins/advtable'; |
|
115 |
// import 'tinymce/plugins/export'; |
28c484
|
116 |
|
128809
|
117 |
import { |
H |
118 |
computed, |
|
119 |
nextTick, |
|
120 |
ref, |
|
121 |
unref, |
|
122 |
watch, |
|
123 |
onDeactivated, |
|
124 |
onBeforeUnmount, |
|
125 |
PropType, |
|
126 |
useAttrs, |
|
127 |
} from 'vue'; |
db42d0
|
128 |
import ImgUpload from '@/components/MyUpload/index.vue'; |
128809
|
129 |
import { |
H |
130 |
plugins as defaultPlugins, |
|
131 |
toolbar as defaultToolbar, |
|
132 |
toolbar_groups as defaultStyleFormats, |
|
133 |
} from './tinymce'; |
|
134 |
import { buildShortUUID } from '@/utils/uuid'; |
|
135 |
import { bindHandlers } from './helper'; |
|
136 |
import { onMountedOrActivated } from '@vben/hooks'; |
|
137 |
import { useDesign } from '@/hooks/web/useDesign'; |
|
138 |
import { isNumber } from '@/utils/is'; |
|
139 |
import { useLocale } from '@/locales/useLocale'; |
|
140 |
import { useAppStore } from '@/store/modules/app'; |
db42d0
|
141 |
const { uploadUrl } = useGlobSetting(); |
H |
142 |
// console.log(uploadUrl,'uploadUrl'); |
f75425
|
143 |
|
128809
|
144 |
defineOptions({ name: 'Tinymce', inheritAttrs: false }); |
bab28a
|
145 |
|
128809
|
146 |
const props = defineProps({ |
H |
147 |
options: { |
|
148 |
type: Object as PropType<Partial<RawEditorSettings>>, |
|
149 |
default: () => ({}), |
39d629
|
150 |
}, |
128809
|
151 |
value: { |
H |
152 |
type: String, |
|
153 |
}, |
39d629
|
154 |
|
128809
|
155 |
toolbar: { |
H |
156 |
type: Array as PropType<string[]>, |
|
157 |
default: defaultToolbar, |
|
158 |
}, |
|
159 |
plugins: { |
|
160 |
type: Array as PropType<string[]>, |
|
161 |
default: defaultPlugins, |
|
162 |
}, |
|
163 |
toolbar_groups: { |
|
164 |
type: Object as PropType<{}>, |
|
165 |
default: defaultStyleFormats, |
|
166 |
}, |
|
167 |
modelValue: { |
|
168 |
type: String, |
|
169 |
}, |
|
170 |
height: { |
|
171 |
type: [Number, String] as PropType<string | number>, |
|
172 |
required: false, |
|
173 |
default: 400, |
|
174 |
}, |
|
175 |
width: { |
|
176 |
type: [Number, String] as PropType<string | number>, |
|
177 |
required: false, |
|
178 |
default: 'auto', |
|
179 |
}, |
|
180 |
showImageUpload: { |
|
181 |
type: Boolean, |
|
182 |
default: true, |
|
183 |
}, |
|
184 |
fontsize: { |
|
185 |
type: String, |
67287b
|
186 |
}, |
H |
187 |
isElse: { |
|
188 |
type: Boolean, |
|
189 |
default: true, |
|
190 |
}, |
|
191 |
isText: { |
|
192 |
type: Boolean, |
|
193 |
default: true, |
|
194 |
}, |
|
195 |
isImg: { |
|
196 |
type: Boolean, |
|
197 |
default: true, |
128809
|
198 |
}, |
H |
199 |
}); |
|
200 |
|
|
201 |
const emit = defineEmits(['change', 'update:modelValue', 'inited', 'init-error']); |
|
202 |
|
|
203 |
const attrs = useAttrs(); |
|
204 |
const editorRef = ref<Editor | null>(null); |
|
205 |
const fullscreen = ref(false); |
|
206 |
const tinymceId = ref<string>(buildShortUUID('tiny-vue')); |
|
207 |
const elRef = ref<HTMLElement | null>(null); |
|
208 |
|
|
209 |
const { prefixCls } = useDesign('tinymce-container'); |
|
210 |
|
|
211 |
const appStore = useAppStore(); |
|
212 |
|
|
213 |
const containerWidth = computed(() => { |
|
214 |
const width = props.width; |
|
215 |
if (isNumber(width)) { |
|
216 |
return `${width}px`; |
|
217 |
} |
|
218 |
return width; |
|
219 |
}); |
|
220 |
|
|
221 |
const skinName = computed(() => { |
|
222 |
return appStore.getDarkMode === 'light' ? 'oxide' : 'oxide-dark'; |
|
223 |
}); |
|
224 |
|
|
225 |
const langName = computed(() => { |
|
226 |
const lang = useLocale().getLocale.value; |
|
227 |
return ['zh_CN', ''].includes(lang) ? lang : 'zh_CN'; |
|
228 |
}); |
|
229 |
|
|
230 |
const initOptions = computed((): RawEditorSettings => { |
|
231 |
const { height, options, toolbar, plugins, toolbar_groups } = props; |
|
232 |
|
|
233 |
const publicPath = import.meta.env.VITE_PUBLIC_PATH || '/'; |
|
234 |
return { |
|
235 |
selector: `#${unref(tinymceId)}`, |
|
236 |
height, |
|
237 |
min_height: 450, |
|
238 |
font_formats: |
|
239 |
'微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;Andale Mono=andale mono,times;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier;Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco;Times New Roman=times new roman,times;Trebuchet MS=trebuchet ms,geneva;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings,zapf dingbats', |
|
240 |
fontsize_formats: '10px 11px 12px 14px 16px 18px 24px 36px 48px 48px 56px 72px', |
|
241 |
image_advtab: true, |
|
242 |
importcss_append: true, // 允许样式生效 |
|
243 |
toolbar, |
|
244 |
toolbar_groups, |
|
245 |
toolbar_sticky: true, // 粘性工具栏(或停靠工具栏),在向下滚动网页直到不再可见编辑器时,将工具栏和菜单停靠在屏幕顶部 |
|
246 |
toolbar_mode: 'floating', //默认wrap不收缩工具栏,取值为floating或sliding时,将第一行放不下的工具栏按钮缩进抽屉(3个点的图标)里,scrolling则采用移动端的横线滚动方式。 |
|
247 |
// style_formats_autohide: true, |
|
248 |
menubar: false, |
|
249 |
branding: false, |
|
250 |
elementpath: false, |
|
251 |
// quickbars_selection_toolbar: 'bold italic | quicklink h2 h3 blockquote quickimage quicktable', |
|
252 |
plugins, |
|
253 |
language_url: publicPath + 'resource/tinymce/langs/' + langName.value + '.js', |
|
254 |
language: langName.value, |
|
255 |
default_link_target: '_blank', |
|
256 |
link_title: false, |
|
257 |
statusbar: false, |
|
258 |
object_resizing: false, |
|
259 |
auto_focus: true, //让编辑器加载完成后自动获得光标焦点 |
|
260 |
autosave_ask_before_unload: true, |
|
261 |
autosave_interval: '30s', |
|
262 |
skin: skinName.value, |
|
263 |
skin_url: publicPath + 'resource/tinymce/skins/ui/' + skinName.value, |
|
264 |
content_css: publicPath + 'resource/tinymce/skins/ui/' + skinName.value + '/content.min.css', |
|
265 |
...options, |
|
266 |
setup: (editor: Editor) => { |
|
267 |
editorRef.value = editor; |
|
268 |
editor.on('init', (e) => initSetup(e)); |
|
269 |
}, |
|
270 |
}; |
|
271 |
}); |
|
272 |
|
|
273 |
const disabled = computed(() => { |
|
274 |
const { options } = props; |
|
275 |
const getdDisabled = options && Reflect.get(options, 'readonly'); |
|
276 |
const editor = unref(editorRef); |
|
277 |
if (editor) { |
|
278 |
editor.setMode(getdDisabled ? 'readonly' : 'design'); |
|
279 |
} |
|
280 |
return getdDisabled ?? false; |
|
281 |
}); |
|
282 |
|
|
283 |
watch( |
|
284 |
() => attrs.disabled, |
|
285 |
() => { |
|
286 |
const editor = unref(editorRef); |
|
287 |
if (!editor) { |
|
288 |
return; |
|
289 |
} |
|
290 |
editor.setMode(attrs.disabled ? 'readonly' : 'design'); |
|
291 |
}, |
|
292 |
); |
|
293 |
|
|
294 |
onMountedOrActivated(() => { |
|
295 |
if (!initOptions.value.inline) { |
|
296 |
tinymceId.value = buildShortUUID('tiny-vue'); |
|
297 |
} |
|
298 |
nextTick(() => { |
|
299 |
setTimeout(() => { |
|
300 |
initEditor(); |
|
301 |
}, 30); |
|
302 |
}); |
|
303 |
}); |
|
304 |
|
|
305 |
onBeforeUnmount(() => { |
|
306 |
destory(); |
|
307 |
}); |
|
308 |
|
|
309 |
onDeactivated(() => { |
|
310 |
destory(); |
|
311 |
}); |
|
312 |
|
|
313 |
function destory() { |
|
314 |
if (tinymce !== null) { |
|
315 |
tinymce?.remove?.(unref(initOptions).selector!); |
|
316 |
} |
170f4d
|
317 |
} |
bab28a
|
318 |
|
128809
|
319 |
function initEditor() { |
H |
320 |
const el = unref(elRef); |
|
321 |
if (el) { |
|
322 |
el.style.visibility = ''; |
|
323 |
} |
|
324 |
tinymce |
|
325 |
.init(unref(initOptions)) |
|
326 |
.then((editor) => { |
|
327 |
emit('inited', editor); |
|
328 |
}) |
|
329 |
.catch((err) => { |
|
330 |
emit('init-error', err); |
|
331 |
}); |
|
332 |
} |
|
333 |
|
|
334 |
function initSetup(e) { |
bab28a
|
335 |
const editor = unref(editorRef); |
170f4d
|
336 |
if (!editor) { |
H |
337 |
return; |
bab28a
|
338 |
} |
128809
|
339 |
const value = props.modelValue || ''; |
170f4d
|
340 |
|
128809
|
341 |
editor.setContent(value); |
H |
342 |
bindModelHandlers(editor); |
|
343 |
bindHandlers(e, attrs, unref(editorRef)); |
170f4d
|
344 |
} |
H |
345 |
|
128809
|
346 |
function setValue(editor: Record<string, any>, val?: string, prevVal?: string) { |
H |
347 |
if ( |
|
348 |
editor && |
|
349 |
typeof val === 'string' && |
|
350 |
val !== prevVal && |
|
351 |
val !== editor.getContent({ format: attrs.outputFormat }) |
|
352 |
) { |
|
353 |
editor.setContent(val); |
|
354 |
} |
170f4d
|
355 |
} |
db42d0
|
356 |
const fileRef = ref(); |
170f4d
|
357 |
|
128809
|
358 |
function bindModelHandlers(editor: any) { |
H |
359 |
const modelEvents = attrs.modelEvents ? attrs.modelEvents : null; |
|
360 |
const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents; |
|
361 |
|
|
362 |
watch( |
|
363 |
() => props.modelValue, |
|
364 |
(val, prevVal) => { |
|
365 |
setValue(editor, val, prevVal); |
|
366 |
}, |
|
367 |
); |
|
368 |
|
|
369 |
watch( |
|
370 |
() => props.value, |
|
371 |
(val, prevVal) => { |
|
372 |
setValue(editor, val, prevVal); |
|
373 |
}, |
|
374 |
{ |
|
375 |
immediate: true, |
|
376 |
}, |
|
377 |
); |
|
378 |
|
|
379 |
editor.on(normalizedEvents ? normalizedEvents : 'change keyup undo redo', () => { |
|
380 |
const content = editor.getContent({ format: attrs.outputFormat }); |
|
381 |
emit('update:modelValue', content); |
|
382 |
const data = { |
|
383 |
content, |
67287b
|
384 |
fileUNID: fileListTemp.value, |
H |
385 |
}; |
128809
|
386 |
emit('change', data); |
170f4d
|
387 |
}); |
H |
388 |
|
128809
|
389 |
editor.on('FullscreenStateChanged', (e) => { |
H |
390 |
fullscreen.value = e.state; |
|
391 |
}); |
170f4d
|
392 |
} |
H |
393 |
|
128809
|
394 |
function handleImageUploading(name: string) { |
H |
395 |
const editor = unref(editorRef); |
|
396 |
if (!editor) { |
|
397 |
return; |
|
398 |
} |
|
399 |
editor.execCommand('mceInsertContent', false, getUploadingImgName(name)); |
|
400 |
const content = editor?.getContent() ?? ''; |
|
401 |
setValue(editor, content); |
170f4d
|
402 |
} |
H |
403 |
|
128809
|
404 |
function handleDone(name: string, url: string) { |
H |
405 |
const editor = unref(editorRef); |
|
406 |
if (!editor) { |
|
407 |
return; |
|
408 |
} |
|
409 |
const content = editor?.getContent() ?? ''; |
|
410 |
const val = content?.replace(getUploadingImgName(name), `<img src="${url}"/>`) ?? ''; |
|
411 |
setValue(editor, val); |
|
412 |
} |
bab28a
|
413 |
|
128809
|
414 |
function getUploadingImgName(name: string) { |
H |
415 |
return `[uploading:${name}]`; |
|
416 |
} |
bab28a
|
417 |
|
128809
|
418 |
// 附件 |
db42d0
|
419 |
const fileListTemp = ref([]); |
H |
420 |
function fileListChange(data) { |
|
421 |
fileListTemp.value = data; |
128809
|
422 |
} |
c0e4c9
|
423 |
</script> |
128809
|
424 |
<style lang="less" scope> |
H |
425 |
@prefix-cls: ~'@{namespace}-tinymce-container'; |
3ad1a4
|
426 |
|
128809
|
427 |
.@{prefix-cls} { |
H |
428 |
position: relative; |
|
429 |
line-height: normal; |
c0e4c9
|
430 |
|
128809
|
431 |
textarea { |
H |
432 |
visibility: hidden; |
|
433 |
z-index: -1; |
|
434 |
} |
c0e4c9
|
435 |
} |
170f4d
|
436 |
|
67287b
|
437 |
.my-upload-list { |
H |
438 |
// 过渡 |
|
439 |
position: absolute; |
|
440 |
left: 78px; |
|
441 |
transition: all 0.3s; |
|
442 |
} |
|
443 |
|
128809
|
444 |
.tox-statusbar { |
H |
445 |
display: flex; |
|
446 |
// position: absolute; |
67287b
|
447 |
min-height: 40px; |
128809
|
448 |
border-bottom-right-radius: 8px; |
H |
449 |
border-bottom-left-radius: 8px; |
|
450 |
background: #f0f2f5; |
|
451 |
} |
170f4d
|
452 |
|
128809
|
453 |
.icon-text { |
H |
454 |
margin-right: 10px; |
|
455 |
font-size: 14px; |
|
456 |
} |
|
457 |
|
|
458 |
.ant-upload-list-picture { |
|
459 |
display: flex; |
00fe0e
|
460 |
flex-wrap: wrap; |
128809
|
461 |
} |
H |
462 |
|
|
463 |
.ant-upload-list-picture-card { |
|
464 |
display: flex; |
|
465 |
justify-content: space-between; |
|
466 |
width: 24vw; |
|
467 |
margin-right: 10px; |
|
468 |
padding: 5px 10px; |
|
469 |
background-color: #edf3f9; |
|
470 |
|
|
471 |
a { |
|
472 |
padding: 0 5px; |
|
473 |
font-size: 12px; |
|
474 |
} |
|
475 |
} |
c0e4c9
|
476 |
</style> |