SpringBoot整合富文本编辑器

最近需要实现发帖子的功能, 考虑到帖子中会包含图片, 原本是想单独把图片设置出来单独上传, 后来想想觉得功能实现不够完整, 在网上看到有推荐使用富文本编辑器嵌入图片的, 于是打算用这个来实现发帖子的功能。

原本搜到了这一篇博客【JavaWeb】之富文本编辑器_´Code_Wang的博客-CSDN博客

原本打算用第一个TinyMCE ,看了一会文档发现官网文档给的上传demo 后端是php ,于是又搜到了集成Editor.md

https://gitee.com/pandao/editor.md

下面总结一下Editor.md 用法

使用springboot+ thymeleaf

Editor.md demo

下载editor.md

Editor下载https://gitee.com/pandao/editor.md

下载后解压导入springboot项目资源路径即可。

文件目录结构如下

可以没必要的示例文件和不需要的功能组件删除

设计数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
create table `t_topic` (
`topic_id` bigint (20),
`user_id` bigint (20),
`tag_id` bigint (20),
`topic_title` varchar (3072),
`topic_content` text ,
`comment_count` bigint (20),
`like_count` bigint (20),
`is_selected` tinyint (4),
`create_time` timestamp ,
`is_delete` int (11),
`update_time` timestamp
);

导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>zzu_miao</groupId>
<artifactId>zzu_miao</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>zzu_miao</name>
<description>zzu_miao</description>

<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--腾讯云cos的依赖jar包-->
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>5.6.89</version>
</dependency>

<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>cos-sts-java</artifactId>
<version>3.0.5</version>
</dependency>

<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
<!--FileUtil-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.10</version>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.7.RELEASE</version>
<configuration>
<mainClass>com.miao.ZzuMiaoApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
spring:
application:
name: zzu_miao
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/zzu_miao?serverTimezone=UTC
username: root
password: ******
rsocket:
server:
port: 8080

jackson:
default-property-inclusion: non_null # JSON处理时忽略非空字段

servlet:
multipart:
max-file-size: 20MB

thymeleaf:
prefix: classpath:/templates/
suffix: .html
mybatis-plus:
mapper-locations:
classpath: mappers/*xml
type-aliases-package: com.miao.domain
global-config:
db-config:
# 配置mybatis-plus 逻辑删除
logic-delete-field: is_delete # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

实体类

TopicDTO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Data
public class TopicDTO {
/**
* 帖子标题
*/
private String topicTitle;

/**
* 帖子内容
*/
private String topicContent;

/**
* 标签
*/
private String tags;

/**
* 作者名称
*/
private String author;
}

Topic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@TableName(value ="t_topic")
@Data
public class Topic implements Serializable {
/**
* 帖子id
*/
@TableId(type = IdType.AUTO)
private Long topicId;

/**
* 发帖用户的id
*/
private Long userId;



/**
* 帖子标题
*/
private String topicTitle;

/**
* 帖子内容
*/
private String topicContent;

/**
* 评论总数
*/
private Long commentCount;

/**
* 点赞总数
*/
private Long likeCount;

/**
* 是否精选
*/
private Integer isSelected;

/**
* 帖子发布时间
*/
private Date createTime;

/**
* 逻辑删除 1表示删除
*/
@TableLogic
private Integer isDelete;

/**
* 话题更新时间
*/
private Date updateTime;

@TableField(exist = false)
private static final long serialVersionUID = 1L;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Topic)) return false;
Topic topic = (Topic) o;
return Objects.equals(getTopicId(), topic.getTopicId()) && Objects.equals(getUserId(), topic.getUserId()) && Objects.equals(getTopicTitle(), topic.getTopicTitle()) && Objects.equals(getTopicContent(), topic.getTopicContent()) && Objects.equals(getCommentCount(), topic.getCommentCount()) && Objects.equals(getLikeCount(), topic.getLikeCount()) && Objects.equals(getIsSelected(), topic.getIsSelected()) && Objects.equals(getCreateTime(), topic.getCreateTime()) && Objects.equals(getIsDelete(), topic.getIsDelete()) && Objects.equals(getUpdateTime(), topic.getUpdateTime());
}

@Override
public int hashCode() {
return Objects.hash(getTopicId(), getUserId(), getTopicTitle(), getTopicContent(), getCommentCount(), getLikeCount(), getIsSelected(), getCreateTime(), getIsDelete(), getUpdateTime());
}
}

mapper

mapper接口

  • 使用mybatis-plus生成
1
2
3
public interface TopicMapper extends BaseMapper<Topic> {

}

mapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.miao.mapper.TopicMapper">

<resultMap id="BaseResultMap" type="com.miao.domain.Topic">
<id property="topicId" column="topic_id" jdbcType="BIGINT"/>
<result property="userId" column="user_id" jdbcType="BIGINT"/>
<result property="tagId" column="tag_id" jdbcType="BIGINT"/>
<result property="topicTitle" column="topic_title" jdbcType="VARCHAR"/>
<result property="topicContent" column="topic_content" jdbcType="VARCHAR"/>
<result property="commentCount" column="comment_count" jdbcType="BIGINT"/>
<result property="likeCount" column="like_count" jdbcType="BIGINT"/>
<result property="isSelected" column="is_selected" jdbcType="TINYINT"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="isDelete" column="is_delete" jdbcType="INTEGER"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
</resultMap>

<sql id="Base_Column_List">
topic_id,user_id,tag_id,
topic_title,topic_content,comment_count,
like_count,is_selected,create_time,
is_delete,update_time
</sql>
</mapper>

编辑视图editorTest.html

注意

  • 视图的处理千万要小心,注意资源文件的位置,资源文件的路径一旦设置错误,将无法显示。
  • 页面内js的路径要设置明白,一定要改成自己的路径。
  • 如果页面无法正常显示或者跳转,一定是路径问题,跳转路径也要注意。
  • 注意表单中的name 要与后端实体类的属性名对应
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" th:href="@{~/editormd/examples/css/style.css}" />
<link rel="stylesheet" th:href="@{~/editormd/css/editormd.css}" />
<link rel="shortcut icon" href="https://pandao.github.io/editor.md/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" th:href="@{~/editormd/css/editormd.preview.css}"/>
<script th:src="@{~/editormd/editormd.js}"></script>
<script th:src="@{~/editormd/examples/js/jquery.min.js}"></script>
<script th:src="@{~/editormd/editormd.min.js}"></script>
<script th:src="@{~/js/editor.js}"></script>
</head>

<body>
<div id="layout">
<div>
<header>
<h1>Simple example</h1>
</header>
</div>
<div>
<form name="mdEditorForm">
<div>标题: <input type="text" name="topicTitle">
</div>
<div id="test-editormd">
<textarea name="topicContent" id="content" style="display:none;">
</textarea>
</div>
</form>
</div>
</div>
</body>
</html>

编写js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
var testEditor;
$(function() {
testEditor = editormd("test-editormd", {
width : "60%",
height : 640,
syncScrolling : "single",
path : "../editormd/lib/",
saveHTMLToTextarea: true, // 保存 HTML 到 Textarea
emoji: true,
// location: left,
theme: "light",//工具栏主题
previewTheme: "light",//预览主题
editorTheme: "pastel-on-light",//编辑主题
tex: true, // 开启科学公式TeX语言支持,默认关闭
flowChart: true, // 开启流程图支持,默认关闭
sequenceDiagram: true, // 开启时序/序列图支持,默认关闭,
// 图片上传
imageUpload: true,
imageFormats: ["jpg", "jpeg", "gif", "png", "bmp", "webp"],
imageUploadURL: "/test/file", //上传图片的请求接口
onload: function () {
console.log('onload', this);
},
/*指定需要显示的功能按钮*/
toolbarIcons: function () {
return ["undo", "redo", "|", "bold", "del", "italic", "quote", "ucwords", "uppercase", "lowercase", "|", "h1", "h2", "h3", "h4", "h5", "h6", "|", "list-ul", "list-ol", "hr", "|", "link", "reference-link", "image", "code", "preformatted- text", "code-block", "table", "datetime", "emoji", "html- entities", "pagebreak", "|", "goto- line", "watch", "preview", "fullscreen", "clear", "search", "|", "help", "info", "releaseIcon", "index"]
},
/*自定义功能按钮,下面我自定义了2个,一个是发布,一个是返回首页*/
toolbarIconTexts: {
releaseIcon: "<span bgcolor=\"gray\">发布</span>",
index: "<span bgcolor=\"red\">返回首页</span>",
},
/*给自定义按钮指定回调函数*/
toolbarHandlers: {
releaseIcon: function (cm, icon, cursor, selection) {
//表单提交
mdEditorForm.method = "post";
mdEditorForm.action = "/test/addTopic";
//提交至服务 器的路径
mdEditorForm.submit();
},
index: function () {
window.location.href = '/';
},
}
});
});

编写控制层代码

  • CosClientUtil : 这里上传图片到腾讯云cos对象存储
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
@RequestMapping("/test")
@Controller
@Slf4j
public class TestController {
@Resource
TopicMapper topicMapper;
@Resource
UserMapper userMapper;
@Resource
CosClientUtil cosClientUtil;

/**
* 根据id 查询 文章
* @param id topicID
* @param model
* @return 定位到thymeleaf 解析器
*/
@GetMapping("/{id}")
public String show(@PathVariable("id") int id, Model model) {
Topic topic = topicMapper.selectById(id);
TopicDTO topicDTO = BeanUtil.copyProperties(topic, TopicDTO.class);
Long userId = 1L;
User user = userMapper.selectById(userId);
topicDTO.setAuthor(user.getUserName());
log.info("帖子标题: {}",topicDTO.getTopicTitle());
model.addAttribute("topicDTO", topicDTO);
return "showTopic";
}

@GetMapping("/toEdit")
public String toEdit() {
log.info("/toEdit");
return "editorTest";
}

@GetMapping("/toTiny")
public String toTiny() {
log.info("/toEdit");
return "TinyMCE";
}

@PostMapping("/file")
@ResponseBody
//editormd-image-file
public Object uploadTopicPicTure(@RequestParam(value = "editormd-image-file") MultipartFile image){
String url = uploadFile(CosClientUtil.TOPIC_FILE, image);
System.out.println("picture url :"+url);
log.info("/uploadTopicPicTure");
Map<String, String> map = new HashMap<>();
map.put("location",url);
return map;
}

@PostMapping("/addTopic")
public String addTopic(TopicDTO topicDTO,Model model) {
log.info("/addTopic");
Long userId= 1L;
// String url = uploadFile(CosClientUtil.TOPIC_FILE);
Topic topic = BeanUtil.copyProperties(topicDTO, Topic.class);
if(topicDTO.getTopicContent()==null){
log.info("topicDTO content null");
}
if(topic.getTopicContent()==null){
log.info("topic content null");
}
topic.setUserId(userId);
// topic.setPictureUrls(url);
int insert = topicMapper.insert(topic);
model.addAttribute("topicDTO",topicDTO);
return "showTopic";
}

/**
* 上传图片
* @param image 需要上传的图片
* @return 返回json对象
*/
@ResponseBody
public BaseResponse<String> uploadPicTure(MultipartFile image) {
String url = uploadFile(CosClientUtil.TOPIC_FILE, image);
System.out.println("picture url :"+url);
return ResultUtil.success(url);
}

/**
* 上传图片
* @param folder 目录
* @param images 图片
* @return 返回上传的图片的 url (多个url使用, 拼接)
*/
private String uploadFile(String folder,MultipartFile...images){
if(images==null||images.length==0){
return "";
}
String[] fileUrls = new String[images.length];
int idx=0;
StringBuilder urls=null;
for (MultipartFile image : images) {
try {
File file = MyFileUtil.multipartFileToFile(image);
MyFileUtil.checkFile(file);
fileUrls[idx++] = cosClientUtil.uploadFile(file, folder);
MyFileUtil.deleteTempFile(file);
} catch (Exception e) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "图片上传失败");
}
urls = new StringBuilder(fileUrls[0]);
for (int i = 1; i < fileUrls.length; i++) {
urls.append(",").append(fileUrls[i]);
}
}
return urls.toString();
}
}

文章详情视图article.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title th:text="${topicDTO.topicTitle}" >showTopic</title>
<link rel="shortcut icon" href="https://pandao.github.io/editor.md/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" th:href="@{~/editormd/css/editormd.preview.css}"/>
<script th:src="@{~/editormd/examples/js/jquery.min.js}"></script>
<script th:src="@{~/editormd/lib/marked.min.js}"></script>
<script th:src="@{~/editormd/lib/prettify.min.js}"></script>
<script th:src="@{~/editormd/lib/raphael.min.js}"></script>
<script th:src="@{~/editormd/lib/underscore.min.js}"></script>
<script th:src="@{~/editormd/lib/sequence-diagram.min.js}"></script>
<script th:src="@{~/editormd/lib/flowchart.min.js}"></script>
<script th:src="@{~/editormd/lib/jquery.flowchart.min.js}"></script>
<script th:src="@{~/editormd/editormd.js}"></script>
<script type="text/javascript">
var testEditor;
$(function () {
testEditor = editormd.markdownToHTML("doc-content", {
//注意:这里是上面 DIV的id
htmlDecode: "style,script,iframe",
emoji: true,
taskList: true,
tex: true, // 默认不解析
flowChart: true, // 默认不解析
sequenceDiagram: true, // 默认不解析
codeFold: false
});
});
</script>
</head>
<body>

<div>
<!--文章头部信息:标题,作者,最后更新日期,导航-->
<h2 style="margin: auto 0" th:text="${topicDTO.topicTitle}">帖子标题</h2>
<span style="float: left" th:text="${topicDTO.author}">帖子作者</span>
<!--文章主体内容-->

<div id="doc-content">
<textarea style="display:none;" placeholder="markdown" th:text="${topicDTO.topicContent}">
</textarea>
</div>
</div>
</body>
</html>

测试内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 # 标题一号
这是个老虎![老虎图片](https://zzu-miao-1025-1308522872.cos.ap-nanjing.myqcloud.com/topic/5b445890-eb86-41f9-a052-52a2eb7d6aa5.jpg "老虎图片")
```java
//java代码测试
private static void inputStreamToFile(InputStream ins, File file) {
try {
OutputStream os = new FileOutputStream(file);
int bytesRead = 0;
byte[] buffer = new byte[8192];
while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.close();
ins.close();
} catch (Exception e) {
e.printStackTrace();
}
}

```
害怕**老虎吗**

显示内容

数据库

可以看到数据库中存储的是Markdown语法的文件

踩坑总结

  1. 引入js的问题

    • 文件问题

      使用thymeleaf渲染的时候, 引入js / css文件的时候, 务必采用thymeleaf的写法, 直接引用文件可能会失效

    • 路径问题

      thymeleaf 使用@{..}引用文件 , 我的静态文件都在static目录下 , 直接用

      <script th:src="@{/editormd/lib/prettify.min.js}"></script>不行, 引用失败

      需要加一个~ 也就变成<script th:src="@{~/editormd/lib/prettify.min.js}"></script>

  2. 配置问题

    就是引入的框架的配置问题

  3. 表单的 name => 要与后端的实体类的属性名对应 , 要么就是加上@RequestParam注解, 但是有时候属性多了就比较麻烦

  4. 关于文件配置

    静态的文件 , 比如js / css这种最好是直接保存到 resource/static ,不要自己乱写目录, 免得节外生枝

  5. spring Model的级别

    request级别

使用TinyMCE

踩了这么多坑, 然后再去试试TinyMCE, 结果也成功了

别的不再赘述

TinyMCE.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdn.tiny.cloud/1/ubo1qbw1ajg3o7qid7btw58tvxay3n7pccrwufntqxuqvhnd/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
<!-- <script th:src="@{~/tinymce/js/tinymce/tinymce.min.js}" referrerpolicy="origin"></script>-->
<script th:src="@{~/js/tinyInit.js}"></script>
</head>
<body>
<h1>TinyMCE Quick Start Guide</h1>

<div>
<header>
<h1>Simple example</h1>
</header>
</div>
<div>
<form name="mdEditorForm" method="post" th:action="@{/test/file}" enctype="multipart/form-data">
标题: <input type="text" name="topicTitle">
<textarea name="topicContent" id="editable"> </textarea>
<input type="submit" value="提交" onclick="submitTopic()"/>
</form>
</div>

</body>
</html>

js代码

js代码这里踩了很多坑, 本来就不会前端

tinymce 自定义图片上传 Cannot read properties of undefined (reading ‘then‘)_韩小大大的博客-CSDN博客

查找了很多博客, 最后删删改改代码也能跑起来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
const images_upload_handler = (blobInfo, progress) => new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.withCredentials = false;
baseURL = "http://localhost:8080/";
xhr.open('POST', baseURL + "test/file");
xhr.upload.onprogress = (e) => {
progress(e.loaded / e.total * 100);
};
xhr.onload = () => {
var json;
if (xhr.status != 200) {
reject('HTTP Error: ' + xhr.status);
return;
}
console.log(xhr.responseText);
json = JSON.parse(xhr.responseText);
// console.log(json.url);
// json.location = json.url;
if (!json || typeof json.location != 'string') {
reject('Invalid JSON: ' + xhr.responseText);
return;
}
resolve(json.location);
};
xhr.onerror = () => {
reject('Image upload failed due to a XHR Transport error. Code: ' + xhr.status);
};
const formData = new FormData();
formData.append('file', blobInfo.blob(), blobInfo.filename());
xhr.send(formData);
});

tinymce.init({
selector: 'textarea#editable', // 前面是标签 , 后面是 id
language: 'zh_CN', //设置语言
plugins: 'codesample | advlist | link | image | lists |uploadimage | |searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking save table contextmenu directionality emoticons template paste textcolor colorpicker textpattern imagetools codesample toc help uploadimage',
/* without images_upload_url set, Upload tab won't show up*/
images_upload_url: '/test/file',//图片上传地址
images_upload_credentials: true,
image_dimensions: false,
paste_word_valid_elements: '*[*]', // word需要它
paste_data_images: true, // 粘贴的同时能把内容里的图片自动上传
paste_convert_word_fake_lists: false, // 插入word文档需要该属性
automatic_uploads: true,
file_picker_types: 'image',
image_class_list: [
{ title: '无', value: '' },
{ title: '预览', value: 'preview' },
],
toolbar: 'undo redo | image | code',
/*自定义功能按钮,下面我自定义了2个,一个是发布,一个是返回首页*/
toolbarIconTexts: {
releaseIcon: "<span bgcolor=\"gray\">发布</span>",
index: "<span bgcolor=\"red\">返回首页</span>",
},
/*给自定义按钮指定回调函数*/
toolbarHandlers: {
releaseIcon: function (cm, icon, cursor, selection) {
//表单提交
mdEditorForm.method = "post";
mdEditorForm.action = "/test/addTopic";
//提交至服务 器的路径
mdEditorForm.submit();
},
index: function () {
window.location.href = '/';
},
},
//第一行菜单
toolbar1: 'index releaseIcon undo redo insert styleselect bold italic alignleft aligncenter alignright alignjustify bullist numlist outdent indent',
toolbar2: 'forecolor backcolor | codesample link image',//第二行菜单
image_advtab: true,
menubar: false,
codesample_languages: [
{ text: 'HTML/XML', value: 'markup' },
{ text: 'JavaScript', value: 'javascript' },
{ text: 'Python', value: 'python' },
{ text: 'Java', value: 'java' },
{ text: 'C', value: 'c' },
{ text: 'C++', value: 'cpp' }
],
/* we override default upload handler to simulate successful upload*/
images_upload_handler: images_upload_handler,
content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:20px }',
});
const submitTopic= function (cm, icon, cursor, selection) {
//表单提交
mdEditorForm.method = "post";
mdEditorForm.action = "/test/addTopic";
//提交至服务 器的路径
mdEditorForm.submit();
}
function updateValue(editId)
{
var textValue = document.getElementById(editId);
textValue.value = tinyMCE.getInstanceById(editId).getBody().innerHTML;
}

showTiny.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<script src="https://cdn.tiny.cloud/1/ubo1qbw1ajg3o7qid7btw58tvxay3n7pccrwufntqxuqvhnd/tinymce/6/tinymce.min.js"
referrerpolicy="origin"></script>
<!-- <script th:src="@{~/tinymce/js/tinymce/tinymce.min.js}" referrerpolicy="origin"></script>-->
<script th:src="@{~/js/tinyInit.js}"></script>
</head>
<body>
<div>
<!--文章头部信息:标题,作者,最后更新日期,导航-->
<h2 style="margin: auto 0" th:text="${topicDTO.topicTitle}">帖子标题</h2>
<span style="float: left" th:text="${topicDTO.author}">帖子作者</span>
<!--文章主体内容-->
<textarea id="editable" th:text="${topicDTO.topicContent}">
</textarea>
</div>

</body>
</html>

测试

编辑帖子

查看帖子

数据库

可以看到数据库中是存储的html标签

总结

两者上手难度相差不大 , 区别就是底层得到存储

一个是存储的Markdown格式的文件 , 另一个是html标签