# 公司介绍(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`,含 `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 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` 为 **`Wrap` + `FilterChip` 多选**: - 一项 `All`("全部门店");其余每项一个门店,`selected` = `_selectedOrgIds.contains(orgId)`。 - 空选集合 = 全部(与现有"全部门店"语义一致)。 - 状态:`List _selectedOrgIds = []`。 - `_applyOrgFilter()`:`_selectedOrgIds.isEmpty ? _allCategories : _allCategories.where((c) => _selectedOrgIds.contains(c.orgId)).toList()`。 - 门店来源:`initState` 中 `OrgService().getStoreListByLoginOrg()` 构建 `Map`(替代从分类推导,更完整)。 ### 5.2 类型编辑(Category)表单 - **新增** 门店下拉 `DropdownButtonFormField`(必填):选项来自门店列表,`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: 关闭表单 + 刷新列表 ```