chat.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. <template>
  2. <view class="chat flex-col">
  3. <view class="content" @tap="showEmoji = false">
  4. <scroll-view style="height: 100%;" :scroll-y="true" :scroll-top="scrollTop" :scroll-into-view="intoView" @scrolltoupper="scrollToupper">
  5. <view class="loading flex row-center" v-if="pageStatus == 'loading'"><u-loading mode="flower" size="40"></u-loading></view>
  6. <view class="chat-lists">
  7. <view
  8. class="chat-item"
  9. v-for="(item, index) in recoreds"
  10. :id="`chat-item_${item.id}`"
  11. :key="item.id"
  12. :class="{
  13. right: item.from_type == 'user',
  14. left: item.from_type == 'kefu',
  15. visibility: showIndex > index
  16. }"
  17. >
  18. <!-- 普通聊天记录 -->
  19. <template v-if="item.type == 1">
  20. <!-- 时间 -->
  21. <view class="text-center m-b-30 white" v-if="timeFormat(item, index)">
  22. <view class="chat-tips xs">{{ timeFormat(item, index) }}</view>
  23. </view>
  24. <view class="chat-info">
  25. <image class="avatar" :src="$getImageUri(item.from_avatar)"></image>
  26. <!-- 文本 -->
  27. <view class="text-box" v-if="item.msg_type == 1"><rich-text :nodes="replaceEmoji(item.msg)" space="nbsp"></rich-text></view>
  28. <!-- 图片 -->
  29. <view class="image-box" v-if="item.msg_type == 2">
  30. <image class="image" mode="widthFix" :src="$getImageUri(item.msg)" @tap="previewImage($getImageUri(item.msg))"></image>
  31. </view>
  32. <!-- 商品 -->
  33. <view class="goods m-r-20 goods-box" v-if="item.msg_type == 3">
  34. <view class="goods-img m-r-20"><image style="width: 140rpx;height: 140rpx;" :src="$getImageUri(item.goods.image)"></image></view>
  35. <view class="goods-info flex-1">
  36. <view class="line-2">{{ item.goods.name }}</view>
  37. <view class="flex m-t-10 row-between">
  38. <price-format
  39. :color="colorConfig.primary"
  40. :subscript-size="26"
  41. :first-size="38"
  42. :second-size="26"
  43. :price="item.goods.min_price"
  44. ></price-format>
  45. </view>
  46. </view>
  47. </view>
  48. </view>
  49. </template>
  50. <!-- 通知类型记录 -->
  51. <template v-else>
  52. <view class="text-center white">
  53. <view class="muted xs">{{ item.msg }}</view>
  54. </view>
  55. </template>
  56. </view>
  57. </view>
  58. <view class="error" v-if="isError">
  59. <view class="error-msg text-center xs">{{ errorMsg }}</view>
  60. </view>
  61. <view id="bottom"></view>
  62. </scroll-view>
  63. </view>
  64. <view class="footer" @tap="showGoods = false">
  65. <view class="footer-input flex">
  66. <view class="album" @tap="uploadFile"><image class="icon" src="@/static/images/icon_album.png"></image></view>
  67. <view class="input-contain flex">
  68. <input v-model="msg" class="text-area" confirm-type="send" maxlength="-1" @focus="scrollToBottom" @confirm="sendText" />
  69. <image class="icon" src="@/static/images/icon_emoji.png" @tap="handleEmojiShow"></image>
  70. </view>
  71. <button size="sm" class="send-btn" @tap="sendText">发送</button>
  72. </view>
  73. <view class="emoji-wrap" :class="{ 'emoji-show': showEmoji }">
  74. <scroll-view style="height:100%;" scroll-y="true"><emoji @input="handleEmojiInput"></emoji></scroll-view>
  75. </view>
  76. </view>
  77. <view class="goods" v-if="showGoods">
  78. <view class="close" @tap="showGoods = false"><u-icon name="close-circle-fill" color="#ccc" size="40"></u-icon></view>
  79. <view class="goods-img m-r-20"><u-image width="140rpx" height="140rpx" :src="goodsInfo.image"></u-image></view>
  80. <view class="goods-info flex-1">
  81. <view class="line-2">{{ goodsInfo.name }}</view>
  82. <view class="flex m-t-10 row-between">
  83. <price-format :color="colorConfig.primary" :subscript-size="26" :first-size="38" :second-size="26" :price="goodsInfo.min_price"></price-format>
  84. <view class="send-btn" @tap="sendGoods">发送链接</view>
  85. </view>
  86. </view>
  87. </view>
  88. </view>
  89. </template>
  90. <script>
  91. import Socket from '@/utils/socket';
  92. import { chatRecord } from '@/api/user';
  93. import { getChatConfig } from '@/api/app';
  94. import { getGoodsDetail } from '@/api/store';
  95. import { client, uploadFile, getRect, debounce } from '@/utils/tools';
  96. import { timeFormatChat } from '@/utils/date';
  97. import { mapMutations } from 'vuex';
  98. export default {
  99. data() {
  100. return {
  101. pageStatus: 'loading',
  102. scrollTop: '',
  103. intoView: '',
  104. page: 1,
  105. msg: '',
  106. socket: {},
  107. kefu: {},
  108. showEmoji: false,
  109. recoreds: [],
  110. errorMsg: '',
  111. goodsInfo: {},
  112. isError: false,
  113. showGoods: false,
  114. showIndex: -1
  115. };
  116. },
  117. computed: {
  118. // 设置记录
  119. timeFormat() {
  120. return (item, index) => {
  121. let timeFmt = timeFormatChat(item.create_time_stamp);
  122. if (index && item.create_time_stamp - this.recoreds[index - 1].create_time_stamp < 300 && !item.show_time) {
  123. timeFmt = '';
  124. }
  125. return timeFmt;
  126. };
  127. },
  128. // 表情转换
  129. replaceEmoji() {
  130. return str => str.replace(/\[em-([a-z_]+)\]/g, `<span class="em em-$1"></span>`);
  131. },
  132. // 获取图片域名
  133. $getImageUri() {
  134. return url => this.$store.state.app.config.base_domain + url;
  135. }
  136. },
  137. watch: {
  138. kefu(val) {
  139. if (val.id) {
  140. this.setTitle(val.nickname);
  141. }
  142. }
  143. },
  144. methods: {
  145. // 初始化
  146. init() {
  147. this.shopId = this.$Route.query.shop_id || 0;
  148. this.goodsId = this.$Route.query.goods_id;
  149. this.socket = new Socket(this.appConfig.ws_domain, {
  150. token: this.$store.getters.token,
  151. type: 'user',
  152. client,
  153. shop_id: this.shopId
  154. });
  155. this.socket.addEvent('connect', () => {
  156. this.setTitle('连接中...');
  157. });
  158. this.socket.addEvent('open', () => {
  159. this.setTitle(this.kefu.nickname);
  160. this.isError = false;
  161. });
  162. this.socket.addEvent('message', data => {
  163. switch (data.event) {
  164. case 'login':
  165. this.loginEvent(data.data);
  166. break;
  167. case 'chat':
  168. this.chatEvent(data.data);
  169. break;
  170. case 'transfer':
  171. this.transferEvent(data.data);
  172. break;
  173. case 'error':
  174. this.errorEvent(data.data);
  175. break;
  176. }
  177. });
  178. this.socket.addEvent('error', data => {
  179. this.setTitle('连接失败');
  180. });
  181. },
  182. showTips(msg) {
  183. if (!msg) {
  184. console.log('1111');
  185. // uni.navigateTo({
  186. // url: '/bundle/pages/contact_offical/contact_offical?id=' + this.shopId,
  187. // success(e) {
  188. // console.log(e, 'cg');
  189. // },
  190. // fail(e) {
  191. // console.log(e, 'sb');
  192. // }
  193. // });
  194. // #ifdef APP
  195. setTimeout(function() {
  196. uni.redirectTo({
  197. url: `/bundle/pages/contact_offical/contact_offical?id=${this.shopId}`
  198. });
  199. }, 2000);
  200. // #endif
  201. // #ifdef H5
  202. uni.redirectTo({
  203. url: `/bundle/pages/contact_offical/contact_offical?id=${this.shopId}`
  204. });
  205. // #endif
  206. return;
  207. } else {
  208. uni.showModal({
  209. title: '温馨提示',
  210. content: msg,
  211. success: res => {
  212. if (res.confirm) {
  213. uni.redirectTo({
  214. url: `/bundle/pages/contact_offical/contact_offical?id=${this.shopId}`
  215. });
  216. } else if (res.cancel) {
  217. this.$Router.back();
  218. }
  219. }
  220. });
  221. }
  222. },
  223. getConfig() {
  224. return getChatConfig({
  225. shop_id: this.shopId
  226. })
  227. .then(res => {
  228. return Promise.resolve(res);
  229. })
  230. .catch(() => {
  231. return Promise.reject();
  232. });
  233. },
  234. // 获取数据
  235. async getData() {
  236. try {
  237. const res = await this.getConfig();
  238. console.log(res);
  239. if (res.code == 0) return this.showTips(res.msg);
  240. await this.getChatRecord();
  241. this.getGoods();
  242. this.scrollToBottom();
  243. if (!this.kefu.id) {
  244. this.setTitle('客服不在线');
  245. return;
  246. }
  247. this.socket.connect();
  248. } catch (e) {}
  249. },
  250. getGoods() {
  251. if (!this.goodsId) return;
  252. getGoodsDetail({
  253. goods_id: this.goodsId
  254. }).then(res => {
  255. if (res.code == 1) {
  256. this.goodsInfo = res.data;
  257. if (this.kefu.id) {
  258. this.showGoods = true;
  259. }
  260. }
  261. });
  262. },
  263. // 图片预览
  264. previewImage(url) {
  265. uni.previewImage({
  266. urls: [url]
  267. });
  268. },
  269. // 上传图片
  270. async uploadFile() {
  271. const [error, success] = await uni.chooseImage({
  272. count: 1
  273. });
  274. if (error) {
  275. return;
  276. }
  277. uni.showLoading({
  278. title: '上传中...'
  279. });
  280. try {
  281. const file = await uploadFile(success.tempFilePaths[0]);
  282. this.send(file.base_uri, 2);
  283. uni.hideLoading();
  284. } catch (e) {
  285. this.$toast({
  286. title: '上传失败,请稍后再试'
  287. });
  288. uni.hideLoading();
  289. }
  290. },
  291. // 发送文本
  292. sendText() {
  293. if (!this.msg) return;
  294. this.send(this.msg, 1);
  295. this.msg = '';
  296. },
  297. // 发送商品
  298. sendGoods() {
  299. this.showGoods = false;
  300. this.send(this.goodsId, 3);
  301. },
  302. // 获取聊天记录
  303. async getChatRecord() {
  304. const { page, pageStatus } = this;
  305. if (pageStatus == 'finish') return;
  306. const res = await chatRecord({
  307. shop_id: this.shopId,
  308. page_no: page
  309. });
  310. if (res.code == 1) {
  311. let toid = 0;
  312. this.page++;
  313. const { kefu, record } = res.data;
  314. this.kefu = kefu;
  315. this.showIndex = record.list.length;
  316. if (this.recoreds.length) {
  317. toid = this.recoreds[0].id;
  318. this.recoreds[0].show_time = true;
  319. }
  320. this.recoreds.unshift(...record.list);
  321. this.$nextTick(() => {
  322. if (!record.more) {
  323. this.pageStatus = 'finish';
  324. }
  325. this.scrollToItem(toid);
  326. this.showIndex = -1;
  327. });
  328. }
  329. },
  330. // 发送消息
  331. send(msg, type) {
  332. this.socket.send({
  333. event: 'chat',
  334. data: {
  335. msg,
  336. msg_type: type, // 暂定 1=>文本;2=>图片;3=>表情
  337. to_id: this.kefu.id, // 接收人id;客服发给用户则为user_id, 用户发给客服则为kefu_id
  338. to_type: 'kefu'
  339. }
  340. });
  341. },
  342. // 显示、隐藏表情库
  343. handleEmojiShow() {
  344. this.showEmoji = !this.showEmoji;
  345. if (!this.showEmoji) return;
  346. setTimeout(() => {
  347. this.scrollToBottom();
  348. }, 300);
  349. },
  350. scrollToupper() {
  351. this.getChatRecord();
  352. },
  353. scrollToBottom() {
  354. this.intoView = 'bottom';
  355. this.$nextTick(() => {
  356. this.intoView = '';
  357. });
  358. },
  359. scrollToItem(id) {
  360. this.intoView = `chat-item_${id}`;
  361. this.$nextTick(() => {
  362. this.intoView = '';
  363. });
  364. },
  365. handleEmojiInput(val) {
  366. this.msg = this.msg + val;
  367. },
  368. chatEvent(data) {
  369. this.isError = false;
  370. if (data.from_type == 'kefu') {
  371. uni.vibrateLong({
  372. success: function() {
  373. console.log('success');
  374. }
  375. });
  376. }
  377. if (data.shop_id != this.shopId) {
  378. return;
  379. }
  380. this.recoreds.push(data);
  381. this.$nextTick(() => {
  382. getRect('#bottom').then(res => {
  383. if (res.bottom < 1000) {
  384. this.scrollToItem(data.id);
  385. }
  386. });
  387. });
  388. },
  389. errorEvent(data) {
  390. this.errorMsg = data.msg;
  391. this.isError = true;
  392. this.$nextTick(() => {
  393. this.scrollToBottom();
  394. });
  395. },
  396. loginEvent(data) {
  397. // 登录成功,发送用户上线通知
  398. this.socket.send({
  399. event: 'user_online',
  400. data: {
  401. kefu_id: this.kefu.id
  402. }
  403. });
  404. },
  405. transferEvent(data) {
  406. this.kefu = data;
  407. },
  408. setTitle(title) {
  409. uni.setNavigationBarTitle({
  410. title
  411. });
  412. }
  413. },
  414. async onLoad() {
  415. this.scrollToupper = debounce(this.scrollToupper, 500, this);
  416. this.init();
  417. this.getData();
  418. },
  419. onUnload() {
  420. this.socket.close();
  421. },
  422. onReady() {}
  423. };
  424. </script>
  425. <style lang="scss">
  426. page {
  427. pading: 0;
  428. height: 100%;
  429. }
  430. .chat {
  431. height: 100%;
  432. .goods {
  433. display: flex;
  434. position: fixed;
  435. width: 600rpx;
  436. right: 20rpx;
  437. bottom: calc(120rpx + env(safe-area-inset-bottom));
  438. border-radius: 14rpx;
  439. background: #fff;
  440. padding: 20rpx;
  441. .close {
  442. position: absolute;
  443. left: -20rpx;
  444. top: -20rpx;
  445. }
  446. .send-btn {
  447. padding: 8rpx 22rpx;
  448. }
  449. }
  450. .content {
  451. transition: all 0.3s;
  452. flex: 1;
  453. min-height: 0;
  454. .loading {
  455. padding: 20rpx;
  456. height: 40px;
  457. }
  458. .chat-lists {
  459. padding: 0 20rpx 30rpx;
  460. overflow: hidden;
  461. position: relative;
  462. .chat-tips {
  463. padding: 4rpx 20rpx;
  464. border-radius: 21rpx;
  465. display: inline-block;
  466. text-align: center;
  467. background-color: rgba(0, 0, 0, 0.2);
  468. }
  469. .chat-item {
  470. padding-top: 30rpx;
  471. &.visibility {
  472. visibility: hidden;
  473. }
  474. .chat-info {
  475. display: flex;
  476. align-items: flex-start;
  477. }
  478. &.right {
  479. .chat-info {
  480. flex-direction: row-reverse;
  481. .text-box {
  482. background-color: #ed5349;
  483. color: #fff;
  484. }
  485. }
  486. }
  487. .avatar {
  488. width: 78rpx;
  489. height: 78rpx;
  490. border-radius: 14rpx;
  491. flex: none;
  492. }
  493. .text-box {
  494. max-width: 500rpx;
  495. min-width: 80rpx;
  496. background-color: #fff;
  497. border-radius: 14rpx;
  498. padding: 16rpx 20rpx;
  499. margin: 0 20rpx;
  500. word-break: break-word;
  501. line-height: 40rpx;
  502. }
  503. .image-box {
  504. max-width: 300rpx;
  505. margin: 0 20rpx;
  506. .image {
  507. max-width: 100%;
  508. }
  509. }
  510. .goods-box {
  511. position: static;
  512. width: 510rpx;
  513. }
  514. }
  515. }
  516. }
  517. .error {
  518. padding: 0 30rpx 30rpx;
  519. .error-msg {
  520. color: #bbb;
  521. word-break: break-word;
  522. }
  523. }
  524. .footer {
  525. background: #f2f2f2;
  526. padding-bottom: env(safe-area-inset-bottom);
  527. .footer-input {
  528. height: 100rpx;
  529. padding: 0 20rpx;
  530. .icon {
  531. width: 52rpx;
  532. height: 52rpx;
  533. }
  534. .input-contain {
  535. margin: 0 20rpx;
  536. background-color: #fff;
  537. height: 68rpx;
  538. border-radius: 60rpx;
  539. flex: 1;
  540. overflow: hidden;
  541. padding: 0 10rpx 0 30rpx;
  542. .text-area {
  543. flex: 1;
  544. height: 100rpx;
  545. word-break: break-all;
  546. }
  547. }
  548. }
  549. }
  550. .emoji-wrap {
  551. height: 0;
  552. transition: all 0.3s;
  553. &.emoji-show {
  554. height: 200px;
  555. }
  556. }
  557. .send-btn {
  558. padding: 0 25rpx;
  559. color: #fff;
  560. background-color: #ed5349;
  561. border-radius: 60rpx;
  562. }
  563. }
  564. </style>