健身房预约系统SSM+Mybatis(五、预约展示)
预约页面的展示
<template><meta charset="UTF-8"><!-- 1.查询条件区域 --><div class="page-container"><el-form :inline="true" :model="formInline"><el-form-item label="预约ID" prop="id"><el-input v-model="formInline.id" placeholder="请输入预约ID" style="width: 130px" clearable/></el-form-item><el-form-item label="课程ID" prop="courseId"><el-input v-model="formInline.courseId" placeholder="请输入课程ID" style="width: 130px" clearable/></el-form-item><el-form-item label="学员ID" prop="memberId"><el-input v-model="formInline.memberId" placeholder="请输入学员ID" style="width: 130px" clearable/></el-form-item><el-form-item label="预约状态" prop="status" style="width: 160px"><el-select v-model="formInline.status" clearable><el-option label="全部" value=""/><el-option label="已预约" :value="1"/><el-option label="已取消" :value="0"/></el-select></el-form-item><el-form-item label="预约时间"><el-date-pickerv-model="formInline.reserveTimeRange"type="daterange"range-separator="至"start-placeholder="开始日期"end-placeholder="结束日期"value-format="YYYY-MM-DD"/></el-form-item></el-form></div><!-- 2.按钮区--><div><div class="mb-4"><el-button type="danger" round @click="batchCancel">批量取消</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="60" align="center"/><el-table-column prop="id" label="预约ID" width="120" align="center"/><el-table-column prop="courseId" label="课程ID" width="120" align="center"/><el-table-column prop="memberId" label="学员ID" width="120" align="center"/><!-- todo:--><el-table-column prop="" label="课程名称" width="120" align="center"/><el-table-column prop="" label="教练" width="120" align="center"/><el-table-column prop="" label="课程教室" width="120" align="center"/><el-table-column prop="reserveTime" label="预约时间" width="180" align="center"/><!--todo--><el-table-column prop="" label="课程时长" width="180" align="center"/><el-table-column prop="" label="预约人数" width="180" align="center"/><el-table-column prop="" label="当前课程允许的最大人数" width="180" align="center"/><el-table-column prop="status" label="状态" width="120" align="center"><template #default="scope"><el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">{{ scope.row.status === 1 ? '已预约' : '已取消' }}</el-tag></template></el-table-column><el-table-column prop="score" label="评分" width="120" align="center"><template #default="scope"><span v-if="scope.row.score">{{ scope.row.score }}分</span><span v-else>-</span></template></el-table-column><el-table-column prop="commentTime" label="评价时间" width="180" align="center"/><el-table-column prop="comment" label="评价内容" show-overflow-tooltip/><el-table-column label="操作" width="180" fixed="right" align="center"><template #default="scope"><el-button type="warning" size="small" @click.stop="cancelReserve(scope.row)" v-if="scope.row.status === 1">取消预约</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="[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>
</template><script setup>
import {reactive, ref, onMounted, toRaw} from 'vue'
import api from "@/utils/api.js";
import {ElMessage, ElMessageBox} from 'element-plus'// 查询表单对象
let formInline = ref({id: null,courseId: null,memberId: null,status: null,reserveTimeRange: []
});// 表格数据对象
let tableData = ref([]);// 分页配置
let memberPi = reactive({pageNo: 1,pageSize: 15,total: 0
});// 查询预约方法
async function select(pageNo = 1, pageSize = 10) {let params = toRaw(formInline.value);// 处理时间范围if (params.reserveTimeRange && params.reserveTimeRange.length === 2) {params.startTime = params.reserveTimeRange[0];params.endTime = params.reserveTimeRange[1];delete params.reserveTimeRange;}try {const resp = await api({url: "/reserves",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,courseId: null,memberId: null,status: null,reserveTimeRange: []};
}// 表格操作
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);removeByIds(ids);}).catch(() => {});}
}async function removeByIds(ids) {let resp = await api({url: "/reserves",method: "delete",data: ids,headers: {'Content-Type': 'application/json;charset=utf-8' // 明确指定编码}});if (resp.success) {ElMessage.success(`删除操作成功,共删除${resp.data}条`);select();} else {ElMessage.error("删除失败,请稍候再试或联系管理员");}
}// 批量取消预约
function batchCancel() {let rows = tableRef.value.getSelectionRows();if (rows.length === 0) {ElMessage.warning("请选中您要取消的预约");} else {const toCancel = rows.filter(row => row.status === 1);if (toCancel.length === 0) {ElMessage.warning("选中的预约中没有可取消的状态");return;}ElMessageBox.confirm(`确定要取消选中的${toCancel.length}条预约记录吗?`, "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(() => {let ids = toCancel.map(it => it.id);cancelReserves(ids);}).catch(() => {});}
}// 取消预约
async function cancelReserves(ids) {try {const resp = await api({url: "/reserves/cancel",method: "put",data: ids});if (resp.success) {ElMessage.success(`成功取消${resp.data}条预约`);select();} else {ElMessage.error("取消预约失败");}} catch (error) {console.error("取消预约失败:", error);ElMessage.error("取消预约失败,请稍候再试");}
}// 单行取消预约
function cancelReserve(row) {if (row.status !== 1) {ElMessage.warning("该预约状态不可取消");return;}ElMessageBox.confirm(`确定要取消会员${row.memberId}的预约吗?`, "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(() => {cancelReserves([row.id]);}).catch(() => {});
}// 单行删除
const deleteRow = (row) => {ElMessageBox.confirm(`是否确认删除预约记录 ${row.id}?`, "警告", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(() => {removeByIds([row.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>
配置路由:
//定义路由转发器
import {createRouter, createWebHistory} from "vue-router";
import {getJwt} from "@/api/jwt.js";//定义路由:
const routes = [{name: "main", // 路由名称(建议英文,便于编程式导航)path: "/main", // 浏览器访问的 URL 路径,(如果是请求main每次浏览器请求的时候就路由到下面的组件)component: () => import("@/components/view/Main.vue"), // 懒加载组件children: [{name: "default",path: "",component: () => import("@/components/view/Default.vue") // 需要创建这个组件},{name: "dashboard",path: "/main/dashboard",component: () => import("@/components/view/Dashboard.vue") // 需要创建这个组件},{name: "members",path: "/main/members",component: () => import("@/components/view/Member.vue")},{name: "reservation",path: "/main/reservation",component: () => import("@/components/view/Reservation.vue")},{name: "admin",path: "/main/admin",component: () => import("@/components/view/Admin.vue")},{name: "coach",path: "/main/coach",component: () => import("@/components/view/Coach.vue")},{name: "course",path: "/main/course",component: () => import("@/components/view/Course.vue")}]
}, {name: "index",path: "", // 空路径(根路径 /)redirect: "/main" //自动重定向:写的是上面路由的地址
},{name: "login",path: "/login",component: () => import("@/components/view/Login.vue")}];//定义路由转发器:导入函数:createRouter
const router = createRouter({routes,//转发哪些路由history: createWebHistory()//记录访问地址,可以实现前进/后退
});//配置路由守卫
router.beforeEach((to, from, next) => {let jwt = getJwt();if (jwt) {if (to.name === "login") {next("/main");} else {next();}} else {if (to.name !== "login") {next("/login");} else {next();}}
});export default router;//把路由转发器导出
页面跳转 :
<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 style="height: 100vh;"><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",children: [{name: "客户列表",url: "/main/members"}]},{name: "课程管理",children: [{name: "课程列表",url: "/main/course"},{name: "课程日历",url: "/main/role"}]},{name: "预约管理",children: [{name: "预约列表",url: "/main/reservation"}]},{name: "教练管理",children: [{name: "教练列表",url: "/main/coach"}]},{name: "管理员管理",children: [{name: "管理员列表",url: "/main/admin"}]}
]);//注销
function logout() {removeJwt();router.push("/login");
}
</script>
预约业务—mybatis实现
1.多表联查数据展示 :
我们想在预约列表展示不仅仅有预约表的消息,还想展示比如课程表和教练表里面的信息 。
首先这些数据的查询都是要从数据库中查询出来的,然后我们关于数据库的操作写在mapper层,在mapper层定义接口,交给对应的映射文件处理:
具体的mybatis的使用详细操作见之前的博客:
https://blog.csdn.net/m0_72900498/article/details/149800105

多表联合查询:查询课程表中的课程名称、时长、开课日期,查询教练表中的教练名字,查询全部预约表中的信息,以及计算同一课程的预约人数 。
我们将上述全部的字段可以封装成一个实体 ,这样方便我们查询后映射 字段赋值 :
package com.study.model.search;import com.study.model.Reserve;
import lombok.Data;
import lombok.EqualsAndHashCode;//新建一个扩展类,包含所有需要展示的字段:
@Data
@EqualsAndHashCode(callSuper = true)
public class ReserveWithDetailsDTO extends Reserve {private String courseName; // 课程名称private String coachName; // 教练姓名private String room; // 教室private Integer length; // 课程时长(分钟)private Integer maxCount; // 课程最大人数private Integer currentCount; // 当前预约人数(需统计)
}
他继承了 Reserve 预约表,也有预约表里面的属性了 。
然后编写方法接口,在映射文件里面提供具体实现。
package com.study.mapper;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.study.model.Reserve;
import com.study.model.search.ReserveWithDetailsDTO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;@Mapper
public interface ReserveMapper extends BaseMapper<Reserve> {Page<ReserveWithDetailsDTO> findAll(Page<ReserveWithDetailsDTO> page, @Param("ew") LambdaQueryWrapper<Reserve> wrapper);
}
具体的映射文件实现:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.study.mapper.ReserveMapper"><!-- <resultMap id="" type="">--><!-- </resultMap>-->
<!--我们将表格展示的其他的数据都封装成一个模型,然后这里resultTyp会将查询结果映射到指定的实体类上去--><select id="findAll" resultType="com.study.model.search.ReserveWithDetailsDTO">SELECTr.*,c.name AS course_name,ch.name AS coach_name,c.room,c.length,c.max_count,(SELECT COUNT(*) FROM reserve WHERE course_id = r.course_id AND status = 1) AS current_countFROMreserve rLEFT JOINcourse c ON r.course_id = c.idLEFT JOINcoach ch ON c.coach_id = ch.id${ew.customSqlSegment}</select>
</mapper>
在前端将要显示的内容绑定prop后端的属性:
<el-table-column prop="courseName" label="课程名称" width="120" align="center"/><el-table-column prop="coachName" label="教练" width="120" align="center"/><el-table-column prop="room" label="课程教室" width="120" align="center"/><el-table-column prop="length" label="课程时长" width="180" align="center"/>
至此前端的预约页面全部的代码实现 :
<template><meta charset="UTF-8"><!-- 1.查询条件区域 --><div class="page-container"><el-form :inline="true" :model="formInline"><el-form-item label="预约ID" prop="id"><el-input v-model="formInline.id" placeholder="请输入预约ID" style="width: 130px" clearable/></el-form-item><el-form-item label="课程ID" prop="courseId"><el-input v-model="formInline.courseId" placeholder="请输入课程ID" style="width: 130px" clearable/></el-form-item><el-form-item label="学员ID" prop="memberId"><el-input v-model="formInline.memberId" placeholder="请输入学员ID" style="width: 130px" clearable/></el-form-item><el-form-item label="预约状态" prop="status" style="width: 160px"><el-select v-model="formInline.status" clearable><el-option label="全部" value=""/><el-option label="已预约" :value="1"/><el-option label="已取消" :value="0"/></el-select></el-form-item><el-form-item label="预约时间"><el-date-pickerv-model="formInline.reserveTimeRange"type="daterange"range-separator="至"start-placeholder="开始日期"end-placeholder="结束日期"value-format="YYYY-MM-DD"/></el-form-item></el-form></div><!-- 2.按钮区--><div><div class="mb-4"><el-button type="danger" round @click="batchCancel">批量取消</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="60" align="center"/><el-table-column prop="memberId" label="学员ID" width="120" align="center"/><el-table-column prop="courseName" label="课程名称" width="120" align="center"/><el-table-column prop="coachName" label="教练" width="120" align="center"/><el-table-column prop="room" label="课程教室" width="120" align="center"/><el-table-column prop="length" label="课程时长" width="180" align="center"/><el-table-column prop="currentCount" label="预约人数" width="180" align="center"/><el-table-column prop="maxCount" label="当前课程允许的最大人数" width="180" align="center"/><el-table-column prop="reserveTime" label="预约时间" width="180" align="center"/><el-table-column prop="status" label="状态" width="120" align="center"><template #default="scope"><el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">{{ scope.row.status === 1 ? '已预约' : '已取消' }}</el-tag></template></el-table-column><el-table-column prop="score" label="评分" width="120" align="center"><template #default="scope"><span v-if="scope.row.score">{{ scope.row.score }}分</span><span v-else>-</span></template></el-table-column><el-table-column prop="commentTime" label="评价时间" width="180" align="center"/><el-table-column prop="comment" label="评价内容" show-overflow-tooltip/><el-table-column label="操作" width="180" fixed="right" align="center"><template #default="scope"><el-button type="warning" size="small" @click.stop="cancelReserve(scope.row)" v-if="scope.row.status === 1">取消预约</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="[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>
</template><script setup>
import {reactive, ref, onMounted, toRaw} from 'vue'
import api from "@/utils/api.js";
import {ElMessage, ElMessageBox} from 'element-plus'// 查询表单对象
let formInline = ref({id: null,courseId: null,memberId: null,status: null,reserveTimeRange: []
});// 表格数据对象
let tableData = ref([]);// 分页配置
let memberPi = reactive({pageNo: 1,pageSize: 15,total: 0
});// 查询预约方法
async function select(pageNo = 1, pageSize = 10) {let params = toRaw(formInline.value);// 处理时间范围if (params.reserveTimeRange && params.reserveTimeRange.length === 2) {params.startTime = params.reserveTimeRange[0];params.endTime = params.reserveTimeRange[1];delete params.reserveTimeRange;}try {const resp = await api({url: "/reserves",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,courseId: null,memberId: null,status: null,reserveTimeRange: []};
}// 表格操作
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);removeByIds(ids);}).catch(() => {});}
}async function removeByIds(ids) {let resp = await api({url: "/reserves",method: "delete",data: ids,headers: {'Content-Type': 'application/json;charset=utf-8' // 明确指定编码}});if (resp.success) {ElMessage.success(`删除操作成功,共删除${resp.data}条`);select();} else {ElMessage.error("删除失败,请稍候再试或联系管理员");}
}// 批量取消预约
function batchCancel() {let rows = tableRef.value.getSelectionRows();if (rows.length === 0) {ElMessage.warning("请选中您要取消的预约");} else {const toCancel = rows.filter(row => row.status === 1);if (toCancel.length === 0) {ElMessage.warning("选中的预约中没有可取消的状态");return;}ElMessageBox.confirm(`确定要取消选中的${toCancel.length}条预约记录吗?`, "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(() => {let ids = toCancel.map(it => it.id);cancelReserves(ids);}).catch(() => {});}
}// 取消预约
async function cancelReserves(ids) {try {const resp = await api({url: "/reserves/cancel",method: "put",data: ids});if (resp.success) {ElMessage.success(`成功取消${resp.data}条预约`);select();} else {ElMessage.error("取消预约失败");}} catch (error) {console.error("取消预约失败:", error);ElMessage.error("取消预约失败,请稍候再试");}
}// 单行取消预约
function cancelReserve(row) {if (row.status !== 1) {ElMessage.warning("该预约状态不可取消");return;}ElMessageBox.confirm(`确定要取消会员${row.memberId}的预约吗?`, "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(() => {cancelReserves([row.id]);}).catch(() => {});
}// 单行删除
const deleteRow = (row) => {ElMessageBox.confirm(`是否确认删除预约记录 ${row.id}?`, "警告", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(() => {removeByIds([row.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>
实现效果:

-多表联查梳理:







2.取消预约
步骤梳理
(1)添加取消预约按钮
(2)前端Js关联事件,提供发送请求的路径
(3)后端根据接口完成响应数据:所谓的取消预约就是去数据库改预约状态。
也就是所谓的业务其实就是对数据库的增删改查 :
前端汇总 :
<template><meta charset="UTF-8"><!-- 1.查询条件区域 --><div class="page-container"><el-form :inline="true" :model="formInline"><el-form-item label="预约ID" prop="id"><el-input v-model="formInline.id" placeholder="请输入预约ID" style="width: 130px" clearable/></el-form-item><el-form-item label="课程ID" prop="courseId"><el-input v-model="formInline.courseId" placeholder="请输入课程ID" style="width: 130px" clearable/></el-form-item><el-form-item label="学员ID" prop="memberId"><el-input v-model="formInline.memberId" placeholder="请输入学员ID" style="width: 130px" clearable/></el-form-item><el-form-item label="预约状态" prop="status" style="width: 160px"><el-select v-model="formInline.status" clearable><el-option label="全部" value=""/><el-option label="已预约" :value="1"/><el-option label="已取消" :value="0"/></el-select></el-form-item><el-form-item label="预约时间"><el-date-pickerv-model="formInline.reserveTimeRange"type="daterange"range-separator="至"start-placeholder="开始日期"end-placeholder="结束日期"value-format="YYYY-MM-DD"/></el-form-item></el-form></div><!-- 2.按钮区--><div><div class="mb-4"><el-button type="danger" round @click="batchCancel">批量取消</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="60" align="center"/><el-table-column prop="memberId" label="学员ID" width="120" align="center"/><el-table-column prop="courseName" label="课程名称" width="120" align="center" fixed/><el-table-column prop="coachName" label="教练" width="120" align="center"/><el-table-column prop="room" label="课程教室" width="120" align="center"/><el-table-column prop="length" label="课程时长" width="180" align="center"/><el-table-column prop="currentCount" label="预约人数" width="180" align="center"/><el-table-column prop="maxCount" label="当前课程允许的最大人数" width="180" align="center"/><el-table-column prop="reserveTime" label="预约时间" width="180" align="center"/><el-table-column prop="status" label="状态" width="120" align="center"><template #default="scope"><el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">{{ scope.row.status === 1 ? '已预约' : '已取消' }}</el-tag></template></el-table-column><el-table-column prop="score" label="评分" width="120" align="center"><template #default="scope"><span v-if="scope.row.score">{{ scope.row.score }}分</span><span v-else>-</span></template></el-table-column><el-table-column prop="commentTime" label="评价时间" width="180" align="center"/><el-table-column prop="comment" label="评价内容" show-overflow-tooltip/><el-table-column label="操作" width="240" fixed="right" align="center"><template #default="scope"><!-- 新增的学员按钮 --><el-buttontype="danger"size="small"@click.stop="showMember(scope.row)">该课程全部学员</el-button><el-button:loading="scope.row.loading"type="warning"size="small"@click.stop="scope.row.status === 1 ? cancelReserve(scope.row) : reReserve(scope.row)">{{ scope.row.status === 1 ? '取消预约' : '重新预约' }}</el-button></template></el-table-column></el-table><el-dialogv-model="memberDialogVisible":title="`课程「${currentCourse}」的预约学员`"width="80%"><!-- 顶部操作按钮区 --><div class="dialog-toolbar"><el-button type="primary" @click="addReservation">添加预约</el-button><el-button type="success" @click="batchCheckIn">批量签到</el-button><el-button type="warning" @click="batchNoShow">批量未到</el-button><el-button type="info" @click="reReserve">重新预约</el-button><el-button type="danger" @click="batchCancelReservation">取消预约</el-button><!-- 搜索框 --><el-inputv-model="memberSearch"placeholder="输入会员ID/姓名/电话搜索"style="width: 300px; margin-left: 20px"clearable@clear="handleMemberSearch"@keyup.enter="handleMemberSearch"><template #append><el-button icon="el-icon-search" @click="handleMemberSearch" /></template></el-input></div><!-- 学员表格 --><el-table:data="filteredMemberList"borderstyle="width: 100%; margin-top: 15px"@selection-change="handleMemberSelectionChange"><el-table-column type="selection" width="55" align="center"/><el-table-column prop="memberId" label="会员ID" width="120" align="center"/><el-table-column prop="memberName" label="姓名" width="120" align="center"/><el-table-column prop="phone" label="电话" width="180" align="center"/><el-table-column prop="reserveTime" label="预约时间" width="180" align="center"><template #default="{row}">{{ formatTime(row.reserveTime) }}</template></el-table-column><el-table-column label="状态" width="120" align="center"><template #default="{row}"><el-tag :type="getStatusTagType(row.status)">{{ getStatusText(row.status) }}</el-tag></template></el-table-column><el-table-column label="操作" width="220" fixed="right" align="center"><template #default="{row}"><el-buttonsize="small"type="success"@click="checkIn(row)":disabled="row.status !== 1">签到</el-button><el-buttonsize="small"type="warning"@click="noShow(row)":disabled="row.status !== 1">未到</el-button><el-buttonsize="small"type="danger"@click="cancelSingleReservation(row)":disabled="row.status !== 1">取消</el-button></template></el-table-column></el-table><!-- 分页 --><div class="pagination" style="margin-top: 15px"><el-paginationv-model:current-page="memberPage.current"v-model:page-size="memberPage.size":page-sizes="[10, 20, 50]"layout="total, sizes, prev, pager, next, jumper":total="memberPage.total"@current-change="handleMemberPageChange"@size-change="handleMemberSizeChange"/></div></el-dialog><!-- 4.分页条--><div class="pagination"><el-paginationv-model:current-page="memberPi.pageNo"v-model:page-size="memberPi.pageSize":page-sizes="[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></div>
</template><script setup>
import {reactive, ref, onMounted, toRaw} from 'vue'
import api from "@/utils/api.js";
import dayjs from 'dayjs';
import {ElMessage, ElMessageBox} from 'element-plus'// 查询表单对象
let formInline = ref({id: null,courseId: null,memberId: null,status: null,reserveTimeRange: []
});// 表格数据对象
let tableData = ref([]);// 分页配置
let memberPi = reactive({pageNo: 1,pageSize: 15,total: 0
});// 查询预约方法
async function select(pageNo = 1, pageSize = 10) {let params = toRaw(formInline.value);// 处理时间范围if (params.reserveTimeRange && params.reserveTimeRange.length === 2) {params.startTime = params.reserveTimeRange[0];params.endTime = params.reserveTimeRange[1];delete params.reserveTimeRange;}try {const resp = await api({url: "/reserves",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,courseId: null,memberId: null,status: null,reserveTimeRange: []};
}// 表格操作
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);removeByIds(ids);}).catch(() => {});}
}async function removeByIds(ids) {let resp = await api({url: "/reserves",method: "delete",data: ids,headers: {'Content-Type': 'application/json;charset=utf-8' // 明确指定编码}});if (resp.success) {ElMessage.success(`删除操作成功,共删除${resp.data}条`);select();} else {ElMessage.error("删除失败,请稍候再试或联系管理员");}
}// 批量取消预约
function batchCancel() {let rows = tableRef.value.getSelectionRows();if (rows.length === 0) {ElMessage.warning("请选中您要取消的预约");} else {const toCancel = rows.filter(row => row.status === 1);if (toCancel.length === 0) {ElMessage.warning("选中的预约中没有可取消的状态");return;}ElMessageBox.confirm(`确定要取消选中的${toCancel.length}条预约记录吗?`, "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(() => {let ids = toCancel.map(it => it.id);cancelReserves(ids);}).catch(() => {});}
}// 取消预约
async function cancelReserves(ids) {try {const resp = await api({url: "/reserves/cancel",method: "put",data: ids});if (resp.success) {ElMessage.success(`成功取消${resp.data}条预约`);select();} else {ElMessage.error("取消预约失败");}} catch (error) {console.error("取消预约失败:", error);ElMessage.error("取消预约失败,请稍候再试");}
}// 单行取消预约
function cancelReserve(row) {if (row.status !== 1) {ElMessage.warning("该预约状态不可取消");return;}ElMessageBox.confirm(`确定要取消会员${row.memberId}的预约吗?`, "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(() => {cancelReserves([row.id]);}).catch(() => {});
}// 单行删除
const deleteRow = (row) => {ElMessageBox.confirm(`是否确认删除预约记录 ${row.id}?`, "警告", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(() => {removeByIds([row.id]);}).catch(() => {});
}// 弹窗相关数据
const memberDialogVisible = ref(false);
const memberList = ref([]);
const filteredMemberList = ref([]);
const currentCourse = ref('');
const currentCourseId = ref(null);
const memberSearch = ref('');
const selectedMembers = ref([]);
const memberPage = reactive({current: 1,size: 10,total: 0
});// 状态映射
const statusMap = {0: { text: '已取消', type: 'danger' },1: { text: '已预约', type: 'primary' },2: { text: '已签到', type: 'success' },3: { text: '未到', type: 'warning' }
};// 显示该课程的所有学员
async function showMember(row) {try {currentCourse.value = row.courseName;currentCourseId.value = row.courseId;await fetchCourseMembers();memberDialogVisible.value = true;} catch (error) {console.error("获取学员列表失败:", error);ElMessage.error("获取学员列表失败");}
}// 获取课程学员
async function fetchCourseMembers() {const resp = await api({url: `/reserves/course/${currentCourseId.value}/members`,method: "get",params: {page: memberPage.current,size: memberPage.size,keyword: memberSearch.value}});memberList.value = resp.data.records;filteredMemberList.value = resp.data.records;memberPage.total = resp.data.total;
}// 处理会员选择
function handleMemberSelectionChange(selection) {selectedMembers.value = selection.map(item => item.memberId);
}// 搜索会员
function handleMemberSearch() {memberPage.current = 1;fetchCourseMembers();
}// 分页变化
function handleMemberPageChange(page) {memberPage.current = page;fetchCourseMembers();
}function handleMemberSizeChange(size) {memberPage.size = size;fetchCourseMembers();
}// 状态显示
function getStatusTagType(status) {return statusMap[status]?.type || 'info';
}function getStatusText(status) {return statusMap[status]?.text || '未知状态';
}// 操作按钮方法
async function addReservation() {// 实现添加预约逻辑
}async function batchCheckIn() {if (selectedMembers.value.length === 0) {ElMessage.warning('请至少选择一位会员');return;}try {await api({url: '/reserves/batch-check-in',method: 'post',data: {courseId: currentCourseId.value,memberIds: selectedMembers.value}});ElMessage.success('批量签到成功');await fetchCourseMembers();} catch (error) {ElMessage.error('批量签到失败');}
}async function batchNoShow() {// 类似batchCheckIn实现
}// 重新预约方法
async function reReserve(row) {try {ElMessageBox.confirm(`确定要重新预约该课程吗?`, "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(async () => {const resp = await api({url: `/reserves/${row.id}/re-reserve`,method: "put"});if (resp.success) {ElMessage.success("重新预约成功");// 更新状态为已预约(1)row.status = 1;// 刷新表格数据select();} else {ElMessage.error("重新预约失败");}}).catch(() => {});} catch (error) {console.error("重新预约失败:", error);ElMessage.error("重新预约失败,请稍候再试");}
}
async function batchCancelReservation() {// 实现批量取消预约逻辑
}async function checkIn(row) {try {await api({url: `/reserves/${row.id}/check-in`,method: 'post'});ElMessage.success('签到成功');await fetchCourseMembers();} catch (error) {ElMessage.error('签到失败');}
}async function noShow(row) {// 类似checkIn实现
}async function cancelSingleReservation(row) {// 类似checkIn实现
}// 时间格式化
function formatTime(time) {return dayjs(time).format('YYYY-MM-DD HH:mm');
}// 组件挂载时加载数据
onMounted(() => {select();
});
</script><style>
.data-grid {margin-top: 6px;
}.pagination {margin-top: 20px;display: flex;justify-content: center;
}.member-pi {margin-top: 6px;
}
.dialog-toolbar {display: flex;align-items: center;margin-bottom: 15px;
}.pagination {margin-top: 15px;display: flex;justify-content: flex-end;
}
</style>

后端:
Controller:
//取消预约:根据ID批量取消,前端接口部分也是根据ids删除:@PutMapping("/cancel")public ResponseEntity<JsonResult<?>> cancel(@RequestBody @Validated List<Integer> ids){int count = reserveService.cancel(ids);if(count!=0){return ResponseEntity.ok(JsonResult.success(count));}else {return ResponseEntity.ok(JsonResult.fail("取消预约失败"));}}
Servicec层:
//取消预约:通过ids实现批量取消预约.mybatis默认返回的是受影响的行数,所以用int类型接收int cancel(List<Integer> ids);
//批量取消预约@Overridepublic int cancel(List<Integer> ids) {return reserveMapper.cancel(ids);}
Mapper层 :
//批量取消:其实就是设置预约表中的状态是0(status=1:预约成功已经预约,status=0就是代表预约失败)//mybatis关于增删改,默认返回的都是受影响的行数,所以用int类型接收int cancel(@Param("ids") List<Integer> ids);
<!-- 取消预约:传递过来的是数组,不是单个值 :默认返回受影响的行数--><update id="cancel">UPDATE reserve SET status = 0WHERE id IN<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach></update>
出现的问题以及解决方案 注意事项:
出现的问题 :
PUT http://localhost:3000/api/reserves/cancel 500 (Internal Server Error)是这样的### Error updating database. Cause: org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [ids, param1]
### The error may exist in com/study/mapper/ReserveMapper.xml
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: UPDATE reserve SET status = 0 WHERE id IN ( ? , ? )
### Cause: org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [ids, param1]] with root causeorg.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [ids, param1]at org.apache.ibatis.binding.MapperMethod$ParamMap.get(MapperMethod.java:210) ~[mybatis-3.5.19.jar:3.5.19]at org.apache.ibatis.reflection.wrapper.MapWrapper.get(MapWrapper.java:46) ~[mybatis-3.5.19.jar:3.5.19]at org.apache.ibatis.reflection.MetaObject.getValue(MetaObject.java:115) ~[mybatis-3.5.19.jar:3.5.19]



<update id="cancel">UPDATE reserve SET status = 0WHERE id IN<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
</update>

3.重新预约
步骤梳理:
(1)添加重新预约按钮
(2)前端Js关联事件,提供发送请求的路径
(3)后端根据接口完成响应数据:所谓的重新预约就是去数据库改预约状态。
也就是所谓的业务其实就是对数据库的增删改查 :
前端汇总 :
<template><meta charset="UTF-8"><!-- 1.查询条件区域 --><div class="page-container"><el-form :inline="true" :model="formInline"><el-form-item label="预约ID" prop="id"><el-input v-model="formInline.id" placeholder="请输入预约ID" style="width: 130px" clearable/></el-form-item><el-form-item label="课程ID" prop="courseId"><el-input v-model="formInline.courseId" placeholder="请输入课程ID" style="width: 130px" clearable/></el-form-item><el-form-item label="学员ID" prop="memberId"><el-input v-model="formInline.memberId" placeholder="请输入学员ID" style="width: 130px" clearable/></el-form-item><el-form-item label="预约状态" prop="status" style="width: 160px"><el-select v-model="formInline.status" clearable><el-option label="全部" value=""/><el-option label="已预约" :value="1"/><el-option label="已取消" :value="0"/></el-select></el-form-item><el-form-item label="预约时间"><el-date-pickerv-model="formInline.reserveTimeRange"type="daterange"range-separator="至"start-placeholder="开始日期"end-placeholder="结束日期"value-format="YYYY-MM-DD"/></el-form-item></el-form></div><!-- 2.按钮区--><div><div class="mb-4"><el-button type="danger" round @click="batchCancel">批量取消</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="60" align="center"/><el-table-column prop="memberId" label="学员ID" width="120" align="center"/><el-table-column prop="courseName" label="课程名称" width="120" align="center" fixed/><el-table-column prop="coachName" label="教练" width="120" align="center"/><el-table-column prop="room" label="课程教室" width="120" align="center"/><el-table-column prop="length" label="课程时长" width="180" align="center"/><el-table-column prop="currentCount" label="预约人数" width="180" align="center"/><el-table-column prop="maxCount" label="当前课程允许的最大人数" width="180" align="center"/><el-table-column prop="reserveTime" label="预约时间" width="180" align="center"/><el-table-column prop="status" label="状态" width="120" align="center"><template #default="scope"><el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">{{ scope.row.status === 1 ? '已预约' : '已取消' }}</el-tag></template></el-table-column><el-table-column prop="score" label="评分" width="120" align="center"><template #default="scope"><span v-if="scope.row.score">{{ scope.row.score }}分</span><span v-else>-</span></template></el-table-column><el-table-column prop="commentTime" label="评价时间" width="180" align="center"/><el-table-column prop="comment" label="评价内容" show-overflow-tooltip/><el-table-column label="操作" width="240" fixed="right" align="center"><template #default="scope"><!-- 新增的学员按钮 --><el-buttontype="danger"size="small"@click.stop="showMember(scope.row)">该课程全部学员</el-button><el-button:loading="scope.row.loading"type="warning"size="small"@click.stop="scope.row.status === 1 ? cancelReserve(scope.row) : reReserve(scope.row)">{{ scope.row.status === 1 ? '取消预约' : '重新预约' }}</el-button></template></el-table-column></el-table><el-dialogv-model="memberDialogVisible":title="`课程「${currentCourse}」的预约学员`"width="80%"><!-- 顶部操作按钮区 --><div class="dialog-toolbar"><el-button type="primary" @click="addReservation">添加预约</el-button><el-button type="success" @click="batchCheckIn">批量签到</el-button><el-button type="warning" @click="batchNoShow">批量未到</el-button><el-button type="info" @click="reReserve">重新预约</el-button><el-button type="danger" @click="batchCancelReservation">取消预约</el-button><!-- 搜索框 --><el-inputv-model="memberSearch"placeholder="输入会员ID/姓名/电话搜索"style="width: 300px; margin-left: 20px"clearable@clear="handleMemberSearch"@keyup.enter="handleMemberSearch"><template #append><el-button icon="el-icon-search" @click="handleMemberSearch" /></template></el-input></div><!-- 学员表格 --><el-table:data="filteredMemberList"borderstyle="width: 100%; margin-top: 15px"@selection-change="handleMemberSelectionChange"><el-table-column type="selection" width="55" align="center"/><el-table-column prop="memberId" label="会员ID" width="120" align="center"/><el-table-column prop="memberName" label="姓名" width="120" align="center"/><el-table-column prop="phone" label="电话" width="180" align="center"/><el-table-column prop="reserveTime" label="预约时间" width="180" align="center"><template #default="{row}">{{ formatTime(row.reserveTime) }}</template></el-table-column><el-table-column label="状态" width="120" align="center"><template #default="{row}"><el-tag :type="getStatusTagType(row.status)">{{ getStatusText(row.status) }}</el-tag></template></el-table-column><el-table-column label="操作" width="220" fixed="right" align="center"><template #default="{row}"><el-buttonsize="small"type="success"@click="checkIn(row)":disabled="row.status !== 1">签到</el-button><el-buttonsize="small"type="warning"@click="noShow(row)":disabled="row.status !== 1">未到</el-button><el-buttonsize="small"type="danger"@click="cancelSingleReservation(row)":disabled="row.status !== 1">取消</el-button></template></el-table-column></el-table><!-- 分页 --><div class="pagination" style="margin-top: 15px"><el-paginationv-model:current-page="memberPage.current"v-model:page-size="memberPage.size":page-sizes="[10, 20, 50]"layout="total, sizes, prev, pager, next, jumper":total="memberPage.total"@current-change="handleMemberPageChange"@size-change="handleMemberSizeChange"/></div></el-dialog><!-- 4.分页条--><div class="pagination"><el-paginationv-model:current-page="memberPi.pageNo"v-model:page-size="memberPi.pageSize":page-sizes="[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></div>
</template><script setup>
import {reactive, ref, onMounted, toRaw} from 'vue'
import api from "@/utils/api.js";
import dayjs from 'dayjs';
import {ElMessage, ElMessageBox} from 'element-plus'// 查询表单对象
let formInline = ref({id: null,courseId: null,memberId: null,status: null,reserveTimeRange: []
});// 表格数据对象
let tableData = ref([]);// 分页配置
let memberPi = reactive({pageNo: 1,pageSize: 15,total: 0
});// 查询预约方法
async function select(pageNo = 1, pageSize = 10) {let params = toRaw(formInline.value);// 处理时间范围if (params.reserveTimeRange && params.reserveTimeRange.length === 2) {params.startTime = params.reserveTimeRange[0];params.endTime = params.reserveTimeRange[1];delete params.reserveTimeRange;}try {const resp = await api({url: "/reserves",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,courseId: null,memberId: null,status: null,reserveTimeRange: []};
}// 表格操作
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);removeByIds(ids);}).catch(() => {});}
}async function removeByIds(ids) {let resp = await api({url: "/reserves",method: "delete",data: ids,headers: {'Content-Type': 'application/json;charset=utf-8' // 明确指定编码}});if (resp.success) {ElMessage.success(`删除操作成功,共删除${resp.data}条`);select();} else {ElMessage.error("删除失败,请稍候再试或联系管理员");}
}// 批量取消预约
function batchCancel() {let rows = tableRef.value.getSelectionRows();if (rows.length === 0) {ElMessage.warning("请选中您要取消的预约");} else {const toCancel = rows.filter(row => row.status === 1);if (toCancel.length === 0) {ElMessage.warning("选中的预约中没有可取消的状态");return;}ElMessageBox.confirm(`确定要取消选中的${toCancel.length}条预约记录吗?`, "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(() => {let ids = toCancel.map(it => it.id);cancelReserves(ids);}).catch(() => {});}
}// 取消预约
async function cancelReserves(ids) {try {const resp = await api({url: "/reserves/cancel",method: "put",data: ids});if (resp.success) {ElMessage.success(`成功取消${resp.data}条预约`);select();} else {ElMessage.error("取消预约失败");}} catch (error) {console.error("取消预约失败:", error);ElMessage.error("取消预约失败,请稍候再试");}
}// 单行取消预约
function cancelReserve(row) {if (row.status !== 1) {ElMessage.warning("该预约状态不可取消");return;}ElMessageBox.confirm(`确定要取消会员${row.memberId}的预约吗?`, "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(() => {cancelReserves([row.id]);}).catch(() => {});
}// 单行删除
const deleteRow = (row) => {ElMessageBox.confirm(`是否确认删除预约记录 ${row.id}?`, "警告", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(() => {removeByIds([row.id]);}).catch(() => {});
}// 弹窗相关数据
const memberDialogVisible = ref(false);
const memberList = ref([]);
const filteredMemberList = ref([]);
const currentCourse = ref('');
const currentCourseId = ref(null);
const memberSearch = ref('');
const selectedMembers = ref([]);
const memberPage = reactive({current: 1,size: 10,total: 0
});// 状态映射
const statusMap = {0: { text: '已取消', type: 'danger' },1: { text: '已预约', type: 'primary' },2: { text: '已签到', type: 'success' },3: { text: '未到', type: 'warning' }
};// 显示该课程的所有学员
async function showMember(row) {try {currentCourse.value = row.courseName;currentCourseId.value = row.courseId;await fetchCourseMembers();memberDialogVisible.value = true;} catch (error) {console.error("获取学员列表失败:", error);ElMessage.error("获取学员列表失败");}
}// 获取课程学员
async function fetchCourseMembers() {const resp = await api({url: `/reserves/course/${currentCourseId.value}/members`,method: "get",params: {page: memberPage.current,size: memberPage.size,keyword: memberSearch.value}});memberList.value = resp.data.records;filteredMemberList.value = resp.data.records;memberPage.total = resp.data.total;
}// 处理会员选择
function handleMemberSelectionChange(selection) {selectedMembers.value = selection.map(item => item.memberId);
}// 搜索会员
function handleMemberSearch() {memberPage.current = 1;fetchCourseMembers();
}// 分页变化
function handleMemberPageChange(page) {memberPage.current = page;fetchCourseMembers();
}function handleMemberSizeChange(size) {memberPage.size = size;fetchCourseMembers();
}// 状态显示
function getStatusTagType(status) {return statusMap[status]?.type || 'info';
}function getStatusText(status) {return statusMap[status]?.text || '未知状态';
}// 操作按钮方法
async function addReservation() {// 实现添加预约逻辑
}async function batchCheckIn() {if (selectedMembers.value.length === 0) {ElMessage.warning('请至少选择一位会员');return;}try {await api({url: '/reserves/batch-check-in',method: 'post',data: {courseId: currentCourseId.value,memberIds: selectedMembers.value}});ElMessage.success('批量签到成功');await fetchCourseMembers();} catch (error) {ElMessage.error('批量签到失败');}
}async function batchNoShow() {// 类似batchCheckIn实现
}// 重新预约方法
async function reReserve(row) {try {await ElMessageBox.confirm(`确定要重新预约该课程吗?`, "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"});const resp = await api({url: "/reserves/re-reserve", // 补全路径前缀method: "put",data: [row.id], // 封装为数组,符合后端 List<Integer> 要求headers: {'Content-Type': 'application/json' // 明确指定 JSON 格式}});if (resp.success) {ElMessage.success("重新预约成功");row.status = 1; // 更新前端状态await select(); // 刷新表格} else {ElMessage.error(resp.message || "操作失败");}} catch (error) {if (error !== "cancel") { // 忽略用户点击取消的情况console.error("重新预约失败:", error);ElMessage.error("请求失败,请检查数据或联系管理员");}}
}async function batchCancelReservation() {// 实现批量取消预约逻辑
}async function checkIn(row) {try {await api({url: `/reserves/${row.id}/check-in`,method: 'post'});ElMessage.success('签到成功');await fetchCourseMembers();} catch (error) {ElMessage.error('签到失败');}
}async function noShow(row) {// 类似checkIn实现
}async function cancelSingleReservation(row) {// 类似checkIn实现
}// 时间格式化
function formatTime(time) {return dayjs(time).format('YYYY-MM-DD HH:mm');
}// 组件挂载时加载数据
onMounted(() => {select();
});
</script><style>
.data-grid {margin-top: 6px;
}.pagination {margin-top: 20px;display: flex;justify-content: center;
}.member-pi {margin-top: 6px;
}
.dialog-toolbar {display: flex;align-items: center;margin-bottom: 15px;
}.pagination {margin-top: 15px;display: flex;justify-content: flex-end;
}
</style>

后端实现:就是去数据库改u,设置状态status为1(预约成功)
Controller:
//重新预约 :@PutMapping("/re-reserve")public ResponseEntity<JsonResult<?>> reserveRe (@RequestBody @Validated List<Integer> ids){int count = reserveService.reserve(ids);if(count!=0){return ResponseEntity.ok(JsonResult.success(count));}else {return ResponseEntity.ok(JsonResult.fail("重新预约失败"));}}
Service层:
//重新预约:int reserve(List<Integer> ids);
//重新预约@Overridepublic int reserve(List<Integer> ids) {return reserveMapper.reserve(ids);}
Mapper层 :
//重新预约:增删改都是返回的受影响的行数:int reserve(List<Integer> ids);
<!-- 重新预约:成功预约:1--><update id="reserve">UPDATE reserve SET status = 1WHERE id IN<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach></update>


其他模块同理
