import 'package:flutter/material.dart'; import 'package:patbond_flutter/core/navigation/fade_route.dart'; import 'package:patbond_flutter/core/network/api_exception.dart'; import 'package:patbond_flutter/core/theme/app_theme.dart'; 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/features/pets/pet_analytics.dart'; import 'package:patbond_flutter/features/pets/pet_display.dart'; import 'package:patbond_flutter/features/pets/pet_exceptions.dart'; import 'package:patbond_flutter/features/pets/pet_form_page.dart'; import 'package:patbond_flutter/features/pets/pet_models.dart'; import 'package:patbond_flutter/features/pets/pets_controller.dart'; import 'package:patbond_flutter/widgets/common.dart'; enum _DetailPhase { loading, ready, error, notFound } /// 宠物详情页(T2-12 / 05 号规范 §4.2 P2 的档案信息部分)。 /// /// 打开即用控制器内存副本首屏渲染,同时经 [PetsController.getPet] /// 拉取最新详情(同步列表副本)。四态:loading / ready / error+重试 / /// notFound(40401 防枚举三态同响应 → 提示后返回列表并刷新)。 /// /// 体重、疫苗进度、健康时间线与提醒区块随 T2-13/14 接入摘要与 /// 记录接口后补充,本单不渲染 demo 占位。 class PetDetailPage extends StatefulWidget { const PetDetailPage({ required this.controller, required this.petId, super.key, this.analytics, }); final PetsController controller; final String petId; final PetAnalytics? analytics; @override State createState() => _PetDetailPageState(); } class _PetDetailPageState extends State { _DetailPhase _phase = _DetailPhase.loading; Pet? _pet; ApiException? _error; @override void initState() { super.initState(); _pet = _fromController(); if (_pet != null) _phase = _DetailPhase.ready; _load(); } Pet? _fromController() { for (final pet in widget.controller.pets) { if (pet.id == widget.petId) return pet; } return null; } Future _load() async { if (_pet == null) { setState(() => _phase = _DetailPhase.loading); } try { final pet = await widget.controller.getPet(widget.petId); if (!mounted) return; setState(() { _pet = pet; _phase = _DetailPhase.ready; }); } on PetNotFoundException { if (!mounted) return; setState(() => _phase = _DetailPhase.notFound); } on ApiException catch (error) { if (!mounted) return; if (_pet == null) { setState(() { _error = error; _phase = _DetailPhase.error; }); } else { // 已有内存副本:刷新失败降级为瞬态提示,不打断阅读。 ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(petLoadErrorMessage(error)))); } } } Future _openEdit() async { final pet = _pet; if (pet == null) return; final updated = await Navigator.of(context).push( // 编辑态不带路由名:pet_form 专属建宠漏斗到达段(06 §1.6), // 编辑曝光不计入,避免「到达→动笔」分母虚高。 fadePageRoute( PetFormPage.edit( controller: widget.controller, pet: pet, analytics: widget.analytics, ), ), ); if (updated != null && mounted) { setState(() { _pet = updated; _phase = _DetailPhase.ready; }); ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('已保存修改'))); } } /// 仅 owner 可改档案(40300:caregiver/viewer 改档案被拒 → 隐藏写入口)。 bool get _canEdit => _phase == _DetailPhase.ready && _pet?.myRole == PetRole.owner; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, foregroundColor: AppColors.ink, actions: [ if (_canEdit) IconButton( tooltip: '编辑资料', onPressed: _openEdit, icon: const Icon(Icons.edit_outlined), ), ], ), body: switch (_phase) { _DetailPhase.loading => const Center( child: CircularProgressIndicator(), ), _DetailPhase.error => Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ InlineErrorBanner(message: petLoadErrorMessage(_error)), const SizedBox(height: 16), FilledButton(onPressed: _load, child: const Text('重试')), ], ), ), ), _DetailPhase.notFound => Center( child: SingleChildScrollView( child: EmptyStateIllustration( icon: Icons.search_off_rounded, title: '宠物不存在或已被删除', description: '档案可能已被移除,返回列表查看最新档案', ctaLabel: '返回列表', onCtaPressed: () { widget.controller.refresh(); Navigator.of(context).pop(); }, ), ), ), _DetailPhase.ready => _content(_pet!), }, ); } Widget _content(Pet pet) { return ListView( padding: const EdgeInsets.fromLTRB(16, 0, 16, 30), children: [ Column( children: [ PetAvatar( size: PetAvatarSize.xl, showEditBadge: _canEdit, onTap: _canEdit ? _openEdit : null, semanticLabel: _canEdit ? '编辑宠物资料' : null, ), const SizedBox(height: 12), Text(pet.name, style: Theme.of(context).textTheme.headlineSmall), const SizedBox(height: 4), Text( petMetaLine(pet), style: const TextStyle(color: AppColors.inkSoft, fontSize: 12), ), if (pet.status != PetStatus.active) ...[ const SizedBox(height: 8), TagPill( petStatusLabel(pet.status), color: pet.status == PetStatus.lost ? AppColors.error : AppColors.muted, ), ], ], ), const SizedBox(height: 24), Text('基本资料', style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 10), SectionCard( child: Column( children: [ _InfoRow(label: '物种', value: petSpeciesLabel(pet.species)), const _RowDivider(), _InfoRow(label: '品种', value: petBreedLabel(pet)), const _RowDivider(), _InfoRow(label: '性别', value: petSexLabel(pet.sex)), const _RowDivider(), _InfoRow( label: '生日', value: pet.birthDate == null ? '未填写' : dateToJson(pet.birthDate!) + (pet.birthDateEstimated ? '(估算)' : ''), ), const _RowDivider(), _InfoRow(label: '芯片号', value: pet.microchipNo ?? '未填写'), const _RowDivider(), _InfoRow(label: '性格', value: pet.personality ?? '未填写'), const _RowDivider(), _InfoRow( label: '绝育日期', value: pet.sterilizedOn == null ? '未填写' : dateToJson(pet.sterilizedOn!), ), ], ), ), if (_canEdit) ...[ const SizedBox(height: 16), OutlinedButton.icon( onPressed: _openEdit, icon: const Icon(Icons.edit_outlined, size: 17), label: const Text('编辑资料'), ), ], ], ); } } /// 键值行(05 §4.3 P3 风格:字段名 12 inkSoft / 值 bodyMedium ink)。 class _InfoRow extends StatelessWidget { const _InfoRow({required this.label, required this.value}); final String label; final String value; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 7), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 72, child: Text( label, style: const TextStyle(color: AppColors.inkSoft, fontSize: 12), ), ), Expanded( child: Text(value, style: Theme.of(context).textTheme.bodyMedium), ), ], ), ); } } class _RowDivider extends StatelessWidget { const _RowDivider(); @override Widget build(BuildContext context) { return const Divider(height: 1, thickness: 1, color: AppColors.border); } }