vue3+vite+ts安装wangeditor富文本编辑器
需求:
实现粘贴,上传图片时本地渲染但并不实现上传功能,工具栏移除不需要的工具
安装方法看官网
安装 | wangEditor
封装子组件
wangEditor.vue
<template>
<div>
<div style="border: 1px solid #ccc; margin-top: 10px">
<Toolbar
:editor="editorRef"
:defaultConfig="toolbarConfig"
:mode="mode"
style="border-bottom: 1px solid #ccc"
/>
<Editor
:defaultConfig="editorConfig"
:mode="mode"
v-model="valueHtml"
:style="{ height: editorHeight, overflowY: 'hidden' }"
@onCreated="handleCreated"
@onChange="handleChange"
@onDestroyed="handleDestroyed"
@onFocus="handleFocus"
@onBlur="handleBlur"
@customPaste="customPaste"
/>
</div>
</div>
</template>
ts文件
export interface ViewDataType {
visible: boolean
item: {
id: string
topDate: string
titleTime: string
author: string
address: string
content1: string
content3: string
content5: string
content6: string
content7: string
imgUrl: string
[key: string]: string // 允许动态属性
}
}
// 列表编辑弹框中的类型定义
export interface EditDataType {
visible: boolean;
datas: {
id: string
topDate: string
titleTime: string
author: string
address: string
imgUrl: string
[key: string]: string // 允许动态属性
}
RuleForm :{
theme: string
beginTime: string
content: string
}
}
// 上传功能
export type InsertFnType = (
url: string,
alt: string,
href: string,
style: string
) => void
js
<script lang="ts" setup>
import '@wangeditor/editor/dist/css/style.css'
import {
onBeforeUnmount,
ref,
shallowRef,
onMounted,
PropType,
nextTick,
} from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { DomEditor } from '@wangeditor/editor'
// 接收父组件传递的参数
const props = defineProps({
initialValue: {
type: String,
default: '',
},
editorHeight: {
type: String,
default: '200px',
},
toolbarConfig: {
type: Object as PropType<Record<string, any>>,
default: () => ({}),
},
editorConfig: {
type: Object as PropType<Record<string, any>>,
default: () => ({ placeholder: '请输入内容...' }),
},
mode: {
type: String,
default: 'default',
},
})
// 编辑器实例,必须用 shallowRef,重要!
const editorRef = shallowRef<typeof Editor | null>(null)
// 内容 HTML
const valueHtml = ref<string>(props.initialValue)
const emit = defineEmits([
'handleChange',
'handleFocus',
'handleDestroyed',
'handleBlur',
'customPaste',
])
// 模拟 ajax 异步获取内容
onMounted(() => {
// setTimeout(() => {
// valueHtml.value = '<p>模拟 Ajax 异步设置内容</p>'
// }, 1500)
})
// 编辑器回调函数
// const handleCreated = (editor: typeof Editor) => {
const handleCreated = (editor) => {
nextTick(() => {
editorRef.value = editor // 记录 editor 实例,重要!
const toolbar = DomEditor.getToolbar(editor)
const curToolbarConfig = toolbar?.getConfig()
console.log(' 当前菜单排序和分组', toolbar) // 当前菜单排序和分组
console.log('curToolbarConfig', curToolbarConfig?.toolbarKeys) //查看工具栏内容
})
}
// 触发事件实现双向绑定
const handleChange = (editor: typeof Editor) => {
emit('handleChange', editor.getHtml())
}
// 卸载事件
const handleDestroyed = (editor: typeof Editor) => {
emit('handleDestroyed', editor)
}
// 失去焦点事件
const handleFocus = (editor: typeof Editor) => {
emit('handleFocus', editor)
}
const handleBlur = (editor: typeof Editor) => {
emit('handleBlur', editor)
}
// 粘贴事件
const customPaste = (
editor: typeof Editor,
event: ClipboardEvent,
callback: (value: boolean) => void
) => {
emit('customPaste', event, editor, callback)
}
// 组件销毁时,也及时销毁编辑器,重要!
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
// 提交事件给父组件
// const emit = defineEmits(['editor'])
// emit('editor', editorRef.value)
// 导出
defineExpose({
editorRef,
})
</script>
父组件调用并使用
<wangEditor
:initialValue="ruleForm.content"
:editorHeight="'200px'"
:toolbarConfig="toolbarConfig"
:editorConfig="editorConfig"
:mode="'default'"
ref="wangEditorRef"
@customPaste="customPaste"
@handleChange="handleChange"
/>
<script lang="ts" setup>
import { onMounted, reactive, ref, unref } from 'vue'
import type { FormInstance, FormRules, TabsPaneContext } from 'element-plus'
import { EditDataType, InsertFnType } from '@/types/messages/securityInfoList'
import wangEditor from './wangEditor.vue'
import { IToolbarConfig } from '@wangeditor/editor'
const headerTitle = ref('编辑安全资讯')
const props = withDefaults(defineProps<EditDataType>(), {
visible: false,
})
const emit = defineEmits(['closeApply'])
// 来获取表单实例,从而调用表单的验证方法
const ruleFormRef = ref<FormInstance>()
// 编辑表单数据
const ruleForm = reactive<EditDataType['RuleForm']>({
content: '',
})
// 表单数据检验
const rules = reactive<FormRules<EditDataType['RuleForm']>>({
content: [
{ required: true, message: '安全资讯内容不能为空!', trigger: 'blur' },
],
})
// 加载完成
onMounted(() => {
console.log('props', props.datas)
Object.keys(ruleForm).forEach((key) => {
ruleForm[key] = props.datas[key]
})
})
const closeApply = () => {
// 清空表单
ruleFormRef.value?.resetFields()
emit('closeApply')
}
// 定义防抖函数
const debounce = (func: Function, delay: number) => {
let timer: NodeJS.Timeout
return (...args: any[]) => {
clearTimeout(timer)
timer = setTimeout(() => {
func(...args)
}, delay)
}
}
// 子组件触发事件
const handleChange = (value) => {
console.log('子组件触发事件', value)
ruleForm.content = value
}
// 定义保存按钮点击事件处理函数
const handlerSave = () => {
// 在这里执行保存操作的代码
submitForm(ruleFormRef.value)
} // 设置防抖时间间隔为1秒
// 保存表单检验
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
console.log('保存提交表单!')
// 清空表单
ruleFormRef.value?.resetFields()
} else {
console.log('缺少必填内容!', fields)
}
})
}
// 定义编辑器工具栏
const toolbarConfig: Partial<IToolbarConfig> = {
// 配置工具栏上移除网络上传图片,视频,表格,代码块,行内代码
excludeKeys: [
'insertImage',
'group-video',
'insertTable',
'codeBlock',
'code',
],
}
// 配置编辑器
const editorConfig = ref<Record<string, any>>({
placeholder: '请输入安全资讯内容==--父组件',
MENU_CONF: {
uploadImage: {
// 选择文件时的类型限制,默认为['image/jpeg', 'image/png', 'image/gif']
allowedFileTypes: ['image/jpeg', 'image/png', 'image/gif'],
async customUpload(file: File, insertFn: any) {
const reader = new FileReader()
reader.onload = async () => {
if (reader.result) {
// reader.result 包含了 Base64 编码的文件数据
const base64Data = reader.result as string
const imgUrl = base64Data //base64传为img标签元素才能设置宽高
const imgHtml = `<img src="${imgUrl}" alt="image" style="width:200px;height:200px;"/>`
let editorRefs = await wangEditorRef?.value.editorRef
const editor = unref(editorRefs)
if (editor) {
// 通过 dangerouslyInsertHtml 插入图片的 HTML,控制宽高
editor.dangerouslyInsertHtml(imgHtml)
}
// 插入图片到编辑器
// insertFn(imgHtml, file.name, '', '')
}
}
// 读取文件并将其转换为 Base64 编码的字符串
reader.readAsDataURL(file)
},
},
},
})
// 粘贴事件
const customPaste = (event, editor, callback) => {
const items = event.clipboardData?.items
if (!items) return
for (let i = 0; i < items.length; i++) {
const item = items[i]
if (item.type.indexOf('image') !== -1) {
const file = item.getAsFile()
if (file) {
// 阻止默认的上传行为
event.preventDefault()
// 创建一个 FileReader 来读取图片数据
const reader = new FileReader()
reader.onload = (e: ProgressEvent<FileReader>) => {
if (e.target?.result) {
// 将图片数据转换为 base64 格式
const base64Data = e.target.result as string
// 创建一个 img 元素并插入到编辑器中setHtml
const img = new Image()
img.src = base64Data
const imgHtml = `<img src="${base64Data}" alt="image" style="width:200px;height:200px;"/>`
let editorRefs = wangEditorRef?.value.editorRef
const editor = unref(editorRefs)
if (editor) {
// 通过 dangerouslyInsertHtml 插入图片的 HTML,控制宽高
editor.dangerouslyInsertHtml(imgHtml)
}
}
}
reader.readAsDataURL(file)
}
}
}
// 返回值(注意,vue 事件的返回值,不能用 return)
// callback(false) // 返回 false ,阻止默认粘贴行为
callback(true) // 返回 true ,继续默认的粘贴行为
}
defineExpose({
headerTitle,
})
</script>
效果图