mahjong_group/docs/1.0.0/历史需求/前端对接文档_预约系统.md
2026-01-01 14:39:23 +08:00

43 KiB
Raw Blame History

预约系统前端对接文档

📌 文档说明

版本v2.0
更新日期2025-12-06
适用范围:预约房间功能前端开发
后端接口版本:预约时段优化版


📋 目录

  1. 业务流程概述
  2. 页面结构
  3. 接口调用流程
  4. 接口详细说明
  5. 数据结构定义
  6. 前端实现指南
  7. 常见问题FAQ
  8. 调试技巧

业务流程概述

用户预约完整流程

┌─────────────────┐
│  进入预约模块    │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ 【房间展示页】   │
│ 1. 选择日期      │
│ 2. 查看房间列表  │
│ 3. 查看时段状态  │
└────────┬────────┘
         │ 点击【预约】
         ▼
┌─────────────────┐
│  【预约页】      │
│ 1. 选择时段      │
│ 2. 填写人数      │
│ 3. 设置鸽子费    │
│ 4. 填写其他信息  │
└────────┬────────┘
         │ 点击【提交】
         ▼
┌─────────────────┐
│   创建预约       │
│ - 成功:跳转列表 │
│ - 失败:显示错误 │
└─────────────────┘

核心概念

时段定义

时段类型 时段名称 时间范围 代码值
凌晨 Dawn 00:00-05:59 0
上午 Morning 06:00-11:59 1
下午 Afternoon 12:00-17:59 2
晚上 Evening 18:00-23:59 3

时段状态

状态值 含义 展示方式 可否预约
available 可预约 绿色✓
reserved 已预约 红色✗
unavailable 不可用 灰色🚫
using 使用中 蓝色🔵

页面结构

页面1房间展示页

路由/rooms/booking

页面元素

┌──────────────────────────────────────┐
│  预约房间                              │
├──────────────────────────────────────┤
│  [ 今天 ] [ 明天 ] [ 12/08 ] ...      │ ← 日期选择器
├──────────────────────────────────────┤
│  [ ] 只显示当前时段可用房间            │ ← 筛选开关
├──────────────────────────────────────┤
│  ┌────────────────────────────────┐   │
│  │ 101包间  [豪华包间]             │   │
│  │ 📷 [房间图片]                   │   │
│  │ 💰 标准价80元/时段             │   │
│  │    会员价60元/时段             │   │
│  │ 时段状态:                       │   │
│  │  凌晨✓ 上午✗ 下午✓ 晚上🔵       │   │
│  │              [ 预约 ] 按钮      │   │
│  └────────────────────────────────┘   │
│  [更多房间...]                        │
└──────────────────────────────────────┘

页面2预约页

路由/rooms/booking/create

页面元素

┌──────────────────────────────────────┐
│  创建预约                              │
├──────────────────────────────────────┤
│  预约日期2025年12月06日  (只读)      │
│  房间号101包间 (豪华包间)  (只读)    │
├──────────────────────────────────────┤
│  预约时间:                            │
│    [ 上午 ▼ ]                         │ ← 下拉选择(可预约时段)
│    时间范围06:00-11:59               │
│    价格标准价80元 | 会员价60元        │
├──────────────────────────────────────┤
│  最晚到店时间:                        │
│    [ 10:30 ]  (时间选择器)            │
├──────────────────────────────────────┤
│  人数:                                │
│    ( ) 2人  ( ) 3人  ( ) 4人          │
│    ( ) 无需组局                        │
├──────────────────────────────────────┤
│  鸽子费:                              │
│    ( ) 0元  ( ) 10元  ( ) 20元        │
│    (•) 自定义:[ 30 ] 元              │
├──────────────────────────────────────┤
│  组局名称:[ 周末开黑 ]                │
│  玩法类型:[ 德州扑克 ]                │
│  ... 其他信息 ...                     │
├──────────────────────────────────────┤
│          [ 提交预约 ] 按钮             │
└──────────────────────────────────────┘

接口调用流程

时序图

前端页面          →  后端API
   │
   │ 1. 进入房间展示页
   ├────────────────→ GET /api/SQ/GetAvailableDates
   │←───────────────  返回未来7天日期列表
   │
   │ 2. 获取房间列表(默认今天)
   ├────────────────→ GET /api/SQ/GetRoomListWithSlotsNew?date=xxx
   │←───────────────  返回房间列表及4个时段状态
   │
   │ 3. 用户切换日期
   ├────────────────→ GET /api/SQ/GetRoomListWithSlotsNew?date=xxx
   │←───────────────  返回:新日期的房间列表
   │
   │ 4. 用户点击【预约】按钮
   │   携带roomId, date, availableSlots
   │   跳转到预约页
   │
   │ 5. (可选)用户选择时段后实时校验
   ├────────────────→ POST /api/SQ/ValidateReservationBySlot
   │←───────────────  返回:是否可以预约
   │
   │ 6. 用户点击【提交预约】
   ├────────────────→ POST /api/SQ/AddSQReservationBySlot
   │←───────────────  返回:预约结果(成功/失败)
   │
   │ 7. 成功后跳转到"我的预约"列表

接口详细说明

接口1获取可选日期列表

基本信息

  • 接口路径GET /api/SQ/GetAvailableDates
  • 调用时机房间展示页初始化时调用1次
  • 是否需要登录:否

请求参数

无参数

返回数据

interface Response {
  code: number;        // 0=成功
  msg: string;         // 消息
  data: DateItem[];    // 日期列表
}

interface DateItem {
  date: number;        // Unix时间戳秒级
  dateText: string;    // 文本:今天/明天/后天/日期
  dateDisplay: string; // 展示文本12月06日 周五
}

返回示例

{
  "code": 0,
  "msg": "ok",
  "data": [
    {
      "date": 1733443200,
      "dateText": "今天",
      "dateDisplay": "12月06日 周五"
    },
    {
      "date": 1733529600,
      "dateText": "明天",
      "dateDisplay": "12月07日 周六"
    },
    {
      "date": 1733616000,
      "dateText": "后天",
      "dateDisplay": "12月08日 周日"
    },
    {
      "date": 1733702400,
      "dateText": "12月09日",
      "dateDisplay": "12月09日 周一"
    }
    // ... 共8条今天+未来7天
  ]
}

前端使用

// 1. 获取日期列表
async function loadDates() {
  const response = await axios.get('/api/SQ/GetAvailableDates');
  this.dates = response.data.data;
  this.selectedDate = this.dates[0].date; // 默认选中今天
}

// 2. 渲染日期选择器
dates.forEach(dateItem => {
  // 展示 dateText 或 dateDisplay
  // 点击后将 dateItem.date 作为参数查询房间
});

接口2获取房间列表及时段状态

基本信息

  • 接口路径GET /api/SQ/GetRoomListWithSlotsNew
  • 调用时机:初始化、切换日期时调用
  • 是否需要登录:否

请求参数

参数名 类型 必填 说明 示例
date number Unix时间戳秒级 1733443200
showOnlyAvailable boolean 是否只显示当前时段可用房间 false
currentTimeSlot number 当前时段类型(0-3)配合showOnlyAvailable 1

返回数据

interface Response {
  code: number;
  msg: string;
  data: RoomItem[];
}

interface RoomItem {
  id: number;                    // 房间ID
  name: string;                  // 房间名称
  room_type_name: string;        // 房间类型
  image_url: string;             // 房间图片
  capacity: number;              // 容纳人数
  description: string;           // 房间描述
  standard_price_desc: string;   // 标准价格说明
  member_price_desc: string;     // 会员价格说明
  time_slots: TimeSlot[];        // 4个时段信息
  status: string;                // 房间状态
  is_available: boolean;         // 是否可用
  can_reserve: boolean;          // 是否可预约
}

interface TimeSlot {
  slot_type: number;             // 时段类型0-3
  slot_name: string;             // 时段名称:凌晨/上午/下午/晚上
  status: string;                // 状态available/reserved/unavailable/using
  standard_price: number;        // 标准价格
  member_price: number;          // 会员价格
  price_desc_standard: string;   // 标准价格说明
  price_desc_member: string;     // 会员价格说明
}

返回示例

{
  "code": 0,
  "msg": "ok",
  "data": [
    {
      "id": 1,
      "name": "101包间",
      "room_type_name": "豪华包间",
      "image_url": "https://example.com/room1.jpg",
      "capacity": 8,
      "description": "宽敞舒适的豪华包间",
      "standard_price_desc": "80元/时段",
      "member_price_desc": "60元/时段",
      "time_slots": [
        {
          "slot_type": 0,
          "slot_name": "凌晨",
          "status": "available",
          "standard_price": 60.00,
          "member_price": 50.00,
          "price_desc_standard": "60元/时段",
          "price_desc_member": "50元/时段"
        },
        {
          "slot_type": 1,
          "slot_name": "上午",
          "status": "reserved",
          "standard_price": 80.00,
          "member_price": 60.00,
          "price_desc_standard": "80元/时段",
          "price_desc_member": "60元/时段"
        },
        {
          "slot_type": 2,
          "slot_name": "下午",
          "status": "available",
          "standard_price": 80.00,
          "member_price": 60.00,
          "price_desc_standard": "80元/时段",
          "price_desc_member": "60元/时段"
        },
        {
          "slot_type": 3,
          "slot_name": "晚上",
          "status": "using",
          "standard_price": 100.00,
          "member_price": 80.00,
          "price_desc_standard": "100元/时段",
          "price_desc_member": "80元/时段"
        }
      ],
      "status": "using",
      "is_available": false,
      "can_reserve": true
    }
  ]
}

前端使用

// 1. 获取房间列表
async function loadRooms(selectedDate) {
  const response = await axios.get('/api/SQ/GetRoomListWithSlotsNew', {
    params: { 
      date: selectedDate,
      showOnlyAvailable: this.filterEnabled,
      currentTimeSlot: this.getCurrentTimeSlot()
    }
  });
  this.rooms = response.data.data;
}

// 2. 获取当前时段类型
function getCurrentTimeSlot() {
  const hour = new Date().getHours();
  if (hour >= 0 && hour < 6) return 0;   // 凌晨
  if (hour >= 6 && hour < 12) return 1;  // 上午
  if (hour >= 12 && hour < 18) return 2; // 下午
  return 3;                              // 晚上
}

// 3. 渲染房间卡片
rooms.forEach(room => {
  // 显示房间信息
  // 显示价格说明
  
  // 渲染4个时段状态
  room.time_slots.forEach(slot => {
    // 根据 slot.status 显示不同颜色/图标
    const color = getStatusColor(slot.status);
    const icon = getStatusIcon(slot.status);
  });
  
  // 控制预约按钮
  if (!room.can_reserve) {
    // 按钮置灰不可点击
  }
});

// 4. 状态映射
function getStatusColor(status) {
  const colorMap = {
    'available': '#52c41a',    // 绿色
    'reserved': '#f5222d',     // 红色
    'unavailable': '#d9d9d9',  // 灰色
    'using': '#1890ff'         // 蓝色
  };
  return colorMap[status] || '#d9d9d9';
}

function getStatusIcon(status) {
  const iconMap = {
    'available': '✓',
    'reserved': '✗',
    'unavailable': '🚫',
    'using': '🔵'
  };
  return iconMap[status] || '';
}

接口3校验是否可创建预约可选

基本信息

  • 接口路径POST /api/SQ/ValidateReservationBySlot
  • 调用时机:用户选择时段后实时校验(可选)
  • 是否需要登录需要Token

请求参数

与"创建预约"接口相同,但不会真正创建预约

返回数据

interface Response {
  code: number;        // 0=可以创建500=不可以
  msg: string;         // 原因说明
  data: {
    canCreate: boolean;  // 是否可以创建
    reason: string;      // 不能创建的原因
  }
}

返回示例

// 可以创建
{
  "code": 0,
  "msg": "",
  "data": {
    "canCreate": true,
    "reason": ""
  }
}

// 不能创建
{
  "code": 500,
  "msg": "您有其它预约时间冲突",
  "data": {
    "canCreate": false,
    "reason": "您有其它预约时间冲突"
  }
}

前端使用

// 用户选择时段时实时校验
async function onSlotChange(timeSlotType) {
  const data = {
    room_id: this.roomId,
    date: this.date,
    time_slot_type: timeSlotType,
    player_count: 4,
    // ... 其他字段
  };
  
  try {
    const response = await axios.post('/api/SQ/ValidateReservationBySlot', data, {
      headers: { Authorization: `Bearer ${this.token}` }
    });
    
    if (response.data.code === 0) {
      // 可以预约,显示价格等信息
      this.showPriceInfo(timeSlotType);
    } else {
      // 不能预约,显示原因
      this.$message.warning(response.data.msg);
    }
  } catch (error) {
    console.error('校验失败', error);
  }
}

接口4创建预约核心

基本信息

  • 接口路径POST /api/SQ/AddSQReservationBySlot
  • 调用时机:用户点击【提交预约】按钮
  • 是否需要登录需要Token

请求参数

interface CreateReservationRequest {
  room_id: number;              // 必填房间ID
  date: number;                 // 必填预约日期Unix时间戳-秒)
  time_slot_type: number;       // 必填时段类型0-3
  latest_arrival_time?: number; // 可选最晚到店时间Unix时间戳-秒)
  is_solo_mode: boolean;        // 必填,是否无需组局
  player_count: number;         // 必填人数无需组局时固定为1
  deposit_fee: number;          // 必填鸽子费0-50整数
  title: string;                // 必填,组局名称
  game_type: string;            // 必填,玩法类型
  game_rule?: string;           // 可选,游戏规则
  extra_info?: string;          // 可选,其他补充
  is_smoking: number;           // 可选是否禁烟0=不限1=禁烟2=不禁烟
  gender_limit: number;         // 可选性别限制0=不限1=男2=女
  credit_limit?: number;        // 可选,最低信誉分
  min_age?: number;             // 可选,最小年龄
  max_age?: number;             // 可选最大年龄0=不限
  important_data?: string;      // 可选重要数据支付相关JSON
}

请求示例

{
  "room_id": 1,
  "date": 1733443200,
  "time_slot_type": 1,
  "latest_arrival_time": 1733461200,
  "is_solo_mode": false,
  "player_count": 4,
  "deposit_fee": 20,
  "title": "周末开黑",
  "game_type": "德州扑克",
  "game_rule": "经典玩法",
  "extra_info": "欢迎新手",
  "is_smoking": 0,
  "gender_limit": 0,
  "credit_limit": 3.5,
  "min_age": 18,
  "max_age": 0,
  "important_data": "{\"paymentId\":\"ORDER123456\"}"
}

返回数据

interface Response {
  code: number;        // 0=成功,其他=失败
  msg: string;         // 消息
  data?: {
    reservation_id: number;  // 预约ID
    start_time: string;      // 开始时间
    end_time: string;        // 结束时间
    actual_price: number;    // 实际价格
  }
}

返回示例(成功)

{
  "code": 0,
  "msg": "预约成功",
  "data": {
    "reservation_id": 123,
    "start_time": "2025-12-06 06:00:00",
    "end_time": "2025-12-06 11:59:59",
    "actual_price": 80.00
  }
}

返回示例(失败)

// 时间冲突
{
  "code": 402,
  "msg": "您有其它预约时间冲突,无法创建该预约!",
  "data": null
}

// 房间已被预约
{
  "code": 500,
  "msg": "该时间段房间已被预约",
  "data": null
}

// 鸽子费超限
{
  "code": 500,
  "msg": "鸽子费必须在0-50元之间",
  "data": null
}

前端使用

async function submitReservation() {
  // 1. 构建请求数据
  const data = {
    room_id: this.roomId,
    date: this.date,
    time_slot_type: this.selectedSlot,
    latest_arrival_time: this.convertToTimestamp(this.arrivalTime),
    is_solo_mode: this.playerCount === 1,
    player_count: this.playerCount,
    deposit_fee: this.depositFee,
    title: this.title,
    game_type: this.gameType,
    game_rule: this.gameRule,
    extra_info: this.extraInfo,
    is_smoking: this.isSmoking,
    gender_limit: this.genderLimit,
    credit_limit: this.creditLimit,
    min_age: this.minAge,
    max_age: this.maxAge,
    important_data: this.getPaymentData()
  };
  
  // 2. 参数校验
  if (!this.validate(data)) {
    return;
  }
  
  // 3. 发送请求
  try {
    const response = await axios.post('/api/SQ/AddSQReservationBySlot', data, {
      headers: { Authorization: `Bearer ${this.token}` }
    });
    
    if (response.data.code === 0) {
      // 成功
      this.$message.success('预约成功!');
      
      // 可以显示预约详情
      console.log('预约ID:', response.data.data.reservation_id);
      console.log('时间:', response.data.data.start_time, '-', response.data.data.end_time);
      console.log('价格:', response.data.data.actual_price);
      
      // 跳转到我的预约列表
      this.$router.push('/my-reservations');
    } else {
      // 失败
      this.$message.error(response.data.msg);
    }
  } catch (error) {
    this.$message.error('预约失败,请重试');
    console.error(error);
  }
}

// 参数校验
function validate(data) {
  if (!data.title) {
    this.$message.warning('请输入组局名称');
    return false;
  }
  if (!data.game_type) {
    this.$message.warning('请选择玩法类型');
    return false;
  }
  if (data.deposit_fee < 0 || data.deposit_fee > 50) {
    this.$message.warning('鸽子费必须在0-50元之间');
    return false;
  }
  if (!Number.isInteger(data.deposit_fee)) {
    this.$message.warning('鸽子费必须是整数');
    return false;
  }
  return true;
}

数据结构定义

TypeScript类型定义

// ========== 基础类型 ==========

/** 时段类型枚举 */
enum TimeSlotType {
  Dawn = 0,       // 凌晨
  Morning = 1,    // 上午
  Afternoon = 2,  // 下午
  Evening = 3     // 晚上
}

/** 时段状态枚举 */
enum TimeSlotStatus {
  Available = 'available',      // 可预约
  Reserved = 'reserved',        // 已预约
  Unavailable = 'unavailable',  // 不可用
  Using = 'using'               // 使用中
}

// ========== 日期相关 ==========

/** 日期项 */
interface DateItem {
  date: number;        // Unix时间戳
  dateText: string;    // 文本显示
  dateDisplay: string; // 完整显示
}

// ========== 房间相关 ==========

/** 时段信息 */
interface TimeSlot {
  slot_type: TimeSlotType;      // 时段类型
  slot_name: string;            // 时段名称
  status: TimeSlotStatus;       // 时段状态
  standard_price: number;       // 标准价格
  member_price: number;         // 会员价格
  price_desc_standard: string;  // 标准价格说明
  price_desc_member: string;    // 会员价格说明
}

/** 房间信息 */
interface RoomItem {
  id: number;                   // 房间ID
  name: string;                 // 房间名称
  room_type_name: string;       // 房间类型
  image_url: string;            // 房间图片
  capacity: number;             // 容纳人数
  description: string;          // 房间描述
  standard_price_desc: string;  // 标准价格说明
  member_price_desc: string;    // 会员价格说明
  time_slots: TimeSlot[];       // 时段列表
  status: string;               // 房间状态
  is_available: boolean;        // 是否可用
  can_reserve: boolean;         // 是否可预约
}

// ========== 预约相关 ==========

/** 创建预约请求 */
interface CreateReservationRequest {
  room_id: number;
  date: number;
  time_slot_type: TimeSlotType;
  latest_arrival_time?: number;
  is_solo_mode: boolean;
  player_count: number;
  deposit_fee: number;
  title: string;
  game_type: string;
  game_rule?: string;
  extra_info?: string;
  is_smoking: number;
  gender_limit: number;
  credit_limit?: number;
  min_age?: number;
  max_age?: number;
  important_data?: string;
}

/** 创建预约响应 */
interface CreateReservationResponse {
  reservation_id: number;
  start_time: string;
  end_time: string;
  actual_price: number;
}

// ========== API响应 ==========

/** 通用API响应 */
interface ApiResponse<T = any> {
  code: number;
  msg: string;
  data: T;
}

前端实现指南

Vue 3 完整示例

1. 房间展示页组件

<template>
  <div class="room-booking-page">
    <!-- 日期选择器 -->
    <div class="date-selector">
      <div 
        v-for="dateItem in dates" 
        :key="dateItem.date"
        :class="['date-item', { active: selectedDate === dateItem.date }]"
        @click="selectDate(dateItem.date)"
      >
        {{ dateItem.dateText }}
      </div>
    </div>
    
    <!-- 筛选开关 -->
    <div class="filter-toggle" v-if="isToday">
      <label>
        <input type="checkbox" v-model="showOnlyAvailable" @change="loadRooms">
        只显示当前时段可用房间
      </label>
    </div>
    
    <!-- 房间列表 -->
    <div class="room-list" v-loading="loading">
      <div 
        v-for="room in rooms" 
        :key="room.id"
        class="room-card"
      >
        <!-- 房间图片 -->
        <img :src="room.image_url" :alt="room.name" class="room-image">
        
        <!-- 房间信息 -->
        <div class="room-info">
          <h3>{{ room.name }} <span class="room-type">{{ room.room_type_name }}</span></h3>
          <p class="room-desc">{{ room.description }}</p>
          <div class="price-info">
            <span>标准价:{{ room.standard_price_desc }}</span>
            <span>会员价:{{ room.member_price_desc }}</span>
          </div>
        </div>
        
        <!-- 时段状态 -->
        <div class="time-slots">
          <div 
            v-for="slot in room.time_slots" 
            :key="slot.slot_type"
            :class="['slot-item', slot.status]"
          >
            <span class="slot-name">{{ slot.slot_name }}</span>
            <span class="slot-icon">{{ getStatusIcon(slot.status) }}</span>
          </div>
        </div>
        
        <!-- 预约按钮 -->
        <button 
          class="reserve-btn"
          :disabled="!room.can_reserve"
          @click="goToReservation(room)"
        >
          {{ room.can_reserve ? '预约' : '不可预约' }}
        </button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import axios from 'axios';

const router = useRouter();

// 状态
const dates = ref<DateItem[]>([]);
const selectedDate = ref<number>(0);
const rooms = ref<RoomItem[]>([]);
const loading = ref(false);
const showOnlyAvailable = ref(false);

// 是否是今天
const isToday = computed(() => {
  if (dates.value.length === 0) return false;
  return selectedDate.value === dates.value[0].date;
});

// 生命周期
onMounted(async () => {
  await loadDates();
  await loadRooms();
});

// 加载日期列表
async function loadDates() {
  try {
    const response = await axios.get('/api/SQ/GetAvailableDates');
    dates.value = response.data.data;
    selectedDate.value = dates.value[0].date;
  } catch (error) {
    console.error('加载日期失败', error);
  }
}

// 加载房间列表
async function loadRooms() {
  loading.value = true;
  try {
    const params: any = {
      date: selectedDate.value
    };
    
    if (showOnlyAvailable.value && isToday.value) {
      params.showOnlyAvailable = true;
      params.currentTimeSlot = getCurrentTimeSlot();
    }
    
    const response = await axios.get('/api/SQ/GetRoomListWithSlotsNew', { params });
    rooms.value = response.data.data;
  } catch (error) {
    console.error('加载房间列表失败', error);
  } finally {
    loading.value = false;
  }
}

// 选择日期
function selectDate(date: number) {
  selectedDate.value = date;
  loadRooms();
}

// 获取当前时段
function getCurrentTimeSlot(): number {
  const hour = new Date().getHours();
  if (hour >= 0 && hour < 6) return 0;
  if (hour >= 6 && hour < 12) return 1;
  if (hour >= 12 && hour < 18) return 2;
  return 3;
}

// 获取状态图标
function getStatusIcon(status: string): string {
  const iconMap: Record<string, string> = {
    'available': '✓',
    'reserved': '✗',
    'unavailable': '🚫',
    'using': '🔵'
  };
  return iconMap[status] || '';
}

// 跳转到预约页
function goToReservation(room: RoomItem) {
  // 筛选可预约的时段
  const availableSlots = room.time_slots.filter(s => s.status === 'available');
  
  if (availableSlots.length === 0) {
    alert('该房间当前无可预约时段');
    return;
  }
  
  router.push({
    name: 'CreateReservation',
    params: {
      roomId: room.id,
      roomName: room.name,
      date: selectedDate.value
    },
    state: {
      availableSlots: availableSlots
    }
  });
}
</script>

<style scoped>
.room-booking-page {
  padding: 20px;
}

.date-selector {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
  overflow-x: auto;
}

.date-item {
  padding: 10px 20px;
  background: #f0f0f0;
  border-radius: 4px;
  cursor: pointer;
  white-space: nowrap;
}

.date-item.active {
  background: #1890ff;
  color: white;
}

.filter-toggle {
  margin-bottom: 20px;
}

.room-card {
  border: 1px solid #e8e8e8;
  border-radius: 8px;
  padding: 16px;
  margin-bottom: 16px;
}

.time-slots {
  display: flex;
  gap: 10px;
  margin: 10px 0;
}

.slot-item {
  padding: 8px 12px;
  border-radius: 4px;
  font-size: 14px;
}

.slot-item.available {
  background: #f6ffed;
  border: 1px solid #b7eb8f;
  color: #52c41a;
}

.slot-item.reserved {
  background: #fff1f0;
  border: 1px solid #ffa39e;
  color: #f5222d;
}

.slot-item.unavailable {
  background: #f5f5f5;
  border: 1px solid #d9d9d9;
  color: #999;
}

.slot-item.using {
  background: #e6f7ff;
  border: 1px solid #91d5ff;
  color: #1890ff;
}

.reserve-btn {
  width: 100%;
  padding: 10px;
  background: #1890ff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.reserve-btn:disabled {
  background: #d9d9d9;
  cursor: not-allowed;
}
</style>

2. 预约页组件

<template>
  <div class="create-reservation-page">
    <h2>创建预约</h2>
    
    <form @submit.prevent="submitReservation">
      <!-- 预约日期只读 -->
      <div class="form-item">
        <label>预约日期</label>
        <input type="text" :value="dateDisplay" readonly>
      </div>
      
      <!-- 房间号只读 -->
      <div class="form-item">
        <label>房间号</label>
        <input type="text" :value="roomName" readonly>
      </div>
      
      <!-- 预约时间 -->
      <div class="form-item">
        <label>预约时间 *</label>
        <select v-model="form.time_slot_type" required @change="onSlotChange">
          <option value="">请选择时段</option>
          <option 
            v-for="slot in availableSlots" 
            :key="slot.slot_type"
            :value="slot.slot_type"
          >
            {{ slot.slot_name }} ({{ slot.price_desc_standard }})
          </option>
        </select>
        <div v-if="selectedSlotInfo" class="slot-info">
          <p>时间范围:{{ selectedSlotInfo.timeRange }}</p>
          <p>标准价:{{ selectedSlotInfo.standardPrice }} | 会员价:{{ selectedSlotInfo.memberPrice }}</p>
        </div>
      </div>
      
      <!-- 最晚到店时间 -->
      <div class="form-item">
        <label>最晚到店时间</label>
        <input 
          type="time" 
          v-model="arrivalTime"
          :min="timeRange.min"
          :max="timeRange.max"
        >
      </div>
      
      <!-- 人数 -->
      <div class="form-item">
        <label>人数 *</label>
        <div class="radio-group">
          <label v-for="num in [2, 3, 4, 5, 6]" :key="num">
            <input type="radio" v-model="form.player_count" :value="num" required>
            {{ num }}人
          </label>
          <label>
            <input type="radio" v-model="form.player_count" :value="1" required>
            无需组局
          </label>
        </div>
      </div>
      
      <!-- 鸽子费 -->
      <div class="form-item">
        <label>鸽子费 *</label>
        <div class="radio-group">
          <label v-for="fee in [0, 10, 20, 30]" :key="fee">
            <input type="radio" v-model="depositFeeOption" :value="fee">
            {{ fee }}元
          </label>
          <label>
            <input type="radio" v-model="depositFeeOption" value="custom">
            自定义
          </label>
        </div>
        <input 
          v-if="depositFeeOption === 'custom'"
          type="number" 
          v-model.number="form.deposit_fee"
          min="0"
          max="50"
          step="1"
          placeholder="请输入0-50的整数"
        >
      </div>
      
      <!-- 组局名称 -->
      <div class="form-item">
        <label>组局名称 *</label>
        <input type="text" v-model="form.title" required maxlength="100">
      </div>
      
      <!-- 玩法类型 -->
      <div class="form-item">
        <label>玩法类型 *</label>
        <input type="text" v-model="form.game_type" required maxlength="50">
      </div>
      
      <!-- 游戏规则 -->
      <div class="form-item">
        <label>游戏规则</label>
        <input type="text" v-model="form.game_rule" maxlength="50">
      </div>
      
      <!-- 其他补充 -->
      <div class="form-item">
        <label>其他补充</label>
        <textarea v-model="form.extra_info" maxlength="255"></textarea>
      </div>
      
      <!-- 提交按钮 -->
      <button type="submit" class="submit-btn" :disabled="submitting">
        {{ submitting ? '提交中...' : '提交预约' }}
      </button>
    </form>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';

const route = useRoute();
const router = useRouter();

// 路由参数
const roomId = ref(Number(route.params.roomId));
const roomName = ref(String(route.params.roomName));
const date = ref(Number(route.params.date));
const availableSlots = ref<TimeSlot[]>(history.state.availableSlots || []);

// 表单状态
const form = ref<CreateReservationRequest>({
  room_id: roomId.value,
  date: date.value,
  time_slot_type: undefined as any,
  latest_arrival_time: undefined,
  is_solo_mode: false,
  player_count: 4,
  deposit_fee: 0,
  title: '',
  game_type: '',
  game_rule: '',
  extra_info: '',
  is_smoking: 0,
  gender_limit: 0,
  credit_limit: undefined,
  min_age: undefined,
  max_age: 0,
  important_data: ''
});

const depositFeeOption = ref<number | 'custom'>(0);
const arrivalTime = ref('');
const submitting = ref(false);

// 日期显示
const dateDisplay = computed(() => {
  const d = new Date(date.value * 1000);
  return d.toLocaleDateString('zh-CN', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });
});

// 选中的时段信息
const selectedSlotInfo = computed(() => {
  if (!form.value.time_slot_type && form.value.time_slot_type !== 0) return null;
  
  const slot = availableSlots.value.find(s => s.slot_type === form.value.time_slot_type);
  if (!slot) return null;
  
  return {
    timeRange: getTimeRange(form.value.time_slot_type),
    standardPrice: slot.price_desc_standard,
    memberPrice: slot.price_desc_member
  };
});

// 时间范围限制
const timeRange = computed(() => {
  if (!form.value.time_slot_type && form.value.time_slot_type !== 0) {
    return { min: '00:00', max: '23:59' };
  }
  
  const ranges = [
    { min: '00:00', max: '05:59' },  // 凌晨
    { min: '06:00', max: '11:59' },  // 上午
    { min: '12:00', max: '17:59' },  // 下午
    { min: '18:00', max: '23:59' }   // 晚上
  ];
  
  return ranges[form.value.time_slot_type] || ranges[0];
});

// 监听人数变化
watch(() => form.value.player_count, (newVal) => {
  form.value.is_solo_mode = newVal === 1;
});

// 监听鸽子费选项
watch(depositFeeOption, (newVal) => {
  if (typeof newVal === 'number') {
    form.value.deposit_fee = newVal;
  }
});

// 时段变化
function onSlotChange() {
  // 可以在这里调用校验接口
  validateReservation();
}

// 校验预约
async function validateReservation() {
  if (!form.value.time_slot_type && form.value.time_slot_type !== 0) return;
  
  try {
    const response = await axios.post('/api/SQ/ValidateReservationBySlot', form.value, {
      headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
    });
    
    if (response.data.code !== 0) {
      alert(response.data.msg);
    }
  } catch (error) {
    console.error('校验失败', error);
  }
}

// 提交预约
async function submitReservation() {
  // 转换最晚到店时间
  if (arrivalTime.value) {
    const d = new Date(date.value * 1000);
    const [hour, minute] = arrivalTime.value.split(':');
    d.setHours(Number(hour), Number(minute), 0, 0);
    form.value.latest_arrival_time = Math.floor(d.getTime() / 1000);
  }
  
  // 参数校验
  if (!validate()) {
    return;
  }
  
  submitting.value = true;
  
  try {
    const response = await axios.post('/api/SQ/AddSQReservationBySlot', form.value, {
      headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
    });
    
    if (response.data.code === 0) {
      alert('预约成功!');
      router.push('/my-reservations');
    } else {
      alert(response.data.msg);
    }
  } catch (error: any) {
    alert(error.response?.data?.msg || '预约失败,请重试');
  } finally {
    submitting.value = false;
  }
}

// 参数校验
function validate(): boolean {
  if (!form.value.title) {
    alert('请输入组局名称');
    return false;
  }
  if (!form.value.game_type) {
    alert('请选择玩法类型');
    return false;
  }
  if (form.value.deposit_fee < 0 || form.value.deposit_fee > 50) {
    alert('鸽子费必须在0-50元之间');
    return false;
  }
  if (!Number.isInteger(form.value.deposit_fee)) {
    alert('鸽子费必须是整数');
    return false;
  }
  return true;
}

// 获取时间范围文本
function getTimeRange(slotType: number): string {
  const ranges = [
    '00:00-05:59',
    '06:00-11:59',
    '12:00-17:59',
    '18:00-23:59'
  ];
  return ranges[slotType] || '';
}
</script>

<style scoped>
.create-reservation-page {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.form-item {
  margin-bottom: 20px;
}

.form-item label {
  display: block;
  margin-bottom: 8px;
  font-weight: bold;
}

.form-item input[type="text"],
.form-item input[type="number"],
.form-item input[type="time"],
.form-item select,
.form-item textarea {
  width: 100%;
  padding: 10px;
  border: 1px solid #d9d9d9;
  border-radius: 4px;
}

.radio-group {
  display: flex;
  flex-wrap: wrap;
  gap: 15px;
}

.radio-group label {
  font-weight: normal;
}

.slot-info {
  margin-top: 10px;
  padding: 10px;
  background: #f0f0f0;
  border-radius: 4px;
}

.submit-btn {
  width: 100%;
  padding: 12px;
  background: #1890ff;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
}

.submit-btn:disabled {
  background: #d9d9d9;
  cursor: not-allowed;
}
</style>

常见问题FAQ

Q1: 时间戳是秒级还是毫秒级?

A: 所有接口使用的都是秒级时间戳Unix Timestamp不是毫秒级。

// 正确:秒级
const timestamp = Math.floor(Date.now() / 1000);

// 错误:毫秒级
const timestamp = Date.now();

Q2: 如何判断当前是哪个时段?

A: 根据当前时间的小时数判断:

function getCurrentTimeSlot() {
  const hour = new Date().getHours();
  if (hour >= 0 && hour < 6) return 0;   // 凌晨
  if (hour >= 6 && hour < 12) return 1;  // 上午
  if (hour >= 12 && hour < 18) return 2; // 下午
  return 3;                              // 晚上
}

Q3: 最晚到店时间如何处理?

A: 最晚到店时间必须在所选时段的时间范围内:

// 1. 获取时段的时间范围
const timeRanges = {
  0: { min: '00:00', max: '05:59' },
  1: { min: '06:00', max: '11:59' },
  2: { min: '12:00', max: '17:59' },
  3: { min: '18:00', max: '23:59' }
};

// 2. 限制时间选择器的范围
const range = timeRanges[selectedSlot];
<input type="time" :min="range.min" :max="range.max">

// 3. 转换为时间戳
const [hour, minute] = arrivalTime.split(':');
const d = new Date(date * 1000);
d.setHours(Number(hour), Number(minute), 0, 0);
const timestamp = Math.floor(d.getTime() / 1000);

Q4: "无需组局"模式如何处理?

A: 当选择"无需组局"时:

// 1. 人数固定为1
if (playerCount === 1) {
  form.is_solo_mode = true;
  form.player_count = 1;
}

// 2. 后端会拒绝其他人加入该预约

Q5: 鸽子费的限制是什么?

A: 鸽子费必须满足:

  • 范围0-50元
  • 类型:整数(不能有小数)
// 校验
if (depositFee < 0 || depositFee > 50) {
  alert('鸽子费必须在0-50元之间');
  return false;
}
if (!Number.isInteger(depositFee)) {
  alert('鸽子费必须是整数');
  return false;
}

Q6: 如何处理时间冲突?

A: 后端会自动检测时间冲突返回错误码402

if (response.data.code === 402) {
  alert('您有其它预约时间冲突,无法创建该预约!');
  // 可以引导用户查看"我的预约"
}

Q7: Token如何传递

A: 所有需要登录的接口都需要在请求头中携带Token

// Axios示例
axios.post('/api/SQ/AddSQReservationBySlot', data, {
  headers: {
    Authorization: `Bearer ${localStorage.getItem('token')}`
  }
});

// 或配置全局拦截器
axios.interceptors.request.use(config => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

Q8: 如何处理已过时段?

A: 后端会自动过滤已过去的时段,前端不需要额外处理。如果某个时段已过去,它会在time_slots中显示为unavailable或不返回。

Q9: 房间图片加载失败怎么办?

A: 建议添加图片加载失败的占位图:

<img 
  :src="room.image_url" 
  :alt="room.name"
  @error="handleImageError"
>

<script>
function handleImageError(e) {
  e.target.src = '/default-room.png'; // 默认图片
}
</script>

Q10: 如何实现实时刷新房间状态?

A: 可以使用定时轮询或WebSocket

// 方式1定时轮询简单
let timer = null;

onMounted(() => {
  loadRooms();
  timer = setInterval(() => {
    loadRooms();
  }, 30000); // 每30秒刷新一次
});

onUnmounted(() => {
  if (timer) clearInterval(timer);
});

// 方式2WebSocket实时性更好需要后端支持
// 连接WebSocket监听房间状态变化

调试技巧

1. 使用浏览器开发者工具

查看网络请求

F12 → Network → XHR
- 查看请求URL、参数、响应
- 检查状态码
- 查看响应时间

查看控制台日志

// 添加详细日志
console.log('请求参数:', data);
console.log('响应数据:', response.data);
console.error('错误信息:', error);

2. Postman测试接口

测试步骤

  1. 先调用登录接口获取Token
  2. 复制Token到环境变量
  3. 测试各个接口
  4. 保存常用请求到Collection

示例请求

GET http://localhost:5000/api/SQ/GetAvailableDates

GET http://localhost:5000/api/SQ/GetRoomListWithSlotsNew?date=1733443200

POST http://localhost:5000/api/SQ/AddSQReservationBySlot
Headers:
  Authorization: Bearer eyJhbGc...
Body: { ... }

3. 常见错误排查

错误 可能原因 解决方法
401 Unauthorized Token过期或无效 重新登录获取Token
404 Not Found 接口路径错误 检查URL是否正确
500 Internal Server Error 服务器内部错误 查看后端日志
CORS错误 跨域配置问题 联系后端配置CORS
参数错误 参数格式不正确 检查参数类型和值

4. 开发环境代理配置

Vite配置示例

// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:5000',
        changeOrigin: true
      }
    }
  }
}

Vue CLI配置示例

// vue.config.js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:5000',
        changeOrigin: true
      }
    }
  }
}

附录

A. 错误码对照表

错误码 说明 处理建议
0 成功 继续业务流程
400 业务错误 显示错误信息
401 未授权 跳转到登录页
402 时间冲突 提示用户选择其他时段
403 权限不足 提示权限不足
404 资源不存在 提示资源不存在
500 系统错误 提示系统错误,稍后重试

B. 测试数据

// 测试用日期时间戳
const testDates = {
  today: Math.floor(Date.now() / 1000),
  tomorrow: Math.floor(Date.now() / 1000) + 86400,
  nextWeek: Math.floor(Date.now() / 1000) + 604800
};

// 测试用房间ID
const testRoomIds = [1, 2, 3];

// 测试用时段类型
const testTimeSlots = [0, 1, 2, 3];

C. 联系方式

技术支持

  • 邮箱jianweie@163.com
  • 文档版本v2.0
  • 更新日期2025-12-06

祝开发顺利! 🚀