git-subtree-dir: app git-subtree-mainline:6ebd902c6bgit-subtree-split:a11a668869
16 KiB
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-agent(Go)API,见 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: 初始化
cd slogan-app
flutter create --org com.slogan --project-name slogan_app .
- Step 2: pubspec.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 + Theme(Material 3,seed 主色)+ 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 adapter:401 触发登出回调;code!=0 抛 ApiException 带 message;成功解析 data)
-
Step 2: 确认失败
flutter test -
Step 3: 实现 api_client.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(AsyncNotifier:login(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: 写失败测试(provider:mock 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 provider:get → 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: 写失败测试(provider:upload 成功追加列表;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: 写失败测试(provider:generate 成功返回 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: 写失败测试(provider:task 轮询 done → 加载 plan list;failed → error)
-
Step 2: task_status_page.dart:轮询 GET /outfit/task/status(Timer.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 化身区(AvatarViewer,Task 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: 定义抽象接口
/// 渲染层抽象:业务代码只依赖此接口,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_jsmGLTFLoader 加载 GLB → Scene 显示(DirectionalLight + AmbientLight + OrbitControls 式手动手势:onPanUpdate → rotateBy 旋转 Object3D,onScaleUpdate → 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: 写失败测试(provider:select-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: 门店/电商(Tab4,MVP 列表展示)
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: 写失败测试(provider:GET /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 hash,256MB 上限(超限按最后访问时间淘汰);方案流预取下一页 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 模拟器各一遍):
- 登录 → 2. 拍照引导上传 4 张(相册选图代替拍摄)→ 3. 身形参数 → 4. 构建化身 → 5. 衣橱上传 3+ 件 → 6. 生成穿搭(日期+地点)→ 7. 任务轮询 → 8. 方案流(3D 查看/发型切换/横滑)→ 9. 选主方案 → 10. 效果图 3 视角查看
-
Step 3: 修复联调问题(网络/字段/状态映射),
flutter analyze0 错误 -
Step 4: Commit
git commit -m "feat: mvp complete, verified against slogan-agent"
Self-Review 备注
- 后端字段命名 snake_case(JSON),Dart 侧解析用 map 取值避免命名映射负担
- 效果图生成在后端为异步任务,App 端轮询 plan/detail 的 images 状态(rendering→done)
- three_dart 若 iOS 渲染异常(着色器兼容),降级路径 placeholder 保证 MVP 可用;GLB 渲染验证放 Task 10 明确检查