import 'package:flutter/material.dart'; import 'package:patbond_flutter/analytics/analytics_page_name.dart'; import 'package:patbond_flutter/core/navigation/fade_route.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/health_record_analytics.dart'; import 'package:patbond_flutter/features/pets/pet_analytics.dart'; import 'package:patbond_flutter/features/pets/pet_detail_page.dart'; import 'package:patbond_flutter/features/pets/pet_display.dart'; import 'package:patbond_flutter/features/pets/pet_form_page.dart'; import 'package:patbond_flutter/features/pets/pets_controller.dart'; import 'package:patbond_flutter/features/pets/pet_models.dart'; import 'package:patbond_flutter/widgets/common.dart'; /// 档案 Tab 落地页:宠物列表(T2-12 / 05 号规范 §4.1 P1)。 /// /// 真实数据经 [PetsController] 四态驱动:loading(居中转圈)、 /// empty(空态插画 + 建档 CTA)、error(横幅 + 重试)、ready(列表)。 /// 页面不直连 ApiClient,不读写 AppState demo 数据。 class PetsPage extends StatefulWidget { const PetsPage({ required this.controller, super.key, this.analytics, this.healthAnalytics, }); final PetsController controller; final PetAnalytics? analytics; /// health_record 域埋点(T2-13,记录页面族透传)。 final HealthRecordAnalytics? healthAnalytics; @override State createState() => _PetsPageState(); } class _PetsPageState extends State { @override void initState() { super.initState(); // 主壳挂载即预取(IndexedStack 各 Tab 同时构建);重登后控制器 // 已被 reset 回 initial,会重新拉取。 if (widget.controller.phase == PetsLoadPhase.initial) { widget.controller.refresh(); } } Future _openCreate(PetCreateEntryPoint entryPoint) async { final created = await Navigator.of(context).push( fadePageRoute( PetFormPage.create( controller: widget.controller, analytics: widget.analytics, entryPoint: entryPoint, ), // 建宠表单曝光由既有 RouteObserver 采集(06 §1.6 pet_form)。 settings: RouteSettings(name: AnalyticsPageName.petForm.pageName), ), ); if (created != null && mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('已为「${created.name}」建立档案 🐾'))); } } void _openDetail(Pet pet) { Navigator.of(context).push( fadePageRoute( PetDetailPage( controller: widget.controller, petId: pet.id, analytics: widget.analytics, healthAnalytics: widget.healthAnalytics, ), settings: RouteSettings(name: AnalyticsPageName.petDetail.pageName), ), ); } @override Widget build(BuildContext context) { return ListenableBuilder( listenable: widget.controller, builder: (context, _) { final controller = widget.controller; switch (controller.phase) { case PetsLoadPhase.initial: case PetsLoadPhase.loading: return const Center(child: CircularProgressIndicator()); case PetsLoadPhase.error: return _LoadErrorView( message: petLoadErrorMessage(controller.lastError), onRetry: controller.refresh, ); case PetsLoadPhase.ready: if (controller.isEmpty) { return Center( child: SingleChildScrollView( child: EmptyStateIllustration( icon: Icons.pets, title: '还没有宠物档案', description: '添加毛孩子,开始记录 TA 的健康点滴', ctaLabel: '添加宠物', onCtaPressed: () => _openCreate(PetCreateEntryPoint.profileEmptyState), ), ), ); } return _petList(controller.pets); } }, ); } Widget _petList(List pets) { return RefreshIndicator( onRefresh: widget.controller.refresh, child: ListView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 30), children: [ Row( children: [ Text('我的宠物', style: Theme.of(context).textTheme.titleLarge), const Spacer(), TextButton.icon( onPressed: () => _openCreate(PetCreateEntryPoint.petList), icon: const Icon(Icons.add, size: 18), label: const Text('添加'), ), ], ), const SizedBox(height: 12), for (final pet in pets) ...[ _PetCard(pet: pet, onTap: () => _openDetail(pet)), const SizedBox(height: 10), ], _AddPetCard(onTap: () => _openCreate(PetCreateEntryPoint.petList)), ], ), ); } } /// 加载失败态:InlineErrorBanner + 重试按钮(05 §4.2 错误三层模型)。 class _LoadErrorView extends StatelessWidget { const _LoadErrorView({required this.message, required this.onRetry}); final String message; final Future Function() onRetry; @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ InlineErrorBanner(message: message), const SizedBox(height: 16), FilledButton(onPressed: onRetry, child: const Text('重试')), ], ), ), ); } } /// 宠物卡(05 §4.1):头像 lg + 名字 + 元信息 + chevron;非 active /// 状态以 TagPill 标示(图文双通道由状态文案承担)。 class _PetCard extends StatelessWidget { const _PetCard({required this.pet, required this.onTap}); final Pet pet; final VoidCallback onTap; @override Widget build(BuildContext context) { return Card( child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(AppRadius.xl), child: Padding( padding: const EdgeInsets.all(14), child: Row( children: [ const PetAvatar(size: PetAvatarSize.lg), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( pet.name, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 4), Text( petMetaLine(pet), style: const TextStyle( color: AppColors.inkSoft, fontSize: 12, ), ), ], ), ), if (pet.status != PetStatus.active) TagPill( petStatusLabel(pet.status), color: pet.status == PetStatus.lost ? AppColors.error : AppColors.muted, ) else const Icon(Icons.chevron_right, color: AppColors.muted), ], ), ), ), ); } } /// 虚线「添加宠物」卡(05 §4.1:border 色 1.5px dashed,radius 24,高 64)。 class _AddPetCard extends StatelessWidget { const _AddPetCard({required this.onTap}); final VoidCallback onTap; @override Widget build(BuildContext context) { return CustomPaint( painter: _DashedBorderPainter( color: AppColors.border, strokeWidth: 1.5, radius: AppRadius.xl, ), child: SizedBox( height: 64, child: Material( color: Colors.transparent, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(AppRadius.xl), child: const Center( child: Text( '+ 添加宠物', style: TextStyle( color: AppColors.primaryStrong, fontSize: 14, fontWeight: FontWeight.w600, ), ), ), ), ), ), ); } } class _DashedBorderPainter extends CustomPainter { const _DashedBorderPainter({ required this.color, required this.strokeWidth, required this.radius, }); final Color color; final double strokeWidth; final double radius; @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = color ..strokeWidth = strokeWidth ..style = PaintingStyle.stroke; final path = Path() ..addRRect( RRect.fromRectAndRadius(Offset.zero & size, Radius.circular(radius)), ); const dashWidth = 6.0; const dashGap = 4.0; for (final metric in path.computeMetrics()) { var distance = 0.0; while (distance < metric.length) { canvas.drawPath( metric.extractPath(distance, distance + dashWidth), paint, ); distance += dashWidth + dashGap; } } } @override bool shouldRepaint(_DashedBorderPainter oldDelegate) => color != oldDelegate.color || strokeWidth != oldDelegate.strokeWidth || radius != oldDelegate.radius; }