import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import '../core/app_colors.dart'; import '../core/app_theme.dart'; import '../core/navigation_provider.dart'; import '../providers/auth_provider.dart'; import '../providers/data_providers.dart'; import '../providers/omron_device_provider.dart'; import '../widgets/common_widgets.dart'; /// 饮食记录列表(趋势+日历+胶囊详情+侧滑编辑删除) class DietRecordListPage extends ConsumerStatefulWidget { const DietRecordListPage({super.key}); @override ConsumerState createState() => _DietRecordListPageState(); } class _DietRecordListPageState extends ConsumerState { List> _data = []; bool _loading = true; int _trendDays = 7; DateTime _selectedDate = DateTime.now(); @override void initState() { super.initState(); _refresh(); } Future _refresh() async { final records = await ref.read(dietServiceProvider).getRecords(); if (mounted) setState(() { _data = records; _loading = false; }); } Future _delete(String id) async { await ref.read(dietServiceProvider).deleteRecord(id); if (mounted) setState(() => _data.removeWhere((d) => d['id']?.toString() == id)); } String _dateKey(DateTime d) => '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; int _dayCalories(DateTime d) { final key = _dateKey(d); return _data.where((r) => (r['recordedAt']?.toString() ?? '').startsWith(key)).fold(0, (s, r) => s + ((r['totalCalories'] as num?)?.toInt() ?? 0)); } List> _dayRecords(DateTime d) { final key = _dateKey(d); return _data.where((r) => (r['recordedAt']?.toString() ?? '').startsWith(key)).toList(); } List _trendData() => List.generate(_trendDays, (i) => _dayCalories(DateTime.now().subtract(Duration(days: _trendDays - 1 - i))).toDouble()); @override Widget build(BuildContext context) { if (_loading) return Scaffold(backgroundColor: const Color(0xFFF5F5F5), appBar: AppBar(backgroundColor: Colors.white, elevation: 0, title: const Text('饮食记录', style: TextStyle(color: AppColors.textPrimary)), leading: IconButton(icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary), onPressed: () => popRoute(ref))), body: const Center(child: CircularProgressIndicator())); final todayRecords = _dayRecords(_selectedDate); final totalCal = todayRecords.fold(0, (s, r) => s + ((r['totalCalories'] as num?)?.toInt() ?? 0)); return Scaffold( backgroundColor: const Color(0xFFF5F5F5), appBar: AppBar(backgroundColor: Colors.white, elevation: 0, title: const Text('饮食记录', style: TextStyle(color: AppColors.textPrimary)), leading: IconButton(icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary), onPressed: () => popRoute(ref))), body: ListView(padding: const EdgeInsets.all(16), children: [ // 热量趋势 Container(padding: const EdgeInsets.all(14), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [const Text('热量趋势', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), const Spacer(), _Toggle('7天', _trendDays == 7, () => setState(() => _trendDays = 7)), const SizedBox(width: 6), _Toggle('30天', _trendDays == 30, () => setState(() => _trendDays = 30)), ]), const SizedBox(height: 10), SizedBox(height: 120, child: _TrendChart(_trendData())), ])), const SizedBox(height: 12), // 饮食日历 Container(padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('饮食日历', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), const SizedBox(height: 8), SingleChildScrollView(scrollDirection: Axis.horizontal, child: Row(children: List.generate(7, (i) { final d = DateTime.now().subtract(Duration(days: 6 - i)); final cal = _dayCalories(d); final isToday = _dateKey(d) == _dateKey(DateTime.now()); final isSel = _dateKey(d) == _dateKey(_selectedDate); return GestureDetector(onTap: () => setState(() => _selectedDate = d), child: Container(width: 42, margin: const EdgeInsets.symmetric(horizontal: 3), padding: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration(color: isSel ? AppColors.primary : (isToday ? AppColors.primary.withOpacity(0.1) : Colors.white), borderRadius: BorderRadius.circular(12), border: Border.all(color: isSel ? AppColors.primary : (isToday ? AppColors.primary : const Color(0xFFE5E7EB)))), child: Column(children: [ Text(['一','二','三','四','五','六','日'][d.weekday-1], style: TextStyle(fontSize: 10, color: isSel ? Colors.white : (isToday ? AppColors.primary : AppColors.textHint))), Text('${d.day}', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: isSel ? Colors.white : (isToday ? AppColors.primary : AppColors.textPrimary))), if (cal > 0) Text('$cal', style: TextStyle(fontSize: 9, color: isSel ? Colors.white70 : AppColors.textHint)), ]))); }))), ])), const SizedBox(height: 12), // 当日详情 Row(children: [ Text('${_selectedDate.month}月${_selectedDate.day}日', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700)), const SizedBox(width: 8), Text('共 $totalCal 千卡', style: const TextStyle(fontSize: 14, color: AppColors.textHint)), const Spacer(), Text('${todayRecords.length}条', style: const TextStyle(fontSize: 13, color: AppColors.textHint)), ]), const SizedBox(height: 8), if (todayRecords.isEmpty) const Padding(padding: EdgeInsets.all(20), child: Center(child: Text('当天暂无记录', style: TextStyle(color: AppColors.textHint)))), ...todayRecords.map((d) { final items = (d['foodItems'] as List?)?.cast>() ?? []; final mealNames = {'Breakfast': '早餐', 'Lunch': '午餐', 'Dinner': '晚餐', 'Snack': '加餐'}; final mealIcons = {'Breakfast': Icons.wb_sunny, 'Lunch': Icons.wb_sunny_outlined, 'Dinner': Icons.nights_stay, 'Snack': Icons.cookie}; final mealLabel = mealNames[d['mealType']?.toString()] ?? ''; final mealIcon = mealIcons[d['mealType']?.toString()] ?? Icons.restaurant; final cal = d['totalCalories'] ?? 0; final id = d['id']?.toString() ?? ''; return Padding(padding: const EdgeInsets.only(bottom: 8), child: ClipRRect(borderRadius: BorderRadius.circular(14), child: _SwipeAction( onEdit: () => _showEditDialog(id, cal), onDelete: () => _delete(id), child: Container(padding: const EdgeInsets.all(14), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)), child: Row(children: [ Container(width: 40, height: 40, decoration: BoxDecoration(color: AppColors.iconBg, borderRadius: BorderRadius.circular(12)), child: Icon(mealIcon, size: 20, color: AppColors.primary)), const SizedBox(width: 12), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [Text(mealLabel, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), const SizedBox(width: 8), Text('$cal 千卡', style: const TextStyle(fontSize: 13, color: AppColors.textSecondary))]), if (items.isNotEmpty) Padding(padding: const EdgeInsets.only(top: 3), child: Text(items.map((f) => f['name']).join(' · '), style: const TextStyle(fontSize: 13, color: AppColors.textHint), maxLines: 1, overflow: TextOverflow.ellipsis)), ])), ])), ))); }), ]), ); } void _showEditDialog(String id, num cal) { final ctrl = TextEditingController(text: '$cal'); showDialog(context: context, builder: (ctx) => AlertDialog( title: const Text('修改热量'), content: TextField(controller: ctrl, keyboardType: TextInputType.number, decoration: const InputDecoration(labelText: '热量(千卡)')), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消')), TextButton(onPressed: () async { Navigator.pop(ctx); await ref.read(dietServiceProvider).updateRecord(id, {'totalCalories': int.tryParse(ctrl.text) ?? 0}); _refresh(); }, child: const Text('保存')), ], )); } } class _Toggle extends StatelessWidget { final String l; final bool a; final VoidCallback t; const _Toggle(this.l, this.a, this.t); @override Widget build(BuildContext c) => GestureDetector(onTap: t, child: Container(padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration(color: a ? AppColors.primary : Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: a ? AppColors.primary : const Color(0xFFE5E7EB))), child: Text(l, style: TextStyle(fontSize: 12, color: a ? Colors.white : AppColors.textSecondary)))); } class _TrendChart extends StatelessWidget { final List data; const _TrendChart(this.data); @override Widget build(BuildContext c) { if (data.isEmpty) return const Center(child: Text('暂无数据', style: TextStyle(color: AppColors.textHint))); final m = data.reduce((a, b) => a > b ? a : b); if (m == 0) return const Center(child: Text('暂无数据', style: TextStyle(color: AppColors.textHint))); return Row(crossAxisAlignment: CrossAxisAlignment.end, children: List.generate(data.length, (i) { final h = (data[i] / m * 100).clamp(4.0, 100.0); return Expanded(child: Padding(padding: EdgeInsets.symmetric(horizontal: data.length > 15 ? 1.0 : 2.0), child: Column(mainAxisAlignment: MainAxisAlignment.end, children: [ if (data.length <= 14) Text('${data[i].toInt()}', style: const TextStyle(fontSize: 8, color: AppColors.textHint)), const SizedBox(height: 2), Container(height: h, decoration: BoxDecoration(color: data[i] > 0 ? AppColors.primary : const Color(0xFFE5E7EB), borderRadius: BorderRadius.circular(2))), ]))); })); } } class _SwipeAction extends StatelessWidget { final Widget child; final VoidCallback onEdit; final VoidCallback onDelete; const _SwipeAction({required this.child, required this.onEdit, required this.onDelete}); @override Widget build(BuildContext c) => Dismissible( key: UniqueKey(), direction: DismissDirection.endToStart, confirmDismiss: (_) async { return false; }, background: Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), color: const Color(0xFF3B82F6)), alignment: Alignment.centerLeft, padding: const EdgeInsets.only(left: 20), child: const Icon(Icons.edit, color: Colors.white)), secondaryBackground: Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), color: AppColors.error), alignment: Alignment.centerRight, padding: const EdgeInsets.only(right: 20), child: const Icon(Icons.delete, color: Colors.white)), onDismissed: (_) => onDelete(), onUpdate: (details) { if (details.reached && details.direction == DismissDirection.startToEnd) { onEdit(); } }, child: child, ); } /// 饮食记录详情页(保留原功能)""" 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 GradientScaffold( appBar: AppBar(leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), title: const Text('饮食详情')), body: FutureBuilder>>( future: service.getRecords(), builder: (ctx, snap) { if (!snap.hasData) return const Center(child: CircularProgressIndicator(color: AppTheme.primary)); 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(AppTheme.rMd)), child: Row(children: [ Container(width: 48, height: 48, decoration: BoxDecoration(color: AppColors.iconBg, borderRadius: BorderRadius.circular(AppTheme.rMd)), child: const Icon(Icons.restaurant, size: 28, color: AppTheme.primary)), const SizedBox(width: 14), Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(mealNames[d['mealType']?.toString()] ?? '', style: const TextStyle(fontSize: 21, fontWeight: FontWeight.w700)), Text('$totalCal 千卡 · ${d['recordedAt'] ?? ''}', style: const TextStyle(fontSize: 16, color: AppColors.textHint)), ]), ])), 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(AppTheme.rMd)), child: Row(children: [ Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(f['name']?.toString() ?? '', style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600)), if ((f['portion']?.toString() ?? '').isNotEmpty) Text(f['portion']!.toString(), style: const TextStyle(fontSize: 16, color: AppColors.textSecondary)), ])), Text('${f['calories'] ?? 0} kcal', style: const TextStyle(fontSize: 18, color: AppTheme.primary, 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); ref.invalidate(currentExercisePlanProvider); _load(); } @override Widget build(BuildContext context) { return GradientScaffold( appBar: AppBar(leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), title: const Text('运动计划')), floatingActionButton: FloatingActionButton(onPressed: () { pushRoute(ref, 'exerciseCreate'); }, backgroundColor: AppTheme.primary, child: const Icon(Icons.add)), body: Container( decoration: const BoxDecoration(gradient: AppColors.bgGradient), child: 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>() ?? []; if (items.isEmpty) return const SizedBox.shrink(); final total = items.length; final done = items.where((it) => it['isCompleted'] == true).length; final weekStart = p['weekStartDate']?.toString() ?? ''; // 用 DayOfWeek 匹配今天(C#: 0=Sun, 6=Sat; Dart weekday%7 对齐) final todayCsDow = DateTime.now().weekday % 7; final todayItem = items.cast>().firstWhere( (it) => it['dayOfWeek'] == todayCsDow, orElse: () => {}, ); final hasTodayItem = todayItem.isNotEmpty; final todayDone = todayItem['isCompleted'] == true; final exerciseName = items.isNotEmpty ? (items.first['exerciseType']?.toString() ?? '运动') : '运动'; return SwipeDeleteTile( key: Key(p['id']?.toString() ?? '$i'), onDelete: () => _deletePlan(p['id']?.toString() ?? ''), onTap: () {}, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(AppTheme.rMd)), child: Row(children: [ Container(width: 44, height: 44, decoration: BoxDecoration(color: AppColors.iconBg, borderRadius: BorderRadius.circular(AppTheme.rMd)), child: const Icon(Icons.directions_run, size: 25, color: AppTheme.primary)), const SizedBox(width: 12), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(exerciseName, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600)), const SizedBox(height: 2), Text('$weekStart 起 · ${items.first['durationMinutes'] ?? 0}分钟/天', style: const TextStyle(fontSize: 16, color: AppColors.textSecondary)), const SizedBox(height: 6), Text('$done/$total 天已完成', style: const TextStyle(fontSize: 15, color: AppTheme.primary)), ])), GestureDetector( onTap: hasTodayItem ? () => _checkIn(p['id']?.toString() ?? '', todayItem['id']?.toString() ?? '') : null, child: Container( width: 40, height: 40, decoration: BoxDecoration( color: todayDone ? AppColors.successLight : AppColors.cardInner, borderRadius: BorderRadius.circular(12), ), child: Icon(todayDone ? Icons.check_circle : Icons.check_circle_outline, size: 28, color: todayDone ? AppTheme.success : AppColors.textHint), ), ), ]), ), ); }); }, ), ), ); } } /// 新建运动计划 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) { popRoute(ref); } } @override Widget build(BuildContext context) { return GradientScaffold( appBar: AppBar(leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), 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: AppTheme.primary, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rMd)), padding: const EdgeInsets.symmetric(vertical: 14)), child: const Text('保存计划', style: TextStyle(fontSize: 19)))), ]), ); } Widget _field(String label, TextEditingController ctrl, {String? hint, bool number = false}) { return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: TextStyle(fontSize: 17, color: AppColors.textSecondary)), 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(10), borderSide: BorderSide(color: AppColors.border)), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: AppColors.primary, width: 1.5))), style: const TextStyle(fontSize: 17)), ]); } Widget _dateField(String label, DateTime val, ValueChanged onChanged) { return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(fontSize: 17, color: AppColors.textSecondary)), 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: 19))), ), ]); } } /// 复查列表(从服务器读取) 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 GradientScaffold( appBar: AppBar(leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), title: const Text('复查随访'), centerTitle: true), body: FutureBuilder>>( future: _future, builder: (ctx, snap) { if (snap.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator(color: AppTheme.primaryLight)); } final list = snap.data ?? []; if (list.isEmpty) { return Center( child: Column(mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.event_note_outlined, size: 64, color: AppColors.textHint), const SizedBox(height: 12), Text('暂无随访安排', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 4), const Text('医生创建随访后将显示在这里', style: TextStyle(fontSize: 16, color: AppColors.textHint)), ]), ); } 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 AppColors.primary; case 'Completed': return AppTheme.success; case 'Cancelled': return AppTheme.error; default: return AppColors.textHint; } } Color _statusBg(String? status) { switch (status) { case 'Upcoming': return AppColors.iconBg; case 'Completed': return AppColors.successLight; case 'Cancelled': return AppColors.errorLight; default: return AppColors.backgroundSoft; } } @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(AppTheme.rMd), boxShadow: [AppTheme.shadowCard], ), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(AppTheme.rSm)), child: Text(label, style: TextStyle(fontSize: 15, color: color, fontWeight: FontWeight.w500)), ), const Spacer(), if (item['doctorName'] != null) Text('👨‍⚕️ ${item['doctorName']}', style: const TextStyle(fontSize: 15, color: AppColors.textHint)), ]), const SizedBox(height: 12), Text(item['title']?.toString() ?? '', style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600)), const SizedBox(height: 4), if (item['scheduledAt'] != null) Text(_formatDateTime(item['scheduledAt']?.toString()), style: const TextStyle(fontSize: 17, color: AppColors.textHint)), if ((item['notes']?.toString() ?? '').isNotEmpty) ...[ const SizedBox(height: 8), Text(item['notes']?.toString() ?? '', style: const TextStyle(fontSize: 16, color: AppColors.textSecondary)), ], ]), ); } 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(); String _birthDate = ''; final _diagnosisCtrl = TextEditingController(); final List> _surgeries = []; final _allergiesCtrl = TextEditingController(); final _chronicCtrl = TextEditingController(); final _dietCtrl = TextEditingController(); final _familyCtrl = TextEditingController(); bool _loading = true; bool _saving = false; @override void initState() { super.initState(); _load(); } @override void dispose() { _nameCtrl.dispose(); _genderCtrl.dispose(); _diagnosisCtrl.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'] ?? ''; _birthDate = p['birthDate'] ?? ''; } final a = await srv.getHealthArchive(); if (a != null && mounted) { _diagnosisCtrl.text = a['diagnosis'] ?? ''; final st = a['surgeryType'] as String?; final sd = a['surgeryDate'] as String?; if (st != null && st.isNotEmpty) _surgeries.add({'type': st, 'date': sd ?? ''}); _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 _pickDate(void Function(String) onPicked) async { final now = DateTime.now(); final d = await showDatePicker( context: context, initialDate: DateTime(now.year - 30), firstDate: DateTime(1900), lastDate: now, locale: const Locale('zh'), ); if (d != null) { onPicked('${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'); } } void _addSurgery() => setState(() => _surgeries.add({'type': '', 'date': ''})); void _removeSurgery(int i) => setState(() => _surgeries.removeAt(i)); Future _save() async { setState(() => _saving = true); try { final srv = ref.read(userServiceProvider); await srv.updateProfile(name: _nameCtrl.text, gender: _genderCtrl.text, birthDate: _birthDate); final surgeryType = _surgeries.map((s) => s['type'] ?? '').where((s) => s.isNotEmpty).join('、'); final surgeryDate = _surgeries.map((s) => s['date'] ?? '').where((s) => s.isNotEmpty).join('、'); await srv.updateHealthArchive({ 'diagnosis': _diagnosisCtrl.text, 'surgeryType': surgeryType, 'surgeryDate': surgeryDate, '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: AppColors.success)); } } catch (_) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('保存失败,请重试'), backgroundColor: AppColors.error)); } } finally { if (mounted) setState(() => _saving = false); } } @override Widget build(BuildContext context) { if (_loading) { return Scaffold(backgroundColor: const Color(0xFFF5F5F5), appBar: AppBar(backgroundColor: Colors.white, elevation: 0, title: const Text('健康档案', style: TextStyle(color: AppColors.textPrimary)), leading: IconButton(icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary), onPressed: () => popRoute(ref))), body: const Center(child: CircularProgressIndicator())); } return Scaffold( backgroundColor: const Color(0xFFF5F5F5), appBar: AppBar(backgroundColor: Colors.white, elevation: 0, title: const Text('健康档案', style: TextStyle(color: AppColors.textPrimary)), leading: IconButton(icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary), onPressed: () => popRoute(ref))), body: ListView(padding: const EdgeInsets.all(16), children: [ _Section('个人资料', Icons.person_outline, [ _F('姓名', _nameCtrl, hint: '请输入姓名'), const SizedBox(height: 12), _F('性别', _genderCtrl, hint: '男/女'), const SizedBox(height: 12), _DateF('出生日期', _birthDate, () => _pickDate((v) => setState(() => _birthDate = v))), ]), const SizedBox(height: 12), _Section('医疗信息', Icons.local_hospital_outlined, [ _F('主要诊断', _diagnosisCtrl, hint: '如: 冠心病'), const SizedBox(height: 14), Row(children: [const Text('手术史', style: TextStyle(fontSize: 14, color: AppColors.textSecondary)), const Spacer(), _AddBtn(() => _addSurgery())]), ..._surgeries.asMap().entries.map((e) { final i = e.key; final s = e.value; return Padding(padding: const EdgeInsets.only(top: 8), child: Row(children: [ Expanded(child: _F2('手术', s['type'] ?? '', (v) => s['type'] = v, hint: '如: 心脏搭桥')), const SizedBox(width: 8), Expanded(child: _DateF2('日期', s['date'] ?? '', () => _pickDate((v) => s['date'] = v), hint: '点击选择')), GestureDetector(onTap: () => _removeSurgery(i), child: Container(width: 32, height: 32, decoration: BoxDecoration(color: AppColors.errorLight, borderRadius: BorderRadius.circular(8)), child: const Icon(Icons.close, size: 16, color: AppColors.error))), ])); }), ]), const SizedBox(height: 12), _Section('病史与限制', Icons.warning_amber_outlined, [ _F('过敏史', _allergiesCtrl, hint: '多个用、分隔,如: 青霉素、海鲜'), const SizedBox(height: 12), _F('慢性病史', _chronicCtrl, hint: '多个用、分隔,如: 高血压、高血脂'), const SizedBox(height: 12), _F('饮食限制', _dietCtrl, hint: '多个用、分隔,如: 低盐、低脂'), const SizedBox(height: 12), _F('家族病史', _familyCtrl, hint: '如: 父亲冠心病'), ]), const SizedBox(height: 24), Container( decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), gradient: AppColors.primaryGradient), padding: const EdgeInsets.all(1.5), child: SizedBox(width: double.infinity, height: 52, child: ElevatedButton( onPressed: _saving ? null : _save, style: ElevatedButton.styleFrom(backgroundColor: Colors.white, foregroundColor: AppColors.primary, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(13)), elevation: 0), child: Text(_saving ? '保存中...' : '保存档案', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600)), )), ), const SizedBox(height: 40), ]), ); } } class _Section extends StatelessWidget { final String title; final IconData icon; final List fields; const _Section(this.title, this.icon, this.fields); @override Widget build(BuildContext c) => Container(padding: const EdgeInsets.all(18), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFE5E7EB))), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Container(width: 36, height: 36, decoration: BoxDecoration(color: AppColors.iconBg, borderRadius: BorderRadius.circular(10)), child: Icon(icon, size: 20, color: AppColors.primary)), const SizedBox(width: 10), Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: AppColors.textPrimary)), ]), const SizedBox(height: 16), ...fields, ])); } class _F extends StatelessWidget { final String label; final TextEditingController ctrl; final String? hint; const _F(this.label, this.ctrl, {this.hint}); @override Widget build(BuildContext c) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(fontSize: 14, color: AppColors.textSecondary)), const SizedBox(height: 6), TextField(controller: ctrl, style: const TextStyle(fontSize: 16), decoration: InputDecoration(hintText: hint, hintStyle: const TextStyle(fontSize: 14, color: AppColors.textHint), filled: true, fillColor: const Color(0xFFF5F5F5), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: AppColors.primary)))), ]); } class _F2 extends StatelessWidget { final String label; final String value; final void Function(String) chg; final String? hint; const _F2(this.label, this.value, this.chg, {this.hint}); @override Widget build(BuildContext c) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)), const SizedBox(height: 4), TextField( controller: TextEditingController(text: value), onChanged: chg, style: const TextStyle(fontSize: 14), decoration: InputDecoration(hintText: hint, hintStyle: const TextStyle(fontSize: 13, color: AppColors.textHint), filled: true, fillColor: const Color(0xFFF5F5F5), contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: AppColors.primary))), ), ]); } class _DateF extends StatelessWidget { final String label; final String value; final VoidCallback onTap; const _DateF(this.label, this.value, this.onTap); @override Widget build(BuildContext c) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(fontSize: 14, color: AppColors.textSecondary)), const SizedBox(height: 6), GestureDetector(onTap: onTap, child: Container(height: 48, padding: const EdgeInsets.symmetric(horizontal: 14), decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(10)), alignment: Alignment.centerLeft, child: Row(children: [ Text(value.isEmpty ? '点击选择日期' : value, style: TextStyle(fontSize: 16, color: value.isEmpty ? AppColors.textHint : AppColors.textPrimary)), const Spacer(), const Icon(Icons.calendar_today, size: 18, color: AppColors.textHint), ]))), ]); } class _DateF2 extends StatelessWidget { final String label; final String value; final VoidCallback onTap; final String? hint; const _DateF2(this.label, this.value, this.onTap, {this.hint}); @override Widget build(BuildContext c) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)), const SizedBox(height: 4), GestureDetector(onTap: onTap, child: Container(height: 44, padding: const EdgeInsets.symmetric(horizontal: 10), decoration: BoxDecoration(color: const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(8)), alignment: Alignment.centerLeft, child: Row(children: [ Text(value.isEmpty ? (hint ?? '点击选择') : value, style: TextStyle(fontSize: 13, color: value.isEmpty ? AppColors.textHint : AppColors.textPrimary)), const Spacer(), const Icon(Icons.calendar_today, size: 14, color: AppColors.textHint), ]))), ]); } class _AddBtn extends StatelessWidget { final VoidCallback onTap; const _AddBtn(this.onTap); @override Widget build(BuildContext c) { return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: const Color(0xFFF0F0FF), borderRadius: BorderRadius.circular(8), ), child: const Row(mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.add, size: 14, color: AppColors.primary), SizedBox(width: 2), Text('添加', style: TextStyle(fontSize: 13, color: AppColors.primary)), ]), ), ); } } /// 健康日历 class HealthCalendarPage extends ConsumerStatefulWidget { const HealthCalendarPage({super.key}); @override ConsumerState createState() => _HealthCalendarPageState(); } class _HealthCalendarPageState extends ConsumerState { DateTime _currentMonth = DateTime.now(); DateTime? _selectedDate; Map>> _events = {}; Map? _selectedDayData; static const _medBlue = Color(0xFF0000FF); static const _exGreen = Color(0xFF00FF00); static const _fupOrange = Color(0xFFFFA500); @override void initState() { super.initState(); _loadMonth(); _selectDate(DateTime.now()); } String _dateKey(DateTime d) => '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; 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; final types = (map['events'] as List?)?.cast() ?? []; events[map['date'] as String] = types.map((t) => {'type': t}).toList(); } if (mounted) setState(() => _events = events); // 如果有选中的日期,刷新详情 if (_selectedDate != null) _selectDate(_selectedDate!); } catch (_) {} } void _selectDate(DateTime d) { setState(() { _selectedDate = d; _selectedDayData = null; }); final key = _dateKey(d); final details = _events[key]; if (details != null && details.isNotEmpty) { // 从后端拉详细数据 _loadDayDetail(d); } } Future _loadDayDetail(DateTime d) async { try { final api = ref.read(apiClientProvider); final res = await api.get('/api/calendar/day', queryParameters: { 'date': _dateKey(d), }); if (mounted) setState(() => _selectedDayData = res.data['data'] as Map?); } catch (_) {} } @override Widget build(BuildContext context) { final today = DateTime.now(); final isToday = _selectedDate != null && _dateKey(_selectedDate!) == _dateKey(today); return Scaffold( backgroundColor: const Color(0xFFF5F5F5), appBar: AppBar(backgroundColor: Colors.white, elevation: 0, title: const Text('健康日历', style: TextStyle(color: AppColors.textPrimary)), leading: IconButton(icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary), onPressed: () => popRoute(ref))), body: Column(children: [ Container(color: Colors.white, child: Column(children: [ _buildMonthHeader(), _buildWeekdayHeader(), _buildCalendarGrid(), ])), const SizedBox(height: 12), // 图例 Padding(padding: const EdgeInsets.symmetric(horizontal: 16), child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ _Legend('用药提醒', _medBlue), const SizedBox(width: 20), _Legend('运动计划', _exGreen), const SizedBox(width: 20), _Legend('复查随访', _fupOrange), ])), const SizedBox(height: 12), // 当日详情 Expanded(child: _buildDayDetail()), ]), ); } Widget _buildMonthHeader() => Padding(padding: const EdgeInsets.symmetric(horizontal: 8), child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton(icon: const Icon(Icons.chevron_left), 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), onPressed: () { setState(() => _currentMonth = DateTime(_currentMonth.year, _currentMonth.month + 1)); _loadMonth(); }), ])); Widget _buildWeekdayHeader() { const wd = ['日', '一', '二', '三', '四', '五', '六']; return Row(children: wd.map((d) => Expanded(child: Center(child: Text(d, style: const TextStyle(fontSize: 14, color: AppColors.textHint))))).toList()); } Widget _buildCalendarGrid() { final first = DateTime(_currentMonth.year, _currentMonth.month, 1); final last = DateTime(_currentMonth.year, _currentMonth.month + 1, 0); final dim = last.day; final sw = first.weekday % 7; final days = List.generate(42, (i) { final di = i - sw; return di < 0 || di >= dim ? null : di + 1; }); final today = DateTime.now(); return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 7), itemCount: 42, itemBuilder: (_, i) { final day = days[i]; if (day == null) return const SizedBox(); final d = DateTime(_currentMonth.year, _currentMonth.month, day); final isToday = _dateKey(d) == _dateKey(today); final isSel = _selectedDate != null && _dateKey(d) == _dateKey(_selectedDate!); final evs = _events[_dateKey(d)] ?? []; return GestureDetector( onTap: () => _selectDate(d), child: Container( margin: const EdgeInsets.all(2), decoration: BoxDecoration( color: isSel ? AppColors.primary.withOpacity(0.08) : (isToday ? AppColors.primary.withOpacity(0.05) : Colors.white), borderRadius: BorderRadius.circular(12), border: isSel ? Border.all(color: AppColors.primary, width: 1.5) : (isToday ? Border.all(color: AppColors.primary.withOpacity(0.3), width: 1) : null), ), child: Stack(alignment: Alignment.center, children: [ Text('$day', style: TextStyle(fontSize: 16, color: isSel ? AppColors.primary : AppColors.textPrimary, fontWeight: isSel || isToday ? FontWeight.w600 : FontWeight.normal)), if (evs.isNotEmpty) Positioned(bottom: 4, child: Row(children: evs.take(3).map((e) => Container(width: 5, height: 5, margin: const EdgeInsets.symmetric(horizontal: 1), decoration: BoxDecoration(color: _dotColor(e['type']?.toString() ?? ''), shape: BoxShape.circle))).toList())), ]), ), ); }, ); } Color _dotColor(String t) => switch (t) { 'medication' => _medBlue, 'exercise' => _exGreen, 'followup' => _fupOrange, _ => AppColors.textHint }; Widget _buildDayDetail() { if (_selectedDate == null) return const Center(child: Text('点击日期查看当天安排', style: TextStyle(color: AppColors.textHint))); if (_selectedDayData == null) { final evs = _events[_dateKey(_selectedDate!)] ?? []; if (evs.isEmpty) return Center(child: Column(mainAxisSize: MainAxisSize.min, children: [const Icon(Icons.event_available, size: 40, color: AppColors.textHint), const SizedBox(height: 8), Text('${_selectedDate!.month}月${_selectedDate!.day}日无事', style: const TextStyle(color: AppColors.textHint))])); } final data = _selectedDayData; if (data == null) return Center(child: Column(mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.event_available, size: 40, color: AppColors.textHint), const SizedBox(height: 8), Text('${_selectedDate!.month}月${_selectedDate!.day}日无事', style: const TextStyle(color: AppColors.textHint)), ])); final meds = (data['medications'] as List?)?.cast>() ?? []; final exs = (data['exercises'] as List?)?.cast>() ?? []; final fups = (data['followUps'] as List?)?.cast>() ?? []; if (meds.isEmpty && exs.isEmpty && fups.isEmpty) { return Center(child: Column(mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.event_available, size: 40, color: AppColors.textHint), const SizedBox(height: 8), Text('${_selectedDate!.month}月${_selectedDate!.day}日无事', style: const TextStyle(color: AppColors.textHint)), ])); } return ListView(padding: const EdgeInsets.symmetric(horizontal: 16), children: [ // 用药 if (meds.isNotEmpty) ...[ _SectionTitle('用药提醒', _medBlue, Icons.medication), ...meds.map((m) => _EventCard('${m['name'] ?? ''} ${m['dosage'] ?? ''}', '${m['timeOfDay'] ?? ''}', _medBlue)), ], // 运动 if (exs.isNotEmpty) ...[ _SectionTitle('运动计划', _exGreen, Icons.fitness_center), ...exs.map((e) => _EventCard('${e['type'] ?? ''}', '${e['duration'] ?? 0}分钟', _exGreen)), ], // 随访 if (fups.isNotEmpty) ...[ _SectionTitle('复查随访', _fupOrange, Icons.event_note), ...fups.map((f) => _EventCard(f['title'] ?? '', f['doctorName'] ?? '', _fupOrange)), ], ]); } } class _SectionTitle extends StatelessWidget { final String title; final Color color; final IconData icon; const _SectionTitle(this.title, this.color, this.icon); @override Widget build(BuildContext c) => Padding(padding: const EdgeInsets.only(top: 12, bottom: 6), child: Row(children: [ Container(width: 4, height: 16, decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(2))), const SizedBox(width: 8), Icon(icon, size: 16, color: color), const SizedBox(width: 6), Text(title, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: color)), ])); } class _EventCard extends StatelessWidget { final String title; final String sub; final Color color; const _EventCard(this.title, this.sub, this.color); @override Widget build(BuildContext c) => Container( margin: const EdgeInsets.only(bottom: 6), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFE5E7EB))), child: Row(children: [ Container(width: 8, height: 8, decoration: BoxDecoration(color: color, shape: BoxShape.circle)), const SizedBox(width: 10), Expanded(child: Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500))), Text(sub, style: const TextStyle(fontSize: 12, color: AppColors.textHint)), ]), ); } class _Legend extends StatelessWidget { final String label; final Color color; const _Legend(this.label, this.color); @override Widget build(BuildContext c) => Row(children: [ Container(width: 8, height: 8, decoration: BoxDecoration(color: color, shape: BoxShape.circle)), const SizedBox(width: 4), Text(label, style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)), ]); } /// 静态文本页 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 GradientScaffold( appBar: AppBar( backgroundColor: Colors.white, elevation: 0, leading: IconButton(icon: const Icon(Icons.chevron_left), onPressed: () => popRoute(ref)), title: Text(titles[type] ?? '', style: TextStyle(color: AppColors.textPrimary, fontWeight: FontWeight.w600)), centerTitle: true, ), body: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Text(contents[type] ?? '内容加载中...', style: TextStyle(fontSize: 17, height: 1.8, color: AppColors.textPrimary)), ), ); } } /// 旧版设备管理页,已移至 device/device_management_page.dart class _DeviceManagementPageLegacy extends ConsumerWidget { const _DeviceManagementPageLegacy({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final device = ref.watch(omronDeviceProvider); return GradientScaffold( appBar: AppBar( backgroundColor: AppColors.cardBackground, title: const Text('蓝牙血压计'), ), body: device.isBound ? _buildBoundCard(context, ref, device) : _buildEmptyState(context, ref), ); } Widget _buildEmptyState(BuildContext context, WidgetRef ref) => Center( child: Padding( padding: const EdgeInsets.all(32), child: Column(mainAxisSize: MainAxisSize.min, children: [ Container( width: 88, height: 88, decoration: BoxDecoration( color: AppColors.iconBg, borderRadius: BorderRadius.circular(28), ), child: Icon(Icons.bluetooth_disabled, size: 44, color: AppColors.textHint), ), const SizedBox(height: 20), const Text('未绑定设备', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), const SizedBox(height: 8), const Text('连接欧姆龙血压计,自动同步测量数据', style: TextStyle(fontSize: 15, color: AppColors.textHint)), const SizedBox(height: 28), SizedBox( width: 220, height: 52, child: ElevatedButton.icon( onPressed: () => pushRoute(ref, 'deviceScan'), icon: const Icon(Icons.add, size: 22), label: const Text('添加设备', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600)), style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rMd)), ), ), ), ]), ), ); Widget _buildBoundCard(BuildContext context, WidgetRef ref, DeviceBindState device) => SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column(children: [ // 设备信息卡片 Container( width: double.infinity, padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: AppColors.cardBackground, borderRadius: BorderRadius.circular(AppTheme.rMd), boxShadow: AppColors.cardShadow, ), child: Column(children: [ Container( width: 72, height: 72, decoration: BoxDecoration( gradient: device.isConnected ? AppColors.primaryGradient : const LinearGradient(colors: [Color(0xFF9CA3AF), Color(0xFF6B7280)]), borderRadius: BorderRadius.circular(AppTheme.rMd), ), child: Icon( device.isConnected ? Icons.bluetooth_connected : Icons.bluetooth_disabled, size: 36, color: Colors.white, ), ), const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( color: device.isConnected ? const Color(0xFFD1FAE5) : const Color(0xFFFEE2E2), borderRadius: BorderRadius.circular(12), ), child: Text( device.isConnected ? '已连接' : '未连接', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: device.isConnected ? const Color(0xFF059669) : const Color(0xFFDC2626)), ), ), const SizedBox(height: 12), Text(device.name ?? '血压计', style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: AppColors.textPrimary)), const SizedBox(height: 6), Text('MAC: ${device.mac ?? '—'}', style: const TextStyle(fontSize: 14, color: AppColors.textHint)), if (device.lastSync != null) ...[ const SizedBox(height: 4), Text('上次同步: ${device.lastSync}', style: const TextStyle(fontSize: 13, color: AppColors.textHint)), ], const SizedBox(height: 24), if (device.isConnected) ...[ // 已连接:显示断开和解绑 Row(children: [ Expanded( child: OutlinedButton.icon( onPressed: () => _unbind(context, ref), icon: const Icon(Icons.delete_outline, size: 18, color: AppColors.error), label: const Text('解绑', style: TextStyle(color: AppColors.error)), style: OutlinedButton.styleFrom( side: const BorderSide(color: AppColors.error), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rMd)), padding: const EdgeInsets.symmetric(vertical: 14), ), ), ), ]), ] else ...[ // 未连接:显示重新连接和解绑 SizedBox( width: double.infinity, height: 52, child: ElevatedButton.icon( onPressed: () => pushRoute(ref, 'deviceScan'), icon: const Icon(Icons.bluetooth_searching, size: 20), label: const Text('重新连接设备', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600)), style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rMd)), ), ), ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: () => _unbind(context, ref), icon: const Icon(Icons.delete_outline, size: 18, color: AppColors.error), label: const Text('解绑设备', style: TextStyle(color: AppColors.error)), style: OutlinedButton.styleFrom( side: const BorderSide(color: AppColors.error), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rMd)), padding: const EdgeInsets.symmetric(vertical: 14), ), ), ), ], ]), ), // 最后读数卡片 if (device.lastReading != null) ...[ const SizedBox(height: 16), Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.cardBackground, borderRadius: BorderRadius.circular(AppTheme.rMd), boxShadow: AppColors.cardShadow, ), child: Column(children: [ Row(children: [ Container( width: 6, height: 18, decoration: BoxDecoration( gradient: AppColors.primaryGradient, borderRadius: BorderRadius.circular(3), ), ), const SizedBox(width: 8), const Text('最近一次测量', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), ]), const SizedBox(height: 16), Row(children: [ Container( width: 56, height: 56, decoration: BoxDecoration( color: AppColors.iconBg, borderRadius: BorderRadius.circular(AppTheme.rMd), ), child: const Text('🩺', style: TextStyle(fontSize: 28)), ), const SizedBox(width: 16), Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(device.lastReading!.display, style: const TextStyle(fontSize: 32, fontWeight: FontWeight.w800, color: AppColors.textPrimary, letterSpacing: -0.5)), Text('mmHg', style: TextStyle(fontSize: 14, color: AppColors.textHint)), ]), const Spacer(), if (device.lastReading!.pulse != null) Column(crossAxisAlignment: CrossAxisAlignment.end, children: [ Text('${device.lastReading!.pulse}', style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: AppColors.primary)), const Text('bpm', style: TextStyle(fontSize: 13, color: AppColors.textHint)), ]), ]), ]), ), ], // 使用说明 const SizedBox(height: 16), Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.iconBg, borderRadius: BorderRadius.circular(AppTheme.rMd), ), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Icon(Icons.lightbulb_outline, size: 18, color: AppColors.primary), const SizedBox(width: 8), const Text('使用说明', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), ]), const SizedBox(height: 10), _guideItem('1', '血压计装好电池,绑好袖带'), _guideItem('2', '血压计按开始键开始测量'), _guideItem('3', '测量完成后数据自动同步到 App'), _guideItem('4', '打开此页面时保持蓝牙开启'), ]), ), ]), ); Widget _guideItem(String num, String text) => Padding( padding: const EdgeInsets.only(bottom: 6), child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('$num.', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.primary)), const SizedBox(width: 6), Expanded(child: Text(text, style: const TextStyle(fontSize: 14, color: AppColors.textSecondary))), ]), ); void _unbind(BuildContext context, WidgetRef ref) async { final ok = await showDialog(context: context, builder: (ctx) => AlertDialog( title: const Text('解绑设备'), content: const Text('解绑后需重新扫描连接,确定吗?'), actions: [ TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')), TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定解绑', style: TextStyle(color: AppColors.error))), ], )); if (ok == true) { await ref.read(omronDeviceProvider.notifier).unbind(); } } } Widget _empty(BuildContext context, String title, String subtitle) => Center(child: Column(mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.inbox_outlined, size: 64, color: AppColors.textHint), const SizedBox(height: 12), Text(subtitle, style: Theme.of(context).textTheme.bodyMedium), ]));