Files
AI-Health/health_app/lib/pages/consultation/consultation_pages.dart
MingNian c70d5d4be6 fix: 管理员系统 + 注册流程重构 + UI改版 + 全链路修复
- 新增管理员角色(手机号12345678910),管理员可增删医生、查看患者
- 注册页重构:去掉角色选择+审核码,改为选医生+填姓名
- 医生端点按DoctorId过滤患者,Patient↔Doctor关系建立
- Doctor/DoctorProfile/User实体新增关联字段
- JSON循环引用修复(IgnoreCycles)
- /api/doctors改为公开接口
- 登录闪屏修复+原生Android启动页
- 输入框全局白底+灰框
- 蓝牙重连同步修复
- Web端(doctor_web+health_app/web)全部删除
- 全局UI改版:白底企业风,新配色和组件系统
- 新品牌图标和启动图
2026-06-16 15:16:44 +08:00

314 lines
10 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/app_colors.dart';
import '../../core/app_theme.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: AppColors.iconBg,
child: Icon(
Icons.forum_outlined,
size: 15,
color: AppColors.primary,
),
),
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: AppColors.divider)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Container(
constraints: const BoxConstraints(maxHeight: 120),
decoration: BoxDecoration(
color: AppColors.backgroundSoft,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.border),
),
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(color: AppColors.border),
),
enabledBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
borderSide: BorderSide(color: AppColors.border),
),
focusedBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
borderSide: BorderSide(color: AppColors.primary),
),
),
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,
),
),
),
],
),
);
}
}