fix: 配房页面修正指南 — 结构化分配按家庭维度+酒店房型联动
这个提交包含在:
父节点
b8a7f4c16c
当前提交
84c1d113e1
@ -0,0 +1,484 @@
|
|||||||
|
# 配房页面修正指南(紧急)
|
||||||
|
|
||||||
|
> **日期**: 2026-03-19
|
||||||
|
> **后端状态**: ✅ 已完成,无需后端修改
|
||||||
|
> **问题**: 当前配房弹窗结构化分配 tab 与后端接口不匹配,缺少关键功能
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 当前问题
|
||||||
|
|
||||||
|
当前"结构化分配"tab 只有一组酒店/房型/数量/日期,**但后端是按家庭维度批量分配的**,需要支持多行。
|
||||||
|
另外缺少"配房可选酒店列表"接口调用,以及选择酒店后联动加载房型列表。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 正确的页面布局
|
||||||
|
|
||||||
|
### 配房信息弹窗(el-dialog)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────┐
|
||||||
|
│ 配房信息 [×] │
|
||||||
|
├──────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ [ 文本录入 ] [ 结构化分配 ] │
|
||||||
|
│ │
|
||||||
|
│ 订单号 HL202603111457027786 │
|
||||||
|
│ 出发日期 2026-03-11 │
|
||||||
|
│ 行程天数 6天5晚 │
|
||||||
|
│ │
|
||||||
|
│ ┌── 家庭 1 ──────────────────────────────── [删除] ┐│
|
||||||
|
│ │ 选择酒店 * [ 下拉选择(调接口9) ▼] ││
|
||||||
|
│ │ 选择房型 * [ 联动下拉(调接口10) ▼] ││
|
||||||
|
│ │ 入住日期 * [2026-03-11] 退房日期 * [2026-03-16]││
|
||||||
|
│ └──────────────────────────────────────────────────┘│
|
||||||
|
│ ┌── 家庭 2 ──────────────────────────────── [删除] ┐│
|
||||||
|
│ │ 选择酒店 * [ 下拉选择 ▼] ││
|
||||||
|
│ │ 选择房型 * [ 联动下拉 ▼] ││
|
||||||
|
│ │ 入住日期 * [2026-03-11] 退房日期 * [2026-03-16]││
|
||||||
|
│ └──────────────────────────────────────────────────┘│
|
||||||
|
│ │
|
||||||
|
│ [+ 添加家庭] │
|
||||||
|
│ │
|
||||||
|
│ [ 取消 ] [ 确定 ] │
|
||||||
|
└──────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关键交互
|
||||||
|
|
||||||
|
1. **点击"配房"按钮** → 调用接口 9 获取可选酒店列表
|
||||||
|
2. **选择酒店** → 用酒店ID调用接口 10 获取该酒店的房型列表
|
||||||
|
3. **点击"+ 添加家庭"** → 动态添加一组分配表单
|
||||||
|
4. **入住/退房日期** → 默认为订单出发日期~结束日期
|
||||||
|
5. **点击"确定"** → 调用接口 2(`PUT /admin/order/{orderId}/hotel-assignment`)提交
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 接口清单
|
||||||
|
|
||||||
|
| # | 接口 | 方法 | 路径 | 说明 |
|
||||||
|
|---|------|------|------|------|
|
||||||
|
| 1 | 更新房间信息(文本) | PUT | `/api/order/admin/order/{orderId}/room-info` | 文本录入 tab 使用 |
|
||||||
|
| 2 | 分配酒店(结构化) | PUT | `/api/order/admin/order/{orderId}/hotel-assignment` | 结构化分配 tab 使用 |
|
||||||
|
| 3 | 检测房型一致性 | POST | `/api/order/admin/order/{orderId}/room-info/check-diff` | 提交后检查 |
|
||||||
|
| 9 | **配房可选酒店列表** | GET | `/api/order/admin/order/{orderId}/available-hotels` | **新增关键接口** |
|
||||||
|
| 10 | **酒店房型列表** | GET | `/api/resource/admin/hotel/{hotelId}/room-types` | **联动加载房型** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 接口 9:配房可选酒店列表(当前页面缺失)
|
||||||
|
|
||||||
|
**使用场景**:打开配房弹窗时调用,获取酒店下拉列表。产品快照中的酒店排在最前面并标注"产品快照"。
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/order/admin/order/{orderId}/available-hotels
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "成功",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"hotelId": "2028730814273536001",
|
||||||
|
"hotelName": "阿尔善国际维景度假温泉酒店",
|
||||||
|
"inSnapshot": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hotelId": "2028730814273536002",
|
||||||
|
"hotelName": "呼和浩特香格里拉大酒店",
|
||||||
|
"inSnapshot": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| hotelId | String | 酒店ID |
|
||||||
|
| hotelName | String | 酒店名称 |
|
||||||
|
| inSnapshot | Boolean | 是否在产品快照中(true=行程已安排的酒店,应优先选择) |
|
||||||
|
|
||||||
|
### 前端实现
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 打开配房弹窗时调用
|
||||||
|
const loadAvailableHotels = async (orderId) => {
|
||||||
|
const res = await request.get(`/api/order/admin/order/${orderId}/available-hotels`)
|
||||||
|
// 快照酒店已自动排在前面
|
||||||
|
hotelOptions.value = res.data.map(h => ({
|
||||||
|
value: h.hotelId,
|
||||||
|
label: h.inSnapshot ? `${h.hotelName}(产品快照)` : h.hotelName,
|
||||||
|
hotelId: h.hotelId,
|
||||||
|
hotelName: h.hotelName
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 接口 10:酒店房型列表(当前页面缺失)
|
||||||
|
|
||||||
|
**使用场景**:选择酒店后联动加载该酒店的可用房型。
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/resource/admin/hotel/{hotelId}/room-types
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "成功",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"roomTypeId": "2029001234567890001",
|
||||||
|
"name": "豪华大床房",
|
||||||
|
"maxOccupancy": 2,
|
||||||
|
"basePrice": 680.00,
|
||||||
|
"category": "DELUXE",
|
||||||
|
"bedType": "KING",
|
||||||
|
"windowType": "WITH_WINDOW",
|
||||||
|
"status": 1,
|
||||||
|
"coverUrl": "https://oss.example.com/room1.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roomTypeId": "2029001234567890002",
|
||||||
|
"name": "标准双人间",
|
||||||
|
"maxOccupancy": 2,
|
||||||
|
"basePrice": 480.00,
|
||||||
|
"category": "STANDARD",
|
||||||
|
"bedType": "TWIN",
|
||||||
|
"windowType": "WITH_WINDOW",
|
||||||
|
"status": 1,
|
||||||
|
"coverUrl": "https://oss.example.com/room2.jpg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| roomTypeId | String | 房型ID |
|
||||||
|
| name | String | 房型名称(用于展示和提交) |
|
||||||
|
| maxOccupancy | Integer | 最大入住人数 |
|
||||||
|
| basePrice | BigDecimal | 参考价(门市价) |
|
||||||
|
| category | String | 房型分类(字典:room_category) |
|
||||||
|
| bedType | String | 床型(字典:bed_type) |
|
||||||
|
| windowType | String | 窗户类型(字典:window_type) |
|
||||||
|
| status | Integer | 状态(1=启用) |
|
||||||
|
| coverUrl | String | 房型封面图 |
|
||||||
|
|
||||||
|
### 前端实现
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 选择酒店后联动加载房型
|
||||||
|
const onHotelChange = async (hotelId, assignmentIndex) => {
|
||||||
|
const res = await request.get(`/api/resource/admin/hotel/${hotelId}/room-types`)
|
||||||
|
// 将房型选项绑定到对应的家庭行
|
||||||
|
roomTypeOptions.value[assignmentIndex] = res.data.map(rt => ({
|
||||||
|
value: rt.name, // 提交时用 name 字段
|
||||||
|
label: `${rt.name}(最多${rt.maxOccupancy}人,¥${rt.basePrice})`
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 接口 2:分配酒店(结构化模式) - 提交
|
||||||
|
|
||||||
|
**使用场景**:点击"确定"按钮提交配房数据。
|
||||||
|
|
||||||
|
```
|
||||||
|
PUT /api/order/admin/order/{orderId}/hotel-assignment
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求体
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"familyIndex": 1,
|
||||||
|
"hotelName": "阿尔善国际维景度假温泉酒店",
|
||||||
|
"roomType": "豪华大床房",
|
||||||
|
"checkInDate": "2026-03-11",
|
||||||
|
"checkOutDate": "2026-03-16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"familyIndex": 2,
|
||||||
|
"hotelName": "阿尔善国际维景度假温泉酒店",
|
||||||
|
"roomType": "标准双人间",
|
||||||
|
"checkInDate": "2026-03-11",
|
||||||
|
"checkOutDate": "2026-03-16"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| assignments | Array | ✅ | 酒店分配列表,不能为空 |
|
||||||
|
| assignments[].familyIndex | Integer | ✅ | 家庭序号(从1开始,每行递增) |
|
||||||
|
| assignments[].hotelName | String | ✅ | 酒店名称(从接口9的返回中取 hotelName) |
|
||||||
|
| assignments[].roomType | String | ✅ | 房型名称(从接口10的返回中取 name) |
|
||||||
|
| assignments[].checkInDate | String | ✅ | 入住日期(yyyy-MM-dd) |
|
||||||
|
| assignments[].checkOutDate | String | ✅ | 退房日期(yyyy-MM-dd) |
|
||||||
|
|
||||||
|
### 校验规则
|
||||||
|
|
||||||
|
- `assignments` 不能为空数组
|
||||||
|
- 每个 assignment 的所有字段都必填
|
||||||
|
- 日期格式 `yyyy-MM-dd`
|
||||||
|
- `checkOutDate` 必须晚于 `checkInDate`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 接口 1:更新房间信息(文本模式)
|
||||||
|
|
||||||
|
**使用场景**:文本录入 tab 使用,简单填写文字描述。
|
||||||
|
|
||||||
|
```
|
||||||
|
PUT /api/order/admin/order/{orderId}/room-info
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求体
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"roomInfo": "标准双人间2间,大床房1间"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 业务规则
|
||||||
|
- 仅限 **ROOM_MANAGER** 或 **SUPER_ADMIN** 角色
|
||||||
|
- 保存成功后自动完成 `ARRANGE_ROOM` 待办,流程自动推进到"待配车"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 接口 3:检测房型一致性
|
||||||
|
|
||||||
|
**使用场景**:配房提交后自动调用,检测当前配置与产品快照是否一致。
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/order/admin/order/{orderId}/room-info/check-diff
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应
|
||||||
|
|
||||||
|
- `data` 为 `null` → 一致,无需提示
|
||||||
|
- `data` 为字符串 → 不一致描述,用 `ElMessageBox.confirm` 弹窗提示
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完整代码示例
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<el-dialog title="配房信息" v-model="visible" width="700px">
|
||||||
|
<!-- Tab 切换 -->
|
||||||
|
<el-tabs v-model="activeTab">
|
||||||
|
<el-tab-pane label="文本录入" name="text">
|
||||||
|
<el-form>
|
||||||
|
<el-form-item label="订单号">{{ order.orderNo }}</el-form-item>
|
||||||
|
<el-form-item label="房间信息" required>
|
||||||
|
<el-input type="textarea" v-model="textForm.roomInfo"
|
||||||
|
:rows="4" placeholder="如:标准双人间2间,大床房1间" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="结构化分配" name="structured">
|
||||||
|
<el-form>
|
||||||
|
<el-form-item label="订单号">{{ order.orderNo }}</el-form-item>
|
||||||
|
|
||||||
|
<!-- 按家庭动态表单 -->
|
||||||
|
<div v-for="(item, index) in assignments" :key="index"
|
||||||
|
class="family-card">
|
||||||
|
<div class="family-header">
|
||||||
|
<span>家庭 {{ index + 1 }}</span>
|
||||||
|
<el-button v-if="assignments.length > 1"
|
||||||
|
type="danger" text size="small"
|
||||||
|
@click="removeFamily(index)">删除</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form-item label="选择酒店" required>
|
||||||
|
<el-select v-model="item.hotelId"
|
||||||
|
placeholder="选择酒店"
|
||||||
|
@change="onHotelChange($event, index)">
|
||||||
|
<el-option v-for="h in hotelOptions" :key="h.hotelId"
|
||||||
|
:value="h.hotelId"
|
||||||
|
:label="h.inSnapshot
|
||||||
|
? `${h.hotelName}(产品快照)`
|
||||||
|
: h.hotelName" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="选择房型" required>
|
||||||
|
<el-select v-model="item.roomType" placeholder="选择房型">
|
||||||
|
<el-option v-for="rt in roomTypeOptions[index]"
|
||||||
|
:key="rt.name" :value="rt.name"
|
||||||
|
:label="`${rt.name}(最多${rt.maxOccupancy}人)`" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="入住日期" required>
|
||||||
|
<el-date-picker v-model="item.checkInDate"
|
||||||
|
type="date" value-format="YYYY-MM-DD" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="退房日期" required>
|
||||||
|
<el-date-picker v-model="item.checkOutDate"
|
||||||
|
type="date" value-format="YYYY-MM-DD" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-button type="primary" plain @click="addFamily">
|
||||||
|
+ 添加家庭
|
||||||
|
</el-button>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const activeTab = ref('structured')
|
||||||
|
const order = ref({})
|
||||||
|
const hotelOptions = ref([])
|
||||||
|
const roomTypeOptions = ref({})
|
||||||
|
const textForm = reactive({ roomInfo: '' })
|
||||||
|
const assignments = ref([
|
||||||
|
{ familyIndex: 1, hotelId: '', hotelName: '', roomType: '',
|
||||||
|
checkInDate: '', checkOutDate: '' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 打开弹窗
|
||||||
|
const open = async (orderData) => {
|
||||||
|
order.value = orderData
|
||||||
|
visible.value = true
|
||||||
|
// 默认日期
|
||||||
|
assignments.value.forEach(a => {
|
||||||
|
a.checkInDate = orderData.departureDate
|
||||||
|
// returnDate = departureDate + tripDays - 1
|
||||||
|
})
|
||||||
|
// 加载酒店列表
|
||||||
|
const res = await request.get(
|
||||||
|
`/api/order/admin/order/${orderData.orderId}/available-hotels`
|
||||||
|
)
|
||||||
|
hotelOptions.value = res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择酒店后联动房型
|
||||||
|
const onHotelChange = async (hotelId, index) => {
|
||||||
|
const hotel = hotelOptions.value.find(h => h.hotelId === hotelId)
|
||||||
|
if (hotel) {
|
||||||
|
assignments.value[index].hotelName = hotel.hotelName
|
||||||
|
}
|
||||||
|
const res = await request.get(
|
||||||
|
`/api/resource/admin/hotel/${hotelId}/room-types`
|
||||||
|
)
|
||||||
|
roomTypeOptions.value[index] = res.data
|
||||||
|
assignments.value[index].roomType = '' // 清空已选房型
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加/删除家庭
|
||||||
|
const addFamily = () => {
|
||||||
|
assignments.value.push({
|
||||||
|
familyIndex: assignments.value.length + 1,
|
||||||
|
hotelId: '', hotelName: '', roomType: '',
|
||||||
|
checkInDate: order.value.departureDate, checkOutDate: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const removeFamily = (index) => {
|
||||||
|
assignments.value.splice(index, 1)
|
||||||
|
// 重新编号
|
||||||
|
assignments.value.forEach((a, i) => a.familyIndex = i + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (activeTab.value === 'text') {
|
||||||
|
await request.put(
|
||||||
|
`/api/order/admin/order/${order.value.orderId}/room-info`,
|
||||||
|
{ roomInfo: textForm.roomInfo }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 结构化分配
|
||||||
|
const data = assignments.value.map((a, i) => ({
|
||||||
|
familyIndex: i + 1,
|
||||||
|
hotelName: a.hotelName,
|
||||||
|
roomType: a.roomType,
|
||||||
|
checkInDate: a.checkInDate,
|
||||||
|
checkOutDate: a.checkOutDate
|
||||||
|
}))
|
||||||
|
await request.put(
|
||||||
|
`/api/order/admin/order/${order.value.orderId}/hotel-assignment`,
|
||||||
|
{ assignments: data }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测房型一致性
|
||||||
|
const diffRes = await request.post(
|
||||||
|
`/api/order/admin/order/${order.value.orderId}/room-info/check-diff`
|
||||||
|
)
|
||||||
|
if (diffRes.data) {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`房型一致性提示:${diffRes.data}`,
|
||||||
|
'提示', { type: 'warning' }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success('配房成功')
|
||||||
|
visible.value = false
|
||||||
|
emit('success')
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open })
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关联字典
|
||||||
|
|
||||||
|
| 字典类型 | 说明 | 用途 |
|
||||||
|
|---------|------|------|
|
||||||
|
| room_category | 房型分类:STANDARD-标准间, DELUXE-豪华间, SUITE-套房, FAMILY-家庭房 | 房型列表展示 |
|
||||||
|
| bed_type | 床型:KING-大床, TWIN-双床, TATAMI-榻榻米 | 房型列表展示 |
|
||||||
|
| window_type | 窗户:WITH_WINDOW-有窗, NO_WINDOW-无窗, SKYLIGHT-天窗 | 房型列表展示 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 删掉的字段
|
||||||
|
|
||||||
|
当前弹窗中的"房间数量"字段应该**删除**——后端按家庭分配,每个家庭一间房,不需要单独设置数量。如果一个家庭需要2间房,就添加2行同样的家庭。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||||||
正在加载...
x
在新工单中引用
屏蔽一个用户