msg.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. <template>
  2. <view class="page">
  3. <view class="flex_c pages" @touchmove="touchmove">
  4. <scroll-view class="scroll-view flex1" scroll-y scroll-with-animation :scroll-top="top" scroll-with-animation @scroll="scroll" @scrolltoupper="scrolltoupper" @scrolltolower="scrolltolower" upper-threshold="80" :scroll-into-view="scrollIntoViewId">
  5. <view class="scroll-view-msg" style="padding: 30rpx 30rpx;">
  6. <template v-for="(item, index) in list">
  7. <view class="msgContent" :id="'msg-' + item.messageId">
  8. <view class="text_26 color__ time">
  9. {{ renderMessageDate(item, index) }}
  10. </view>
  11. <view class="message" :class="[item.userType]">
  12. <image :src="item.avatar" v-if="item.userType === 'friend'" class="avatar" mode="widthFix"></image>
  13. <view class="content">
  14. <view class="" v-if="item.msgType == 'quick'">
  15. <view class="quickCon">{{item.content}}</view>
  16. <view class="quickList" v-if="item.list.length > 0">
  17. <view class="list-items" v-for="(qItem,indx) in item.list" @click="quickQs(qItem)">
  18. {{qItem.question}}
  19. </view>
  20. </view>
  21. </view>
  22. <view class="m-address" v-if="item.msgType == 'address'">
  23. <view class="image"><image :src="item.content.img" mode="widthFix"></image></view>
  24. <view class="infos">
  25. <view class="title">{{item.content.name}}</view>
  26. <view class="desc">{{item.content.order?item.content.order:'-'}}</view>
  27. </view>
  28. </view>
  29. <view class="m-address" v-if="item.msgType == 'order'">
  30. <view class="image"><image :src="item.content.img" mode="widthFix"></image></view>
  31. <view class="infos">
  32. <view class="title">{{item.content.name}}</view>
  33. <view class="desc">{{item.content.order?item.content.order:'-'}}</view>
  34. </view>
  35. </view>
  36. <view class="m-image" v-if="item.msgType == 'image'">
  37. <image :src="item.content" mode="widthFix" @click="previewImg(item.content)"></image>
  38. </view>
  39. <view class="m-address" v-if="item.msgType == 'pay'">
  40. <view class="image"><image :src="item.content.img" mode="widthFix"></image></view>
  41. <view class="infos">
  42. <view class="title">{{item.content.name}}</view>
  43. <view class="price">¥ {{item.content.price?item.content.price:'-'}}</view>
  44. </view>
  45. </view>
  46. <view v-if="item.msgType == 'text'">
  47. <view v-html="renderTextMessage(item.content)"></view>
  48. </view>
  49. </view>
  50. <image :src="item.avatar" v-if="item.userType === 'self'" class="avatar" mode="widthFix"></image>
  51. </view>
  52. </view>
  53. </template>
  54. </view>
  55. </scroll-view>
  56. <view class="tool">
  57. <view class="tool-option">
  58. <view class="input-text">
  59. <textarea class="input" placeholder="输入点什么呢..." auto-height="true" confirm-type="send" type="text"
  60. :maxlength="-1" :adjust-position="false" v-model="text" confirm-hold
  61. :show-confirm-bar="false" @confirm="sendingText" @keyboardheightchange="keyboardheightchange" />
  62. </view>
  63. <view class="check-img">
  64. <view class="check-icon" @click="tapEmoji"><image src="@/static/image/face.svg" mode="aspectFill"></image></view>
  65. <view class="check-icon" @click="tapMore"><image src="@/static/image/more.svg" mode="aspectFill"></image></view>
  66. </view>
  67. </view>
  68. </view>
  69. <template v-if="keyboardHeight>0">
  70. <view class="keyboardheight" :style="{ height: keyboardHeight + 'px' }"></view>
  71. </template>
  72. <view>
  73. <emoji v-model="isEmoji" @onEmoji="onEmoji" @deleteFn="deleteFn" @sendingText="sendingText" @sendingEmojiPack="sendingEmojiPack"></emoji>
  74. </view>
  75. </view>
  76. </view>
  77. </template>
  78. <script>
  79. var {Push} = require("@/static/js/push.js")
  80. import * as Api from "@/static/api/msg.js";
  81. import form from "@/static/js/form.js";
  82. let cursor = 0; //输入框光标
  83. var app;
  84. import emoji from '@/components/bottom-operation/emoji.vue';
  85. import { mapState } from 'vuex';
  86. import {
  87. show,
  88. formatDate,
  89. throttle,
  90. openimg,
  91. getLocation,
  92. to as tofn
  93. } from '@/utils/index.js';
  94. import { EmojiDecoder, emojiMap } from '@/utils/EmojiDecoder.js';
  95. const emojiUrl = 'https://imgcache.qq.com/open/qcloud/tim/assets/emoji/';
  96. const decoder = new EmojiDecoder(emojiUrl, emojiMap);
  97. export default {
  98. components: {
  99. emoji
  100. },
  101. data() {
  102. return {
  103. text: '',
  104. list: [],
  105. allLoaded:false,
  106. msgPage:2,
  107. top: 0, // 绑定给 scroll-view 的滚动位置
  108. isEmoji:false,
  109. isMore:false,
  110. keyboardHeight:0,
  111. pushObj:null,
  112. wsAuth:"https://tran.jsshuita.cn/plugin/webman/push/auth",
  113. wsUrl: 'wss://tran.jsshuita.cn/ws',
  114. _friendAvatar:"",
  115. _selfAvatar:"",
  116. isKeyboard:false,
  117. screenWidth:0,
  118. currentTop: 0,
  119. anchorOffset: 0, // 锚点相对于容器的偏移
  120. scrollIntoViewId:""
  121. };
  122. },
  123. computed: mapState({
  124. //显示时间
  125. renderMessageDate() {
  126. return (message, index) => {
  127. if (this.list[index + 1]?.timestamp - message.timestamp > 3 * 60 * 1000) {
  128. return formatDate(message.timestamp, 'timestamp');
  129. }
  130. return '';
  131. };
  132. },
  133. }),
  134. onLoad(options) {
  135. app = this;
  136. app.optionsData = options;
  137. uni.setNavigationBarTitle({
  138. title:"加载中..."
  139. });
  140. const systemInfo = uni.getSystemInfoSync();
  141. app.screenWidth = systemInfo.screenWidth;
  142. app.getService(options)
  143. },
  144. beforeDestroy() {
  145. cursor = 0;
  146. this.pushObj.disconnect()
  147. },
  148. methods: {
  149. anchorScroll(id) {
  150. // 直接赋值已声明变量,无需$set
  151. this.scrollIntoViewId = 'msg-' + id
  152. },
  153. scroll(e){
  154. if (!this.allLoaded) {
  155. this.currentTop = e.detail.scrollTop
  156. };
  157. },
  158. // 滚动到底部
  159. scrolltolower() {
  160. // if (this.history.allLoaded) return;
  161. // console.log('触底')
  162. // this.getMoreMsg()
  163. // this.loadHistoryMessage();
  164. },
  165. previewImg(url){
  166. uni.previewImage({
  167. urls:[url],
  168. success() {},
  169. fail() {}
  170. })
  171. },
  172. // 滚动到顶部
  173. scrolltoupper() {
  174. if (this.allLoaded) return;
  175. this.getMoreMsg()
  176. },
  177. getMoreMsg(){
  178. this.allLoaded = true;
  179. var formData = this.optionsData;
  180. const tokenPoi = uni.getStorageSync("contact");
  181. formData.page = this.msgPage;
  182. formData.poi = tokenPoi.poi_id;
  183. const anchorMsgId = this.list[0].messageId
  184. Api.msg(formData).then(async (res)=>{
  185. if(res.code == 0) {
  186. return this.$dialog.showSuccess(res.msg)
  187. }
  188. if (res.data.rows.length > 0) {
  189. this.msgPage ++;
  190. // this.allLoaded = false;
  191. var msgData = res.data.rows;
  192. var pushData = [];
  193. if (msgData.length > 0) {
  194. msgData.forEach(item => pushData.push(app.formatMsg(item)))
  195. }
  196. // console.log("pushData",pushData)
  197. // app.list = [...app.list,...pushData]
  198. app.list.unshift(...pushData)
  199. this.$nextTick(() => {
  200. this.anchorScroll(anchorMsgId)
  201. })
  202. setTimeout(() => this.allLoaded = false, 60)
  203. } else {
  204. this.allLoaded = true;
  205. }
  206. })
  207. },
  208. // 实测新增DOM高度(精准适配动态气泡高度)
  209. calcNewAddHeight(addCount) {
  210. return new Promise(resolve => {
  211. uni.createSelectorQuery()
  212. .in(this)
  213. .selectAll('.scroll-view-msg')
  214. .boundingClientRect(rects => {
  215. // 前面addCount条是本次新增历史消息
  216. const addRects = rects.slice(0, addCount)
  217. const sum = addRects.reduce((p, c) => p + c.height, 0)
  218. resolve(sum)
  219. })
  220. .exec()
  221. })
  222. },
  223. quickQs(data){
  224. setTimeout(()=>{
  225. console.log('quickQs',data)
  226. },200)
  227. if (!data.answer) return ;
  228. app.list.push({
  229. content: data.answer,
  230. userType: 'self',
  231. avatar: app._selfAvatar,
  232. msgType:"text",
  233. list:[],
  234. messageId: Date.now(),
  235. timestamp: Date.now(),
  236. })
  237. app.scrollToBottom()
  238. },
  239. renderTextMessage(text) {
  240. if (!text) return "";
  241. text = text.replace(/\n/g, '');
  242. if (!text) return '<span>' + '[未知内容]' + '</span>';
  243. return '<span>' + decoder.decode(text) + '</span>';
  244. },
  245. getService(options){
  246. Api.checkIn(options).then((res)=>{
  247. uni.setNavigationBarTitle({
  248. title:"与"+res.data.store.name+"对话中"
  249. })
  250. app._selfAvatar = res.data.avatar.self;
  251. app._friendAvatar = res.data.avatar.friend;
  252. var msgData = res.data.msg.rows;
  253. var pushData = [];
  254. if (msgData.length > 0) {
  255. msgData.forEach(item => pushData.push(app.formatMsg(item)))
  256. }
  257. var quickData = res.data.quick;
  258. pushData.push({
  259. content: quickData.star_msg,
  260. userType: 'friend',
  261. avatar: res.data.avatar.friend,
  262. msgType:"quick",
  263. list:quickData.quick
  264. })
  265. // app.list.unshift(...pushData)
  266. app.list = [...app.list,...pushData]
  267. this.$nextTick(() => {
  268. this.anchorScroll(this.list.at(-1).messageId)
  269. })
  270. this.scrollToBottom()
  271. })
  272. },
  273. formatMsg(item){
  274. var payload = "";
  275. if (item.type == 'text') {
  276. payload = item.content
  277. }
  278. if (item.type == 'order') {
  279. var order = JSON.parse(item.content)
  280. payload = order
  281. }
  282. if (item.type == 'pay') {
  283. var order = JSON.parse(item.content)
  284. payload = order
  285. }
  286. if (item.type == 'address') {
  287. var order = JSON.parse(item.content)
  288. payload = order
  289. }
  290. if (item.type == 'image') {
  291. payload = item.content
  292. }
  293. const message = {
  294. content: payload,
  295. userType: item.userType,
  296. avatar: item.avatar,
  297. msgType:item.type,
  298. list:[],
  299. messageId: item.msgId,
  300. timestamp: item.time,
  301. };
  302. console.log("message",message)
  303. return message
  304. },
  305. tapMore(){
  306. },
  307. keyboardheightchange(e){
  308. // app.$dialog.showSuccess('asdasdasd')
  309. const height = e.detail.height;
  310. if (height > 0) {
  311. this.isEmoji = false;
  312. this.isMore = false;
  313. this.isKeyboard = true;
  314. } else {
  315. this.isKeyboard = false;
  316. }
  317. this.keyboardHeight = height;
  318. setTimeout(()=>{
  319. this.scrollToBottom()
  320. }, 150)
  321. },
  322. touchmove(){
  323. this.isFocus = false;
  324. this.isFocisEmojius = false;
  325. this.isEmoji = false;
  326. this.isMore = false;
  327. this.isKeyboard = false;
  328. this.keyboardHeight = 0
  329. },
  330. onEmoji(key) {
  331. const text = `${this.text.slice(0, cursor)}${key}${this.text.slice(cursor)}`;
  332. this.text = text;
  333. },
  334. sendingEmojiPack(){},
  335. // 删除表情
  336. deleteFn() {
  337. const str = this.text.charAt(this.text.length - 1);
  338. if (str === ']') {
  339. let metaChars = /\[.*?(\u4e00*\u597d*)\]/g;
  340. let xstr = '';
  341. this.text.replace(metaChars, (match) => {
  342. xstr = match;
  343. });
  344. var text = this.text;
  345. function del(str) {
  346. return text.slice(0, text.length - str.length);
  347. }
  348. this.text = del(xstr);
  349. } else {
  350. this.text = this.text.substring(0, this.text.length - 1);
  351. }
  352. },
  353. tapEmoji() {
  354. this.isEmoji = !this.isEmoji;
  355. if (this.isEmoji) {
  356. this.isKeyboard = true;
  357. }
  358. this.isMore = false;
  359. setTimeout(()=>{
  360. this.scrollToBottom()
  361. }, 150)
  362. },
  363. sendingText() {
  364. if (!this.text) return ;
  365. app.sendMessage(this.text,"text");
  366. this.text = ''
  367. this.scrollToBottom()
  368. },
  369. sendMessage(payload,type){
  370. const message = {
  371. content: payload,
  372. userType: 'self',
  373. avatar: app._selfAvatar,
  374. msgType:type,
  375. list:[],
  376. messageId: Date.now(),
  377. timestamp: Date.now(),
  378. };
  379. app.list.push(message);
  380. },
  381. chooseImage() {
  382. uni.chooseImage({
  383. // sourceType: 'album',
  384. success: (res) => {
  385. this.list.push({
  386. content: res.tempFilePaths[0],
  387. userType: 'self',
  388. messageType: 'image',
  389. avatar: this._selfAvatar
  390. })
  391. this.scrollToBottom()
  392. // 模拟对方回复
  393. setTimeout(()=>{
  394. this.list.push({
  395. content: '你好呀,朋友~',
  396. userType: 'friend',
  397. avatar: this._friendAvatar
  398. })
  399. this.scrollToBottom()
  400. }, 1500)
  401. }
  402. })
  403. },
  404. scrollToBottom() {
  405. if (this.isEmoji) {
  406. var px = 600 * (app.screenWidth / 750)
  407. this.top = (this.list.length + px) * 1000
  408. } else {
  409. this.top = (this.list.length + parseInt(this.keyboardHeight)) * 1000
  410. }
  411. },
  412. pushList(message){
  413. app.list.push(message);
  414. }
  415. }
  416. }
  417. </script>
  418. <style lang="scss" scoped>
  419. .pages {
  420. position: fixed;
  421. z-index: 1;
  422. top: 0;
  423. left: 0;
  424. bottom: 0;
  425. right: 0;
  426. background-color: #ededed;
  427. }
  428. .m-image image{width: 200rpx;}
  429. .m-address{display: flex;align-items: center;gap: 20rpx;min-width: 70vw;}
  430. .self .m-address{transform: rotate(180);}
  431. .m-address .image image{width: 120upx;}
  432. .m-address .infos{flex: 1;overflow: hidden;}
  433. .m-address .infos .title{font-size: 28upx;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;}
  434. .m-address .infos .desc{font-size: 20upx;color: #999;margin-top: 20rpx;}
  435. .m-address .infos .price{font-size: 28upx;color: #f00;margin-top: 20rpx;}
  436. .safe-box{width: 100%;height: env(safe-area-inset-bottom);}
  437. .scroll-view {
  438. /* #ifdef H5 */
  439. height: calc(100vh - 44px);
  440. /* #endif */
  441. /* #ifndef H5 */
  442. height: 0;
  443. /* #endif */
  444. background: #eee;
  445. box-sizing: border-box;
  446. }
  447. .message {
  448. display: flex;
  449. align-items: flex-start;
  450. margin-bottom: 30rpx;
  451. .quickList{
  452. margin-top: 10upx;
  453. .list-items {
  454. color: blue;
  455. font-size: 28upx;
  456. line-height: 48upx;
  457. }
  458. }
  459. .avatar {
  460. width: 80rpx;
  461. height: 80rpx;
  462. border-radius: 10rpx;
  463. margin-right: 30rpx;
  464. }
  465. .content {
  466. min-height: 80rpx;
  467. max-width: 80vw;
  468. box-sizing: border-box;
  469. font-size: 28rpx;
  470. line-height: 1.3;
  471. padding: 20rpx;
  472. border-radius: 10rpx;
  473. background: #fff;
  474. image {
  475. // width: 200rpx;
  476. }
  477. }
  478. &.self {
  479. justify-content: flex-end;
  480. .avatar {
  481. margin: 0 0 0 30rpx;
  482. }
  483. .content {
  484. position: relative;
  485. &::after {
  486. position: absolute;
  487. content: '';
  488. width: 0;
  489. height: 0;
  490. border: 16rpx solid transparent;
  491. border-left: 16rpx solid #fff;
  492. right: -28rpx;
  493. top: 24rpx;
  494. }
  495. }
  496. }
  497. &.friend {
  498. .content {
  499. position: relative;
  500. &::after {
  501. position: absolute;
  502. content: '';
  503. width: 0;
  504. height: 0;
  505. border: 16rpx solid transparent;
  506. border-right: 16rpx solid #fff;
  507. left: -28rpx;
  508. top: 24rpx;
  509. }
  510. }
  511. }
  512. }
  513. .tool {
  514. // min-height: 120rpx;
  515. // position: fixed;
  516. // right: 0;
  517. // left: 0;
  518. // bottom: 0;
  519. background: #fff;
  520. padding: 20rpx 0 20rpx 0;
  521. padding-bottom: calc(20rpx + constant(safe-area-inset-bottom)/2) !important;
  522. padding-bottom: calc(20rpx + env(safe-area-inset-bottom)/2) !important;
  523. .tool-option{
  524. display: flex;
  525. align-items: flex-start;
  526. box-sizing: border-box;
  527. padding: 0 20rpx;
  528. }
  529. .input-text{
  530. flex: 1;width: 100%;
  531. box-sizing: border-box;
  532. padding: 10rpx 14rpx;
  533. min-height: 84rpx;
  534. max-height: 300rpx;
  535. overflow: auto;
  536. border-radius: 10rpx;
  537. background-color: #eee;
  538. .input{
  539. margin: 10rpx 0;
  540. width: 100%;
  541. font-size: 28upx;
  542. height: 100%;
  543. background-color: #eee;
  544. min-height: 48rpx;
  545. max-height: 300rpx;
  546. }
  547. }
  548. .check-img{
  549. display: flex;align-items: center;justify-content: center;
  550. .check-icon{width: 84rpx;height: 84rpx;display: flex;align-items: center;justify-content: center;}
  551. image{width: 75%;height: 75%;}
  552. }
  553. .thumb {
  554. width: 64rpx;
  555. }
  556. }
  557. .time {
  558. width: 100%;
  559. color: #a3a3a3;
  560. line-height: 100rpx;
  561. text-align: center;
  562. font-size: 24rpx;
  563. }
  564. </style>