知识库系统开发记录(一)
一、创建springboot项目
二、Hello World
新建controller包和testController
testController
package com.li.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class testController {
@RequestMapping("/hello")
public String hello(){
return "hello world";
}
}
三、使用HTTP Client测试接口
新建http目录和test.http
test.http
GET http://localhost:8080/hello
Accept: application/json
四、设置application.yml
server:
port: 8080
五、配置IDEA集成热部署
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
设置
开启动态自动编译
六、完善后端架构
数据库
本地数据库
新建数据库“wiki”
阿里云数据库(后期购买)
SQL
# 电子书表
drop table if exists `ebook`;
create table `ebook` (
`id` bigint not null comment 'id',
`name` varchar(50) comment '名称',
`category1_id` bigint comment '分类1',
`category2_id` bigint comment '分类2',
`description` varchar(200) comment '描述',
`cover` varchar(200) comment '封面',
`doc_count` int not null default 0 comment '文档数',
`view_count` int not null default 0 comment '阅读数',
`vote_count` int not null default 0 comment '点赞数',
primary key (`id`)
) engine=innodb default charset=utf8mb4 comment='电子书';
insert into `ebook` (id, name, description) values (1, 'Spring Boot 入门教程', '零基础入门 Java 开发,企业级应用开发最佳首选框架');
insert into `ebook` (id, name, description) values (2, 'Vue 入门教程', '零基础入门 Vue 开发,企业级应用开发最佳首选框架');
insert into `ebook` (id, name, description) values (3, 'Python 入门教程', '零基础入门 Python 开发,企业级应用开发最佳首选框架');
insert into `ebook` (id, name, description) values (4, 'Mysql 入门教程', '零基础入门 Mysql 开发,企业级应用开发最佳首选框架');
insert into `ebook` (id, name, description) values (5, 'Oracle 入门教程', '零基础入门 Oracle 开发,企业级应用开发最佳首选框架');
drop table if exists `test`;
create table `test` (
`id` bigint not null comment 'id',
`name` varchar(50) comment '名称',
`password` varchar(50) comment '密码',
primary key (`id`)
) engine=innodb default charset=utf8mb4 comment='测试';
insert into `test` (id, name, password) values (1, '测试', 'password');
# 分类
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);
-- 文档表
drop table if exists `doc`;
create table `doc` (
`id` bigint not null comment 'id',
`ebook_id` bigint not null default 0 comment '电子书id',
`parent` bigint not null default 0 comment '父id',
`name` varchar(50) not null comment '名称',
`sort` int comment '顺序',
`view_count` int default 0 comment '阅读数',
`vote_count` int default 0 comment '点赞数',
primary key (`id`)
) engine=innodb default charset=utf8mb4 comment='文档';
insert into `doc` (id, ebook_id, parent, name, sort, view_count, vote_count) values (1, 1, 0, '文档1', 1, 0, 0);
insert into `doc` (id, ebook_id, parent, name, sort, view_count, vote_count) values (2, 1, 1, '文档1.1', 1, 0, 0);
insert into `doc` (id, ebook_id, parent, name, sort, view_count, vote_count) values (3, 1, 0, '文档2', 2, 0, 0);
insert into `doc` (id, ebook_id, parent, name, sort, view_count, vote_count) values (4, 1, 3, '文档2.1', 1, 0, 0);
insert into `doc` (id, ebook_id, parent, name, sort, view_count, vote_count) values (5, 1, 3, '文档2.2', 2, 0, 0);
insert into `doc` (id, ebook_id, parent, name, sort, view_count, vote_count) values (6, 1, 5, '文档2.2.1', 1, 0, 0);
-- 文档内容
drop table if exists `content`;
create table `content` (
`id` bigint not null comment '文档id',
`content` mediumtext not null comment '内容',
primary key (`id`)
) engine=innodb default charset=utf8mb4 comment='文档内容';
-- 用户表
drop table if exists `user`;
create table `user` (
`id` bigint not null comment 'ID',
`login_name` varchar(50) not null comment '登陆名',
`name` varchar(50) comment '昵称',
`password` char(32) not null comment '密码',
primary key (`id`),
unique key `login_name_unique` (`login_name`)
) engine=innodb default charset=utf8mb4 comment='用户';
insert into `user` (id, `login_name`, `name`, `password`) values (1, 'test', '测试', 'e70e2222a9d67c4f2eae107533359aa4');
-- 电子书快照表
drop table if exists `ebook_snapshot`;
create table `ebook_snapshot` (
`id` bigint auto_increment not null comment 'id',
`ebook_id` bigint not null default 0 comment '电子书id',
`date` date not null comment '快照日期',
`view_count` int not null default 0 comment '阅读数',
`vote_count` int not null default 0 comment '点赞数',
`view_increase` int not null default 0 comment '阅读增长',
`vote_increase` int not null default 0 comment '点赞增长',
primary key (`id`),
unique key `ebook_id_date_unique` (`ebook_id`, `date`)
) engine=innodb default charset=utf8mb4 comment='电子书快照表';
drop table if exists `demo`;
create table `demo` (
`id` bigint not null comment 'id',
`name` varchar(50) comment '名称',
primary key (`id`)
) engine=innodb default charset=utf8mb4 comment='测试';
insert into `demo` (id, name) values (1, '测试');
集成Mybatis
pom.xml
<!-- 引入mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 引入mybatis依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.1</version>
</dependency>
数据库连接配置(application.yml)
server:
port: 8080
# 数据库连接配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3307/wiki?serverTimezone=UTC
username: root
password: 1234
测试mybatis
新建实体类(entity) —— Test
package com.li.entity;
public class Test {
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public System getPassword() {
return password;
}
public void setPassword(System password) {
this.password = password;
}
private Integer id;
private String name;
private System password;
}
新建mapper类TestMapper
package com.li.mapper;
import com.li.entity.Test;
import java.util.List;
public interface TestMapper {
public List<Test> list();
}
在resource目录下新建mapper文件夹
新建TestMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.li.mapper.TestMapper" >
<select id="list" resultType="com.li.entity.Test">
select `id`, `name`, `password` from `test`
</select>
</mapper>
修改启动类(Wiki1107NightApplication)
增加“扫描注解”
@MapperScan("com.li.mapper")
@SpringBootApplication
@MapperScan("com.li.mapper")
public class Wiki1107NightApplication {
public static void main(String[] args) {
SpringApplication.run(Wiki1107NightApplication.class, args);
}
}
配置mybatis(application.yml)
## mybatis配置
mybatis:
mapper-locations: classpath:mapper/*.xml # 定位mapper文件的位置,当xml文件和mapper接口路径一致时可以不用配置
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 标准日志输出
map-underscore-to-camel-case: true # 开启下划线转驼峰
type-aliases-package: com.li.entity # 实体类起别名
新建service包 - TestService
@Service
public class TestService {
@Resource
private TestMapper testMapper;
public List<Test> list() {
return testMapper.list();
}
}
TestController
@RestController
public class testController {
@Value("${test.hello:TEST}")
private String testHello;
@Resource
private TestService testService;
@RequestMapping("/hello")
public String hello(){
return "hello world100";
}
@GetMapping("/test/list")
public List<Test> list(){
return testService.list();
}
}
测试
访问http://localhost:8080/test/list
Mybatis Generator
pom.xml
<!-- mybatis generator 自动生成代码插件 -->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.0</version>
<configuration>
<configurationFile>src/main/resources/generator/generator-config.xml</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
</dependencies>
</plugin>
resource目录下新建generator文件夹
新建generator-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="Mysql" targetRuntime="MyBatis3" defaultModelType="flat">
<!-- 自动检查关键字,为关键字增加反引号 -->
<property name="autoDelimitKeywords" value="true"/>
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<!--覆盖生成XML文件-->
<plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" />
<!-- 生成的实体类添加toString()方法 -->
<plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>
<!-- 不生成注释 -->
<commentGenerator>
<property name="suppressAllComments" value="true"/>
</commentGenerator>
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3307/wiki?serverTimezone=UTC"
userId="root"
password="1234">
</jdbcConnection>
<!-- domain类的位置 -->
<javaModelGenerator targetProject="src\main\java"
targetPackage="com.li.entity"/>
<!-- mapper xml的位置 -->
<sqlMapGenerator targetProject="src\main\resources"
targetPackage="mapper"/>
<!-- mapper类的位置 -->
<javaClientGenerator targetProject="src\main\java"
targetPackage="com.li.mapper"
type="XMLMAPPER"/>
<!--<table tableName="demo" domainObjectName="Demo"/>-->
<!--<table tableName="ebook"/>-->
<!--<table tableName="category"/>-->
<!--<table tableName="doc"/>-->
<!--<table tableName="content"/>-->
<!--<table tableName="user"/>-->
<table tableName="demo"/>
</context>
</generatorConfiguration>
新建运行配置
mybatis-generator:generate -e
运行mybatis-generator:generate -e
demo表
- 自动生成这四个文件。
测试
新建DemoController和DemoService
DemoController
@RestController
@RequestMapping("/demo")
public class DemoController {
@Resource
private DemoService demoService;
@GetMapping("/list")
public List<Demo> list(){
return demoService.list();
}
}
DemoService
@Service
public class DemoService {
@Resource
private DemoMapper demoMapper;
public List<Demo> list() {
return demoMapper.selectByExample(null);
}
}
访问http://localhost:8080/demo/list
完成电子书列表查询接口
电子书表结构设计
# 电子书表
drop table if exists `ebook`;
create table `ebook` (
`id` bigint not null comment 'id',
`name` varchar(50) comment '名称',
`category1_id` bigint comment '分类1',
`category2_id` bigint comment '分类2',
`description` varchar(200) comment '描述',
`cover` varchar(200) comment '封面',
`doc_count` int not null default 0 comment '文档数',
`view_count` int not null default 0 comment '阅读数',
`vote_count` int not null default 0 comment '点赞数',
primary key (`id`)
) engine=innodb default charset=utf8mb4 comment='电子书';
insert into `ebook` (id, name, description) values (1, 'Spring Boot 入门教程', '零基础入门 Java 开发,企业级应用开发最佳首选框架');
insert into `ebook` (id, name, description) values (2, 'Vue 入门教程', '零基础入门 Vue 开发,企业级应用开发最佳首选框架');
insert into `ebook` (id, name, description) values (3, 'Python 入门教程', '零基础入门 Python 开发,企业级应用开发最佳首选框架');
insert into `ebook` (id, name, description) values (4, 'Mysql 入门教程', '零基础入门 Mysql 开发,企业级应用开发最佳首选框架');
insert into `ebook` (id, name, description) values (5, 'Oracle 入门教程', '零基础入门 Oracle 开发,企业级应用开发最佳首选框架');
使用代码生成器快速开发列表接口(电子书表)
修改generator-config.xml
- 注释原有demo代码。
<table tableName="ebook"/>
<!-- <table tableName="demo"/>-->
测试
新建EbookController和EbookService
EbookController
@RestController
@RequestMapping("/ebook")
public class EbookController {
@Resource
private EbookService ebookService;
@GetMapping("/list")
public List<Ebook> list(){
return ebookService.list();
}
}
EbookService
@Service
public class EbookService {
@Resource
private EbookMapper ebookMapper;
public List<Ebook> list() {
return ebookMapper.selectByExample(null);
}
}
访问http://localhost:8080/ebook/list
构建通用返回类CommonResp
新建response目录和通用返回类CommonResp
package com.li.response;
public class CommonResp<T> {
/**
* 业务上的成功或失败
*/
private boolean success = true;
/**
* 返回信息
*/
private String message;
/**
* 返回泛型数据,自定义类型
*/
private T content;
public boolean getSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("ResponseDto{");
sb.append("success=").append(success);
sb.append(", message='").append(message).append('\'');
sb.append(", content=").append(content);
sb.append('}');
return sb.toString();
}
}
改造EbookController
@RestController
@RequestMapping("/ebook")
public class EbookController {
@Resource
private EbookService ebookService;
@GetMapping("/list")
public CommonResp list(){
CommonResp<List<Ebook>> response = new CommonResp<>();
List<Ebook> list = ebookService.list();
response.setContent(list);
return response;
}
}
测试
- 根据返回值来做处理
- 如果返回的是true,那就拿content里的内容;
- 如果返回的是false,那就弹出”message“。
七、封装请求参数和返回参数
根据名称模糊查询电子书
EbookController
@RestController
@RequestMapping("/ebook")
public class EbookController {
@Resource
private EbookService ebookService;
@GetMapping("/list")
public CommonResp list(String name){
CommonResp<List<Ebook>> response = new CommonResp<>();
List<Ebook> list = ebookService.list(name);
response.setContent(list);
return response;
}
}
EbookService
@Service
public class EbookService {
@Resource
private EbookMapper ebookMapper;
public List<Ebook> list(String name) {
EbookExample ebookExample = new EbookExample();
// Criteria:相当于Where条件
EbookExample.Criteria criteria = ebookExample.createCriteria();
criteria.andNameLike("%"+name+"%");
return ebookMapper.selectByExample(ebookExample);
}
}
测试
- GET http://localhost:8080/ebook/list?name=Spring
封装请求和返回参数
- 当然,这样每一个请求参数都加上去的话,岂不很麻烦。
- 所以我们要将所有参数封装成一个类。
新建request包(请求)
八、制作CopyUtil工具类封装BeanUtils
CopyUtil工具类
package com.li.utils;
import org.springframework.beans.BeanUtils;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
public class CopyUtil {
/**
* 单体复制
*/
public static <T> T copy(Object source, Class<T> clazz) {
if (source == null) {
return null;
}
T obj = null;
try {
obj = clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
return null;
}
BeanUtils.copyProperties(source, obj);
return obj;
}
/**
* 列表复制
*/
public static <T> List<T> copyList(List source, Class<T> clazz) {
List<T> target = new ArrayList<>();
if (!CollectionUtils.isEmpty(source)){
for (Object c: source) {
T obj = copy(c, clazz);
target.add(obj);
}
}
return target;
}
}
九、Vue3 + Vue CLI项目搭建
创建vue项目
集成Ant Design Vue
- npm i --save ant-design-vue
测试案例Ant Design Vue
main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
// 全局完整注册ant-design-vue
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';
import './assets/main.css'
const app = createApp(App)
app.use(router)
// 全局完整注册ant-design-vue
app.use(Antd)
app.mount('#app')
HelloWorld.vue(danger按钮)
<a-button danger :size="size">Danger</a-button>
首页布局开发
加入Ant Design Vue布局
App.vue
<template>
<a-layout id="components-layout-demo-top-side-2">
<a-layout-header class="header">
<div class="logo" />
<a-menu
theme="dark"
mode="horizontal"
v-model:selectedKeys="selectedKeys1"
:style="{ lineHeight: '64px' }"
>
<a-menu-item key="1">
nav 1
</a-menu-item>
<a-menu-item key="2">
nav 2
</a-menu-item>
<a-menu-item key="3">
nav 3
</a-menu-item>
</a-menu>
</a-layout-header>
<a-layout>
<a-layout-sider width="200" style="background: #fff">
<a-menu
mode="inline"
v-model:selectedKeys="selectedKeys2"
v-model:openKeys="openKeys"
:style="{ height: '100%', borderRight: 0 }"
>
<a-sub-menu key="sub1">
<template #title>
<span><user-outlined />subnav 1</span>
</template>
<a-menu-item key="1">option1</a-menu-item>
<a-menu-item key="2">option2</a-menu-item>
<a-menu-item key="3">option3</a-menu-item>
<a-menu-item key="4">option4</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub2">
<template #title>
<span><laptop-outlined />subnav 2</span>
</template>
<a-menu-item key="5">option5</a-menu-item>
<a-menu-item key="6">option6</a-menu-item>
<a-menu-item key="7">option7</a-menu-item>
<a-menu-item key="8">option8</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub3">
<template #title>
<span><notification-outlined />subnav 3</span>
</template>
<a-menu-item key="9">option9</a-menu-item>
<a-menu-item key="10">option10</a-menu-item>
<a-menu-item key="11">option11</a-menu-item>
<a-menu-item key="12">option12</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
Content
</a-layout-content>
</a-layout>
<a-layout-footer style="text-align: center">
wiki
</a-layout-footer>
</a-layout>
</template>
<style>
#components-layout-demo-top-side-2 .logo {
width: 120px;
height: 31px;
background: rgba(255, 255, 255, 0.2);
margin: 16px 28px 16px 0;
float: left;
}
</style>
首页路由开发
App.vue
<template>
<a-layout id="components-layout-demo-top-side-2">
<a-layout-header class="header">
<div class="logo" />
<a-menu
theme="dark"
mode="horizontal"
v-model:selectedKeys="selectedKeys1"
:style="{ lineHeight: '64px' }"
>
<a-menu-item key="1">
nav 1
</a-menu-item>
<a-menu-item key="2">
nav 2
</a-menu-item>
<a-menu-item key="3">
nav 3
</a-menu-item>
</a-menu>
</a-layout-header>
<a-layout>
<router-view/>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
Content
</a-layout-content>
</a-layout>
<a-layout-footer style="text-align: center">
wiki
</a-layout-footer>
</a-layout>
</template>
<style>
#components-layout-demo-top-side-2 .logo {
width: 120px;
height: 31px;
background: rgba(255, 255, 255, 0.2);
margin: 16px 28px 16px 0;
float: left;
}
</style>
views目录 - Home.vue
Home.vue
<template>
<a-layout>
<a-layout-sider width="200" style="background: #fff">
<a-menu
mode="inline"
v-model:selectedKeys="selectedKeys2"
v-model:openKeys="openKeys"
:style="{ height: '100%', borderRight: 0 }"
>
<a-sub-menu key="sub1">
<template #title>
<span><user-outlined />subnav 11111</span>
</template>
<a-menu-item key="1">option1</a-menu-item>
<a-menu-item key="2">option2</a-menu-item>
<a-menu-item key="3">option3</a-menu-item>
<a-menu-item key="4">option4</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub2">
<template #title>
<span><laptop-outlined />subnav 2</span>
</template>
<a-menu-item key="5">option5</a-menu-item>
<a-menu-item key="6">option6</a-menu-item>
<a-menu-item key="7">option7</a-menu-item>
<a-menu-item key="8">option8</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub3">
<template #title>
<span><notification-outlined />subnav 3</span>
</template>
<a-menu-item key="9">option9</a-menu-item>
<a-menu-item key="10">option10</a-menu-item>
<a-menu-item key="11">option11</a-menu-item>
<a-menu-item key="12">option12</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
Content
</a-layout-content>
</a-layout>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'Home',
// components: {
// HelloWorld,
// },
});
</script>
制作Vue自定义组件
制作the-header组件(components - the-header.vue)
<template>
<a-layout-header class="header">
<div class="logo" />
<a-menu
theme="dark"
mode="horizontal"
v-model:selectedKeys="selectedKeys1"
:style="{ lineHeight: '64px' }"
>
<a-menu-item key="1">
nav 1111
</a-menu-item>
<a-menu-item key="2">
nav 2
</a-menu-item>
<a-menu-item key="3">
nav 3
</a-menu-item>
</a-menu>
</a-layout-header>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'the-header'
});
</script>
App.vue
<template>
<a-layout id="components-layout-demo-top-side-2">
<the-header></the-header>
<router-view/>
<a-layout-footer style="text-align: center">
wiki
</a-layout-footer>
</a-layout>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import TheHeader from '@/components/the-header.vue';
export default defineComponent({
name: 'app',
components: {
TheHeader,
},
});
</script>
<style>
#components-layout-demo-top-side-2 .logo {
width: 120px;
height: 31px;
background: rgba(255, 255, 255, 0.2);
margin: 16px 28px 16px 0;
float: left;
}
</style>
制作the-footer组件(components - the-footer.vue)
<template>
<a-layout-footer style="text-align: center">
wiki
</a-layout-footer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'the-footer'
});
</script>
App.vue
<template>
<a-layout id="components-layout-demo-top-side-2">
<the-header></the-header>
<router-view/>
<the-footer></the-footer>
</a-layout>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import TheHeader from '@/components/the-header.vue';
import TheFooter from '@/components/the-footer.vue';
export default defineComponent({
name: 'app',
components: {
TheHeader,
TheFooter,
},
});
</script>
<style>
#components-layout-demo-top-side-2 .logo {
width: 120px;
height: 31px;
background: rgba(255, 255, 255, 0.2);
margin: 16px 28px 16px 0;
float: left;
}
</style>
十、前后端交互整合
集成HTTP库axios
集成HTTP库axios
npm install axios@0.21.0 --save
Home.vue
<script lang="ts">
import { defineComponent } from 'vue';
import axios from 'axios';
export default defineComponent({
name: 'Home',
// setup()vue3新增的初始化方法
setup(){
console.log("hello");
axios.get("http://localhost:8080/ebook/list?name=Spring").then((
response =>{
console.log(response);
}
))
}
});
</script>
前后端分离常见的跨域报错
- 跨域可以这样理解,来自一个IP端口的页面(vue项目),要访问另一个IP端口的资源(springboot请求接口),会产生跨域访问。
完成电子书列表接口前后端交互
在后端项目中新建config包和CorsConfig配置类
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedHeaders(CorsConfiguration.ALL)
.allowedMethods(CorsConfiguration.ALL)
.allowCredentials(true)
.maxAge(3600); // 1小时内不需要再预检(发OPTIONS请求)
}
}
测试
电子书列表界面展示 - Vue3数据绑定显示列表数据
- Vue核心功能:数据双向绑定
使用Vue3 ref实现数据绑定
Home.vue
<script lang="ts">
import { defineComponent,onMounted,ref } from 'vue';
import axios from 'axios';
export default defineComponent({
name: 'Home',
// setup()vue3新增的初始化方法
setup(){
console.log("setup");
// 响应式数据ref
const ebooks = ref();
onMounted(() => {
console.log("onMounted");
axios.get("http://localhost:8080/ebook/list?name=Spring").then((response) =>{
const data = response.data;
ebooks.value = data.content;
console.log(response);
});
})
return{
ebooks
}
}
});
</script>
将content替换为{{ebooks}}测试效果
<!-- content-->
<pre>
{{ebooks}}
</pre>
使用Vue3 reactive实现数据绑定
Home.vue
<script lang="ts">
import { defineComponent,onMounted,ref,reactive,toRef } from 'vue';
import axios from 'axios';
export default defineComponent({
name: 'Home',
// setup()vue3新增的初始化方法
setup(){
console.log("setup");
// 响应式数据ref
const ebooks = ref();
//reactive
// reactive里面一般放一个对象
const ebooks1 = reactive({books:[]});
onMounted(() => {
console.log("onMounted");
axios.get("http://localhost:8080/ebook/list?name=Spring").then((response) =>{
const data = response.data;
ebooks.value = data.content;
ebooks1.books = data.content;
console.log(response);
});
});
return{
ebooks,
ebooks2:toRef(ebooks1,"books")
}
}
});
</script>
渲染{{ebooks2}} —— 测试效果
<!-- content-->
<pre>
{{ebooks}}
{{ebooks2}}
</pre>
电子书列表界面展示
Ant Design Vue现成的组件
Home.vue
<template>
<a-layout>
<a-layout-sider width="200" style="background: #fff">
<a-menu
mode="inline"
:style="{ height: '100%', borderRight: 0 }"
>
<a-sub-menu key="sub1">
<template #title>
<span><user-outlined />subnav 11111</span>
</template>
<a-menu-item key="1">option1</a-menu-item>
<a-menu-item key="2">option2</a-menu-item>
<a-menu-item key="3">option3</a-menu-item>
<a-menu-item key="4">option4</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub2">
<template #title>
<span><laptop-outlined />subnav 2</span>
</template>
<a-menu-item key="5">option5</a-menu-item>
<a-menu-item key="6">option6</a-menu-item>
<a-menu-item key="7">option7</a-menu-item>
<a-menu-item key="8">option8</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub3">
<template #title>
<span><notification-outlined />subnav 3</span>
</template>
<a-menu-item key="9">option9</a-menu-item>
<a-menu-item key="10">option10</a-menu-item>
<a-menu-item key="11">option11</a-menu-item>
<a-menu-item key="12">option12</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<a-list item-layout="vertical" size="large" :pagination="pagination" :data-source="listData">
<template #footer>
<div><b>ant design vue</b> footer part</div>
</template>
<template #renderItem="{ item }">
<a-list-item key="item.title">
<template #actions>
<span v-for="{ type, text } in actions" :key="type">
<component v-bind:is="type" style="margin-right: 8px" />
{{ text }}
</span>
</template>
<template #extra>
<img
width="272"
alt="logo"
src="https://gw.alipayobjects.com/zos/rmsportal/mqaQswcyDLcXyDKnZfES.png"
/>
</template>
<a-list-item-meta :description="item.description">
<template #title>
<a :href="item.href">{{ item.title }}</a>
</template>
<template #avatar><a-avatar :src="item.avatar"/></template>
</a-list-item-meta>
{{ item.content }}
</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';
const listData: any = [];
for (let i = 0; i < 23; i++) {
listData.push({
href: 'https://www.antdv.com/',
title: `ant design vue part ${i}`,
avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
description:
'Ant Design, a design language for background applications, is refined by Ant UED Team.',
content:
'We supply a series of design principles, practical patterns and high quality design resources (Sketch and Axure), to help people create their product prototypes beautifully and efficiently.',
});
}
export default defineComponent({
name: 'Home',
setup() {
console.log("setup");
const ebooks = ref();
const ebooks1 = reactive({books: []});
onMounted(() => {
console.log("onMounted");
axios.get("http://127.0.0.1:8880/ebook/list?name=Spring").then((response) => {
const data = response.data;
ebooks.value = data.content;
ebooks1.books = data.content;
console.log(response);
});
});
return {
ebooks,
ebooks2: toRef(ebooks1, "books"),
listData,
pagination: {
onChange: (page: any) => {
console.log(page);
},
pageSize: 3,
},
actions: [
{ type: 'StarOutlined', text: '156' },
{ type: 'LikeOutlined', text: '156' },
{ type: 'MessageOutlined', text: '2' },
],
}
}
});
</script>
引入ant design图标库
npm install @ant-design/icons-vue --save
修改main.ts
import { createApp } from 'vue'
import App from './App.vue'
// @ts-ignore
import router from './router'
// import store from './store'
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';
import * as Icons from '@ant-design/icons-vue';
const app = createApp(App);
app.use(router).use(Antd).mount('#app');
//全局使用图标
const icons: any = Icons;
for (const i in icons){
app.component(i,icons[i]);
}
运行测试
将列表数据按组件样式显示到界面上
电子书布局调整 - Home.vue
<template>
<a-layout>
<a-layout-sider width="200" style="background: #fff">
<a-menu
mode="inline"
:style="{ height: '100%', borderRight: 0 }"
>
<a-sub-menu key="sub1">
<template #title>
<span><user-outlined />subnav 11111</span>
</template>
<a-menu-item key="1">option1</a-menu-item>
<a-menu-item key="2">option2</a-menu-item>
<a-menu-item key="3">option3</a-menu-item>
<a-menu-item key="4">option4</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub2">
<template #title>
<span><laptop-outlined />subnav 2</span>
</template>
<a-menu-item key="5">option5</a-menu-item>
<a-menu-item key="6">option6</a-menu-item>
<a-menu-item key="7">option7</a-menu-item>
<a-menu-item key="8">option8</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub3">
<template #title>
<span><notification-outlined />subnav 3</span>
</template>
<a-menu-item key="9">option9</a-menu-item>
<a-menu-item key="10">option10</a-menu-item>
<a-menu-item key="11">option11</a-menu-item>
<a-menu-item key="12">option12</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<a-list 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 v-for="{ type, text } in actions" :key="type">
<component v-bind:is="type" style="margin-right: 8px" />
{{ text }}
</span>
</template>
<a-list-item-meta :description="item.description">
<template #title>
<a :href="item.href">{{ item.name }}</a>
</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';
const listData: any = [];
for (let i = 0; i < 23; i++) {
listData.push({
href: 'https://www.antdv.com/',
title: `ant design vue part ${i}`,
avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
description:
'Ant Design, a design language for background applications, is refined by Ant UED Team.',
content:
'We supply a series of design principles, practical patterns and high quality design resources (Sketch and Axure), to help people create their product prototypes beautifully and efficiently.',
});
}
export default defineComponent({
name: 'Home',
setup() {
console.log("setup");
const ebooks = ref();
const ebooks1 = reactive({books: []});
onMounted(() => {
console.log("onMounted");
axios.get("http://127.0.0.1:8080/ebook/list?name=Spring").then((response) => {
const data = response.data;
ebooks.value = data.content;
ebooks1.books = data.content;
console.log(response);
});
});
return {
ebooks,
ebooks2: toRef(ebooks1, "books"),
listData,
pagination: {
onChange: (page: any) => {
console.log(page);
},
pageSize: 3,
},
actions: [
{ type: 'StarOutlined', text: '156' },
{ type: 'LikeOutlined', text: '156' },
{ type: 'MessageOutlined', text: '2' },
],
}
}
});
</script>
列表查询接口支持动态SQL
Home.vue
<script lang="ts">
import { defineComponent, onMounted, ref, reactive, toRef } from 'vue';
import axios from 'axios';
const listData: any = [];
for (let i = 0; i < 23; i++) {
listData.push({
href: 'https://www.antdv.com/',
title: `ant design vue part ${i}`,
avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
description:
'Ant Design, a design language for background applications, is refined by Ant UED Team.',
content:
'We supply a series of design principles, practical patterns and high quality design resources (Sketch and Axure), to help people create their product prototypes beautifully and efficiently.',
});
}
export default defineComponent({
name: 'Home',
setup() {
console.log("setup");
const ebooks = ref();
const ebooks1 = reactive({books: []});
onMounted(() => {
console.log("onMounted");
axios.get("http://127.0.0.1:8080/ebook/list").then((response) => {
const data = response.data;
ebooks.value = data.content;
ebooks1.books = data.content;
console.log(response);
});
});
return {
ebooks,
ebooks2: toRef(ebooks1, "books"),
listData,
pagination: {
onChange: (page: any) => {
console.log(page);
},
pageSize: 3,
},
actions: [
{ type: 'StarOutlined', text: '156' },
{ type: 'LikeOutlined', text: '156' },
{ type: 'MessageOutlined', text: '2' },
],
}
}
});
</script>
<style scoped>
.ant-avatar {
width: 50px;
height: 50px;
line-height: 50px;
border-radius: 8%;
margin: 5px 0;
}
</style>
在后端项目中新增EbookResp类和EbookReq类
package com.li.response;
public class EbookResp {
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();
}
}
package com.li.request;
public class EbookReq {
private Long id;
private String name;
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;
}
@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("]");
return sb.toString();
}
}
修改EbookService和EbookController
@Service
public class EbookService {
@Resource
private EbookMapper ebookMapper;
public List<EbookResp> list(EbookReq req) {
EbookExample ebookExample = new EbookExample();
EbookExample.Criteria criteria = ebookExample.createCriteria();
if (!ObjectUtils.isEmpty(req.getName())) {
criteria.andNameLike("%" + req.getName() + "%");
}
List<Ebook> ebookList = ebookMapper.selectByExample(ebookExample);
// 列表复制
List<EbookResp> list = CopyUtil.copyList(ebookList, EbookResp.class);
return list;
}
}
@RestController
@RequestMapping("/demo")
public class DemoController {
@Resource
private DemoService demoService;
@GetMapping("/list")
public List<Demo> list(){
return demoService.list();
}
}
运行测试
使用Axios拦截器打印前端日志
配置axios拦截器打印请求参数和返回参数
main.ts
import axios from 'axios';
/**
* axios拦截器
*/
axios.interceptors.request.use(function (config) {
console.log('请求参数:', config);
return config;
}, error => {
return Promise.reject(error);
});
axios.interceptors.response.use(function (response) {
console.log('返回结果:', response);
return response;
}, error => {
console.log('返回错误:', error);
return Promise.reject(error);
});
SpringBoot过滤器的使用
配置过滤器,打印接口耗时
filter / LogFilter
package com.li.filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@Component
public class LogFilter implements Filter {
private static final Logger LOG = LoggerFactory.getLogger(LogFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 打印请求信息
HttpServletRequest request = (HttpServletRequest) servletRequest;
LOG.info("------------- LogFilter 开始 -------------");
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("远程地址: {}", request.getRemoteAddr());
long startTime = System.currentTimeMillis();
filterChain.doFilter(servletRequest, servletResponse);
LOG.info("------------- LogFilter 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
}
}
SpringBoot拦截器的使用
配置拦截器,打印接口耗时
Interceptor / LogInterceptor
package com.li.interceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印 /login
*/
@Component
public class LogInterceptor implements HandlerInterceptor {
private static final Logger LOG = LoggerFactory.getLogger(LogInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 打印请求信息
LOG.info("------------- LogInterceptor 开始 -------------");
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("远程地址: {}", request.getRemoteAddr());
long startTime = System.currentTimeMillis();
request.setAttribute("requestStartTime", startTime);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
long startTime = (Long) request.getAttribute("requestStartTime");
LOG.info("------------- LogInterceptor 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
}
}
拦截器还需要增加个配置类
SpringMvcConfig
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
@Resource
LogInterceptor logInterceptor;
public void addInterceptor(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor)
.addPathPatterns("/**").excludePathPatterns("/login");
}
}
SpringBoot AOP的使用
配置AOP,打印接口耗时、请求参数、返回参数
新建aspect包 - 新增AOP类 - LogAspect
@Aspect
@Component
public class LogAspect {
private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class);
/** 定义一个切点 */
@Pointcut("execution(public * com.li.*.controller..*Controller.*(..))")
public void controllerPointcut() {}
// @Resource
// private SnowFlake snowFlake;
@Before("controllerPointcut()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// // 增加日志流水号
// MDC.put("LOG_ID", String.valueOf(snowFlake.nextId()));
// 开始打印请求日志
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Signature signature = joinPoint.getSignature();
String name = signature.getName();
// 打印请求信息
LOG.info("------------- 开始 -------------");
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name);
LOG.info("远程地址: {}", request.getRemoteAddr());
// RequestContext.setRemoteAddr(getRemoteIp(request));
// 打印请求参数
Object[] args = joinPoint.getArgs();
// LOG.info("请求参数: {}", JSONObject.toJSONString(args));
Object[] arguments = new Object[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof ServletRequest
|| args[i] instanceof ServletResponse
|| args[i] instanceof MultipartFile) {
continue;
}
arguments[i] = args[i];
}
// 排除字段,敏感字段或太长的字段不显示
String[] excludeProperties = {"password", "file"};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludefilter));
}
@Around("controllerPointcut()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
// 排除字段,敏感字段或太长的字段不显示
String[] excludeProperties = {"password", "file"};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludefilter));
LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
return result;
}
/**
* 使用nginx做反向代理,需要用该方法才能取到真实的远程IP
* @param request
* @return
*/
public String getRemoteIp(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
添加maven依赖 - aop - fastjson
<!-- aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
现在有了AOP,就可以把拦截器和过滤器注释了
- 注释 LogFilter 和 LogInterceptor 和 SpringMvcConfig。
十一、 电子书管理功能开发
增加电子书管理页面
新增about.vue
<template>
<div class="about">
<h1>hello,这是关于页面</h1>
</div>
</template>
index.ts添加about相关代码
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/about.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/',
name: '/about',
component: About
},
// {
// path: '/about',
// name: 'about',
// // route level code-splitting
// // this generates a separate chunk (About.[hash].js) for this route
// // which is lazy-loaded when the route is visited.
// component: () => import('../views/AboutView.vue')
// }
]
})
export default router
测试关于页面
增加电子书页面 - admin-ebook.vue
<template>
<div class="about">
<h1>电子书管理页面</h1>
</div>
</template>
增加电子书菜单 - the-header.vue
<template>
<a-layout-header class="header">
<div class="logo" />
<a-menu
theme="dark"
mode="horizontal"
v-model:selectedKeys="selectedKeys1"
: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>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'the-header'
});
</script>
增加电子书路由 - main.ts
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/about.vue'
import AdminEbook from '../views/admin/admin-ebook.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
},
{
path: '/admin/ebook',
name: 'AdminEbook',
component: AdminEbook
},
// {
// path: '/about',
// name: 'about',
// // route level code-splitting
// // this generates a separate chunk (About.[hash].js) for this route
// // which is lazy-loaded when the route is visited.
// component: () => import('../views/AboutView.vue')
// }
]
})
export default router
电子书表格展示
新增.eslintrc.js
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/typescript/recommended'
],
parserOptions: {
ecmaVersion: 2020
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'vue/no-unused-components': 'off',
'@typescript-eslint/no-explicit-any': 0,
'vue/no-unused-vars': 0,
'@typescript-eslint/no-unused-vars': 0,
}
}
修改admin-ebook.vue
<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("http://127.0.0.1:8080/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>
使用PageHelper实现后端分页
集成PageHelper插件 - pom.xml
<!-- pagehelper-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.13</version>
</dependency>
EbookService
@Service
public class EbookService {
private static final Logger LOG = LoggerFactory.getLogger(EbookService.class);
@Resource
private EbookMapper ebookMapper;
public List<EbookResp> list(EbookReq req) {
EbookExample ebookExample = new EbookExample();
EbookExample.Criteria criteria = ebookExample.createCriteria();
if (!ObjectUtils.isEmpty(req.getName())) {
criteria.andNameLike("%" + req.getName() + "%");
}
PageHelper.startPage(1, 3);
List<Ebook> ebookList = ebookMapper.selectByExample(ebookExample);
PageInfo<Ebook> pageInfo = new PageInfo<>(ebookList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
// 列表复制
List<EbookResp> list = CopyUtil.copyList(ebookList, EbookResp.class);
return list;
}
}
修改电子书列表接口,支持分页(假分页数据)
https://coding.imooc.com/lesson/474.html#mid=41803
封装分页请求参数和返回参数
请求参数封装,PageReq
PageReq
public class PageReq {
private int page;
private int size;
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("PageReq{");
sb.append("page=").append(page);
sb.append(", size=").append(size);
sb.append('}');
return sb.toString();
}
}
EbookReq继承PageReq
public class EbookReq extends PageReq
返回结果封装,PageResp
PageResp
package com.li.response;
import java.util.List;
public class PageResp<T> {
private long total;
private List<T> list;
public long getTotal() {
return total;
}
public void setTotal(long total) {
this.total = total;
}
public List<T> getList() {
return list;
}
public void setList(List<T> list) {
this.list = list;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("PageResp{");
sb.append("total=").append(total);
sb.append(", list=").append(list);
sb.append('}');
return sb.toString();
}
}
修改EbookService
@Service
public class EbookService {
private static final Logger LOG = LoggerFactory.getLogger(EbookService.class);
@Resource
private EbookMapper ebookMapper;
public PageResp<EbookResp> list(EbookReq req) {
EbookExample ebookExample = new EbookExample();
EbookExample.Criteria criteria = ebookExample.createCriteria();
if (!ObjectUtils.isEmpty(req.getName())) {
criteria.andNameLike("%" + req.getName() + "%");
}
PageHelper.startPage(req.getPage(), req.getSize());
List<Ebook> ebookList = ebookMapper.selectByExample(ebookExample);
PageInfo<Ebook> pageInfo = new PageInfo<>(ebookList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
// 列表复制
List<EbookResp> list = CopyUtil.copyList(ebookList, EbookResp.class);
PageResp<EbookResp> pageResp = new PageResp();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
}
修改EbookController
@RestController
@RequestMapping("/ebook")
public class EbookController {
@Resource
private EbookService ebookService;
@GetMapping("/list")
public CommonResp list(EbookReq req) {
CommonResp<PageResp<EbookResp>> resp = new CommonResp<>();
PageResp<EbookResp> list = ebookService.list(req);
resp.setContent(list);
return resp;
}
}
前后端分页功能整合
前端修改列表查询分页参数 - 前端修改接收列表查询结果
修改admin-ebook.vue
<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: 4,
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("http://127.0.0.1:8080/ebook/list", {
params: {
page: params.page,
size: params.size
}
}).then((response) => {
loading.value = false;
const data = response.data;
ebooks.value = data.content.list;
// 重置分页按钮
pagination.value.current = params.page;
pagination.value.total = data.content.total;
});
};
/**
* 表格点击页码时触发
*/
const handleTableChange = (pagination: any) => {
console.log("看看自带的分页参数都有啥:" + pagination);
handleQuery({
page: pagination.current,
size: pagination.pageSize
});
};
onMounted(() => {
handleQuery({
page: 1,
size: pagination.value.pageSize,
});
});
return {
ebooks,
pagination,
columns,
loading,
handleTableChange
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
电子书管理页面和首页都需要改
修改home.vue
<template>
<a-layout>
<a-layout-sider width="200" style="background: #fff">
<a-menu
mode="inline"
:style="{ height: '100%', borderRight: 0 }"
>
<a-sub-menu key="sub1">
<template #title>
<span><user-outlined />subnav 11111</span>
</template>
<a-menu-item key="1">option1</a-menu-item>
<a-menu-item key="2">option2</a-menu-item>
<a-menu-item key="3">option3</a-menu-item>
<a-menu-item key="4">option4</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub2">
<template #title>
<span><laptop-outlined />subnav 2</span>
</template>
<a-menu-item key="5">option5</a-menu-item>
<a-menu-item key="6">option6</a-menu-item>
<a-menu-item key="7">option7</a-menu-item>
<a-menu-item key="8">option8</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub3">
<template #title>
<span><notification-outlined />subnav 3</span>
</template>
<a-menu-item key="9">option9</a-menu-item>
<a-menu-item key="10">option10</a-menu-item>
<a-menu-item key="11">option11</a-menu-item>
<a-menu-item key="12">option12</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<a-list 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 v-for="{ type, text } in actions" :key="type">
<component v-bind:is="type" style="margin-right: 8px" />
{{ text }}
</span>
</template>
<a-list-item-meta :description="item.description">
<template #title>
<a :href="item.href">{{ item.name }}</a>
</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';
export default defineComponent({
name: 'Home',
setup() {
const ebooks = ref();
const ebooks1 = reactive({books: []});
onMounted(() => {
axios.get("http://127.0.0.1:8080/ebook/list", {
params: {
page: 1,
size: 1000
}
}).then((response) => {
const data = response.data;
ebooks.value = data.content.list;
// ebooks1.books = data.content;
});
});
return {
ebooks,
// ebooks2: toRef(ebooks1, "books"),
// listData,
pagination: {
onChange: (page: any) => {
console.log(page);
},
pageSize: 3,
},
actions: [
{ type: 'StarOutlined', text: '156' },
{ type: 'LikeOutlined', text: '156' },
{ type: 'MessageOutlined', text: '2' },
],
}
}
});
</script>
<style scoped>
.ant-avatar {
width: 50px;
height: 50px;
line-height: 50px;
border-radius: 8%;
margin: 5px 0;
}
</style>
制作电子书表单
点击每一行编辑按钮,弹出编辑框
“表单”部分 - 修改admin-ebook.vue
<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" @click="edit">
编辑
</a-button>
<a-button type="danger">
删除
</a-button>
</a-space>
</template>
</a-table>
</a-layout-content>
</a-layout>
<a-modal
title="电子书表单"
v-model:visible="modalVisible"
:confirm-loading="modalLoading"
@ok="handleModalOk"
>
<p>test</p>
</a-modal>
</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: 4,
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("http://127.0.0.1:8080/ebook/list", {
params: {
page: params.page,
size: params.size
}
}).then((response) => {
loading.value = false;
const data = response.data;
ebooks.value = data.content.list;
// 重置分页按钮
pagination.value.current = params.page;
pagination.value.total = data.content.total;
});
};
/**
* 表格点击页码时触发
*/
const handleTableChange = (pagination: any) => {
console.log("看看自带的分页参数都有啥:" + pagination);
handleQuery({
page: pagination.current,
size: pagination.pageSize
});
};
// -------- 表单 ---------
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;
};
onMounted(() => {
handleQuery({
page: 1,
size: pagination.value.pageSize,
});
});
return {
ebooks,
pagination,
columns,
loading,
handleTableChange,
edit,
modalVisible,
modalLoading,
handleModalOk
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
编辑框显示电子书表单
"a-form"部分 - 修改admin-ebook.vue
<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" @click="edit(record)">
编辑
</a-button>
<a-button type="danger">
删除
</a-button>
</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-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>
</a-modal>
</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: 4,
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("http://127.0.0.1:8080/ebook/list", {
params: {
page: params.page,
size: params.size
}
}).then((response) => {
loading.value = false;
const data = response.data;
ebooks.value = data.content.list;
// 重置分页按钮
pagination.value.current = params.page;
pagination.value.total = data.content.total;
});
};
/**
* 表格点击页码时触发
*/
const handleTableChange = (pagination: any) => {
console.log("看看自带的分页参数都有啥:" + pagination);
handleQuery({
page: pagination.current,
size: pagination.pageSize
});
};
// -------- 表单 ---------
const ebook = ref({});
const modalVisible = ref(false);
const modalLoading = ref(false);
const handleModalOk = () => {
modalLoading.value = true;
setTimeout(() => {
modalVisible.value = false;
modalLoading.value = false;
}, 2000);
};
/**
* 编辑
*/
const edit = (record: any) => {
modalVisible.value = true;
ebook.value = record
};
onMounted(() => {
handleQuery({
page: 1,
size: pagination.value.pageSize,
});
});
return {
ebooks,
pagination,
columns,
loading,
handleTableChange,
edit,
ebook,
modalVisible,
modalLoading,
handleModalOk
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
电子书编辑功能
增加后端保存接口 - 点击保存时,调用保存接口
增加save接口 - 修改EbookController.java
@RestController
@RequestMapping("/ebook")
public class EbookController {
@Resource
private EbookService ebookService;
@GetMapping("/list")
public CommonResp list(EbookQueryReq req) {
CommonResp<PageResp<EbookQueryResp>> resp = new CommonResp<>();
PageResp<EbookQueryResp> list = ebookService.list(req);
resp.setContent(list);
return resp;
}
//增加save接口 - 修改EbookController.java
@PostMapping("/save")
public CommonResp save(@RequestBody EbookSaveReq req) {
CommonResp resp = new CommonResp<>();
ebookService.save(req);
return resp;
}
}
重命名EbookReq为EbookQueryReq
public class EbookQueryReq extends PageReq {
新增EbookSaveReq
package com.li.request;
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为EbookQueryResp
public class EbookQueryResp
新增save方法 - 修改EbookService.java
/**
* 保存
*/
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.vue
<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" @click="edit(record)">
编辑
</a-button>
<a-button type="danger">
删除
</a-button>
</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-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>
</a-modal>
</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: 4,
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("http://127.0.0.1:8080/ebook/list", {
params: {
page: params.page,
size: params.size
}
}).then((response) => {
loading.value = false;
const data = response.data;
ebooks.value = data.content.list;
// 重置分页按钮
pagination.value.current = params.page;
pagination.value.total = data.content.total;
});
};
/**
* 表格点击页码时触发
*/
const handleTableChange = (pagination: any) => {
console.log("看看自带的分页参数都有啥:" + pagination);
handleQuery({
page: pagination.current,
size: pagination.pageSize
});
};
// -------- 表单 ---------
const ebook = ref({});
const modalVisible = ref(false);
const modalLoading = ref(false);
const handleModalOk = () => {
modalLoading.value = true;
axios.post("http://127.0.0.1:8080/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,
});
}
});
};
/**
* 编辑
*/
const edit = (record: any) => {
modalVisible.value = true;
ebook.value = record
};
onMounted(() => {
handleQuery({
page: 1,
size: pagination.value.pageSize,
});
});
return {
ebooks,
pagination,
columns,
loading,
handleTableChange,
edit,
ebook,
modalVisible,
modalLoading,
handleModalOk
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
雪花算法与新增功能
雪花算法精度丢失问题
- 雪花算法ID是Long类型,前端是number类型,精度上不一样,Long类型超过一定长度后,前端接收到的值会不准确。
- 会导致新增的记录不能编辑、删除。
时间戳概念
雪花算法工具类
SnowFlake.java
package com.li.utils;
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);
}
}
}
完成新增功能
修改EbookService.java
@Service
public class EbookService {
private static final Logger LOG = LoggerFactory.getLogger(EbookService.class);
@Resource
private EbookMapper ebookMapper;
@Resource
private SnowFlake snowFlake;
public PageResp<EbookQueryResp> list(EbookQueryReq req) {
EbookExample ebookExample = new EbookExample();
EbookExample.Criteria criteria = ebookExample.createCriteria();
if (!ObjectUtils.isEmpty(req.getName())) {
criteria.andNameLike("%" + req.getName() + "%");
}
PageHelper.startPage(req.getPage(), req.getSize());
List<Ebook> ebookList = ebookMapper.selectByExample(ebookExample);
PageInfo<Ebook> pageInfo = new PageInfo<>(ebookList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
// 列表复制
List<EbookQueryResp> list = CopyUtil.copyList(ebookList, EbookQueryResp.class);
PageResp<EbookQueryResp> pageResp = new PageResp();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
/**
* 保存
*/
public void save(EbookSaveReq req) {
Ebook ebook = CopyUtil.copy(req, Ebook.class);
if (ObjectUtils.isEmpty(req.getId())) {
// 新增
ebook.setId(snowFlake.nextId());
ebook.setDocCount(0);
ebook.setViewCount(0);
ebook.setVoteCount(0);
ebookMapper.insert(ebook);
} else {
// 更新
ebookMapper.updateByPrimaryKey(ebook);
}
}
}
增加“新增”方法 - 修改admin-ebook.vue
<template>
<a-layout>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<p>
<a-button type="primary" @click="add()" size="large">
新增
</a-button>
</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:action="{ text, record }">
<a-space size="small">
<a-button type="primary" @click="edit(record)">
编辑
</a-button>
<a-button type="danger">
删除
</a-button>
</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-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>
</a-modal>
</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: 4,
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("http://127.0.0.1:8080/ebook/list", {
params: {
page: params.page,
size: params.size
}
}).then((response) => {
loading.value = false;
const data = response.data;
ebooks.value = data.content.list;
// 重置分页按钮
pagination.value.current = params.page;
pagination.value.total = data.content.total;
});
};
/**
* 表格点击页码时触发
*/
const handleTableChange = (pagination: any) => {
console.log("看看自带的分页参数都有啥:" + pagination);
handleQuery({
page: pagination.current,
size: pagination.pageSize
});
};
// -------- 表单 ---------
const ebook = ref({});
const modalVisible = ref(false);
const modalLoading = ref(false);
const handleModalOk = () => {
modalLoading.value = true;
axios.post("http://127.0.0.1:8080/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,
});
}
});
};
/**
* 编辑
*/
const edit = (record: any) => {
modalVisible.value = true;
ebook.value = record
};
/**
* 新增
*/
const add = () => {
modalVisible.value = true;
ebook.value = {};
};
onMounted(() => {
handleQuery({
page: 1,
size: pagination.value.pageSize,
});
});
return {
ebooks,
pagination,
columns,
loading,
handleTableChange,
edit,
add,
ebook,
modalVisible,
modalLoading,
handleModalOk
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
测试
增加删除电子书功能
- 电子书管理页面,点击某一行的删除按钮时,删除该行电子书。
- 后端增加删除接口。
- 前端点击删除按钮时调用后端删除接口。
- 删除时需要有一个确认框。
后端增加删除接口
EbookController.java
@DeleteMapping("/delete/{id}")
public CommonResp delete(@PathVariable Long id) {
CommonResp resp = new CommonResp<>();
ebookService.delete(id);
return resp;
}
EbookService.java
public void delete(Long id) {
ebookMapper.deleteByPrimaryKey(id);
}
前端点击删除按钮时调用后端删除接口 - 删除时需要有一个确认框
handleDelete - admin-ebook.vue
<template>
<a-layout>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<p>
<a-button type="primary" @click="add()" size="large">
新增
</a-button>
</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: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="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.description" type="textarea" />
</a-form-item>
</a-form>
</a-modal>
</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: 4,
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("http://127.0.0.1:8080/ebook/list", {
params: {
page: params.page,
size: params.size
}
}).then((response) => {
loading.value = false;
const data = response.data;
ebooks.value = data.content.list;
// 重置分页按钮
pagination.value.current = params.page;
pagination.value.total = data.content.total;
});
};
/**
* 表格点击页码时触发
*/
const handleTableChange = (pagination: any) => {
console.log("看看自带的分页参数都有啥:" + pagination);
handleQuery({
page: pagination.current,
size: pagination.pageSize
});
};
// -------- 表单 ---------
const ebook = ref({});
const modalVisible = ref(false);
const modalLoading = ref(false);
const handleModalOk = () => {
modalLoading.value = true;
axios.post("http://127.0.0.1:8080/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,
});
}
});
};
/**
* 编辑
*/
const edit = (record: any) => {
modalVisible.value = true;
ebook.value = record
};
/**
* 新增
*/
const add = () => {
modalVisible.value = true;
ebook.value = {};
};
const handleDelete = (id: number) => {
axios.delete("http://127.0.0.1:8080/ebook/delete/" + id).then((response) => {
const data = response.data; // data = commonResp
if (data.success) {
// 重新加载列表
handleQuery({
page: pagination.value.current,
size: pagination.value.pageSize,
});
}
});
};
onMounted(() => {
handleQuery({
page: 1,
size: pagination.value.pageSize,
});
});
return {
ebooks,
pagination,
columns,
loading,
handleTableChange,
edit,
add,
ebook,
modalVisible,
modalLoading,
handleModalOk,
handleDelete
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
测试
集成Validation做参数校验
- 对电子书查询和保存做参数校验。
- 集成spring-boot-starter-validation对保存接口和查询接口增加参数校验。
- 校验不通过时,前端弹出错误提示
集成spring-boot-starter-validation
pom.xml
<!-- validation-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
对保存接口和查询接口增加参数校验
新增ControllerExceptionHandler.java
/**
* 统一异常处理、数据预处理等
*/
@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;
}
}
@Valid - EbookController.java
public CommonResp list(@Valid EbookQueryReq req)
public CommonResp save(@Valid @RequestBody EbookSaveReq req)
@NotNull - @Max - PageReq.java
@NotNull(message = "【页码】不能为空")
private int page;
@NotNull(message = "【每页条数】不能为空")
@Max(value = 1000, message = "【每页条数】不能超过1000")
private int size;
@NotNull - EbookSaveReq
@NotNull(message = "【名称】不能为空")
校验不通过时,前端弹出错误提示
if (data.success) - admin-ebook.vue
<template>
<a-layout>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<p>
<a-button type="primary" @click="add()" size="large">
新增
</a-button>
</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: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="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.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';
export default defineComponent({
name: 'AdminEbook',
setup() {
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: '分类一',
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("http://127.0.0.1:8080/ebook/list", {
params: {
page: params.page,
size: params.size
}
}).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
});
};
// -------- 表单 ---------
const ebook = ref({});
const modalVisible = ref(false);
const modalLoading = ref(false);
const handleModalOk = () => {
modalLoading.value = true;
axios.post("http://127.0.0.1:8080/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 = record
};
/**
* 新增
*/
const add = () => {
modalVisible.value = true;
ebook.value = {};
};
const handleDelete = (id: number) => {
axios.delete("http://127.0.0.1:8080/ebook/delete/" + id).then((response) => {
const data = response.data; // data = commonResp
if (data.success) {
// 重新加载列表
handleQuery({
page: pagination.value.current,
size: pagination.value.pageSize,
});
}
});
};
onMounted(() => {
handleQuery({
page: 1,
size: pagination.value.pageSize,
});
});
return {
ebooks,
pagination,
columns,
loading,
handleTableChange,
edit,
add,
ebook,
modalVisible,
modalLoading,
handleModalOk,
handleDelete
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
电子书管理功能优化
增加名称查询
<a-form layout="inline" :model="param">
- 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: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="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.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';
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: '分类一',
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("http://127.0.0.1:8080/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
});
};
// -------- 表单 ---------
const ebook = ref({});
const modalVisible = ref(false);
const modalLoading = ref(false);
const handleModalOk = () => {
modalLoading.value = true;
axios.post("http://127.0.0.1:8080/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 = record
};
/**
* 新增
*/
const add = () => {
modalVisible.value = true;
ebook.value = {};
};
const handleDelete = (id: number) => {
axios.delete("http://127.0.0.1:8080/ebook/delete/" + id).then((response) => {
const data = response.data; // data = commonResp
if (data.success) {
// 重新加载列表
handleQuery({
page: pagination.value.current,
size: pagination.value.pageSize,
});
}
});
};
onMounted(() => {
handleQuery({
page: 1,
size: pagination.value.pageSize,
});
});
return {
param,
ebooks,
pagination,
columns,
loading,
handleTableChange,
handleQuery,
edit,
add,
ebook,
modalVisible,
modalLoading,
handleModalOk,
handleDelete
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
编辑时复制对象,修改表单时,不会影响列表数据
前端新增工具类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));
}
}
}
ebook.value = Tool.copy(record);
- admin-ebook.vue
import {Tool} from "@/util/tool";
/* ebook.value = record*/
ebook.value = Tool.copy(record);
Comments | NOTHING