chat.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800
  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
  15. :src="roleInfo.avatar||'https://fs-1319721001.cos.ap-chongqing.myqcloud.com/fs/20240229/be32b8d2ae9f497297d10327656bb43c.png'"
  16. mode="aspectFill"></image>
  17. <view class="nav-bar-head">
  18. <view class="nav-bar-name textOne">{{roleName}}</view>
  19. <view class="textOne" v-show="roleInfo.title">{{roleInfo.title}}</view>
  20. </view>
  21. <!-- <uni-icons type="more-filled" size="26" style="flex-shrink: 0;" color="#000000"></uni-icons> -->
  22. </view>
  23. </view>
  24. </view>
  25. <scroll-view class="msg-scroll" :scroll-top="scrollTop" scroll-y="true" :scroll-with-animation="true">
  26. <view class="container-body" :style="{paddingTop: `calc(100rpx + ${statusBarHeight}px)`}">
  27. <view class="banner-box" v-if="roleInfo.imageUrl||roleInfo.textDescription">
  28. <image :src="roleInfo.imageUrl" mode="aspectFill"></image>
  29. <view class="banner-txt" v-show="roleInfo.textDescription">{{roleInfo.textDescription}}</view>
  30. </view>
  31. <view class="ques-box" v-if="quesList&&quesList.length>0">
  32. <view v-for="(ques,i) in quesList" :key="i" @click="handleQues(ques)">{{ques}}</view>
  33. </view>
  34. <view class="TUI-message-list es-mt-24" @touchstart="handleTouchStart">
  35. <view v-for="(item, index) in historyList" :key="item.id" :id="'view' + item.id">
  36. <view :class="item.type == 1 ? 'msg-item my-msg':'msg-item ai-msg'">
  37. <image v-if="item.type == 1" class="avatar"
  38. :src="userInfo.avatar||'https://fs-1319721001.cos.ap-chongqing.myqcloud.com/fs/20240229/1d7eb0607a074892964dd32e8735e540.jpg'"
  39. mode="aspectFill"></image>
  40. <image v-if="item.type != 1" class="avatar"
  41. :src="roleInfo.avatar||'https://fs-1319721001.cos.ap-chongqing.myqcloud.com/fs/20240229/be32b8d2ae9f497297d10327656bb43c.png'"
  42. mode="aspectFill"></image>
  43. <view>
  44. <view class="msg-text">
  45. <uaMarkdown :source="item.content" :showLine="false" style="font-size: 32rpx" />
  46. </view>
  47. <view class="x-f" style="align-items: center;">
  48. <view v-if="item.type != 1&&item.content" class="x-c copybtn" @click="copyData(item.content)"><u-icon
  49. name="file-text" color="#FF5030" size="20"
  50. style="margin-right: 5rpx;"></u-icon>复制</view>
  51. </view>
  52. </view>
  53. </view>
  54. </view>
  55. <view v-for="(item, index) in msgs" :key="item.id" :id="'view' + item.id">
  56. <view :class="item.type == 1 ? 'msg-item my-msg':'msg-item ai-msg'">
  57. <image v-if="item.type == 1" class="avatar"
  58. :src="userInfo.avatar||'https://fs-1319721001.cos.ap-chongqing.myqcloud.com/fs/20240229/1d7eb0607a074892964dd32e8735e540.jpg'"
  59. mode="aspectFill"></image>
  60. <image v-if="item.type != 1" class="avatar"
  61. :src="roleInfo.avatar||'https://fs-1319721001.cos.ap-chongqing.myqcloud.com/fs/20240229/be32b8d2ae9f497297d10327656bb43c.png'"
  62. mode="aspectFill"></image>
  63. <view>
  64. <view class="msg-text">
  65. <uaMarkdown :source="item.content" :showLine="false" style="font-size: 32rpx" />
  66. </view>
  67. <view class="x-f" style="align-items: center;">
  68. <view v-if="item.type != 1&&item.content" class="x-c copybtn" @click="copyData(item.content)"><u-icon
  69. name="file-text" color="#FF5030" size="20"
  70. style="margin-right: 5rpx;"></u-icon>复制</view>
  71. <view style="margin-top: 6px;margin-left: 10rpx;" v-if="item.retry==1" @click="handleInput()">
  72. <u-icon name="reload" color="#999" size="20" labelPos="right" labelSize="24rpx" label="重试"></u-icon>
  73. </view>
  74. </view>
  75. </view>
  76. </view>
  77. </view>
  78. </view>
  79. </view>
  80. </scroll-view>
  81. <view class="chatinput">
  82. <u--input placeholder="请输入想要咨询的问题" border="none" v-model.trim="inputText" clearable
  83. @confirm="handleInputClick"></u--input>
  84. <!-- <image src="/static/images/update_pic_icon.png" mode="aspectFill" class="icon" @click="choosePic">
  85. </image> -->
  86. <image src="/static/images/send_message_icon.png" mode="aspectFill" class="icon2"
  87. :style="{opacity:isFirst==0&&inputText?1:0.7}" @click="handleInputClick"></image>
  88. </view>
  89. </view>
  90. </template>
  91. <script>
  92. import uaMarkdown from "@/components/ua2-markdown/ua-markdown.vue"
  93. import {
  94. getSessionDetailInfo,
  95. replyAppUserContent,
  96. getAiJsonMsgCount
  97. } from "@/api/ai.js"
  98. export default {
  99. components: {
  100. uaMarkdown
  101. },
  102. data() {
  103. return {
  104. sessionId: '',
  105. statusBarHeight: uni.getSystemInfoSync().statusBarHeight,
  106. roleName: '',
  107. chatList: [],
  108. quesList: [],
  109. roleInfo: {},
  110. inputText: '',
  111. triggered: false,
  112. isCompleted: false,
  113. scrollTop: 0,
  114. scrollInto: '', // 用于 scroll-into-view
  115. oldMessageTime: 0,
  116. roleId: null,
  117. msgTimes: null, // 5 分钟无消息定时器
  118. isSocketOpen: false,
  119. socket: null,
  120. isSend: true,
  121. userInfo: {},
  122. doctorId: '',
  123. msgEnd: false,
  124. msgs: [],
  125. source: '',
  126. maxReconnect: 1,
  127. reconnectCount: 2,
  128. reconnecting: false,
  129. needResend: false, // 是否有待重发消息
  130. resendText: '', // 待重发内容
  131. idleClose: false, //是否5分钟自动断开
  132. isFirst: 0,
  133. idleTimer: null,
  134. reportPlaceholderId:'',
  135. replyId:'',
  136. startTime: '',
  137. endTime: '',
  138. deviceId: '',
  139. isRefresh: '',
  140. historyList:[],
  141. isInitReportAi: 0
  142. }
  143. },
  144. onLoad(options) {
  145. this.source = options.source || ''
  146. // if (this.source !== 'healthWeekReport') uni.removeStorageSync('aiSuggestion')
  147. this.roleName = options.roleName || ''
  148. this.roleId = options.roleId || ''
  149. this.userInfo = uni.getStorageSync('userInfo') ? JSON.parse(uni.getStorageSync('userInfo')) : {}
  150. this.getSessionDetailInfo(options.sessionId)
  151. this.sessionId = options.sessionId || null
  152. this.isFirst = this.source == 'healthWeekReport'?1:0
  153. if(this.source == 'healthWeekReport') {
  154. const id = 'report_placeholder_' + Date.now()
  155. this.reportPlaceholderId = id
  156. this.startTime = options.startTime
  157. this.endTime = options.endTime
  158. this.deviceId = options.deviceId
  159. this.isRefresh = options.isRefresh
  160. // this.sessionId = null
  161. this.getAiJsonMsgCount(id)
  162. } else {
  163. this.initSocket(1)
  164. }
  165. uni.onKeyboardHeightChange(this.boardHeightChange);
  166. },
  167. onReady() {
  168. // this.scrollToBottom()
  169. },
  170. onUnload() {
  171. uni.$emit('getSessionId')
  172. uni.offKeyboardHeightChange(this.boardHeightChange);
  173. if (this.socket) {
  174. this.maxReconnect = 0
  175. this.socket.close();
  176. this.socket = null
  177. }
  178. clearTimeout(this.idleTimer) // 原 clearInterval(this.msgTimes) // 会话结束清占位
  179. },
  180. onBackPress() {
  181. uni.$emit('getSessionId')
  182. if (this.socket) {
  183. this.maxReconnect = 0
  184. this.socket.close();
  185. this.socket = null
  186. }
  187. clearTimeout(this.idleTimer)
  188. },
  189. methods: {
  190. boardHeightChange:function(res){
  191. this.scrollToBottom()
  192. },
  193. /* ---------- 页面交互 ---------- */
  194. back() {
  195. uni.navigateBack()
  196. },
  197. getAiJsonMsgCount(id) {
  198. const param = {
  199. startTime: this.startTime,
  200. endTime: this.endTime,
  201. message: uni.getStorageSync('aiSuggestion')||'',
  202. userId: this.userInfo.userId,
  203. roleId: this.roleId,
  204. }
  205. getAiJsonMsgCount(param).then(res=>{
  206. if(res.code==200&&res.count<1) {
  207. this.isInitReportAi = 1
  208. this.addMsg(2, '报告解读中...', 1, 0,id)
  209. this.initSocket(1)
  210. }else {
  211. this.isFirst = 0
  212. this.isInitReportAi = 0
  213. this.reportPlaceholderId = ''
  214. this.initSocket()
  215. }
  216. })
  217. },
  218. handleQues(ques) {
  219. this.inputText = ques
  220. this.handleInputClick()
  221. },
  222. handleInputClick(){
  223. if(this.isFirst==1){
  224. uni.showToast({
  225. title: '报告解读中,请稍后',
  226. icon: 'none'
  227. });
  228. return
  229. }
  230. this.handleInput()
  231. },
  232. handleInput() {
  233. // 1. 输入为空直接 return
  234. if (!this.inputText.trim()&&this.isFirst !=1) return;
  235. // 2. 连接断了 -> 走原有重连逻辑
  236. if (!this.isSocketOpen) {
  237. this.isSend = true
  238. this.idleClose = false
  239. this.trySend();
  240. return;
  241. }
  242. // 3. 连接正常,但对方还在回复
  243. if (!this.isSend) {
  244. uni.showToast({
  245. title: '对方正在回复,请稍等',
  246. icon: 'none'
  247. });
  248. return; // 直接阻断,不继续往下走
  249. }
  250. // 4. 真正发消息
  251. this.trySend();
  252. },
  253. /* ---------- 发送控制 ---------- */
  254. trySend() {
  255. const text = this.isFirst ==1&&this.source=='healthWeekReport' ? uni.getStorageSync('aiSuggestion') :this.inputText.trim() || this.resendText
  256. if (!text) return
  257. // 1. 链路不通 → 立即重连
  258. if (!this.isSocketOpen) {
  259. if (this.reconnecting) {
  260. uni.showToast({
  261. title: '正在重连,请稍候',
  262. icon: 'none'
  263. })
  264. return
  265. }
  266. this.reconnecting = true
  267. // uni.showToast({
  268. // title: '网络断开,正在重连…',
  269. // icon: 'none'
  270. // })
  271. if (this.socket) {
  272. this.socket.close();
  273. this.socket = null
  274. }
  275. this.initSocket()
  276. return
  277. }
  278. // 2. 有待重发内容优先发
  279. if (this.needResend) {
  280. this.resendText = ''
  281. this.needResend = false
  282. this.sendMsg(text)
  283. } else {
  284. this.sendMsg(text)
  285. }
  286. },
  287. /* ---------- WebSocket ---------- */
  288. initSocket(type) {
  289. let that = this
  290. if (this.socket) return // 并发短路
  291. const userId = this.userInfo.userId
  292. let url = this.source == 'healthWeekReport' ? '/app/replyAppUserWebSocket/' : '/app/interestAiWebSocket/'
  293. this.socket = uni.connectSocket({
  294. url: getApp().globalData.aiWSUrl + url + userId,
  295. multiple: true,
  296. success: res => {
  297. console.log('WebSocket连接已打开1!', Date.now());
  298. that.isSocketOpen = true
  299. },
  300. error: res => {
  301. console.log(res)
  302. },
  303. })
  304. this.socket.onOpen(() => {
  305. console.log('WebSocket 已打开2', Date.now())
  306. this.isSocketOpen = true
  307. this.reconnectCount = 0
  308. this.reconnecting = false
  309. // console.log("this.isFirst!=1&&this.replyId=",this.isFirst,this.replyId)
  310. if(this.isFirst ==1&&this.source=='healthWeekReport'&&this.isInitReportAi==1) {
  311. // console.log('healthWeekReport')
  312. this.handleInput()
  313. }
  314. if(this.isFirst!=1&&this.replyId){
  315. // console.log("网络异常,请重新发送")
  316. this.isSend = true
  317. this.addMsg(2, '网络异常,请重新发送', 2)
  318. }
  319. })
  320. this.socket.onMessage(res => {
  321. if (!res.data || res.data.trim() === '') return
  322. if(res.data=='[无视]') {
  323. return
  324. }
  325. // console.log('WebSocket onMessage2=========', res)
  326. this.isSend = true
  327. let data = res.data
  328. this.addMsg(2, data, 2)
  329. })
  330. this.socket.onClose(() => {
  331. this.isSocketOpen = false
  332. console.log('WebSocket 已关闭', Date.now())
  333. this.socket = null
  334. this.reconnecting = false
  335. this.isSend = true
  336. if (this.idleTimer) clearTimeout(this.idleTimer)
  337. if(this.isFirst ==1&&this.source=='healthWeekReport') {
  338. this.addMsg(2, '报告解读失败', 2,1,this.reportPlaceholderId)
  339. return
  340. }
  341. if (this.idleClose) return;
  342. if(this.maxReconnect == 0) return;
  343. this.reconnectCount++
  344. console.log("this.reconnectCount",this.reconnectCount)
  345. if (this.reconnectCount <= this.maxReconnect) {
  346. setTimeout(() => this.initSocket(), 5000)
  347. } else {
  348. // if(this.isFirst ==1&&this.source=='healthWeekReport') {
  349. // this.addMsg(2, '报告解读失败', 2,1,this.reportPlaceholderId)
  350. // }
  351. uni.showToast({
  352. title: '网络异常,请手动重试',
  353. icon: 'none',
  354. duration: 3000
  355. })
  356. }
  357. })
  358. this.socket.onError(() => {
  359. console.log('WebSocket 错误', Date.now())
  360. this.isSocketOpen = false
  361. this.socket = null
  362. this.reconnecting = false
  363. this.isSend = true
  364. // this.clearHeart()
  365. if (this.msgTimes) {
  366. clearInterval(this.msgTimes);
  367. this.msgTimes = null
  368. }
  369. })
  370. },
  371. /* ---------- 发送/重试 ---------- */
  372. sendMsg(text) {
  373. if (!this.isSocketOpen || !this.isSend) return
  374. const data = {
  375. userId: this.userInfo.userId,
  376. roleId: this.roleId,
  377. nickName: this.userInfo.nickName,
  378. avatar: this.userInfo.avatar,
  379. roleName: this.roleName,
  380. message: text,
  381. sessionId: this.sessionId,
  382. // isFirst: this.isFirst,//去掉
  383. startTime: this.startTime,
  384. endTime: this.endTime,
  385. deviceId: this.deviceId,
  386. isRefresh: this.isRefresh,
  387. }
  388. // console.log('data报告解读中', JSON.stringify(data))
  389. this.socket.send({
  390. data: JSON.stringify(data),
  391. success: () => {
  392. if(this.isFirst !=1) {
  393. this.addMsg(1, text, 1)
  394. const id = 'replyId_' + Date.now()
  395. this.replyId = id
  396. this.addMsg(2, '正在思考中…', 1, 0)
  397. }
  398. this.isSend = false
  399. this.inputText = '' // 只有成功才清空
  400. },
  401. fail: () => {
  402. // 进入待重发模式
  403. this.needResend = true
  404. this.resendText = text
  405. this.isSend = true
  406. if(this.isFirst ==1&&this.source=='healthWeekReport') {
  407. this.addMsg(2, '报告解读失败', 2,1,this.reportPlaceholderId)
  408. uni.showToast({
  409. title: '发送失,请重试',
  410. icon: 'none'
  411. })
  412. return
  413. }
  414. uni.showToast({
  415. title: '发送失,请重新发送',
  416. icon: 'none'
  417. })
  418. }
  419. })
  420. },
  421. addHistoryList(type, content, inputType, retry = 0,pid) {
  422. // console.log("=addMsg===inputType==",content,inputType)
  423. const id = pid||Date.now() + '_' + Math.random().toString(36).slice(2, 8)
  424. const obj = {
  425. type,
  426. content,
  427. retry,
  428. id
  429. }
  430. this.historyList.push(obj)
  431. this.scrollToBottom()
  432. },
  433. addMsg(type, content, inputType, retry = 0,pid) {
  434. // console.log("=addMsg===inputType==",content,inputType)
  435. const id = pid||Date.now() + '_' + Math.random().toString(36).slice(2, 8)
  436. const obj = {
  437. type,
  438. content,
  439. retry,
  440. id
  441. }
  442. if (inputType == 2) {
  443. if(this.reportPlaceholderId) {
  444. // console.log("===pid",pid)
  445. this.isFirst = pid?1:0
  446. const idx = this.msgs.findIndex(m => m.id == this.reportPlaceholderId)
  447. if (idx > -1) {
  448. this.$set(this.msgs, idx, {
  449. ...this.msgs[idx],
  450. retry:retry,
  451. content:content
  452. })
  453. }
  454. this.reportPlaceholderId = pid?this.reportPlaceholderId:''
  455. } else {
  456. // console.log("=33333==",obj,this.msgs)
  457. if (this.msgs.length) this.msgs.pop();
  458. this.msgs.push(obj);
  459. this.replyId = ''
  460. }
  461. } else if (inputType == 1) {
  462. this.msgs.push(obj)
  463. }
  464. this.scrollToBottom()
  465. // 重置 5 分钟空闲计时
  466. if (this.idleTimer) clearTimeout(this.idleTimer)
  467. this.idleTimer = setTimeout(() => {
  468. this.idleClose = true
  469. if (this.socket) this.socket.close();
  470. }, 5 * 60 * 1000)
  471. },
  472. /* ---------- 初始数据 ---------- */
  473. getSessionDetailInfo(sessionId) {
  474. getSessionDetailInfo(sessionId, this.roleId).then(res => {
  475. if (res.code == 200) {
  476. const chatList = res.chatList || []
  477. this.roleInfo = res.roleInfo || {}
  478. this.roleName = this.roleInfo.roleName || ''
  479. this.quesList = this.roleInfo.wordList ? this.roleInfo.wordList.split('||') : []
  480. // if (this.source !== 'healthWeekReport') {
  481. const list = []
  482. if (this.source !== 'healthWeekReport'&&this.roleInfo.welcomeMessage) {
  483. list.push({
  484. sendType: 2,
  485. content: this.roleInfo.welcomeMessage
  486. })
  487. }
  488. list.push(...chatList)
  489. list.forEach(v => this.addHistoryList(v.sendType, v.content, 1))
  490. // }
  491. } else {
  492. uni.showToast({
  493. title: res.msg,
  494. icon: 'none'
  495. })
  496. this.scrollToBottom()
  497. }
  498. }).catch(() => {
  499. this.scrollToBottom()
  500. })
  501. },
  502. /* ---------- 工具 ---------- */
  503. clearAllTimer() {
  504. clearTimeout(this.idleTimer)
  505. this.idleTimer = null
  506. },
  507. handleTouchStart() {
  508. // uni.hideKeyboard()
  509. // this.$nextTick(() => this.scrollToBottom())
  510. },
  511. /* ---------- 滚动 ---------- */
  512. scrollToBottom() {
  513. setTimeout(() => {
  514. uni.createSelectorQuery()
  515. .in(this)
  516. .select('.msg-scroll') // scroll-view 本身
  517. .boundingClientRect(scrollRect => {
  518. uni.createSelectorQuery()
  519. .in(this)
  520. .select('.container-body') // 内容区
  521. .boundingClientRect(bodyRect => {
  522. if (scrollRect && bodyRect) {
  523. // 内容高 - 滚动容器高 = 需要滚动的距离
  524. const max = bodyRect.height - scrollRect.height;
  525. this.scrollTop = max < 0 ? 0 : max;
  526. }
  527. })
  528. .exec();
  529. })
  530. .exec();
  531. }, 500);
  532. },
  533. /* ---------- 剪切板 ---------- */
  534. copyData(data) {
  535. uni.setClipboardData({
  536. data,
  537. success: () => uni.showToast({
  538. title: '复制成功',
  539. icon: 'none'
  540. }),
  541. fail: () => uni.showToast({
  542. title: '复制失败',
  543. icon: 'none'
  544. })
  545. })
  546. },
  547. }
  548. }
  549. </script>
  550. <style scoped lang="scss">
  551. @mixin u-flex($flexD, $alignI, $justifyC) {
  552. display: flex;
  553. flex-direction: $flexD;
  554. align-items: $alignI;
  555. justify-content: $justifyC;
  556. }
  557. ::v-deep .msg-text {
  558. p {
  559. white-space: pre-line;
  560. }
  561. }
  562. .copybtn {
  563. margin-top: 12rpx;
  564. font-family: PingFang SC, PingFang SC;
  565. font-weight: 500;
  566. font-size: 28rpx;
  567. color: #FF5030;
  568. background-color: #ffe2d1;
  569. padding: 4rpx 10rpx;
  570. border-radius: 10rpx;
  571. display: inline-flex;
  572. }
  573. .nav-bar {
  574. position: fixed;
  575. z-index: 9999;
  576. top: 0;
  577. left: 0;
  578. width: 100%;
  579. overflow: hidden;
  580. .nav-bg {
  581. width: 100%;
  582. height: 100%;
  583. position: absolute;
  584. left: 0;
  585. top: 0;
  586. z-index: 1;
  587. background-color: #fff;
  588. }
  589. &-box {
  590. position: relative;
  591. padding: 0 24rpx;
  592. @include u-flex(row, center, flex-start);
  593. height: 88rpx;
  594. box-sizing: border-box;
  595. z-index: 3;
  596. }
  597. &-left {
  598. width: 100%;
  599. @include u-flex(row, center, flex-start);
  600. overflow: hidden;
  601. image {
  602. flex-shrink: 0;
  603. width: 64rpx;
  604. height: 64rpx;
  605. border-radius: 12rpx 12rpx 12rpx 12rpx;
  606. }
  607. }
  608. &-name {
  609. font-family: PingFang SC, PingFang SC;
  610. font-weight: 600;
  611. font-size: 28rpx;
  612. color: #222222;
  613. }
  614. &-head {
  615. flex: 1;
  616. overflow: hidden;
  617. margin-left: 22rpx;
  618. margin-right: 22rpx;
  619. font-family: PingFang SC, PingFang SC;
  620. font-weight: 400;
  621. font-size: 23rpx;
  622. color: #999999;
  623. }
  624. }
  625. .page-bg {
  626. position: absolute;
  627. top: 0;
  628. left: 0;
  629. }
  630. .container-body {
  631. position: relative;
  632. padding: 32rpx 30rpx;
  633. z-index: 2;
  634. @include u-flex(column, flex-star, center);
  635. .banner-box {
  636. width: 100%;
  637. min-width: 686rpx;
  638. overflow: hidden;
  639. background: rgba(255, 255, 255, 0.9);
  640. border-radius: 24rpx 24rpx 24rpx 24rpx;
  641. border: 4rpx solid #FFFFFF;
  642. box-sizing: border-box;
  643. image {
  644. width: 100%;
  645. min-width: 690rpx;
  646. height: 264rpx;
  647. background: #f7f7f7;
  648. }
  649. }
  650. .banner-txt {
  651. padding: 32rpx;
  652. font-family: PingFang SC, PingFang SC;
  653. font-weight: 400;
  654. font-size: 32rpx;
  655. color: #333333;
  656. word-break: break-all;
  657. }
  658. .ques-box {
  659. @include u-flex(row, flex-star, flex-star);
  660. flex-wrap: wrap;
  661. margin: 22rpx -8rpx -10rpx;
  662. view {
  663. padding: 12rpx 36rpx;
  664. background: #FFFFFF;
  665. border-radius: 16rpx 16rpx 16rpx 16rpx;
  666. border: 1rpx solid #ECECEC;
  667. font-family: PingFang SC, PingFang SC;
  668. font-weight: 500;
  669. font-size: 28rpx;
  670. color: #333333;
  671. margin: 10rpx 8rpx;
  672. }
  673. }
  674. }
  675. .chatinput {
  676. position: fixed;
  677. left: 32rpx;
  678. right: 32rpx;
  679. z-index: 999;
  680. bottom: calc(var(--window-bottom) + 24rpx);
  681. height: 96rpx;
  682. background-color: green;
  683. background: #FFFFFF;
  684. box-shadow: 0rpx 8rpx 21rpx 0rpx rgba(0, 0, 0, 0.1);
  685. border-radius: 24rpx 24rpx 24rpx 24rpx;
  686. @include u-flex(row, center, center);
  687. padding: 0 24rpx;
  688. box-sizing: border-box;
  689. .icon {
  690. height: 48rpx;
  691. width: 48rpx;
  692. margin-left: 32rpx;
  693. }
  694. .icon2 {
  695. height: 56rpx;
  696. width: 56rpx;
  697. margin-left: 32rpx;
  698. }
  699. }
  700. .msg-scroll {
  701. height: calc(100vh - var(--window-bottom) - 120rpx);
  702. }
  703. .TUI-message-list {
  704. width: 100%;
  705. box-sizing: border-box;
  706. .time-container {
  707. font-family: PingFang SC, PingFang SC;
  708. font-weight: 400;
  709. font-size: 24rpx;
  710. color: #999999;
  711. padding: 36rpx 0;
  712. margin: 10px;
  713. text-align: center;
  714. }
  715. .avatar {
  716. flex-shrink: 0;
  717. width: 88rpx;
  718. height: 88rpx;
  719. background: #FFFFFF;
  720. border-radius: 12rpx 12rpx 12rpx 12rpx;
  721. }
  722. .msg-item {
  723. margin-bottom: 24rpx;
  724. }
  725. .ai-msg {
  726. @include u-flex(row, flex-start, flex-start);
  727. .avatar {
  728. margin-right: 24rpx;
  729. }
  730. .msg-text {
  731. border-radius: 0rpx 24rpx 24rpx 24rpx;
  732. }
  733. }
  734. .my-msg {
  735. @include u-flex(row-reverse, flex-start, flex-start);
  736. .avatar {
  737. margin-left: 24rpx;
  738. }
  739. .msg-text {
  740. border-radius: 24rpx 0 24rpx 24rpx;
  741. background: #FEC75C;
  742. }
  743. }
  744. .msg-text {
  745. padding: 24rpx 24rpx 0 24rpx;
  746. background: #FFFFFF;
  747. overflow: hidden;
  748. }
  749. }
  750. </style>