Springboot+Vue实现简单的前端后分离数据交互
目录
在这样一体化的构造中,如果后端业务逻辑需要更改或者数据获取出现问题,前端就要跟后端协调沟通,或者业务功能更加复杂时,这样一体化的弊端就会愈发明显。耦合度高,开发麻烦,严重影响开发效率。
2.2,前后端分离结构开发
采用前后端分离的方式进行开发时,前端只需要独立去编写客户端代码,后端专心于编写服务端代码,然后提供数据接口即可。前端开发人员与后端人员通过约定好接口文档(URL,数据类型,参数)就可以分别进行独立开发,并且,前端还可以进行伪数据构造进行数据展示测试。不需要完全依赖于后端,最后集成一下就能实现相应的业务功能了,从而达到前后端的解耦,大大提高开发效率。
整体结构如图所示:
这样开发以后,前后端开发人员可以更专注于自己擅长的领域,实现职责分离。
- 前后端仅仅通过异步接口(AJAX/JSONP)来编程
- 前后端都各自有自己的开发流程,构建工具,测试集合
- 关注点分离,前后端变得相对独立并解耦合
前端 | 后端 |
接受,展示数据 | 提供数据 |
处理渲染逻辑 | 处理业务逻辑 |
MVVM架构 | MVC架构 |
专注于客户端代码构造 | 专注于服务器代码构造 |
三,简单实现前后端数据交互
1,准备的环境及工具
开发准备 | 前端 | 后端 |
环境 | node.js | jdk1.8,tomcat9,mysql8 |
技术集成 | vue,axios,element-plus | Springboot,MyBatis-plus |
开发工具 | Visual Studio Code | IntelliJ IDEA 2022.1,Navicat Premium ,(ApiPost6) |
2,开发步骤
2.1,后端部分
2.1.1,构造一个数据库,准备一张用于数据展示的数据表
create table test_user
(
id int(20) auto_increment comment '用户id'
primary key,
name varchar(30) null comment '用户姓名',
sex tinyint(1) null comment '性别(1为男,0为女)',
address varchar(45) null comment '用户地址',
createTime datetime null,
constraint id_index
unique (id) comment 'id为唯一索引'
);
构建存储过程快速插入100条数据,详情方法查看如何使用存储过程快速插入数据
2.1.2,在IDEA里创建一个SpringBoot项目,并导入相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- MP代码生成器依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.1</version>
</dependency>
<!-- 3.0版本的swagger依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.5</version>
</dependency>
2.1.3,配置yml文件里面的数据库配置
server:
port: 8090
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
password: 123456
username: root
url: jdbc:mysql://localhost:3306/db_user?&useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
mvc:
format:
date-time: yyyy-MM-dd HH:mm
mybatis-plus:
mapper-locations: classPath*:/mapper/*.xml
configuration:
map-underscore-to-camel-case: false # 禁止大写变小写时自动添加下划线
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
2.1.4,通过MyBatis-plus代码生成器,直接构建基础的项目结构(pojo,service,dao,serviceImpl,controller),通过MyBatis-Plus自动生成代码后,我们基本的一些代码全部省去了,包括业务需要的简单增删改查也全部轻松自动搞定。需要注意的是,自动生成的代码并不能完全适用于我们所有业务,如果业务需求有变,还是需要我们自己手动编写动态SQL,不要过于依赖框架哦~~~~这样后端基本的框架就搭建成功了
2.1.5, 考虑到在前后端数据对介绍会涉及到跨域问题,接口文档对接问题,因此需要简单编写一下跨域,Swagger的配置类。
package com.yy.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* @author young
* @date 2022/8/23 17:33
* @description: 跨域配置
*/
@Configuration
public class CorsConfig {
/**
* 最大有效时长
*/
private static final long MAX_TIMEOUT=24*60*60;
@Bean
public CorsConfiguration corsConfiguration(){
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setMaxAge(MAX_TIMEOUT);
//设置访问源请求头
corsConfiguration.addAllowedHeader("*");
//设置访问源地址
corsConfiguration.addAllowedOrigin("*");
//设置访问源请求方法
corsConfiguration.addAllowedMethod("*");
return corsConfiguration;
}
/**
* 设置跨域配置
* @return
*/
@Bean
public CorsFilter corsFilter(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**",corsConfiguration());
return new CorsFilter(source);
}
}
package com.yy.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
@Configuration
@EnableSwagger2//开启Swagger2
public class SwaggerConfig {
//添加分组
@Bean
public Docket docket1(){
return new Docket(DocumentationType.SWAGGER_2).groupName("黎治跃");
}
//配置Swagger的Docket的bean实例
@Bean
public Docket docket(Environment environment){
//设置要显示的Swagger环境
Profiles profiles = Profiles.of("dev", "test");
//获取项目环境
boolean b = environment.acceptsProfiles(profiles);
System.out.println(b);
return new Docket(DocumentationType.SWAGGER_2).groupName("YY")
.apiInfo(apiInfo())
.enable(b) //是否启动Swagger,false,则浏览器无法访问Swagger
.select()
//RequestHandlerSelectors,配置要扫描的接口方式
//basePackage指定要扫描的包
//any : 扫描全部
//none :不扫描
//withClassAnnotation : 扫描类上的注解
.apis(RequestHandlerSelectors.basePackage("com.yy.controller"))
//path :过滤路径
//.paths(PathSelectors.ant("/yy/**"))
.build();
}
//配置Swagger信息=apiinfo
private ApiInfo apiInfo(){
Contact contact = new Contact("YY", "https://www.4399.com", "2463252763@qq.com");
return new ApiInfo("YY的SwaggerAPI文档",
"黎治跃失恋了,2022/8/19",
"1.0v",
"urn:tos",
contact,
"Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList<>());
}
}
为了更好地清晰的让我们获取的信息与前端统一,也可以编写一个统一返回值的类,让返回结果以自定义的JSON格式展示出来,方便我们阅读。
定义一个方便获取常量的枚举类
/**
* @author young
* @date 2022/8/19 21:36
* @description: 响应结果枚举类
*/
@AllArgsConstructor
@Getter
public enum ResponseEnum {
/**响应成功**/
SUCCESS(200, "操作成功"),
/**操作失败*/
FAIL(201,"获取数据失败"),
/**错误请求**/
ERROR(400,"错误请求"),
/**页面未找到**/
NOT_FOUND(404,"页面未找到"),
/**系统异常**/
SYS_ERROR(-1,"系统异常"),
/*信息已存在*/
MSG_ERROR(409,"信息已存在");
/**响应码**/
private final Integer code;
/** 结果 **/
private final String resultMessage;
public static ResponseEnum getResultCode(Integer code){
for (ResponseEnum value : ResponseEnum.values()) {
if (code.equals(value.getCode())){
return value;
}
}
return ResponseEnum.ERROR;
}
/*
简单测试一下
*/
public static void main(String[] args) {
ResponseEnum resultCode = ResponseEnum.getResultCode(100);
System.out.println(resultCode);
}
}
定义统一返回值的类
package com.yy.utils;
import com.yy.enums.ResponseEnum;
import lombok.Data;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* @author young
* @date 2022/8/19 21:52
* @description: 统一返回结果的类
*/
@Data
public class R<T> implements Serializable {
private static final long serialVersionUID = 56665257248936049L;
/**响应码**/
private Integer code;
/**返回消息**/
private String message;
/**返回数据**/
private T data;
private R(){}
/**
* 操作成功ok方法
*/
public static <T> R<T> ok(T data) {
R<T> response = new R<>();
response.setCode(ResponseEnum.SUCCESS.getCode());
response.setMessage(ResponseEnum.SUCCESS.getResultMessage());
response.setData(data);
return response;
}
/**
* 编译失败方法
*/
public static <T> R<T> buildFailure(Integer errCode, String errMessage){
R<T> response = new R<>();
response.setCode(errCode);
response.setMessage(errMessage);
return response;
}
}
如果需要在后端对获取的响应数据用mybatis-plus进行分页呢。还需要配置一下mybatis-plus的配置类
/**
* @author young
* @date 2022/8/29 21:27
* @description: MyBatis-Plus分页配置
*/
@Configuration
@MapperScan("com.yy.dao")
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
// paginationInnerInterceptor.setOverflow(false);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
// paginationInnerInterceptor.setMaxLimit(500L);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
}
当然,这个并不是必须的,后面在前端通过element-plus也能对数据自动进行分页展示,个人认为更加方便,没有必要在后端进行分页。如果是为了更好的按条件查询或其他之用,也可以考虑 。
2.1.6,编写controller去提取前端页面需要的数据
/**
* @author young
* @date 2022/8/26 9:36
* @description:
*/
@Slf4j
@RestController
@RequestMapping("/mysql")
public class TestUserController {
private static final DateFormat dataFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Resource
private TestUserServiceImpl testUserService;
/* 前后端分离测试 */
/**
* 添加用户
*
* @param request
* @return
*/
@PostMapping("/addUserTo")
public R<Object> addUser(HttpServletRequest request) {
JSONObject object = new JSONObject();
String name = request.getParameter("name").trim();
String sex = request.getParameter("sex").trim();
String address = request.getParameter("address").trim();
String createTime = request.getParameter("createTime").trim();
if ("".equals(name)) {
object.put("code", 0);
object.put("msg", "用户不能为空");
return R.ok(object);
}
TestUser user = new TestUser();
Date date = new Date();
try {
date = dataFormat.parse(createTime);
} catch (Exception e) {
e.printStackTrace();
}
user.setName(name);
user.setSex(Boolean.valueOf(sex));
user.setAddress(address);
user.setCreateTime(date);
try {
boolean add = testUserService.save(user);
if (add) {
object.put("code", 1);
object.put("success", true);
object.put("msg", "添加成功");
object.put("type", "success");
return R.ok(object);
} else {
object.put("code", 0);
object.put("success", false);
object.put("msg", "添加失败");
object.put("type", "error");
return R.buildFailure(ResponseEnum.FAIL.getCode(), ResponseEnum.FAIL.getResultMessage());
}
} catch (DuplicateKeyException e) {
object.put("code", 2);
object.put("success", false);
object.put("msg", "用户已存在");
object.put("type", "error");
return R.buildFailure(ResponseEnum.MSG_ERROR.getCode(), ResponseEnum.MSG_ERROR.getResultMessage());
}
}
/**
* 前端获取所有数据
*
* @return
*/
@GetMapping("/getAllTo")
public R<List<TestUser>> allUser() {
return R.ok(testUserService.list());
}
/**
* 返回指定id的用户
*
* @param request
* @return
*/
@GetMapping("/getById")
public R<TestUser> getById(HttpServletRequest request) {
String id = request.getParameter("id");
TestUser user = testUserService.getById(id);
return R.ok(user);
}
/**
* 删除用户
*
* @param request
* @return
*/
@DeleteMapping("deleteUserTo")
public R<Boolean> deleteUserTo(HttpServletRequest request) {
String id = request.getParameter("id");
return R.ok(testUserService.removeById(id));
}
/**
* 更新用户信息
*
* @param request
* @return
*/
@PostMapping("/updateUserTo")
public R<Object> updateUserTo(HttpServletRequest request) {
JSONObject jsonObject = new JSONObject();
String id = request.getParameter("id").trim();
String name = request.getParameter("name").trim();
String sex = request.getParameter("sex").trim();
String address = request.getParameter("address").trim();
String createTime = request.getParameter("createTime").trim();
TestUser testUser = new TestUser();
Date date = new Date();
try {
date = dataFormat.parse(createTime);
} catch (Exception e) {
e.printStackTrace();
}
testUser.setId(Integer.parseInt(id));
testUser.setName(name);
testUser.setSex(Boolean.valueOf(sex));
testUser.setAddress(address);
testUser.setCreateTime(date);
boolean res = testUserService.updateById(testUser);
if (res) {
jsonObject.put("code", 1);
jsonObject.put("msg", "修改成功!");
R.ok(jsonObject).toString();
return R.ok(jsonObject);
} else {
jsonObject.put("code", 0);
jsonObject.put("msg", "修改失败");
return R.buildFailure(ResponseEnum.FAIL.getCode(), ResponseEnum.FAIL.getResultMessage());
}
}
}
最后,通过接口测试工具(Swagger,ApiPost,Postman都可)对我们写的数据接口测试一下,数据返回值符合预期的话,那么后端代码就该一段落了!
2.2,前端部分
前端主要通Vue框架构建项目,主要是对客户端界面进行构造。由于笔者对于前端基础不怎么好,因此主要用Element-Plus进行界面构造,axios解决前后端交互。vue使用的是vue3,但是函数方法上仍旧采用的vue2的形式,主要实现过程如下:
2.2.1,在main.js上全局配置需要使用到的插件
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import * as ELIcons from '@element-plus/icons-vue'
// 配置路由器
import router from './router'
import './assets/global.css'
createApp(App).use(router).use(ELIcons).use(ElementPlus,{size:'small'}).mount('#app')
2.2.2,封装aixos请求后端的请求方式request.js
import axios from 'axios'
import {BASE_URL} from '../util/name'
axios.defaults.timeout = 5000 // 超时时间设置
axios.defaults.baseURL = BASE_URL
// Content-Type 响应头
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'
// response 拦截器
// 可以在接口响应后统一处理结果
axios.interceptors.response.use(
response => {
let res = response.data;
// 如果是返回的文件
if (response.config.responseType === 'blob') {
return res
}
// 兼容服务端返回的字符串数据
if (typeof res === 'string') {
res = res ? JSON.parse(res) : res
}
return res;
}
,
error => {
console.log('err' + error) // for debug
return Promise.reject(error)
}
)
/**
* 封装get方法
* @param url
* @param data
* @returns {Promise}
*/
export function get (url, params = {}, responseType = 'json') {
return new Promise((resolve, reject) => {
axios.get(url, {
params: params,
responseType
})
.then(response => {
resolve(response.data)
})
.catch(err => {
reject(err)
})
})
}
/**
* 封装post请求
* @param url
* @param data
* @returns {Promise}
*/
export function post (url, data = {}) {
return new Promise((resolve, reject) => {
axios.post(url, data)
.then(response => {
resolve(response.data)
}, err => {
reject(err)
})
})
}
/**
* 封装delete请求
* @param url
* @param data
* @returns {Promise}
*/
export function deletes (url, data = {}) {
return new Promise((resolve, reject) => {
axios.delete(url, data)
.then(response => {
resolve(response.data)
}, err => {
reject(err)
})
})
}
2.2.3,封装前后端api对应的请求接口index.js
import {get,post,deletes} from '../util/request'
const HttpManager={
// 前端用到的函数 后端对应的接口
//返回所有用户
getAllUser: () => get(`mysql/getAllTo`),
// 返回指定ID的用户
getUserOfId: (id) => get(`mysql/getById?id=${id}`),
// 添加用户
addUser: (params) => post(`mysql/addUserTo`, params),
// 更新用户信息
updateUserMsg: (params) => post(`mysql/updateUserTo`, params),
// 删除用户
deleteUser: (id) => deletes(`mysql/deleteUserTo?id=${id}`),
//模糊查询
likeSelect:(params)=>get(`mysql/likeSelect`,params)
}
export {HttpManager}
2.2.4,提取公共的methos,放在mixins中,这样就能将共用的功能以对象的方式传入 mixins选项中,当组件使用 mixins对象时所有mixins对象的选项都将被混入该组件本身的选项中来,这样就可以提高代码的重用性,使代码保持干净和易于维护。
<!--mixins/index.js-->
export const mixin = {
methods: {
// 获取要删除列表的id
handleDelete (id) {
this.idx = id
this.delVisible = true
},
// 获取批量要删除的列表
handleSelectionChange (val) {
this.multipleSelection = val
},
// 批量删除
delAll () {
console.log("执行该方法")
for (let item of this.multipleSelection) {
this.handleDelete(item.id)
this.deleteRow(item.id)
}
this.multipleSelection = []
},
getTime (val) {
let time = String(val).match(/[0-9-]+(?=\s)/)
return Array.isArray(time) ? time[0] : time
},
changeSex (value) {
if (value === false) {
return '女'
} else if (value === true) {
return '男'
}
},
toggleSex (value) {
if (value === '女') {
return false
} else if (value === '男') {
return true
}
},
// 更新图片
handleAvatarSuccess (res, file) {
if (res.code === 1) {
this.imageUrl = URL.createObjectURL(file.raw)
this.getData()
this.$notify({
title: '上传成功',
type: 'success'
})
} else {
this.$notify({
title: '上传失败',
type: 'error'
})
}
},
beforeAvatarUpload (file) {
const isJPG = (file.type === 'image/jpeg') || (file.type === 'image/png')
const isLt2M = file.size / 1024 / 1024 < 2
if (!isJPG) {
this.$message.error('上传头像图片只能是 JPG 格式!')
}
if (!isLt2M) {
this.$message.error('上传头像图片大小不能超过 2MB!')
}
return isJPG && isLt2M
},
}
}
2.2.5,抽离出侧边栏组件AsiderBody,头部组件HeaderBody,直接从官网提取,并进行简单的样式更改
<!--HeaderBody.vue -->
<template>
<!-- 收缩 -->
<div style="font-size: 14px;line-height: 60px; display: flex">
<div style="flex: 1;font-size: 20px">
<el-icon style="cursor: pointer" @click="collapse"><Fold /></el-icon>
</div>
<div class="toolbar" style="width: 70px">
<el-dropdown style="cursor: pointer">
<el-icon style="margin-right: 8px; margin-top: 24px"
><ArrowDown
/></el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>个人信息</el-dropdown-item>
<el-dropdown-item>退出</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span>张三</span>
</div>
</div>
</template>
<script>
import {Fold,ArrowDown} from '@element-plus/icons-vue'
export default {
name:'HeaderBody',
components:{
Fold,
ArrowDown
},
props:['collapse'],
}
</script>
<style>
</style>
<!--AsiderBody.vue -->
<template>
<el-scrollbar style="background-color: #304156">
<el-menu
:default-openeds="['1', '3']"
background-color="#304156"
text-color="rgb(91, 119, 211)"
active-text-color="#ffd04b"
overflow="hidden"
:collapse="isCollapse"
:collapse-transition="false"
router
>
<!-- 添加头部log -->
<div style="height: 60px; line-height: 60px; text-align: center">
<img
src="../assets/yp.png"
style="width: 20px; posotion: relative; top: 5px; margin-right: 5px"
/>
<b style="color: white" v-show="logoTestShow">后台管理系统</b>
</div>
<el-sub-menu index="/">
<template #title>
<el-icon><Menu /></el-icon><span>导航 主页</span>
</template>
<el-menu-item-group>
<template #title>分组 1</template>
<el-menu-item index="/user">用户管理 1</el-menu-item>
<el-menu-item index="/about">关于-登录</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="分组 2">
<el-menu-item index="1-3">选项 3</el-menu-item>
</el-menu-item-group>
<el-sub-menu index="1-4">
<template #title>选项4</template>
<el-menu-item index="1-4-1">选项 4-1</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-sub-menu insdex="2">
<template #title>
<el-icon><Message /></el-icon>
<span>导航 Two</span>
</template>
<el-menu-item-group>
<template #title>分组 1</template>
<el-menu-item index="2-1">选项 1</el-menu-item>
<el-menu-item index="2-2">选项 2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="Group 2">
<el-menu-item index="2-3">选项 3</el-menu-item>
</el-menu-item-group>
<el-sub-menu index="2-4">
<template #title>选项 4</template>
<el-menu-item index="2-4-1">选项 4-1</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-sub-menu index="3">
<template #title>
<el-icon><House /></el-icon>
<span>导航 Three</span>
</template>
<el-menu-item-group>
<template #title>分组 1</template>
<el-menu-item index="3-1">选项 1</el-menu-item>
<el-menu-item index="3-2">选项 2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="Group 2">
<el-menu-item index="3-3">选项 3</el-menu-item>
</el-menu-item-group>
<el-sub-menu index="3-4">
<template #title>选项 4</template>
<el-menu-item index="3-4-1">选项 4-1</el-menu-item>
</el-sub-menu>
</el-sub-menu>
</el-menu>
</el-scrollbar>
</template>
<script>
import { Menu, Message, House } from "@element-plus/icons-vue";
export default {
name: "AsiderBody",
props: {
isCollapse: Boolean,
logoTestShow: Boolean,
},
components: {
Menu,
Message,
House,
},
};
</script>
<style>
</style>
2.2.6,构建主要的信息展示界面,这样只有el-main里面的内容会随路由的改变而切换,而侧边栏AsiderBody,头部栏HeaderBody不会改动,从而实现组件复用。
<!--HomePage.vue-->
<template>
<el-container class="layout-container-demo" style="height: 100vh">
<el-aside :width="sideWidth + 'px'" background-color="rgb(238,241,246)">
<asider-body :isCollapse="isCollapse" :logoTestShow="logoTestShow" :collapse="collapse"/>
</el-aside>
<el-container>
<el-header style="border-bottom:1px solid #ccc">
<HeaderBody :collapse="isCollapse"/>
</el-header>
<el-main style="margin-left: 40px">
<router-view/>
</el-main>
</el-container>
</el-container>
</template>
<script>
import { mixin } from "../mixins/index";
import AsiderBody from "@/components/AsiderBody.vue"
import HeaderBody from "@/components/HeaderBody.vue"
export default {
name: "HomePage",
mixins: [mixin],
components: {
AsiderBody,
HeaderBody
},
data() {
return {
isCollapse: false,
sideWidth: 200,
logoTestShow: true,
}
},
methods:{
// 收缩侧边栏
collapse() {
this.isCollapse = !this.isCollapse;
if (this.isCollapse) {
(this.sideWidth = 64), (this.logoTestShow = false);
} else {
(this.sideWidth = 200), (this.logoTestShow = true);
}
},
}
}
</script>
<style scoped>
.layout-container-demo .el-header {
position: relative;
background-color: var(--el-color-primary-light-7);
color: var(--el-text-color-primary);
}
.layout-container-demo .el-aside {
color: var(--el-text-color-primary);
background: var(--el-color-primary-light-8);
}
.layout-container-demo .el-menu {
border-right: none;
}
.layout-container-demo .el-main {
padding: 0;
}
.layout-container-demo .toolbar {
display: inline-flex;
align-items: center;
justify-content: center;
height: 100%;
right: 20px;
}
</style>
2.2.7,用户信息展示界面构造,以及相关函数编写调用
<!--UserMsg.vue-->
<template>
<div>
<div style="margin-top: 20px; text-size: 20px">
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item
><a href="/user">用户信息</a></el-breadcrumb-item
></el-breadcrumb
>
</div>
<div style="margin-top: 10px">
<el-input
v-model="select_word"
placeholder="筛选相关用户"
style="width: 200px"
>
<template #suffix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button
round
color="#db6b2b"
class="ml-5"
plain
@click="reSet"
>
重置</el-button
>
</div>
<div style="padding: 10px 0; display: inline-flex">
<el-button type="primary" @click="delAll"
>批量删除
<el-icon><DeleteFilled /></el-icon>
</el-button>
<el-button
type="primary"
class="mr-5"
@click="centerDialogVisible = true"
>
添加用户<el-icon><Plus /></el-icon>
</el-button>
<el-upload
ref="upload"
action="#"
accept=".xlsx, .xls"
:auto-upload="false"
:on-change="uploadFile"
:show-file-list="false"
>
<el-button type="primary" class="ml-5">
导入数据<el-icon><Download /></el-icon>
</el-button>
</el-upload>
<el-button
type="primary"
style="margin-left: 10px"
@click="exportE()"
>
导出数据<el-icon><Upload /></el-icon>
</el-button>
</div>
<el-table
:data="datas"
style="text-align: center"
border
stripe
@selection-change="handleSelectionChange"
>
<el-table-column
type="selection"
width="40"
align="center"
></el-table-column>
<el-table-column prop="id" label="ID" width="80px" align="center" />
<el-table-column
prop="createTime"
label="日期"
width="240px"
align="center"
>
<!-- <template v-slot="scope">
<div>{{ getTime(scope.row.createTime) }}</div>
</template> -->
</el-table-column>
<el-table-column
prop="name"
label="姓名"
width="200px"
align="center"
/>
<el-table-column prop="sex" label="性别" width="200px" align="center">
<!-- eslint-disable-next-line -->
<template v-slot="scope">
<div>{{ changeSex(scope.row.sex) }}</div>
</template>
</el-table-column>
<el-table-column
prop="address"
label="地址"
width="200px"
align="center"
/>
<el-table-column label="操作" align="center">
<!-- 解决scope没被使用的问题 -->
<!-- eslint-disable-next-line -->
<template v-slot="scope">
<el-button
type="success"
class="mr-5"
@click="handleEdit(scope.row)"
>编辑<el-icon><Edit /></el-icon
></el-button>
<el-button
type="danger"
class="mr-5"
@click="handleDelete(scope.row.id)"
>删除<el-icon><DeleteFilled /></el-icon
></el-button>
<el-button type="warning" class="mr-5"
>权限<el-icon><ElementPlus /></el-icon
></el-button>
</template>
</el-table-column>
</el-table>
<div style="padding: 10px 0">
<el-pagination
@current-change="handleCurrentChange"
background
layout="total, prev, pager, next"
:current-page="currentPage"
:page-size="pageSize"
:total="tableData.length"
>
</el-pagination>
</div>
<!--添加新用户-->
<el-dialog
title="添加用户"
v-model="centerDialogVisible"
width="400px"
center
>
<el-form
:model="registerForm"
status-icon
:rules="rules"
ref="registerForm"
label-width="70px"
class="demo-ruleForm"
>
<el-form-item label="用户名" prop="name" size="small">
<el-input
v-model="registerForm.name"
placeholder="用户名"
></el-input>
</el-form-item>
<el-form-item label="性别" size="small">
<el-radio-group v-model="registerForm.sex">
<el-radio :label="false">女</el-radio>
<el-radio :label="true">男</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="创建时间" prop="createTime" size="small">
<el-date-picker
type="date"
placeholder="选择日期"
v-model="registerForm.createTime"
style="width: 100%"
></el-date-picker>
</el-form-item>
<el-form-item label="地址" prop="address" size="small">
<el-input
v-model="registerForm.address"
placeholder="地址"
></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button size="small" @click="centerDialogVisible = false"
>取 消</el-button
>
<el-button type="primary" size="small" @click="addPeople"
>确 定</el-button
>
</span>
</template>
</el-dialog>
<!-- 编辑弹出框 -->
<el-dialog title="编辑" v-model="editVisible" width="400px">
<el-form ref="form" :model="form" label-width="60px">
<el-form-item label="用户名" size="small">
<el-input v-model="form.name" :disabled="true"></el-input>
</el-form-item>
<el-form-item label="性别" size="small">
<el-radio-group v-model="form.sex">
<el-radio :label="false">女</el-radio>
<el-radio :label="true">男</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="创建日期" prop="createTime" size="small">
<el-date-picker
type="date"
placeholder="选择日期"
v-model="form.createTime"
style="width: 100%"
></el-date-picker>
</el-form-item>
<el-form-item label="地址" size="small">
<el-input v-model="form.address"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button size="small" @click="editVisible = false"
>取 消</el-button
>
<el-button type="primary" size="small" @click="saveEdit"
>确 定</el-button
>
</span>
</template>
<!-- 删除提示框 -->
</el-dialog>
<DelDialog
:delVisible="delVisible"
@deleteRow="deleteRow"
@cancelRow="delVisible = $event"
></DelDialog>
</div>
</template>
<script>
import { HttpManager } from "../api/index";
import { getDateTime } from "../util/DataUtil";
import DelDialog from "@/components/DelDialog.vue";
import { mixin } from "../mixins/index";
import {
Plus,
Upload,
Download,
Search,
Edit,
DeleteFilled,
ElementPlus,
} from "@element-plus/icons-vue";
export default {
name:'UserMsg',
mixins: [mixin],
components:{
Plus,
Upload,
Download,
Search,
Edit,
DeleteFilled,
ElementPlus,
DelDialog,
},
data() {
return {
tableData: [],
multipleSelection: [], // 记录要删除的用户信息
centerDialogVisible: false,
editVisible: false, // 显示编辑框
delVisible: false, // 显示删除框
select_word: "", // 记录输入框输入的内容
pageSize: 10, // 页数
currentPage: 1, // 当前页
idx: -1, // 记录当前点中的行
tempDate: [],
is_Search: false,
excelVisible: false,
registerForm: {
// 添加用户
name: "",
sex: "",
createTime: "",
address: "",
},
form: {
// 记录编辑的信息
id: "",
name: "",
sex: "",
createTime: "",
address: "",
//updateTime: ''
},
};
},
computed: {
// 计算当前表格中的数据
datas() {
return this.tableData.slice(
(this.currentPage - 1) * this.pageSize,
this.currentPage * this.pageSize
);
},
},
//模糊查询
watch: {
select_word() {
if (this.select_word === "") {
this.tableData = this.tempDate;
} else {
this.tableData = [];
for (let item of this.tempDate) {
if (item.name.includes(this.select_word) )
{
this.tableData.push(item);
}
}
}
},
},
created() {
//获取分页数据信息
this.getData();
},
methods: {
// 获取用户信息
getData() {
this.tableData = [];
this.tempDate = [];
HttpManager.getAllUser().then((res) => {
this.tableData = res;
this.tempDate = res;
this.currentPage = 1;
console.log(this.tempDate)
});
},
//重置搜索信息
reSet() {
this.select_word = "";
this.getData();
},
// 添加用户
addPeople() {
let createTime = getDateTime(this.registerForm.createTime);
let params = new URLSearchParams();
params.append("name", this.registerForm.name);
params.append("sex", this.registerForm.sex);
params.append("createTime", createTime);
params.append("address", this.registerForm.address);
HttpManager.addUser(params)
.then((res) => {
if (res.code === 1) {
this.getData();
this.registerForm = {};
this.$notify({
title: "添加成功",
type: "success",
});
} else {
this.$notify({
title: "添加失败",
type: "error",
});
}
})
.catch((err) => {
console.error(err);
});
this.centerDialogVisible = false;
},
// 分页
// handleSizeChange(pageSize) {
// console.log(`每页 ${pageSize} 条`);
// this.pageSize = pageSize;
// // this.getData();
// },
handleCurrentChange(val) {
this.currentPage = val;
},
// 编辑
handleEdit(row) {
this.idx = row.id;
this.form = {
id: row.id,
name: row.name,
sex: row.sex,
createTime: row.createTime,
address: row.address,
};
this.editVisible = true;
},
// 保存编辑
saveEdit() {
let datetime = getDateTime(new Date(this.form.createTime));
let params = new URLSearchParams();
params.append("id", this.form.id);
params.append("name", this.form.name);
params.append("sex", this.form.sex);
params.append("createTime", datetime);
params.append("address", this.form.address);
HttpManager.updateUserMsg(params)
.then((res) => {
if (res.code === 1) {
this.getData();
this.$notify({
title: "修改成功",
type: "success",
});
} else {
this.$notify({
title: "修改失败",
type: "error",
});
}
})
.catch((err) => {
console.error(err);
});
this.editVisible = false;
},
// 确定删除
deleteRow() {
HttpManager.deleteUser(this.idx)
.then((res) => {
if (res) {
this.getData();
this.$notify({
title: "删除成功",
type: "success",
});
} else {
this.$notify({
title: "删除失败",
type: "error",
});
}
})
.catch((error) => {
console.error(error);
});
this.delVisible = false;
},
},
};
</script>
<style>
/* 分页 */
.demo-pagination-block + .demo-pagination-block {
margin-top: 10px;
}
.demo-pagination-block .demonstration {
margin-bottom: 16px;
}
</style>
2.2.8,简单配置一下router,这样运行项目后,localhst:8080访问进入到“/home”请求对应的组件界面(随便写一个即可)上,点击侧边栏上的用户管理就能跳转到对应的信息展示界面。
import { createRouter, createWebHistory } from 'vue-router'
import HomePage from '@/view/HomePage'
const routes = [
{
path: '/',
name: 'HomePage',
component: HomePage,
redirect: '/home',
children: [
{
path: 'home',
name: 'HomeTest',
component: () => import('../components/HomeTest.vue')
},
{
path: 'user',
name: 'UserMsg',
component: () => import('../view/UserMsg.vue')
},
{
path: 'about',
name: 'About',
component: () => import('../components/AboutTest.vue')
},
]
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
2.2.9,在vue.config.js上通过chainWebpack配置后端代理地址,这样由于后端已经配置了跨域,前端就可以通过对后端的请求访问到后端对应的接口了。
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
chainWebpack: config => {
config.plugin('define').tap(definitions => {
Object.assign(definitions[0]['process.env'], {
NODE_HOST: '"http://localhost:8090"',
});
return definitions;
});
}
})
2.3,项目运行及实现结果
前端通过 npm install 安装项目所需的依赖,然后npm run serve运行项目即可得到初始的HomePage界面。因为前端学的并不扎实,页面配色布局可能一言难尽……
展开导航主页,点击用户展示界面,得到相应界面,此时并没有数据展示,只有初始的一些界面 ,因为后端服务没有开启,数据获取不到。
运行后端项目,重新刷新一下前端界面,数据通过element-plus中的el-pagination分页插件已经实现了分页效果。
并且该能实现一些基本增删改查操作,后端数据库的信息也会相应进行更改!
至此,简单的数据交互就实现了。
四,简单总结
这个测试项目还有一些小BUG正在完善中,还有一些实现的功能并没有完全放在博客中,这篇文章仅限于展示一些数据,实现前后端数据之间的交互。后期会继续完善成为一个简易的后台管理系统,供学习练习之用,项目会陆续上传到GitHub/Gitee上……有问题的地方希望大家指正交流,共同进步。
补充:
由于项目中涉及一些mysql以及mongodb的数据库,但是考虑比较简单,只是作为数据模拟使用的,因此没有放在项目中,前端也比较较简单没有放在gitee中,大家自行构建。