import 'package:flutter/material.dart'; import 'package:patbond_flutter/core/theme/app_theme.dart'; import 'package:patbond_flutter/data/demo_data.dart'; import 'package:patbond_flutter/models/models.dart'; import 'package:patbond_flutter/state/app_state.dart'; import 'package:patbond_flutter/widgets/common.dart'; class PetsPage extends StatelessWidget { const PetsPage({required this.appState, super.key}); final AppState appState; String ageLabel(String value) { final birthday = DateTime.tryParse(value); if (birthday == null) return '年龄未知'; final today = DateTime.now(); var age = today.year - birthday.year; if (today.month < birthday.month || (today.month == birthday.month && today.day < birthday.day)) { age--; } return '${age < 0 ? 0 : age} 岁'; } Future editPet(BuildContext context) async { final value = await showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, builder: (context) => EditPetSheet(pet: appState.pet), ); if (value != null) await appState.updatePet(value); } Future editVaccines(BuildContext context) async { final value = await showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, builder: (context) => VaccineSheet(record: appState.vaccines), ); if (value != null) await appState.updateVaccines(value); } @override Widget build(BuildContext context) { final pet = appState.pet; final vaccines = appState.vaccines; final progress = vaccines.totalDoses == 0 ? 0.0 : vaccines.completedDoses / vaccines.totalDoses; return ListView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 30), children: [ Column( children: [ Stack( children: [ InkWell( onTap: () => editPet(context), borderRadius: BorderRadius.circular(54), child: RemoteImage( url: pet.avatarUrl, width: 104, height: 104, borderRadius: BorderRadius.circular(52), ), ), Positioned( right: 0, bottom: 0, child: CircleAvatar( radius: 16, backgroundColor: AppColors.primary, child: IconButton( padding: EdgeInsets.zero, tooltip: '编辑资料', onPressed: () => editPet(context), icon: const Icon( Icons.edit, color: Colors.white, size: 15, ), ), ), ), ], ), const SizedBox(height: 12), Text(pet.name, style: Theme.of(context).textTheme.headlineSmall), const SizedBox(height: 4), Text( '${pet.breed} · ${pet.gender == PetGender.male ? '男孩' : '女孩'} · ${ageLabel(pet.birthday)} · 活泼', style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 10), OutlinedButton.icon( onPressed: () => editPet(context), icon: const Icon(Icons.settings_outlined, size: 17), label: const Text('编辑资料'), ), ], ), const SizedBox(height: 24), Row( children: [ Text('宠物数据', style: Theme.of(context).textTheme.titleLarge), const Spacer(), TextButton.icon( onPressed: () => editPet(context), icon: const Icon(Icons.edit_outlined, size: 16), label: const Text('编辑'), ), ], ), const SizedBox(height: 8), Row( children: [ Expanded( child: _StatCard( icon: Icons.monitor_weight_outlined, color: AppColors.primary, value: '${pet.weight.toStringAsFixed(1)}kg', label: '体重', ), ), const SizedBox(width: 10), Expanded( child: _StatCard( icon: Icons.vaccines_outlined, color: AppColors.success, value: '${vaccines.completedDoses}/${vaccines.totalDoses}', label: '疫苗进度', onTap: () => editVaccines(context), ), ), const SizedBox(width: 10), const Expanded( child: _StatCard( icon: Icons.payments_outlined, color: AppColors.warning, value: '¥328', label: '本月花费', ), ), ], ), const SizedBox(height: 18), SectionCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text('健康概览', style: Theme.of(context).textTheme.titleMedium), const Spacer(), Text('更新于今日', style: Theme.of(context).textTheme.bodySmall), ], ), const SizedBox(height: 18), Row( children: [ SizedBox.square( dimension: 62, child: Stack( fit: StackFit.expand, children: [ CircularProgressIndicator( value: progress, strokeWidth: 7, backgroundColor: AppColors.border, color: AppColors.success, ), Center( child: Text( '${(progress * 100).round()}%', style: const TextStyle( color: AppColors.success, fontWeight: FontWeight.w800, ), ), ), ], ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( progress >= 1 ? '疫苗接种完成' : '疫苗接种进行中', style: const TextStyle(fontWeight: FontWeight.w800), ), const SizedBox(height: 5), Text( '下一针:${vaccines.reminderVaccine}\n预计 ${vaccines.reminderDate}', style: Theme.of(context).textTheme.bodySmall, ), ], ), ), IconButton( tooltip: '管理疫苗', onPressed: () => editVaccines(context), icon: const Icon(Icons.edit_outlined), ), ], ), ], ), ), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: AppColors.successSurface, borderRadius: BorderRadius.circular(24), border: Border.all(color: AppColors.success.withAlpha(140)), ), child: const Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.auto_awesome, color: AppColors.success), SizedBox(width: 10), Expanded( child: Text( '健康提醒:已经半年没有进行体内外驱虫,建议本周安排一次。', style: TextStyle(color: AppColors.successInk, height: 1.5), ), ), ], ), ), const SizedBox(height: 22), Text('成长足迹', style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 10), const _TimelineTile( icon: Icons.medical_services_outlined, title: '医疗 · 狂犬疫苗接种', subtitle: '2025-06-12 · 瑞派宠物医院', status: '已完成', ), const _TimelineTile( icon: Icons.restaurant_outlined, title: '喂养 · 更换幼犬粮', subtitle: '2025-05-20 · 体重增长稳定', status: '已记录', ), ], ); } } class _StatCard extends StatelessWidget { const _StatCard({ required this.icon, required this.color, required this.value, required this.label, this.onTap, }); final IconData icon; final Color color; final String value; final String label; final VoidCallback? onTap; @override Widget build(BuildContext context) { return Card( child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(24), child: Padding( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8), child: Column( children: [ Icon(icon, color: color), const SizedBox(height: 6), Text(value, style: const TextStyle(fontWeight: FontWeight.w800)), const SizedBox(height: 2), Text(label, style: Theme.of(context).textTheme.bodySmall), ], ), ), ), ); } } class _TimelineTile extends StatelessWidget { const _TimelineTile({ required this.icon, required this.title, required this.subtitle, required this.status, }); final IconData icon; final String title; final String subtitle; final String status; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 10), child: SectionCard( child: Row( children: [ CircleAvatar(child: Icon(icon, color: AppColors.primary, size: 20)), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle(fontWeight: FontWeight.w800), ), const SizedBox(height: 3), Text(subtitle, style: Theme.of(context).textTheme.bodySmall), ], ), ), TagPill(status, color: AppColors.success), ], ), ), ); } } class EditPetSheet extends StatefulWidget { const EditPetSheet({required this.pet, super.key}); final PetProfile pet; @override State createState() => _EditPetSheetState(); } class _EditPetSheetState extends State { late final TextEditingController nameController; late final TextEditingController weightController; late String breed; late PetGender gender; late DateTime birthday; late String avatar; static const breeds = ['柴犬', '金毛寻回犬', '柯基', '哈士奇', '英国短毛猫', '其他']; static const avatars = [ petAvatar, 'https://images.unsplash.com/photo-1517849845537-4d257902454a?auto=format&fit=crop&w=600&q=85', 'https://images.unsplash.com/photo-1543466835-00a7907e9de1?auto=format&fit=crop&w=600&q=85', ]; @override void initState() { super.initState(); nameController = TextEditingController(text: widget.pet.name); weightController = TextEditingController(text: '${widget.pet.weight}'); breed = breeds.contains(widget.pet.breed) ? widget.pet.breed : '其他'; gender = widget.pet.gender; birthday = DateTime.tryParse(widget.pet.birthday) ?? DateTime(2024, 5, 15); avatar = widget.pet.avatarUrl; } @override void dispose() { nameController.dispose(); weightController.dispose(); super.dispose(); } String dateText(DateTime value) { return '${value.year}-${value.month.toString().padLeft(2, '0')}-${value.day.toString().padLeft(2, '0')}'; } @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.fromLTRB( 20, 10, 20, MediaQuery.viewInsetsOf(context).bottom + 20, ), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const _SheetHandle(), Row( children: [ Text('编辑宠物资料', style: Theme.of(context).textTheme.titleLarge), const Spacer(), IconButton( onPressed: () => Navigator.pop(context), icon: const Icon(Icons.close), ), ], ), const SizedBox(height: 14), Center( child: InkWell( onTap: () { final index = avatars.indexOf(avatar); setState( () => avatar = avatars[(index + 1) % avatars.length], ); }, borderRadius: BorderRadius.circular(48), child: Stack( children: [ RemoteImage( url: avatar, width: 96, height: 96, borderRadius: BorderRadius.circular(48), ), const Positioned( right: 0, bottom: 0, child: CircleAvatar( radius: 15, child: Icon(Icons.edit, size: 15), ), ), ], ), ), ), const SizedBox(height: 18), TextField( controller: nameController, decoration: const InputDecoration(labelText: '宠物昵称'), ), const SizedBox(height: 12), DropdownButtonFormField( initialValue: breed, decoration: const InputDecoration(labelText: '品种'), items: breeds .map( (value) => DropdownMenuItem(value: value, child: Text(value)), ) .toList(), onChanged: (value) { if (value != null) setState(() => breed = value); }, ), const SizedBox(height: 12), SegmentedButton( segments: const [ ButtonSegment( value: PetGender.male, icon: Icon(Icons.male), label: Text('男孩'), ), ButtonSegment( value: PetGender.female, icon: Icon(Icons.female), label: Text('女孩'), ), ], selected: {gender}, onSelectionChanged: (value) => setState(() => gender = value.first), ), const SizedBox(height: 12), ListTile( shape: RoundedRectangleBorder( side: const BorderSide(color: AppColors.border), borderRadius: BorderRadius.circular(18), ), leading: const Icon(Icons.cake_outlined), title: const Text('生日'), subtitle: Text(dateText(birthday)), trailing: const Icon(Icons.calendar_month_outlined), onTap: () async { final value = await showDatePicker( context: context, initialDate: birthday, firstDate: DateTime(2000), lastDate: DateTime.now(), ); if (value != null) setState(() => birthday = value); }, ), const SizedBox(height: 12), TextField( controller: weightController, keyboardType: const TextInputType.numberWithOptions( decimal: true, ), decoration: const InputDecoration(labelText: '体重(kg)'), ), const SizedBox(height: 20), FilledButton.icon( style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(50), ), onPressed: () { final name = nameController.text.trim(); final weight = double.tryParse(weightController.text); if (name.isEmpty || weight == null || weight <= 0) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('请填写有效的昵称和体重'))); return; } Navigator.pop( context, widget.pet.copyWith( name: name, breed: breed, gender: gender, birthday: dateText(birthday), weight: weight, avatarUrl: avatar, ), ); }, icon: const Icon(Icons.save_outlined), label: const Text('保存修改'), ), ], ), ), ); } } class VaccineSheet extends StatefulWidget { const VaccineSheet({required this.record, super.key}); final VaccineRecord record; @override State createState() => _VaccineSheetState(); } class _VaccineSheetState extends State { late List items; late int totalDoses; late final TextEditingController reminderController; late final TextEditingController dateController; int get completed => items.where((item) => item.status == VaccineStatus.completed).length; @override void initState() { super.initState(); items = List.from(widget.record.items); totalDoses = widget.record.totalDoses; reminderController = TextEditingController( text: widget.record.reminderVaccine, ); dateController = TextEditingController(text: widget.record.reminderDate); } @override void dispose() { reminderController.dispose(); dateController.dispose(); super.dispose(); } void changeTotal(int delta) { final minimum = completed > items.length ? completed : items.length; setState(() => totalDoses = (totalDoses + delta).clamp(minimum, 20)); } @override Widget build(BuildContext context) { final progress = totalDoses == 0 ? 0.0 : completed / totalDoses; return Padding( padding: EdgeInsets.fromLTRB( 20, 10, 20, MediaQuery.viewInsetsOf(context).bottom + 20, ), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const _SheetHandle(), Row( children: [ const CircleAvatar(child: Icon(Icons.vaccines_outlined)), const SizedBox(width: 10), Text('疫苗接种管理', style: Theme.of(context).textTheme.titleLarge), const Spacer(), IconButton( onPressed: () => Navigator.pop(context), icon: const Icon(Icons.close), ), ], ), const SizedBox(height: 16), SectionCard( child: Column( children: [ Row( children: [ const Text('已接种 / 总规划'), const Spacer(), IconButton.filledTonal( onPressed: () => changeTotal(-1), icon: const Icon(Icons.remove), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Text( '$completed / $totalDoses', style: const TextStyle(fontWeight: FontWeight.w800), ), ), IconButton.filled( onPressed: () => changeTotal(1), icon: const Icon(Icons.add), ), ], ), const SizedBox(height: 12), LinearProgressIndicator(value: progress, minHeight: 9), ], ), ), const SizedBox(height: 16), Text('接种详情', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), ...items.asMap().entries.map((entry) { final index = entry.key; final item = entry.value; final done = item.status == VaccineStatus.completed; return Card( margin: const EdgeInsets.only(bottom: 8), child: CheckboxListTile( value: done, title: Text(item.name), subtitle: Text( done ? '接种时间:${item.date}' : '计划接种:${item.date}', ), secondary: Icon( done ? Icons.check_circle : Icons.radio_button_unchecked, color: done ? AppColors.success : AppColors.muted, ), onChanged: (_) { final today = DateTime.now(); final date = '${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}'; setState(() { items[index] = item.copyWith( status: done ? VaccineStatus.pending : VaccineStatus.completed, date: done ? '待定' : date, ); }); }, ), ); }), const SizedBox(height: 10), TextField( controller: reminderController, decoration: const InputDecoration(labelText: '下一针提醒'), ), const SizedBox(height: 12), TextField( controller: dateController, decoration: const InputDecoration(labelText: '预计日期(YYYY-MM-DD)'), ), const SizedBox(height: 20), FilledButton.icon( style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(50), ), onPressed: () => Navigator.pop( context, VaccineRecord( completedDoses: completed, totalDoses: totalDoses, items: items, reminderVaccine: reminderController.text.trim(), reminderDate: dateController.text.trim(), ), ), icon: const Icon(Icons.save_outlined), label: const Text('保存疫苗记录'), ), ], ), ), ); } } class _SheetHandle extends StatelessWidget { const _SheetHandle(); @override Widget build(BuildContext context) { return Center( child: Container( width: 44, height: 5, margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: AppColors.border, borderRadius: BorderRadius.circular(99), ), ), ); } }