<think>我们已经在之前的回答中详细介绍了如何在Vue3中使用@wangeditor/editor,包括安装、组件创建、图片上传等。现在根据用户提供的引用内容,用户可能已经在项目中使用了@pureadmin/utils工具库,并且希望将富文本编辑器封装为全局组件。下面将结合用户提供的引用信息,给出更符合实际项目的实现方案。
### 步骤1:安装依赖
根据引用内容,需要安装以下依赖:
```bash
npm install @wangeditor/editor @wangeditor/editor-for-vue @pureadmin/utils
# 或者
yarn add @wangeditor/editor @wangeditor/editor-for-vue @pureadmin/utils
```
### 步骤2:创建编辑器组件
在`src/components/ReEditor/src/Editor.vue`中创建组件(注意路径与引用一致):
```vue
<template>
<div style="border: 1px solid #ccc">
<Toolbar
style="border-bottom: 1px solid #ccc"
:editor="editorRef"
:defaultConfig="toolbarConfig"
/>
<Editor
style="height: 500px; overflow-y: hidden"
v-model="valueHtml"
:defaultConfig="editorConfig"
@onCreated="handleCreated"
/>
</div>
</template>
<script setup lang="ts">
import { ref, shallowRef, onBeforeUnmount } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import '@wangeditor/editor/dist/css/style.css'
const props = defineProps({
modelValue: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue'])
// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef()
// 内容 HTML,使用计算属性实现双向绑定
const valueHtml = ref(props.modelValue)
// 工具栏配置
const toolbarConfig = { excludeKeys: ['uploadImage'] } // 排除上传图片菜单
// 编辑器配置
const editorConfig = {
placeholder: '请输入内容...',
MENU_CONF: {
// 配置上传图片(根据实际API调整)
uploadImage: {
server: '/api/upload',
fieldName: 'image'
}
}
}
// 组件销毁时,及时销毁编辑器
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
const handleCreated = (editor: any) => {
editorRef.value = editor // 记录 editor 实例
}
// 监听内容变化,更新父组件绑定值
watch(valueHtml, (newVal) => {
emit('update:modelValue', newVal)
})
</script>
```
### 步骤3:全局注册组件(使用@pureadmin/utils)
在`src/components/ReEditor/index.ts`中:
```typescript
import editor from "./src/Editor.vue"
import { withInstall } from "@pureadmin/utils"
/** 编辑器组件 */
export const RwEditor = withInstall(editor)
export default RwEditor
```
### 步骤4:在main.ts中全局注册(可选)
如果需要在全局使用,可以在`main.ts`中:
```typescript
import { createApp } from 'vue'
import App from './App.vue'
import RwEditor from '@/components/ReEditor'
const app = createApp(App)
app.component('RwEditor', RwEditor)
app.mount('#app')
```
### 步骤5:在父组件中使用
```vue
<template>
<div>
<h1>文章编辑器</h1>
<RwEditor v-model="html" />
<button @click="submit">提交</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const html = ref('')
const submit = () => {
console.log('提交内容:', html.value)
}
</script>
```
### 图片上传自定义(根据实际API调整)
```javascript
// 在editorConfig的uploadImage中配置
uploadImage: {
server: 'http://your-api-address/upload',
fieldName: 'file',
// 自定义上传逻辑
async customUpload(file: File, insertFn: (url: string) => void) {
const formData = new FormData()
formData.append('file', file)
try {
const { data } = await axios.post('http://your-api-address/upload', formData)
if (data.code === 0) {
insertFn(data.url)
} else {
console.error('上传失败', data.message)
}
} catch (error) {
console.error('上传失败', error)
}
}
}
```
### 注意事项
1. **样式引入**:确保在组件中引入`@wangeditor/editor/dist/css/style.css`
2. **双向绑定**:通过`v-model`实现父子组件数据同步
3. **销毁实例**:在组件销毁前调用`editor.destroy()`防止内存泄漏
4. **TypeScript支持**:wangEditor提供了完整的类型定义,无需额外安装
### 常见问题解决
- **无法显示工具栏**:检查是否同时引入了`Toolbar`和`Editor`组件
- **图片上传失败**:检查跨域问题及后端接口格式
- **内容绑定失效**:确保使用`watch`监听变化并触发`update:modelValue`事件
### 与表单验证集成
```vue
<template>
<el-form :model="form" :rules="rules">
<el-form-item label="内容" prop="content">
<RwEditor v-model="form.content" />
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
const form = ref({
content: ''
})
const rules = ref({
content: [
{
validator: (_, value, callback) => {
// 过滤空内容(wangEditor默认空内容是'<p><br></p>')
if (!value || value === '<p><br></p>') {
callback(new Error('内容不能为空'))
} else {
callback()
}
},
trigger: 'blur'
}
]
})
</script>
```
[^1]: wangEditor官方文档提供了详细的Vue3集成示例
[^2]: @pureadmin/utils的`withInstall`方法可以简化全局组件注册</think>下面我将详细介绍在 Vue3 项目中集成 `@wangeditor/editor` 富文本编辑器的完整步骤,包括组件封装、图片上传、表单验证等实用功能。
### 一、安装依赖
```bash
npm install @wangeditor/editor @wangeditor/editor-for-vue
# 或
yarn add @wangeditor/editor @wangeditor/editor-for-vue
```
### 二、创建可复用的编辑器组件
`src/components/RwEditor/src/Editor.vue`:
```vue
<template>
<div class="editor-wrapper" :style="{ border: error ? '1px solid #f56c6c' : '1px solid #dcdfe6' }">
<Toolbar
class="editor-toolbar"
:editor="editorRef"
:defaultConfig="toolbarConfig"
/>
<Editor
class="editor-content"
v-model="valueHtml"
:defaultConfig="editorConfig"
@onCreated="handleCreated"
@onChange="handleChange"
/>
</div>
<div v-if="error" class="el-form-item__error">{{ error }}</div>
</template>
<script setup lang="ts">
import { ref, shallowRef, watch, onBeforeUnmount } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import '@wangeditor/editor/dist/css/style.css'
import type { IDomEditor } from '@wangeditor/editor'
const props = defineProps({
modelValue: {
type: String,
default: ''
},
placeholder: {
type: String,
default: '请输入内容...'
},
error: String
})
const emit = defineEmits(['update:modelValue', 'change'])
// 编辑器实例
const editorRef = shallowRef<IDomEditor | null>(null)
// 内容 HTML
const valueHtml = ref(props.modelValue)
// 工具栏配置
const toolbarConfig = {
excludeKeys: [
'uploadVideo', // 排除上传视频
'fullScreen' // 排除全屏
]
}
// 编辑器配置
const editorConfig = ref({
placeholder: props.placeholder,
autoFocus: false,
MENU_CONF: {
uploadImage: {
server: '/api/upload/image', // 图片上传接口
fieldName: 'file', // 表单字段名
maxFileSize: 10 * 1024 * 1024, // 10MB
allowedFileTypes: ['image/*'],
customInsert(res: any, insertFn: any) {
// 处理上传响应
if (res.code === 0) {
insertFn(res.data.url)
} else {
console.error('图片上传失败', res.msg)
}
}
}
}
})
// 创建编辑器实例
const handleCreated = (editor: IDomEditor) => {
editorRef.value = editor
}
// 内容变化处理
const handleChange = (editor: IDomEditor) => {
emit('update:modelValue', editor.getHtml())
emit('change', editor.getHtml())
}
// 监听外部值变化
watch(() => props.modelValue, (newVal) => {
if (newVal !== valueHtml.value) {
valueHtml.value = newVal
}
})
// 组件销毁时销毁编辑器
onBeforeUnmount(() => {
if (editorRef.value) {
editorRef.value.destroy()
editorRef.value = null
}
})
</script>
<style scoped>
.editor-wrapper {
border-radius: 4px;
overflow: hidden;
}
.editor-toolbar {
border-bottom: 1px solid #eee;
}
.editor-content {
min-height: 300px;
overflow-y: auto;
}
</style>
```
### 三、全局注册组件
`src/components/RwEditor/index.ts`:
```typescript
import editor from "./src/Editor.vue"
import { withInstall } from "@pureadmin/utils"
/** 富文本编辑器组件 */
export const RwEditor = withInstall(editor)
export default RwEditor
```
### 四、在父组件中使用
```vue
<template>
<div class="container">
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="内容" prop="content" :error="contentError">
<RwEditor
v-model="form.content"
:placeholder="请输入文章内容"
:error="contentError"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import RwEditor from '@/components/RwEditor'
import type { FormInstance } from 'element-plus'
const formRef = ref<FormInstance>()
const contentError = ref('')
const form = reactive({
title: '',
content: ''
})
// 验证规则
const rules = reactive({
title: [
{ required: true, message: '标题不能为空', trigger: 'blur' },
{ min: 5, max: 50, message: '长度在 5 到 50 个字符', trigger: 'blur' }
],
content: [
{
validator: (_, value, callback) => {
if (!value || value === '<p><br></p>') {
callback(new Error('内容不能为空'))
} else if (value.length > 10000) {
callback(new Error('内容长度不能超过10000字符'))
} else {
callback()
}
},
trigger: 'blur'
}
]
})
// 提交表单
const submitForm = async () => {
try {
await formRef.value?.validate()
// 实际提交逻辑
console.log('提交数据:', {
title: form.title,
content: form.content
})
// 提交到API
// await axios.post('/api/article', form)
ElMessage.success('提交成功')
} catch (error) {
// 处理内容错误
if (error?.fields?.content) {
contentError.value = error.fields.content[0].message
}
ElMessage.error('表单验证失败')
}
}
// 重置表单
const resetForm = () => {
formRef.value?.resetFields()
contentError.value = ''
}
</script>
```
### 五、图片上传处理(后端示例)
```typescript
// 服务端上传处理 (Node.js示例)
import express from 'express'
import multer from 'multer'
import path from 'path'
const app = express()
const upload = multer({ dest: 'uploads/' })
app.post('/api/upload/image', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ code: 400, msg: '未上传文件' })
}
// 实际项目中应处理文件重命名、云存储等
const fileUrl = `https://your-domain.com/uploads/${req.file.filename}`
res.json({
code: 0,
msg: '上传成功',
data: {
url: fileUrl,
alt: req.file.originalname,
href: fileUrl
}
})
})
```
### 六、高级功能扩展
#### 1. 自定义工具栏按钮
```javascript
// 在编辑器配置中添加
toolbarConfig: {
insertKeys: {
index: 5, // 插入位置
keys: ['uploadImage', 'insertVideo'] // 自定义按钮
}
}
```
#### 2. XSS 防护
```javascript
// 安装 xss 库
npm install xss
// 在提交前过滤
import xss from 'xss'
const safeHtml = xss(form.content, {
whiteList: {
a: ['href', 'title', 'target'],
img: ['src', 'alt'],
// 其他允许的标签和属性
}
})
```
#### 3. 内容字数统计
```vue
<!-- 在编辑器组件中添加 -->
<div class="editor-footer">
字数: {{ wordCount }} | 图片: {{ imageCount }}
</div>
<script>
// 在组件中添加
const wordCount = ref(0)
const imageCount = ref(0)
watch(valueHtml, (html) => {
// 计算纯文字字数
const text = editorRef.value?.getText() || ''
wordCount.value = text.trim().length
// 计算图片数量
imageCount.value = (html.match(/<img/g) || []).length
})
</script>
```
### 七、常见问题解决
1. **样式冲突问题**:
```css
/* 在全局样式中添加 */
.w-e-toolbar {
z-index: 2 !important;
}
.w-e-menu {
padding: 0 5px;
}
```
2. **TypeScript 类型支持**:
```bash
npm install @types/wangeditor__editor -D
```
3. **内容初始化问题**:
```javascript
// 使用 nextTick 确保编辑器初始化完成
import { nextTick } from 'vue'
nextTick(() => {
if (editorRef.value) {
editorRef.value.setHtml(props.modelValue)
}
})
```
4. **只读模式切换**:
```javascript
const toggleReadonly = () => {
if (editorRef.value) {
editorRef.value.disable()
// 启用:editorRef.value.enable()
}
}
```
### 八、性能优化建议
1. 使用 `shallowRef` 代替 `ref` 存储编辑器实例
2. 大文档使用虚拟滚动(需自定义实现)
3. 避免在 `onChange` 中执行重操作
4. 使用防抖处理自动保存:
```javascript
import { debounce } from 'lodash-es'
const autoSave = debounce((content) => {
localStorage.setItem('draft', content)
}, 1000)
watch(() => form.content, autoSave)
```
[^1]: wangEditor 官方文档提供了详细的 Vue3 集成示例
[^2]: @pureadmin/utils 的 `withInstall` 方法简化了全局组件注册
[^3]: Element Plus 表单验证需要特殊处理富文本内容