97a1f46e3f
CI / flutter-gates (push) Successful in 1m31s
- 档案 Tab 替换为真实宠物列表(05 号规范 P1):PetsController 四态 驱动(loading/empty/error+重试/ready),空态插画 + 建档 CTA, 下拉刷新,虚线添加卡;页面一律走 T2-11 数据层、不直连 ApiClient - 新增 PetDetailPage(P2 档案信息部分):内存副本首屏 + getPet 刷新, 40401 防枚举 → 不存在态返回列表并刷新;仅 owner 显示编辑入口 (40300 语义);体重/疫苗/时间线区块留待 T2-13/14 接摘要与记录接口 - 新增 PetFormPage(建档/编辑):品种目录接口 + 自定义品种互斥 (目录失败回落自定义 + 重试)、性别契约必填、生日+估算、芯片号; 失焦+提交双校验、错误三层模型沿用登录纵切 - 40902 版本冲突:明确提示 + 自动拉取最新 version 重提路径(有测试); 40903 芯片号冲突字段级报错(有测试) - 埋点挂接:pet_create_started(表单首次输入、每次进入一次)/ succeeded / failed 三事件接线;建宠表单路由名 pet_form、详情页 pet_detail 进入既有 RouteObserver 采集;档案 Tab 页名 pet_archive 改报 pet_list(字典 v2);编辑表单不带路由名(漏斗到达段不掺编辑) - app.dart 装配:pet 服务 ApiClient(:8083)共享 TokenRefresher; 登出 reset 控制器防跨账号泄漏;PetsController.loadBreeds 按物种缓存 - AppState 清理:pets 页原 demo 消费方移除,vaccines 及其模型/持久化 删除;pet demo 仅存首页问候/创作页/主壳头像(后续工单收敛) - 头像按 ADR-010 本地占位,不做上传 - 测试 145 → 177 全绿(列表/详情/表单四态与冲突路径 widget 测试、 控制器 loadBreeds/reset、展示纯函数);analyze 0;format 无 diff Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
118 lines
4.2 KiB
Dart
118 lines
4.2 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;
|
||
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();
|
||
}
|
||
}
|