当前位置: 首页 > news >正文

健身房预约系统SSM+Mybatis实现(三、校验 +页面完善+头像上传)

文章目录

  • 前言
  • 一 、添加后端参数校验
    • 1.参数校验的具体使用
      • (1)引入依赖
      • (2)参数校验
      • (3)对后端异常进行统一处理 (捕获 )
    • 2.后端校验大全
    • 3.总结
  • 二 、页面继续完善
    • 1.操作按钮
    • 2.性别的图标显示
    • 3.添加菜单栏,实现跳转
    • 4.客户页面继续完善(会员等级,到期时间)
      • 整体的步骤 :
  • 三、前端校验
    • 1.首先添加校验规则
    • 2.绑定校验规则
    • 3.校验结果测试
  • 四、头像上传
    • 1.查看数据库有无字段
    • 2.后端实体类属性和数据库表中字段对应
    • 3.添加前端展示内容
    • 4.后端响应:

前言

环境搭建:

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;}//查询全部客户:@GetMappingpublic 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-itemv-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", // 添加父级urlchildren: [{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-pickerv-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()" stripeborder 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-tagv-if="scope.row.vip === '普通会员'"type="info"size="large">{{scope.row.vip}}</el-tag><el-tagv-else-if="scope.row.vip === '白银会员'"type=""size="large">{{scope.row.vip}}</el-tag><el-tagv-else-if="scope.row.vip === '黄金会员'"type="warning"size="large">{{scope.row.vip}}</el-tag><el-tagv-else-if="scope.row.vip === '钻石会员'"type="success"size="large">{{scope.row.vip}}</el-tag><el-tagv-elsetype="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-paginationv-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-pickerv-model="formInline2.createTime"type="date"placeholder="注册日期"value-format="YYYY-MM-DD"/></el-form-item><el-form-item label="到期时间" prop="endTime"><el-date-pickerv-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-pickerv-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-pickerv-model="formInline3.createTime"type="date"placeholder="注册日期"value-format="YYYY-MM-DD"/></el-form-item><el-form-item label="到期时间" prop="endTime"><el-date-pickerv-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-pickerv-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) returntableRef.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);//获取选中的删除IdremoveByIds(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.birthdayRangeconst resp = await api({url: "/members",method: "post",data: params})if (resp.success) {ElMessage.success("新增会员成功")dialogVisible.value = falseselect() // 刷新表格}} 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 = falseselect() // 刷新表格}} 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.Driverurl: jdbc:mysql://127.0.0.1:3306/system1?serverTimezone=GMT%2b8username: rootpassword: 123456# 配置mybatismybatis: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;//文件上传路径@Overridepublic 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-999fileName = 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;@Autowiredpublic 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;@Autowiredpublic void setCoachService(CoachService coachService) {this.coachService = coachService;}//图片上传的依赖注入:private UploadService  uploadService;@Autowiredpublic void setUploadService(UploadService uploadService) {this.uploadService = uploadService;}//查询全部教练:controller层响应给前端,返回值是 ResponseEntity<Coach>@GetMappingpublic 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));}//新增教练:@PostMappingpublic ResponseEntity<JsonResult<?>> add(@RequestBody Coach coach){boolean add = coachService.add(coach);if(add){return ResponseEntity.ok(JsonResult.success("新增教练成功"));}else return ResponseEntity.ok(JsonResult.fail("新增教练失败"));}//修改教练:@PutMappingpublic ResponseEntity<JsonResult<?>> edit(@RequestBody  Coach coach){boolean update = coachService.edit(coach);if(update){return ResponseEntity.ok(JsonResult.success("修改教练成功"));}else return ResponseEntity.ok(JsonResult.fail("修改教练失败"));}//删除教练 :@DeleteMappingpublic 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

在这里插入图片描述

实现的效果:
在这里插入图片描述
且本地电脑对应 位置也有,数据库中也有 :

在这里插入图片描述
在这里插入图片描述

http://www.lryc.cn/news/623978.html

相关文章:

  • RISC-V汇编新手入门
  • 【LeetCode】单链表经典算法:移除元素,反转链表,约瑟夫环问题,找中间节点,分割链表
  • 开发指南132-DOM的宽度、高度属性
  • HTTP0.9/1.0/1.1/2.0
  • SWE-bench:真实世界软件工程任务的“试金石”
  • 人工智能入门②:AI基础知识(下)
  • C++入门自学Day11-- String, Vector, List 复习
  • 如何利用gemini-cli快速了解一个项目以及学习新的组件?
  • 数据结构03(Java)--(递归行为和递归行为时间复杂度估算,master公式)
  • 人脸AI半球梯控/门禁读头的功能参数与技术实现方案
  • MySQL的事务基础概念:
  • 力扣刷题904——水果成篮
  • 黑马商城day08-Elasticsearch作业(个人记录、仅供参考、详细图解)
  • MLArena:一款不错的AutoML工具介绍
  • 【Linux】IO多路复用
  • SpringCloud 07 微服务网关
  • linux-高级IO(上)
  • 【撸靶笔记】第五关:GET - Double Injection - Single Quotes - String
  • Linux目录介绍
  • 002.Redis 配置及数据类型
  • 第三十八天(Node.JS)
  • AUTOSAR ARXML介绍
  • gin结合minio来做文件存储
  • Oracle Undo Tablespace 使用率暴涨案例分析
  • UE5多人MOBA+GAS 49、创建大厅
  • java设计模式之迪米特法则使用场景分析
  • ​​Vue 3 开发速成手册
  • PHP现代化全栈开发:测试驱动开发与持续交付实践
  • MCP原理与开发及与大模型交互流程
  • 最小路径和