import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/navigation_provider.dart'; import '../providers/auth_provider.dart'; import '../providers/data_providers.dart'; /// 饮食记录列表(左滑删除) class DietRecordListPage extends ConsumerStatefulWidget { const DietRecordListPage({super.key}); @override ConsumerState createState() => _DietRecordListPageState(); } class _DietRecordListPageState extends ConsumerState { Future>>? _future; @override void initState() { super.initState(); _refresh(); } void _refresh() { setState(() { _future = ref.read(dietServiceProvider).getRecords(); }); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF8F9FC), appBar: AppBar(title: const Text('饮食记录')), body: FutureBuilder>>( future: _future, builder: (ctx, snap) { if (snap.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator(color: Color(0xFF6C5CE7))); final data = snap.data ?? []; if (data.isEmpty) return _empty(context, '饮食记录', '暂无饮食记录,可通过「拍饮食」录入'); return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8), itemCount: data.length, itemBuilder: (ctx, i) { final d = data[i]; final items = (d['foodItems'] as List?)?.cast>() ?? []; final mealNames = {'Breakfast':'早餐','Lunch':'午餐','Dinner':'晚餐','Snack':'加餐'}; final mealLabel = mealNames[d['mealType']?.toString()] ?? d['mealType']?.toString() ?? ''; final mealIcons = {'Breakfast':'🌅','Lunch':'☀️','Dinner':'🌙','Snack':'🍪'}; return _SwipeDeleteTile( key: Key(d['id']?.toString() ?? '$i'), onDelete: () async { await ref.read(dietServiceProvider).deleteRecord(d['id']?.toString() ?? ''); _refresh(); }, onTap: () => pushRoute(ref, 'dietDetail', params: {'id': d['id']?.toString() ?? ''}), child: Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), padding: const EdgeInsets.all(14), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)), child: Row(children: [ Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFEDEAFF), borderRadius: BorderRadius.circular(12)), child: Center(child: Text(mealIcons[d['mealType']?.toString()] ?? '🍽️', style: const TextStyle(fontSize: 20)))), const SizedBox(width: 12), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Text(mealLabel, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), const SizedBox(width: 8), Text('${d['totalCalories'] ?? 0}千卡', style: const TextStyle(fontSize: 13, color: Color(0xFF6C5CE7))), ]), const SizedBox(height: 4), Text(items.map((f) => f['name']).join(' · '), style: const TextStyle(fontSize: 13, color: Color(0xFF888888)), maxLines: 1, overflow: TextOverflow.ellipsis), ])), const Icon(Icons.chevron_right, size: 18, color: Color(0xFFCCCCCC)), ]), ), ); }, ); }, ), ); } } /// 饮食记录详情页 class DietRecordDetailPage extends ConsumerWidget { final String id; const DietRecordDetailPage({super.key, required this.id}); @override Widget build(BuildContext context, WidgetRef ref) { final service = ref.watch(dietServiceProvider); return Scaffold( backgroundColor: const Color(0xFFF8F9FC), appBar: AppBar(title: const Text('饮食详情')), body: FutureBuilder>>( future: service.getRecords(), builder: (ctx, snap) { if (!snap.hasData) return const Center(child: CircularProgressIndicator(color: Color(0xFF6C5CE7))); final d = snap.data!.firstWhere((r) => r['id']?.toString() == id, orElse: () => {}); if (d.isEmpty) return const Center(child: Text('记录不存在')); final items = (d['foodItems'] as List?)?.cast>() ?? []; final totalCal = d['totalCalories'] ?? 0; final mealNames = {'Breakfast':'早餐','Lunch':'午餐','Dinner':'晚餐','Snack':'加餐'}; return ListView(padding: const EdgeInsets.all(16), children: [ Container(padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)), child: Row(children: [ Container(width: 48, height: 48, decoration: BoxDecoration(color: const Color(0xFFEDEAFF), borderRadius: BorderRadius.circular(14)), child: const Icon(Icons.restaurant, size: 24, color: Color(0xFF6C5CE7))), const SizedBox(width: 14), Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(mealNames[d['mealType']?.toString()] ?? '', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700)), Text('$totalCal 千卡 · ${d['recordedAt'] ?? ''}', style: const TextStyle(fontSize: 13, color: Color(0xFF999999))), ]), ])), const SizedBox(height: 16), ...items.map((f) => Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(14), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)), child: Row(children: [ Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(f['name']?.toString() ?? '', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), if ((f['portion']?.toString() ?? '').isNotEmpty) Text(f['portion']!.toString(), style: const TextStyle(fontSize: 13, color: Color(0xFF888888))), ])), Text('${f['calories'] ?? 0} kcal', style: const TextStyle(fontSize: 15, color: Color(0xFF6C5CE7), fontWeight: FontWeight.w600)), ]), )), ]); }, ), ); } } /// 运动计划列表(含打卡) class ExercisePlanPage extends ConsumerStatefulWidget { const ExercisePlanPage({super.key}); @override ConsumerState createState() => _ExercisePlanPageState(); } class _ExercisePlanPageState extends ConsumerState { Future>>? _future; @override void initState() { super.initState(); _load(); } void _load() => setState(() { _future = ref.read(exerciseServiceProvider).getPlans(); }); Future _checkIn(String id, String itemId) async { await ref.read(exerciseServiceProvider).checkIn(itemId); ref.invalidate(currentExercisePlanProvider); _load(); } Future _deletePlan(String id) async { await ref.read(exerciseServiceProvider).deletePlan(id); _load(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF8F9FC), appBar: AppBar(title: const Text('运动计划')), floatingActionButton: FloatingActionButton(onPressed: () { pushRoute(ref, 'exerciseCreate'); }, backgroundColor: const Color(0xFF6C5CE7), child: const Icon(Icons.add)), body: FutureBuilder>>( future: _future, builder: (ctx, snap) { final plans = snap.data ?? []; if (plans.isEmpty) return _empty(context, '运动计划', '暂无计划,点击右下角新建'); return ListView.builder(padding: const EdgeInsets.all(12), itemCount: plans.length, itemBuilder: (ctx, i) { final p = plans[i]; final items = (p['items'] as List?)?.cast>() ?? []; final total = items.length; final done = items.where((it) => it['isCompleted'] == true).length; // 基于weekStartDate推算今天对应的条目索引 final weekStart = p['weekStartDate']?.toString() ?? ''; int? todayIdx; if (weekStart.isNotEmpty) { final ws = DateTime.tryParse(weekStart); if (ws != null) { final diff = DateTime.now().difference(ws).inDays; if (diff >= 0 && diff < items.length) todayIdx = diff; } } final todayItem = todayIdx != null ? items[todayIdx] : null; final todayDone = todayItem?['isCompleted'] == true; final exerciseName = items.isNotEmpty ? (items.first['exerciseType']?.toString() ?? '运动') : '运动'; final startDate = weekStart; final endIdx = items.length - 1; final endDay = items.isNotEmpty ? items.last['dayOfWeek'] as int? : 6; return _SwipeDeleteTile( key: Key(p['id']?.toString() ?? '$i'), onDelete: () => _deletePlan(p['id']?.toString() ?? ''), onTap: () {}, child: Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)), child: Row(children: [ Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFEDEAFF), borderRadius: BorderRadius.circular(12)), child: const Icon(Icons.directions_run, size: 22, color: Color(0xFF6C5CE7))), const SizedBox(width: 12), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(exerciseName, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 2), Text('$startDate 起 · ${items.first['durationMinutes'] ?? 0}分钟/天', style: const TextStyle(fontSize: 13, color: Color(0xFF888888))), const SizedBox(height: 6), Text('$done/$total 天已完成', style: const TextStyle(fontSize: 12, color: Color(0xFF6C5CE7))), ])), GestureDetector( onTap: todayItem != null ? () => _checkIn(p['id']?.toString() ?? '', todayItem['id']?.toString() ?? '') : null, child: AnimatedContainer( duration: const Duration(milliseconds: 300), width: 40, height: 40, decoration: BoxDecoration( color: todayDone ? const Color(0xFFDCFCE7) : (todayItem != null ? const Color(0xFFEDEAFF) : const Color(0xFFF5F5F5)), borderRadius: BorderRadius.circular(12), ), child: Icon(todayDone ? Icons.check_circle : Icons.check_circle_outline, size: 24, color: todayDone ? const Color(0xFF43A047) : const Color(0xFFBBBBBB)), ), ), ]), ), ); }); }, ), ); } } /// 新建运动计划 class ExercisePlanCreatePage extends ConsumerStatefulWidget { const ExercisePlanCreatePage({super.key}); @override ConsumerState createState() => _ExercisePlanCreatePageState(); } class _ExercisePlanCreatePageState extends ConsumerState { final _nameCtrl = TextEditingController(); final _durationCtrl = TextEditingController(text: '30'); DateTime _start = DateTime.now(); DateTime _end = DateTime.now().add(const Duration(days: 6)); @override void dispose() { _nameCtrl.dispose(); _durationCtrl.dispose(); super.dispose(); } Future _save() async { final name = _nameCtrl.text.trim(); if (name.isEmpty) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请输入运动名称'))); return; } final dur = int.tryParse(_durationCtrl.text) ?? 30; await ref.read(exerciseServiceProvider).createPlanSimple({ 'exerciseType': name, 'durationMinutes': dur, 'startDate': '${_start.year}-${_start.month.toString().padLeft(2,'0')}-${_start.day.toString().padLeft(2,'0')}', 'endDate': '${_end.year}-${_end.month.toString().padLeft(2,'0')}-${_end.day.toString().padLeft(2,'0')}', }); ref.invalidate(currentExercisePlanProvider); if (mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('计划已创建'), backgroundColor: Color(0xFF43A047))); popRoute(ref); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF8F9FC), appBar: AppBar(title: const Text('新建计划')), body: ListView(padding: const EdgeInsets.all(16), children: [ _field('运动名称', _nameCtrl, hint: '如: 散步、慢跑、太极'), const SizedBox(height: 16), _field('每天时长(分钟)', _durationCtrl, hint: '30', number: true), const SizedBox(height: 16), _dateField('开始日期', _start, (d) => setState(() => _start = d)), const SizedBox(height: 16), _dateField('结束日期', _end, (d) => setState(() => _end = d)), const SizedBox(height: 32), SizedBox(width: double.infinity, child: ElevatedButton(onPressed: _save, style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF6C5CE7), foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), padding: const EdgeInsets.symmetric(vertical: 14)), child: const Text('保存计划', style: TextStyle(fontSize: 16)))), ]), ); } Widget _field(String label, TextEditingController ctrl, {String? hint, bool number = false}) { return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(fontSize: 14, color: Color(0xFF666666))), const SizedBox(height: 6), TextField(controller: ctrl, keyboardType: number ? TextInputType.number : null, decoration: InputDecoration(hintText: hint, filled: true, fillColor: Colors.white, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none)), style: const TextStyle(fontSize: 16)), ]); } Widget _dateField(String label, DateTime val, ValueChanged onChanged) { return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(fontSize: 14, color: Color(0xFF666666))), const SizedBox(height: 6), GestureDetector( onTap: () async { final d = await showDatePicker(context: context, initialDate: val, firstDate: DateTime(2024), lastDate: DateTime(2030)); if (d != null) onChanged(d); }, child: Container(width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), child: Text('${val.year}-${val.month.toString().padLeft(2,'0')}-${val.day.toString().padLeft(2,'0')}', style: const TextStyle(fontSize: 16))), ), ]); } } /// 复查列表(从服务器读取) class FollowUpListPage extends ConsumerStatefulWidget { const FollowUpListPage({super.key}); @override ConsumerState createState() => _FollowUpListPageState(); } class _FollowUpListPageState extends ConsumerState { Future>>? _future; @override void initState() { super.initState(); _load(); } void _load() => setState(() { _future = ref.read(followUpServiceProvider).getList(); }); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('复查随访'), centerTitle: true), body: FutureBuilder>>( future: _future, builder: (ctx, snap) { if (snap.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator(color: Color(0xFF8B9CF7))); } final list = snap.data ?? []; if (list.isEmpty) { return Center( child: Column(mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.event_note_outlined, size: 64, color: Colors.grey[300]), const SizedBox(height: 12), Text('暂无随访安排', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 4), const Text('医生创建随访后将显示在这里', style: TextStyle(fontSize: 13, color: Color(0xFF999999))), ]), ); } return ListView( padding: const EdgeInsets.symmetric(vertical: 8), children: list.map((item) => _FollowUpItem(item: item)).toList(), ); }, ), ); } } class _FollowUpItem extends StatelessWidget { final Map item; const _FollowUpItem({required this.item}); String _statusLabel(String? status) { switch (status) { case 'Upcoming': return '即将到来'; case 'Completed': return '已完成'; case 'Cancelled': return '已取消'; default: return status ?? ''; } } Color _statusColor(String? status) { switch (status) { case 'Upcoming': return const Color(0xFF4F6EF7); case 'Completed': return const Color(0xFF43A047); case 'Cancelled': return const Color(0xFFE53935); default: return const Color(0xFF999999); } } Color _statusBg(String? status) { switch (status) { case 'Upcoming': return const Color(0xFFEDF2FF); case 'Completed': return const Color(0xFFDCFCE7); case 'Cancelled': return const Color(0xFFFFF5F5); default: return const Color(0xFFF5F5F5); } } @override Widget build(BuildContext context) { final status = item['status']?.toString(); final label = _statusLabel(status); final color = _statusColor(status); final bg = _statusBg(status); return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(10), blurRadius: 4, offset: const Offset(0, 2))], ), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(8)), child: Text(label, style: TextStyle(fontSize: 12, color: color, fontWeight: FontWeight.w500)), ), const Spacer(), if (item['doctorName'] != null) Text('👨‍⚕️ ${item['doctorName']}', style: const TextStyle(fontSize: 12, color: Color(0xFF999999))), ]), const SizedBox(height: 12), Text(item['title']?.toString() ?? '', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 4), if (item['scheduledAt'] != null) Text(_formatDateTime(item['scheduledAt']?.toString()), style: TextStyle(fontSize: 14, color: Colors.grey[500])), if ((item['notes']?.toString() ?? '').isNotEmpty) ...[ const SizedBox(height: 8), Text(item['notes']?.toString() ?? '', style: TextStyle(fontSize: 13, color: Colors.grey[600])), ], ]), ); } String _formatDateTime(String? iso) { if (iso == null) return ''; final dt = DateTime.tryParse(iso); if (dt == null) return iso; return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; } } /// 健康档案(可编辑) class HealthArchivePage extends ConsumerStatefulWidget { const HealthArchivePage({super.key}); @override ConsumerState createState() => _HealthArchivePageState(); } class _HealthArchivePageState extends ConsumerState { final _nameCtrl = TextEditingController(); final _genderCtrl = TextEditingController(); final _birthCtrl = TextEditingController(); final _diagnosisCtrl = TextEditingController(); final _surgeryCtrl = TextEditingController(); final _surgeryDateCtrl = TextEditingController(); final _allergiesCtrl = TextEditingController(); final _chronicCtrl = TextEditingController(); final _dietCtrl = TextEditingController(); final _familyCtrl = TextEditingController(); bool _loading = true; @override void initState() { super.initState(); _load(); } @override void dispose() { _nameCtrl.dispose(); _genderCtrl.dispose(); _birthCtrl.dispose(); _diagnosisCtrl.dispose(); _surgeryCtrl.dispose(); _surgeryDateCtrl.dispose(); _allergiesCtrl.dispose(); _chronicCtrl.dispose(); _dietCtrl.dispose(); _familyCtrl.dispose(); super.dispose(); } Future _load() async { final srv = ref.read(userServiceProvider); try { final p = await srv.getProfile(); if (p != null && mounted) { _nameCtrl.text = p['name'] ?? ''; _genderCtrl.text = p['gender'] ?? ''; _birthCtrl.text = p['birthDate'] ?? ''; } final a = await srv.getHealthArchive(); if (a != null && mounted) { _diagnosisCtrl.text = a['diagnosis'] ?? ''; _surgeryCtrl.text = a['surgeryType'] ?? ''; _surgeryDateCtrl.text = a['surgeryDate'] ?? ''; _allergiesCtrl.text = (a['allergies'] as List?)?.join('、') ?? ''; _chronicCtrl.text = (a['chronicDiseases'] as List?)?.join('、') ?? ''; _dietCtrl.text = (a['dietRestrictions'] as List?)?.join('、') ?? ''; _familyCtrl.text = a['familyHistory'] ?? ''; } if (mounted) setState(() => _loading = false); } catch (_) { if (mounted) setState(() => _loading = false); } } Future _save() async { final srv = ref.read(userServiceProvider); try { await srv.updateProfile(name: _nameCtrl.text, gender: _genderCtrl.text, birthDate: _birthCtrl.text); await srv.updateHealthArchive({ 'diagnosis': _diagnosisCtrl.text, 'surgeryType': _surgeryCtrl.text, 'surgeryDate': _surgeryDateCtrl.text, 'allergies': _allergiesCtrl.text.split('、').where((s) => s.isNotEmpty).toList(), 'chronicDiseases': _chronicCtrl.text.split('、').where((s) => s.isNotEmpty).toList(), 'dietRestrictions': _dietCtrl.text.split('、').where((s) => s.isNotEmpty).toList(), 'familyHistory': _familyCtrl.text, }); if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已保存'), backgroundColor: Color(0xFF43A047))); } catch (_) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('保存失败'), backgroundColor: Color(0xFFE53935))); } } @override Widget build(BuildContext context) { if (_loading) return Scaffold(appBar: AppBar(title: const Text('健康档案')), body: const Center(child: CircularProgressIndicator(color: Color(0xFF6C5CE7)))); return Scaffold( backgroundColor: const Color(0xFFF8F9FC), appBar: AppBar(title: const Text('健康档案'), actions: [ TextButton(onPressed: _save, child: const Text('保存', style: TextStyle(color: Color(0xFF6C5CE7), fontWeight: FontWeight.w600))), ]), body: ListView(padding: const EdgeInsets.all(16), children: [ _sectionTitle('个人资料'), _editableCard(children: [ _field('姓名', _nameCtrl), Row(children: [Expanded(child: _field('性别', _genderCtrl)), const SizedBox(width: 12), Expanded(child: _field('出生日期', _birthCtrl, hint: 'YYYY-MM-DD'))]), ]), _sectionTitle('医疗信息'), _editableCard(children: [ _field('主要诊断', _diagnosisCtrl, hint: '如: 冠心病'), Row(children: [Expanded(child: _field('手术类型', _surgeryCtrl, hint: '如: PCI支架植入术')), const SizedBox(width: 12), Expanded(child: _field('手术日期', _surgeryDateCtrl, hint: 'YYYY-MM-DD'))]), ]), _sectionTitle('病史与限制'), _editableCard(children: [ _field('过敏史', _allergiesCtrl, hint: '多个用、分隔 如: 青霉素、海鲜'), _field('慢性病史', _chronicCtrl, hint: '多个用、分隔 如: 高血压、高血脂'), _field('饮食限制', _dietCtrl, hint: '多个用、分隔 如: 低盐、低脂'), _field('家族病史', _familyCtrl, hint: '如: 父亲冠心病'), ]), const SizedBox(height: 20), SizedBox(width: double.infinity, child: ElevatedButton(onPressed: _save, child: const Text('保存档案')),), const SizedBox(height: 40), ]), ); } Widget _sectionTitle(String title) => Padding( padding: const EdgeInsets.only(left: 4, top: 16, bottom: 8), child: Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Color(0xFF6C5CE7))), ); Widget _editableCard({required List children}) => Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: const Color(0xFF6C5CE7).withAlpha(8), blurRadius: 8, offset: const Offset(0, 2))]), child: Column(children: children), ); Widget _field(String label, TextEditingController ctrl, {String? hint}) => Padding( padding: const EdgeInsets.only(bottom: 12), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF888888))), const SizedBox(height: 4), TextField(controller: ctrl, decoration: InputDecoration(hintText: hint, filled: true, fillColor: const Color(0xFFF4F5FA), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none)), style: const TextStyle(fontSize: 15)), ]), ); } /// 编辑资料(已合并到健康档案,保留路由兼容) class EditProfilePage extends ConsumerWidget { final String? id; const EditProfilePage({super.key, this.id}); @override Widget build(BuildContext context, WidgetRef ref) => const HealthArchivePage(); } /// 健康日历 class HealthCalendarPage extends ConsumerStatefulWidget { const HealthCalendarPage({super.key}); @override ConsumerState createState() => _HealthCalendarPageState(); } class _HealthCalendarPageState extends ConsumerState { DateTime _currentMonth = DateTime.now(); Map> _events = {}; @override void initState() { super.initState(); _loadMonth(); } Future _loadMonth() async { try { final api = ref.read(apiClientProvider); final res = await api.get('/api/calendar', queryParameters: { 'year': _currentMonth.year, 'month': _currentMonth.month, }); final list = (res.data['data'] as List?) ?? []; final events = >{}; for (final item in list) { final map = item as Map; events[map['date'] as String] = List.from(map['events'] ?? []); } if (mounted) setState(() => _events = events); } catch (_) {} } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('健康日历'), centerTitle: true), body: Column(children: [ _buildMonthHeader(), _buildWeekdayHeader(), _buildCalendarGrid(), const SizedBox(height: 16), _buildLegend(), ]), ); } Widget _buildMonthHeader() { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( icon: const Icon(Icons.chevron_left, size: 32), onPressed: () { setState(() => _currentMonth = DateTime(_currentMonth.year, _currentMonth.month - 1)); _loadMonth(); }, ), Text( '${_currentMonth.year}年${_currentMonth.month}月', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), ), IconButton( icon: const Icon(Icons.chevron_right, size: 32), onPressed: () { setState(() => _currentMonth = DateTime(_currentMonth.year, _currentMonth.month + 1)); _loadMonth(); }, ), ], ); } Widget _buildWeekdayHeader() { const weekdays = ['日', '一', '二', '三', '四', '五', '六']; return Row(children: weekdays.map((day) => Expanded( child: Center(child: Text(day, style: TextStyle(fontSize: 14, color: Colors.grey[500]))), )).toList()); } Widget _buildCalendarGrid() { final firstDay = DateTime(_currentMonth.year, _currentMonth.month, 1); final lastDay = DateTime(_currentMonth.year, _currentMonth.month + 1, 0); final daysInMonth = lastDay.day; final startWeekday = firstDay.weekday % 7; final days = List.generate(42, (i) { final dayIndex = i - startWeekday; if (dayIndex < 0 || dayIndex >= daysInMonth) return null; return dayIndex + 1; }); return Expanded( child: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 7), itemCount: 42, itemBuilder: (ctx, i) { final day = days[i]; if (day == null) return const SizedBox(); return _buildDayCell(day); }, ), ); } Widget _buildDayCell(int day) { final date = DateTime(_currentMonth.year, _currentMonth.month, day); final today = DateTime.now(); final isToday = date.year == today.year && date.month == today.month && date.day == today.day; final events = _getEvents(date); return Container( decoration: isToday ? BoxDecoration( color: const Color(0xFF8B9CF7), borderRadius: BorderRadius.circular(20), ) : null, child: Stack( alignment: Alignment.center, children: [ Text( '$day', style: TextStyle( fontSize: 16, color: isToday ? Colors.white : Colors.black, fontWeight: isToday ? FontWeight.w600 : FontWeight.normal, ), ), if (events.isNotEmpty) Positioned( bottom: 4, child: Row(children: events.map((type) => Container( width: 6, height: 6, margin: const EdgeInsets.symmetric(horizontal: 1), decoration: BoxDecoration( color: _getEventColor(type), borderRadius: BorderRadius.circular(3), ), )).toList()), ), ], ), ); } List _getEvents(DateTime date) { final key = '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; return _events[key] ?? []; } Color _getEventColor(String type) { switch (type) { case 'medication': return const Color(0xFF8B9CF7); case 'exercise': return const Color(0xFF43A047); case 'followup': return const Color(0xFFF59E0B); default: return Colors.grey; } } Widget _buildLegend() { final items = [ {'color': const Color(0xFF8B9CF7), 'label': '用药提醒'}, {'color': const Color(0xFF43A047), 'label': '运动计划'}, {'color': const Color(0xFFF59E0B), 'label': '复查随访'}, ]; return Container( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row(mainAxisAlignment: MainAxisAlignment.center, children: items.map((item) => Row(children: [ Container(width: 10, height: 10, decoration: BoxDecoration(color: item['color'] as Color, borderRadius: BorderRadius.circular(5))), const SizedBox(width: 4), Text(item['label'] as String, style: TextStyle(fontSize: 12, color: Colors.grey[600])), const SizedBox(width: 20), ])).toList()), ); } } /// 静态文本页 class StaticTextPage extends ConsumerWidget { final String type; const StaticTextPage({super.key, required this.type}); @override Widget build(BuildContext context, WidgetRef ref) { final titles = {'privacy': '隐私协议', 'terms': '服务协议', 'about': '关于健康管家'}; final contents = { 'privacy': '''## 隐私政策 更新日期:2026年1月1日 ### 一、信息收集 我们收集以下类型的信息: - 账户信息:手机号、昵称、头像(您主动提供) - 健康数据:血压、心率、血糖、血氧、体重等健康指标记录 - 用药信息:药品名称、剂量、服药时间等用药计划数据 - 饮食记录:通过拍照或手动录入的饮食数据 - 设备信息:设备型号、操作系统版本(用于适配优化) - 日志信息:App 使用情况、崩溃报告 ### 二、信息使用 我们使用您的信息用于以下目的: - 提供和改进健康管理服务 - AI 健康分析和个性化建议 - 用药提醒和复查通知推送 - App 功能优化和问题修复 ### 三、信息保护 - 所有健康数据均采用 HTTPS 加密传输 - 数据存储于安全服务器,采用行业标准的加密措施 - 我们不会向任何第三方出售、出租或共享您的个人健康数据 - 医生仅可查看其签约患者的数据,且需经过您的授权 ### 四、信息保留 - 对话记录保留 30 天后自动删除 - 您可以随时删除自己的健康数据和对话记录 - 账号注销后,所有数据将在 7 天内永久删除 ### 五、您的权利 - 查看和导出您的个人数据 - 修改不准确的个人信息 - 删除不需要的数据 - 注销账号并清除所有数据 - 关闭推送通知 ### 六、联系我们 如有任何关于隐私的问题,请联系: 邮箱:privacy@healthbutler.com 电话:400-xxx-xxxx''', 'about': '''## 关于健康管家 版本:v1.0.0 (Build 20260101) ### 产品介绍 健康管家是一款面向心脏术后康复患者的私人 AI 健康管理应用。以对话为核心交互方式,患者可以通过自然语言记录健康数据、获取饮食运动建议、管理用药、解读检查报告。 ### 核心功能 - AI 智能问诊:基于大语言模型的健康咨询服务 - 健康数据管理:血压、心率、血糖、血氧、体重的记录与趋势分析 - 智能用药管理:AI 解析处方,自动生成用药计划和提醒 - 饮食识别分析:拍照即可识别食物种类、估算热量营养素 - 报告智能解读:上传检查报告,AI 自动提取指标并预解读 - 运动计划管理:制定和追踪每日运动目标 - 在线医生问诊:与签约医生进行远程咨询 ### 开发团队 由专业医疗团队与 AI 技术团队联合打造。 ### 技术支持 如遇到问题或有建议,请通过以下方式联系我们: - 在线客服:App 内「设置」→「意见反馈」 - 客服热线:400-xxx-xxxx(工作日 9:00-18:00) ### 版权声明 © 2025-2026 健康管家团队。保留所有权利。 本软件受中华人民共和国著作权法保护。''', }; return Scaffold( appBar: AppBar( backgroundColor: Colors.white, elevation: 0, leading: IconButton(icon: const Icon(Icons.chevron_left), onPressed: () => popRoute(ref)), title: Text(titles[type] ?? '', style: const TextStyle(color: Color(0xFF1A1A1A), fontWeight: FontWeight.w600)), centerTitle: true, ), body: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Text(contents[type] ?? '内容加载中...', style: const TextStyle(fontSize: 14, height: 1.8, color: Color(0xFF333333))), ), ); } } /// 设备管理(占位) class DeviceManagementPage extends ConsumerWidget { const DeviceManagementPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) => _empty(context, '设备管理', '暂无绑定设备'); } // ═══════════════════ 自定义滑动删除组件 ═══════════════════ class _SwipeDeleteTile extends StatefulWidget { final Widget child; final VoidCallback onDelete; final VoidCallback onTap; const _SwipeDeleteTile({super.key, required this.child, required this.onDelete, required this.onTap}); @override State<_SwipeDeleteTile> createState() => _SwipeDeleteTileState(); } class _SwipeDeleteTileState extends State<_SwipeDeleteTile> with SingleTickerProviderStateMixin { double _dx = 0; static const _maxSlide = 80.0; static const _threshold = 40.0; void _onDragUpdate(DragUpdateDetails d) { setState(() => _dx = (_dx + d.delta.dx).clamp(-_maxSlide, 0.0)); } void _onDragEnd(DragEndDetails d) { if (_dx < -_threshold) { setState(() => _dx = -_maxSlide); } else { setState(() => _dx = 0); } } void _onDelete() { widget.onDelete(); setState(() => _dx = 0); } void _closeSwipe() => setState(() => _dx = 0); @override Widget build(BuildContext context) { return Stack(children: [ Positioned.fill(child: Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), decoration: BoxDecoration(color: const Color(0xFFE53935), borderRadius: BorderRadius.circular(16)), child: const Align( alignment: Alignment.centerRight, child: Padding(padding: EdgeInsets.only(right: 20), child: Icon(Icons.delete_outline, color: Colors.white, size: 24)), ), )), GestureDetector( onTap: _dx < 0 ? () { widget.onDelete(); setState(() => _dx = 0); } : widget.onTap, onHorizontalDragUpdate: _onDragUpdate, onHorizontalDragEnd: _onDragEnd, child: Transform.translate(offset: Offset(_dx, 0), child: widget.child), ), ]); } } Widget _empty(BuildContext context, String title, String subtitle) => Center(child: Column(mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.inbox_outlined, size: 64, color: Colors.grey[300]), const SizedBox(height: 12), Text(subtitle, style: Theme.of(context).textTheme.bodyMedium), ]));