<template> <BaseContainer flex> <NavBar title="客服" blue-theme /> <view class="broadcast-details_order"> <!-- 商品信息 --> <view class="broadcast-details_box" v-if="productId && productInfo.id"> <view class="broadcast_details_img"> <image class="goods-img" :src="productInfo.image" /> </view> <view class="broadcast_details_picBox"> <view class="broadcast_details_tit">{{ productInfo.store_name }}</view> <view class="acea-row row-between"> <view class="broadcast_details_pic" :class="{ hidden: productInfo.price === undefined }"> ¥{{ productInfo.price }} <text class="broadcast_details_pic_num" v-if="productInfo.ot_price">¥{{ productInfo.ot_price }}</text> </view> <view class="broadcast_details_btn" @click="sendProduct">发送客服</view> </view> </view> </view> <!-- 订单信息 --> <view class="broadcast_box" v-if="orderId && orderInfo.id"> <view class="broadcast-details_num broadcast_num"> <text>订单号:{{ orderInfo.order_id }}</text> <text>{{ orderInfo.add_time }}</text> </view> <view class="broadcast-details_box"> <view class="broadcast_details_img"> <image class="goods-img" :src="orderInfo.cartInfo[0].productInfo.image" /> <view class="broadcast_details_model"> {{ orderInfo.cartInfo ? orderInfo.cartInfo.length : 0 }}件商品 </view> </view> <view class="broadcast_details_picBox"> <view class="broadcast_details_tit">{{ orderInfo.cartInfo[0].productInfo.store_name }}</view> <view class="acea-row row-between"> <view class="broadcast_details_pic"> ¥{{ orderInfo.cartInfo[0].productInfo.price }} <text class="broadcast_details_pic_num">¥{{ orderInfo.cartInfo[0].costPrice }}</text> </view> <view class="broadcast_details_btn" @click="sendOrder">发送客服</view> </view> </view> </view> </view> </view> <view class="chat-scroll-box flex-auto"> <scroll-view scroll-y="true" style="height: 100%;" :scroll-top="scrollTop" @scrolltoupper="scrollToTop" :scroll-into-view="scrollTargetId" scroll-with-animation> <Loading :loaded="status" :loading="loading"></Loading> <view id="box" class="chat" ref="chat"> <view v-for="(item, index) in records" :key="index" :id="`msg-${item.id}`"> <view class="day-box" v-if="item.show">{{ item.add_time }}</view> <view class="transfer-notice" v-if="item.msn_type == 24 && item.kefu_info">客服{{ item.kefu_info.nickname }}将为您服务</view> <view class="chat-item" v-else-if="item.msn_type != 24" :class="{ 'right-box': item.uid == myUid && item.is_kefu_send == 0 }"> <image class="avatar" :src="item.avatar" mode=""></image> <!-- 消息 --> <view class="msg-box" v-if="item.msn_type <= 2" v-html="item.msn"></view> <!-- 图片 --> <view class="img-box" v-if="item.msn_type == 3"> <image :src="item.msn" mode="widthFix" @tap="previewImage(item.msn)"></image> </view> <!-- 商品 --> <view class="product-box" v-if="item.msn_type == 5" @click="goProduct(item)"> <image :src="item.productInfo.image" mode="widthFix"></image> <view class="info"> <view class="price"> <text>¥</text> {{ item.productInfo.price }} </view> <view class="name line2">{{ item.productInfo.store_name }}</view> </view> </view> <!-- 订单 --> <view class="order-box" v-if="item.msn_type == 21" @click="goOrder(item)"> <view class="title">订单ID: {{ item.orderInfo.order_id }}</view> <view class="info"> <image :src="item.orderInfo.cartInfo[0].productInfo.image"></image> <view class="product-info"> <view class="name line2">{{ item.orderInfo.cartInfo[0].productInfo.store_name }} </view> <view class="price">¥{{ item.orderInfo.cartInfo[0].productInfo.price }}</view> </view> </view> </view> </view> </view> </view> </scroll-view> </view> <view class="footer-box flex flex-center-x"> <view class="words" @click="uploadImg"><text class="iconfont4 icon-tupian"></text></view> <view class="input-box"> <input type="text" placeholder="请输入内容" v-model="con" confirm-type="send" @confirm="sendText" /> <text class="iconfont4 icon-fasong" @click="sendText" :class="{ isSend: isSend }"></text> </view> <view class="emoji" @click="isSwiper = !isSwiper"><text class="iconfont4 icon-biaoqing"></text></view> </view> <!-- 表情 --> <view class="banner slider-banner" v-if="isSwiper"> <swiper :autoplay="autoplay" :circular="circular" :interval="interval" :duration="duration" v-if="emojiGroup.length > 0"> <block v-for="(emojiList, index) in emojiGroup" :key="index"> <swiper-item><i class="em" :class="emoji" v-for="emoji in emojiList" :key="emoji" @click="addEmoji(emoji)"></i></swiper-item> </block> </swiper> </view> </BaseContainer> </template> <script> import { getProductInfo, getOrderInfo, getSupportInfo } from "@/api/kefu"; import Socket from '@/libs/socket'; import emojiList from '@/utils/emoji'; import Loading from '@/components/Loading'; import dayjs from "dayjs"; import { CHAT_MSN_TYPE, CUSTOMER_DETAIL_TYPE } from '@/constants/customer-type'; const chunk = function (arr, num) { num = num * 1 || 1; var ret = []; arr.forEach(function (item, i) { if (i % num === 0) { ret.push([]); } ret[ret.length - 1].push(item); }); return ret; }; export default { data() { return { status: false, loading: false, isSwiper: false, autoplay: false, circular: true, interval: 3000, duration: 500, emojiGroup: chunk(emojiList, 21), con: '', toUid: 0, limit: 15, upperId: 0, chatList: [], scrollTop: 0, active: true, isScroll: true, myUid: 0, productId: 0, productInfo: {}, orderId: 0, page: 1, orderInfo: {}, uidTo: 0, titleName: '', optionsParams: {}, scrollTargetId: "", mer_id:0 }; }, components: { Loading }, computed: { isSend() { if (this.con.length == 0) { return false; } else { return true; } }, records() { return this.chatList.map((item, index) => { if (index) { if (item.add_time - this.chatList[index - 1].add_time >= 300) { item.show = true; } else { item.show = false; } } else { item.show = true; } return item; }); } }, onLoad({ support_type = 0, type = 0, id = 0 ,mer_id = 0 }) { this.$util.checkLogin(() => { uni.showLoading({ title: '客服连接中...' }); this.myUid = this.$store.getters.userInfo.uid; this.mer_id = mer_id; this.$socket = new Socket(); this.$socket.onStart(); this.initSocketListen(); }, true); if (support_type && type && id) { Object.assign(this.optionsParams, { support_type, type, id }); if (support_type == CUSTOMER_DETAIL_TYPE.GOOD) { this.productId = id; this.getproductInfo(); } else if (support_type == CUSTOMER_DETAIL_TYPE.ORDER) { this.orderId = id; this.getOrderInfo(); } } }, onUnload() { this.$socket.onClose(); uni.$off(); }, methods: { initSocketListen() { uni.$once('socketOpen', () => { const token = this.$store.getters.token; // 登录 this.$socket.send({ data: token, type: 'user_login' }); }); // 链接成功 uni.$once('login_success', () => { uni.hideLoading(); this.$socket.init(); this.getChatList(); }); // 监听客服转接 uni.$on('to_transfer', data => { this.toUid = data.toUid; this.chatList.forEach(el => { if (el.uid != this.myUid) { el.avatar = data.avatar } }) }); // 消息接收 uni.$on(['reply', 'chat'], data => { if (data.is_kefu_send == 1 && data.uid != this.toUid) return; if (data.msn_type == 1) { data.msn = this.replace_em(data.msn); } data.add_time = dayjs(data.add_time * 1000).format("YYYY/MM/DD HH:mm:ss"); this.chatList.push(data); this.$nextTick(() => { this.scrollTargetId = `msg-` + data.id; }); }); uni.$on('socket_error', () => { this.$util.showMsg('连接失败'); }); uni.$on('err_tip', (e) => { this.$util.showMsg(e.msg); }); uni.$on('transfer', data => { this.toUid = data.kefu_id; }); }, previewImage(n) { uni.previewImage({ urls: [n] }); }, // 返回 goBack() { uni.navigateBack(); }, // 商品信息 getproductInfo() { if (!this.productId) return; getProductInfo({ id: this.productId, type: this.optionsParams.type }).then(res => { this.productInfo = res.data; }); }, // 商品信息 goProduct(item) { uni.navigateTo({ url: `/pages/goods_details/index?id=${item.msn}` }); }, // 订单详情 goOrder(item) { uni.navigateTo({ url: `/pages/users/order_details/index?order_id=${item.msn}` }); }, // 订单消息 getOrderInfo() { if (!this.orderId) return; getOrderInfo({ id: this.orderId, type: this.optionsParams.type }).then(res => { this.orderInfo = res.data; }); }, // 表情点击 addEmoji(item) { let val = `[${item}]`; this.con += val; }, // 聊天表情转换 replace_em(str) { str = str.replace(/\[em-([a-z_]*)\]/g, "<span class='em em-$1'></span>"); return str; }, // 获取聊天列表 getChatList() { getSupportInfo({ limit: this.limit, uidTo: this.uidTo, toUid: this.toUid, mer_id:this.mer_id }) .then(res => { let id = ''; if (res.data.serviceList.length) { if (this.uidTo == 0) { id = res.data.serviceList[res.data.serviceList.length - 1].id; } else { id = res.data.serviceList[0].id; } } let arr = []; var sH = 0; uni.hideLoading(); uni.setNavigationBarTitle({ title: res.data.nickname }); this.titleName = res.data.nickname; this.toUid = res.data.uid; res.data.serviceList.forEach(el => { el.add_time = dayjs(el.add_time * 1000).format("YYYY/MM/DD HH:mm:ss"); if (el.msn_type == 1 || el.msn_type == 2) { el.msn = this.replace_em(el.msn); } }); this.loading = false; this.chatList = [...res.data.serviceList, ...this.chatList]; this.$nextTick(() => { setTimeout(() => { this.scrollTargetId = `msg-` + id; this.isScroll = res.data.serviceList.length >= this.limit; }) }); }) .catch(error => { uni.hideLoading(); this.$util.showMsg(error.msg); this.loading = false; this.isScroll = false uni.redirectTo({ url: '/pages/customer/feedback?mer_id='+this.mer_id }); }); }, // 发送消息 sendText() { if (!this.isSend) { return this.$util.showMsg('请输入内容'); } this.sendMsg(this.con, CHAT_MSN_TYPE.TEXT); this.con = ''; }, // ws发送 sendMsg(msn, msn_type, attach_type = 0) { let type = 0; // #ifdef H5 type = 3 // #endif // #ifdef MP type = 2 // #endif // #ifdef APP-PLUS type = 9 // #endif this.$socket.send({ data: { msn, msn_type, attach_type, to_uid: this.toUid, type }, type: 'chat' }); }, uploadImg() { this.$util.pickerOneImg() .then(path => { this.sendMsg(path, CHAT_MSN_TYPE.IMG); }); }, // 发送商品 sendProduct() { this.sendMsg(this.productId, CHAT_MSN_TYPE.GOOD, this.optionsParams.type); this.productId = 0; this.productInfo = {}; }, // 发送订单 sendOrder() { this.sendMsg(this.orderId, CHAT_MSN_TYPE.ORDER, this.optionsParams.type); this.orderId = 0; this.orderInfo = {}; }, // 滚动到顶部 scrollToTop() { let self = this; if (this.isScroll) { this.loading = true; this.uidTo = this.chatList[0].id; this.isScroll = false; setTimeout(res => { this.getChatList(); }, 800); } } } }; </script> <style lang="scss"> $bg-path: getAssetsPath('/kefu-assets/assets/images/emoji.png'); // #ifndef H5 @import "@/static/style/google.min.css"; // #endif /deep/ .em { background-image: url($bg-path); display: inline-block; width: 50rpx; height: 50rpx; margin: 40rpx 0 0 50rpx; } /deep/ .chat .em { margin: 0; } .emoji-outer { position: absolute; right: 50rpx; bottom: 30rpx; width: 50rpx; height: 50rpx; } .base-container { background: #f0f1f2; } </style> <style lang="scss" scoped> @import "@/static/style/iconfont3.scss"; $kf-theme : #3875EA; .scroll-box { flex: 1; } .footer-box { padding: 0 30rpx; color: rgba(0, 0, 0, 0.8); background: #f7f7f7; height: 100rpx; height: calc(100rpx + var(--safe-bottom)); .words .icon-tupian { font-size: 50rpx; } .input-box { display: flex; align-items: center; flex: 1; height: 64rpx; padding-right: 5rpx; margin-left: 18rpx; background-color: #ffffff; border-radius: 32rpx; input { flex: 1; padding-left: 20rpx; height: 100%; font-size: 28rpx; font-weight: normal; } .icon-fasong { font-size: 50rpx; color: #ccc; font-weight: normal; } .isSend { color: $kf-theme; } } .emoji .icon-biaoqing { margin-left: 18rpx; font-size: 50rpx; } .more .icon-gengduozhankai { margin-left: 18rpx; font-size: 50rpx; } } .tool-wrapper { display: flex; justify-content: space-between; padding: 45rpx 60rpx; background: #ffffff; font-size: 24rpx; .tool-item { text-align: center; image { width: 104rpx; height: 104rpx; } } } .slider-banner { background: #ffffff; } .words-mask { z-index: 50; position: fixed; left: 0; top: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); .content { position: absolute; left: 0; right: 0; top: 114rpx; bottom: 0; display: flex; flex-direction: column; padding: 0 30rpx; background: #ffffff; border-radius: 6rpx 6rpx 0px 0px; .title-box { position: relative; height: 125rpx; line-height: 125rpx; text-align: center; font-size: 32rpx; .icon-cha1 { position: absolute; right: 0; top: 50%; transform: translateY(-50%); } } .scroll-box { flex: 1; overflow: hidden; .msg-item { padding: 25rpx 0; border-bottom: 1px solid #eceff8; } } } } .chat-scroll-box { overflow: hidden; .chat { padding: 30rpx 30rpx 0.1rpx; } .chat-item { display: flex; margin-bottom: 36rpx; -webkit-user-select: auto; .avatar { width: 80rpx; height: 80rpx; border-radius: 50%; } .msg-box { display: flex; align-items: center; max-width: 452rpx; margin-left: 22rpx; padding: 10rpx 24rpx; background: #ffffff; border-radius: 14rpx; word-break: break-all; -webkit-user-select: auto; } .img-box { width: 270rpx; margin-left: 22rpx; image { width: 270rpx; } } .product-box { width: 452rpx; margin-left: 22rpx; background-color: #ffffff; border-radius: 14rpx; overflow: hidden; image { width: 452rpx; } .info { padding: 16rpx 26rpx; .price { font-size: 36rpx; color: #e93323; text { font-size: 28rpx; } } } } .order-box { width: 452rpx; margin-left: 22rpx; background-color: #ffffff; border-radius: 14rpx; .title { padding: 15rpx 20rpx; font-size: 26rpx; color: #282828; border-bottom: 1px solid #eceff8; } .info { display: flex; padding: 20rpx; image { width: 124rpx; height: 124rpx; border-radius: 6rpx; } .product-info { flex: 1; display: flex; flex-direction: column; justify-content: space-between; margin-left: 16rpx; .name { font-size: 26rpx; } .price { font-size: 30rpx; color: #e93323; } } } } &.right-box { flex-direction: row-reverse; .msg-box { margin-left: 0; margin-right: 22rpx; background-color: #9cec60; } .img-box { margin-left: 0; margin-right: 22rpx; } .product-box { margin-left: 0; margin-right: 22rpx; } .order-box { margin-left: 0; margin-right: 22rpx; } } } } .broadcast-details_box { display: flex; background: #ffffff; border-radius: 6px; padding: 24rpx; } .broadcast_details_img { width: 140rpx; height: 140rpx; border-radius: 8px; overflow: hidden; position: relative; } .broadcast_details_img .goods-img { width: 100%; height: 100%; } .broadcast_details_picBox { width: 75%; margin-left: 24rpx; } .broadcast_details_tit { font-size: 28rpx; color: #333333; height: 85rpx; font-weight: 800; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; text-align: left !important; } .broadcast_details_pic { font-size: 36rpx; color: #e93323; text-align: left; &.hidden { visibility: hidden; } } .broadcast_details_pic_num { text-decoration: line-through; font-size: 28rpx; color: rgba(0, 0, 0, 0.5); margin-left: 0.1rem; } .broadcast_details_btn { width: 130rpx; height: 50rpx; background: rgba(233, 51, 35, 1); opacity: 1; border-radius: 125rpx; color: #ffffff; font-size: 24rpx; text-align: center; line-height: 50rpx; } .broadcast-details_num { width: 100%; height: 80rpx; line-height: 80rpx; color: #000000; font-size: 26rpx; display: flex; justify-content: space-between; background: #ffffff; border-bottom: 1px dashed rgba(0, 0, 0, 0.2); padding: 0 24rpx; } .transfer-notice, .day-box { font-size: 24rpx; color: #999999; text-align: center; margin-bottom: 36rpx; } </style>