Files
patbond-flutter/lib/features/pets/health_record_display.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

87 lines
3.5 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.
/// 体重 / 疫苗展示与输入解析的纯函数集合(列表 / 表单共用,可单测)。
library;
import 'package:flutter/material.dart';
import 'package:patbond_flutter/core/theme/app_theme.dart';
import 'package:patbond_flutter/features/pets/pet_models.dart';
/// 体重输入解析:契约区间 (0, 500]、最多两位小数(numeric(6,2))。
/// 非法(非数字、越界、三位小数)返回 null,由表单层给 errorText。
double? parseWeightKgInput(String raw) {
final text = raw.trim();
if (!RegExp(r'^\d{1,3}(\.\d{1,2})?$').hasMatch(text)) return null;
final value = double.parse(text);
if (value <= 0 || value > 500) return null;
return value;
}
/// 体重展示:去掉无意义尾零(4.35 → 4.35、5.00 → 5、4.50 → 4.5)。
String formatWeightKg(double weightKg) {
var text = weightKg.toStringAsFixed(2);
if (text.contains('.')) {
text = text.replaceFirst(RegExp(r'0+$'), '');
text = text.replaceFirst(RegExp(r'\.$'), '');
}
return text;
}
String vaccinationStatusLabel(VaccinationStatus status) => switch (status) {
VaccinationStatus.scheduled => '计划中',
VaccinationStatus.completed => '已完成',
VaccinationStatus.cancelled => '已取消',
};
/// 状态标签基色(TagPill 淡染底;文字深变体由 TagPill 内置映射)。
Color vaccinationStatusColor(VaccinationStatus status) => switch (status) {
VaccinationStatus.scheduled => AppColors.accent,
VaccinationStatus.completed => AppColors.success,
VaccinationStatus.cancelled => AppColors.muted,
};
/// 剂次展示:doseLabel 优先,缺席回落「第 N 针」。
String vaccinationDoseLabel(Vaccination vaccination) =>
vaccination.doseLabel ?? '第 ${vaccination.doseNo} 针';
/// 疫苗条目副行:按状态给日期语义(计划 / 接种 + 下次到期 / 取消)。
String vaccinationDateLine(
Vaccination vaccination,
) => switch (vaccination.status) {
VaccinationStatus.scheduled =>
'计划 ${vaccination.plannedOn == null ? '未定' : dateToJson(vaccination.plannedOn!)}',
VaccinationStatus.completed =>
'接种 ${vaccination.administeredOn == null ? '—' : dateToJson(vaccination.administeredOn!)}'
'${vaccination.nextDueOn == null ? '' : ' · 下次 ${dateToJson(vaccination.nextDueOn!)}'}',
VaccinationStatus.cancelled => '已取消',
};
/// 摘要疫苗进度展示:契约 null 语义——无登记为 null(不是 0/0),
/// 调用方对 null 自行给空态文案。
String vaccinationProgressLabel(SummaryVaccinationProgress progress) =>
'${progress.completedDoses}/${progress.totalDoses}';
/// 疫苗状态-日期规则前端校验(契约 42201 规则的前置拦截;纯函数可单测):
/// scheduled 必有 plannedOncompleted 必有 administeredOn
/// nextDueOn 与 administeredOn 同时存在时须 nextDueOn ≥ administeredOn。
/// 通过返回 null,违反返回给用户的拦截文案。
String? vaccinationDateRuleError({
required VaccinationStatus status,
DateTime? plannedOn,
DateTime? administeredOn,
DateTime? nextDueOn,
}) {
if (status == VaccinationStatus.scheduled && plannedOn == null) {
return '请选择计划接种日期';
}
if (status == VaccinationStatus.completed && administeredOn == null) {
return '请选择接种日期';
}
if (administeredOn != null &&
nextDueOn != null &&
nextDueOn.isBefore(
DateTime(administeredOn.year, administeredOn.month, administeredOn.day),
)) {
return '下次接种日期不能早于接种日期';
}
return null;
}