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 { const DoctorListPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final doctors = ref.watch(doctorListProvider); return Scaffold( appBar: AppBar(title: const Text('选择医生')), body: doctors.when( data: (list) { if (list.isEmpty) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.person_search, size: 64, color: Colors.grey[300]), const SizedBox(height: 12), Text('暂无可用医生', style: Theme.of(context).textTheme.bodyMedium), ], ), ); } return ListView.builder( itemCount: list.length, itemBuilder: (ctx, i) { final d = list[i]; return Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Padding( padding: const EdgeInsets.all(16), child: Row(children: [ CircleAvatar( radius: 28, backgroundColor: const Color(0xFFF0F2FF), child: Text( (d['name'] as String?)?.isNotEmpty == true ? d['name']![0] : '?', style: const TextStyle(fontSize: 22, color: Color(0xFF8B9CF7)), ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Text(d['name'] ?? '', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), const SizedBox(width: 8), Text(d['title'] ?? '', style: const TextStyle(fontSize: 14, color: Color(0xFF666666))), ]), const SizedBox(height: 4), Text(d['department'] ?? '', style: const TextStyle(fontSize: 14, color: Color(0xFF8B9CF7))), const SizedBox(height: 2), Text(d['introduction'] ?? '', style: const TextStyle(fontSize: 14, color: Color(0xFF999999))), ], ), ), ElevatedButton( onPressed: () => pushRoute(ref, 'consultation', params: {'id': d['id']?.toString() ?? ''}), child: const Text('咨询'), ), ]), ), ); }, ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (_, _) => Center( child: Text('加载失败', style: Theme.of(context).textTheme.bodyMedium), ), ), ); } } /// 问诊对话页 — 完整的 AI 分身聊天界面 class DoctorChatPage extends ConsumerStatefulWidget { final String id; const DoctorChatPage({super.key, required this.id}); @override ConsumerState createState() => _DoctorChatPageState(); } class _DoctorChatPageState extends ConsumerState { 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: 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, ), ]), ); } }