/// 体重 / 疫苗展示与输入解析的纯函数集合(列表 / 表单共用,可单测)。 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; }