网站首页 > 技术文章 正文
给大家分享一个好用的富文本编辑器
项目功能介绍
前后端分离
前端 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站视频链接
制作不易 还望大家三连支持
?
猜你喜欢
- 2025-06-03 基于OpenAI的新NLP文本编写APP—GPT-2,随时随地和你一起写作
- 2025-06-03 推荐6套非常热门的微信小程序开源项目
- 2025-06-03 腾讯开源的Markdown编辑器,开箱即用、轻量简洁、易扩展
- 2025-06-03 挖洞经验|UEditor编辑器存储型XSS漏洞
- 2025-06-03 介绍几款表单设计器(anyreport表单设计器)
- 2025-06-03 来了!JavaScript 最强大的 8 个 DOM API
- 2025-06-03 2014年最优秀JavaScript编辑器大盘点
- 2025-06-03 2021年最值得推荐的13个提高开发效率工具,程序员必备
- 2025-06-03 LATEX文本编辑器:MiKTeX 23.10(中文latex编辑器)
- 2025-06-03 所见即所得的 markdown 编辑器:Typora
你 发表评论:
欢迎- 06-24发现一款开源宝藏级工作流低代码快速开发平台
- 06-24程序员危险了,这是一个 无代码平台+AI+code做项目的案例
- 06-24一款全新的工作流,低代码快速开发平台
- 06-24如何用好AI,改造自己的设计工作流?
- 06-24濮阳网站开发(濮阳网站建设)
- 06-24AI 如何重塑前端开发,我们该如何适应
- 06-24应届生靠这个Java简历模板拿下了5个offer
- 06-24服务端性能测试实战3-性能测试脚本开发
- 567℃几个Oracle空值处理函数 oracle处理null值的函数
- 566℃Oracle分析函数之Lag和Lead()使用
- 550℃Oracle数据库的单、多行函数 oracle执行多个sql语句
- 545℃0497-如何将Kerberos的CDH6.1从Oracle JDK 1.8迁移至OpenJDK 1.8
- 543℃Oracle 12c PDB迁移(一) oracle迁移到oceanbase
- 536℃【数据统计分析】详解Oracle分组函数之CUBE
- 526℃最佳实践 | 提效 47 倍,制造业生产 Oracle 迁移替换
- 519℃Oracle有哪些常见的函数? oracle中常用的函数
- 最近发表
- 标签列表
-
- 前端设计模式 (75)
- 前端性能优化 (51)
- 前端模板 (66)
- 前端跨域 (52)
- 前端缓存 (63)
- 前端react (48)
- 前端aes加密 (58)
- 前端脚手架 (56)
- 前端md5加密 (54)
- 前端富文本编辑器 (47)
- 前端路由 (61)
- 前端数组 (73)
- 前端js面试题 (50)
- 前端定时器 (59)
- Oracle RAC (73)
- oracle恢复 (76)
- oracle 删除表 (48)
- oracle 用户名 (74)
- oracle 工具 (55)
- oracle 内存 (50)
- oracle 导出表 (57)
- oracle 中文 (51)
- oracle的函数 (57)
- 前端调试 (52)
- 前端登录页面 (48)
本文暂时没有评论,来添加一个吧(●'◡'●)