新增:体重录入与历史列表接入真实数据(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>
This commit is contained in:
2026-09-08 13:17:17 +08:00
parent 97a1f46e3f
commit 5b34fa336b
10 changed files with 1528 additions and 0 deletions
+95
View File
@@ -129,6 +129,44 @@ Pet buildPet(
),
);
Map<String, dynamic> sampleVaccineCatalogJson({
String id = 'v-1',
String code = 'rabies',
String name = '狂犬疫苗',
String species = 'dog',
}) => {
'id': id,
'code': code,
'name': name,
'species': species,
'description': null,
};
/// 快速构造体重记录(widget 测试共用)。
WeightRecord buildWeight(
String id, {
double weightKg = 4.35,
String measuredAt = '2026-09-07T09:00:00+08:00',
Map<String, Object?> overrides = const {},
}) => WeightRecord.fromJson({
...sampleWeightJson(),
'id': id,
'weightKg': weightKg,
'measuredAt': measuredAt,
...overrides,
});
/// 快速构造疫苗记录。
Vaccination buildVaccination(
String id, {
Map<String, Object?> overrides = const {},
}) =>
Vaccination.fromJson({...sampleVaccinationJson(), 'id': id, ...overrides});
/// 快速构造摘要(缺省为「三聚合齐备」样本;overrides 可置 null 验证空态)。
PetSummary buildSummary({Map<String, Object?> overrides = const {}}) =>
PetSummary.fromJson({...samplePetSummaryJson(), ...overrides});
/// 假仓库:各方法可注入行为;listPets 默认空列表,其余未注入的方法抛
/// UnimplementedError22 号报告 §7widget 测试注入假仓库的共享实现)。
class FakePetsRepository implements PetsRepository {
@@ -137,6 +175,16 @@ class FakePetsRepository implements PetsRepository {
Future<Pet> Function(String)? getPetHandler;
Future<Pet> Function(String, UpdatePetRequest)? updatePetHandler;
Future<List<Breed>> Function(PetSpecies?)? listBreedsHandler;
Future<CursorPage<WeightRecord>> Function(String, int?, String?)?
listWeightsHandler;
Future<WeightRecord> Function(String, CreateWeightRequest)?
createWeightHandler;
Future<List<VaccineCatalogItem>> Function(PetSpecies?)?
listVaccineCatalogHandler;
Future<List<Vaccination>> Function(String)? listVaccinationsHandler;
Future<Vaccination> Function(String, CreateVaccinationRequest)?
createVaccinationHandler;
Future<PetSummary> Function(String, String?)? getPetSummaryHandler;
@override
Future<List<Pet>> listPets() =>
@@ -157,6 +205,53 @@ class FakePetsRepository implements PetsRepository {
listBreedsHandler?.call(species) ??
Future.value([Breed.fromJson(sampleBreedJson())]);
@override
Future<CursorPage<WeightRecord>> listWeights(
String petId, {
int? limit,
String? cursor,
}) => listWeightsHandler!(petId, limit, cursor);
@override
Future<WeightRecord> createWeight(
String petId,
CreateWeightRequest request,
) => createWeightHandler!(petId, request);
@override
Future<List<VaccineCatalogItem>> listVaccineCatalog({PetSpecies? species}) =>
listVaccineCatalogHandler?.call(species) ??
Future.value([VaccineCatalogItem.fromJson(sampleVaccineCatalogJson())]);
@override
Future<List<Vaccination>> listVaccinations(String petId) =>
listVaccinationsHandler!(petId);
@override
Future<Vaccination> createVaccination(
String petId,
CreateVaccinationRequest request,
) => createVaccinationHandler!(petId, request);
@override
Future<PetSummary> getPetSummary(String petId, {String? tz}) =>
getPetSummaryHandler?.call(petId, tz) ??
// 缺省给「无任何记录」空摘要(契约 null 语义),既有详情页测试
// 不必逐个注入。
Future.value(
PetSummary.fromJson({
'petId': petId,
'latestWeight': null,
'vaccinationProgress': null,
'nextVaccination': null,
'monthlyExpense': {
'month': '2026-09',
'timezone': 'UTC',
'amountCents': 0,
},
}),
);
@override
dynamic noSuchMethod(Invocation invocation) =>
throw UnimplementedError('${invocation.memberName}');