43 KiB
43 KiB
预约系统前端对接文档
📌 文档说明
版本:v2.0
更新日期:2025-12-06
适用范围:预约房间功能前端开发
后端接口版本:预约时段优化版
📋 目录
业务流程概述
用户预约完整流程
┌─────────────────┐
│ 进入预约模块 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 【房间展示页】 │
│ 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);
});
// 方式2:WebSocket(实时性更好,需要后端支持)
// 连接WebSocket监听房间状态变化
调试技巧
1. 使用浏览器开发者工具
查看网络请求
F12 → Network → XHR
- 查看请求URL、参数、响应
- 检查状态码
- 查看响应时间
查看控制台日志
// 添加详细日志
console.log('请求参数:', data);
console.log('响应数据:', response.data);
console.error('错误信息:', error);
2. Postman测试接口
测试步骤
- 先调用登录接口获取Token
- 复制Token到环境变量
- 测试各个接口
- 保存常用请求到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
祝开发顺利! 🚀