1.增加电子书管理页面
⭐增加一个页面需要增加三个部分的内容:
①页面 ②菜单(点击菜单才会跳到页面) ③路由(路由跟页面是绑定起来的)
⭐增加页面,是在views下
改页面名称:对应的路由也要改router
①增加电子书管理页面(登录之后才可以操作):views-admin-admin-ebook
<template>
<div class="about">
<h1>电子书管理</h1>
</div>
</template>
②增加路由:router -index文件中
import AdminEbook from '../views/admin/admin-ebook.vue'
path: '/admin/ebook',
name: 'AdminEbook',
component: AdminEbook
③增加菜单:components-the.header.vue中
<a-menu-item key="/admin/ebook">
<router-link to="/admin/ebook">电子书管理</router-link>
</a-menu-item>
跳转用router-link to ,点击电子书管理就会跳到/admin/ebook
key和to的值是一样的
2.电子书表格展示(admin-ebook.vue)
①改造页面,修改布局:
<template>
<a-layout>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<div class="about">
<h1>电子书管理</h1>
</div>
</a-layout-content>
</a-layout>
</template>
②改造管理页面
<template>
<a-layout>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<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:action="{ text, record }">
<a-space size="small">
<a-button type="primary">
编辑
</a-button>
<a-button type="danger">
删除
</a-button>
</a-space>
</template>
</a-table>
</a-layout-content>
</a-layout>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue';
import axios from 'axios';
export default defineComponent({
name: 'AdminEbook',
setup() {
const ebooks = ref();
const pagination = ref({
current: 1,
pageSize: 2,
total: 0
});
const loading = ref(false);
const columns = [
{
title: '封面',
dataIndex: 'cover',
slots: { customRender: 'cover' }
},
{
title: '名称',
dataIndex: 'name'
},
{
title: '分类一',
key: 'category1Id',
dataIndex: 'category1Id'
},
{
title: '分类二',
dataIndex: 'category2Id'
},
{
title: '文档数',
dataIndex: 'docCount'
},
{
title: '阅读数',
dataIndex: 'viewCount'
},
{
title: '点赞数',
dataIndex: 'voteCount'
},
{
title: 'Action',
key: 'action',
slots: { customRender: 'action' }
}
];
/**
* 数据查询
**/
const handleQuery = (params: any) => {
loading.value = true;
axios.get("/ebook/list", params).then((response) => {
loading.value = false;
const data = response.data;
ebooks.value = data.content;
// 重置分页按钮
pagination.value.current = params.page;
});
};
/**
* 表格点击页码时触发
*/
const handleTableChange = (pagination: any) => {
console.log("看看自带的分页参数都有啥:" + pagination);
handleQuery({
page: pagination.current,
size: pagination.pageSize
});
};
onMounted(() => {
handleQuery({});
});
return {
ebooks,
pagination,
columns,
loading,
handleTableChange
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
在eslintrc中
'vue/no-unused-vars': 0,
3.使用PageHelper实现后端分页
①集成PageHelper插件
在pom文件中 增加依赖
<!--pagehelper 插件-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.13</version>
</dependency>
②修改电子书列表接口,支持分页(假分页数据)
⭐分页最重要的四个元素:两个请求参数,两个返回参数
前端传过来的参数(要查第几页 第几条), 返回给前端的参数(列表 total)
⭐分页最基本要查两次:一次是总数据,一次是当前页的列表数据
⭐⭐在EbookService中
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger LOG = LoggerFactory.getLogger(EbookService.class);
PageHelper.startPage(1, 3);
PageInfo<Ebook> pageInfo = new PageInfo<>(ebookList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
⭐⭐在application.properties文件中
将mapper层的日志级别设置为trace,最低级别,所有日志都会打印出来
#打印所有的sql日志:sql,参数,结果
logging.level.com.jiawa.wiki.mapper=trace
4.封装分页请求参数和返回参数
⭐分页最重要的四个元素:两个请求参数,两个返回参数
前端传过来的参数(要查第几页 第几条), 返回给前端的参数(列表 total)
将这四个参数封装为两个类
请求参数封装PageReq
①req-PageReq:两个参数 页码和条数 get set tostring(打日志)方法
②EbookReq类继承PageReq类
③EbookService中:把参数变为动态的,而不是写死的
PageHelper.startPage(req.getPage(), req.getSize());
返回结果封装PageResp
①resp-PageResp:两个参数 total list列表(电子书,用户等等 不确定,用泛型,泛型的名字可以随意) get set tostring(打日志)方法
private long total;
private List<T> list;
②EbookService中:返回整个PageResp
import com.jiawa.wiki.resp.PageResp;
public PageResp<EbookResp> list(EbookReq req) {
PageResp<EbookResp> pageResp = new PageResp();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
③EbookController
import com.jiawa.wiki.resp.PageResp;
CommonResp<PageResp<EbookResp>> resp = new CommonResp<>();
PageResp<EbookResp> list = ebookService.list(req);
5.前后端分页功能整合
①电子书管理页面前后端分页整合
admin-ebook.vue中:
pageSize: 4,
axios.get("/ebook/list", {
params: {
page: params.page,
size: params.size
}
}).then((response) => {
ebooks.value = data.content.list;
pagination.value.total = data.content.total;
handleQuery({
page: 1,
size: pagination.value.pageSize,
});
②首页显示电子书列表,使用固定分页参数,一次性查出所有的电子书
home.vue中:
axios.get("/ebook/list", {
params: {
page: 1,
size: 1000
}
}).then((response) => {
ebooks.value = data.content.list;
6.制作电子书表单
⭐return:将用到的变量,方法返回给html
①点击每一行编辑按钮,弹出编辑框
'@typescript-eslint/no-unused-vars'
在admin-ebook.vue中:
<a-button type="primary" @click="edit">
<a-modal
title="电子书表单"
v-model:visible="modalVisible"
:confirm-loading="modalLoading"+ @ok="handleModalOk"
>
<p>test</p>
</a-modal>
-------- 表单 ---------
const modalVisible = ref(false);
const modalLoading = ref(false);
const handleModalOk = () => {
modalLoading.value = true;
setTimeout(() => {
modalVisible.value = false;
modalLoading.value = false;
}, 2000);
};
/**
* 编辑
*/
const edit = () => {+ modalVisible.value = true;
};
handleTableChange,
edit,
modalVisible,
modalLoading,
handleModalOk
②编辑框显示电子书表单
<a-button type="primary" @click="edit(record)">
<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-input v-model:value="ebook.category1Id" />
</a-form-item>
<a-form-item label="分类二">
<a-input v-model:value="ebook.category2Id" />
</a-form-item>
<a-form-item label="描述">
<a-input v-model:value="ebook.desc" type="textarea" />
</a-form-item>
</a-form>
const ebook = ref({});
const edit = (record: any) => {
ebook.value = record
ebook,
7.完成电子书编辑功能
⭐保存类,更新类用Post,查询用get
RequestBody:json方式提交
①增加后端保存接口(ebook保存接口)
②点击保存时,调用保存接口
③保存成功刷新列表
在EbookController中:
com.jiawa.wiki.req.EbookQueryReq;
com.jiawa.wiki.req.EbookSaveReq;
com.jiawa.wiki.resp.EbookQueryResp;
org.springframework.web.bind.annotation.*;
public CommonResp list(EbookQueryReq req) {
CommonResp<PageResp<EbookQueryResp>> resp = new CommonResp<>();
PageResp<EbookQueryResp> list = ebookService.list(req);
@PostMapping("/save")
public CommonResp save(@RequestBody EbookSaveReq req) {
CommonResp resp = new CommonResp<>();
ebookService.save(req);
return resp;
在EbookReq中:
public class EbookQueryReq extends PageReq {
新建类--EbookSaveReq:
package com.jiawa.wiki.req;
public class EbookSaveReq {
private Long id;
private String name;
private Long category1Id;
private Long category2Id;
private String description;
private String cover;
private Integer docCount;
private Integer viewCount;
private Integer voteCount;
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 getCategory1Id() {
return category1Id;
}
public void setCategory1Id(Long category1Id) {
this.category1Id = category1Id;
}
public Long getCategory2Id() {
return category2Id;
}
public void setCategory2Id(Long category2Id) {
this.category2Id = category2Id;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getCover() {
return cover;
}
public void setCover(String cover) {
this.cover = cover;
}
public Integer getDocCount() {
return docCount;
}
public void setDocCount(Integer docCount) {
this.docCount = docCount;
}
public Integer getViewCount() {
return viewCount;
}
public void setViewCount(Integer viewCount) {
this.viewCount = viewCount;
}
public Integer getVoteCount() {
return voteCount;
}
public void setVoteCount(Integer voteCount) {
this.voteCount = voteCount;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", id=").append(id);
sb.append(", name=").append(name);
sb.append(", category1Id=").append(category1Id);
sb.append(", category2Id=").append(category2Id);
sb.append(", description=").append(description);
sb.append(", cover=").append(cover);
sb.append(", docCount=").append(docCount);
sb.append(", viewCount=").append(viewCount);
sb.append(", voteCount=").append(voteCount);
sb.append("]");
return sb.toString();
}
}
在EbookResp中:
public class EbookQueryResp {
在EbookService中:
import com.jiawa.wiki.req.EbookQueryReq;
import com.jiawa.wiki.req.EbookSaveReq;
import com.jiawa.wiki.resp.EbookQueryResp;
public PageResp<EbookQueryResp> list(EbookQueryReq req) {
List<EbookQueryResp> list = CopyUtil.copyList(ebookList, EbookQueryResp.class);
PageResp<EbookQueryResp> pageResp = new PageResp();
/**
* 保存
*/
public void save(EbookSaveReq req) {
Ebook ebook = CopyUtil.copy(req, Ebook.class);
if (ObjectUtils.isEmpty(req.getId())) {
// 新增
ebookMapper.insert(ebook);
} else {
// 更新
ebookMapper.updateByPrimaryKey(ebook);
}
}
在admin-ebook中:
axios.post("/ebook/save", ebook.value).then((response) => {
const data = response.data; // data = commonResp
if (data.success) {
modalVisible.value = false;
modalLoading.value = false;
// 重新加载列表
handleQuery({
page: pagination.value.current,
size: pagination.value.pageSize,
});
}
});
8.雪花算法与新增功能
⭐雪花算法:是一个工具类,用来生成数据库id
在EbookServic中:
import com.jiawa.wiki.util.SnowFlake;
@Resource
private SnowFlake snowFlake;
ebook.setId(snowFlake.nextId());
在util下新建雪花类SnowFlake:
package com.jiawa.wiki.util;
import org.springframework.stereotype.Component;
import java.text.ParseException;
/**
* Twitter的分布式自增ID雪花算法
**/
@Component
public class SnowFlake {
/**
* 起始的时间戳
*/
private final static long START_STMP = 1609459200000L; // 2021-01-01 00:00:00
/**
* 每一部分占用的位数
*/
private final static long SEQUENCE_BIT = 12; //序列号占用的位数
private final static long MACHINE_BIT = 5; //机器标识占用的位数
private final static long DATACENTER_BIT = 5;//数据中心占用的位数
/**
* 每一部分的最大值
*/
private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
/**
* 每一部分向左的位移
*/
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;
private long datacenterId = 1; //数据中心
private long machineId = 1; //机器标识
private long sequence = 0L; //序列号
private long lastStmp = -1L;//上一次时间戳
public SnowFlake() {
}
public SnowFlake(long datacenterId, long machineId) {
if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
}
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
}
this.datacenterId = datacenterId;
this.machineId = machineId;
}
/**
* 产生下一个ID
*
* @return
*/
public synchronized long nextId() {
long currStmp = getNewstmp();
if (currStmp < lastStmp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (currStmp == lastStmp) {
//相同毫秒内,序列号自增
sequence = (sequence + 1) & MAX_SEQUENCE;
//同一毫秒的序列数已经达到最大
if (sequence == 0L) {
currStmp = getNextMill();
}
} else {
//不同毫秒内,序列号置为0
sequence = 0L;
}
lastStmp = currStmp;
return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
| datacenterId << DATACENTER_LEFT //数据中心部分
| machineId << MACHINE_LEFT //机器标识部分
| sequence; //序列号部分
}
private long getNextMill() {
long mill = getNewstmp();
while (mill <= lastStmp) {
mill = getNewstmp();
}
return mill;
}
private long getNewstmp() {
return System.currentTimeMillis();
}
public static void main(String[] args) throws ParseException {
// 时间戳
// System.out.println(System.currentTimeMillis());
// System.out.println(new Date().getTime());
//
// String dateTime = "2021-01-01 08:00:00";
// SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
// System.out.println(sdf.parse(dateTime).getTime());
SnowFlake snowFlake = new SnowFlake(1, 1);
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
System.out.println(snowFlake.nextId());
System.out.println(System.currentTimeMillis() - start);
}
}
}
在admin-ebook中:
<p>
<a-button type="primary" @click="add()" size="large">
新增
</a-button>
</p>
/**
* 新增
*/
const add = () => {
modalVisible.value = true;
ebook.value = {};
};
add,
9.增加删除电子书功能
⭐电子书管理页面,点击某一行的删除按钮时,删除该行电子书
后端增加删除接口,前端点击删除按钮时调用后端删除接口,删除时需要有一个确认框
在EbookController中:
@DeleteMapping("/delete/{id}")
public CommonResp delete(@PathVariable Long id) {
CommonResp resp = new CommonResp<>();
ebookService.delete(id);
return resp;
}
在EbookService中:
public void delete(Long id) {
ebookMapper.deleteByPrimaryKey(id);
}
在admin-ebook中:
<a-popconfirm
title="删除后不可恢复,确认删除?"
ok-text="是"
cancel-text="否"
@confirm="handleDelete(record.id)"
>
<a-button type="danger">
删除
</a-button>
</a-popconfirm>
<a-input v-model:value="ebook.description" type="textarea" />
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,
});
}
});
};
handleModalOk,
handleDelete
10.集成Validation做参数校验
对电子书查询和保存做参数校验
⭐集成spring-boot-starter-validation
⭐对保存接口和查询接口增加参数校验
⭐校验不通过时,前端弹出错误提示
①集成Validation,为列表接口增加参数校验
在pom文件中增加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
在controller下新增ControllerExceptionHandler类
package com.jiawa.wiki.controller;
import com.jiawa.wiki.resp.CommonResp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 统一异常处理、数据预处理等
*/
@ControllerAdvice
public class ControllerExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(ControllerExceptionHandler.class);
/**
* 校验异常统一处理
* @param e
* @return
*/
@ExceptionHandler(value = BindException.class)
@ResponseBody
public CommonResp validExceptionHandler(BindException e) {
CommonResp commonResp = new CommonResp();
LOG.warn("参数校验失败:{}", e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
commonResp.setSuccess(false);
commonResp.setMessage(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
return commonResp;
}
}
在EbookController中:
import javax.validation.Valid;
public CommonResp list(@Valid EbookQueryReq req) {
在PageReq中:
import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
@NotNull(message = "【页码】不能为空")
@NotNull(message = "【每页条数】不能为空")
@Max(value = 1000, message = "【每页条数】不能超过1000")
在admin-ebook中:
import { message } from 'ant-design-vue';
pageSize: 1001,
if (data.success) {
ebooks.value = data.content.list;
// 重置分页按钮
pagination.value.current = params.page;
pagination.value.total = data.content.total;
} else {
message.error(data.message);
}
②为保存接口增加参数校验
在EbookController中:
public CommonResp save(@Valid @RequestBody EbookSaveReq req) {
在EbookSaveReq中:
import javax.validation.constraints.NotNull;
@NotNull(message = "【名称】不能为空")
在admin-ebook中:
pageSize: 10,
modalLoading.value = false;
} else {
message.error(data.message);
11.电子书管理功能优化
①增加按名字查询电子书功能
<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>
const param = ref();
param.value = {};
size: params.size,
name: param.value.name
param,
handleQuery,
②编辑时复制对象,修改表单时,不会影响列表数据
在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));
}
}
}
在admin-ebook中:
import {Tool} from "@/util/tool";
ebook.value = Tool.copy(record);