ops-console v1.0.2 (versionCode 8) 源码入库
This commit is contained in:
87
docs/class-diagram.mermaid
Normal file
87
docs/class-diagram.mermaid
Normal file
@@ -0,0 +1,87 @@
|
||||
classDiagram
|
||||
class StoreInfo {
|
||||
+String orgId
|
||||
+String name
|
||||
+String address
|
||||
+String contactPhone
|
||||
+String activationCode
|
||||
+int status
|
||||
+DateTime? expireTime
|
||||
+DateTime? createTime
|
||||
<<新增>>
|
||||
+String? themeId
|
||||
+String? splashUrl
|
||||
+String? logoUrl
|
||||
+String? provinceCode
|
||||
+String? cityCode
|
||||
+String? districtCode
|
||||
+String? addressDetail
|
||||
+StoreInfo.fromJson(Map)
|
||||
+Map toJson()
|
||||
+String get regionText()
|
||||
}
|
||||
|
||||
class RegionNode {
|
||||
+String code
|
||||
+String name
|
||||
+List~RegionNode~ sub
|
||||
+RegionNode.fromJson(Map)
|
||||
}
|
||||
|
||||
class StoreTheme {
|
||||
<<enum>>
|
||||
classic
|
||||
ocean
|
||||
forest
|
||||
sunset
|
||||
mono
|
||||
+String get id
|
||||
+String get label
|
||||
+Color get primary
|
||||
}
|
||||
|
||||
class StoreThemePresets {
|
||||
<<const>>
|
||||
+List~StoreTheme~ all
|
||||
+StoreTheme? byId(String)
|
||||
}
|
||||
|
||||
class RegionData {
|
||||
-List~RegionNode~ _tree
|
||||
+Future~void~ load()
|
||||
+List~RegionNode~ get provinces
|
||||
+List~RegionNode~ citiesOf(String)
|
||||
+List~RegionNode~ districtsOf(String,String)
|
||||
+String nameOf(String)
|
||||
+String tryMapAddress(String)~List~String?~
|
||||
}
|
||||
|
||||
class StoreLocalCache {
|
||||
+Future~void~ saveTheme(String orgId, String themeId)
|
||||
+Future~String?~ getTheme(String orgId)
|
||||
+Future~void~ saveBranding(String orgId, String splashUrl, String logoUrl)
|
||||
+Future~Map?~ getBranding(String orgId)
|
||||
+Future~void~ saveRegion(String orgId, String p, String c, String d, String detail)
|
||||
}
|
||||
|
||||
class OrgService {
|
||||
+Future~void~ saveStore(Map) // TODO 对接 POST /sys/store/save
|
||||
+Future~void~ updateStore(Map) // TODO 对接 POST /sys/store/update
|
||||
+Future~void~ setStoreTheme(String,String) // TODO POST /sys/store/theme
|
||||
+Future~void~ setStoreBranding(String,String,String) // TODO POST /sys/store/branding
|
||||
}
|
||||
|
||||
class DeviceService {
|
||||
+Future~void~ bindDeviceByScan(String scanPayload, String storeId, String? groupId) // TODO POST /device/bind
|
||||
}
|
||||
|
||||
class DeviceScanPage {
|
||||
+String mode // 'query' | 'bind'
|
||||
+String? storeId
|
||||
+String? groupId
|
||||
}
|
||||
|
||||
StoreInfo "1" *-- "1" StoreTheme : themeId 关联
|
||||
RegionData "1" *-- "0..*" RegionNode : 树
|
||||
OrgService ..> StoreInfo : 读写
|
||||
DeviceService ..> DeviceScanPage : 扫码结果
|
||||
270
docs/intro_v38_design.md
Normal file
270
docs/intro_v38_design.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# 公司介绍(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: 关闭表单 + 刷新列表
|
||||
```
|
||||
70
docs/sequence-diagram.mermaid
Normal file
70
docs/sequence-diagram.mermaid
Normal file
@@ -0,0 +1,70 @@
|
||||
%% 主流程:门店编辑(省市区+主题+品牌)
|
||||
sequenceDiagram
|
||||
actor U as 用户
|
||||
participant S as _StoreFormSheet
|
||||
participant RP as RegionPicker
|
||||
participant RD as RegionData
|
||||
participant TG as StoreThemeGrid
|
||||
participant TP as StoreThemePreview
|
||||
participant BE as BrandingEditor
|
||||
participant IP as image_picker
|
||||
participant OS as OrgService
|
||||
participant LC as StoreLocalCache
|
||||
|
||||
U->>S: 打开新增/编辑门店
|
||||
S->>RD: load() (懒加载 province.json)
|
||||
U->>RP: 点「地址」
|
||||
RP->>RD: citiesOf/ districtsOf
|
||||
RP-->>S: provinceCode/cityCode/districtCode + text
|
||||
U->>TG: 点「主题皮肤」
|
||||
TG->>TP: 选 themeId → 预览模拟头部
|
||||
TG-->>S: themeId
|
||||
U->>BE: 点「启动图/Logo」
|
||||
BE->>IP: 选图
|
||||
IP-->>BE: XFile.path
|
||||
BE-->>S: splashUrl/logoUrl (本地路径)
|
||||
U->>S: 点「保存」
|
||||
S->>OS: saveStore(data) [占位]
|
||||
OS->>LC: saveTheme/saveBranding/saveRegion
|
||||
OS-->>S: 完成(未真实上报 TODO)
|
||||
S-->>U: 关闭 sheet, 刷新列表
|
||||
|
||||
%% 设备扫码:查询模式
|
||||
sequenceDiagram
|
||||
actor U as 用户
|
||||
participant DL as device_list_page
|
||||
participant SP as device_scan_page
|
||||
participant MS as mobile_scanner
|
||||
participant DS as DeviceService
|
||||
|
||||
U->>DL: AppBar 扫码图标
|
||||
DL->>SP: go('/device/scan?mode=query')
|
||||
SP->>MS: 实时扫码 / 相册识别
|
||||
MS-->>SP: scanPayload (mac/sn)
|
||||
SP->>DS: fetchDevices(mac: payload)
|
||||
DS-->>SP: Device?
|
||||
alt 命中
|
||||
SP-->>U: go('/device/:id') 跳详情
|
||||
else 未命中
|
||||
SP-->>U: 提示「未找到设备」
|
||||
end
|
||||
|
||||
%% 设备扫码:绑定模式
|
||||
sequenceDiagram
|
||||
actor U as 用户
|
||||
participant DD as device_detail_page
|
||||
participant SP as device_scan_page
|
||||
participant MS as mobile_scanner
|
||||
participant DS as DeviceService
|
||||
participant LC as StoreLocalCache
|
||||
|
||||
U->>DD: 「扫二维码绑定」
|
||||
DD->>SP: go('/device/scan?mode=bind&deviceId=xxx')
|
||||
SP->>MS: 扫码得门店/分组码
|
||||
MS-->>SP: scanPayload
|
||||
SP->>U: 弹窗确认绑定门店/分组
|
||||
U->>SP: 确认
|
||||
SP->>DS: bindDeviceByScan(payload, storeId, groupId) [占位]
|
||||
DS->>LC: updateDevice(groupId/storeName)
|
||||
DS-->>SP: 完成(真实 POST TODO)
|
||||
SP-->>DD: 返回, 展示更新归属
|
||||
292
docs/system_design.md
Normal file
292
docs/system_design.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# 云端管理端 — 增量架构设计(4 项功能:前端占位实现)
|
||||
|
||||
> 版本:v1.0|架构师:高见远|基于 PRD + 现有代码事实
|
||||
> 关键决策:**4 项功能全部「前端先占位」实现**——本地模型 + 预留接口 TODO 占位,不强制依赖真实后台(后端当前缺 `themeId`/`splashUrl`/`logoUrl` 字段、无图片上传/扫码绑定/门店 save 接口)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 实现方案 + 框架选型
|
||||
|
||||
### 1.1 技术难点分析
|
||||
|
||||
| 功能 | 难点 | 解决思路 |
|
||||
|------|------|----------|
|
||||
| 门店主题/皮肤 | 后台缺主题字段,需纯前端内置主题库 + 本地预览 | 主题枚举 + 内置占位资源(纯色块/Assets 图),本地模型存 `themeId`,预留 `setStoreTheme` |
|
||||
| 省市级联 | 34 省三级联动数据加载与回显映射 | 复用 `province.json`,封装 `RegionPicker` 组件,结构化存 `code` + 拼接文本,既有文本地址尽力映射 |
|
||||
| 扫码绑定 | 跨平台扫码(Android/iOS/鸿蒙),替代老 ScanKit | 引入 `mobile_scanner`(通用、维护活跃),解析后本地查找设备并跳转 |
|
||||
| 启动图/Logo | 图片选择 + 手机外框预览,后台无上传接口 | `image_picker` 选图本地预览,`branding` 字段存本地路径,预留 `uploadAsset`/`setStoreBranding` |
|
||||
|
||||
### 1.2 框架 / 包选型
|
||||
|
||||
沿用现有栈 **Flutter 3.27.4 + riverpod + go_router + Dio**,不引入新状态框架。新增 2 个能力包:
|
||||
|
||||
| 包 | 版本(建议) | 用途 |
|
||||
|----|------|------|
|
||||
| `mobile_scanner` | ^5.2.0 | 跨平台相机扫码 + 相册识别二维码(替代华为 ScanKit) |
|
||||
| `image_picker` | ^1.1.2 | 启动图/Logo 图片选择(相册/拍照) |
|
||||
|
||||
> 注:`mobile_scanner` 需在 `AndroidManifest.xml`/`Info.plist`/`ohos` 配置相机权限,本设计只到「接口与组件占位」层级,权限字符串由工程师按平台补。
|
||||
|
||||
### 1.3 pubspec.yaml 改动
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
# 现有:flutter / go_router / flutter_riverpod / dio / intl
|
||||
mobile_scanner: ^5.2.0
|
||||
image_picker: ^1.1.2
|
||||
|
||||
flutter:
|
||||
assets:
|
||||
- assets/data/MCityPiker/province.json
|
||||
- assets/theme/ # 新增:内置主题占位图(如 theme_1.png ...)
|
||||
```
|
||||
|
||||
### 1.4 架构模式
|
||||
|
||||
保持现有 **Widget + Service + Provider** 分层,无新增抽象层:
|
||||
- 模型(`lib/models/`):`StoreInfo` 扩展字段
|
||||
- 服务(`lib/services/`):`OrgService`/`DeviceService` 新增预留 TODO 方法
|
||||
- 组件(`lib/widgets/`):`RegionPicker`、`StoreThemeGrid`、`BrandingEditor`
|
||||
- 页面(`lib/screens/`):门店表单接入、设备扫码入口
|
||||
|
||||
---
|
||||
|
||||
## 2. 文件列表(新增 / 修改,相对路径)
|
||||
|
||||
### 新增文件
|
||||
- `lib/data/region_data.dart` — 加载 `province.json` 并解析为 `RegionNode` 树;提供 `provinceName(code)`/`cityName`/`districtName` 反查工具
|
||||
- `lib/models/store_theme.dart` — `StoreTheme` 枚举 + 内置主题清单(`StoreThemePresets`)
|
||||
- `lib/widgets/region_picker.dart` — 省→市→区三级联动底部弹起选择器
|
||||
- `lib/widgets/store_theme_grid.dart` — 主题皮肤网格卡片 + 选中态
|
||||
- `lib/widgets/store_theme_preview.dart` — 模拟主页头部预览(依据 `themeId`)
|
||||
- `lib/widgets/branding_editor.dart` — 启动图/Logo 卡片:选图 + 手机外框预览
|
||||
- `lib/screens/device/device_scan_page.dart` — 扫码页(`mobile_scanner` 封装,含相册识别)
|
||||
- `lib/utils/store_local_cache.dart` — 门店扩展字段本地持久化(SharedPreferences 封装,存 `themeId`/`branding` 等)
|
||||
|
||||
### 修改文件
|
||||
- `lib/models/store_info.dart` — 增加 `themeId`/`splashUrl`/`logoUrl`/`provinceCode`/`cityCode`/`districtCode`/`addressDetail` + `fromJson`/`toJson` 兼容
|
||||
- `lib/screens/store/store_list_page.dart` — `_StoreFormSheet` 接入 `RegionPicker` + 主题 + `BrandingEditor`;详情 sheet 展示主题/品牌
|
||||
- `lib/screens/home/device_list_page.dart` — AppBar 加扫码入口 → 跳 `device_scan_page`
|
||||
- `lib/screens/home/device_detail_page.dart` — 加「扫二维码绑定」按钮 → 跳 `device_scan_page`(`mode=bind`)
|
||||
- `lib/services/org_service.dart` — 加 `saveStore`/`updateStore`/`setStoreTheme`/`setStoreBranding` TODO
|
||||
- `lib/services/device_service.dart` — 加 `bindDeviceByScan` TODO
|
||||
- `lib/app/router.dart` — 注册 `/device/scan` 路由(含 `mode` 参数)
|
||||
- `pubspec.yaml` — 加依赖 + `assets` 注册(见 1.3)
|
||||
|
||||
---
|
||||
|
||||
## 3. 数据模型与接口(类结构)
|
||||
|
||||
### 3.1 类图(Mermaid classDiagram)
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class StoreInfo {
|
||||
+String orgId
|
||||
+String name
|
||||
+String address
|
||||
+String contactPhone
|
||||
+String activationCode
|
||||
+int status
|
||||
+DateTime? expireTime
|
||||
+DateTime? createTime
|
||||
<<新增>>
|
||||
+String? themeId
|
||||
+String? splashUrl
|
||||
+String? logoUrl
|
||||
+String? provinceCode
|
||||
+String? cityCode
|
||||
+String? districtCode
|
||||
+String? addressDetail
|
||||
+StoreInfo.fromJson(Map)
|
||||
+Map toJson()
|
||||
+String get regionText()
|
||||
}
|
||||
|
||||
class RegionNode {
|
||||
+String code
|
||||
+String name
|
||||
+List~RegionNode~ sub
|
||||
+RegionNode.fromJson(Map)
|
||||
}
|
||||
|
||||
class StoreTheme {
|
||||
<<enum>>
|
||||
classic
|
||||
ocean
|
||||
forest
|
||||
sunset
|
||||
mono
|
||||
+String get id
|
||||
+String get label
|
||||
+Color get primary
|
||||
}
|
||||
|
||||
class StoreThemePresets {
|
||||
<<const>>
|
||||
+List~StoreTheme~ all
|
||||
+StoreTheme? byId(String)
|
||||
}
|
||||
|
||||
class RegionData {
|
||||
-List~RegionNode~ _tree
|
||||
+Future~void~ load()
|
||||
+List~RegionNode~ get provinces
|
||||
+List~RegionNode~ citiesOf(String)
|
||||
+List~RegionNode~ districtsOf(String,String)
|
||||
+String nameOf(String)
|
||||
+String tryMapAddress(String)~List~String?~
|
||||
}
|
||||
|
||||
class StoreLocalCache {
|
||||
+Future~void~ saveTheme(String orgId, String themeId)
|
||||
+Future~String?~ getTheme(String orgId)
|
||||
+Future~void~ saveBranding(String orgId, String splashUrl, String logoUrl)
|
||||
+Future~Map?~ getBranding(String orgId)
|
||||
+Future~void~ saveRegion(String orgId, String p, String c, String d, String detail)
|
||||
}
|
||||
|
||||
class OrgService {
|
||||
+Future~void~ saveStore(Map) // TODO 对接 POST /sys/store/save
|
||||
+Future~void~ updateStore(Map) // TODO 对接 POST /sys/store/update
|
||||
+Future~void~ setStoreTheme(String,String) // TODO POST /sys/store/theme
|
||||
+Future~void~ setStoreBranding(String,String,String) // TODO POST /sys/store/branding
|
||||
}
|
||||
|
||||
class DeviceService {
|
||||
+Future~void~ bindDeviceByScan(String scanPayload, String storeId, String? groupId) // TODO POST /device/bind
|
||||
}
|
||||
|
||||
class DeviceScanPage {
|
||||
+String mode // 'query' | 'bind'
|
||||
+String? storeId
|
||||
+String? groupId
|
||||
}
|
||||
|
||||
StoreInfo "1" *-- "1" StoreTheme : themeId 关联
|
||||
RegionData "1" *-- "0..*" RegionNode : 树
|
||||
OrgService ..> StoreInfo : 读写
|
||||
DeviceService ..> DeviceScanPage : 扫码结果
|
||||
```
|
||||
|
||||
### 3.2 关键模型说明
|
||||
|
||||
**StoreInfo 新增字段**(全部可空,向后兼容既有后端响应):
|
||||
```dart
|
||||
String? themeId; // 主题枚举 id,如 'ocean'
|
||||
String? splashUrl; // 启动图本地路径 / 远程 URL(占位)
|
||||
String? logoUrl; // Logo 本地路径 / 远程 URL(占位)
|
||||
String? provinceCode; // 省 code(6 位)
|
||||
String? cityCode; // 市 code
|
||||
String? districtCode; // 区 code
|
||||
String? addressDetail; // 街道/门牌等详情文本
|
||||
// 拼接展示:regionText = 省名+市名+区名+addressDetail
|
||||
```
|
||||
|
||||
**预留 Service 方法签名(含 TODO)**:
|
||||
```dart
|
||||
// OrgService — 当前后端无门店 save/update/theme/branding 接口
|
||||
Future<void> saveStore(Map<String, dynamic> data) async {
|
||||
// TODO(后端): POST /sys/store/save,body 含 themeId/splashUrl/logoUrl/省市区
|
||||
// 前端占位:仅写 StoreLocalCache,不发起真实请求
|
||||
}
|
||||
Future<void> setStoreTheme(String orgId, String themeId) async {
|
||||
// TODO(后端): POST /sys/store/setTheme {orgId, themeId}
|
||||
}
|
||||
Future<void> setStoreBranding(String orgId, String splashUrl, String logoUrl) async {
|
||||
// TODO(后端): POST /sys/store/setBranding {orgId, splashUrl, logoUrl}
|
||||
// 注意:真实上传需先调 uploadAsset 拿 URL
|
||||
}
|
||||
|
||||
// DeviceService — 当前后端无扫码绑定接口
|
||||
Future<void> bindDeviceByScan(String scanPayload, String storeId, String? groupId) async {
|
||||
// TODO(后端): POST /device/bind {scanPayload, storeId, groupId}
|
||||
// 前端占位:本地匹配设备后用 updateDevice 写 groupId/storeName
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 程序调用流程(时序,文字描述)
|
||||
|
||||
### 4.1 门店编辑 — 省市区 + 主题 + 品牌(主流程)
|
||||
1. 用户点「新增/编辑门店」→ `_StoreFormSheet` 打开。
|
||||
2. 点「地址」→ `RegionPicker` 从 `RegionData`(已加载 `province.json`)弹起三级选择 → 存 `provinceCode/cityCode/districtCode` + 回填文本到 `addressDetail`。
|
||||
3. 点「主题皮肤」→ `StoreThemeGrid` 展示 `StoreThemePresets.all` → 选 `themeId` → `StoreThemePreview` 实时预览模拟头部。
|
||||
4. 点「启动图/Logo」→ `BrandingEditor` 调 `image_picker` 选图 → 手机外框预览 → 存本地路径到 `splashUrl/logoUrl`。
|
||||
5. 点「保存」→ `_StoreFormSheet._save()` 组装 data → 调 `OrgService.saveStore`(占位写 `StoreLocalCache`,并留 `setStoreTheme`/`setStoreBranding` TODO)→ 关闭 sheet,`_loadData()` 刷新。
|
||||
|
||||
### 4.2 设备扫码 — 查询模式
|
||||
1. 设备列表 AppBar 扫码图标 → `context.go('/device/scan?mode=query')`。
|
||||
2. `DeviceScanPage` 用 `mobile_scanner` 实时扫码(或相册识别)→ 得到 `scanPayload`(设备标识,如 mac/sn)。
|
||||
3. 本地用 `DeviceService.fetchDevices(mac:...)` 或列表缓存查找 → 命中则 `context.go('/device/:id')` 跳详情;未命中提示「未找到设备」。
|
||||
|
||||
### 4.3 设备扫码 — 绑定模式
|
||||
1. 设备详情「扫二维码绑定」→ `context.go('/device/scan?mode=bind&deviceId=xxx')`。
|
||||
2. 扫码得到目标门店/分组码 → 弹窗确认「绑定到当前门店/分组」→ `DeviceService.bindDeviceByScan`(占位:本地 `updateDevice` 写 `groupId`/`storeName`,留真实 POST TODO)。
|
||||
3. 返回详情,展示更新后的归属。
|
||||
|
||||
### 4.4 既有门店地址回显
|
||||
- 打开编辑既有门店:`_StoreFormSheet.initState` 若 `store.provinceCode` 非空 → `RegionPicker` 直接定位;若为空但有旧文本 `address` → `RegionData.tryMapAddress()` 尽力用关键词匹配省/市/区,匹配不到则仅填 `addressDetail` 文本。
|
||||
|
||||
---
|
||||
|
||||
## 5. 有序任务列表(按实现顺序,含依赖)
|
||||
|
||||
> 规则:不超过 5 个任务,每任务 ≥3 文件,首个为基础设施。
|
||||
|
||||
| 任务 | 名称 | 源文件 | 依赖 | 优先级 |
|
||||
|------|------|--------|------|--------|
|
||||
| **T01** | 项目基础设施(依赖 + assets + 数据/主题基座) | `pubspec.yaml`、`lib/data/region_data.dart`、`lib/models/store_theme.dart`、`lib/utils/store_local_cache.dart` | — | P0 |
|
||||
| **T02** | 扩展 StoreInfo 模型 + 预留 Service 接口 | `lib/models/store_info.dart`、`lib/services/org_service.dart`、`lib/services/device_service.dart` | T01 | P0 |
|
||||
| **T03** | 门店表单增强组件(省市区 + 主题 + 品牌) | `lib/widgets/region_picker.dart`、`lib/widgets/store_theme_grid.dart`、`lib/widgets/store_theme_preview.dart`、`lib/widgets/branding_editor.dart` | T01、T02 | P1 |
|
||||
| **T04** | 门店表单接入 + 设备扫码模块 + 路由 | `lib/screens/store/store_list_page.dart`、`lib/screens/home/device_list_page.dart`、`lib/screens/home/device_detail_page.dart`、`lib/screens/device/device_scan_page.dart`、`lib/app/router.dart` | T03、T02 | P1 |
|
||||
| **T05** | 构建验证与联调(编译/权限/占位跑通) | 复用上述全部文件 + `android/Info.plist/ohos` 权限配置 | T04 | P2 |
|
||||
|
||||
### 任务依赖图
|
||||
```mermaid
|
||||
graph TD
|
||||
T01[T01 基础设施] --> T02[T02 模型+Service]
|
||||
T01 --> T03[T03 组件]
|
||||
T02 --> T03
|
||||
T03 --> T04[T04 接入+扫码+路由]
|
||||
T02 --> T04
|
||||
T04 --> T05[T05 构建验证]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 依赖包列表
|
||||
|
||||
```
|
||||
- mobile_scanner@^5.2.0 : 跨平台相机/相册二维码扫码(替代华为 ScanKit)
|
||||
- image_picker@^1.1.2 : 启动图/Logo 本地图片选择(相册/拍照)
|
||||
# 以下为现有包(已存在,仅列示上下文)
|
||||
- flutter / go_router@^14.2.0 / flutter_riverpod@^2.5.1 / dio@^5.4.3 / intl@^0.19.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 共享知识(跨文件约定)
|
||||
|
||||
- **province.json 加载**:统一由 `lib/data/region_data.dart` 的 `RegionData.load()` 懒加载并缓存为 `List<RegionNode>` 树;组件禁止直接 `rootBundle` 读取,避免重复解析(文件 284KB)。
|
||||
- **主题枚举位置**:`lib/models/store_theme.dart` 定义 `StoreTheme` 枚举与 `StoreThemePresets`;所有主题相关 UI 仅引用枚举 id(`themeId`),不硬编码颜色。
|
||||
- **预留接口 TODO 规范**:所有待对接后台的方法必须含 `// TODO(后端): POST /sys/...` 注释,写明预期路径与 body;前端占位实现只写 `StoreLocalCache`,并 `print`/debug 标注「占位未上报」。
|
||||
- **本地持久化**:门店扩展字段统一经 `StoreLocalCache`(SharedPreferences,key 形如 `store_theme_$orgId`),不在内存态以外散落存储。
|
||||
- **扫码 payload**:约定扫码内容优先解析为 `mac` 或 `deviceSn`;无法识别时原样透传,由 `DeviceService` 模糊匹配。
|
||||
- **图片路径**:`image_picker` 返回的 `XFile.path` 直接存 `splashUrl/logoUrl`(占位);真实上传走 `uploadAsset` 后再替换为 URL(TODO)。
|
||||
- **路由参数**:`/device/scan?mode=query|bind&deviceId=xxx&storeId=xxx`,由 `device_scan_page` 解析。
|
||||
|
||||
---
|
||||
|
||||
## 8. 待明确事项
|
||||
|
||||
1. **主题占位资源形式**:内置纯色块 + 名称,还是需要 `assets/theme/*.png` 缩略图?当前设计按「纯色块 + 名称」实现,若需图片请在 T01 前提供 4–5 张占位图。
|
||||
2. **扫码绑定语义**:「绑定到当前门店/分组」中「当前门店」取自设备详情页的 `storeName` 还是用户手动选?本设计默认取详情页所属门店,绑定弹窗可改分组。
|
||||
3. **省市级联回显映射**:既有纯文本地址(如「北京市朝阳区xx路」)能否映射到 code 取决于关键词命中,映射不到仅回填 `addressDetail`,是否接受?
|
||||
4. **鸿蒙权限**:`mobile_scanner` 在 ohos 平台的相机权限声明方式需工程师按鸿蒙规范确认(本设计不含具体 ohos 权限代码)。
|
||||
5. **本地缓存生命周期**:`StoreLocalCache` 数据在重新登录/切换组织时是否清空?默认不清,如需隔离按 `orgId` 已做 key 隔离,可后续细化。
|
||||
|
||||
---
|
||||
|
||||
> 交付完毕:本设计为纯架构/任务分解,**不含实现代码**。工程师按 T01→T05 顺序落地即可。
|
||||
1486
docs/手机端UI优化方案-v2.md
Normal file
1486
docs/手机端UI优化方案-v2.md
Normal file
File diff suppressed because it is too large
Load Diff
1475
docs/手机端UI优化方案.md
Normal file
1475
docs/手机端UI优化方案.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user