解决uui 冲突

feature/pay-0615-ch
ch 3 years ago
commit f6fb143a69

@ -0,0 +1,4 @@
VUE_APP_BASE_URL = https://k8s-horse-gateway.mashibing.cn
VUE_APP_STATIC_URL = https://k8s-shop-app.mashibing.cn
#VUE_APP_IM_URL = ws://192.168.10.94:8090
VUE_APP_IM_URL = wss://k8s-horse-gateway.mashibing.cn

@ -0,0 +1,3 @@
VUE_APP_BASE_URL = https://you-gateway.mashibing.com
VUE_APP_STATIC_URL = https://you-gateway.mashibing.com
VUE_APP_IM_URL = wss://you-gateway.mashibing.com

@ -0,0 +1,3 @@
VUE_APP_BASE_URL = https://you-gateway.mashibing.com
VUE_APP_STATIC_URL = https://you-gateway.mashibing.com
VUE_APP_IM_URL = wss://you-gateway.mashibing.com

@ -0,0 +1,3 @@
VUE_APP_BASE_URL = https://k8s-horse-gateway.mashibing.com
VUE_APP_STATIC_URL = https://k8s-shop-app.mashibing.com
VUE_APP_IM_URL = wss://k8s-horse-gateway.mashibing.cn

@ -2,7 +2,7 @@
* @Author: ch * @Author: ch
* @Date: 2022-05-05 14:40:00 * @Date: 2022-05-05 14:40:00
* @LastEditors: ch * @LastEditors: ch
* @LastEditTime: 2022-05-26 19:04:26 * @LastEditTime: 2022-06-09 11:25:44
* @Description: 根据git分支生成对应环境的环境变量 * @Description: 根据git分支生成对应环境的环境变量
* 开发时如果环境变量换了可以不用重启服务直接运行node env.config.js即可 * 开发时如果环境变量换了可以不用重启服务直接运行node env.config.js即可
*/ */
@ -13,6 +13,7 @@ const envConfig = {
dev : { dev : {
baseUrl: 'https://k8s-horse-gateway.mashibing.cn', baseUrl: 'https://k8s-horse-gateway.mashibing.cn',
staticUrl : 'https://k8s-shop-app.mashibing.cn', staticUrl : 'https://k8s-shop-app.mashibing.cn',
// imUrl : 'ws://192.168.10.94:8090'
imUrl : 'wss://k8s-horse-gateway.mashibing.cn' imUrl : 'wss://k8s-horse-gateway.mashibing.cn'
}, },
test : { test : {

@ -1,48 +1,16 @@
// /* /*
// * @Author: ch * @Author: ch
// * @Date: 2022-05-18 15:21:10 * @Date: 2022-05-27 17:44:36
// * @LastEditors: ch * @LastEditors: ch
// * @LastEditTime: 2022-05-18 17:24:37 * @LastEditTime: 2022-06-09 11:34:36
// * @Description: file content * @Description: file content
// */ */
// import { MsbSkt } from "../utils/webSkt"; import {ToAsyncAwait, MsbRequestTk} from '@/common/utils';
// import { CreateUUID } from '@/common/utils'; const BASE_URL = '/mall/im';
// /** /**
// * 系统消息心跳 * 获取soket登录秘钥
// */ */
// export const ApiSktSysHeart = () => MsbSkt.send({ export const ApiGetSoketTicket = () => ToAsyncAwait(MsbRequestTk.get(`${BASE_URL}/ticket`, {
// traceId: CreateUUID(), ticketType: 'CONNECT_TICKET'
// traceType: '0', }));
// content: { text: "ping" }
// });
// /**
// * 获取系统通知会话
// * @param {*} content
// */
// export const ApiSktSysGetSession = (content) => MsbSkt.send({
// traceId: CreateUUID(),
// traceType: '1',
// content
// });
// /**
// * 获取系统消息历史消息
// * @param {*} content
// */
// export const ApiSktSysGetHistory = (content) => MsbSkt.send({
// traceId: CreateUUID(),
// traceType: '2',
// content
// });
// /**
// * 系统消息已读
// * @param {*} content
// */
// export const ApiSktSysGetHistory = (content) => MsbSkt.send({
// traceId: CreateUUID(),
// traceType: '6',
// content
// });

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

File diff suppressed because one or more lines are too long

@ -2,7 +2,7 @@
* @Author: ch * @Author: ch
* @Date: 2022-03-22 18:28:52 * @Date: 2022-03-22 18:28:52
* @LastEditors: ch * @LastEditors: ch
* @LastEditTime: 2022-06-10 16:51:08 * @LastEditTime: 2022-06-14 17:33:42
* @Description: file content * @Description: file content
*/ */
import Vue from 'vue' import Vue from 'vue'

@ -2,13 +2,14 @@
* @Author: ch * @Author: ch
* @Date: 2022-05-20 11:00:07 * @Date: 2022-05-20 11:00:07
* @LastEditors: ch * @LastEditors: ch
* @LastEditTime: 2022-05-23 14:54:50 * @LastEditTime: 2022-06-13 10:11:33
* @Description: file content * @Description: file content
*/ */
import MsbIm from '@/common/plugins/msbIm' ; import MsbIm from '@/common/plugins/msbIm' ;
import { ToAsyncAwait } from '@/common/utils'; import { ToAsyncAwait, FormatJsonSearch } from '@/common/utils';
import { ApiGetCurrentUser } from '@/common/api/account'; import { ApiGetCurrentUser } from '@/common/api/account';
import { ApiGetSoketTicket } from '@/common/api/im';
import $store from '@/common/store'; import $store from '@/common/store';
import ENV from '@/common/config/env'; import ENV from '@/common/config/env';
@ -16,13 +17,21 @@ const Im = new MsbIm({
reconnect: true, reconnect: true,
}); });
const ImInit = async () => { const ImInit = async () => {
const { error } = await ApiGetCurrentUser(); const { error, result } = await ApiGetSoketTicket();
if (error) { if (error) {
return false; return false;
} }
const par = FormatJsonSearch({
client: result.client,
ticket: result.ticket,
// 1普通用户 2客服链接
connect: 1,
user: $store.state.userInfo.id,
nickname: $store.state.userInfo.nickname,
avatar : $store.state.userInfo.avatar
})
await ToAsyncAwait(Im.init({ await ToAsyncAwait(Im.init({
// url: `wss://k8s-horse-gateway.mashibing.cn/ws?client=${$store.state.token}&type=1` url: `${ENV.imUrl}/ws${par}`
url: `${ENV.imUrl}/ws?client=${$store.state.token}&type=1`
})) }))
}; };
@ -32,11 +41,10 @@ Im.interceptors.dataChangeAfter = () => {
Im.sessionData.forEach(i => { Im.sessionData.forEach(i => {
msgCount += i.unreadCount; msgCount += i.unreadCount;
}) })
console.log(Im.sessionData)
$store.commit('SET_IM_MSG_COUNT', msgCount); $store.commit('SET_IM_MSG_COUNT', msgCount);
} }
Im.interceptors.onLogout = () => { Im.interceptors.onClose = () => {
Im.setSessionData([]); Im.setSessionData([]);
Im.setCurSessionId(null); Im.setCurSessionId(null);
$store.commit('SET_IM_DATA', []); $store.commit('SET_IM_DATA', []);

@ -2,7 +2,7 @@
* @Author: ch * @Author: ch
* @Date: 2022-03-17 17:42:32 * @Date: 2022-03-17 17:42:32
* @LastEditors: ch * @LastEditors: ch
* @LastEditTime: 2022-06-10 16:52:26 * @LastEditTime: 2022-06-14 17:33:14
* @Description: 项目接口请求统一处理器返回一个需要token和不需要token的请求封装方法 * @Description: 项目接口请求统一处理器返回一个需要token和不需要token的请求封装方法
*/ */
@ -82,6 +82,7 @@ const clearRepeat = (option) =>{
// 不需要token的接口封装 // 不需要token的接口封装
const MsbRequest = new MsbUniRequest(); const MsbRequest = new MsbUniRequest();
console.log(process.env,'process.envprocess.envprocess.env');
MsbRequest.baseUrl = ENV.baseUrl; MsbRequest.baseUrl = ENV.baseUrl;
MsbRequest.use('request', (option) => { MsbRequest.use('request', (option) => {

@ -2,7 +2,7 @@
* @Author: ch * @Author: ch
* @Date: 2022-03-17 19:15:10 * @Date: 2022-03-17 19:15:10
* @LastEditors: ch * @LastEditors: ch
* @LastEditTime: 2022-05-31 17:36:08 * @LastEditTime: 2022-06-02 14:55:45
* @Description: 一些无法归类的公共方法容器 * @Description: 一些无法归类的公共方法容器
*/ */
@ -11,7 +11,8 @@ import {
isPhone as IsPhone, isPhone as IsPhone,
formatDate as FormatDate, formatDate as FormatDate,
creatUuid as CreateUUID, creatUuid as CreateUUID,
formatSearchJson as FormatSearchJson formatSearchJson as FormatSearchJson,
formatJsonSearch as FormatJsonSearch
} from "js-util-all"; } from "js-util-all";
import ENV from '@/common/config/env'; import ENV from '@/common/config/env';
@ -79,6 +80,7 @@ export {
// 时间格式化 // 时间格式化
FormatDate, FormatDate,
FormatSearchJson, FormatSearchJson,
FormatJsonSearch,
CreateUUID, CreateUUID,
// 防抖函数 // 防抖函数
Debounce, Debounce,

@ -2,7 +2,7 @@
* @Author: ch * @Author: ch
* @Date: 2021-07-26 23:22:16 * @Date: 2021-07-26 23:22:16
* @LastEditors: ch * @LastEditors: ch
* @LastEditTime: 2022-05-23 20:55:47 * @LastEditTime: 2022-06-13 10:15:03
* @Description: file content * @Description: file content
*/ */
import Vue from 'vue'; import Vue from 'vue';
@ -16,14 +16,22 @@ import {ApiGetOpenId, ApiGetAuthUrl} from '@/common/api/wx';
import {Im, ImInit} from '@/common/utils'; import {Im, ImInit} from '@/common/utils';
import { ApiSktSysGetSession, ApiSktSysHeart } from './common/api/im'; import { ApiSktSysGetSession, ApiSktSysHeart } from './common/api/im';
if (store.state.token) { const socketInit = () => {
if (!store.state.userInfo.id) {
setTimeout(() => {
socketInit();
},10000)
return false;
}
// 初始化IM // 初始化IM
ImInit().then(() => { ImInit().then(() => {
// 获取到会话列表 // 获取到会话列表
Im.getSessionList({ Im.getSessionList();
content: { sysId : 1 }
});
}); });
}
if (store.state.token) {
socketInit();
} }

@ -1,75 +0,0 @@
{
"name": "",
"appid": "",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"app-plus": { /* 5+App */
"usingComponents": true,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules": { /* */
},
"distribute": { /* */
"android": { /* android */
"permissions": ["<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios": { /* ios */
},
"sdkConfigs": { /* SDK */
}
}
},
"quickapp": { /* */
},
"mp-weixin": { /* */
"appid": "",
"setting": {
"urlCheck": false
},
"usingComponents": true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"mp-qq" : {
"usingComponents" : true
}
}

@ -2,7 +2,7 @@
* @Author: ch * @Author: ch
* @Date: 2022-03-26 14:32:03 * @Date: 2022-03-26 14:32:03
* @LastEditors: ch * @LastEditors: ch
* @LastEditTime: 2022-05-30 17:49:28 * @LastEditTime: 2022-06-14 17:13:17
* @Description: file content * @Description: file content
--> -->
<template> <template>
@ -11,6 +11,7 @@
<view class="send" :key="item.id" v-if="item.fromId == $store.state.userInfo.id"> <view class="send" :key="item.id" v-if="item.fromId == $store.state.userInfo.id">
<text class="send--status" v-if="item.sendStatus === 'loading'"></text> <text class="send--status" v-if="item.sendStatus === 'loading'"></text>
<text class="send--status send--status__fail" v-if="item.sendStatus === 'fail'" @click="resend(item)"></text> <text class="send--status send--status__fail" v-if="item.sendStatus === 'fail'" @click="resend(item)"></text>
<template v-if="item.type == MSG_TYPE.CUSTOM"> <template v-if="item.type == MSG_TYPE.CUSTOM">
<GoodsInfo class="send--box" position="msg" v-if="item.payload.id" :goodsInfo="item.payload"/> <GoodsInfo class="send--box" position="msg" v-if="item.payload.id" :goodsInfo="item.payload"/>
<OrderInfo class="send--box" position="msg" v-if="item.payload.orderId" :orderInfo="item.payload"/> <OrderInfo class="send--box" position="msg" v-if="item.payload.orderId" :orderInfo="item.payload"/>
@ -26,6 +27,9 @@
<view class="tips" :key="item.id" v-else-if="item.type == MSG_TYPE.TIP"> <view class="tips" :key="item.id" v-else-if="item.type == MSG_TYPE.TIP">
<view class="tips--box">{{item.payload.text}}</view> <view class="tips--box">{{item.payload.text}}</view>
</view> </view>
<view class="tips" :key="item.id" v-else-if="item.type === MSG_TYPE.CUSTOM && item.payload.customType == 'transferWaiterSession'">
<view class="tips--box">现在由客服{{item.payload.toNickname}}为您服务</view>
</view>
<view class="receive" :key="item.id" v-else> <view class="receive" :key="item.id" v-else>
<image class="avatar" :src="item.fromAvatar || require('@/static/message/xt.png')" mode="widthFix"/> <image class="avatar" :src="item.fromAvatar || require('@/static/message/xt.png')" mode="widthFix"/>
<view> <view>
@ -80,6 +84,9 @@ export default {
return this.curSessionData ? this.curSessionData.messageList : []; return this.curSessionData ? this.curSessionData.messageList : [];
} }
}, },
destroyed(){
Im.setCurSessionId(null);
},
watch:{ watch:{
msgData(){ msgData(){
this.$nextTick(()=>{ this.$nextTick(()=>{
@ -108,12 +115,7 @@ export default {
this.orderId = this.$Route.query.orderId; this.orderId = this.$Route.query.orderId;
this.sessionId = this.$Route.query.sessionId; this.sessionId = this.$Route.query.sessionId;
if(this.sessionId){ this.socketInit();
this.getHistoryMsg();
this.readMsg();
}else{
this.createSessionMain();
}
if(this.goodsId){ if(this.goodsId){
this.getGoodsInfo(); this.getGoodsInfo();
} }
@ -129,14 +131,35 @@ export default {
}, },
methods:{ methods:{
socketInit(){
if(!Im.isOpen){
setTimeout(()=>{
this.socketInit();
}, 100)
return false;
}
if(this.sessionId){
if(!this.msgData?.length){
this.getHistoryMsg();
}
this.readMsg();
}else{
this.createSessionMain();
}
},
/** /**
* 创建会话主体 * 创建会话主体
* 如果是从商品或订单进来需要创建会话 * 如果是从商品或订单进来需要创建会话
*/ */
async createSessionMain(){ async createSessionMain(){
if(!Im.isOpen){
setTimeout(()=>this.createSessionMain(),1000);
return false;
}
const {error, result} = await ToAsyncAwait(Im.createSession({ const {error, result} = await ToAsyncAwait(Im.createSession({
content : { content : {
storeId : 1 sessionType : 3
} }
})); }));
@ -144,23 +167,24 @@ export default {
uni.$u.toast(error.message); uni.$u.toast(error.message);
return false; return false;
} }
this.sessionId = result.content.id this.sessionId = result.content.id;
Im.setCurSessionId(this.sessionId);
this.getHistoryMsg(); this.getHistoryMsg();
this.readMsg(); // this.readMsg();
}, },
/** /**
* 获取历史消息 * 获取历史消息
*/ */
async getHistoryMsg(){ async getHistoryMsg(){
if(!this.curSessionData.id){
setTimeout(()=>{
this.getHistoryMsg();
}, 500)
return false;
}
this.loading = true; this.loading = true;
const lastMsg = this.msgData?.length ? this.msgData[0] : {}; const {error, result} = await ToAsyncAwait(Im.getHistoryMsg());
const {error, result} = await ToAsyncAwait(Im.getHistoryMsg({
content : {
sessionId : this.sessionId,
topMessageId : lastMsg.id || null
}
}));
if(error){ if(error){
uni.$u.toast(error.errMsg || error.message); uni.$u.toast(error.errMsg || error.message);
return false return false
@ -178,6 +202,7 @@ export default {
})); }));
if(error){ if(error){
uni.$u.toast(error.errMsg || error.message); uni.$u.toast(error.errMsg || error.message);
console.log(error);
return false return false
} }
@ -252,6 +277,7 @@ page{
color: #333; color: #333;
font-size: 32rpx; font-size: 32rpx;
line-height: 40rpx; line-height: 40rpx;
word-break: break-all;
} }
&__img{ &__img{
height: 140rpx; height: 140rpx;

@ -2,7 +2,7 @@
* @Author: ch * @Author: ch
* @Date: 2022-05-11 11:45:08 * @Date: 2022-05-11 11:45:08
* @LastEditors: ch * @LastEditors: ch
* @LastEditTime: 2022-05-24 22:54:29 * @LastEditTime: 2022-06-14 12:55:27
* @Description: file content * @Description: file content
--> -->
<template> <template>
@ -158,11 +158,11 @@ export default {
this.msgCtx += str; this.msgCtx += str;
}, },
sendGoods(){ sendGoods(){
this.send(this.simpleGoods, MSG_TYPE.CUSTOM); this.send({...this.simpleGoods, customType:'goodsInfo'}, MSG_TYPE.CUSTOM);
this.goodsShow = false; this.goodsShow = false;
}, },
sendOrder(){ sendOrder(){
this.send(this.simpleOrder, MSG_TYPE.CUSTOM); this.send({...this.simpleOrder, customType : 'orderInfo'}, MSG_TYPE.CUSTOM);
this.orderShow = false; this.orderShow = false;
}, },
uploadImg(val){ uploadImg(val){

@ -2,7 +2,7 @@
* @Author: ch * @Author: ch
* @Date: 2022-03-22 16:13:00 * @Date: 2022-03-22 16:13:00
* @LastEditors: ch * @LastEditors: ch
* @LastEditTime: 2022-05-27 15:32:18 * @LastEditTime: 2022-06-14 17:12:37
* @Description: file content * @Description: file content
--> -->
<template> <template>
@ -18,11 +18,14 @@
<text class="msgItem--text" v-if="item.lastMessage"> <text class="msgItem--text" v-if="item.lastMessage">
<template v-if="item.lastMessage.type == msgType.TXT">{{item.lastMessage.payload.content || item.lastMessage.payload.text}}</template> <template v-if="item.lastMessage.type == msgType.TXT">{{item.lastMessage.payload.content || item.lastMessage.payload.text}}</template>
<template v-if="item.lastMessage.type == msgType.CUSTOM"> <template v-if="item.lastMessage.type == msgType.CUSTOM">
<template v-if="item.lastMessage.payload.customType === 'orderAutoDelivery'"> <template v-if="['returnLogistics','orderAutoDelivery'].includes(item.lastMessage.payload.customType)">
{{item.lastMessage.payload.content}} {{item.lastMessage.payload.content}}
</template> </template>
<template v-else> <template v-if="item.lastMessage.payload.customType === 'app_push'">
[链接] {{item.lastMessage.payload.link ? '[链接]' : item.lastMessage.payload.content}}
</template>
<template v-if="item.lastMessage.payload.customType === 'transferWaiterSession'">
[客服转移]
</template> </template>
</template> </template>
<template v-if="item.lastMessage.type == msgType.IMG">[]</template> <template v-if="item.lastMessage.type == msgType.IMG">[]</template>
@ -40,7 +43,7 @@
<script> <script>
import BsEmpty from '../../../components/BsEmpty.vue'; import BsEmpty from '../../../components/BsEmpty.vue';
import {MSG_TYPE} from '@/common/dicts/im'; import {MSG_TYPE} from '@/common/dicts/im';
import {FormatDate} from '@/common/utils'; import {FormatDate, Im} from '@/common/utils';
export default { export default {
components: { BsEmpty }, components: { BsEmpty },
data (){ data (){
@ -56,18 +59,20 @@ export default {
}, },
computed:{ computed:{
sessionData (){ sessionData (){
return this.$store.state.imData; return this.$store.state.imData.sort((a,b) => b.updateTimeStamp - a.updateTimeStamp);
} }
}, },
methods:{ methods:{
FormatDate, FormatDate,
openMsg(item){ openMsg(item){
if(item.type === 3){ Im.setCurSessionId(item.id);
this.$Router.push(`/messageChat?sessionId=${item.id}`); if(item.type === 4){
}else{ if(JSON.parse(item.payload).type === 'system'){
this.$Router.push(`/messageSystem?sessionId=${item.id}`); this.$Router.push(`/messageSystem?sessionId=${item.id}`);
return false;
}
} }
this.$Router.push(`/messageChat?sessionId=${item.id}`);
} }
} }
} }
@ -107,6 +112,7 @@ export default {
display:-webkit-box; display:-webkit-box;
-webkit-box-orient:vertical; -webkit-box-orient:vertical;
-webkit-line-clamp:2; -webkit-line-clamp:2;
} }
&--right{ &--right{
text-align: right; text-align: right;

@ -2,7 +2,7 @@
* @Author: ch * @Author: ch
* @Date: 2022-03-26 14:32:03 * @Date: 2022-03-26 14:32:03
* @LastEditors: ch * @LastEditors: ch
* @LastEditTime: 2022-05-27 17:02:59 * @LastEditTime: 2022-06-02 18:07:09
* @Description: file content * @Description: file content
--> -->
<template> <template>
@ -21,7 +21,6 @@
<text class="msg-item--desc-link" v-if="i.shipType === 1" @click="openLink(i)" :key="i.shipContent">[]</text> <text class="msg-item--desc-link" v-if="i.shipType === 1" @click="openLink(i)" :key="i.shipContent">[]</text>
<text v-else :key="i.shipContent">{{i.shipContent}}</text> <text v-else :key="i.shipContent">{{i.shipContent}}</text>
</template> </template>
</template> </template>
</view> </view>
</view> </view>
@ -78,13 +77,7 @@ export default {
}, },
async getHistoryMsg(){ async getHistoryMsg(){
this.loading = true; this.loading = true;
const lastMsg = this.msgData?.length ? this.msgData[this.msgData.length - 1] : {}; await ToAsyncAwait(Im.getHistoryMsg());
await ToAsyncAwait(Im.getHistoryMsg({
content : {
sessionId : this.$route.query.sessionId,
topMessageId : lastMsg.id || null
}
}));
}, },
/** /**
* 把当前会话消息置为已读 * 把当前会话消息置为已读

@ -2,7 +2,7 @@
* @Author: ch * @Author: ch
* @Date: 2022-03-22 15:09:06 * @Date: 2022-03-22 15:09:06
* @LastEditors: ch * @LastEditors: ch
* @LastEditTime: 2022-05-24 17:24:35 * @LastEditTime: 2022-06-12 13:46:04
* @Description: file content * @Description: file content
--> -->
<template> <template>
@ -43,7 +43,7 @@ export default {
return false; return false;
} }
this.$store.commit('SET_TOKEN'); this.$store.commit('SET_TOKEN');
Im.logout(); Im.close();
this.$Router.replace('/login'); this.$Router.replace('/login');
} }
} }

@ -2,7 +2,7 @@
* @Author: ch * @Author: ch
* @Date: 2022-03-28 15:38:23 * @Date: 2022-03-28 15:38:23
* @LastEditors: ch * @LastEditors: ch
* @LastEditTime: 2022-05-17 11:44:50 * @LastEditTime: 2022-06-13 20:49:32
* @Description: file content * @Description: file content
--> -->
<template> <template>
@ -38,7 +38,7 @@ export default {
nickname : this.userInfo.nickname nickname : this.userInfo.nickname
}); });
if(error){ if(error){
ui.$u.totas(error.message); uni.$u.toast(error.message);
return false; return false;
} }
this.$store.commit('SET_USER_INFO', {...this.userInfo}); this.$store.commit('SET_USER_INFO', {...this.userInfo});

@ -2,7 +2,7 @@
* @Author: ch * @Author: ch
* @Date: 2022-03-28 15:38:23 * @Date: 2022-03-28 15:38:23
* @LastEditors: ch * @LastEditors: ch
* @LastEditTime: 2022-05-24 09:57:14 * @LastEditTime: 2022-06-13 20:59:54
* @Description: file content * @Description: file content
--> -->
<template> <template>
@ -106,7 +106,7 @@ export default {
const avatar = `${oss.host}/${oss.dir}${fileName}`; const avatar = `${oss.host}/${oss.dir}${fileName}`;
const {error, result} = await ApiPutUser({avatar}); const {error, result} = await ApiPutUser({avatar});
if(error){ if(error){
ui.$u.totas(error.message); uni.$u.toast(error.message);
return false return false
} }
this.$store.commit('SET_USER_INFO', {...this.userInfo, avatar}); this.$store.commit('SET_USER_INFO', {...this.userInfo, avatar});

@ -2,7 +2,7 @@
* @Author: ch * @Author: ch
* @Date: 2022-03-23 17:27:21 * @Date: 2022-03-23 17:27:21
* @LastEditors: ch * @LastEditors: ch
* @LastEditTime: 2022-05-30 17:44:24 * @LastEditTime: 2022-06-02 14:39:30
* @Description: file content * @Description: file content
--> -->
<template> <template>
@ -169,10 +169,7 @@ page {
width: 50rpx; width: 50rpx;
height: 50rpx; height: 50rpx;
left: 40rpx; left: 40rpx;
/* #ifdef H5 */
top: 40rpx; top: 40rpx;
/* #endif */
/* #ifndef H5 */ /* #ifndef H5 */
top: 128rpx; top: 128rpx;
/* #endif */ /* #endif */

@ -2,7 +2,7 @@
* @Author: ch * @Author: ch
* @Date: 2019-08-22 19:41:20 * @Date: 2019-08-22 19:41:20
* @LastEditors: ch * @LastEditors: ch
* @LastEditTime: 2022-05-31 17:35:15 * @LastEditTime: 2022-06-13 14:25:25
* @Description: file content * @Description: file content
--> -->
<template> <template>
@ -113,7 +113,7 @@ export default {
async getSeckillList(){ async getSeckillList(){
const {error, result} = await ApiGetHomeSeckill(); const {error, result} = await ApiGetHomeSeckill();
if(result){ if(result){
this.seckillData = result this.seckillData = result;
} }
}, },
AdJump AdJump

@ -2,7 +2,7 @@
* @Author: ch * @Author: ch
* @Date: 2022-03-22 15:36:46 * @Date: 2022-03-22 15:36:46
* @LastEditors: ch * @LastEditors: ch
* @LastEditTime: 2022-05-24 09:57:06 * @LastEditTime: 2022-06-13 10:20:21
* @Description: file content * @Description: file content
--> -->
<template> <template>
@ -119,9 +119,7 @@ export default {
// IM // IM
ImInit().then(() => { ImInit().then(() => {
// //
Im.getSessionList({ Im.getSessionList();
content: { sysId : 1 }
});
}); });
this.goBack(); this.goBack();

@ -2,7 +2,7 @@
* @Author: ch * @Author: ch
* @Date: 2022-03-21 18:08:07 * @Date: 2022-03-21 18:08:07
* @LastEditors: ch * @LastEditors: ch
* @LastEditTime: 2022-05-05 10:52:27 * @LastEditTime: 2022-06-13 14:25:12
* @Description: file content * @Description: file content
--> -->
<template> <template>

Loading…
Cancel
Save