预约页面的展示
html
<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-picker
v-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()" stripe
border 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-pagination
v-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) 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);
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>
配置路由:
js
//定义路由转发器
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;//把路由转发器导出
页面跳转 :
js
<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-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",
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

多表联合查询:查询课程表中的课程名称、时长、开课日期,查询教练表中的教练名字,查询全部预约表中的信息,以及计算同一课程的预约人数 。
我们将上述全部的字段可以封装成一个实体 ,这样方便我们查询后映射 字段赋值 :
java
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 预约表,也有预约表里面的属性了 。
然后编写方法接口,在映射文件里面提供具体实现。
java
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);
}
具体的映射文件实现:
java
<?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">
SELECT
r.*,
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_count
FROM
reserve r
LEFT JOIN
course c ON r.course_id = c.id
LEFT JOIN
coach ch ON c.coach_id = ch.id
${ew.customSqlSegment}
</select>
</mapper>
在前端将要显示的内容绑定prop后端的属性:
html
<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"/>
至此前端的预约页面全部的代码实现 :
html
<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-picker
v-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()" stripe
border 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-pagination
v-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) 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);
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)后端根据接口完成响应数据:所谓的取消预约就是去数据库改预约状态。
也就是所谓的业务其实就是对数据库的增删改查 :
前端汇总 :
html
<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-picker
v-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()" stripe
border 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-button
type="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-dialog
v-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-input
v-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"
border
style="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-button
size="small"
type="success"
@click="checkIn(row)"
:disabled="row.status !== 1"
>
签到
</el-button>
<el-button
size="small"
type="warning"
@click="noShow(row)"
:disabled="row.status !== 1"
>
未到
</el-button>
<el-button
size="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-pagination
v-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-pagination
v-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) 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);
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:
java
//取消预约:根据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层:
java
//取消预约:通过ids实现批量取消预约.mybatis默认返回的是受影响的行数,所以用int类型接收
int cancel(List<Integer> ids);
java
//批量取消预约
@Override
public int cancel(List<Integer> ids) {
return reserveMapper.cancel(ids);
}
Mapper层 :
java
//批量取消:其实就是设置预约表中的状态是0(status=1:预约成功已经预约,status=0就是代表预约失败)
//mybatis关于增删改,默认返回的都是受影响的行数,所以用int类型接收
int cancel(@Param("ids") List<Integer> ids);
java
<!-- 取消预约:传递过来的是数组,不是单个值 :默认返回受影响的行数-->
<update id="cancel">
UPDATE reserve SET status = 0
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
出现的问题以及解决方案 注意事项:
出现的问题 :
java
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 cause
org.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]



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

3.重新预约
步骤梳理:
(1)添加重新预约按钮
(2)前端Js关联事件,提供发送请求的路径
(3)后端根据接口完成响应数据:所谓的重新预约就是去数据库改预约状态。
也就是所谓的业务其实就是对数据库的增删改查 :
前端汇总 :
html
<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-picker
v-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()" stripe
border 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-button
type="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-dialog
v-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-input
v-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"
border
style="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-button
size="small"
type="success"
@click="checkIn(row)"
:disabled="row.status !== 1"
>
签到
</el-button>
<el-button
size="small"
type="warning"
@click="noShow(row)"
:disabled="row.status !== 1"
>
未到
</el-button>
<el-button
size="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-pagination
v-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-pagination
v-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) 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);
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:
java
//重新预约 :
@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层:
java
//重新预约:
int reserve(List<Integer> ids);
java
//重新预约
@Override
public int reserve(List<Integer> ids) {
return reserveMapper.reserve(ids);
}
Mapper层 :
java
//重新预约:增删改都是返回的受影响的行数:
int reserve(List<Integer> ids);
java
<!-- 重新预约:成功预约:1-->
<update id="reserve">
UPDATE reserve SET status = 1
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>


其他模块同理