Files
CloudHome-Ops/docs/intro_v38_design.md

271 lines
17 KiB
Markdown
Raw 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.

# 公司介绍(Company Intro)模块增强 — 架构设计文档
**版本演进**:`v6-fix37` → `v6-fix38`
**模块**:`lib/screens/admin/intro/*`(公司介绍 / 类型编辑 / 内容编辑)
**后端**:`https://ips-test.ainetv.com`(测试),合同参考 `screen-front/src/api/companyIntro.ts`
**目标**:多门店筛选、表单字段与 Web 后台对齐、内容图片上传预览、兼容既有代码风格。
---
## 1. Scope & Assumptions(范围与假设)
### 1.1 范围(In Scope)
- 大类列表页:**门店多选筛选**(客户端按 `orgId` 过滤)。
- **类型编辑(Category)** 表单对齐 Web:新增 `orgId/orgName` 选择;移除 Web 不存在的 `status` 字段。
- **内容编辑(Subcategory / Detail)** 表单对齐 Web:补齐 `sortOrder`、修正 `status` 语义、Subcategory 补齐可选的 `categoryType`。
- 内容编辑支持 **图片上传 + 本地预览 + 回写 `mediaUrl`**。
- `save` / `update` 按 `id` 是否存在分流,对齐 Web 后台。
### 1.2 假设(Assumptions)
- A1. 分类列表已全量加载(`getIntroCategoryList(limit: 200)`),客户端多选过滤足够;后端 list 接口仅支持单 `orgId`,**不支持多门店服务端过滤**,故采用客户端过滤。
- A2. 门店数据源复用既有 `OrgService.getStoreListByLoginOrg()`(返回 `List<StoreInfo>`,含 `orgId`/`name`),替代当前从分类推导 `_orgMap` 的脆弱写法。
- A3. 图片上传后端端点假定为 `POST /sys/upload`(表单字段名 `file`,返回 URL 字符串或 `{url}`)。**确切端点与返回结构待后端确认**(见 §9 风险)。若端点不可用,保留手动 `mediaUrl` 输入作为兜底。
- A4. 描述(`description`)为纯文本字段,不做富文本图片内嵌;"插入图片" 等价于上传并写入 `mediaUrl` + `mediaType='image'`(与 Web 模型一致)。
- A5. 不改动后端、不改 `pubspec` 依赖(`image_picker`、`dio` 已具备)。
- A6. 既有 `ApiInterceptor` 已统一解包 `{code,msg,data}` 信封并附带 Bearer 令牌,新上传请求复用 `ApiClient.instance.dio` 即可,无需额外鉴权处理。
---
## 2. File Changes List(文件变更清单)
| 相对路径 | 变更类型 | 说明 |
|---|---|---|
| `lib/utils/i10n.dart` | 修改 | 新增门店筛选 / 表单字段 / 上传相关 i18n 键(见 §8)。 |
| `lib/models/intro_category.dart` | 修改 | 修正 `status` 语义(= 后端原值,`1`=启用/`0`=禁用),删除 `_parseStatus` 反转逻辑;模型字段本身已含 `orgId/orgName/sort`,无需新增字段。 |
| `lib/services/intro_service.dart` | 修改 | `saveIntroCategory / saveIntroSubcategory / saveIntroDetail` 按 `id` 是否存在分流 `/save`↔`/update`(对齐 Web)。 |
| `lib/services/upload_service.dart` | **新增** | `UploadService.uploadImage(XFile)`:Dio `FormData` 多部件上传,返回 URL 字符串。 |
| `lib/screens/admin/intro/intro_category_page.dart` | 修改 | (a) 门店筛选改为 `FilterChip` 多选;(b) 大类表单加门店下拉、移除 `status`;(c) 小类/详情表单加 `sortOrder`、`status` 对齐、图片上传+预览;(d) `initState` 预载门店列表。 |
| `lib/services/org_service.dart` | 复用(不改) | 复用 `getStoreListByLoginOrg()` 提供门店列表。 |
| `lib/models/store_info.dart` | 复用(不改) | 复用 `StoreInfo.orgId` / `StoreInfo.name`。 |
> 变更集中在 1 个页面 + 2 个 service + 1 个 model + 1 个 i18n,符合"最小侵入、兼容既有风格"。
---
## 3. Data Model / Field Changes(数据模型与字段变更)
### 3.1 后端字段 vs 当前 Flutter 缺口对照
| 实体 | 后端字段(companyIntro.ts) | 当前 Flutter 缺口 |
|---|---|---|
| **Category** | `id, orgId, orgName, categoryType, name, createTime, updateTime` | 表单**缺 `orgId/orgName` 选择**;表单**多出 Web 不存在的 `status`** |
| **Subcategory** | `id, categoryId, categoryType, mediaType, mediaUrl, title, description, status, sortOrder` | 表单**缺 `sortOrder`**;`status` **语义反转**;**缺 `categoryType`**(可选);**无图片上传** |
| **Detail** | `id, subcategoryId, subcategoryTitle, mediaType, mediaUrl, title, description, status, sortOrder` | 表单**缺 `sortOrder`**;`status` **语义反转**;**无图片上传** |
### 3.2 `IntroCategory` / `IntroSubcategory` / `IntroDetail` 模型变更
- **`status` 语义统一为后端原值**:`1`=启用(正常)、`0`=禁用。
- 模型 `_parseStatus(dynamic)` 改为:`int.tryParse(value?.toString() ?? '') ?? 1`(缺省启用,对齐 Web `?? 1`);**删除** "1→0 / 0→1" 反转映射。
- `toJson()` 输出 `status` 原值,保存时**不再反转**。
- **`sort` 字段**(已存在,映射 `sortOrder`)保持不变;仅需在表单中暴露编辑(见 §5)。
- **`orgId` / `orgName`**(已存在)保持不变;仅需在 Category 表单中可编辑并随保存上送。
- 模型不再需要新增字段;`categoryType`(Subcategory 可选)可由表单 `TextEditingController` 直接读取父级大类值,无需模型新增。
### 3.3 新增 `UploadService`(伪代码,非实现)
```
class UploadService {
final Dio _dio; // = ApiClient.instance.dio
Future<String> uploadImage(XFile file) async {
// FormData: { 'file': MultipartFile.fromFile(file.path, filename: basename) }
// POST /sys/upload (复用 ApiInterceptor 解包)
// 返回 data 为 URL 字符串,或 {url: '...'} 时取 url
}
}
```
---
## 4. API Call Changes(接口调用变更)
### 4.1 `IntroService` 保存分流(对齐 Web `save* / update*`)
| 方法 | 当前 | 目标 |
|---|---|---|
| `saveIntroCategory(data)` | 始终 `POST /company/introCategory/save` | `id` 非空 → `…/update`,否则 `…/save` |
| `saveIntroSubcategory(data)` | 始终 `…/save` | 同上(含 `categoryId, categoryType?, mediaType, mediaUrl, title, description, status, sortOrder`) |
| `saveIntroDetail(data)` | 始终 `…/save` | 同上(含 `subcategoryId, mediaType, mediaUrl, title, description, status, sortOrder`) |
> 端点、`list/info/delete` 不变;`list` 仍客户端过滤(见 A1)。
### 4.2 新增上传调用
- `UploadService.uploadImage(XFile)` → `POST /sys/upload`(多部件)。(端点待确认,见 §9)
### 4.3 门店列表(复用,不改写)
- `OrgService().getStoreListByLoginOrg()` → `GET /sys/store/listByLoginOrg`,用于筛选 chips 与 Category 表单门店下拉。
---
## 5. UI/UX Changes(界面与交互变更)
### 5.1 大类列表 — 多门店筛选
- 替换现有单选 `DropdownButton<String?>` 为 **`Wrap` + `FilterChip` 多选**:
- 一项 `All`("全部门店");其余每项一个门店,`selected` = `_selectedOrgIds.contains(orgId)`。
- 空选集合 = 全部(与现有"全部门店"语义一致)。
- 状态:`List<String> _selectedOrgIds = []`。
- `_applyOrgFilter()`:`_selectedOrgIds.isEmpty ? _allCategories : _allCategories.where((c) => _selectedOrgIds.contains(c.orgId)).toList()`。
- 门店来源:`initState` 中 `OrgService().getStoreListByLoginOrg()` 构建 `Map<orgId, name>`(替代从分类推导,更完整)。
### 5.2 类型编辑(Category)表单
- **新增** 门店下拉 `DropdownButtonFormField<String>`(必填):选项来自门店列表,`value=orgId`,联动 `orgName`。
- **保留** `name`、`categoryType`(类型编码,纯字符串无中文)。
- **移除** `status` 下拉(Web 大类无 `status` 字段)。
- 保存 body:`{id?, orgId, orgName, categoryType, name}`(无 status/sort)。
### 5.3 小类(Subcategory)/ 详情(Detail)表单
- **新增 `sortOrder`**:`TextField`(`keyboardType: TextInputType.number`),上送 `sortOrder`。
- **`status` 对齐**:下拉项 `1`=启用/正常、`0`=禁用,默认 `editing?.status ?? 1`;保存上送原值(不再反转)。
- **小类新增可选 `categoryType`**:`TextField`,预填父大类 `categoryType`(Web 允许与大类一致)。
- **媒体区改造(图片上传)**:
- 保留 `mediaType` 下拉(image/video)与 `mediaUrl` 手动输入(兜底)。
- 新增「上传图片」按钮:`ImagePicker.pickImage(source: gallery)` → 本地 `Image.file` 预览 → 调 `UploadService.uploadImage` → 成功后回填 `mediaUrlCtrl.text=url` 且 `mediaType='image'`,并切换为 `Image.network(url)` 预览。
- 上传中显示 `CircularProgressIndicator` + `introUploading` 文案;失败 `SnackBar(introUploadFailed)`。
- 列表 tile 的 `status` Chip 文案改为 `status==1 ? introEnabled : introDisabled`。
### 5.4 交互一致性
- 继续复用现有 `showModalBottomSheet` + `StatefulBuilder` 表单骨架、`PopupMenuButton` 编辑/删除、`ScaffoldMessenger` 提示,保持与既有代码风格一致。
---
## 6. Image Upload Implementation Approach(图片上传实现方案)
1. **选型**:复用既有 `image_picker`(相册选择)+ `dio`(`FormData` 多部件)。**不引入新依赖**。
2. **服务层**:新增 `UploadService`(`lib/services/upload_service.dart`),封装 `POST /sys/upload`,复用 `ApiClient.instance.dio`(自动带令牌、解包信封、401 刷新)。
3. **预览**:
- 选中后本地即时预览:`Image.file(File(xFile.path))`(需 `dart:io`)。
- 上传成功回填 URL 后:`Image.network(url)` 预览(与现有 tile 预览一致)。
4. **存储**:上传返回 URL 字符串写入 `mediaUrlCtrl` → 随 `save*` 上送 `mediaUrl` + `mediaType='image'`;不本地持久化原文件路径(与 Web 模型一致)。
5. **兜底**:保留 `mediaUrl` 手动输入,支持直接填视频/已托管图片地址;上传失败不阻断保存(可后补)。
6. **文件名**:用 `xFile.path.split(Platform.pathSeparator).last` 取 basename,避免引入 `path` 包。
> 注意:上传端点 `/sys/upload` 为假设,返回结构(字符串 vs `{url}`)需与后端确认;`UploadService` 两种形态均兼容。
---
## 7. Incremental Task List(增量任务清单,按实现顺序)
| ID | 任务 | 涉及文件 | 依赖 | 优先级 |
|---|---|---|---|---|
| **T1** | **i18n 键新增**:门店筛选/表单字段/上传文案(§8 全部键) | `lib/utils/i10n.dart` | — | P0 |
| **T2** | **模型 status 语义修正**:`_parseStatus` 改为后端原值(`1`=启用),删除反转;`toJson` 不再反转 | `lib/models/intro_category.dart` | T1 | P0 |
| **T3** | **新增 `UploadService.uploadImage`**(Dio 多部件,兼容字符串/`{url}` 返回) | `lib/services/upload_service.dart`(新) | — | P1 |
| **T4** | **`IntroService` 保存分流**:三个 `save*` 按 `id` 是否存在走 `/save`↔`/update` | `lib/services/intro_service.dart` | — | P0 |
| **T5** | **多门店筛选 UI**:`FilterChip` 多选 + `initState` 预载 `getStoreListByLoginOrg` 构建门店 map + `_applyOrgFilter` 改为多值过滤 | `lib/screens/admin/intro/intro_category_page.dart` | T1 | P0 |
| **T6** | **大类表单对齐**:加门店下拉(必填,`orgId/orgName` 联动)、移除 `status` | `lib/screens/admin/intro/intro_category_page.dart` | T2, T4, T5 | P0 |
| **T7** | **小类表单对齐**:加 `sortOrder`、`categoryType`(预填)、`status` 对齐(1=启用)、图片上传+预览 | `lib/screens/admin/intro/intro_category_page.dart` | T2, T3, T4 | P0 |
| **T8** | **详情表单对齐**:加 `sortOrder`、`status` 对齐、图片上传+预览、tile 状态文案改用 `introEnabled/introDisabled` | `lib/screens/admin/intro/intro_category_page.dart` | T2, T3, T4 | P0 |
| **T9** | **联调与冒烟**:`flutter analyze` + 真机/模拟器跑通多店筛选、增改、图片上传,`v6-fix38` 构建 | 全部 | T1–T8 | P1 |
> 依赖示意:`T1→T2→{T6,T7,T8}`;`T4→{T6,T7,T8}`;`T3→{T7,T8}`;`T5→T6`。
---
## 8. i18n Key Additions(需新增的 i18n 键)
新增于 `lib/utils/i10n.dart` 的「公司介绍」分区(中 / 英):
| Key | 中文 | English | 用途 |
|---|---|---|---|
| `introFilterStores` | 筛选门店 | Filter Stores | 多选 chip 区标题 |
| `introSelectStoresHint` | 可多选门店 | Select multiple stores | 空态提示 |
| `introAllStores` | *已存在* | *已存在* | 全部门店(复用) |
| `introCategoryStore` | 所属门店 | Store | 大类表单门店下拉 label |
| `introStoreRequired` | 请选择所属门店 | Please select a store | 门店必填校验 |
| `introSortOrder` | 排序号 | Sort Order | 小类/详情 `sortOrder` label |
| `introEnabled` | 启用 | Enabled | `status==1` 显示 |
| `introDisabled` | 禁用 | Disabled | `status==0` 显示 |
| `introUploadImage` | 上传图片 | Upload Image | 图片上传按钮 |
| `introPickImage` | 选择图片 | Pick Image | 选择前占位按钮 |
| `introImagePreview` | 图片预览 | Image Preview | 预览区标题 |
| `introUploading` | 上传中… | Uploading… | 上传 loading 文案 |
| `introUploadFailed` | 图片上传失败 | Image upload failed | 上传失败提示 |
| `introInsertImage` | 插入图片 | Insert Image | (可选)上传并写入 mediaUrl 按钮 |
> 注:`statusNormal`/`statusDisabled` 已存在但语义与 Web 相反,改用新增的 `introEnabled`/`introDisabled` 以避免歧义;如团队倾向复用既有键,可二选一,但需同步修正显示判断。
---
## 9. Potential Risks / Open Questions(风险与待确认项)
1. **上传端点未知(高)**:`/sys/upload` 为假设;后端实际路径、字段名(`file`?)、返回结构(字符串 / `{url}` / `{data}`)需后端确认。`UploadService` 已兼容常见形态,但仍可能需微调。
2. **`status` 反转回归风险(中)**:当前模型反转 + 表单再反转"巧合正确",修改后需重点回归小类/详情的显示与保存往返,确保 `1`=启用、`0`=禁用。
3. **大类 `status` 移除(中)**:现有 Flutter 大类模型含 `status` 且仅列表展示;移除表单字段后,模型字段保留(防御性默认 0/1 不影响),但需确认后端 `save` 忽略该字段或忽略 null。
4. **门店列表权限(低)**:`getStoreListByLoginOrg` 返回当前登录组织下门店;若运维账号可见全部门店则符合预期,否则筛选范围受登录态限制(与现有门店管理一致)。
5. **分类超过 200 条(低)**:列表 `limit:200` 上限下,多选过滤可能漏掉未加载分类;属既有限制,v6-fix38 暂不处理分页服务端过滤(后端不支持多 `orgId`)。
6. **`categoryType` 必填性(低)**:Web 小类 `categoryType` 为可选,表单预填父大类值即可;若后端要求必填需补校验。
7. **鸿蒙端 `image_picker`(低)**:`image_picker` 在 HarmonyOS 需对应插件适配;若不可用,图片上传在该端降级为手动 `mediaUrl` 输入(兜底已设计)。
8. **视频上传(低)**:本期仅做图片上传;视频仍走 `mediaUrl` 手动输入(与 Web 一致),不做视频文件上传。
---
## 附录:关键关系与流程(Mermaid)
### A.1 类 / 服务关系
```mermaid
classDiagram
class IntroCategoryPage {
+List~String~ _selectedOrgIds
+Map~String,String~ _orgMap
+_buildOrgFilter() FilterChip
+_showCategoryForm() 加门店/去status
+_showSubcategoryForm() 加sortOrder+上传
+_showDetailForm() 加sortOrder+上传
}
class IntroService {
+saveIntroCategory(data) /save|/update
+saveIntroSubcategory(data) /save|/update
+saveIntroDetail(data) /save|/update
}
class UploadService {
+uploadImage(XFile) String
}
class OrgService {
+getStoreListByLoginOrg() List~StoreInfo~
}
class StoreInfo {
+String orgId
+String name
}
class IntroCategory {
+String orgId
+String orgName
+int status // =后端原值 1启用/0禁用
}
class IntroSubcategory {
+int sort // sortOrder
+int status
}
class IntroDetail {
+int sort
+int status
}
IntroCategoryPage ..> IntroService : 保存分流
IntroCategoryPage ..> UploadService : 图片上传
IntroCategoryPage ..> OrgService : 门店列表
OrgService ..> StoreInfo
IntroService ..> IntroCategory
IntroService ..> IntroSubcategory
IntroService ..> IntroDetail
```
### A.2 小类保存 + 图片上传时序
```mermaid
sequenceDiagram
participant U as 用户
participant P as IntroCategoryPage
participant UP as UploadService
participant S as IntroService
participant B as 后端
U->>P: 打开小类表单,选图「上传图片」
P->>P: ImagePicker.pickImage → 本地预览
P->>UP: uploadImage(xFile)
UP->>B: POST /sys/upload (FormData file)
B-->>UP: {code,data: url}
UP-->>P: url
P->>P: mediaUrlCtrl=url, mediaType='image', 网络预览
U->>P: 填 sortOrder/status/title → 保存
P->>S: saveIntroSubcategory({id?,categoryId,...,sortOrder,status,mediaUrl})
S->>B: POST /update (有id) 或 /save
B-->>S: {code,data}
S-->>P: 成功
P->>P: 关闭表单 + 刷新列表
```