This commit is contained in:
zpc 2025-12-06 13:57:23 +08:00
parent db0250db89
commit fc04321108
5 changed files with 619 additions and 32 deletions

View File

@ -0,0 +1,390 @@
# API接口文档 - 预约房间页面 (book-room-page)
## 文档说明
本文档为 `uniapp/mahjong_group/pages/appointment/book-room-page.vue` 页面提供完整的API接口设计方案。
---
## 📊 数据库设计评估
### 现有字段SQRooms表
✅ **已有字段:**
- `id`: 房间ID
- `name`: 房间名称
- `price_per_hour`: 价格/小时
- `capacity`: 可容纳人数
- `status`: 可用状态 (bool)
⚠️ **缺失字段(需要扩展):**
- `image_url`: 房间图片URL
- `room_type`: 房间类型(如:小包、中包、大包、豪华包)
- `description`: 房间描述
### 已新增字段
```sql
-- 扩展 SQRooms 表,添加以下字段:
ALTER TABLE SQRooms ADD image_url NVARCHAR(500) NULL;
ALTER TABLE SQRooms ADD room_type NVARCHAR(50) NULL;
ALTER TABLE SQRooms ADD description NVARCHAR(500) NULL;
```
---
## 📡 API接口设计
### 1⃣ 获取房间列表(带时段占用信息)
**接口名称:** GetRoomListWithTimeSlots
**请求方式:** `GET`
**接口路径:** `/api/SQ/GetRoomListWithTimeSlots`
**功能说明:** 获取指定日期的所有房间列表,包含房间详细信息、价格、当前状态以及时段占用情况
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| date | long | 是 | 查询日期Unix时间戳-秒级传入当天0点的时间戳 |
| showTimeSlots | bool | 否 | 是否返回时段占用信息默认false |
**示例:**
```
GET /api/SQ/GetRoomListWithTimeSlots?date=1733443200&showTimeSlots=true
```
#### 响应数据
```json
{
"code": 0,
"msg": "ok",
"data": [
{
"id": 1,
"name": "304号-大包",
"room_type": "大包",
"image_url": "https://example.com/rooms/304.jpg",
"price_per_hour": 30.00,
"vip_price_per_hour": 25.00,
"capacity": 6,
"description": "豪华大包厢,配有独立卫生间",
"status": "available", // available-可预约, using-使用中, unavailable-不可用
"is_available": true,
"time_slots": { // 仅当 showTimeSlots=true 时返回
"dawn": { // 凌晨 0:00-6:00
"is_occupied": true,
"reservations": [
{ "start_time": "2025-12-06 02:00:00", "end_time": "2025-12-06 06:00:00" }
]
},
"morning": { // 上午 6:00-12:00
"is_occupied": false,
"reservations": []
},
"afternoon": { // 下午 12:00-18:00
"is_occupied": true,
"reservations": [
{ "start_time": "2025-12-06 14:00:00", "end_time": "2025-12-06 18:00:00" }
]
},
"evening": { // 晚上 18:00-24:00
"is_occupied": false,
"reservations": []
}
}
},
{
"id": 2,
"name": "305号-中包",
"room_type": "中包",
"image_url": "https://example.com/rooms/305.jpg",
"price_per_hour": 20.00,
"vip_price_per_hour": null,
"capacity": 4,
"description": "标准中包厢",
"status": "using",
"is_available": false,
"time_slots": null
}
]
}
```
#### 响应字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| id | int | 房间ID |
| name | string | 房间名称 |
| room_type | string | 房间类型 |
| image_url | string | 房间图片URL若为空前端显示默认图 |
| price_per_hour | decimal | 标准价格/小时 |
| vip_price_per_hour | decimal | 会员价/小时null表示无会员价 |
| capacity | int | 可容纳人数 |
| description | string | 房间描述 |
| status | string | 房间状态:<br/>- `available`: 可预约<br/>- `using`: 当前使用中<br/>- `unavailable`: 不可用(维护中等) |
| is_available | bool | 是否可预约 |
| time_slots | object | 时段占用信息(仅当请求参数 showTimeSlots=true 时返回) |
---
### 2⃣ 新增获取可预约房间列表
**接口名称:** GetReservationRoomListNew
**请求方式:** `GET`
**接口路径:** `/api/SQ/GetReservationRoomListNew`
**功能说明:** 获取指定时间段内可预约的房间列表(不包含已预约和不可用时段的房间)
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| startTime | long | 是 | 开始时间Unix时间戳-秒级) |
| endTime | long | 是 | 结束时间Unix时间戳-秒级) |
**示例:**
```
GET /api/SQ/GetReservationRoomListNew?startTime=1733457600&endTime=1733472000
```
#### 响应数据
```json
{
"code": 0,
"msg": "ok",
"data": [
{
"id": 1,
"name": "304号-大包",
"room_type": "大包",
"image_url": "https://example.com/rooms/304.jpg",
"price_per_hour": 30.00,
"vip_price_per_hour": 25.00,
"capacity": 6,
"description": "豪华大包厢",
"display_name": "304号-大包 30.00/小时" // 用于前端直接显示
},
{
"id": 3,
"name": "306号-小包",
"room_type": "小包",
"image_url": null,
"price_per_hour": 15.00,
"vip_price_per_hour": null,
"capacity": 4,
"description": "标准小包厢",
"display_name": "306号-小包 15.00/小时"
}
]
}
```
---
### 3⃣ 获取营业时间配置
**接口名称:** GetBusinessHours
**请求方式:** `GET`
**接口路径:** `/api/Common/GetBusinessHours`
**功能说明:** 获取店铺营业时间配置
#### 请求参数
#### 响应数据
```json
{
"code": 0,
"msg": "ok",
"data": {
"open_time": "09:00",
"close_time": "23:00",
"is_24_hours": false,
"description": "早9点 至 晚23点"
}
}
```
#### 响应字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| open_time | string | 开始营业时间HH:mm格式 |
| close_time | string | 结束营业时间HH:mm格式 |
| is_24_hours | bool | 是否24小时营业 |
| description | string | 营业时间描述文本 |
---
### 4⃣ 获取房间详情
**接口名称:** GetRoomDetail
**请求方式:** `GET`
**接口路径:** `/api/SQ/GetRoomDetail`
**功能说明:** 获取指定房间的详细信息
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| roomId | int | 是 | 房间ID |
| date | long | 否 | 查询日期(默认今天) |
#### 响应数据
```json
{
"code": 0,
"msg": "ok",
"data": {
"id": 1,
"name": "304号-大包",
"room_type": "大包",
"image_url": "https://example.com/rooms/304.jpg",
"images": [ // 多图展示
"https://example.com/rooms/304_1.jpg",
"https://example.com/rooms/304_2.jpg"
],
"price_per_hour": 30.00,
"vip_price_per_hour": 25.00,
"capacity": 6,
"description": "豪华大包厢配有独立卫生间、智能麻将桌、空调、Wi-Fi等设施",
"amenities": ["独立卫生间", "智能麻将桌", "空调", "Wi-Fi"],
"status": "available",
"today_reservations": [ // 今日预约情况
{
"start_time": "2025-12-06 14:00:00",
"end_time": "2025-12-06 18:00:00",
"status": 0
}
]
}
}
```
---
## 🔄 前端调用流程
### 页面加载流程
```javascript
// 1. 页面初始化
onLoad() {
this.getDateList(); // 生成7天日期列表前端
this.loadRoomList(); // 加载房间列表
this.getBusinessHours(); // 获取营业时间
}
// 2. 加载房间列表(基础模式)
async loadRoomList() {
const selectedDate = this.dateList[this.currentTimeIndex];
const timestamp = this.dateToTimestamp(selectedDate.time);
const res = await uni.request({
url: '/api/SQ/GetRoomListWithTimeSlots',
data: {
date: timestamp,
showTimeSlots: this.showFreeTime // 根据开关决定是否获取时段
}
});
this.roomList = res.data.data;
}
// 3. 切换日期时重新加载
clickTime(index) {
this.currentTimeIndex = index;
this.loadRoomList();
}
// 4. 切换时段显示开关
change(value) {
this.showFreeTime = value;
this.loadRoomList(); // 重新加载,包含时段信息
}
// 5. 点击预约按钮
handleBookRoom(room) {
if (room.status !== 'available') {
uni.showToast({ title: '该房间不可预约', icon: 'none' });
return;
}
// 跳转到预约详情页,传递房间信息
uni.navigateTo({
url: '/pages/appointment/appointment-page?' +
'roomId=' + room.id +
'&roomName=' + encodeURIComponent(room.name) +
'&date=' + this.dateList[this.currentTimeIndex].time
});
}
```
## ✅ 总结
### 数据库设计评估结果
**现有设计基本满足需求**,但建议扩展以下字段以提升用户体验:
1. ⚠️ **必要扩展**(影响核心功能):
- 无,现有字段可满足基本功能
2. ✨ **建议扩展**(提升用户体验):
- `image_url`: 房间图片
- `room_type`: 房间类型
- `vip_price_per_hour`: 会员价
- `description`: 房间描述
### API实现优先级
| 优先级 | 接口 | 说明 |
|--------|------|------|
| 🔴 **P0** | GetRoomListWithTimeSlots | 核心接口,提供房间列表和时段信息 |
| 🟡 **P1** | 优化 GetReservationRoomList | 返回更丰富的房间信息 |
| 🟢 **P2** | GetBusinessHours | 营业时间配置 |
| 🔵 **P3** | GetRoomDetail | 房间详情 |
---
## 📝 注意事项
1. **时区处理**:
- 前端传递时间戳时需统一使用UTC+8时区
- 后端 `DateTimeOffset.FromUnixTimeSeconds()` 自动处理时区转换
2. **默认图片**:
- 前端需准备默认房间图片:`/static/default-room.png`
- 当 `image_url` 为空时使用默认图
3. **缓存策略**:
- 房间列表建议缓存5分钟
- 时段信息实时查询,不缓存
4. **性能优化**:
- 首次加载不查询时段信息showTimeSlots=false
- 仅当用户打开开关时再查询时段详情
---
**文档版本**: v1.0
**创建日期**: 2025-12-06
**最后更新**: 2025-12-06

View File

@ -59,4 +59,16 @@ export const uploadImages = async (formData) => {
return res.data;
}
return null;
}
/**
* 获取营业时间配置
* @returns {Promise<any>} 返回营业时间信息 {open_time, close_time, is_24_hours, description}
*/
export const getBusinessHours = async () => {
const res = await request.get("Common/GetBusinessHours");
if (res.code == 0) {
return res.data;
}
return null;
}

View File

@ -254,6 +254,41 @@ export const checkInReservation = async (checkInData) => {
/**
* 获取房间列表带时段占用信息
* @param {number} date 查询日期Unix时间戳-秒级传入当天0点的时间戳
* @param {boolean} showTimeSlots 是否返回时段占用信息默认false
* @returns {Promise<any>} 返回房间列表
*/
export const getRoomListWithTimeSlots = async (date, showTimeSlots = false) => {
const res = await request.get("sq/GetRoomListWithTimeSlots", {
date: date,
showTimeSlots: showTimeSlots
});
if (res.code == 0) {
return res.data;
}
return null;
}
/**
* 获取可预约房间列表新版包含更丰富信息
* @param {number} startTime 开始时间Unix时间戳-秒级
* @param {number} endTime 结束时间Unix时间戳-秒级
* @returns {Promise<any>} 返回房间列表包含图片类型价格等详细信息
*/
export const getReservationRoomListNew = async (startTime, endTime) => {
const res = await request.get("sq/GetReservationRoomListNew", {
startTime: startTime,
endTime: endTime
});
if (res.code == 0) {
return res.data;
}
return null;
}
export const sqInterface = {
canCreateSQReservation,
getReservationList,
@ -265,6 +300,8 @@ export const sqInterface = {
getReputationByUser,
getPaymentRecords,
getReservationRoomList,
getRoomListWithTimeSlots,
getReservationRoomListNew,
addSQReservation,
joinReservation,
cancelReservation,

View File

@ -5,104 +5,248 @@
<view style="flex: 1;"></view>
<text class="page-title">发起预约</text>
<view class="row" style="width: 90%; height: 120rpx; margin: 20rpx auto 0;">
<view class="column center" v-for="(item,index) in dateList" :style="setBgColor(index)"
<view class="column center" v-for="(item,index) in dateList" :key="index" :style="setBgColor(index)"
@click="clickTime(index)" style="width: 96rpx; height: 100%;">
<text style="font-size: 26rpx;">{{item.time}}</text>
<text style="font-size: 22rpx;">{{item.weekday}}</text>
</view>
</view>
</view>
<text style="margin-top: 20rpx; margin-left: 32rpx; font-size: 24rpx;">营业时间早XX点 晚XX点</text>
<text style="margin-top: 20rpx; margin-left: 32rpx; font-size: 24rpx;">
营业时间{{ businessHours ? businessHours.description : '加载中...' }}
</text>
<view class="row"
style="width: 686rpx; height: 56rpx; background-color: #2ED9BF; border-radius: 5rpx; margin: 20rpx auto 0; align-items: center;">
<text style="font-size: 22rpx; color: #262626; margin-left: 16rpx;">查看当前时段空闲时间</text>
<up-switch style="margin-right: 16rpx; margin-left: auto; " v-model="value" @change="change"
<up-switch style="margin-right: 16rpx; margin-left: auto;" v-model="showFreeTime" @change="change"
size="20"></up-switch>
</view>
<view class="" style="width: 100%; flex: 1; margin-top: 12rpx; overflow-y: auto;">
<!-- 加载状态 -->
<view v-if="loading" class="center" style="padding: 60rpx 0;">
<text style="font-size: 28rpx; color: #999;">加载中...</text>
</view>
<view class="" v-for="(item,index) in roomList"
<!-- 无数据提示 -->
<view v-else-if="roomList.length === 0" class="center" style="padding: 60rpx 0;">
<text style="font-size: 28rpx; color: #999;">暂无房间信息</text>
</view>
<!-- 房间列表 -->
<view class="" v-for="(item,index) in roomList" :key="item.id"
style="position: relative; width: 706rpx; height: 270rpx; border-radius: 20rpx; background-color: white; margin: 0 auto 20rpx;">
<image src=""
<!-- 房间图片 -->
<image :src="item.image_url || '/static/default_room.jpg'"
style="width: 180rpx; height: 156rpx; background-color: #DEDEDE; border-radius: 14rpx; position: absolute; top: 10rpx; left: 10rpx;"
mode=""></image>
mode="aspectFill"></image>
<!-- 房间信息 -->
<view class="column" style=" position: absolute; left: 200rpx; top: 20rpx;">
<text style="font-size: 28rpx;">房间号或房间名</text>
<text style="font-size: 20rpx;">房间类型</text>
<text style="font-size: 28rpx;">{{ item.name }}</text>
<text style="font-size: 20rpx; color: #666; margin-top: 4rpx;">{{ item.room_type || '标准房' }}</text>
<text style="font-size: 24rpx; margin-top: 24rpx;">¥30/4小时</text>
<text style="font-size: 24rpx; color: #FF0000;">会员价¥30/5小时</text>
<text style="font-size: 24rpx; margin-top: 24rpx;">¥{{ item.price_per_hour }}/小时</text>
<text style="font-size: 24rpx; color: #FF0000;">会员价¥XX/小时</text>
</view>
<text
style="font-size: 20rpx; position: absolute; top: 10rpx; right: 16rpx; color: #FF9901;">当前使用中</text>
<!-- 房间状态 -->
<text v-if="getRoomStatusText(item.status)"
:style="{fontSize: '20rpx', position: 'absolute', top: '10rpx', right: '16rpx', color: getRoomStatusColor(item.status)}">
{{ getRoomStatusText(item.status) }}
</text>
<view class="center"
style="width: 92rpx; height: 46rpx; background-color: #20BBA4; border-radius: 52rpx; font-size: 26rpx; color: white; position: absolute; bottom: 94rpx; right: 10rpx;">
<!-- 预约按钮 -->
<view class="center" @click="handleBookRoom(item)"
:style="{
width: '92rpx',
height: '46rpx',
backgroundColor: item.status === 'available' ? '#20BBA4' : '#CCCCCC',
borderRadius: '52rpx',
fontSize: '26rpx',
color: 'white',
position: 'absolute',
bottom: '94rpx',
right: '10rpx'
}">
预约
</view>
<view class="row" style="align-items: center; position: absolute; left: 10rpx; bottom: 60rpx;">
<!-- 时段说明 - 仅在showFreeTime为true时显示 -->
<view v-if="showFreeTime" class="row" style="align-items: center; position: absolute; left: 10rpx; bottom: 60rpx;">
<view style="width: 24rpx; height: 4rpx; background-color: #FF9901;"></view>
<text style="font-size: 16rpx; color: #323232; margin-left: 8rpx;">已预定时段</text>
<text style="font-size: 16rpx; color: #323232; margin-left: 8rpx;">已预定</text>
<view style="width: 24rpx; height: 4rpx; background-color: #E6E6E6; margin-left: 22rpx;"></view>
<text style="font-size: 16rpx; color: #323232; margin-left: 8rpx;">已预定时段</text>
<text style="font-size: 16rpx; color: #323232; margin-left: 8rpx;">空闲</text>
</view>
<view class="row" style="align-items: center; position: absolute; left: 10rpx; bottom: 14rpx;">
<!-- 时段占用情况 - 仅在showFreeTime为true时显示 -->
<view v-if="showFreeTime" class="row" style="align-items: center; position: absolute; left: 10rpx; bottom: 14rpx;">
<view class="column center" style="margin-right: 20rpx;">
<view style="width: 70rpx; height: 4rpx; background-color: #FF9901;"></view>
<view :style="{width: '70rpx', height: '4rpx', backgroundColor: getSlotColor(item.time_slots, 'dawn')}"></view>
<text style="font-size: 12rpx; margin-top: 4rpx;">凌晨</text>
</view>
<view class="column center" style="margin-right: 20rpx;">
<view style="width: 70rpx; height: 4rpx; background-color: #E6E6E6;"></view>
<view :style="{width: '70rpx', height: '4rpx', backgroundColor: getSlotColor(item.time_slots, 'morning')}"></view>
<text style="font-size: 12rpx; margin-top: 4rpx;">上午</text>
</view>
<view class="column center" style="margin-right: 20rpx;">
<view style="width: 70rpx; height: 4rpx; background-color: #FF9901;"></view>
<view :style="{width: '70rpx', height: '4rpx', backgroundColor: getSlotColor(item.time_slots, 'afternoon')}"></view>
<text style="font-size: 12rpx; margin-top: 4rpx;">下午</text>
</view>
<view class="column center" style="margin-right: 20rpx;">
<view style="width: 70rpx; height: 4rpx; background-color: #E6E6E6;"></view>
<view :style="{width: '70rpx', height: '4rpx', backgroundColor: getSlotColor(item.time_slots, 'evening')}"></view>
<text style="font-size: 12rpx; margin-top: 4rpx;">晚上</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { getRoomListWithTimeSlots } from '@/common/server/interface/sq.js';
import { getBusinessHours } from '@/common/server/interface/common.js';
export default {
data() {
return {
currentTimeIndex: 0,
dateList: [],
value: false,
roomList: [1, 2, 3, 4, 5, 6],
showFreeTime: false, //
roomList: [],
businessHours: null, //
loading: false, //
}
},
onLoad() {
this.getDateList();
this.loadBusinessHours();
this.loadRoomList();
},
methods: {
change(e) {
console.log('change', e);
/**
* 加载营业时间
*/
async loadBusinessHours() {
const data = await getBusinessHours();
if (data) {
this.businessHours = data;
}
},
/**
* 加载房间列表
*/
async loadRoomList() {
if (this.loading) return;
this.loading = true;
try {
const selectedDate = this.dateList[this.currentTimeIndex];
const timestamp = this.dateToTimestamp(selectedDate);
const data = await getRoomListWithTimeSlots(timestamp, this.showFreeTime);
if (data) {
this.roomList = data;
} else {
this.roomList = [];
}
} catch (error) {
console.error('加载房间列表失败', error);
uni.showToast({ title: '加载失败', icon: 'none' });
this.roomList = [];
} finally {
this.loading = false;
}
},
/**
* 日期对象转时间戳秒级
* @param {Object} dateItem 日期对象 {time: "12.6", weekday: "今天"}
* @returns {number} 时间戳
*/
dateToTimestamp(dateItem) {
const [month, day] = dateItem.time.split('.');
const year = new Date().getFullYear();
const date = new Date(year, month - 1, day, 0, 0, 0);
return Math.floor(date.getTime() / 1000);
},
/**
* 获取时段颜色
* @param {Object} timeSlots 时段占用信息
* @param {string} period 时段名称dawn/morning/afternoon/evening
* @returns {string} 颜色值
*/
getSlotColor(timeSlots, period) {
if (!timeSlots || !timeSlots[period]) {
return '#E6E6E6'; //
}
return timeSlots[period].is_occupied ? '#FF9901' : '#E6E6E6';
},
/**
* 获取房间状态文本
* @param {string} status 状态值available/using/unavailable
* @returns {string} 状态文本
*/
getRoomStatusText(status) {
const statusMap = {
'available': '可预约',
'using': '当前使用中',
'unavailable': '不可用'
};
return statusMap[status] || '';
},
/**
* 获取房间状态文本颜色
* @param {string} status 状态值
* @returns {string} 颜色值
*/
getRoomStatusColor(status) {
const colorMap = {
'available': '#20BBA4',
'using': '#FF9901',
'unavailable': '#999999'
};
return colorMap[status] || '#999999';
},
/**
* 处理预约按钮点击
* @param {Object} room 房间对象
*/
handleBookRoom(room) {
if (room.status !== 'available') {
const message = room.status === 'using' ? '当前使用中,无法预约' : '房间不可用';
uni.showToast({ title: message, icon: 'none' });
return;
}
//
const dateItem = this.dateList[this.currentTimeIndex];
uni.navigateTo({
url: `/pages/appointment/appointment-page?roomId=${room.id}&roomName=${encodeURIComponent(room.name)}&date=${dateItem.time}`
});
},
/**
* 切换时段显示开关
*/
change(value) {
this.showFreeTime = value;
this.loadRoomList();
},
/**
* 生成日期列表当天+后续6天共7天
*/
@ -120,7 +264,7 @@
const day = current.getDate();
const time = `${month}.${day}`;
// 2.
// 2. ""
let weekday;
if (i === 0) {
weekday = "今天"; //
@ -153,8 +297,12 @@
}
},
/**
* 点击日期切换
*/
clickTime(index) {
this.currentTimeIndex = index;
this.loadRoomList();
}
}
}

BIN
static/default_room.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB