component-富文本实现(WangEditor)
1.富文本
富文本是指的是在文本内容中嵌入格式,样式,图像,链接等多媒体元素的文本格式。
2.WangEditor开源富文本编辑器
开源的富文本编辑器,在vue3前端项目中的引入如下
npm install @wangeditor/editor @wangeditor/editor-for-vue
对应的官网如下
https://www.wangeditor.com/
https://www.wangeditor.com/
3.应用与引入
3.1父组件
<template><div class="header"><div class="header-title">父组件</div><divv-if="!isEditing"class="edit-button"@click="handleEdit">编辑</div><divv-if="isEditing"class="edit-button"@click="handleSave">保存</div><divv-if="isEditing"class="edit-button-cancel"@click="handCancel">取消</div></div><el-divider /><divv-if="!isEditing"class="content"><RichContentv-model="richTextContent":readonly="true":show-toolbar="false"/></div><!-- 编辑状态 --><divv-elseclass="content"><RichContentv-model="tempRichText":readonly="false"/></div>
</template>
<script setup lang="tsx">
import { onMounted, ref, watch } from 'vue'
import RichContent from '@/views/digital-matrix/components/RichContent.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const isEditing = ref(false)
// const richContent = ref()
// 存储最终保存的富文本内容(HTML 格式)
const richTextContent = ref('')
const id = ref(null)
const system_link = ref(null)
// 编辑时的临时内容(避免未保存就修改原内容)
const tempRichText = ref('')
const handleEdit = () => {isEditing.value = truetempRichText.value = richTextContent.value
}watch(() => tempRichText,(newValue) => {console.log('富文本标签', newValue)}
)const handleSave = async () => {try {await ElMessageBox.confirm(`确定保存并覆盖原来内容`, '', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'})// await deleteMaterialApi(row.id)if (!tempRichText.value.trim()) {ElMessage.warning('请输入内容后再保存')} else {console.log('富文本标签', tempRichText.value)richTextContent.value = tempRichText.value// await saveRichTextApi({// type: 'ORG',// content: richTextContent.value,// id: id.value// })isEditing.value = false}ElMessage.success('修改成功')} catch (error) {if (error !== 'cancel') {ElMessage.error('修改失败')}}
}const handCancel = () => {tempRichText.value = ''isEditing.value = false
}// 图片上传接口,返回图片url
const handleImageUpload = async (file: File): Promise<string> => {// 自定义图片上传逻辑return 'https://example.com/image.jpg'
}onMounted(async () => {// await getRichTextApi({ type: 'ORG' }).then((response) => {// console.log('~~~~~~~~~~~~~~response', response)// richTextContent.value = response.data.content || '<p>暂无内容</p >'// id.value = response.data.id || null// system_link.value = response.data.system_link || null// })
})
</script>
<style lang="scss" scoped>
@use '@/styles/mixins' as *;@function vh($px) {@return calc($px / 1080) * 100vh;
}.header {display: flex;gap: 12px;&-title {@include text-style(var(--font-20), var(--el-font-family-bold), rgba(255, 255, 255, 1));}&-href {cursor: pointer;user-select: none;@include text-style(var(--font-20), var(--el-font-family-bold), #409eff);}
}.edit-button {@include panel;min-width: 80px;height: vh(32);margin-left: auto;line-height: vh(32);text-align: center;cursor: pointer;background: rgb(79 172 254 / 50%) !important;border: 1px solid #409eff !important;box-shadow: inset 0 0 20px 1px #0093f2 !important;@include text-style(var(--font-16), var(--el-font-family-regular), #fff);
}.edit-button-cancel {@include panel;min-width: 80px;height: vh(32);margin-left: 12px;line-height: vh(32);text-align: center;cursor: pointer;background: rgb(79 172 254 / 50%) !important;border: 1px solid #409eff !important;box-shadow: inset 0 0 20px 1px #0093f2 !important;@include text-style(var(--font-16), var(--el-font-family-regular), #fff);
}.content {display: flex;flex: 1;gap: 12px;padding: 12px;overflow: hidden;
}</style>
3.2 富文本子组件
<template><div class="rich-text-editor"><Toolbarv-if="showToolbar"class="toolbar":editor="editorRef":default-config="toolbarConfig":mode="mode"/><Editorv-model="valueHtml"class="editor":default-config="editorConfig":mode="mode"@on-created="handleCreated"@on-change="handleChange"/></div>
</template><script setup lang="ts">
import { onBeforeUnmount, ref, shallowRef, watch, nextTick } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import type { IDomEditor, IEditorConfig } from '@wangeditor/editor'
import '@wangeditor/editor/dist/css/style.css'// 定义 Props
interface Props {modelValue?: stringplaceholder?: stringreadonly?: booleanshowToolbar?: booleanuploadImage?: (file: File) => Promise<string>
}const props = withDefaults(defineProps<Props>(), {modelValue: '',placeholder: '请输入内容...',readonly: false,showToolbar: true,uploadImage: undefined
})// 定义 Emits
const emit = defineEmits<{'update:modelValue': [value: string]change: [value: string]created: [editor: IDomEditor]
}>()// 编辑器实例
const editorRef = shallowRef<IDomEditor>()
const valueHtml = ref(props.modelValue)
const mode = 'default'// 监听外部值变化
watch(() => props.modelValue,(newValue) => {if (newValue !== valueHtml.value) {valueHtml.value = newValue}}
)// 监听内部值变化
watch(valueHtml, (newValue) => {emit('update:modelValue', newValue)emit('change', newValue)
})// 工具栏配置
const toolbarConfig = {excludeKeys: ['group-video', 'fullScreen', 'insertTable']
}// 编辑器配置
const editorConfig: Partial<IEditorConfig> = {placeholder: props.placeholder,readOnly: props.readonly,MENU_CONF: {uploadImage: {allowedFileTypes: ['image/*'],maxFileSize: 10 * 1024 * 1024,maxNumberOfFiles: 10,async customUpload(file: File, insertFn: (url: string) => void) {try {if (props.uploadImage) {// 使用自定义上传函数const imageUrl = await props.uploadImage(file)insertFn(imageUrl)} else {// 默认使用本地预览const imageUrl = URL.createObjectURL(file)insertFn(imageUrl)}} catch (err) {console.error('图片上传失败', err)throw err}}}}
}// 编辑器创建回调
const handleCreated = (editor: IDomEditor) => {editorRef.value = editoremit('created', editor)
}// 内容变化回调
const handleChange = (editor: IDomEditor) => {// 内容变化已经在 watch 中处理
}// 设置只读状态
const setReadonly = (readonly: boolean) => {if (editorRef.value) {if (readonly) {editorRef.value.disable()} else {editorRef.value.enable()}}
}// 销毁编辑器
onBeforeUnmount(() => {const editor = editorRef.valueif (editor) {editor.destroy()}
})// 暴露方法给父组件
defineExpose({getEditor: () => editorRef.value,setReadonly,clear: () => {valueHtml.value = ''},getContent: () => valueHtml.value,setContent: (content: string) => {valueHtml.value = content}
})
</script><style scoped lang="scss">
.rich-text-editor {flex: 1;overflow: hidden;border-radius: 4px;// border: 1px solid #fff;.toolbar {border-bottom: 1px solid #dcdfe6;}.editor {flex: 1;overflow-y: auto;}
}// 编辑器样式调整
:deep(.w-e-text-container) {height: 100% !important;// 富文本默认颜色color: #fff;background: transparent;// 代码块pre > code {background-color: rgb(0 0 0 / 40%);}h1 {font-size: 36px;}h2 {font-size: 30px;}h3 {font-size: 26px;}h4 {font-size: 20px;}
}// toolBar样式
:deep(.w-e-bar) {background-color: transparent;svg {fill: #fff;}
}:deep(.w-e-bar-item) {color: #fff;
}// 滚动条
:deep(.w-e-scroll) {&::-webkit-scrollbar {width: 2px;background: transparent;}&::-webkit-scrollbar-track {background: rgb(54 148 255 / 20%);border-radius: 2px;}&::-webkit-scrollbar-thumb {background: #3694ff;border-radius: 2px;}
}:deep(.w-e-bar-item button) {color: inherit;
}:deep(.w-e-bar-item .active) {background-color: rgba($color: #a4bcff, $alpha: 20%);
}// 外圈
:deep(.w-e-bar-item:hover) {background-color: rgba($color: #a4bcff, $alpha: 20%);
}// 内圈
:deep(.w-e-bar-item button:hover) {background-color: transparent;
}:deep(.w-e-bar-item.active) {color: #409eff;background-color: #ecf5ff;
}// 下箭头呼出的容器
:deep(.w-e-drop-panel) {background: transparent;
}:deep(.w-e-select-list) {background: transparent;&::-webkit-scrollbar {width: 2px;background: transparent;}&::-webkit-scrollbar-track {background: rgb(54 148 255 / 20%);border-radius: 2px;}&::-webkit-scrollbar-thumb {background: #3694ff;border-radius: 2px;}
}:deep(.w-e-select-list ul li:hover) {background-color: rgba($color: #a4bcff, $alpha: 20%);
}:deep(.w-e-select-list ul .selected) {background: transparent;
}:deep(.w-e-bar-item-group .w-e-bar-item-menus-container) {background: transparent;
}
</style>
