文章目录
前言
环境搭建:
https://blog.csdn.net/m0_72900498/article/details/150282255?spm=1001.2014.3001.5501
增删改查的实现:
https://blog.csdn.net/m0_72900498/article/details/150351753?spm=1001.2014.3001.5502
一 、添加后端参数校验
1.参数校验的具体使用
我们前面只是实现了数据输入,但是并没有对数据进行校验 ,接下来我们就进行前端数据校验问题:
(1)引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
引入spring-boot-starter-validation 包后,可以看见包中依赖了hibernate-validator
(2)参数校验
以修改会员信息为例进行参数校验:
首先可以在后端Controller层参数的位置添加注解: @Validated
同理,我们的增加修改删除查询都需要进行参数校验,所以也在参数前面加上@Validated
注解。
然后这是只是添加校验的注解 ,再添加一下校验的具体规则注解:
@NotNull:不允许为空
@Size(min=1)最少一个
示例:
package com.study.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.study.model.Member;
import com.study.model.search.MemberSearchBean;
import com.study.service.MemberService;
import com.study.util.JsonResult;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("api/v1/members" )//接收前端的请求,路径与前端发送请求的路径一致
public class MemberController {
private MemberService memberService;
@Autowired//依赖注入:创建对象:
public void setMemberService(MemberService memberService) {
this.memberService = memberService;
}
//查询全部客户:
@GetMapping
public ResponseEntity<JsonResult<?>> findAll(
@RequestParam(defaultValue = "1") Integer pageNo,
@RequestParam(defaultValue = "15") Integer pageSize,
MemberSearchBean msb){
Page<Member> page = new Page<>(pageNo,pageSize);
Page<Member> all = memberService.findAll(page, msb);
return ResponseEntity.ok(JsonResult.success(all));
}
@DeleteMapping//删除操作
public ResponseEntity<JsonResult<?>> delete(
@RequestBody
@Validated
@NotNull @Size(min = 1) Integer[] ids){
int count = memberService.delete(List.of(ids));
if(count==0){
return ResponseEntity.ok(JsonResult.fail("删除会员失败"));
}
else {
return ResponseEntity.ok(JsonResult.success(count));
}
}
@PostMapping//新增操作
public ResponseEntity<JsonResult<?>> add(@RequestBody @Validated Member member){
boolean success = memberService.add(member);
if(success){
return ResponseEntity.ok(JsonResult.success("新增会员成功"));
}else return ResponseEntity.ok(JsonResult.fail("新增会员失败"));
}
@PutMapping//修改操作
public ResponseEntity<JsonResult<?>> edit(@RequestBody @Validated Member member){
boolean success = memberService.edit(member);
if(success){
return ResponseEntity.ok(JsonResult.success("修改会员成功"));
}else return ResponseEntity.ok(JsonResult.fail("修改会员失败"));
}
}
其次我们可以在实体类的属性上面添加校验
package com.study.model;
import com.baomidou.mybatisplus.annotation.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.*;
import java.time.LocalDate;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@TableName("member")//指明和哪个表进行绑定
public class Member {
@TableId(type = IdType.AUTO)//指定表的主键
private Integer id;
@TableField(condition = SqlCondition.LIKE)//mybatis-plus默认底层是=比较,模糊查询添加注解
//后端参数校验:具体的校验到什么程度取决于具体的业务需求
@NotBlank(message = "手机号不可为空")
@Pattern(regexp = "^\\d{11}$",message = "手机号必须是11位")
private String phone;
@NotBlank(message = "姓名不可为空")
@TableField(condition = SqlCondition.LIKE,whereStrategy = FieldStrategy.NOT_EMPTY)
private String name;
private String createTime;
private Integer age;
@TableField(condition = SqlCondition.LIKE,whereStrategy = FieldStrategy.NOT_EMPTY)
private String address;
private String remark;
private LocalDate birthday;
@NotBlank(message = "性别不可为空")
@Pattern(regexp = "^[男,女]$",message = "性别只能为男女")
@TableField(whereStrategy = FieldStrategy.NOT_EMPTY)//当该字段的值为空(null 或空字符串)时,自动忽略该字段,不将其拼接到 SQL 的 WHERE 条件中。
private String sex;
}
(3)对后端异常进行统一处理 (捕获 )
比如后端产生的异常,前端是处理不了的,我们可以对后端出现的这种异常做统一异常处理–只返回自定义的message的信息即可 :比如 手机号不可为空 。
方法:模板代码:
package com.study.config;
import com.study.util.JsonResult;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
import java.util.stream.Collectors;
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 当控制器中的方法出现参数校验异常时,即会调用此方法响应值。
*
* @param ex 参数校验异常
* @return 响应结果
*/
@ExceptionHandler(HandlerMethodValidationException.class)
public ResponseEntity<JsonResult<?>> handle(HandlerMethodValidationException ex) {
String msg = ex.getAllErrors().stream()
.map(MessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest().body(JsonResult.fail(msg));
}
/**
* 需要同时监听HandlerMethodValidationException和MethodArgumentNotValidException,二者都可能会出现
* 两个是完全不同的异常类型,继承体系结构也不一样,没办法合并为一个。只是恰巧都包含getAllErrors方法而已
*
* @param ex 参数校验异常
* @return 响应结果
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<JsonResult<?>> handle(MethodArgumentNotValidException ex) {
String msg = ex.getAllErrors().stream()
.map(MessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest().body(JsonResult.fail(msg));
}
}
这样在前端看见异常处理信息了:
2.后端校验大全
推荐博客:
https://blog.csdn.net/nuoya989/article/details/131493071
3.总结
二 、页面继续完善
1.操作按钮
之前的页面,现在对页面继续完善
我们这是健身房会员管理页面,里面有客户的信息,现在我们想在表格展示的部分添加相关操作:
先添加按钮,然后对按钮添加对应的事件即可。
添加操作这一列以及里面的按钮:
<el-table-column label="操作" width="160" fixed="right" align="center">
<template #default="scope">
<!-- editRow(scope.row)传参是scope.row,意思是获取当行的数据,点就选中了 -->
<el-button type="primary" size="small" @click.stop="editRow(scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click.stop="deleteRow(scope.row)">删除</el-button>
</template>
</el-table-column>
添加事件:
// 单行编辑
function editRow(row) {
// 将当前行数据填充到修改表单中
formInline3.value = {
...row
};
dialogVisible2.value = true;
}
//单行删除
const deleteRow = (row) => {
ElMessageBox.confirm(`是否确认删除会员 ${row.name}?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
removeByIds([row.id]); // 调用批量删除方法,传入当前行的id
}).catch(() => {
// 用户取消操作
});
}
展示效果 :
2.性别的图标显示
其实性别的图标显示和第一个操作按钮部分是一样的
页面:
<el-table-column prop="sex" label="性别" width="120" align="center">
<template #default="scope">
<!-- editRow(scope.row)传参是scope.row,意思是获取当行的数据,点就选中了 -->
<el-tag v-if="scope.row.sex=='男'" type="primary" size="large">{{scope.row.sex}}</el-tag>
<el-tag v-else type="danger" size="large">{{scope.row.sex}}</el-tag>
</template>
3.添加菜单栏,实现跳转
页面布局:
https://element-plus.org/zh-CN/component/container.html
布局:左边要放导航菜单
https://element-plus.org/zh-CN/component/menu.html
添加实现这两个部分:
<template>
<!-- 页面布局-->
<div class="common-layout h100">
<el-container class="h100">
<!--头部-->
<el-header>
<div class="logo"></div>
<h1 class="system-title">健身会馆客户预约管理系统</h1>
<!-- <div>-->
<!-- <a class="logout-btn" href="#" @click="logout">注销</a>-->
<!-- </div>-->
</el-header>
<el-container>
<el-aside width="200px" >
<!-- 导航菜单,加上路由是实现跳转 -->
<el-menu class="nav h100" router text-color="#fff" active-text-color="#ffd04b"
background-color="#545c64" default-active="/dashboard">
<!-- /dashboard是数据看板页/欢迎页-->
<!--遍历循环:mi.children(children是名字,跟下面是对应的)-->
<template v-for="mi in menuItems">
<el-sub-menu v-if="Array.isArray(mi.children)" :index="mi.url || mi.name">
<template #title>
<span>{{ mi.name }}</span>
</template>
<el-menu-item
v-for="smi in mi.children"
:index="smi.url"
:key="smi.url"
>
<span>{{ smi.name }}</span>
</el-menu-item>
</el-sub-menu>
<el-menu-item v-else :index="mi.url" :key="mi.url">
<span>{{ mi.name }}</span>
</el-menu-item>
</template>
</el-menu>
</el-aside>
<!-- 二级导航 :router -->
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
<style scoped>
.h100 {
height: 100%;
}
header {
height: 135px;
background-color: aliceblue;
display: flex;
}
header > .logo {
height: 135px;
width: 170px;
background: url("@/assets/logo.png") no-repeat center center/cover;
}
aside {
width: 200px;
background-color: #545c64;
}
.nav {
border-right: none;
}
.logout-btn {
display: inline-block;
position: absolute;
right: 10px;
top: 25px;
}
aside {
width: 200px;
background-color: #545c64;
}
.nav {
border-right: none;
height: 100%;
}
.el-header {
display: flex;
align-items: center; /* 垂直居中 */
justify-content: center; /* 水平居中 */
height: 75px;
background-color: aliceblue;
position: relative; /* 为logo定位做准备 */
}
.system-title {
font-size: 24px; /* 调整字体大小 */
font-weight: bold; /* 加粗 */
margin: 0; /* 去除默认边距 */
text-align: center; /* 文字居中 */
flex-grow: 1; /* 占据剩余空间 */
}
</style>
<script setup>
import {reactive} from "vue";
// import {removeJwt} from "@/api/jwt.js";
import router from "@/router/index.js";
//所有导航菜单
const menuItems = reactive([
{
name: "数据看板",
url: "/main/dashboard"
},
{ name: "客户管理",
url: "/main/members", // 添加父级url
children: [
{
name: "客户列表",
url: "/main/members" // 修改为/main/members
}
]
},
{
name: "课程管理",
children: [
{
name: "课程列表",
url: "/main/role"
},
{
name: "课程日历",
url: "/main/role"
}
]
},
{
name: "教练管理",
children: [
{
name: "教练列表",
url: "/main/club"
}
]
},
{
name: "管理员管理",
children: [
{
name: "管理员列表",
url: "/main/role"
}
]
}
]);
// //注销
// function logout() {
// removeJwt();
// router.push("/login");
// }
</script>
index.js
//定义路由转发器
import {createRouter, createWebHistory} from "vue-router";
//定义路由:
const routes = [{
name: "main", // 路由名称(建议英文,便于编程式导航)
path: "/main", // 浏览器访问的 URL 路径,(如果是请求main每次浏览器请求的时候就路由到下面的组件)
component: () => import("@/components/view/Main.vue"), // 懒加载组件
children: [
{
name: "dashboard",
path: "/main/dashboard",
component: () => import("@/components/view/Dashboard.vue") // 需要创建这个组件
},{
name: "members",
path: "/main/members",
component: () => import("@/components/view/Member.vue")
}
]
}, {
name: "index",
path: "", // 空路径(根路径 /)
redirect: "/main" //自动重定向:写的是上面路由的地址
}];
//定义路由转发器:导入函数:createRouter
const router = createRouter({
routes,//转发哪些路由
history: createWebHistory()//记录访问地址,可以实现前进/后退
});
export default router;//把路由转发器导出
最终的实现效果:
4.客户页面继续完善(会员等级,到期时间)
整体的步骤 :
(1)数据库完善字段和数据库内的信息
(2)后端实体类添加对应的属性和数据库中的列名对应(mybatis-plus自动实现驼峰式转换 )
(3)在对应的.vue里面添加html页面框架,然后利用属性prop和 后端数据进行绑定
(4)修改其对应的数据模型,把新增的属性加进去。
<template>
<!-- 1.查询条件区域:想作为查询条件的是:ID、姓名、手机号、性别、年龄、地址 、出生日期范围-->
<div class="page-container">
<!--行内样式、双向绑定数据模型formInline.prop:和后端字段绑定-->
<el-form :inline="true" :model="formInline">
<el-form-item label="会员卡号" prop="id">
<el-input v-model="formInline.id" placeholder="请输入卡号" style="width: 130px" clearable/>
</el-form-item>
<el-form-item label="姓名" prop="name">
<el-input v-model="formInline.name" placeholder="请输入客户姓名" style="width: 160px" clearable/>
</el-form-item>
<el-form-item label="电话" prop="phone">
<el-input v-model="formInline.phone" placeholder="请输入客户电话" clearable/>
</el-form-item>
<el-form-item label="客户等级" prop="vip" style="width: 190px">
<el-select v-model="formInline.vip" clearable>
<el-option label="不限" value="不限"/>
<el-option label="普通会员" value="普通会员"/>
<el-option label="黄金会员" value="黄金会员"/>
<el-option label="钻石会员" value="钻石会员"/>
<el-option label="黑金会员" value="黑金会员"/>
</el-select>
</el-form-item>
<el-form-item label="性别" prop="sex" style="width: 160px">
<el-select v-model="formInline.sex" clearable>
<el-option label="不限" value="不限"/>
<el-option label="男" value="男"/>
<el-option label="女" value="女"/>
</el-select>
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input v-model="formInline.age" placeholder="请输入客户年龄" clearable/>
</el-form-item>
<el-form-item label="地址" prop="address">
<el-input v-model="formInline.address" placeholder="请输入客户地址" clearable/>
</el-form-item>
<el-form-item label="出生日期">
<el-date-picker
v-model="formInline.birthdayRange"
type="daterange"
start-placeholder="起始日期"
end-placeholder="终止日期"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-form>
</div>
<!-- 2.按钮区-->
<div>
<div class="mb-4">
<el-button type="primary" round @click="openAddDialog">增加会员</el-button>
<el-button type="success" round @click="edit">修改会员</el-button>
<el-button type="info" round @click="select()">查询会员</el-button>
<el-button type="primary" round @click="reset">重置</el-button>
<el-button type="danger" round @click="remove">删除会员</el-button>
</div>
</div>
<!-- 3.表格展示成员数据-->
<div>
<el-table ref="tableRef" :data="tableData" style="width: 100%" class="data-grid"
@row-click="tblRowClick()" stripe
border highlight-current-row show-header :header-cell-style="{
background: '#5da6e6',
color: 'white',
fontWeight: 'bold',
}"
>
<el-table-column type="selection" width="160" align="center" height="160" name="custom-selection-col"/>
<el-table-column fixed prop="id" label="会员卡号" width="160" height="230px" align="center"/>
<el-table-column fixed prop="name" label="姓名" width="130"/>
<el-table-column prop="phone" label="电话" width="150" align="center"/>
<!-- <el-table-column prop="vip" label="会员等级" width="150" align="center"/>-->
<el-table-column prop="vip" label="会员等级" width="150" align="center">
<template #default="scope">
<el-tag
v-if="scope.row.vip === '普通会员'"
type="info"
size="large"
>
{{scope.row.vip}}
</el-tag>
<el-tag
v-else-if="scope.row.vip === '白银会员'"
type=""
size="large"
>
{{scope.row.vip}}
</el-tag>
<el-tag
v-else-if="scope.row.vip === '黄金会员'"
type="warning"
size="large"
>
{{scope.row.vip}}
</el-tag>
<el-tag
v-else-if="scope.row.vip === '钻石会员'"
type="success"
size="large"
>
{{scope.row.vip}}
</el-tag>
<el-tag
v-else
type="danger"
size="large"
>
{{scope.row.vip || '未知等级'}}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="age" label="年龄" width="120" align="center"/>
<el-table-column prop="createTime" label="开卡时间" width="180" align="center"/>
<el-table-column prop="endTime" label="到期时间" width="180" align="center"/>
<el-table-column prop="address" label="地址" width="200" align="center"/>
<el-table-column prop="sex" label="性别" width="120" align="center">
<template #default="scope">
<!-- editRow(scope.row)传参是scope.row,意思是获取当行的数据,点就选中了 -->
<el-tag v-if="scope.row.sex=='男'" type="primary" size="large">{{scope.row.sex}}</el-tag>
<el-tag v-else type="danger" size="large">{{scope.row.sex}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" width="200" align="center"/>
<el-table-column prop="birthday" label="出生日期" min-width="180" align="center"/>
<el-table-column label="操作" width="160" fixed="right" align="center">
<template #default="scope">
<!-- editRow(scope.row)传参是scope.row,意思是获取当行的数据,点就选中了 -->
<el-button type="primary" size="small" @click.stop="editRow(scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click.stop="deleteRow(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 4.分页条-->
<div class="pagination">
<el-pagination
v-model:current-page="memberPi.pageNo"
v-model:page-size="memberPi.pageSize"
:page-sizes="[1,5,10,15,20]"
layout="total, sizes, prev, pager, next, jumper"
:total="memberPi.total"
class="member-pi"
background
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
<!-- 5-增加会员:-->
<div>
<el-dialog v-model="dialogVisible" title="新增会员信息" width="500" draggable>
<el-form-item label="ID" prop="id" v-if="false">
<el-input v-model="formInline2.id" placeholder="请输入ID" style="width: 130px" clearable/>
</el-form-item>
<el-form-item label="姓名" prop="name">
<el-input v-model="formInline2.name" placeholder="请输入客户姓名" style="width: 160px" clearable/>
</el-form-item>
<el-form-item label="电话" prop="phone">
<el-input v-model="formInline2.phone" placeholder="请输入客户电话" clearable/>
</el-form-item>
<el-form-item label="会员等级" prop="vip" style="width: 190px">
<el-select v-model="formInline2.vip" clearable>
<el-option label="普通会员" value="普通会员"/>
<el-option label="黄金会员" value="黄金会员"/>
<el-option label="钻石会员" value="钻石会员"/>
<el-option label="黑金会员" value="黑金会员"/>
</el-select>
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input v-model="formInline2.age" placeholder="请输入客户年龄" clearable/>
</el-form-item>
<el-form-item label="注册时间" prop="createTime">
<el-date-picker
v-model="formInline2.createTime"
type="date"
placeholder="注册日期"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="到期时间" prop="endTime">
<el-date-picker
v-model="formInline2.endTime"
type="date"
placeholder="到期日期"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="地址" prop="address">
<el-input v-model="formInline2.address" placeholder="请输入客户地址" clearable/>
</el-form-item>
<el-form-item label="性别" prop="sex" style="width: 160px">
<el-select v-model="formInline2.sex" clearable>
<el-option label="男" value="男"/>
<el-option label="女" value="女"/>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark" :rows="4">
<el-input v-model="formInline2.remark"
width="260px" placeholder="请输入客户信息备注" clearable/>
</el-form-item>
<el-form-item label="出生日期" prop="birthday">
<el-date-picker
v-model="formInline2.birthday"
type="date"
placeholder="出生日期"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitAdd">
确定
</el-button>
</div>
</template>
</el-dialog>
</div>
<!-- 6-修改会员:-->
<div>
<el-dialog v-model="dialogVisible2" title="修改会员信息" width="500" draggable>
<el-form :model="formInline3">
<el-form-item label="ID" prop="id" v-if="false">
<el-input v-model="formInline3.id" placeholder="请输入ID" style="width: 130px" clearable/>
</el-form-item>
<el-form-item label="姓名" prop="name">
<el-input v-model="formInline3.name" placeholder="请输入客户姓名" style="width: 160px" clearable/>
</el-form-item>
<el-form-item label="电话" prop="phone">
<el-input v-model="formInline3.phone" placeholder="请输入客户电话" clearable/>
</el-form-item>
<el-form-item label="会员等级" prop="vip" style="width: 190px">
<el-select v-model="formInline3.vip" clearable>
<el-option label="普通会员" value="普通会员"/>
<el-option label="黄金会员" value="黄金会员"/>
<el-option label="钻石会员" value="钻石会员"/>
<el-option label="黑金会员" value="黑金会员"/>
</el-select>
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input v-model="formInline3.age" placeholder="请输入客户年龄" clearable/>
</el-form-item>
<el-form-item label="注册时间" prop="createTime">
<el-date-picker
v-model="formInline3.createTime"
type="date"
placeholder="注册日期"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="到期时间" prop="endTime">
<el-date-picker
v-model="formInline3.endTime"
type="date"
placeholder="到期日期"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="地址" prop="address">
<el-input v-model="formInline3.address" placeholder="请输入客户地址" clearable/>
</el-form-item>
<el-form-item label="性别" prop="sex" style="width: 160px">
<el-select v-model="formInline3.sex" clearable>
<el-option label="男" value="男"/>
<el-option label="女" value="女"/>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark" :rows="4">
<el-input v-model="formInline3.remark"
width="260px" placeholder="请输入客户信息备注" clearable/>
</el-form-item>
<el-form-item label="出生日期" prop="birthday">
<el-date-picker
v-model="formInline3.birthday"
type="date"
placeholder="出生日期"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible2 = false">取消</el-button>
<el-button type="primary" @click="submitEdit">
确定
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {reactive, ref, onMounted, toRaw} from 'vue'
import api from "@/utils/api.js";
import {ElMessage, ElMessageBox} from 'element-plus'
const size = ref('default');
const disabled = ref(false);
// 对话框控制:新增页面
const dialogVisible = ref(false)
// 对话框控制:修改页面
const dialogVisible2 = ref(false)
// 查询表单对象
let formInline = ref({
id: null,
name: null,
phone: null,
vip:null,
sex: null,
age: null,
address: null,
birthdayRange: []
});
// 表格数据对象
let tableData = ref([]);
// 分页配置
let memberPi = reactive({
pageNo: 1,
pageSize: 15,
total: 0
});
// 新增会员表单数据
let formInline2 = ref({
id: null,
name: null,
phone: null,
vip:null,
sex: null,
age: null,
address: null,
birthday: null,
createTime: null,
endTime:null,
remark: null
});
// 修改会员表单数据
let formInline3 = ref({
id: null,
name: null,
phone: null,
vip:null,
sex: null,
age: null,
address: null,
birthday: null,
createTime: null,
endTime:null,
remark: null
});
// 查询会员方法
async function select(pageNo = 1, pageSize = 10) {
let params = toRaw(formInline.value);
if (params.birthdayRange) {
params.birthdayFrom = params.birthdayRange[0];
params.birthdayTo = params.birthdayRange[1];
delete params.birthdayRange;
}
try {
const resp = await api({
url: "/members",
method: "get",
params: {
pageNo,
pageSize,
...params
}
});
tableData.value = resp.data.records;
memberPi.pageNo = resp.data.current;
memberPi.pageSize = resp.data.size;
memberPi.total = resp.data.total;
} catch (error) {
console.error("查询失败:", error);
}
}
// 分页变化处理
const handlePageChange = (currentPage) => {
memberPi.pageNo = currentPage;
select(currentPage, memberPi.pageSize);
};
const handleSizeChange = (pageSize) => {
memberPi.pageSize = pageSize;
select(1, pageSize);
};
// 重置表单
function reset() {
formInline.value = {
id: null,
name: null,
phone: null,
sex: null,
age: null,
address: null,
birthday: null
};
}
// 表格操作
const tableRef = ref()
function tblRowClick(row) {
if (!row || !tableRef.value) return
tableRef.value.toggleRowSelection(row)
}
//删除会员按钮:实现只选中一行数据
function remove() {
let rows = tableRef.value.getSelectionRows();//通过实例获取选中的表格的是哪一行
if (rows.length === 0) {
ElMessage.warning("请选中您要删除的行");//设置提示信息
} else {
ElMessageBox.confirm("是否确认删除选中的行?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
//执行操作
let ids = rows.map(it => it.id);//获取选中的删除Id
removeByIds(ids);//校验只选中一行成功之后,调用removeByIds方法真正删除,并传递要删除的会员的Id值
}).catch(() => {
//捕获之后
});
}
}
async function removeByIds(ids) {
let resp = await api({
url: "/members",
method: "delete",
data: ids
});
if (resp.success) {
ElMessage.success("删除操作成功,共删除" + resp.data + "条");
select(); // 刷新表格
} else {
ElMessage.error("删除失败,请稍候再试或联系管理员");
}
}
// 打开新增对话框
function openAddDialog() {
formInline2.value = {
id: null,
name: null,
phone: null,
sex: null,
age: null,
address: null,
birthday: null,
createTime: null,
remark: null
}
dialogVisible.value = true
}
// 提交新增
async function submitAdd() {
try {
// 处理日期数据
const params = {
...toRaw(formInline2.value),
birthdayFrom: formInline2.value.birthdayRange?.[0],
birthdayTo: formInline2.value.birthdayRange?.[1]
}
delete params.birthdayRange
const resp = await api({
url: "/members",
method: "post",
data: params
})
if (resp.success) {
ElMessage.success("新增会员成功")
dialogVisible.value = false
select() // 刷新表格
}
} catch (error) {
console.error("新增失败:", error)
ElMessage.error("新增失败,请稍候再试")
}
}
//新增表单对象
let memberFormRef = ref();
let mode = "add";//标志位
//修改按钮
function edit() {
let rows = tableRef.value.getSelectionRows();
if (rows.length === 0) {
ElMessage.warning("请选中您要修改的行");
} else if (rows.length > 1) {
ElMessage.warning("您一次只能修改一行");
} else {
// 将选中的行数据填充到表单中
formInline3.value = {
...rows[0]
};
dialogVisible2.value = true;
}
}
//提交修改
async function submitEdit() {
try {
const resp = await api({
url: "/members",
method: "put",
data: toRaw(formInline3.value)
});
if (resp.success) {
ElMessage.success("修改会员信息成功");
dialogVisible2.value = false;
select(); // 刷新表格
}
} catch (error) {
console.error("修改失败:", error);
ElMessage.error("修改失败,请稍候再试");
}
}
// 单行编辑
function editRow(row) {
// 将当前行数据填充到修改表单中
formInline3.value = {
...row
};
dialogVisible2.value = true;
}
//单行删除
const deleteRow = (row) => {
ElMessageBox.confirm(`是否确认删除会员 ${row.name}?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
removeByIds([row.id]); // 调用批量删除方法,传入当前行的id
}).catch(() => {
// 用户取消操作
});
}
// 组件挂载时加载数据
onMounted(() => {
select();
});
</script>
<style>
.data-grid {
margin-top: 6px;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: center;
}
.member-pi {
margin-top: 6px;
}
</style>
三、前端校验
前端输入数据的时候 ,进行校验,比如新增/修改的时候,手机号不能为空,以及客户到期日期不可早于注册日期等等 。
1.首先添加校验规则
//表单校验:校验规则
//校验规则
const rules={
phone:[{
required:true,
message:"手机号不可为空",
trigger:"blur"//失去焦点就触发
},{
min:11,
max:11,
message:"手机号必须是11位"
},{
validator:validatePhone,
trigger: "blur"
}],
name:[{
required:true,
message:"姓名不可为空",
trigger:"blur"
}],
endTime: [{
required: true,
message: "到期时间不能为空",
trigger: "blur"
}, {
validator: validateEndTime,
trigger: "blur"
}]
};
//手机号以1开头校验
function validatePhone(rule,value,cb){
if(value.startsWith("1")){
return cb();
}else{
return cb(new Error("手机号必须以1开头"));
}
}
// 到期时间校验
function validateEndTime(rule, value, cb) {
// 获取表单中的createTime值
const createTime = formInline2.value.createTime;
// 1. 检查到期时间是否为空
if (!value) {
return cb(new Error("到期时间不能为空"));
}
// 2. 检查开卡时间是否已填写
if (!createTime) {
return cb(new Error("请先填写开卡时间"));
}
// 3. 比较时间
if (new Date(value) <= new Date(createTime)) {
return cb(new Error("到期时间必须晚于开卡时间"));
}
// 4. 校验通过
return cb();
}
2.绑定校验规则
在对应需要校验的模板头上加上属性 :rules="提供的校验方法”
3.校验结果测试
修改的校验也是如此。
四、头像上传
https://element-plus.org/zh-CN/component/upload.html
1.查看数据库有无字段
(1)数据库中要有上传图片对应的字段(没有的话自定义),然后我们通常是在数据库上传的文件的地址,不是文件本身。
2.后端实体类属性和数据库表中字段对应
(2)后端:定义和数据库中上传图片对应的属性。如果定义的不一致,就需要在后端实体类对应的字段上面用注解@TableField("数据库列名")
指定对应的数据库表中对应的列名
3.添加前端展示内容
(3)前端:在.vue里面引入网站上的模板自己修改一下
模板:
<el-upload class="avatar" action="" :on-success="handleAvatarSuccess">
<img v-if="imageUrl" :src="imageUrl" class="avatar" alt=""/>
<el-icon v-else class="avatar-uploader-icon">
<Plus/>
</el-icon>
</el-upload>
自己改:
<el-upload
class="avatar"
action="/api/coach/photo"
:on-success="avatarUploadSuccess"
:show-file-list="false"
>
<div v-if="formInline2.photo" class="img"
:style="'background-image: url(' + (baseUrl + formInline2.photo) + ')'"></div>
<el-icon v-else class="icon"><Plus /></el-icon>
</el-upload>
这里新增了一个photo,所以不要忘记增加数据模型里面的内容:
// 新增会员表单数据
let formInline2 = ref({
id: null,
name: null,
phone: null,
vip:null,
sex: null,
age: null,
address: null,
birthday: null,
createTime: null,
endTime:null,
remark: null,
photo:null
});
同时:一定不要在提交的方法里面把photo传进去,这样才能把数据图片存到 数据库里面,才能实现展示效果:
// 提交新增
async function submitAdd() {
try {
// 确保所有必填字段都有值
const params = {
name: formInline2.value.name,
phone: formInline2.value.phone,
wechat: formInline2.value.wechat, // 必填字段
sex: formInline2.value.sex,
recomm: formInline2.value.recomm,
photo: formInline2.value.photo // 确保包含 photo 字段
}
const resp = await api({
url: "/coach", // 确保URL正确
method: "post",
data: params
})
if (resp.success) {
ElMessage.success("新增教练成功")
dialogVisible.value = false
select() // 刷新表格
}
} catch (error) {
console.error("新增失败:", error)
ElMessage.error("新增失败: " + (error.response?.data?.message || "请检查输入数据"))
}
}
css:
.avatar {
width: 140px;
height: 140px;
border: 1px dashed #ccc;
border-radius: 4px;
margin-left: 8px;
display: flex;
}
.avatar .icon {
font-size: 28px;
justify-content: center;
align-items: center;
}
.avatar .img {
width: 140px;
height: 140px;
background-repeat: no-repeat;
background-size: contain;
background-position: center center;
}
.row-avatar {
width: 60px;
height: 60px;
background-repeat: no-repeat;
background-size: contain;
background-position: center center;
border: 1px solid #ccc;
}
js:
//(新增的时候)头像上传成功(所以用的是新增的数据模型)
function avatarUploadSuccess(resp) {
//console.log(url)
formInline2.value.photo = resp.data;
}
效果 :
4.后端响应:
前端有写传递的接口:
所以后端编写对应的接口进行响应:
图片上传默认名字就是file
这个接口对应的应该是/photo
上传经常使用(比如会员 、管理员一系列的都可以上传头像),所以可以封装成工具类/业务类 :(实现通用化)
(1)获取两个参数的工具类:
package com.study.util;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
//获取两个参数的泛型
public class Tuple<T1,T2> {
private T1 first;
private T2 second;
public static <T1,T2> Tuple<T1,T2> of(T1 t1,T2 t2){
return new Tuple<>(t1, t2);
}
}
(2)用户上传文件路径配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/system1?serverTimezone=GMT%2b8
username: root
password: 123456
# 配置mybatis
mybatis:
configuration:
# 在映射为java对象,将表中的下划线命名自动转驼峰式命名
map-underscore-to-camel-case: true
# 日志前缀,可选
log-prefix: mybatis.
# 日志实现类,可选
log-impl: org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl
# 动态sql文件存储位置
mapper-locations: classpath:/mapper/**/*.xml
# 配置日志显示sql
logging:
level:
# 指定日志前缀
mybatis: debug
# 文件上传位置:
upload:
location: F:/project1/upload/
(3)Service接口 :
package com.study.service;
import com.study.util.Tuple;
import org.springframework.web.multipart.MultipartFile;
public interface UploadService {
//上传图片:两个地址:访问地址和存储地址.String type:上传的类型,以上传类型为目录创建文件夹存储
String uploadImage(MultipartFile file,String type);
}
(3)接口的具体实现:
添加静态资源位置
#spring web静态资源路径
web:
resources:
static-locations: classpath:/resources/, classpath:/static/, file:/${upload.location}
package com.study.service.impl;
import com.study.service.UploadService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Random;
@Service
public class UploadServiceImpl implements UploadService {
//--上传到哪里?:用户自己配置上传路径:在配置文件里面添加通用路径.添加完之后,我们要获取,注入到业务类(本类中)
@Value("${upload.location}")
private String uploadLocation;//文件上传路径
@Override
public String uploadImage(MultipartFile file, String type) {
//完成文件上传:
//1.创建目录
File dir = new File(uploadLocation + "/images/" + type);
//2.判断目录是否存在,如果不存在创建级联目录:
if (!dir.exists()) {
boolean b = dir.mkdirs();//创建级联目录
if (!b) {
throw new RuntimeException("级联创建目录异常");
}
}
//3.给上传的文件起名字
LocalDateTime now = LocalDateTime.now();//获取当前时间
String fileName = now.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));//以当前时间起名字,加上时分秒
//时分秒同一时刻也可能重复 ,所以再添加随机数作为名字
Random random = new Random();
int sid = random.nextInt(1000);//0-999
fileName = fileName + "-" + sid;
//4.拼上扩展名
String originalFilename = file.getOriginalFilename();//上传文件名
int idx = originalFilename.lastIndexOf(".");
String ext = originalFilename.substring(idx);
fileName = fileName + ext;
//完整文件名
String fullName = dir.getAbsolutePath()+"/"+fileName;
//要存储的目标文件
File target = new File(fullName);
//4.存储文件:
try{
file.transferTo(target);
}catch (IOException e){
throw new RuntimeException("保存文件失败");
}
//5.返回访问地址和存储地址
//需求:往数据库中存一个地址,前端要想能访问也需要一个地址,只能访问项目目录下的文件
//所以继续配置一下:配置spring web静态资源路径,一旦是静态资源,那么就可以通过HTTP访问了
//存储在数据库中的地址:
return "/images/" + type +"/" +fileName;
}
}
(4)Coontroller层依赖注入调用方法
//图片上传的依赖注入:
private UploadService uploadService;
@Autowired
public void setUploadService(UploadService uploadService) {
this.uploadService = uploadService;
}
package com.study.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.study.model.Coach;
import com.study.service.CoachService;
import com.study.service.UploadService;
import com.study.util.JsonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@RestController
@RequestMapping("/api/v1/coach")
public class CoachController {
//依赖注入
private CoachService coachService;
@Autowired
public void setCoachService(CoachService coachService) {
this.coachService = coachService;
}
//图片上传的依赖注入:
private UploadService uploadService;
@Autowired
public void setUploadService(UploadService uploadService) {
this.uploadService = uploadService;
}
//查询全部教练:controller层响应给前端,返回值是 ResponseEntity<Coach>
@GetMapping
public ResponseEntity<JsonResult<?>> findAl(
@RequestParam(defaultValue = "1") Integer pageNo,
@RequestParam(defaultValue = "10") Integer pageSize,
Coach coach) {
Page<Coach> page = new Page<>(pageNo, pageSize);
Page<Coach> all = coachService.findAll(page, coach);
return ResponseEntity.ok(JsonResult.success(all));
}
//新增教练:
@PostMapping
public ResponseEntity<JsonResult<?>> add(@RequestBody Coach coach){
boolean add = coachService.add(coach);
if(add){
return ResponseEntity.ok(JsonResult.success("新增教练成功"));
}else return ResponseEntity.ok(JsonResult.fail("新增教练失败"));
}
//修改教练:
@PutMapping
public ResponseEntity<JsonResult<?>> edit(@RequestBody Coach coach){
boolean update = coachService.edit(coach);
if(update){
return ResponseEntity.ok(JsonResult.success("修改教练成功"));
}else return ResponseEntity.ok(JsonResult.fail("修改教练失败"));
}
//删除教练 :
@DeleteMapping
public ResponseEntity<JsonResult<?>> delete(@RequestBody List<Integer> ids){
int count = coachService.delete(ids);
if(count==0){
return ResponseEntity.ok(JsonResult.fail("删除教练失败"));
}else{
return ResponseEntity.ok(JsonResult.success(count));
}
}
//上传头像 :
@PostMapping("/photo")
public ResponseEntity<JsonResult<?>> uploadMemberAvatar(MultipartFile file) {
String path = this.uploadService.uploadImage(file, "coach_photo");
return ResponseEntity.ok(JsonResult.success(path));
}
}
(5)定义前端的全局遍量 :
在前端导入 :
const baseUrl = "http://localhost:8080";
export default baseUrl
实现的效果:
且本地电脑对应 位置也有,数据库中也有 :