feat: 问诊对话+建档引导+报告+日历+趋势页重写+视觉统一
- 问诊对话页: 新建consultation_provider, 完整AI分身聊天UI - 首次建档引导: OnboardingPrompt+ManageArchiveTool+前端卡片 - 报告模块: POST端点 + report_agent_handler接VLM - 健康日历: GET /api/calendar端点 + 前端接API - 趋势页重写: 删除Mock数据 + 真实API + 手动录入 - 饮食保存: submit按钮接后端API - 侧边栏: 健康概览横排 + 统一紫色配色 - 运动计划: 修复JSON反序列化(record→手动解析) - VLM: prompt优化 + 模型切qwen3-vl-plus + 1280px压缩 - UI颜色: 加深主色 8B9CF7→6C5CE7 - 个人信息页精简 + 健康档案可编辑重设计 - 保洁: 删除无用agent_bar + 删除意见反馈/关于我们 - 新增AI提示词文档
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/data_providers.dart';
|
||||
import '../../providers/consultation_provider.dart';
|
||||
|
||||
/// 医生列表页
|
||||
class DoctorListPage extends ConsumerWidget {
|
||||
@@ -78,15 +80,196 @@ class DoctorListPage extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 问诊对话页
|
||||
class DoctorChatPage extends ConsumerWidget {
|
||||
/// 问诊对话页 — 完整的 AI 分身聊天界面
|
||||
class DoctorChatPage extends ConsumerStatefulWidget {
|
||||
final String id;
|
||||
const DoctorChatPage({super.key, required this.id});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) => Scaffold(
|
||||
appBar: AppBar(title: const Text('问诊对话')),
|
||||
body: Center(
|
||||
child: Text('问诊 #$id', style: Theme.of(context).textTheme.bodyLarge),
|
||||
),
|
||||
);
|
||||
ConsumerState<DoctorChatPage> createState() => _DoctorChatPageState();
|
||||
}
|
||||
|
||||
class _DoctorChatPageState extends ConsumerState<DoctorChatPage> {
|
||||
final _textCtrl = TextEditingController();
|
||||
final _scrollCtrl = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(consultationChatProvider.notifier).init(widget.id);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
ref.read(consultationChatProvider.notifier).stopPolling();
|
||||
_textCtrl.dispose();
|
||||
_scrollCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _send() {
|
||||
final text = _textCtrl.text.trim();
|
||||
if (text.isEmpty) return;
|
||||
_textCtrl.clear();
|
||||
ref.read(consultationChatProvider.notifier).sendMessage(text);
|
||||
}
|
||||
|
||||
String _statusText(String status) {
|
||||
switch (status) {
|
||||
case 'AiTalking':
|
||||
return 'AI分身对话中';
|
||||
case 'WaitingDoctor':
|
||||
return '已转接医生,请耐心等待';
|
||||
case 'DoctorReplied':
|
||||
return '医生已回复';
|
||||
case 'Closed':
|
||||
return '对话已结束';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = ref.watch(consultationChatProvider);
|
||||
final canSend = state.status == 'AiTalking' && !state.isSending;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8F9FC),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
title: Column(children: [
|
||||
Text(state.doctorName.isNotEmpty ? '${state.doctorName} · ${state.doctorDepartment}' : '问诊对话',
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
if (state.doctorName.isNotEmpty)
|
||||
Text(_statusText(state.status), style: const TextStyle(fontSize: 12, color: Color(0xFF8B9CF7))),
|
||||
]),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF0F2FF),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text('剩余${state.quotaRemaining}/${state.quotaTotal}次',
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF8B9CF7), fontWeight: FontWeight.w500)),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: state.isLoading
|
||||
? const Center(child: CircularProgressIndicator(color: Color(0xFF8B9CF7)))
|
||||
: Column(children: [
|
||||
Expanded(child: _buildMessageList(state)),
|
||||
_buildInputBar(canSend),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessageList(ConsultationChatState state) {
|
||||
if (state.messages.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('暂无消息', style: TextStyle(color: Color(0xFFBBBBBB))));
|
||||
}
|
||||
return ListView.builder(
|
||||
controller: _scrollCtrl,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
itemCount: state.messages.length,
|
||||
itemBuilder: (ctx, i) => _buildBubble(state, state.messages[i]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBubble(ConsultationChatState state, ConsultationMsg msg) {
|
||||
final isUser = msg.senderType == 'User';
|
||||
final isAi = msg.senderType == 'Ai';
|
||||
|
||||
return Align(
|
||||
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.82),
|
||||
decoration: BoxDecoration(
|
||||
color: isUser ? const Color(0xFF8B9CF7) : const Color(0xFFFEFEFF),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(isUser ? 20 : 4),
|
||||
topRight: Radius.circular(isUser ? 4 : 20),
|
||||
bottomLeft: const Radius.circular(20),
|
||||
bottomRight: const Radius.circular(20),
|
||||
),
|
||||
border: isUser ? null : Border.all(color: const Color(0xFFD8DCFD), width: 1.5),
|
||||
boxShadow: isUser
|
||||
? []
|
||||
: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(12), blurRadius: 10, offset: const Offset(0, 3))],
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
if (!isUser && msg.senderName != null) ...[
|
||||
Row(children: [
|
||||
const CircleAvatar(radius: 10, backgroundColor: Color(0xFFF0F2FF),
|
||||
child: Icon(Icons.smart_toy, size: 12, color: Color(0xFF8B9CF7))),
|
||||
const SizedBox(width: 6),
|
||||
Text(msg.senderName!, style: const TextStyle(fontSize: 12, color: Color(0xFF9E9E9E))),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
if (isUser)
|
||||
Text(msg.content, style: const TextStyle(fontSize: 16, color: Colors.white, height: 1.4))
|
||||
else
|
||||
MarkdownBody(
|
||||
data: msg.content,
|
||||
selectable: true,
|
||||
styleSheet: MarkdownStyleSheet(
|
||||
p: const TextStyle(fontSize: 16, color: Color(0xFF1A1A1A), height: 1.5),
|
||||
),
|
||||
),
|
||||
if (isAi) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF8E1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text('🏷️ 以上为AI分析,具体请咨询${state.doctorName}',
|
||||
style: const TextStyle(fontSize: 10, color: Color(0xFFF9A825))),
|
||||
),
|
||||
],
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputBar(bool canSend) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: const Border(top: BorderSide(color: Color(0xFFEEEEEE))),
|
||||
),
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _textCtrl,
|
||||
enabled: canSend,
|
||||
style: const TextStyle(fontSize: 15),
|
||||
decoration: InputDecoration(
|
||||
hintText: canSend ? '描述您的症状...' : '对话已结束',
|
||||
hintStyle: const TextStyle(fontSize: 15, color: Color(0xFFBBBBBB)),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onSubmitted: (_) => _send(),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.send, size: 24,
|
||||
color: canSend ? const Color(0xFF8B9CF7) : const Color(0xFFCCCCCC)),
|
||||
onPressed: canSend ? _send : null,
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user