7fb9031f8d
CI / flutter-gates (push) Successful in 1m12s
- pet_models.dart:pets 域 DTO 逐字段对齐冻结契约(宠物/品种/体重/ 疫苗目录/疫苗/健康事件/照护提醒/聚合摘要 + cursor 分页信封), 枚举严格解析,请求体部分更新语义(缺席字段不出现) - pets_repository.dart:PetsRepository 抽象 + ApiPetsRepository, 12 路径 18 操作全覆盖;四个 POST 携带 Idempotency-Key; 复用既有 ApiClient token 拦截与单飞刷新重放 - pet_exceptions.dart:新 8 错误码映射为类型化异常 (40300/40401/40402/40902/40903/40904/42201/42202), 仍可按 ApiBusinessException 基类捕获 - pets_controller.dart:宠物档案状态自 AppState 拆出 (Controller → Repository → API Client 分层,四态就绪供 T2-12) - money.dart:元/分换算工具(DTO 层保持整数分,UI 层 T2-14 使用) - api_client.dart:新增 patbondPetApiBaseUrl(:8083,--dart-define 可覆盖)与 query 参数支持;api_exception.dart 补 pets 域错误码 - 测试 64 → 126:DTO 映射、错误类型化映射、请求线路 (路径/方法/鉴权/分页/幂等键/tz)、控制器状态机、金额换算 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
97 lines
3.4 KiB
Dart
97 lines
3.4 KiB
Dart
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;
|
||
|
||
PetsLoadPhase _phase = PetsLoadPhase.initial;
|
||
List<Pet> _pets = const [];
|
||
ApiException? _lastError;
|
||
bool _disposed = false;
|
||
|
||
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();
|
||
}
|
||
|
||
void _notify() {
|
||
if (!_disposed) notifyListeners();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_disposed = true;
|
||
super.dispose();
|
||
}
|
||
}
|