接着Spring Boot&Vue3前后端分离实战wiki知识库系统<八>--分类管理功能开发二继续往下,这次咱们则来到文档管理功能的开发学习了,对于知识库的核心功能当然得是能发表文章及查看对吧,所以接下来一步一个脚印来完成它,先来提前感受一下这个功能的样式:
功能还是挺复杂的,反正对于小白的我肯定是搞不定的,费话不多说,直接开撸。
文档表设计与代码生成:文档表设计:这里就直接给出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);
这里表字段有一个关联电子书的字段:
因为文档是属于某个电子书的,所以这里在插入数据时,需要保证这个id是存在于电子书表记录当中的:
其它的字段都比较好理解,就不过多说明,下面将sql执行一下,然后确保表成功创建了:
代码生成:接下来咱们就可以使用mybatis generator【如果对它木有印象的可以参考https://www.cnblogs.com/webor2006/p/17139526.html】来生成mybatis相关的代码了,如下:
此时相关的代码就已经生成了:
相当的happy。
完成文档表基本增删改查功能:接下来则通过CV大法快速实现文档表的增删改查功能,关于这块的技巧在之前https://www.cnblogs.com/webor2006/p/17291405.html分类管理时已经使用过了,有些忘了,下面则再来复习一下这整个过程。
1、DocController:这里直接拷贝分类的代码:
接下来打开新拷贝的类,进行如下替换:
然后再换成小写替换一次:
此时替换完之后有一堆报错,不用管,因为还没整体替换完。
2、 DocService:然后打开类,跟Controller一样的替换规则,这里就不再重复啰嗦了,替换完之后也是一堆报错。
3、req实体:对于分类请求实体有两个:
这里就不能无脑的CV了,先来看一下查询的:
这里倒可以直接copy,里面没有涉及到字段,如下:
而保存的实体这里由于会涉及到表字段:
很显然不能直接拷贝分类的了,这里还是直接基于文档的domain进行拷贝比较好:
然后在里面再标注非空的字段,如下:
4、resp实体:这个也是直接从文档的domain实体进行拷贝:
这样整个后端的代码就拷贝完了,报错也没有了。
5、admin-doc.vue:接下来回到前端进行同样的CV大法,也拷贝自分类页面:
然后进行大小写的替换:
这里就不过多说明了, 另外对于前端还需要替换一个中文:
接下来通常需要修改一下表单的内容了,毕竟业务表的字段是不一样的,凑巧这里的文档和分类目前的字段是一样的,暂时先不用改,之后等做到时再细化。
6、添加路由:接下来则配置一下文档页面的路由:
7、电子书管理增加跳转入口:由于文档的管理是需要从某个电子书中点击进来的,毕竟文档是隶属于电子书的,所以咱们回到电子书管理页面,在这块增加一个跳转文档管理的入口:
8、运行:通过这么一顿CV大法之后,咱们运行看一下初步的效果:
增删改查都有了,效率还是相当高的,不过从运行效果中可以发现编辑时,目前只能选择一级分类作为父文档,照理应该是可以选择任务级文档作为当前的父文档的,关于这块的细节之后再来完善。
使用树形选择组件选择父节点:概述:在上面也说了现在在编辑文档时,只能选择一级文档做为父节点:
接下来咱们得将它改成树形的选择框。
实现:1、找选择框组件:这里可以用这个组件:
其中我们使用最基础的效果就可以了:
点开代码查看一下它的基本用法,貌似发现这个树形菜单的生成不是基于json数据:
很明显我们得根据文档树形的json数据来进行菜单的生成对吧,所以这种方式很显然不适合咱们使用,那得找使用json数据生成的,刚好在下面就有一个:
简单了解一下它是如何通过json数据来生成的:
而treeData则是定义的一个树形结构的json:
interface TreeDataItem { value: string; key: string; title?: string; slots?: Record<string, string>; children?: TreeDataItem[];}const treeData: TreeDataItem[] = [ { title: 'Node1', value: '0-0', key: '0-0', children: [ { value: '0-0-1', key: '0-0-1', slots: { title: 'title', }, }, { title: 'Child Node2', value: '0-0-2', key: '0-0-2', }, ], }, { title: 'Node2', value: '0-1', key: '0-1', },];
而这个树形结构其实刚好跟咱们定义的文档树形结构是一样的,好,接下来咱们来将它集成到工程中。
2、 集成TreeSelect树选择组件:模板代码从ant design进行拷贝,如下:
运行看一下效果:
树形结构已经有了,但是现在里面的数据没正常显示出来,其实原因是因为这个组件默认需要的json中的属性是这样的:
而咱们的树形json的key是id和name:
那这个组件有有木有转换的属性呢?答案是有的:
咱们来使用一下:
运行发现不好使。。这里其实是故意挖个坑来让自己对vue的语法理解得更加深刻一些,错误的原因是replaceFields前面没有加一个“:”导致,咱们录个动图来感受一下不加":"和加了“:”的区别:
有木有发现加了冒号之后,它设置的json的颜色都不一样了,也就是如果属性加了冒号,它里面的值是动态生成的,如果不加冒号则会将它里面的值当一个string,是一个死的值,基础巩固一下,下面再运行:
3、完成文档编辑功能:接下来我们来完成文档的编辑功能,目前编辑功能其实是有很多问题的,下面一一来进行解决。
问题一:父节点可选择自己及子节点
这个问题其实在之前分类管理时就遇到过,下面来演示一下:
看到木有,也就是将“文档111”的父节点设置成了它的孩子“文档1.1”,这样就造成了这俩文档相互引用,在查询树形菜单时则会将这样的数据给过滤掉,造成设置之后该数据不见了,也就是该数据从树形结构中断掉了,很显然不合理,而解决这种问题思路就是不让它可以选择自己及孩子做为它的父节点,效果如下:
下面来解决一下,对于某个项不让用户点,其实只需要在树形结构中将对应的节点设置一下disabled=true既可,如官方的示例所示:
所以下面咱们来处理一下树形选择框绑定的数据:
这里需要用到递归了,因为是无限级的树形菜单,有可能有很多层,处理逻辑这里直接贴出来了:
接下来就是集中来处理setDisable,逻辑如下:
// 将某节点及其子孙节点全部置为disabled const setDisable = (treeSelectData: any, id: any) => { // 遍历数组,即遍历某一层节点 for (let i = 0; i < treeSelectData.length; i++) { const node = treeSelectData[i]; if (node.id === id) { // 如果当前节点就是目标节点 console.log("disabled", node); // 将目标节点设置为disabled node.disabled = true; // 遍历所有子节点,将所有子节点全部都加上disabled const 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); } } } }
下面来运行看一下效果,由于之前我们为了演示问题,目前列表中只展示文档2的内容了,这里到数据库中将文档1.1的父id给手动改为0让其列表中可以多显示点数据:
木问题,但是我再编辑一下文档2 ,bug就产生了:
咋文档2编辑时,全部都无法选择了,照理应该只有它和它的孩子节点才不能被选择才对,难道是写的setDisable的逻辑有问题么?其实不然,是因为我们目前编辑修改的是level1:
而对于它的修改是会破坏原数据的,所以解决之道也比较简单,每次编辑时克隆一份树形结构的数据,之后编辑所产生的修改只修改拷贝,不直接修改原数据就好了,如下:
好,关键的显示也需要进行替换了:
再运行:
其实还有个bug,就是如果我刷新一下界面,直接点新增,你会发现:
父节点没有数据了。。是因为我们现在选择组件绑定的是treeSelectData这个数据了,而目前对于添加我们没有进行任何的处理:
加一句代码既可:
再运行:
问题二:无法设置父节点为顶层节点:
接下来继续解决bug,就是现在新增文档时,是无法新建一个顶层节点的,因为父文档这块必须要选择一个现有的文档节点:
很明显是不合理的,所以有必要再增加一个自定义的结点叫“无”,如果选择父节点为“无”,那么该文档则为顶层节点,那如何在编辑或新增时给显示的下拉选项中增加一个“无”呢?这里需要用到一个新的api了,叫“unshift”,先网上了解一下它:
其实也就是使用unshift往数组的开头添加元素,所以处理如下:
此时再运行:
Vue页面参数传递完成新增文档功能:问题描述:现在编辑功能已经好使了,接下来则差文档的新增功能,不过目前新增功能不好使,报错了,演示一下:
为啥报电子书为空呢?我们不是很明显是从电子书的管理列表中的一项点击来查看它底下的文档列表么,是的,但是我们目前在文档列表页面并没有把电子书的id给保存起来,所以在保存时是不支持电子书id的。
实现:1、文档管理点击增加参数:为了能在文档管理的界面上接收到电子书的id,所以咱们需要在点击它时增加一个参数:
对应的代码如下:
很明显加一个参数是需要将它变成一个动态的值,所以此时的to需要加一个冒号了:
而对于里面不变的我们需要将它用单引号括起来:
此时再加上动态的电子书的id既可:
2、文档页面接收电子书id:接下来咱们就可以在文档页面来读取路由传过来的电子书id的参数了,如何获取呢?如下:
接下来我们在新增的时候就可以这样来获取路由的参数了:
接下来咱们就可以这样修改一下:
其中route.query其实就是取的我们在路由传弟的这种参数:
3、运行:接下来咱们运行看一下:
回到数据库中看一下是否插入正常:
增加删除文档功能:需求:接下来再来完成删除功能,目前删除功能只能删除某个文档,对于无限级的文档树来说,目前是不满足正常的业务需求的,实际的业务需求应该是这样的:
而应该将它及它下面的子孙结点全部给删除,也就是:
实现:其实现思路其实也比较简单,这里就又要用到递归来得到当前文档及它下面的所有文档的id集合了,然后再将所有要删除的id传给后端,后端接收之后进行id的删除, 当然这里不是轮循一个个id进行删除,而是通过一个sql来删除多个id的记录。
1、调研前端传一个带逗号分隔符的string让后端进行接收:这样试一下:
在正式改造之前,先来调个研:
后端接口也改一下:
下面运行看一下:
嗯,成功获取到前端传过来的多个id的String值了。
2、修改后端:接下来后端增加一个删除多个id的方法,如下:
接着我们在service中需要定义一个对应参数的删除方法:
那我们通过IDE的提示改一下:
也就是改的它:
3、修改前端:接下来咱们来将前端id的获取修改一下,先来定义一个存所有id的集合:
然后在删除时,通过递归手段来获取所有的id,然后将其存于新声明的这个集合中,这个递归逻辑跟之前文档树中子树不能选的是一样的,修改如下:
此时运行看一下效果:
此时看一下后端控制台确认是执行了删除sql了:
同时再确认一下表内容:
确实成功的删除了。
4、优化:由于删除可能会删除多个文档,所以这个是一个非常危险的操作,这里打算给用户一个二次确认的提示,在这个二次确认的提示中会把涉及到要删除的所有的文档名称给列出来提示给用户,这样的交互体验也大大的增强了,具体实现如下:1、先声明一个集合响应式变量用来存储要删除的文档名称:
然后在递归时把文档名称都取出来往这个集合中塞:
2、删除时弹出二次确认框:
接着在删除时,需要加一下它:
其中二次确认框来自于Ant Design Vue的这块:
好,再来运行看一下,在运行之前,由于数据库里木有太多数据了,这里重新对表进行一个初始化,再运行:
总结:本次实现的功能,说实话还是挺复杂的,其实倒没有什么新的技术点,主要是业务型的代码,这次先到这,下次则要开始进行文档内容的开发了,保持学习热情继续往前~~
关注个人公众号,解锁完整文章