【wiki知识库】04.SpringBoot后端实现电子书的增删改查以及前端界面的展示

前端 0

  📝个人主页:哈__

期待您的关注 

目录

一、🔥今日内容

二、🌏前端页面的改造

2.1新增电子书管理页面

2.2新增路由规则

2.3修改the-header代码

三、🚗SpringBoot后端Ebook模块改造

3.1增加电子书增/改接口

3.1.1新增EbookSaveParam

3.1.2添加Controller代码

3.1.3在Ebook实体类上增加一个注解

3.2 增加电子书删除接口

四、🔨测试 

4.1添加功能测试

4.2修改功能测试。

4.3删除功能测试


一、🔥今日内容

【wiki知识库】03.前后端的初步交互(展现所有的电子书)-CSDN博客

上一次带领大家把前端的首页部分实现了一下,成功的从数据库当中取出了我们的信息并且展示在前端页面,到了下图的部分。

今天主要是把这个网页的界面初步优化一下,修改一下导航栏以及增加电子书管理模块。包含电子书的查询功能、新增功能、编辑功能和删除功能(不包括文档管理)。

二、🌏前端页面的改造

2.1新增电子书管理页面

我在src下新建了admin文件夹,这个文件夹中的内容是给网站管理员看到的,所以放到了admin目录,名字为admin-ebook.vue。

 admin-ebook.vue的具体内容如下。这个文件里我注释掉了一些信息,而且这个文件中的内容包含了页面需要的功能很多,有的一些并不是今天要讲解的内容,所以并没有使用到。今天主要就是想带着大家做出一个电子书管理的模块来。

<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-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="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-cascader          v-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 = commonResp          if (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 = commonResp          if (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(() => {        handleQuery({              page: pagination.value.current,              size: pagination.value.pageSize,            });      });      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>

上边的内容很多,但我们今天核心的前端调用部分是下边的代码。

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);          }        });      };

当我们进去这个页面的时候,首先就会调用下方代码,请求路径也恰好是我们后端之前写过的list接口,用来分页查询电子书信息。

onMounted(() => {        handleQuery({              page: pagination.value.current,              size: pagination.value.pageSize,            });      });

2.2新增路由规则

既然都要新增一个电子书的管理页面了,那我们也要为这个页面分配一个能够匹配到的路由路径。

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'import HomeView from '../views/HomeView.vue'import AdminEbook from '@/views/admin/admin-ebook.vue'import AboutView from '../views/AboutView.vue'const routes: Array<RouteRecordRaw> = [  {    path: '/',    name: 'home',    component: HomeView  },  {    path: '/about',    name: 'about',    component:AboutView  },  {    path: '/admin/ebook',    name: 'AdminEbook',    component: AdminEbook  },]const router = createRouter({  history: createWebHistory(process.env.BASE_URL),  routes})export default router

2.3修改the-header代码

 我们新增的组件是通过点击the-header组件中的按钮实现跳转的,这里要修改一些代码。我在这个页面添加了一些路由用于跳转我们的组件。

<template>     <a-layout-header class="header">      <div class="logo" ></div>      <a-menu        theme="dark"        mode="horizontal"        v-model:selectedKeys="sselectedKeys1"        :style="{ lineHeight: '64px' }"      >        <a-menu-item key="/"><router-link to="/">首页</router-link></a-menu-item>        <a-menu-item key="/admin/ebook"><router-link to="/admin/ebook">电子书管理</router-link></a-menu-item>        <a-menu-item key="/about"><router-link to="/about">关于我们</router-link></a-menu-item>      </a-menu>    </a-layout-header></template>

至此我们前端改造成功,接下来就是后端了。

三、🚗SpringBoot后端Ebook模块改造

3.1增加电子书增/改接口

在我们点击新增按钮或者编辑按钮的时候,会弹出一个窗口来添加或者修改电子书的信息,当我们点击确定之后会向后端发送请求。请求接口是/ebook/save,注意,这里的save指代两个功能,第一个是新增,第二个是修改。

3.1.1新增EbookSaveParam

这个实体类用于封装我们前端传过来的电子书的信息。

@Datapublic class EbookSaveParam {    private Long id;    @NotNull(message = "【名称】不能为空")    private String name;    private Long category1Id;    private Long category2Id;    private String description;    private String cover;    private Integer docCount;    private Integer viewCount;    private Integer voteCount;}

3.1.2添加Controller代码

这里我直接使用的MybatisPlus封装好的函数

    /**     *  保存/修改电子书     * @param ebookQueryParam     * @return     */    @PostMapping("/save")    public CommonResp save(@Validated @RequestBody EbookSaveParam ebookQueryParam){        boolean res = ebookService.saveOrUpdate(CopyUtil.copy(ebookQueryParam,Ebook.class));        String message = Boolean.TRUE.equals(res) ? "操作成功":"操作失败";        return new CommonResp<>(true,message,null);    }

3.1.3在Ebook实体类上增加一个注解

我们要使用雪花算法生成的id存储在数据库当中。

  /**     * id     */    @TableId(type = IdType.ASSIGN_UUID)    private Long id;

当然除了雪花id还有其他的id可供选择。这里就不一一给大家说了。


3.2 增加电子书删除接口

删除功能的接口是下边图中所示。采用的是Restful风格的请求。

 对应Controller代码。

 /**     * 删除电子书     * @param id 电子书id     * @return     */    @DeleteMapping("/delete/{id}")    public CommonResp delete(@PathVariable("id") Long id){        boolean res = ebookService.removeById(id);        String message = Boolean.TRUE.equals(res) ? "删除成功":"删除失败";        return new CommonResp<>(true,message,null);    }

四、🔨测试 

4.1添加功能测试

测试之前还要注释两行代码。因为我们的分类模块还没写,这里不能传值。

随便加一个电子书上去。

结果还是没问题的。

4.2修改功能测试。

不在截图展示了,点击编辑按钮之后哦修改数据我这里是正确的。

4.3删除功能测试

这时就有问题了,我删除怎么成功不了?那么你是否会分析原因呢?先看看前端的打印。

仔细看看我们传过去的id是什么,再看看你的数据库里是否有这个id? 显然是没有的。

这里就要说一下前后端传输数据的数据精度丢失问题了,因为我们传的数据是一个整形,而且数值很大,在传输的过程总是有精度问题得,想要解决就需要在后端加一个配置类。

package com.my.hawiki.config;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.databind.module.SimpleModule;import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;/** * 统一注解,解决前后端交互Long类型精度丢失的问题 */@Configurationpublic class JacksonConfig {    @Bean    public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {        ObjectMapper objectMapper = builder.createXmlMapper(false).build();        SimpleModule simpleModule = new SimpleModule();        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);        objectMapper.registerModule(simpleModule);        return objectMapper;    }}

之后在运行代码试试。大功告成。

 电子书管理页面的基本几个功能差不多就这么多了。

也许您对下面的内容还感兴趣: