1727 lines
43 KiB
Markdown
1727 lines
43 KiB
Markdown
# 预约系统前端对接文档
|
||
|
||
## 📌 文档说明
|
||
|
||
**版本**:v2.0
|
||
**更新日期**:2025-12-06
|
||
**适用范围**:预约房间功能前端开发
|
||
**后端接口版本**:预约时段优化版
|
||
|
||
---
|
||
|
||
## 📋 目录
|
||
|
||
1. [业务流程概述](#业务流程概述)
|
||
2. [页面结构](#页面结构)
|
||
3. [接口调用流程](#接口调用流程)
|
||
4. [接口详细说明](#接口详细说明)
|
||
5. [数据结构定义](#数据结构定义)
|
||
6. [前端实现指南](#前端实现指南)
|
||
7. [常见问题FAQ](#常见问题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次
|
||
- **是否需要登录**:否
|
||
|
||
#### 请求参数
|
||
无参数
|
||
|
||
#### 返回数据
|
||
```typescript
|
||
interface Response {
|
||
code: number; // 0=成功
|
||
msg: string; // 消息
|
||
data: DateItem[]; // 日期列表
|
||
}
|
||
|
||
interface DateItem {
|
||
date: number; // Unix时间戳(秒级)
|
||
dateText: string; // 文本:今天/明天/后天/日期
|
||
dateDisplay: string; // 展示文本:12月06日 周五
|
||
}
|
||
```
|
||
|
||
#### 返回示例
|
||
```json
|
||
{
|
||
"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天)
|
||
]
|
||
}
|
||
```
|
||
|
||
#### 前端使用
|
||
```javascript
|
||
// 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 |
|
||
|
||
#### 返回数据
|
||
```typescript
|
||
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; // 会员价格说明
|
||
}
|
||
```
|
||
|
||
#### 返回示例
|
||
```json
|
||
{
|
||
"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
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### 前端使用
|
||
```javascript
|
||
// 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)
|
||
|
||
#### 请求参数
|
||
与"创建预约"接口相同,但不会真正创建预约
|
||
|
||
#### 返回数据
|
||
```typescript
|
||
interface Response {
|
||
code: number; // 0=可以创建,500=不可以
|
||
msg: string; // 原因说明
|
||
data: {
|
||
canCreate: boolean; // 是否可以创建
|
||
reason: string; // 不能创建的原因
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 返回示例
|
||
```json
|
||
// 可以创建
|
||
{
|
||
"code": 0,
|
||
"msg": "",
|
||
"data": {
|
||
"canCreate": true,
|
||
"reason": ""
|
||
}
|
||
}
|
||
|
||
// 不能创建
|
||
{
|
||
"code": 500,
|
||
"msg": "您有其它预约时间冲突",
|
||
"data": {
|
||
"canCreate": false,
|
||
"reason": "您有其它预约时间冲突"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 前端使用
|
||
```javascript
|
||
// 用户选择时段时实时校验
|
||
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)
|
||
|
||
#### 请求参数
|
||
```typescript
|
||
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)
|
||
}
|
||
```
|
||
|
||
#### 请求示例
|
||
```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\"}"
|
||
}
|
||
```
|
||
|
||
#### 返回数据
|
||
```typescript
|
||
interface Response {
|
||
code: number; // 0=成功,其他=失败
|
||
msg: string; // 消息
|
||
data?: {
|
||
reservation_id: number; // 预约ID
|
||
start_time: string; // 开始时间
|
||
end_time: string; // 结束时间
|
||
actual_price: number; // 实际价格
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 返回示例(成功)
|
||
```json
|
||
{
|
||
"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
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 返回示例(失败)
|
||
```json
|
||
// 时间冲突
|
||
{
|
||
"code": 402,
|
||
"msg": "您有其它预约时间冲突,无法创建该预约!",
|
||
"data": null
|
||
}
|
||
|
||
// 房间已被预约
|
||
{
|
||
"code": 500,
|
||
"msg": "该时间段房间已被预约",
|
||
"data": null
|
||
}
|
||
|
||
// 鸽子费超限
|
||
{
|
||
"code": 500,
|
||
"msg": "鸽子费必须在0-50元之间",
|
||
"data": null
|
||
}
|
||
```
|
||
|
||
#### 前端使用
|
||
```javascript
|
||
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类型定义
|
||
|
||
```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. 房间展示页组件
|
||
|
||
```vue
|
||
<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. 预约页组件
|
||
|
||
```vue
|
||
<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),不是毫秒级。
|
||
|
||
```javascript
|
||
// 正确:秒级
|
||
const timestamp = Math.floor(Date.now() / 1000);
|
||
|
||
// 错误:毫秒级
|
||
const timestamp = Date.now();
|
||
```
|
||
|
||
### Q2: 如何判断当前是哪个时段?
|
||
|
||
**A:** 根据当前时间的小时数判断:
|
||
|
||
```javascript
|
||
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:** 最晚到店时间必须在所选时段的时间范围内:
|
||
|
||
```javascript
|
||
// 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:** 当选择"无需组局"时:
|
||
|
||
```javascript
|
||
// 1. 人数固定为1
|
||
if (playerCount === 1) {
|
||
form.is_solo_mode = true;
|
||
form.player_count = 1;
|
||
}
|
||
|
||
// 2. 后端会拒绝其他人加入该预约
|
||
```
|
||
|
||
### Q5: 鸽子费的限制是什么?
|
||
|
||
**A:** 鸽子费必须满足:
|
||
- 范围:0-50元
|
||
- 类型:整数(不能有小数)
|
||
|
||
```javascript
|
||
// 校验
|
||
if (depositFee < 0 || depositFee > 50) {
|
||
alert('鸽子费必须在0-50元之间');
|
||
return false;
|
||
}
|
||
if (!Number.isInteger(depositFee)) {
|
||
alert('鸽子费必须是整数');
|
||
return false;
|
||
}
|
||
```
|
||
|
||
### Q6: 如何处理时间冲突?
|
||
|
||
**A:** 后端会自动检测时间冲突,返回错误码402:
|
||
|
||
```javascript
|
||
if (response.data.code === 402) {
|
||
alert('您有其它预约时间冲突,无法创建该预约!');
|
||
// 可以引导用户查看"我的预约"
|
||
}
|
||
```
|
||
|
||
### Q7: Token如何传递?
|
||
|
||
**A:** 所有需要登录的接口都需要在请求头中携带Token:
|
||
|
||
```javascript
|
||
// 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:** 建议添加图片加载失败的占位图:
|
||
|
||
```vue
|
||
<img
|
||
:src="room.image_url"
|
||
:alt="room.name"
|
||
@error="handleImageError"
|
||
>
|
||
|
||
<script>
|
||
function handleImageError(e) {
|
||
e.target.src = '/default-room.png'; // 默认图片
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### Q10: 如何实现实时刷新房间状态?
|
||
|
||
**A:** 可以使用定时轮询或WebSocket:
|
||
|
||
```javascript
|
||
// 方式1:定时轮询(简单)
|
||
let timer = null;
|
||
|
||
onMounted(() => {
|
||
loadRooms();
|
||
timer = setInterval(() => {
|
||
loadRooms();
|
||
}, 30000); // 每30秒刷新一次
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
if (timer) clearInterval(timer);
|
||
});
|
||
|
||
// 方式2:WebSocket(实时性更好,需要后端支持)
|
||
// 连接WebSocket监听房间状态变化
|
||
```
|
||
|
||
---
|
||
|
||
## 调试技巧
|
||
|
||
### 1. 使用浏览器开发者工具
|
||
|
||
#### 查看网络请求
|
||
```
|
||
F12 → Network → XHR
|
||
- 查看请求URL、参数、响应
|
||
- 检查状态码
|
||
- 查看响应时间
|
||
```
|
||
|
||
#### 查看控制台日志
|
||
```javascript
|
||
// 添加详细日志
|
||
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配置示例
|
||
```javascript
|
||
// vite.config.js
|
||
export default {
|
||
server: {
|
||
proxy: {
|
||
'/api': {
|
||
target: 'http://localhost:5000',
|
||
changeOrigin: true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Vue CLI配置示例
|
||
```javascript
|
||
// vue.config.js
|
||
module.exports = {
|
||
devServer: {
|
||
proxy: {
|
||
'/api': {
|
||
target: 'http://localhost:5000',
|
||
changeOrigin: true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 附录
|
||
|
||
### A. 错误码对照表
|
||
|
||
| 错误码 | 说明 | 处理建议 |
|
||
|-------|------|---------|
|
||
| 0 | 成功 | 继续业务流程 |
|
||
| 400 | 业务错误 | 显示错误信息 |
|
||
| 401 | 未授权 | 跳转到登录页 |
|
||
| 402 | 时间冲突 | 提示用户选择其他时段 |
|
||
| 403 | 权限不足 | 提示权限不足 |
|
||
| 404 | 资源不存在 | 提示资源不存在 |
|
||
| 500 | 系统错误 | 提示系统错误,稍后重试 |
|
||
|
||
### B. 测试数据
|
||
|
||
```javascript
|
||
// 测试用日期时间戳
|
||
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
|
||
|
||
---
|
||
|
||
**祝开发顺利!** 🚀
|
||
|