touyun-elderly-care/docs/superpowers/plans/2026-10-15-护理员APP-Flutter.md
liuyun e8a702eb93 docs: 护理员 APP Flutter 实施计划
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-15 08:42:47 +08:00

8.0 KiB
Raw Permalink Blame History

护理员 APP(Flutter)实施计划

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: 在 sources/ec_flutter(全新 Flutter 工程)落地护理员 APP:登录、M1 今日任务、M2 服务执行(签到/勾项目/拍照/签退)、异常上报、扑空、离线队列+自动补传。

Architecture: 全新客户端,feature-first。Riverpod(状态)+ dio(HTTP)+ sqflite(离线队列)+ go_router(路由)+ shared_preferences(token)。对接 C3 后端(无后端改动)。

Tech Stack: Flutter 3.38 + Dart 3.10、riverpod、dio、sqflite、go_router。

Spec: openspec/changes/nurse-app-flutter/;设计背景 docs/superpowers/specs/2026-10-15-护理员APP-Flutter-design.md。

Global Constraints

  • 后端 base http://localhost:3000/api/v1(用 --dart-define=API_BASE_URL= 可覆盖)。
  • 统一响应 {code,message,data}:code=0 成功、非 0 抛业务错误(message 含规则号 3001/1001/2001/3003)。
  • token 走 Authorization: Bearer;token 落 shared_preferences。
  • 签到/签退时间不传客户端值(服务器取时);定位失败不阻塞(标「未获取」)。
  • exec_result 枚举 done/undo_not_done/undo_refused(禁用 not_done)。
  • 必拍未拍:前端签退按钮前置校验 + 后端 3001 双保险;拒拍出路(选原因放行)。
  • 措辞纪律:留痕口径「签到/签退/服务执行」,禁「打卡/考勤」;异常不禁「报警」。
  • 界面大字 ≥20pt、常态 ≤3 击、首屏即任务、误操作可撤销。

Review Focus

  1. 服务器取时:checkin/checkout 请求体不得含 checkin_time/checkout_time/duration_min。
  2. 必拍未拍:必拍项未拍照时签退按钮禁用;拒拍出路选原因后放行。
  3. 离线幂等:每条离线记录生成 client_md5(内容 hash),补传按 md5 幂等,不重复。
  4. exec_result 措辞:未执行用 undo_not_done(非 not_done);老人拒做用 undo_refused。
  5. 扑空证据:abnormal_type=not_met 时到访时间/定位/原因必填,缺一不可提交。

Task 1: 工程骨架(依赖 + core)

Files: sources/ec_flutter/pubspec.yaml、lib/core/**、lib/main.dart。

  • Step 1: pubspec.yaml 加依赖
dependencies:
  flutter: { sdk: flutter }
  cupertino_icons: ^1.0.8
  flutter_riverpod: ^2.5.1
  dio: ^5.4.0
  sqflite: ^2.3.0
  shared_preferences: ^2.2.2
  go_router: ^14.0.0
  path_provider: ^2.1.2
  path: ^1.9.0
  image_picker: ^1.0.7
  geolocator: ^12.0.0
  crypto: ^3.0.3
  • Step 2: lib/core/api/api_client.dart:dio 实例(baseUrl 从 String.fromEnvironment('API_BASE_URL', defaultValue:'http://localhost:3000/api/v1'));拦截器加 Authorization: Bearer <token>;响应拦截器解包 {code,message,data}(code≠0 抛 ApiException(code,message))。

  • Step 3: lib/core/api/:auth_api.dart(login)、order_api.dart(today)、record_api.dart(checkin/itemSave/photoUpload/checkout/offlineSync)、abnormal_api.dart(report)。

  • Step 4: lib/core/auth/auth_provider.dart:token 存取(shared_preferences)+ 登录状态 provider。

  • Step 5: lib/core/router.dart:go_router 路由(login/today/service/abnormal)。

  • Step 6: lib/core/theme.dart:Material 3 主题,正文 ≥20pt。

  • Step 7: lib/main.dart:ProviderScope + MaterialApp.router。

  • Step 8: flutter analyze 干净 + commit

git -C sources/ec_flutter add -A
git -C sources/ec_flutter commit -m "feat(nurse): 工程骨架(dio/riverpod/sqflite/router/theme)"

Task 2: 登录 + M1 今日任务

Files: lib/features/auth/**、lib/features/today/**。

  • Step 1: 登录页:租户 code + 账号 + 密码三个输入框 → POST /auth/login(body {tenantCode, username, password},返回 token)→ 存 token → context.go('/today')。登录失败显示 ApiException.message。

  • Step 2: 今日任务页:GET /order/today → 卡片列表(老人名/时段/项目名/工单状态),空态「今日暂无任务」,点卡片 context.go('/service?orderId=...')。

  • Step 3: flutter analyze + widget test(登录/今日任务基本渲染)+ commit

git -C sources/ec_flutter commit -am "feat(nurse): 登录 + M1 今日任务"

Task 3: M2 服务执行(签到/勾项目/拍照/签退)

Files: lib/features/service/**。

  • Step 1: 签到:超大「签到」按钮 → geolocator 取定位(失败标「未获取」不阻塞)→ POST /record/checkin(body {orderId, locateStatus, checkinLat?, checkinLng?},不传 checkin_time)→ 记 recordId。

  • Step 2: 勾项目:项目列表(从 order.itemList 或 GET /order/:id),默认全勾 done,可改为 undo_not_done/undo_refused(老人拒做)→ POST /record/item/save(body {recordId, items:[{svcId, execResult}]})。

  • Step 3: 拍照:必拍项(photoTier==='must')显式标「需拍照」→ image_picker 拍照 + 压缩 ≤500KB → POST /record/photo/upload(multipart,file + recordId + svcId)→ 压疮项附结构化标签(部位 9 选/分期 6 选,可跳过);「无法拍摄」出路(选原因)→ 该项放行。

  • Step 4: 签退:超大「签退」按钮——前置校验「必拍项已拍或已拒拍」,未满足则禁用+提示「这项需要拍一张照片」→ POST /record/checkout(body {recordId},不传 checkout_time/duration_min)→ 成功回今日任务。

  • Step 5: 测试 + analyze + commit

git -C sources/ec_flutter commit -am "feat(nurse): M2 服务执行(签到/勾项目/拍照/签退)"

Task 4: 异常上报 + 扑空

Files: lib/features/abnormal/**。

  • Step 1: 异常上报:9 宫格分类(必选,dict abnormal_type)+ 描述/语音/照片(可选)→ POST /abnormal(body {abnormalType, abnormalDesc?, recordId?})。

  • Step 2: 扑空:服务执行页「未遇」入口 → 到访时间(自动)/定位(自动)/原因(必选下拉:老人不在/拒绝服务/临时改期)→ POST /abnormal(body {abnormalType:'not_met', orderId, elderId, visitTime, visitLat, visitLng, abnormalDesc})→ 工单转 not_met。

  • Step 3: 测试 + commit

git -C sources/ec_flutter commit -am "feat(nurse): 异常上报 + 扑空"

Task 5: 离线队列

Files: lib/core/db/**、lib/core/offline/**。

  • Step 1: sqflite 队列表:offline_queue(id, type, payload_json, client_md5, created_at, synced);本地照片文件存 path_provider 临时目录。

  • Step 2: 落队逻辑:签到/签退/勾项目/照片/异常在无网时写本地队列 + 生成 client_md5(crypto md5 对 payload 内容 hash);有网时直接走正常 API。

  • Step 3: 补传:联网检测(dio 错误判定)→ 逐条 POST /record/offline/sync(body 含 client_md5 + 完整记录)→ 成功标 synced;照片补传后保留 ≥7 天(本地文件)。

  • Step 4: 测试 + commit

git -C sources/ec_flutter commit -am "feat(nurse): 离线队列 + 自动补传"

Task 6: 验证

  • Step 1: flutter analyze 0 error + flutter test 全绿。
  • Step 2: 起后端,用 nurse01/Touyun@123/TY20261015 冒烟(模拟器或 web):登录 → 今日任务(张三工单)→ 签到 → 勾项目 → 拍照 → 签退 → 后端留痕 submitted;异常上报/扑空落库;断网签到恢复补传。
  • Step 3: 停后端清理。

Self-Review Notes

  • Spec 覆盖:frontend/nurse-app 7 Requirement → T2(登录/M1)、T3(M2/拍照)、T4(异常/扑空)、T5(离线)。
  • Review Focus 5 条均有测试/校验(T3/T4/T5)。
  • 类型一致:ApiClient/ApiException/API 方法名在 T1 定义、T2–T5 使用一致。