- Android 添加相机/存储权限,拍照和相册功能可用 - AI 回复支持 Markdown 渲染(加粗/表格不再显示**乱码) - 附件按钮接线,支持拍照/相册/文件选择 - 智能体面板按钮全部接线(拍照/上传/手动录入/导航) - 侧边栏 AI 录入后自动刷新健康数据 - 运动计划页增加创建按钮 + 打卡功能 - 后端运动计划支持 AI 创建和打卡(Tool Calling) - 修复 CreateExercisePlanRequest JSON 反序列化
257 lines
8.8 KiB
Dart
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),
|
|
]),
|
|
);
|
|
}
|
|
}
|