Files
patbond-flutter/lib/features/pets/pets_controller.dart
T
lixi 5b34fa336b 新增:体重录入与历史列表接入真实数据(T2-13 体重半边)
- WeightRecordsPage:cursor 分页(加载更多/末页收起/翻页失败保留重试)、
  loading/empty/error/retry 四态、下拉刷新;viewer 隐藏录入入口
- WeightFormPage:weightKg 契约区间 (0,500] 与两位小数前端校验 + 40000
  兜底;称重时间转 UTC 上送;错误三层分层沿用登录纵切
- health_record 域埋点强类型封装(06 §1.4:create_started/succeeded/
  failed + viewed,httpStatus 由业务码推导;viewed 按工单口径取列表曝光)
- health_record_display 纯函数(体重解析/展示、疫苗状态映射、42201
  规则前端拦截函数);PetsController 暴露 repository(22 号 §7 交接)
- 测试 177 → 205(+28)全绿

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 13:17:17 +08:00

122 lines
4.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/foundation.dart';
import 'package:patbond_flutter/core/network/api_exception.dart';
import 'package:patbond_flutter/features/pets/pet_models.dart';
import 'package:patbond_flutter/features/pets/pets_repository.dart';
/// 网络数据四态(第 9 节硬要求:loading / empty / error / retry
/// data 态含空列表判定,页面据 [PetsLoadPhase] + 数据渲染四态)。
enum PetsLoadPhase { initial, loading, ready, error }
/// pets feature 状态控制器(开发计划 §4.2 分层:
/// Page/Widget → PetsController → PetsRepository → ApiClient)。
///
/// 宠物档案状态自此从 `AppState` 拆出:本 feature 不读写 AppState 的
/// 宠物/疫苗 demo 数据;服务端是唯一事实来源,内存副本仅作展示缓存
/// (无本地持久化——档案数据以 [refresh] 为失效/刷新策略)。
///
/// 本单只承载列表 + 详情主链路状态;体重/疫苗/事件/提醒的页面级
/// 状态由 T2-12~14 按页面直接经 Repository 取数或扩展本控制器。
class PetsController extends ChangeNotifier {
PetsController({required this._repository});
final PetsRepository _repository;
/// 记录级页面(体重/疫苗/事件/提醒)直接经仓库取数(22 号报告 §7
/// 交接约定:页面级状态按页自建,不膨胀本控制器)。
PetsRepository get repository => _repository;
PetsLoadPhase _phase = PetsLoadPhase.initial;
List<Pet> _pets = const [];
ApiException? _lastError;
bool _disposed = false;
final Map<PetSpecies, List<Breed>> _breedsCache = {};
PetsLoadPhase get phase => _phase;
/// 当前用户可见宠物(服务端 created_at DESC 排序原样保留)。
List<Pet> get pets => _pets;
/// ready 且列表为空 → 空态(新用户建档引导)。
bool get isEmpty => _phase == PetsLoadPhase.ready && _pets.isEmpty;
/// 最近一次加载失败的类型化错误(error 态时非 null)。
ApiException? get lastError => _lastError;
/// 加载 / 重试宠物列表。错误不外抛,收敛为 error 态供页面渲染 + retry。
Future<void> refresh() async {
_phase = PetsLoadPhase.loading;
_lastError = null;
_notify();
try {
_pets = await _repository.listPets();
_phase = PetsLoadPhase.ready;
} on ApiException catch (error) {
_lastError = error;
_phase = PetsLoadPhase.error;
}
_notify();
}
/// 创建宠物:成功后就地插入列表头(服务端 created_at DESC,新建最前),
/// 失败按类型化异常外抛给表单层处理(40903 芯片号冲突等)。
Future<Pet> createPet(CreatePetRequest request) async {
final pet = await _repository.createPet(request);
_pets = [pet, ..._pets];
if (_phase != PetsLoadPhase.ready) _phase = PetsLoadPhase.ready;
_notify();
return pet;
}
/// 拉取单只宠物详情并同步列表内存副本。
Future<Pet> getPet(String petId) async {
final pet = await _repository.getPet(petId);
_replaceInList(pet);
return pet;
}
/// 更新宠物档案:成功后同步列表副本;40902 版本冲突等外抛,
/// 页面提示后应调用 [getPet] 刷新取新 version 重提。
Future<Pet> updatePet(String petId, UpdatePetRequest request) async {
final pet = await _repository.updatePet(petId, request);
_replaceInList(pet);
return pet;
}
void _replaceInList(Pet pet) {
final index = _pets.indexWhere((item) => item.id == pet.id);
if (index == -1) return;
_pets = [..._pets]..[index] = pet;
_notify();
}
/// 品种目录(只读字典):按物种缓存,会话内目录不变;
/// 失败按类型化异常外抛,供表单层内联重试。
Future<List<Breed>> loadBreeds(PetSpecies species) async {
final cached = _breedsCache[species];
if (cached != null) return cached;
final breeds = await _repository.listBreeds(species: species);
_breedsCache[species] = breeds;
return breeds;
}
/// 登出清空:回 initial 态并清缓存,避免上一账号档案跨会话泄漏;
/// 重新登录后主壳重建,列表页 initState 重新触发 [refresh]。
void reset() {
_phase = PetsLoadPhase.initial;
_pets = const [];
_lastError = null;
_breedsCache.clear();
_notify();
}
void _notify() {
if (!_disposed) notifyListeners();
}
@override
void dispose() {
_disposed = true;
super.dispose();
}
}