Merge branch 'feat-im-0607-ch' into msb_test

msb_test
ch 3 years ago
commit dec23c473a

@ -2,4 +2,7 @@ src/assets
src/icons src/icons
public public
dist dist
node_modules node_modules
src/utils/msb-im.js
src/utils/proto-rsq.js
src/utils/proto-rsp.js

7017
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,3 +1,10 @@
/*
* @Author: xwk
* @Date: 2022-05-24 17:00:26
* @LastEditors: ch
* @LastEditTime: 2022-06-09 10:10:47
* @Description: file content
*/
import request from '@/utils/request.js'; import request from '@/utils/request.js';
export const login = (params) => { export const login = (params) => {
return request({ return request({
@ -33,3 +40,37 @@ export const searchSummary = (params) => {
params, params,
}); });
}; };
/**
* 获取链接凭证
*/
export const getCustomeServiceTicket = () => {
return request({
url: '/mall/im/admin/ticket',
method: 'get',
params: {
ticketType: 'CONNECT_TICKET',
},
});
};
/**
* 获取可转移客服列表
* @param {*} params
*/
export const customerServiceList = (params) => {
return request({
url: '/mall/im/admin/waiter',
method: 'get',
params,
});
};
/**
* 转移客服
* @param {*} params
*/
export const transferCustomerService = (data) => {
return request({
url: '/mall/im/admin/waiter/transfer',
method: 'post',
data,
});
};

@ -8,14 +8,115 @@ const state = () => ({
opts: { opts: {
init: false, init: false,
}, },
sessionData: [],
customerServiceList: [],
messageType: { 1: 'text', 2: 'audio', 3: 'image', 4: 'video', 5: 'revoke', 6: 'custom', 7: 'notify' },
}); });
const getters = {}; const getters = {
parseTime: () => {
return (timestamp) => {
dayjs(new Date(timestamp)).format('MM-DD HH:mm:ss');
};
},
parseText: () => {
return ({ payload, type }) => {
if (type === 2) {
payload = '[语音]';
} else if (type === 3) {
payload = '[图片]';
} else if (type === 4) {
payload = '[视频]';
} else if (type === 5) {
payload = '[撤回消息]';
} else if (type === 6 || type === 1) {
try {
// payload = JSON.parse(payload.value);
if ('text' in payload) {
payload = payload.text;
} else if ('linkJump' in payload) {
payload = '[超链接]';
} else if ('orderNo' in payload) {
payload = '[订单信息]';
} else if ('productImageUrl' in payload) {
payload = '[商品信息]';
} else {
payload = '[未知数据]';
}
} catch (e) {
payload = '[解析异常]';
}
} else if (type === 7) {
payload = '[撤回消息]';
} else {
payload = '[未知类型]';
}
return payload;
};
},
parseImage: () => {
return (payload) => {
try {
// payload = JSON.parse(payload);
if ('url' in payload) {
payload = payload.url;
} else {
payload = '[未知图片]';
}
} catch (e) {
payload = '[解析异常]';
}
return payload;
};
},
parseVideo: () => {
return (payload) => {
try {
// payload = JSON.parse(payload);
if ('url' in payload) {
payload = payload.url;
} else {
payload = '[未知视频]';
}
} catch (e) {
payload = '[解析异常]';
}
return payload;
};
},
parseContent: () => {
return (payload) => {
try {
// payload = JSON.parse(payload);
if ('linkJump' in payload) {
payload.type = 'link';
} else if ('orderNo' in payload) {
payload.type = 'order';
} else if ('productImageUrl' in payload) {
payload.type = 'product';
} else {
payload = '[未知消息]';
}
} catch (e) {
payload = '[解析异常]';
}
return payload;
};
},
};
const mutations = { const mutations = {
setCode: (state, data) => (state.code = data), setCode: (state, data) => (state.code = data),
setCondition: (state, data) => (state.condition = data), setCondition: (state, data) => (state.condition = data),
setList: (state, data) => (state.list = data), setList: (state, data) => (state.list = data),
setTotal: (state, data) => (state.total = data), setTotal: (state, data) => (state.total = data),
setOpts: (state, data) => (state.opts = data), setOpts: (state, data) => (state.opts = data),
SET_SESSION_DATA(state, data) {
state.sessionData = data;
},
SET_SERVICE_LIST(state, data) {
state.customerServiceList = data;
},
}; };
const actions = { const actions = {
search: async ({ state, commit, rootGetters }) => { search: async ({ state, commit, rootGetters }) => {
@ -74,6 +175,28 @@ const actions = {
} }
} }
}, },
/**
* 查询可转移客服列表
*/
queryCustomerService: ({ commit }) => {
api.customerServiceList({
length: 100,
pageIndex: 1,
}).then((res) => {
commit('SET_SERVICE_LIST', res.records);
});
},
/**
* 提交转移会话
*/
submitTransferSession: ({ dispatch }, data) => {
api.transferCustomerService({
storeId: 1,
...data,
}).then((res) => {
console.log(res, 'resresres');
});
},
}; };
export default { export default {
state, state,

@ -0,0 +1,67 @@
/*
* @Author: ch
* @Date: 2022-06-07 15:52:37
* @LastEditors: ch
* @LastEditTime: 2022-06-09 10:59:15
* @Description: file content
*/
import * as api from '@/api/chat';
import config from '@/configs';
import $store from '@/store';
import MsbIm from '@/utils/msb-im';
import { FormatJsonSearch, ToAsyncAwait } from '@/utils/utils';
const Im = new MsbIm({
reconnect: true,
});
const ImInit = () => {
return new Promise((reslove, reject) => {
const storeUc = $store.state.auth.userInfo;
if (!storeUc) {
ImInit();
return false;
}
api.getCustomeServiceTicket().then(async (res) => {
const par = FormatJsonSearch({
client: res.client,
ticket: res.ticket,
user: storeUc.userId,
// nickname: storeUc.employeeName,
// avatar: storeUc.avatar,
// 1普通用户 2客服链接
connect: 2,
// user: 2,
// client: 'yan_xuan',
// ticket: '9kpEgiLzVG14znSTvElLOJE5MEMa/EGdexhab4CbDmLzDGnE+UXmVOvUs4ixyPeQ',
// nickname: '周渺',
// avatar: 'https://msb-edu-dev.oss-cn-beijing.aliyuncs.com/uc/account-avatar/桌面水果.jpg',
});
const { error, result } = await ToAsyncAwait(
Im.init({
url: `${config.socketURL}${par}`,
})
);
if (error) {
reject(error);
} else {
reslove(result);
}
});
});
};
Im.interceptors.dataChangeAfter = () => {
$store.commit('im/SET_SESSION_DATA', JSON.parse(JSON.stringify(Im.sessionData)));
// let msgCount = 0;
// Im.sessionData.forEach((i) => {
// msgCount += i.unreadCount;
// });
// $store.commit('SET_IM_MSG_COUNT', msgCount);
};
Im.interceptors.onLogout = () => {
Im.setSessionData([]);
// Im.setCurSessionId(null);
$store.commit('im/SET_SESSION_DATA', []);
// $store.commit('SET_IM_MSG_COUNT', 0);
};
export { Im, ImInit };

@ -0,0 +1,443 @@
/*
* @Author: ch
* @Date: 2022-05-18 14:54:47
* @LastEditors: ch
* @LastEditTime: 2022-06-09 11:50:54
* @Description: file content
*/
import '@/utils/poto-req';
import '@/utils/proto-rsp';
import { CreateUUID, ToAsyncAwait } from '@/utils/utils';
const connect = Symbol('connect');
const send = Symbol('send');
const onMessage = Symbol('onMessage');
const fromatPotoReq = (traceId, traceType, content) => {
let messageModel = new proto.ReqModel();
messageModel.setTraceid(traceId);
messageModel.setTracetype(traceType);
content && messageModel.setContent(JSON.stringify(content));
return messageModel.serializeBinary();
},
fromatPotoRsp = (data) => {
const res = proto.RspModel.deserializeBinary(new Uint8Array(data));
let ctx = res.getContent();
ctx = ctx ? JSON.parse(ctx) : {};
if (ctx.payload) {
ctx.payload = JSON.parse(ctx.payload);
}
return {
content: ctx,
traceId: res.getTraceid(),
traceType: res.getTracetype(),
code: res.getCode(),
message: res.getMessage(),
};
};
class MsbIm {
defaultOption = {
ioKey: 'traceId',
reconnect: true,
};
socket = null;
isOpen = false;
queue = {};
interceptors = {
dataChangeBefore: null,
dataChangeAfter: null,
onClose: null,
onMessage: null,
};
sessionData = [];
curSessionId = null;
constructor(option) {
this.option = {
...this.defaultOption,
...option,
};
}
/**
* 创建连接返回一个Promise 创建成功并成功打开连接算连接成功
* @param {*} option
*/
[connect](option) {
return new Promise((resolve, reject) => {
const open = () => {
console.log('[im] open');
this.isOpen = true;
resolve(this.socket);
};
const message = async (res) => {
const result = fromatPotoRsp(res.data);
this.interceptors.onMessage && this.interceptors.onMessage(result);
// 处理服务端主动推送的消息
this[onMessage](result);
// 如果再消息堆里有此消息回调,则执行回调,并删除
const cbk = this.queue[result[this.option.ioKey]];
if (cbk) {
cbk(result.code !== 200 ? { error: result } : { result: result });
delete this.queue[result[this.option.ioKey]];
}
};
const close = () => {
console.log('[im] close');
this.interceptors.onClose && this.interceptors.onClose();
};
if (uni) {
this.socket = uni.connectSocket({
...this.option,
fail(e) {
reject(e);
},
});
this.socket.onOpen(() => {
open();
this.socket.onMessage((res) => {
message(res);
});
});
this.socket.onClose(() => {
close();
});
} else if (WebSocket) {
try {
this.socket = new WebSocket(this.option.url);
this.socket.binaryType = 'arraybuffer';
this.socket.onopen = () => {
open();
};
this.socket.onmessage = (res) => {
message(res);
};
this.socket.onclose = () => {
close();
};
} catch (e) {
reject(e);
}
}
});
}
/**
* 向服务端发送消息||请求返回一个Promise对象收到ioKey对应的消息会算一个同步完成
* @param {*} data
*/
[send](data) {
return new Promise((resolve, reject) => {
if (!this.isOpen) {
return reject('连接未打开');
}
this.queue[data[this.option.ioKey]] = ({ result, error }) => {
if (result) {
resolve(result);
} else {
reject(error);
}
};
const par = fromatPotoReq(data.traceId, data.traceType, data.content);
if (uni) {
this.socket.send({
data: par,
fail(e) {
reject({ error: e });
},
});
} else if (WebSocket) {
this.socket.send(par);
}
});
}
/**
* 服务端推送消息只处理服务端主动推送的消息
* @param {*} data
*/
[onMessage](data) {
// 判断非服务端主动推送消息不做处理
if (data[this.option.ioKey] || data.code !== 200) {
return false;
}
let ctx = data.content;
let historyData = [...this.sessionData],
newData = [];
const hisIndex = historyData.findIndex((i) => i.id === ctx.sessionId);
if (hisIndex >= 0) {
// 存在会话往现有会话增加一条消息,并修改最后一条消息为当前消息
const curHisData = historyData[hisIndex];
curHisData.messageList.push(ctx);
curHisData.lastMessage = ctx;
// 不在当前会话窗口则向会话消息加1条未读
if (ctx.sessionId !== this.curSessionId) {
curHisData.unreadCount++;
}
newData = historyData;
} else {
// 会话列表不存在,则创建一个会话
newData = [
...historyData,
{
fromAvatar: ctx.fromAvatar,
fromId: ctx.fromId,
fromNickname: ctx.fromNickname,
id: ctx.id,
lastMessage: ctx,
messageList: [ctx],
unreadCount: 1,
},
];
}
this.setSessionData(newData);
}
init(config) {
return new Promise((resolve, reject) => {
const heart = () => {
// 要优化 心跳没回复需要重连
setTimeout(async () => {
if (this.isOpen) {
await this[send]({
traceId: CreateUUID(),
traceType: 0,
content: { text: 'ping' },
});
}
heart();
}, 1000);
};
this.option = {
...this.option,
...config,
};
this[connect]()
.then((res) => {
resolve(res);
heart();
})
.catch((e) => {
console.log('eeeee', e);
});
});
}
/**
* 设置数据
*/
setSessionData(data) {
let newData = JSON.parse(JSON.stringify(data));
this.interceptors.dataChangeBefore && this.interceptors.dataChangeBefore(newData, this.sessionData);
this.sessionData = newData;
this.interceptors.dataChangeAfter && this.interceptors.dataChangeAfter(this.sessionData);
}
/**
* 设置当前聊天窗口
* Data为Session数据
* @param {*} data
*/
setCurSessionId(id) {
this.curSessionId = id;
}
/**
* 获取会话列表
* @param {*} params
*/
async getSessionList(params) {
const par = {
traceId: CreateUUID(),
traceType: 1,
...params,
};
console.log('[im] 获取会话列表--start', par);
let { error, result } = await ToAsyncAwait(this[send](par));
console.log('[im] 获取会话列表--end', result, error);
if (error) {
return Promise.reject(error);
}
const { content } = result;
content.sessionVOS.forEach((item) => {
if (item.lastMessage) {
item.lastMessage.payload = JSON.parse(item.lastMessage.payload || {});
}
let historyData = this.sessionData;
let hisIndex = historyData.findIndex((i) => i.id === item.id);
if (hisIndex >= 0) {
historyData[hisIndex].lastMessage = item.lastMessage;
historyData[hisIndex].unreadCount++;
this.setSessionData(historyData);
} else {
item.messageList = [];
const newData = [...historyData, item];
this.setSessionData(newData);
}
});
return Promise.resolve(result);
}
/**
* 获取会话的历史消息记录
* @param {*} params
*/
async getHistoryMsg() {
const curSessionIdx = this.sessionData.findIndex((i) => i.id === this.curSessionId);
const curSession = this.sessionData[curSessionIdx];
const msgList = curSession.messageList || [];
const par = {
traceId: CreateUUID(),
traceType: 2,
content: {
sessionId: this.curSessionId,
topMessageId: msgList.length ? msgList[0].id : null,
},
};
console.log('[im] 获取会话历史消息--start', par);
const { error, result } = await ToAsyncAwait(this[send](par));
console.log('[im] 获取会话历史消息--end', result, error);
if (error) {
return Promise.reject(error);
}
const { content } = result;
if (content.length) {
let newData = this.sessionData;
content.forEach((item) => {
item.payload = JSON.parse(item.payload);
});
newData[curSessionIdx].messageList = content.concat(newData[curSessionIdx].messageList);
this.setSessionData(newData);
}
return Promise.resolve(result);
}
/**
* 会话已读
* @param {*} params
*/
async setRead(params) {
const par = {
traceId: CreateUUID(),
traceType: '6',
...params,
};
console.log('[im] 会话已读--start', par);
const { error, result } = await this[send](par);
console.log('[im] 会话已读--end', result, error);
let newData = this.sessionData.map((item) => {
if (item.id == params.content.sessionId) {
item.unreadCount = 0;
}
return item;
});
this.setSessionData(newData);
}
/**
* 发送消息
* @param {*} params
*/
async sendMsg(params) {
const index = this.sessionData.findIndex((i) => i.id === this.curSessionId);
let curSession = this.sessionData[index];
// 临时消息体
let par = {
...params,
traceId: CreateUUID(),
traceType: 3,
};
let msgCtx = {
...params.content,
...par,
createTimeStamp: new Date().getTime(),
sendStatus: 'loading',
};
// 点发送,立即把消息加入消息列表,标记为发送中状态
curSession.messageList.push(msgCtx);
// 超过时间未返回视为发送失败
this.timerStatus(msgCtx);
console.log('[im] 发送消息--start', par);
const { error, result } = await ToAsyncAwait(this[send](par));
console.log('[im] 发送消息--end', result, error);
// 接到通知,标记消息是否发送成功
for (let i = curSession.messageList.length; i--; ) {
const item = curSession.messageList[i];
if (item[this.option.ioKey] === par[this.option.ioKey]) {
curSession.messageList[i].sendStatus = msgCtx.sendStatus = error ? 'fail' : 'success';
break;
}
}
let newData = [...this.sessionData];
newData[index] = curSession;
this.setSessionData(newData);
if (error) {
return Promise.reject(error);
}
return Promise.resolve(result);
}
/**
* 发送失败时重新发送
* @param {*} params
*/
async resend(params) {
params.sendStatus = 'loading';
this.timerStatus(params);
console.log('[im] 重新发送消息--start', params);
const { error, result } = await ToAsyncAwait(
this[send]({
traceId: params.traceId,
traceType: params.traceType,
content: params.content,
})
);
console.log('[im] 重新发送消息--end', result, error);
params.createTimeStamp = result.createTimeStamp;
if (error) {
params.sendStatus = 'fail';
return Promise.reject(error);
}
params.sendStatus = 'success';
return Promise.resolve(result);
}
timerStatus(msg) {
setTimeout(() => {
if (msg.sendStatus === 'loading') {
msg.sendStatus = 'fail';
delete this.queue[msg.traceId];
}
}, 3000);
}
/**
* 主动创建会话
* @param {*} params
*/
async createSession(params) {
const par = {
traceId: CreateUUID(),
traceType: 9,
...params,
};
console.log('[im] 主动创建会话--start', par);
const { result, error } = await ToAsyncAwait(this[send](par));
console.log('[im] 主动创建会话--end', result, error);
if (error) {
return Promise.reject(error);
}
const { content } = result;
let historyData = this.sessionData;
let curSession = historyData.find((i) => i.id === content.id);
if (!curSession) {
curSession = {
...content,
unreadCount: 0,
messageList: [],
};
const newData = [...historyData, curSession];
this.setSessionData(newData);
}
return Promise.resolve(result);
}
}
export default MsbIm;
// export default (app) => {
// app.config.globalProperties.$im = MsbIm;
// };

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -0,0 +1,36 @@
/*
* @Author: ch
* @Date: 2022-06-07 11:34:12
* @LastEditors: ch
* @LastEditTime: 2022-06-07 16:12:42
* @Description: file content
*/
//生成UUID
const CreateUUID = () => {
let d = _.now();
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (d + _.random(16)) % 16 | 0;
d = Math.floor(d / 16);
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
});
};
const ToAsyncAwait = (promise, fromatResult = true) => {
if (!fromatResult) {
return promise;
}
return promise.then((res) => ({ error: null, result: res })).catch((err) => ({ error: err, result: null }));
};
const FormatJsonSearch = (myJson, connector = '?') => {
if (myJson.constructor !== Object) {
throw new Error('必须是JSON对象');
}
let arr = [];
for (let i in myJson) {
arr.push(`${i}=${encodeURIComponent(myJson[i])}`);
}
return `${connector}${arr.join('&')}`;
};
export { CreateUUID, ToAsyncAwait, FormatJsonSearch };

@ -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>

@ -37,12 +37,12 @@
<div class="row"> <div class="row">
<div class="session-name">{{ item.fromNickname }}</div> <div class="session-name">{{ item.fromNickname }}</div>
<div class="session-time"> <div class="session-time">
{{ store.getters['chat/parseTime'](item.lastMessage.createTimeStamp) }} {{ store.getters['im/parseTime'](item.lastMessage.createTimeStamp) }}
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="session-content"> <div class="session-content">
{{ store.getters['chat/parseText'](item.lastMessage) }} {{ store.getters['im/parseText'](item.lastMessage) }}
</div> </div>
</div> </div>
</div> </div>
@ -68,11 +68,16 @@
</div> </div>
</div> </div>
<el-scrollbar ref="refsMessageList" class="message-list"> <el-scrollbar ref="refsMessageList" class="message-list">
<el-button v-if="sessionMessageList.length" class="load" type="text" @click="handleLoadMore"> <el-button
v-if="currentSession.messageList?.length"
class="load"
type="text"
@click="handleLoadMore"
>
加载更多 加载更多
</el-button> </el-button>
<message-item <message-item
v-for="(item, index) in sessionMessageList" v-for="(item, index) in currentSession.messageList"
:key="index" :key="index"
:message="item" :message="item"
:session="currentSession" :session="currentSession"
@ -137,14 +142,17 @@
<script setup lang="jsx"> <script setup lang="jsx">
import { upload } from '@/api/file'; import { upload } from '@/api/file';
import { emojiData, entitiestoUtf16 } from '@/utils/chat.js'; import { emojiData, entitiestoUtf16 } from '@/utils/chat.js';
import { Im, ImInit } from '@/utils/im.js';
import { ElButton } from 'element-plus/es/components/button/index'; import { ElButton } from 'element-plus/es/components/button/index';
import 'element-plus/es/components/button/style/css'; import 'element-plus/es/components/button/style/css';
import MessageItem from './message.vue'; import MessageItem from './message.vue';
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
const router = useRouter(); const router = useRouter();
const store = useStore(); const store = useStore();
store.dispatch('chat/connect');
store.dispatch('chat/querySession'); ImInit().then(() => {
Im.getSessionList();
});
const opts = computed(() => store.state.chat.opts); const opts = computed(() => store.state.chat.opts);
// //
@ -158,25 +166,31 @@
}); });
// //
const currentSessionId = computed(() => store.state.chat.currentSession); let currentSessionId = ref();
const state = reactive({ const state = reactive({
message: '', message: '',
}); });
const sessionList = computed(() => { const sessionList = computed(() => store.state.im.sessionData);
return store.state.chat.sessionData?.sessionVOS || [];
});
const currentSession = computed(() => sessionList.value.find((item) => item.id === currentSessionId.value)); const currentSession = computed(() => sessionList.value.find((item) => item.id === currentSessionId.value));
const handleChangeSession = (id) => { const handleChangeSession = (id) => {
store.dispatch('chat/revoke', 28); currentSessionId.value = id;
store.dispatch('chat/revoke', 31); // ID
store.commit('chat/setCurrentSession', id); Im.setCurSessionId(id);
store.commit('chat/setMessageList', []); //
store.dispatch('chat/querySessionMessage'); Im.setRead({
store.dispatch('chat/submitRead'); content: {
sessionId: id,
},
});
if (!sessionMessageList.length) {
Im.getHistoryMsg();
}
}; };
// //
const sessionMessageList = computed(() => store.state.chat.messageList); const sessionMessageList = computed(() => {
return currentSession.messageList || [];
});
const refsMessageList = ref(null); const refsMessageList = ref(null);
watch(sessionMessageList, (value, old) => { watch(sessionMessageList, (value, old) => {
let offset = refsMessageList.value let offset = refsMessageList.value
@ -191,14 +205,21 @@
}); });
}); });
const handleLoadMore = () => { const handleLoadMore = () => {
store.dispatch('chat/querySessionMessage', { topMessageId: unref(sessionMessageList)[0].id }); Im.getHistoryMsg();
}; };
const handleSendMessage = (e) => { const handleSendMessage = (e) => {
if (e && e.shiftKey) { if (e && e.shiftKey) {
state.message += '\n'; state.message += '\n';
} else { } else {
if (state.message) { if (state.message) {
store.dispatch('chat/submitMessage', { text: state.message }); Im.sendMsg({
fromId: userInfo.value.userId,
content: {
toSessionId: currentSessionId.value,
payload: { text: state.message },
type: 1,
},
});
state.message = ''; state.message = '';
} else { } else {
proxy.$message.warning('发送消息不能为空'); proxy.$message.warning('发送消息不能为空');
@ -218,12 +239,12 @@
columns: [ columns: [
{ {
label: '账号', label: '账号',
prop: 'account', prop: 'waiterId',
width: 160, width: 160,
}, },
{ {
label: '客户昵称', label: '客户昵称',
prop: 'nickname', prop: 'waiterNickname',
minWidth: 160, minWidth: 160,
}, },
{ {
@ -247,9 +268,9 @@
}, },
], ],
}); });
const customerServiceList = computed(() => store.state.chat.customerServiceList); const customerServiceList = computed(() => store.state.im.customerServiceList);
const handleTransferSession = () => { const handleTransferSession = () => {
store.dispatch('chat/queryCustomerService'); store.dispatch('im/queryCustomerService');
transferVisible.value = true; transferVisible.value = true;
}; };
const handleConfirmTransfer = async (row) => { const handleConfirmTransfer = async (row) => {
@ -258,8 +279,8 @@
confirmButtonText: '确定', confirmButtonText: '确定',
}); });
if (res.action === 'confirm') { if (res.action === 'confirm') {
store.dispatch('chat/submitTransferSession', { store.dispatch('im/submitTransferSession', {
toWaiterId: row.userId, toWaiterId: row.waiterId,
sessionId: unref(currentSessionId), sessionId: unref(currentSessionId),
reason: res.value, reason: res.value,
}); });
@ -287,7 +308,16 @@
const file = e.target.files[0]; const file = e.target.files[0];
e.target.value = null; e.target.value = null;
let url = await upload('mall-product', 'im/', file); let url = await upload('mall-product', 'im/', file);
store.dispatch('chat/submitImage', { url }); // store.dispatch('chat/submitImage', { url });
Im.sendMsg({
fromId: 2, //userInfo.value.id,
content: {
toSessionId: currentSessionId.value,
payload: { url },
type: 3,
},
});
}; };
// //
@ -299,7 +329,16 @@
const file = e.target.files[0]; const file = e.target.files[0];
e.target.value = null; e.target.value = null;
let url = await upload('mall-product', 'im/', file); let url = await upload('mall-product', 'im/', file);
store.dispatch('chat/submitVideo', { url }); // store.dispatch('chat/submitVideo', { url });
Im.sendMsg({
fromId: 2, //userInfo.value.id,
content: {
toSessionId: currentSessionId.value,
payload: { url },
type: 4,
},
});
}; };
</script> </script>

@ -73,26 +73,26 @@
<div v-else-if="messageType[props.message.type] === 'image'" class="content shadow"> <div v-else-if="messageType[props.message.type] === 'image'" class="content shadow">
<el-image <el-image
alt="[图片消息]" alt="[图片消息]"
:src="store.getters['chat/parseImage'](props.message.payload)" :src="store.getters['im/parseImage'](props.message.payload)"
style="max-width: 240px" style="max-width: 240px"
/> />
</div> </div>
<div v-else-if="messageType[props.message.type] === 'video'" class="shadow"> <div v-else-if="messageType[props.message.type] === 'video'" class="shadow">
<video controls width="240"> <video controls width="240">
<source :src="store.getters['chat/parseVideo'](props.message.payload)" type="video/mp4" /> <source :src="store.getters['im/parseVideo'](props.message.payload)" type="video/mp4" />
<object <object
:data="store.getters['chat/parseVideo'](props.message.payload)" :data="store.getters['im/parseVideo'](props.message.payload)"
height="240" height="240"
width="320" width="320"
></object> ></object>
</video> </video>
</div> </div>
<div v-else class="content shadow"> <div v-else class="content shadow">
{{ store.getters['chat/parseText'](props.message) }} {{ store.getters['im/parseText'](props.message) }}
</div> </div>
</div> </div>
<div v-if="!['revoke', 'notify'].includes(messageType[props.message.type])" class="time"> <div v-if="!['revoke', 'notify'].includes(messageType[props.message.type])" class="time">
{{ store.getters['chat/parseTime'](props.message.createTimeStamp) }} {{ store.getters['im/parseTime'](props.message.createTimeStamp) }}
</div> </div>
</div> </div>
</template> </template>
@ -111,7 +111,7 @@
}, },
}); });
const messageType = computed(() => store.state.chat.messageType); const messageType = computed(() => store.state.chat.messageType);
const content = computed(() => store.getters['chat/parseContent'](props.message.payload)); const content = computed(() => store.getters['im/parseContent'](props.message.payload));
const handleProduct = (id) => { const handleProduct = (id) => {
router.push({ router.push({
name: 'UpdateProduct', name: 'UpdateProduct',

@ -85,7 +85,7 @@ export default (configEnv) => {
sourcePath: 'src/styles', sourcePath: 'src/styles',
}), }),
legacy({ legacy({
targets: ['defaults', 'not IE 11'], targets: ['defaults', 'not IE 11', 'chrome 52'],
}), }),
{ {
...eslintPlugin({ ...eslintPlugin({

Loading…
Cancel
Save