8.0 KiB
护理员 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
- 服务器取时:checkin/checkout 请求体不得含 checkin_time/checkout_time/duration_min。
- 必拍未拍:必拍项未拍照时签退按钮禁用;拒拍出路选原因后放行。
- 离线幂等:每条离线记录生成 client_md5(内容 hash),补传按 md5 幂等,不重复。
- exec_result 措辞:未执行用 undo_not_done(非 not_done);老人拒做用 undo_refused。
- 扑空证据: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 analyze0 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 使用一致。