fix: 图片发送/医生加载/运动超时/用药黑屏/服药打卡

- sendImage: 本地预览→上传→远程URL替换
- doctorListProvider: 8s超时+mock医生fallback
- currentExercisePlanProvider: 8s超时→显示空状态
- 用药编辑: try-catch防黑屏+刷新列表
- 服药打卡: 接入后端confirm()接口
This commit is contained in:
MingNian
2026-06-03 20:03:17 +08:00
parent 95bf5732f6
commit e3b9716f7c
11 changed files with 916 additions and 393 deletions

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
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';
@@ -18,6 +19,7 @@ class _HomePageState extends ConsumerState<HomePage> {
final _textCtrl = TextEditingController();
final _scrollCtrl = ScrollController();
bool _taskCardsExpanded = true;
String? _pickedImagePath;
double _lastScrollOffset = 0;
DateTime? _lastCollapseTime;
bool _exerciseDone = false;
@@ -55,10 +57,15 @@ class _HomePageState extends ConsumerState<HomePage> {
void _sendMessage() {
final text = _textCtrl.text.trim();
if (text.isEmpty) return;
final imagePath = _pickedImagePath;
if (text.isEmpty && imagePath == null) return;
_textCtrl.clear();
setState(() => _taskCardsExpanded = false);
ref.read(chatProvider.notifier).sendMessage(text);
setState(() { _taskCardsExpanded = false; _pickedImagePath = null; });
if (imagePath != null) {
ref.read(chatProvider.notifier).sendImage(imagePath, text);
} else {
ref.read(chatProvider.notifier).sendMessage(text);
}
}
@override Widget build(BuildContext context) {
@@ -67,6 +74,16 @@ class _HomePageState extends ConsumerState<HomePage> {
final user = auth.user;
final selectedAgent = ref.watch(selectedAgentProvider);
ref.listen(cameraActionProvider, (prev, next) {
if (next == 'camera') {
_pickImage(ImageSource.camera);
ref.read(cameraActionProvider.notifier).clear();
} else if (next == 'gallery') {
_pickImage(ImageSource.gallery);
ref.read(cameraActionProvider.notifier).clear();
}
});
return Scaffold(
drawer: const HealthDrawer(),
backgroundColor: const Color(0xFFF8F7FF),
@@ -130,19 +147,19 @@ class _HomePageState extends ConsumerState<HomePage> {
);
}
// 折叠状态:只显示一行可点击的标题
// 折叠状态:与展开态容器完全相同,只保留标题
return GestureDetector(
onTap: () => setState(() => _taskCardsExpanded = true),
behavior: HitTestBehavior.opaque,
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(mainAxisSize: MainAxisSize.min, children: [
const Text('今日任务', style: TextStyle(fontSize: 13, color: Color(0xFF635BFF))),
const SizedBox(width: 4),
const Text('', style: TextStyle(fontSize: 12, color: Color(0xFF635BFF))),
]),
),
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(0xFF635BFF).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(0xFF635BFF))),
]),
),
);
}
@@ -301,27 +318,11 @@ class _HomePageState extends ConsumerState<HomePage> {
dateLabel = '$diff天后';
}
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => pushRoute(ref, 'followups'),
child: Row(children: [
Container(
width: 30, height: 30,
decoration: BoxDecoration(
color: const Color(0xFFF5F3FF),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.event_available, size: 15, color: Color(0xFF635BFF)),
),
const SizedBox(width: 10),
Expanded(child: Text(
'📋 $dateLabel ${followUp['hospital']} ${followUp['department']} ${followUp['type']}',
style: const TextStyle(fontSize: 13, color: Color(0xFF333333)),
)),
]),
),
return _taskRow(
icon: Icons.event_available,
label: '📋 $dateLabel ${followUp['hospital']} ${followUp['department']} ${followUp['type']}',
status: 'pending',
onTap: () => pushRoute(ref, 'followups'),
);
}
@@ -340,7 +341,7 @@ class _HomePageState extends ConsumerState<HomePage> {
color: isOverdue ? const Color(0xFFFFEBEE) : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Row(children: [
child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
Container(width: 30, height: 30, decoration: BoxDecoration(color: const Color(0xFFF5F3FF), borderRadius: BorderRadius.circular(8)), child: Icon(icon, size: 15, color: const Color(0xFF635BFF))),
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),
@@ -381,11 +382,14 @@ class _HomePageState extends ConsumerState<HomePage> {
return GestureDetector(
onTap: () {
final notifier = ref.read(selectedAgentProvider.notifier);
final newAgent = isActive ? null : agent;
notifier.select(newAgent);
if (newAgent != null) {
ref.read(chatProvider.notifier).setAgent(newAgent);
ref.read(chatProvider.notifier).insertAgentWelcome(newAgent);
if (isActive) {
notifier.select(null);
} else {
notifier.select(agent);
ref.read(chatProvider.notifier).setAgent(agent);
if (_welcomedAgents.add(agent)) {
ref.read(chatProvider.notifier).insertAgentWelcome(agent);
}
}
},
child: Container(
@@ -414,83 +418,60 @@ class _HomePageState extends ConsumerState<HomePage> {
// 智能体胶囊栏常驻高度36
_buildAgentBar(selectedAgent),
// 输入框(紧凑
// 图片预览(有选中图片时显示
if (_pickedImagePath != null) _buildImagePreview(),
// 输入框
_buildCompactInputBar(context),
]);
}
Widget _buildCompactAgentPanel(ActiveAgent agent) {
final titles = {ActiveAgent.consultation: 'AI 问诊', ActiveAgent.health: '记数据', ActiveAgent.diet: '拍饮食', ActiveAgent.medication: '药管家', ActiveAgent.report: '看报告', ActiveAgent.exercise: '运动计划'};
final tips = {ActiveAgent.consultation: '或直接对我说你的症状', ActiveAgent.health: '或直接对我说:"血压 135/85"', ActiveAgent.diet: '或直接对我说:"中午吃了牛肉面"', ActiveAgent.medication: '或直接对我说:"医生让我吃阿托伐他汀 20mg"', ActiveAgent.report: '或直接上传报告图片', ActiveAgent.exercise: '或直接对我说:"每周一三五散步 30 分钟"'};
Widget _buildImagePreview() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: BoxDecoration(color: Colors.white, border: Border(top: BorderSide(color: const Color(0xFFEEEEEE)))),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Row(children: [
Text(titles[agent] ?? '', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
const SizedBox(width: 6),
Expanded(child: Text(tips[agent] ?? '', style: TextStyle(fontSize: 10, color: Colors.grey[500]))),
GestureDetector(onTap: () => ref.read(selectedAgentProvider.notifier).select(null), child: Icon(Icons.close, size: 16, color: Colors.grey[400])),
]),
const SizedBox(height: 4),
SingleChildScrollView(scrollDirection: Axis.horizontal, child: Row(children: _getAgentButtons(agent))),
]));
}
Widget _buildCompactInputBar(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(color: Colors.white, border: Border(top: BorderSide(color: const Color(0xFFEEEEEE)))),
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
decoration: const BoxDecoration(color: Colors.white, border: Border(top: BorderSide(color: Color(0xFFEEEEEE)))),
child: Row(children: [
IconButton(icon: const Icon(Icons.attach_file, size: 20, color: Color(0xFF666666)), onPressed: () => _showAttachmentPicker(context), padding: const EdgeInsets.all(4)),
Expanded(child: TextField(controller: _textCtrl, decoration: InputDecoration(hintText: '输入你想说的...', contentPadding: const EdgeInsets.symmetric(horizontal: 8), border: InputBorder.none, isDense: true, hintStyle: const TextStyle(fontSize: 13)), onSubmitted: (_) => _sendMessage())),
IconButton(icon: const Icon(Icons.send, size: 20, color: Color(0xFF635BFF)), onPressed: _sendMessage, padding: const EdgeInsets.all(4)),
Stack(children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(File(_pickedImagePath!), width: 60, height: 60, fit: BoxFit.cover),
),
Positioned(top: -4, right: -4, child: GestureDetector(
onTap: () => setState(() => _pickedImagePath = null),
child: Container(width: 20, height: 20, decoration: const BoxDecoration(color: Color(0xFF333333), shape: BoxShape.circle), child: const Icon(Icons.close, size: 14, color: Colors.white)),
)),
]),
const Spacer(),
Text('点击发送上传图片', style: TextStyle(fontSize: 12, color: Colors.grey[500])),
]),
);
}
List<Widget> _getAgentButtons(ActiveAgent agent) {
switch (agent) {
case ActiveAgent.health: return [_agentBtn('录入血压', Icons.favorite), _agentBtn('录入血糖', Icons.bloodtype), _agentBtn('录入心率', Icons.monitor_heart), _agentBtn('录入血氧', Icons.air), _agentBtn('录入体重', Icons.monitor_weight)];
case ActiveAgent.diet: return [_agentBtn('拍照识别', Icons.camera_alt), _agentBtn('上传照片', Icons.photo_library)];
case ActiveAgent.medication: return [_agentBtn('用药管理', Icons.medication), _agentBtn('用药提醒', Icons.alarm)];
case ActiveAgent.consultation: return [_agentBtn('找医生', Icons.person_search)];
case ActiveAgent.exercise: return [_agentBtn('本周计划', Icons.calendar_view_week), _agentBtn('新建计划', Icons.add_circle_outline)];
default: return [];
}
}
Widget _agentBtn(String label, IconData icon) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: ElevatedButton.icon(
onPressed: () => _onAgentAction(label),
icon: Icon(icon, size: 14),
label: Text(label, style: const TextStyle(fontSize: 12)),
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFF5F3FF), foregroundColor: const Color(0xFF635BFF), elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12)),
),
Widget _buildCompactInputBar(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(color: Colors.white, border: Border(top: BorderSide(color: const Color(0xFFEEEEEE)))),
child: Row(children: [
IconButton(icon: const Icon(Icons.attach_file, size: 24, color: Color(0xFF666666)), onPressed: () => _showAttachmentPicker(context)),
Expanded(child: TextField(
controller: _textCtrl,
style: const TextStyle(fontSize: 15),
decoration: const InputDecoration(hintText: '输入你想说的...', hintStyle: TextStyle(fontSize: 15, color: Color(0xFFBBBBBB)), contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), border: InputBorder.none),
onSubmitted: (_) => _sendMessage(),
)),
IconButton(icon: const Icon(Icons.send, size: 24, color: Color(0xFF635BFF)), onPressed: _sendMessage),
]),
);
}
void _onAgentAction(String label) {
switch (label) {
case '拍照识别': case '上传照片': pushRoute(ref, 'dietCapture');
case '录入血压': _textCtrl.text = '血压 ';
case '录入血糖': _textCtrl.text = '血糖 ';
case '录入心率': _textCtrl.text = '心率 ';
case '录入血氧': _textCtrl.text = '血氧 ';
case '录入体重': _textCtrl.text = '体重 ';
case '用药管理': pushRoute(ref, 'medications');
case '找医生': pushRoute(ref, 'doctors');
case '本周计划': case '新建计划': pushRoute(ref, 'exercisePlan');
}
}
Future<void> _pickImage(ImageSource source) async {
final picker = ImagePicker();
final picked = await picker.pickImage(source: source, imageQuality: 85);
if (picked != null) { final token = await ref.read(apiClientProvider).accessToken; if (token == null) return; _textCtrl.text = '[图片已上传]'; if (mounted) setState(() {}); }
if (picked != null) {
final token = await ref.read(apiClientProvider).accessToken;
if (token == null) return;
setState(() => _pickedImagePath = picked.path);
}
}
void _showAttachmentPicker(BuildContext context) {