百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 博客教程 > 正文

一款好用的富文本编辑器「wangeditor」运用(附源码+视频讲解)

connygpt 2024-10-05 14:49 3 浏览




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





项目功能介绍


前后端分离

前端 vue3+typescript+wangEditor+axios

后端 springboot+mybatis-plus+swagger

项目精简 不引入过多框架

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

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


资源介绍



swagger接口文档




编辑器功能展示




项目目录讲解


前端



后端



部分代码展示


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


<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>



后端 文章查询保存 serviceImpl


/**
 * <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站视频链接

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



?

相关推荐

自学Python,写一个挨打的游戏代码来初识While循环

自学Python的第11天。旋转~跳跃~,我~闭着眼!学完循环,沐浴着while的光芒,闲来无事和同事一起扯皮,我说:“编程语言好神奇,一个小小的循环,竟然在生活中也可以找到原理和例子”,同事也...

常用的 Python 工具与资源,你知道几个?

最近几年你会发现,越来越多的人开始学习Python,工欲善其事必先利其器,今天纬软小编就跟大家分享一些常用的Python工具与资源,记得收藏哦!不然下次就找不到我了。1、PycharmPychar...

一张思维导图概括Python的基本语法, 一周的学习成果都在里面了

一周总结不知不觉已经自学Python一周的时间了,这一周,从认识Python到安装Python,再到基本语法和基本数据类型,对于小白的我来说无比艰辛的,充满坎坷。最主要的是每天学习时间有限。只...

三日速成python?打工人,小心钱包,别当韭菜

随着人工智能的热度越来越高,许多非计算机专业的同学们也都纷纷投入到学习编程的道路上来。而Python,作为一种相对比较容易上手的语言,也越来越受欢迎。网络上各类网课层出不穷,各式广告令人眼花缭乱。某些...

Python自动化软件测试怎么学?路线和方法都在这里了

Python自动化测试是指使用Python编程语言和相关工具,对软件系统进行自动化测试的过程。学习Python自动化测试需要掌握以下技术:Python编程语言:学习Python自动化测试需要先掌握Py...

Python从放弃到入门:公众号历史文章爬取为例谈快速学习技能

这篇文章不谈江流所专研的营销与运营,而聊一聊技能学习之路,聊一聊Python这门最简单的编程语言该如何学习,我完成的第一个Python项目,将任意公众号的所有历史文章导出成PDF电子书。或许我这个Py...

【黑客必会】python学习计划

阅读Python文档从Python官方网站上下载并阅读Python最新版本的文档(中文版),这是学习Python的最好方式。对于每个新概念和想法,请尝试运行一些代码片段,并检查生成的输出。这将帮助您更...

公布了!2025CDA考试安排

CDA数据分析师报考流程数据分析师是指在不同行业中专门从事行业数据搜集、整理、分析依据数据作出行业研究评估的专业人员CDA证书分为1-3级,中英文双证就业面广,含金量高!!?报考条件:满18...

一文搞懂全排列、组合、子集问题(经典回溯递归)

原创公众号:【bigsai】头条号:程序员bigsai前言Hello,大家好,我是bigsai,longtimenosee!在刷题和面试过程中,我们经常遇到一些排列组合类的问题,而全排列、组合...

「西法带你学算法」一次搞定前缀和

我花了几天时间,从力扣中精选了五道相同思想的题目,来帮助大家解套,如果觉得文章对你有用,记得点赞分享,让我看到你的认可,有动力继续做下去。467.环绕字符串中唯一的子字符串[1](中等)795.区...

平均数的5种方法,你用过几种方法?

平均数,看似很简单的东西,其实里面包含着很多学问。今天,分享5种经常会用到的平均数方法。1.算术平均法用到最多的莫过于算术平均法,考试平均分、平均工资等等,都是用到这个。=AVERAGE(B2:B11...

【干货收藏】如何最简单、通俗地理解决策树分类算法?

决策树(Decisiontree)是基于已知各种情况(特征取值)的基础上,通过构建树型决策结构来进行分析的一种方式,是常用的有监督的分类算法。决策树算法是机器学习中的一种经典算法,它通过一系列的规则...

面试必备:回溯算法详解

我们刷leetcode的时候,经常会遇到回溯算法类型题目。回溯算法是五大基本算法之一,一般大厂也喜欢问。今天跟大家一起来学习回溯算法的套路,文章如果有不正确的地方,欢迎大家指出哈,感谢感谢~什么是回溯...

「机器学习」决策树——ID3、C4.5、CART(非常详细)

决策树是一个非常常见并且优秀的机器学习算法,它易于理解、可解释性强,其可作为分类算法,也可用于回归模型。本文将分三篇介绍决策树,第一篇介绍基本树(包括ID3、C4.5、CART),第二篇介绍Ran...

大话AI算法: 决策树

所谓的决策树算法,通俗的说就是建立一个树形的结构,通过这个结构去一层一层的筛选判断问题是否好坏的算法。比如判断一个西瓜是否好瓜,有20条西瓜的样本提供给你,让你根据这20条(通过机器学习)建立起...