• 一款好用的富文本编辑器(wangeditor)运用 Vue3+Springboot


    目录

    🧰项目功能介绍🧰

    🚕资源介绍🚕

    🛺swagger接口文档🛺

    🚙编辑器功能展示🚙

    🛻项目目录讲解🛻

    🎨前端🎨

    🌃后端🌃

    🚌部分代码展示🚌

    🍭前端 富文本编辑器页面App.vue🍭

    🧋后端 文章查询保存 serviceImpl🧋

    🚒功能演示 源码分享🚒


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

    🧰项目功能介绍🧰

    前后端分离

         🍳前端 vue3+typescript+wangEditor+axios🍳      

    🥨 后端 springboot+mybatis-plus+swagger🥨

    🥐项目精简 不引入过多框架🥐

    🌯1. 自定义图片上传🌯
    🍗2. 自定义视频上传🍗

    (wangEditor 有默认配置 但要返回response body有格式要求 故自己编写后端自定义实现) 
    🍖3. 后端 对于html的处理 转义安全字符 解义🍖
    🧀4. 文章的保存🧀
    🥗5. 文章的查询🥗

    🚕资源介绍🚕

    🛺swagger接口文档🛺

      

    🚙编辑器功能展示🚙

    🛻项目目录讲解🛻

    🎨前端🎨

    🌃后端🌃

    🚌部分代码展示🚌

    🍭前端 富文本编辑器页面App.vue🍭

    1. <script setup lang="ts">
    2. import "@wangeditor/editor/dist/css/style.css"; // 引入 css
    3. import {
    4. onBeforeUnmount,
    5. ref,
    6. shallowRef,
    7. onMounted,
    8. reactive
    9. } from "vue";
    10. import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
    11. import { IEditorConfig } from "@wangeditor/core";
    12. import { uploadPic, deleteFile, uploadVideo, toSaveArticleAndFile ,toQueryArticleApi} from "./request/api";
    13. import { resourceUrl } from "./common/path";
    14. import {
    15. IWangEPic,
    16. IWangEVid,
    17. IRichData,
    18. IToSaveAricle,
    19. IReQueryArticle
    20. } from "./pageTs/index";
    21. //图片 视频 类型声明
    22. const richData = reactive(new IRichData());
    23. const saveArticleData=reactive(new IToSaveAricle());
    24. // 编辑器实例,必须用 shallowRef
    25. const editorRef = shallowRef();
    26. // 内容 HTML
    27. const valueHtml = ref("");
    28. // 模拟 axios 异步获取内容
    29. onMounted(() => {
    30. setTimeout(() => {
    31. valueHtml.value = "

      大大帅将军 小小怪下士

      "
      ;
    32. }, 1500);
    33. });
    34. //编辑器初始化
    35. const toolbarConfig = {};
    36. const editorConfig: Partial<IEditorConfig> = { MENU_CONF: {} };
    37. // 编辑器创建完毕时的回调函数。
    38. const handleCreated = (editor: any) => {
    39. editorRef.value = editor; // 记录 editor 实例,重要!
    40. };
    41. //图片类型定义
    42. type InsertPicType = (url: string) => void;
    43. //图片上传
    44. editorConfig.MENU_CONF!["uploadImage"] = {
    45. // 自定义上传 InsertFnType
    46. async customUpload(richPic: File, insertFn: InsertPicType) {
    47. //图片上传接口调用
    48. uploadPic(richPic).then((res) => {
    49. console.log(res.data);
    50. //返回给编辑器 图片地址
    51. insertFn(resourceUrl + res.data);
    52. //上传成功后 记录图片地址
    53. richData.preFileList.push(res.data);
    54. });
    55. },
    56. };
    57. //上传视频 url 视频地址 poster 视频展示图片地址
    58. type InsertVidType = (url: string, poster: string) => void;
    59. editorConfig.MENU_CONF!["uploadVideo"] = {
    60. // 自定义上传
    61. async customUpload(file: File, insertFn: InsertVidType) {
    62. //视频上传接口调用
    63. uploadVideo(file).then((res) => {
    64. console.log(res.data);
    65. //返回给编辑器 图片地址
    66. insertFn(resourceUrl + res.data, "/src/assets/bg.png");
    67. //上传成功后 记录视频地址
    68. richData.preFileList.push(res.data);
    69. });
    70. },
    71. };
    72. // 组件销毁时,也及时销毁编辑器
    73. onBeforeUnmount(() => {
    74. const editor = editorRef.value;
    75. if (editor == null) return;
    76. editor.destroy();
    77. });
    78. //保存文章
    79. const toSaveArcitle = () => {
    80. const editor = editorRef.value;
    81. // 1.获取最后保存的文章图片 视频 list数组
    82. editor.getElemsByType("image").forEach((item: IWangEPic) => {
    83. //排除掉外部资源
    84. if(item.src.indexOf(resourceUrl) !=-1){
    85. richData.articleFileUrl.push(item.src);
    86. }
    87. });
    88. editor.getElemsByType("video").forEach((item: IWangEVid) => {
    89. if(item.src.indexOf(resourceUrl) !=-1){
    90. richData.articleFileUrl.push(item.src);
    91. }
    92. });
    93. //2.对于全部图片 视频 对比 获取已删除图片
    94. richData.preFileList.forEach((item) => {
    95. //articleFileList 数组展示的是图片完全路径
    96. //preFileList 保存的是图片部分路径
    97. //所以要通过添加resourceUrl常量进行对比
    98. if (richData.articleFileUrl.indexOf(resourceUrl + item) == -1) {
    99. //保存到需删除的数组中
    100. richData.deleteFileList.push(item);
    101. }else{
    102. //保存需要存入数据库的数组
    103. saveArticleData.articleFileUrl.push(item);
    104. }
    105. });
    106. //3.调后台接口 删除图片 视频
    107. deleteFile(richData.deleteFileList).catch((err) => {
    108. console.log(err.msg);
    109. });
    110. //4.调后台接口保存文章
    111. //参数赋值
    112. saveArticleData.articleName=richData.articleName;
    113. saveArticleData.articleAuthor=richData.articleAuthor;
    114. saveArticleData.articleContent=valueHtml.value;
    115. toSaveArticleAndFile(saveArticleData).then(res=>{
    116. console.log("保存文章成功");
    117. //保存文章后 所有数据清空
    118. valueHtml.value="";
    119. richData.articleAuthor="";
    120. richData.articleName="";
    121. })
    122. };
    123. //文章查询
    124. const queryArticle = reactive(new IReQueryArticle());
    125. const toQueryArticle=()=>{
    126. toQueryArticleApi(1566944471915032576n).then(res=>{
    127. console.log(res.data);
    128. queryArticle.articleAuthor=res.data.articleAuthor;
    129. queryArticle.articleContent=res.data.articleContent;
    130. queryArticle.articleId=res.data.articleId;
    131. queryArticle.gmtUpdate=res.data.gmtUpdate;
    132. queryArticle.articleName=res.data.articleName;
    133. })
    134. }
    135. script>
    136. <template>
    137. <div>
    138. 文章名称:<input
    139. class="demoInput"
    140. type="text"
    141. v-model="richData.articleName"
    142. placeholder="请输入文章名称"
    143. />
    144. div>
    145. <div>
    146. 文章作者:<input
    147. class="demoInput"
    148. type="text"
    149. v-model="richData.articleAuthor"
    150. placeholder="请输入文章作者"
    151. />
    152. div>
    153. <div style="border: 1px solid #ccc">
    154. <Toolbar
    155. style="border-bottom: 1px solid #ccc"
    156. :editor="editorRef"
    157. :defaultConfig="toolbarConfig"
    158. :mode="richData.model"
    159. />
    160. <Editor
    161. style="height: 550px; overflow-y: hidden"
    162. v-model="valueHtml"
    163. :defaultConfig="editorConfig"
    164. :mode="richData.model"
    165. @onCreated="handleCreated"
    166. />
    167. div>
    168. <button @click="toSaveArcitle">保存button>
    169. <div>==========================================div>
    170. <button @click="toQueryArticle">查询文章button>
    171. <div v-if="queryArticle!=null">
    172. <h1>{{queryArticle.articleName}}h1>
    173. <h5>{{queryArticle.articleAuthor}}h5>
    174. <h6>{{queryArticle.gmtUpdate}}h6>
    175. <div v-html="queryArticle.articleContent">
    176. div>
    177. div>
    178. template>
    179. <style lang="scss" scoped>
    180. .demoInput {
    181. outline-style: none;
    182. border: 1px solid #ccc;
    183. border-radius: 3px;
    184. padding: 13px 14px;
    185. width: 320px;
    186. font-size: 14px;
    187. font-weight: 320;
    188. margin: 20px;
    189. font-family: "Microsoft soft";
    190. }
    191. .demoInput:focus {
    192. border-color: #66afe9;
    193. outline: 0;
    194. -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
    195. 0 0 8px rgba(102, 175, 233, 0.6);
    196. box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
    197. 0 0 8px rgba(102, 175, 233, 0.6);
    198. }
    199. style>

    🧋后端 文章查询保存 serviceImpl🧋

    1. /**
    2. *

    3. * 服务实现类
    4. *

    5. *
    6. * @author 小王八
    7. * @since 2022-09-05
    8. */
    9. @Service
    10. public class ArticleServiceImpl extends ServiceImpl implements ArticleService {
    11. @Autowired
    12. private ArticleMapper articleMapper;
    13. @Autowired
    14. private ArticleFileService articleFileService;
    15. @Override
    16. @Transactional(rollbackFor = Exception.class)
    17. public String toSaveArticle(ToSaveArticle toSaveArticle) {
    18. //1.生成文章id 插入图片article_id
    19. long articleId = IdUtil.getSnowflakeNextId();
    20. //2.文章内容转换
    21. //过滤HTML文本,防止XSS攻击 可用 会清理掉html元素标签 只留下文本
    22. //String articleContent = HtmlUtil.filter(toSaveArticle.getArticleContent());
    23. //html=>安全字符
    24. String escape = HtmlUtil.escape(toSaveArticle.getArticleContent());
    25. //3.文章入数据库
    26. save(new Article()
    27. .setPkId(articleId)
    28. .setArticleName(toSaveArticle.getArticleName())
    29. .setArticleAuthor(toSaveArticle.getArticleAuthor())
    30. .setArticleContent(escape)
    31. );
    32. //4.文章资源入数据库
    33. if (ObjectUtil.isNotEmpty(toSaveArticle.getArticleFileUrl())){
    34. articleFileService.toSaveFile(toSaveArticle.getArticleFileUrl(),articleId);
    35. }
    36. return "文章保存成功";
    37. }
    38. @Override
    39. public ReShowArticle toShowArticle(Long articleId) {
    40. return ReShowArticle.toReShow(getById(articleId));
    41. }

    🚒功能演示 源码分享🚒

     关于功能的动态详细展示 

    我专门录制的一期B站视频 作为讲解

    🥝 具体源码🥝

    放在视频简介(gitee 前后端地址都有)

    【一款好用的富文本编辑器 wangEditor 前后端 vue3+springboot】 

    B站视频链接

    🥰制作不易  还望大家三连支持🥰

  • 相关阅读:
    SpringBoot整合RabbitMQ及其原理分析
    GET 请求和 POST 请求的区别和使用
    探索技术之上科技伦理,阿里巴巴成立科技伦理治理委员会
    【项目】仿muduo库One Thread One Loop式主从Reactor模型实现高并发服务器(TcpServer板块)
    C语言利用计算机找系统的最小通路集的算法
    java(抽象内部类)笔记
    视频剪辑软件Corel VideoStudio 会声会影2023新功能介绍及安装激活教程
    JavaScript数据类型BigInt实践之id数值太大,导致前后端交互异常
    【Golang】gin客户端动态加载Html格式文件
    动态链接库的使用记录
  • 原文地址:https://blog.csdn.net/Little___Turtle/article/details/126716931