Files
rui-docs/superpowers/specs/2026-06-08-store-new-fields-design.md

394 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 门店管理新增字段设计规范
**工单**: rui/rui-frontend#6 — 门店管理适配后端新增字段
**日期**: 2026-06-08
**关联 Issue**: rui/rui-cashier#6(后端门店表新增字段)
**优先级**: P2
---
## 1. 目标(Goal
在 admin-ui 门店管理模块的列表页和表单弹窗中,适配后端门店表新增的 9 个字段(storeType、amenities、longitude、latitude、roomCount、freeRoomCount、serviceFeeRate、openingDate、legalPerson)。表单弹窗新增 7 个可编辑字段和 2 个只读展示字段,列表页新增 3 列(门店类型标签、包间信息、设施标签)和 1 个筛选条件(门店类型下拉)。所有变更在现有 `useApiForm` + `ApiFormDialog` + `RuiTable` 框架内完成,不引入新依赖。
## 2. 非目标(Non-Goals
明确不在本期范围内的事项:
- **不修改后端代码或 API 接口定义**:后端已提供字段,前端仅适配展示和交互。
- **不做地图组件集成**:经纬度字段使用纯数字输入框,不嵌入高德/百度地图选点组件。
- **不修改 storeService.ts**:现有 BaseService 的 CRUD 方法已覆盖需求,无需扩展。
- **不新增独立详情页**:沿用当前「列表 + 表单弹窗」模式,只读字段在编辑弹窗中展示。
- **不做包间数量编辑**roomCount 和 freeRoomCount 由后端计算,前端仅只读展示。
- **不引入新 npm 依赖**:所有 UI 组件使用现有 Element Plus + useApiForm 字段类型体系。
## 3. 背景与上下文(Context
### 3.1 现有门店模块结构
- **列表页** `admin-ui/src/views/cashier/store/Index.vue`:使用 `RuiTable` 组件,现有 7 列(门店名称、门店编码、联系人、联系电话、地址、营业时间、状态),2 个筛选条件(门店名称、状态),支持增删改查和状态切换。
- **表单弹窗** `admin-ui/src/views/cashier/store/StoreFormDialog.vue`:使用 `useApiForm` composable + `ApiFormDialog` 组件,现有 7 个字段(storeName、storeCode、address、contactName、contactPhone、businessHours、status),通过 `v-model:visible` / `row` prop 控制显示和数据回填。
- **服务层** `admin-ui/src/service/cashier/storeService.ts`:继承 `BaseService('/cashier/admin/store')`,13 行代码,自动拥有 page/add/update/remove/changeStatus 能力。
### 3.2 useApiForm 字段类型支持
`useApiForm` 支持 9 种字段类型:`input``textarea``select``radio``checkbox``number``tree-select``date``datetime`。每个字段支持 `disabled` 属性(布尔值或返回布尔值的函数),可用于按编辑/新增模式动态禁用。
`ApiFormDialog` 在所有标准字段之后渲染 `<slot name="custom-fields" :form="formData" />`,用于放置无法用标准字段类型表达的 UI 内容。
### 3.3 后端新增字段一览
| 字段 | camelCase 键 | Java 类型 | 后端存储格式 |
|------|-------------|----------|-------------|
| 门店类型 | storeType | String | 纯字符串:`FLAGSHIP` / `STANDARD` / `COMMUNITY` |
| 设施标签 | amenities | String | JSON 数组字符串:`"[\"免费停车\",\"免费WiFi\"]"` |
| 经度 | longitude | BigDecimal | 小数 |
| 纬度 | latitude | BigDecimal | 小数 |
| 包间总数 | roomCount | Integer | 整数(后端计算) |
| 空闲包间数 | freeRoomCount | Integer | 整数(后端计算) |
| 平台服务费率 | serviceFeeRate | BigDecimal | 小数,如 `0.05` 表示 5% |
| 开业日期 | openingDate | LocalDate | `yyyy-MM-dd` 字符串 |
| 法人姓名 | legalPerson | String | 纯文本 |
## 4. 关键设计决策
| # | 决策项 | 选定方案 | 理由 |
|---|--------|---------|------|
| 1 | 整体方案 | 在现有 useApiForm + ApiFormDialog 框架内扩展 | 复用现有基础设施,保持与其他模块一致的开发模式 |
| 2 | amenities 表现层 | 使用 `checkbox` 字段类型,options 固定 6 项 | useApiForm 原生支持 checkbox,无需自定义渲染 |
| 3 | amenities 数据转换 | 提交时 `JSON.stringify`,编辑回填时 `JSON.parse` | 后端存 JSON 字符串,前端表单使用 `string[]` |
| 4 | serviceFeeRate 展示 | 前端用百分比数值(5 表示 5%),提交时除以 100,编辑回填时乘以 100 | 用户体验直观,避免手动输入 0.05 这样的小数 |
| 5 | roomCount / freeRoomCount | 使用 `#custom-fields` 插槽渲染只读文本 | 这两个字段仅编辑模式下只读展示,不需要 useApiForm 字段配置 |
| 6 | 门店类型枚举 | 前端硬编码 3 个选项 | 后端接口稳定,选项固定,无需动态加载 |
| 7 | 经纬度输入 | `number` 类型字段,精度限制 6 位小数 | 经纬度通常保留 6 位即可满足定位需求 |
## 5. 字段配置详情
### 5.1 useApiForm 字段新增(7 个可编辑字段)
以下字段追加到 `StoreFormDialog.vue``useApiForm``fields` 数组:
```ts
{
key: 'storeType',
label: '门店类型',
type: 'select',
required: true,
options: [
{ label: '旗舰店', value: 'FLAGSHIP' },
{ label: '标准店', value: 'STANDARD' },
{ label: '社区店', value: 'COMMUNITY' },
],
},
{
key: 'amenities',
label: '设施标签',
type: 'checkbox',
options: [
{ label: '免费停车', value: '免费停车' },
{ label: '免费WiFi', value: '免费WiFi' },
{ label: '充电桩', value: '充电桩' },
{ label: '24小时营业', value: '24小时营业' },
{ label: '包厢', value: '包厢' },
{ label: '吸烟区', value: '吸烟区' },
],
},
{
key: 'longitude',
label: '经度',
type: 'number',
props: { min: -180, max: 180, precision: 6, step: 0.000001 },
},
{
key: 'latitude',
label: '纬度',
type: 'number',
props: { min: -90, max: 90, precision: 6, step: 0.000001 },
},
{
key: 'serviceFeeRate',
label: '平台服务费率(%)',
type: 'number',
props: { min: 0, max: 100, precision: 2, step: 0.01 },
},
{
key: 'openingDate',
label: '开业日期',
type: 'date',
props: { valueFormat: 'YYYY-MM-DD' },
},
{
key: 'legalPerson',
label: '法人姓名',
type: 'input',
},
```
### 5.2 弹窗宽度调整
`StoreFormDialog``ApiFormDialog` 追加 `width` prop 为 `720px`(原默认 600px),因为新增字段较多,需要更宽的弹窗空间。
### 5.3 只读展示字段(2 个,通过 custom-fields 插槽)
`roomCount``freeRoomCount` 不加入 `fields` 数组,而是在 `ApiFormDialog``#custom-fields` 插槽中以 `el-form-item` + 纯文本方式渲染。仅在编辑模式(`form.id` 存在)时显示:
```vue
<template #custom-fields="{ form }">
<template v-if="form.id">
<el-form-item label="包间总数">
<span>{{ form.roomCount ?? '-' }}</span>
</el-form-item>
<el-form-item label="空闲包间数">
<span>{{ form.freeRoomCount ?? '-' }}</span>
</el-form-item>
</template>
</template>
```
### 5.4 initial 默认值扩展
`useApiForm``initial` 对象中追加新字段默认值:
```ts
initial: {
// ...现有字段
storeType: 'STANDARD',
amenities: [],
longitude: undefined,
latitude: undefined,
serviceFeeRate: undefined,
openingDate: '',
legalPerson: '',
},
```
### 5.5 数据转换逻辑
#### 提交时(`onSubmit` 回调内)
```ts
onSubmit: async (rawData) => {
const data = { ...rawData }
// amenities: string[] → JSON 字符串
if (Array.isArray(data.amenities)) {
data.amenities = JSON.stringify(data.amenities)
}
// serviceFeeRate: 百分比 → 小数
if (data.serviceFeeRate != null && data.serviceFeeRate !== '') {
data.serviceFeeRate = Number(data.serviceFeeRate) / 100
}
// ...现有 add/update 逻辑
},
```
#### 编辑回填时(`watch(visible)` 内)
```ts
watch(() => props.visible, (val) => {
if (val && props.row) {
const rowData = { ...props.row }
// amenities: JSON 字符串 → string[]
if (typeof rowData.amenities === 'string' && rowData.amenities) {
try {
rowData.amenities = JSON.parse(rowData.amenities)
} catch {
rowData.amenities = []
}
} else if (!Array.isArray(rowData.amenities)) {
rowData.amenities = []
}
// serviceFeeRate: 小数 → 百分比
if (rowData.serviceFeeRate != null) {
rowData.serviceFeeRate = Number(rowData.serviceFeeRate) * 100
}
form.value = rowData
}
})
```
## 6. 列表页变更(Index.vue
### 6.1 新增列配置
`columns` 数组中,`address` 列之后追加以下 3 列:
```ts
{
prop: 'storeType',
label: '门店类型',
width: 100,
align: 'center',
slot: true,
},
{
prop: 'roomInfo',
label: '包间',
width: 120,
align: 'center',
slot: true,
},
{
prop: 'amenities',
label: '设施标签',
minWidth: 200,
slot: true,
},
```
### 6.2 新增列 Slot 模板
#### 门店类型列(彩色 Tag
```vue
<template #column-storeType="{ row }">
<el-tag
:type="row.storeType === 'FLAGSHIP' ? 'danger' : row.storeType === 'STANDARD' ? '' : 'info'"
size="small"
>
{{ { FLAGSHIP: '旗舰店', STANDARD: '标准店', COMMUNITY: '社区店' }[row.storeType] || '-' }}
</el-tag>
</template>
```
#### 包间信息列("空闲X/总数Y" 格式)
```vue
<template #column-roomInfo="{ row }">
<span>{{ row.freeRoomCount ?? '-' }}/{{ row.roomCount ?? '-' }}</span>
</template>
```
#### 设施标签列(多个小 Tag)
```vue
<template #column-amenities="{ row }">
<template v-if="parseAmenities(row.amenities).length">
<el-tag
v-for="tag in parseAmenities(row.amenities)"
:key="tag"
size="small"
type="info"
class="mr-1 mb-1"
>
{{ tag }}
</el-tag>
</template>
<span v-else>-</span>
</template>
```
其中 `parseAmenities` 为组件内的工具函数:
```ts
function parseAmenities(val: any): string[] {
if (Array.isArray(val)) return val
if (typeof val === 'string' && val) {
try { return JSON.parse(val) } catch { return [] }
}
return []
}
```
### 6.3 新增筛选条件
`queryParams` 中增加 `storeType` 字段,在查询区增加门店类型下拉:
```ts
const queryParams = ref({
storeName: '',
status: undefined as number | undefined,
storeType: undefined as string | undefined,
})
```
查询区模板中,状态筛选后追加:
```vue
<el-form-item label="门店类型">
<el-select v-model="queryParams.storeType" placeholder="请选择门店类型" clearable>
<el-option label="旗舰店" value="FLAGSHIP" />
<el-option label="标准店" value="STANDARD" />
<el-option label="社区店" value="COMMUNITY" />
</el-select>
</el-form-item>
```
`handleReset` 方法中补充 `storeType: undefined` 重置。
## 7. 涉及文件清单(Files To Change
| # | 文件 | 操作 | 变更说明 |
|---|------|------|---------|
| 1 | `admin-ui/src/views/cashier/store/StoreFormDialog.vue` | 修改 | 新增 7 个 useApiForm 字段、custom-fields 插槽渲染 roomCount/freeRoomCount、数据转换逻辑、弹窗宽度调整为 720px |
| 2 | `admin-ui/src/views/cashier/store/Index.vue` | 修改 | 新增 3 列配置(storeType/roomInfo/amenities)及对应 slot 模板、新增 storeType 筛选条件、queryParams 扩展、parseAmenities 工具函数、handleReset 补充 |
**变更统计**:修改 2 个文件。**不新建文件,不修改服务层和路由**。
## 8. 测试策略
### 8.1 静态检查
```bash
pnpm --filter admin-ui type-check # 0 errors
pnpm --filter admin-ui lint # 0 errors
```
### 8.2 功能验证(手动)
| # | 验证场景 | 预期结果 |
|---|---------|---------|
| 1 | 列表加载 | 新增 3 列正确展示:门店类型(Tag)、包间(X/Y 格式)、设施标签(多个小 Tag) |
| 2 | 门店类型筛选 | 下拉选择后点击查询,列表按类型过滤;重置后筛选条件清空 |
| 3 | 无数据的门店 | storeType 为空显示 `-`amenities 为空显示 `-`,包间无数据显示 `-/--` |
| 4 | 新增门店 | 弹窗宽度 720px,7 个新字段正确渲染,roomCount/freeRoomCount 不显示(新增模式) |
| 5 | 新增提交 | amenities 提交为 JSON 字符串,serviceFeeRate 提交为小数(5→0.05 |
| 6 | 编辑门店 | 弹窗回填所有字段:amenities 从 JSON 字符串解析为勾选状态,serviceFeeRate 从小数转为百分比(0.05→5) |
| 7 | 编辑模式只读字段 | roomCount 和 freeRoomCount 以纯文本显示,不可编辑 |
| 8 | 新增模式无只读字段 | 新增门店时 roomCount 和 freeRoomCount 区域不显示 |
| 9 | 门店类型必填校验 | storeType 为空时提交,表单校验不通过 |
### 8.3 验证清单(提交前必过)
- [ ] `pnpm --filter admin-ui type-check` 通过
- [ ] `pnpm --filter admin-ui lint` 通过
- [ ] 列表新增 3 列正确展示
- [ ] 门店类型筛选功能正常
- [ ] 新增门店:7 个新字段可正常填写和提交
- [ ] 编辑门店:所有新字段正确回填,只读字段不可编辑
- [ ] amenities 数据双向转换正确(JSON 字符串 ↔ 数组)
- [ ] serviceFeeRate 数据双向转换正确(小数 ↔ 百分比)
## 9. 风险与缓解(Risks And Mitigations
| # | 风险 | 缓解措施 |
|---|------|---------|
| 1 | **amenities JSON 解析失败**:后端返回格式异常或 null 时,前端 JSON.parse 抛错导致表单崩溃 | 在编辑回填时用 try-catch 包裹 JSON.parse,解析失败降级为空数组 `[]`。在列表 parseAmenities 工具函数中同样做防御性解析。 |
| 2 | **serviceFeeRate 精度问题**BigDecimal 除以 100 或乘以 100 可能产生浮点精度误差 | 使用 `Number()` 转换后通过 `el-input-number``precision: 2` 限制小数位数,避免显示多余精度。后端使用 BigDecimal 不会丢失精度。 |
| 3 | **后端字段尚未部署**:前端先于后端部署时,新增列显示空值 | 列表和表单均对 undefined/null 做降级处理(显示 `-`),不影响现有功能。前端部署无阻断风险。 |
| 4 | **弹窗字段过多导致布局拥挤**:原有 7 个字段 + 新增 9 个字段共 16 个表单项 | 弹窗宽度从 600px 增至 720px;新增模式不显示 roomCount/freeRoomCount,实际展示 14 项。后续如仍拥挤可考虑分组或 tabs 布局。 |
## 10. 决策摘要(Decision Summary
- **架构**:在现有 `useApiForm` + `ApiFormDialog` + `RuiTable` 框架内扩展,不新建文件、不引入新依赖。
- **amenities**:使用 `checkbox` 字段类型,options 固定 6 项。提交时 `JSON.stringify`,编辑回填时 `JSON.parse`,均做防御性处理。
- **serviceFeeRate**:前端以百分比输入/显示,提交时 `÷100`,回填时 `×100`
- **roomCount / freeRoomCount**:通过 `#custom-fields` 插槽只读展示,仅在编辑模式显示。
- **storeType**`select` 字段,3 个固定选项(FLAGSHIP/STANDARD/COMMUNITY),列表用彩色 Tag 展示,增加筛选条件。
- **longitude / latitude**`number` 字段,精度 6 位小数,范围限制经度 [-180, 180]、纬度 [-90, 90]。
- **openingDate**`date` 字段,valueFormat 为 `YYYY-MM-DD`
- **legalPerson**`input` 字段,无特殊处理。
- **列表新增**:3 列(门店类型 Tag、包间 X/Y、设施多 Tag)+ 1 个筛选条件。
- **弹窗宽度**:从默认 600px 增至 720px。
- **文件变更**:仅修改 2 个文件(StoreFormDialog.vue、Index.vue)。
---
**设计评审状态**: 待评审
**下一步**: 用户评审通过后,编写实施计划(Plan)