import 'package:cached_network_image/cached_network_image.dart'; import 'package:dio/dio.dart'; import 'package:eitc_erm_app/widget/circular_loading.dart'; import 'package:eitc_erm_app/widget/image_error.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import '../bean/normal_response2.dart'; import '../image_view.dart'; import '../online_consultation_detail.dart'; import '../utils/Component.dart'; import '../utils/Constants.dart'; import '../utils/logger.dart'; import 'chat_disease.dart'; import 'chat_scroll_behavior.dart'; import 'chat_scroll_physics.dart'; import 'socket_core.dart'; /*void main() { runApp(ChatHome()); }*/ class ChatHome extends StatefulWidget { final String? doctorId; final String? doctorName; ChatHome( {super.key, required String this.doctorId, required this.doctorName}); @override ChatHomeState createState() => ChatHomeState(); } class ChatHomeState extends State { bool _isLoading = false; final List> _chatRecords = [ /*{ "host": true, "name": "你", "content": "问诊人:刘娟\n问诊资料:之前做过根管,最近有点疼不知道是哪里的问题" },*/ ]; final ScrollController _scrollController = ScrollController(); final TextEditingController _textEditingController = TextEditingController(); final FocusNode _focusNode = FocusNode(); WebSocketUtility socket = WebSocketUtility(); String? userId = Global.userId; String? doctorId = Global.doctor.data?[Global.selectDoctor].userId.toString(); bool sendEnable = true; late Future _future; @override void initState() { super.initState(); _scrollController.addListener(scrollListener); _focusNode.addListener(textFocusListener); if (widget.doctorId != null && widget.doctorId!.isNotEmpty) { doctorId = widget.doctorId!; } socket.initWebSocket(onOpen: () { socket.sendMessage( "[LOGIN][${DateTime.now().millisecondsSinceEpoch}][${userId}][${doctorId}][2]"); // socket.initHeartBeat(); }, onMessage: (data) { if (data.toString().contains("SYSTEM")) { if (data.toString().contains("已上线")) { Component.toast("已连线", 2); sendEnable = true; } /*else { Component.toast("连线失败,请稍后重试!", 0); sendEnable = false; }*/ } else { if (!data.toString().contains("[$userId][$doctorId]")) { if (data.toString().contains("[image]")) { _chatRecords.insert(0, { "host": false, "name": widget.doctorName, "content": data.toString().split("[image]")[1].trim(), "type": 1 }); } else { _chatRecords.insert(0, { "host": false, "name": widget.doctorName, "content": data .toString() .split("-")[1] .trim() .replaceAll("
", "\n"), "type": 0 }); } } else { if (data.toString().contains("[image]")) { _chatRecords.insert(0, { "host": true, "name": widget.doctorName, "content": data.toString().split("[image]")[1].trim(), "type": 1 }); } else if (data.toString().contains("[patient]")) { _chatRecords.insert(0, { "host": true, "name": widget.doctorName, "content": data.toString().split("[patient]")[1].trim(), "type": 2 }); } else { _chatRecords.insert(0, { "host": true, "name": widget.doctorName, "content": data .toString() .split("-")[1] .trim() .replaceAll("
", "\n"), "type": 0 }); } } } logd(data); setState(() {}); }, onError: (e) { logd(e); }); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { if (_scrollController.position.maxScrollExtent == 0) { // not scroll content, call load more if (_isLoading) return; _isLoading = true; onLoadMore(); } }); } @override void dispose() { logd("结束聊天..."); socket.sendMessage( "[LOGOUT][${DateTime.now().millisecondsSinceEpoch}][${userId}][${doctorId}][2]"); socket.closeSocket(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('问诊', style: TextStyle( color: Colors.white, )), centerTitle: true, elevation: 0.5, backgroundColor: Global.StatusBarColor, leading: IconButton( tooltip: '返回上一页', icon: const Icon( Icons.arrow_back_ios, color: Colors.white, ), onPressed: () { Navigator.pop(context); }, ), ), backgroundColor: Global.BackgroundColor, body: Column( children: [ Container( color: Colors.green[300], padding: const EdgeInsets.only(left: 10), child: Row(children: [ Text( "${Global.patient.data![Global.selectPatient].patientName}的病历", style: const TextStyle(fontSize: 16, color: Colors.white), ), const Spacer(), TextButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => OnlineConsultationDetail("")), ); }, child: const Text( "点击查看详情", style: TextStyle(color: Colors.white), )), TextButton( onPressed: () { onSendMessage(2, "[patient]${Global.patient.data![Global.selectPatient].patientId}"); }, child: const Text( "发送", style: TextStyle(color: Colors.white), )) ]), ), Expanded( child: ScrollConfiguration( behavior: ChatScrollBehavior(), child: ListView.builder( padding: const EdgeInsets.only(bottom: 10), itemBuilder: (ctx, index) { return index == _chatRecords.length ? const Center( child: SizedBox( height: 0, width: 0, child: CircularProgressIndicator( strokeWidth: 0, ), ), ) : chatItemWidget(index); }, controller: _scrollController, physics: const ChatScrollPhysics( parent: AlwaysScrollableScrollPhysics()), reverse: true, itemCount: _chatRecords.length + 1, ), ), ), editMessageWidget(), ], ), /*floatingActionButton: FloatingActionButton( onPressed: () { // 处理点击事件 logd('FloatingActionButton was pressed.'); }, tooltip: 'Increment', child: Icon(Icons.add), ),*/ ); } Widget chatItemWidget(int index) { return Column( children: [ const SizedBox( height: 10, ), _chatRecords[index]['host'] ? Row( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox( width: 10, ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ const Text( "你", style: TextStyle( color: Colors.grey, fontSize: 12, ), ), const SizedBox( height: 4, ), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: const Color(0xFF95EC6A), borderRadius: BorderRadius.circular(4), ), alignment: Alignment.centerRight, child: ConstrainedBox( constraints: const BoxConstraints( maxWidth: 220, ), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ if (_chatRecords[index]['type'] == 0) Text(_chatRecords[index]['content'], maxLines: 30, style: const TextStyle( color: Colors.black, )), if (_chatRecords[index]['type'] == 1) GestureDetector( onTap: () async { logd(_chatRecords[index]['content'] .toString()); Navigator.push( context, MaterialPageRoute( builder: (context) => ImagePreviewPage( _chatRecords[index] ['content'] .toString() .replaceAll( "[image]", "")), )); }, child: Hero( tag: _chatRecords[index]['content'] .toString() .replaceAll("[image]", ""), child: CachedNetworkImage( imageUrl: _chatRecords[index] ['content'] .toString() .replaceAll("[image]", ""), width: 80, fit: BoxFit.fitWidth, progressIndicatorBuilder: (ctx, _, progress) => Center( child: Circularloading( width: 25, height: 25, value: progress.progress, ), ), errorWidget: (context, error, stackTrace) { return const ImageError( icon: Icons.broken_image_outlined, size: 25, color: Colors.white, ); // 显示一个进度指示器作为错误占位 }, )), ), if (_chatRecords[index]['type'] == 2) InkWell( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => OnlineConsultationDetail("")), ); }, child: Text( "${Global.patient.data![Global.selectPatient].patientName}的病历\n【点击查看详情】", textAlign: TextAlign.center, style: const TextStyle( decoration: TextDecoration.underline, )), ), /*if (index == _chatRecords.length - 1) InkWell( onTap: () { Navigator.push( context, MaterialPageRoute(builder: (context) => OnlineConsultationDetail()), ); }, child: Text('【点击查看详情】', style: TextStyle( color: Colors.blue, )), )*/ ]), ), ), ], ), const SizedBox( width: 10, ), Container( decoration: BoxDecoration( shape: BoxShape.circle, color: _chatRecords[index]['host'] ? Colors.green[400] : Colors.blue[400], ), width: 40, height: 40, alignment: Alignment.center, child: const Text( "你", style: TextStyle( color: Colors.white, fontSize: 12, ), ), ), const SizedBox( width: 10, ), ], ) : Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox( width: 10, ), Container( decoration: const BoxDecoration( shape: BoxShape.circle, color: Colors.blue, ), width: 40, height: 40, alignment: Alignment.center, child: Text( _chatRecords[index]['name'] ?? "医生", maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white, fontSize: 12, ), ), ), const SizedBox( width: 10, ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _chatRecords[index]['name'] ?? "", style: const TextStyle( color: Colors.grey, fontSize: 12, ), ), const SizedBox( height: 4, ), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), ), alignment: Alignment.centerRight, child: ConstrainedBox( constraints: const BoxConstraints( maxWidth: 220, ), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ if (_chatRecords[index]['type'] == 0) Text(_chatRecords[index]['content'], maxLines: 30, style: const TextStyle( color: Colors.black, )), if (_chatRecords[index]['type'] == 1) GestureDetector( onTap: () async { logd(_chatRecords[index]['content'] .toString()); Navigator.push( context, MaterialPageRoute( builder: (context) => ImagePreviewPage( _chatRecords[index] ['content'] .toString() .replaceAll( "[image]", "")), )); }, child: Hero( tag: _chatRecords[index]['content'] .toString() .replaceAll("[image]", ""), child: CachedNetworkImage( imageUrl: _chatRecords[index] ['content'] .toString() .replaceAll("[image]", ""), width: 80, fit: BoxFit.fitWidth, progressIndicatorBuilder: (ctx, _, progress) => Circularloading( value: progress.progress, ), errorWidget: (context, error, stackTrace) { return const Center( child: Icon( Icons.error, color: Colors.white, ), ); // 显示一个进度指示器作为错误占位 }, )), ), /*if (index == _chatRecords.length - 1) InkWell( onTap: () { Navigator.push( context, MaterialPageRoute(builder: (context) => OnlineConsultationDetail()), ); }, child: Text('【点击查看详情】', style: TextStyle( color: Colors.blue, )), )*/ ]), ), ), ], ), const SizedBox( width: 10, ), ], ), ], ); } Widget editMessageWidget() { return Container( padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 10), color: Colors.grey[100], child: Row( children: [ Expanded( child: Container( color: Colors.white, child: TextField( enabled: sendEnable, focusNode: _focusNode, controller: _textEditingController, minLines: 1, maxLines: 9, cursorColor: Colors.black87, decoration: const InputDecoration( isDense: true, border: OutlineInputBorder(borderSide: BorderSide.none), contentPadding: EdgeInsets.all(8), ), ), ), ), const SizedBox( width: 10, ), Material( color: const Color(0xFF08C060), borderRadius: BorderRadius.circular(5), child: InkWell( onTap: () { onSendMessage(0, ""); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), alignment: Alignment.center, child: const Text( "发送", style: TextStyle( color: Colors.white, ), )), ), ), const SizedBox( width: 5, ), GestureDetector( onTap: () async { String uploadImg = await uploadFile(); if (uploadImg != "") { onSendMessage(1, uploadImg.toString()); } uploadImg = ""; }, child: const Icon( Icons.add_circle_outline, color: Colors.black87, size: 30, ), ), ], ), ); } Future uploadFile() async { FilePickerResult? result = await FilePicker.platform.pickFiles( allowedExtensions: ["jpg", "avi", "mov"], type: FileType.custom); if (result != null) { String fileName = result.files.single.name; String? filePath = result.files.single.path; FormData formData = FormData.fromMap({ "file": await MultipartFile.fromFile(filePath!, filename: fileName), "path": "chat", }); try { Map headers = { 'token': Global.token, }; Response response = await Dio().post( '${Global.BaseUrl}common/minioUploadImage', data: formData, options: Options(headers: headers)); if (response.statusCode == 200) { var json = decodeBodyToJson(response.data); logd("聊天上传文件结果=$json"); Normal2Response mNormal2Response = new Normal2Response.fromJson(json); if (mNormal2Response.code == Global.responseSuccessCode) { // Component.toast("上传成功!", 2); return mNormal2Response.msg.toString(); } else { Component.toast(mNormal2Response.msg.toString(), 0); return ""; } } else { Component.toast("出错了,请稍后再试!", 0); return ""; } } catch (e) { logd(e); return ""; } } return ""; } void scrollListener() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent) { if (_isLoading) return; _isLoading = true; onLoadMore(); } } void textFocusListener() { _scrollController.animateTo(0.0, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut); } void onLoadMore() async { // 模拟请求接口 await Future.delayed(const Duration(seconds: 1)); // for (int i = 0; i < 10; i++) { // _chatRecords.addAll([ // { // "host": i % 2 == 0 ? false : true, // "name": i % 2 == 0 ? "大夫" : "你", // "content": i % 2 == 0 ? "hello" : "hi" // }, // ]); // } _isLoading = false; setState(() {}); } void onSendMessage(int msgType, String info) async { String socketMsg = ""; switch (msgType) { case 0: // 文字 if (_textEditingController.text.trim().isEmpty) return; /*_chatRecords.insert(0, { "host": true, "name": "你", "content": _textEditingController.text.trim(), "type": 0, "info": info, });*/ socketMsg = "[CHAT][${DateTime.now().millisecondsSinceEpoch}][$userId][][$doctorId][2] - ${_textEditingController.text.trim().replaceAll("\n", "
")}"; break; case 1: // 图片 /*_chatRecords.insert(0, { "host": true, "name": "你", "content": _textEditingController.text.trim(), "type": 1, "info": info, });*/ socketMsg = "[CHAT][${DateTime.now().millisecondsSinceEpoch}][$userId][][$doctorId][2] - [image]$info"; break; case 2: // 病历卡 /*_chatRecords.insert(0, { "host": true, "name": "你", "content": _textEditingController.text.trim(), "type": 2, "info": info, });*/ socketMsg = "[CHAT][${DateTime.now().millisecondsSinceEpoch}][$userId][][$doctorId][2] - $info"; break; } socket.sendMessage(socketMsg); _textEditingController.text = ""; setState(() {}); } Future fetchData() async { logd(Global.token); final response = await http.get( Uri.parse( '${Global.BaseUrl}chat/getChatDisease?patientId=${Global.selectPatient}'), headers: jsonHeaders(withToken: true)); if (response.statusCode == 200) { final json = decodeBodyToJson(response.bodyBytes); logd("获取病例结果=json"); ChatDisease mChatDisease = ChatDisease.fromJson(json); if (mChatDisease.code == Global.responseSuccessCode) { logd(mChatDisease.data?.patientId); } else { Component.toast(mChatDisease.msg.toString(), 0); return null; } return mChatDisease; } else { Component.toast("出错了,请稍后再试!", 0); return null; } } }