健身房预约系统SSM+Mybatis(五、预约展示)

预约页面的展示

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>


其他模块同理

相关推荐
晴殇i4 小时前
DOM嵌套关系全解析:前端必备的4大判断方法与性能优化实战
前端·javascript·面试
北城以北88884 小时前
SSM--MyBatis框架之动态SQL
java·开发语言·数据库·sql·mybatis
似水流年_zyh4 小时前
canvas涂抹,擦除功能组件
前端
胖虎2654 小时前
前端多文件上传核心功能实现:格式支持、批量上传与状态可视化
前端
胖虎2654 小时前
Vue2 项目常用配置合集:多语言、SVG 图标、代码格式化、权限指令 + 主题切换
前端
一键定乾坤4 小时前
npm 源修改
前端
parade岁月4 小时前
Vue 3 响应式陷阱:对象引用丢失导致的数据更新失效
前端
掘金安东尼4 小时前
GPT-6 会带来科学革命?奥特曼最新设想:AI CEO、便宜医疗与全新计算机
前端·vue.js·github
申阳5 小时前
Day 5:03. 基于Nuxt开发博客项目-页面结构组织
前端·后端·程序员