chat_home.dart 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753
  1. import 'package:cached_network_image/cached_network_image.dart';
  2. import 'package:dio/dio.dart';
  3. import 'package:eitc_erm_app/widget/circular_loading.dart';
  4. import 'package:eitc_erm_app/widget/image_error.dart';
  5. import 'package:file_picker/file_picker.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:http/http.dart' as http;
  8. import '../bean/normal_response2.dart';
  9. import '../image_view.dart';
  10. import '../online_consultation_detail.dart';
  11. import '../utils/Component.dart';
  12. import '../utils/Constants.dart';
  13. import '../utils/logger.dart';
  14. import 'chat_disease.dart';
  15. import 'chat_scroll_behavior.dart';
  16. import 'chat_scroll_physics.dart';
  17. import 'socket_core.dart';
  18. /*void main() {
  19. runApp(ChatHome());
  20. }*/
  21. class ChatHome extends StatefulWidget {
  22. final String? doctorId;
  23. final String? doctorName;
  24. ChatHome(
  25. {super.key, required String this.doctorId, required this.doctorName});
  26. @override
  27. ChatHomeState createState() => ChatHomeState();
  28. }
  29. class ChatHomeState extends State<ChatHome> {
  30. bool _isLoading = false;
  31. final List<Map<String, dynamic>> _chatRecords = [
  32. /*{
  33. "host": true,
  34. "name": "你",
  35. "content": "问诊人:刘娟\n问诊资料:之前做过根管,最近有点疼不知道是哪里的问题"
  36. },*/
  37. ];
  38. final ScrollController _scrollController = ScrollController();
  39. final TextEditingController _textEditingController = TextEditingController();
  40. final FocusNode _focusNode = FocusNode();
  41. WebSocketUtility socket = WebSocketUtility();
  42. String? userId = Global.userId;
  43. String? doctorId = Global.doctor.data?[Global.selectDoctor].userId.toString();
  44. bool sendEnable = true;
  45. late Future<ChatDisease?> _future;
  46. @override
  47. void initState() {
  48. super.initState();
  49. _scrollController.addListener(scrollListener);
  50. _focusNode.addListener(textFocusListener);
  51. if (widget.doctorId != null && widget.doctorId!.isNotEmpty) {
  52. doctorId = widget.doctorId!;
  53. }
  54. socket.initWebSocket(onOpen: () {
  55. socket.sendMessage(
  56. "[LOGIN][${DateTime.now().millisecondsSinceEpoch}][${userId}][${doctorId}][2]");
  57. // socket.initHeartBeat();
  58. }, onMessage: (data) {
  59. if (data.toString().contains("SYSTEM")) {
  60. if (data.toString().contains("已上线")) {
  61. Component.toast("已连线", 2);
  62. sendEnable = true;
  63. } /*else {
  64. Component.toast("连线失败,请稍后重试!", 0);
  65. sendEnable = false;
  66. }*/
  67. } else {
  68. if (!data.toString().contains("[$userId][$doctorId]")) {
  69. if (data.toString().contains("[image]")) {
  70. _chatRecords.insert(0, {
  71. "host": false,
  72. "name": widget.doctorName,
  73. "content": data.toString().split("[image]")[1].trim(),
  74. "type": 1
  75. });
  76. } else {
  77. _chatRecords.insert(0, {
  78. "host": false,
  79. "name": widget.doctorName,
  80. "content": data
  81. .toString()
  82. .split("-")[1]
  83. .trim()
  84. .replaceAll("<br/>", "\n"),
  85. "type": 0
  86. });
  87. }
  88. } else {
  89. if (data.toString().contains("[image]")) {
  90. _chatRecords.insert(0, {
  91. "host": true,
  92. "name": widget.doctorName,
  93. "content": data.toString().split("[image]")[1].trim(),
  94. "type": 1
  95. });
  96. } else if (data.toString().contains("[patient]")) {
  97. _chatRecords.insert(0, {
  98. "host": true,
  99. "name": widget.doctorName,
  100. "content": data.toString().split("[patient]")[1].trim(),
  101. "type": 2
  102. });
  103. } else {
  104. _chatRecords.insert(0, {
  105. "host": true,
  106. "name": widget.doctorName,
  107. "content": data
  108. .toString()
  109. .split("-")[1]
  110. .trim()
  111. .replaceAll("<br/>", "\n"),
  112. "type": 0
  113. });
  114. }
  115. }
  116. }
  117. logd(data);
  118. setState(() {});
  119. }, onError: (e) {
  120. logd(e);
  121. });
  122. WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
  123. if (_scrollController.position.maxScrollExtent == 0) {
  124. // not scroll content, call load more
  125. if (_isLoading) return;
  126. _isLoading = true;
  127. onLoadMore();
  128. }
  129. });
  130. }
  131. @override
  132. void dispose() {
  133. logd("结束聊天...");
  134. socket.sendMessage(
  135. "[LOGOUT][${DateTime.now().millisecondsSinceEpoch}][${userId}][${doctorId}][2]");
  136. socket.closeSocket();
  137. super.dispose();
  138. }
  139. @override
  140. Widget build(BuildContext context) {
  141. return Scaffold(
  142. appBar: AppBar(
  143. title: const Text('问诊',
  144. style: TextStyle(
  145. color: Colors.white,
  146. )),
  147. centerTitle: true,
  148. elevation: 0.5,
  149. backgroundColor: Global.StatusBarColor,
  150. leading: IconButton(
  151. tooltip: '返回上一页',
  152. icon: const Icon(
  153. Icons.arrow_back_ios,
  154. color: Colors.white,
  155. ),
  156. onPressed: () {
  157. Navigator.pop(context);
  158. },
  159. ),
  160. ),
  161. backgroundColor: Global.BackgroundColor,
  162. body: Column(
  163. children: <Widget>[
  164. Container(
  165. color: Colors.green[300],
  166. padding: const EdgeInsets.only(left: 10),
  167. child: Row(children: [
  168. Text(
  169. "${Global.patient.data![Global.selectPatient].patientName}的病历",
  170. style: const TextStyle(fontSize: 16, color: Colors.white),
  171. ),
  172. const Spacer(),
  173. TextButton(
  174. onPressed: () {
  175. Navigator.push(
  176. context,
  177. MaterialPageRoute(
  178. builder: (context) => OnlineConsultationDetail("")),
  179. );
  180. },
  181. child: const Text(
  182. "点击查看详情",
  183. style: TextStyle(color: Colors.white),
  184. )),
  185. TextButton(
  186. onPressed: () {
  187. onSendMessage(2,
  188. "[patient]${Global.patient.data![Global.selectPatient].patientId}");
  189. },
  190. child: const Text(
  191. "发送",
  192. style: TextStyle(color: Colors.white),
  193. ))
  194. ]),
  195. ),
  196. Expanded(
  197. child: ScrollConfiguration(
  198. behavior: ChatScrollBehavior(),
  199. child: ListView.builder(
  200. padding: const EdgeInsets.only(bottom: 10),
  201. itemBuilder: (ctx, index) {
  202. return index == _chatRecords.length
  203. ? const Center(
  204. child: SizedBox(
  205. height: 0,
  206. width: 0,
  207. child: CircularProgressIndicator(
  208. strokeWidth: 0,
  209. ),
  210. ),
  211. )
  212. : chatItemWidget(index);
  213. },
  214. controller: _scrollController,
  215. physics: const ChatScrollPhysics(
  216. parent: AlwaysScrollableScrollPhysics()),
  217. reverse: true,
  218. itemCount: _chatRecords.length + 1,
  219. ),
  220. ),
  221. ),
  222. editMessageWidget(),
  223. ],
  224. ),
  225. /*floatingActionButton: FloatingActionButton(
  226. onPressed: () {
  227. // 处理点击事件
  228. logd('FloatingActionButton was pressed.');
  229. },
  230. tooltip: 'Increment',
  231. child: Icon(Icons.add),
  232. ),*/
  233. );
  234. }
  235. Widget chatItemWidget(int index) {
  236. return Column(
  237. children: <Widget>[
  238. const SizedBox(
  239. height: 10,
  240. ),
  241. _chatRecords[index]['host']
  242. ? Row(
  243. mainAxisAlignment: MainAxisAlignment.end,
  244. crossAxisAlignment: CrossAxisAlignment.start,
  245. children: <Widget>[
  246. const SizedBox(
  247. width: 10,
  248. ),
  249. Column(
  250. crossAxisAlignment: CrossAxisAlignment.end,
  251. children: <Widget>[
  252. const Text(
  253. "你",
  254. style: TextStyle(
  255. color: Colors.grey,
  256. fontSize: 12,
  257. ),
  258. ),
  259. const SizedBox(
  260. height: 4,
  261. ),
  262. Container(
  263. padding: const EdgeInsets.all(8),
  264. decoration: BoxDecoration(
  265. color: const Color(0xFF95EC6A),
  266. borderRadius: BorderRadius.circular(4),
  267. ),
  268. alignment: Alignment.centerRight,
  269. child: ConstrainedBox(
  270. constraints: const BoxConstraints(
  271. maxWidth: 220,
  272. ),
  273. child: Column(
  274. mainAxisAlignment: MainAxisAlignment.start,
  275. children: [
  276. if (_chatRecords[index]['type'] == 0)
  277. Text(_chatRecords[index]['content'],
  278. maxLines: 30,
  279. style: const TextStyle(
  280. color: Colors.black,
  281. )),
  282. if (_chatRecords[index]['type'] == 1)
  283. GestureDetector(
  284. onTap: () async {
  285. logd(_chatRecords[index]['content']
  286. .toString());
  287. Navigator.push(
  288. context,
  289. MaterialPageRoute(
  290. builder: (context) =>
  291. ImagePreviewPage(
  292. _chatRecords[index]
  293. ['content']
  294. .toString()
  295. .replaceAll(
  296. "[image]", "")),
  297. ));
  298. },
  299. child: Hero(
  300. tag: _chatRecords[index]['content']
  301. .toString()
  302. .replaceAll("[image]", ""),
  303. child: CachedNetworkImage(
  304. imageUrl: _chatRecords[index]
  305. ['content']
  306. .toString()
  307. .replaceAll("[image]", ""),
  308. width: 80,
  309. fit: BoxFit.fitWidth,
  310. progressIndicatorBuilder:
  311. (ctx, _, progress) => Center(
  312. child: Circularloading(
  313. width: 25,
  314. height: 25,
  315. value: progress.progress,
  316. ),
  317. ),
  318. errorWidget:
  319. (context, error, stackTrace) {
  320. return const ImageError(
  321. icon: Icons.broken_image_outlined,
  322. size: 25,
  323. color: Colors.white,
  324. ); // 显示一个进度指示器作为错误占位
  325. },
  326. )),
  327. ),
  328. if (_chatRecords[index]['type'] == 2)
  329. InkWell(
  330. onTap: () {
  331. Navigator.push(
  332. context,
  333. MaterialPageRoute(
  334. builder: (context) =>
  335. OnlineConsultationDetail("")),
  336. );
  337. },
  338. child: Text(
  339. "${Global.patient.data![Global.selectPatient].patientName}的病历\n【点击查看详情】",
  340. textAlign: TextAlign.center,
  341. style: const TextStyle(
  342. decoration: TextDecoration.underline,
  343. )),
  344. ),
  345. /*if (index == _chatRecords.length - 1)
  346. InkWell(
  347. onTap: () {
  348. Navigator.push(
  349. context,
  350. MaterialPageRoute(builder: (context) => OnlineConsultationDetail()),
  351. );
  352. },
  353. child: Text('【点击查看详情】',
  354. style: TextStyle(
  355. color: Colors.blue,
  356. )),
  357. )*/
  358. ]),
  359. ),
  360. ),
  361. ],
  362. ),
  363. const SizedBox(
  364. width: 10,
  365. ),
  366. Container(
  367. decoration: BoxDecoration(
  368. shape: BoxShape.circle,
  369. color: _chatRecords[index]['host']
  370. ? Colors.green[400]
  371. : Colors.blue[400],
  372. ),
  373. width: 40,
  374. height: 40,
  375. alignment: Alignment.center,
  376. child: const Text(
  377. "你",
  378. style: TextStyle(
  379. color: Colors.white,
  380. fontSize: 12,
  381. ),
  382. ),
  383. ),
  384. const SizedBox(
  385. width: 10,
  386. ),
  387. ],
  388. )
  389. : Row(
  390. mainAxisAlignment: MainAxisAlignment.start,
  391. crossAxisAlignment: CrossAxisAlignment.start,
  392. children: <Widget>[
  393. const SizedBox(
  394. width: 10,
  395. ),
  396. Container(
  397. decoration: const BoxDecoration(
  398. shape: BoxShape.circle,
  399. color: Colors.blue,
  400. ),
  401. width: 40,
  402. height: 40,
  403. alignment: Alignment.center,
  404. child: Text(
  405. _chatRecords[index]['name'] ?? "医生",
  406. maxLines: 1,
  407. overflow: TextOverflow.ellipsis,
  408. style: const TextStyle(
  409. color: Colors.white,
  410. fontSize: 12,
  411. ),
  412. ),
  413. ),
  414. const SizedBox(
  415. width: 10,
  416. ),
  417. Column(
  418. crossAxisAlignment: CrossAxisAlignment.start,
  419. children: <Widget>[
  420. Text(
  421. _chatRecords[index]['name'] ?? "",
  422. style: const TextStyle(
  423. color: Colors.grey,
  424. fontSize: 12,
  425. ),
  426. ),
  427. const SizedBox(
  428. height: 4,
  429. ),
  430. Container(
  431. padding: const EdgeInsets.all(8),
  432. decoration: BoxDecoration(
  433. color: Colors.white,
  434. borderRadius: BorderRadius.circular(4),
  435. ),
  436. alignment: Alignment.centerRight,
  437. child: ConstrainedBox(
  438. constraints: const BoxConstraints(
  439. maxWidth: 220,
  440. ),
  441. child: Column(
  442. mainAxisAlignment: MainAxisAlignment.start,
  443. children: [
  444. if (_chatRecords[index]['type'] == 0)
  445. Text(_chatRecords[index]['content'],
  446. maxLines: 30,
  447. style: const TextStyle(
  448. color: Colors.black,
  449. )),
  450. if (_chatRecords[index]['type'] == 1)
  451. GestureDetector(
  452. onTap: () async {
  453. logd(_chatRecords[index]['content']
  454. .toString());
  455. Navigator.push(
  456. context,
  457. MaterialPageRoute(
  458. builder: (context) =>
  459. ImagePreviewPage(
  460. _chatRecords[index]
  461. ['content']
  462. .toString()
  463. .replaceAll(
  464. "[image]", "")),
  465. ));
  466. },
  467. child: Hero(
  468. tag: _chatRecords[index]['content']
  469. .toString()
  470. .replaceAll("[image]", ""),
  471. child: CachedNetworkImage(
  472. imageUrl: _chatRecords[index]
  473. ['content']
  474. .toString()
  475. .replaceAll("[image]", ""),
  476. width: 80,
  477. fit: BoxFit.fitWidth,
  478. progressIndicatorBuilder:
  479. (ctx, _, progress) =>
  480. Circularloading(
  481. value: progress.progress,
  482. ),
  483. errorWidget:
  484. (context, error, stackTrace) {
  485. return const Center(
  486. child: Icon(
  487. Icons.error,
  488. color: Colors.white,
  489. ),
  490. ); // 显示一个进度指示器作为错误占位
  491. },
  492. )),
  493. ),
  494. /*if (index == _chatRecords.length - 1)
  495. InkWell(
  496. onTap: () {
  497. Navigator.push(
  498. context,
  499. MaterialPageRoute(builder: (context) => OnlineConsultationDetail()),
  500. );
  501. },
  502. child: Text('【点击查看详情】',
  503. style: TextStyle(
  504. color: Colors.blue,
  505. )),
  506. )*/
  507. ]),
  508. ),
  509. ),
  510. ],
  511. ),
  512. const SizedBox(
  513. width: 10,
  514. ),
  515. ],
  516. ),
  517. ],
  518. );
  519. }
  520. Widget editMessageWidget() {
  521. return Container(
  522. padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 10),
  523. color: Colors.grey[100],
  524. child: Row(
  525. children: <Widget>[
  526. Expanded(
  527. child: Container(
  528. color: Colors.white,
  529. child: TextField(
  530. enabled: sendEnable,
  531. focusNode: _focusNode,
  532. controller: _textEditingController,
  533. minLines: 1,
  534. maxLines: 9,
  535. cursorColor: Colors.black87,
  536. decoration: const InputDecoration(
  537. isDense: true,
  538. border: OutlineInputBorder(borderSide: BorderSide.none),
  539. contentPadding: EdgeInsets.all(8),
  540. ),
  541. ),
  542. ),
  543. ),
  544. const SizedBox(
  545. width: 10,
  546. ),
  547. Material(
  548. color: const Color(0xFF08C060),
  549. borderRadius: BorderRadius.circular(5),
  550. child: InkWell(
  551. onTap: () {
  552. onSendMessage(0, "");
  553. },
  554. child: Container(
  555. padding:
  556. const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
  557. alignment: Alignment.center,
  558. child: const Text(
  559. "发送",
  560. style: TextStyle(
  561. color: Colors.white,
  562. ),
  563. )),
  564. ),
  565. ),
  566. const SizedBox(
  567. width: 5,
  568. ),
  569. GestureDetector(
  570. onTap: () async {
  571. String uploadImg = await uploadFile();
  572. if (uploadImg != "") {
  573. onSendMessage(1, uploadImg.toString());
  574. }
  575. uploadImg = "";
  576. },
  577. child: const Icon(
  578. Icons.add_circle_outline,
  579. color: Colors.black87,
  580. size: 30,
  581. ),
  582. ),
  583. ],
  584. ),
  585. );
  586. }
  587. Future<String> uploadFile() async {
  588. FilePickerResult? result = await FilePicker.platform.pickFiles(
  589. allowedExtensions: ["jpg", "avi", "mov"], type: FileType.custom);
  590. if (result != null) {
  591. String fileName = result.files.single.name;
  592. String? filePath = result.files.single.path;
  593. FormData formData = FormData.fromMap({
  594. "file": await MultipartFile.fromFile(filePath!, filename: fileName),
  595. "path": "chat",
  596. });
  597. try {
  598. Map<String, String> headers = {
  599. 'token': Global.token,
  600. };
  601. Response response = await Dio().post(
  602. '${Global.BaseUrl}common/minioUploadImage',
  603. data: formData,
  604. options: Options(headers: headers));
  605. if (response.statusCode == 200) {
  606. var json = decodeBodyToJson(response.data);
  607. logd("聊天上传文件结果=$json");
  608. Normal2Response mNormal2Response = new Normal2Response.fromJson(json);
  609. if (mNormal2Response.code == Global.responseSuccessCode) {
  610. // Component.toast("上传成功!", 2);
  611. return mNormal2Response.msg.toString();
  612. } else {
  613. Component.toast(mNormal2Response.msg.toString(), 0);
  614. return "";
  615. }
  616. } else {
  617. Component.toast("出错了,请稍后再试!", 0);
  618. return "";
  619. }
  620. } catch (e) {
  621. logd(e);
  622. return "";
  623. }
  624. }
  625. return "";
  626. }
  627. void scrollListener() {
  628. if (_scrollController.position.pixels >=
  629. _scrollController.position.maxScrollExtent) {
  630. if (_isLoading) return;
  631. _isLoading = true;
  632. onLoadMore();
  633. }
  634. }
  635. void textFocusListener() {
  636. _scrollController.animateTo(0.0,
  637. duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
  638. }
  639. void onLoadMore() async {
  640. // 模拟请求接口
  641. await Future.delayed(const Duration(seconds: 1));
  642. // for (int i = 0; i < 10; i++) {
  643. // _chatRecords.addAll([
  644. // {
  645. // "host": i % 2 == 0 ? false : true,
  646. // "name": i % 2 == 0 ? "大夫" : "你",
  647. // "content": i % 2 == 0 ? "hello" : "hi"
  648. // },
  649. // ]);
  650. // }
  651. _isLoading = false;
  652. setState(() {});
  653. }
  654. void onSendMessage(int msgType, String info) async {
  655. String socketMsg = "";
  656. switch (msgType) {
  657. case 0:
  658. // 文字
  659. if (_textEditingController.text.trim().isEmpty) return;
  660. /*_chatRecords.insert(0, {
  661. "host": true,
  662. "name": "你",
  663. "content": _textEditingController.text.trim(),
  664. "type": 0,
  665. "info": info,
  666. });*/
  667. socketMsg =
  668. "[CHAT][${DateTime.now().millisecondsSinceEpoch}][$userId][][$doctorId][2] - ${_textEditingController.text.trim().replaceAll("\n", "<br/>")}";
  669. break;
  670. case 1:
  671. // 图片
  672. /*_chatRecords.insert(0, {
  673. "host": true,
  674. "name": "你",
  675. "content": _textEditingController.text.trim(),
  676. "type": 1,
  677. "info": info,
  678. });*/
  679. socketMsg =
  680. "[CHAT][${DateTime.now().millisecondsSinceEpoch}][$userId][][$doctorId][2] - [image]$info";
  681. break;
  682. case 2:
  683. // 病历卡
  684. /*_chatRecords.insert(0, {
  685. "host": true,
  686. "name": "你",
  687. "content": _textEditingController.text.trim(),
  688. "type": 2,
  689. "info": info,
  690. });*/
  691. socketMsg =
  692. "[CHAT][${DateTime.now().millisecondsSinceEpoch}][$userId][][$doctorId][2] - $info";
  693. break;
  694. }
  695. socket.sendMessage(socketMsg);
  696. _textEditingController.text = "";
  697. setState(() {});
  698. }
  699. Future<ChatDisease?> fetchData() async {
  700. logd(Global.token);
  701. final response = await http.get(
  702. Uri.parse(
  703. '${Global.BaseUrl}chat/getChatDisease?patientId=${Global.selectPatient}'),
  704. headers: jsonHeaders(withToken: true));
  705. if (response.statusCode == 200) {
  706. final json = decodeBodyToJson(response.bodyBytes);
  707. logd("获取病例结果=json");
  708. ChatDisease mChatDisease = ChatDisease.fromJson(json);
  709. if (mChatDisease.code == Global.responseSuccessCode) {
  710. logd(mChatDisease.data?.patientId);
  711. } else {
  712. Component.toast(mChatDisease.msg.toString(), 0);
  713. return null;
  714. }
  715. return mChatDisease;
  716. } else {
  717. Component.toast("出错了,请稍后再试!", 0);
  718. return null;
  719. }
  720. }
  721. }