- 分类基本增删改查功能:表设计,生成持久层代码,从电子书管理拷贝出一套分类管理代码
- 分类表格显示优化:不需要分页,树形表格
- 分类编辑功能优化:新增/编辑类时,支持选中某一分类作为父分类,或者无分类
- 电子书管理功能优化:编辑电子书时,可以选择分类一,分类二,表格显示分类名称
- 首页显示分类菜单
- 点击某一分类时,显示该分类下所有的电子书
✿分类表设计与代码生成
1. 分类表设计
我们执行下面的代码sql,这个就是可以形成树形分类的一个设计,每条数据都包含一个id和一个父id,这样可以递归出一个树形结构,我们这个是一个二级分类:
drop table if exists `category`;create table `category` (`id` bigint not null comment 'id',`parent` bigint not null default 0 comment '父id',`name` varchar(50) not null comment '名称',`sort` int comment '顺序',primary key (`id`)) engine=innodb default charset=utf8mb4 comment='分类';insert into `category` (id, parent, name, sort) values (100, 000,'前端开发', 100);insert into `category` (id, parent, name, sort) values (101, 100,'Vue', 101);insert into `category` (id, parent, name, sort) values (102, 100,'HTML & CSS', 102);insert into `category` (id, parent, name, sort) values (200, 000,'Java', 200);insert into `category` (id, parent, name, sort) values (201, 200,'基础应用', 201);insert into `category` (id, parent, name, sort) values (202, 200,'框架应用', 202);insert into `category` (id, parent, name, sort) values (300, 000,'Python', 300);insert into `category` (id, parent, name, sort) values (301, 300,'基础应用', 301);insert into `category` (id, parent, name, sort) values (302, 300,'进阶方向应用', 302);insert into `category` (id, parent, name, sort) values (400, 000,'数据库', 400);insert into `category` (id, parent, name, sort) values (401, 400,'MySQL', 401);insert into `category` (id, parent, name, sort) values (500, 000,'其他', 500);insert into `category` (id, parent, name, sort) values (501, 500,'服务器', 501);insert into `category` (id, parent, name, sort) values (502, 500,'开发工具', 502);insert into `category` (id, parent, name, sort) values (503, 500,'热门服务端语言', 503);
2. 生成持久层代码
我们依旧进入generator-config.xml,修改最后的table标签,然后去执行mybatis-generator命令,特别注意生成的四个文件不要去动,这样后续数据库有扩展的话可以重新生成。
<table tableName="category"/>
✿分类基本增删改查
1. 后端改造
从后端改造这个分类的增删改查,我们可以直接去复制一下之前的ebook的一下代码,改造的顺序应该是controller -> service -> 各种实体类,其中进入文件就Ctrl + R键,将ebook改成category,Ebook改为Category即可:
@RestController@RequestMapping("/category")public class CategoryController {@Resourceprivate CategoryService categoryService;@GetMapping("/list")public CommonResp list(@Valid CategoryQueryReq req) {// controller层尽量不要出现Category这个实体类// 因为实体类是和数据库一一对应的CommonResp<PageResp<CategoryQueryResp>> resp = new CommonResp<>();PageResp<CategoryQueryResp> list = categoryService.list(req);resp.setContent(list);return resp;}@PostMapping("/save")public CommonResp save(@Valid @RequestBody CategorySaveReq req) {CommonResp resp = new CommonResp<>();categoryService.save(req);return resp;}@DeleteMapping("/delete/{id}")public CommonResp delete(@PathVariable long id) {CommonResp resp = new CommonResp<>();categoryService.delete(id);return resp;}}
@Servicepublic class CategoryService {// 这个就是用来打印的,在WikiApplication当中也用到了private static final Logger LOG = LoggerFactory.getLogger(CategoryService.class);@Resourceprivate CategoryMapper categoryMapper;@Resourceprivate SnowFlake snowFlake;/*** 查询电子书* @param req 电子书查询参数* @return 电子书分页列表*/public PageResp<CategoryQueryResp> list(CategoryQueryReq req) {CategoryExample categoryExample = new CategoryExample();// createCriteria相当于where条件CategoryExample.Criteria criteria = categoryExample.createCriteria();// 根据categoryExample条件查询PageHelper.startPage(req.getPage(), req.getSize());List<Category> categorysList = categoryMapper.selectByExample(categoryExample);PageInfo<Category> pageInfo = new PageInfo<>(categorysList);LOG.info("总行数:{}",pageInfo.getTotal()); // 总行数LOG.info("总页数:{}",pageInfo.getPages()); // 总页数,一般不需要,前端会根据总数自动计算总页数List<CategoryQueryResp> respList = CopyUtil.copyList(categorysList, CategoryQueryResp.class);PageResp<CategoryQueryResp> pageResp = new PageResp();pageResp.setTotal(pageInfo.getTotal());pageResp.setList(respList);return pageResp;}/*** 电子书保存接口(保存包括编辑保存->更新, 也包括新增保存->新增, 根据req是否有id来判断)* @param req 电子书保存参数*/public void save(CategorySaveReq req) {Category category = CopyUtil.copy(req, Category.class);if(ObjectUtils.isEmpty(req.getId())) {// 新增category.setId(snowFlake.nextId());categoryMapper.insert(category);} else {// 更新// 因为updateByPrimaryKey传递的Category类型的参数,所以需要将CategorySaveReq 转化成CategorycategoryMapper.updateByPrimaryKey(category);}}/*** 电子书删除接口(按照id进行删除)* @param id*/public void delete(long id) {categoryMapper.deleteByPrimaryKey(id);}}
接下来就是实体类,我们有三个实体类,分别在对应的req和resp文件下创建即可:CategoryQueryReq, CategorySaveReq, CategoryQueryResp。
2. 前端代码展示
前端需要在views/admin下面创建admin-category.vue文件,然后实际上就是将admin-ebook修修改改即可:
<template><a-layout><a-layout-content:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"><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><p><a-alertclass="tip"message="小提示:这里的分类会显示到首页的侧边菜单"type="info"closable/></p><a-tablev-if="level1.length > 0":columns="columns":row-key="record => record.id":data-source="level1":loading="loading":pagination="false":defaultExpandAllRows="true"><template #cover="{ text: cover }"><img v-if="cover" :src="cover" alt="avatar" /></template><template v-slot:action="{ text, record }"><a-space size="small"><a-button type="primary" @click="edit(record)">编辑</a-button><a-popconfirmtitle="删除后不可恢复,确认删除?"ok-text="是"cancel-text="否"@confirm="handleDelete(record.id)"><a-button type="danger">删除</a-button></a-popconfirm></a-space></template></a-table></a-layout-content></a-layout><a-modaltitle="分类表单"v-model:visible="modalVisible":confirm-loading="modalLoading"@ok="handleModalOk"><a-form :model="category" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }"><a-form-item label="名称"><a-input v-model:value="category.name" /></a-form-item><a-form-item label="父分类"><a-selectv-model:value="category.parent"ref="select"><a-select-option :value="0">无</a-select-option><a-select-option v-for="c in level1" :key="c.id" :value="c.id" :disabled="category.id === c.id">{{c.name}}</a-select-option></a-select></a-form-item><a-form-item label="顺序"><a-input v-model:value="category.sort" /></a-form-item></a-form></a-modal></template><script lang="ts">import { defineComponent, onMounted, ref } from 'vue';import axios from 'axios';import { message } from 'ant-design-vue';import {Tool} from "@/util/tool";export default defineComponent({name: 'AdminCategory',setup() {const param = ref();param.value = {};const categorys = ref();const loading = ref(false);const columns = [{title: '名称',dataIndex: 'name'},// {// title: '父分类',// key: 'parent',// dataIndex: 'parent'// },{title: '顺序',dataIndex: 'sort'},{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("/category/all").then((response) => {loading.value = false;const data = response.data;if (data.success) {categorys.value = data.content;console.log("原始数组:", categorys.value);level1.value = [];level1.value = Tool.array2Tree(categorys.value, 0);console.log("树形结构:", level1);} else {message.error(data.message);}});};// -------- 表单 ---------const category = ref({});const modalVisible = ref(false);const modalLoading = ref(false);const handleModalOk = () => {modalLoading.value = true;axios.post("/category/save", category.value).then((response) => {modalLoading.value = false;const data = response.data; // data = commonRespif (data.success) {modalVisible.value = false;// 重新加载列表handleQuery();} else {message.error(data.message);}});};/*** 编辑*/const edit = (record: any) => {modalVisible.value = true;category.value = Tool.copy(record);};/*** 新增*/const add = () => {modalVisible.value = true;category.value = {};};const handleDelete = (id: number) => {axios.delete("/category/delete/" + id).then((response) => {const data = response.data; // data = commonRespif (data.success) {// 重新加载列表handleQuery();} else {message.error(data.message);}});};onMounted(() => {handleQuery();});return {param,// categorys,level1,columns,loading,handleQuery,edit,add,category,modalVisible,modalLoading,handleModalOk,handleDelete}}});</script><style scoped>img {width: 50px;height: 50px;}</style>
✿分类表格显示优化
分类查询实际上不需要分页,一次查出全部数据,另外我们需要改为树形表格展示,所以我们前后端都来改造一下:
1. 后端改造
后端我们之前书写的list先留着,我们新书写一个all的接口,一次性将所有的分类查出来:
@GetMapping("/all")public CommonResp all(@Valid CategoryQueryReq req) {// controller层尽量不要出现Category这个实体类// 因为实体类是和数据库一一对应的CommonResp<List<CategoryQueryResp>> resp = new CommonResp<>();List<CategoryQueryResp> list = categoryService.all(req);resp.setContent(list);return resp;}
/*** 查询分类* @param req 分类查询参数* @return 分类分页列表*/public List<CategoryQueryResp> all(CategoryQueryReq req) {CategoryExample categoryExample = new CategoryExample();categoryExample.setOrderByClause("sort asc"); // 按照sort排序List<Category> categorysList = categoryMapper.selectByExample(categoryExample);List<CategoryQueryResp> respList = CopyUtil.copyList(categorysList, CategoryQueryResp.class);return respList;}
2. 前端改造
前端的代码我们前面在分类基本增删改查的时候就全部展示了,现在我们需要展示一下如何将数据展示成为树形结构,这个递归比较重要,所以好好理解一下:
/*** 使用递归将数组转为树形结构* 父ID属性为parent*/public static array2Tree (array: any, parentId: number) {if (Tool.isEmpty(array)) {return [];}const result = [];for (let i = 0; i < array.length; i++) {const c = array[i];// console.log(Number(c.parent), Number(parentId));if (Number(c.parent) === Number(parentId)) {result.push(c);// 递归查看当前节点对应的子节点const children = Tool.array2Tree(array, c.id);if (Tool.isNotEmpty(children)) {c.children = children;}}}return result;}
分类编辑功能优化
由于分类比较特殊,在编辑(新增/修改)分类时,支持选中某一个分类作为父分类,或者没有分类,所以我们需要将,分类应该将输入框改变成为下拉框,下拉框的选项我们在这里是写死的,如果有活的必须够短有提供接口,代码我们可以在分类基本增删改查-前端代码展示当中看到完整的前端代码。
电子书管理增加分类选择
电子书的管理页面当中的分类进行了优化,显示的就不再是具体的分类代码,而是使用a-cascader级联组件进行了优化,所以具体的项目代码我们展示在下面,前端代码无法仔细详解,大家仔细研究:
<template><a-layout><a-layout-content:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"><p><a-form layout="inline" :model="param"><a-form-item><a-input v-model:value="param.name" placeholder="名称"></a-input></a-form-item><a-form-item><a-button type="primary" @click="handleQuery({page: 1, size: pagination.pageSize})">查询</a-button></a-form-item><a-form-item><a-button type="primary" @click="add()">新增</a-button></a-form-item></a-form></p><a-table:columns="columns":row-key="record => record.id":data-source="ebooks":pagination="pagination":loading="loading"@change="handleTableChange"><template #cover="{ text: cover }"><img v-if="cover" :src="cover" alt="avatar" /></template><template v-slot:category="{ text, record }"><span>{{ getCategoryName(record.category1Id) }} / {{ getCategoryName(record.category2Id) }}</span></template><template v-slot:action="{ text, record }"><a-space size="small"><router-link :to="'/admin/doc?ebookId=' + record.id"><a-button type="primary">文档管理</a-button></router-link><a-button type="primary" @click="edit(record)">编辑</a-button><a-popconfirmtitle="删除后不可恢复,确认删除?"ok-text="是"cancel-text="否"@confirm="handleDelete(record.id)"><a-button type="danger">删除</a-button></a-popconfirm></a-space></template></a-table></a-layout-content></a-layout><a-modaltitle="电子书表单"v-model:visible="modalVisible":confirm-loading="modalLoading"@ok="handleModalOk"><a-form :model="ebook" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }"><a-form-item label="封面"><a-input v-model:value="ebook.cover" /></a-form-item><a-form-item label="名称"><a-input v-model:value="ebook.name" /></a-form-item><a-form-item label="分类"><a-cascaderv-model:value="categoryIds":field-names="{ label: 'name', value: 'id', children: 'children' }":options="level1"/></a-form-item><a-form-item label="描述"><a-input v-model:value="ebook.description" type="textarea" /></a-form-item></a-form></a-modal></template><script lang="ts">import { defineComponent, onMounted, ref } from 'vue';import axios from 'axios';import { message } from 'ant-design-vue';import {Tool} from "@/util/tool";export default defineComponent({name: 'AdminEbook',setup() {const param = ref();param.value = {};const ebooks = ref();const pagination = ref({current: 1,pageSize: 10,total: 0});const loading = ref(false);const columns = [{title: '封面',dataIndex: 'cover',slots: { customRender: 'cover' }},{title: '名称',dataIndex: 'name'},{title: '分类',slots: { customRender: 'category' }},{title: '文档数',dataIndex: 'docCount'},{title: '阅读数',dataIndex: 'viewCount'},{title: '点赞数',dataIndex: 'voteCount'},{title: 'Action',key: 'action',slots: { customRender: 'action' }}];/*** 数据查询**/const handleQuery = (params: any) => {loading.value = true;// 如果不清空现有数据,则编辑保存重新加载数据后,再点编辑,则列表显示的还是编辑前的数据ebooks.value = [];axios.get("/ebook/list", {params: {page: params.page,size: params.size,name: param.value.name}}).then((response) => {loading.value = false;const data = response.data;if (data.success) {ebooks.value = data.content.list;// 重置分页按钮pagination.value.current = params.page;pagination.value.total = data.content.total;} else {message.error(data.message);}});};/*** 表格点击页码时触发*/const handleTableChange = (pagination: any) => {console.log("看看自带的分页参数都有啥:" + pagination);handleQuery({page: pagination.current,size: pagination.pageSize});};// -------- 表单 ---------/*** 数组,[100, 101]对应:前端开发 / Vue*/const categoryIds = ref();const ebook = ref();const modalVisible = ref(false);const modalLoading = ref(false);const handleModalOk = () => {modalLoading.value = true;ebook.value.category1Id = categoryIds.value[0];ebook.value.category2Id = categoryIds.value[1];axios.post("/ebook/save", ebook.value).then((response) => {modalLoading.value = false;const data = response.data; // data = commonRespif (data.success) {modalVisible.value = false;// 重新加载列表handleQuery({page: pagination.value.current,size: pagination.value.pageSize,});} else {message.error(data.message);}});};/*** 编辑*/const edit = (record: any) => {modalVisible.value = true;ebook.value = Tool.copy(record);categoryIds.value = [ebook.value.category1Id, ebook.value.category2Id]};/*** 新增*/const add = () => {modalVisible.value = true;ebook.value = {};};const handleDelete = (id: number) => {axios.delete("/ebook/delete/" + id).then((response) => {const data = response.data; // data = commonRespif (data.success) {// 重新加载列表handleQuery({page: pagination.value.current,size: pagination.value.pageSize,});} else {message.error(data.message);}});};const level1 = ref();let categorys: any;/*** 查询所有分类**/const handleQueryCategory = () => {loading.value = true;axios.get("/category/all").then((response) => {loading.value = false;const data = response.data;if (data.success) {categorys = data.content;console.log("原始数组:", categorys);level1.value = [];level1.value = Tool.array2Tree(categorys, 0);console.log("树形结构:", level1.value);// 加载完分类后,再加载电子书,否则如果分类树加载很慢,则电子书渲染会报错handleQuery({page: 1,size: pagination.value.pageSize,});} else {message.error(data.message);}});};const getCategoryName = (cid: number) => {// console.log(cid)let result = "";categorys.forEach((item: any) => {if (item.id === cid) {// return item.name; // 注意,这里直接return不起作用result = item.name;}});return result;};onMounted(() => {handleQueryCategory();});return {param,ebooks,pagination,columns,loading,handleTableChange,handleQuery,getCategoryName,edit,add,ebook,modalVisible,modalLoading,handleModalOk,categoryIds,level1,handleDelete}}});</script><style scoped>img {width: 50px;height: 50px;}</style>
首页显示分类菜单
思路非常简单,第一步加载数据变成树形结构,第二步将菜单做出循环,然后使用a-sub-menu这个组件写成树形的菜单。
<template><a-layout><a-layout-sider width="200" style="background: #fff"><a-menumode="inline":style="{ height: '100%', borderRight: 0 }"@click="handleClick":openKeys="openKeys"><a-menu-item key="welcome"><MailOutlined /><span>欢迎</span></a-menu-item><a-sub-menu v-for="item in level1" :key="item.id" :disabled="true"><template v-slot:title><span><user-outlined />{{item.name}}</span></template><a-menu-item v-for="child in item.children" :key="child.id"><MailOutlined /><span>{{child.name}}</span></a-menu-item></a-sub-menu><a-menu-item key="tip" :disabled="true"><span>以上菜单在分类管理配置</span></a-menu-item></a-menu></a-layout-sider><a-layout-content:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"><div class="welcome" v-show="isShowWelcome"><the-welcome></the-welcome></div><a-list v-show="!isShowWelcome" item-layout="vertical" size="large" :grid="{ gutter: 20, column: 3 }" :data-source="ebooks"><template #renderItem="{ item }"><a-list-item key="item.name"><template #actions><span><component v-bind:is="'FileOutlined'" style="margin-right: 8px" />{{ item.docCount }}</span><span><component v-bind:is="'UserOutlined'" style="margin-right: 8px" />{{ item.viewCount }}</span><span><component v-bind:is="'LikeOutlined'" style="margin-right: 8px" />{{ item.voteCount }}</span></template><a-list-item-meta :description="item.description"><template #title><router-link :to="'/doc?ebookId=' + item.id">{{ item.name }}</router-link></template><template #avatar><a-avatar :src="item.cover"/></template></a-list-item-meta></a-list-item></template></a-list></a-layout-content></a-layout></template><script lang="ts">import { defineComponent, onMounted, ref, reactive, toRef } from 'vue';import axios from 'axios';import { message } from 'ant-design-vue';import {Tool} from "@/util/tool";import TheWelcome from '@/components/the-welcome.vue';export default defineComponent({name: 'Home',components: {TheWelcome},setup() {const ebooks = ref();// const ebooks1 = reactive({books: []});const openKeys = ref();const level1 = ref();let categorys: any;/*** 查询所有分类**/const handleQueryCategory = () => {axios.get("/category/all").then((response) => {const data = response.data;if (data.success) {categorys = data.content;console.log("原始数组:", categorys);// 加载完分类后,将侧边栏全部展开openKeys.value = [];for (let i = 0; i < categorys.length; i++) {openKeys.value.push(categorys[i].id)}level1.value = [];level1.value = Tool.array2Tree(categorys, 0);console.log("树形结构:", level1.value);} else {message.error(data.message);}});};const isShowWelcome = ref(true);let categoryId2 = 0;/*** 查询数据*/const handleQueryEbook = () => {axios.get("/ebook/list", {params: {page: 1,size: 1000,categoryId2: categoryId2}}).then((response) => {const data = response.data;ebooks.value = data.content.list;// ebooks1.books = data.content;});};const handleClick = (value: any) => {// console.log("menu click", value)if (value.key === 'welcome') {isShowWelcome.value = true;} else {categoryId2 = value.key;isShowWelcome.value = false;handleQueryEbook();}// isShowWelcome.value = value.key === 'welcome';};onMounted(() => {handleQueryCategory();});return {ebooks,pagination: {onChange: (page: any) => {console.log(page);},pageSize: 3,},handleClick,level1,isShowWelcome,openKeys}}});</script><style scoped>.ant-avatar {width: 50px;height: 50px;line-height: 50px;border-radius: 8%;margin: 5px 0;}</style>
点击分类菜单显示电子书
后端我们就做两个事情,由于前端需要查询电子书的时候,需要添加上分类,所以查询的时候要带上分类参数categoryId2,于是我们就需要在EbookQueryReq.java当中去添加categoryId2。
package com.taopoppy.wiki.req;public class EbookQueryReq extends PageReq{private Long id;private String name;private Long categoryId2; // 添加该参数public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public Long getCategoryId2() {return categoryId2;}public void setCategoryId2(Long categoryId2) {this.categoryId2 = categoryId2;}@Overridepublic String toString() {return "EbookQueryReq{" +"id=" + id +", name='" + name + '\'' +", categoryId2=" + categoryId2 +'}';}}
/*** 查询电子书* @param req 电子书查询参数* @return 电子书分页列表*/public PageResp<EbookQueryResp> list(EbookQueryReq req) {EbookExample ebookExample = new EbookExample();EbookExample.Criteria criteria = ebookExample.createCriteria();if(!ObjectUtils.isEmpty(req.getName())) {criteria.andNameLike("%" + req.getName() + "%");}// 添加category的动态sql的写法if(!ObjectUtils.isEmpty(req.getCategoryId2())) {criteria.andCategory2IdEqualTo(req.getCategoryId2());}PageHelper.startPage(req.getPage(), req.getSize());List<Ebook> ebooksList = ebookMapper.selectByExample(ebookExample);PageInfo<Ebook> pageInfo = new PageInfo<>(ebooksList);LOG.info("总行数:{}",pageInfo.getTotal());LOG.info("总页数:{}",pageInfo.getPages());List<EbookQueryResp> respList = CopyUtil.copyList(ebooksList, EbookQueryResp.class);PageResp<EbookQueryResp> pageResp = new PageResp();pageResp.setTotal(pageInfo.getTotal());pageResp.setList(respList);return pageResp;}
前端的代码在home.vue,完整的代码我们已经在首页显示分类菜单当中完整的展示。
