• 分类基本增删改查功能:表设计,生成持久层代码,从电子书管理拷贝出一套分类管理代码
  • 分类表格显示优化:不需要分页,树形表格
  • 分类编辑功能优化:新增/编辑类时,支持选中某一分类作为父分类,或者无分类
  • 电子书管理功能优化:编辑电子书时,可以选择分类一,分类二,表格显示分类名称
  • 首页显示分类菜单
  • 点击某一分类时,显示该分类下所有的电子书

分类表设计与代码生成

1. 分类表设计

我们执行下面的代码sql,这个就是可以形成树形分类的一个设计,每条数据都包含一个id和一个父id,这样可以递归出一个树形结构,我们这个是一个二级分类:

  1. drop table if exists `category`;
  2. create table `category` (
  3. `id` bigint not null comment 'id',
  4. `parent` bigint not null default 0 comment '父id',
  5. `name` varchar(50) not null comment '名称',
  6. `sort` int comment '顺序',
  7. primary key (`id`)
  8. ) engine=innodb default charset=utf8mb4 comment='分类';
  9. insert into `category` (id, parent, name, sort) values (100, 000,'前端开发', 100);
  10. insert into `category` (id, parent, name, sort) values (101, 100,'Vue', 101);
  11. insert into `category` (id, parent, name, sort) values (102, 100,'HTML & CSS', 102);
  12. insert into `category` (id, parent, name, sort) values (200, 000,'Java', 200);
  13. insert into `category` (id, parent, name, sort) values (201, 200,'基础应用', 201);
  14. insert into `category` (id, parent, name, sort) values (202, 200,'框架应用', 202);
  15. insert into `category` (id, parent, name, sort) values (300, 000,'Python', 300);
  16. insert into `category` (id, parent, name, sort) values (301, 300,'基础应用', 301);
  17. insert into `category` (id, parent, name, sort) values (302, 300,'进阶方向应用', 302);
  18. insert into `category` (id, parent, name, sort) values (400, 000,'数据库', 400);
  19. insert into `category` (id, parent, name, sort) values (401, 400,'MySQL', 401);
  20. insert into `category` (id, parent, name, sort) values (500, 000,'其他', 500);
  21. insert into `category` (id, parent, name, sort) values (501, 500,'服务器', 501);
  22. insert into `category` (id, parent, name, sort) values (502, 500,'开发工具', 502);
  23. insert into `category` (id, parent, name, sort) values (503, 500,'热门服务端语言', 503);

2. 生成持久层代码

我们依旧进入generator-config.xml,修改最后的table标签,然后去执行mybatis-generator命令,特别注意生成的四个文件不要去动,这样后续数据库有扩展的话可以重新生成

  1. <table tableName="category"/>

分类基本增删改查

1. 后端改造

从后端改造这个分类的增删改查,我们可以直接去复制一下之前的ebook的一下代码,改造的顺序应该是controller -> service -> 各种实体类,其中进入文件就Ctrl + R键,将ebook改成category,Ebook改为Category即可:

  1. @RestController
  2. @RequestMapping("/category")
  3. public class CategoryController {
  4. @Resource
  5. private CategoryService categoryService;
  6. @GetMapping("/list")
  7. public CommonResp list(@Valid CategoryQueryReq req) {
  8. // controller层尽量不要出现Category这个实体类
  9. // 因为实体类是和数据库一一对应的
  10. CommonResp<PageResp<CategoryQueryResp>> resp = new CommonResp<>();
  11. PageResp<CategoryQueryResp> list = categoryService.list(req);
  12. resp.setContent(list);
  13. return resp;
  14. }
  15. @PostMapping("/save")
  16. public CommonResp save(@Valid @RequestBody CategorySaveReq req) {
  17. CommonResp resp = new CommonResp<>();
  18. categoryService.save(req);
  19. return resp;
  20. }
  21. @DeleteMapping("/delete/{id}")
  22. public CommonResp delete(@PathVariable long id) {
  23. CommonResp resp = new CommonResp<>();
  24. categoryService.delete(id);
  25. return resp;
  26. }
  27. }
  1. @Service
  2. public class CategoryService {
  3. // 这个就是用来打印的,在WikiApplication当中也用到了
  4. private static final Logger LOG = LoggerFactory.getLogger(CategoryService.class);
  5. @Resource
  6. private CategoryMapper categoryMapper;
  7. @Resource
  8. private SnowFlake snowFlake;
  9. /**
  10. * 查询电子书
  11. * @param req 电子书查询参数
  12. * @return 电子书分页列表
  13. */
  14. public PageResp<CategoryQueryResp> list(CategoryQueryReq req) {
  15. CategoryExample categoryExample = new CategoryExample();
  16. // createCriteria相当于where条件
  17. CategoryExample.Criteria criteria = categoryExample.createCriteria();
  18. // 根据categoryExample条件查询
  19. PageHelper.startPage(req.getPage(), req.getSize());
  20. List<Category> categorysList = categoryMapper.selectByExample(categoryExample);
  21. PageInfo<Category> pageInfo = new PageInfo<>(categorysList);
  22. LOG.info("总行数:{}",pageInfo.getTotal()); // 总行数
  23. LOG.info("总页数:{}",pageInfo.getPages()); // 总页数,一般不需要,前端会根据总数自动计算总页数
  24. List<CategoryQueryResp> respList = CopyUtil.copyList(categorysList, CategoryQueryResp.class);
  25. PageResp<CategoryQueryResp> pageResp = new PageResp();
  26. pageResp.setTotal(pageInfo.getTotal());
  27. pageResp.setList(respList);
  28. return pageResp;
  29. }
  30. /**
  31. * 电子书保存接口(保存包括编辑保存->更新, 也包括新增保存->新增, 根据req是否有id来判断)
  32. * @param req 电子书保存参数
  33. */
  34. public void save(CategorySaveReq req) {
  35. Category category = CopyUtil.copy(req, Category.class);
  36. if(ObjectUtils.isEmpty(req.getId())) {
  37. // 新增
  38. category.setId(snowFlake.nextId());
  39. categoryMapper.insert(category);
  40. } else {
  41. // 更新
  42. // 因为updateByPrimaryKey传递的Category类型的参数,所以需要将CategorySaveReq 转化成Category
  43. categoryMapper.updateByPrimaryKey(category);
  44. }
  45. }
  46. /**
  47. * 电子书删除接口(按照id进行删除)
  48. * @param id
  49. */
  50. public void delete(long id) {
  51. categoryMapper.deleteByPrimaryKey(id);
  52. }
  53. }

接下来就是实体类,我们有三个实体类,分别在对应的req和resp文件下创建即可:CategoryQueryReq, CategorySaveReq, CategoryQueryResp。

2. 前端代码展示

前端需要在views/admin下面创建admin-category.vue文件,然后实际上就是将admin-ebook修修改改即可:

  1. <template>
  2. <a-layout>
  3. <a-layout-content
  4. :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
  5. >
  6. <p>
  7. <a-form layout="inline" :model="param">
  8. <a-form-item>
  9. <a-button type="primary" @click="handleQuery()">
  10. 查询
  11. </a-button>
  12. </a-form-item>
  13. <a-form-item>
  14. <a-button type="primary" @click="add()">
  15. 新增
  16. </a-button>
  17. </a-form-item>
  18. </a-form>
  19. </p>
  20. <p>
  21. <a-alert
  22. class="tip"
  23. message="小提示:这里的分类会显示到首页的侧边菜单"
  24. type="info"
  25. closable
  26. />
  27. </p>
  28. <a-table
  29. v-if="level1.length > 0"
  30. :columns="columns"
  31. :row-key="record => record.id"
  32. :data-source="level1"
  33. :loading="loading"
  34. :pagination="false"
  35. :defaultExpandAllRows="true"
  36. >
  37. <template #cover="{ text: cover }">
  38. <img v-if="cover" :src="cover" alt="avatar" />
  39. </template>
  40. <template v-slot:action="{ text, record }">
  41. <a-space size="small">
  42. <a-button type="primary" @click="edit(record)">
  43. 编辑
  44. </a-button>
  45. <a-popconfirm
  46. title="删除后不可恢复,确认删除?"
  47. ok-text="是"
  48. cancel-text="否"
  49. @confirm="handleDelete(record.id)"
  50. >
  51. <a-button type="danger">
  52. 删除
  53. </a-button>
  54. </a-popconfirm>
  55. </a-space>
  56. </template>
  57. </a-table>
  58. </a-layout-content>
  59. </a-layout>
  60. <a-modal
  61. title="分类表单"
  62. v-model:visible="modalVisible"
  63. :confirm-loading="modalLoading"
  64. @ok="handleModalOk"
  65. >
  66. <a-form :model="category" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
  67. <a-form-item label="名称">
  68. <a-input v-model:value="category.name" />
  69. </a-form-item>
  70. <a-form-item label="父分类">
  71. <a-select
  72. v-model:value="category.parent"
  73. ref="select"
  74. >
  75. <a-select-option :value="0">
  76. </a-select-option>
  77. <a-select-option v-for="c in level1" :key="c.id" :value="c.id" :disabled="category.id === c.id">
  78. {{c.name}}
  79. </a-select-option>
  80. </a-select>
  81. </a-form-item>
  82. <a-form-item label="顺序">
  83. <a-input v-model:value="category.sort" />
  84. </a-form-item>
  85. </a-form>
  86. </a-modal>
  87. </template>
  88. <script lang="ts">
  89. import { defineComponent, onMounted, ref } from 'vue';
  90. import axios from 'axios';
  91. import { message } from 'ant-design-vue';
  92. import {Tool} from "@/util/tool";
  93. export default defineComponent({
  94. name: 'AdminCategory',
  95. setup() {
  96. const param = ref();
  97. param.value = {};
  98. const categorys = ref();
  99. const loading = ref(false);
  100. const columns = [
  101. {
  102. title: '名称',
  103. dataIndex: 'name'
  104. },
  105. // {
  106. // title: '父分类',
  107. // key: 'parent',
  108. // dataIndex: 'parent'
  109. // },
  110. {
  111. title: '顺序',
  112. dataIndex: 'sort'
  113. },
  114. {
  115. title: 'Action',
  116. key: 'action',
  117. slots: { customRender: 'action' }
  118. }
  119. ];
  120. /**
  121. * 一级分类树,children属性就是二级分类
  122. * [{
  123. * id: "",
  124. * name: "",
  125. * children: [{
  126. * id: "",
  127. * name: "",
  128. * }]
  129. * }]
  130. */
  131. const level1 = ref(); // 一级分类树,children属性就是二级分类
  132. level1.value = [];
  133. /**
  134. * 数据查询
  135. **/
  136. const handleQuery = () => {
  137. loading.value = true;
  138. // 如果不清空现有数据,则编辑保存重新加载数据后,再点编辑,则列表显示的还是编辑前的数据
  139. level1.value = [];
  140. axios.get("/category/all").then((response) => {
  141. loading.value = false;
  142. const data = response.data;
  143. if (data.success) {
  144. categorys.value = data.content;
  145. console.log("原始数组:", categorys.value);
  146. level1.value = [];
  147. level1.value = Tool.array2Tree(categorys.value, 0);
  148. console.log("树形结构:", level1);
  149. } else {
  150. message.error(data.message);
  151. }
  152. });
  153. };
  154. // -------- 表单 ---------
  155. const category = ref({});
  156. const modalVisible = ref(false);
  157. const modalLoading = ref(false);
  158. const handleModalOk = () => {
  159. modalLoading.value = true;
  160. axios.post("/category/save", category.value).then((response) => {
  161. modalLoading.value = false;
  162. const data = response.data; // data = commonResp
  163. if (data.success) {
  164. modalVisible.value = false;
  165. // 重新加载列表
  166. handleQuery();
  167. } else {
  168. message.error(data.message);
  169. }
  170. });
  171. };
  172. /**
  173. * 编辑
  174. */
  175. const edit = (record: any) => {
  176. modalVisible.value = true;
  177. category.value = Tool.copy(record);
  178. };
  179. /**
  180. * 新增
  181. */
  182. const add = () => {
  183. modalVisible.value = true;
  184. category.value = {};
  185. };
  186. const handleDelete = (id: number) => {
  187. axios.delete("/category/delete/" + id).then((response) => {
  188. const data = response.data; // data = commonResp
  189. if (data.success) {
  190. // 重新加载列表
  191. handleQuery();
  192. } else {
  193. message.error(data.message);
  194. }
  195. });
  196. };
  197. onMounted(() => {
  198. handleQuery();
  199. });
  200. return {
  201. param,
  202. // categorys,
  203. level1,
  204. columns,
  205. loading,
  206. handleQuery,
  207. edit,
  208. add,
  209. category,
  210. modalVisible,
  211. modalLoading,
  212. handleModalOk,
  213. handleDelete
  214. }
  215. }
  216. });
  217. </script>
  218. <style scoped>
  219. img {
  220. width: 50px;
  221. height: 50px;
  222. }
  223. </style>

分类表格显示优化

分类查询实际上不需要分页,一次查出全部数据,另外我们需要改为树形表格展示,所以我们前后端都来改造一下:

1. 后端改造

后端我们之前书写的list先留着,我们新书写一个all的接口,一次性将所有的分类查出来:

  1. @GetMapping("/all")
  2. public CommonResp all(@Valid CategoryQueryReq req) {
  3. // controller层尽量不要出现Category这个实体类
  4. // 因为实体类是和数据库一一对应的
  5. CommonResp<List<CategoryQueryResp>> resp = new CommonResp<>();
  6. List<CategoryQueryResp> list = categoryService.all(req);
  7. resp.setContent(list);
  8. return resp;
  9. }
  1. /**
  2. * 查询分类
  3. * @param req 分类查询参数
  4. * @return 分类分页列表
  5. */
  6. public List<CategoryQueryResp> all(CategoryQueryReq req) {
  7. CategoryExample categoryExample = new CategoryExample();
  8. categoryExample.setOrderByClause("sort asc"); // 按照sort排序
  9. List<Category> categorysList = categoryMapper.selectByExample(categoryExample);
  10. List<CategoryQueryResp> respList = CopyUtil.copyList(categorysList, CategoryQueryResp.class);
  11. return respList;
  12. }

2. 前端改造

前端的代码我们前面在分类基本增删改查的时候就全部展示了,现在我们需要展示一下如何将数据展示成为树形结构,这个递归比较重要,所以好好理解一下:

  1. /**
  2. * 使用递归将数组转为树形结构
  3. * 父ID属性为parent
  4. */
  5. public static array2Tree (array: any, parentId: number) {
  6. if (Tool.isEmpty(array)) {
  7. return [];
  8. }
  9. const result = [];
  10. for (let i = 0; i < array.length; i++) {
  11. const c = array[i];
  12. // console.log(Number(c.parent), Number(parentId));
  13. if (Number(c.parent) === Number(parentId)) {
  14. result.push(c);
  15. // 递归查看当前节点对应的子节点
  16. const children = Tool.array2Tree(array, c.id);
  17. if (Tool.isNotEmpty(children)) {
  18. c.children = children;
  19. }
  20. }
  21. }
  22. return result;
  23. }

分类编辑功能优化

由于分类比较特殊,在编辑(新增/修改)分类时,支持选中某一个分类作为父分类,或者没有分类,所以我们需要将,分类应该将输入框改变成为下拉框,下拉框的选项我们在这里是写死的,如果有活的必须够短有提供接口,代码我们可以在分类基本增删改查-前端代码展示当中看到完整的前端代码。

电子书管理增加分类选择

电子书的管理页面当中的分类进行了优化,显示的就不再是具体的分类代码,而是使用a-cascader级联组件进行了优化,所以具体的项目代码我们展示在下面,前端代码无法仔细详解,大家仔细研究:

  1. <template>
  2. <a-layout>
  3. <a-layout-content
  4. :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
  5. >
  6. <p>
  7. <a-form layout="inline" :model="param">
  8. <a-form-item>
  9. <a-input v-model:value="param.name" placeholder="名称">
  10. </a-input>
  11. </a-form-item>
  12. <a-form-item>
  13. <a-button type="primary" @click="handleQuery({page: 1, size: pagination.pageSize})">
  14. 查询
  15. </a-button>
  16. </a-form-item>
  17. <a-form-item>
  18. <a-button type="primary" @click="add()">
  19. 新增
  20. </a-button>
  21. </a-form-item>
  22. </a-form>
  23. </p>
  24. <a-table
  25. :columns="columns"
  26. :row-key="record => record.id"
  27. :data-source="ebooks"
  28. :pagination="pagination"
  29. :loading="loading"
  30. @change="handleTableChange"
  31. >
  32. <template #cover="{ text: cover }">
  33. <img v-if="cover" :src="cover" alt="avatar" />
  34. </template>
  35. <template v-slot:category="{ text, record }">
  36. <span>{{ getCategoryName(record.category1Id) }} / {{ getCategoryName(record.category2Id) }}</span>
  37. </template>
  38. <template v-slot:action="{ text, record }">
  39. <a-space size="small">
  40. <router-link :to="'/admin/doc?ebookId=' + record.id">
  41. <a-button type="primary">
  42. 文档管理
  43. </a-button>
  44. </router-link>
  45. <a-button type="primary" @click="edit(record)">
  46. 编辑
  47. </a-button>
  48. <a-popconfirm
  49. title="删除后不可恢复,确认删除?"
  50. ok-text="是"
  51. cancel-text="否"
  52. @confirm="handleDelete(record.id)"
  53. >
  54. <a-button type="danger">
  55. 删除
  56. </a-button>
  57. </a-popconfirm>
  58. </a-space>
  59. </template>
  60. </a-table>
  61. </a-layout-content>
  62. </a-layout>
  63. <a-modal
  64. title="电子书表单"
  65. v-model:visible="modalVisible"
  66. :confirm-loading="modalLoading"
  67. @ok="handleModalOk"
  68. >
  69. <a-form :model="ebook" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
  70. <a-form-item label="封面">
  71. <a-input v-model:value="ebook.cover" />
  72. </a-form-item>
  73. <a-form-item label="名称">
  74. <a-input v-model:value="ebook.name" />
  75. </a-form-item>
  76. <a-form-item label="分类">
  77. <a-cascader
  78. v-model:value="categoryIds"
  79. :field-names="{ label: 'name', value: 'id', children: 'children' }"
  80. :options="level1"
  81. />
  82. </a-form-item>
  83. <a-form-item label="描述">
  84. <a-input v-model:value="ebook.description" type="textarea" />
  85. </a-form-item>
  86. </a-form>
  87. </a-modal>
  88. </template>
  89. <script lang="ts">
  90. import { defineComponent, onMounted, ref } from 'vue';
  91. import axios from 'axios';
  92. import { message } from 'ant-design-vue';
  93. import {Tool} from "@/util/tool";
  94. export default defineComponent({
  95. name: 'AdminEbook',
  96. setup() {
  97. const param = ref();
  98. param.value = {};
  99. const ebooks = ref();
  100. const pagination = ref({
  101. current: 1,
  102. pageSize: 10,
  103. total: 0
  104. });
  105. const loading = ref(false);
  106. const columns = [
  107. {
  108. title: '封面',
  109. dataIndex: 'cover',
  110. slots: { customRender: 'cover' }
  111. },
  112. {
  113. title: '名称',
  114. dataIndex: 'name'
  115. },
  116. {
  117. title: '分类',
  118. slots: { customRender: 'category' }
  119. },
  120. {
  121. title: '文档数',
  122. dataIndex: 'docCount'
  123. },
  124. {
  125. title: '阅读数',
  126. dataIndex: 'viewCount'
  127. },
  128. {
  129. title: '点赞数',
  130. dataIndex: 'voteCount'
  131. },
  132. {
  133. title: 'Action',
  134. key: 'action',
  135. slots: { customRender: 'action' }
  136. }
  137. ];
  138. /**
  139. * 数据查询
  140. **/
  141. const handleQuery = (params: any) => {
  142. loading.value = true;
  143. // 如果不清空现有数据,则编辑保存重新加载数据后,再点编辑,则列表显示的还是编辑前的数据
  144. ebooks.value = [];
  145. axios.get("/ebook/list", {
  146. params: {
  147. page: params.page,
  148. size: params.size,
  149. name: param.value.name
  150. }
  151. }).then((response) => {
  152. loading.value = false;
  153. const data = response.data;
  154. if (data.success) {
  155. ebooks.value = data.content.list;
  156. // 重置分页按钮
  157. pagination.value.current = params.page;
  158. pagination.value.total = data.content.total;
  159. } else {
  160. message.error(data.message);
  161. }
  162. });
  163. };
  164. /**
  165. * 表格点击页码时触发
  166. */
  167. const handleTableChange = (pagination: any) => {
  168. console.log("看看自带的分页参数都有啥:" + pagination);
  169. handleQuery({
  170. page: pagination.current,
  171. size: pagination.pageSize
  172. });
  173. };
  174. // -------- 表单 ---------
  175. /**
  176. * 数组,[100, 101]对应:前端开发 / Vue
  177. */
  178. const categoryIds = ref();
  179. const ebook = ref();
  180. const modalVisible = ref(false);
  181. const modalLoading = ref(false);
  182. const handleModalOk = () => {
  183. modalLoading.value = true;
  184. ebook.value.category1Id = categoryIds.value[0];
  185. ebook.value.category2Id = categoryIds.value[1];
  186. axios.post("/ebook/save", ebook.value).then((response) => {
  187. modalLoading.value = false;
  188. const data = response.data; // data = commonResp
  189. if (data.success) {
  190. modalVisible.value = false;
  191. // 重新加载列表
  192. handleQuery({
  193. page: pagination.value.current,
  194. size: pagination.value.pageSize,
  195. });
  196. } else {
  197. message.error(data.message);
  198. }
  199. });
  200. };
  201. /**
  202. * 编辑
  203. */
  204. const edit = (record: any) => {
  205. modalVisible.value = true;
  206. ebook.value = Tool.copy(record);
  207. categoryIds.value = [ebook.value.category1Id, ebook.value.category2Id]
  208. };
  209. /**
  210. * 新增
  211. */
  212. const add = () => {
  213. modalVisible.value = true;
  214. ebook.value = {};
  215. };
  216. const handleDelete = (id: number) => {
  217. axios.delete("/ebook/delete/" + id).then((response) => {
  218. const data = response.data; // data = commonResp
  219. if (data.success) {
  220. // 重新加载列表
  221. handleQuery({
  222. page: pagination.value.current,
  223. size: pagination.value.pageSize,
  224. });
  225. } else {
  226. message.error(data.message);
  227. }
  228. });
  229. };
  230. const level1 = ref();
  231. let categorys: any;
  232. /**
  233. * 查询所有分类
  234. **/
  235. const handleQueryCategory = () => {
  236. loading.value = true;
  237. axios.get("/category/all").then((response) => {
  238. loading.value = false;
  239. const data = response.data;
  240. if (data.success) {
  241. categorys = data.content;
  242. console.log("原始数组:", categorys);
  243. level1.value = [];
  244. level1.value = Tool.array2Tree(categorys, 0);
  245. console.log("树形结构:", level1.value);
  246. // 加载完分类后,再加载电子书,否则如果分类树加载很慢,则电子书渲染会报错
  247. handleQuery({
  248. page: 1,
  249. size: pagination.value.pageSize,
  250. });
  251. } else {
  252. message.error(data.message);
  253. }
  254. });
  255. };
  256. const getCategoryName = (cid: number) => {
  257. // console.log(cid)
  258. let result = "";
  259. categorys.forEach((item: any) => {
  260. if (item.id === cid) {
  261. // return item.name; // 注意,这里直接return不起作用
  262. result = item.name;
  263. }
  264. });
  265. return result;
  266. };
  267. onMounted(() => {
  268. handleQueryCategory();
  269. });
  270. return {
  271. param,
  272. ebooks,
  273. pagination,
  274. columns,
  275. loading,
  276. handleTableChange,
  277. handleQuery,
  278. getCategoryName,
  279. edit,
  280. add,
  281. ebook,
  282. modalVisible,
  283. modalLoading,
  284. handleModalOk,
  285. categoryIds,
  286. level1,
  287. handleDelete
  288. }
  289. }
  290. });
  291. </script>
  292. <style scoped>
  293. img {
  294. width: 50px;
  295. height: 50px;
  296. }
  297. </style>

首页显示分类菜单

思路非常简单,第一步加载数据变成树形结构,第二步将菜单做出循环,然后使用a-sub-menu这个组件写成树形的菜单。

  1. <template>
  2. <a-layout>
  3. <a-layout-sider width="200" style="background: #fff">
  4. <a-menu
  5. mode="inline"
  6. :style="{ height: '100%', borderRight: 0 }"
  7. @click="handleClick"
  8. :openKeys="openKeys"
  9. >
  10. <a-menu-item key="welcome">
  11. <MailOutlined />
  12. <span>欢迎</span>
  13. </a-menu-item>
  14. <a-sub-menu v-for="item in level1" :key="item.id" :disabled="true">
  15. <template v-slot:title>
  16. <span><user-outlined />{{item.name}}</span>
  17. </template>
  18. <a-menu-item v-for="child in item.children" :key="child.id">
  19. <MailOutlined /><span>{{child.name}}</span>
  20. </a-menu-item>
  21. </a-sub-menu>
  22. <a-menu-item key="tip" :disabled="true">
  23. <span>以上菜单在分类管理配置</span>
  24. </a-menu-item>
  25. </a-menu>
  26. </a-layout-sider>
  27. <a-layout-content
  28. :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
  29. >
  30. <div class="welcome" v-show="isShowWelcome">
  31. <the-welcome></the-welcome>
  32. </div>
  33. <a-list v-show="!isShowWelcome" item-layout="vertical" size="large" :grid="{ gutter: 20, column: 3 }" :data-source="ebooks">
  34. <template #renderItem="{ item }">
  35. <a-list-item key="item.name">
  36. <template #actions>
  37. <span>
  38. <component v-bind:is="'FileOutlined'" style="margin-right: 8px" />
  39. {{ item.docCount }}
  40. </span>
  41. <span>
  42. <component v-bind:is="'UserOutlined'" style="margin-right: 8px" />
  43. {{ item.viewCount }}
  44. </span>
  45. <span>
  46. <component v-bind:is="'LikeOutlined'" style="margin-right: 8px" />
  47. {{ item.voteCount }}
  48. </span>
  49. </template>
  50. <a-list-item-meta :description="item.description">
  51. <template #title>
  52. <router-link :to="'/doc?ebookId=' + item.id">
  53. {{ item.name }}
  54. </router-link>
  55. </template>
  56. <template #avatar><a-avatar :src="item.cover"/></template>
  57. </a-list-item-meta>
  58. </a-list-item>
  59. </template>
  60. </a-list>
  61. </a-layout-content>
  62. </a-layout>
  63. </template>
  64. <script lang="ts">
  65. import { defineComponent, onMounted, ref, reactive, toRef } from 'vue';
  66. import axios from 'axios';
  67. import { message } from 'ant-design-vue';
  68. import {Tool} from "@/util/tool";
  69. import TheWelcome from '@/components/the-welcome.vue';
  70. export default defineComponent({
  71. name: 'Home',
  72. components: {
  73. TheWelcome
  74. },
  75. setup() {
  76. const ebooks = ref();
  77. // const ebooks1 = reactive({books: []});
  78. const openKeys = ref();
  79. const level1 = ref();
  80. let categorys: any;
  81. /**
  82. * 查询所有分类
  83. **/
  84. const handleQueryCategory = () => {
  85. axios.get("/category/all").then((response) => {
  86. const data = response.data;
  87. if (data.success) {
  88. categorys = data.content;
  89. console.log("原始数组:", categorys);
  90. // 加载完分类后,将侧边栏全部展开
  91. openKeys.value = [];
  92. for (let i = 0; i < categorys.length; i++) {
  93. openKeys.value.push(categorys[i].id)
  94. }
  95. level1.value = [];
  96. level1.value = Tool.array2Tree(categorys, 0);
  97. console.log("树形结构:", level1.value);
  98. } else {
  99. message.error(data.message);
  100. }
  101. });
  102. };
  103. const isShowWelcome = ref(true);
  104. let categoryId2 = 0;
  105. /**
  106. * 查询数据
  107. */
  108. const handleQueryEbook = () => {
  109. axios.get("/ebook/list", {
  110. params: {
  111. page: 1,
  112. size: 1000,
  113. categoryId2: categoryId2
  114. }
  115. }).then((response) => {
  116. const data = response.data;
  117. ebooks.value = data.content.list;
  118. // ebooks1.books = data.content;
  119. });
  120. };
  121. const handleClick = (value: any) => {
  122. // console.log("menu click", value)
  123. if (value.key === 'welcome') {
  124. isShowWelcome.value = true;
  125. } else {
  126. categoryId2 = value.key;
  127. isShowWelcome.value = false;
  128. handleQueryEbook();
  129. }
  130. // isShowWelcome.value = value.key === 'welcome';
  131. };
  132. onMounted(() => {
  133. handleQueryCategory();
  134. });
  135. return {
  136. ebooks,
  137. pagination: {
  138. onChange: (page: any) => {
  139. console.log(page);
  140. },
  141. pageSize: 3,
  142. },
  143. handleClick,
  144. level1,
  145. isShowWelcome,
  146. openKeys
  147. }
  148. }
  149. });
  150. </script>
  151. <style scoped>
  152. .ant-avatar {
  153. width: 50px;
  154. height: 50px;
  155. line-height: 50px;
  156. border-radius: 8%;
  157. margin: 5px 0;
  158. }
  159. </style>

点击分类菜单显示电子书

后端我们就做两个事情,由于前端需要查询电子书的时候,需要添加上分类,所以查询的时候要带上分类参数categoryId2,于是我们就需要在EbookQueryReq.java当中去添加categoryId2。

  1. package com.taopoppy.wiki.req;
  2. public class EbookQueryReq extends PageReq{
  3. private Long id;
  4. private String name;
  5. private Long categoryId2; // 添加该参数
  6. public Long getId() {
  7. return id;
  8. }
  9. public void setId(Long id) {
  10. this.id = id;
  11. }
  12. public String getName() {
  13. return name;
  14. }
  15. public void setName(String name) {
  16. this.name = name;
  17. }
  18. public Long getCategoryId2() {
  19. return categoryId2;
  20. }
  21. public void setCategoryId2(Long categoryId2) {
  22. this.categoryId2 = categoryId2;
  23. }
  24. @Override
  25. public String toString() {
  26. return "EbookQueryReq{" +
  27. "id=" + id +
  28. ", name='" + name + '\'' +
  29. ", categoryId2=" + categoryId2 +
  30. '}';
  31. }
  32. }
  1. /**
  2. * 查询电子书
  3. * @param req 电子书查询参数
  4. * @return 电子书分页列表
  5. */
  6. public PageResp<EbookQueryResp> list(EbookQueryReq req) {
  7. EbookExample ebookExample = new EbookExample();
  8. EbookExample.Criteria criteria = ebookExample.createCriteria();
  9. if(!ObjectUtils.isEmpty(req.getName())) {
  10. criteria.andNameLike("%" + req.getName() + "%");
  11. }
  12. // 添加category的动态sql的写法
  13. if(!ObjectUtils.isEmpty(req.getCategoryId2())) {
  14. criteria.andCategory2IdEqualTo(req.getCategoryId2());
  15. }
  16. PageHelper.startPage(req.getPage(), req.getSize());
  17. List<Ebook> ebooksList = ebookMapper.selectByExample(ebookExample);
  18. PageInfo<Ebook> pageInfo = new PageInfo<>(ebooksList);
  19. LOG.info("总行数:{}",pageInfo.getTotal());
  20. LOG.info("总页数:{}",pageInfo.getPages());
  21. List<EbookQueryResp> respList = CopyUtil.copyList(ebooksList, EbookQueryResp.class);
  22. PageResp<EbookQueryResp> pageResp = new PageResp();
  23. pageResp.setTotal(pageInfo.getTotal());
  24. pageResp.setList(respList);
  25. return pageResp;
  26. }

前端的代码在home.vue,完整的代码我们已经在首页显示分类菜单当中完整的展示。