652 lines
19 KiB
Vue
652 lines
19 KiB
Vue
<template>
|
|
<view class="container">
|
|
<lay-out id='layOut' ref="layOut" :scrollTop="scrollTop" :scrollWithAnimation="false" @goTop="goTop"
|
|
@layoutScroll="scroll">
|
|
<!-- #ifdef H5 -->
|
|
<view slot="header" class="header">
|
|
<view class="header_left" @click="goBackToMessageList"><span class="iconfont icon-fanhui"></span></view>
|
|
<view class="header_center">
|
|
{{ userData.remark_nickname ? userData.remark_nickname : userData.nickname }}
|
|
</view>
|
|
<view class="header_right" @click="selectCustomerServerOfTopRight(userData)"><span
|
|
class="iconfont"></span></view>
|
|
</view>
|
|
<!-- #endif -->
|
|
<view slot="content" class="content" id="contentMessage">
|
|
<view class="content_list" v-for="(item, index) in messageList" :key="index"
|
|
:class="{ right_box: customerServerData.user_ids && customerServerData.user_ids.indexOf(item.user_id) !== -1 }">
|
|
<view class="content_list_avar" @click="selectCustomerServer(item)">
|
|
<image :src="item.avatar" mode="widthFix"></image>
|
|
</view>
|
|
<view class="content_list_message">
|
|
<view class="content_list_message_value" v-if="item.msn_type <= 2"><text selectable="true"
|
|
v-html="replace_em(item.msn)"></text></view>
|
|
<view class="content_list_message_product" v-if="item.msn_type == 5">
|
|
<view class="content_list_message_orderId">
|
|
<span class="content_list_message_orderId_label">商品名称:</span>
|
|
<span class="content_list_message_orderId_value">{{ item.other.store_name }}</span>
|
|
</view>
|
|
|
|
<view class="content_list_message_detils">
|
|
<view class="content_list_message_detils_image">
|
|
<image :src="item.other.image" mode="widthFix"></image>
|
|
</view>
|
|
<view class="content_list_message_detils_value">
|
|
<view class="content_list_message_detils_value_shopMessag">
|
|
<view class="content_list_message_detils_value_shopMessag_item">
|
|
<view class="content_list_message_detils_value_shopMessag_item_label">
|
|
<span>库存:</span>
|
|
</view>
|
|
<view class="content_list_message_detils_value_shopMessag_item_value">
|
|
<span>{{ item.other.stock }}</span>
|
|
</view>
|
|
</view>
|
|
<view class="content_list_message_detils_value_shopMessag_item">
|
|
<view class="content_list_message_detils_value_shopMessag_item_label">
|
|
<span>销量:</span>
|
|
</view>
|
|
<view class="content_list_message_detils_value_shopMessag_item_value">
|
|
<span>{{ parseInt(item.other.sales) + parseInt(item.other.ficti ? item.other.ficti : 0) }}</span>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view class="content_list_message_detils_value_shopPrice">
|
|
<span>¥{{ item.other.price }}</span>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="content_list_message_image" v-if="item.msn_type == 3">
|
|
<image @load="imageLoad" :src="item.msn" mode="widthFix" @click="showImage(item.msn)">
|
|
</image>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="chat_result" v-if="item.user_id == customerServerData.user_id">
|
|
<view class="chat_result_ing" v-if="item.is_send == 0">
|
|
<!-- 正在发送 -->
|
|
<view></view>
|
|
</view>
|
|
<view class="chat_result_faile" v-if="item.is_send == -1">
|
|
<!-- 发送失败 -->
|
|
<view>!</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view slot="bottom" class="footer" :class="{ translateY0: selectModel }">
|
|
<view class="footer_input">
|
|
<view class="footer_input_icon mr18" @click="selectScriptLibary"><span
|
|
class="iconfont"></span></view>
|
|
<view class="footer_input_content mr18">
|
|
<textarea v-model="sendMessage" @input="textareaChange($event)" @focus="textareaFocus"
|
|
class="font" value="" @confirm="sendText" auto-height confirm-type="send"
|
|
:focus="sendMessageFocus" placeholder="" />
|
|
<!-- <p v-html="pCont" class="font"></p> -->
|
|
<!-- <span class="iconfont" :class="{ canSend: sendMessage }" @click="sendText"></span> -->
|
|
</view>
|
|
<view class="footer_input_icon">
|
|
<span class="iconfont mr18" @click="selectOption(1)"></span>
|
|
<span class="sendMessage" v-if="sendMessage" @click.stop="sendText">发送</span>
|
|
<span class="iconfont" @click="selectOption(2)" v-if="!sendMessage"></span>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="footer_option" :style="{ height: selectModel ? '440rpx' : '0' }">
|
|
<view class="option" v-if="selectModel == 2">
|
|
<view @click="uploadImage">
|
|
<image src="~static/image/messageList/picture.png" mode="widthFix"></image>
|
|
<view>图片</view>
|
|
</view>
|
|
<view @click="transfer">
|
|
<image src="~static/image/messageList/connection.png" mode="widthFix"></image>
|
|
<view>转接</view>
|
|
</view>
|
|
<view @click="authReply">
|
|
<image src="~static/images/auth-reply.png" mode="widthFix"></image>
|
|
<view>自动回复</view>
|
|
</view>
|
|
</view>
|
|
|
|
<scroll-view class="emoji" v-if="selectModel == 1" scroll-y>
|
|
<view class="emoji-item" v-for="(item, index) in emoji" :key="index" @click="sendEmoji(item)"><i
|
|
class="em" :class="item"></i></view>
|
|
</scroll-view>
|
|
</view>
|
|
</view>
|
|
</lay-out>
|
|
|
|
<uni-popup ref="transferModel" animation type="bottom">
|
|
<transfer @close="transferModelClose" :userInfo="userData" @handleSubmit="handleSubmitTransfer"></transfer>
|
|
</uni-popup>
|
|
|
|
<uni-popup ref="scriptLibaryModel" animation type="bottom">
|
|
<view class="scriptLibary">
|
|
<view class="scriptLibary_header">
|
|
<view class="scriptLibary_header_title">
|
|
<view class="tit" v-for="(item, index) in speechArtTypeList" :key="index" @click="selectSpeechArtType(item)"
|
|
:class="{ speechArtSelectEd: speechArtType == item.id }">
|
|
<span>{{ item.label }}</span>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="scriptLibary_header_icon">
|
|
<view @click="editScript"><span class="iconfont icon_edit"></span></view>
|
|
<view @click="closeScript"><span class="iconfont"></span></view>
|
|
</view>
|
|
</view>
|
|
<view class="scriptLibary_search">
|
|
<view class="search">
|
|
<span @click="handleSetTitle" class="iconfont icon_search"></span>
|
|
<input v-model="scriptTitle" type="text" placeholder="搜索快捷回复" />
|
|
</view>
|
|
</view>
|
|
<view class="scriptLibary_content">
|
|
<scripts-libary :tagList="speechTagList" :speechArtType="speechArtType" maxHeight="1000rpx"
|
|
height="900rpx" :title="scriptTitle" @sendSpeechMessage="sendSpeechMessage"></scripts-libary>
|
|
</view>
|
|
</view>
|
|
</uni-popup>
|
|
|
|
<uni-popup ref="showImage" animation type="center">
|
|
<view class="image_content">
|
|
<span class="iconfont" @click="closeImage"></span>
|
|
<image :src="selectMessageImage" mode="widthFix"></image>
|
|
</view>
|
|
</uni-popup>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
import emoji from 'pages/utils/emoji.js';
|
|
import {
|
|
navigateBack,
|
|
navigateTo,
|
|
serialize,
|
|
chooseImage,
|
|
Toast,
|
|
Modal
|
|
} from 'pages/utils/uniApi.js';
|
|
import transfer from './component/transfer.vue';
|
|
import scriptsLibary from './component/scriptLibary.vue';
|
|
import http from 'pages/api/index';
|
|
import api from 'pages/api/api.js';
|
|
import {
|
|
mapGetters
|
|
} from 'vuex';
|
|
const app = getApp();
|
|
export default {
|
|
components: {
|
|
transfer, // 转接客服
|
|
scriptsLibary // 话术库
|
|
},
|
|
computed: mapGetters(['chatList', 'token']),
|
|
data() {
|
|
return {
|
|
selectMessageImage: '', // 选中的图片
|
|
sendMessageFocus: false,
|
|
pCont: '',
|
|
sendMessage: '', // 发送的消息
|
|
selectModel: 0, // 1:选择表情 2: 功能选择(转接,上传图片)
|
|
emoji: [],
|
|
messageList: [],
|
|
userData: {},
|
|
pageData: {
|
|
limit: 20,
|
|
upperId: 0
|
|
},
|
|
speechArtType: 1, // 0 全局 1 私有
|
|
speechTagList: [], // 话术分类
|
|
scriptTitle: '', // 搜索输入的内容
|
|
propstitle: '',
|
|
speechArtTypeList: [{
|
|
id: 1,
|
|
label: '个人库'
|
|
},
|
|
{
|
|
id: 0,
|
|
label: '公共库'
|
|
}
|
|
],
|
|
customerServerData: {
|
|
user_ids: []
|
|
},
|
|
scrollTop: 0, // 滚动位置
|
|
refresherTriggered: true, // 下拉刷新装状态
|
|
audioFun: '', // 音频对象
|
|
old: {
|
|
scrollTop: 0
|
|
},
|
|
kefuInfo: {}, //当前客服信息
|
|
userId: 0, //当前用户user_id
|
|
toChatSend: false //是否发送过to_chat
|
|
};
|
|
},
|
|
watch: {
|
|
messageList: {
|
|
deep: true,
|
|
handler() {
|
|
this.messageList.map((item, index) => {
|
|
if (index) {
|
|
if (item.add_time - this.messageList[index - 1].add_time >= 300) {
|
|
item.show = true;
|
|
} else {
|
|
item.show = false;
|
|
}
|
|
} else {
|
|
item.show = true;
|
|
}
|
|
return item;
|
|
});
|
|
}
|
|
}
|
|
},
|
|
onLoad(opt) {
|
|
this.emoji = emoji;
|
|
this.userId = opt.to_user_id;
|
|
if (!this.userId) {
|
|
return navigateBack(1);
|
|
}
|
|
this.customerServerData = this.$store.getters.kefuInfo;
|
|
console.log(this.customerServerData)
|
|
this.kefuInfo = this.$store.getters.kefuInfo;
|
|
this.messageList = this.$store.getters.chatLog(this.userId);
|
|
this.userData = this.$store.getters.userInfo(this.userId);
|
|
this.getUserInfo();
|
|
this.getKefuInfo();
|
|
this.initData();
|
|
this.sockEventListenr();
|
|
this.$store.dispatch('updataUserCount').then(res => {
|
|
this.scoket.setBadgeNumber(res.count);
|
|
});
|
|
},
|
|
onUnload() {
|
|
console.log('聊天界面卸载');
|
|
uni.$off('chat', this.chatEvent);
|
|
uni.$off('chat_auth', this.chatAuthEvent);
|
|
uni.$off('reply', this.replyEvent);
|
|
uni.$off('success', this.successTochat);
|
|
uni.$off('close', this.closeEvent);
|
|
uni.$off('rm_transfer', this.rmTransferEvent);
|
|
this.toChatSend = false;
|
|
this.scoket.send({
|
|
data: {
|
|
id: 0
|
|
},
|
|
type: 'to_chat'
|
|
});
|
|
},
|
|
// 监听,点击界面右上角的按钮
|
|
onNavigationBarButtonTap(e) {
|
|
this.selectCustomerServerOfTopRight();
|
|
},
|
|
|
|
methods: {
|
|
authReply() {
|
|
navigateTo(1, '/pages/view/authReply/index');
|
|
},
|
|
getKefuInfo() {
|
|
this.$store.dispatch('getKeufuInfo').then(res => {
|
|
this.kefuInfo = res;
|
|
});
|
|
},
|
|
//获取用户信息
|
|
getUserInfo() {
|
|
this.$store
|
|
.dispatch('getUserInfo', this.userId)
|
|
.then(res => {
|
|
this.userData = res; // 获取用户信息
|
|
uni.setNavigationBarTitle({
|
|
title: this.userData.remark_nickname ? this.userData.remark_nickname : this
|
|
.userData.nickname
|
|
});
|
|
})
|
|
.catch(err => {
|
|
uni.setNavigationBarTitle({
|
|
title: this.userData.remark_nickname ? this.userData.remark_nickname : this
|
|
.userData.nickname
|
|
});
|
|
});
|
|
},
|
|
scroll(value) {
|
|
this.old.scrollTop = value;
|
|
},
|
|
successTochat() {
|
|
console.log(this.toChatSend);
|
|
//没发过在发送to_chat
|
|
if (!this.toChatSend) {
|
|
this.scoket.send({
|
|
data: {
|
|
id: this.userId
|
|
},
|
|
type: 'to_chat'
|
|
});
|
|
this.toChatSend = true;
|
|
}
|
|
},
|
|
replyEvent(data) {
|
|
this.pushMessageToList(data);
|
|
},
|
|
chatAuthEvent(data) {
|
|
if (data.length) {
|
|
data.map(item => {
|
|
item.msn = this.replace_em(item.msn);
|
|
this.pushMessageToList(item);
|
|
})
|
|
}
|
|
},
|
|
chatEvent(data) {
|
|
this.messageList.map(item => {
|
|
if (item.guid === data.guid) {
|
|
item.is_send = 1;
|
|
}
|
|
});
|
|
this.$store.commit('delChatList', {
|
|
id: this.userId,
|
|
guid: data.guid
|
|
});
|
|
},
|
|
closeEvent() {
|
|
this.toChatSend = false;
|
|
},
|
|
rmTransferEvent(data) {
|
|
if (data.recored.to_user_id == this.userId) {
|
|
navigateBack(1);
|
|
}
|
|
},
|
|
// socket监听
|
|
sockEventListenr() {
|
|
if (!this.toChatSend && this.scoket.connectStatus) {
|
|
this.scoket.send({
|
|
data: {
|
|
id: this.userId
|
|
},
|
|
type: 'to_chat'
|
|
});
|
|
this.toChatSend = true;
|
|
}
|
|
uni.$on('success', this.successTochat);
|
|
uni.$on('chat', this.chatEvent);
|
|
uni.$on('chat_auth', this.chatAuthEvent);
|
|
uni.$on('reply', this.replyEvent);
|
|
uni.$on('close', this.closeEvent);
|
|
uni.$on('rm_transfer', this.rmTransferEvent);
|
|
},
|
|
|
|
// 获取聊天列表
|
|
initData() {
|
|
this.$store
|
|
.dispatch('getChatLogList', {
|
|
user_id: this.userId,
|
|
pageData: this.pageData
|
|
})
|
|
.then(res => {
|
|
let upperId = this.pageData.upperId;
|
|
this.messageList = upperId === 0 ? res : res.concat(this.messageList);
|
|
this.pageData.upperId = this.messageList.length ? this.messageList[0].id : 0;
|
|
if (this.chatList[this.userId]) {
|
|
this.chatList[this.userId].map(item => {
|
|
this.messageList.push(item);
|
|
});
|
|
}
|
|
if (upperId === 0) {
|
|
this.$nextTick(() => {
|
|
this.goBottom();
|
|
});
|
|
}
|
|
})
|
|
.catch(err => {
|
|
if (this.chatList[this.userId]) {
|
|
this.chatList[this.userId].map(item => {
|
|
this.messageList.push(item);
|
|
});
|
|
}
|
|
if (upperId === 0) {
|
|
this.$nextTick(() => {
|
|
this.goBottom();
|
|
});
|
|
}
|
|
});
|
|
},
|
|
// 滑动到顶部
|
|
goTop() {
|
|
this.refresherTriggered = true;
|
|
this.initData();
|
|
},
|
|
|
|
// 发送文本消息
|
|
sendText(e) {
|
|
if (this.sendMessage) {
|
|
// this.sendMessageFocus = true;
|
|
this.sendMsg(this.sendMessage, 1);
|
|
this.sendMessage = '';
|
|
} else {
|
|
return;
|
|
// Toast('请输入要发送的消息');
|
|
}
|
|
},
|
|
// 选择表情
|
|
sendEmoji(item) {
|
|
this.sendMessage += `[${item}]`;
|
|
},
|
|
// 发送图片
|
|
uploadImage() {
|
|
let that = this;
|
|
chooseImage(1).then(res => {
|
|
//#ifndef H5
|
|
uni.compressImage({
|
|
src: res.tempFilePaths[0],
|
|
width: 'auto',
|
|
height: 'auto',
|
|
success(res) {
|
|
that.upload(res.tempFilePath);
|
|
},
|
|
fail(e) {
|
|
console.log(e);
|
|
that.upload(res.tempFilePaths[0]);
|
|
}
|
|
});
|
|
//#endif
|
|
//#ifdef H5
|
|
that.upload(res.tempFilePaths[0]);
|
|
//#endif
|
|
});
|
|
},
|
|
upload(tempFilePath) {
|
|
uni.uploadFile({
|
|
url: app.globalData.http + api.kefuUploadAvatar.url,
|
|
filePath: tempFilePath,
|
|
name: 'file',
|
|
formData: {
|
|
// file: res.tempFilePaths[0],
|
|
filename: 'file'
|
|
},
|
|
header: {
|
|
// #ifdef MP || APP-PLUS
|
|
'Content-Type': 'multipart/form-data',
|
|
// #endif
|
|
'Authori-zation': 'Bearer ' + this.$store.state.token
|
|
},
|
|
success: uploadFileRes => {
|
|
let unloadResponse = JSON.parse(uploadFileRes.data);
|
|
console.log(unloadResponse);
|
|
if (unloadResponse.status == 200) {
|
|
this.sendMsg(unloadResponse.data.url, 3);
|
|
}
|
|
},
|
|
fail: res => {
|
|
Toast('发送图片失败');
|
|
},
|
|
complete: res => {
|
|
console.log(res);
|
|
}
|
|
});
|
|
},
|
|
pushMessageToList(data) {
|
|
this.messageList.push(data);
|
|
this.$nextTick(() => {
|
|
this.goBottom();
|
|
});
|
|
},
|
|
imageLoad() {
|
|
|
|
},
|
|
goBottom() {
|
|
this.$nextTick(() => {
|
|
const query = uni.createSelectorQuery().in(this);
|
|
query
|
|
.select('#contentMessage')
|
|
.boundingClientRect(data => {
|
|
this.scrollTop = this.old.scrollTop;
|
|
this.scrollTop = data.height + 50;
|
|
console.log(data, this.scrollTop);
|
|
|
|
})
|
|
.exec();
|
|
});
|
|
},
|
|
// textarea获取焦点
|
|
textareaFocus() {
|
|
this.selectModel = 0;
|
|
this.$nextTick(() => {
|
|
this.goBottom();
|
|
});
|
|
},
|
|
chatOptinos(guid, msn, type, other) {
|
|
return {
|
|
msn,
|
|
msn_type: type,
|
|
to_user_id: this.userId,
|
|
is_send: 0,
|
|
is_tourist: 0,
|
|
avatar: this.kefuInfo.avatar,
|
|
user_id: this.kefuInfo.user_id,
|
|
appid: this.kefuInfo.appid,
|
|
other: other || {},
|
|
type: 0,
|
|
guid
|
|
};
|
|
},
|
|
// 发送消息统一处理
|
|
sendMsg(msn, type) {
|
|
let guid = this.scoket.guid();
|
|
let chat = this.chatOptinos(guid, msn, type);
|
|
this.$store.commit('setChatList', {
|
|
id: this.userId,
|
|
list: chat
|
|
});
|
|
chat.msn = this.replace_em(msn);
|
|
this.pushMessageToList(chat);
|
|
http(api.sendMessage, {
|
|
guid,
|
|
msn,
|
|
msn_type: type,
|
|
to_user_id: this.userId,
|
|
}).then(data => {
|
|
this.chatEvent({
|
|
guid: data.guid
|
|
});
|
|
})
|
|
|
|
},
|
|
selectScriptLibary() {
|
|
this.$refs.scriptLibaryModel.open();
|
|
},
|
|
// 选择话术库,个人,工共
|
|
selectSpeechArtType(item) {
|
|
this.speechArtType = item.id;
|
|
this.selectScriptLibary();
|
|
},
|
|
// 选中话术
|
|
sendSpeechMessage(item) {
|
|
this.sendMessage = item.message;
|
|
this.$refs.scriptLibaryModel.close(); // 关闭话术库弹框
|
|
},
|
|
// 关闭话术弹框
|
|
closeScript() {
|
|
this.$refs.scriptLibaryModel.close();
|
|
},
|
|
// 修改话术库
|
|
editScript() {
|
|
this.$refs.scriptLibaryModel.close();
|
|
navigateTo(1, '/pages/view/customerServer/script', {
|
|
speechArtType: this.speechArtType
|
|
});
|
|
},
|
|
// 话术搜索
|
|
handleSetTitle() {
|
|
this.propstitle = serialize(this.scriptTitle);
|
|
},
|
|
// 转接
|
|
transfer() {
|
|
this.selectModel = 0;
|
|
this.$refs.transferModel.open();
|
|
},
|
|
// 确认转接
|
|
handleSubmitTransfer() {
|
|
this.$refs.transferModel.close();
|
|
navigateTo(4, '/pages/view/messageList/index');
|
|
},
|
|
// 关闭转接弹框
|
|
transferModelClose() {
|
|
this.$refs.transferModel.close();
|
|
},
|
|
// 头部方法,回到聊天列表页
|
|
goBackToMessageList() {
|
|
navigateTo(4, '/pages/view/messageList/index');
|
|
},
|
|
// 查看用户详情
|
|
selectCustomerServer(item) {
|
|
if (this.customerServerData.user_ids && this.customerServerData.user_ids.indexOf(item.user_id) === -1) {
|
|
navigateTo(1, '/pages/view/customerServer/customerMessage', item);
|
|
}
|
|
},
|
|
selectCustomerServerOfTopRight() {
|
|
navigateTo(1, '/pages/view/customerServer/customerMessage', {
|
|
user_id: this.userId
|
|
});
|
|
},
|
|
// 选择上传图片,转接
|
|
selectOption(item) {
|
|
this.selectModel = item;
|
|
},
|
|
// 聊天表情转换
|
|
replace_em(str) {
|
|
str = str.replace(/\[em-([\s\S]*)\]/g, "<span class='em em-$1'/></span>");
|
|
return str;
|
|
},
|
|
textareaChange(e) {
|
|
let strCont = e.target.value.replace(/\n\s(\s)*/gi, '\n'); // 将多个回车换行合并为 1个
|
|
strCont = strCont.replace(/^\s*/gi, ''); // 清除首行的 空格与换行
|
|
let strHtml = strCont.replace(/</gi, '<'); // 将所有的 < 转义为 < 防止html标签被转义
|
|
strHtml = strCont.replace(/\n(\n)*/gi, '<br>'); // 回车换行替换为 <br>
|
|
strHtml = strHtml.replace(/\s+/gi, ' '); // 一个或过个空格 替换为
|
|
strCont = strHtml.replace(/ /gi, ' '); // 逆向处理
|
|
strCont = strCont.replace(/<br>/gi, '\n'); // 逆向处理
|
|
strCont = strCont.replace(/</gi, '<');
|
|
/** 如果 p 标签最后的字符为 <br> 并不会单独另起一行, 会导致与 textarea 的高度相差一行,
|
|
* 所以在最后添加一个字符, 这样就能保证 P 标签的高度与 textarea 的高度一致
|
|
*/
|
|
this.pCont = strHtml + '.';
|
|
},
|
|
// 点击聊天内容
|
|
// 点击图片
|
|
showImage(item) {
|
|
console.log(item);
|
|
this.selectMessageImage = item;
|
|
this.$refs.showImage.open();
|
|
},
|
|
closeImage() {
|
|
this.$refs.showImage.close();
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped lang="less">
|
|
@import './less/customerServer.less';
|
|
@import './less/scriptLibary.less';
|
|
</style>
|