chat.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  1. <template>
  2. <view>
  3. <view class="flex_c page" @touchmove="touchmove">
  4. <scroll-view class="flex1 scroll-Y" @tap.stop="onPage" id="scroll-view" lower-threshold="100" scroll-y
  5. scroll-with-animation :scroll-top="scroll_top" @scroll="scroll" @scrolltoupper="scrolltoupper"
  6. @scrolltolower="scrolltolower">
  7. <view class="scroll-view-str" :style="{height: `${reserveHeight}px`}" v-if="reserveHeight>0"></view>
  8. <view class="messageList_">
  9. <template v-for="(item, index) in history.messages">
  10. <!-- #ifdef APP || H5 -->
  11. <view class="z_index2" :class="`oneheight_${index}`" style="transform: rotate(-180deg)"
  12. :key="item.messageId + index" v-if="!item.isHide">
  13. <view class="icon_ text_26 color__ time">
  14. {{ renderMessageDate(item, index) }}
  15. </view>
  16. <view :key="item.messageId + index" v-if="!item.recalled">
  17. <item :isMy="isSelf(item.senderId)" :myid="myid" :item="item" @onClick="onItem"
  18. @onLongpress="onLongpress" @mention="mention" @imgLoad="imgLoad"></item>
  19. </view>
  20. <view class="icon_ text_26 recalled" v-else>
  21. <view class="">
  22. <text v-if="isSelf(item.senderId)">你</text>
  23. <text v-else>{{ item.senderData.name }}</text>
  24. 撤回了一条消息
  25. </view>
  26. <view class="recalled-edit" v-if="item.type === 'text' && isSelf(item.senderId)"
  27. @click="recalledEdit(item)">重新编辑</view>
  28. </view>
  29. </view>
  30. <!-- #endif -->
  31. <!-- #ifdef MP -->
  32. <view class="z_index2" style="transform: rotate(-180deg)" :key="item.messageId"
  33. v-if="!item.isHide">
  34. <view class="icon_ text_26 color__ time">
  35. {{ renderMessageDate(item, index) }}
  36. </view>
  37. <view :key="item.messageIds" v-if="!item.recalled">
  38. <item :isMy="isSelf(item.senderId)" :myid="myid" :item="item" @onClick="onItem"
  39. @onLongpress="onLongpress" @mention="mention"></item>
  40. </view>
  41. <view class="icon_ text_26 recalled" v-else>
  42. <view class="">
  43. <text v-if="isSelf(item.senderId)">你</text>
  44. <text v-else>{{ item.senderData.name }}</text>
  45. 撤回了一条消息
  46. </view>
  47. <view class="recalled-edit" v-if="item.type === 'text' && isSelf(item.senderId)"
  48. @click="recalledEdit(item)">重新编辑</view>
  49. </view>
  50. </view>
  51. <!-- #endif -->
  52. </template>
  53. </view>
  54. </scroll-view>
  55. <view class="bottomOperationRef">
  56. <view class="bottom-btn-group">
  57. <view class="btn-item">
  58. <image src="https://jymini.oss-cn-guangzhou.aliyuncs.com/mini/star.svg"></image>
  59. <view class="name">评价服务</view>
  60. </view>
  61. <view class="btn-item" @click="showOrder(1)">
  62. <image src="https://jymini.oss-cn-guangzhou.aliyuncs.com/mini/order.svg"></image>
  63. <view class="name">我的订单</view>
  64. </view>
  65. </view>
  66. <view class="bottom-operation-box">
  67. <view class="input-text">
  68. <textarea class="input" auto-height="true" confirm-type="send" type="text" :focus="isFocus"
  69. :maxlength="-1" :adjust-position="false" v-model="text" confirm-hold
  70. :show-confirm-bar="false" @input="input" @confirm="sendingText" @focus="focus"
  71. @blur="blured" @keyboardheightchange="keyboardheightchange" />
  72. </view>
  73. <view class="check-img">
  74. <view class="check-icon" @click="tapEmoji"><image src="@/static/image/face.svg" mode="aspectFill"></image></view>
  75. <view class="check-icon" @click="tapMore"><image src="@/static/image/more.svg" mode="aspectFill"></image></view>
  76. </view>
  77. </view>
  78. <view class="safe-box"></view>
  79. <view class="keyboardheight" :style="{ height: keyboardHeight + 'px' }"></view>
  80. </view>
  81. <view>
  82. <emoji v-model="isEmoji" @onEmoji="onEmoji" @deleteFn="deleteFn" @sendingText="sendingText"
  83. @sendingEmojiPack="sendingEmojiPack"></emoji>
  84. </view>
  85. <view>
  86. <more v-model="isMore" @onMore="onMore"></more>
  87. </view>
  88. </view>
  89. <uni-popup ref="orderpopup" type="bottom" border-radius="10px 10px 0 0" background-color="#ffffff" :mask-click="false">
  90. <view class="order-popup">
  91. <view class="order-title">
  92. <view class="name">我的订单</view>
  93. <view class="close" @click="showOrder(2)"><image src="/static/image/close.png"></image></view>
  94. </view>
  95. <view class="order-body"></view>
  96. </view>
  97. </uni-popup>
  98. </view>
  99. </template>
  100. <script>
  101. var app;
  102. let getSelectedTextRangeSetInterval = null;
  103. let inputValue = '';
  104. let cursor = 0; //输入框光标
  105. let scroll_top = 0;
  106. import emoji from '@/components/bottom-operation/emoji.vue';
  107. import more from '@/components/bottom-operation/more.vue';
  108. import item from '@/components/item/index';
  109. // 浏览照片数组
  110. let imageList = [];
  111. // 是否是手动触发的列表滑动
  112. let isBottomOperationScrollToBottom = false;
  113. import {
  114. show,
  115. formatDate,
  116. throttle,
  117. openimg,
  118. getLocation,
  119. to as tofn
  120. } from '@/utils/index.js';
  121. import {
  122. mapState
  123. } from 'vuex';
  124. var {Push} = require("@/static/js/push.js")
  125. import * as Api from "@/static/api/service.js";
  126. export default {
  127. components: {
  128. emoji,
  129. more,
  130. item
  131. },
  132. data() {
  133. return {
  134. serviceId:null, // 客服ID
  135. reserveHeight:0,
  136. isFocus: false, //键盘焦点
  137. isEmoji:false,
  138. isMore:false,
  139. keyboardHeight:0,
  140. optionsData:null,
  141. testData:null,
  142. text:"",
  143. // 历史数据
  144. history: {
  145. messages: [],
  146. allLoaded: false
  147. },
  148. showOrderState:false,
  149. storeData:null,
  150. sendId:null,
  151. myid:"",
  152. pushObj:null,
  153. wsAuth:"http://192.168.2.183:9881/plugin/webman/push/auth",
  154. // wsAuth:"https://panel.huiyinduo.cn/plugin/webman/push/auth",
  155. wsUrl: 'ws://192.168.2.183:3131',
  156. page:2,
  157. size:10
  158. }
  159. },
  160. computed: mapState({
  161. //显示时间
  162. renderMessageDate() {
  163. return (message, index) => {
  164. if (message.timestamp - this.history.messages[index + 1]?.timestamp > 3 * 60 * 1000) {
  165. return formatDate(message.timestamp, 'timestamp');
  166. }
  167. return '';
  168. };
  169. },
  170. // 是否本人isMy
  171. isSelf() {
  172. return (senderId) => {
  173. const {
  174. member_id = this.myid
  175. } = {"member_id":this.myid};
  176. return senderId === `${member_id}`;
  177. };
  178. },
  179. }),
  180. onLoad(options) {
  181. var app = this;
  182. app.optionsData = options;
  183. uni.setNavigationBarTitle({
  184. title:"加载中..."
  185. });
  186. app.getService(options)
  187. },
  188. beforeDestroy() {
  189. cursor = 0;
  190. this.pushObj.disconnect()
  191. },
  192. onPageScroll(e) {
  193. // this.$refs.bottomOperationRef.closeAll();
  194. },
  195. onReady() {
  196. // this.videoPlayer.context = uni.createVideoContext('videoPlayer', this);
  197. },
  198. methods: {
  199. initPush(){
  200. var _this = this;
  201. this.pushObj = new Push({
  202. "url":this.wsUrl,
  203. "app_key":"265c33b73d5c04f918978577df2c48d2",
  204. "auth":this.wsAuth
  205. });
  206. var user_channel = this.pushObj.subscribe('user-' + this.myid);
  207. user_channel.on('message', function (data) {
  208. // console.log(data);
  209. _this.formatMsg(data);
  210. })
  211. },
  212. imgLoad(data){},
  213. onItem(data){
  214. console.log(data)
  215. switch(data.type) {
  216. case "order":
  217. var order = data.payload.order;
  218. break;
  219. case "image":
  220. uni.previewImage({
  221. urls:[data.payload.url]
  222. })
  223. break;
  224. case "pay":
  225. var payInfo = data.payload.pay;
  226. if (payInfo.status == 1) {
  227. return this.$dialog.showSuccess("已完成支付")
  228. }
  229. Api.pricePay({order:payInfo.order}).then((res)=>{
  230. if (res.code == 0) {
  231. return app.$dialog.showSuccess(res.msg)
  232. }
  233. uni.requestPayment({
  234. orderInfo:res.data.pay,
  235. service:5,
  236. success(resp){
  237. if (resp.code == 0) { // res.code=0时,才表示支付成功
  238. app.$dialog.showSuccess("支付成功","none",function(){
  239. app.getService(app.optionsData)
  240. });
  241. }
  242. },
  243. fail(resp) {
  244. app.$dialog.showSuccess("支付失败,可再次重试","none");
  245. }
  246. })
  247. })
  248. break;
  249. default:
  250. break;
  251. }
  252. },
  253. onLongpress(data){
  254. },
  255. // 滚动中
  256. scroll(e) {
  257. scroll_top = e.detail.scrollTop;
  258. // this.$refs.operateRef.close();
  259. if (isBottomOperationScrollToBottom) return;
  260. // this.$refs.bottomOperationRef.closeAll();
  261. },
  262. // 滚动到底部
  263. scrolltolower() {
  264. if (this.history.allLoaded) return;
  265. console.log('触底')
  266. this.getMoreMsg()
  267. // this.loadHistoryMessage();
  268. },
  269. // 滚动到顶部
  270. scrolltoupper() {
  271. console.log('滚动到顶部')
  272. },
  273. showOrder(type){
  274. if (type == 2) {
  275. this.$refs.orderpopup.close()
  276. return ;
  277. }
  278. this.$refs.orderpopup.open()
  279. },
  280. getMoreMsg(){
  281. this.history.allLoaded = true;
  282. var formData = this.optionsData;
  283. formData.page = this.page;
  284. Api.msg(formData).then((res)=>{
  285. if(res.code == 0) {
  286. return this.$dialog.showSuccess(res.msg)
  287. }
  288. if (res.data.rows.length > 0) {
  289. this.page ++;
  290. var msgData = []
  291. var list = res.data.rows
  292. // 同步混入数据
  293. list.forEach((item, ix) => {
  294. var payload = {};
  295. if (item.type == 'text') {
  296. payload.text = item.content
  297. }
  298. if (item.type == 'order') {
  299. var order = JSON.parse(item.content)
  300. payload.order = order
  301. }
  302. if (item.type == 'pay') {
  303. var order = JSON.parse(item.content)
  304. payload.pay = order
  305. }
  306. if (item.type == 'image') {
  307. payload = {
  308. contentType: 'image/png',
  309. name: 'uni-image.png',
  310. size: 82942,
  311. url: item.content,
  312. width: 2732,
  313. height: 2732,
  314. thumbnail: item.content
  315. };
  316. }
  317. const message = {
  318. groupId: item.poi_id,
  319. senderData: {
  320. avatar:item.avatar
  321. },
  322. senderId: (item.source==1?item.openid:item.service_id),
  323. messageId: item.msgId,
  324. payload: payload,
  325. timestamp: item.time,
  326. type: item.type,
  327. recalled: false,
  328. status: 'success',
  329. isHide: 0
  330. };
  331. msgData[ix] = message
  332. });
  333. // console.log(msgData)
  334. // 模拟只有少量数据
  335. // this.history.messages = [list[0],list[1],list[2]];
  336. this.history.messages = [...this.history.messages, ...msgData];
  337. this.history.allLoaded = false;
  338. // if (this.history.messages.length > 20) return;
  339. // msgData.forEach((item,index)=>{
  340. // this.formatMsg(item);
  341. // })
  342. } else {
  343. this.history.allLoaded = true;
  344. }
  345. })
  346. },
  347. getService(options){
  348. Api.shareout(options).then((res)=>{
  349. if (res.code && res.code == 0) {
  350. return app.$dialog.showSuccess(res.msg)
  351. }
  352. console.log(res.data)
  353. uni.setNavigationBarTitle({
  354. title:"与"+res.data.store.poi_name+"对话中"
  355. })
  356. this.storeData = res.data.store;
  357. this.sendId = res.data.sendId;
  358. this.serviceId = res.data.serviceId;
  359. this.myid = res.data.sendId;
  360. if (res.data.code == 3) { // 无客服在线
  361. }
  362. if (res.data.msg.rows.length > 0) {
  363. var msgData = res.data.msg.rows
  364. msgData.forEach((item,index)=>{
  365. this.formatMsg(item);
  366. })
  367. }
  368. this.initPush()
  369. })
  370. },
  371. formatMsg(item){
  372. var payload = {};
  373. if (item.type == 'text') {
  374. payload.text = item.content
  375. }
  376. if (item.type == 'order') {
  377. var order = JSON.parse(item.content)
  378. payload.order = order
  379. }
  380. if (item.type == 'pay') {
  381. var order = JSON.parse(item.content)
  382. payload.pay = order
  383. }
  384. if (item.type == 'image') {
  385. payload = {
  386. contentType: 'image/png',
  387. name: 'uni-image.png',
  388. size: 82942,
  389. url: item.content,
  390. width: 2732,
  391. height: 2732,
  392. thumbnail: item.content
  393. };
  394. }
  395. const message = {
  396. groupId: item.poi_id,
  397. senderData: {
  398. avatar:item.avatar
  399. },
  400. senderId: (item.source==1?item.openid:item.service_id),
  401. messageId: item.msgId,
  402. payload: payload,
  403. timestamp: item.time,
  404. type: item.type,
  405. recalled: false,
  406. status: 'success',
  407. isHide: 0
  408. };
  409. this.pushList(message);
  410. },
  411. // 更多操作相关===============
  412. onMore(item) {
  413. switch (item.type) {
  414. case 'img':
  415. this.sendImageMessage();
  416. break;
  417. case 'video':
  418. this.sendVideoMessage();
  419. break;
  420. case 'order':
  421. this.sendOrder();
  422. break;
  423. }
  424. },
  425. sendOrder(){
  426. },
  427. // 创建发送照片内容
  428. sendImageMessage() {
  429. uni.chooseImage({
  430. count: 9,
  431. success: async (res) => {
  432. res.tempFiles.forEach((file) => {
  433. console.log(file);
  434. this.createImageMessage(file);
  435. });
  436. }
  437. });
  438. },
  439. // 创建发送照片内容
  440. createImageMessage(file) {
  441. this.sendMessage({
  442. payload: {
  443. contentType: 'image/png',
  444. name: 'uni-image.png',
  445. size: 82942,
  446. url: file.path,
  447. width: 2732,
  448. height: 2732,
  449. thumbnail: file.path
  450. },
  451. type: 'image'
  452. });
  453. },
  454. // 创建发送视频内容
  455. sendVideoMessage() {
  456. uni.chooseVideo({
  457. success: async (res) => {
  458. console.log(res);
  459. this.createVideoMessage(res);
  460. }
  461. });
  462. },
  463. createVideoMessage(file) {
  464. this.sendMessage({
  465. payload: {
  466. video: {
  467. name: '3003009356267921_uni-video.mp4',
  468. url: file.tempFilePath,
  469. width: 640,
  470. height: 352,
  471. contentType: 'video/mp4',
  472. size: 501774,
  473. duration: 8.32
  474. },
  475. thumbnail: {
  476. name: 'uni-thumbnail.jpg',
  477. url: '封面路径',
  478. width: 364,
  479. height: 200,
  480. contentType: 'image/jpg'
  481. }
  482. },
  483. type: 'video'
  484. });
  485. },
  486. sendMessage({payload,type}){
  487. const message = {
  488. groupId: this.storeData.poi_id,
  489. senderData: {},
  490. senderId: this.sendId,
  491. messageId: Date.now(),
  492. payload: payload,
  493. timestamp: Date.now(),
  494. type: type,
  495. recalled: false,
  496. status: 'success',
  497. isHide: 0
  498. };
  499. this.pushList(message);
  500. },
  501. // 组装item
  502. initMessageItem(message, index) {
  503. message['isHide'] = 0;
  504. if (index === 0 && (message.type === 'text' || message.type === 'text_quote')) {
  505. this.onSetText(message.payload.text);
  506. }
  507. },
  508. // 文本触发效果相关========
  509. onSetText(text) {
  510. // 触发礼花
  511. throttle(() => {
  512. if (text.includes('[彩带]')) {
  513. this.$refs.mScreenAnimationLihua.show();
  514. uni.vibrateLong();
  515. }
  516. }, 4000);
  517. },
  518. sendingEmojiPack(){
  519. },
  520. // 删除表情
  521. deleteFn() {
  522. const str = this.text.charAt(this.text.length - 1);
  523. if (str === ']') {
  524. let metaChars = /\[.*?(\u4e00*\u597d*)\]/g;
  525. let xstr = '';
  526. this.text.replace(metaChars, (match) => {
  527. xstr = match;
  528. });
  529. var text = this.text;
  530. function del(str) {
  531. return text.slice(0, text.length - str.length);
  532. }
  533. this.text = del(xstr);
  534. } else {
  535. this.text = this.text.substring(0, this.text.length - 1);
  536. }
  537. },
  538. onEmoji(key) {
  539. const text = `${this.text.slice(0, cursor)}${key}${this.text.slice(cursor)}`;
  540. this.text = text;
  541. },
  542. tapEmoji() {
  543. this.isEmoji = !this.isEmoji;
  544. if (this.isEmoji) {
  545. this.isKeyboard = true;
  546. }
  547. this.isMore = false;
  548. },
  549. tapMore() {
  550. this.isMore = !this.isMore;
  551. this.isEmoji = false;
  552. },
  553. touchmove(){
  554. this.isFocus = false;
  555. this.isFocisEmojius = false;
  556. this.isMore = false;
  557. this.isKeyboard = false;
  558. this.keyboardHeight = 0
  559. },
  560. onPage(){},
  561. // 监听输入
  562. input() {
  563. // if (inputValue.length > this.text.length) {} else {
  564. // const str = this.text.charAt(this.text.length - 1);
  565. // }
  566. // inputValue = this.text;
  567. },
  568. blured(){
  569. this.isFocus = false;
  570. this.keyboardHeight = 0
  571. },
  572. // 获取焦点
  573. focus(e) {
  574. this.testData = JSON.stringify(e.detail);
  575. this.isFocus = true;
  576. this.isEmoji = false;
  577. this.isMore = false;
  578. this.keyboardHeight = (e.detail.height==100)?0:e.detail.height
  579. },
  580. keyboardheightchange(e){
  581. // app.$dialog.showSuccess('asdasdasd')
  582. console.log(e)
  583. },
  584. sendingText() {
  585. if (this.text === '')
  586. return ;
  587. let body = this.text;
  588. if (this.text.length >= 50) {
  589. body = this.text.substring(0, 30) + '...';
  590. }
  591. Api.send({type:"text",content: this.text,sendId: this.serviceId,groupId: this.storeData.poi_id}).then((res)=>{
  592. if (res.code == 0) {
  593. return this.$dialog.showSuccess(res.msg)
  594. }
  595. this.sendMessage({
  596. payload: {
  597. text: this.text
  598. },
  599. type: 'text'
  600. });
  601. this.text = '';
  602. })
  603. },
  604. async pushList(message) {
  605. this.initMessageItem(message);
  606. this.history.messages.unshift(message);
  607. // 是否触发文字动效果
  608. if (message.type === 'text' || message.type === 'text_quote') {
  609. this.onSetText(message.payload.text);
  610. }
  611. // 缓存照片地址,
  612. if (message.type === 'image' || message.type === 'image_transmit') {
  613. imageList.push(message.payload.url);
  614. }
  615. },
  616. }
  617. }
  618. </script>
  619. <style lang="scss">
  620. @import '@/static/index.scss';
  621. .order-popup .order-title{display: flex;align-items: center;justify-content: space-between;font-size: 28upx;padding: 20upx;}
  622. .order-popup .order-title image{width: 40upx;height: 40upx;}
  623. .order-popup .order-body{height: 40vh;overflow: auto;}
  624. .bottomOperationRef{background-color: #f6f6f6;}
  625. .bottom-btn-group{display: flex;gap: 20upx;padding: 10rpx 14rpx;}
  626. .bottom-btn-group .btn-item{display: flex;align-items: center;gap: 10upx;font-size: 24upx;background-color: #f6f6f6;padding: 10rpx 14rpx;border-radius: 10upx;}
  627. .bottom-btn-group .btn-item image{width: 30upx;height: 30upx;}
  628. .bottom-operation-box{display: flex;padding: 20rpx 20rpx 0 20rpx;gap: 20rpx;align-items: flex-end;}
  629. .bottom-operation-box .check-img{display: flex;align-items: center;justify-content: center;}
  630. .bottom-operation-box .check-img .check-icon{width: 84rpx;height: 84rpx;display: flex;align-items: center;justify-content: center;}
  631. .bottom-operation-box .check-img image{width: 75%;height: 75%;}
  632. .bottom-operation-box .input-text{flex: 1;width: 100%;
  633. box-sizing: border-box;
  634. padding: 10rpx 14rpx;
  635. min-height: 84rpx;
  636. max-height: 300rpx;
  637. overflow: auto;
  638. border-radius: 10rpx;
  639. background-color: #fff}
  640. .bottom-operation-box .input{margin: 10rpx 0;width: 100%;font-size: 24upx;height: 100%;background-color: transparent;min-height: 48rpx;
  641. max-height: 300rpx;}
  642. .safe-box{width: 100%;height: env(safe-area-inset-bottom);background-color: #f6f6f6;}
  643. .page {
  644. position: fixed;
  645. z-index: 1;
  646. top: 0;
  647. left: 0;
  648. bottom: 0;
  649. right: 0;
  650. background-color: #ededed;
  651. }
  652. .scroll-Y {
  653. width: 100%;
  654. height: 0;
  655. transition: all 0.2s;
  656. transform: rotate(180deg);
  657. background-color: #ededed;
  658. padding: 20upx 0;
  659. ::-webkit-scrollbar {
  660. display: none;
  661. }
  662. }
  663. .scroll-view-str {
  664. width: 100%;
  665. }
  666. .time {
  667. width: 100%;
  668. color: #a3a3a3;
  669. line-height: 100rpx;
  670. }
  671. .recalled {
  672. width: 100%;
  673. height: 50rpx;
  674. margin: 20rpx 0;
  675. color: #a3a3a3;
  676. .recalled-edit {
  677. color: #5a6693;
  678. margin-left: 14rpx;
  679. }
  680. }
  681. </style>