chat.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. <template>
  2. <view>
  3. <image class="page-bg" src="https://cos.his.cdwjyyh.com/fs/20250506/730750385fab4dd5a8a268b1550c81d6.png"
  4. mode="widthFix" style="width: 100%;"></image>
  5. <view class="nav-bar" :style="{height: `calc(88rpx + ${statusBarHeight}px)`}">
  6. <view class="statusBar" :style="{height: statusBarHeight + 'px'}"></view>
  7. <image class="nav-bg" src="https://cos.his.cdwjyyh.com/fs/20250506/730750385fab4dd5a8a268b1550c81d6.png"
  8. mode="widthFix"></image>
  9. <view class="nav-bar-box">
  10. <view class="uni-page-head-hd es-mr-24" @click="$navBack()">
  11. <uni-icons type="left" size="26" color="#000000"></uni-icons>
  12. </view>
  13. <view class="nav-bar-left">
  14. <image :src="roleInfo.avatar||'https://fs-1319721001.cos.ap-chongqing.myqcloud.com/fs/20240229/be32b8d2ae9f497297d10327656bb43c.png'" mode="aspectFill"></image>
  15. <view class="nav-bar-head">
  16. <view class="nav-bar-name textOne">{{roleName}}</view>
  17. <view class="textOne" v-show="roleInfo.title">{{roleInfo.title}}</view>
  18. </view>
  19. <!-- <uni-icons type="more-filled" size="26" style="flex-shrink: 0;" color="#000000"></uni-icons> -->
  20. </view>
  21. </view>
  22. </view>
  23. <scroll-view class="msg-scroll" :scroll-top="scrollTop" scroll-y="true"
  24. :scroll-with-animation="true">
  25. <view class="container-body" :style="{paddingTop: `calc(100rpx + ${statusBarHeight}px)`}">
  26. <view class="banner-box" v-if="roleInfo.imageUrl||roleInfo.textDescription">
  27. <image :src="roleInfo.imageUrl" mode="aspectFill"></image>
  28. <view class="banner-txt" v-show="roleInfo.textDescription">{{roleInfo.textDescription}}</view>
  29. </view>
  30. <view class="ques-box" v-if="quesList&&quesList.length>0">
  31. <view v-for="(ques,i) in quesList" :key="i" @click="handleQues(ques)">{{ques}}</view>
  32. </view>
  33. <view class="TUI-message-list es-mt-24" @touchstart="handleTouchStart">
  34. <!-- <view class="loading-text" v-if="isCompleted">没有更多</view> -->
  35. <view v-for="(item, index) in msgs" :key="index" :id="'view' + index">
  36. <!-- <view class="time-container" v-if="item.showTime">{{ caculateTimeago(item.time * 1000) }}</view> -->
  37. <view :class="item.type == 1 ? 'msg-item my-msg':'msg-item ai-msg'">
  38. <image v-if="item.type == 1" class="avatar" :src="userInfo.avatar||'https://fs-1319721001.cos.ap-chongqing.myqcloud.com/fs/20240229/1d7eb0607a074892964dd32e8735e540.jpg'" mode="aspectFill"></image>
  39. <image v-if="item.type != 1" class="avatar" :src="roleInfo.avatar||'https://fs-1319721001.cos.ap-chongqing.myqcloud.com/fs/20240229/be32b8d2ae9f497297d10327656bb43c.png'" mode="aspectFill"></image>
  40. <view class="msg-text"><text>{{item.content}}</text></view>
  41. </view>
  42. </view>
  43. </view>
  44. </view>
  45. </scroll-view>
  46. <view class="chatinput">
  47. <u--input placeholder="请输入想要咨询的问题" border="none" v-model.trim="inputText" clearable @confirm="handleInput"></u--input>
  48. <!-- <image src="/static/images/update_pic_icon.png" mode="aspectFill" class="icon" @click="choosePic">
  49. </image> -->
  50. <image src="/static/images/send_message_icon.png" mode="aspectFill" class="icon2" :style="{opacity:inputText?1:0.7}"
  51. @click="handleInput"></image>
  52. </view>
  53. </view>
  54. </template>
  55. <script>
  56. import { getSessionDetailInfo } from "@/api/ai.js"
  57. export default {
  58. data() {
  59. return {
  60. sessionId: '',
  61. statusBarHeight: uni.getSystemInfoSync().statusBarHeight,
  62. roleName: '',
  63. chatList: [],
  64. quesList:[],
  65. roleInfo: {},
  66. inputText: '',
  67. triggered: false,
  68. isCompleted: false,
  69. scrollTop: 0,
  70. oldMessageTime: 0,
  71. inputText:"",
  72. roleId:null,
  73. msgTimes:null,
  74. isSocketOpen:false,
  75. socket:null,
  76. isSend:true,
  77. userInfo: {},
  78. doctorId: '',
  79. sessionId: null,
  80. msgEnd: false,
  81. msgs: []
  82. }
  83. },
  84. onLoad(options) {
  85. this.roleName = options.roleName || ''
  86. this.roleId = options.roleId || ''
  87. this.userInfo = uni.getStorageSync("userInfo") ? JSON.parse(uni.getStorageSync("userInfo")) : {}
  88. this.sessionId = options.sessionId || null
  89. this.getSessionDetailInfo()
  90. this.initSocket();
  91. },
  92. // 监听数据初次渲染,展示最新一条消息
  93. // TODO app 中获取不到DOM 元素
  94. onReady() {
  95. this.handleScrollBottom();
  96. },
  97. mounted() {
  98. // this.handleShowTime();
  99. },
  100. methods: {
  101. back() {
  102. uni.navigateBack()
  103. },
  104. getSessionDetailInfo() {
  105. let that = this
  106. getSessionDetailInfo(this.sessionId,this.roleId).then(res=>{
  107. if(res.code == 200) {
  108. let chatList = res.chatList
  109. this.roleInfo = res.roleInfo;
  110. this.quesList = res.roleInfo&&res.roleInfo.wordList ? res.roleInfo.wordList.split('||') : [];
  111. let list = []
  112. if(res.roleInfo.welcomeMessage) {
  113. list = [{
  114. sendType: 2,
  115. content: res.roleInfo.welcomeMessage
  116. }]
  117. }
  118. if(chatList&&chatList.length > 0) {
  119. list = list.concat(chatList)
  120. }
  121. list.forEach(function(value, index, array) {
  122. that.addMsg(value.sendType, value.content, 1);
  123. });
  124. }
  125. })
  126. },
  127. choosePic() {
  128. },
  129. handleQues(ques) {
  130. this.inputText = ques
  131. this.handleInput()
  132. },
  133. handleInput() {
  134. if(this.msgEnd || !this.isSocketOpen) {
  135. // 重新发起会话
  136. this.initSocket()
  137. } else {
  138. this.sendMsg();
  139. }
  140. },
  141. handleScrollBottom() {
  142. setTimeout(() => {
  143. uni.createSelectorQuery()
  144. .select('.container-body')
  145. .boundingClientRect((res) => {
  146. if(res) {
  147. const scrollH = res.height;
  148. this.scrollTop = res.height;
  149. }
  150. }).exec();
  151. },500);
  152. },
  153. // 滑动触发时,失焦收起键盘
  154. handleTouchStart() {
  155. uni.hideKeyboard();
  156. },
  157. handleShowTime() {
  158. if (this.msgs&&this.msgs.length>0) {
  159. this.msgs.forEach((item) => {
  160. if (item.time - this.oldMessageTime > 5 * 60) {
  161. this.oldMessageTime = item.time;
  162. item.showTime = true;
  163. } else {
  164. item.showTime = false;
  165. }
  166. });
  167. console.log(this.msgs)
  168. }
  169. },
  170. caculateTimeago(time) {
  171. return time
  172. },
  173. initSocket() {
  174. //创建一个socket连接
  175. var userId = this.userInfo.userId;
  176. var that = this;
  177. if (this.socket) {
  178. this.socket.close()
  179. this.socket = null;
  180. }
  181. this.socket = uni.connectSocket({
  182. url: getApp().globalData.aiWSUrl + "/app/interestAiWebSocket/" + userId,
  183. multiple: true,
  184. success: res => {
  185. console.log('WebSocket连接已打开1!');
  186. that.isSocketOpen = true
  187. },
  188. error: res => {
  189. console.log(res)
  190. },
  191. })
  192. this.socket.onMessage((res) => {
  193. console.log("收到消息",res)
  194. that.isSend = true;
  195. that.addMsg(2, res.data, 2);
  196. })
  197. //监听socket打开
  198. this.socket.onOpen(() => {
  199. console.log('WebSocket连接已打开2!');
  200. that.isSocketOpen = true
  201. if(that.msgEnd) {
  202. // 重新发起会话
  203. that.isSend = true;
  204. that.sendMsg();
  205. }
  206. })
  207. //监听socket关闭
  208. this.socket.onClose(() => {
  209. that.isSocketOpen = false
  210. that.socket = null
  211. console.log('WebSocket连接已关闭!');
  212. that.msgEnd = true
  213. that.handleScrollBottom();
  214. if(that.msgTimes) {
  215. clearInterval(that.msgTimes)
  216. that.msgTimes = null
  217. }
  218. })
  219. //监听socket错误
  220. this.socket.onError(() => {
  221. that.isSocketOpen = false
  222. that.socket = null
  223. that.msgEnd = true
  224. console.log('WebSocket连接打开失败');
  225. if(that.msgTimes) {
  226. clearInterval(that.msgTimes)
  227. that.msgTimes = null
  228. }
  229. })
  230. },
  231. sendMsg() {
  232. if (this.inputText == "") {
  233. return;
  234. }
  235. if (!this.isSend) {
  236. return;
  237. }
  238. if (this.isSocketOpen) {
  239. var userId = this.userInfo.userId;
  240. var data = {
  241. userId: this.userInfo.userId,
  242. roleId: this.roleId,
  243. nickName: this.userInfo.nickName,
  244. avatar: this.userInfo.avatar,
  245. roleName: this.roleName,
  246. message: this.inputText,
  247. sessionId: this.sessionId,
  248. };
  249. this.socket.send({
  250. data: JSON.stringify(data),
  251. success: () => {
  252. console.log("发送成功")
  253. this.addMsg(1, this.inputText, 1);
  254. this.addMsg(2, "正在思考中...", 1);
  255. this.isSend = false;
  256. },
  257. fail: () => {
  258. console.log("发送失败")
  259. }
  260. });
  261. }
  262. },
  263. addMsg(type, content, inputType) {
  264. this.msgEnd = false
  265. var obj = {
  266. type: type,
  267. content: content
  268. }
  269. if (inputType == 2) {
  270. this.msgs.splice(-1);
  271. this.msgs.push(obj);
  272. } else if (inputType == 1) {
  273. this.msgs.push(obj)
  274. }
  275. this.inputText = ""
  276. var that = this;
  277. that.handleScrollBottom();
  278. //先确保清除了之前的消息定时器
  279. if(that.msgTimes) {
  280. clearInterval(that.msgTimes)
  281. that.msgTimes = null
  282. }
  283. // 5分钟无消息自动结束
  284. that.msgTimes=setInterval(()=>{
  285. console.log("5分钟无消息自动结束")
  286. clearInterval(that.msgTimes)
  287. if(this.socket!=null){
  288. this.socket.close()
  289. }
  290. that.msgTimes = null
  291. },300000)
  292. },
  293. },
  294. onUnload() {
  295. this.msgEnd = true
  296. if(this.socket!=null){
  297. this.socket.close()
  298. }
  299. if(this.msgTimes) {
  300. clearInterval(this.msgTimes)
  301. this.msgTimes = null
  302. }
  303. uni.$off('refreshOrderPatient')
  304. }
  305. }
  306. </script>
  307. <style scoped lang="scss">
  308. @mixin u-flex($flexD, $alignI, $justifyC) {
  309. display: flex;
  310. flex-direction: $flexD;
  311. align-items: $alignI;
  312. justify-content: $justifyC;
  313. }
  314. .nav-bar {
  315. position: fixed;
  316. z-index: 9999;
  317. top: 0;
  318. left: 0;
  319. width: 100%;
  320. overflow: hidden;
  321. .nav-bg {
  322. width: 100%;
  323. height: 100%;
  324. position: absolute;
  325. left: 0;
  326. top: 0;
  327. z-index: 1;
  328. background-color: #fff;
  329. }
  330. &-box {
  331. position: relative;
  332. padding: 0 24rpx;
  333. @include u-flex(row, center, flex-start);
  334. height: 88rpx;
  335. box-sizing: border-box;
  336. z-index: 3;
  337. }
  338. &-left {
  339. width: 100%;
  340. @include u-flex(row, center, flex-start);
  341. overflow: hidden;
  342. image {
  343. flex-shrink: 0;
  344. width: 64rpx;
  345. height: 64rpx;
  346. border-radius: 12rpx 12rpx 12rpx 12rpx;
  347. }
  348. }
  349. &-name {
  350. font-family: PingFang SC, PingFang SC;
  351. font-weight: 600;
  352. font-size: 28rpx;
  353. color: #222222;
  354. }
  355. &-head {
  356. flex: 1;
  357. overflow: hidden;
  358. margin-left: 22rpx;
  359. margin-right: 22rpx;
  360. font-family: PingFang SC, PingFang SC;
  361. font-weight: 400;
  362. font-size: 23rpx;
  363. color: #999999;
  364. }
  365. }
  366. .page-bg {
  367. position: absolute;
  368. top: 0;
  369. left: 0;
  370. }
  371. .container-body {
  372. position: relative;
  373. padding: 32rpx 30rpx;
  374. z-index: 2;
  375. @include u-flex(column, flex-star, center);
  376. .banner-box {
  377. width: 100%;
  378. min-width: 686rpx;
  379. overflow: hidden;
  380. background: rgba(255, 255, 255, 0.9);
  381. border-radius: 24rpx 24rpx 24rpx 24rpx;
  382. border: 4rpx solid #FFFFFF;
  383. box-sizing: border-box;
  384. image {
  385. width: 100%;
  386. min-width: 690rpx;
  387. height: 264rpx;
  388. background: #f7f7f7;
  389. }
  390. }
  391. .banner-txt {
  392. padding: 32rpx;
  393. font-family: PingFang SC, PingFang SC;
  394. font-weight: 400;
  395. font-size: 32rpx;
  396. color: #333333;
  397. word-break: break-all;
  398. }
  399. .ques-box {
  400. @include u-flex(row, flex-star, flex-star);
  401. flex-wrap: wrap;
  402. margin: 22rpx -8rpx -10rpx;
  403. view {
  404. padding: 12rpx 36rpx;
  405. background: #FFFFFF;
  406. border-radius: 16rpx 16rpx 16rpx 16rpx;
  407. border: 1rpx solid #ECECEC;
  408. font-family: PingFang SC, PingFang SC;
  409. font-weight: 500;
  410. font-size: 28rpx;
  411. color: #333333;
  412. margin: 10rpx 8rpx;
  413. }
  414. }
  415. }
  416. .chatinput {
  417. position: fixed;
  418. left: 32rpx;
  419. right: 32rpx;
  420. z-index: 999;
  421. bottom: calc(var(--window-bottom) + 24rpx);
  422. height: 96rpx;
  423. background-color: green;
  424. background: #FFFFFF;
  425. box-shadow: 0rpx 8rpx 21rpx 0rpx rgba(0, 0, 0, 0.1);
  426. border-radius: 24rpx 24rpx 24rpx 24rpx;
  427. @include u-flex(row, center, center);
  428. padding: 0 24rpx;
  429. box-sizing: border-box;
  430. .icon {
  431. height: 48rpx;
  432. width: 48rpx;
  433. margin-left: 32rpx;
  434. }
  435. .icon2 {
  436. height: 56rpx;
  437. width: 56rpx;
  438. margin-left: 32rpx;
  439. }
  440. }
  441. .msg-scroll {
  442. height: calc(100vh - var(--window-bottom) - 120rpx);
  443. }
  444. .TUI-message-list {
  445. width: 100%;
  446. box-sizing: border-box;
  447. .time-container {
  448. font-family: PingFang SC, PingFang SC;
  449. font-weight: 400;
  450. font-size: 24rpx;
  451. color: #999999;
  452. padding: 36rpx 0;
  453. margin: 10px;
  454. text-align: center;
  455. }
  456. .avatar {
  457. flex-shrink: 0;
  458. width: 88rpx;
  459. height: 88rpx;
  460. background: #FFFFFF;
  461. border-radius: 12rpx 12rpx 12rpx 12rpx;
  462. }
  463. .msg-item {
  464. margin-bottom: 24rpx;
  465. }
  466. .ai-msg {
  467. @include u-flex(row, flex-start, flex-start);
  468. .avatar {
  469. margin-right: 24rpx;
  470. }
  471. .msg-text {
  472. border-radius: 0rpx 24rpx 24rpx 24rpx;
  473. }
  474. }
  475. .my-msg {
  476. @include u-flex(row-reverse, flex-start, flex-start);
  477. .avatar {
  478. margin-left: 24rpx;
  479. }
  480. .msg-text {
  481. border-radius: 24rpx 0 24rpx 24rpx;
  482. background: #FEC75C;
  483. }
  484. }
  485. .msg-text {
  486. padding: 24rpx;
  487. background: #FFFFFF;
  488. overflow: hidden;
  489. }
  490. }
  491. </style>