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'; /// 医生列表页 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 SizedBox( height: 330, child: ListView.builder( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), itemCount: list.length, itemBuilder: (ctx, i) { final d = list[i]; return Container( width: 155, margin: const EdgeInsets.symmetric(horizontal: 5), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow(color: AppTheme.primary.withAlpha(15), blurRadius: 12, offset: const Offset(0, 4)) ], ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), child: Column( mainAxisSize: MainAxisSize.min, children: [ CircleAvatar( radius: 28, backgroundColor: AppTheme.primaryLight, child: Text( (d['name'] as String?)?.isNotEmpty == true ? d['name']![0] : '?', style: const TextStyle(fontSize: 25, fontWeight: FontWeight.w600, color: AppTheme.primaryLight), ), ), const SizedBox(height: 10), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(6), ), child: Text(d['title'] ?? '', style: const TextStyle(fontSize: 15, color: AppTheme.primaryLight, fontWeight: FontWeight.w500)), ), const SizedBox(height: 6), Text(d['department'] ?? '', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF333333))), const SizedBox(height: 8), Text(d['introduction'] ?? '', style: const TextStyle(fontSize: 15, color: Color(0xFF888888), height: 1.4), maxLines: 4, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center), const SizedBox(height: 14), SizedBox( width: double.infinity, height: 34, child: ElevatedButton( onPressed: () => pushRoute(ref, 'consultation', params: {'id': d['id']?.toString() ?? ''}), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primary, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), padding: EdgeInsets.zero, ), child: const Text('立即咨询', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), ), ), ], ), ), ); }, ), ); }, 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: 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, ), ]), ); } }