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

merge-requests/1/head
ch 3 years ago
commit fe38ced8a2

@ -2,3 +2,4 @@ VITE_BASE_URL=/api
VITE_SOCKET_URL=wss://k8s-horse-gateway.mashibing.cn/ws VITE_SOCKET_URL=wss://k8s-horse-gateway.mashibing.cn/ws
#VITE_SOCKET_URL=ws://192.168.10.93:8090/ws #VITE_SOCKET_URL=ws://192.168.10.93:8090/ws
VITE_REQUEST_TIMEOUT=5000 VITE_REQUEST_TIMEOUT=5000
VITE_BROWSER_URL = https://k8s-shop-pc.mashibing.cn

@ -1,3 +1,4 @@
VITE_BASE_URL=https://you-gateway.mashibing.com VITE_BASE_URL=https://you-gateway.mashibing.com
VITE_SOCKET_URL=wss://you-gateway.mashibing.com/ws VITE_SOCKET_URL=wss://you-gateway.mashibing.com/ws
VITE_REQUEST_TIMEOUT=20000 VITE_REQUEST_TIMEOUT=20000
VITE_BROWSER_URL = https://k8s-shop-pc.mashibing.cn

@ -1,3 +1,4 @@
VITE_BASE_URL=https://k8s-horse-gateway.mashibing.cn/ VITE_BASE_URL=https://k8s-horse-gateway.mashibing.cn/
VITE_SOCKET_URL=wss://k8s-horse-gateway.mashibing.cn/ws VITE_SOCKET_URL=wss://k8s-horse-gateway.mashibing.cn/ws
VITE_REQUEST_TIMEOUT=20000 VITE_REQUEST_TIMEOUT=20000
VITE_BROWSER_URL = https://k8s-shop-pc.mashibing.cn

@ -3,3 +3,6 @@ src/icons
public public
dist dist
node_modules node_modules
src/utils/msb-im.js
src/utils/poto-req.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-13 14:26:53
* @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,47 @@ export const searchSummary = (params) => {
params, params,
}); });
}; };
/**
* 获取链接凭证
*/
export const getCustomeServiceTicket = () => {
return request({
url: '/mall/im/admin/ticket',
method: 'get',
params: {
ticketType: 'CONNECT_TICKET',
},
});
};
/**
* 获取当前客服
*/
export const getCustomerService = () => {
return request({
url: '/mall/im/admin/waiterUser/getWaiterByUserId',
method: 'get',
});
};
/**
* 获取可转移客服列表
* @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,
});
};

@ -0,0 +1,363 @@
import { login } from '@/api/chat';
import config from '@/configs';
import { ElMessage } from '@/plugins/element-plus';
import { UUID } from '@/utils/chat';
import dayjs from 'dayjs';
const state = () => ({
socket: null,
heart: null,
queue: [],
task: [],
currentSession: null,
messageList: [],
messageType: { 1: 'text', 2: 'audio', 3: 'image', 4: 'video', 5: 'revoke', 6: 'custom', 7: 'notify' },
curCustomerService: {},
sessionData: [],
customerServiceList: [],
opts: {
customerServiceType: [
{
label: '售前',
value: 1,
},
{
label: '售后',
value: 2,
},
{
label: '发货',
value: 3,
},
],
},
});
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);
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 = {
setSocket: (state, data) => (state.socket = data),
setHeart: (state, data) => (state.heart = data),
setTask: (state, data) => (state.task = data),
addTask: (state, data) => state.task.push(data),
delTask: (state, data) => state.task.splice(data, 1),
setCurrentSession: (state, data) => (state.currentSession = data),
setSessionData: (state, data) => (state.sessionData = data),
setMessageList: (state, data) => (state.messageList = data),
setCustomerServiceList: (state, data) => (state.customerServiceList = data),
SET_CUR_SERVICE(state, data) {
state.curCustomerService = data || {};
},
SET_SESSION_DATA(state, data) {
state.sessionData = data;
},
SET_SERVICE_LIST(state, data) {
state.customerServiceList = data;
},
};
const actions = {
/**
* 创建连接
*/
connect: async ({ state, commit, dispatch }) => {
let { ticket } = await login({ storeId: 1 });
return new Promise((resolve, reject) => {
if (window.WebSocket) {
const socket = new WebSocket(`${config.socketURL}?client=${ticket}&type=2`);
socket.onmessage = ({ data }) => {
dispatch('receive', data);
};
socket.onopen = () => {
commit(
'setHeart',
setInterval(() => {
dispatch('heart');
}, 3000)
);
console.info('[chat] open');
resolve(socket);
};
socket.onclose = () => {
clearInterval(state.heart);
console.info('[chat] close');
};
socket.onerror = (e) => {
clearInterval(state.heart);
console.info('[chat] error', e);
reject(e);
};
commit('setSocket', socket);
} else {
ElMessage.error('当前浏览器不支持 WebSocket');
reject('not support websocket');
}
});
},
/**
* 发送心跳任务监测
*/
heart: ({ state, dispatch }) => {
dispatch('send', {
traceType: 26,
content: { storeId: 1 },
});
// console.info('[chat] heart');
state.task.forEach((item) => {
dispatch('send', item);
});
},
/**
* 执行任务
*/
invoke: ({ commit, dispatch }, data) => {
data.traceId = UUID();
commit('addTask', data);
dispatch('send', data);
},
/**
* 撤销任务
*/
revoke: ({ state, commit }, data) => {
console.info(state.task.length);
commit(
'setTask',
state.task.filter((item) => item.traceType !== data)
);
console.info(state.task.length);
},
/**
* 发送数据
*/
send: ({ state }, data) => {
if (window.WebSocket) {
if (state.socket?.readyState === WebSocket.OPEN) {
data.traceId = data.traceId || UUID();
state.socket.send(JSON.stringify(data));
if (data.traceType !== 26) {
console.info('[chat] send', data);
}
}
}
},
/**
* 接收数据
*/
receive: ({ state, commit, dispatch }, data) => {
data = JSON.parse(data);
if (data.traceType !== 0) {
let index = state.task.findIndex((item) => item.traceId === data.traceId);
if (index !== -1) {
console.info('[chat] data', data);
commit('delTask', index);
dispatch('handle', data);
} else if (data.traceType === 25) {
console.info('[chat] msg', data);
dispatch('handle', data);
} else {
console.info('[chat] deprecated', data);
}
}
},
/**
* 处理数据
*/
handle: ({ state, commit, dispatch }, { code, traceType, content }) => {
if (code === 200) {
switch (traceType) {
case 25: // 收到消息
if (content.sessionId === state.currentSession) {
commit('setMessageList', [...state.messageList, content]);
dispatch('submitRead');
} else {
dispatch('querySession');
}
break;
case 32: // 发送消息
commit('setMessageList', [...state.messageList, content]);
break;
case 27: // 会话列表
commit('setSessionData', content);
break;
case 28: // 消息列表
commit('setMessageList', [...content, ...state.messageList]);
break;
case 29: // 客服列表
commit('setCustomerServiceList', content);
break;
case 31: // 已读消息
dispatch('querySession');
break;
default:
break;
}
}
},
/**
* 查询会话列表
*/
querySession: ({ dispatch }) => {
dispatch('invoke', {
traceType: 27,
content: { storeId: 1 },
});
},
/**
* 查询会话消息列表
*/
querySessionMessage: ({ state, dispatch }, data) => {
dispatch('invoke', {
traceType: 28,
content: { sessionId: state.currentSession, size: 10, topMessageId: null, ...data },
});
},
/**
* 查询可转移客服列表
*/
queryCustomerService: ({ dispatch }) => {
dispatch('invoke', {
traceType: 29,
content: { storeId: 1 },
});
},
/**
* 提交转移会话
*/
submitTransferSession: ({ dispatch }, data) => {
dispatch('invoke', {
traceType: 30,
content: { storeId: 1, ...data },
});
},
/**
* 提交已读消息
*/
submitRead: ({ state, dispatch }) => {
dispatch('invoke', {
traceType: 31,
content: { sessionId: state.currentSession },
});
},
/**
* 提交发送消息
*/
submitMessage: ({ state, dispatch }, payload) => {
dispatch('invoke', {
traceType: 32,
content: { payload, toSessionId: state.currentSession, type: 1 },
});
},
/**
* 提交发送图片
*/
submitImage: ({ state, dispatch }, payload) => {
dispatch('invoke', {
traceType: 32,
content: { payload, toSessionId: state.currentSession, type: 3 },
});
},
/**
* 提交发送视频
*/
submitVideo: ({ state, dispatch }, payload) => {
dispatch('invoke', {
traceType: 32,
content: { payload, toSessionId: state.currentSession, type: 4 },
});
},
};
export default {
state,
getters,
mutations,
actions,
};

@ -1,18 +1,18 @@
import { login } from '@/api/chat'; /*
import config from '@/configs'; * @Author: ch
import { ElMessage } from '@/plugins/element-plus'; * @Date: 2022-06-07 15:41:05
import { UUID } from '@/utils/chat'; * @LastEditors: ch
* @LastEditTime: 2022-06-14 15:26:44
* @Description: file content
*/
import * as api from '@/api/chat';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
const state = () => ({ const state = {
socket: null, curCustomerService: {},
heart: null, sessionData: [],
queue: [],
task: [],
sessionData: {},
currentSession: null,
messageList: [],
messageType: { 1: 'text', 2: 'audio', 3: 'image', 4: 'video', 5: 'revoke', 6: 'custom', 7: 'notify' },
customerServiceList: [], customerServiceList: [],
messageType: { 1: 'text', 2: 'audio', 3: 'image', 4: 'video', 5: 'revoke', 6: 'custom', 7: 'notify' },
opts: { opts: {
customerServiceType: [ customerServiceType: [
{ {
@ -29,7 +29,8 @@ const state = () => ({
}, },
], ],
}, },
}); };
const getters = { const getters = {
parseTime: () => { parseTime: () => {
return (timestamp) => { return (timestamp) => {
@ -47,8 +48,15 @@ const getters = {
} else if (type === 5) { } else if (type === 5) {
payload = '[撤回消息]'; payload = '[撤回消息]';
} else if (type === 6 || type === 1) { } else if (type === 6 || type === 1) {
if (payload.customType === 'transferWaiterSession') {
let str = `${payload.fromNickname}将会话转移给了${payload.toNickname}`;
if (payload.reason) {
str += `,并留言:${payload.reason}`;
}
payload = str;
} else {
try { try {
payload = JSON.parse(payload); // payload = JSON.parse(payload.value);
if ('text' in payload) { if ('text' in payload) {
payload = payload.text; payload = payload.text;
} else if ('linkJump' in payload) { } else if ('linkJump' in payload) {
@ -63,8 +71,9 @@ const getters = {
} catch (e) { } catch (e) {
payload = '[解析异常]'; payload = '[解析异常]';
} }
}
} else if (type === 7) { } else if (type === 7) {
payload = '[撤回消息]'; payload = payload.text;
} else { } else {
payload = '[未知类型]'; payload = '[未知类型]';
} }
@ -74,7 +83,7 @@ const getters = {
parseImage: () => { parseImage: () => {
return (payload) => { return (payload) => {
try { try {
payload = JSON.parse(payload); // payload = JSON.parse(payload);
if ('url' in payload) { if ('url' in payload) {
payload = payload.url; payload = payload.url;
} else { } else {
@ -89,7 +98,7 @@ const getters = {
parseVideo: () => { parseVideo: () => {
return (payload) => { return (payload) => {
try { try {
payload = JSON.parse(payload); // payload = JSON.parse(payload);
if ('url' in payload) { if ('url' in payload) {
payload = payload.url; payload = payload.url;
} else { } else {
@ -103,8 +112,11 @@ const getters = {
}, },
parseContent: () => { parseContent: () => {
return (payload) => { return (payload) => {
if (payload.customType === 'transferWaiterSession') {
payload.type = 'transferWaiterSession';
} else {
try { try {
payload = JSON.parse(payload); // payload = JSON.parse(payload);
if ('linkJump' in payload) { if ('linkJump' in payload) {
payload.type = 'link'; payload.type = 'link';
} else if ('orderNo' in payload) { } else if ('orderNo' in payload) {
@ -117,228 +129,51 @@ const getters = {
} catch (e) { } catch (e) {
payload = '[解析异常]'; payload = '[解析异常]';
} }
}
return payload; return payload;
}; };
}, },
}; };
const mutations = { const mutations = {
setSocket: (state, data) => (state.socket = data), SET_CUR_SERVICE(state, data) {
setHeart: (state, data) => (state.heart = data), state.curCustomerService = data || {};
setTask: (state, data) => (state.task = data),
addTask: (state, data) => state.task.push(data),
delTask: (state, data) => state.task.splice(data, 1),
setCurrentSession: (state, data) => (state.currentSession = data),
setSessionData: (state, data) => (state.sessionData = data),
setMessageList: (state, data) => (state.messageList = data),
setCustomerServiceList: (state, data) => (state.customerServiceList = data),
};
const actions = {
/**
* 创建连接
*/
connect: async ({ state, commit, dispatch }) => {
let { ticket } = await login({ storeId: 1 });
return new Promise((resolve, reject) => {
if (window.WebSocket) {
const socket = new WebSocket(`${config.socketURL}?client=${ticket}&type=2`);
socket.onmessage = ({ data }) => {
dispatch('receive', data);
};
socket.onopen = () => {
commit(
'setHeart',
setInterval(() => {
dispatch('heart');
}, 3000)
);
console.info('[chat] open');
resolve(socket);
};
socket.onclose = () => {
clearInterval(state.heart);
console.info('[chat] close');
};
socket.onerror = (e) => {
clearInterval(state.heart);
console.info('[chat] error', e);
reject(e);
};
commit('setSocket', socket);
} else {
ElMessage.error('当前浏览器不支持 WebSocket');
reject('not support websocket');
}
});
}, },
/** SET_SESSION_DATA(state, data) {
* 发送心跳任务监测 state.sessionData = data;
*/
heart: ({ state, dispatch }) => {
dispatch('send', {
traceType: 26,
content: { storeId: 1 },
});
// console.info('[chat] heart');
state.task.forEach((item) => {
dispatch('send', item);
});
}, },
/** SET_SERVICE_LIST(state, data) {
* 执行任务 state.customerServiceList = data;
*/
invoke: ({ commit, dispatch }, data) => {
data.traceId = UUID();
commit('addTask', data);
dispatch('send', data);
},
/**
* 撤销任务
*/
revoke: ({ state, commit }, data) => {
console.info(state.task.length);
commit(
'setTask',
state.task.filter((item) => item.traceType !== data)
);
console.info(state.task.length);
},
/**
* 发送数据
*/
send: ({ state }, data) => {
if (window.WebSocket) {
if (state.socket?.readyState === WebSocket.OPEN) {
data.traceId = data.traceId || UUID();
state.socket.send(JSON.stringify(data));
if (data.traceType !== 26) {
console.info('[chat] send', data);
}
}
}
}, },
/** };
* 接收数据 const actions = {
*/ queryCurCustomerService: ({ commit }) => {
receive: ({ state, commit, dispatch }, data) => { api.getCustomerService().then((res) => {
data = JSON.parse(data); commit('SET_CUR_SERVICE', res);
if (data.traceType !== 0) {
let index = state.task.findIndex((item) => item.traceId === data.traceId);
if (index !== -1) {
console.info('[chat] data', data);
commit('delTask', index);
dispatch('handle', data);
} else if (data.traceType === 25) {
console.info('[chat] msg', data);
dispatch('handle', data);
} else {
console.info('[chat] deprecated', data);
}
}
},
/**
* 处理数据
*/
handle: ({ state, commit, dispatch }, { code, traceType, content }) => {
if (code === 200) {
switch (traceType) {
case 25: // 收到消息
if (content.sessionId === state.currentSession) {
commit('setMessageList', [...state.messageList, content]);
dispatch('submitRead');
} else {
dispatch('querySession');
}
break;
case 32: // 发送消息
commit('setMessageList', [...state.messageList, content]);
break;
case 27: // 会话列表
commit('setSessionData', content);
break;
case 28: // 消息列表
commit('setMessageList', [...content, ...state.messageList]);
break;
case 29: // 客服列表
commit('setCustomerServiceList', content);
break;
case 31: // 已读消息
dispatch('querySession');
break;
default:
break;
}
}
},
/**
* 查询会话列表
*/
querySession: ({ dispatch }) => {
dispatch('invoke', {
traceType: 27,
content: { storeId: 1 },
});
},
/**
* 查询会话消息列表
*/
querySessionMessage: ({ state, dispatch }, data) => {
dispatch('invoke', {
traceType: 28,
content: { sessionId: state.currentSession, size: 10, topMessageId: null, ...data },
}); });
}, },
/** /**
* 查询可转移客服列表 * 查询可转移客服列表
*/ */
queryCustomerService: ({ dispatch }) => { queryCustomerService: ({ commit }) => {
dispatch('invoke', { api.customerServiceList({
traceType: 29, length: 100,
content: { storeId: 1 }, pageIndex: 1,
}).then((res) => {
commit('SET_SERVICE_LIST', res.records);
}); });
}, },
/** /**
* 提交转移会话 * 提交转移会话
*/ */
submitTransferSession: ({ dispatch }, data) => { submitTransferSession: ({}, data) => {
dispatch('invoke', { return api
traceType: 30, .transferCustomerService({
content: { storeId: 1, ...data }, storeId: 1,
}); ...data,
}, })
/** .then((res) => {
* 提交已读消息 console.log(res, 'resresres');
*/
submitRead: ({ state, dispatch }) => {
dispatch('invoke', {
traceType: 31,
content: { sessionId: state.currentSession },
});
},
/**
* 提交发送消息
*/
submitMessage: ({ state, dispatch }, payload) => {
dispatch('invoke', {
traceType: 32,
content: { payload, toSessionId: state.currentSession, type: 1 },
});
},
/**
* 提交发送图片
*/
submitImage: ({ state, dispatch }, payload) => {
dispatch('invoke', {
traceType: 32,
content: { payload, toSessionId: state.currentSession, type: 3 },
});
},
/**
* 提交发送视频
*/
submitVideo: ({ state, dispatch }, payload) => {
dispatch('invoke', {
traceType: 32,
content: { payload, toSessionId: state.currentSession, type: 4 },
}); });
}, },
}; };

@ -0,0 +1,55 @@
/*
* @Author: ch
* @Date: 2022-06-07 15:52:37
* @LastEditors: ch
* @LastEditTime: 2022-06-13 17:32:19
* @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 = (waiterId) => {
return new Promise((reslove, reject) => {
api.getCustomeServiceTicket().then(async (res) => {
console.log(res);
const par = FormatJsonSearch({
client: res.client,
ticket: res.ticket,
user: waiterId,
connect: 2,
});
const { error, result } = await ToAsyncAwait(
Im.init({
url: `${config.socketURL}${par}`,
})
);
if (error) {
reject(error);
} else {
reslove(result);
}
});
});
};
Im.interceptors.dataChangeAfter = () => {
$store.commit('chat/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('chat/SET_SESSION_DATA', []);
// $store.commit('SET_IM_MSG_COUNT', 0);
};
export { Im, ImInit };

@ -0,0 +1,483 @@
/*
* @Author: ch
* @Date: 2022-05-18 14:54:47
* @LastEditors: ch
* @LastEditTime: 2022-06-14 17:20:50
* @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();
};
let isUni = false;
try {
isUni = uni;
} catch (e) {
isUni = false;
}
if (isUni) {
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);
let isUni = false;
try {
isUni = uni;
} catch (e) {
isUni = false;
}
if (isUni) {
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;
}
console.log('[im] 主动接收的消息', data);
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++;
} else {
this.setRead({
content: {
sessionId: this.curSessionId,
},
});
}
newData = historyData;
} else {
// 会话列表不存在,则创建一个会话
newData = [
...historyData,
{
fromAvatar: ctx.session.fromAvatar,
fromId: ctx.session.fromId,
fromNickname: ctx.session.fromNickname,
id: ctx.sessionId,
lastMessage: ctx,
messageList: [ctx],
updateTimeStamp: ctx.createTimeStamp,
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;
// let newData = [];
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) {
item.messageList = [];
}
// let historyData = this.sessionData;
// let hisIndex = historyData.findIndex((i) => i.id === item.id);
// if (hisIndex >= 0) {
// historyData[hisIndex].lastMessage = item.lastMessage;
// historyData[hisIndex].unreadCount++;
// newData.push(historyData[hisIndex]);
// } else {
// item.messageList = [];
// newData = [...newData, item];
// }
});
this.setSessionData(content.sessionVOS);
return Promise.resolve(result);
}
/**
* 获取会话的历史消息记录
* @param {*} params
*/
async getHistoryMsg() {
const curSessionIdx = this.sessionData.findIndex((i) => i.id === this.curSessionId);
const curSession = this.sessionData[curSessionIdx];
console.log(curSession, this.curSessionId, 'this.curSessionId');
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',
};
if (typeof msgCtx.payload === 'string') {
msgCtx.payload = JSON.parse(msgCtx.payload);
}
// 点发送,立即把消息加入消息列表,标记为发送中状态
curSession.lastMessage = msgCtx;
curSession.messageList.push(msgCtx);
this.setSessionData(this.sessionData);
// 超过时间未返回视为发送失败
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];
curSession.lastMessage = msgCtx;
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);
}
close() {
this.socket.close();
this.socket = null;
this.isOpen = false;
this.setSessionData([]);
}
}
export default 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>

@ -49,7 +49,7 @@
</li> </li>
</el-scrollbar> </el-scrollbar>
</div> </div>
<div v-if="currentSession" class="content"> <div v-if="currentSession.id" class="content">
<div class="content-header"> <div class="content-header">
<div class="content-header-left"> <div class="content-header-left">
<div class="name" :class="{ [`sex-` + currentSession?.fromSex]: true }"> <div class="name" :class="{ [`sex-` + currentSession?.fromSex]: true }">
@ -137,14 +137,30 @@
<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/queryCurCustomerService');
store.dispatch('chat/querySession'); const socketInit = () => {
let waiterId = store.state.chat.curCustomerService.waiterId;
if (!waiterId) {
setTimeout(() => {
socketInit();
}, 1000);
return false;
}
ImInit(waiterId).then(() => {
Im.getSessionList();
});
};
onMounted(() => {
socketInit();
});
const opts = computed(() => store.state.chat.opts); const opts = computed(() => store.state.chat.opts);
// //
@ -158,47 +174,67 @@
}); });
// //
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.chat.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'); //
store.dispatch('chat/submitRead'); Im.setRead({
content: {
sessionId: id,
},
});
setTimeout(() => {
if (!sessionMessageList.value.length) {
Im.getHistoryMsg();
}
}, 100);
}; };
// //
const sessionMessageList = computed(() => store.state.chat.messageList); const sessionMessageList = ref([]);
watch(currentSession, () => {
sessionMessageList.value = currentSession.value.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
? refsMessageList.value.resize$.scrollHeight - refsMessageList.value.resize$.scrollTop ? refsMessageList.value.resize$.scrollHeight - refsMessageList.value.resize$.scrollTop
: 0; : 0;
nextTick(() => { nextTick(() => {
if (!old?.length || value.indexOf(old[0]) === 0) { // if (!old?.length || value.indexOf(old[0]) === 0) {
refsMessageList.value.setScrollTop(refsMessageList.value.resize$.scrollHeight); refsMessageList.value && refsMessageList.value.setScrollTop(refsMessageList.value.resize$.scrollHeight);
} else { // } else {
refsMessageList.value.setScrollTop(refsMessageList.value.resize$.scrollHeight - offset); // refsMessageList.value.setScrollTop(refsMessageList.value.resize$.scrollHeight - offset);
} // }
}); });
}); });
const handleLoadMore = () => { const handleLoadMore = () => {
store.dispatch('chat/querySessionMessage', { topMessageId: unref(sessionMessageList)[0].id }); Im.getHistoryMsg();
}; };
const handleSendMessage = (e) => { const handleSendMessage = (e) => {
const curService = store.state.chat.curCustomerService;
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: curService.waiterId,
fromAvatar: curService.waiterAvatar,
fromNickname: curService.waiterNickname,
content: {
toSessionId: currentSessionId.value,
payload: { text: state.message },
type: 1,
},
});
state.message = ''; state.message = '';
} else { } else {
proxy.$message.warning('发送消息不能为空'); proxy.$message.warning('发送消息不能为空');
@ -218,22 +254,22 @@
columns: [ columns: [
{ {
label: '账号', label: '账号',
prop: 'account', prop: 'waiterId',
width: 160, width: 160,
}, },
{ {
label: '客户昵称', label: '客户昵称',
prop: 'nickname', prop: 'waiterNickname',
minWidth: 160, minWidth: 160,
}, },
{ // {
label: '类型', // label: '',
prop: 'type', // prop: 'type',
width: 160, // width: 160,
slots: { // slots: {
default: ({ row }) => proxy.$dict(unref(opts).customerServiceType, row.type), // default: ({ row }) => proxy.$dict(unref(opts).customerServiceType, row.type),
}, // },
}, // },
{ {
label: '操作', label: '操作',
width: 100, width: 100,
@ -258,10 +294,16 @@
confirmButtonText: '确定', confirmButtonText: '确定',
}); });
if (res.action === 'confirm') { if (res.action === 'confirm') {
store.dispatch('chat/submitTransferSession', { store
toWaiterId: row.userId, .dispatch('chat/submitTransferSession', {
toWaiterId: row.waiterId,
sessionId: unref(currentSessionId), sessionId: unref(currentSessionId),
reason: res.value, reason: res.value,
})
.then((res) => {
currentSessionId.value = null;
Im.setCurSessionId(null);
Im.getSessionList();
}); });
transferVisible.value = false; transferVisible.value = false;
} }
@ -285,9 +327,23 @@
}; };
const handleSendImage = async (e) => { const handleSendImage = async (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (!file.type.includes('image')) {
proxy.$message.warning('只能发送图片哦~');
return false;
}
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 }); const curService = store.state.chat.curCustomerService;
Im.sendMsg({
fromId: curService.waiterId,
fromAvatar: curService.waiterAvatar,
fromNickname: curService.waiterNickname,
content: {
toSessionId: currentSessionId.value,
payload: { url },
type: 3,
},
});
}; };
// //
@ -297,9 +353,25 @@
}; };
const handleSendVideo = async (e) => { const handleSendVideo = async (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (!file.type.includes('video')) {
proxy.$message.warning('只能发送视频哦~');
return false;
}
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 });
const curService = store.state.chat.curCustomerService;
Im.sendMsg({
fromId: curService.waiterId,
fromAvatar: curService.waiterAvatar,
fromNickname: curService.waiterNickname,
content: {
toSessionId: currentSessionId.value,
payload: { url },
type: 4,
},
});
}; };
</script> </script>
@ -323,6 +395,7 @@
display: flex; display: flex;
border: 1px solid #ebeef5; border: 1px solid #ebeef5;
.aside { .aside {
width: 240px;
border-right: 1px solid #ebeef5; border-right: 1px solid #ebeef5;
.aside-header { .aside-header {
height: 60px; height: 60px;

@ -3,15 +3,30 @@
class="message-item" class="message-item"
:class="{ :class="{
[`--${messageType[props.message.type]}`]: true, [`--${messageType[props.message.type]}`]: true,
'--self': props.message.fromId !== props.session.fromId, '--notify': messageType[props.message.type] === 'custom' && content.type === 'transferWaiterSession',
'--self': props.message.fromId == $store.state.chat.curCustomerService.waiterId,
}" }"
> >
<!-- --> <!-- -->
<div v-if="!['revoke', 'notify'].includes(messageType[props.message.type])" class="avatar"> <template v-if="['notify', 'revoke'].includes(messageType[props.message.type])">
<div class="content shadow">
{{ store.getters['chat/parseText'](props.message) }}
</div>
</template>
<template v-else-if="messageType[props.message.type] === 'custom' && content.type === 'transferWaiterSession'">
<div class="message-body">
<div v-if="content.type === 'transferWaiterSession'" class="content shadow">
{{ store.getters['chat/parseText'](props.message) }}
</div>
</div>
</template>
<template v-else>
<div class="avatar">
<el-avatar :src="props.message.fromAvatar" /> <el-avatar :src="props.message.fromAvatar" />
</div> </div>
<div class="message-body"> <div class="message-body">
<div v-if="!['revoke', 'notify'].includes(messageType[props.message.type])" class="name"> <div class="name">
{{ props.message.fromNickname }} {{ props.message.fromNickname }}
</div> </div>
<template v-if="messageType[props.message.type] === 'custom'"> <template v-if="messageType[props.message.type] === 'custom'">
@ -22,10 +37,10 @@
<template #header> <template #header>
<div class="flex"> <div class="flex">
<div class="left">商品编号{{ content.id }}</div> <div class="left">商品编号{{ content.id }}</div>
<el-button type="text">复制</el-button> <el-button type="text" @click="$copy(content.id)"></el-button>
</div> </div>
</template> </template>
<div class="flex"> <div class="flex product" @click="handleProductDetail(content.id)">
<el-image :alt="content.name" height="64px" :src="content.productImageUrl" width="64px" /> <el-image :alt="content.name" height="64px" :src="content.productImageUrl" width="64px" />
<div class="right"> <div class="right">
<div class="name">{{ content.name }}</div> <div class="name">{{ content.name }}</div>
@ -91,9 +106,10 @@
{{ store.getters['chat/parseText'](props.message) }} {{ store.getters['chat/parseText'](props.message) }}
</div> </div>
</div> </div>
<div v-if="!['revoke', 'notify'].includes(messageType[props.message.type])" class="time"> <div class="time">
{{ store.getters['chat/parseTime'](props.message.createTimeStamp) }} {{ store.getters['chat/parseTime'](props.message.createTimeStamp) }}
</div> </div>
</template>
</div> </div>
</template> </template>
@ -111,7 +127,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['chat/parseContent']({ ...props.message.payload }));
const handleProduct = (id) => { const handleProduct = (id) => {
router.push({ router.push({
name: 'UpdateProduct', name: 'UpdateProduct',
@ -121,6 +137,9 @@
}, },
}); });
}; };
const handleProductDetail = (id) => {
window.open(`${import.meta.env.VITE_BROWSER_URL}/goods/detail/${id}`, '_blank');
};
const handleOrder = (id) => { const handleOrder = (id) => {
router.push({ router.push({
name: 'OrderDetail', name: 'OrderDetail',
@ -148,6 +167,9 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.product {
cursor: pointer;
}
.avatar { .avatar {
margin: 0 @layout-space; margin: 0 @layout-space;
} }
@ -228,5 +250,10 @@
} }
} }
} }
&.--custom {
.message-body {
width: 300px;
}
}
} }
</style> </style>

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