- 照护提醒页:status 过滤(服务端白名单视图)、due_at ASC、逾期红标 双通道标识,四态齐备;创建表单(四类)+ 完成(completedAt 必带、 支持补记)/ 忽略(禁带 completedAt)流转,42202/40902 兜底重拉 - 档案页:「健康提醒」卡改真实待办数据驱动(取代 demo 硬编码文案, 最近到期一条 + 逾期警示形态),照护提醒导航入口带待办数副行 - 疫苗列表(25 号报告遗留①②):scheduled 行「标记完成/取消登记」 PATCH 流转;完成对话框补录厂商/批号(契约可选字段); 42201/40902/40402 兜底 - 埋点:create 三事件 + viewed(recordType=reminder)挂通; edit_succeeded/failed 挂 vaccine 流转(failureReason 含 conflict); 提醒完成/忽略按 06 §7 缺口 3 既定取舍不埋 - 测试 250 → 272 全绿(+22,较基线 +48);analyze 0 问题;format 无 diff Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ 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/care_reminders_page.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';
|
||||
@@ -24,6 +25,8 @@ enum _DetailPhase { loading, ready, error, notFound }
|
||||
|
||||
enum _SummaryPhase { loading, ready, error }
|
||||
|
||||
enum _RemindersPhase { loading, ready, error }
|
||||
|
||||
/// 宠物详情页(T2-12 / 05 号规范 §4.2 P2 的档案信息部分)。
|
||||
///
|
||||
/// 打开即用控制器内存副本首屏渲染,同时经 [PetsController.getPet]
|
||||
@@ -59,6 +62,9 @@ class _PetDetailPageState extends State<PetDetailPage> {
|
||||
_SummaryPhase _summaryPhase = _SummaryPhase.loading;
|
||||
PetSummary? _summary;
|
||||
|
||||
_RemindersPhase _remindersPhase = _RemindersPhase.loading;
|
||||
List<CareReminder> _pendingReminders = const [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -66,6 +72,7 @@ class _PetDetailPageState extends State<PetDetailPage> {
|
||||
if (_pet != null) _phase = _DetailPhase.ready;
|
||||
_load();
|
||||
_loadSummary();
|
||||
_loadPendingReminders();
|
||||
}
|
||||
|
||||
Pet? _fromController() {
|
||||
@@ -126,6 +133,27 @@ class _PetDetailPageState extends State<PetDetailPage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 待办提醒(?status=pending,due_at ASC):驱动「健康提醒」卡与
|
||||
/// 提醒入口副行——demo 时代的硬编码提醒文案自此为真实数据取代。
|
||||
/// 失败只降级为入口副行提示,不阻塞档案主链路。
|
||||
Future<void> _loadPendingReminders() async {
|
||||
setState(() => _remindersPhase = _RemindersPhase.loading);
|
||||
try {
|
||||
final reminders = await widget.controller.repository.listCareReminders(
|
||||
widget.petId,
|
||||
status: CareReminderStatus.pending,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_pendingReminders = reminders;
|
||||
_remindersPhase = _RemindersPhase.ready;
|
||||
});
|
||||
} on ApiException {
|
||||
if (!mounted) return;
|
||||
setState(() => _remindersPhase = _RemindersPhase.error);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openEdit() async {
|
||||
final pet = _pet;
|
||||
if (pet == null) return;
|
||||
@@ -205,6 +233,21 @@ class _PetDetailPageState extends State<PetDetailPage> {
|
||||
if (mounted) await _loadSummary();
|
||||
}
|
||||
|
||||
Future<void> _openReminders(Pet pet) async {
|
||||
await Navigator.of(context).push(
|
||||
fadePageRoute(
|
||||
CareRemindersPage(
|
||||
repository: widget.controller.repository,
|
||||
petId: pet.id,
|
||||
canWrite: _canWriteRecords,
|
||||
analytics: widget.healthAnalytics,
|
||||
),
|
||||
),
|
||||
);
|
||||
// 待办可能已变化(完成/忽略/新建):返回即重拉待办。
|
||||
if (mounted) await _loadPendingReminders();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -338,27 +381,72 @@ class _PetDetailPageState extends State<PetDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 记录导航区:健康时间线入口(T2-14;照护提醒入口随提醒半边落位)。
|
||||
/// 记录导航区:健康时间线与照护提醒入口(T2-14)。待办提醒非空时
|
||||
/// 上方渲染真实数据驱动的「健康提醒」卡(正典 alert-card 形态,
|
||||
/// 取代 demo 硬编码文案),点卡与点入口同去提醒页。
|
||||
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),
|
||||
final nearest = _pendingReminders.isEmpty ? null : _pendingReminders.first;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (nearest != null) ...[
|
||||
_ReminderAlertCard(
|
||||
reminder: nearest,
|
||||
overdue: isReminderOverdue(nearest, DateTime.now()),
|
||||
onTap: () => _openReminders(pet),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
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),
|
||||
),
|
||||
const Divider(height: 1, thickness: 1, color: AppColors.border),
|
||||
ListTile(
|
||||
leading: const Icon(
|
||||
Icons.notifications_outlined,
|
||||
color: AppColors.inkSoft,
|
||||
),
|
||||
title: const Text('照护提醒', style: TextStyle(fontSize: 14)),
|
||||
subtitle: Text(
|
||||
switch (_remindersPhase) {
|
||||
_RemindersPhase.loading => '加载中…',
|
||||
_RemindersPhase.error => '提醒加载失败,点击查看',
|
||||
_RemindersPhase.ready when _pendingReminders.isEmpty =>
|
||||
'暂无待办提醒',
|
||||
_RemindersPhase.ready => '${_pendingReminders.length} 条待办',
|
||||
},
|
||||
style: const TextStyle(
|
||||
color: AppColors.inkSoft,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
color: AppColors.muted,
|
||||
),
|
||||
onTap: () => _openReminders(pet),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -550,3 +638,62 @@ class _RowDivider extends StatelessWidget {
|
||||
return const Divider(height: 1, thickness: 1, color: AppColors.border);
|
||||
}
|
||||
}
|
||||
|
||||
/// 「健康提醒」卡(正典 alert-card:successSurface 底 + dot + 文字):
|
||||
/// 数据源为最近到期的待办提醒(真实数据驱动,取代 demo 硬编码文案);
|
||||
/// 逾期时文案切警示深色(双通道:前缀文字 + 颜色)。
|
||||
class _ReminderAlertCard extends StatelessWidget {
|
||||
const _ReminderAlertCard({
|
||||
required this.reminder,
|
||||
required this.overdue,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final CareReminder reminder;
|
||||
final bool overdue;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textColor = overdue ? AppColors.errorDark : AppColors.successInk;
|
||||
return Material(
|
||||
color: AppColors.successSurface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'健康提醒:${reminder.title}'
|
||||
'(${overdue ? '已逾期' : '${dateToJson(reminder.dueAt.toLocal())} 到期'})',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(Icons.chevron_right, size: 18, color: textColor),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user