📝个人主页:哈__
期待您的关注
目录
一、🔥今日目标
【wiki知识库】04.SpringBoot后端实现电子书的增删改查以及前端界面的展示-CSDN博客
上回带大家把电子书管理的模块做了出来,能够实现电子书的添加、修改和删除功能了,今天带着大家把分类的接口实现一下。下方我添加了一个分类管理的组件,展示我们当前的所有分类,你可以看到这个分类页面还是一个树形结构的。
除了分类管理,我们的首页也变动了一下。首页的导航栏加载的是我们已经有的分类,同时还加上了一个欢迎页面。
二、🌏前端部分的改造
2.1 新增一个tool.ts
在util包下建立一个tool.ts文件,这个文件是我们的工具文件,之前的代码可能也用到过了,我忘记给大家了。
export class Tool { /** * 空校验 null或""都返回true */ public static isEmpty(obj: any) { if ((typeof obj === 'string')) { return !obj || obj.replace(/\s+/g, "") === "" } else { return (!obj || JSON.stringify(obj) === "{}" || obj.length === 0); } } /** * 非空校验 */ public static isNotEmpty(obj: any) { return !this.isEmpty(obj); } /** * 对象复制 * @param obj */ public static copy(obj: object) { if (Tool.isNotEmpty(obj)) { return JSON.parse(JSON.stringify(obj)); } } /** * 使用递归将数组转为树形结构 * 父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; } /** * 随机生成[len]长度的[radix]进制数 * @param len * @param radix 默认62 * @returns {string} */ public static uuid(len: number, radix = 62) { const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); const uuid = []; radix = radix || chars.length; for (let i = 0; i < len; i++) { uuid[i] = chars[0 | Math.random() * radix]; } return uuid.join(''); } }
2.2 新增admin-categoty.vue
在admin包下,创建admin-category.vue,这个组件用来展示我们的分类信息。这一部分我带着大家稍微过一下。
分类添加功能:
在我们点击添加或者编辑功能的时候,会把下边的代码以一个窗口的模式弹出,在这个窗口中展示了当前分类的名称,当前分类的父分类是谁以及当前分类的分类序号。在我们为一个分类进行添加或者修改的时候,我们都要涉及到这个分类到底是第一分类还是第二分类的问题,我们使用了一个level1变量来保存我们的分类结构,这个结构下边在讲。
<a-modal title="分类表单" 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-select v-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>
我们的分类结构可以用一张图来表示。 我给每一个分类都排上了一个编号,第一级分类的parent编号都为0,二级分类的parent编号则要相对应其父分类的编号。
level变量的封装过程:
我们的level变量是和我们全部的分类变量有关的,我们先要获取所有的分类然后对分类进行重新组合。
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); } }); };
这时候打开我们的tool.ts来看一看。你会看到一共有两个参数,第一个参数呢我们传的是一个数据数组,第二个我们传来的是0。
首先我们判断一下传过来的数组是不是空的,如果是空的直接返回,否则的话执行下方逻辑。
首先遍历我们所有的分类,检查每一个分类的父分类的编号是不是我们传过来的0,这里你应该会理解为什么这样做,因为我们要把一个数据数据重新格式化成树的形式,那我们一定要先找到这棵树的一级分类,也就是父节点编号为0的分类,找到了之后呢我们把这个分类加到result当中。然后呢我们再次调用array2Tree这个方法,同时传入两个参数,第一个参数还是之前的全部分类的数组,但是第二个参数就不是0了,是我们刚才加入到result中的分类的编号,我们这次调用这个方法的目的是为了找到一级分类的子分类:二级分类。
这样的一次递归调用就可以将数据进行树化。(不懂私信我)
/** * 使用递归将数组转为树形结构 * 父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; }
全部代码如下:
<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()"> 查询 </a-button> </a-form-item> <a-form-item> <a-button type="primary" @click="add()"> 新增 </a-button> </a-form-item> </a-form> </p> <p> <a-alert class="tip" message="小提示:这里的分类会显示到首页的侧边菜单" type="info" closable /> </p> <a-table v-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-popconfirm title="删除后不可恢复,确认删除?" 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-modal title="分类表单" 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-select v-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 = commonResp if (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 = commonResp if (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>
2.3 添加新的路由规则
{ path: '/admin/category', name: 'AdminCateGory', component: AdminCategory },
2.4 添加the-welcome.vue
在component文件夹下边 添加the-welcome.vue页面。
<template> <span>欢迎</span> </template> <script lang="ts"> name: 'the-welcome' </script> <style scoped> </style>
2.5 修改HomeView.vue
这里做出了一些修改,一个是the-welcome组件的展示,还有一个就是页面侧边栏的展示,我们之前展示的是页面默认的,现在我们要展示数据库当中的分类结构。里边我们有一些代码还用不到,但是我没有注释掉。
<template> <a-layout> <a-layout-sider width="200" style="background: #fff"> <a-menu mode="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(); // handleQueryEbook(); }); return { ebooks, // ebooks2: toRef(ebooks1, "books"), // listData, 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>
三、❗注意
在之前写的admin-ebook.vue当中还有一些代码是注释掉的,因为之前没有写分类模块,现在我们需要使用分类的功能了,我们还需要解除一些注释。大家可以看看哪些地方有关category的被注释掉了,大家可以打开,后端接口下一篇文章就会带大家写出来。
这里需要修改一些onMounted()函数中的代码,修改成下边的部分。
onMounted(() => { handleQueryCategory(); // handleQueryEbook(); });
此外,这个网站的标题头部的信息可能我之前没加上去,就是这个。
在the-header.vue中修改一下自己的代码。
在下边的style中加上样式。然后就可以展示出来了。
<style> .logo { width: 120px; height: 31px; /*background: rgba(255, 255, 255, 0.2);*/ /*margin: 16px 28px 16px 0;*/ float: left; color: white; font-size: 18px; } </style>