- 报告模块:重写AI解读页,统一白色卡片+三栏布局(指标→解读→医生审核);加删除功能+后端删接口;VLM自动识别报告类型生成中文标题;去五颜六色
- 饮食分析:全面重设计,暖橙主题配色,图片自适应,餐次emoji选择器,去紫色
- 运动确认卡片:修复duration_minutes JSON类型转换,支持中文数字识别(三十分钟/半小时→30);主区域只显示运动名,字段行改为横向排列
- 流式输出:简化为最基础逐字追加,去buffer+Timer+淡入动画
- 欢迎卡片:每智能体独立渐变色(青/橙/蓝/紫/绿/粉),加卡片入场滑入+淡入动画(600ms)
- 确认卡片:头部去紫色背景改浅灰白,字段行去图标改纯信息横排,编辑框去双重边框
- 用药管理:胶囊改TabBar,添加按钮改右下FAB;打卡按钮改对号圆圈形式;药丸图标白底
- 侧边栏:加VIP服务/保险栏+图标,标题字体放大;服务包去查看更多+去VIP金色标签
- 胶囊:首页智能体胶囊白底深色字; side胶囊加阴影
- 对话流:reverse=false,今日健康出现在顶部; 对话页input bar匹配主页面样式
- 其他:个人资料去紫色+去多余入口;设置页白底图标;蓝牙页加返回按钮;报告管理改名
- 后端:加DELETE /api/reports/{id}; 修复manage_exercise数据类型转换;AI提示优化中文数字
220 lines
8.2 KiB
Dart
220 lines
8.2 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||
import '../../core/app_colors.dart';
|
||
import '../../core/app_theme.dart';
|
||
import '../../core/navigation_provider.dart';
|
||
import '../../providers/data_providers.dart';
|
||
import '../../providers/consultation_provider.dart';
|
||
|
||
/// 问诊对话页 — 完整的 AI 分身聊天界面
|
||
class DoctorChatPage extends ConsumerStatefulWidget {
|
||
final String id;
|
||
const DoctorChatPage({super.key, required this.id});
|
||
|
||
@override
|
||
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).stop();
|
||
_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 GradientScaffold(
|
||
appBar: AppBar(
|
||
backgroundColor: Colors.white,
|
||
title: Column(children: [
|
||
Text(state.doctorName.isNotEmpty ? '${state.doctorName} · ${state.doctorDepartment}' : '问诊对话',
|
||
style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||
if (state.doctorName.isNotEmpty)
|
||
Text(_statusText(state.status), style: const TextStyle(fontSize: 15, color: AppColors.textSecondary)),
|
||
]),
|
||
centerTitle: true,
|
||
actions: [
|
||
Container(
|
||
margin: const EdgeInsets.only(right: 12),
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: Text('剩余${state.quotaRemaining}/${state.quotaTotal}次',
|
||
style: const TextStyle(fontSize: 15, color: AppColors.textPrimary, fontWeight: FontWeight.w500)),
|
||
),
|
||
],
|
||
),
|
||
body: state.isLoading
|
||
? const Center(child: CircularProgressIndicator(color: AppTheme.primaryLight))
|
||
: 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 ? AppTheme.primary : 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: AppTheme.primary.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: 15, color: AppTheme.primaryLight)),
|
||
const SizedBox(width: 6),
|
||
Text(msg.senderName!, style: const TextStyle(fontSize: 15, color: Color(0xFF9E9E9E))),
|
||
]),
|
||
const SizedBox(height: 8),
|
||
],
|
||
if (isUser)
|
||
Text(msg.content, style: const TextStyle(fontSize: 19, color: Colors.white, height: 1.4))
|
||
else
|
||
MarkdownBody(
|
||
data: msg.content,
|
||
selectable: true,
|
||
styleSheet: MarkdownStyleSheet(
|
||
p: const TextStyle(fontSize: 19, 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: 13, color: Color(0xFFF9A825))),
|
||
),
|
||
],
|
||
]),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildInputBar(bool canSend) {
|
||
return Container(
|
||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 10),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
border: const Border(top: BorderSide(color: Color(0xFFEEEEEE))),
|
||
),
|
||
child: Row(crossAxisAlignment: CrossAxisAlignment.end, children: [
|
||
Expanded(
|
||
child: Container(
|
||
constraints: const BoxConstraints(maxHeight: 120),
|
||
decoration: BoxDecoration(
|
||
color: AppColors.backgroundSoft,
|
||
borderRadius: BorderRadius.circular(20),
|
||
),
|
||
child: TextField(
|
||
controller: _textCtrl,
|
||
enabled: canSend,
|
||
maxLines: null,
|
||
style: const TextStyle(fontSize: 18),
|
||
decoration: InputDecoration(
|
||
hintText: canSend ? '描述您的症状...' : '对话已结束',
|
||
hintStyle: const TextStyle(fontSize: 18, color: Color(0xFFBBBBBB)),
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||
border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none),
|
||
enabledBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none),
|
||
focusedBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none),
|
||
),
|
||
onSubmitted: (_) => _send(),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
GestureDetector(
|
||
onTap: canSend ? _send : null,
|
||
child: Container(
|
||
width: 40, height: 40,
|
||
decoration: BoxDecoration(
|
||
color: canSend ? AppColors.blueMeasure : const Color(0xFFCCCCCC),
|
||
borderRadius: BorderRadius.circular(20),
|
||
),
|
||
child: const Icon(Icons.send_rounded, size: 20, color: Colors.white),
|
||
),
|
||
),
|
||
]),
|
||
);
|
||
}
|
||
}
|