557 lines
17 KiB
Markdown
557 lines
17 KiB
Markdown
# 预约时间自由选择 - 改动文档
|
||
|
||
## 1. 需求概述
|
||
|
||
### 1.1 当前问题
|
||
- 用户只能选择固定时段(凌晨0-6、上午6-12、下午12-18、晚上18-24)
|
||
- 无法灵活选择具体的开始和结束时间
|
||
- 跨时段预约显示不直观
|
||
|
||
### 1.2 改动目标
|
||
1. **时段显示优化**:如果预约时间为15:00-20:00,房间列表页应显示该房间"下午"和"晚上"都被预定
|
||
2. **自由时间选择**:创建预约界面允许用户自由选择开始时间和结束时间
|
||
|
||
---
|
||
|
||
## 2. 改动前分析
|
||
|
||
### 2.1 当前时段定义
|
||
```
|
||
时段类型 | 时间范围 | 对应值
|
||
--------|----------|-------
|
||
凌晨 | 00:00-06:00 | 0
|
||
上午 | 06:00-12:00 | 1
|
||
下午 | 12:00-18:00 | 2
|
||
晚上 | 18:00-24:00 | 3
|
||
```
|
||
|
||
### 2.2 当前预约创建流程
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A[用户选择日期] --> B[选择房间]
|
||
B --> C[显示可用时段]
|
||
C --> D{选择固定时段}
|
||
D -->|凌晨| E1[设置 00:00-06:00]
|
||
D -->|上午| E2[设置 06:00-12:00]
|
||
D -->|下午| E3[设置 12:00-18:00]
|
||
D -->|晚上| E4[设置 18:00-24:00]
|
||
E1 --> F[提交预约]
|
||
E2 --> F
|
||
E3 --> F
|
||
E4 --> F
|
||
F --> G[后端创建预约]
|
||
```
|
||
|
||
### 2.3 当前前端时间计算逻辑
|
||
|
||
**文件**: `pages/appointment/appointment-page.vue`
|
||
|
||
```javascript
|
||
// 当前代码:根据时段类型计算固定时间
|
||
const calculateTimeFromSlot = () => {
|
||
const date = new Date(selectedDate.value);
|
||
let startHour, endHour;
|
||
|
||
switch (selectedTimeSlot.value) {
|
||
case 0: // 凌晨
|
||
startHour = 0;
|
||
endHour = 6;
|
||
break;
|
||
case 1: // 上午
|
||
startHour = 6;
|
||
endHour = 12;
|
||
break;
|
||
case 2: // 下午
|
||
startHour = 12;
|
||
endHour = 18;
|
||
break;
|
||
case 3: // 晚上
|
||
startHour = 18;
|
||
endHour = 24;
|
||
break;
|
||
}
|
||
// ... 设置开始和结束时间
|
||
};
|
||
```
|
||
|
||
### 2.4 当前房间时段状态显示
|
||
|
||
**文件**: `pages/appointment/book-room-page.vue`
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ 房间名称 │
|
||
├─────────────────────────────────────────┤
|
||
│ [凌晨] [上午] [下午] [晚上] │
|
||
│ 🟢 🟢 🟠 🟢 │
|
||
│ │
|
||
│ 🟢 可预约 🟠 已预约 ⚫ 不可用 🔵 使用中 │
|
||
└─────────────────────────────────────────┘
|
||
|
||
当前问题:预约15:00-20:00时,仅显示一个时段被占用
|
||
```
|
||
|
||
### 2.5 当前后端时段判断逻辑
|
||
|
||
**文件**: `CoreCms.Net.Services/SQ/SQRoomsServices.cs`
|
||
|
||
```csharp
|
||
// 后端已有重叠判断逻辑(可复用)
|
||
bool isReserved = reservations?.Any(r =>
|
||
r.start_time < slotEnd && r.end_time > slotStart) ?? false;
|
||
```
|
||
|
||
**分析**:后端逻辑已支持跨时段检测,问题在于前端只能选择单一时段。
|
||
|
||
---
|
||
|
||
## 3. 改动后方案
|
||
|
||
### 3.1 新的预约创建流程
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A[用户选择日期] --> B[选择房间]
|
||
B --> C[显示房间详情]
|
||
C --> D[选择开始时间]
|
||
D --> E[选择结束时间]
|
||
E --> F{时间校验}
|
||
F -->|结束时间 > 开始时间| G[计算跨越时段]
|
||
F -->|无效| H[提示错误]
|
||
G --> I[检查时段冲突]
|
||
I -->|无冲突| J[提交预约]
|
||
I -->|有冲突| K[提示时段已被预约]
|
||
J --> L[后端创建预约]
|
||
L --> M[更新房间时段显示]
|
||
```
|
||
|
||
### 3.2 新的时间选择UI设计
|
||
|
||
```
|
||
改动前(固定时段选择):
|
||
┌─────────────────────────────────────────┐
|
||
│ 选择时段 │
|
||
├─────────────────────────────────────────┤
|
||
│ ○ 凌晨 (00:00-06:00) │
|
||
│ ○ 上午 (06:00-12:00) │
|
||
│ ● 下午 (12:00-18:00) ← 只能单选 │
|
||
│ ○ 晚上 (18:00-24:00) │
|
||
└─────────────────────────────────────────┘
|
||
|
||
改动后(自由时间选择):
|
||
┌─────────────────────────────────────────┐
|
||
│ 选择时间 │
|
||
├─────────────────────────────────────────┤
|
||
│ 开始时间: [15:00 ▼] ← 时间选择器 │
|
||
│ 结束时间: [20:00 ▼] ← 时间选择器 │
|
||
│ │
|
||
│ 预计时长: 5小时 │
|
||
│ 跨越时段: 下午、晚上 │
|
||
│ │
|
||
│ 💡 提示:结束时间必须晚于开始时间 │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
### 3.3 新的房间列表时段显示
|
||
|
||
```
|
||
改动前(15:00-20:00预约后):
|
||
┌─────────────────────────────────────────┐
|
||
│ [凌晨] [上午] [下午] [晚上] │
|
||
│ 🟢 🟢 🟠 🟢 ← 只显示下午 │
|
||
└─────────────────────────────────────────┘
|
||
|
||
改动后(15:00-20:00预约后):
|
||
┌─────────────────────────────────────────┐
|
||
│ [凌晨] [上午] [下午] [晚上] │
|
||
│ 🟢 🟢 🟠 🟠 ← 下午+晚上 │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
### 3.4 时段重叠判断逻辑
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
subgraph 预约时间 15:00-20:00
|
||
P1[15:00] --> P2[20:00]
|
||
end
|
||
|
||
subgraph 时段判断
|
||
S1[凌晨 0-6] --> R1{重叠?}
|
||
S2[上午 6-12] --> R2{重叠?}
|
||
S3[下午 12-18] --> R3{重叠?}
|
||
S4[晚上 18-24] --> R4{重叠?}
|
||
end
|
||
|
||
R1 -->|否| N1[🟢]
|
||
R2 -->|否| N2[🟢]
|
||
R3 -->|是| Y3[🟠]
|
||
R4 -->|是| Y4[🟠]
|
||
```
|
||
|
||
**重叠判断公式**:
|
||
```
|
||
预约与时段重叠 = (预约开始时间 < 时段结束时间) AND (预约结束时间 > 时段开始时间)
|
||
|
||
示例:预约15:00-20:00
|
||
- 凌晨(0-6): 15 < 6 = false → 不重叠 🟢
|
||
- 上午(6-12): 15 < 12 = false → 不重叠 🟢
|
||
- 下午(12-18): 15 < 18 = true AND 20 > 12 = true → 重叠 🟠
|
||
- 晚上(18-24): 15 < 24 = true AND 20 > 18 = true → 重叠 🟠
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 需要改动的文件清单
|
||
|
||
### 4.1 前端改动
|
||
|
||
| 序号 | 文件路径 | 改动内容 | 复杂度 |
|
||
|------|----------|----------|--------|
|
||
| 1 | `pages/appointment/appointment-page.vue` | 将固定时段选择改为时间选择器 | ⭐⭐⭐ |
|
||
| 2 | `pages/appointment/book-room-page.vue` | 时段状态显示逻辑优化(可能无需改动,后端已支持) | ⭐ |
|
||
| 3 | `components/com/page/reservation-item.vue` | 预约详情显示时间格式调整 | ⭐ |
|
||
|
||
### 4.2 后端改动
|
||
|
||
| 序号 | 文件路径 | 改动内容 | 复杂度 |
|
||
|------|----------|----------|--------|
|
||
| 1 | `SQController.cs` | 接收自定义开始/结束时间参数 | ⭐⭐ |
|
||
| 2 | `SQRoomsServices.cs` | 时段状态判断逻辑已支持,可能需微调 | ⭐ |
|
||
| 3 | `SQReservationsServices.cs` | 预约冲突检测逻辑验证 | ⭐ |
|
||
|
||
---
|
||
|
||
## 5. 详细改动说明
|
||
|
||
### 5.1 appointment-page.vue 改动
|
||
|
||
#### 改动前代码
|
||
```vue
|
||
<template>
|
||
<!-- 时段选择(单选按钮) -->
|
||
<view class="time-slot-selector">
|
||
<view v-for="slot in timeSlotOptions" :key="slot.value"
|
||
:class="['slot-item', selectedTimeSlot === slot.value ? 'active' : '']"
|
||
@click="selectTimeSlot(slot.value)">
|
||
{{ slot.label }}
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
const selectedTimeSlot = ref(null);
|
||
const timeSlotOptions = ref([]);
|
||
|
||
const calculateTimeFromSlot = () => {
|
||
switch (selectedTimeSlot.value) {
|
||
case 0: startHour = 0; endHour = 6; break;
|
||
case 1: startHour = 6; endHour = 12; break;
|
||
case 2: startHour = 12; endHour = 18; break;
|
||
case 3: startHour = 18; endHour = 24; break;
|
||
}
|
||
};
|
||
</script>
|
||
```
|
||
|
||
#### 改动后代码
|
||
```vue
|
||
<template>
|
||
<!-- 时间选择器 -->
|
||
<view class="time-picker-container">
|
||
<view class="time-picker-row">
|
||
<text class="label">开始时间</text>
|
||
<picker mode="time" :value="startTime" @change="onStartTimeChange">
|
||
<view class="picker-value">{{ startTime || '请选择' }}</view>
|
||
</picker>
|
||
</view>
|
||
<view class="time-picker-row">
|
||
<text class="label">结束时间</text>
|
||
<picker mode="time" :value="endTime" @change="onEndTimeChange">
|
||
<view class="picker-value">{{ endTime || '请选择' }}</view>
|
||
</picker>
|
||
</view>
|
||
<view class="time-info">
|
||
<text>预计时长: {{ calculateDuration() }}</text>
|
||
<text>跨越时段: {{ calculateCrossSlots() }}</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
const startTime = ref('');
|
||
const endTime = ref('');
|
||
|
||
// 开始时间变更
|
||
const onStartTimeChange = (e) => {
|
||
startTime.value = e.detail.value;
|
||
validateTimeRange();
|
||
};
|
||
|
||
// 结束时间变更
|
||
const onEndTimeChange = (e) => {
|
||
endTime.value = e.detail.value;
|
||
validateTimeRange();
|
||
};
|
||
|
||
// 计算时长
|
||
const calculateDuration = () => {
|
||
if (!startTime.value || !endTime.value) return '-';
|
||
const start = parseTime(startTime.value);
|
||
const end = parseTime(endTime.value);
|
||
const hours = (end - start) / (1000 * 60 * 60);
|
||
return `${hours}小时`;
|
||
};
|
||
|
||
// 计算跨越的时段
|
||
const calculateCrossSlots = () => {
|
||
if (!startTime.value || !endTime.value) return '-';
|
||
const slots = [];
|
||
const startHour = parseInt(startTime.value.split(':')[0]);
|
||
const endHour = parseInt(endTime.value.split(':')[0]);
|
||
|
||
if (startHour < 6 || endHour > 0 && endHour <= 6) slots.push('凌晨');
|
||
if ((startHour < 12 && endHour > 6) || (startHour >= 6 && startHour < 12)) slots.push('上午');
|
||
if ((startHour < 18 && endHour > 12) || (startHour >= 12 && startHour < 18)) slots.push('下午');
|
||
if (endHour > 18 || startHour >= 18) slots.push('晚上');
|
||
|
||
return slots.join('、') || '-';
|
||
};
|
||
|
||
// 验证时间范围
|
||
const validateTimeRange = () => {
|
||
if (startTime.value && endTime.value) {
|
||
const start = parseTime(startTime.value);
|
||
const end = parseTime(endTime.value);
|
||
if (end <= start) {
|
||
uni.showToast({ title: '结束时间必须晚于开始时间', icon: 'none' });
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
};
|
||
|
||
// 构建预约数据(提交时)
|
||
const buildReservationData = () => {
|
||
const date = new Date(selectedDate.value);
|
||
const [startH, startM] = startTime.value.split(':').map(Number);
|
||
const [endH, endM] = endTime.value.split(':').map(Number);
|
||
|
||
const startDateTime = new Date(date);
|
||
startDateTime.setHours(startH, startM, 0, 0);
|
||
|
||
const endDateTime = new Date(date);
|
||
endDateTime.setHours(endH, endM, 0, 0);
|
||
|
||
return {
|
||
start_time: startDateTime.toISOString(),
|
||
end_time: endDateTime.toISOString(),
|
||
// ... 其他字段
|
||
};
|
||
};
|
||
</script>
|
||
```
|
||
|
||
### 5.2 后端 SQController.cs 改动
|
||
|
||
#### 改动说明
|
||
后端接口已支持接收 `start_time` 和 `end_time` 参数,主要验证逻辑需确认:
|
||
|
||
```csharp
|
||
// 添加预约时的冲突检测
|
||
[HttpPost("AddSQReservation")]
|
||
public async Task<IActionResult> AddSQReservation([FromBody] AddReservationRequest request)
|
||
{
|
||
// 验证时间有效性
|
||
if (request.end_time <= request.start_time)
|
||
{
|
||
return BadRequest("结束时间必须晚于开始时间");
|
||
}
|
||
|
||
// 检查时段冲突(已有逻辑)
|
||
var conflicts = await _reservationService.CheckTimeConflict(
|
||
request.room_id,
|
||
request.start_time,
|
||
request.end_time
|
||
);
|
||
|
||
if (conflicts.Any())
|
||
{
|
||
return BadRequest("所选时段已被预约");
|
||
}
|
||
|
||
// 创建预约...
|
||
}
|
||
```
|
||
|
||
### 5.3 后端时段显示逻辑验证
|
||
|
||
**文件**: `SQRoomsServices.cs`
|
||
|
||
当前逻辑已正确支持跨时段检测:
|
||
```csharp
|
||
// 判断预约是否与时段重叠
|
||
bool isReserved = reservations?.Any(r =>
|
||
r.start_time < slotEnd && r.end_time > slotStart) ?? false;
|
||
```
|
||
|
||
**验证场景**:
|
||
- 预约 15:00-20:00
|
||
- 下午时段(12:00-18:00): `15:00 < 18:00 && 20:00 > 12:00` = true ✅
|
||
- 晚上时段(18:00-24:00): `15:00 < 24:00 && 20:00 > 18:00` = true ✅
|
||
|
||
**结论**:后端逻辑无需改动,已支持跨时段显示。
|
||
|
||
---
|
||
|
||
## 6. 数据流对比
|
||
|
||
### 6.1 改动前数据流
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant U as 用户
|
||
participant F as 前端
|
||
participant B as 后端
|
||
participant DB as 数据库
|
||
|
||
U->>F: 选择日期
|
||
U->>F: 选择房间
|
||
F->>B: 获取房间时段状态
|
||
B->>DB: 查询已有预约
|
||
DB-->>B: 返回预约列表
|
||
B-->>F: 返回4个时段状态
|
||
F-->>U: 显示可选时段
|
||
U->>F: 选择"下午"时段
|
||
F->>F: calculateTimeFromSlot()
|
||
Note over F: 固定设置 12:00-18:00
|
||
F->>B: 提交预约(12:00-18:00)
|
||
B->>DB: 保存预约
|
||
DB-->>B: 保存成功
|
||
B-->>F: 返回成功
|
||
F-->>U: 预约成功
|
||
```
|
||
|
||
### 6.2 改动后数据流
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant U as 用户
|
||
participant F as 前端
|
||
participant B as 后端
|
||
participant DB as 数据库
|
||
|
||
U->>F: 选择日期
|
||
U->>F: 选择房间
|
||
F->>B: 获取房间时段状态
|
||
B->>DB: 查询已有预约
|
||
DB-->>B: 返回预约列表
|
||
B-->>F: 返回4个时段状态
|
||
F-->>U: 显示时间选择器
|
||
U->>F: 选择开始时间 15:00
|
||
U->>F: 选择结束时间 20:00
|
||
F->>F: validateTimeRange()
|
||
F->>F: calculateCrossSlots()
|
||
Note over F: 显示"下午、晚上"
|
||
F->>B: 提交预约(15:00-20:00)
|
||
B->>B: 检查时段冲突
|
||
B->>DB: 保存预约
|
||
DB-->>B: 保存成功
|
||
B-->>F: 返回成功
|
||
F-->>U: 预约成功
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 测试用例
|
||
|
||
### 7.1 时间选择测试
|
||
|
||
| 测试场景 | 开始时间 | 结束时间 | 预期结果 |
|
||
|----------|----------|----------|----------|
|
||
| 正常选择 | 15:00 | 20:00 | 成功,显示跨越"下午、晚上" |
|
||
| 同时段选择 | 14:00 | 17:00 | 成功,显示跨越"下午" |
|
||
| 跨三时段 | 10:00 | 20:00 | 成功,显示跨越"上午、下午、晚上" |
|
||
| 无效时间 | 20:00 | 15:00 | 失败,提示"结束时间必须晚于开始时间" |
|
||
| 相同时间 | 15:00 | 15:00 | 失败,提示"结束时间必须晚于开始时间" |
|
||
|
||
### 7.2 时段冲突测试
|
||
|
||
| 已有预约 | 新预约 | 预期结果 |
|
||
|----------|--------|----------|
|
||
| 15:00-20:00 | 10:00-14:00 | 成功(无重叠) |
|
||
| 15:00-20:00 | 14:00-16:00 | 失败(重叠) |
|
||
| 15:00-20:00 | 19:00-22:00 | 失败(重叠) |
|
||
| 15:00-20:00 | 20:00-22:00 | 成功(边界无重叠) |
|
||
|
||
### 7.3 房间列表显示测试
|
||
|
||
| 预约时间 | 凌晨 | 上午 | 下午 | 晚上 |
|
||
|----------|------|------|------|------|
|
||
| 02:00-05:00 | 🟠 | 🟢 | 🟢 | 🟢 |
|
||
| 10:00-14:00 | 🟢 | 🟠 | 🟠 | 🟢 |
|
||
| 15:00-20:00 | 🟢 | 🟢 | 🟠 | 🟠 |
|
||
| 22:00-04:00 | 🟠 | 🟢 | 🟢 | 🟠 |
|
||
|
||
---
|
||
|
||
## 8. 工作量评估
|
||
|
||
| 模块 | 工作内容 | 复杂度 |
|
||
|------|----------|--------|
|
||
| 前端-预约页面 | 时间选择器组件替换、时间计算逻辑 | ⭐⭐⭐ |
|
||
| 前端-房间列表 | 验证显示逻辑(可能无需改动) | ⭐ |
|
||
| 前端-预约详情 | 时间格式显示调整 | ⭐ |
|
||
| 后端-控制器 | 参数验证、冲突检测 | ⭐⭐ |
|
||
| 后端-服务层 | 逻辑验证(可能无需改动) | ⭐ |
|
||
| 测试 | 各场景测试 | ⭐⭐ |
|
||
|
||
**总体评估**:中等复杂度改动,主要工作集中在前端预约页面的UI和逻辑重构。
|
||
|
||
---
|
||
|
||
## 9. 风险与注意事项
|
||
|
||
1. **向后兼容**:已有的固定时段预约数据仍可正常显示
|
||
2. **边界条件**:跨天预约(如23:00-02:00)需特殊处理
|
||
3. **时间精度**:建议时间选择以30分钟为最小单位
|
||
4. **用户体验**:需添加明确的时长提示和时段跨越提示
|
||
5. **数据验证**:前后端都需要进行时间有效性验证
|
||
|
||
---
|
||
|
||
## 10. 附录:时段判断工具函数
|
||
|
||
```javascript
|
||
/**
|
||
* 判断时间范围跨越哪些时段
|
||
* @param {number} startHour 开始小时 (0-23)
|
||
* @param {number} endHour 结束小时 (0-24)
|
||
* @returns {Array} 跨越的时段数组
|
||
*/
|
||
function getCrossedSlots(startHour, endHour) {
|
||
const slots = [];
|
||
const slotRanges = [
|
||
{ name: '凌晨', start: 0, end: 6 },
|
||
{ name: '上午', start: 6, end: 12 },
|
||
{ name: '下午', start: 12, end: 18 },
|
||
{ name: '晚上', start: 18, end: 24 }
|
||
];
|
||
|
||
for (const slot of slotRanges) {
|
||
// 重叠判断:预约开始 < 时段结束 AND 预约结束 > 时段开始
|
||
if (startHour < slot.end && endHour > slot.start) {
|
||
slots.push(slot.name);
|
||
}
|
||
}
|
||
|
||
return slots;
|
||
}
|
||
|
||
// 使用示例
|
||
getCrossedSlots(15, 20); // ['下午', '晚上']
|
||
getCrossedSlots(10, 14); // ['上午', '下午']
|
||
getCrossedSlots(2, 5); // ['凌晨']
|
||
```
|