目录
🥰给大家分享一个好用的富文本编辑器🥰


前后端分离
🍳前端 vue3+typescript+wangEditor+axios🍳
🥨 后端 springboot+mybatis-plus+swagger🥨
🥐项目精简 不引入过多框架🥐
🌯1. 自定义图片上传🌯
🍗2. 自定义视频上传🍗(wangEditor 有默认配置 但要返回response body有格式要求 故自己编写后端自定义实现)
🍖3. 后端 对于html的处理 转义安全字符 解义🍖
🧀4. 文章的保存🧀
🥗5. 文章的查询🥗






- <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 = "
大大帅将军 小小怪下士
"; - }, 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>
- /**
- *
- * 服务实现类
- *
- *
- * @author 小王八
- * @since 2022-09-05
- */
- @Service
- public class ArticleServiceImpl extends ServiceImpl
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】
🥰制作不易 还望大家三连支持🥰
