WangEdtior富文本编辑器
WangEdtior官网,提供了vue2、vue3的安装方式>>
原生js使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link href="https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css" rel="stylesheet">
<script src="https://unpkg.com/@wangeditor/editor@latest/dist/index.js"></script>
<style>
#editor—wrapper {
border: 1px solid #ccc;
z-index: 100;
/* 按需定义 */
width: 800px;
margin: 10px auto;
}
#toolbar-container {
border-bottom: 1px solid #ccc;
}
#editor-container {
height: 150px;
}
p {
/* 将p的margin改为0, 不然会出现改了#editor-container的高度,编辑器已经足够放下内容了,但是仍然会有滚动条出现的问题 */
margin: 0 !important;
line-height: 1.3em;
}
.w-e-scroll>div:first-child {
padding-top: 15px; /* 将输入起始位置与“请输入内容对齐” */
}
</style>
</head>
<body>
<div id="editor—wrapper">
<!-- 工具栏 -->
<div id="toolbar-container"></div>
<!-- 编辑器 -->
<div id="editor-container"></div>
</div>
</body>
<script>
const { createEditor, createToolbar } = window.wangEditor
/* 编辑器配置 */
const editorConfig = {
placeholder: '请输入内容...',
onChange(editor) {
const html = editor.getHtml()
console.log('editor content', html)
}
}
/* 编辑器 */
const editor = createEditor({
selector: '#editor-container',
html: '<p><br></p>',
config: editorConfig,
mode: 'default', // or 'simple'
})
/* 工具栏配置 */
const toolbarConfig = {}
/* 工具栏 */
const toolbar = createToolbar({
editor,
selector: '#toolbar-container',
config: toolbarConfig,
mode: 'default', // or 'simple'
})
</script>
</html>
vue2
安装wangEditor
npm install @wangeditor/editor --save
npm install @wangeditor/editor-for-vue --save
ArticleWang.vue
<style>
p {
/* 将p的margin改为0, 不然会出现改了#editor-container的高度,编辑器已经足够放下内容了,但是仍然会有滚动条出现的问题 */
margin: 0 !important;
line-height: 1.3em;
}
.w-e-scroll>div:first-child {
padding-top: 15px;
/* 将输入起始位置与“请输入内容对齐” */
}
</style>
<template>
<div style="border: 1px solid red;padding: 10px;">
<!-- 按钮组 -->
<div style="text-align: center;">
<el-button type="success" plain @click="getHtml">getHtml</el-button>
<el-button type="success" plain @click="setHtml">setHtml</el-button>
<el-button type="success" plain @click="resetHtml">resetHtml</el-button>
<el-button type="success" plain @click="getText">getText</el-button>
<el-button type="success" plain @click="focus">focus</el-button>
<el-button type="success" plain @click="saveHtml">saveHtml</el-button>
</div>
<div style="text-align: center;margin-top: 10px;">
<el-button type="success" plain @click="playToolbar">playToolbar</el-button>
<el-button type="success" plain @click="enableEdit">enableEdit</el-button>
</div>
<div style="text-align: center;margin-top: 10px;">
<el-button type="success" plain @click="getUploadImageMenuConfig">getUploadImageMenuConfig</el-button>
</div>
<!-- 编辑器 -->
<div style="border: 1px solid #ccc; width: 800px;margin: 10px auto 0;">
<!-- 工具栏 -->
<Toolbar style="border-bottom: 1px solid #ccc" :editor="editor" :defaultConfig="toolbarConfig" :mode="mode" />
<!-- 编辑器 -->
<Editor style="height: 310px;" v-model="htmlContent" :defaultConfig="editorConfig" :mode="mode"
@onCreated="onCreated" @onChange="onChange"/>
</div>
<div style="width: 800px;margin: 10px auto;" v-html="htmlContent"></div>
</div>
</template>
<script>
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
/* 可以用来获取toolbar */
import { DomEditor } from '@wangeditor/editor'
/* 引入wangEditor的样式 */
import '@wangeditor/editor/dist/css/style.css'
import axiosInstance from '@/utils/request'
export default {
name: 'ArticleWang',
components: {
Editor, Toolbar
},
data() {
return {
editor: null, /* 编辑器实例,在编辑器创建完成时,赋值 */
htmlContent: '<p>hello</p>', /* 编辑器内容 */
toolbarConfig: { /* 工具栏配置 */
excludeKeys: [
'group-video' // 排除视频这个菜单
]
},
editorConfig: { /* 编辑器配置 */
placeholder: '请输入内容...',
// maxLength: 50, // 限制长度50
MENU_CONF: {
emotion: { // 表情菜单
emotions: '😀 😃 😄 😁 😆 😅 😂 🤣 😊 😇 🙂 🙃 😉'.split(' ') // 数组
},
uploadImage: { // 图片上传
server: 'http://localhost:8083/article/uploadImg', /* 上传接口 */
fieldName: 'mfile', /* 文件参数名 */
"allowedFileTypes": [ /* 允许的文件类型 */
"image/*"
],
maxFileSize: 10 * 1024 * 1024, /* 单个文件的最大体积限制,默认为 2M */
maxNumberOfFiles: 10, /* 最多可上传几个文件,默认为 100 */
// 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。
meta: {
token: 'xxx',
otherKey: 'yyy'
},
// 将 meta 拼接到 url 参数中,默认 false
metaWithUrl: false,
// 自定义增加 http header
headers: {
Accept: 'application/json;charset=utf-8',
otherKey: 'xxx'
},
// 跨域是否传递 cookie ,默认为 false
withCredentials: true,
// 超时时间,默认为 10 秒
timeout: 5 * 1000, // 5 秒
// 自定义插入图片
/* 上传成功的返回格式:{
"errno": 0, // 注意:值是数字,不能是字符串
"data": {
"url": "xxx", // 图片 src ,必须
"alt": "yyy", // 图片描述文字,非必须
"href": "zzz" // 图片的链接,非必须
}
}
上传失败的返回格式:{
"errno": 1, // 只要不等于 0 就行
"message": "失败信息"
}
如果你的服务端 response body 无法按照上文规定的格式,则无法插入图片,提示失败。
但你可以使用 customInsert 来自定义插入图片) */
/* customInsert(res, insertFn) {
// 从 res 中找到 url alt href ,然后插入图片
// insertFn(url, alt, href)
insertFn(`http://localhost:8083/img/${res.data}`)
}, */
/* 自己实现文件上传 */
customUpload(file, insertFn) {
// file 即选中的文件
// 自己实现上传,并得到图片 url alt href
// 最后插入图片
// 拿到 files 之后上传到文件服务器,然后向编辑框中插入对应的内容
let formData = new FormData()
formData.append('mfile', file)
axiosInstance({
url: "http://127.0.0.1:8083/article/uploadImg",
method: 'POST',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
}).then(res => {
insertFn('http://127.0.0.1:8083/img/' + res, res)
console.log('插入成功');
})
},
},
insertImage: {
/* 插入过的图片都会回调这个方法,最后保存富文本时,比较插入的记录和最终获取的image(editor.getElemsByType('image')),就能知道该删除哪些图片 */
onInsertedImage(imageNode) {
console.log('onInsertedImage->imageNode',imageNode);
}
}
}
},
mode: 'default',
editorEnabled: true,
}
},
methods: {
onCreated(editor) {/* 编辑器创建完毕时的回调函数。 */
this.editor = Object.seal(editor) // 一定要用 Object.seal() ,否则会报错
},
onChange(editor) {/* 编辑器内容、选区变化时的回调函数 */
console.log('有人在动编辑器');
},
getHtml() {
console.log(this.editor.getHtml());
},
setHtml() {
/* 这里设置的内容只能是从wangEditor中获取的内容哦 */
this.editor.setHtml('<p>模拟 Ajax 异步设置内容 SETHTML</p>')
},
resetHtml() {
this.editor.setHtml('')
},
getText() {
console.log(this.editor.getText());
},
focus() {
this.editor.focus() // 获取焦点,光标的位置默认会回到之前的位置闪烁
},
saveHtml() {
axiosInstance({
url: "http://127.0.0.1:8083/article/save",
method: 'POST',
data: { htmlContent: this.htmlContent },
}).then(res => {
console.log('成功');
})
},
playToolbar() {
const toolbar = DomEditor.getToolbar(this.editor)
const curToolbarConfig = toolbar.getConfig()
/* 当前菜单排序和分组, 目的是可以知道默认的工具栏配置 */
console.log(curToolbarConfig.toolbarKeys)
/* 可获取编辑器所有菜单 */
console.log(this.editor.getAllMenuKeys());
console.log(curToolbarConfig === this.toolbarConfig) // false, 不是同一个
},
enableEdit() {
/* 开启或禁用 编辑 */
if (this.editorEnabled) {
this.editor.disable()
} else {
this.editor.enable()
}
this.editorEnabled = !this.editorEnabled
},
getUploadImageMenuConfig() { /* 获取图片上传菜单的默认配置 */
console.log(this.editor.getMenuConfig('uploadImage'));
console.log(JSON.stringify(this.editor.getMenuConfig('uploadImage'))); // 获取 uploadImage菜单 的当前配置
/*
{
"server":"",
"fieldName":"wangeditor-uploaded-image",
"maxFileSize":2097152,
"maxNumberOfFiles":100,
"allowedFileTypes":[
"image/*"
],
"meta":{
},
"metaWithUrl":false,
"withCredentials":false,
"timeout":10000,
"base64LimitSize":0
}
*/
}
},
mounted() {
// 模拟 ajax 请求,异步渲染编辑器
setTimeout(() => {
this.htmlContent = '<p>模拟 Ajax 异步设置内容 HTML</p>'
}, 1500)
},
beforeDestroy() {
const editor = this.editor
if (editor == null) return
editor.destroy() // 组件销毁时,及时销毁编辑器
}
}
</script>
request.js
import axios from 'axios'
import router from '@/router'
const instance = axios.create({
baseURL: 'http://localhost:8083',
timeout: 60000,
withCredentials: true /* 需要设置这个选项,axios发送请求时,才会携带cookie, 否则不会携带 */
})
// Add a request interceptor
instance.interceptors.request.use(function (config) {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
// Add a response interceptor
instance.interceptors.response.use(function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
console.log('收到响应',response);
if(response.data.code == 401) {
router.push('/login')
}
return response.data.data;
}, function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
return Promise.reject(error);
});
export default instance
WebConfig
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry
.addMapping("/**")
.maxAge(3600)
.allowCredentials(true)
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*")
.exposedHeaders("token","Authorization")
;
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/img/**")
.addResourceLocations("file:/D:\\Projects\\vue-springboot\\src\\main\\resources\\static\\img\\");
}
}
ArticleController
@RestController
@RequestMapping("article")
public class ArticleController {
@PostMapping("uploadImg")
public Result uploadImg(@RequestParam("mfile") MultipartFile mfile) throws IOException {
String filename = mfile.getOriginalFilename();
mfile.transferTo(new File("D:\\Projects\\vue-springboot\\src\\main\\resources\\static\\img\\"+filename));
return Result.ok(filename);
}
}
yml配置
server:
port: 8083
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://localhost:3306/vue-springboot?serverTimezone=UTC&useSSL=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
servlet:
multipart:
max-file-size: 50MB
max-request-size: 50MB
mybatis-plus:
mapper-locations: classpath:/mapper/**.xml