274 lines
12 KiB
Dart
274 lines
12 KiB
Dart
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';
|
|
import '../providers/chat_provider.dart';
|
|
|
|
/// 侧滑抽屉——健康概览 + 历史对话 + 菜单
|
|
class HealthDrawer extends ConsumerWidget {
|
|
const HealthDrawer({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final auth = ref.watch(authProvider);
|
|
final user = auth.user;
|
|
final latestHealth = ref.watch(latestHealthProvider);
|
|
final conversations = ref.watch(conversationListProvider);
|
|
|
|
return Drawer(
|
|
child: SafeArea(
|
|
child: ListView(
|
|
padding: EdgeInsets.zero,
|
|
children: [
|
|
// 用户信息
|
|
Container(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
GestureDetector(
|
|
onTap: () => pushRoute(ref, 'profile'),
|
|
child: CircleAvatar(
|
|
radius: 28,
|
|
backgroundColor: const Color(0xFFEDEBFF),
|
|
child: Icon(Icons.person, size: 32, color: Theme.of(context).colorScheme.primary),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(user?.name ?? '未设置昵称', style: Theme.of(context).textTheme.titleMedium),
|
|
if (user != null) const SizedBox(height: 4),
|
|
Text(user?.phone ?? '', style: Theme.of(context).textTheme.labelMedium),
|
|
],
|
|
),
|
|
),
|
|
_DrawerItem(icon: Icons.settings, label: '设置', onTap: () => pushRoute(ref, 'settings')),
|
|
const Divider(),
|
|
|
|
// 健康概览——接真实数据
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
|
child: Text('健康概览', style: Theme.of(context).textTheme.labelMedium!.copyWith(fontWeight: FontWeight.w600)),
|
|
),
|
|
latestHealth.when(
|
|
data: (data) => Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
child: Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: [
|
|
_HealthMetricChip(icon: Icons.favorite, label: '血压', value: _bpText(data['BloodPressure']), onTap: () => pushRoute(ref, 'trend', params: {'type': 'blood_pressure'})),
|
|
_HealthMetricChip(icon: Icons.monitor_heart, label: '心率', value: _metricText(data['HeartRate'], ''), onTap: () => pushRoute(ref, 'trend', params: {'type': 'heart_rate'})),
|
|
_HealthMetricChip(icon: Icons.bloodtype, label: '血糖', value: _metricText(data['Glucose'], ''), onTap: () => pushRoute(ref, 'trend', params: {'type': 'glucose'})),
|
|
_HealthMetricChip(icon: Icons.air, label: '血氧', value: _metricText(data['SpO2'], '%'), onTap: () => pushRoute(ref, 'trend', params: {'type': 'spo2'})),
|
|
],
|
|
),
|
|
),
|
|
loading: () => const Padding(padding: EdgeInsets.all(16), child: Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)))),
|
|
error: (_, _) => Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
child: Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: [
|
|
const _HealthMetricChip(icon: Icons.favorite, label: '血压', value: '--'),
|
|
const _HealthMetricChip(icon: Icons.monitor_heart, label: '心率', value: '--'),
|
|
const _HealthMetricChip(icon: Icons.bloodtype, label: '血糖', value: '--'),
|
|
const _HealthMetricChip(icon: Icons.air, label: '血氧', value: '--'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
const Divider(),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
|
child: Text('功能', style: Theme.of(context).textTheme.labelMedium!.copyWith(fontWeight: FontWeight.w600)),
|
|
),
|
|
_DrawerItem(icon: Icons.description, label: '报告管理', onTap: () => pushRoute(ref, 'reports')),
|
|
_DrawerItem(icon: Icons.calendar_today, label: '健康日历', onTap: () => pushRoute(ref, 'calendar')),
|
|
_DrawerItem(icon: Icons.restaurant, label: '饮食记录', onTap: () => pushRoute(ref, 'dietRecords')),
|
|
_DrawerItem(icon: Icons.event_note, label: '复查随访', onTap: () => pushRoute(ref, 'followups')),
|
|
const Divider(),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
|
child: Row(children: [
|
|
Text('历史对话', style: Theme.of(context).textTheme.labelMedium!.copyWith(fontWeight: FontWeight.w600)),
|
|
const Spacer(),
|
|
TextButton(onPressed: () => ref.invalidate(conversationListProvider), child: const Text('刷新', style: TextStyle(fontSize: 12, color: Color(0xFF635BFF)))),
|
|
]),
|
|
),
|
|
conversations.when(
|
|
data: (items) {
|
|
if (items.isEmpty) {
|
|
return const Padding(padding: EdgeInsets.symmetric(vertical: 20), child: Center(child: Text('暂无历史对话', style: TextStyle(color: Color(0xFF999999), fontSize: 13))));
|
|
}
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: items.map((item) => _ConversationItem(item: item, ref: ref)).toList(),
|
|
),
|
|
);
|
|
},
|
|
loading: () => const Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
|
error: (_, __) => const Center(child: Text('加载失败', style: TextStyle(color: Color(0xFF999999), fontSize: 14))),
|
|
),
|
|
|
|
const Divider(),
|
|
_DrawerItem(icon: Icons.logout, label: '退出登录', onTap: () async {
|
|
final ok = await showDialog<bool>(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('确定'))]));
|
|
if (ok == true) { await ref.read(authProvider.notifier).logout(); goRoute(ref, 'login'); }
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
String _bpText(dynamic bp) {
|
|
if (bp == null) return '--';
|
|
if (bp is Map) return '${bp['systolic'] ?? '--'}/${bp['diastolic'] ?? '--'}';
|
|
return '--';
|
|
}
|
|
|
|
String _metricText(dynamic metric, String unit) {
|
|
if (metric == null) return '--';
|
|
if (metric is Map) {
|
|
final v = metric['value'];
|
|
return v != null ? '$v $unit' : '--';
|
|
}
|
|
return '--';
|
|
}
|
|
}
|
|
|
|
class _DrawerItem extends StatelessWidget {
|
|
final IconData icon; final String label; final VoidCallback onTap;
|
|
const _DrawerItem({required this.icon, required this.label, required this.onTap});
|
|
@override Widget build(BuildContext context) => ListTile(leading: Icon(icon, size: 20, color: const Color(0xFF666666)), title: Text(label, style: const TextStyle(fontSize: 16)), onTap: onTap, dense: true);
|
|
}
|
|
|
|
class _HealthMetricChip extends StatelessWidget {
|
|
final IconData icon;
|
|
final String label;
|
|
final String value;
|
|
final VoidCallback? onTap;
|
|
|
|
const _HealthMetricChip({required this.icon, required this.label, required this.value, this.onTap});
|
|
|
|
@override
|
|
Widget build(BuildContext context) => GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
width: 80,
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: const Color(0xFFEDEBFF)),
|
|
),
|
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
|
Icon(icon, size: 14, color: const Color(0xFF635BFF)),
|
|
const SizedBox(width: 4),
|
|
Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
|
|
Text(label, style: TextStyle(fontSize: 10, color: Colors.grey[600])),
|
|
Text(value, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
|
]),
|
|
]),
|
|
),
|
|
);
|
|
}
|
|
|
|
class _ConversationItem extends ConsumerWidget {
|
|
final ConversationItem item;
|
|
final WidgetRef ref;
|
|
const _ConversationItem({required this.item, required this.ref});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF8F7FF),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: ListTile(
|
|
leading: Container(
|
|
width: 32,
|
|
height: 32,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFEDEBFF),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(_getAgentIcon(item.agent), size: 16, color: const Color(0xFF635BFF)),
|
|
),
|
|
title: Text(item.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
|
|
subtitle: Text(item.lastMessage, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 10, color: Colors.grey[500])),
|
|
trailing: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(_formatTime(item.updatedAt), style: const TextStyle(fontSize: 9, color: Color(0xFFCCCCCC))),
|
|
PopupMenuButton<int>(
|
|
icon: const Icon(Icons.more_vert, size: 14, color: Color(0xFFCCCCCC)),
|
|
itemBuilder: (_) => [
|
|
const PopupMenuItem(value: 1, child: Text('继续聊')),
|
|
const PopupMenuItem(value: 2, child: Text('删除')),
|
|
],
|
|
onSelected: (v) async {
|
|
if (v == 1) {
|
|
ref.read(chatProvider.notifier).setAgent(item.agent);
|
|
Navigator.pop(context);
|
|
} else if (v == 2) {
|
|
final ok = await showDialog<bool>(
|
|
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('确定')),
|
|
],
|
|
),
|
|
);
|
|
if (ok == true) {
|
|
ref.invalidate(conversationListProvider);
|
|
}
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
onTap: () {
|
|
ref.read(chatProvider.notifier).setAgent(item.agent);
|
|
Navigator.pop(context);
|
|
},
|
|
dense: true,
|
|
),
|
|
);
|
|
}
|
|
|
|
IconData _getAgentIcon(ActiveAgent agent) {
|
|
switch (agent) {
|
|
case ActiveAgent.health: return Icons.health_and_safety;
|
|
case ActiveAgent.diet: return Icons.restaurant;
|
|
case ActiveAgent.medication: return Icons.medication;
|
|
case ActiveAgent.report: return Icons.file_open;
|
|
case ActiveAgent.exercise: return Icons.directions_run;
|
|
case ActiveAgent.consultation: return Icons.chat;
|
|
default: return Icons.chat_bubble_outline;
|
|
}
|
|
}
|
|
|
|
String _formatTime(DateTime time) {
|
|
final now = DateTime.now();
|
|
final diff = now.difference(time);
|
|
if (diff.inMinutes < 60) return '${diff.inMinutes}分钟前';
|
|
if (diff.inHours < 24) return '${diff.inHours}小时前';
|
|
if (diff.inDays < 7) return '${diff.inDays}天前';
|
|
return '${time.month}/${time.day}';
|
|
}
|
|
}
|