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

发布于:2025-08-18 ⋅ 阅读:(15) ⋅ 点赞:(0)

前言

环境搭建:

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

在这里插入图片描述

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

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


网站公告

今日签到

点亮在社区的每一天
去签到