- 重点学习无限极树的管理功能设计&富文本编辑框的使用
- 文档表设计与代码生成
- 按照分类管理的代码,复制出一套文档树管理
- 关于无限极树的增删改查功能开发
- 文档内容保存于显示:富文本框的使用
- 首页点击某个电子书时,跳转到文档页面,显示文档树
- 点击某个文档时,加载文档内容
✿文档表设计与代码生成
1. 文档表设计
给文档表起名doc,然后sql如下:
drop table if exists `doc`;create table `doc` (`id` bigint not null comment 'id',`ebook_id` bigint not null default 0 comment '电子书id',`parent` bigint not null default 0 comment '父id',`name` varchar(50) not null comment '名称',`sort` int comment '排序',`view_count` int default 0 comment '阅读数',`vote_count` int default 0 comment '点赞数',primary key (`id`)) engine=innodb default charset=utf8mb4 comment='文档';insert into `doc` (id, ebook_id, parent,name, sort, view_count, vote_count) values (1,1,0,'文档1',1,0,0);insert into `doc` (id, ebook_id, parent,name, sort, view_count, vote_count) values (2,1,1,'文档1.1',1,0,0);insert into `doc` (id, ebook_id, parent,name, sort, view_count, vote_count) values (3,1,0,'文档2',2,0,0);insert into `doc` (id, ebook_id, parent,name, sort, view_count, vote_count) values (4,1,3,'文档2.1',1,0,0);insert into `doc` (id, ebook_id, parent,name, sort, view_count, vote_count) values (5,1,3,'文档2.2',2,0,0);insert into `doc` (id, ebook_id, parent,name, sort, view_count, vote_count) values (6,1,5,'文档2.2.1',1,0,0);
2. 生成持久层代码
生成持久层代码,依旧去generator-config.xml,将最后一行的table改成doc,然后去执行mybatis-generator这个命令,去生成四个文件,依次是Doc.java,DocExample.java,DocMapper.java,DocMapper.xml。
<table tableName="doc"/>
✿文档表增删改查
1. 后端改造
按照分类管理,复制出一套文档管理的代码,每个文件复制过来之后使用ctrl + R将关键字全部改掉即可,代码如下:
package com.taopoppy.wiki.controller;import com.taopoppy.wiki.req.DocQueryReq;import com.taopoppy.wiki.req.DocSaveReq;import com.taopoppy.wiki.resp.DocQueryResp;import com.taopoppy.wiki.resp.CommonResp;import com.taopoppy.wiki.resp.PageResp;import com.taopoppy.wiki.service.DocService;import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;import javax.validation.Valid;import java.util.List;@RestController@RequestMapping("/doc")public class DocController {@Resourceprivate DocService docService;@GetMapping("/all")public CommonResp all(@Valid DocQueryReq req) {// controller层尽量不要出现Doc这个实体类// 因为实体类是和数据库一一对应的CommonResp<List<DocQueryResp>> resp = new CommonResp<>();List<DocQueryResp> list = docService.all(req);resp.setContent(list);return resp;}@GetMapping("/list")public CommonResp list(@Valid DocQueryReq req) {// controller层尽量不要出现Doc这个实体类// 因为实体类是和数据库一一对应的CommonResp<PageResp<DocQueryResp>> resp = new CommonResp<>();PageResp<DocQueryResp> list = docService.list(req);resp.setContent(list);return resp;}@PostMapping("/save")public CommonResp save(@Valid @RequestBody DocSaveReq req) {CommonResp resp = new CommonResp<>();docService.save(req);return resp;}@DeleteMapping("/delete/{id}")public CommonResp delete(@PathVariable long id) {CommonResp resp = new CommonResp<>();docService.delete(id);return resp;}}
package com.taopoppy.wiki.service;import com.github.pagehelper.PageHelper;import com.github.pagehelper.PageInfo;import com.taopoppy.wiki.domain.Doc;import com.taopoppy.wiki.domain.DocExample;import com.taopoppy.wiki.mapper.DocMapper;import com.taopoppy.wiki.req.DocQueryReq;import com.taopoppy.wiki.req.DocSaveReq;import com.taopoppy.wiki.resp.DocQueryResp;import com.taopoppy.wiki.resp.PageResp;import com.taopoppy.wiki.util.CopyUtil;import com.taopoppy.wiki.util.SnowFlake;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Service;import org.springframework.util.ObjectUtils;import javax.annotation.Resource;import java.util.List;@Servicepublic class DocService {// 这个就是用来打印的,在WikiApplication当中也用到了private static final Logger LOG = LoggerFactory.getLogger(DocService.class);@Resourceprivate DocMapper docMapper;@Resourceprivate SnowFlake snowFlake;/*** 查询分类* @param req 分类查询参数* @return 分类分页列表*/public List<DocQueryResp> all(DocQueryReq req) {DocExample docExample = new DocExample();docExample.setOrderByClause("sort asc"); // 按照sort排序List<Doc> docsList = docMapper.selectByExample(docExample);List<DocQueryResp> respList = CopyUtil.copyList(docsList, DocQueryResp.class);return respList;}/*** 查询分类* @param req 分类查询参数* @return 分类分页列表*/public PageResp<DocQueryResp> list(DocQueryReq req) {DocExample docExample = new DocExample();docExample.setOrderByClause("sort asc"); // 按照sort排序// createCriteria相当于where条件DocExample.Criteria criteria = docExample.createCriteria();// 根据docExample条件查询PageHelper.startPage(req.getPage(), req.getSize());List<Doc> docsList = docMapper.selectByExample(docExample);PageInfo<Doc> pageInfo = new PageInfo<>(docsList);LOG.info("总行数:{}",pageInfo.getTotal()); // 总行数LOG.info("总页数:{}",pageInfo.getPages()); // 总页数,一般不需要,前端会根据总数自动计算总页数List<DocQueryResp> respList = CopyUtil.copyList(docsList, DocQueryResp.class);PageResp<DocQueryResp> pageResp = new PageResp();pageResp.setTotal(pageInfo.getTotal());pageResp.setList(respList);return pageResp;}/*** 分类保存接口(保存包括编辑保存->更新, 也包括新增保存->新增, 根据req是否有id来判断)* @param req 分类保存参数*/public void save(DocSaveReq req) {Doc doc = CopyUtil.copy(req, Doc.class);if(ObjectUtils.isEmpty(req.getId())) {// 新增doc.setId(snowFlake.nextId());docMapper.insert(doc);} else {// 更新// 因为updateByPrimaryKey传递的Doc类型的参数,所以需要将DocSaveReq 转化成DocdocMapper.updateByPrimaryKey(doc);}}/*** 分类删除接口(按照id进行删除)* @param id*/public void delete(long id) {docMapper.deleteByPrimaryKey(id);}}
package com.taopoppy.wiki.req;public class DocQueryReq extends PageReq{@Overridepublic String toString() {return "DocQueryReq{} " + super.toString();}}
package com.taopoppy.wiki.req;import javax.validation.constraints.NotNull;public class DocSaveReq {private Long id;@NotNull(message = "【电子书】不能为空")private Long ebookId;@NotNull(message = "【父文档】不能为空")private Long parent;@NotNull(message = "【名称】不能为空")private String name;@NotNull(message = "【排序】不能为空")private Integer sort;private Integer viewCount;private Integer voteCount;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public Long getEbookId() {return ebookId;}public void setEbookId(Long ebookId) {this.ebookId = ebookId;}public Long getParent() {return parent;}public void setParent(Long parent) {this.parent = parent;}public String getName() {return name;}public void setName(String name) {this.name = name;}public Integer getSort() {return sort;}public void setSort(Integer sort) {this.sort = sort;}public Integer getViewCount() {return viewCount;}public void setViewCount(Integer viewCount) {this.viewCount = viewCount;}public Integer getVoteCount() {return voteCount;}public void setVoteCount(Integer voteCount) {this.voteCount = voteCount;}@Overridepublic String toString() {StringBuilder sb = new StringBuilder();sb.append(getClass().getSimpleName());sb.append(" [");sb.append("Hash = ").append(hashCode());sb.append(", id=").append(id);sb.append(", ebookId=").append(ebookId);sb.append(", parent=").append(parent);sb.append(", name=").append(name);sb.append(", sort=").append(sort);sb.append(", viewCount=").append(viewCount);sb.append(", voteCount=").append(voteCount);sb.append("]");return sb.toString();}}
package com.taopoppy.wiki.resp;public class DocQueryResp {private Long id;private Long ebookId;private Long parent;private String name;private Integer sort;private Integer viewCount;private Integer voteCount;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public Long getEbookId() {return ebookId;}public void setEbookId(Long ebookId) {this.ebookId = ebookId;}public Long getParent() {return parent;}public void setParent(Long parent) {this.parent = parent;}public String getName() {return name;}public void setName(String name) {this.name = name;}public Integer getSort() {return sort;}public void setSort(Integer sort) {this.sort = sort;}public Integer getViewCount() {return viewCount;}public void setViewCount(Integer viewCount) {this.viewCount = viewCount;}public Integer getVoteCount() {return voteCount;}public void setVoteCount(Integer voteCount) {this.voteCount = voteCount;}@Overridepublic String toString() {StringBuilder sb = new StringBuilder();sb.append(getClass().getSimpleName());sb.append(" [");sb.append("Hash = ").append(hashCode());sb.append(", id=").append(id);sb.append(", ebookId=").append(ebookId);sb.append(", parent=").append(parent);sb.append(", name=").append(name);sb.append(", sort=").append(sort);sb.append(", viewCount=").append(viewCount);sb.append(", voteCount=").append(voteCount);sb.append("]");return sb.toString();}}
2. 前端代码改造
<template><a-layout><a-layout-content :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"><h3 v-if="level1.length === 0">对不起,找不到相关文档!</h3><a-row><a-col :span="6"><a-treev-if="level1.length > 0":tree-data="level1"@select="onSelect":replaceFields="{title: 'name', key: 'id', value: 'id'}":defaultExpandAll="true":defaultSelectedKeys="defaultSelectedKeys"></a-tree></a-col><a-col :span="18"><div><h2>{{doc.name}}</h2><div><span>阅读数:{{doc.viewCount}}</span> <span>点赞数:{{doc.voteCount}}</span></div><a-divider style="height: 2px; background-color: #9999cc"/></div><div class="wangeditor" :innerHTML="html"></div><div class="vote-div"><a-button type="primary" shape="round" :size="'large'" @click="vote"><template #icon><LikeOutlined /> 点赞:{{doc.voteCount}} </template></a-button></div></a-col></a-row></a-layout-content></a-layout></template><script lang="ts">import { defineComponent, onMounted, ref, createVNode } from 'vue';import axios from 'axios';import {message} from 'ant-design-vue';import {Tool} from "@/util/tool";import {useRoute} from "vue-router";export default defineComponent({name: 'Doc',setup() {const route = useRoute();const docs = ref();const html = ref();const defaultSelectedKeys = ref();defaultSelectedKeys.value = [];// 当前选中的文档const doc = ref();doc.value = {};/*** 一级文档树,children属性就是二级文档* [{* id: "",* name: "",* children: [{* id: "",* name: "",* }]* }]*/const level1 = ref(); // 一级文档树,children属性就是二级文档level1.value = [];/*** 内容查询**/const handleQueryContent = (id: number) => {axios.get("/doc/find-content/" + id).then((response) => {const data = response.data;if (data.success) {html.value = data.content;} else {message.error(data.message);}});};/*** 数据查询**/const handleQuery = () => {axios.get("/doc/all/" + route.query.ebookId).then((response) => {const data = response.data;if (data.success) {docs.value = data.content;level1.value = [];level1.value = Tool.array2Tree(docs.value, 0);if (Tool.isNotEmpty(level1)) {defaultSelectedKeys.value = [level1.value[0].id];handleQueryContent(level1.value[0].id);// 初始显示文档信息doc.value = level1.value[0];}} else {message.error(data.message);}});};const onSelect = (selectedKeys: any, info: any) => {console.log('selected', selectedKeys, info);if (Tool.isNotEmpty(selectedKeys)) {// 选中某一节点时,加载该节点的文档信息doc.value = info.selectedNodes[0].props;// 加载内容handleQueryContent(selectedKeys[0]);}};// 点赞const vote = () => {axios.get('/doc/vote/' + doc.value.id).then((response) => {const data = response.data;if (data.success) {doc.value.voteCount++;} else {message.error(data.message);}});};onMounted(() => {handleQuery();});return {level1,html,onSelect,defaultSelectedKeys,doc,vote}}});</script><style>/* wangeditor默认样式, 参照: http://www.wangeditor.com/doc/pages/02-%E5%86%85%E5%AE%B9%E5%A4%84%E7%90%86/03-%E8%8E%B7%E5%8F%96html.html *//* table 样式 */.wangeditor table {border-top: 1px solid #ccc;border-left: 1px solid #ccc;}.wangeditor table td,.wangeditor table th {border-bottom: 1px solid #ccc;border-right: 1px solid #ccc;padding: 3px 5px;}.wangeditor table th {border-bottom: 2px solid #ccc;text-align: center;}/* blockquote 样式 */.wangeditor blockquote {display: block;border-left: 8px solid #d0e5f2;padding: 5px 10px;margin: 10px 0;line-height: 1.4;font-size: 100%;background-color: #f1f1f1;}/* code 样式 */.wangeditor code {display: inline-block;*display: inline;*zoom: 1;background-color: #f1f1f1;border-radius: 3px;padding: 3px 5px;margin: 0 3px;}.wangeditor pre code {display: block;}/* ul ol 样式 */.wangeditor ul, ol {margin: 10px 0 10px 20px;}/* 和antdv p冲突,覆盖掉 */.wangeditor blockquote p {font-family:"YouYuan";margin: 20px 10px !important;font-size: 16px !important;font-weight:600;}/* 点赞 */.vote-div {padding: 15px;text-align: center;}/* 图片自适应 */.wangeditor img {max-width: 100%;height: auto;}/* 视频自适应 */.wangeditor iframe {width: 100%;height: 400px;}</style>
<template><a-layout><a-layout-content:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"><a-row :gutter="24"><a-col :span="8"><p><a-form layout="inline" :model="param"><a-form-item><a-button type="primary" @click="handleQuery()">查询</a-button></a-form-item><a-form-item><a-button type="primary" @click="add()">新增</a-button></a-form-item></a-form></p><a-tablev-if="level1.length > 0":columns="columns":row-key="record => record.id":data-source="level1":loading="loading":pagination="false"size="small":defaultExpandAllRows="true"><template #name="{ text, record }">{{record.sort}} {{text}}</template><template v-slot:action="{ text, record }"><a-space size="small"><a-button type="primary" @click="edit(record)" size="small">编辑</a-button><a-popconfirmtitle="删除后不可恢复,确认删除?"ok-text="是"cancel-text="否"@confirm="handleDelete(record.id)"><a-button type="danger" size="small">删除</a-button></a-popconfirm></a-space></template></a-table></a-col><a-col :span="16"><p><a-form layout="inline" :model="param"><a-form-item><a-button type="primary" @click="handleSave()">保存</a-button></a-form-item></a-form></p><a-form :model="doc" layout="vertical"><a-form-item><a-input v-model:value="doc.name" placeholder="名称"/></a-form-item><a-form-item><a-tree-selectv-model:value="doc.parent"style="width: 100%":dropdown-style="{ maxHeight: '400px', overflow: 'auto' }":tree-data="treeSelectData"placeholder="请选择父文档"tree-default-expand-all:replaceFields="{title: 'name', key: 'id', value: 'id'}"></a-tree-select></a-form-item><a-form-item><a-input v-model:value="doc.sort" placeholder="顺序"/></a-form-item><a-form-item><a-button type="primary" @click="handlePreviewContent()"><EyeOutlined /> 内容预览</a-button></a-form-item><a-form-item><div id="content"></div></a-form-item></a-form></a-col></a-row><a-drawer width="900" placement="right" :closable="false" :visible="drawerVisible" @close="onDrawerClose"><div class="wangeditor" :innerHTML="previewHtml"></div></a-drawer></a-layout-content></a-layout><!--<a-modal--><!-- title="文档表单"--><!-- v-model:visible="modalVisible"--><!-- :confirm-loading="modalLoading"--><!-- @ok="handleModalOk"--><!-->--><!-- --><!--</a-modal>--></template><script lang="ts">import { defineComponent, onMounted, ref, createVNode } from 'vue';import axios from 'axios';import {message, Modal} from 'ant-design-vue';import {Tool} from "@/util/tool";import {useRoute} from "vue-router";import ExclamationCircleOutlined from "@ant-design/icons-vue/ExclamationCircleOutlined";import E from 'wangeditor'export default defineComponent({name: 'AdminDoc',setup() {const route = useRoute();console.log("路由:", route);console.log("route.path:", route.path);console.log("route.query:", route.query);console.log("route.param:", route.params);console.log("route.fullPath:", route.fullPath);console.log("route.name:", route.name);console.log("route.meta:", route.meta);const param = ref();param.value = {};const docs = ref();const loading = ref(false);// 因为树选择组件的属性状态,会随当前编辑的节点而变化,所以单独声明一个响应式变量const treeSelectData = ref();treeSelectData.value = [];const columns = [{title: '名称',dataIndex: 'name',slots: { customRender: 'name' }},{title: 'Action',key: 'action',slots: { customRender: 'action' }}];/*** 一级文档树,children属性就是二级文档* [{* id: "",* name: "",* children: [{* id: "",* name: "",* }]* }]*/const level1 = ref(); // 一级文档树,children属性就是二级文档level1.value = [];/*** 数据查询**/const handleQuery = () => {loading.value = true;// 如果不清空现有数据,则编辑保存重新加载数据后,再点编辑,则列表显示的还是编辑前的数据level1.value = [];axios.get("/doc/all/" + route.query.ebookId).then((response) => {loading.value = false;const data = response.data;if (data.success) {docs.value = data.content;console.log("原始数组:", docs.value);level1.value = [];level1.value = Tool.array2Tree(docs.value, 0);console.log("树形结构:", level1);// 父文档下拉框初始化,相当于点击新增treeSelectData.value = Tool.copy(level1.value) || [];// 为选择树添加一个"无"treeSelectData.value.unshift({id: 0, name: '无'});} else {message.error(data.message);}});};// -------- 表单 ---------const doc = ref();doc.value = {ebookId: route.query.ebookId};const modalVisible = ref(false);const modalLoading = ref(false);const editor = new E('#content');editor.config.zIndex = 0;// 显示上传图片按钮,转成Base64存储,同时也支持拖拽图片// 上传图片文档:https://doc.wangeditor.com/pages/07-%E4%B8%8A%E4%BC%A0%E5%9B%BE%E7%89%87/01-%E9%85%8D%E7%BD%AE%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%8E%A5%E5%8F%A3.html// 上传视频文档:https://doc.wangeditor.com/pages/07-%E4%B8%8A%E4%BC%A0%E8%A7%86%E9%A2%91/01-%E9%85%8D%E7%BD%AE%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%8E%A5%E5%8F%A3.htmleditor.config.uploadImgShowBase64 = true;const handleSave = () => {modalLoading.value = true;doc.value.content = editor.txt.html();axios.post("/doc/save", doc.value).then((response) => {modalLoading.value = false;const data = response.data; // data = commonRespif (data.success) {// modalVisible.value = false;message.success("保存成功!");// 重新加载列表handleQuery();} else {message.error(data.message);}});};/*** 将某节点及其子孙节点全部置为disabled*/const setDisable = (treeSelectData: any, id: any) => {// console.log(treeSelectData, id);// 遍历数组,即遍历某一层节点for (let i = 0; i < treeSelectData.length; i++) {const node = treeSelectData[i];if (node.id === id) {// 如果当前节点就是目标节点console.log("disabled", node);// 将目标节点设置为disablednode.disabled = true;// 遍历所有子节点,将所有子节点全部都加上disabledconst children = node.children;if (Tool.isNotEmpty(children)) {for (let j = 0; j < children.length; j++) {setDisable(children, children[j].id)}}} else {// 如果当前节点不是目标节点,则到其子节点再找找看。const children = node.children;if (Tool.isNotEmpty(children)) {setDisable(children, id);}}}};const deleteIds: Array<string> = [];const deleteNames: Array<string> = [];/*** 查找整根树枝*/const getDeleteIds = (treeSelectData: any, id: any) => {// console.log(treeSelectData, id);// 遍历数组,即遍历某一层节点for (let i = 0; i < treeSelectData.length; i++) {const node = treeSelectData[i];if (node.id === id) {// 如果当前节点就是目标节点console.log("delete", node);// 将目标ID放入结果集ids// node.disabled = true;deleteIds.push(id);deleteNames.push(node.name);// 遍历所有子节点const children = node.children;if (Tool.isNotEmpty(children)) {for (let j = 0; j < children.length; j++) {getDeleteIds(children, children[j].id)}}} else {// 如果当前节点不是目标节点,则到其子节点再找找看。const children = node.children;if (Tool.isNotEmpty(children)) {getDeleteIds(children, id);}}}};/*** 内容查询**/const handleQueryContent = () => {axios.get("/doc/find-content/" + doc.value.id).then((response) => {const data = response.data;if (data.success) {editor.txt.html(data.content)} else {message.error(data.message);}});};/*** 编辑*/const edit = (record: any) => {// 清空富文本框editor.txt.html("");modalVisible.value = true;doc.value = Tool.copy(record);handleQueryContent();// 不能选择当前节点及其所有子孙节点,作为父节点,会使树断开treeSelectData.value = Tool.copy(level1.value);setDisable(treeSelectData.value, record.id);// 为选择树添加一个"无"treeSelectData.value.unshift({id: 0, name: '无'});};/*** 新增*/const add = () => {// 清空富文本框editor.txt.html("");modalVisible.value = true;doc.value = {ebookId: route.query.ebookId};treeSelectData.value = Tool.copy(level1.value) || [];// 为选择树添加一个"无"treeSelectData.value.unshift({id: 0, name: '无'});};const handleDelete = (id: number) => {// console.log(level1, level1.value, id)// 清空数组,否则多次删除时,数组会一直增加deleteIds.length = 0;deleteNames.length = 0;getDeleteIds(level1.value, id);Modal.confirm({title: '重要提醒',icon: createVNode(ExclamationCircleOutlined),content: '将删除:【' + deleteNames.join(",") + "】删除后不可恢复,确认删除?",onOk() {// console.log(ids)axios.delete("/doc/delete/" + deleteIds.join(",")).then((response) => {const data = response.data; // data = commonRespif (data.success) {// 重新加载列表handleQuery();} else {message.error(data.message);}});},});};// ----------------富文本预览--------------const drawerVisible = ref(false);const previewHtml = ref();const handlePreviewContent = () => {const html = editor.txt.html();previewHtml.value = html;drawerVisible.value = true;};const onDrawerClose = () => {drawerVisible.value = false;};onMounted(() => {handleQuery();editor.create();});return {param,// docs,level1,columns,loading,handleQuery,edit,add,doc,modalVisible,modalLoading,handleSave,handleDelete,treeSelectData,drawerVisible,previewHtml,handlePreviewContent,onDrawerClose,}}});</script><style scoped>img {width: 50px;height: 50px;}</style>
树形选择组件
因为是个无限极树形,然后我们需要使用a-tree-select组件去作为父节点的选择组件,前端代码在上面有。
Vue页面参数传递
在电子书管理页面点击【文档管理】,跳到文档管理页面时,带上当前电子书id参数ebookID,在admin-ebook当中有这么一段代码就是跳转路由并且携带ID:
<router-link :to="'/admin/doc?ebookId=' + record.id"><a-button type="primary">文档管理</a-button></router-link>
在admin-doc.vue当中就可以通过route拿到信息:
const route = useRoute();console.log("路由:", route);console.log("route.path:", route.path);console.log("route.query:", route.query); // admin/doc?ebookid=1console.log("route.param:", route.params);// admin/doc/1console.log("route.fullPath:", route.fullPath);console.log("route.name:", route.name);console.log("route.meta:", route.meta);setup() {axios.get("/doc/all/" + route.query.ebookId).then((response) => {});}
✿删除文档功能
删除某个文档的时候,其下所有的文档也应该删除,也就是删掉文档2的时候,下面的文档2.1、文档2.2、包括文档2.2.1都要删掉。
由于这个功能需要递归,所以我们需要将其书写在前端,这样可以减少服务器的压力,就是递归将所有的id拿出来,然后传个后端,让后端删除。
1. 后端的代码展示
前端传入后端的是类似于这样的字符串:”1,2,3,4,5”, 后端要将其转换成[1,2,3,4,5]这样的数组。
@DeleteMapping("/delete/{idsStr}")public CommonResp delete(@PathVariable String idsStr) {CommonResp resp = new CommonResp<>();// String 转成List<Long>的集合List<String> list = Arrays.asList(idsStr.split(","));docService.delete(list);return resp;}
/*** 分类删除接口(按照id进行删除)* @param ids*/public void delete(List<String> ids) {DocExample docExample = new DocExample();DocExample.Criteria criteria = docExample.createCriteria();List<Long> result = new ArrayList<>();ids.forEach(item -> {result.add(Long.parseLong(item));});// 数组直接传入查询条件criteria.andIdIn(result);docMapper.deleteByExample(docExample);}
2. 前端代码展示
const deleteIds: Array<string> = [];const deleteNames: Array<string> = [];/*** 查找整根树枝*/const getDeleteIds = (treeSelectData: any, id: any) => {// console.log(treeSelectData, id);// 遍历数组,即遍历某一层节点for (let i = 0; i < treeSelectData.length; i++) {const node = treeSelectData[i];if (node.id === id) {// 如果当前节点就是目标节点console.log("delete", node);// 将目标ID放入结果集ids// node.disabled = true;deleteIds.push(id);deleteNames.push(node.name);// 遍历所有子节点const children = node.children;if (Tool.isNotEmpty(children)) {for (let j = 0; j < children.length; j++) {getDeleteIds(children, children[j].id)}}} else {// 如果当前节点不是目标节点,则到其子节点再找找看。const children = node.children;if (Tool.isNotEmpty(children)) {getDeleteIds(children, id);}}}};const handleDelete = (id: number) => {// console.log(level1, level1.value, id)// 清空数组,否则多次删除时,数组会一直增加deleteIds.length = 0;deleteNames.length = 0;getDeleteIds(level1.value, id);Modal.confirm({title: '重要提醒',icon: createVNode(ExclamationCircleOutlined),content: '将删除:【' + deleteNames.join(",") + "】删除后不可恢复,确认删除?",onOk() {// console.log(ids)axios.delete("/doc/delete/" + deleteIds.join(",")).then((response) => {const data = response.data; // data = commonRespif (data.success) {// 重新加载列表handleQuery();} else {message.error(data.message);}});},});};
✿集成富文本插件wangEditor
wangEditor是Typescript开发的富文本编辑器,官网地址是https://www.wangeditor.com/,这个比较重要,因为这个是开源免费的,使用下面的方式来下载:
npm i wangeditor --save
下面我们将在admin-doc.vue当中相关的wangeditor的代码都粘出来:
<template><a-drawer width="900" placement="right" :closable="false" :visible="drawerVisible" @close="onDrawerClose"><div class="wangeditor" :innerHTML="previewHtml"></div></a-drawer></template><script lang="ts">import E from 'wangeditor'export default defineComponent({name: 'AdminDoc',setup() {const editor = new E('#content');editor.config.zIndex = 0;// 显示上传图片按钮,转成Base64存储,同时也支持拖拽图片// 上传图片文档:https://doc.wangeditor.com/pages/07-%E4%B8%8A%E4%BC%A0%E5%9B%BE%E7%89%87/01-%E9%85%8D%E7%BD%AE%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%8E%A5%E5%8F%A3.html// 上传视频文档:https://doc.wangeditor.com/pages/07-%E4%B8%8A%E4%BC%A0%E8%A7%86%E9%A2%91/01-%E9%85%8D%E7%BD%AE%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%8E%A5%E5%8F%A3.htmleditor.config.uploadImgShowBase64 = true;/*** 编辑*/const edit = (record: any) => {// 清空富文本框editor.txt.html("");modalVisible.value = true;doc.value = Tool.copy(record);handleQueryContent();// 不能选择当前节点及其所有子孙节点,作为父节点,会使树断开treeSelectData.value = Tool.copy(level1.value);setDisable(treeSelectData.value, record.id);// 为选择树添加一个"无"treeSelectData.value.unshift({id: 0, name: '无'});};/*** 新增*/const add = () => {// 清空富文本框editor.txt.html("");modalVisible.value = true;doc.value = {ebookId: route.query.ebookId};treeSelectData.value = Tool.copy(level1.value) || [];// 为选择树添加一个"无"treeSelectData.value.unshift({id: 0, name: '无'});};const handleDelete = (id: number) => {// console.log(level1, level1.value, id)// 清空数组,否则多次删除时,数组会一直增加deleteIds.length = 0;deleteNames.length = 0;getDeleteIds(level1.value, id);Modal.confirm({title: '重要提醒',icon: createVNode(ExclamationCircleOutlined),content: '将删除:【' + deleteNames.join(",") + "】删除后不可恢复,确认删除?",onOk() {// console.log(ids)axios.delete("/doc/delete/" + deleteIds.join(",")).then((response) => {const data = response.data; // data = commonRespif (data.success) {// 重新加载列表handleQuery();} else {message.error(data.message);}});},});};// ----------------富文本预览--------------const drawerVisible = ref(false);const previewHtml = ref();const handlePreviewContent = () => {const html = editor.txt.html();previewHtml.value = html;drawerVisible.value = true;};const onDrawerClose = () => {drawerVisible.value = false;};onMounted(() => {handleQuery();editor.create();});}});</script><style scoped>img {width: 50px;height: 50px;}</style>
✿文档内容表设计与代码生成
1. 文档表设计
这个表的设计是这样,这个content原本是doc表当中的一个字段,但是由于内容比较大,如果从数据库一下查出来很多doc内容,那么content也会随之查询出来,这样数据库压力就很大,我们设计的content的id和doc的id是一样的。这种叫做纵向分表,如果是一月份一张表,二月份一张表,这个随着业务的增加,这个就是横向的分表
drop table if exists `content`;create table `content` (`id` bigint not null comment '文档id',`content` mediumtext not null comment '内容',primary key (`id`)) engine=innodb default charset=utf8mb4 comment='文档内容';
2. 生成持久层的代码
生成持久层代码,依旧去generator-config.xml,将最后一行的table改成doc,然后去执行mybatis-generator这个命令,去生成四个文件,依次是Content.java,DocExample.java,DocMapper.java,DocMapper.xml。
<table tableName="content"/>
文档管理页面布局修改
之前编辑是使用了弹窗的形式,我们现在使用左右布局的形式,这种形式适合字段不多的列表,如果列表显示的字段非常多的话,就不适用了。 使用栅格区域,在ant design vue当中是Grid组件。
✿文档内容的保存与显示
文档的内容保存的是一个html的字符串,这个字符串可以以html的方式显示在富文本框当中。
1. 后端代码展示
(1)文档内容的保存
在保存我们编辑的文档的时候,文档的信息要保存在文档doc表中,而文档的文字内容要保存在content表当中,所以我们在保存的时候要分开保存这些字段:
public class DocSaveReq {@NotNull(message = "【内容】不能为空")private String content;public String getContent() {return content;}public void setContent(String content) {this.content = content;}}
public void save(DocSaveReq req) {Doc doc = CopyUtil.copy(req, Doc.class);Content content = CopyUtil.copy(req,Content.class);if(ObjectUtils.isEmpty(req.getId())) {// 新增文档docdoc.setId(snowFlake.nextId());docMapper.insert(doc);// 同时新增内容content(保证id和doc是一致的)content.setId(doc.getId());contentMapper.insert(content);} else {// 更新docMapper.updateByPrimaryKey(doc);// BLOB方法包含大字段的操作int count = contentMapper.updateByPrimaryKeyWithBLOBs(content);if(count == 0) {// 如果content当中没有该id的记录,就要插入contentMapper.insert(content);}}}
- 特别要主要生成器当中有updateByPrimaryKeyWithBLOBs方法,BLOBs相关的方法都是包含修改大字段的方法,富文本生成的html内容就是大的字段,所以需要使用这个方法。
(4)文档内容的显示
文档内容的显示需要增加单独获取内容的接口,前端得到html字符串后,放入富文本框当中 ```java
/**
- 根据id去content表当中查找内容
- @param id
- @return
*/
public String findContent(Long id) {
Content content = contentMapper.selectByPrimaryKey(id);
return content.getContent();
}
java
@GetMapping(“/find-content/{id}”)
public CommonResp findContent(@PathVariable Long id) {
CommonResp
<a name="VoNYJ"></a>### 2. 前端代码的显示前端代码我们前面已经给过完整展示。<a name="dli4E"></a>## **✿**文档页面功能开发关于文档页面开发我们还优化了很多东西,我们来看看<a name="lQ2aT"></a>### 1. 后端代码展示我们在查文档的时候,有个all接口没有写动态参数,我们不应该查所有的文档,我们只能查一个电子书下面的所有文档,所以我们需要修改一下all的接口:```java@GetMapping("/all/{ebookId}")public CommonResp all(@PathVariable Long ebookId) {CommonResp<List<DocQueryResp>> resp = new CommonResp<>();List<DocQueryResp> list = docService.all(ebookId);resp.setContent(list);return resp;}
public List<DocQueryResp> all(Long ebookId) {DocExample docExample = new DocExample();// 添加一个动态查询DocExample.Criteria criteria = docExample.createCriteria();criteria.andEbookIdEqualTo(ebookId);docExample.setOrderByClause("sort asc"); // 按照sort排序List<Doc> docsList = docMapper.selectByExample(docExample);List<DocQueryResp> respList = CopyUtil.copyList(docsList, DocQueryResp.class);return respList;}
另外我们前面书写的根据id查询content内容的时候,没有做非空判断:
public String findContent(Long id) {Content content = contentMapper.selectByPrimaryKey(id);if(ObjectUtils.isEmpty(content)) {return "";} else {return content.getContent();}}
2. 前端代码展示
前端代码前面有完整的展示。
