Files
slogan/app/docs/superpowers/plans/2026-07-31-slogan-app-mvp.md
T
admin b53c57e02d Add 'app/' from commit 'a11a66886941bba128d89c1124115e1b1c87a128'
git-subtree-dir: app
git-subtree-mainline: 6ebd902c6b
git-subtree-split: a11a668869
2026-08-04 14:59:55 +08:00

16 KiB
Raw Blame History

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: 初始化

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.dartMaterialApp + 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

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: TokenStorageshared_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.dartAsyncNotifierlogin(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.dartBottomNavigationBar + 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.dartPageView 横滑)

  • 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.dartGET /outfit/plan/list + detail(含 items/images/hairstyle

  • Step 4: plan_flow_page.dartPageView.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.dartthree_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_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.dartAvatarViewer 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.dartGLB 本地 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-agentgo 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 明确检查