|
|
|
@ -0,0 +1,475 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="chat-container" @click="emojiVisible = false">
|
|
|
|
|
<div class="header">
|
|
|
|
|
<p>当前客服:{{ userInfo?.employeeName }}</p>
|
|
|
|
|
<!-- <el-button type="text" @click="summaryVisible = !summaryVisible">
|
|
|
|
|
{{ summaryVisible ? '收起' : '统计数据' }}
|
|
|
|
|
</el-button> -->
|
|
|
|
|
</div>
|
|
|
|
|
<div class="summary">
|
|
|
|
|
<el-table v-show="summaryVisible" border :data="[summary]">
|
|
|
|
|
<el-table-column align="center" header-align="center" label="今日接待次数" prop="todayTimes" />
|
|
|
|
|
<el-table-column align="center" header-align="center" label="今日接待人数" prop="todayCount" />
|
|
|
|
|
<el-table-column align="center" header-align="center" label="历史接待次数" prop="historyTimes" />
|
|
|
|
|
<el-table-column align="center" header-align="center" label="历史接待人数" prop="historyCount" />
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="body">
|
|
|
|
|
<div class="aside">
|
|
|
|
|
<div class="aside-header">近期会话</div>
|
|
|
|
|
<el-scrollbar class="session-list" tag="ul">
|
|
|
|
|
<li
|
|
|
|
|
v-for="(item, index) in sessionList"
|
|
|
|
|
:key="index"
|
|
|
|
|
class="session-item"
|
|
|
|
|
:class="{ active: currentSessionId === item.id }"
|
|
|
|
|
@click="handleChangeSession(item.id)"
|
|
|
|
|
>
|
|
|
|
|
<el-badge
|
|
|
|
|
class="session-count"
|
|
|
|
|
:hidden="item.unreadCount === 0"
|
|
|
|
|
:max="99"
|
|
|
|
|
:value="item.unreadCount"
|
|
|
|
|
>
|
|
|
|
|
<el-avatar circle :src="item.fromAvatar" />
|
|
|
|
|
</el-badge>
|
|
|
|
|
<div class="session-info">
|
|
|
|
|
<div class="row">
|
|
|
|
|
<div class="session-name">{{ item.fromNickname }}</div>
|
|
|
|
|
<div class="session-time">
|
|
|
|
|
{{ store.getters['chat/parseTime'](item.lastMessage.createTimeStamp) }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="row">
|
|
|
|
|
<div class="session-content">
|
|
|
|
|
{{ store.getters['chat/parseText'](item.lastMessage) }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</li>
|
|
|
|
|
</el-scrollbar>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="currentSession" class="content">
|
|
|
|
|
<div class="content-header">
|
|
|
|
|
<div class="content-header-left">
|
|
|
|
|
<div class="name" :class="{ [`sex-` + currentSession?.fromSex]: true }">
|
|
|
|
|
{{ currentSession?.fromNickname }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="content-header-right">
|
|
|
|
|
<el-button @click="handleOrder">
|
|
|
|
|
<el-icon name="clipboard" />
|
|
|
|
|
个人订单
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-button @click="handleTransferSession">
|
|
|
|
|
<el-icon name="chat-forward" />
|
|
|
|
|
转移会话
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<el-scrollbar ref="refsMessageList" class="message-list">
|
|
|
|
|
<el-button v-if="sessionMessageList.length" class="load" type="text" @click="handleLoadMore">
|
|
|
|
|
加载更多
|
|
|
|
|
</el-button>
|
|
|
|
|
<message-item
|
|
|
|
|
v-for="(item, index) in sessionMessageList"
|
|
|
|
|
:key="index"
|
|
|
|
|
:message="item"
|
|
|
|
|
:session="currentSession"
|
|
|
|
|
/>
|
|
|
|
|
</el-scrollbar>
|
|
|
|
|
<div class="operation-bar">
|
|
|
|
|
<el-button type="text" @click.stop="emojiVisible = !emojiVisible">
|
|
|
|
|
<el-icon name="chat-smile-3-fill" size="20" />
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-button type="text" @click="handlePickImage">
|
|
|
|
|
<el-icon name="image-fill" size="20" />
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-button type="text" @click="handlePickVideo">
|
|
|
|
|
<el-icon name="movie-fill" size="20" />
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-scrollbar v-show="emojiVisible" class="emoji-panel" tag="ul">
|
|
|
|
|
<li v-for="(item, index) in emojiList" :key="index" @click="handleAddEmoji(item)">
|
|
|
|
|
{{ entitiestoUtf16(item) }}
|
|
|
|
|
</li>
|
|
|
|
|
</el-scrollbar>
|
|
|
|
|
<input
|
|
|
|
|
ref="refsImage"
|
|
|
|
|
accept="image/*"
|
|
|
|
|
style="display: none"
|
|
|
|
|
type="file"
|
|
|
|
|
@change="handleSendImage"
|
|
|
|
|
/>
|
|
|
|
|
<input
|
|
|
|
|
ref="refsVideo"
|
|
|
|
|
accept="video/*"
|
|
|
|
|
style="display: none"
|
|
|
|
|
type="file"
|
|
|
|
|
@change="handleSendVideo"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="input">
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="state.message"
|
|
|
|
|
placeholder="请输入要发送的内容,shift+enter换行,enter发送"
|
|
|
|
|
type="textarea"
|
|
|
|
|
@keypress.enter.prevent="handleSendMessage"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="send">
|
|
|
|
|
<el-button type="primary" @click="handleSendMessage()">发送</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else class="content empty">请点击左侧会话列表与买家进行聊天</div>
|
|
|
|
|
</div>
|
|
|
|
|
<el-dialog v-model="transferVisible" title="转移会话">
|
|
|
|
|
<table-list
|
|
|
|
|
code="TransferCustomerService"
|
|
|
|
|
:config="transferConfig"
|
|
|
|
|
:data="customerServiceList"
|
|
|
|
|
:operation="[]"
|
|
|
|
|
style="height: 500px"
|
|
|
|
|
/>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="jsx">
|
|
|
|
|
import { upload } from '@/api/file';
|
|
|
|
|
import { emojiData, entitiestoUtf16 } from '@/utils/chat.js';
|
|
|
|
|
import { ElButton } from 'element-plus/es/components/button/index';
|
|
|
|
|
import 'element-plus/es/components/button/style/css';
|
|
|
|
|
import MessageItem from './message.vue';
|
|
|
|
|
const { proxy } = getCurrentInstance();
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const store = useStore();
|
|
|
|
|
store.dispatch('chat/connect');
|
|
|
|
|
store.dispatch('chat/querySession');
|
|
|
|
|
const opts = computed(() => store.state.chat.opts);
|
|
|
|
|
|
|
|
|
|
// 统计
|
|
|
|
|
const userInfo = computed(() => store.state.auth.userInfo);
|
|
|
|
|
const summaryVisible = ref(false);
|
|
|
|
|
const summary = ref({
|
|
|
|
|
historyTimes: 0,
|
|
|
|
|
historyCount: 0,
|
|
|
|
|
todayTimes: 0,
|
|
|
|
|
todayCount: 0,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 会话
|
|
|
|
|
const currentSessionId = computed(() => store.state.chat.currentSession);
|
|
|
|
|
const state = reactive({
|
|
|
|
|
message: '',
|
|
|
|
|
});
|
|
|
|
|
const sessionList = computed(() => {
|
|
|
|
|
return store.state.chat.sessionData?.sessionVOS || [];
|
|
|
|
|
});
|
|
|
|
|
const currentSession = computed(() => sessionList.value.find((item) => item.id === currentSessionId.value));
|
|
|
|
|
const handleChangeSession = (id) => {
|
|
|
|
|
store.dispatch('chat/revoke', 28);
|
|
|
|
|
store.dispatch('chat/revoke', 31);
|
|
|
|
|
store.commit('chat/setCurrentSession', id);
|
|
|
|
|
store.commit('chat/setMessageList', []);
|
|
|
|
|
store.dispatch('chat/querySessionMessage');
|
|
|
|
|
store.dispatch('chat/submitRead');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 聊天
|
|
|
|
|
const sessionMessageList = computed(() => store.state.chat.messageList);
|
|
|
|
|
const refsMessageList = ref(null);
|
|
|
|
|
watch(sessionMessageList, (value, old) => {
|
|
|
|
|
let offset = refsMessageList.value
|
|
|
|
|
? refsMessageList.value.resize$.scrollHeight - refsMessageList.value.resize$.scrollTop
|
|
|
|
|
: 0;
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
if (!old?.length || value.indexOf(old[0]) === 0) {
|
|
|
|
|
refsMessageList.value.setScrollTop(refsMessageList.value.resize$.scrollHeight);
|
|
|
|
|
} else {
|
|
|
|
|
refsMessageList.value.setScrollTop(refsMessageList.value.resize$.scrollHeight - offset);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
const handleLoadMore = () => {
|
|
|
|
|
store.dispatch('chat/querySessionMessage', { topMessageId: unref(sessionMessageList)[0].id });
|
|
|
|
|
};
|
|
|
|
|
const handleSendMessage = (e) => {
|
|
|
|
|
if (e && e.shiftKey) {
|
|
|
|
|
state.message += '\n';
|
|
|
|
|
} else {
|
|
|
|
|
if (state.message) {
|
|
|
|
|
store.dispatch('chat/submitMessage', { text: state.message });
|
|
|
|
|
state.message = '';
|
|
|
|
|
} else {
|
|
|
|
|
proxy.$message.warning('发送消息不能为空');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 个人订单
|
|
|
|
|
const handleOrder = () => {
|
|
|
|
|
router.push({ name: 'OrderManagement', query: { id: currentSession.value.fromId } });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 转移会话
|
|
|
|
|
const transferVisible = ref(false);
|
|
|
|
|
const transferConfig = ref({
|
|
|
|
|
page: false,
|
|
|
|
|
columns: [
|
|
|
|
|
{
|
|
|
|
|
label: '账号',
|
|
|
|
|
prop: 'account',
|
|
|
|
|
width: 160,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: '客户昵称',
|
|
|
|
|
prop: 'nickname',
|
|
|
|
|
minWidth: 160,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: '类型',
|
|
|
|
|
prop: 'type',
|
|
|
|
|
width: 160,
|
|
|
|
|
slots: {
|
|
|
|
|
default: ({ row }) => proxy.$dict(unref(opts).customerServiceType, row.type),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: '操作',
|
|
|
|
|
width: 100,
|
|
|
|
|
slots: {
|
|
|
|
|
default: ({ row }) => (
|
|
|
|
|
<ElButton type="text" onClick={() => handleConfirmTransfer(row)}>
|
|
|
|
|
转移
|
|
|
|
|
</ElButton>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
const customerServiceList = computed(() => store.state.chat.customerServiceList);
|
|
|
|
|
const handleTransferSession = () => {
|
|
|
|
|
store.dispatch('chat/queryCustomerService');
|
|
|
|
|
transferVisible.value = true;
|
|
|
|
|
};
|
|
|
|
|
const handleConfirmTransfer = async (row) => {
|
|
|
|
|
try {
|
|
|
|
|
let res = await proxy.$prompt('请输入转移原因', '转移会话', {
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
});
|
|
|
|
|
if (res.action === 'confirm') {
|
|
|
|
|
store.dispatch('chat/submitTransferSession', {
|
|
|
|
|
toWaiterId: row.userId,
|
|
|
|
|
sessionId: unref(currentSessionId),
|
|
|
|
|
reason: res.value,
|
|
|
|
|
});
|
|
|
|
|
transferVisible.value = false;
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.info('取消转移会话', e);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 表情
|
|
|
|
|
const emojiVisible = ref(false);
|
|
|
|
|
const emojiList = computed(() => emojiData.map((item) => item.list).flat());
|
|
|
|
|
const handleAddEmoji = (data) => {
|
|
|
|
|
let str = ' ' + entitiestoUtf16(data) + ' ';
|
|
|
|
|
state.message += str;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 图片
|
|
|
|
|
const refsImage = ref(null);
|
|
|
|
|
const handlePickImage = () => {
|
|
|
|
|
refsImage.value.click();
|
|
|
|
|
};
|
|
|
|
|
const handleSendImage = async (e) => {
|
|
|
|
|
const file = e.target.files[0];
|
|
|
|
|
e.target.value = null;
|
|
|
|
|
let url = await upload('mall-product', 'im/', file);
|
|
|
|
|
store.dispatch('chat/submitImage', { url });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 视频
|
|
|
|
|
const refsVideo = ref(null);
|
|
|
|
|
const handlePickVideo = () => {
|
|
|
|
|
refsVideo.value.click();
|
|
|
|
|
};
|
|
|
|
|
const handleSendVideo = async (e) => {
|
|
|
|
|
const file = e.target.files[0];
|
|
|
|
|
e.target.value = null;
|
|
|
|
|
let url = await upload('mall-product', 'im/', file);
|
|
|
|
|
store.dispatch('chat/submitVideo', { url });
|
|
|
|
|
};
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="less" scoped>
|
|
|
|
|
.chat-container {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
.header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: @layout-space 0;
|
|
|
|
|
.el-button {
|
|
|
|
|
margin-left: @layout-space-super;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.body {
|
|
|
|
|
width: 100%;
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
display: flex;
|
|
|
|
|
border: 1px solid #ebeef5;
|
|
|
|
|
.aside {
|
|
|
|
|
border-right: 1px solid #ebeef5;
|
|
|
|
|
.aside-header {
|
|
|
|
|
height: 60px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: @layout-space;
|
|
|
|
|
border-bottom: 1px solid #ebeef5;
|
|
|
|
|
font-size: @layout-h3;
|
|
|
|
|
font-weight: bolder;
|
|
|
|
|
}
|
|
|
|
|
.session-list {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
.session-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: @layout-space;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
&:hover {
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
}
|
|
|
|
|
&.active {
|
|
|
|
|
background-color: #eee;
|
|
|
|
|
}
|
|
|
|
|
.session-count {
|
|
|
|
|
margin-right: @layout-space;
|
|
|
|
|
:deep(.el-badge__content) {
|
|
|
|
|
top: calc(@layout-space / 2);
|
|
|
|
|
right: calc(@layout-space * 1.25);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.session-info {
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
margin-left: @layout-space;
|
|
|
|
|
.row {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
}
|
|
|
|
|
.session-time,
|
|
|
|
|
.session-content {
|
|
|
|
|
font-size: 0.8em;
|
|
|
|
|
color: #999;
|
|
|
|
|
}
|
|
|
|
|
.session-content {
|
|
|
|
|
width: 150px;
|
|
|
|
|
margin-top: @layout-space-small;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
+ .session-item {
|
|
|
|
|
border-top: 1px solid #ebeef5;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.content {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
&.empty {
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
color: #999;
|
|
|
|
|
}
|
|
|
|
|
.content-header {
|
|
|
|
|
height: 60px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
padding: @layout-space;
|
|
|
|
|
border-bottom: 1px solid #ebeef5;
|
|
|
|
|
.content-header-left {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
.name {
|
|
|
|
|
font-size: @layout-h3;
|
|
|
|
|
font-weight: bolder;
|
|
|
|
|
&.sex-0::before {
|
|
|
|
|
content: '♀';
|
|
|
|
|
color: var(--el-color-danger);
|
|
|
|
|
font-weight: bolder;
|
|
|
|
|
font-family: '黑体';
|
|
|
|
|
}
|
|
|
|
|
&.sex-1::before {
|
|
|
|
|
content: '♂';
|
|
|
|
|
color: var(--el-color-primary);
|
|
|
|
|
font-weight: bolder;
|
|
|
|
|
font-family: '黑体';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.message-list {
|
|
|
|
|
flex: 1;
|
|
|
|
|
background-color: #f1f1f1;
|
|
|
|
|
.load {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.operation-bar {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 0 @layout-space;
|
|
|
|
|
border-top: 1px solid #ebeef5;
|
|
|
|
|
border-bottom: 1px solid #ebeef5;
|
|
|
|
|
position: relative;
|
|
|
|
|
.emoji-panel {
|
|
|
|
|
position: absolute;
|
|
|
|
|
z-index: 9999;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
|
|
|
|
|
bottom: 52px;
|
|
|
|
|
width: 300px;
|
|
|
|
|
height: 200px;
|
|
|
|
|
padding: 10px 6px 10px 10px;
|
|
|
|
|
background: #eee;
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
:deep(ul) {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fill, 30px);
|
|
|
|
|
column-gap: auto;
|
|
|
|
|
li {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.input {
|
|
|
|
|
height: 64px;
|
|
|
|
|
.el-textarea {
|
|
|
|
|
height: 100%;
|
|
|
|
|
:deep(textarea) {
|
|
|
|
|
height: 100% !important;
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.send {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
padding: @layout-space-small;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|