新增:健康事件时间线接入真实数据(T2-14 时间线半边)
- 时间线页:occurred_at DESC cursor 分页 + 月分组组头,四态齐备; 六类事件经 RecordTypeDot 映射(增补喂养/洗护/测量三型,色族复用 已审计色对),类型 TagPill 双通道 - 事件录入表单:六类类型选择器、金额以元录入/整数分传输(money.dart 换算)、UTC 时间戳约定与体重表单一致 - 事件编辑页:顶层 PATCH 差量提交(title/notes/amountCents + version), 40902 照 T2-12 模式自动经时间线检索取新 version 重提(保留输入) - 档案页:月度花费卡接 summary.monthlyExpense(分→元展示)、tz 透传 设备时区固定偏移(T2-13 遗留③)、健康时间线导航入口 - 埋点:health_record 域 create 三事件 + viewed(recordType=health_event) 挂通;新增 edit_succeeded/failed 封装(failureReason 含 conflict) - 测试 224 → 250 全绿(+26);analyze 0 问题;format 无 diff Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,8 +6,10 @@ import 'package:patbond_flutter/core/widgets/empty_state_illustration.dart';
|
||||
import 'package:patbond_flutter/core/widgets/inline_error_banner.dart';
|
||||
import 'package:patbond_flutter/core/widgets/pet_avatar.dart';
|
||||
import 'package:patbond_flutter/core/widgets/record_type_dot.dart';
|
||||
import 'package:patbond_flutter/features/pets/health_events_page.dart';
|
||||
import 'package:patbond_flutter/features/pets/health_record_analytics.dart';
|
||||
import 'package:patbond_flutter/features/pets/health_record_display.dart';
|
||||
import 'package:patbond_flutter/features/pets/money.dart';
|
||||
import 'package:patbond_flutter/features/pets/pet_analytics.dart';
|
||||
import 'package:patbond_flutter/features/pets/pet_display.dart';
|
||||
import 'package:patbond_flutter/features/pets/pet_exceptions.dart';
|
||||
@@ -104,11 +106,14 @@ class _PetDetailPageState extends State<PetDetailPage> {
|
||||
}
|
||||
|
||||
/// 摘要实时聚合(40401 由主链路 notFound 态承载,摘要只降级为 error 态)。
|
||||
/// `tz` 透传设备时区固定偏移(T2-13 遗留③):monthlyExpense 的月度
|
||||
/// 窗口随设备时区取边界,与用户直觉一致。
|
||||
Future<void> _loadSummary() async {
|
||||
setState(() => _summaryPhase = _SummaryPhase.loading);
|
||||
try {
|
||||
final summary = await widget.controller.repository.getPetSummary(
|
||||
widget.petId,
|
||||
tz: tzOffsetQueryValue(DateTime.now().timeZoneOffset),
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
@@ -185,6 +190,21 @@ class _PetDetailPageState extends State<PetDetailPage> {
|
||||
if (mounted) await _loadSummary();
|
||||
}
|
||||
|
||||
Future<void> _openTimeline(Pet pet) async {
|
||||
await Navigator.of(context).push(
|
||||
fadePageRoute(
|
||||
HealthEventsPage(
|
||||
repository: widget.controller.repository,
|
||||
petId: pet.id,
|
||||
canWrite: _canWriteRecords,
|
||||
analytics: widget.healthAnalytics,
|
||||
),
|
||||
),
|
||||
);
|
||||
// 事件可能已变化:返回即重拉摘要(月度花费实时聚合)。
|
||||
if (mounted) await _loadSummary();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -271,6 +291,8 @@ class _PetDetailPageState extends State<PetDetailPage> {
|
||||
Text('健康数据', style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 10),
|
||||
_summarySection(pet),
|
||||
const SizedBox(height: 16),
|
||||
_recordsSection(pet),
|
||||
const SizedBox(height: 24),
|
||||
Text('基本资料', style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 10),
|
||||
@@ -316,7 +338,31 @@ class _PetDetailPageState extends State<PetDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 数据卡行(05 §4.2 stat-row):三卡取数全部来自 summary 实时聚合,
|
||||
/// 记录导航区:健康时间线入口(T2-14;照护提醒入口随提醒半边落位)。
|
||||
Widget _recordsSection(Pet pet) {
|
||||
return SectionCard(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(
|
||||
Icons.event_note_outlined,
|
||||
color: AppColors.inkSoft,
|
||||
),
|
||||
title: const Text('健康时间线', style: TextStyle(fontSize: 14)),
|
||||
subtitle: const Text(
|
||||
'就医 · 喂养 · 驱虫 · 洗护 · 测量 · 随手记',
|
||||
style: TextStyle(color: AppColors.inkSoft, fontSize: 12),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right, color: AppColors.muted),
|
||||
onTap: () => _openTimeline(pet),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 数据卡行(05 §4.2 stat-row):四卡取数全部来自 summary 实时聚合,
|
||||
/// 不落任何本地展示字符串(第 4.3 节红线)。null 语义 → 空态文案。
|
||||
Widget _summarySection(Pet pet) {
|
||||
switch (_summaryPhase) {
|
||||
@@ -348,13 +394,15 @@ class _PetDetailPageState extends State<PetDetailPage> {
|
||||
final weight = summary.latestWeight;
|
||||
final progress = summary.vaccinationProgress;
|
||||
final next = summary.nextVaccination;
|
||||
final expense = summary.monthlyExpense;
|
||||
return IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _SummaryCard(
|
||||
type: RecordType.weight,
|
||||
icon: recordTypeStyles[RecordType.weight]!.icon,
|
||||
iconColor: recordTypeStyles[RecordType.weight]!.iconColor,
|
||||
value: weight == null
|
||||
? '暂无记录'
|
||||
: '${formatWeightKg(weight.weightKg)} kg',
|
||||
@@ -366,7 +414,8 @@ class _PetDetailPageState extends State<PetDetailPage> {
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _SummaryCard(
|
||||
type: RecordType.vaccine,
|
||||
icon: recordTypeStyles[RecordType.vaccine]!.icon,
|
||||
iconColor: recordTypeStyles[RecordType.vaccine]!.iconColor,
|
||||
// 契约:totalDoses=0 → 整体 null(不是 0/0)→ 空态文案。
|
||||
value: progress == null
|
||||
? '未登记'
|
||||
@@ -379,13 +428,26 @@ class _PetDetailPageState extends State<PetDetailPage> {
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _SummaryCard(
|
||||
type: RecordType.vaccine,
|
||||
icon: recordTypeStyles[RecordType.vaccine]!.icon,
|
||||
iconColor: recordTypeStyles[RecordType.vaccine]!.iconColor,
|
||||
value: next == null ? '暂无安排' : dateToJson(next.dueOn),
|
||||
emphasized: next != null,
|
||||
label: next == null ? '下一针' : '下一针·${next.vaccineName}',
|
||||
onTap: () => _openVaccinations(pet),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _SummaryCard(
|
||||
icon: Icons.payments_outlined,
|
||||
iconColor: AppColors.primaryStrong,
|
||||
// monthlyExpense 恒非 null(契约);金额整数分 → 元展示。
|
||||
value: '¥${formatCentsAsYuan(expense.amountCents)}',
|
||||
emphasized: expense.amountCents > 0,
|
||||
label: '本月花费',
|
||||
onTap: () => _openTimeline(pet),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -393,18 +455,20 @@ class _PetDetailPageState extends State<PetDetailPage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 数据卡(正典 stat-card 形态):类型图标 + 数值 15/w800 + 标签 12 inkSoft;
|
||||
/// 数据卡(正典 stat-card 形态):图标 + 数值 15/w800 + 标签 12 inkSoft;
|
||||
/// 空态数值降级 inkSoft 常规字重(区分「有数据」与「空态」两种视觉)。
|
||||
class _SummaryCard extends StatelessWidget {
|
||||
const _SummaryCard({
|
||||
required this.type,
|
||||
required this.icon,
|
||||
required this.iconColor,
|
||||
required this.value,
|
||||
required this.label,
|
||||
required this.emphasized,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final RecordType type;
|
||||
final IconData icon;
|
||||
final Color iconColor;
|
||||
final String value;
|
||||
final String label;
|
||||
final bool emphasized;
|
||||
@@ -412,7 +476,6 @@ class _SummaryCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = recordTypeStyles[type]!;
|
||||
return Card(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
@@ -422,7 +485,7 @@ class _SummaryCard extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(style.icon, size: 18, color: style.iconColor),
|
||||
Icon(icon, size: 18, color: iconColor),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
value,
|
||||
|
||||
Reference in New Issue
Block a user