service.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982
  1. <template>
  2. <view class="wanl-chat">
  3. <view @touchstart="hideDrawer">
  4. <scroll-view class="cu-chat" scroll-y="true" :scroll-with-animation="scrollAnimation" :scroll-top="scrollTop"
  5. :scroll-into-view="scrollToView" upper-threshold="50">
  6. <view v-for="(row, index) in msgList" :key="index" :id="'msg' + row.id">
  7. <!-- 自己的消息 -->
  8. <view class="cu-item self" v-if="parseInt(row.form.id) === parseInt(user_id)">
  9. <view class="main" v-if="row.message.type == 'text'">
  10. <view class="content bg-green"><rich-text :nodes="row.message.content.text"></rich-text></view>
  11. </view>
  12. <!-- 图片消息 -->
  13. <view class="main" v-if="row.message.type == 'img'">
  14. <image :src="row.message.content.url" @tap="showPic(row.message)"
  15. :style="{ width: row.message.content.w + 'px', height: row.message.content.h + 'px' }"></image>
  16. </view>
  17. <!-- 语音消息 -->
  18. <view class="main" v-if="row.message.type == 'voice'" @tap="playVoice(row.message)"
  19. :class="playMsgid == row.message.id ? 'play' : ''">
  20. <view class="action text-bold text-grey" style="padding-right: 2rpx;">
  21. {{ row.message.content.length }}
  22. <text class="wlIcon-miao"></text>
  23. </view>
  24. <view class="content bg-green">
  25. <text :style="{ width: row.message.content.length * 6 + 'rpx' }"></text>
  26. <text class="wlIcon-yuyinyou text-xxl padding-left-xl"></text>
  27. </view>
  28. </view>
  29. <view class="cu-avatar round"
  30. :style="{ backgroundImage: 'url(' + buildOssUrl(row.form.avatar, 44, 44, 2, 'avatar') + ')' }"></view>
  31. </view>
  32. <!-- 对方的消息 -->
  33. <view class="cu-item" v-else>
  34. <view class="cu-avatar round" :style="{ backgroundImage: 'url(' + buildOssUrl(row.form.avatar) + ')' }"
  35. v-if="row.form.avatar"></view>
  36. <view class="cu-avatar round" :style="{ backgroundImage: 'url(' + appLogo + ')' }" v-else></view>
  37. <!-- 文字消息 -->
  38. <view class="main" v-if="row.message.type == 'text'">
  39. <view class="content"><rich-text :nodes="row.message.content.text"></rich-text></view>
  40. </view>
  41. <!-- 列表消息 -->
  42. <view class="main" v-if="row.message.type == 'list'">
  43. <view class="content">
  44. <view class="list">
  45. <view v-if="row.message.content.length > 0">
  46. <view>
  47. {{ $t('service.askQuestion') }}
  48. <view class="ai solid-top solid-bottom">
  49. <view v-for="(item, index) in row.message.content" :key="item.id"
  50. @tap="aiSend(item.keywords ? item.keywords : this.$t('service.noKeywords'))" class="text-cut">
  51. <text>{{ item.title }}</text></view>
  52. </view>
  53. </view>
  54. <view>
  55. {{ $t('service.notMatch') }} <text class="click-here"
  56. @tap="aiSend(this.$t('service.artificialService'))">{{ $t('service.clickHere') }}</text>{{ $t('service.orReplyHuman') }}
  57. </view>
  58. </view>
  59. <view v-else>
  60. {{ $t('service.noHelpContent') }}<text class="click-here"
  61. @tap="aiSend(this.$t('service.artificialService'))">{{ $t('service.clickHere') }}</text>{{ $t('service.orReplyHuman') }}
  62. </view>
  63. </view>
  64. </view>
  65. </view>
  66. <!-- 文章消息 -->
  67. <view class="main" v-if="row.message.type == 'article'"
  68. @tap="onDetails(row.message.content.id, row.message.content.title)">
  69. <view class="content">
  70. <view style="width: 100%;">
  71. <view>
  72. {{ row.message.content.title }}
  73. </view>
  74. <view class="article solid-top">
  75. <rich-text :nodes="row.message.content.content"></rich-text>
  76. </view>
  77. </view>
  78. </view>
  79. </view>
  80. <!-- 图片消息 -->
  81. <view class="main" v-if="row.message.type == 'img'">
  82. <image :src="row.message.content.url" @tap="showPic(row.message)"
  83. :style="{ width: row.message.content.w + 'px', height: row.message.content.h + 'px' }"></image>
  84. </view>
  85. <view class="date">{{ timeToChat(row.createtime) }} </view>
  86. </view>
  87. </view>
  88. </scroll-view>
  89. </view>
  90. <!-- 抽屉栏 -->
  91. <view class="popup-layer" :class="{ showLayer: popupLayerClass }" @touchmove.stop.prevent="discard">
  92. <!-- 表情 -->
  93. <view :class="{ hidden: hideEmoji }">
  94. <view class="emoji">
  95. <scroll-view class="emojinav" scroll-x scroll-with-animation>
  96. <view class="item">
  97. <view :class="item == TabCur ? 'emojibg' : ''" v-for="(item, index) in emojiList.categories" :key="index"
  98. @tap="tabSelect" :data-id="item"
  99. :style="{ backgroundImage: 'url(' + emojiList.groups[item][0].url + ')' }"></view>
  100. </view>
  101. </scroll-view>
  102. <!-- 列表 -->
  103. <scroll-view v-for="(emoji, groups) in emojiList.groups" :key="groups" v-if="TabCur == groups" class="subject"
  104. scroll-y scroll-with-animation>
  105. <view class="item grid margin-bottom text-center col-5">
  106. <view v-for="(item, index) in emoji" :key="index" @tap="addEmoji(item.value)"
  107. :style="{ backgroundImage: 'url(' + item.url + ')' }"></view>
  108. </view>
  109. </scroll-view>
  110. </view>
  111. </view>
  112. <!-- 更多功能-->
  113. <view class="solid-top" :class="{ hidden: hideMore }">
  114. <view class="opmenu solid-top">
  115. <view class="box" @tap="chooseImage">
  116. <view class="icon"><text class="wlIcon-tupian1"></text></view>
  117. <text class="text-sm">{{ $t('service.photos') }}</text>
  118. </view>
  119. <!-- #ifndef H5 -->
  120. <view class="box" @tap="camera">
  121. <view class="icon"><text class="wlIcon-31paishexuanzhong"></text></view>
  122. <text class="text-sm">{{ $t('service.camera') }}</text>
  123. </view>
  124. <!-- #endif -->
  125. </view>
  126. </view>
  127. </view>
  128. <!-- 底部输入栏 -->
  129. <view class="input-box" :class="{ emptybottom: emptybottom, showLayer: popupLayerClass}" @touchmove.stop.prevent="discard">
  130. <!-- H5下不能录音,输入栏布局改动一下 -->
  131. <!-- #ifndef H5 -->
  132. <view class="voice"><view :class="isVoice ? 'wlIcon-jianpanqiehuan' : 'wlIcon-yuyin'" @tap="switchVoice"></view></view>
  133. <!-- #endif -->
  134. <!-- #ifdef H5 -->
  135. <view class="more" @tap="showMore"><view class="wlIcon-yuanquanjiahao"></view></view>
  136. <!-- #endif -->
  137. <view class="textbox">
  138. <view
  139. class="voice-mode"
  140. :class="[isVoice ? '' : 'hidden', recording ? 'recording' : '']"
  141. @touchstart="voiceBegin"
  142. @touchmove.stop.prevent="voiceIng"
  143. @touchend="voiceEnd"
  144. @touchcancel="voiceCancel"
  145. >
  146. {{ voiceTis }}
  147. </view>
  148. <view class="text-mode" :class="isVoice ? 'hidden' : ''">
  149. <view class="box">
  150. <textarea auto-height="true" maxlength="300" :show-confirm-bar="false" cursor-spacing="90" v-model="textMsg" @focus="textareaFocus" @blur="textareaBlur" @confirm="sendText" />
  151. </view>
  152. <view class="em" @tap="chooseEmoji"><view class="wlIcon-biaoqing2"></view></view>
  153. </view>
  154. </view>
  155. <!-- #ifndef H5 -->
  156. <view class="more" @tap="showMore" style="margin-right: -12rpx;"><view class="wlIcon-yuanquanjiahao"></view></view>
  157. <!-- #endif -->
  158. <view class="send" :class="isVoice ? 'hidden' : ''" @tap="sendText">
  159. <text class="wlIcon-zhifeiji" v-if="textMsg"></text>
  160. <text class="wlIcon-fasong" v-else></text>
  161. </view>
  162. </view>
  163. <!-- 录音UI效果 -->
  164. <view class="record" :class="recording ? '' : 'hidden'">
  165. <view class="ing" :class="willStop ? 'hidden' : ''">
  166. <view class="wlIcon-huatong01"></view>
  167. </view>
  168. <view class="cancel" :class="willStop ? '' : 'hidden'">
  169. <view class="wlIcon-shanchu2"></view>
  170. </view>
  171. <view class="tis" :class="willStop ? 'change' : ''">{{ recordTis }}</view>
  172. </view>
  173. </view>
  174. </template>
  175. <script>
  176. const emotions = require('@/static/json/emotions.json')
  177. import { mapState } from 'vuex'
  178. import { buildOssUrl } from '@/core/media/oss-url'
  179. import { getUploadConfig } from '@/features/mall/gateway/chat'
  180. export default {
  181. data() {
  182. return {
  183. user_id: parseInt(this.$store.state.user.id),
  184. avatar: this.$store.state.user.avatar,
  185. nickname: this.$store.state.user.nickname,
  186. to_id: 0,
  187. textMsg: '',
  188. scrollAnimation: false,
  189. scrollTop: 0,
  190. scrollToView: '',
  191. msgList: [],
  192. msgImgList: [],
  193. emptybottom: false,
  194. // #ifndef H5
  195. RECORDER: uni.getRecorderManager(),
  196. // #endif
  197. isVoice: false,
  198. voiceTis: this.$t('service.holdToSpeak'),
  199. recordTis: this.$t('service.slideUpToCancel'),
  200. recording: false,
  201. willStop: false,
  202. initPoint: { identifier: 0, Y: 0 },
  203. recordTimer: null,
  204. recordLength: 0,
  205. AUDIO: uni.createInnerAudioContext(),
  206. playMsgid: null,
  207. VoiceTimer: null,
  208. popupLayerClass: false,
  209. hideMore: true,
  210. TabCur: this.$t('service.default'),
  211. hideEmoji: true,
  212. emojiList: this.emojiData(),
  213. QnUrl: ''
  214. }
  215. },
  216. computed: {
  217. ...mapState({
  218. appLogo: state => (state.common && state.common.appConfig && state.common.appConfig.logo) || ''
  219. })
  220. },
  221. onLoad(option) {
  222. this.loadData()
  223. this.AUDIO.onEnded(res => {
  224. this.playMsgid = null
  225. })
  226. // #ifndef H5
  227. this.RECORDER.onStart(e => {
  228. this.recordBegin(e)
  229. })
  230. this.RECORDER.onStop(e => {
  231. this.recordEnd(e)
  232. })
  233. // #endif
  234. uni.$on('onService', this.onChat)
  235. },
  236. onShow() {
  237. this.scrollTop = 9999999
  238. },
  239. onUnload() {
  240. uni.$off('onService', this.onChat)
  241. },
  242. methods: {
  243. buildOssUrl,
  244. async loadData() {
  245. await uni.request({
  246. url: '/wanlshop/chat/hello',
  247. method: 'POST',
  248. data: {
  249. id: 1,
  250. type: 'service',
  251. form_id: this.user_id
  252. }
  253. })
  254. },
  255. async sendMsg(content, type) {
  256. let lastid = 2
  257. if (this.msgList.length) {
  258. lastid = this.msgList[this.msgList.length - 1].id
  259. lastid++
  260. }
  261. let data = {
  262. id: lastid,
  263. type: 'service',
  264. to_id: this.to_id,
  265. form: {
  266. id: this.user_id,
  267. avatar: this.avatar,
  268. name: this.nickname
  269. },
  270. message: {
  271. type: type,
  272. content: content
  273. },
  274. createtime: parseInt(new Date().getTime() / 1000)
  275. }
  276. this.onChat(JSON.parse(JSON.stringify(data)), true)
  277. await uni.request({
  278. url: '/wanlshop/chat/service',
  279. method: 'POST',
  280. data: data
  281. })
  282. },
  283. onChat(msg, type) {
  284. if (!type) {
  285. this.to_id = msg.form.id
  286. if (msg.form.hasOwnProperty('name')) {
  287. uni.setNavigationBarTitle({ title: msg.form.name })
  288. }
  289. }
  290. if (msg.message.type == 'list') {
  291. this.addListMsg(msg)
  292. }
  293. if (msg.message.type == 'article') {
  294. this.addArticleMsg(msg)
  295. }
  296. if (msg.message.type == 'text') {
  297. this.addTextMsg(msg)
  298. }
  299. if (msg.message.type == 'voice') {
  300. this.addVoiceMsg(msg)
  301. }
  302. if (msg.message.type == 'img') {
  303. this.addImgMsg(msg)
  304. }
  305. if (msg.message.type == 'end') {
  306. uni.showToast({ title: msg.message.content, icon: 'none' })
  307. }
  308. this.$nextTick(() => {
  309. this.scrollToView = 'msg' + msg.id
  310. })
  311. },
  312. addListMsg(msg) {
  313. this.msgList.push(msg)
  314. },
  315. addArticleMsg(msg) {
  316. this.msgList.push(msg)
  317. },
  318. addTextMsg(msg) {
  319. msg.message.content.text = this.replaceEmoji(msg.message.content.text)
  320. this.msgList.push(msg)
  321. },
  322. addVoiceMsg(msg) {
  323. this.msgList.push(msg)
  324. },
  325. addImgMsg(msg) {
  326. msg.message.content = this.setPicSize(msg.message.content)
  327. this.msgImgList.push(msg.message.content.url)
  328. this.msgList.push(msg)
  329. },
  330. chooseImage() {
  331. this.getImage('album')
  332. },
  333. camera() {
  334. this.getImage('camera')
  335. },
  336. getImage(type) {
  337. this.hideDrawer()
  338. uni.chooseImage({
  339. sourceType: [type],
  340. sizeType: ['original', 'compressed'],
  341. success: async res => {
  342. try {
  343. const updata = await getUploadConfig()
  344. for (let i = 0; i < res.tempFilePaths.length; i++) {
  345. uni.getImageInfo({
  346. src: res.tempFilePaths[i],
  347. success: image => {
  348. uni.uploadFile({
  349. url: updata.uploadurl,
  350. filePath: image.path,
  351. name: 'file',
  352. formData: updata.storage == 'local' ? null : updata.multipart,
  353. success: data => {
  354. this.sendMsg({ url: JSON.parse(data.data).data.fullurl, w: image.width, h: image.height }, 'img')
  355. },
  356. fail: error => {
  357. uni.showToast({ title: JSON.parse(error.data).msg, icon: 'none' })
  358. }
  359. })
  360. }
  361. })
  362. }
  363. } catch (e) {
  364. console.error('uploadConfig error:', e)
  365. }
  366. }
  367. })
  368. },
  369. sendText() {
  370. this.hideDrawer()
  371. if (!this.textMsg) return
  372. let msg = { text: this.textMsg }
  373. this.sendMsg(msg, 'text')
  374. this.textMsg = ''
  375. },
  376. aiSend(text) {
  377. this.sendMsg({ text: text }, 'text')
  378. },
  379. showPic(message) {
  380. uni.previewImage({
  381. indicator: 'none',
  382. current: message.content.url,
  383. urls: this.msgImgList
  384. })
  385. },
  386. playVoice(message) {
  387. this.playMsgid = message.id
  388. this.AUDIO.src = message.content.url
  389. this.$nextTick(() => {
  390. this.AUDIO.play()
  391. })
  392. },
  393. voiceBegin(e) {
  394. if (e.touches.length > 1) return
  395. this.initPoint.Y = e.touches[0].clientY
  396. this.initPoint.identifier = e.touches[0].identifier
  397. this.RECORDER.start({ format: 'mp3' })
  398. },
  399. recordBegin(e) {
  400. this.recording = true
  401. this.voiceTis = this.$t('service.releaseToEnd')
  402. this.recordLength = 0
  403. this.recordTimer = setInterval(() => {
  404. this.recordLength++
  405. }, 1000)
  406. },
  407. voiceCancel() {
  408. this.recording = false
  409. this.voiceTis = this.$t('service.holdToSpeak')
  410. this.recordTis = this.$t('service.slideUpToCancel')
  411. this.willStop = true
  412. this.RECORDER.stop()
  413. },
  414. voiceIng(e) {
  415. if (!this.recording) return
  416. let touche = e.touches[0]
  417. if (this.initPoint.Y - touche.clientY >= uni.upx2px(200)) {
  418. this.willStop = true
  419. this.recordTis = this.$t('service.releaseToCancel')
  420. } else {
  421. this.willStop = false
  422. this.recordTis = this.$t('service.slideUpToCancel')
  423. }
  424. },
  425. voiceEnd(e) {
  426. if (!this.recording) return
  427. this.recording = false
  428. this.voiceTis = this.$t('service.holdToSpeak')
  429. this.recordTis = this.$t('service.slideUpToCancel')
  430. this.RECORDER.stop()
  431. },
  432. async recordEnd(e) {
  433. clearInterval(this.recordTimer)
  434. if (!this.willStop) {
  435. try {
  436. const updata = await getUploadConfig()
  437. uni.uploadFile({
  438. url: updata.uploadurl,
  439. filePath: e.tempFilePath,
  440. name: 'file',
  441. formData: updata.storage == 'local' ? null : updata.multipart,
  442. header: {
  443. token: this.$store.state.user.token
  444. },
  445. success: data => {
  446. let msg = { length: 0, url: JSON.parse(data.data).data.fullurl }
  447. msg.length = this.recordLength % 60
  448. if (msg.length > 0) {
  449. this.sendMsg(msg, 'voice')
  450. }
  451. },
  452. fail: error => {
  453. uni.showToast({ title: JSON.parse(error.data).msg, icon: 'none' })
  454. }
  455. })
  456. } catch (err) {
  457. console.error('upload voice error:', err)
  458. }
  459. }
  460. this.willStop = false
  461. },
  462. switchVoice() {
  463. this.hideDrawer()
  464. this.isVoice = this.isVoice ? false : true
  465. },
  466. emojiData() {
  467. var groups = {},
  468. categories = [],
  469. map = {}
  470. emotions.forEach(emotion => {
  471. var cate = emotion.category.length > 0 ? emotion.category : '默認'
  472. if (!groups[cate]) {
  473. groups[cate] = []
  474. categories.push(cate)
  475. }
  476. groups[cate].push(emotion)
  477. map[emotion.phrase] = emotion.icon
  478. })
  479. return { groups, categories, map }
  480. },
  481. replaceEmoji(str) {
  482. if (!str) return ''
  483. let replacedStr = str
  484. Object.keys(this.emojiList.map).forEach(key => {
  485. if (str.indexOf(key) !== -1) {
  486. const url = this.emojiList.map[key]
  487. if (url) {
  488. replacedStr = replacedStr.split(key).join('<img src="' + url + '" style="width:18px;vertical-align:middle;">')
  489. }
  490. }
  491. })
  492. return replacedStr.replace(/(\r\n)|(\n)/g, '<br>')
  493. },
  494. tabSelect(e) {
  495. this.TabCur = e.currentTarget.dataset.id
  496. },
  497. setPicSize(content) {
  498. let maxW = uni.upx2px(350)
  499. let maxH = uni.upx2px(350)
  500. if (content.w > maxW || content.h > maxH) {
  501. let scale = content.w / content.h
  502. content.w = scale > 1 ? maxW : maxH * scale
  503. content.h = scale > 1 ? maxW / scale : maxH
  504. }
  505. return content
  506. },
  507. showMore() {
  508. this.isVoice = false
  509. this.hideEmoji = true
  510. if (this.hideMore) {
  511. this.hideMore = false
  512. this.openDrawer()
  513. } else {
  514. this.hideDrawer()
  515. }
  516. },
  517. openDrawer() {
  518. this.emptybottom = true
  519. this.popupLayerClass = true
  520. },
  521. hideDrawer() {
  522. this.emptybottom = false
  523. this.popupLayerClass = false
  524. setTimeout(() => {
  525. this.hideMore = true
  526. this.hideEmoji = true
  527. }, 150)
  528. },
  529. chooseEmoji() {
  530. this.hideMore = true
  531. if (this.hideEmoji) {
  532. this.hideEmoji = false
  533. this.openDrawer()
  534. } else {
  535. this.hideDrawer()
  536. }
  537. },
  538. addEmoji(em) {
  539. this.textMsg += em
  540. },
  541. textareaFocus() {
  542. this.emptybottom = true
  543. if (this.popupLayerClass && this.hideMore == false) {
  544. this.hideDrawer()
  545. }
  546. },
  547. textareaBlur() {
  548. this.emptybottom = false
  549. },
  550. onDetails(id, title) {
  551. uni.navigateTo({ url: `/pages/article/details?id=${id}&title=${title}` })
  552. },
  553. timeToChat(timestamp) {
  554. if (!timestamp) return ''
  555. const date = new Date(timestamp * 1000)
  556. const now = new Date()
  557. const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
  558. const msgDate = new Date(date.getFullYear(), date.getMonth(), date.getDate())
  559. const diffDays = Math.floor((today - msgDate) / (1000 * 60 * 60 * 24))
  560. const hours = date.getHours().toString().padStart(2, '0')
  561. const minutes = date.getMinutes().toString().padStart(2, '0')
  562. if (diffDays === 0) return hours + ':' + minutes
  563. if (diffDays === 1) return this.$t('service.yesterday') + ' ' + hours + ':' + minutes
  564. const month = (date.getMonth() + 1).toString().padStart(2, '0')
  565. const day = date.getDate().toString().padStart(2, '0')
  566. return month + '/' + day + ' ' + hours + ':' + minutes
  567. },
  568. discard() {
  569. return
  570. }
  571. }
  572. }
  573. </script>
  574. <style>
  575. .cu-chat .cu-item>.main {
  576. margin: 0 20rpx;
  577. }
  578. .cu-chat .cu-item>.main .content:after {
  579. width: 0;
  580. height: 0;
  581. }
  582. .cu-chat .cu-item>.main .content {
  583. font-size: 30rpx;
  584. border-radius: 10rpx 30rpx 30rpx 30rpx;
  585. }
  586. .cu-chat .cu-item.self>.main .content {
  587. border-radius: 30rpx 10rpx 30rpx 30rpx;
  588. }
  589. .cu-chat .cu-item>.main .content .article {
  590. margin-top: 10rpx;
  591. padding-top: 10rpx;
  592. width: 100%;
  593. overflow: hidden;
  594. }
  595. .cu-chat .cu-item>.main .content .list {
  596. width: 100%;
  597. font-size: 28rpx;
  598. }
  599. .cu-chat .cu-item>.main .content .list text {
  600. color: #FF6A00;
  601. }
  602. .cu-chat .cu-item>.main .content .list .ai {
  603. padding: 16rpx 0;
  604. margin: 16rpx 0;
  605. line-height: 1.5;
  606. }
  607. .cu-chat .cu-item [class*='wlIcon-'] {
  608. font-size: 34rpx;
  609. }
  610. .opmenu {
  611. display: flex;
  612. margin-top: 2rpx;
  613. color: #4c4c4c;
  614. }
  615. .opmenu .box {
  616. padding-top: 35rpx;
  617. padding-left: 50rpx;
  618. text-align: center;
  619. }
  620. .opmenu .box .icon {
  621. height: 130rpx;
  622. width: 130rpx;
  623. display: flex;
  624. align-items: center;
  625. justify-content: center;
  626. justify-items: center;
  627. background-color: #ffffff;
  628. border-radius: 20rpx;
  629. font-size: 70rpx;
  630. margin-bottom: 10rpx;
  631. }
  632. .hidden {
  633. display: none !important;
  634. }
  635. .popup-layer {
  636. transition: all 0.15s linear;
  637. width: 100%;
  638. height: 480rpx;
  639. background-color: #f7f7fa;
  640. position: fixed;
  641. z-index: 2000;
  642. top: 100%;
  643. }
  644. .popup-layer.showLayer {
  645. transform: translate3d(0, -480rpx, 0);
  646. }
  647. .popup-layer .emoji .emojinav {
  648. background-color: #f8f8f8;
  649. }
  650. .popup-layer .emoji .emojinav .item {
  651. display: flex;
  652. align-items: center;
  653. height: 100rpx;
  654. padding-left: 10rpx;
  655. }
  656. .popup-layer .emoji .emojinav .item .emojibg {
  657. background-color: #dedede;
  658. }
  659. .popup-layer .emoji .emojinav .item>view {
  660. margin: 0 25rpx;
  661. width: 60rpx;
  662. height: 60rpx;
  663. border-radius: 18rpx;
  664. background-repeat: no-repeat;
  665. background-size: 80%;
  666. background-position: center;
  667. }
  668. .popup-layer .emoji .subject {
  669. height: 380rpx;
  670. background-color: #f1f1f1;
  671. }
  672. .popup-layer .emoji .subject .item {
  673. padding: 25rpx;
  674. }
  675. .popup-layer .emoji .subject .item>view {
  676. background-repeat: no-repeat;
  677. background-size: 56%;
  678. background-position: center;
  679. width: 12.5%;
  680. height: 100rpx;
  681. }
  682. .input-box {
  683. width: 100%;
  684. min-height: 100rpx;
  685. padding-bottom: env(safe-area-inset-bottom);
  686. background-color: #f7f7fa;
  687. display: flex;
  688. align-items: flex-end;
  689. position: fixed;
  690. z-index: 2000;
  691. bottom: -2rpx;
  692. transition: all 0.15s linear;
  693. }
  694. .input-box [class*='wlIcon-'] {
  695. font-size: 50rpx;
  696. color: #4c4c4c;
  697. }
  698. .input-box .wlIcon-zhifeiji {
  699. color: #fe6600;
  700. }
  701. .input-box.showLayer {
  702. transform: translate3d(0, -480rpx, 0);
  703. }
  704. .input-box .voice,
  705. .input-box .more {
  706. flex-shrink: 0;
  707. width: 90rpx;
  708. height: 100rpx;
  709. display: flex;
  710. justify-content: center;
  711. align-items: center;
  712. }
  713. .input-box .send {
  714. flex-shrink: 0;
  715. width: 90rpx;
  716. height: 100rpx;
  717. display: flex;
  718. justify-content: center;
  719. align-items: center;
  720. }
  721. .input-box .send .btn {
  722. width: 110rpx;
  723. height: 70rpx;
  724. display: flex;
  725. justify-content: center;
  726. align-items: center;
  727. border-radius: 16rpx;
  728. font-size: 32rpx;
  729. }
  730. .input-box .textbox {
  731. width: 100%;
  732. }
  733. .input-box .textbox .voice-mode {
  734. width: calc(100% - 2upx);
  735. height: 80rpx;
  736. margin: 10rpx 0;
  737. border-radius: 16rpx;
  738. display: flex;
  739. justify-content: center;
  740. align-items: center;
  741. font-size: 28rpx;
  742. background-color: #fff;
  743. color: #555;
  744. }
  745. .input-box .textbox .voice-mode.recording {
  746. background-color: #e5e5e5;
  747. }
  748. .input-box .textbox .text-mode {
  749. width: 100%;
  750. min-height: 80rpx;
  751. margin: 10rpx 0;
  752. display: flex;
  753. background-color: #ffffff;
  754. border-radius: 16rpx;
  755. }
  756. .input-box .textbox .text-mode .box {
  757. width: 100%;
  758. padding-left: 30rpx;
  759. min-height: 80rpx;
  760. display: flex;
  761. align-items: center;
  762. }
  763. .input-box .textbox .text-mode .box textarea {
  764. width: 100%;
  765. }
  766. .input-box .textbox .text-mode .em {
  767. flex-shrink: 0;
  768. width: 80rpx;
  769. padding-left: 10rpx;
  770. height: 80rpx;
  771. display: flex;
  772. justify-content: center;
  773. align-items: center;
  774. }
  775. .record {
  776. width: 39vw;
  777. height: 39vw;
  778. position: fixed;
  779. top: 35%;
  780. left: 30%;
  781. background-color: rgba(0, 0, 0, 0.8);
  782. border-radius: 40rpx;
  783. }
  784. .record .ing {
  785. width: 100%;
  786. height: 30vw;
  787. display: flex;
  788. justify-content: center;
  789. align-items: center;
  790. }
  791. @keyframes volatility {
  792. 0% {
  793. background-position: 0% 130%;
  794. }
  795. 20% {
  796. background-position: 0% 150%;
  797. }
  798. 30% {
  799. background-position: 0% 155%;
  800. }
  801. 40% {
  802. background-position: 0% 160%;
  803. }
  804. 50% {
  805. background-position: 0% 145%;
  806. }
  807. 70% {
  808. background-position: 0% 150%;
  809. }
  810. 80% {
  811. background-position: 0% 170%;
  812. }
  813. 90% {
  814. background-position: 0% 160%;
  815. }
  816. 100% {
  817. background-position: 0% 135%;
  818. }
  819. }
  820. .record .ing [class*='wlIcon'] {
  821. background-image: linear-gradient(to bottom, #ffffff, #565656 50%);
  822. background-size: 100% 200%;
  823. animation: volatility 1.5s ease-in-out -1.5s infinite alternate;
  824. -webkit-background-clip: text;
  825. -webkit-text-fill-color: transparent;
  826. font-size: 140rpx;
  827. padding-top: 40rpx;
  828. color: #f09b37;
  829. }
  830. .record .cancel {
  831. width: 100%;
  832. height: 30vw;
  833. display: flex;
  834. justify-content: center;
  835. align-items: center;
  836. }
  837. .record .cancel [class*='wlIcon'] {
  838. color: #fff;
  839. font-size: 150rpx;
  840. }
  841. .record .tis {
  842. width: 100%;
  843. height: 10vw;
  844. display: flex;
  845. justify-content: center;
  846. font-size: 24rpx;
  847. color: #fff;
  848. }
  849. .record .tis.change {
  850. color: #f09b37;
  851. }
  852. .content {
  853. width: 100%;
  854. }
  855. .content .msg-list,
  856. .cu-chat {
  857. position: absolute;
  858. top: 0;
  859. bottom: 100rpx;
  860. bottom: calc(env(safe-area-inset-bottom) + 100rpx);
  861. }
  862. .loading {
  863. display: flex;
  864. justify-content: center;
  865. }
  866. @keyframes stretchdelay {
  867. 0%,
  868. 40%,
  869. 100% {
  870. transform: scaleY(0.6);
  871. }
  872. 20% {
  873. transform: scaleY(1);
  874. }
  875. }
  876. .loading .spinner {
  877. margin: 20upx 0;
  878. width: 60upx;
  879. height: 100upx;
  880. display: flex;
  881. align-items: center;
  882. justify-content: space-between;
  883. }
  884. .loading .spinner view {
  885. background-color: #dadada;
  886. height: 50upx;
  887. width: 6upx;
  888. border-radius: 6upx;
  889. animation: stretchdelay 1.2s infinite ease-in-out;
  890. }
  891. .loading .spinner .rect2 {
  892. animation-delay: -1.1s;
  893. }
  894. .loading .spinner .rect3 {
  895. animation-delay: -1s;
  896. }
  897. .loading .spinner .rect4 {
  898. animation-delay: -0.9s;
  899. }
  900. .loading .spinner .rect5 {
  901. animation-delay: -0.8s;
  902. }
  903. .emptybottom {
  904. padding-bottom: 0 !important;
  905. }
  906. </style>