This commit is contained in:
zpc 2025-12-07 21:17:34 +08:00
parent fc04321108
commit a58b4e431b
14 changed files with 3974 additions and 833 deletions

View File

@ -1,390 +0,0 @@
# 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

@ -9,8 +9,8 @@ const development = {
// baseUrl: 'https://sqqp.zpc-xy.com',
// baseUrl: 'http://1.15.21.245:2401',
// host: ['https://sqqp.zpc-xy.com'],
baseUrl: 'http://192.168.1.21:2016',
host: ['http://192.168.1.21:2016'],
baseUrl: 'http://192.168.1.24:2016',
host: ['http://192.168.1.24:2016'],
imageUrl: 'https://guyu-1308826010.cos.ap-shanghai.myqcloud.com',
};

View File

@ -59,16 +59,4 @@ 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

@ -0,0 +1,53 @@
import request from '@/common/system/request';
/**
* 获取消息列表
* @param {number} pageIndex 页码从1开始
* @param {number} pageSize 每页数量
* @param {number} messageType 消息类型0=全部1=私信
* @returns {Promise<Array>}
*/
export const getMessageList = async (pageIndex = 1, pageSize = 20, messageType = 0) => {
const res = await request.getOrCache(
"sq/GetMessageList",
{ pageIndex, pageSize, messageType },
1 // 缓存1秒
);
if (res.code == 0) {
return res.data || [];
}
return [];
}
/**
* 获取未读消息数量
* @returns {Promise<number>}
*/
export const getUnreadCount = async () => {
const res = await request.get("sq/GetUnreadCount");
if (res.code == 0) {
return res.data?.count || 0;
}
return 0;
}
/**
* 标记所有消息为已读
* @returns {Promise<boolean>}
*/
export const markAllAsRead = async () => {
const res = await request.post("sq/MarkAllAsRead");
if (res.code == 0) {
return true;
}
return false;
}
// 导出消息接口对象
export const messageInterface = {
getMessageList,
getUnreadCount,
markAllAsRead
}
export default messageInterface;

View File

@ -141,6 +141,52 @@ export const getPaymentRecords = async (pageIndex = 1, pageSize = 20) => {
return null;
}
/**
* 获取可选日期列表今天+未来7天
* @returns {Promise<any>} [{date:时间戳(), dateText:"今天/明天/后天/日期", dateDisplay:"12月06日 周五"}]
*/
export const getAvailableDates = async () => {
const res = await request.get("sq/GetAvailableDates");
if (res.code == 0) {
return res.data;
}
return null;
}
/**
* 获取房间列表及时段状态按时段预约版本
* @param {number} date 预约日期 时间戳()
* @param {boolean} showOnlyAvailable 是否只显示当前时段可用房间
* @param {number} currentTimeSlot 当前时段类型(0-3)配合showOnlyAvailable使用
* @returns {Promise<any>} 房间列表包含4个时段状态
*/
export const getRoomListWithSlotsNew = async (date, showOnlyAvailable = false, currentTimeSlot = null) => {
const params = { date: date };
if (showOnlyAvailable && currentTimeSlot !== null) {
params.showOnlyAvailable = showOnlyAvailable;
params.currentTimeSlot = currentTimeSlot;
}
const res = await request.get("sq/GetRoomListWithSlotsNew", params);
if (res.code == 0) {
return res.data;
}
return null;
}
/**
* 获取房间详情
* @param {number} roomId 房间ID
* @param {number} date 预约日期 时间戳()
* @returns {Promise<any>} 房间详情数据
*/
export const getRoomDetail = async (roomId, date) => {
const res = await request.get("sq/GetRoomDetail", { roomId: roomId, date: date });
if (res.code == 0) {
return res.data;
}
return null;
}
/**
* 获取可预约的房间列表
* @param {number} startTime 开始时间 时间戳()
@ -254,41 +300,6 @@ 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,
@ -299,9 +310,10 @@ export const sqInterface = {
addEvaluateServices,
getReputationByUser,
getPaymentRecords,
getAvailableDates,
getRoomListWithSlotsNew,
getRoomDetail,
getReservationRoomList,
getRoomListWithTimeSlots,
getReservationRoomListNew,
addSQReservation,
joinReservation,
cancelReservation,

File diff suppressed because it is too large Load Diff

View File

@ -9,277 +9,324 @@
@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;">
营业时间{{ businessHours ? businessHours.description : '加载中...' }}
</text>
<text style="margin-top: 20rpx; margin-left: 32rpx; font-size: 24rpx;">营业时间早00点 晚23点</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="showFreeTime" @change="change"
<up-switch style="margin-right: 16rpx; margin-left: auto; " v-model="value" @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>
<text style="color: #999;">加载中...</text>
</view>
<!-- 数据提示 -->
<!-- 数据提示 -->
<view v-else-if="roomList.length === 0" class="center" style="padding: 60rpx 0;">
<text style="font-size: 28rpx; color: #999;">暂无房间信息</text>
<text style="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;">
<view class="" v-for="item in roomList" :key="item.id"
style="position: relative; width: 706rpx; height: 270rpx; border-radius: 20rpx; background-color: white; margin: 0 auto 20rpx;"
@click.stop="handleRoomClick(item)">
<!-- 房间图片 -->
<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="aspectFill"></image>
<!-- 房间信息 -->
<view class="column" style=" position: absolute; left: 200rpx; top: 20rpx;">
<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: 20rpx;">{{ item.room_type_name }}</text>
<text style="font-size: 24rpx; margin-top: 24rpx;">¥{{ item.price_per_hour }}/小时</text>
<text style="font-size: 24rpx; color: #FF0000;">会员价¥XX/小时</text>
<text style="font-size: 24rpx; margin-top: 24rpx;">{{ item.standard_price_desc || '价格详询' }}</text>
<text style="font-size: 24rpx; color: #FF0000;">会员价{{ item.member_price_desc || '详询' }}</text>
</view>
<!-- 房间状态 -->
<text v-if="getRoomStatusText(item.status)"
:style="{fontSize: '20rpx', position: 'absolute', top: '10rpx', right: '16rpx', color: getRoomStatusColor(item.status)}">
{{ getRoomStatusText(item.status) }}
</text>
<text v-if="item.status === 'using'"
style="font-size: 20rpx; position: absolute; top: 10rpx; right: 16rpx; color: #FF9901;">当前使用中</text>
<!-- 预约按钮 -->
<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 class="center" @click.stop="handleReservation(item)"
style="width: 92rpx; height: 46rpx; background-color: #20BBA4; border-radius: 52rpx; font-size: 26rpx; color: white; position: absolute; bottom: 94rpx; right: 10rpx;">
预约
</view>
<!-- 时段说明 - 仅在showFreeTime为true时显示 -->
<view v-if="showFreeTime" class="row" style="align-items: center; position: absolute; left: 10rpx; bottom: 60rpx;">
<view 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: #52c41a; margin-left: 22rpx;"></view>
<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>
<!-- 时段占用情况 - 仅在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', 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', 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', 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', backgroundColor: getSlotColor(item.time_slots, 'evening')}"></view>
<text style="font-size: 12rpx; margin-top: 4rpx;">晚上</text>
<view class="row" style="align-items: center; position: absolute; left: 10rpx; bottom: 14rpx;">
<view class="column center" style="margin-right: 20rpx;" v-for="slot in item.time_slots" :key="slot.slot_type">
<view :style="{ width: '70rpx', height: '4rpx', backgroundColor: getSlotColor(slot.status) }"></view>
<text style="font-size: 12rpx; margin-top: 4rpx;">{{ slot.slot_name }}</text>
</view>
</view>
</view>
</view>
<!-- 房间详情弹窗 -->
<uni-popup ref="roomDetailPopup" type="center" :is-mask-click="false">
<view class="room-detail-popup">
<!-- 关闭按钮 -->
<view class="popup-close" @click="closeRoomDetail">
<text style="font-size: 40rpx; color: #999;">×</text>
</view>
<!-- 加载状态 -->
<view v-if="roomDetailLoading" class="popup-loading center">
<text style="color: #999;">加载中...</text>
</view>
<!-- 房间详情内容 -->
<scroll-view v-else-if="roomDetail" scroll-y class="popup-content" :style="{ maxHeight: '80vh' }">
<!-- 图片轮播 -->
<view class="detail-swiper-container">
<swiper v-if="roomDetailImages && roomDetailImages.length > 0"
class="detail-swiper"
:indicator-dots="roomDetailImages.length > 1"
:autoplay="false"
:circular="true"
indicator-color="rgba(255, 255, 255, 0.5)"
indicator-active-color="#20BBA4">
<swiper-item v-for="(img, index) in roomDetailImages" :key="index">
<image :src="img" mode="aspectFill" class="detail-swiper-image"></image>
</swiper-item>
</swiper>
<image v-else :src="roomDetail.image_url || '/static/default_room.jpg'"
mode="aspectFill"
class="detail-swiper-image"></image>
</view>
<!-- 房间基本信息 -->
<view class="detail-section">
<view class="detail-title-row">
<text class="detail-room-name">{{ roomDetail.name }}</text>
<view v-if="roomDetail.status === 'using'" class="detail-status-tag using">
<text>使用中</text>
</view>
<view v-else-if="roomDetail.can_reserve" class="detail-status-tag available">
<text>可预约</text>
</view>
</view>
<text class="detail-room-type">{{ roomDetail.room_type_name }}</text>
<view class="detail-price-row">
<text class="detail-price-label">标准价</text>
<text class="detail-price-value">{{ roomDetail.standard_price_desc || '价格详询' }}</text>
</view>
<view class="detail-price-row">
<text class="detail-price-label">会员价</text>
<text class="detail-price-member">{{ roomDetail.member_price_desc || '详询' }}</text>
</view>
<view class="detail-info-row">
<text class="detail-info-label">可容纳人数</text>
<text class="detail-info-value">{{ roomDetail.capacity || 0 }}</text>
</view>
</view>
<!-- 房间描述 -->
<view v-if="roomDetail.description" class="detail-section">
<text class="detail-section-title">房间描述</text>
<text class="detail-description">{{ roomDetail.description }}</text>
</view>
<!-- 设施标签 -->
<view v-if="roomDetail.amenities && roomDetail.amenities.length > 0" class="detail-section">
<text class="detail-section-title">房间设施</text>
<view class="amenities-container">
<view v-for="(amenity, index) in roomDetail.amenities" :key="index" class="amenity-tag">
<text>{{ amenity }}</text>
</view>
</view>
</view>
<!-- 时段状态 -->
<view class="detail-section">
<text class="detail-section-title">时段状态</text>
<view class="time-slots-container">
<view v-for="slot in roomDetail.time_slots" :key="slot.slot_type" class="time-slot-item">
<view class="time-slot-header">
<text class="time-slot-name">{{ slot.slot_name }}</text>
<view :style="{
width: '12rpx',
height: '12rpx',
borderRadius: '50%',
backgroundColor: getSlotColor(slot.status),
marginLeft: '12rpx'
}"></view>
</view>
<view class="time-slot-content">
<text class="time-slot-status">{{ getSlotStatusText(slot.status) }}</text>
<view class="time-slot-price">
<text class="time-slot-price-text">标准{{ slot.price_desc_standard || '详询' }}</text>
<text class="time-slot-price-text member">会员{{ slot.price_desc_member || '详询' }}</text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 底部操作按钮 -->
<view v-if="roomDetail && !roomDetailLoading" class="popup-footer">
<view class="popup-btn secondary" @click="closeRoomDetail">
<text>关闭</text>
</view>
<view v-if="roomDetail.can_reserve" class="popup-btn primary" @click="handleReservationFromDetail">
<text>立即预约</text>
</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import { getRoomListWithTimeSlots } from '@/common/server/interface/sq.js';
import { getBusinessHours } from '@/common/server/interface/common.js';
import { getAvailableDates, getRoomListWithSlotsNew, getRoomDetail } from '@/common/server/interface/sq.js';
export default {
data() {
return {
currentTimeIndex: 0,
dateList: [],
showFreeTime: false, //
value: false,
roomList: [],
businessHours: null, //
loading: false, //
loading: false,
selectedDate: null, //
roomDetail: null, //
roomDetailLoading: false, //
}
},
onLoad() {
this.getDateList();
this.loadBusinessHours();
this.loadRoomList();
this.loadDates();
},
methods: {
change(e) {
console.log('change', e);
//
this.loadRoomList();
},
/**
* 加载营业时间
* 加载可选日期列表
*/
async loadBusinessHours() {
const data = await getBusinessHours();
if (data) {
this.businessHours = data;
async loadDates() {
try {
const res = await getAvailableDates();
if (res && res.length > 0) {
// 使
this.dateList = res.map(item => ({
time: item.dateText,
weekday: item.dateDisplay,
timestamp: item.date //
}));
//
this.selectedDate = this.dateList[0].timestamp;
this.currentTimeIndex = 0;
//
this.loadRoomList();
}
} catch (error) {
console.error('加载日期列表失败', error);
uni.showToast({
title: '加载日期失败',
icon: 'none'
});
}
},
/**
* dateDisplay 中提取星期部分
* @param {string} dateDisplay 例如"12月06日 周五"
* @returns {string} 例如"周五"
*/
extractWeekday(dateDisplay) {
const parts = dateDisplay.split(' ');
return parts.length > 1 ? parts[1] : '';
},
/**
* 加载房间列表
*/
async loadRoomList() {
if (this.loading) return;
if (!this.selectedDate) return;
this.loading = true;
try {
const selectedDate = this.dateList[this.currentTimeIndex];
const timestamp = this.dateToTimestamp(selectedDate);
const showOnlyAvailable = this.value; //
const currentTimeSlot = this.getCurrentTimeSlot();
const data = await getRoomListWithTimeSlots(timestamp, this.showFreeTime);
if (data) {
this.roomList = data;
const res = await getRoomListWithSlotsNew(
this.selectedDate,
showOnlyAvailable,
showOnlyAvailable ? currentTimeSlot : null
);
if (res) {
this.roomList = res;
} else {
this.roomList = [];
}
} catch (error) {
console.error('加载房间列表失败', error);
uni.showToast({ title: '加载失败', icon: 'none' });
uni.showToast({
title: '加载房间失败',
icon: 'none'
});
this.roomList = [];
} finally {
this.loading = false;
}
},
/**
* 日期对象转时间戳秒级
* @param {Object} dateItem 日期对象 {time: "12.6", weekday: "今天"}
* @returns {number} 时间戳
* 获取当前时段类型
* @returns {number} 0=凌晨, 1=上午, 2=下午, 3=晚上
*/
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);
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; //
},
/**
* 获取时段颜色
* @param {Object} timeSlots 时段占用信息
* @param {string} period 时段名称dawn/morning/afternoon/evening
* 根据时段状态获取背景色
* @param {string} status available/reserved/unavailable/using
* @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) {
getSlotColor(status) {
const colorMap = {
'available': '#20BBA4',
'using': '#FF9901',
'unavailable': '#999999'
'available': '#52c41a', // 绿
'reserved': '#FF9901', //
'unavailable': '#E6E6E6', //
'using': '#1890ff' //
};
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天
*/
getDateList() {
const list = [];
const today = new Date(); //
const weekNames = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
for (let i = 0; i < 7; i++) {
const current = new Date();
current.setDate(today.getDate() + i); // i
// 1. .10.112.30
const month = current.getMonth() + 1; // 0
const day = current.getDate();
const time = `${month}.${day}`;
// 2. ""
let weekday;
if (i === 0) {
weekday = "今天"; //
} else {
const weekIndex = current.getDay(); // 0=6=
weekday = weekNames[weekIndex];
}
list.push({
time,
weekday
});
}
this.dateList = list;
return colorMap[status] || '#E6E6E6';
},
//
@ -297,12 +344,140 @@
}
},
/**
* 点击日期切换
*/
clickTime(index) {
this.currentTimeIndex = index;
this.selectedDate = this.dateList[index].timestamp;
//
this.loadRoomList();
},
/**
* 点击预约按钮
*/
handleReservation(room) {
if (!room || !room.id) return;
//
const params = {
roomId: room.id,
roomName: room.name || '未知房间',
date: this.selectedDate, //
};
//
if (room.capacity) {
params.capacity = room.capacity;
}
const query = Object.keys(params)
.map(key => `${key}=${encodeURIComponent(params[key])}`)
.join('&');
uni.navigateTo({
url: `/pages/appointment/appointment-page?${query}`
});
},
/**
* 点击房间卡片显示详情弹窗
*/
async handleRoomClick(room) {
if (!room || !room.id) return;
this.roomDetailLoading = true;
this.roomDetail = null;
//
this.$refs.roomDetailPopup.open();
try {
const detail = await getRoomDetail(room.id, this.selectedDate);
if (detail) {
this.roomDetail = detail;
} else {
uni.showToast({
title: '获取房间详情失败',
icon: 'none'
});
this.closeRoomDetail();
}
} catch (error) {
console.error('加载房间详情失败', error);
uni.showToast({
title: '加载房间详情失败',
icon: 'none'
});
this.closeRoomDetail();
} finally {
this.roomDetailLoading = false;
}
},
/**
* 关闭房间详情弹窗
*/
closeRoomDetail() {
this.$refs.roomDetailPopup.close();
this.roomDetail = null;
},
/**
* 从详情弹窗中点击预约
*/
handleReservationFromDetail() {
if (this.roomDetail) {
this.closeRoomDetail();
//
setTimeout(() => {
//
const params = {
roomId: this.roomDetail.id,
roomName: this.roomDetail.name || '未知房间',
date: this.selectedDate,
};
if (this.roomDetail.capacity) {
params.capacity = this.roomDetail.capacity;
}
const query = Object.keys(params)
.map(key => `${key}=${encodeURIComponent(params[key])}`)
.join('&');
uni.navigateTo({
url: `/pages/appointment/appointment-page?${query}`
});
}, 300);
}
},
/**
* 获取时段状态文本
*/
getSlotStatusText(status) {
const statusMap = {
'available': '可预约',
'reserved': '已预约',
'unavailable': '不可预约',
'using': '使用中'
};
return statusMap[status] || '未知';
}
},
computed: {
/**
* 计算属性房间图片列表
*/
roomDetailImages() {
if (!this.roomDetail) return [];
if (this.roomDetail.images && this.roomDetail.images.length > 0) {
return this.roomDetail.images;
}
if (this.roomDetail.image_url) {
return [this.roomDetail.image_url];
}
return [];
}
}
}
@ -321,4 +496,248 @@
text-align: center;
margin-bottom: 20rpx;
}
/* 房间详情弹窗样式 */
.room-detail-popup {
width: 680rpx;
max-height: 80vh;
background-color: #FFFFFF;
border-radius: 20rpx;
position: relative;
overflow: hidden;
}
.popup-close {
position: absolute;
top: 10rpx;
right: 10rpx;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 50%;
}
.popup-loading {
padding: 100rpx 0;
min-height: 400rpx;
}
.popup-content {
width: 100%;
}
/* 图片轮播容器 */
.detail-swiper-container {
width: 100%;
height: 400rpx;
background-color: #DEDEDE;
}
.detail-swiper {
width: 100%;
height: 100%;
}
.detail-swiper-image {
width: 100%;
height: 100%;
}
/* 详情区域 */
.detail-section {
padding: 30rpx;
border-bottom: 1rpx solid #F0F0F0;
}
.detail-section:last-child {
border-bottom: none;
}
.detail-title-row {
display: flex;
align-items: center;
margin-bottom: 10rpx;
}
.detail-room-name {
font-size: 36rpx;
font-weight: bold;
color: #262626;
flex: 1;
}
.detail-status-tag {
padding: 4rpx 16rpx;
border-radius: 8rpx;
font-size: 20rpx;
}
.detail-status-tag.using {
background-color: #FF9901;
color: #FFFFFF;
}
.detail-status-tag.available {
background-color: #52c41a;
color: #FFFFFF;
}
.detail-room-type {
font-size: 24rpx;
color: #999999;
margin-bottom: 20rpx;
}
.detail-price-row {
display: flex;
align-items: center;
margin-bottom: 10rpx;
}
.detail-price-label {
font-size: 24rpx;
color: #666666;
}
.detail-price-value {
font-size: 28rpx;
color: #262626;
font-weight: 500;
}
.detail-price-member {
font-size: 28rpx;
color: #FF0000;
font-weight: 500;
}
.detail-info-row {
display: flex;
align-items: center;
margin-top: 20rpx;
}
.detail-info-label {
font-size: 24rpx;
color: #666666;
}
.detail-info-value {
font-size: 24rpx;
color: #262626;
}
.detail-section-title {
font-size: 28rpx;
font-weight: bold;
color: #262626;
margin-bottom: 20rpx;
display: block;
}
.detail-description {
font-size: 26rpx;
color: #666666;
line-height: 1.6;
}
/* 设施标签 */
.amenities-container {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.amenity-tag {
padding: 8rpx 20rpx;
background-color: #F5F5F5;
border-radius: 8rpx;
font-size: 24rpx;
color: #666666;
}
/* 时段状态 */
.time-slots-container {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.time-slot-item {
background-color: #F8F8F8;
border-radius: 12rpx;
padding: 20rpx;
}
.time-slot-header {
display: flex;
align-items: center;
margin-bottom: 12rpx;
}
.time-slot-name {
font-size: 26rpx;
font-weight: 500;
color: #262626;
}
.time-slot-content {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.time-slot-status {
font-size: 24rpx;
color: #666666;
}
.time-slot-price {
display: flex;
align-items: center;
gap: 20rpx;
}
.time-slot-price-text {
font-size: 22rpx;
color: #666666;
}
.time-slot-price-text.member {
color: #FF0000;
}
/* 底部操作按钮 */
.popup-footer {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
border-top: 1rpx solid #F0F0F0;
gap: 20rpx;
}
.popup-btn {
flex: 1;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12rpx;
font-size: 28rpx;
font-weight: 500;
}
.popup-btn.secondary {
background-color: #F5F5F5;
color: #666666;
}
.popup-btn.primary {
background-color: #20BBA4;
color: #FFFFFF;
}
</style>

View File

@ -18,18 +18,38 @@
<view class="column"
style="width: 100%; flex: 1; margin-top: 20rpx; overflow-y: auto;">
<view class="column" v-for="(item,index) in messageList"
style="width: 686rpx; margin: 0rpx auto 30rpx; background-color: white; border-radius: 32rpx; ">
<text style="font-size: 32rpx; margin-top: 22rpx; margin-left: 30rpx;">标题标题标题</text>
<!-- 消息列表 -->
<view class="column" v-for="(item,index) in messageList" :key="item.id"
style="width: 686rpx; margin: 0rpx auto 30rpx; background-color: white; border-radius: 32rpx; padding: 22rpx 30rpx 10rpx;">
<!-- 消息标题 -->
<view class="row" style="align-items: center; justify-content: space-between;">
<text style="font-size: 32rpx; font-weight: bold;">{{ item.title || '无标题' }}</text>
<!-- 未读标识 -->
<view v-if="!item.isRead"
style="width: 16rpx; height: 16rpx; background-color: #FF3B30; border-radius: 50%;"></view>
</view>
<text
style="font-size: 32rpx; margin-top: 10rpx; margin-left: 30rpx;">正文正文正文正文正文正文正文正文正文正文正文正文正文正文正文正文正文正文正文正文正文正文
正文正文正文正文正文正文正文</text>
<text
style="font-size: 32rpx; margin-top: 70rpx; margin-left: auto; margin-right: 18rpx; margin-bottom: 10rpx;">2025//1/1
1112</text>
<!-- 消息正文 -->
<text style="font-size: 28rpx; margin-top: 10rpx; color: #666; line-height: 1.6;">
{{ item.content || '暂无内容' }}
</text>
<!-- 消息时间 -->
<text style="font-size: 24rpx; margin-top: 20rpx; margin-left: auto; color: #999;">
{{ formatTime(item.createTime) }}
</text>
</view>
<!-- 空数据提示 -->
<view v-if="!loading && messageList.length === 0"
class="column center" style="width: 100%; padding-top: 200rpx;">
<image src="/static/no_content.png" style="width: 200rpx; height: 200rpx;" mode=""></image>
<text style="font-size: 28rpx; color: #999; margin-top: 30rpx;">暂无消息</text>
</view>
<!-- 加载提示 -->
<view v-if="loading" class="center" style="width: 100%; padding: 40rpx 0;">
<text style="font-size: 28rpx; color: #999;">加载中...</text>
</view>
</view>
@ -39,14 +59,26 @@
</template>
<script>
import { getMessageList, markAllAsRead } from '@/common/server/interface/message.js'
export default {
data() {
return {
currentIndex: 0,
teabList: ["全部", "私信"],
messageList: [1, 2, 3, 4, 5]
messageList: [],
pageIndex: 1,
pageSize: 20,
loading: false,
hasMore: true
}
},
onLoad() {
//
this.loadMessageList();
//
this.markMessagesRead();
},
methods: {
//
goBack() {
@ -73,6 +105,72 @@
clickTab(index) {
this.currentIndex = index;
//
this.pageIndex = 1;
this.hasMore = true;
this.loadMessageList();
},
/**
* 加载消息列表
*/
async loadMessageList() {
if (this.loading) return;
try {
this.loading = true;
// messageType: 0=1=
const messageType = this.currentIndex;
const data = await getMessageList(this.pageIndex, this.pageSize, messageType);
if (this.pageIndex === 1) {
this.messageList = data || [];
} else {
this.messageList = [...this.messageList, ...(data || [])];
}
//
this.hasMore = data && data.length >= this.pageSize;
} catch (error) {
console.error('获取消息列表失败:', error);
uni.showToast({
title: '获取消息失败',
icon: 'none'
});
} finally {
this.loading = false;
}
},
/**
* 标记所有消息为已读
*/
async markMessagesRead() {
try {
await markAllAsRead();
} catch (error) {
console.error('标记已读失败:', error);
}
},
/**
* 格式化时间显示
*/
formatTime(createTime) {
if (!createTime) return '';
//
if (typeof createTime === 'number') {
const date = new Date(createTime);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hour = String(date.getHours()).padStart(2, '0');
const minute = String(date.getMinutes()).padStart(2, '0');
return `${year}/${month}/${day} ${hour}:${minute}`;
}
//
return createTime.replace(/-/g, '/');
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,197 @@
# 我的消息页面接口参数说明
## 📋 页面功能概述
**页面路径**`pages/me/my-message-page.vue`
**主要功能**
- 显示用户的消息列表
- 支持按类型筛选(全部/私信)
- 展示消息的标题、正文内容和时间
---
## 🔌 接口需求
### 接口1获取消息列表
#### 基本信息
- **接口路径**:建议 `GET /api/user/GetMessageList``POST /api/user/GetMessageList`
- **调用时机**
- 页面初始化时
- 切换标签(全部/私信)时
- 下拉刷新时
- **是否需要登录**需要Token
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|-------|------|------|------|------|
| pageIndex | number | 否 | 页码从1开始 | 1 |
| pageSize | number | 否 | 每页数量 | 20 |
| messageType | number | 否 | 消息类型0=全部1=私信 | 0 |
**请求参数说明**
- `messageType`:根据页面标签 `currentIndex` 传递
- `currentIndex = 0`(全部)→ `messageType = 0` 或不传
- `currentIndex = 1`(私信)→ `messageType = 1`
#### 返回数据结构
```typescript
interface Response {
code: number; // 0=成功
msg: string; // 消息
data: MessageItem[]; // 消息列表
}
interface MessageItem {
id: number; // 消息ID必填
title: string; // 消息标题(必填)
content: string; // 消息正文内容(必填)
createTime: string; // 创建时间格式YYYY-MM-DD HH:mm 或时间戳(必填)
messageType: number; // 消息类型0=系统消息1=私信(可选)
isRead: boolean; // 是否已读(可选)
}
```
#### 返回示例
```json
{
"code": 0,
"msg": "ok",
"data": [
{
"id": 1,
"title": "系统通知",
"content": "您的预约已成功,请按时到达。",
"createTime": "2025-01-01 11:12",
"messageType": 0,
"isRead": false
},
{
"id": 2,
"title": "私信消息",
"content": "您好,我想咨询一下房间预约的相关问题。",
"createTime": "2025-01-01 10:30",
"messageType": 1,
"isRead": true
}
]
}
```
---
## 📝 页面数据映射
### 当前页面显示字段
根据 `my-message-page.vue` 模板代码分析:
| 页面显示位置 | 对应字段 | 数据类型 | 说明 |
|------------|---------|---------|------|
| 第23行标题 | `title` | string | 消息标题 |
| 第25-27行正文 | `content` | string | 消息正文内容 |
| 第29-31行时间 | `createTime` | string | 消息创建时间 |
### 标签筛选逻辑
| 标签索引 | 标签名称 | 对应参数值 | 说明 |
|---------|---------|-----------|------|
| 0 | 全部 | `messageType = 0` 或不传 | 显示所有消息 |
| 1 | 私信 | `messageType = 1` | 只显示私信类型消息 |
---
## 🔄 接口调用建议
### 1. 接口定义位置
建议在 `common/server/interface/user.js` 中添加:
```javascript
/**
* 获取消息列表
* @param {number} pageIndex 页码
* @param {number} pageSize 每页数量
* @param {number} messageType 消息类型0=全部1=私信
* @returns {Promise<MessageItem[]>}
*/
export const getMessageList = async (pageIndex = 1, pageSize = 20, messageType = 0) => {
const res = await request.getOrCache(
"user/GetMessageList",
{ pageIndex, pageSize, messageType },
1 // 缓存1秒
);
if (res.code == 0) {
return res.data;
}
return [];
}
```
### 2. 页面调用示例
```javascript
import { getMessageList } from '@/common/server/interface/user.js'
// 在 methods 中添加
async loadMessageList() {
try {
const type = this.currentIndex === 0 ? 0 : 1;
const data = await getMessageList(1, 20, type);
this.messageList = data || [];
} catch (error) {
console.error('获取消息列表失败:', error);
uni.showToast({
title: '获取消息失败',
icon: 'none'
});
}
}
// 在 clickTab 方法中调用
clickTab(index) {
this.currentIndex = index;
this.loadMessageList(); // 切换标签时重新加载
}
```
---
## ✅ 必填字段总结
### 接口返回必须包含的字段:
1. **id** (number) - 消息ID用于唯一标识
2. **title** (string) - 消息标题显示在第23行
3. **content** (string) - 消息正文显示在第25-27行
4. **createTime** (string) - 创建时间显示在第29-31行
### 可选但建议包含的字段:
- **messageType** (number) - 用于前端筛选和分类
- **isRead** (boolean) - 用于显示未读标识(如果后续需要)
---
## 📌 注意事项
1. **时间格式**:建议后端返回格式化的时间字符串(如:`2025-01-01 11:12`),或返回时间戳由前端格式化
2. **分页支持**:如果消息数量较多,建议支持分页加载
3. **空数据处理**:当没有消息时,返回空数组 `[]`
4. **错误处理**:接口失败时返回 `code != 0`,前端需要处理错误情况
---
## 🔍 后续扩展建议
如果后续需要添加以下功能,可以考虑增加字段:
- **未读消息数**:在标签上显示未读数量
- **消息详情**:点击消息跳转到详情页,需要 `id` 字段
- **删除消息**:需要 `id` 字段
- **标记已读**:需要 `id``isRead` 字段

View File

@ -0,0 +1,252 @@
# 消息功能前端接入说明
## 完成时间
2025-12-07
## 已完成的工作
### 1. API 接口文件
**文件位置**`common/server/interface/message.js`
**包含的接口**
- `getMessageList(pageIndex, pageSize, messageType)` - 获取消息列表
- `getUnreadCount()` - 获取未读消息数量
- `markAllAsRead()` - 标记所有消息为已读
**接口说明**
```javascript
// 获取消息列表
// pageIndex: 页码从1开始默认1
// pageSize: 每页数量默认20
// messageType: 消息类型0=全部1=私信
const messageList = await getMessageList(1, 20, 0);
// 获取未读消息数量
const unreadCount = await getUnreadCount();
// 标记所有消息为已读
const success = await markAllAsRead();
```
### 2. 消息页面
**文件位置**`pages/me/my-message-page.vue`
**已实现的功能**
- ✅ 页面加载时自动获取消息列表
- ✅ 页面加载时自动标记所有消息为已读
- ✅ 标签切换(全部/私信)时重新加载对应消息
- ✅ 显示消息标题、内容、时间
- ✅ 显示未读消息红点标识
- ✅ 空数据时显示提示
- ✅ 加载状态显示
- ✅ 时间格式化显示(支持时间戳和字符串格式)
### 3. 数据绑定说明
**消息对象结构**
```javascript
{
id: 1, // 消息ID必填
title: "消息标题", // 标题(必填)
content: "消息内容", // 正文内容(必填)
createTime: "2025-12-07 10:30", // 创建时间(必填)
messageType: 0, // 消息类型0=系统通知1=私信
isRead: false // 是否已读
}
```
---
## 接口路径重要提示
⚠️ **注意**:实际接口路径使用 `/api/sq/` 前缀,而不是需求文档中建议的 `/api/user/`
**实际接口地址**
- `GET /api/sq/GetMessageList` - 获取消息列表
- `GET /api/sq/GetUnreadCount` - 获取未读数量
- `POST /api/sq/MarkAllAsRead` - 标记全部已读
**原因**:站内信功能统一放在 SQ预约模块下管理便于后续维护。
---
## 页面使用方式
### 从其他页面跳转到消息页面
```javascript
// 跳转到消息页面
uni.navigateTo({
url: '/pages/me/my-message-page'
});
```
### 显示未读消息数量(红点提示)
在"我的"页面或其他需要显示未读消息的地方:
```vue
<template>
<view class="message-icon" @click="goToMessage">
<image src="/static/message.png"></image>
<!-- 未读消息红点 -->
<view v-if="unreadCount > 0" class="badge">{{ unreadCount }}</view>
</view>
</template>
<script>
import { getUnreadCount } from '@/common/server/interface/message.js'
export default {
data() {
return {
unreadCount: 0
}
},
onShow() {
// 页面显示时获取未读消息数量
this.loadUnreadCount();
},
methods: {
async loadUnreadCount() {
this.unreadCount = await getUnreadCount();
},
goToMessage() {
uni.navigateTo({
url: '/pages/me/my-message-page'
});
}
}
}
</script>
<style>
.message-icon {
position: relative;
}
.badge {
position: absolute;
top: -10rpx;
right: -10rpx;
background-color: #FF3B30;
color: white;
font-size: 20rpx;
padding: 4rpx 8rpx;
border-radius: 20rpx;
min-width: 32rpx;
text-align: center;
}
</style>
```
---
## 数据流程说明
### 1. 用户进入消息页面
1. 调用 `getMessageList(1, 20, 0)` 获取第一页"全部"消息
2. 调用 `markAllAsRead()` 标记所有消息为已读
3. 渲染消息列表
### 2. 用户切换到"私信"标签
1. `currentIndex` 变为 1
2. 调用 `getMessageList(1, 20, 1)` 获取私信消息
3. 更新消息列表显示
### 3. 用户返回"我的"页面
1. 未读消息数量应该变为 0因为进入消息页面时已标记为已读
---
## 后续优化建议
### 短期优化(可选)
1. **下拉刷新**:添加下拉刷新功能
```vue
<scroll-view
scroll-y
@scrolltolower="loadMore"
refresher-enabled
@refresherrefresh="onRefresh">
<!-- 消息列表 -->
</scroll-view>
```
2. **上拉加载更多**:支持分页加载
```javascript
async loadMore() {
if (!this.hasMore || this.loading) return;
this.pageIndex++;
await this.loadMessageList();
}
```
3. **消息详情页**:点击消息查看完整内容
```javascript
goToDetail(messageId) {
uni.navigateTo({
url: `/pages/me/message-detail?id=${messageId}`
});
}
```
### 长期优化(可选)
1. 消息删除功能
2. 消息搜索功能
3. 消息分类标签
4. 推送通知集成
---
## 测试清单
### 功能测试
- [ ] 页面正常显示消息列表
- [ ] 切换"全部/私信"标签功能正常
- [ ] 未读消息显示红点
- [ ] 时间格式正确显示
- [ ] 空数据时显示"暂无消息"提示
- [ ] 返回按钮功能正常
### 接口测试
- [ ] 能正常获取消息列表
- [ ] 能正常获取未读数量
- [ ] 能正常标记消息为已读
### 异常测试
- [ ] 网络异常时显示错误提示
- [ ] 接口返回错误时不会崩溃
- [ ] 数据为空时正常显示
---
## 常见问题
### Q1: 为什么进入消息页面后未读数量还是显示?
A: 需要在"我的"页面的 `onShow()` 生命周期中重新调用 `getUnreadCount()` 获取最新未读数量。
### Q2: 时间格式显示不正确?
A: 后端可能返回时间戳或字符串格式,前端 `formatTime()` 方法已做兼容处理,支持两种格式。
### Q3: 切换标签后消息没有更新?
A: 检查 `clickTab()` 方法是否正确调用了 `loadMessageList()`,并且 `pageIndex` 已重置为 1。
### Q4: 接口返回404
A: 检查接口路径是否为 `/api/sq/GetMessageList`,确保后端已正确部署。
---
## 相关文档
- [我的消息页面接口参数说明.md](./我的消息页面接口参数说明.md) - 详细接口参数说明
- [站内信功能开发总结.md](./站内信功能开发总结.md) - 后端功能开发总结
---
## 联系支持
如遇到问题,请检查:
1. 后端是否已部署站内信功能
2. 数据库表是否已创建
3. Token 是否有效(需要登录)
4. 网络请求是否正常

View File

@ -0,0 +1,293 @@
# 站内信功能开发总结
## 开发完成时间
2025-12-07
## 功能概述
新增独立的站内信消息系统,支持指定用户消息和全员广播,集成到预约系统的组局成功/失败自动通知。
---
## 一、数据库表
### 1. SQMessage站内信消息表
**位置**`数据库\SqlServer\创建站内信表.sql`
**字段说明**
- `id`消息ID主键自增
- `user_id`接收用户ID全员广播时为NULL
- `target_type`目标类型0=指定用户1=全员广播)
- `title`:消息标题
- `content`:消息正文
- `message_type`消息类型0=系统通知1=私信)
- `is_read`:是否已读(仅对指定用户消息有效)
- `sender_id`发送者ID后台管理员ID
- `related_type`关联业务类型1=组局)
- `related_id`关联业务ID
- `created_at`:创建时间
- `updated_at`:更新时间
### 2. SQMessageRead已读记录表
**用途**:专门记录全员广播消息的已读状态
**字段说明**
- `id`记录ID主键自增
- `message_id`消息ID
- `user_id`用户ID
- `read_at`:阅读时间
**索引**
- 唯一索引:`IX_SQMessageRead_msg_user (message_id, user_id)` 防止重复
- 普通索引:`IX_SQMessageRead_user_id` 查询优化
---
## 二、代码文件清单
### Model层实体
- `CoreCms.Net.Model\Entities\SQ\SQMessage.cs`
- `CoreCms.Net.Model\Entities\SQ\SQMessageRead.cs`
### Repository层数据访问
- `CoreCms.Net.IRepository\SQ\ISQMessageRepository.cs`
- `CoreCms.Net.IRepository\SQ\ISQMessageReadRepository.cs`
- `CoreCms.Net.Repository\SQ\SQMessageRepository.cs`
- `CoreCms.Net.Repository\SQ\SQMessageReadRepository.cs`
### Services层业务逻辑
- `CoreCms.Net.IServices\SQ\ISQMessageServices.cs`
- `CoreCms.Net.Services\SQ\SQMessageServices.cs`
### Controller层
- **前端API**`CoreCms.Net.Web.WebApi\Controllers\SQController.cs`新增3个接口
- **后台管理**`CoreCms.Net.Web.Admin\Controllers\SQ\SQMessageController.cs`
### 后台页面
- `CoreCms.Net.Web.Admin\wwwroot\views\sq\sqmessage\index.html`(消息列表页)
- `CoreCms.Net.Web.Admin\wwwroot\views\sq\sqmessage\details.html`(消息详情页)
- `CoreCms.Net.Web.Admin\wwwroot\views\sq\sqmessage\broadcast.html`(发送全员广播页)
### 业务集成
- `CoreCms.Net.Services\SQ\SQReservationsServices.cs`(修改,添加站内信通知)
---
## 三、前端API接口
### 1. 获取消息列表
**接口**`GET /api/SQ/GetMessageList`
**参数**
- `pageIndex`页码默认1
- `pageSize`每页数量默认20
- `messageType`消息类型0=全部1=私信)
**返回**
```json
{
"code": 0,
"msg": "获取成功",
"data": [
{
"id": 1,
"title": "组局成功通知",
"content": "恭喜您!组局"周末欢乐局"已成功!",
"createTime": "2025-12-07 10:30",
"messageType": 0,
"isRead": false
}
]
}
```
### 2. 获取未读消息数量
**接口**`GET /api/SQ/GetUnreadCount`
**返回**
```json
{
"code": 0,
"msg": "获取成功",
"data": {
"count": 5
}
}
```
### 3. 标记所有消息为已读
**接口**`POST /api/SQ/MarkAllAsRead`
**返回**
```json
{
"code": 0,
"msg": "标记成功"
}
```
---
## 四、后台管理功能
### 1. 消息列表管理(已开发页面)
**访问路径**:后台菜单 → SQ管理 → 站内信管理
**功能特性**
- 查看所有消息记录
- 筛选条件消息ID、标题、目标类型指定用户/全员广播)、消息类型(系统通知/私信)
- 支持查看详情、删除、批量删除
- 数据分页显示可选10-200条/页)
### 2. 发送全员广播(已开发页面)
**操作方式**
1. 在消息列表页点击"发送全员广播"按钮
2. 填写消息标题和内容
3. 点击"立即发送",消息将发送给所有用户
**接口**`POST /api/SQMessage/SendBroadcast`
**请求示例**
```json
{
"title": "系统维护通知",
"content": "系统将于今晚22:00-23:00进行维护请提前做好准备。",
"senderId": 1
}
```
### 3. 其他管理接口
**后台可调用的接口**
- `POST /api/SQMessage/SendToUser`发送给指定用户可通过API调用
- `POST /api/SQMessage/SendToUsers`发送给多个用户可通过API调用
- `POST /api/SQMessage/GetUserList`:查询用户列表(用于扩展发送对象选择)
---
## 五、自动通知场景
### 1. 组局成功通知
**触发时机**:预约系统调用 `NotifyReservationSuccessAsync`
**通知内容**
```
标题:组局成功通知
内容:恭喜您!组局"{组局名称}"已成功!
房间:{房间名称}
时间:{开始时间}
请准时到达,祝您游戏愉快!
```
### 2. 组局失败通知
**触发时机**:预约系统调用 `NotifyReservationFailedAsync`
**通知内容**
```
标题:组局失败通知
内容:您参与的组局"{组局名称}"因人数未满已自动解散。
房间:{房间名称}
时间:{开始时间}
原因:{失败原因}
```
---
## 六、查询逻辑说明
### 用户消息列表查询
合并查询:
- 指定给当前用户的消息(`target_type=0` 且 `user_id=当前用户`
- 全员广播消息(`target_type=1`
### 已读状态判断
- **私信**:直接读取 `SQMessage.is_read` 字段
- **全员广播**:查询 `SQMessageRead` 表是否存在记录
### 标记全部已读
1. 更新私信:`UPDATE SQMessage SET is_read=1`
2. 插入广播已读记录:`INSERT INTO SQMessageRead`
---
## 七、扩展功能建议
### 短期扩展
1. 消息详情页(点击消息查看完整内容)
2. 删除消息功能
3. 消息搜索功能
4. 批量操作(批量标记已读/删除)
### 长期扩展
1. 消息分类标签
2. 消息推送到手机端
3. 消息提醒声音/震动设置
4. 定时发送消息
5. 消息模板管理
---
## 八、使用说明
### 前端对接
1. 在"我的"页面添加"我的消息"入口
2. 调用 `GetUnreadCount` 接口显示红点
3. 进入列表页调用 `GetMessageList` 获取消息
4. 自动调用 `MarkAllAsRead` 标记已读
### 后台使用
1. 访问后台消息管理模块
2. 可以手动发送通知给指定用户或全体用户
3. 查看历史消息记录
---
## 九、注意事项
1. **数据库**:请先执行 `数据库\SqlServer\创建站内信表.sql` 创建表
2. **依赖注入**:系统已自动注入服务,无需手动配置
3. **性能优化**:已添加索引,支持大数据量查询
4. **消息类型**
- `message_type=0`:系统通知(自动发送)
- `message_type=1`:私信(手动发送)
5. **目标类型**
- `target_type=0`:指定用户
- `target_type=1`:全员广播
---
## 十、测试建议
### 功能测试
1. 发送指定用户消息
2. 发送全员广播消息
3. 查看消息列表(分页、筛选)
4. 标记已读功能
5. 未读数量显示
6. 组局成功/失败自动通知
### 压力测试
1. 大量用户同时查询消息
2. 全员广播1000+用户)
3. 并发标记已读
---
## 完成状态
### 代码开发
**100% 完成** - 所有代码文件已创建并实现
### 需要完成的部署工作
**待执行**
1. **数据库部署**:执行 `数据库\SqlServer\创建站内信表.sql` 创建表
2. **依赖注入验证**:确认服务已正确注入(通常自动扫描已包含)
3. **接口测试**:使用 Postman 测试所有接口
4. **前端对接**
- 注意接口路径是 `/api/SQ/GetMessageList`(不是 `/api/user/GetMessageList`
- 实现消息列表页面
- 实现未读数量红点显示
### 重要修正
📝 **接口路径说明**
- 需求文档建议:`/api/user/GetMessageList`
- 实际实现路径:`/api/SQ/GetMessageList`
- 原因:统一放在 SQ 预约模块下管理
- 前端对接时请使用实际路径
详细检查清单请查看:`站内信功能完成情况检查.md`

View File

@ -0,0 +1,608 @@
# 我的收益页面接口参数说明
## 📋 页面功能概述
**页面路径**`pages/me/my-earnings-page.vue`
**主要功能**
1. 显示收益统计(待提取收益、已提取收益)
2. 提现功能(申请提现、查看最高可提现金额)
3. 查看收益规则说明
4. 收益记录列表(时间、房号/房名、房费、收益)
5. 提现记录列表(时间、提现金额、状态)
---
## 🔌 接口需求清单
### 接口1获取收益统计信息
#### 基本信息
- **接口路径**:建议 `GET /api/user/GetEarningsSummary``POST /api/user/GetEarningsSummary`
- **调用时机**
- 页面初始化时
- 提现成功后刷新
- **是否需要登录**需要Token
#### 请求参数
无需参数从Token中获取用户信息
#### 返回数据结构
```typescript
interface Response {
code: number; // 0=成功
msg: string; // 消息
data: {
pendingAmount: number; // 待提取收益(元)
extractedAmount: number; // 已提取收益(元)
}
}
```
#### 返回示例
```json
{
"code": 0,
"msg": "ok",
"data": {
"pendingAmount": 120.50,
"extractedAmount": 500.00
}
}
```
#### 页面映射
- `pendingAmount` → 第19行待提取收益
- `extractedAmount` → 第23行已提取收益
---
### 接口2获取收益规则说明
#### 基本信息
- **接口路径**:建议 `GET /api/user/GetEarningsRule``POST /api/user/GetEarningsRule`
- **调用时机**
- 点击"点击查看收益规则"时第35-37行
- 打开规则弹窗时第86-96行
- **是否需要登录**:否(可公开)
#### 请求参数
无需参数
#### 返回数据结构
```typescript
interface Response {
code: number; // 0=成功
msg: string; // 消息
data: {
content: string; // 规则说明正文
}
}
```
#### 返回示例
```json
{
"code": 0,
"msg": "ok",
"data": {
"content": "收益规则说明:\n1. 收益来源于房间预约成功后的分成\n2. 收益可随时提现\n3. 提现将在3-5个工作日内到账\n4. 最低提现金额为10元"
}
}
```
#### 页面映射
- `content` → 第89-93行规则说明弹窗正文
---
### 接口3获取收益记录列表
#### 基本信息
- **接口路径**:建议 `GET /api/user/GetEarningsRecordList``POST /api/user/GetEarningsRecordList`
- **调用时机**
- 页面初始化时currentIndex = 0
- 切换到"收益记录"标签时
- 下拉刷新时
- **是否需要登录**需要Token
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|-------|------|------|------|------|
| pageIndex | number | 否 | 页码从1开始 | 1 |
| pageSize | number | 否 | 每页数量 | 20 |
#### 返回数据结构
```typescript
interface Response {
code: number; // 0=成功
msg: string; // 消息
data: EarningsRecordItem[];
}
interface EarningsRecordItem {
id: number; // 记录ID必填
date: string; // 时间格式YYYY/MM/DD 或 YYYY-MM-DD必填
roomNumber: string; // 房号305必填
roomName: string; // 房名,如:大包(必填)
roomFee: number; // 房费(元)(必填)
earnings: number; // 收益(元)(必填)
reservationId?: number; // 预约ID可选
}
```
#### 返回示例
```json
{
"code": 0,
"msg": "ok",
"data": [
{
"id": 1,
"date": "2025/1/1",
"roomNumber": "305",
"roomName": "大包",
"roomFee": 12.00,
"earnings": 0.12,
"reservationId": 123
},
{
"id": 2,
"date": "2025/1/2",
"roomNumber": "306",
"roomName": "中包",
"roomFee": 10.00,
"earnings": 0.10,
"reservationId": 124
}
]
}
```
#### 页面映射
- `date` → 第68行时间
- `roomNumber + roomName` → 第69行305大包
- `roomFee` → 第70行¥12
- `earnings` → 第71行¥0.12
**注意**:页面显示格式为 `{roomNumber}{roomName}`,如:`305大包`
---
### 接口4获取提现记录列表
#### 基本信息
- **接口路径**:建议 `GET /api/user/GetWithdrawRecordList``POST /api/user/GetWithdrawRecordList`
- **调用时机**
- 切换到"提现记录"标签时currentIndex = 1
- 下拉刷新时
- **是否需要登录**需要Token
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|-------|------|------|------|------|
| pageIndex | number | 否 | 页码从1开始 | 1 |
| pageSize | number | 否 | 每页数量 | 20 |
#### 返回数据结构
```typescript
interface Response {
code: number; // 0=成功
msg: string; // 消息
data: WithdrawRecordItem[];
}
interface WithdrawRecordItem {
id: number; // 记录ID必填
date: string; // 时间格式YYYY/MM/DD 或 YYYY-MM-DD必填
amount: number; // 提现金额(元)(必填)
status: string; // 状态:提现中/已到账/已拒绝(必填)
statusCode?: number; // 状态码0=提现中1=已到账2=已拒绝(可选)
remark?: string; // 备注(可选)
}
```
#### 返回示例
```json
{
"code": 0,
"msg": "ok",
"data": [
{
"id": 1,
"date": "2025/1/1",
"amount": 100.00,
"status": "提现中",
"statusCode": 0
},
{
"id": 2,
"date": "2024/12/30",
"amount": 50.00,
"status": "已到账",
"statusCode": 1
},
{
"id": 3,
"date": "2024/12/28",
"amount": 200.00,
"status": "已拒绝",
"statusCode": 2,
"remark": "银行卡信息错误"
}
]
}
```
#### 页面映射
- `date` → 第77行时间
- `amount` → 第78行¥12
- `status` → 第79行提现中
---
### 接口5申请提现
#### 基本信息
- **接口路径**:建议 `POST /api/user/ApplyWithdraw`
- **调用时机**
- 点击"申请提现"按钮时第114行
- **是否需要登录**需要Token
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|-------|------|------|------|------|
| amount | number | 是 | 提现金额(元) | 100.00 |
#### 返回数据结构
```typescript
interface Response {
code: number; // 0=成功,其他=失败
msg: string; // 消息说明
data?: {
withdrawId?: number; // 提现记录ID可选
}
}
```
#### 返回示例
```json
// 成功
{
"code": 0,
"msg": "提现申请已提交",
"data": {
"withdrawId": 123
}
}
// 失败 - 余额不足
{
"code": 500,
"msg": "提现金额不能超过待提取收益"
}
// 失败 - 金额过小
{
"code": 500,
"msg": "最低提现金额为10元"
}
```
---
### 接口6获取最高可提现金额可选
#### 基本信息
- **接口路径**:建议 `GET /api/user/GetMaxWithdrawAmount` 或使用接口1的 `pendingAmount`
- **调用时机**
- 打开提现弹窗时第98-118行
- 用于显示"最高可提现0.00元"第108行
- **是否需要登录**需要Token
#### 说明
此接口可以复用**接口1获取收益统计信息**中的 `pendingAmount` 字段,无需单独接口。
如果后端需要单独接口可参考接口1的返回结构只返回 `pendingAmount`
---
## 📝 页面数据映射总览
### 收益统计卡片第13-32行
| 显示位置 | 数据来源 | 字段名 |
|---------|---------|--------|
| 第19行待提取收益 | 接口1 | `pendingAmount` |
| 第23行已提取收益 | 接口1 | `extractedAmount` |
| 第108行最高可提现 | 接口1 | `pendingAmount` |
### 收益记录列表第65-73行currentIndex = 0
| 显示位置 | 数据来源 | 字段名 | 显示格式 |
|---------|---------|--------|---------|
| 第68行时间 | 接口3 | `date` | 直接显示 |
| 第69行房号/房名 | 接口3 | `roomNumber` + `roomName` | `{roomNumber}{roomName}` |
| 第70行房费 | 接口3 | `roomFee` | `¥{roomFee}` |
| 第71行收益 | 接口3 | `earnings` | `¥{earnings}` |
### 提现记录列表第74-81行currentIndex = 1
| 显示位置 | 数据来源 | 字段名 | 显示格式 |
|---------|---------|--------|---------|
| 第77行时间 | 接口4 | `date` | 直接显示 |
| 第78行提现金额 | 接口4 | `amount` | `¥{amount}` |
| 第79行状态 | 接口4 | `status` | 直接显示 |
### 规则说明弹窗第86-96行
| 显示位置 | 数据来源 | 字段名 |
|---------|---------|--------|
| 第89-93行规则正文 | 接口2 | `content` |
---
## 🔄 接口调用建议
### 1. 接口定义位置
建议在 `common/server/interface/user.js` 中添加:
```javascript
/**
* 获取收益统计信息
* @returns {Promise<{pendingAmount: number, extractedAmount: number}>}
*/
export const getEarningsSummary = async () => {
const res = await request.getOrCache("user/GetEarningsSummary", {}, 1);
if (res.code == 0) {
return res.data;
}
return { pendingAmount: 0, extractedAmount: 0 };
}
/**
* 获取收益规则说明
* @returns {Promise<string>}
*/
export const getEarningsRule = async () => {
const res = await request.getOrCache("user/GetEarningsRule", {}, 300);
if (res.code == 0) {
return res.data.content;
}
return '';
}
/**
* 获取收益记录列表
* @param {number} pageIndex 页码
* @param {number} pageSize 每页数量
* @returns {Promise<EarningsRecordItem[]>}
*/
export const getEarningsRecordList = async (pageIndex = 1, pageSize = 20) => {
const res = await request.getOrCache(
"user/GetEarningsRecordList",
{ pageIndex, pageSize },
1
);
if (res.code == 0) {
return res.data;
}
return [];
}
/**
* 获取提现记录列表
* @param {number} pageIndex 页码
* @param {number} pageSize 每页数量
* @returns {Promise<WithdrawRecordItem[]>}
*/
export const getWithdrawRecordList = async (pageIndex = 1, pageSize = 20) => {
const res = await request.getOrCache(
"user/GetWithdrawRecordList",
{ pageIndex, pageSize },
1
);
if (res.code == 0) {
return res.data;
}
return [];
}
/**
* 申请提现
* @param {number} amount 提现金额
* @returns {Promise<boolean>}
*/
export const applyWithdraw = async (amount) => {
const res = await request.post("user/ApplyWithdraw", { amount });
if (res.code == 0) {
return true;
}
return false;
}
```
### 2. 页面调用示例
```javascript
import {
getEarningsSummary,
getEarningsRule,
getEarningsRecordList,
getWithdrawRecordList,
applyWithdraw
} from '@/common/server/interface/user.js'
export default {
data() {
return {
currentIndex: 0,
show: false,
reflectShow: false,
dataList: [],
value: '',
pendingAmount: 0.00,
extractedAmount: 0.00,
maxWithdrawAmount: 0.00,
ruleContent: ''
}
},
onLoad() {
this.loadEarningsSummary();
this.loadRuleContent();
this.loadDataList();
},
methods: {
// 加载收益统计
async loadEarningsSummary() {
const data = await getEarningsSummary();
if (data) {
this.pendingAmount = data.pendingAmount || 0.00;
this.extractedAmount = data.extractedAmount || 0.00;
this.maxWithdrawAmount = data.pendingAmount || 0.00;
}
},
// 加载规则内容
async loadRuleContent() {
this.ruleContent = await getEarningsRule();
},
// 加载列表数据
async loadDataList() {
if (this.currentIndex === 0) {
// 收益记录
this.dataList = await getEarningsRecordList(1, 20);
} else {
// 提现记录
this.dataList = await getWithdrawRecordList(1, 20);
}
},
// 切换标签
clickTab(index) {
this.currentIndex = index;
this.loadDataList();
},
// 打开提现弹窗
openWithdrawPopup() {
this.reflectShow = true;
this.loadEarningsSummary(); // 刷新最高可提现金额
},
// 申请提现
async submitWithdraw() {
const amount = parseFloat(this.value);
if (!amount || amount <= 0) {
uni.showToast({
title: '请输入正确的提现金额',
icon: 'none'
});
return;
}
if (amount > this.maxWithdrawAmount) {
uni.showToast({
title: '提现金额不能超过待提取收益',
icon: 'none'
});
return;
}
const success = await applyWithdraw(amount);
if (success) {
uni.showToast({
title: '提现申请已提交',
icon: 'success'
});
this.reflectShow = false;
this.value = '';
// 刷新数据
this.loadEarningsSummary();
if (this.currentIndex === 1) {
this.loadDataList();
}
} else {
uni.showToast({
title: '提现申请失败',
icon: 'none'
});
}
},
// 全部提现
allWithdraw() {
this.value = this.maxWithdrawAmount.toFixed(2);
}
}
}
```
---
## ✅ 必填字段总结
### 接口1获取收益统计
- `pendingAmount` (number) - 待提取收益
- `extractedAmount` (number) - 已提取收益
### 接口2获取收益规则
- `content` (string) - 规则说明正文
### 接口3获取收益记录列表
- `id` (number) - 记录ID
- `date` (string) - 时间
- `roomNumber` (string) - 房号
- `roomName` (string) - 房名
- `roomFee` (number) - 房费
- `earnings` (number) - 收益
### 接口4获取提现记录列表
- `id` (number) - 记录ID
- `date` (string) - 时间
- `amount` (number) - 提现金额
- `status` (string) - 状态
### 接口5申请提现
- 请求参数:`amount` (number) - 提现金额
---
## 📌 注意事项
1. **金额格式**:所有金额字段建议使用 `number` 类型保留2位小数
2. **时间格式**:建议后端返回格式化的时间字符串(如:`2025/1/1` 或 `2025-01-01`
3. **分页支持**:收益记录和提现记录建议支持分页加载
4. **空数据处理**:当没有记录时,返回空数组 `[]`
5. **错误处理**:接口失败时返回 `code != 0`,前端需要处理错误情况
6. **提现金额验证**
- 前端需要验证:不能超过 `pendingAmount`
- 后端需要验证:最低提现金额、余额是否充足等
7. **状态显示**:提现状态建议使用中文显示(提现中/已到账/已拒绝)
---
## 🔍 后续扩展建议
如果后续需要添加以下功能,可以考虑增加字段:
- **收益详情**:点击收益记录查看详情,需要 `reservationId` 字段
- **提现详情**:点击提现记录查看详情,需要 `remark` 字段
- **提现方式**:如果支持多种提现方式,需要增加提现方式字段
- **收益统计图表**:如果需要图表展示,可以增加按时间段的统计数据

41
收益 需求文档.md Normal file
View File

@ -0,0 +1,41 @@
我的收益
(1)入口
1.我的页面,新增【我的收益】。
站内信 入口
(2)详情页
我的收益 详情页
1.待提现收益,未提现的剩余金额。
2.已提现收益,所有已提现的金额总和。
3.点击【查看规则】,弹出“规则说明弹窗”。
弹窗说明
3.1.内容后台配置。
4.收益记录,展示每次收益的数据。
收益记录
5.提现记录,展示每次提现的数据。
提现记录
5.1.多种状态,“提现中”“已提现”“已取消”。
5.1.1.提现中:申请后,处于该状态。
5.1.2.已提现:已提现,线下打款后处于该状态。
5.1.3.已取消:后台取消/拒绝该提现申请。
(3)收益如何获取
1.线下员工通过后台,给发起者的账号添加佣金。
1.1.只有发起者有佣金,参与者没有。
2.佣金 = 房费10%。
(4)提现
1.点击【去提现】,弹出提现弹窗。
提现弹窗
1.1.支持小数点后两位。
1.2.点击【全部提现】,自动输入所有待提现金额。
1.3.点击【申请提现】:
1.3.1.高于待提现金额,弹出系统提示“超出可提现金额”。
1.3.2.无其他问题,关闭弹窗,弹出系统提示“已提交申请”,页面自动刷新,更新待提现金额。
2.后台展示收到的提现申请记录,可对申请进行操作。
2.1.同意:同意该申请,线下联系客户打款。
2.2.拒绝/取消:因其他原因拒绝该申请。
2.3.已打款:通过线下转帐后,将该记录手动改变为本状态。