chatOld.vue 15 KB

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