Files
AI-Health/health_app/lib/pages/home/home_page.dart
MingNian 498708e568 fix: 修复 Flutter 前端多项功能 + 后端运动计划 API
- Android 添加相机/存储权限,拍照和相册功能可用
- AI 回复支持 Markdown 渲染(加粗/表格不再显示**乱码)
- 附件按钮接线,支持拍照/相册/文件选择
- 智能体面板按钮全部接线(拍照/上传/手动录入/导航)
- 侧边栏 AI 录入后自动刷新健康数据
- 运动计划页增加创建按钮 + 打卡功能
- 后端运动计划支持 AI 创建和打卡(Tool Calling)
- 修复 CreateExercisePlanRequest JSON 反序列化
2026-06-02 16:34:36 +08:00

257 lines
8.8 KiB
Dart

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 '../../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();
}
class _HomePageState extends ConsumerState<HomePage> {
final _textCtrl = TextEditingController();
final _scrollCtrl = ScrollController();
bool _taskCardsExpanded = true;
@override
void dispose() {
_textCtrl.dispose();
_scrollCtrl.dispose();
super.dispose();
}
void _sendMessage() {
final text = _textCtrl.text.trim();
if (text.isEmpty) return;
_textCtrl.clear();
ref.read(chatProvider.notifier).sendMessage(text);
}
@override
Widget build(BuildContext context) {
final chatState = ref.watch(chatProvider);
final selectedAgent = ref.watch(selectedAgentProvider);
return Scaffold(
drawer: const HealthDrawer(),
body: SafeArea(
child: Column(children: [
_buildHeader(context),
if (_taskCardsExpanded) _buildTaskCards(chatState),
Expanded(child: ChatMessagesView(scrollCtrl: _scrollCtrl, messages: chatState.messages)),
if (selectedAgent != null) _buildAgentPanel(context, selectedAgent),
const AgentBar(),
_buildInputBar(),
]),
),
);
}
Widget _buildHeader(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(children: [
Builder(builder: (ctx) => IconButton(
icon: const Icon(Icons.menu, size: 24),
onPressed: () => Scaffold.of(ctx).openDrawer(),
)),
const Spacer(),
Text('健康管家', style: Theme.of(context).textTheme.titleLarge),
const Spacer(),
const SizedBox(width: 48),
]),
);
}
Widget _buildTaskCards(ChatState chatState) {
return GestureDetector(
onVerticalDragUpdate: (d) { if (d.delta.dy < -10) setState(() => _taskCardsExpanded = false); },
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFEDEBFF),
borderRadius: BorderRadius.circular(12),
),
child: Column(children: [
Row(children: [
const Icon(Icons.wb_sunny, size: 18, color: Color(0xFF635BFF)),
const SizedBox(width: 8),
const Text('早上好!', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
const Spacer(),
GestureDetector(
onTap: () => setState(() => _taskCardsExpanded = false),
child: const Icon(Icons.keyboard_arrow_up, size: 20, color: Color(0xFF666666)),
),
]),
if (chatState.noticeText != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(chatState.noticeText!, style: const TextStyle(fontSize: 14, color: Color(0xFF666666))),
),
]),
),
);
}
Widget _buildAgentPanel(BuildContext context, ActiveAgent agent) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(20), blurRadius: 8, offset: const Offset(0, -2))],
),
child: Column(mainAxisSize: MainAxisSize.min, 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));
} 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));
}
return buttons;
}
Widget _panelBtn(String label, IconData icon) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () => _onAgentAction(label),
icon: Icon(icon, size: 20),
label: Text(label),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF635BFF),
side: const BorderSide(color: Color(0xFF635BFF)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
);
}
void _onAgentAction(String label) {
switch (label) {
case '拍照':
_pickImage(ImageSource.camera);
break;
case '上传照片':
_pickImage(ImageSource.gallery);
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;
}
}
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(() {});
}
}
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(() {});
}
},
),
],
),
),
);
}
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),
]),
);
}
}