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/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/record_type_dot.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/pet_display.dart'; import 'package:patbond_flutter/features/pets/pet_models.dart'; import 'package:patbond_flutter/features/pets/pets_repository.dart'; import 'package:patbond_flutter/features/pets/vaccination_form_page.dart'; import 'package:patbond_flutter/widgets/common.dart'; enum _ListPhase { loading, ready, error } /// 疫苗记录列表页(T2-13):契约不分页,服务端按 /// `series_key, dose_no, created_at, id` 排序,客户端按系列直接分组; /// 含 cancelled 行原样展示(取消后同剂次可重新登记的事实留痕)。 /// 四态齐备;登记经 [VaccinationFormPage]。 /// /// 曝光埋点:每次进入首个成功加载上报一次 /// `health_record_viewed(recordType=vaccine, source=pet_detail)`。 class VaccinationRecordsPage extends StatefulWidget { const VaccinationRecordsPage({ required this.repository, required this.petId, required this.petSpecies, required this.canWrite, super.key, this.analytics, }); final PetsRepository repository; final String petId; /// 疫苗目录按宠物物种过滤(契约:疫苗 species 须与宠物一致)。 final PetSpecies petSpecies; /// owner/caregiver 可写;viewer 隐藏登记入口。 final bool canWrite; final HealthRecordAnalytics? analytics; @override State createState() => _VaccinationRecordsPageState(); } class _VaccinationRecordsPageState extends State { _ListPhase _phase = _ListPhase.loading; List _records = const []; ApiException? _error; bool _viewedFired = false; @override void initState() { super.initState(); _load(); } Future _load() async { setState(() { _phase = _ListPhase.loading; _error = null; }); try { final records = await widget.repository.listVaccinations(widget.petId); if (!mounted) return; setState(() { _records = records; _phase = _ListPhase.ready; }); if (!_viewedFired) { _viewedFired = true; widget.analytics?.viewed( recordType: HealthRecordType.vaccine, source: HealthRecordViewSource.petDetail, ); } } on ApiException catch (error) { if (!mounted) return; setState(() { _error = error; _phase = _ListPhase.error; }); } } Future _openCreate() async { final created = await Navigator.of(context).push( fadePageRoute( VaccinationFormPage( repository: widget.repository, petId: widget.petId, petSpecies: widget.petSpecies, analytics: widget.analytics, ), settings: RouteSettings(name: AnalyticsPageName.recordForm.pageName), ), ); if (created != null && mounted) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('已登记疫苗'))); // 排序键在服务端(series_key, dose_no),重新拉取而非本地猜位置。 await _load(); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, foregroundColor: AppColors.ink, title: const Text('疫苗记录'), centerTitle: true, titleTextStyle: const TextStyle( color: AppColors.ink, fontSize: 16, fontWeight: FontWeight.w800, ), actions: [ if (widget.canWrite && _phase == _ListPhase.ready) IconButton( tooltip: '登记疫苗', onPressed: _openCreate, icon: const Icon(Icons.add), ), ], ), body: switch (_phase) { _ListPhase.loading => const Center(child: CircularProgressIndicator()), _ListPhase.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('重试')), ], ), ), ), _ListPhase.ready when _records.isEmpty => Center( child: SingleChildScrollView( child: EmptyStateIllustration( icon: Icons.vaccines_outlined, title: '还没有疫苗记录', description: '登记接种计划与完成情况,不错过每一针', ctaLabel: widget.canWrite ? '登记第一针' : null, onCtaPressed: widget.canWrite ? _openCreate : null, ), ), ), _ListPhase.ready => _list(), }, ); } /// 按系列分组渲染:服务端排序保证同系列相邻,系列变化处插组头 /// (疫苗名 · 系列键)。 Widget _list() { final children = []; String? currentSeries; for (final record in _records) { final seriesId = '${record.vaccineId}/${record.seriesKey}'; if (seriesId != currentSeries) { currentSeries = seriesId; children.add( Padding( padding: EdgeInsets.only(top: children.isEmpty ? 0 : 14, bottom: 8), child: Text( '${record.vaccineName} · ${record.seriesKey}', style: const TextStyle( color: AppColors.inkSoft, fontSize: 12, fontWeight: FontWeight.w700, ), ), ), ); } children ..add(_VaccinationTile(record: record)) ..add(const SizedBox(height: 10)); } return RefreshIndicator( onRefresh: _load, child: ListView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 30), children: children, ), ); } } /// 疫苗条目:RecordTypeDot(疫苗) + 剂次标题 + 日期副行 + 状态 TagPill /// (图标+文字双通道,不单靠颜色区分)。 class _VaccinationTile extends StatelessWidget { const _VaccinationTile({required this.record}); final Vaccination record; @override Widget build(BuildContext context) { return Card( child: Padding( padding: const EdgeInsets.all(14), child: Row( children: [ const RecordTypeDot( type: RecordType.vaccine, size: RecordTypeDotSize.md, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( vaccinationDoseLabel(record), style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 4), Text( vaccinationDateLine(record), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.inkSoft, fontSize: 12, ), ), ], ), ), const SizedBox(width: 8), TagPill( vaccinationStatusLabel(record.status), color: vaccinationStatusColor(record.status), ), ], ), ), ); } }