17 KiB
17 KiB
公司介绍(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 的
statusChip 文案改为status==1 ? introEnabled : introDisabled。
5.4 交互一致性
- 继续复用现有
showModalBottomSheet+StatefulBuilder表单骨架、PopupMenuButton编辑/删除、ScaffoldMessenger提示,保持与既有代码风格一致。
6. Image Upload Implementation Approach(图片上传实现方案)
- 选型:复用既有
image_picker(相册选择)+dio(FormData多部件)。不引入新依赖。 - 服务层:新增
UploadService(lib/services/upload_service.dart),封装POST /sys/upload,复用ApiClient.instance.dio(自动带令牌、解包信封、401 刷新)。 - 预览:
- 选中后本地即时预览:
Image.file(File(xFile.path))(需dart:io)。 - 上传成功回填 URL 后:
Image.network(url)预览(与现有 tile 预览一致)。
- 选中后本地即时预览:
- 存储:上传返回 URL 字符串写入
mediaUrlCtrl→ 随save*上送mediaUrl+mediaType='image';不本地持久化原文件路径(与 Web 模型一致)。 - 兜底:保留
mediaUrl手动输入,支持直接填视频/已托管图片地址;上传失败不阻断保存(可后补)。 - 文件名:用
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(风险与待确认项)
- 上传端点未知(高):
/sys/upload为假设;后端实际路径、字段名(file?)、返回结构(字符串 /{url}/{data})需后端确认。UploadService已兼容常见形态,但仍可能需微调。 status反转回归风险(中):当前模型反转 + 表单再反转"巧合正确",修改后需重点回归小类/详情的显示与保存往返,确保1=启用、0=禁用。- 大类
status移除(中):现有 Flutter 大类模型含status且仅列表展示;移除表单字段后,模型字段保留(防御性默认 0/1 不影响),但需确认后端save忽略该字段或忽略 null。 - 门店列表权限(低):
getStoreListByLoginOrg返回当前登录组织下门店;若运维账号可见全部门店则符合预期,否则筛选范围受登录态限制(与现有门店管理一致)。 - 分类超过 200 条(低):列表
limit:200上限下,多选过滤可能漏掉未加载分类;属既有限制,v6-fix38 暂不处理分页服务端过滤(后端不支持多orgId)。 categoryType必填性(低):Web 小类categoryType为可选,表单预填父大类值即可;若后端要求必填需补校验。- 鸿蒙端
image_picker(低):image_picker在 HarmonyOS 需对应插件适配;若不可用,图片上传在该端降级为手动mediaUrl输入(兜底已设计)。 - 视频上传(低):本期仅做图片上传;视频仍走
mediaUrl手动输入(与 Web 一致),不做视频文件上传。
附录:关键关系与流程(Mermaid)
A.1 类 / 服务关系
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 小类保存 + 图片上传时序
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: 关闭表单 + 刷新列表