给大家分享一个好用的富文本编辑器
项目功能介绍
前后端分离
前端 vue3 typescript wangEditor axios
后端 springboot mybatis-plus swagger
项目精简 不引入过多框架
1. 自定义图片上传2. 自定义视频上传
(wangEditor 有默认配置 但要返回response body有格式要求 故自己编写后端自定义实现) 3. 后端 对于html的处理 转义安全字符 解义4. 文章的保存5. 文章的查询
资源介绍swagger接口文档
编辑器功能展示
项目目录讲解前端
后端
部分代码展示前端 富文本编辑器页面App.vue
后端 文章查询保存 serviceImpl
<script setup lang="ts"> import "@wangeditor/editor/dist/css/style.css"; // 引入 css import { onBeforeUnmount, ref, shallowRef, onMounted, reactive } from "vue"; import { Editor, Toolbar } from "@wangeditor/editor-for-vue"; import { IEditorConfig } from "@wangeditor/core"; import { uploadPic, deleteFile, uploadVideo, toSaveArticleAndFile ,toQueryArticleApi} from "./request/api"; import { resourceUrl } from "./common/path"; import { IWangEPic, IWangEVid, IRichData, IToSaveAricle, IReQueryArticle } from "./pageTs/index"; //图片 视频 类型声明 const richData = reactive(new IRichData()); const saveArticleData=reactive(new IToSaveAricle()); // 编辑器实例,必须用 shallowRef const editorRef = shallowRef(); // 内容 HTML const valueHtml = ref(""); // 模拟 axios 异步获取内容 onMounted(() => { setTimeout(() => { valueHtml.value = "<p>大大帅将军 小小怪下士</p>"; }, 1500); }); //编辑器初始化 const toolbarConfig = {}; const editorConfig: Partial<IEditorConfig> = { MENU_CONF: {} }; // 编辑器创建完毕时的回调函数。 const handleCreated = (editor: any) => { editorRef.value = editor; // 记录 editor 实例,重要! }; //图片类型定义 type InsertPicType = (url: string) => void; //图片上传 editorConfig.MENU_CONF!["uploadImage"] = { // 自定义上传 InsertFnType async customUpload(richPic: File, insertFn: InsertPicType) { //图片上传接口调用 uploadPic(richPic).then((res) => { console.log(res.data); //返回给编辑器 图片地址 insertFn(resourceUrl res.data); //上传成功后 记录图片地址 richData.preFileList.push(res.data); }); }, }; //上传视频 url 视频地址 poster 视频展示图片地址 type InsertVidType = (url: string, poster: string) => void; editorConfig.MENU_CONF!["uploadVideo"] = { // 自定义上传 async customUpload(file: File, insertFn: InsertVidType) { //视频上传接口调用 uploadVideo(file).then((res) => { console.log(res.data); //返回给编辑器 图片地址 insertFn(resourceUrl res.data, "/src/assets/bg.png"); //上传成功后 记录视频地址 richData.preFileList.push(res.data); }); }, }; // 组件销毁时,也及时销毁编辑器 onBeforeUnmount(() => { const editor = editorRef.value; if (editor == null) return; editor.destroy(); }); //保存文章 const toSaveArcitle = () => { const editor = editorRef.value; // 1.获取最后保存的文章图片 视频 list数组 editor.getElemsByType("image").forEach((item: IWangEPic) => { //排除掉外部资源 if(item.src.indexOf(resourceUrl) !=-1){ richData.articleFileUrl.push(item.src); } }); editor.getElemsByType("video").forEach((item: IWangEVid) => { if(item.src.indexOf(resourceUrl) !=-1){ richData.articleFileUrl.push(item.src); } }); //2.对于全部图片 视频 对比 获取已删除图片 richData.preFileList.forEach((item) => { //articleFileList 数组展示的是图片完全路径 //preFileList 保存的是图片部分路径 //所以要通过添加resourceUrl常量进行对比 if (richData.articleFileUrl.indexOf(resourceUrl item) == -1) { //保存到需删除的数组中 richData.deleteFileList.push(item); }else{ //保存需要存入数据库的数组 saveArticleData.articleFileUrl.push(item); } }); //3.调后台接口 删除图片 视频 deleteFile(richData.deleteFileList).catch((err) => { console.log(err.msg); }); //4.调后台接口保存文章 //参数赋值 saveArticleData.articleName=richData.articleName; saveArticleData.articleAuthor=richData.articleAuthor; saveArticleData.articleContent=valueHtml.value; toSaveArticleAndFile(saveArticleData).then(res=>{ console.log("保存文章成功"); //保存文章后 所有数据清空 valueHtml.value=""; richData.articleAuthor=""; richData.articleName=""; }) }; //文章查询 const queryArticle = reactive(new IReQueryArticle()); const toQueryArticle=()=>{ toQueryArticleApi(1566944471915032576n).then(res=>{ console.log(res.data); queryArticle.articleAuthor=res.data.articleAuthor; queryArticle.articleContent=res.data.articleContent; queryArticle.articleId=res.data.articleId; queryArticle.gmtUpdate=res.data.gmtUpdate; queryArticle.articleName=res.data.articleName; }) } </script> <template> <div> 文章名称:<input class="demoInput" type="text" v-model="richData.articleName" placeholder="请输入文章名称" /> </div> <div> 文章作者:<input class="demoInput" type="text" v-model="richData.articleAuthor" placeholder="请输入文章作者" /> </div> <div style="border: 1px solid #ccc"> <Toolbar style="border-bottom: 1px solid #ccc" :editor="editorRef" :defaultConfig="toolbarConfig" :mode="richData.model" /> <Editor style="height: 550px; overflow-y: hidden" v-model="valueHtml" :defaultConfig="editorConfig" :mode="richData.model" @onCreated="handleCreated" /> </div> <button @click="toSaveArcitle">保存</button> <div>==========================================</div> <button @click="toQueryArticle">查询文章</button> <div v-if="queryArticle!=null"> <h1>{{queryArticle.articleName}}</h1> <h5>{{queryArticle.articleAuthor}}</h5> <h6>{{queryArticle.gmtUpdate}}</h6> <div v-html="queryArticle.articleContent"> </div> </div> </template> <style lang="scss" scoped> .demoInput { outline-style: none; border: 1px solid #ccc; border-radius: 3px; padding: 13px 14px; width: 320px; font-size: 14px; font-weight: 320; margin: 20px; font-family: "Microsoft soft"; } .demoInput:focus { border-color: #66afe9; outline: 0; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); } </style>
功能演示 源码分享
/** * <p> * 服务实现类 * </p> * * @author 小王八 * @since 2022-09-05 */ @Service public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService { @Autowired private ArticleMapper articleMapper; @Autowired private ArticleFileService articleFileService; @Override @Transactional(rollbackFor = Exception.class) public String toSaveArticle(ToSaveArticle toSaveArticle) { //1.生成文章id 插入图片article_id long articleId = IdUtil.getSnowflakeNextId(); //2.文章内容转换 //过滤HTML文本,防止XSS攻击 可用 会清理掉html元素标签 只留下文本 //String articleContent = HtmlUtil.filter(toSaveArticle.getArticleContent()); //html=>安全字符 String escape = HtmlUtil.escape(toSaveArticle.getArticleContent()); //3.文章入数据库 save(new Article() .setPkId(articleId) .setArticleName(toSaveArticle.getArticleName()) .setArticleAuthor(toSaveArticle.getArticleAuthor()) .setArticleContent(escape) ); //4.文章资源入数据库 if (ObjectUtil.isNotEmpty(toSaveArticle.getArticleFileUrl())){ articleFileService.toSaveFile(toSaveArticle.getArticleFileUrl(),articleId); } return "文章保存成功"; } @Override public ReShowArticle toShowArticle(Long articleId) { return ReShowArticle.toReShow(getById(articleId)); }
关于功能的动态详细展示
我专门录制的一期B站视频 作为讲解
具体源码
放在视频简介(gitee 前后端地址都有)
【一款好用的富文本编辑器 wangEditor 前后端 vue3 springboot】
B站视频链接
制作不易 还望大家三连支持
,