fix: 修复 Flutter 前端多项功能 + 后端运动计划 API

- Android 添加相机/存储权限,拍照和相册功能可用
- AI 回复支持 Markdown 渲染(加粗/表格不再显示**乱码)
- 附件按钮接线,支持拍照/相册/文件选择
- 智能体面板按钮全部接线(拍照/上传/手动录入/导航)
- 侧边栏 AI 录入后自动刷新健康数据
- 运动计划页增加创建按钮 + 打卡功能
- 后端运动计划支持 AI 创建和打卡(Tool Calling)
- 修复 CreateExercisePlanRequest JSON 反序列化
This commit is contained in:
MingNian
2026-06-02 16:34:36 +08:00
parent df263baa5d
commit 498708e568
11 changed files with 212 additions and 18 deletions

View File

@@ -1,5 +1,10 @@
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';
@@ -137,7 +142,7 @@ class _HomePageState extends ConsumerState<HomePage> {
child: SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () {},
onPressed: () => _onAgentAction(label),
icon: Icon(icon, size: 20),
label: Text(label),
style: OutlinedButton.styleFrom(
@@ -151,6 +156,83 @@ class _HomePageState extends ConsumerState<HomePage> {
);
}
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),
@@ -159,7 +241,7 @@ class _HomePageState extends ConsumerState<HomePage> {
border: Border(top: BorderSide(color: Colors.grey.shade200)),
),
child: Row(children: [
IconButton(icon: const Icon(Icons.attach_file, size: 24, color: Color(0xFF666666)), onPressed: () {}),
IconButton(icon: const Icon(Icons.attach_file, size: 24, color: Color(0xFF666666)), onPressed: () => _showAttachmentPicker(context)),
Expanded(
child: TextField(
controller: _textCtrl,

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../providers/chat_provider.dart';
@@ -56,10 +57,17 @@ class ChatMessagesView extends ConsumerWidget {
children: [
if (!isUser && chatState.isStreaming && msg.content.isEmpty)
_buildThinkingIndicator()
else if (isUser)
Text(msg.content, style: const TextStyle(fontSize: 16, color: Colors.white))
else
Text(
msg.content.isEmpty && !isUser ? '...' : msg.content,
style: TextStyle(fontSize: 16, color: isUser ? Colors.white : const Color(0xFF1A1A1A)),
MarkdownBody(
data: msg.content.isEmpty ? '...' : msg.content,
selectable: true,
styleSheet: MarkdownStyleSheet(
p: const TextStyle(fontSize: 16, color: Color(0xFF1A1A1A)),
h1: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
code: TextStyle(fontSize: 14, backgroundColor: Colors.grey[200]),
),
),
if (!isUser && msg.content.isNotEmpty && !chatState.isStreaming)
Padding(