- 新增 AppColors 统一配色方案(清爽紫蓝,参考蚂蚁阿福风格) - 新增 common_widgets(GradientBorderButton / CardActionButton / IconBox) - AgentWelcomeCard 美化:渐变 header + AI标签 + 核心功能区 + 快捷操作区 - DataConfirmCard 统一:三合一(健康/药品/运动),可编辑字段列表 - 删除死代码:medicationConfirm/dietAnalysis/reportAnalysis/quickOptions 卡片 - 删除死路由:doctors/profileEdit/editProfile - AI 对话去除 Markdown 符号(_stripMd),统一行间距 1.5 - 逐字淡入动画修复:key 改用 stableId 避免重复触发 - 健康概览时间修复:前端发送 UTC 避免时区歧义 - PG 数据目录迁入项目 backend/pgdata/,加入 .gitignore - 新增欧姆龙血压计实施方案文档
203 lines
7.3 KiB
Dart
203 lines
7.3 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_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 Scaffold(
|
||
backgroundColor: AppTheme.bg,
|
||
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: AppTheme.primaryLight)),
|
||
]),
|
||
centerTitle: true,
|
||
actions: [
|
||
Container(
|
||
margin: const EdgeInsets.only(right: 12),
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||
decoration: BoxDecoration(
|
||
color: AppTheme.primaryLight,
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: Text('剩余${state.quotaRemaining}/${state.quotaTotal}次',
|
||
style: const TextStyle(fontSize: 15, color: AppTheme.primaryLight, 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.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: 18),
|
||
decoration: InputDecoration(
|
||
hintText: canSend ? '描述您的症状...' : '对话已结束',
|
||
hintStyle: const TextStyle(fontSize: 18, color: Color(0xFFBBBBBB)),
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||
border: InputBorder.none,
|
||
),
|
||
onSubmitted: (_) => _send(),
|
||
),
|
||
),
|
||
IconButton(
|
||
icon: Icon(Icons.send, size: 28,
|
||
color: canSend ? AppTheme.primary : const Color(0xFFCCCCCC)),
|
||
onPressed: canSend ? _send : null,
|
||
),
|
||
]),
|
||
);
|
||
}
|
||
}
|