fix: VLM 参数优化 - temperature 0.7, top_p 0.8, 指令放 system+user

- VisionAsync 新增 Temperature=0.7, TopP=0.8
- system prompt 用专业营养识别指令
- userText 用简短"请看图识别食物"配合图片
- 修复重复 prompt 导致 VLM 误读文本的 bug
This commit is contained in:
MingNian
2026-06-03 11:12:06 +08:00
parent c6395ea9b4
commit 78573eaa5f
46 changed files with 955 additions and 801 deletions

View File

@@ -2,20 +2,16 @@ 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 '../../core/api_client.dart';
import '../../core/navigation_provider.dart';
import '../../providers/auth_provider.dart';
import '../../providers/chat_provider.dart';
import '../../providers/data_providers.dart';
import '../../widgets/agent_bar.dart';
import '../../widgets/health_drawer.dart';
import 'widgets/chat_messages_view.dart';
/// 首页——主界面
class HomePage extends ConsumerStatefulWidget {
const HomePage({super.key});
@override
ConsumerState<HomePage> createState() => _HomePageState();
@override ConsumerState<HomePage> createState() => _HomePageState();
}
class _HomePageState extends ConsumerState<HomePage> {
@@ -24,25 +20,12 @@ class _HomePageState extends ConsumerState<HomePage> {
bool _taskCardsExpanded = true;
bool _showExpandButton = false;
@override
void initState() {
super.initState();
_scrollCtrl.addListener(_onScroll);
}
@override
void dispose() {
_textCtrl.dispose();
_scrollCtrl.dispose();
super.dispose();
}
@override void initState() { super.initState(); _scrollCtrl.addListener(_onScroll); }
@override void dispose() { _textCtrl.dispose(); _scrollCtrl.dispose(); super.dispose(); }
void _onScroll() {
if (_scrollCtrl.offset > 50 && !_showExpandButton) {
setState(() => _showExpandButton = true);
} else if (_scrollCtrl.offset <= 50 && _showExpandButton) {
setState(() => _showExpandButton = false);
}
if (_scrollCtrl.offset > 50 && !_showExpandButton) setState(() => _showExpandButton = true);
else if (_scrollCtrl.offset <= 50 && _showExpandButton) setState(() => _showExpandButton = false);
}
void _sendMessage() {
@@ -52,22 +35,25 @@ class _HomePageState extends ConsumerState<HomePage> {
ref.read(chatProvider.notifier).sendMessage(text);
}
@override
Widget build(BuildContext context) {
@override Widget build(BuildContext context) {
final chatState = ref.watch(chatProvider);
final auth = ref.watch(authProvider);
final user = auth.user;
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
final selectedAgent = ref.watch(selectedAgentProvider);
return Scaffold(
drawer: const HealthDrawer(),
backgroundColor: const Color(0xFFF8F7FF),
body: SafeArea(
child: Stack(children: [
Column(children: [
_buildHeader(context),
_buildHeader(user),
if (_taskCardsExpanded) _buildTaskCards(),
Expanded(child: ChatMessagesView(scrollCtrl: _scrollCtrl, messages: chatState.messages)),
_buildAgentPanel(context, selectedAgent),
const AgentBar(),
_buildInputBar(),
if (selectedAgent != null) _buildAgentPanel(context, selectedAgent),
_buildInputBar(context),
SizedBox(height: bottomInset > 0 ? bottomInset : 0),
]),
_buildExpandButton(),
]),
@@ -75,249 +61,93 @@ class _HomePageState extends ConsumerState<HomePage> {
);
}
Widget _buildExpandButton() {
if (!_showExpandButton || _taskCardsExpanded) return const SizedBox.shrink();
return Positioned(
top: 60,
right: 16,
child: AnimatedOpacity(
opacity: _showExpandButton ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300),
child: FloatingActionButton(
onPressed: () => setState(() => _taskCardsExpanded = true),
mini: true,
backgroundColor: const Color(0xFF635BFF),
child: const Icon(Icons.keyboard_arrow_down, size: 20),
),
),
);
}
Widget _buildHeader(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
Widget _buildHeader(dynamic user) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: Row(children: [
Builder(builder: (ctx) => IconButton(
icon: const Icon(Icons.menu, size: 24),
onPressed: () => Scaffold.of(ctx).openDrawer(),
Builder(builder: (ctx) => GestureDetector(
onTap: () => Scaffold.of(ctx).openDrawer(),
child: CircleAvatar(radius: 22, backgroundColor: const Color(0xFFEDEBFF), backgroundImage: user?.avatarUrl != null ? NetworkImage(user!.avatarUrl!) : null, child: user?.avatarUrl == null ? const Icon(Icons.person, size: 26, color: Color(0xFF635BFF)) : null),
)),
const Spacer(),
Text('健康管家', style: Theme.of(context).textTheme.titleLarge),
const Spacer(),
const SizedBox(width: 48),
const SizedBox(width: 12),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(mainAxisSize: MainAxisSize.min, children: [Icon(Icons.smart_toy_outlined, size: 18, color: const Color(0xFF635BFF)), const SizedBox(width: 4), Text('AI 健康管家', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.grey[600]))]),
const SizedBox(height: 2),
Text('${_getGreeting()}${user?.name ?? '张三'}', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF1A1A1A))),
])),
Icon(Icons.notifications_none, size: 24, color: Colors.grey[600]),
]),
);
}
Widget _buildTaskCards() {
final latestHealth = ref.watch(latestHealthProvider);
return latestHealth.when(
data: (data) {
final tasks = _getTaskCards(data);
if (tasks.isEmpty) return const SizedBox.shrink();
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFEFEFF),
borderRadius: BorderRadius.circular(24),
boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))],
),
child: Column(children: [
Row(children: [
const Icon(Icons.wb_sunny, size: 20, color: Color(0xFFFFB800)),
const SizedBox(width: 8),
Text(_getGreeting(), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
const Spacer(),
GestureDetector(
onTap: () => setState(() => _taskCardsExpanded = false),
child: const Icon(Icons.keyboard_arrow_down, size: 22, color: Color(0xFF999999)),
),
]),
const SizedBox(height: 12),
Column(children: tasks),
]),
);
},
loading: () => const SizedBox.shrink(),
error: (_, __) {
final tasks = _getTaskCards(const {});
if (tasks.isEmpty) return const SizedBox.shrink();
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFEFEFF),
borderRadius: BorderRadius.circular(24),
boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))],
),
child: Column(children: [
Row(children: [
const Icon(Icons.wb_sunny, size: 20, color: Color(0xFFFFB800)),
const SizedBox(width: 8),
Text(_getGreeting(), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
const Spacer(),
GestureDetector(
onTap: () => setState(() => _taskCardsExpanded = false),
child: const Icon(Icons.keyboard_arrow_down, size: 22, color: Color(0xFF999999)),
),
]),
const SizedBox(height: 12),
Column(children: tasks),
]),
);
},
);
}
String _getGreeting() {
final hour = DateTime.now().hour;
if (hour < 6) return '夜深了';
if (hour < 9) return '早上好';
if (hour < 12) return '上午好';
if (hour < 14) return '中午好';
if (hour < 18) return '下午好';
if (hour < 22) return '晚上好';
return '夜深了';
return '晚上好';
}
List<Widget> _getTaskCards(Map<String, dynamic> healthData) {
final cards = <Widget>[];
cards.add(_buildMedicationCard());
cards.add(_buildExerciseCard());
cards.add(_buildMeasurementCard());
final abnormalCards = _buildAbnormalCards(healthData);
cards.addAll(abnormalCards);
final summaryCard = _buildSummaryCard(healthData);
if (summaryCard != null) cards.add(summaryCard);
return cards;
Widget _buildExpandButton() {
if (!_showExpandButton || _taskCardsExpanded) return const SizedBox.shrink();
return Positioned(top: 60, right: 16, child: AnimatedOpacity(opacity: _showExpandButton ? 1.0 : 0.0, duration: const Duration(milliseconds: 300), child: FloatingActionButton(onPressed: () => setState(() => _taskCardsExpanded = true), mini: true, backgroundColor: const Color(0xFF635BFF), child: const Icon(Icons.keyboard_arrow_down, size: 20))));
}
Widget _buildMedicationCard() {
return _buildTaskCard(
'💊',
'计划 8:00 吃 阿司匹林 100mg',
Icons.check_circle_outline,
() => _handleMedicationCheck(),
type: 'medication',
Widget _buildTaskCards() {
final latestHealth = ref.watch(latestHealthProvider);
return latestHealth.when(
data: (data) => Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))]),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [Text('今日任务', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const Spacer(), GestureDetector(onTap: () => setState(() => _taskCardsExpanded = false), child: Text('全部展开', style: TextStyle(fontSize: 13, color: const Color(0xFF635BFF))))]),
const SizedBox(height: 12), ..._getTodayTasks(data),
]),
),
loading: () => const SizedBox.shrink(),
error: (_, __) => Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))]),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [const Text('今日任务', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const Spacer(), GestureDetector(onTap: () => setState(() => _taskCardsExpanded = false), child: Text('全部展开', style: TextStyle(fontSize: 13, color: const Color(0xFF635BFF))))]),
const SizedBox(height: 12), ..._getTodayTasks({}),
],
),
),
);
}
Widget _buildExerciseCard() {
return _buildTaskCard(
'🏃',
'今日待运动:散步 30 分钟',
Icons.check_circle_outline,
() => _handleExerciseCheck(),
type: 'exercise',
);
List<Widget> _getTodayTasks(Map<String, dynamic> healthData) {
return [
_taskRow(icon: Icons.medication_rounded, label: '计划 8:00 吃 阿司匹林 100mg', status: 'done', onTap: _handleMedicationCheck),
_taskRow(icon: Icons.directions_run, label: '今日待运动:散步 30 分钟', status: 'pending', onTap: null),
_taskRow(icon: Icons.today, label: '今日测量:血压', status: 'pending', onTap: () => _textCtrl.text = '血压 '),
..._buildAbnormalRows(healthData),
];
}
Widget _buildMeasurementCard() {
return _buildTaskCard(
'🩺',
'今日待测量:血压',
Icons.arrow_forward_ios,
() => _textCtrl.text = '血压 ',
type: 'measurement',
);
}
List<Widget> _buildAbnormalCards(Map<String, dynamic> healthData) {
final cards = <Widget>[];
List<Widget> _buildAbnormalRows(Map<String, dynamic> healthData) {
final rows = <Widget>[];
final bp = healthData['BloodPressure'];
if (bp != null && bp is Map) {
final systolic = bp['systolic'];
final diastolic = bp['diastolic'];
if (systolic != null && systolic >= 140) {
cards.add(_buildTaskCard(
'⚠️',
'昨日血压 ${systolic}/${diastolic ?? '--'},偏高',
Icons.arrow_forward_ios,
() => pushRoute(ref, 'trend', params: {'type': 'blood_pressure'}),
type: 'warning',
highlight: true,
));
}
}
final hr = healthData['HeartRate'];
if (hr != null && hr is Map) {
final value = hr['value'];
if (value != null && (value > 100 || value < 60)) {
cards.add(_buildTaskCard(
'⚠️',
'昨日心率 $value${value > 100 ? '偏高' : '偏低'}',
Icons.arrow_forward_ios,
() => pushRoute(ref, 'trend', params: {'type': 'heart_rate'}),
type: 'warning',
highlight: true,
));
}
}
return cards;
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? _buildSummaryCard(Map<String, dynamic> healthData) {
final values = <String>[];
final bp = healthData['BloodPressure'];
if (bp != null && bp is Map) {
final sys = bp['systolic'];
final dia = bp['diastolic'];
if (sys != null && dia != null) values.add('血压 $sys/$dia');
}
final hr = healthData['HeartRate'];
if (hr != null && hr is Map && hr['value'] != null) {
values.add('心率 ${hr['value']}');
}
final glucose = healthData['Glucose'];
if (glucose != null && glucose is Map && glucose['value'] != null) {
values.add('血糖 ${glucose['value']}');
}
if (values.isEmpty) return null;
return _buildTaskCard(
'📊',
'今日已记录:${values.join('')}',
Icons.arrow_forward_ios,
() => pushRoute(ref, 'trend', params: {'type': 'blood_pressure'}),
type: 'summary',
);
}
Widget _buildTaskCard(String icon, String text, IconData actionIcon, VoidCallback onTap, {String type = '', bool highlight = false}) {
Widget _taskRow({required IconData icon, required String label, required String status, VoidCallback? onTap}) {
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};
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: highlight ? BoxDecoration(
color: const Color(0xFFFDF2F2),
borderRadius: BorderRadius.circular(12),
) : null,
padding: const EdgeInsets.only(bottom: 12),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Row(children: [
Text(icon, style: const TextStyle(fontSize: 20)),
const SizedBox(width: 10),
Expanded(child: Text(text, style: TextStyle(
fontSize: 14,
color: highlight ? const Color(0xFFDC2626) : const Color(0xFF333333),
))),
GestureDetector(
onTap: onTap,
child: Icon(actionIcon, size: 20, color: highlight ? const Color(0xFFDC2626) : const Color(0xFF635BFF)),
),
Container(width: 32, height: 32, decoration: BoxDecoration(color: const Color(0xFFF5F3FF), borderRadius: BorderRadius.circular(8)), child: Icon(icon, size: 16, color: const Color(0xFF635BFF))),
const SizedBox(width: 10), Expanded(child: Text(label, style: const TextStyle(fontSize: 14, color: Color(0xFF333333)))),
Icon(icons[status], size: 20, color: colors[status] ?? Colors.grey),
]),
),
);
@@ -325,211 +155,73 @@ class _HomePageState extends ConsumerState<HomePage> {
void _handleMedicationCheck() async {
await ref.read(medicationServiceProvider).confirm('');
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('已记录服药 ✅'),
backgroundColor: Color(0xFF635BFF),
duration: Duration(seconds: 2),
));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已记录服药 ✅'), backgroundColor: Color(0xFF635BFF)));
}
void _handleExerciseCheck() async {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('已完成运动 ✅'),
backgroundColor: Color(0xFF635BFF),
duration: Duration(seconds: 2),
));
}
Widget _buildAgentPanel(BuildContext context, 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 _buildAgentPanel(BuildContext context, ActiveAgent? agent) {
if (agent == null) return const SizedBox.shrink();
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFEFEFF),
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(20), blurRadius: 12, offset: const Offset(0, -4))],
),
child: Column(mainAxisSize: MainAxisSize.min, children: [
_buildAgentPanelHeader(agent),
const SizedBox(height: 12),
..._getAgentButtons(agent),
]),
);
}
Widget _buildAgentPanelHeader(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 分钟"',
};
return Column(children: [
return AnimatedContainer(duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white, borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(15), blurRadius: 12, offset: const Offset(0, -4))]), child: Column(mainAxisSize: MainAxisSize.min, children: [
Text(titles[agent] ?? '', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
const SizedBox(height: 4),
Text(tips[agent] ?? '', style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
]);
const SizedBox(height: 4), Text(tips[agent] ?? '', style: TextStyle(fontSize: 12, color: Colors.grey[500])),
const SizedBox(height: 12), Wrap(spacing: 8, runSpacing: 8, children: _getAgentButtons(agent)),
]));
}
List<Widget> _getAgentButtons(ActiveAgent agent) {
final buttons = <Widget>[];
if (agent == ActiveAgent.health) {
buttons.add(_panelBtn('手动录入血压', Icons.favorite));
buttons.add(_panelBtn('手动录入血糖', Icons.bloodtype));
buttons.add(_panelBtn('手动录入心率', Icons.monitor_heart));
buttons.add(_panelBtn('手动录入血氧', Icons.air));
buttons.add(_panelBtn('手动录入体重', Icons.monitor_weight));
} else if (agent == ActiveAgent.diet) {
buttons.add(_panelBtn('拍照', Icons.camera_alt));
buttons.add(_panelBtn('上传照片', Icons.photo_library));
} else if (agent == ActiveAgent.medication) {
buttons.add(_panelBtn('用药管理', Icons.medication));
buttons.add(_panelBtn('用药提醒', Icons.alarm));
} else if (agent == ActiveAgent.consultation) {
buttons.add(_panelBtn('找医生', Icons.person_search));
} else if (agent == ActiveAgent.exercise) {
buttons.add(_panelBtn('查看本周计划', Icons.calendar_view_week));
buttons.add(_panelBtn('创建新计划', Icons.add_circle_outline));
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 [];
}
return buttons;
}
Widget _panelBtn(String label, IconData icon) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => _onAgentAction(label),
icon: Icon(icon, size: 18),
label: Text(label, style: const TextStyle(fontSize: 14)),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFF5F3FF),
foregroundColor: const Color(0xFF635BFF),
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
),
),
),
Widget _agentBtn(String label, IconData icon) {
return ElevatedButton.icon(
onPressed: () => _onAgentAction(label),
icon: Icon(icon, size: 16),
label: Text(label, style: const TextStyle(fontSize: 13)),
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFF5F3FF), foregroundColor: const Color(0xFF635BFF), elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14)),
);
}
void _onAgentAction(String label) {
switch (label) {
case '拍照':
pushRoute(ref, 'dietCapture');
break;
case '上传照片':
pushRoute(ref, 'dietCapture');
break;
case '手动录入血压':
_textCtrl.text = '血压 ';
break;
case '手动录入血糖':
_textCtrl.text = '血糖 ';
break;
case '手动录入心率':
_textCtrl.text = '心率 ';
break;
case '手动录入血氧':
_textCtrl.text = '血氧 ';
break;
case '手动录入体重':
_textCtrl.text = '体重 ';
break;
case '用药管理':
pushRoute(ref, 'medications');
break;
case '找医生':
pushRoute(ref, 'doctors');
break;
case '查看本周计划':
pushRoute(ref, 'exercisePlan');
break;
case '创建新计划':
pushRoute(ref, 'exercisePlan');
break;
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 = '[图片已上传] $baseUrl/api/files/${picked.path.split('/').last}';
setState(() {});
}
if (picked != null) { final token = await ref.read(apiClientProvider).accessToken; if (token == null) return; _textCtrl.text = '[图片已上传]'; setState(() {}); }
}
void _showAttachmentPicker(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (ctx) => SafeArea(
child: Wrap(
children: [
ListTile(
leading: const Icon(Icons.camera_alt),
title: const Text('拍照'),
onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.camera); },
),
ListTile(
leading: const Icon(Icons.photo_library),
title: const Text('从相册选'),
onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.gallery); },
),
ListTile(
leading: const Icon(Icons.attach_file),
title: const Text('传文件'),
onTap: () async {
Navigator.pop(ctx);
final result = await FilePicker.platform.pickFiles();
if (result != null && result.files.isNotEmpty) {
_textCtrl.text = '[文件已选择] ${result.files.first.name}';
setState(() {});
}
},
),
],
),
),
);
showModalBottomSheet(context: context, builder: (ctx) => SafeArea(child: Wrap(children: [
ListTile(leading: const Icon(Icons.camera_alt), title: const Text('拍照'), onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.camera); }),
ListTile(leading: const Icon(Icons.photo_library), title: const Text('从相册选'), onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.gallery); }),
ListTile(leading: const Icon(Icons.attach_file), title: const Text('传文件'), onTap: () async { Navigator.pop(ctx); final result = await FilePicker.platform.pickFiles(); if (result != null && result.files.isNotEmpty) { _textCtrl.text = '[文件已选择] ${result.files.first.name}'; setState(() {}); }}),
])));
}
Widget _buildInputBar() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.grey.shade200)),
),
child: Row(children: [
IconButton(icon: const Icon(Icons.attach_file, size: 24, color: Color(0xFF666666)), onPressed: () => _showAttachmentPicker(context)),
Expanded(
child: TextField(
controller: _textCtrl,
decoration: const InputDecoration(hintText: '输入你想说的...', contentPadding: EdgeInsets.symmetric(horizontal: 12), border: InputBorder.none),
onSubmitted: (_) => _sendMessage(),
),
),
IconButton(icon: const Icon(Icons.send, size: 24, color: Color(0xFF635BFF)), onPressed: _sendMessage),
]),
);
Widget _buildInputBar(BuildContext context) {
return Container(padding: const EdgeInsets.symmetric(horizontal: 12, 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, decoration: InputDecoration(hintText: '输入你想说的...', contentPadding: const EdgeInsets.symmetric(horizontal: 12), border: InputBorder.none), onSubmitted: (_) => _sendMessage())),
IconButton(icon: const Icon(Icons.send, size: 24, color: Color(0xFF635BFF)), onPressed: _sendMessage),
]));
}
}