feat: app 骨架 + 网络层 + 认证(登录/注册)

- flutter create 初始化 + 依赖(riverpod 3/dio/go_router/three_dart 0.0.16)
- ApiClient:JWT 拦截器 + 统一响应解包 + 401 登出回调 + multipart 上传
- TokenStorage(shared_preferences)+ AuthNotifier 登录/登出
- 登录页(含注册对话框)+ 4 Tab 主框架
- 6 个单元测试通过(网络层 4 + 认证 2)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 12:39:01 +08:00
co-authored by Claude Opus 4.7
commit acbe60c13c
144 changed files with 6887 additions and 0 deletions
@@ -0,0 +1,372 @@
# slogan-app MVP 实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 实现 slogan-app Flutter MVP:登录 → 4 Tab 框架 → 拍照上传 → 衣橱管理 → 化身查看 → 穿搭生成(日期/地点 → 轮询 → 方案流 3D+2D 切换查看)。
**Architecture:** Flutter 单仓,Riverpod 状态管理,Dio 网络层(JWT 拦截器 + 统一响应),AvatarViewer 渲染抽象层(three_dart v1 实现,flutter_scene 后续替换),核心交互为方案 PageView 横滑 + 3D 化身拖拽旋转。
**Tech Stack:** Flutter (stable) / Riverpod / Dio / go_router / three_dart + three_dart_jsm / cached_network_image / camera + image_picker / shared_preferences
**后端对接:** slogan-agentGoAPI,见 slogan-agent 仓库 `docs/superpowers/specs/2026-07-31-slogan-agent-design.md` 第 11 节路由表。
---
### Task 1: 项目骨架
**Files:**
- Run: `flutter create` 初始化(org 自定,项目名 slogan_app
- Modify: `pubspec.yaml`(依赖)
- Create: `lib/main.dart`(入口 + 主题 + go_router 路由表)
- Create: `lib/core/config/app_config.dart`
- [ ] **Step 1: 初始化**
```bash
cd slogan-app
flutter create --org com.slogan --project-name slogan_app .
```
- [ ] **Step 2: pubspec.yaml 依赖**
```yaml
dependencies:
flutter_riverpod: ^2.6.0
dio: ^5.7.0
go_router: ^14.0.0
cached_network_image: ^3.4.0
image_picker: ^1.1.0
camera: ^0.11.0
shared_preferences: ^2.3.0
three_dart: ^0.2.0
three_dart_jsm: ^0.2.0
fluttertoast: ^8.2.0
```
(版本以 pub.dev 最新稳定为准,`flutter pub add` 逐个添加)
- [ ] **Step 3: main.dart**MaterialApp + ThemeMaterial 3seed 主色)+ go_router 路由(login / home(4 tab) / photo-guide / plan-flow / plan-detail);启动时读取 token → 无 token 重定向登录页
- [ ] **Step 4: 验证** `flutter analyze` 无错误 + `flutter test` 默认通过
- [ ] **Step 5: Commit** `git add -A && git commit -m "feat: flutter skeleton with router and theme"`
---
### Task 2: 网络层(ApiClient + JWT 拦截器)
**Files:**
- Create: `lib/core/network/api_client.dart`
- Create: `lib/core/network/api_exception.dart`
- Create: `lib/core/storage/token_storage.dart`
- Test: `test/core/network/api_client_test.dart`
- [ ] **Step 1: 写失败测试**mock Dio adapter401 触发登出回调;code!=0 抛 ApiException 带 message;成功解析 data
- [ ] **Step 2: 确认失败** `flutter test`
- [ ] **Step 3: 实现 api_client.dart**
```dart
class ApiClient {
ApiClient({Dio? dio, required TokenStorage tokenStorage})
: _tokenStorage = tokenStorage {
_dio = dio ?? Dio(BaseOptions(
baseUrl: AppConfig.baseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 30),
));
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
final token = _tokenStorage.token;
if (token != null) options.headers['Authorization'] = 'Bearer $token';
handler.next(options);
},
onError: (e, handler) {
if (e.response?.statusCode == 401) onUnauthorized?.call();
handler.next(e);
},
));
}
Future<T> post<T>(String path, Map<String, dynamic> body,
{T Function(dynamic data)? parse}) async {
final res = await _dio.post(path, data: body);
return _unwrap<T>(res, parse);
}
Future<T> get<T>(String path, {Map<String, dynamic>? query, T Function(dynamic data)? parse}) async { ... }
Future<T> upload<T>(String path, Map<String, dynamic> fields, String fileField, String filePath, {String Function(dynamic)? parse}) async { ... }
T _unwrap<T>(Response res, ...) {
final code = res.data['code'] as int;
final message = res.data['message'] as String? ?? '';
if (code != 0) throw ApiException(code, message);
return parse?.call(res.data['data']) ?? res.data['data'] as T;
}
VoidCallback? onUnauthorized;
}
```
- [ ] **Step 4: TokenStorage**shared_preferences 封装:token 读写 + 清空)
- [ ] **Step 5: 测试通过** + `flutter analyze` + **Commit**
---
### Task 3: 认证(登录页 + 状态)
**Files:**
- Create: `lib/core/auth/auth_provider.dart`
- Create: `lib/features/auth/login_page.dart`
- Test: `test/core/auth/auth_provider_test.dart`
- [ ] **Step 1: 写失败测试**ProviderContainer:登录成功 → token 持久化 + 状态 authenticated;失败 → 状态 error 携带 message
- [ ] **Step 2: 实现 auth_provider.dart**AsyncNotifierlogin(account, password) → ApiClient.post('/user/login') → 存 token
- [ ] **Step 3: login_page.dart**:账号/密码输入 + 登录按钮 + 加载态 + 错误提示(fluttertoast);登录成功 go_router push 替换到 home
- [ ] **Step 4: 测试通过** + **Commit**
---
### Task 4: 4 Tab 主框架
**Files:**
- Create: `lib/features/home/home_page.dart`BottomNavigationBar + IndexedStack 4 Tab
- Create: `lib/features/profile/profile_page.dart`(占位)
- Create: `lib/features/wardrobe/wardrobe_page.dart`(占位)
- Create: `lib/features/outfit/outfit_page.dart`(占位)
- Create: `lib/features/commercial/commercial_page.dart`(占位)
- [ ] **Step 1: 实现 4 Tab 框架**:底部导航(我的形象/我的衣橱/穿搭方案/门店电商)+ 图标 + IndexedStack 保状态
- [ ] **Step 2: Widget 测试**:切 Tab 显示对应页面
- [ ] **Step 3: 测试通过** + **Commit**
---
### Task 5: 拍照引导 + 照片上传(Tab1)
**Files:**
- Create: `lib/features/profile/photo_guide_page.dart`(引导 + 拍摄)
- Create: `lib/features/profile/photo_guide_item.dart`(单张拍摄卡片)
- Create: `lib/features/profile/photo_upload_provider.dart`
- Create: `lib/features/profile/profile_page.dart`(集成:照片齐备度展示 + 上传入口)
- Test: `test/features/profile/photo_upload_provider_test.dart`
- [ ] **Step 1: 写失败测试**providermock ApiClient.upload 成功 → 状态更新;失败 → error)
- [ ] **Step 2: 实现 provider**:4 个类型照片上传(type 1-4),逐张上传成功后标记完成;本地压缩(`image_picker` 自带 maxWidth: 2048
- [ ] **Step 3: photo_guide_page.dart**:4 张卡片(大头照/全身正面/侧面/背面,各含拍摄示例说明文案 + 相机按钮 image_picker 拍摄)+ 上传进度 + 完成态跳转
- [ ] **Step 4: profile_page.dart**:显示 4 张照片状态(已传/未传)+ "进入拍摄引导" 按钮 + 身形参数入口(Task 6)
- [ ] **Step 5: 测试通过** + **Commit**
---
### Task 6: 身形参数 + 化身状态(Tab1)
**Files:**
- Create: `lib/features/profile/body_tune_page.dart`(滑杆微调)
- Create: `lib/features/profile/body_provider.dart`
- Create: `lib/features/profile/avatar_provider.dart`
- Test: `test/features/profile/avatar_provider_test.dart`
- [ ] **Step 1: 写失败测试**avatar providerget → build → 状态 done + glbUrl 非空)
- [ ] **Step 2: body_tune_page.dart**:身高(145-200cm)/体重/肤色(1-5) 滑杆,保存 → POST /body-measurement/save
- [ ] **Step 3: avatar_provider.dart**:页面进入时 GET /avatar/get → 无记录则提示先传照片 → POST /avatar/build → 轮询 build_status(每 2s × 最多 30 次)→ done 后展示 glb_url
- [ ] **Step 4: profile_page.dart** 集成:身形参数卡片 + 化身构建按钮 + 构建状态展示
- [ ] **Step 5: 测试通过** + **Commit**
---
### Task 7: 衣橱管理(Tab2
**Files:**
- Create: `lib/features/wardrobe/wardrobe_provider.dart`
- Create: `lib/features/wardrobe/wardrobe_page.dart`(网格)
- Create: `lib/features/wardrobe/wardrobe_upload_page.dart`(上传表单:照片 + 分类 + 季节 + 风格标签)
- Test: `test/features/wardrobe/wardrobe_provider_test.dart`
- [ ] **Step 1: 写失败测试**providerupload 成功追加列表;delete 移除;list 加载)
- [ ] **Step 2: 实现 provider** + 上传表单页(DropdownButton 分类[上衣/下装/鞋/配饰] + 季节 + 标签输入)
- [ ] **Step 3: 网格页**GridView 服装照片 + 长按删除确认 + 空态引导("衣橱空空如也,去上传第一件衣服吧")
- [ ] **Step 4: 测试通过** + **Commit**
---
### Task 8: 生成入口(Tab3 上半)
**Files:**
- Create: `lib/features/outfit/generate_page.dart`
- Create: `lib/features/outfit/outfit_generate_provider.dart`
- Test: `test/features/outfit/outfit_generate_provider_test.dart`
- [ ] **Step 1: 写失败测试**providergenerate 成功返回 taskId;开始轮询状态)
- [ ] **Step 2: 生成入口页**:日期范围(showDateRangePicker+ 地点(TextField + 定位按钮[geolocator 可选,v1 手动输入]+ "生成穿搭" 按钮(校验:日期非空/地点非空/衣橱非空提示)
- [ ] **Step 3: 生成确认后** → 跳转任务状态页(Task 9
- [ ] **Step 4: 测试通过** + **Commit**
---
### Task 9: 任务轮询 + 方案流(Tab3 核心)
**Files:**
- Create: `lib/features/outfit/task_status_page.dart`
- Create: `lib/features/outfit/plan_flow_page.dart`PageView 横滑)
- Create: `lib/features/outfit/plan_provider.dart`
- Test: `test/features/outfit/plan_provider_test.dart`
- [ ] **Step 1: 写失败测试**providertask 轮询 done → 加载 plan listfailed → error
- [ ] **Step 2: task_status_page.dart**:轮询 GET /outfit/task/statusTimer.periodic 3s),状态文案映射(pending=准备中/planning=方案规划中/scoring=方案评分中/rendering=效果图生成中/done=完成/failed=失败+error 展示);done → 跳转方案流;失败显示重试
- [ ] **Step 3: plan_provider.dart**GET /outfit/plan/list + detail(含 items/images/hairstyle
- [ ] **Step 4: plan_flow_page.dart**PageView.builder 每页一张方案卡片:
- 3D 化身区(AvatarViewerTask 10
- 方案摘要(标题/评分 Chip/来源标签:衣橱组合=蓝 / AI 推荐=橙)
- 发型切换(横排发型 chip)+ 发色取色(HSV 面板)
- 底部条目列表(slot 图标 + 名称 + 描述;推荐条目带"查看商品"入口,v1 占位)
- "选为主方案" 按钮 → POST select-main → 效果图页(Task 11
- [ ] **Step 5: 测试通过** + **Commit**
---
### Task 10: AvatarViewer 抽象 + three_dart 实现
**Files:**
- Create: `lib/features/outfit/viewer/avatar_viewer.dart`(接口 + controller
- Create: `lib/features/outfit/viewer/avatar_viewer_three_dart.dart`three_dart 实现)
- Create: `lib/features/outfit/viewer/avatar_viewer_placeholder.dart`(降级占位:头像图 + 手势提示)
- Create: `lib/features/outfit/viewer/viewer_factory.dart`
- Test: `test/features/outfit/viewer/avatar_viewer_placeholder_test.dart`
- [ ] **Step 1: 定义抽象接口**
```dart
/// 渲染层抽象:业务代码只依赖此接口,flutter_scene 进 stable 后提供第二实现
abstract class AvatarViewerController {
Future<void> loadAvatar(String glbUrl); // 头像+体型主体
Future<void> loadHairstyle(String glbUrl); // 发型层
void setHairColor(Color color); // 发色 PBR baseColor
void rotateBy(double dx, double dy);
void zoomBy(double scale);
void resetView();
}
class AvatarViewer extends StatefulWidget {
final AvatarViewerController Function() controllerFactory;
...
}
```
- [ ] **Step 2: 实现 three_dart 版**`three_dart` + `three_dart_jsm` GLTFLoader 加载 GLB → Scene 显示(DirectionalLight + AmbientLight + OrbitControls 式手动手势:onPanUpdate → rotateBy 旋转 Object3DonScaleUpdate → zoomBy 缩放相机/模型)—— 参考 `three_js_advanced_loaders` 示例;GLB 本地缓存(Task 13
- [ ] **Step 3: 降级实现**:加载失败(网络/格式)→ placeholder(用户大头照 Image + "3D 模型加载失败,显示照片效果" 文案)
- [ ] **Step 4: viewer_factory.dart**`AvatarViewer createViewer()` → three_dart 实现(有 GLB url 时)/ placeholder(无 url 时)
- [ ] **Step 5: Widget 测试**placeholder 渲染)+ `flutter analyze` + **Commit**
---
### Task 11: 效果图查看 + 方案详情(Tab3 下半)
**Files:**
- Create: `lib/features/outfit/effect_image_page.dart`
- Create: `lib/features/outfit/plan_detail_provider.dart`
- Test: `test/features/outfit/plan_detail_provider_test.dart`
- [ ] **Step 1: 写失败测试**providerselect-main → 轮询 detail.images 直到 3 张完成)
- [ ] **Step 2: select-main 后跳转效果图页**:3 视角(正面/侧面/背面)Tab/滑块切换,cached_network_image 加载,生成中展示进度(轮询 plan/detail images status
- [ ] **Step 3: 方案详情页**(从列表进入):完整 detail 渲染(items 图片/名称/描述 + 效果图 + 收藏按钮 POST review
- [ ] **Step 4: 测试通过** + **Commit**
---
### Task 12: 门店/电商(Tab4MVP 列表展示)
**Files:**
- Create: `lib/features/commercial/store_page.dart`(附近门店列表)
- Create: `lib/features/commercial/store_provider.dart`
- Create: `lib/features/commercial/subscription_page.dart`(订阅占位)
- Test: `test/features/commercial/store_provider_test.dart`
- [ ] **Step 1: 写失败测试**providerGET /partner-store/list 解析列表)
- [ ] **Step 2: 门店列表页**:类型筛选 chip(形象设计/服装门店)+ ListView 卡片(名称/类型/地址/距离占位)
- [ ] **Step 3: 订阅页**:标准版/Pro 权益卡片 + "开通"按钮(v1 占位 toast"支付功能开发中"
- [ ] **Step 4: 测试通过** + **Commit**
---
### Task 13: 缓存与性能
**Files:**
- Modify: `lib/core/storage/`(新增 glb_cache.dart
- Create: `lib/core/storage/glb_cache.dart`GLB 本地 LRU
- Test: `test/core/storage/glb_cache_test.dart`
- [ ] **Step 1: 写失败测试**(缓存:put → hit;LRU 超限淘汰;过期清理)
- [ ] **Step 2: 实现**:文件缓存目录 `getApplicationSupportDirectory()/glb_cache/`key=url hash256MB 上限(超限按最后访问时间淘汰);方案流预取下一页 GLB
- [ ] **Step 3: 测试通过** + **Commit**
---
### Task 14: 错误处理与加载状态组件
**Files:**
- Create: `lib/shared/widgets/loading_view.dart`(骨架屏)
- Create: `lib/shared/widgets/error_view.dart`(错误 + 重试)
- Create: `lib/shared/widgets/empty_view.dart`(空态 + 引导动作)
- [ ] **Step 1: 三个组件**loading 骨架、error 文案+重试回调、empty 图标+文案+按钮)
- [ ] **Step 2: 接入**profile/wardrobe/outfit/commercial 各页加载态/空态/错误态替换
- [ ] **Step 3: Widget 测试** + **Commit**
---
### Task 15: 集成冒烟(联调 slogan-agent
- [ ] **Step 1: 本地起 slogan-agent**`go run main.go`,3007 端口,mock 效果图供应商)
- [ ] **Step 2: App 联调**iOS 模拟器 + Android 模拟器各一遍):
1. 登录 → 2. 拍照引导上传 4 张(相册选图代替拍摄)→ 3. 身形参数 → 4. 构建化身 → 5. 衣橱上传 3+ 件 → 6. 生成穿搭(日期+地点)→ 7. 任务轮询 → 8. 方案流(3D 查看/发型切换/横滑)→ 9. 选主方案 → 10. 效果图 3 视角查看
- [ ] **Step 3: 修复联调问题**(网络/字段/状态映射),`flutter analyze` 0 错误
- [ ] **Step 4: Commit** `git commit -m "feat: mvp complete, verified against slogan-agent"`
---
## Self-Review 备注
- 后端字段命名 snake_caseJSON),Dart 侧解析用 map 取值避免命名映射负担
- 效果图生成在后端为异步任务,App 端轮询 plan/detail 的 images 状态(rendering→done
- three_dart 若 iOS 渲染异常(着色器兼容),降级路径 placeholder 保证 MVP 可用;GLB 渲染验证放 Task 10 明确检查
@@ -0,0 +1,174 @@
# slogan-app 客户端设计方案
> 日期:2026-07-31
> 关联:slogan-agent 服务端方案(Go + GoFrame)见 slogan-agent 仓库对应文档
## 1. 项目概述
slogan 是一个"人形象设计"应用:用户上传大头照和全身多角度照片、维护个人服装资产(衣橱),指定日期范围和地点后一键生成最适合的穿搭方案(含发型、发色、服装穿搭),方案以 3D 化身 + 2D 效果图双形态呈现,手指滑动切换方案与查看角度。
本仓库为客户端(slogan-app),Flutter 实现,一次编写双端(iOS/Android)运行,追求类原生用户操作体验。
## 2. 技术选型
| 决策点 | 选择 | 理由 |
|--------|------|------|
| 语言/框架 | Flutter (Dart) | 一次编写双端;自绘引擎保证体验一致;动画/手势流畅 |
| 状态管理 | Riverpod | 类型安全、可测试,适合表单/异步任务/缓存类状态 |
| 网络层 | Dio + 拦截器 | JWT 自动注入、统一响应解析 `{code,message,data}`、超时重试 |
| 3D 渲染 | three_dart v1 + 渲染抽象层 | 稳定发布版(v0.3.0GLTF/GLB loader,纯 Dart 双端);flutter_scene 进 stable 后经抽象层无缝替换 |
| 图片缓存 | cached_network_image | 效果图/服装照片懒加载缓存 |
| 拍照/相册 | camera + image_picker | 大头照/全身多角度拍摄引导 |
| 本地存储 | shared_preferences | token/偏好;身形微调参数本地实时生效 |
| 手势 | 原生 GestureDetector 组合 | 方案横滑切换(PageView)+ 化身旋转拖拽/捏合缩放 + 惯性滚动 |
**3D 渲染层风险控制**`AvatarViewer` 接口抽象(loadGLB / setHairstyle / setHairColor / rotate / zoom / switchOutfit),v1 实现为 three_dartflutter_scene(官方,基于 Flutter GPU)进入 stable 后提供第二实现,业务代码零改动。
## 3. 项目结构
```
slogan-app/
├── lib/
│ ├── main.dart # 入口 + 路由 + 主题
│ ├── core/
│ │ ├── network/ # Dio 封装:JWT 拦截器/统一响应/错误码映射
│ │ ├── auth/ # 登录页 + token 管理(账号密码,复用服务端 /user/login
│ │ ├── config/ # API 地址/环境
│ │ ├── storage/ # shared_preferences 封装(token/偏好)
│ │ └── router/ # go_router(未登录 → 登录页)
│ ├── features/
│ │ ├── profile/ # Tab1 我的形象
│ │ │ ├── photo_guide/ # 拍照引导页(大头照/全身多角度拍摄指引)
│ │ │ ├── avatar_viewer/ # 3D 化身查看(AvatarViewer 抽象层实现)
│ │ │ └── body_tune/ # 滑杆微调(身高/胖瘦/肤色,本地实时)
│ │ ├── wardrobe/ # Tab2 我的衣橱
│ │ │ ├── upload/ # 服装照片上传 + 分类标签
│ │ │ └── item_grid/ # 服装资产网格/详情
│ │ ├── outfit/ # Tab3 穿搭方案
│ │ │ ├── generate/ # 生成入口(日期范围选择器/地点选择)
│ │ │ ├── task_status/ # 生成任务进度(轮询)
│ │ │ ├── plan_flow/ # 方案流(PageView 横滑切换方案)
│ │ │ ├── viewer_3d/ # 3D 方案查看(发型切换/发色取色/旋转/捏合)
│ │ │ ├── effect_images/ # 2D 效果图(正面/侧面/背面切换)
│ │ │ └── review/ # 收藏/反馈
│ │ └── commercial/ # Tab4 门店/电商
│ │ ├── stores/ # LBS 附近门店(形象设计/服装)
│ │ ├── leads/ # 导流订单/到店核销
│ │ ├── products/ # CPS 商品跳转
│ │ └── subscription/ # 会员订阅
│ └── shared/ # 组件/主题/工具(日期选择器/评分展示等)
├── assets/
│ ├── avatars/ # 模板/发型 GLB 缓存(与后端 assets 对应,v1 从服务端拉取)
│ └── images/ # 图标/占位图
└── test/ # 单元/widget 测试
```
## 4. 页面与交互设计(4 Tab 主框架)
```
┌─────────────────────────────────────────┐
│ Tab 架构(底部导航,类原生体验) │
│ ┌────────┬────────┬────────┬─────────┐ │
│ │ 我的形象│ 我的衣橱│ 穿搭方案 │ 门店/电商│ │
│ └────────┴────────┴────────┴─────────┘ │
└─────────────────────────────────────────┘
```
### Tab1 我的形象
- 首次进入:拍照引导流程(大头照 + 全身正面/侧面/背面 4 张,含姿势示例图)
- 化身构建进度(后端任务轮询)
- 3D 化身查看:单指拖拽旋转、双指捏合缩放
- 滑杆微调:身高/胖瘦/肤色,调参即时反映(本地计算,GLB 缩放参数 + 肤色材质)
### Tab2 我的衣橱
- 服装照片上传(多选)+ 分类(上衣/下装/鞋/配饰)+ 季节/风格标签自动识别(后端 AI 辅助,本地可手改)
- 网格展示 + 详情编辑/删除
### Tab3 穿搭方案(核心)
- 生成入口:日期范围选择器 + 地点选择(定位/搜索)
- 生成任务进度页(规则规划 → 方案评分 → 完成)
- 方案流:PageView 左右滑动切换 3 套方案;每套方案卡片 = 3D 化身 + 方案摘要(评分/来源标签:衣橱组合/AI 推荐)
- 3D 查看:发型点击切换 + 发色取色盘(HSV 调色实时渲染)+ 拖拽旋转 + 捏合缩放
- 主方案选定 → 触发 2D 效果图生成(3 视角:正面/侧面/背面,切换查看)
- 方案条目明细:每件服装(衣橱照片 或 电商商品图)+ 单品操作(跳转商品/加入衣橱)
- 收藏/反馈 → 回流 Agent 优化下次生成
### Tab4 门店/电商
- LBS 附近合作门店(类型筛选:形象设计/服装),导航/预约/到店核销
- CPS 商品推荐列表(跳转电商)
- 会员订阅入口与权益展示
## 5. 3D 查看器设计(核心模块)
### AvatarViewer 抽象接口
```dart
abstract class AvatarViewer extends StatelessWidget {
// 由具体实现提供(three_dart v1 / flutter_scene v2
}
abstract class AvatarViewerController {
Future<void> loadAvatar(AvatarSpec spec); // 头像 + 体型 + 皮肤贴图
Future<void> loadHairstyle(String glbUrl); // 加载发型
void setHairColor(Color color); // 发色(PBR baseColor 调色)
void setOutfit(List<OutfitLayer> layers); // 换装(简模 GLB 层)
void rotateBy(double dx, double dy); // 旋转
void zoomBy(double scale); // 缩放
void resetView();
}
```
- **v1 实现(three_dart**:加载服务端 `avatar_model.glb`(头部/身体/发型分离分层),发型切换 = 换层 + 发色材质 HSV 调整
- **性能**GLB 经服务端 glTF-Transform 压缩;发型资产首次加载后本地缓存;页面级预取下一套方案
- **降级**:GLB 加载失败 → 显示用户大头照 + 方案文本卡片(核心功能不受 3D 影响)
### 手势实现
- 方案切换:`PageView`(水平滑动 + 惯性 + 阻尼边缘效果)
- 化身旋转:`GestureDetector.onPanUpdate` → controller.rotateBy,松手无惯性(或轻量衰减)
- 缩放:`ScaleGestureRecognizer` 双指捏合,1.0-4.0 范围限制
- 发色:`HSVColorPicker` 自定义取色盘,onChanged 实时 setHairColor
## 6. 网络层与状态管理
- `ApiClient`Dio):baseUrl 可配置、token 注入、`code==0` 判定、401 自动登出、超时 30s
- 任务轮询:`outfit/task/status` 每 3s 轮询(Riverpod `StreamProvider`),任务完成自动停止
- 上传:Multipart,进度条反馈
- 缓存:效果图/服装照片 cached_network_image;化身 GLB 本地文件缓存(LRU,256MB 上限)
## 7. 拍摄引导设计
- 大头照:正面、面部无遮挡、光线均匀说明图;相机取景框对齐提示
- 全身照:距镜 2-3 米、全身入框、正面/侧面/背面 三角度示例图(类原生"手势引导"UI
- 照片本地压缩(宽边 ≤ 2048)后上传
## 8. 错误处理与加载状态
- 统一 `ErrorView` / `LoadingView` 组件(骨架屏)
- 生成任务失败:明确错误文案("衣橱为空,请先添加服装"等)+ 重试按钮
- 网络离线:离线提示 + 本地缓存优先展示(方案历史本地快照)
- 轮询超时(>10 分钟):提示"生成时间较长"并提供结果通知路径(v2 推送)
## 9. 测试策略
- 单元测试:手势计算/发色 HSV 调色/方案缓存 key 逻辑
- Widget 测试:方案流滑动切换、3D 查看器骨架降级、拍照引导流程
- 集成(v1 手工 + 冒烟脚本):登录 → 上传 → 生成 → 查看全链路
- 渲染层测试:AvatarViewer 接口 mock,业务测试不依赖具体 3D 实现
## 10. 与后端 API 对接清单
见 slogan-agent 方案第 11 节 API 路由表。App 端关键时序:
```
登录 → 上传照片(4张) → 填写身形 → [build 化身(异步)] → 上传衣橱服装
→ 生成穿搭 {日期范围, 地点} → 轮询任务 → 方案流(3D 即时查看)
→ 选主方案 → 效果图生成(异步) → 3 视角查看 → 收藏/跳转商品/门店预约
```
## 11. 开发规范约束(App 端)
- 状态管理统一 Riverpod,禁止 setState 在页面间传递业务状态
- 所有网络请求必须走 `ApiClient`,禁止散落 Dio 实例
- 3D 渲染只允许通过 `AvatarViewer` 抽象层,禁止业务代码直接依赖 three_dart 类型
- 命名:目录 `features/<domain>/`,组件 `shared/`;文件 snake_case,类 PascalCase
- 测试随功能同步编写(TDD:先写失败测试再实现)