Flutter 框架跨平台鸿蒙开发 - 宠物驱虫记录器应用开发教程
Flutter宠物驱虫记录器应用开发教程
项目简介
宠物驱虫记录器是一款专为宠物主人设计的健康管理应用,帮助用户系统化管理宠物的驱虫记录、制定驱虫计划、设置提醒通知,并提供健康监测功能。应用集成了宠物信息管理、驱虫记录追踪、智能提醒系统、健康状态监测等功能,为宠物主人提供全方位的驱虫管理解决方案。
运行效果图




核心功能特性
- 宠物档案管理:完整的宠物信息录入,包含基本信息、健康状况、成长记录
- 驱虫记录追踪:详细记录每次驱虫情况,包含药物信息、剂量、效果评价
- 智能驱虫计划:制定个性化驱虫计划,支持定期、季节性、按需等多种模式
- 提醒通知系统:智能提醒驱虫时间,支持多种提醒方式和自定义提醒周期
- 健康状态监测:记录宠物日常健康状况,及时发现异常情况
- 数据统计分析:全面的驱虫数据统计,包含费用分析、效果评估、趋势分析
技术架构
开发环境
- 框架:Flutter 3.x
- 开发语言:Dart
- UI组件:Material Design 3
- 状态管理:StatefulWidget + setState
- 动画效果:AnimationController + Tween
- 数据存储:本地存储 + SharedPreferences
项目结构
lib/
├── main.dart # 应用入口和主要逻辑
├── models/ # 数据模型
│ ├── pet.dart # 宠物模型
│ ├── deworming_record.dart # 驱虫记录模型
│ ├── deworming_plan.dart # 驱虫计划模型
│ ├── reminder.dart # 提醒模型
│ ├── health_monitoring.dart # 健康监测模型
│ └── stats.dart # 统计数据模型
├── pages/ # 页面组件
│ ├── records_page.dart # 驱虫记录页面
│ ├── pets_page.dart # 宠物管理页面
│ ├── plans_page.dart # 驱虫计划页面
│ ├── reminders_page.dart # 提醒页面
│ └── stats_page.dart # 统计分析页面
└── widgets/ # 自定义组件
├── pet_card.dart # 宠物卡片组件
├── record_card.dart # 记录卡片组件
└── stats_chart.dart # 统计图表组件
数据模型设计
宠物信息模型(Pet)
宠物模型是应用的核心数据结构,记录每只宠物的详细信息:
class Pet {
final String id; // 宠物唯一标识
final String name; // 宠物名称
final String species; // 物种:猫、狗、兔子等
final String breed; // 品种
final String gender; // 性别:公、母
final DateTime birthDate; // 出生日期
final double weight; // 体重(kg)
final String color; // 毛色
final String avatarUrl; // 头像图片路径
final String microchipId; // 芯片号
final String notes; // 备注信息
final DateTime createdDate; // 创建日期
final bool isActive; // 是否活跃状态
}
驱虫记录模型(DewormingRecord)
驱虫记录模型详细记录每次驱虫的完整信息:
class DewormingRecord {
final String id; // 记录唯一标识
final String petId; // 关联宠物ID
final String petName; // 宠物名称
final DateTime treatmentDate; // 驱虫日期
final String medicineType; // 药物类型:内驱、外驱、内外同驱
final String medicineName; // 药物名称
final String medicineBrand; // 药物品牌
final double dosage; // 用药剂量
final String dosageUnit; // 剂量单位:ml、片、滴等
final String administrationMethod; // 给药方式:口服、滴剂、喷剂等
final double petWeight; // 驱虫时宠物体重
final String veterinarian; // 兽医师姓名
final String clinic; // 诊所名称
final double cost; // 费用
final String notes; // 备注信息
final List<String> symptoms; // 驱虫前症状
final List<String> sideEffects; // 副作用记录
final String effectiveness; // 效果评价:优秀、良好、一般、差
final List<String> photos; // 相关照片
final DateTime nextDueDate; // 下次驱虫日期
}
驱虫计划模型(DewormingPlan)
驱虫计划模型用于制定和管理长期驱虫策略:
class DewormingPlan {
final String id; // 计划唯一标识
final String petId; // 关联宠物ID
final String petName; // 宠物名称
final String planType; // 计划类型:定期、季节性、按需
final String medicineType; // 药物类型
final int intervalDays; // 间隔天数
final DateTime startDate; // 开始日期
final DateTime? endDate; // 结束日期(可选)
final bool isActive; // 是否激活
final String reminderTime; // 提醒时间
final List<String> reminderDays; // 提醒天数(提前几天)
final String notes; // 备注信息
final DateTime createdDate; // 创建日期
}
提醒模型(DewormingReminder)
提醒模型管理所有驱虫相关的提醒通知:
class DewormingReminder {
final String id; // 提醒唯一标识
final String petId; // 关联宠物ID
final String petName; // 宠物名称
final DateTime reminderDate; // 提醒日期
final String reminderType; // 提醒类型:驱虫到期、计划提醒
final String medicineType; // 药物类型
final String title; // 提醒标题
final String message; // 提醒内容
final bool isCompleted; // 是否已完成
final DateTime createdDate; // 创建日期
}
健康监测模型(HealthMonitoring)
健康监测模型记录宠物的日常健康状况:
class HealthMonitoring {
final String id; // 记录唯一标识
final String petId; // 关联宠物ID
final String petName; // 宠物名称
final DateTime checkDate; // 检查日期
final double weight; // 体重
final String appetite; // 食欲状况:正常、减退、亢进
final String energy; // 精神状态:活跃、正常、萎靡
final String stoolCondition; // 大便状况:正常、稀软、腹泻、便秘
final bool hasVomiting; // 是否呕吐
final bool hasItching; // 是否瘙痒
final String skinCondition; // 皮肤状况:正常、红疹、脱毛、干燥
final List<String> symptoms; // 其他症状
final String notes; // 备注信息
final List<String> photos; // 相关照片
}
统计数据模型(DewormingStats)
统计模型提供全面的数据分析功能:
class DewormingStats {
final int totalPets; // 宠物总数
final int totalRecords; // 驱虫记录总数
final int activePlans; // 活跃计划数
final int pendingReminders; // 待处理提醒数
final double totalCost; // 总费用
final Map<String, int> medicineTypeDistribution; // 药物类型分布
final Map<String, int> monthlyTreatments; // 月度治疗次数
final Map<String, double> costByMonth; // 月度费用统计
final String mostUsedMedicine; // 最常用药物
final String mostFrequentClinic; // 最常去诊所
}
应用主界面设计
主页面结构
应用采用底部导航栏设计,包含五个主要功能模块:
class PetDewormingHomePage extends StatefulWidget {
const PetDewormingHomePage({super.key});
State<PetDewormingHomePage> createState() => _PetDewormingHomePageState();
}
class _PetDewormingHomePageState extends State<PetDewormingHomePage>
with TickerProviderStateMixin {
int _selectedIndex = 0;
// 数据存储
List<Pet> _pets = [];
List<DewormingRecord> _dewormingRecords = [];
List<DewormingPlan> _dewormingPlans = [];
List<DewormingReminder> _reminders = [];
List<HealthMonitoring> _healthRecords = [];
DewormingStats? _stats;
// 筛选和搜索
String _searchQuery = '';
String? _selectedPet;
String? _selectedMedicineType;
DateTime? _selectedDateRange;
// 动画控制器
late AnimationController _fadeAnimationController;
late Animation<double> _fadeAnimation;
late AnimationController _slideAnimationController;
late Animation<Offset> _slideAnimation;
}
底部导航栏设计
底部导航栏提供五个主要功能入口:
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() => _selectedIndex = index);
},
destinations: const [
NavigationDestination(
icon: Icon(Icons.medical_services),
label: '驱虫记录',
),
NavigationDestination(
icon: Icon(Icons.pets),
label: '我的宠物',
),
NavigationDestination(
icon: Icon(Icons.schedule),
label: '驱虫计划',
),
NavigationDestination(
icon: Icon(Icons.notifications),
label: '提醒',
),
NavigationDestination(
icon: Icon(Icons.analytics),
label: '统计',
),
],
)
动画效果实现
应用使用多种动画效果提升用户体验:
void _setupAnimations() {
_fadeAnimationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeAnimationController,
curve: Curves.easeInOut,
));
_slideAnimationController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideAnimationController,
curve: Curves.elasticOut,
));
_fadeAnimationController.forward();
_slideAnimationController.forward();
}
驱虫记录功能实现
驱虫记录页面
驱虫记录页面是应用的核心功能,展示所有驱虫记录:
Widget _buildRecordsPage() {
final filteredRecords = _getFilteredRecords();
return Column(
children: [
// 快速统计卡片
if (_stats != null) _buildQuickStatsCard(),
// 筛选标签显示
if (_searchQuery.isNotEmpty ||
_selectedPet != null ||
_selectedMedicineType != null)
Container(
padding: const EdgeInsets.all(16),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
if (_searchQuery.isNotEmpty)
Chip(
label: Text('搜索: $_searchQuery'),
onDeleted: () {
setState(() => _searchQuery = '');
},
),
if (_selectedPet != null)
Chip(
label: Text('宠物: $_selectedPet'),
onDeleted: () {
setState(() => _selectedPet = null);
},
),
if (_selectedMedicineType != null)
Chip(
label: Text('类型: $_selectedMedicineType'),
onDeleted: () {
setState(() => _selectedMedicineType = null);
},
),
],
),
),
// 驱虫记录列表
Expanded(
child: filteredRecords.isEmpty
? _buildEmptyState()
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: filteredRecords.length,
itemBuilder: (context, index) {
final record = filteredRecords[index];
return _buildRecordCard(record);
},
),
),
],
);
}
快速统计卡片
快速统计卡片显示关键指标概览:
Widget _buildQuickStatsCard() {
final stats = _stats!;
return Container(
margin: const EdgeInsets.all(16),
child: Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.dashboard,
color: Colors.teal.shade600,
size: 24,
),
const SizedBox(width: 8),
Text(
'驱虫概览',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.teal.shade700,
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildStatItem(
Icons.pets, '宠物数量', '${stats.totalPets}只', Colors.teal,
),
),
Expanded(
child: _buildStatItem(
Icons.medical_services, '驱虫记录', '${stats.totalRecords}次', Colors.blue,
),
),
Expanded(
child: _buildStatItem(
Icons.schedule, '活跃计划', '${stats.activePlans}个', Colors.green,
),
),
Expanded(
child: _buildStatItem(
Icons.notifications, '待处理', '${stats.pendingReminders}个', Colors.orange,
),
),
],
),
],
),
),
),
);
}
驱虫记录卡片设计
每条驱虫记录以卡片形式展示详细信息:
Widget _buildRecordCard(DewormingRecord record) {
final daysSinceLastTreatment = DateTime.now().difference(record.treatmentDate).inDays;
final daysUntilNext = record.nextDueDate.difference(DateTime.now()).inDays;
final isOverdue = daysUntilNext < 0;
return Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
onTap: () => _showRecordDetail(record),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题行
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: _getMedicineTypeColor(record.medicineType).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
_getMedicineTypeIcon(record.medicineType),
color: _getMedicineTypeColor(record.medicineType),
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
record.petName,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
'${record.medicineType} • ${record.medicineName}',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getMedicineTypeColor(record.medicineType),
borderRadius: BorderRadius.circular(12),
),
child: Text(
record.medicineType,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 4),
Text(
_formatDate(record.treatmentDate),
style: TextStyle(
color: Colors.grey.shade500,
fontSize: 10,
),
),
],
),
],
),
const SizedBox(height: 12),
// 药物信息
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.teal.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.teal.withValues(alpha: 0.3)),
),
child: Row(
children: [
Icon(
Icons.medication,
color: Colors.teal.shade600,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'${record.medicineBrand} ${record.dosage}${record.dosageUnit}',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.teal.shade700,
),
),
),
Text(
'¥${record.cost.toStringAsFixed(0)}',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.teal.shade600,
),
),
],
),
),
const SizedBox(height: 12),
// 下次驱虫信息
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'下次驱虫',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 4),
Row(
children: [
Icon(
isOverdue ? Icons.warning : Icons.schedule,
size: 16,
color: isOverdue ? Colors.red : Colors.green,
),
const SizedBox(width: 4),
Text(
isOverdue
? '已逾期 ${(-daysUntilNext)}天'
: '还有 ${daysUntilNext}天',
style: TextStyle(
fontSize: 12,
color: isOverdue ? Colors.red : Colors.green,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getEffectivenessColor(record.effectiveness).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _getEffectivenessColor(record.effectiveness).withValues(alpha: 0.3),
),
),
child: Text(
'效果: ${record.effectiveness}',
style: TextStyle(
fontSize: 10,
color: _getEffectivenessColor(record.effectiveness),
fontWeight: FontWeight.bold,
),
),
),
],
),
// 诊所信息
if (record.clinic.isNotEmpty) ...[
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.local_hospital,
size: 14,
color: Colors.grey.shade600,
),
const SizedBox(width: 4),
Text(
'${record.clinic} • ${record.veterinarian}',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 11,
),
),
],
),
],
],
),
),
),
);
}
宠物管理功能
宠物列表页面
宠物页面展示所有宠物的基本信息和驱虫状态:
Widget _buildPetsPage() {
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _pets.length,
itemBuilder: (context, index) {
final pet = _pets[index];
return _buildPetCard(pet);
},
);
}
宠物卡片设计
每只宠物以卡片形式展示详细信息:
Widget _buildPetCard(Pet pet) {
final age = DateTime.now().difference(pet.birthDate).inDays ~/ 365;
final petRecords = _dewormingRecords.where((r) => r.petId == pet.id).toList();
final lastRecord = petRecords.isNotEmpty
? petRecords.reduce((a, b) => a.treatmentDate.isAfter(b.treatmentDate) ? a : b)
: null;
return Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
onTap: () => _showPetDetail(pet),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 宠物基本信息
Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: _getSpeciesColor(pet.species).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(30),
),
child: Icon(
_getSpeciesIcon(pet.species),
color: _getSpeciesColor(pet.species),
size: 30,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
pet.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'${pet.species} • ${pet.breed}',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
),
),
Text(
'${age}岁 • ${pet.gender} • ${pet.weight}kg',
style: TextStyle(
color: Colors.grey.shade500,
fontSize: 12,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: pet.isActive ? Colors.green : Colors.grey,
borderRadius: BorderRadius.circular(12),
),
child: Text(
pet.isActive ? '活跃' : '非活跃',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 16),
// 驱虫状态
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'驱虫状态',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildStatItem(
Icons.medical_services,
'总次数',
'${petRecords.length}次',
Colors.teal,
),
),
Expanded(
child: _buildStatItem(
Icons.attach_money,
'总费用',
'¥${petRecords.fold(0.0, (sum, r) => sum + r.cost).toStringAsFixed(0)}',
Colors.green,
),
),
Expanded(
child: _buildStatItem(
Icons.schedule,
'上次驱虫',
lastRecord != null
? '${DateTime.now().difference(lastRecord.treatmentDate).inDays}天前'
: '无记录',
Colors.blue,
),
),
],
),
],
),
),
// 备注信息
if (pet.notes.isNotEmpty) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.note,
size: 16,
color: Colors.blue.shade600,
),
const SizedBox(width: 8),
Expanded(
child: Text(
pet.notes,
style: TextStyle(
color: Colors.blue.shade700,
fontSize: 12,
),
),
),
],
),
),
],
],
),
),
),
);
}
添加宠物对话框
添加新宠物的对话框界面:
void _showAddPetDialog() {
final nameController = TextEditingController();
final breedController = TextEditingController();
final weightController = TextEditingController();
final colorController = TextEditingController();
final notesController = TextEditingController();
String selectedSpecies = '猫';
String selectedGender = '公';
DateTime selectedBirthDate = DateTime.now().subtract(const Duration(days: 365));
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('添加新宠物'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: '宠物名称',
hintText: '请输入宠物名称',
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: DropdownButtonFormField<String>(
value: selectedSpecies,
decoration: const InputDecoration(labelText: '物种'),
items: ['猫', '狗', '兔子', '仓鼠', '鸟类'].map((species) {
return DropdownMenuItem(
value: species,
child: Text(species),
);
}).toList(),
onChanged: (value) {
selectedSpecies = value!;
},
),
),
const SizedBox(width: 16),
Expanded(
child: DropdownButtonFormField<String>(
value: selectedGender,
decoration: const InputDecoration(labelText: '性别'),
items: ['公', '母'].map((gender) {
return DropdownMenuItem(
value: gender,
child: Text(gender),
);
}).toList(),
onChanged: (value) {
selectedGender = value!;
},
),
),
],
),
const SizedBox(height: 16),
TextField(
controller: breedController,
decoration: const InputDecoration(
labelText: '品种',
hintText: '请输入品种',
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextField(
controller: weightController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: '体重',
hintText: '0.0',
suffixText: 'kg',
),
),
),
const SizedBox(width: 16),
Expanded(
child: TextField(
controller: colorController,
decoration: const InputDecoration(
labelText: '毛色',
hintText: '请输入毛色',
),
),
),
],
),
const SizedBox(height: 16),
TextField(
controller: notesController,
maxLines: 3,
decoration: const InputDecoration(
labelText: '备注',
hintText: '请输入备注信息',
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () {
if (nameController.text.isNotEmpty) {
final newPet = Pet(
id: 'pet${_pets.length + 1}',
name: nameController.text,
species: selectedSpecies,
breed: breedController.text.isNotEmpty ? breedController.text : '未知品种',
gender: selectedGender,
birthDate: selectedBirthDate,
weight: double.tryParse(weightController.text) ?? 0.0,
color: colorController.text.isNotEmpty ? colorController.text : '未知',
avatarUrl: '',
microchipId: '',
notes: notesController.text,
createdDate: DateTime.now(),
isActive: true,
);
setState(() {
_pets.add(newPet);
_calculateStats();
});
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('宠物添加成功!')),
);
}
},
child: const Text('添加'),
),
],
),
);
}
驱虫计划功能
驱虫计划页面
驱虫计划页面展示所有制定的驱虫计划:
Widget _buildPlansPage() {
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _dewormingPlans.length,
itemBuilder: (context, index) {
final plan = _dewormingPlans[index];
return _buildPlanCard(plan);
},
);
}
驱虫计划卡片设计
每个驱虫计划以卡片形式展示:
Widget _buildPlanCard(DewormingPlan plan) {
final nextDueDate = _calculateNextDueDate(plan);
final daysUntilNext = nextDueDate?.difference(DateTime.now()).inDays ?? 0;
return Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
onTap: () => _showPlanDetail(plan),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题行
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: plan.isActive
? Colors.green.withValues(alpha: 0.1)
: Colors.grey.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
plan.isActive ? Icons.schedule : Icons.pause_circle,
color: plan.isActive ? Colors.green : Colors.grey,
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
plan.petName,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
'${plan.planType} • ${plan.medicineType}',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: plan.isActive ? Colors.green : Colors.grey,
borderRadius: BorderRadius.circular(12),
),
child: Text(
plan.isActive ? '活跃' : '暂停',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 4),
Text(
'每${plan.intervalDays}天',
style: TextStyle(
color: Colors.grey.shade500,
fontSize: 10,
),
),
],
),
],
),
const SizedBox(height: 16),
// 计划详情
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'开始日期',
style: TextStyle(
fontSize: 10,
color: Colors.grey.shade600,
),
),
Text(
_formatDate(plan.startDate),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
),
if (plan.endDate != null)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'结束日期',
style: TextStyle(
fontSize: 10,
color: Colors.grey.shade600,
),
),
Text(
_formatDate(plan.endDate!),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'提醒时间',
style: TextStyle(
fontSize: 10,
color: Colors.grey.shade600,
),
),
Text(
plan.reminderTime,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
if (nextDueDate != null) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: daysUntilNext <= 7
? Colors.orange.withValues(alpha: 0.1)
: Colors.blue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: daysUntilNext <= 7
? Colors.orange.withValues(alpha: 0.3)
: Colors.blue.withValues(alpha: 0.3),
),
),
child: Row(
children: [
Icon(
daysUntilNext <= 7 ? Icons.warning : Icons.schedule,
size: 16,
color: daysUntilNext <= 7 ? Colors.orange : Colors.blue,
),
const SizedBox(width: 8),
Text(
'下次驱虫: ${_formatDate(nextDueDate)} (${daysUntilNext}天后)',
style: TextStyle(
fontSize: 11,
color: daysUntilNext <= 7 ? Colors.orange.shade700 : Colors.blue.shade700,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
],
),
),
// 备注
if (plan.notes.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
plan.notes,
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 12,
),
),
],
],
),
),
),
);
}
提醒通知功能
提醒页面
提醒页面展示所有待处理的驱虫提醒:
Widget _buildRemindersPage() {
final sortedReminders = _reminders
.where((reminder) => !reminder.isCompleted)
.toList()
..sort((a, b) => a.reminderDate.compareTo(b.reminderDate));
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: sortedReminders.length,
itemBuilder: (context, index) {
final reminder = sortedReminders[index];
return _buildReminderCard(reminder);
},
);
}
提醒卡片设计
每个提醒以卡片形式展示,根据紧急程度使用不同颜色:
Widget _buildReminderCard(DewormingReminder reminder) {
final daysUntilReminder = reminder.reminderDate.difference(DateTime.now()).inDays;
final isOverdue = daysUntilReminder < 0;
final isToday = daysUntilReminder == 0;
return Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
onTap: () => _showReminderDetail(reminder),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题行
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isOverdue
? Colors.red.withValues(alpha: 0.1)
: isToday
? Colors.orange.withValues(alpha: 0.1)
: Colors.blue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
isOverdue
? Icons.warning
: isToday
? Icons.today
: Icons.schedule,
color: isOverdue
? Colors.red
: isToday
? Colors.orange
: Colors.blue,
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
reminder.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
'${reminder.petName} • ${reminder.medicineType}',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isOverdue
? Colors.red
: isToday
? Colors.orange
: Colors.blue,
borderRadius: BorderRadius.circular(12),
),
child: Text(
isOverdue
? '已逾期'
: isToday
? '今天'
: '${daysUntilReminder}天后',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 4),
Text(
_formatDate(reminder.reminderDate),
style: TextStyle(
color: Colors.grey.shade500,
fontSize: 10,
),
),
],
),
],
),
const SizedBox(height: 12),
// 提醒内容
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Text(
reminder.message,
style: TextStyle(
color: Colors.grey.shade700,
fontSize: 13,
),
),
),
const SizedBox(height: 12),
// 操作按钮
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => _postponeReminder(reminder),
child: const Text('推迟'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => _completeReminder(reminder),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.teal,
foregroundColor: Colors.white,
),
child: const Text('完成'),
),
],
),
],
),
),
),
);
}
提醒操作功能
提醒的推迟和完成操作:
void _postponeReminder(DewormingReminder reminder) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('推迟提醒'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Text('推迟1天'),
onTap: () {
_updateReminderDate(reminder, 1);
Navigator.of(context).pop();
},
),
ListTile(
title: const Text('推迟3天'),
onTap: () {
_updateReminderDate(reminder, 3);
Navigator.of(context).pop();
},
),
ListTile(
title: const Text('推迟1周'),
onTap: () {
_updateReminderDate(reminder, 7);
Navigator.of(context).pop();
},
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
],
),
);
}
void _updateReminderDate(DewormingReminder reminder, int days) {
setState(() {
final index = _reminders.indexWhere((r) => r.id == reminder.id);
if (index != -1) {
_reminders[index] = DewormingReminder(
id: reminder.id,
petId: reminder.petId,
petName: reminder.petName,
reminderDate: reminder.reminderDate.add(Duration(days: days)),
reminderType: reminder.reminderType,
medicineType: reminder.medicineType,
title: reminder.title,
message: reminder.message,
isCompleted: reminder.isCompleted,
createdDate: reminder.createdDate,
);
}
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('提醒已推迟${days}天')),
);
}
void _completeReminder(DewormingReminder reminder) {
setState(() {
final index = _reminders.indexWhere((r) => r.id == reminder.id);
if (index != -1) {
_reminders[index] = DewormingReminder(
id: reminder.id,
petId: reminder.petId,
petName: reminder.petName,
reminderDate: reminder.reminderDate,
reminderType: reminder.reminderType,
medicineType: reminder.medicineType,
title: reminder.title,
message: reminder.message,
isCompleted: true,
createdDate: reminder.createdDate,
);
}
_calculateStats();
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('提醒已完成')),
);
}
统计分析功能
统计页面结构
统计页面提供全面的数据分析功能:
Widget _buildStatsPage() {
if (_stats == null) {
return const Center(child: CircularProgressIndicator());
}
final stats = _stats!;
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 总体统计卡片
_buildOverallStatsCard(),
const SizedBox(height: 20),
// 药物类型分布
Text(
'药物类型分布',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
_buildMedicineTypeDistributionCard(),
const SizedBox(height: 24),
// 月度趋势
Text(
'月度驱虫趋势',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
_buildMonthlyTrendCard(),
const SizedBox(height: 24),
// 费用分析
Text(
'费用分析',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
_buildCostAnalysisCard(),
],
),
);
}
总体统计卡片
总体统计展示关键指标:
Widget _buildOverallStatsCard() {
final stats = _stats!;
return Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.analytics,
color: Colors.teal.shade600,
size: 28,
),
const SizedBox(width: 12),
Text(
'驱虫统计总览',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.teal.shade700,
),
),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: _buildStatItem(
Icons.pets,
'宠物总数',
'${stats.totalPets}只',
Colors.teal,
),
),
Expanded(
child: _buildStatItem(
Icons.medical_services,
'驱虫记录',
'${stats.totalRecords}次',
Colors.blue,
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildStatItem(
Icons.schedule,
'活跃计划',
'${stats.activePlans}个',
Colors.green,
),
),
Expanded(
child: _buildStatItem(
Icons.notifications,
'待处理提醒',
'${stats.pendingReminders}个',
Colors.orange,
),
),
],
),
const SizedBox(height: 16),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.green.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.green.withValues(alpha: 0.3)),
),
child: Column(
children: [
Icon(
Icons.attach_money,
color: Colors.green.shade600,
size: 32,
),
const SizedBox(height: 8),
Text(
'总费用',
style: TextStyle(
fontSize: 14,
color: Colors.green.shade600,
),
),
Text(
'¥${stats.totalCost.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.green.shade700,
),
),
],
),
),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'偏好分析',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey.shade700,
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'最常用药物',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 4),
Text(
stats.mostUsedMedicine,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'最常去诊所',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 4),
Text(
stats.mostFrequentClinic,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
],
),
),
],
),
),
);
}
药物类型分布统计
药物类型分布以进度条形式展示:
Widget _buildMedicineTypeDistributionCard() {
final stats = _stats!;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: stats.medicineTypeDistribution.entries.map((entry) {
final total = stats.medicineTypeDistribution.values.fold(0, (sum, value) => sum + value);
final percentage = total > 0 ? entry.value / total : 0.0;
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
_getMedicineTypeIcon(entry.key),
color: _getMedicineTypeColor(entry.key),
size: 16,
),
const SizedBox(width: 8),
Text(entry.key),
],
),
Text(
'${entry.value}次 (${(percentage * 100).toStringAsFixed(1)}%)',
style: TextStyle(
color: _getMedicineTypeColor(entry.key),
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 4),
LinearProgressIndicator(
value: percentage,
backgroundColor: Colors.grey.shade300,
valueColor: AlwaysStoppedAnimation<Color>(
_getMedicineTypeColor(entry.key),
),
),
],
),
);
}).toList(),
),
),
);
}
月度趋势分析
月度驱虫趋势展示:
Widget _buildMonthlyTrendCard() {
final stats = _stats!;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'最近6个月驱虫次数',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.grey.shade700,
),
),
const SizedBox(height: 16),
SizedBox(
height: 200,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: stats.monthlyTreatments.length,
itemBuilder: (context, index) {
final entry = stats.monthlyTreatments.entries.elementAt(index);
final maxValue = stats.monthlyTreatments.values.isNotEmpty
? stats.monthlyTreatments.values.reduce((a, b) => a > b ? a : b)
: 1;
final height = (entry.value / maxValue) * 150;
return Container(
width: 60,
margin: const EdgeInsets.only(right: 8),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
'${entry.value}',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Container(
width: 40,
height: height,
decoration: BoxDecoration(
color: Colors.teal,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 8),
Text(
entry.key.substring(5),
style: TextStyle(
fontSize: 10,
color: Colors.grey.shade600,
),
),
],
),
);
},
),
),
],
),
),
);
}
工具函数和辅助方法
颜色和图标映射
应用使用多种颜色和图标来区分不同的药物类型和宠物种类:
Color _getMedicineTypeColor(String medicineType) {
switch (medicineType) {
case '内驱':
return Colors.blue;
case '外驱':
return Colors.green;
case '内外同驱':
return Colors.purple;
default:
return Colors.grey;
}
}
IconData _getMedicineTypeIcon(String medicineType) {
switch (medicineType) {
case '内驱':
return Icons.medication;
case '外驱':
return Icons.pest_control;
case '内外同驱':
return Icons.medical_services;
default:
return Icons.healing;
}
}
Color _getSpeciesColor(String species) {
switch (species) {
case '猫':
return Colors.orange;
case '狗':
return Colors.brown;
case '兔子':
return Colors.pink;
case '仓鼠':
return Colors.amber;
case '鸟类':
return Colors.cyan;
default:
return Colors.grey;
}
}
IconData _getSpeciesIcon(String species) {
switch (species) {
case '猫':
return Icons.pets;
case '狗':
return Icons.pets;
case '兔子':
return Icons.cruelty_free;
case '仓鼠':
return Icons.pets;
case '鸟类':
return Icons.flutter_dash;
default:
return Icons.pets;
}
}
Color _getEffectivenessColor(String effectiveness) {
switch (effectiveness) {
case '优秀':
return Colors.green;
case '良好':
return Colors.blue;
case '一般':
return Colors.orange;
case '差':
return Colors.red;
default:
return Colors.grey;
}
}
时间格式化
时间显示格式化函数:
String _formatDate(DateTime dateTime) {
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')}';
}
String _formatDateTime(DateTime dateTime) {
return '${_formatDate(dateTime)} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
}
统计项组件
统计项组件用于显示各种数值指标:
Widget _buildStatItem(IconData icon, String label, String value, Color color) {
return Column(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 10,
color: Colors.grey.shade600,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 2),
Text(
value,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: color,
),
textAlign: TextAlign.center,
),
],
);
}
筛选和搜索功能
筛选记录的核心逻辑:
List<DewormingRecord> _getFilteredRecords() {
return _dewormingRecords.where((record) {
// 搜索过滤
if (_searchQuery.isNotEmpty) {
final query = _searchQuery.toLowerCase();
if (!record.petName.toLowerCase().contains(query) &&
!record.medicineName.toLowerCase().contains(query) &&
!record.medicineBrand.toLowerCase().contains(query) &&
!record.clinic.toLowerCase().contains(query) &&
!record.veterinarian.toLowerCase().contains(query)) {
return false;
}
}
// 宠物过滤
if (_selectedPet != null && record.petName != _selectedPet) {
return false;
}
// 药物类型过滤
if (_selectedMedicineType != null && record.medicineType != _selectedMedicineType) {
return false;
}
return true;
}).toList()
..sort((a, b) => b.treatmentDate.compareTo(a.treatmentDate));
}
搜索对话框
搜索功能通过对话框实现:
void _showSearchDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('搜索驱虫记录'),
content: TextField(
autofocus: true,
decoration: const InputDecoration(
hintText: '输入宠物名称、药物名称、品牌或诊所',
prefixIcon: Icon(Icons.search),
),
onChanged: (value) {
_searchQuery = value;
},
onSubmitted: (value) {
Navigator.of(context).pop();
setState(() {});
},
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
setState(() {});
},
child: const Text('搜索'),
),
],
),
);
}
筛选对话框
筛选对话框提供多维度筛选:
void _showFilterDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('筛选驱虫记录'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('宠物:'),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: [
FilterChip(
label: const Text('全部'),
selected: _selectedPet == null,
onSelected: (selected) {
setState(() {
_selectedPet = selected ? null : _selectedPet;
});
},
),
..._pets.map((pet) => FilterChip(
label: Text(pet.name),
selected: _selectedPet == pet.name,
onSelected: (selected) {
setState(() {
_selectedPet = selected ? pet.name : null;
});
},
)),
],
),
const SizedBox(height: 16),
const Text('药物类型:'),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: [
FilterChip(
label: const Text('全部'),
selected: _selectedMedicineType == null,
onSelected: (selected) {
setState(() {
_selectedMedicineType = selected ? null : _selectedMedicineType;
});
},
),
...['内驱', '外驱', '内外同驱'].map((type) => FilterChip(
label: Text(type),
selected: _selectedMedicineType == type,
onSelected: (selected) {
setState(() {
_selectedMedicineType = selected ? type : null;
});
},
)),
],
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
setState(() {});
},
child: const Text('应用'),
),
],
),
);
}
数据初始化和统计计算
数据初始化
应用启动时初始化示例数据:
void _initializeData() {
// 初始化宠物数据
_pets = [
Pet(
id: 'pet001',
name: '小白',
species: '猫',
breed: '英国短毛猫',
gender: '母',
birthDate: DateTime(2022, 3, 15),
weight: 4.2,
color: '银渐层',
avatarUrl: 'assets/pets/cat_white.jpg',
microchipId: 'MC001234567',
notes: '性格温顺,喜欢晒太阳',
createdDate: DateTime.now().subtract(const Duration(days: 365)),
isActive: true,
),
Pet(
id: 'pet002',
name: '旺财',
species: '狗',
breed: '金毛寻回犬',
gender: '公',
birthDate: DateTime(2021, 8, 20),
weight: 28.5,
color: '金黄色',
avatarUrl: 'assets/pets/dog_golden.jpg',
microchipId: 'MC001234568',
notes: '活泼好动,喜欢游泳',
createdDate: DateTime.now().subtract(const Duration(days: 400)),
isActive: true,
),
Pet(
id: 'pet003',
name: '小灰',
species: '兔子',
breed: '荷兰兔',
gender: '公',
birthDate: DateTime(2023, 1, 10),
weight: 1.8,
color: '灰白色',
avatarUrl: 'assets/pets/rabbit_gray.jpg',
microchipId: 'MC001234569',
notes: '胆小安静,喜欢吃胡萝卜',
createdDate: DateTime.now().subtract(const Duration(days: 200)),
isActive: true,
),
];
// 初始化驱虫记录
_dewormingRecords = [
DewormingRecord(
id: 'dr001',
petId: 'pet001',
petName: '小白',
treatmentDate: DateTime.now().subtract(const Duration(days: 30)),
medicineType: '内驱',
medicineName: '拜宠清',
medicineBrand: '拜耳',
dosage: 1.0,
dosageUnit: '片',
administrationMethod: '口服',
petWeight: 4.1,
veterinarian: '张医生',
clinic: '爱宠动物医院',
cost: 45.0,
notes: '驱虫前发现有轻微腹泻症状',
symptoms: ['腹泻', '食欲不振'],
sideEffects: [],
effectiveness: '良好',
photos: ['deworming_001.jpg'],
nextDueDate: DateTime.now().add(const Duration(days: 60)),
),
DewormingRecord(
id: 'dr002',
petId: 'pet002',
petName: '旺财',
treatmentDate: DateTime.now().subtract(const Duration(days: 45)),
medicineType: '外驱',
medicineName: '福来恩',
medicineBrand: '勃林格',
dosage: 2.68,
dosageUnit: 'ml',
administrationMethod: '滴剂',
petWeight: 28.3,
veterinarian: '李医生',
clinic: '宠物之家诊所',
cost: 120.0,
notes: '夏季预防跳蚤和蜱虫',
symptoms: ['瘙痒', '皮肤红疹'],
sideEffects: ['轻微皮肤刺激'],
effectiveness: '优秀',
photos: ['deworming_002.jpg', 'deworming_003.jpg'],
nextDueDate: DateTime.now().add(const Duration(days: 45)),
),
DewormingRecord(
id: 'dr003',
petId: 'pet003',
petName: '小灰',
treatmentDate: DateTime.now().subtract(const Duration(days: 60)),
medicineType: '内驱',
medicineName: '兔用驱虫药',
medicineBrand: '小宠',
dosage: 0.5,
dosageUnit: 'ml',
administrationMethod: '口服',
petWeight: 1.7,
veterinarian: '王医生',
clinic: '小动物专科医院',
cost: 25.0,
notes: '定期预防性驱虫',
symptoms: [],
sideEffects: [],
effectiveness: '良好',
photos: [],
nextDueDate: DateTime.now().add(const Duration(days: 30)),
),
];
// 初始化驱虫计划
_dewormingPlans = [
DewormingPlan(
id: 'dp001',
petId: 'pet001',
petName: '小白',
planType: '定期',
medicineType: '内驱',
intervalDays: 90,
startDate: DateTime.now().subtract(const Duration(days: 30)),
isActive: true,
reminderTime: '09:00',
reminderDays: ['7', '3', '1'],
notes: '每3个月进行一次内驱',
createdDate: DateTime.now().subtract(const Duration(days: 100)),
),
DewormingPlan(
id: 'dp002',
petId: 'pet002',
petName: '旺财',
planType: '季节性',
medicineType: '外驱',
intervalDays: 30,
startDate: DateTime(2024, 4, 1),
endDate: DateTime(2024, 10, 31),
isActive: true,
reminderTime: '18:00',
reminderDays: ['5', '2'],
notes: '夏季防虫计划',
createdDate: DateTime.now().subtract(const Duration(days: 80)),
),
];
// 初始化提醒
_reminders = [
DewormingReminder(
id: 'rm001',
petId: 'pet001',
petName: '小白',
reminderDate: DateTime.now().add(const Duration(days: 3)),
reminderType: '驱虫到期',
medicineType: '内驱',
title: '小白驱虫提醒',
message: '小白的内驱即将到期,请及时安排驱虫',
isCompleted: false,
createdDate: DateTime.now().subtract(const Duration(days: 4)),
),
DewormingReminder(
id: 'rm002',
petId: 'pet002',
petName: '旺财',
reminderDate: DateTime.now().add(const Duration(days: 7)),
reminderType: '计划提醒',
medicineType: '外驱',
title: '旺财外驱计划',
message: '根据夏季防虫计划,旺财需要进行外驱',
isCompleted: false,
createdDate: DateTime.now().subtract(const Duration(days: 2)),
),
];
// 初始化健康监测记录
_healthRecords = [
HealthMonitoring(
id: 'hm001',
petId: 'pet001',
petName: '小白',
checkDate: DateTime.now().subtract(const Duration(days: 1)),
weight: 4.2,
appetite: '正常',
energy: '活跃',
stoolCondition: '正常',
hasVomiting: false,
hasItching: false,
skinCondition: '正常',
symptoms: [],
notes: '整体状况良好',
photos: [],
),
HealthMonitoring(
id: 'hm002',
petId: 'pet002',
petName: '旺财',
checkDate: DateTime.now().subtract(const Duration(days: 2)),
weight: 28.5,
appetite: '正常',
energy: '活跃',
stoolCondition: '正常',
hasVomiting: false,
hasItching: true,
skinCondition: '轻微红疹',
symptoms: ['轻微瘙痒'],
notes: '可能需要检查是否有寄生虫',
photos: ['health_001.jpg'],
),
];
}
统计数据计算
实时计算各种统计指标:
void _calculateStats() {
final totalPets = _pets.where((pet) => pet.isActive).length;
final totalRecords = _dewormingRecords.length;
final activePlans = _dewormingPlans.where((plan) => plan.isActive).length;
final pendingReminders = _reminders.where((reminder) => !reminder.isCompleted).length;
final totalCost = _dewormingRecords.fold(0.0, (sum, record) => sum + record.cost);
// 计算药物类型分布
final medicineTypeDistribution = <String, int>{};
for (final record in _dewormingRecords) {
medicineTypeDistribution[record.medicineType] =
(medicineTypeDistribution[record.medicineType] ?? 0) + 1;
}
// 计算月度治疗次数
final monthlyTreatments = <String, int>{};
for (final record in _dewormingRecords) {
final monthKey = '${record.treatmentDate.year}-${record.treatmentDate.month.toString().padLeft(2, '0')}';
monthlyTreatments[monthKey] = (monthlyTreatments[monthKey] ?? 0) + 1;
}
// 计算月度费用
final costByMonth = <String, double>{};
for (final record in _dewormingRecords) {
final monthKey = '${record.treatmentDate.year}-${record.treatmentDate.month.toString().padLeft(2, '0')}';
costByMonth[monthKey] = (costByMonth[monthKey] ?? 0.0) + record.cost;
}
// 计算最常用药物
final medicineCount = <String, int>{};
for (final record in _dewormingRecords) {
medicineCount[record.medicineName] = (medicineCount[record.medicineName] ?? 0) + 1;
}
final mostUsedMedicine = medicineCount.entries.isNotEmpty
? medicineCount.entries.reduce((a, b) => a.value > b.value ? a : b).key
: '暂无';
// 计算最常去的诊所
final clinicCount = <String, int>{};
for (final record in _dewormingRecords) {
clinicCount[record.clinic] = (clinicCount[record.clinic] ?? 0) + 1;
}
final mostFrequentClinic = clinicCount.entries.isNotEmpty
? clinicCount.entries.reduce((a, b) => a.value > b.value ? a : b).key
: '暂无';
_stats = DewormingStats(
totalPets: totalPets,
totalRecords: totalRecords,
activePlans: activePlans,
pendingReminders: pendingReminders,
totalCost: totalCost,
medicineTypeDistribution: medicineTypeDistribution,
monthlyTreatments: monthlyTreatments,
costByMonth: costByMonth,
mostUsedMedicine: mostUsedMedicine,
mostFrequentClinic: mostFrequentClinic,
);
}
快速记录功能
快速记录对话框
快速记录功能让用户能够快速添加驱虫记录:
void _showQuickRecordDialog() {
if (_pets.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请先添加宠物信息')),
);
return;
}
String selectedPetId = _pets.first.id;
String selectedMedicineType = '内驱';
final medicineNameController = TextEditingController();
final dosageController = TextEditingController();
final costController = TextEditingController();
DateTime selectedDate = DateTime.now();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('快速记录驱虫'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DropdownButtonFormField<String>(
value: selectedPetId,
decoration: const InputDecoration(labelText: '选择宠物'),
items: _pets.map((pet) {
return DropdownMenuItem(
value: pet.id,
child: Text(pet.name),
);
}).toList(),
onChanged: (value) {
selectedPetId = value!;
},
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: selectedMedicineType,
decoration: const InputDecoration(labelText: '药物类型'),
items: ['内驱', '外驱', '内外同驱'].map((type) {
return DropdownMenuItem(
value: type,
child: Text(type),
);
}).toList(),
onChanged: (value) {
selectedMedicineType = value!;
},
),
const SizedBox(height: 16),
TextField(
controller: medicineNameController,
decoration: const InputDecoration(
labelText: '药物名称',
hintText: '请输入药物名称',
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextField(
controller: dosageController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: '剂量',
hintText: '1.0',
),
),
),
const SizedBox(width: 16),
Expanded(
child: TextField(
controller: costController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: '费用',
hintText: '0.00',
prefixText: '¥',
),
),
),
],
),
const SizedBox(height: 16),
ListTile(
title: const Text('驱虫日期'),
subtitle: Text(_formatDate(selectedDate)),
trailing: const Icon(Icons.calendar_today),
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: selectedDate,
firstDate: DateTime.now().subtract(const Duration(days: 365)),
lastDate: DateTime.now(),
);
if (date != null) {
selectedDate = date;
}
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () {
if (medicineNameController.text.isNotEmpty) {
final selectedPet = _pets.firstWhere((pet) => pet.id == selectedPetId);
final newRecord = DewormingRecord(
id: 'dr${_dewormingRecords.length + 1}',
petId: selectedPetId,
petName: selectedPet.name,
treatmentDate: selectedDate,
medicineType: selectedMedicineType,
medicineName: medicineNameController.text,
medicineBrand: '未知品牌',
dosage: double.tryParse(dosageController.text) ?? 1.0,
dosageUnit: selectedMedicineType == '外驱' ? 'ml' : '片',
administrationMethod: selectedMedicineType == '外驱' ? '滴剂' : '口服',
petWeight: selectedPet.weight,
veterinarian: '自行记录',
clinic: '家庭护理',
cost: double.tryParse(costController.text) ?? 0.0,
notes: '快速记录',
symptoms: [],
sideEffects: [],
effectiveness: '良好',
photos: [],
nextDueDate: selectedDate.add(Duration(
days: selectedMedicineType == '外驱' ? 30 : 90,
)),
);
setState(() {
_dewormingRecords.add(newRecord);
_calculateStats();
});
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('驱虫记录添加成功!')),
);
}
},
child: const Text('保存'),
),
],
),
);
}
详细记录对话框
完整的驱虫记录添加功能:
void _showAddRecordDialog() {
if (_pets.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请先添加宠物信息')),
);
return;
}
String selectedPetId = _pets.first.id;
String selectedMedicineType = '内驱';
final medicineNameController = TextEditingController();
final medicineBrandController = TextEditingController();
final dosageController = TextEditingController();
final veterinarianController = TextEditingController();
final clinicController = TextEditingController();
final costController = TextEditingController();
final notesController = TextEditingController();
DateTime selectedDate = DateTime.now();
String selectedEffectiveness = '良好';
String selectedDosageUnit = '片';
String selectedAdministrationMethod = '口服';
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('添加驱虫记录'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DropdownButtonFormField<String>(
value: selectedPetId,
decoration: const InputDecoration(labelText: '选择宠物'),
items: _pets.map((pet) {
return DropdownMenuItem(
value: pet.id,
child: Text('${pet.name} (${pet.species})'),
);
}).toList(),
onChanged: (value) {
selectedPetId = value!;
},
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: selectedMedicineType,
decoration: const InputDecoration(labelText: '药物类型'),
items: ['内驱', '外驱', '内外同驱'].map((type) {
return DropdownMenuItem(
value: type,
child: Text(type),
);
}).toList(),
onChanged: (value) {
selectedMedicineType = value!;
// 根据药物类型自动调整默认值
if (value == '外驱') {
selectedDosageUnit = 'ml';
selectedAdministrationMethod = '滴剂';
} else {
selectedDosageUnit = '片';
selectedAdministrationMethod = '口服';
}
},
),
const SizedBox(height: 16),
TextField(
controller: medicineNameController,
decoration: const InputDecoration(
labelText: '药物名称',
hintText: '请输入药物名称',
),
),
const SizedBox(height: 16),
TextField(
controller: medicineBrandController,
decoration: const InputDecoration(
labelText: '药物品牌',
hintText: '请输入品牌名称',
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextField(
controller: dosageController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: '剂量',
hintText: '1.0',
),
),
),
const SizedBox(width: 8),
Expanded(
child: DropdownButtonFormField<String>(
value: selectedDosageUnit,
decoration: const InputDecoration(labelText: '单位'),
items: ['片', 'ml', '滴', 'g'].map((unit) {
return DropdownMenuItem(
value: unit,
child: Text(unit),
);
}).toList(),
onChanged: (value) {
selectedDosageUnit = value!;
},
),
),
],
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: selectedAdministrationMethod,
decoration: const InputDecoration(labelText: '给药方式'),
items: ['口服', '滴剂', '喷剂', '注射'].map((method) {
return DropdownMenuItem(
value: method,
child: Text(method),
);
}).toList(),
onChanged: (value) {
selectedAdministrationMethod = value!;
},
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextField(
controller: veterinarianController,
decoration: const InputDecoration(
labelText: '兽医师',
hintText: '请输入兽医师姓名',
),
),
),
const SizedBox(width: 16),
Expanded(
child: TextField(
controller: clinicController,
decoration: const InputDecoration(
labelText: '诊所',
hintText: '请输入诊所名称',
),
),
),
],
),
const SizedBox(height: 16),
TextField(
controller: costController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: '费用',
hintText: '0.00',
prefixText: '¥',
),
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: selectedEffectiveness,
decoration: const InputDecoration(labelText: '效果评价'),
items: ['优秀', '良好', '一般', '差'].map((effectiveness) {
return DropdownMenuItem(
value: effectiveness,
child: Text(effectiveness),
);
}).toList(),
onChanged: (value) {
selectedEffectiveness = value!;
},
),
const SizedBox(height: 16),
TextField(
controller: notesController,
maxLines: 3,
decoration: const InputDecoration(
labelText: '备注',
hintText: '请输入备注信息',
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () {
if (medicineNameController.text.isNotEmpty) {
final selectedPet = _pets.firstWhere((pet) => pet.id == selectedPetId);
final newRecord = DewormingRecord(
id: 'dr${_dewormingRecords.length + 1}',
petId: selectedPetId,
petName: selectedPet.name,
treatmentDate: selectedDate,
medicineType: selectedMedicineType,
medicineName: medicineNameController.text,
medicineBrand: medicineBrandController.text.isNotEmpty
? medicineBrandController.text
: '未知品牌',
dosage: double.tryParse(dosageController.text) ?? 1.0,
dosageUnit: selectedDosageUnit,
administrationMethod: selectedAdministrationMethod,
petWeight: selectedPet.weight,
veterinarian: veterinarianController.text.isNotEmpty
? veterinarianController.text
: '未知',
clinic: clinicController.text.isNotEmpty
? clinicController.text
: '未知',
cost: double.tryParse(costController.text) ?? 0.0,
notes: notesController.text,
symptoms: [],
sideEffects: [],
effectiveness: selectedEffectiveness,
photos: [],
nextDueDate: selectedDate.add(Duration(
days: selectedMedicineType == '外驱' ? 30 : 90,
)),
);
setState(() {
_dewormingRecords.add(newRecord);
_calculateStats();
});
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('驱虫记录添加成功!')),
);
}
},
child: const Text('保存'),
),
],
),
);
}
应用特色功能
智能提醒系统
应用具备智能提醒功能:
- 自动计算下次驱虫时间:根据药物类型和宠物特征自动计算
- 多级提醒机制:提前7天、3天、1天多次提醒
- 逾期警告:超过预定时间后显示逾期警告
- 个性化提醒:根据宠物品种和年龄调整提醒频率
健康状态监测
应用提供全面的健康监测功能:
- 症状记录:详细记录驱虫前后的症状变化
- 副作用追踪:记录药物副作用,为后续选药提供参考
- 体重监测:跟踪宠物体重变化,调整用药剂量
- 效果评估:评价驱虫效果,优化治疗方案
数据分析洞察
应用提供深度数据分析:
- 费用统计:分析驱虫费用趋势,合理规划预算
- 药物效果对比:比较不同药物的效果和副作用
- 季节性分析:分析不同季节的驱虫需求
- 宠物健康趋势:长期跟踪宠物健康状况变化
用户体验优化
应用注重用户体验:
- 快速记录:一键快速记录常用驱虫信息
- 智能搜索:支持多关键词模糊搜索
- 数据导出:支持记录数据导出和备份
- 离线使用:支持离线记录,网络恢复后同步
技术优化建议
性能优化
- 数据缓存机制:实现本地数据缓存,减少重复计算
- 懒加载实现:大数据列表采用懒加载方式提升性能
- 图片优化:宠物照片采用缩略图和原图分离策略
- 内存管理:及时释放不必要的资源和监听器
用户体验优化
- 离线功能:支持离线记录和查看,网络恢复后同步
- 快捷操作:提供更多快捷记录和编辑方式
- 数据备份:支持云端备份和多设备同步
- 多语言支持:国际化支持,适配不同语言环境
功能扩展建议
- 兽医预约:集成兽医预约功能
- 药物提醒:药物库存不足时自动提醒
- 社区功能:宠物主人经验分享社区
- AI建议:基于宠物特征和历史数据提供AI驱虫建议
项目部署和发布
pubspec.yaml 配置
name: pet_deworming_tracker
description: Flutter宠物驱虫记录器应用
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
shared_preferences: ^2.2.2
path_provider: ^2.1.1
image_picker: ^1.0.4
share_plus: ^7.2.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
uses-material-design: true
assets:
- assets/images/
- assets/pets/
构建和发布
Android 发布
- 生成签名密钥:
keytool -genkey -v -keystore ~/pet-deworming-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias pet-deworming
- 构建 APK:
flutter build apk --release
iOS 发布
- 在 Xcode 中配置签名和证书
- 构建 iOS 应用:
flutter build ios --release
总结
Flutter宠物驱虫记录器应用是一个功能完整、设计精美的宠物健康管理工具。应用通过Material Design 3设计语言,提供了直观友好的用户界面;通过完善的数据模型设计,实现了全面的驱虫记录管理;通过智能提醒和统计分析,帮助宠物主人科学管理宠物健康。
应用的核心价值在于:
- 系统化管理:科学记录和管理宠物驱虫信息,提高健康管理效率
- 智能提醒:自动计算和提醒驱虫时间,避免遗漏
- 数据分析:全面的统计分析,帮助优化驱虫策略
- 健康监测:持续跟踪宠物健康状况,及时发现问题
通过本教程的学习,开发者可以掌握Flutter应用开发的核心技术,包括状态管理、UI设计、数据处理、动画效果等。同时,也能了解如何设计和实现一个完整的健康管理应用,为后续的项目开发奠定坚实基础。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)