chat.vue 14 KB

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