feat: 侧边栏重设计 - 彩色分区卡片+动画入场
This commit is contained in:
@@ -3,7 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'dart:io';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../providers/chat_provider.dart';
|
||||
import '../../providers/data_providers.dart';
|
||||
@@ -18,49 +17,18 @@ class HomePage extends ConsumerStatefulWidget {
|
||||
class _HomePageState extends ConsumerState<HomePage> {
|
||||
final _textCtrl = TextEditingController();
|
||||
final _scrollCtrl = ScrollController();
|
||||
bool _taskCardsExpanded = true;
|
||||
String? _pickedImagePath;
|
||||
double _lastScrollOffset = 0;
|
||||
DateTime? _lastCollapseTime;
|
||||
bool _exerciseDone = false;
|
||||
final Set<ActiveAgent> _welcomedAgents = {};
|
||||
|
||||
static final _mockFollowUps = [
|
||||
{'hospital': '协和医院', 'department': '心内科', 'date': DateTime.now().add(const Duration(days: 2)), 'type': '复查'},
|
||||
{'hospital': '人民医院', 'department': '心内科', 'date': DateTime.now().add(const Duration(days: 3)), 'type': '复诊'},
|
||||
];
|
||||
|
||||
@override void initState() { super.initState(); _scrollCtrl.addListener(_onScroll); _textCtrl.addListener(_onTextChange); }
|
||||
@override void initState() { super.initState(); }
|
||||
@override void dispose() { _textCtrl.dispose(); _scrollCtrl.dispose(); super.dispose(); }
|
||||
|
||||
void _onTextChange() {
|
||||
if (_textCtrl.text.isNotEmpty && _taskCardsExpanded) {
|
||||
setState(() => _taskCardsExpanded = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (!_scrollCtrl.hasClients) return;
|
||||
final offset = _scrollCtrl.offset;
|
||||
if (offset < _lastScrollOffset && _taskCardsExpanded) {
|
||||
final delta = _lastScrollOffset - offset;
|
||||
if (delta > 50) {
|
||||
final now = DateTime.now();
|
||||
if (_lastCollapseTime == null || now.difference(_lastCollapseTime!) > const Duration(seconds: 2)) {
|
||||
_lastCollapseTime = now;
|
||||
setState(() => _taskCardsExpanded = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
_lastScrollOffset = offset;
|
||||
}
|
||||
|
||||
void _sendMessage() {
|
||||
final text = _textCtrl.text.trim();
|
||||
final imagePath = _pickedImagePath;
|
||||
if (text.isEmpty && imagePath == null) return;
|
||||
_textCtrl.clear();
|
||||
setState(() { _taskCardsExpanded = false; _pickedImagePath = null; });
|
||||
setState(() => _pickedImagePath = null);
|
||||
if (imagePath != null) {
|
||||
ref.read(chatProvider.notifier).sendImage(imagePath, text);
|
||||
} else {
|
||||
@@ -131,229 +99,6 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
return '晚上好';
|
||||
}
|
||||
|
||||
// ═════════════════════ 今日任务(可折叠/展开) ═════════════════════
|
||||
|
||||
Widget _buildTaskCardsArea() {
|
||||
final latestHealth = ref.watch(latestHealthProvider);
|
||||
|
||||
if (_taskCardsExpanded) {
|
||||
return latestHealth.when(
|
||||
data: (data) => _taskCardContent(data),
|
||||
loading: () => _taskCardContent({}),
|
||||
error: (_, __) => _taskCardContent({}),
|
||||
);
|
||||
}
|
||||
|
||||
// 折叠状态:与展开态容器完全相同,只保留标题行
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _taskCardsExpanded = true),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(8), blurRadius: 6, offset: const Offset(0, 1))]),
|
||||
child: Row(children: [
|
||||
const Text('今日任务', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
const Spacer(),
|
||||
const Text('展开 ▾', style: TextStyle(fontSize: 12, color: Color(0xFF8B9CF7))),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _taskCardContent(Map<String, dynamic> healthData) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(8), blurRadius: 6, offset: const Offset(0, 1))]),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
const Text('今日任务', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
const Spacer(),
|
||||
GestureDetector(onTap: () => setState(() => _taskCardsExpanded = false), child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Text('收起', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
const Text('∧', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
])),
|
||||
]),
|
||||
const SizedBox(height: 10),
|
||||
..._getTodayTasks(healthData),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _getTodayTasks(Map<String, dynamic> healthData) {
|
||||
final now = DateTime.now();
|
||||
final tasks = <Widget>[];
|
||||
|
||||
// 1. 数据摘要卡片(有今日指标数据时显示)
|
||||
final summaryParts = <String>[];
|
||||
final bp = healthData['BloodPressure'];
|
||||
if (bp is Map) {
|
||||
final s = bp['systolic'];
|
||||
final d = bp['diastolic'];
|
||||
if (s != null && d != null) summaryParts.add('血压 $s/$d');
|
||||
}
|
||||
final hr = healthData['HeartRate'];
|
||||
if (hr is int) summaryParts.add('心率 $hr');
|
||||
final bs = healthData['BloodSugar'];
|
||||
if (bs is num) summaryParts.add('血糖 $bs');
|
||||
final bo = healthData['BloodOxygen'];
|
||||
if (bo is num) summaryParts.add('血氧 $bo');
|
||||
final wt = healthData['Weight'];
|
||||
if (wt is num) summaryParts.add('体重 $wt');
|
||||
|
||||
if (summaryParts.isNotEmpty) {
|
||||
tasks.add(_summaryCard(summaryParts));
|
||||
}
|
||||
|
||||
// 2. 用药提醒(从后端拉取真实数据)
|
||||
final reminders = ref.watch(medicationReminderProvider);
|
||||
reminders.whenData((meds) {
|
||||
for (final m in meds) {
|
||||
final name = m['name'] ?? '';
|
||||
final dosage = m['dosage'] ?? '';
|
||||
final times = (m['timeOfDay'] as List?)?.map((t) => t.toString().substring(0, 5)).join(', ') ?? '';
|
||||
final medOverdue = now.hour >= 8;
|
||||
tasks.add(_taskRow(
|
||||
icon: Icons.medication_rounded,
|
||||
label: '$name $dosage ($times)',
|
||||
status: 'pending',
|
||||
isOverdue: medOverdue,
|
||||
onTap: () => _handleMedicationCheck,
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
// 无提醒时显示默认用药卡片
|
||||
if (tasks.length <= (summaryParts.isNotEmpty ? 1 : 0)) {
|
||||
tasks.add(_taskRow(
|
||||
icon: Icons.medication_rounded,
|
||||
label: '暂无用药提醒',
|
||||
status: 'pending',
|
||||
onTap: null,
|
||||
));
|
||||
}
|
||||
|
||||
// 3. 运动卡片(超时变红)
|
||||
final exOverdue = now.hour >= 18 && !_exerciseDone;
|
||||
tasks.add(_taskRow(
|
||||
icon: Icons.directions_run,
|
||||
label: '今日待运动:散步 30 分钟',
|
||||
status: _exerciseDone ? 'done' : 'pending',
|
||||
isOverdue: exOverdue,
|
||||
onTap: _exerciseDone ? null : () => setState(() => _exerciseDone = true),
|
||||
));
|
||||
|
||||
// 4. 测量卡片
|
||||
tasks.add(_taskRow(
|
||||
icon: Icons.today,
|
||||
label: '今日测量:血压',
|
||||
status: 'pending',
|
||||
onTap: () => _textCtrl.text = '血压 ',
|
||||
));
|
||||
|
||||
// 5. 异常指标
|
||||
tasks.addAll(_buildAbnormalRows(healthData));
|
||||
|
||||
// 6. 复查提醒(未来3天内有复查安排时显示)
|
||||
final upcomingFollowUps = _mockFollowUps.where((f) {
|
||||
final date = f['date'] as DateTime;
|
||||
return date.difference(now).inDays <= 3 && date.isAfter(now);
|
||||
}).toList();
|
||||
|
||||
if (upcomingFollowUps.isNotEmpty) {
|
||||
tasks.add(_followUpCard(upcomingFollowUps.first));
|
||||
}
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
List<Widget> _buildAbnormalRows(Map<String, dynamic> healthData) {
|
||||
final rows = <Widget>[];
|
||||
final bp = healthData['BloodPressure'];
|
||||
if (bp is Map) { final s = bp['systolic']; if (s is int && s >= 140) rows.add(_taskRow(icon: Icons.warning_amber_rounded, label: '血压 $s/${bp['diastolic'] ?? '--'} 偏高', status: 'warning', onTap: () => pushRoute(ref, 'trend', params: {'type': 'blood_pressure'}))); }
|
||||
return rows;
|
||||
}
|
||||
|
||||
Widget _summaryCard(List<String> parts) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => pushRoute(ref, 'trend'),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F8E9),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(children: [
|
||||
const Icon(Icons.check_circle, size: 18, color: Color(0xFF43A047)),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: Text(
|
||||
'今日已记录:${parts.join('、')}',
|
||||
style: const TextStyle(fontSize: 13, color: Color(0xFF333333)),
|
||||
)),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _followUpCard(Map<String, dynamic> followUp) {
|
||||
final date = followUp['date'] as DateTime;
|
||||
final now = DateTime.now();
|
||||
final diff = date.difference(now).inDays;
|
||||
String dateLabel;
|
||||
if (diff == 0) {
|
||||
dateLabel = '今天';
|
||||
} else if (diff == 1) {
|
||||
dateLabel = '明天';
|
||||
} else if (diff == 2) {
|
||||
dateLabel = '后天';
|
||||
} else {
|
||||
dateLabel = '$diff天后';
|
||||
}
|
||||
|
||||
return _taskRow(
|
||||
icon: Icons.event_available,
|
||||
label: '📋 $dateLabel ${followUp['hospital']} ${followUp['department']} ${followUp['type']}',
|
||||
status: 'pending',
|
||||
onTap: () => pushRoute(ref, 'followups'),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _taskRow({required IconData icon, required String label, required String status, VoidCallback? onTap, bool isOverdue = false}) {
|
||||
final colors = {'done': const Color(0xFF43A047), 'warning': const Color(0xFFFF9800), 'pending': const Color(0xFF9E9E9E)};
|
||||
final icons = {'done': Icons.check_circle, 'warning': Icons.warning, 'pending': Icons.circle_outlined};
|
||||
final effectiveStatus = isOverdue ? 'warning' : status;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isOverdue ? const Color(0xFFFFEBEE) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
|
||||
Container(width: 30, height: 30, decoration: BoxDecoration(color: const Color(0xFFF0F2FF), borderRadius: BorderRadius.circular(8)), child: Icon(icon, size: 15, color: const Color(0xFF8B9CF7))),
|
||||
const SizedBox(width: 10), Expanded(child: Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF333333)))),
|
||||
Icon(icons[effectiveStatus], size: 18, color: colors[effectiveStatus] ?? Colors.grey),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleMedicationCheck() async {
|
||||
await ref.read(medicationServiceProvider).confirm('');
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已记录服药 ✅'), backgroundColor: Color(0xFF8B9CF7)));
|
||||
}
|
||||
|
||||
// ═════════════════════ 智能体选择条(常驻) ═════════════════════
|
||||
|
||||
static final _agentDefs = [
|
||||
@@ -372,7 +117,7 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: _agentDefs.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 6),
|
||||
separatorBuilder: (_, i) => const SizedBox(width: 6),
|
||||
itemBuilder: (_, i) {
|
||||
final (agent, label, icon) = _agentDefs[i];
|
||||
final isActive = selected == agent;
|
||||
|
||||
Reference in New Issue
Block a user