# 云端管理端 — 增量架构设计(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 { <> classic ocean forest sunset mono +String get id +String get label +Color get primary } class StoreThemePresets { <> +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 saveStore(Map data) async { // TODO(后端): POST /sys/store/save,body 含 themeId/splashUrl/logoUrl/省市区 // 前端占位:仅写 StoreLocalCache,不发起真实请求 } Future setStoreTheme(String orgId, String themeId) async { // TODO(后端): POST /sys/store/setTheme {orgId, themeId} } Future setStoreBranding(String orgId, String splashUrl, String logoUrl) async { // TODO(后端): POST /sys/store/setBranding {orgId, splashUrl, logoUrl} // 注意:真实上传需先调 uploadAsset 拿 URL } // DeviceService — 当前后端无扫码绑定接口 Future 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` 树;组件禁止直接 `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 顺序落地即可。