2026-08-17 12:25:42 +08:00
|
|
|
<template>
|
|
|
|
|
<div class="rich-editor" :style="{ zIndex: 3000 }">
|
|
|
|
|
<Toolbar
|
|
|
|
|
class="rich-editor__toolbar"
|
|
|
|
|
:editor="editorRef"
|
|
|
|
|
:defaultConfig="toolbarConfig"
|
|
|
|
|
:mode="mode"
|
|
|
|
|
/>
|
|
|
|
|
<Editor
|
|
|
|
|
class="rich-editor__body"
|
|
|
|
|
:style="{ height: height, overflowY: 'hidden' }"
|
|
|
|
|
v-model="valueHtml"
|
|
|
|
|
:defaultConfig="editorConfig"
|
|
|
|
|
:mode="mode"
|
|
|
|
|
@onCreated="handleCreated"
|
|
|
|
|
@onChange="handleChange"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import '@wangeditor/editor/dist/css/style.css'
|
2026-09-21 11:17:59 +08:00
|
|
|
import { computed, onBeforeUnmount, ref, shallowRef, watch } from 'vue'
|
2026-08-17 12:25:42 +08:00
|
|
|
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
|
|
|
|
import { useAuthStore } from '@/stores/auth'
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
modelValue: { type: String, default: '' },
|
|
|
|
|
height: { type: String, default: '320px' },
|
|
|
|
|
placeholder: { type: String, default: '请输入内容…' },
|
|
|
|
|
mode: { type: String, default: 'default' }, // 'default' | 'simple'
|
2026-09-21 11:17:59 +08:00
|
|
|
// 是否开放表格菜单。综合大题题干需要富文本表格,故显式开启;
|
|
|
|
|
// 默认 false 保持资讯正文等既有调用方的工具栏不变(行为兼容)。
|
|
|
|
|
enableTable: { type: Boolean, default: false },
|
2026-08-17 12:25:42 +08:00
|
|
|
})
|
|
|
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
|
|
|
|
|
|
const authStore = useAuthStore()
|
|
|
|
|
|
|
|
|
|
// 编辑器实例:必须用 shallowRef
|
|
|
|
|
const editorRef = shallowRef()
|
|
|
|
|
// 内部 HTML 值
|
|
|
|
|
const valueHtml = ref(props.modelValue || '')
|
|
|
|
|
|
|
|
|
|
// 外部值变化时同步到编辑器(避免打开弹窗时内容不回填)
|
|
|
|
|
watch(
|
|
|
|
|
() => props.modelValue,
|
|
|
|
|
(val) => {
|
|
|
|
|
if (val !== valueHtml.value) {
|
|
|
|
|
valueHtml.value = val || ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
2026-09-21 11:17:59 +08:00
|
|
|
const toolbarConfig = computed(() => ({
|
2026-08-17 12:25:42 +08:00
|
|
|
excludeKeys: [
|
2026-09-21 11:17:59 +08:00
|
|
|
'group-video', // 题干/正文不需要视频
|
|
|
|
|
// 未开启表格时隐藏插入表格入口;开启后除去该项,表格相关菜单恢复默认可见
|
|
|
|
|
...(props.enableTable ? [] : ['insertTable']),
|
2026-08-17 12:25:42 +08:00
|
|
|
'codeBlock',
|
|
|
|
|
'fullScreen',
|
|
|
|
|
],
|
2026-09-21 11:17:59 +08:00
|
|
|
}))
|
2026-08-17 12:25:42 +08:00
|
|
|
|
|
|
|
|
const editorConfig = {
|
|
|
|
|
placeholder: props.placeholder,
|
|
|
|
|
MENU_CONF: {
|
|
|
|
|
// 图片上传复用后台统一图片上传接口
|
|
|
|
|
uploadImage: {
|
|
|
|
|
server: '/api/admin/upload/image',
|
|
|
|
|
fieldName: 'file',
|
|
|
|
|
maxFileSize: 5 * 1024 * 1024, // 5MB
|
|
|
|
|
allowedFileTypes: ['image/*'],
|
|
|
|
|
headers: {
|
|
|
|
|
get Authorization() {
|
|
|
|
|
return authStore.token ? `Bearer ${authStore.token}` : ''
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// 适配后端返回结构 { code:0, data:{ imageUrl } }
|
|
|
|
|
customInsert(res, insertFn) {
|
|
|
|
|
const url = res?.data?.imageUrl || res?.data?.image_url || res?.data?.url || ''
|
|
|
|
|
if (url) {
|
|
|
|
|
insertFn(url, '', url)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleCreated(editor) {
|
|
|
|
|
editorRef.value = editor
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleChange(editor) {
|
|
|
|
|
emit('update:modelValue', editor.getHtml())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 组件销毁时务必销毁编辑器实例,防止内存泄漏
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
const editor = editorRef.value
|
|
|
|
|
if (editor == null) return
|
|
|
|
|
editor.destroy()
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.rich-editor {
|
|
|
|
|
border: 1px solid #dcdfe6;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
.rich-editor__toolbar {
|
|
|
|
|
border-bottom: 1px solid #e4e7ed;
|
|
|
|
|
}
|
|
|
|
|
.rich-editor__body {
|
|
|
|
|
background: #fff;
|
|
|
|
|
}
|
|
|
|
|
</style>
|