新增:体重录入与历史列表接入真实数据(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:
@@ -0,0 +1,86 @@
|
||||
/// 体重 / 疫苗展示与输入解析的纯函数集合(列表 / 表单共用,可单测)。
|
||||
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 必有 plannedOn;completed 必有 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;
|
||||
}
|
||||
Reference in New Issue
Block a user