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_design_tokens.dart'; import '../core/navigation_provider.dart'; import '../providers/auth_provider.dart'; import '../providers/chat_provider.dart'; import '../providers/conversation_history_provider.dart'; import 'app_toast.dart'; import '../providers/data_providers.dart'; import 'drawer_shell.dart'; class HealthDrawer extends ConsumerWidget { const HealthDrawer({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final auth = ref.watch(authProvider); final latestHealth = ref.watch(latestHealthProvider); return DrawerShell( widthFactor: 0.9, showBackground: false, child: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ SliverToBoxAdapter( child: _DrawerHero( user: auth.user, latestHealth: latestHealth, ref: ref, ), ), SliverToBoxAdapter( child: Container( padding: const EdgeInsets.fromLTRB(14, 10, 14, 18), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(28)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _NavigationSection(ref: ref), const SizedBox(height: 14), const Divider( height: 1, thickness: 0.7, color: Color(0xFFE8ECF2), ), const SizedBox(height: 14), _HistorySection(ref: ref), ], ), ), ), ], ), ); } } class _DrawerHero extends StatelessWidget { final dynamic user; final AsyncValue> latestHealth; final WidgetRef ref; const _DrawerHero({ required this.user, required this.latestHealth, required this.ref, }); @override Widget build(BuildContext context) { final topPadding = MediaQuery.paddingOf(context).top; return Stack( children: [ Positioned.fill( child: Image.asset( 'assets/branding/drawer_background_v1.png', fit: BoxFit.cover, alignment: Alignment.topLeft, ), ), Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.white.withValues(alpha: 0.12), Colors.white.withValues(alpha: 0.35), Colors.white.withValues(alpha: 0.82), ], stops: const [0.0, 0.58, 1.0], ), ), ), ), Padding( padding: EdgeInsets.fromLTRB(16, topPadding + 16, 16, 12), child: Column( children: [ _AccountHeader(user: user, ref: ref), const SizedBox(height: 16), _HealthDashboard(latestHealth: latestHealth, ref: ref), ], ), ), ], ); } } class _AccountHeader extends StatelessWidget { final dynamic user; final WidgetRef ref; const _AccountHeader({required this.user, required this.ref}); @override Widget build(BuildContext context) { final name = (user?.name?.toString().isNotEmpty ?? false) ? user.name.toString() : '未设置昵称'; final phone = (user?.phone?.toString().isNotEmpty ?? false) ? user.phone.toString() : '未登录'; return Row( children: [ InkWell( onTap: () => pushRoute(ref, 'profile'), borderRadius: BorderRadius.circular(33), child: Container( width: 66, height: 66, decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), boxShadow: [ BoxShadow( color: const Color(0xFF7C5CFF).withValues(alpha: 0.16), blurRadius: 20, offset: const Offset(0, 10), ), ], ), child: const Icon( Icons.person_rounded, color: Color(0xFF94A3B8), size: 34, ), ), ), const SizedBox(width: 14), Expanded( child: InkWell( onTap: () => pushRoute(ref, 'profile'), borderRadius: BorderRadius.circular(14), child: Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( name, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 22, fontWeight: FontWeight.w900, color: AppColors.textPrimary, ), ), const SizedBox(height: 5), Text( phone, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textSecondary, ), ), ], ), ), ), ), _IconButton( icon: Icons.settings_rounded, onTap: () => pushRoute(ref, 'settings'), ), ], ); } } class _HealthDashboard extends StatelessWidget { final AsyncValue> latestHealth; final WidgetRef ref; const _HealthDashboard({required this.latestHealth, required this.ref}); @override Widget build(BuildContext context) { return _Panel( title: '健康仪表盘', trailing: _TextAction( label: '详情', light: true, onTap: () => pushRoute(ref, 'trend'), ), titleColor: Colors.white, backgroundGradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFF60A5FA), Color(0xFF8B5CF6), Color(0xFFF472B6)], ), child: latestHealth.when( data: (data) => _DashboardMetrics(data: data, ref: ref), loading: () => const SizedBox( height: 118, child: Center(child: CircularProgressIndicator(strokeWidth: 2)), ), error: (error, stackTrace) => _DashboardMetrics(data: const {}, ref: ref), ), ); } } class _DashboardMetrics extends StatelessWidget { final Map data; final WidgetRef ref; const _DashboardMetrics({required this.data, required this.ref}); @override Widget build(BuildContext context) { final items = [ _MetricInfo( label: '血压', value: _bpText(data['BloodPressure']), unit: 'mmHg', routeType: 'blood_pressure', ), _MetricInfo( label: '心率', value: _metricVal(data['HeartRate']), unit: 'bpm', routeType: 'heart_rate', ), _MetricInfo( label: '血糖', value: _metricVal(data['Glucose']), unit: 'mmol/L', routeType: 'glucose', ), _MetricInfo( label: '血氧', value: _metricVal(data['SpO2']), unit: '%', routeType: 'spo2', ), ]; return Row( children: [ for (var i = 0; i < items.length; i++) ...[ Expanded( child: _MetricTile( info: items[i], onTap: () => pushRoute(ref, 'trend', params: {'type': items[i].routeType}), ), ), if (i != items.length - 1) const SizedBox(width: 6), ], ], ); } static String _bpText(dynamic bp) { if (bp is Map) { final systolic = bp['systolic'] ?? bp['value'] ?? '--'; final diastolic = bp['diastolic']; if (diastolic == null) return systolic.toString(); return '$systolic/$diastolic'; } return '--'; } static String _metricVal(dynamic val, {String unit = ''}) { if (val == null) return '--'; dynamic raw = val; if (val is Map) raw = val['value'] ?? val['val'] ?? val['data']; if (raw == null) return '--'; final text = raw is num ? raw.toStringAsFixed(raw is double ? 1 : 0) : raw.toString(); if (text.isEmpty || text == '--') return '--'; return unit.isEmpty ? text : '$text $unit'; } } class _MetricTile extends StatelessWidget { final _MetricInfo info; final VoidCallback onTap; const _MetricTile({required this.info, required this.onTap}); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(18), child: Container( height: 100, padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 10), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.22), borderRadius: BorderRadius.circular(18), border: Border.all(color: Colors.white.withValues(alpha: 0.40)), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( height: 32, child: FittedBox( fit: BoxFit.scaleDown, child: Text( info.value, maxLines: 1, textAlign: TextAlign.center, style: const TextStyle( fontSize: 26, fontWeight: FontWeight.w900, color: Colors.white, ), ), ), ), if (info.unit != null) Text( info.unit!, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xE0FFFFFF), ), ), const SizedBox(height: 2), Text( info.label, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w800, color: Colors.white, ), ), ], ), ), ); } } class _NavigationSection extends StatelessWidget { final WidgetRef ref; const _NavigationSection({required this.ref}); @override Widget build(BuildContext context) { final items = [ _NavItem( icon: LucideIcons.folderHeart, title: '档案', route: 'healthArchive', colors: AppColors.healthGradient.colors, ), _NavItem( icon: LucideIcons.fileText, title: '报告', route: 'reports', colors: AppColors.reportGradient.colors, ), _NavItem( icon: LucideIcons.pill, title: '用药', route: 'medications', colors: AppColors.medicationGradient.colors, ), _NavItem( icon: LucideIcons.utensils, title: '饮食', route: 'dietRecords', colors: AppColors.dietGradient.colors, ), _NavItem( icon: LucideIcons.calendarDays, title: '日历', route: 'calendar', colors: const [Color(0xFFC4B5FD), Color(0xFF8B5CF6)], ), _NavItem( icon: LucideIcons.calendarCheck, title: '随访', route: 'followups', colors: const [Color(0xFFFBCFE8), Color(0xFFF9A8D4)], ), _NavItem( icon: LucideIcons.footprints, title: '运动', route: 'exercisePlan', colors: AppColors.exerciseGradient.colors, ), _NavItem( icon: LucideIcons.bluetooth, title: '设备', route: 'devices', colors: AppColors.deviceGradient.colors, ), ]; return _LightSection( title: '常用功能', child: GridView.builder( padding: EdgeInsets.zero, itemCount: items.length, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, mainAxisExtent: 82, mainAxisSpacing: 7, crossAxisSpacing: 8, ), itemBuilder: (context, index) { final item = items[index]; return _NavTile(item: item, onTap: () => pushRoute(ref, item.route)); }, ), ); } } class _NavTile extends StatelessWidget { final _NavItem item; final VoidCallback onTap; const _NavTile({required this.item, required this.onTap}); String get _title => switch (item.route) { 'healthArchive' => '健康档案', 'reports' => '报告管理', 'medications' => '用药管理', 'dietRecords' => '饮食记录', 'calendar' => '健康日历', 'followups' => '复查随访', 'exercisePlan' => '运动计划', 'devices' => '蓝牙设备', _ => item.title, }; List get _colors => switch (item.route) { 'healthArchive' => AppColors.healthGradient.colors, 'reports' => AppColors.reportGradient.colors, 'medications' => AppColors.medicationGradient.colors, 'dietRecords' => AppColors.dietGradient.colors, 'calendar' => const [AppColors.calendarLight, AppColors.calendar], 'followups' => const [AppColors.followupLight, AppColors.followup], 'exercisePlan' => AppColors.exerciseGradient.colors, 'devices' => AppColors.deviceGradient.colors, _ => item.colors, }; @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: AppRadius.mdBorder, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 2), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Container( width: 52, height: 52, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: _colors, ), borderRadius: AppRadius.mdBorder, boxShadow: [ BoxShadow( color: _colors.last.withValues(alpha: 0.18), blurRadius: 10, offset: const Offset(0, 5), ), ], ), child: Icon(item.icon, color: Colors.white, size: 26), ), const SizedBox(height: 6), Text( _title, maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w800, color: AppColors.textPrimary, height: 1.1, ), ), ], ), ), ); } } class _LightSection extends StatelessWidget { final String title; final Widget child; const _LightSection({required this.title, required this.child}); @override Widget build(BuildContext context) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 2), child: Row( children: [ Expanded( child: Text( title, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w800, color: AppColors.textPrimary, ), ), ), ], ), ), const SizedBox(height: 2), child, ], ); } class _Panel extends StatelessWidget { final String title; final Widget child; final Widget? trailing; final LinearGradient? backgroundGradient; final Color titleColor; const _Panel({ required this.title, required this.child, this.trailing, this.backgroundGradient, this.titleColor = AppColors.textPrimary, }); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( gradient: backgroundGradient ?? LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Colors.white.withValues(alpha: 0.92), const Color(0xFFF8F4FF).withValues(alpha: 0.84), ], ), borderRadius: AppRadius.xlBorder, boxShadow: AppShadows.soft, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( title, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w800, color: titleColor, ), ), ), ?trailing, ], ), const SizedBox(height: 12), child, ], ), ); } } class _TextAction extends StatelessWidget { final String label; final VoidCallback onTap; final bool light; const _TextAction({ required this.label, required this.onTap, this.light = false, }); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(999), child: Container( padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 6), decoration: BoxDecoration( color: light ? Colors.white.withValues(alpha: 0.24) : const Color(0xFFEDE9FE), borderRadius: BorderRadius.circular(999), border: light ? Border.all(color: Colors.white.withValues(alpha: 0.34)) : null, ), child: Text( label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w900, color: light ? Colors.white : Color(0xFF6D28D9), ), ), ), ); } } class _IconButton extends StatelessWidget { final IconData icon; final VoidCallback onTap; const _IconButton({required this.icon, required this.onTap}); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(18), child: Container( width: 42, height: 42, decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.84), borderRadius: BorderRadius.circular(18), border: Border.all(color: Colors.white, width: 1.2), ), child: Icon(icon, size: 22, color: const Color(0xFF475569)), ), ); } } class _MetricInfo { final String label; final String value; final String? unit; final String routeType; const _MetricInfo({ required this.label, required this.value, this.unit, required this.routeType, }); } class _NavItem { final IconData icon; final String title; final String route; final List colors; const _NavItem({ required this.icon, required this.title, required this.route, required this.colors, }); } class _HistorySection extends ConsumerStatefulWidget { final WidgetRef ref; const _HistorySection({required this.ref}); static const int _previewCount = 5; @override ConsumerState<_HistorySection> createState() => _HistorySectionState(); } class _HistorySectionState extends ConsumerState<_HistorySection> { String? _selectedId; bool _selecting = false; bool _deleting = false; void _enterSelect(String id) { setState(() { _selecting = true; _selectedId = id; }); } void _toggleSelect(String id) { setState(() { if (_selectedId == id) { _selectedId = null; _selecting = false; } else { _selectedId = id; } }); } void _exitSelect() { setState(() { _selecting = false; _selectedId = null; }); } @override Widget build(BuildContext context) { final async = ref.watch(conversationHistoryProvider); return _LightSection( title: '对话记录', child: async.when( loading: () => const Padding( padding: EdgeInsets.symmetric(vertical: 14), child: Center( child: SizedBox( width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2), ), ), ), error: (error, stackTrace) => const Padding( padding: EdgeInsets.symmetric(vertical: 14), child: Text( '加载失败', style: TextStyle(fontSize: 13, color: AppColors.textSecondary), ), ), data: (list) { if (list.isEmpty) { return const Padding( padding: EdgeInsets.symmetric(vertical: 12), child: Text( '暂无历史,发起对话后会显示在这里', style: TextStyle(fontSize: 13, color: AppColors.textSecondary), ), ); } final preview = list.take(_HistorySection._previewCount).toList(); return ConstrainedBox( constraints: const BoxConstraints(minHeight: 48), child: Stack( clipBehavior: Clip.none, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ for (var i = 0; i < preview.length; i++) _DrawerHistoryTile( item: preview[i], isLast: i == preview.length - 1, selecting: _selecting, selected: _selectedId == preview[i].id, onTap: () async { if (_selecting) { _toggleSelect(preview[i].id); return; } final error = await ref .read(chatProvider.notifier) .loadConversation(preview[i].id); if (!context.mounted) return; if (error != null) { AppToast.show( context, error, type: AppToastType.error, ); return; } Navigator.of(context).maybePop(); }, onLongPress: () => _enterSelect(preview[i].id), ), if (list.length > _HistorySection._previewCount) ...[ const SizedBox(height: 8), Align( alignment: Alignment.centerRight, child: InkWell( onTap: () { Navigator.of(context).maybePop(); pushRoute(ref, 'conversationHistory'); }, borderRadius: BorderRadius.circular(999), child: const Padding( padding: EdgeInsets.symmetric( horizontal: 8, vertical: 6, ), child: Text( '查看全部', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w800, color: AppColors.textSecondary, ), ), ), ), ), ], ], ), if (_selecting && _selectedId != null) Positioned( top: -46, right: 2, child: _HistorySelectionBar( deleting: _deleting, onCancel: _exitSelect, onDelete: () => _deleteSelected(context), ), ), ], ), ); }, ), ); } Future _deleteSelected(BuildContext context) async { final id = _selectedId; if (id == null || _deleting) return; setState(() => _deleting = true); try { await ref.read(conversationHistoryProvider.notifier).deleteOne(id); _exitSelect(); } catch (_) { if (context.mounted) { AppToast.show(context, '删除失败,请稍后重试', type: AppToastType.error); } } finally { if (mounted) setState(() => _deleting = false); } } } class _HistorySelectionBar extends StatelessWidget { final bool deleting; final VoidCallback onCancel; final VoidCallback onDelete; const _HistorySelectionBar({ required this.deleting, required this.onCancel, required this.onDelete, }); @override Widget build(BuildContext context) { return Align( alignment: Alignment.centerRight, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(18), border: Border.all(color: const Color(0xFFE5E7EB)), boxShadow: AppColors.cardShadowLight, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ _HistoryActionButton( icon: Icons.delete_outline_rounded, label: deleting ? '删除中' : '删除', color: AppColors.error, onTap: deleting ? null : onDelete, ), const SizedBox(width: 8), Container(width: 1, height: 22, color: const Color(0xFFE5E7EB)), const SizedBox(width: 8), _HistoryActionButton( icon: Icons.close_rounded, label: '取消', color: AppColors.textSecondary, onTap: onCancel, ), ], ), ), ); } } class _HistoryActionButton extends StatelessWidget { final IconData icon; final String label; final Color color; final VoidCallback? onTap; const _HistoryActionButton({ required this.icon, required this.label, required this.color, required this.onTap, }); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( icon, size: 18, color: onTap == null ? AppColors.textHint : color, ), const SizedBox(width: 5), Text( label, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w800, color: onTap == null ? AppColors.textHint : color, ), ), ], ), ), ); } } class _DrawerHistoryTile extends StatelessWidget { final ConversationListItem item; final bool isLast; final bool selecting; final bool selected; final VoidCallback onTap; final VoidCallback onLongPress; const _DrawerHistoryTile({ required this.item, required this.isLast, required this.selecting, required this.selected, required this.onTap, required this.onLongPress, }); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, onLongPress: onLongPress, borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.symmetric(horizontal: 2), decoration: selected ? BoxDecoration( color: const Color(0xFFF1F3F7), borderRadius: BorderRadius.circular(8), ) : null, child: Column( children: [ Padding( padding: const EdgeInsets.symmetric(vertical: 11), child: Row( children: [ Expanded( child: Text( _displaySummary(item), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w800, color: selected ? AppColors.textSecondary : AppColors.textPrimary, height: 1.2, ), ), ), const SizedBox(width: 10), Text( _shortDate(item.updatedAt), style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: AppColors.textHint, ), ), ], ), ), if (!isLast) const Divider( height: 1, thickness: 0.7, color: Color(0xFFE8ECF2), ), ], ), ), ); } static String _displayTitle(ConversationListItem item) { final s = item.summary?.trim(); if (s != null && s.isNotEmpty) return s; final t = item.title?.trim(); if (t != null && t.isNotEmpty) return t; return '未命名对话'; } static String _displaySummary(ConversationListItem item) { final s = item.summary?.trim(); if (s != null && s.isNotEmpty) return s.replaceAll(RegExp(r'\s+'), ' '); return _displayTitle(item); } static String _shortDate(DateTime time) { final now = DateTime.now(); final local = time.toLocal(); if (local.year == now.year && local.month == now.month && local.day == now.day) { return '今天'; } final yesterday = now.subtract(const Duration(days: 1)); if (local.year == yesterday.year && local.month == yesterday.month && local.day == yesterday.day) { return '昨天'; } return '${local.month}/${local.day}'; } }