main
15004070936 4 weeks ago
parent c2903e3b2a
commit 95e91728a0

@ -3,25 +3,33 @@
//import checkupdate from "@/uni_modules/uni-upgrade-center-app/utils/check-update.js"
//
// import callCheckVersion from '@/uni_modules/uni-upgrade-center-app/utils/call-check-version';
import { myCache } from '@/utils/utils.js';
import * as wsApi from './common/wssocket';
import chatStore from "./store/chatStore";
import * as enums from "./common/enums";
import * as msgType from "./common/messageType";
//#ifdef APP-PLUS
const jpushModule = uni.requireNativePlugin('JG-JPush');
//#endif
export default {
data() {
return {
userid:"",
userName:"",
userheadimg:"",
isInit: false, //
isExit: false, // 退
audioTip: null,
reconnecting: false //
}
},
onLaunch() {
//
//checkupdate();
//#ifdef APP-PLUS
//
// jpushModule.setLoggerEnable(true);
@ -30,7 +38,7 @@
// let connectEnable = result.connectEnable
// console.log("jpush", connectEnable)
// });
//
// var user= JSON.parse(uni.getStorageSync("user"));
// if(user&&user.yhdm){
@ -39,7 +47,7 @@
// 'sequence': 1
// })
// }
// jpushModule.addTagAliasListener(result => {
// let code = result.code
// let sequence = result.sequence
@ -49,7 +57,7 @@
// let alias = result.alias
// console.log(alias, '')
// });
// //
// jpushModule.addNotificationListener(result => {
// let notificationEventType = result.notificationEventType
@ -78,19 +86,340 @@
// console.log("", result)
// })
//#endif
var user = myCache('user');
this.userid = user.userid? user.userid:'';
this.userId = user.userid? user.userid:'';
this.userName=user.nickName?user.nickName:"";
this.userheadimg=user.avatar?user.avatar:require("@/static/image/girl.png");
//---------------------------------------------------
//
if(uni.getStorageSync("token")&&this.userid){
this.startHeartbeat();
}
// if(uni.getStorageSync("token")&&this.userid){
// this.startHeartbeat();
// }
//---------------------------------------------------
//
let token =uni.getStorageSync("token");
if(token){
this.init(this.userId);
}else {
//
uni.navigateTo({
url: "/pages/login/login"
})
}
//---------------------------------------------------
},
methods: {
//---------------------------------------------------
init(userId) {
if (!this.userId && userId){
this.userId=userId;
}
this.reconnecting = false;
this.isExit = false;
//
this.loadStore().then(() => {
// websocket
this.initWebSocket();
this.isInit = true;
}).catch((e) => {
console.log(e);
this.exit();
})
},
// ws
initWebSocket(){
let token = uni.getStorageSync("token");
wsApi.connect(this.$wsUrl, token);
wsApi.onConnect(() => {
if (this.reconnecting) {
//
this.onReconnectWs();
} else {
// 线
this.pullPrivateOfflineMessage(this.$store.state.chatStore.privateMsgMaxId);
this.pullGroupOfflineMessage(this.$store.state.chatStore.groupMsgMaxId);
}
});
wsApi.onMessage((cmd, msgInfo) => {
if (cmd == 2) {
// 线
uni.showModal({
content: '您已在其他地方登录,将被强制下线',
showCancel: false,
})
this.exit();
} else if (cmd == 3) {
//
this.handlePrivateMessage(msgInfo);
} else if (cmd == 4) {
//
this.handleGroupMessage(msgInfo);
} else if (cmd == 5) {
//
this.handleSystemMessage(msgInfo);
}
});
wsApi.onClose((res) => {
console.log("ws断开", res);
//
this.reconnectWs();
})
},
loadStore() {
const promises = [];
promises.push(this.$store.dispatch('chatStore/loadChat'));
promises.push(this.$store.dispatch('groupStore/loadGroup'));
promises.push(this.$store.dispatch('friendStore/loadFriend'));
return Promise.all(promises);
},
reconnectWs() {
// 退
if (this.isExit) {
return;
}
//
this.reconnecting = true;
// token
uni.$http.get('/api/getInfo').then((res) => {
uni.showToast({
title: '连接已断开,尝试重新连接...',
icon: 'none'
})
//
let token = uni.getStorageSync("token");
wsApi.reconnect(this.$wsUrl, token);
}).catch(() => {
// 5s
setTimeout(() => {
this.reconnectWs();
}, 5000)
})
},
exit() {
console.log("exit");
this.isExit = true;
wsApi.close(3099);
uni.removeStorageSync("token");
uni.reLaunch({
url: "/pages/login/login"
})
this.unloadStore();
},
unloadStore() {
this.$store.dispatch('chatStore/clear')
},
async pullPrivateOfflineMessage(minId) {
this.$store.dispatch('chatStore/setLoadingPrivateMsg',true)
try {
console.log("拉取私信")
await uni.$http.get("/api/message/private/pullOfflineMessage", { minId:minId });
} catch (error) {
console.error("pullPrivateOfflineMessage error", error);
} finally {
this.$store.dispatch('chatStore/setLoadingPrivateMsg',false)
}
},
async pullGroupOfflineMessage(minId) {
this.$store.dispatch('chatStore/setLoadingGroupMsg',true)
try {
console.log("拉取群聊")
await uni.$http.get('/api/message/group/pullOfflineMessage',{minId:minId});
} catch (error) {
console.error("pullGroupOfflineMessage error", error);
} finally {
this.$store.dispatch('chatStore/setLoadingGroupMsg',false)
}
},
handlePrivateMessage(msg) {
msg.selfSend = msg.sendId === this.userId;
const friendId = msg.selfSend ? msg.recvId : msg.sendId;
const chatInfo = { type: 'PRIVATE', targetId: friendId };
const type=Number(msg.type)
if (type === enums.MESSAGE_TYPE.LOADING) {
this.$store.dispatch('chatStore/setLoadingPrivateMsg', JSON.parse(msg.content));
return;
}
if (type === enums.MESSAGE_TYPE.READED) {
this.$store.dispatch('chatStore/resetUnreadCount', chatInfo);
return;
}
// ,
if (type === enums.MESSAGE_TYPE.RECEIPT) {
console.log("消息回执处理")
this.$store.dispatch('chatStore/readedMessage', { friendId: msg.sendId });
return;
}
if (type === enums.MESSAGE_TYPE.RECALL) {
this.$store.dispatch('chatStore/recallMessage', { msgInfo: msg, chatInfo });
return;
}
if (type === enums.MESSAGE_TYPE.FRIEND_NEW) {
this.$store.dispatch('friendStore/addFriend', JSON.parse(msg.content));
// this.friendStore.addFriend(JSON.parse(msg.content));
return;
}
// if (msg.type === enums.MESSAGE_TYPE.FRIEND_DEL) {
// this.friendStore.removeFriend(friendId);
// return;
// }
//
// if (msgType.isRtcPrivate(msg.type)) {
// this.handleRtcPrivate(msg);
// return;
// }
//
if (msgType.isNormal(type) || msgType.isTip(type) || msgType.isAction(type)) {
const friend = this.loadFriendInfo(friendId);
this.insertPrivateMessage(friend, msg);
}
},
handleGroupMessage(msg) {
msg.selfSend = msg.sendId === this.userId;
const chatInfo = { type: 'GROUP', targetId: msg.groupId };
const type=Number(msg.type)
if (type === enums.MESSAGE_TYPE.LOADING) {
console.log("收到setLoadingGroupMsg")
this.$store.dispatch('chatStore/setLoadingGroupMsg', JSON.parse(msg.content));
return;
}
if (type === enums.MESSAGE_TYPE.READED) {
this.$store.dispatch('chatStore/resetUnreadCount', chatInfo);
return;
}
if (type === enums.MESSAGE_TYPE.RECEIPT) {
this.$store.dispatch('chatStore/updateMessage', {
msgInfo: {
id: msg.id,
groupId: msg.groupId,
readedCount: msg.readedCount,
receiptOk: msg.receiptOk,
},
chatInfo
});
return;
}
if (type === enums.MESSAGE_TYPE.RECALL) {
this.$store.dispatch('chatStore/recallMessage', { msgInfo: msg, chatInfo });
return;
}
if (type === enums.MESSAGE_TYPE.GROUP_NEW) {
this.$store.dispatch('groupStore/addGroup', JSON.parse(msg.content));
return;
}
if (type === enums.MESSAGE_TYPE.GROUP_DEL) {
this.$store.dispatch('groupStore/removeGroup', msg.groupId);
return;
}
//
// if (msgType.isRtcGroup(msg.type)) {
// this.handleRtcGroup(msg);
// return;
// }
if (msgType.isNormal(type) || msgType.isTip(type) || msgType.isAction(type)) {
const group = this.loadGroupInfo(msg.groupId);
this.insertGroupMessage(group, msg);
}
},
handleSystemMessage(msg) {
if (msg.type === enums.MESSAGE_TYPE.USER_BANNED) {
wsApi.close(3099);
uni.showModal({
content: `您的账号已被管理员封禁,原因:${msg.content}`,
showCancel: false,
});
this.exit();
}
},
insertPrivateMessage(friend,msg) {
const chatInfo = {
type: 'PRIVATE',
targetId: msg.sendId,
showName: friend.nickName,
headImage: friend.headImage,
};
this.$store.dispatch('chatStore/openChat', chatInfo);
this.$store.dispatch('chatStore/insertMessage', { msgInfo: msg, chatInfo });
this.playAudioTip();
},
insertGroupMessage(group, msg) {
const chatInfo = {
type: 'GROUP',
targetId: group.id,
showName: group.name,
headImage: group.headImageThumb,
};
this.$store.dispatch('chatStore/openChat', chatInfo);
this.$store.dispatch('chatStore/insertMessage', { msgInfo: msg, chatInfo });
this.playAudioTip();
},
onReconnectWs() {
this.reconnecting = false;
Promise.all([
this.$store.dispatch('friendStore/loadFriend'),
this.$store.dispatch('groupStore/loadGroup')
]).then(() => {
uni.showToast({
title: "已重新连接",
icon: 'none'
});
// 线
const chatState = this.$store.state.chatStore;
this.pullPrivateOfflineMessage(chatState.privateMsgMaxId);
this.pullGroupOfflineMessage(chatState.groupMsgMaxId);
}).catch((e) => {
console.log(e);
this.exit();
})
},
loadFriendInfo(id, callback) {
let friend = this.$store.getters['friendStore/findFriend'](id)
if (!friend) {
console.log("未知用户:", id)
friend = {
id: id,
nickName: "未知用户",
headImage: ""
}
}
return friend;
},
loadGroupInfo(id) {
let group = this.$store.getters['groupStore/findGroup'](id)
if (!group) {
group = {
id: id,
showGroupName: "未知群聊",
headImageThumb: ""
}
}
return group;
},
playAudioTip() {
//
// this.audioTip = uni.createInnerAudioContext();
// this.audioTip.src = "/static/audio/tip.wav";
// this.audioTip.play();
},
//---------------------------------------------------
//
/**
startHeartbeat() {
console.log("startHeartbeat");
var that=this;
@ -150,7 +479,7 @@
console.log(res,'socket连接成功complete');
}
});
uni.onSocketOpen(resopen => {
console.log('WebSocket连接已打开');
this.isConnected = true;
@ -171,12 +500,12 @@
console.log('消息发送失败!')
}
});
//
this.startHeartbeat();
});
//
uni.onSocketClose(res => {
console.log('WebSocket连接已关闭',res);
@ -193,7 +522,7 @@
// socket
this.reconnect();
});
//
uni.onSocketMessage(res => {
console.log('收到WebSocket服务器消息');
@ -210,7 +539,7 @@
// 0: 1: 2: 3: 4: 10, "" 11, " "12, " " 30,""
if(data.type==0||data.type==1||data.type==2||data.type==3||data.type==4||data.type==5||data.type==6){
if(data.type==0){
data.content=decodeURIComponent(data.content);
data.content=decodeURIComponent(data.content);
}
var id="privatechat-" + data.recvId +"-" + data.sendId;
var index=this.ifadd(id);
@ -264,11 +593,11 @@
//
// 0: 1: 2: 3: 4: 10, "" 11, " "12, " " 30,""
if(data.type==0||data.type==1||data.type==2||data.type==3||data.type==4||data.type==5||data.type==6){
if(data.type==0){
data.content=decodeURIComponent(data.content);
data.content=decodeURIComponent(data.content);
}
var id="groupchat-" + data.groupId;
var index=this.ifadd(id);
var chatinfo=null;
@ -311,14 +640,14 @@
}
else{
// 10, "" 11, " " 12, " " 30,""
}
}
}
}
}
});
},
//
ifadd(id){
@ -398,7 +727,7 @@
//
this.reorder();
myCache("chatlist-"+ this.userid,this.grouplist);
//
//
var name=info[info.sendId]?info[info.sendId].name:"匿名",img=info[info.sendId]?info[info.sendId].img:require("@/static/image/girl.png");
@ -446,7 +775,7 @@
msgchat.push(firstmsg);
myCache(info.id,msgchat);
}
}
},
async updateChatList(info){
@ -466,7 +795,7 @@
//
this.reorder();
myCache("chatlist-"+this.userid,this.grouplist);
//
//
var name=info[info.fromuser]?info[info.fromuser].name:"匿名",img=info[info.fromuser]?info[info.fromuser].img:require("@/static/image/girl.png");
@ -516,11 +845,13 @@
myCache(info.id,msgchat);
}
}
},
**/
},
}
</script>
@ -530,6 +861,8 @@
@import "common/demo.scss";
@import 'colorui/main.css';
@import 'colorui/icon.css';
@import "/im.scss";
@import url('./static/icon/iconfont.css');
body{
font-family: 'FZLTZHUNHJW--GB1-0';
}
@ -567,7 +900,7 @@
.page {
width: 100%;
}
/* 当屏幕宽度小于600px时调整布局 */
@media (max-width: 600px) {
.page {
@ -602,7 +935,7 @@
.imgload{
background-image: url('/static/image/nopic.png');
background-size: 66% auto;
background-position: center center;
background-position: center center;
background-repeat: no-repeat;
}
</style>
</style>

@ -0,0 +1,66 @@
let toTimeText = (timeStamp, simple) => {
var dateTime = new Date(timeStamp)
var currentTime = Date.parse(new Date()); //当前时间
var timeDiff = currentTime - dateTime; //与当前时间误差
var timeText = '';
if (timeDiff <= 60000) { //一分钟内
timeText = '刚刚';
} else if (timeDiff > 60000 && timeDiff < 3600000) {
//1小时内
timeText = Math.floor(timeDiff / 60000) + '分钟前';
} else if (timeDiff >= 3600000 && timeDiff < 86400000 && !isYestday(dateTime)) {
//今日
timeText = formatDateTime(dateTime).substr(11, 5);
} else if (isYestday(dateTime)) {
//昨天
timeText = '昨天' + formatDateTime(dateTime).substr(11, 5);
} else if (isYear(dateTime)) {
//今年
timeText = formatDateTime(dateTime).substr(5, simple ? 5 : 14);
} else {
//不属于今年
timeText = formatDateTime(dateTime);
if (simple) {
timeText = timeText.substr(2, 8);
}
}
return timeText;
}
let isYestday = (date) => {
var yesterday = new Date(new Date() - 1000 * 60 * 60 * 24);
return yesterday.getYear() === date.getYear() &&
yesterday.getMonth() === date.getMonth() &&
yesterday.getDate() === date.getDate();
}
let isYear = (date) => {
return date.getYear() === new Date().getYear();
}
let formatDateTime = (date) => {
if (date === '' || !date) {
return ''
}
var dateObject = new Date(date)
var y = dateObject.getFullYear()
var m = dateObject.getMonth() + 1
m = m < 10 ? ('0' + m) : m
var d = dateObject.getDate()
d = d < 10 ? ('0' + d) : d
var h = dateObject.getHours()
h = h < 10 ? ('0' + h) : h
var minute = dateObject.getMinutes()
minute = minute < 10 ? ('0' + minute) : minute
var second = dateObject.getSeconds()
second = second < 10 ? ('0' + second) : second
return y + '/' + m + '/' + d + ' ' + h + ':' + minute + ':' + second
}
export {
toTimeText,
isYestday,
isYear,
formatDateTime
}

@ -0,0 +1,56 @@
// 表情图片基础路径(若环境变量未定义,使用默认路径 /static/emoji/
const EMO_BASE_URL = '/static/emoji/'
const emoTextList = [
'憨笑', '媚眼', '开心', '坏笑', '可怜', '爱心', '笑哭', '拍手', '惊喜', '打气',
'大哭', '流泪', '饥饿', '难受', '健身', '示爱', '色色', '眨眼', '暴怒', '惊恐',
'思考', '头晕', '大吐', '酷笑', '翻滚', '享受', '鼻涕', '快乐', '雀跃', '微笑',
'贪婪', '红心', '粉心', '星星', '大火', '眼睛', '音符', '叹号', '问号', '绿叶',
'燃烧', '喇叭', '警告', '信封', '房子', '礼物', '点赞', '举手', '拍手', '点头',
'摇头', '偷瞄', '庆祝', '疾跑', '打滚', '惊吓', '起跳'
]
const regex = /\#[\u4E00-\u9FA5]{1,3}\;/gi
/**
* 判断文本是否包含表情标记
* @param {string} content 文本内容
* @returns {boolean}
*/
const containEmoji = (content) => regex.test(content)
/**
* 将文本中的表情标记替换为 <img> 标签
* @param {string} content 原始文本
* @param {string} extClass 图片的 CSS 类名
* @returns {string} 替换后的 HTML 字符串
*/
const transform = (content, extClass) => {
return content.replace(regex, (emoText) => {
const word = emoText.replace(/\#|\;/g, '')
const idx = emoTextList.indexOf(word)
if (idx === -1) return emoText
const path = textToPath(emoText)
return `<img src="${path}" class="${extClass}" />`
})
}
/**
* 根据表情标记生成图片路径
* @param {string} emoText 表情标记 '#开心;'
* @returns {string} 图片完整路径
*/
const textToPath = (emoText) => {
const word = emoText.replace(/\#|\;/g, '')
const idx = emoTextList.indexOf(word)
// 若找不到对应表情,返回占位图或空字符串(此处返回原标记)
if (idx === -1) return emoText
return EMO_BASE_URL + idx + '.gif'
}
export default {
containEmoji,
emoTextList,
transform,
textToPath
}

@ -0,0 +1,68 @@
const MESSAGE_TYPE = {
TEXT: 0,
IMAGE: 1,
FILE: 2,
AUDIO: 3,
VIDEO: 4,
ORDER: 5,
PRODUCT: 6,
RECALL: 10,
READED: 11,
RECEIPT: 12,
TIP_TIME: 20,
TIP_TEXT: 21,
LOADING: 30,
ACT_RT_VOICE: 40,
ACT_RT_VIDEO: 41,
USER_BANNED: 50,
FRIEND_NEW: 80,
FRIEND_DEL: 81,
GROUP_NEW: 90,
GROUP_DEL: 91,
RTC_CALL_VOICE: 100,
RTC_CALL_VIDEO: 101,
RTC_ACCEPT: 102,
RTC_REJECT: 103,
RTC_CANCEL: 104,
RTC_FAILED: 105,
RTC_HANDUP: 106,
RTC_CANDIDATE: 107,
RTC_GROUP_SETUP: 200,
RTC_GROUP_ACCEPT: 201,
RTC_GROUP_REJECT: 202,
RTC_GROUP_FAILED: 203,
RTC_GROUP_CANCEL: 204,
RTC_GROUP_QUIT: 205,
RTC_GROUP_INVITE: 206,
RTC_GROUP_JOIN: 207,
RTC_GROUP_OFFER: 208,
RTC_GROUP_ANSWER: 209,
RTC_GROUP_CANDIDATE: 210,
RTC_GROUP_DEVICE: 211
}
const USER_STATE = {
OFFLINE: 0,
FREE: 1,
BUSY: 2
}
const TERMINAL_TYPE = {
WEB: 0,
APP: 1
}
const MESSAGE_STATUS = {
UNSEND: 0,
SENDED: 1,
RECALL: 2,
READED: 3
}
export {
MESSAGE_TYPE,
USER_STATE,
TERMINAL_TYPE,
MESSAGE_STATUS
}

@ -0,0 +1,40 @@
// 是否普通消息
let isNormal = function (type) {
return type >= 0 && type < 10;
}
// 是否状态消息
let isStatus = function (type) {
return type >= 10 && type < 20;
}
// 是否提示消息
let isTip = function (type) {
return type >= 20 && type < 30;
}
// 操作交互类消息
let isAction = function (type) {
return type >= 40 && type < 50;
}
// 单人通话信令
let isRtcPrivate = function (type) {
return type >= 100 && type < 200;
}
// 多人通话信令
let isRtcGroup = function (type) {
return type >= 200 && type < 300;
}
export {
isNormal,
isStatus,
isTip,
isAction,
isRtcPrivate,
isRtcGroup
}

@ -0,0 +1,15 @@
let replaceURLWithHTMLLinks = (content, color) => {
// 使用正则表达式匹配更广泛的URL格式(此正则由deepseek生成)
const urlRegex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|]|\bwww\.[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
return content.replace(urlRegex, (url) => {
// 如果URL不以http(s)://开头则添加http://前缀
if (!url.startsWith("http")) {
url = "http://" + url;
}
return `<a href="${url}" target="_blank" style="color: ${color};text-decoration: underline;">${url}</a>`;
});
}
export default {
replaceURLWithHTMLLinks
}

@ -0,0 +1,211 @@
// common/websocket-manager.js
import * as wsApi from './wssocket';
import * as msgType from './messageType';
import * as enums from './enums';
class WebSocketManager {
constructor() {
if (WebSocketManager.instance) return WebSocketManager.instance;
WebSocketManager.instance = this;
// 状态标志
this.isInit = false;
this.isExit = false;
this.reconnecting = false;
this.audioTip = null;
this.WS_URL=this.$wsUrl
// Store 引用(需外部注入)
this.chatStore = null;
this.userId=null;
// 事件监听器(供页面注册)
this.messageListeners = [];
}
/**
* 初始化 WebSocket 管理器
* @param {Object} stores - 各个 store 实例
* @param {string} accessToken - 用户 token
*/
init(stores, accessToken) {
if (this.isInit) return;
this.chatStore = stores.chatStore;
this.userId=stores.userId;
this.isExit = false;
this.reconnecting = false;
// 加载本地数据并启动 WebSocket
this.loadStore()
.then(() => {
this.initWebSocket(accessToken);
this.isInit = true;
})
.catch((e) => {
console.error(e);
this.exit();
});
}
initWebSocket(accessToken) {
wsApi.connect(this.WS_URL, accessToken);
wsApi.onConnect(() => {
if (this.reconnecting) {
this.onReconnectWs();
} else {
// 首次连接,拉取离线消息
this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId).catch(() => {});
this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId).catch(() => {});
}
});
wsApi.onMessage((cmd, msgInfo) => {
// 分发原始消息给外部监听器
this.messageListeners.forEach(listener => listener(cmd, msgInfo));
// 内部业务处理
if (cmd == 2) {
// 异地登录
uni.showModal({
content: '您已在其他地方登录,将被强制下线',
showCancel: false,
});
this.exit();
} else if (cmd == 3) {
this.handlePrivateMessage(msgInfo);
} else if (cmd == 4) {
this.handleGroupMessage(msgInfo);
} else if (cmd == 5) {
this.handleSystemMessage(msgInfo);
}
});
wsApi.onClose((res) => {
console.log("ws断开", res);
this.reconnect(accessToken);
});
}
/**
* 重新连接token 刷新后调用
* @param {string} accessToken
*/
reconnect(accessToken) {
if (this.isExit) return;
this.reconnecting = true;
wsApi.reconnect(this.WS_URL, accessToken);
}
/**
* 关闭连接并退出登录
*/
exit() {
console.log("WebSocket exit");
this.isExit = true;
wsApi.close(3099);
// uni.removeStorageSync("loginInfo");
// uni.reLaunch({ url: "/pages/login/login" });
this.unloadStore();
}
/**
* 注册消息监听器页面可用来接收新消息通知
* @param {Function} listener - (cmd, msgInfo) => {}
*/
addMessageListener(listener) {
if (typeof listener === 'function') {
this.messageListeners.push(listener);
}
}
/**
* 移除消息监听器
*/
removeMessageListener(listener) {
const index = this.messageListeners.indexOf(listener);
if (index !== -1) this.messageListeners.splice(index, 1);
}
// ============= 私有方法 =============
// 视频通话相关(可根据需要保留或删除)
handleRtcPrivate(msg) {
// #ifdef MP-WEIXIN
// return; // 小程序不支持
// // #endif
// let delayTime = 100;
// if (msg.type === enums.MESSAGE_TYPE.RTC_CALL_VOICE || msg.type === enums.MESSAGE_TYPE.RTC_CALL_VIDEO) {
// const mode = msg.type === enums.MESSAGE_TYPE.RTC_CALL_VIDEO ? "video" : "voice";
// const pages = getCurrentPages();
// const curPage = pages[pages.length - 1].route;
// if (curPage !== "pages/chat/chat-private-video") {
// const friend = this.loadFriendInfo(msg.selfSend ? msg.recvId : msg.sendId);
// const friendInfo = encodeURIComponent(JSON.stringify(friend));
// uni.navigateTo({
// url: `/pages/chat/chat-private-video?mode=${mode}&friend=${friendInfo}&isHost=false`,
// });
// delayTime = 500;
// }
// }
// setTimeout(() => {
// uni.$emit('WS_RTC_PRIVATE', msg);
// }, delayTime);
}
handleRtcGroup(msg) {
// #ifdef MP-WEIXIN
return;
// #endif
let delayTime = 100;
if (msg.type === enums.MESSAGE_TYPE.RTC_GROUP_SETUP) {
const pages = getCurrentPages();
const curPage = pages[pages.length - 1].route;
if (curPage !== "pages/chat/chat-group-video") {
const userInfos = encodeURIComponent(msg.content);
const inviterId = msg.sendId;
const groupId = msg.groupId;
uni.navigateTo({
url: `/pages/chat/chat-group-video?groupId=${groupId}&isHost=false&inviterId=${inviterId}&userInfos=${userInfos}`,
});
delayTime = 500;
}
}
setTimeout(() => {
uni.$emit('WS_RTC_GROUP', msg);
}, delayTime);
}
playAudioTip() {
// 音频播放实现(如需)
// this.audioTip = uni.createInnerAudioContext();
// this.audioTip.src = "/static/audio/tip.wav";
// this.audioTip.play();
}
async refreshToken(loginInfo) {
// if (!loginInfo || !loginInfo.refreshToken) {
// throw new Error("No refreshToken");
// }
// const newLoginInfo = await http({
// url: '/refreshToken',
// method: 'PUT',
// header: { refreshToken: loginInfo.refreshToken },
// });
// uni.setStorageSync("loginInfo", newLoginInfo);
// return newLoginInfo;
}
}
// 导出单例
const wsManager = new WebSocketManager();
export default wsManager;

@ -0,0 +1,159 @@
let accessToken = "";
let messageCallBack = null;
let closeCallBack = null;
let connectCallBack = null;
let isConnect = false; //连接标识 避免重复连接
let rec = null;
let lastConnectTime = new Date(); // 最后一次连接时间
let socketTask = null;
let connect = (wsurl, token) => {
accessToken = token;
if (isConnect) {
return;
}
lastConnectTime = new Date();
socketTask = uni.connectSocket({
url: wsurl,
success: (res) => {
console.log("websocket连接成功");
},
fail: (e) => {
console.log(e);
console.log("websocket连接失败10s后重连");
setTimeout(() => {
connect();
}, 10000)
}
});
socketTask.onOpen((res) => {
console.log("WebSocket连接已打开");
isConnect = true;
// 发送登录命令
let loginInfo = {
cmd: 0,
data: {
accessToken: accessToken
}
};
socketTask.send({
data: JSON.stringify(loginInfo)
});
})
socketTask.onMessage((res) => {
let sendInfo = JSON.parse(res.data)
if (sendInfo.cmd == 0) {
heartCheck.start()
connectCallBack && connectCallBack();
console.log('WebSocket登录成功')
} else if (sendInfo.cmd == 1) {
// 重新开启心跳定时
heartCheck.reset();
} else {
// 其他消息转发出去
// console.log("接收到消息", sendInfo);
messageCallBack && messageCallBack(sendInfo.cmd, sendInfo.data)
// console.log("cmd",sendInfo.cmd)
// console.log("msgInfo",sendInfo.data)
}
})
socketTask.onClose((res) => {
console.log('WebSocket连接关闭')
isConnect = false;
closeCallBack && closeCallBack(res);
})
socketTask.onError((e) => {
console.log(e)
isConnect = false;
// APP 应用切出超过一定时间(约1分钟)会触发报错,此处回调给应用进行重连
closeCallBack && closeCallBack({ code: 1006 });
})
}
//定义重连函数
let reconnect = (wsurl, accessToken) => {
console.log("尝试重新连接");
if (isConnect) {
return;
}
// 延迟10秒重连 避免过多次过频繁请求重连
let timeDiff = new Date().getTime() - lastConnectTime.getTime()
let delay = timeDiff < 10000 ? 10000 - timeDiff : 0;
rec && clearTimeout(rec);
rec = setTimeout(function() {
connect(wsurl, accessToken);
}, delay);
};
//设置关闭连接
let close = (code) => {
if (!isConnect) {
return;
}
socketTask.close({
code: code,
complete: (res) => {
console.log("关闭websocket连接");
isConnect = false;
},
fail: (e) => {
console.log("关闭websocket连接失败", e);
}
});
};
// 心跳设置
let heartCheck = {
timeout: 20000, // 每段时间发送一次心跳包 这里设置为20s
timeoutObj: null, // 延时发送消息对象(启动心跳新建这个对象,收到消息后重置对象)
start: function() {
if (isConnect) {
console.log('发送WebSocket心跳')
let heartBeat = {
cmd: 1,
data: {}
};
sendMessage(JSON.stringify(heartBeat))
}
},
reset: function() {
clearTimeout(this.timeoutObj);
this.timeoutObj = setTimeout(() => heartCheck.start(), this.timeout);
}
};
let sendMessage = (message) => {
socketTask.send({ data: message })
}
let onConnect = (callback) => {
connectCallBack = callback;
}
let onMessage = (callback) => {
messageCallBack = callback;
}
let onClose = (callback) => {
closeCallBack = callback;
}
// 将方法暴露出去
export {
connect,
reconnect,
close,
sendMessage,
onConnect,
onMessage,
onClose
}

@ -0,0 +1,173 @@
<template>
<uni-popup ref="popup" type="bottom" @change="onChange">
<view class="chat-at-box">
<view class="chat-at-top">
<view class="chat-at-tip"> 选择要提醒的人</view>
<button class="chat-at-btn" type="warn" size="mini" @click="onClean()"> </button>
<button class="chat-at-btn" type="primary" size="mini" @click="onOk()">({{ atUserIds.length }})
</button>
</view>
<scroll-view v-show="atUserIds.length > 0" scroll-x="true" scroll-left="120">
<view class="at-user-items">
<view v-for="m in checkedMembers" class="at-user-item" :key="m.userId">
<head-image :name="m.showNickName" :url="m.headImage" size="mini"></head-image>
</view>
</view>
</scroll-view>
<view class="search-bar">
<uni-search-bar v-model="searchText" cancelButton="none" radius="100" placeholder="搜索"></uni-search-bar>
</view>
<view class="member-items">
<virtual-scroller :items="memberItems">
<template v-slot="{ item }">
<view class="member-item" @click="onSwitchChecked(item)">
<head-image :name="item.showNickName" :online="item.online" :url="item.headImage"
size="small"></head-image>
<view class="member-name">{{ item.showNickName }}</view>
<radio :checked="item.checked" :disabled="item.locked"
@click.stop="onSwitchChecked(item)" />
</view>
</template>
</virtual-scroller>
</view>
</view>
</uni-popup>
</template>
<script>
export default {
name: "chat-at-box",
props: {
ownerId: {
type: Number,
},
members: {
type: Array
}
},
data() {
return {
searchText: "",
showMembers: []
};
},
methods: {
init(atUserIds) {
this.showMembers = [];
let userId = this.userStore.userInfo.id;
if (this.ownerId == userId) {
this.showMembers.push({
userId: -1,
showNickName: "全体成员"
})
}
this.members.forEach((m) => {
if (!m.quit && m.userId != userId) {
m.checked = atUserIds.indexOf(m.userId) >= 0;
this.showMembers.push(m);
}
});
},
open() {
this.$refs.popup.open();
},
onSwitchChecked(member) {
member.checked = !member.checked;
},
onClean() {
this.showMembers.forEach((m) => {
m.checked = false;
})
},
onOk() {
this.$refs.popup.close();
},
onChange(e) {
if (!e.show) {
this.$emit("complete", this.atUserIds)
}
}
},
computed: {
atUserIds() {
return this.showMembers.filter(m => m.checked).map(m => m.userId);
},
checkedMembers() {
return this.showMembers.filter(m => m.checked);
},
memberItems() {
return this.showMembers.filter(m => m.showNickName.includes(this.searchText));
}
}
}
</script>
<style lang="scss" scoped>
.chat-at-box {
position: relative;
display: flex;
flex-direction: column;
background-color: white;
padding: 10rpx;
//border-radius: 15rpx;
.chat-at-top {
display: flex;
align-items: center;
height: 70rpx;
padding: 10rpx;
.chat-at-tip {
flex: 1;
}
.chat-at-btn {
margin-left: 10rpx;
}
}
.at-user-items {
display: flex;
align-items: center;
height: 90rpx;
.at-user-item {
padding: 3rpx;
}
}
.member-items {
position: relative;
flex: 1;
overflow: hidden;
.member-item {
height: 110rpx;
display: flex;
position: relative;
padding: 0 30rpx;
align-items: center;
background-color: white;
white-space: nowrap;
margin-bottom: 1px;
&:hover {
background-color: $im-bg-active;
}
.member-name {
flex: 1;
padding-left: 20rpx;
font-size: $im-font-size;
line-height: 60rpx;
white-space: nowrap;
overflow: hidden;
}
}
}
}
</style>

@ -0,0 +1,147 @@
<template>
<uni-popup ref="popup" type="right" >
<view class="chat-group-readed">
<view class="uni-padding-wrap uni-common-mt">
<uni-segmented-control :current="current" :values="items" style-type="button"
active-color="#9CE0DCFF"
in-active-color="white"
@clickItem="onClickItem" />
</view>
<view class="content">
<!-- 已读列表 -->
<view v-if="current === 0" class="member-list">
<scroll-view scroll-y="true" style="max-height: 400rpx;">
<view v-for="(item, index) in readedMembers" :key="index" class="member-item">
<head-image :name="item.showNickName" :online="item.online" :url="item.headImage"
:size="90"></head-image>
<view class="member-name">{{ item.showNickName }}</view>
</view>
</scroll-view>
</view>
<!-- 未读列表 -->
<view v-if="current === 1" class="member-list">
<scroll-view scroll-y="true" style="max-height: 400rpx;">
<view v-for="(item, index) in unreadMembers" :key="index" class="member-item">
<head-image :name="item.showNickName" :online="item.online" :url="item.headImage"
:size="90"></head-image>
<view class="member-name">{{ item.showNickName }}</view>
</view>
</scroll-view>
</view>
</view>
</view>
</uni-popup>
</template>
<script>
export default {
name: "chat-group-readed",
props: {
msgInfo: {
type: Object,
required: true
},
groupMembers: {
type: Array,
default: () => []
}
},
data() {
return {
items: ['已读', '未读'],
current: 0,
readedMembers: [],
unreadMembers: []
};
},
methods: {
open() {
this.$refs.popup.open();
// this.loadReadedUser();
},
async loadReadedUser() {
this.readedMembers = [];
this.unreadMembers = [];
await uni.$http.get( `/api/message/group/findReadedUsers?groupId=${this.msgInfo.groupId}&messageId=${this.msgInfo.id}`)
.then(res => {
this.groupMembers.forEach(member => {
// 退
if (member.userId == this.msgInfo.sendId || member.quit) {
return;
}
if (res.data.data.find(userId => member.userId == Number(userId))) {
this.readedMembers.push(member);
console.log(member)
} else {
this.unreadMembers.push(member);
}
});
this.items[0] = `已读(${this.readedMembers.length})`;
this.items[1] = `未读(${this.unreadMembers.length})`;
// Vuex
const chatInfo = { type: 'GROUP', targetId: this.msgInfo.groupId };
const msgInfo = {
id: this.msgInfo.id,
groupId: this.msgInfo.groupId,
readedCount: this.readedMembers.length
};
// mapActions updateMessage this.updateMessage({ msgInfo, chatInfo })
// 使 dispatch
if (this.updateMessage) {
this.updateMessage({ msgInfo, chatInfo });
} else {
this.$store.dispatch('chatStore/updateMessage', { msgInfo, chatInfo });
}
}).catch(err => {
console.error('加载已读用户失败', err);
});
},
onClickItem(e) {
this.current = e.currentIndex;
}
}
}
</script>
<style lang="scss" scoped>
.chat-group-readed {
// 穿 scoped
::v-deep .uni-segmented-control__item {
color: #333 !important; //
&.uni-segmented-control__item--active {
color: #007AFF !important; //
//
background-color: transparent !important; //
}
}
}
.chat-group-readed {
position: relative;
display: flex;
flex-direction: column;
background-color: white;
padding: 10rpx;
}
.member-item {
height: 120rpx;
display: flex;
position: relative;
padding: 0 30rpx;
align-items: center;
background-color: white;
white-space: nowrap;
.member-name {
flex: 1;
padding-left: 20rpx;
font-size: 30rpx;
font-weight: 600;
line-height: 60rpx;
white-space: nowrap;
overflow: hidden;
}
}
</style>

@ -0,0 +1,169 @@
<template>
<view class="chat-item" :class="active ? 'active' : ''">
<!-- rich-text中的表情包会屏蔽事件所以这里用一个遮罩层捕获点击事件 -->
<view class="mask" @tap="showChatBox()"></view>
<view class="left">
<head-image :url="chat.headImage" :name="chat.showName"></head-image>
</view>
<view class="chat-right">
<view class="chat-name">
<view class="chat-name-text">
<view>{{ chat.showName }}</view>
</view>
<view class="chat-time">{{ $date.toTimeText(chat.lastSendTime, true) }}</view>
</view>
<view class="chat-content">
<view class="chat-at-text">{{ atText }}</view>
<view class="chat-send-name" v-if="isShowSendName">{{ chat.sendNickName + ':&nbsp;' }}</view>
<rich-text class="chat-content-text" :nodes="$emo.transform(chat.lastContent,'emoji-small')"></rich-text>
<uni-badge v-if="chat.unreadCount > 0" :max-num="99" :text="chat.unreadCount" />
</view>
</view>
</view>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: "chatItem",
props: {
chat: {
type: Object
},
index: {
type: Number
},
active: {
type: Boolean,
default: false
}
},
computed: {
// isLoading getter
...mapGetters('chatStore', ['isLoading']),
isShowSendName() {
if (!this.chat.sendNickName) return false
const size = this.chat.messages.length
if (size === 0) return false
const lastMsg = this.chat.messages[size - 1]
return this.$msgType.isNormal(lastMsg.type)
},
atText() {
if (this.chat.atMe) return "[有人@我]"
if (this.chat.atAll) return "[@全体成员]"
return ""
}
},
methods: {
showChatBox() {
//
if (!getApp().$vm.isInit || this.isLoading()) {
uni.showToast({
title: "正在初始化页面,请稍后...",
icon: 'none'
})
return
}
uni.navigateTo({
url: "/pages/chat/chat-box?chatIdx=" + this.index
})
}
}
}
</script>
<style scoped lang="scss">
.chat-item {
height: 96rpx;
display: flex;
margin-bottom: 2rpx;
position: relative;
padding: 18rpx 20rpx;
align-items: center;
background-color: white;
white-space: nowrap;
&:hover {
background-color: $im-bg-active;
}
&.active {
background-color: $im-bg-active;
}
.mask {
position: absolute;
width: 100%;
height: 100%;
left: 0;
right: 0;
z-index: 99;
}
.left {
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 100rpx;
height: 100rpx;
}
.chat-right {
height: 100%;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding-left: 20rpx;
text-align: left;
overflow: hidden;
.chat-name {
display: flex;
.chat-name-text {
flex: 1;
font-size: $im-font-size-large;
white-space: nowrap;
overflow: hidden;
display: flex;
align-items: center;
}
.chat-time {
font-size: $im-font-size-smaller-extra;
color: $im-text-color-lighter;
text-align: right;
white-space: nowrap;
overflow: hidden;
}
}
.chat-content {
display: flex;
font-size: $im-font-size-smaller;
color: $im-text-color-lighter;
padding-top: 8rpx;
align-items: center;
.chat-at-text {
color: $im-color-danger;
}
.chat-send-name {
font-size: $im-font-size-smaller;
}
.chat-content-text {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
</style>

@ -0,0 +1,559 @@
<template>
<view class="chat-message-item">
<view class="message-tip" v-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">
{{ msgInfo.content }}
</view>
<view class="message-tip" v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME">
{{ $date.toTimeText(msgInfo.sendTime) }}
</view>
<view class="message-normal" v-else-if="isNormal" :class="{ 'message-mine': msgInfo.selfSend }">
<head-image class="avatar" @longpress.prevent="$emit('longPressHead')" :id="msgInfo.sendId" :url="headImage"
:name="showName" size="small"></head-image>
<view class="content">
<view v-if="msgInfo.groupId && !msgInfo.selfSend" class="top">
<text>{{ showName }}</text>
</view>
<view class="bottom">
<view v-if="msgInfo.type == $enums.MESSAGE_TYPE.TEXT">
<long-press-menu :items="menuItems" @select="onSelectMenu">
<rich-text v-if="$emo.containEmoji(msgInfo.content)" class="message-text"
:nodes="nodesText"></rich-text>
<u-parse v-else class="message-text" :showImgMenu="false" :html="nodesText"></u-parse>
</long-press-menu>
</view>
<view class="message-image" v-if="msgInfo.type == $enums.MESSAGE_TYPE.IMAGE">
<long-press-menu :items="menuItems" @select="onSelectMenu">
<view class="image-box">
<image class="send-image" mode="heightFix" :src="JSON.parse(msgInfo.content).thumbUrl"
lazy-load="true" @click.stop="onShowFullImage()">
</image>
<loading v-if="loading"></loading>
</view>
</long-press-menu>
<text title="发送失败" v-if="loadFail" @click="onSendFail"
class="send-fail iconfont icon-warning-circle-fill"></text>
</view>
<view class="message-video" v-if="msgInfo.type == $enums.MESSAGE_TYPE.VIDEO">
<long-press-menu :items="menuItems" @select="onSelectMenu">
<view class="video-box">
<myVideo :videoUrl="videoUrl" />
</view>
</long-press-menu>
<loading v-if="loading"></loading>
<text title="发送失败" v-if="loadFail" @click="onSendFail"
class="send-fail iconfont icon-warning-circle-fill"></text>
</view>
<!-- <view class="message-file" v-if="msgInfo.type == $enums.MESSAGE_TYPE.FILE">-->
<!-- <long-press-menu :items="menuItems" @select="onSelectMenu">-->
<!-- <view class="file-box">-->
<!-- <view class="file-info">-->
<!-- <uni-link class="file-name" :text="data.name" showUnderLine="true"-->
<!-- color="#007BFF" :href="data.url"></uni-link>-->
<!-- <view class="file-size">{{ fileSize }}</view>-->
<!-- </view>-->
<!-- <view class="file-icon iconfont icon-file"></view>-->
<!-- <loading v-if="loading"></loading>-->
<!-- </view>-->
<!-- </long-press-menu>-->
<!-- <text title="发送失败" v-if="loadFail" @click="onSendFail"-->
<!-- class="send-fail iconfont icon-warning-circle-fill"></text>-->
<!-- </view>-->
<long-press-menu v-if="msgInfo.type == $enums.MESSAGE_TYPE.AUDIO" :items="menuItems"
@select="onSelectMenu">
<view class="message-audio message-text" @click="onPlayAudio()">
<text class="iconfont icon-voice-play"></text>
<text class="chat-audio-text">{{ JSON.parse(msgInfo.content).duration + '"' }}</text>
<text v-if="audioPlayState == 'PAUSE'" class="iconfont icon-play"></text>
<text v-if="audioPlayState == 'PLAYING'" class="iconfont icon-pause"></text>
</view>
</long-press-menu>
<!-- <long-press-menu v-if="isAction" :items="menuItems" @select="onSelectMenu">-->
<!-- <view class="chat-realtime message-text" @click="$emit('call')">-->
<!-- <text v-if="msgInfo.type == $enums.MESSAGE_TYPE.ACT_RT_VOICE"-->
<!-- class="iconfont icon-chat-voice"></text>-->
<!-- <text v-if="msgInfo.type == $enums.MESSAGE_TYPE.ACT_RT_VIDEO"-->
<!-- class="iconfont icon-chat-video"></text>-->
<!-- <text>{{ msgInfo.content }}</text>-->
<!-- </view>-->
<!-- </long-press-menu>-->
<view class="message-status" v-if="!isAction">
<text class="chat-readed" v-if="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status == $enums.MESSAGE_STATUS.READED">已读</text>
<text class="chat-unread" v-if="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status != $enums.MESSAGE_STATUS.READED">未读</text>
</view>
<view class="chat-receipt" v-if="msgInfo.receipt && msgInfo.groupId" @click="onShowReadedBox">
<text v-if="msgInfo.receiptOk" class="tool-icon iconfont icon-ok"></text>
<text v-else>{{ msgInfo.readedCount }}</text>
</view>
</view>
</view>
</view>
<chat-group-readed ref="chatGroupReaded" :groupMembers="groupMembers" :msgInfo="msgInfo"></chat-group-readed>
</view>
</template>
<script>
import myVideo from "@/components/myVideo.vue";
export default {
name: "chat-message-item",
props: {
headImage: {
type: String,
required: true
},
showName: {
type: String,
required: true
},
msgInfo: {
type: Object,
required: true
},
groupMembers: {
type: Array
}
},
components: {
myVideo
},
data() {
return {
audioPlayState: 'STOP',
innerAudioContext: null,
menu: {
show: false,
style: ""
}
}
},
computed: {
loading() {
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "loading";
},
loadFail() {
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "fail";
},
data() {
return JSON.parse(this.msgInfo.content)
},
videoUrl() {
try {
const content = JSON.parse(this.msgInfo.content);
return content.url || content.originUrl || '';
} catch {
return '';
}
},
videoPoster() {
try {
const content = JSON.parse(this.msgInfo.content);
return content.thumbUrl || content.poster || '';
} catch {
return '';
}
},
fileSize() {
let size = this.data.size;
if (size > 1024 * 1024) {
return Math.round(size / 1024 / 1024) + "M";
}
if (size > 1024) {
return Math.round(size / 1024) + "KB";
}
return size + "B";
},
menuItems() {
let items = [];
if (this.msgInfo.type == this.$enums.MESSAGE_TYPE.TEXT) {
items.push({
key: 'COPY',
name: '复制',
icon: 'bars'
});
}
// if (this.msgInfo.selfSend && this.msgInfo.id > 0) {
// items.push({
// key: 'RECALL',
// name: '',
// icon: 'refreshempty'
// });
// }
items.push({
key: 'DELETE',
name: '删除',
icon: 'trash',
color: '#e64e4e'
});
// if (this.msgInfo.type == this.$enums.MESSAGE_TYPE.FILE) {
// items.push({
// key: 'DOWNLOAD',
// name: '',
// icon: 'download'
// });
// }
return items;
},
isAction() {
return this.$msgType.isAction(this.msgInfo.type);
},
isNormal() {
const type = this.msgInfo.type;
return this.$msgType.isNormal(type) || this.$msgType.isAction(type)
},
nodesText() {
let color = this.msgInfo.selfSend ? 'white' : '';
let text = this.$url.replaceURLWithHTMLLinks(this.msgInfo.content, color)
return this.$emo.transform(text, 'emoji-normal')
}
},
methods: {
onSendFail() {
uni.showToast({
title: "该文件已发送失败,目前不支持自动重新发送,建议手动重新发送",
icon: "none"
})
},
onPlayAudio() {
if (!this.innerAudioContext) {
this.innerAudioContext = uni.createInnerAudioContext();
let url = JSON.parse(this.msgInfo.content).url;
this.innerAudioContext.src = url;
this.innerAudioContext.onEnded((e) => {
console.log('停止')
this.audioPlayState = "STOP"
this.emit();
})
this.innerAudioContext.onError((e) => {
this.audioPlayState = "STOP"
console.log("播放音频出错");
console.log(e)
this.emit();
});
}
if (this.audioPlayState == 'STOP') {
this.innerAudioContext.play();
this.audioPlayState = "PLAYING";
} else if (this.audioPlayState == 'PLAYING') {
this.innerAudioContext.pause();
this.audioPlayState = "PAUSE"
} else if (this.audioPlayState == 'PAUSE') {
this.innerAudioContext.play();
this.audioPlayState = "PLAYING"
}
this.emit();
},
onSelectMenu(item) {
this.$emit(item.key.toLowerCase(), this.msgInfo);
this.menu.show = false;
},
onShowFullImage() {
let imageUrl = JSON.parse(this.msgInfo.content).originUrl;
uni.previewImage({
urls: [imageUrl]
})
},
async onShowReadedBox() {
await this.$refs.chatGroupReaded.loadReadedUser();
this.$refs.chatGroupReaded.open();
},
emit() {
this.$emit("audioStateChange", this.audioPlayState, this.msgInfo);
},
stopPlayAudio() {
if (this.innerAudioContext) {
this.innerAudioContext.stop();
this.innerAudioContext = null;
this.audioPlayState = "STOP"
}
}
}
}
</script>
<style scoped lang="scss">
.chat-message-item {
padding: 2rpx 20rpx;
.message-tip {
line-height: 60rpx;
text-align: center;
color: $im-text-color-lighter;
font-size: $im-font-size-smaller-extra;
padding: 10rpx;
}
.message-normal {
position: relative;
margin-bottom: 22rpx;
padding-left: 110rpx;
min-height: 80rpx;
.avatar {
position: absolute;
top: 0;
left: 0;
}
.content {
text-align: left;
.top {
display: flex;
flex-wrap: nowrap;
color: $im-text-color-lighter;
font-size: $im-font-size-smaller;
line-height: $im-font-size-smaller;
height: $im-font-size-smaller;
}
.bottom {
display: inline-block;
padding-right: 80rpx;
margin-top: 5rpx;
.message-text {
position: relative;
line-height: 1.6;
margin-top: 10rpx;
padding: 16rpx 24rpx;
background-color: $im-bg;
border-radius: 20rpx;
color: $im-text-color;
font-size: $im-font-size;
text-align: left;
display: block;
word-break: break-all;
white-space: pre-line;
&:after {
content: "";
position: absolute;
left: -20rpx;
top: 26rpx;
width: 6rpx;
height: 6rpx;
border-style: solid dashed dashed;
border-color: $im-bg transparent transparent;
overflow: hidden;
border-width: 18rpx;
}
}
.message-image {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
align-items: center;
.image-box {
position: relative;
.send-image {
min-width: 200rpx;
max-width: 420rpx;
height: 350rpx;
cursor: pointer;
border-radius: 4px;
}
}
.send-fail {
color: $im-color-danger;
font-size: $im-font-size;
cursor: pointer;
margin: 0 20px;
}
}
.message-video {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
align-items: center;
.video-box {
position: relative;
max-width: 540rpx;
height: 300rpx;
border-radius: 4px;
overflow: hidden;
display: flex;
flex-direction: row;
}
.send-fail {
color: $im-color-danger;
font-size: $im-font-size;
cursor: pointer;
margin: 0 20px;
}
}
.message-file {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
align-items: center;
cursor: pointer;
.file-box {
position: relative;
display: flex;
flex-wrap: nowrap;
align-items: center;
min-height: 60px;
border-radius: 4px;
padding: 10px 15px;
box-shadow: $im-box-shadow-dark;
.file-info {
flex: 1;
height: 100%;
text-align: left;
font-size: 14px;
width: 300rpx;
.file-name {
font-weight: 600;
margin-bottom: 15px;
word-break: break-all;
}
}
.file-icon {
font-size: 80rpx;
color: #d42e07;
}
}
.send-fail {
color: #e60c0c;
font-size: 50rpx;
cursor: pointer;
margin: 0 20rpx;
}
}
.message-audio {
display: flex;
align-items: center;
.chat-audio-text {
padding-right: 8px;
}
.icon-voice-play {
font-size: 18px;
padding-right: 8px;
}
}
.chat-realtime {
display: flex;
align-items: center;
.iconfont {
font-size: 20px;
padding-right: 8px;
}
}
.message-status {
line-height: $im-font-size-smaller-extra;
font-size: $im-font-size-smaller-extra;
padding-top: 2rpx;
.chat-readed {
display: block;
padding-top: 2rpx;
color: $im-text-color-lighter;
}
.chat-unread {
color: $im-color-danger;
}
}
.chat-receipt {
font-size: $im-font-size-smaller;
color: $im-text-color-lighter;
font-weight: 600;
.icon-ok {
font-size: 20px;
color: $im-color-success;
}
}
}
}
&.message-mine {
text-align: right;
padding-left: 0;
padding-right: 110rpx;
.avatar {
left: auto;
right: 0;
}
.content {
text-align: right;
.bottom {
padding-left: 80rpx;
padding-right: 0;
.message-text {
margin-left: 10px;
background-color: $im-color-primary-light-2;
color: #fff;
&:after {
left: auto;
right: -9px;
border-top-color: $im-color-primary-light-2;
}
}
.message-image {
flex-direction: row-reverse;
}
.message-file {
flex-direction: row-reverse;
}
.message-audio {
flex-direction: row-reverse;
.chat-audio-text {
padding-right: 0;
padding-left: 8px;
}
.icon-voice-play {
transform: rotateY(180deg);
}
}
.chat-realtime {
display: flex;
flex-direction: row-reverse;
.iconfont {
transform: rotateY(180deg);
}
}
}
}
}
}
}
</style>

@ -0,0 +1,221 @@
<template>
<view class="chat-record">
<view class="chat-record-bar" :class="{ recording: recording }" id="chat-record-bar" @click.stop=""
@touchstart.prevent="onStartRecord" @touchmove.prevent="onTouchMove" @touchend.prevent="onEndRecord">
{{ recording ? '正在录音' : '长按 说话' }}</view>
<view v-if="recording" class="chat-record-window" :style="recordWindowStyle">
<view class="rc-wave">
<text class="note" v-for="i in 7" :key="i" :style="{ animationDelay: (0.1 * (i - 1)) + 's' }"></text>
</view>
<view class="rc-tip">{{ recordTip }}</view>
<view class="cancel-btn" @click="onCancel">
<uni-icons :class="moveToCancel ? 'red' : 'black'" type="clear" :size="moveToCancel ? 45 : 40"></uni-icons>
</view>
<view class="opt-tip" :class="moveToCancel ? 'red' : 'black'">{{ moveToCancel ? '松手取消' : '松手发送,上划取消' }}</view>
</view>
</view>
</template>
<script>
export default {
name: "chat-record",
data() {
return {
recording: false,
moveToCancel: false,
recordBarTop: 0,
duration: 0,
rcTimer: null
}
},
methods: {
onTouchMove(e) {
const moveY = e.touches[0].clientY;
this.moveToCancel = moveY < this.recordBarTop - 40;
},
onCancel() {
if (this.recording) {
this.moveToCancel = true;
this.onEndRecord();
}
},
onStartRecord() {
if (this.recording) {
return;
}
console.log("开始录音")
this.moveToCancel = false;
this.initRecordBar();
if (!this.$rc.checkIsEnable()) {
return;
}
this.$rc.start().then(() => {
this.recording = true;
console.log("开始录音成功")
this.startTimer();
}).catch((e) => {
console.log("录音失败" + JSON.stringify(e))
uni.showToast({
title: "录音失败",
icon: "none"
});
});
},
onEndRecord() {
if (!this.recording) {
return;
}
this.recording = false;
this.stopTimer();
this.$rc.close();
if (this.moveToCancel) {
console.log("录音取消")
return;
}
if (this.duration <= 1) {
uni.showToast({
title: "说话时间太短",
icon: 'none'
})
return;
}
this.$rc.upload().then((data) => {
this.$emit("send", data);
}).catch((e) => {
uni.showToast({
title: e,
icon: 'none'
})
})
},
startTimer() {
this.duration = 0;
this.stopTimer();
this.rcTimer = setInterval(() => {
this.duration++;
if (this.duration >= 60) {
this.onEndRecord();
}
}, 1000)
},
stopTimer() {
this.rcTimer && clearInterval(this.rcTimer);
this.rcTimer = null;
},
initRecordBar() {
const query = uni.createSelectorQuery().in(this);
query.select('#chat-record-bar').boundingClientRect((rect) => {
this.recordBarTop = rect.top;
}).exec()
}
},
computed: {
recordWindowStyle() {
const windowHeight = uni.getSystemInfoSync().windowHeight;
const bottom = windowHeight - this.recordBarTop + 12;
return `bottom:${bottom}px;`
},
recordTip() {
if (this.duration > 50) {
return `${60 - this.duration}s后将停止录音`;
}
return `录音时长:${this.duration}s`;
}
},
// Vue 2
beforeDestroy() {
this.stopTimer();
this.recording = false;
}
}
</script>
<style lang="scss" scoped>
.chat-record {
.rc-wave {
display: flex;
align-items: flex-end;
justify-content: center;
position: relative;
height: 80rpx;
.note {
background: linear-gradient(to top, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
width: 4px;
height: 50%;
border-radius: 5rpx;
margin-right: 4px;
animation: loading 0.5s infinite linear;
@keyframes loading {
0% {
background-image: linear-gradient(to right, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
height: 20%;
border-radius: 5rpx;
}
50% {
background-image: linear-gradient(to top, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
height: 80%;
border-radius: 5rpx;
}
100% {
background-image: linear-gradient(to top, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
height: 20%;
border-radius: 5rpx;
}
}
}
}
.chat-record-bar {
padding: 10rpx;
margin: 10rpx;
border-radius: 10rpx;
text-align: center;
box-shadow: $im-box-shadow;
&.recording {
background-color: $im-color-primary;
color: #fff;
}
}
.chat-record-window {
position: fixed;
left: 0;
right: 0;
height: 360rpx;
background-color: rgba(255, 255, 255, 0.95);
padding: 30rpx;
.icon-microphone {
text-align: center;
font-size: 80rpx;
padding: 10rpx;
}
.rc-tip {
text-align: center;
font-size: $im-font-size-small;
color: $im-text-color-light;
margin-top: 20rpx;
}
.cancel-btn {
text-align: center;
margin-top: 40rpx;
height: 80rpx;
}
.opt-tip {
text-align: center;
font-size: 30rpx;
padding: 20rpx;
}
.red {
color: $im-color-danger !important;
}
}
}
</style>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,591 @@
<template>
<view>
<view class="submit">
<view class="submit-chat">
<view class="bt-img" @tap="records">
<image :src="toc"></image>
</view>
<!-- 文本框 -->
<textarea :maxlength="-1" :auto-height="true" :adjust-position="true" confirm-type="send" :cursor-spacing="10"
:selection-start="selectionStart" :selection-end="selectionEnd" class="chat-send btnt" :class="{displaynone:isrecord}"
@confirm="sendMsg" @input="inputs" @focus="focus" v-model="msg"></textarea>
<view class="record btn" :class="{displaynone:!isrecord}" @touchstart="touchstart" @touchend="touchend"
@touchmove="touchmove">
按住说话
</view>
<view class="bt-img" @tap="emoji">
<image src="../../static/icon/emote.png"></image>
</view>
<view v-if="ifmore" class="bt-img" @tap="more">
<image src="../../static/icon/more.png"></image>
</view>
<u-button v-if="!ifmore" @tap="sendMsg" style="background-color: #00a89b; width: 88rpx;color: #fff;border-radius: 10rpx;"></u-button>
</view>
<!-- 表情框 -->
<view class="emoji" :class="{displaynone:!isemoji}">
<view class="emoji-send">
<!-- <view class="emoji-send-det" @tap="emojiBack">
<image src="../../static/icon/emote.png"></image>
</view> -->
<view class="emoji-send-bt" @tap="emojiSend"></view>
</view>
<emoji @emotion="emotion" :height="260"></emoji>
</view>
<!-- 更多操作框 -->
<view class="more" :class="{displaynone:!ismore}">
<view class="more-list" @tap="sendImg('album')">
<image src="../../static/icon/image.png"></image>
<view class="more-list-title">图片</view>
</view>
<view class="more-list" @tap="sendImg('camera')">
<image src="../../static/icon/photo.png"></image>
<view class="more-list-title">拍照</view>
</view>
<!-- <view class="more-list" @tap="choseLocation">
<image src="../../static/icon/location.png"></image>
<view class="more-list-title">定位</view>
</view> -->
<view class="more-list" @click="chooseVideo()">
<image src="../../static/icon/video.png"></image>
<view class="more-list-title">拍摄视频</view>
</view>
<!-- <view class="more-list">
<image src="../../static/icon/file.png"></image>
<view class="more-list-title">文件</view>
</view> -->
</view>
</view>
<view class="voice-bg" :class="{displaynone:!voicebg}">
<view class="voice-bg-len">
<view class="voice-bg-time" :style="{width:vlength/0.6+'%'}">
{{vlength}}
</view>
<view class="voice-del">上滑取消录音</view>
</view>
</view>
<u-toast ref="uToast" />
</view>
</template>
<script>
//
import emoji from '../emoji/emoji.vue'
//
const recorderManager = uni.getRecorderManager();
export default {
components: {
emoji,
},
data() {
return {
isrecord: false,
isemoji: false,
ismore: false,
ifmore: true,
voicebg: false,
pageY: 0,
msg: "",
// require
toc: require('../../static/icon/voice.png'),
timer: '', //
vlength: 0,
selectionStart: 0,
selectionEnd: 0
};
},
methods: {
//
getElementHeight() {
const query = uni.createSelectorQuery().in(this);
query.select('.submit').boundingClientRect(data => {
this.$emit('heights', data.height);
}).exec();
},
//
records() {
//
this.ismore = false
this.isemoji = false
//
setTimeout(() => {
this.getElementHeight();
}, 10)
if (this.isrecord) {
this.isrecord = false;
this.toc = require("../../static/icon/voice.png");
} else {
this.isrecord = true;
this.toc = require("../../static/icon/keyboard.png");
}
},
//
emoji() {
console.log('emoji')
this.isemoji = !this.isemoji;
//
this.ismore = false
this.isrecord = false;
this.toc = require("../../static/icon/voice.png");
//
setTimeout(() => {
this.getElementHeight();
}, 10)
},
//
emotion(e) {
console.log(e),
this.msg = this.msg + e
},
//
sendMsg(e) {
console.log(e.detail.value,this.msg)
// var chatm = e.detail.value;
// var pos = chatm.indexOf('\n');
this.updateCursor();
// -1
// if (pos != -1 && chatm.length > 1) {
// this.$emit('inputs', this.msg);
// setTimeout(() => {
// this.msg = '';
// }, 0)
// }
if (this.msg.length > 0) {
// 0
this.send(this.msg, 'txt');
this.ifmore=true;
}
},
inputs(e) {
var chatm = e.detail.value;
if (chatm.length > 0) {
this.ifmore=false;
}
else{
this.ifmore=true;
}
},
updateCursor() {
// text
this.selectionStart = this.msg.length;
this.selectionEnd = this.msg.length;
},
//
focus() {
console.log('focus');
//
// this.isemoji = false;
// this.ismore = false;
setTimeout(() => {
this.getElementHeight()
}, 10);
},
//
emojiSend() {
// if (this.msg.length > 0) {
// this.$emit('inputs', this.msg);
// setTimeout(() => {
// this.msg = '';
// }, 0)
// }
if (this.msg.length > 0) {
//0
this.send(this.msg, 'txt')
}
},
// 退
emojiBack() {
if (this.msg.length > 0) {
this.msg = this.msg.substring(0, this.msg.length - 1);
}
},
//
more() {
console.log('more')
this.ismore = !this.ismore;
//
this.isemoji = false
this.isrecord = false;
this.toc = require("../../static/icon/voice.png");
setTimeout(() => {
this.getElementHeight();
}, 10)
},
//
chooseVideo(){
var that =this;
uni.chooseVideo({
sourceType: ['camera', 'album'],
maxDuration: 60, //
success: function (res) {
console.log('选择视频成功,返回的参数:', res);
// 使 res.tempFilePath
const filePath = res.tempFilePath;
console.log(filePath);
that.checkImageSize(filePath,2);
},
fail: function (err) {
console.error('选择视频失败:', err);
}
});
},
//
sendImg(e) {
var that =this;
let count = 9;
if (e == 'album') {
count = 9;
} else {
count = 1;
}
uni.chooseImage({
count: count, //9
sizeType: ['original', 'compressed'], //
sourceType: [e], //
// success: function (res) { //functionsend
success: (res) => {
console.log(JSON.stringify(res.tempFilePaths));
const filePaths = res.tempFilePaths;
for (let i = 0; i < filePaths.length; i++) {
that.checkImageSize(filePaths[i],1);
}
}
});
},
checkImageSize(filePath,type) {
var that = this;
uni.getFileInfo({
filePath: filePath,
success: function (res) {
console.log('文件大小(字节):', res.size);
if(type==1){
//
if (res.size > 2*1024*1024) { // 20MB
console.log('文件过大');
//
that.$refs.uToast.show({
title: "图片大小不能超过2M...",
type: "error",
duration: 2000,
});
} else {
console.log('文件大小合适');
//
that.send(filePath, 'image');
}
}
else if(type==2){
//
if (res.size > 10*1024*1024) { // 20MB
console.log('文件过大');
//
that.$refs.uToast.show({
title: "视频大小不能超过10M...",
type: "error",
duration: 2000,
});
} else {
console.log('文件大小合适');
//
that.send(filePath, 'video');
}
}
},
fail: function (err) {
console.error('获取文件信息失败:', err);
if(type==1){
that.$refs.uToast.show({
title: "图片大小不能超过2M...",
type: "error",
duration: 2000,
});
}
else if(type==1){
that.$refs.uToast.show({
title: "视频大小不能超过10M...",
type: "error",
duration: 2000,
});
}
}
});
},
//
//
touchstart(e) {
console.log("开始录音")
console.log("点击产生数据", e)
this.pageY = e.changedTouches[0].pageY;
this.voicebg = true;
let i = 1;
this.timer = setInterval(() => {
this.vlength = i;
i++;
//
if (i > 60) {
clearInterval(this.timer);
this.touchend();
}
}, 1000)
recorderManager.start();
},
//
touchmove(e) {
// console.log("y",e.changedTouches[0].pageY);
if (this.pageY - e.changedTouches[0].pageY > 100) {
//
this.voicebg = false;
}
},
//
touchend() {
console.log("结束录音")
clearInterval(this.timer);
recorderManager.stop();
// recorderManager.onStop(function(res) {
recorderManager.onStop((res) => {
if(this.vlength<1){
this.$refs.uToast.show({
title: "录音时间过短...",
type: "warning",
duration: 2000,
});
}
else{
let data = {
voice: res.tempFilePath,
time: this.vlength
}
if (this.voicebg) {
this.send(data, 'audio');
}
}
// //
this.vlength = 0;
this.voicebg = false;
console.log('recorder stop' + JSON.stringify(res));
// self.voicePath = res.tempFilePath;
});
},
//
choseLocation() {
uni.chooseLocation({
// success: function(res) {
success: res => {
let data = {
name: res.name,
address: res.address,
latitude: res.latitude,
longitude: res.longitude
}
this.send(data, 3);
// console.log('' + res.name);
// console.log('' + res.address);
// console.log('' + res.latitude);
// console.log('' + res.longitude);
}
});
},
//
send(msg, type) {
console.log("send")
console.log(msg, type)
let date = {
message: msg,
type: type
}
this.$emit('inputs', date);
setTimeout(() => {
this.msg = '';
}, 0)
}
}
};
</script>
<style lang="scss" scoped>
.submit {
background: rgba(244, 244, 244, 0.96);
border-top: 1px solid rgba(39, 40, 50, 0.1);
width: 100%;
position: fixed;
bottom: 0;
z-index: 999;
padding-bottom: var(--status-bar-height);
// padding-bottom: env(safe-area-inset-bottom);
}
.displaynone {
display: none;
}
.submit-chat {
width: 100%;
display: flex;
align-items: flex-end;
box-sizing: border-box;
padding: 14rpx;
image {
width: 60rpx;
height: 60rpx;
margin: 0 8rpx;
flex: auto;
}
.btnt {
flex: auto;
background-color: #fff;
border-radius: 10rpx;
padding: 20rpx;
max-height: 240rpx;
margin: 0 10rpx;
overflow-y: auto;
}
.btn {
flex: auto;
background-color: #fff;
border-radius: 10rpx;
padding: 20rpx;
max-height: 160rpx;
margin: 0 10rpx;
}
.chat-send {
line-height: 44rpx;
}
.record {
line-height: 44rpx;
text-align: center;
font-size: 24rpx;
color: rgba(39, 40, 50, 0.6);
}
}
.emoji {
width: 100%;
height: 460rpx;
background: rgba(236, 237, 238, 1);
box-shadow: 0px 11rpx 0px 0px rgba(0, 0, 0, 0.1);
.emoji-send {
width: 150rpx;
height: 104rpx;
padding-top: 24rpx;
background-color: rgba(236, 237, 238, 0.8);
position: fixed;
// bottom: 0;
bottom: env(safe-area-inset-bottom);
right: 0;
display: flex;
.emoji-send-bt {
flex: 1;
margin: 0 32rpx 0 20rpx;
height: 80rpx;
background: rgba(255, 228, 49, 1);
font-size: 32rpx;
text-align: center;
line-height: 80rpx;
border-radius: 12rpx;
}
.emoji-send-det {
flex: 1;
margin-left: 24rpx;
height: 80rpx;
background: #fff;
font-size: 32rpx;
text-align: center;
line-height: 80rpx;
border-radius: 12rpx;
image {
width: 42rpx;
height: 32rpx;
}
}
}
}
.more {
width: 100%;
min-height: 220rpx;
background: rgba(236, 237, 238, 1);
box-shadow: 0px 11rpx 0px 0px rgba(0, 0, 0, 0.1);
bottom: env(safe-area-inset-bottom);
padding: 8rpx 20rpx;
box-sizing: border-box;
.more-list {
width: 25%;
text-align: center;
float: left;
padding-top: 32rpx;
image {
width: 90rpx;
height: 90rpx;
padding: 24rpx;
background: rgba(255, 255, 255, 1);
border-radius: 24rpx;
}
.more-list-title {
font-size: 24rpx;
color: rgba(39, 40, 50, 0.5);
line-height: 34rpx;
}
}
}
.voice-bg {
height: 100%;
width: 100%;
background-color: rgba(0, 0, 0, 0.3);
position: fixed;
top: 0;
bottom: 0;
z-index: 1001;
.voice-bg-len {
height: 84rpx;
width: 600rpx;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 42rpx;
text-align: center;
}
.voice-bg-time {
display: inline-block;
min-width: 120rpx;
line-height: 84rpx;
background-color: #00a89b;
border-radius: 42rpx;
color:#ffffff;
}
.voice-del {
position: absolute;
bottom: -480rpx;
width: 100%;
text-align: center;
color: #fff;
font-size: 28rpx;
}
}
</style>

@ -0,0 +1,180 @@
<template>
<uni-popup ref="popup" type="bottom">
<view class="group-member-selector">
<view class="top-bar">
<view class="top-tip">选择成员</view>
<button class="top-btn" type="warn" size="mini" @click="onClean()"> </button>
<button class="top-btn" type="primary" size="mini" @click="onOk()">({{ checkedIds.length }})
</button>
</view>
<scroll-view v-show="checkedIds.length > 0" scroll-x="true" scroll-left="120">
<view class="checked-users">
<view v-for="m in checkedMembers" class="user-item" :key="m.userId">
<head-image :name="m.showNickName" :url="m.headImage" :size="60"></head-image>
</view>
</view>
</scroll-view>
<view class="search-bar">
<uni-search-bar v-model="searchText" cancelButton="none" placeholder="搜索"></uni-search-bar>
</view>
<view class="member-items">
<virtual-scroller :items="showMembers">
<template v-slot="{ item }">
<view class="member-item" @click="onSwitchChecked(item)">
<head-image :name="item.showNickName" :online="item.online" :url="item.headImage"
:size="90"></head-image>
<view class="member-name">{{ item.showNickName }}
</view>
<view class="member-checked">
<radio :checked="item.checked" :disabled="item.locked"
@click.stop="onSwitchChecked(item)" />
</view>
</view>
</template>
</virtual-scroller>
</view>
</view>
</uni-popup>
</template>
<script>
export default {
name: "group-member-selector",
props: {
group: {
type: Object
},
members: {
type: Array
},
maxSize: {
type: Number,
default: 50
}
},
data() {
return {
searchText: "",
};
},
methods: {
init(checkedIds, lockedIds, hideIds) {
this.members.forEach((m) => {
m.checked = checkedIds.indexOf(m.userId) >= 0;
m.locked = lockedIds.indexOf(m.userId) >= 0;
m.hide = hideIds.indexOf(m.userId) >= 0;
});
},
open() {
this.$refs.popup.open();
},
onSwitchChecked(m) {
if (!m.locked) {
m.checked = !m.checked;
}
//
if (this.maxSize > 0 && this.checkedIds.length > this.maxSize) {
m.checked = false;
uni.showToast({
title: `最多选择${this.maxSize}位用户`,
icon: "none"
})
}
},
onClean() {
this.members.forEach((m) => {
if (!m.locked && m.checked) {
m.checked = false;
}
})
},
onOk() {
this.$refs.popup.close();
this.$emit("complete", this.checkedIds)
}
},
computed: {
checkedIds() {
return this.checkedMembers.map(m => m.userId)
},
checkedMembers() {
return this.members.filter((m) => !m.quit && !m.hide && m.checked);
},
showMembers() {
return this.members.filter(m => !m.quit && !m.hide && m.showNickName.includes(this.searchText))
}
}
}
</script>
<style lang="scss" scoped>
.group-member-selector {
position: relative;
display: flex;
flex-direction: column;
background-color: white;
padding: 10rpx;
border-radius: 15rpx 15rpx 0 0;
overflow: hidden;
.top-bar {
display: flex;
align-items: center;
height: 70rpx;
padding: 10rpx 30rpx;
.top-tip {
flex: 1;
}
.top-btn {
margin-left: 10rpx;
}
}
.checked-users {
display: flex;
align-items: center;
height: 90rpx;
padding: 0 30rpx;
.user-item {
padding: 3rpx;
}
}
.member-items {
position: relative;
flex: 1;
overflow: hidden;
.member-item {
height: 120rpx;
display: flex;
position: relative;
padding: 0 30rpx;
align-items: center;
background-color: white;
white-space: nowrap;
.member-name {
display: flex;
align-items: center;
flex: 1;
padding-left: 20rpx;
font-size: 30rpx;
font-weight: 600;
line-height: 60rpx;
white-space: nowrap;
overflow: hidden;
.uni-tag {
margin-left: 5rpx;
}
}
}
}
}
</style>

@ -0,0 +1,88 @@
<template>
<uni-popup ref="popup" type="center">
<uni-popup-dialog mode="base" :duration="2000" title="是否加入通话?" confirmText="加入" @confirm="onOk">
<div class="group-rtc-join">
<div class="host-info">
<div>发起人</div>
<head-image :name="rtcInfo.host.nickName" :url="rtcInfo.host.headImage" :size="80"></head-image>
</div>
<div class="user-info">
<div>{{ rtcInfo.userInfos.length + '人正在通话中' }}</div>
<scroll-view scroll-x="true" scroll-left="120">
<view class="user-list">
<view v-for="user in rtcInfo.userInfos" class="user-item" :key="user.id">
<head-image :name="user.nickName" :url="user.headImage" :size="80"></head-image>
</view>
</view>
</scroll-view>
</div>
</div>
</uni-popup-dialog>
</uni-popup>
</template>
<script>
export default {
data() {
return {
rtcInfo: {}
}
},
props: {
groupId: {
type: Number
}
},
methods: {
open(rtcInfo) {
this.rtcInfo = rtcInfo;
this.$refs.popup.open();
},
onOk() {
let users = this.rtcInfo.userInfos;
let mine = this.userStore.userInfo;
//
if (!users.find((user) => user.id == mine.id)) {
users.push({
id: mine.id,
nickName: mine.nickName,
headImage: mine.headImageThumb,
isCamera: false,
isMicroPhone: true
})
}
const userInfos = encodeURIComponent(JSON.stringify(users));
uni.navigateTo({
url: `/pages/chat/chat-group-video?groupId=${this.groupId}&isHost=false
&inviterId=${mine.id}&userInfos=${userInfos}`
})
}
}
}
</script>
<style lang="scss" scoped>
.group-rtc-join {
width: 100%;
.host-info {
font-size: 16px;
padding: 10px;
}
.user-info {
font-size: 16px;
padding: 10px;
}
.user-list {
display: flex;
align-items: center;
height: 90rpx;
.user-item {
padding: 3rpx;
}
}
}
</style>

@ -0,0 +1,124 @@
<template>
<view class="head-image" @click="showUserInfo($event)" :title="name">
<image class="avatar-image" v-if="url" :src="url" :style="avatarImageStyle" lazy-load="true"
mode="aspectFill" />
<view class="avatar-text" v-if="!url" :style="avatarTextStyle">
{{ (name ? name.substring(0, 1) : '').toUpperCase() }}
</view>
<view v-if="online" class="online" title="用户当前在线">
</view>
</view>
</template>
<script>
export default {
name: "head-image",
data() {
return {
colors: ["#5daa31", "#c7515a", "#e03697", "#85029b",
"#c9b455", "#326eb6"]
}
},
props: {
id: {
type: String
},
size: {
type: [Number, String],
default: 'default'
},
url: {
type: String
},
name: {
type: String,
default: null
},
online: {
type: Boolean,
default: false
},
},
methods: {
showUserInfo(e) {
if (this.id && this.id > 0) {
uni.navigateTo({
url: "/pages/common/user-info?id=" + this.id
})
}
}
},
computed: {
_size() {
if (typeof this.size === 'number') {
return this.size;
} else if (typeof this.size === 'string') {
const sizeMap = {
'default': 96,
'small': 84,
'smaller': 72,
'mini': 60,
'minier': 48,
'lage': 108,
'lager': 120,
};
return sizeMap[this.size] || 96;
}
return 96;
},
avatarImageStyle() {
return `width:${this._size}rpx;
height:${this._size}rpx;`
},
avatarTextStyle() {
return `width: ${this._size}rpx;
height:${this._size}rpx;
background-color:${this.name ? this.textColor : '#fff'};
font-size:${this._size * 0.5}rpx;
`
},
textColor() {
let hash = 0;
if (this.name) {
for (var i = 0; i < this.name.length; i++) {
hash += this.name.charCodeAt(i);
}
}
return this.colors[hash % this.colors.length];
}
}
}
</script>
<style scoped lang="scss">
.head-image {
position: relative;
cursor: pointer;
.avatar-image {
position: relative;
overflow: hidden;
border-radius: 50%;
vertical-align: bottom;
}
.avatar-text {
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.online {
position: absolute;
right: -10%;
bottom: 0;
width: 24rpx;
height: 24rpx;
background: limegreen;
border-radius: 50%;
border: 6rpx solid white;
}
}
</style>

@ -0,0 +1,62 @@
<template>
<view class="loading-box" :style="loadingStyle">
<view class="rotate iconfont icon-loading" :style="icontStyle"></view>
<slot></slot>
</view>
</template>
<script>
export default {
data() {
return {}
},
props: {
size: {
type: Number,
default: 100
},
mask: {
type: Boolean,
default: true
}
},
computed: {
icontStyle() {
return `font-size:${this.size}rpx`;
},
loadingStyle() {
return this.mask ? "background: rgba(0, 0, 0, 0.3);" : "";
}
}
}
</script>
<style lang="scss" scoped>
.loading-box {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
z-index: 10000;
display: flex;
justify-content: center;
align-items: center;
}
.rotate {
animation: rotate 2s ease-in-out infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg)
}
to {
transform: rotate(360deg)
}
}
</style>

@ -0,0 +1,114 @@
<template>
<view>
<view @longpress.stop="onLongPress($event)" @touchmove="onTouchMove" @touchend="onTouchEnd">
<slot></slot>
</view>
<view v-if="isShowMenu" class="menu-mask" @touchstart="onClose()" @contextmenu.prevent=""></view>
<view v-if="isShowMenu" class="menu" :style="menuStyle">
<view class="menu-item" v-for="(item) in items" :key="item.key" @click.prevent="onSelectMenu(item)">
<text :style="itemStyle(item)"> {{ item.name }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: "long-press-menu",
data() {
return {
isShowMenu: false,
isTouchMove: false,
menuStyle: "" //
}
},
props: {
items: {
type: Array,
default: () => [] //
}
},
methods: {
onLongPress(e) {
if (this.isTouchMove) return
uni.getSystemInfo({
success: (res) => {
const touches = e.touches[0]
let style = ""
if (touches.clientY > res.windowHeight / 2) {
style = `bottom:${res.windowHeight - touches.clientY}px;`
} else {
style = `top:${touches.clientY}px;`
}
if (touches.clientX > res.windowWidth / 2) {
style += `right:${res.windowWidth - touches.clientX}px;`
} else {
style += `left:${touches.clientX}px;`
}
this.menuStyle = style
this.$nextTick(() => {
this.isShowMenu = true
})
}
})
},
onTouchMove() {
this.onClose()
this.isTouchMove = true
},
onTouchEnd() {
this.isTouchMove = false
},
onSelectMenu(item) {
this.$emit("select", item)
this.isShowMenu = false
},
onClose() {
this.isShowMenu = false
},
itemStyle(item) {
return item.color ? `color:${item.color};` : `color:#000;`
}
}
}
</script>
<style lang="scss" scoped>
.menu-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 999;
}
.menu {
position: fixed;
border-radius: 4px;
overflow: hidden;
background-color: #fff;
z-index: 1000;
box-shadow: $im-box-shadow-dark;
.menu-item {
height: 28px;
min-width: 120rpx;
line-height: 28px;
font-size: $im-font-size-small;
display: flex;
padding: 6px 20px;
justify-content: flex-start;
&:hover {
background: $im-bg-active;
}
.menu-icon {
margin-right: 10rpx;
}
}
}
</style>

@ -6,31 +6,31 @@
export default {
name: "myVideo",
props: {
videoUrl: {
//
type: String,
default: "",
},
videoUrl: {
//
type: String,
default: "",
},
},
data() {
return {
onloadCode: `
const url = '${this.videoUrl}'
this.contentWindow.document.body.innerHTML = '<video style="width: 100%;height: 100%" controls="controls" src="'+url+'"></video>';
const iframe = top.document.getElementsByTagName('iframe')[0]
var evObj = document.createEvent('MouseEvents');
evObj.initEvent( 'event1', true, false );
iframe.dispatchEvent(evObj)
`
// onloadCode: `
// const url = 'https://vd3.bdstatic.com/mda-mja2qsy47mbyh1xc/sc/cae_h264_clips/1633918903861514556/mda-mja2qsy47mbyh1xc.mp4?auth_key=1634030413-0-0-7898726f7bd328302e2119fdf694fd5e&bcevod_channel=searchbox_feed&pd=1&pt=3&abtest='
// this.contentWindow.document.body.innerHTML = '<video style="width: 100%;height: 100%" controls="controls" src="'+url+'"></video>';
// const iframe = top.document.getElementsByTagName('iframe')[0]
// var evObj = document.createEvent('MouseEvents');
// evObj.initEvent( 'event1', true, false );
// iframe.dispatchEvent(evObj)
// `
};
return {
onloadCode: `
const url = '${this.videoUrl}'
this.contentWindow.document.body.innerHTML = '<video style="width: 100%;height: 100%" controls="controls" src="'+url+'"></video>';
const iframe = top.document.getElementsByTagName('iframe')[0]
var evObj = document.createEvent('MouseEvents');
evObj.initEvent( 'event1', true, false );
iframe.dispatchEvent(evObj)
`
// onloadCode: `
// const url = 'https://vd3.bdstatic.com/mda-mja2qsy47mbyh1xc/sc/cae_h264_clips/1633918903861514556/mda-mja2qsy47mbyh1xc.mp4?auth_key=1634030413-0-0-7898726f7bd328302e2119fdf694fd5e&bcevod_channel=searchbox_feed&pd=1&pt=3&abtest='
// this.contentWindow.document.body.innerHTML = '<video style="width: 100%;height: 100%" controls="controls" src="'+url+'"></video>';
// const iframe = top.document.getElementsByTagName('iframe')[0]
// var evObj = document.createEvent('MouseEvents');
// evObj.initEvent( 'event1', true, false );
// iframe.dispatchEvent(evObj)
// `
};
}
}
</script>
</script>

@ -0,0 +1,114 @@
<template>
<view class="im-nav-bar">
<!-- #ifndef H5 -->
<view style="height: var(--status-bar-height)"></view>
<!-- #endif -->
<view class="im-nav-bar-content">
<view class="back" @click="handleBackClick" v-if="back">
<uni-icons type="back" :size="iconFontSize"></uni-icons>
</view>
<view class="title" v-if="title">
<slot></slot>
</view>
<view class="btn">
<uni-icons class="btn-item" v-if="search" type="search" :size="iconFontSize"
@click="$emit('search')"></uni-icons>
<uni-icons class="btn-item" v-if="add" type="plusempty" :size="iconFontSize" @click="$emit('add')"></uni-icons>
<uni-icons class="btn-item" v-if="more" type="more-filled" :size="iconFontSize"
@click="$emit('more')"></uni-icons>
</view>
</view>
</view>
</template>
<script>
export default {
name: "nav-bar",
props: {
back: {
type: Boolean,
default: false
},
title: {
type: Boolean,
default: true
},
search: {
type: Boolean,
default: false
},
add: {
type: Boolean,
default: false
},
more: {
type: Boolean,
default: false
},
iconFontSize: {
type: Number,
default: 24
}
},
methods: {
handleBackClick() {
uni.navigateBack({
delta: 1
})
}
}
}
</script>
<style scoped lang="scss">
/* SCSS common/global.scss
$im-nav-bar-height, $im-text-color, $im-font-size-large, $im-border-light
*/
.im-nav-bar {
background-color: #fff;
position: fixed;
top: 0;
width: 100%;
color: $im-text-color;
border-bottom: 1px solid $im-border-light;
font-size: $im-font-size-large;
z-index: 99;
.im-nav-bar-content {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
height: $im-nav-bar-height;
.title {
// flex
}
.back {
position: absolute;
left: 0;
height: 100%;
display: flex;
align-items: center;
padding: 12px;
font-size: 22px;
box-sizing: border-box;
}
.btn {
position: absolute;
right: 0;
height: 100%;
display: flex;
padding: 12px;
align-items: center;
box-sizing: border-box;
.btn-item {
margin-left: 8px;
}
}
}
}
</style>

@ -0,0 +1,60 @@
//
$im-color-primary: #00a89b;
$im-color-primary-light-1: mix(#fff, $im-color-primary, 10%);
$im-color-primary-light-2: mix(#fff, $im-color-primary, 20%);
$im-color-primary-light-3: mix(#fff, $im-color-primary, 30%);
$im-color-primary-light-4: mix(#fff, $im-color-primary, 40%);
$im-color-primary-light-5: mix(#fff, $im-color-primary, 50%);
$im-color-primary-light-6: mix(#fff, $im-color-primary, 60%);
$im-color-primary-light-7: mix(#fff, $im-color-primary, 70%);
$im-color-primary-light-8: mix(#fff, $im-color-primary, 80%);
$im-color-primary-light-9: mix(#fff, $im-color-primary, 90%);
$im-color-primary-dark-1: mix(#000, $im-color-primary, 10%);
$im-color-primary-dark-2: mix(#000, $im-color-primary, 20%);
$im-color-primary-dark-3: mix(#000, $im-color-primary, 30%);
$im-color-primary-dark-4: mix(#000, $im-color-primary, 40%);
$im-color-success: #18bc37;
$im-color-warning: #f3a73f;
$im-color-danger: #e43d33;
$im-color-info: #8f939c;
//
$im-text-color: #000000;
$im-text-color-light: #6a6a6a;
$im-text-color-lighter: #909399;
$im-text-color-lighter-extra: #c7c7c7;
//
$im-border: #F0F0F0;
$im-border-light: #EDEDED;
$im-border-lighter: #DCDCDC;
$im-border-lighter-extra: #B9B9B9;
//
$im-font-size: 30rpx;
$im-font-size-small: 28rpx;
$im-font-size-smaller: 26rpx;
$im-font-size-smaller-extra: 24rpx;
$im-font-size-large: 32rpx;
$im-font-size-larger: 34rpx;
$im-font-size-larger-extra: 36rpx;
//
$im-box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);
$im-box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
$im-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, .12);
$im-box-shadow-dark: 0px 1px 10px 2px rgba($color: #a5a4a4, $alpha: 0.5);
//
$im-bg: #f7f7f7;
$im-bg-active: #f1f1f1;
//
$im-title-size: 26px;
$im-title-size-1: 22px;
$im-title-size-2: 18px;
$font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
$im-nav-bar-height: 45px;

@ -0,0 +1,217 @@
/** 原生button样式 **/
uni-button {
font-size: $im-font-size !important;
}
uni-button[type='primary'] {
color: #fff !important;
background-color: $im-color-primary !important;
}
uni-button[type='primary'][plain] {
color: $im-color-primary !important;
border: 1px solid $im-color-primary;
background-color: transparent;
}
uni-button[type='warn'] {
color: #fff !important;
background-color: $im-color-danger !important;
}
uni-button[type='warn'][plain] {
color: $im-color-danger !important;
border: 1px solid $im-color-danger !important;
background-color: transparent !important;
}
uni-button[size='mini'] {
font-size: $im-font-size-smaller !important;
}
// #ifdef MP-WEIXIN
// wxbutton,uni-botton
button {
font-size: $im-font-size !important;
}
button[type='primary'] {
color: #fff !important;
background-color: $im-color-primary !important;
}
button[type='primary'][plain] {
color: $im-color-primary !important;
border: 1px solid $im-color-primary;
background-color: transparent;
}
button[type='warn'] {
color: #fff !important;
background-color: $im-color-danger !important;
}
button[type='warn'][plain] {
color: $im-color-danger !important;
border: 1px solid $im-color-danger !important;
background-color: transparent !important;
}
button[size='mini'] {
font-size: $im-font-size-smaller !important;
}
// #endif
.button-hover[type='primary'] {
color: #fff !important;
background-color: $im-color-primary-dark-1 !important;
}
/** uni-ui input激活后边框、图标颜色 **/
.uni-easyinput__content.is-focused:not(.is-input-error-border) {
border-color: $im-color-primary-light-2 !important;
.content-clear-icon {
color: $im-color-primary-light-2 !important;
}
}
/** 底部导航 **/
.uni-tabbar-bottom .uni-tabbar {
box-shadow: $im-box-shadow;
}
.uni-tabbar-border {
display: none;
}
.segmented-control {
border-color: $im-color-primary !important;
.segmented-control__item--button {
border-color: $im-color-primary !important;
}
.segmented-control__item--button--active {
background-color: $im-color-primary !important;
.segmented-control__text{
color: #fff !important;
}
}
.segmented-control__text{
color: $im-color-primary !important;
}
}
.uni-radio-input svg{
border-color: white !important;
background-color: $im-color-primary !important;
}
.uni-radio-input svg {
background-color: $im-color-primary !important;
border-color: $im-color-primary !important;
background-clip: content-box !important;
box-sizing: border-box;
border-radius: 50%;
transform: translate(-50%, -50%) scale(0.7)!important;
}
.uni-radio-input svg path{
fill: $im-color-primary !important;
}
.uni-radio-input {
background-color: white !important;
border-color: $im-color-primary !important;
}
.uni-radio-input-disabled {
background-color: rgb(225, 225, 225) !important;
border-color: rgb(209, 209, 209) !important;
opacity: 0.5;
}
.uni-section__content-title {
font-size: $im-font-size !important;
color: $im-text-color-light;
}
.uni-forms-item__label {
color: $im-text-color;
font-size: $im-font-size !important;
}
.uni-forms-item {
//margin-bottom: 8px !important;
}
.uni-easyinput__content-input {
font-size: $im-font-size !important;
}
.uni-easyinput__placeholder-class {
color: $im-text-color-lighter;
font-size: $im-font-size !important;;
}
.uni-easyinput__content-textarea {
font-size: $im-font-size !important;;
}
.uni-input-input:disabled {
color: $im-text-color-light;
}
.uni-forms-item.is-direction-top .uni-forms-item__label {
padding: 0 !important;
}
.uni-data-checklist .checklist-group .checklist-box .checklist-content .checklist-text {
font-size: $im-font-size !important;
}
.uni-card .uni-card__content {
color: unset !important;
padding: 10px 0 !important;
}
.uni-tag-text--small{
font-size: 10px !important;
font-weight: bolder !important;
}
.nav-bar {
height: 100rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
background-color: white;
border-bottom: 1px solid $im-border;
.nav-search {
flex: 1;
}
.nav-add {
cursor: pointer;
}
}
.bottom-btn {
margin: 40rpx 40rpx;
uni-button + uni-button {
margin-top: 20rpx;
}
}
.emoji-large {
width: 64rpx !important;
height: 64rpx !important;
vertical-align: bottom !important;
}
.emoji-normal {
width: 54rpx !important;
height: 54rpx !important;
vertical-align: bottom !important;
}
.emoji-small {
width: 36rpx !important;
height: 36rpx !important;
vertical-align: bottom !important;
}

@ -1,9 +1,11 @@
import Vue from "vue";
import App from "./App";
//配置公共方法
//配置公共方法
import store from './store'
import * as enums from './common/enums.js';
import * as date from './common/date';
import * as messageType from './common/messageType';
import emotion from './common/emotion.js';
import url from './common/url.js';
// 导入网络请求的包
import { $http } from '@escook/request-miniprogram';
@ -12,21 +14,29 @@ uni.$http = $http;
// 请求的根路径
// #ifdef H5
$http.baseUrl = 'https://www.sanduolantoyoga.com/yoga'
// $http.baseUrl = 'http://192.168.13.9:8083'
// #endif
//#ifdef APP-PLUS
$http.baseUrl = 'https://www.sanduolantoyoga.com/yoga'
// $http.baseUrl = 'http://192.168.13.9:8083'
//#endif
// WebSocket 地址(可根据平台条件编译)
// #ifdef H5
// Vue.prototype.$wsUrl = 'ws://192.168.13.9:8528/im' // 开发环境
Vue.prototype.$wsUrl = 'wss://www.sanduolantoyoga.com/yoga-imserver' // 生产环境
// #endif
// #ifdef APP-PLUS
// Vue.prototype.$wsUrl = 'ws://192.168.13.9:8528/im' // 开发环境
Vue.prototype.$wsUrl = 'wss://www.sanduolantoyoga.com/yoga-imserver' // 生产环境
// #endif
// 请求开始之前做一些事情
$http.beforeRequest = function (options) {
// 判断是否需要登录
// 暂时隐藏加载
// uni.showLoading({
// title: '数据加载中...'
// });
var iftoken = uni.getStorageSync("iftoken");
if(iftoken){
// 去掉身份认证
@ -40,18 +50,18 @@ $http.beforeRequest = function (options) {
// 判断请求是否为有权限的 API 接口
if(token) {
// 为请求头添加身份认证
options.header = {
options.header = {
Authorization: token
}
}
uni.setStorageSync('iftoken', "");
}
};
// 请求完成之后做一些事情
$http.afterRequest = function (response) {
uni.hideLoading();
// 拦截未登录,跳转到登录
if (response.data.status == 10105) {
@ -65,7 +75,7 @@ $http.afterRequest = function (response) {
url: '/pages/login/login'
});
}, 2000);
}
//
else if (response.data.status == 10106) {
@ -89,7 +99,11 @@ Vue.config.productionTip = false;
Vue.prototype.isConnected = false; // ws是否已经连接;
Vue.prototype.heartbeatInterval = null; // 心跳;
Vue.prototype.heartbeatTimeout =20000; // ws20秒执行一次
Vue.prototype.$enums = enums
Vue.prototype.$date = date
Vue.prototype.$msgType = messageType
Vue.prototype.$emo = emotion
Vue.prototype.$url = url
App.mpType = "app";
// 引入全局uView
@ -97,7 +111,8 @@ import uView from "uview-ui";
Vue.use(uView);
const app = new Vue({
...App
store,
...App
})
app.$mount();

21
package-lock.json generated

@ -149,6 +149,11 @@
"to-regex-range": "^5.0.1"
}
},
"immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"immutable": {
"version": "5.0.3",
"resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.0.3.tgz",
@ -175,6 +180,22 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"optional": true
},
"lie": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/lie/-/lie-3.1.1.tgz",
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
"requires": {
"immediate": "~3.0.5"
}
},
"localforage": {
"version": "1.10.0",
"resolved": "https://registry.npmmirror.com/localforage/-/localforage-1.10.0.tgz",
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
"requires": {
"lie": "3.1.1"
}
},
"micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz",

@ -23,6 +23,7 @@
"dependencies": {
"@escook/request-miniprogram": "^0.2.1",
"@hyoga/uni-socket.io": "^3.0.4",
"localforage": "^1.10.0",
"mp-html": "^2.5.1",
"sass": "^1.86.0"
},

@ -53,7 +53,7 @@
},
{
"path" : "pages/category/detail",
"style" :
"style" :
{
"navigationBarTitleText": "",
"enablePullDownRefresh": false
@ -74,7 +74,7 @@
},
{
"path" : "pages/teacher/course",
"style" :
"style" :
{
"navigationBarTitleText": "课程表",
"enablePullDownRefresh": false
@ -82,7 +82,7 @@
},
{
"path" : "pages/user/mycourse",
"style" :
"style" :
{
"navigationBarTitleText": "我的课程",
"enablePullDownRefresh": false
@ -90,7 +90,7 @@
},
{
"path" : "pages/user/mycourseinfo",
"style" :
"style" :
{
"navigationBarTitleText": "课程详情",
"enablePullDownRefresh": false
@ -98,7 +98,7 @@
},
{
"path" : "pages/user/mycustom",
"style" :
"style" :
{
"navigationBarTitleText": "我的客户",
"enablePullDownRefresh": false
@ -106,7 +106,7 @@
},
{
"path" : "pages/user/mycollect",
"style" :
"style" :
{
"navigationBarTitleText": "我的收藏",
"enablePullDownRefresh": false
@ -114,7 +114,7 @@
},
{
"path" : "pages/user/tclist",
"style" :
"style" :
{
"navigationBarTitleText": "提成统计",
"enablePullDownRefresh": false
@ -122,7 +122,7 @@
},
{
"path" : "pages/user/tclist",
"style" :
"style" :
{
"navigationBarTitleText": "提成统计",
"enablePullDownRefresh": false
@ -130,7 +130,7 @@
},
{
"path" : "pages/user/kslist",
"style" :
"style" :
{
"navigationBarTitleText": "课时统计",
"enablePullDownRefresh": false
@ -138,7 +138,7 @@
},
{
"path" : "pages/user/xslist",
"style" :
"style" :
{
"navigationBarTitleText": "销售信息",
"enablePullDownRefresh": false
@ -146,7 +146,7 @@
},
{
"path" : "pages/user/cdlist",
"style" :
"style" :
{
"navigationBarTitleText": "场地销售",
"enablePullDownRefresh": false
@ -154,21 +154,30 @@
},
{
"path" : "pages/user/jslist",
"style" :
"style" :
{
"navigationBarTitleText": "教练费用",
"enablePullDownRefresh": false
}
},
{
"path" : "pages/chat/chat",
"style" :
{
"navigationBarTitleText": "教练咨询",
"enablePullDownRefresh": false
}
},
{
// {
// "path" : "pages/chat/chat",
// "style" :
// {
// "navigationBarTitleText": "教练咨询",
// "enablePullDownRefresh": false
// }
// },
{
"path" : "pages/chatbox/chat-box",
"style" :
{
"navigationBarTitleText": "消息2",
"enablePullDownRefresh": false
}
},
{
"path" : "pages/product/list",
"style": {
"navigationBarTextStyle": "black",
@ -215,7 +224,7 @@
},
{
"path" : "pages/chat/groupchat",
"style" :
"style" :
{
"navigationBarTitleText": "客服",
"enablePullDownRefresh": false,
@ -237,7 +246,7 @@
},
{
"path" : "pages/user/serviceInfo",
"style" :
"style" :
{
"navigationBarTitleText": "客服",
"enablePullDownRefresh": false
@ -331,6 +340,16 @@
}
}
},
{
"path": "pages/chatbox/chat",
"style": {
"enablePullDownRefresh": true,
"navigationBarBackgroundColor": "#00a89b",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/message/contact",
"style": {
@ -421,7 +440,7 @@
"enablePullDownRefresh": false
}
},
//
{
"path": "uni_modules/uni-upgrade-center-app/pages/upgrade-popup",
@ -438,7 +457,7 @@
}
}
}
],
"globalStyle": {
"pageOrientation": "auto",
@ -471,8 +490,14 @@
"iconPath": "static/image/icon/good.png",
"selectedIconPath": "/static/image/icon/good-active.png"
},
// {
// "pagePath": "pages/message/group",
// "text": "消息1",
// "iconPath": "/static/image/icon/message.png",
// "selectedIconPath": "/static/image/icon/message-active.png"
// },
{
"pagePath": "pages/message/group",
"pagePath": "pages/chatbox/chat",
"text": "消息",
"iconPath": "/static/image/icon/message.png",
"selectedIconPath": "/static/image/icon/message-active.png"

@ -85,6 +85,7 @@
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import { myCache,getRemoteFile } from '../../utils/utils.js';
import openlogin from "../components/openlogin.vue";
export default {
@ -353,6 +354,9 @@
],
};
},
computed: {
...mapGetters('chatStore', ['findChatIdx']), //
},
onLoad(option) {
if(option.id||option.id==0){
if(parseInt(option.id)==0){
@ -390,6 +394,7 @@
},100);
},
methods: {
...mapActions('chatStore', ['openChat']),
openQR(qr){
console.log("openQR")
this.qr='data:image/png;base64,'+qr;
@ -507,24 +512,33 @@
title: '会话创建中...'
});
//
var chatid="privatechat-" + this.userid +"-"+ item.teacherAppId;
var timestamp = new Date().getTime();
var info={
chatId: "privatechat-" + this.userid +"-"+ item.teacherAppId,
chatType: "coach",
chatName: item.teacherName,
chatAvatar: item.pic?item.pic:'/static/image/kf.png',
chatTime: timestamp,
userid: this.userid,
friendId: item.teacherAppId, // userid
minId: "", // id
sort:"privatechat", // privatechat groupchat
from:"yh" // yh message
}
var data=encodeURIComponent(JSON.stringify(info));
uni.navigateTo({
url: `/pages/chat/chat?data=${data}`
});
let chat = {
type: 'PRIVATE',
targetId: this.userid,
showName: item.teacherName,
headImage: item.pic?item.pic:'/static/image/kf.png',
};
this.openChat(chat);
let chatIdx = this.findChatIdx(chat);
uni.navigateTo({ url: `/pages/chatbox/chat-box?chatIdx=${chatIdx}` });
// var chatid="privatechat-" + this.userid +"-"+ item.teacherAppId;
// var timestamp = new Date().getTime();
// var info={
// chatId: "privatechat-" + this.userid +"-"+ item.teacherAppId,
// chatType: "coach",
// chatName: item.teacherName,
// chatAvatar: item.pic?item.pic:'/static/image/kf.png',
// chatTime: timestamp,
// userid: this.userid,
// friendId: item.teacherAppId, // userid
// minId: "", // id
// sort:"privatechat", // privatechat groupchat
// from:"yh" // yh message
// }
// var data=encodeURIComponent(JSON.stringify(info));
// uni.navigateTo({
// url: `/pages/chat/chat?data=${data}`
// });
}
else{
uni.showModal({

@ -1,8 +1,8 @@
<template>
<view class="content">
<!-- 聊天内容 -->
<scroll-view class="chat" scroll-y="true" scroll-with-animation="true" :scroll-into-view="scrollToView"
refresher-enabled="true" :refresher-triggered="triggered" :refresher-threshold="100"
<scroll-view class="chat" scroll-y="true" scroll-with-animation="true" :scroll-into-view="scrollToView"
refresher-enabled="true" :refresher-triggered="triggered" :refresher-threshold="100"
refresher-background="#f0f0f0" @refresherpulling="onPulling"
@refresherrefresh="onRefresh" @refresherrestore="onRestore" @refresherabort="onAbort">
<view class="chat-main" :style="{paddingBottom:inputh+'px'}">
@ -16,7 +16,7 @@
<image class="uimg" :src="item.headimg" @error="handleImageError($event,index)" mode="aspectFill"></image>
<text class="uname">{{item.fromname}}</text>
</view>
<view class="message" v-if="item.type == 'txt'">
<!-- 文字 -->
<view class="msg-text">
@ -41,9 +41,9 @@
{{item.time}}
</view>
</view>
<view class="message" v-if="item.type == 'product'">
<view v-if="item.send" class="msg-product" @click="gotoDetail(item)">
<view v-if="item.send.type==3" class="zx">
<view class="zxname">订单咨询</view>
@ -52,7 +52,7 @@
<view class="msg-con">
<image class="img" :src="item.send.pic" mode="aspectFit"></image>
<view class="info">
<view class="name">
<view class="name">
<text class="ntxt">{{item.send.name}}</text>
<text v-if="item.send.spData" v-for="(value, key) in item.send.spData" :key="key" class="txt">
{{ key }}: {{ value }}
@ -64,7 +64,7 @@
购买金额:¥ <text style="font-size: 30rpx;">{{item.send.price}}</text>{{(item.send.unit?'/'+item.send.unit:'')}}
</view>
<view v-else class="price">
¥{{item.send.price+(item.send.unit?'/'+item.send.unit:'')}}
¥{{item.send.price+(item.send.unit?'/'+item.send.unit:'')}}
</view>
<view v-if="item.send.quantity" class="qty">
{{item.send.quantity?'共('+item.send.quantity+(item.send.unit?item.send.unit:'件')+')':''}}
@ -79,9 +79,9 @@
</view>
</view>
</view>
</view>
</view>
<view class="msg-m msg-right" v-if="item.fromuser == userid">
<view class="user-img" >
@ -142,22 +142,22 @@
购买金额:¥ <text style="font-size: 30rpx;">{{item.send.price}}</text>{{(item.send.unit?'/'+item.send.unit:'')}}
</view>
<view v-else class="price">
¥{{item.send.price+(item.send.unit?'/'+item.send.unit:'')}}
¥{{item.send.price+(item.send.unit?'/'+item.send.unit:'')}}
</view>
<view class="qty">
{{item.send.quantity?'共('+item.send.quantity+(item.send.unit?item.send.unit:'件')+')':''}}
</view>
</view>
<button class="btn" type="primary" v-if="item.send.type==1"></button>
<button class="btn" type="primary" v-if="item.send.type==2"></button>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
@ -171,14 +171,14 @@
<text v-if="sendinfo.spData" v-for="(value, key) in sendinfo.spData" :key="key" class="txt">
{{ key }}: {{ value }}
</text>
</view>
</view>
<view class="xq">
<view class="list">
<view v-if="sendinfo.type==3" class="ord">
购买金额:¥ <text style="font-size: 30rpx;">{{sendinfo.price}}</text>{{(sendinfo.unit?'/'+sendinfo.unit:'')}}
</view>
<view v-else class="price">
¥{{sendinfo.price+(sendinfo.unit?'/'+sendinfo.unit:'')}}
¥{{sendinfo.price+(sendinfo.unit?'/'+sendinfo.unit:'')}}
</view>
<view class="qty">
{{sendinfo.quantity?'共('+sendinfo.quantity+(sendinfo.unit?sendinfo.unit:'件')+')':''}}
@ -196,7 +196,7 @@
</view>
<submit @inputs="inputs" @heights="heights"></submit>
<u-toast ref="uToast" />
</view>
</template>
@ -239,7 +239,7 @@
oldTime: new Date(),
inputh: '90',
ifaudio:false, //
// isConnected: false, // WebSocket
// isConnected: false, // WebSocket
// heartbeatInterval: null, // 20
// heartbeatTimeout: 20000, // 20
//
@ -268,13 +268,12 @@
if(options.data){
//
this.info=JSON.parse(decodeURIComponent(options.data));
console.log("chat",this.info)
//
var title = this.info.chatName;
uni.setNavigationBarTitle({
title: title,
});
if(this.info.sendinfo){
this.ifsend=true;
this.sendinfo=this.info.sendinfo;
@ -286,12 +285,12 @@
this.$forceUpdate();
}
}
},
onShow() {
//
this.getchatstore();
var that=this;
// :id
setTimeout(() => {
@ -301,7 +300,7 @@
that.reconnect();
}
}, 500);
},
methods: {
//
@ -343,8 +342,10 @@
// ws
socketinit(){
var that = this;
uni.connectSocket({
url: "wss://www.sanduolantoyoga.com/yoga-imserver/",
let socketTask = null;
socketTask=uni.connectSocket({
url: "ws://192.168.13.9:8528/im",
header: {
// 'content-type': 'application/json',
Authorization: uni.getStorageSync("token"),
@ -361,10 +362,10 @@
console.log(res,'socket连接成功complete');
}
});
uni.onSocketOpen(res => {
socketTask.onOpen(res => {
console.log('WebSocket连接已打开');
that.isConnected = true;
//
uni.sendSocketMessage({
data: JSON.stringify({
@ -382,31 +383,31 @@
console.log('消息发送失败!')
}
});
//
this.startHeartbeat();
this.startHeartbeat();
});
uni.onSocketClose(res => {
socketTask.onClose(res => {
console.log('WebSocket连接已关闭');
this.isConnected = false;
this.$forceUpdate();
// socket
this.reconnect();
});
uni.onSocketError(err => {
socketTask.onError(err => {
console.error('WebSocket连接打开失败请检查', err);
this.isConnected = false;
this.$forceUpdate();
// socket
this.reconnect();
});
uni.onSocketMessage(res => {
socketTask.onMessage(res => {
console.log('收到WebSocket服务器消息');
if(res.data){
let data=JSON.parse(res.data);
console.log("onSocketMessage",data);
if(data.data){
if(data.data){
data=data.data;
if(data){
//
@ -428,8 +429,8 @@
"fromname": this.info.chatName,
"fromuser": this.info.friendId,
"headimg": this.info.chatAvatar,
"toname": this.userName,
"touser": this.userid,
"toname": this.userName,
"touser": this.userid,
"content": data.content,
"time": data.type==3?5:0,
"ifaudio":data.type==3? true:false,
@ -440,7 +441,7 @@
"sendId": data.sendId
};
this.setMsglist(mdata)
// socket
var chatlastinfo={
id: this.info.chatId,
@ -473,7 +474,7 @@
}
}
});
},
gotoDetail(item){
var send=item.send;
@ -494,7 +495,7 @@
//
uni.navigateTo({
url: `/pages/order/orderinfo?id=${send.orderId}`
});
});
}
}
},
@ -505,6 +506,7 @@
},
//
sendServer(){
let data = {
"fromname": this.userName,
"fromuser": this.userid,
@ -518,6 +520,7 @@
"type": "product", // this.sendinfo.type==3?"5":"6" // 5 6
"send":this.sendinfo
};
this.unshiftmsg.push(data);
var rindex=this.unshiftmsg.length-1;
//
@ -551,11 +554,11 @@
try{
this.unshiftmsg=[];
//
var data= myCache(this.info.chatId)?myCache(this.info.chatId):[];
var data= myCache(this.info.chatId)?myCache(this.info.chatId):[];
data.forEach((cell,i)=>{
cell["ifaudio"]=false;
//
if (i < this.unshiftmsg.length - 1) {
if (i < this.unshiftmsg.length - 1) {
//
let t = dateTime.spaceTime(this.oldTime, cell.fromtime);
if (t) {
@ -585,7 +588,7 @@
"toname": this.userName, //
"touser": this.userid, //
"content": cont,
"minId":this.info.minId,
"minId":this.info.minId,
"time": 0,
"ifaudio":false,
"fromtime": new Date(),
@ -605,7 +608,7 @@
"toname": this.userName, //
"touser": this.userid, //
"content": cont,
"minId":this.info.minId,
"minId":this.info.minId,
"time": 0,
"ifaudio":false,
"fromtime": new Date(),
@ -613,7 +616,7 @@
}
];
this.$forceUpdate();
}
}
}
else{
// :id
@ -625,10 +628,10 @@
catch(err){
console.log(err);
}
// socket
this.socketinit();
},
// minId
ifpull(){
@ -651,7 +654,7 @@
return ret;
},
async setMsglist(data){
data.send=data.type==6?await this.getProduct(data.content):(data.type==5?await this.getOrder(data.content):null);
data.send=data.type==6?await this.getProduct(data.content):(data.type==5?await this.getOrder(data.content):null);
data.type=data.type==0?'txt':(data.type==1?'image':data.type==3?'audio':(data.type==4?'video':(data.type==5?'order':(data.type==6?'product':'')))),
this.unshiftmsg.push(data);
this.$forceUpdate();
@ -728,7 +731,7 @@
return rets;
}
},
//
async readed() {
const {data: res} = await uni.$http.put('/api/message/private/readed?friendId='+this.info.friendId);
@ -757,7 +760,7 @@
type=cell.type;
cell["ifaudio"]=false;
//
if (i < this.unshiftmsg.length - 1) {
if (i < this.unshiftmsg.length - 1) {
//
let t = dateTime.spaceTime(this.oldTime, cell.fromtime);
if (t) {
@ -771,7 +774,7 @@
}
else if(cell.type==0){
cell.content=decodeURIComponent(cell.content);
}
}
// // 0: 1: 2: 3: 4: 5 6 10, "" 11, " "12, " " 30,""
let mdata = {
"fromname": this.userName,
@ -790,17 +793,17 @@
};
this.unshiftmsg.unshift(mdata);
});
// :id
setTimeout(() => {
that.screendo(that.unshiftmsg.length-1);
}, 100);
//
myCache(this.info.chatId,this.unshiftmsg);
this.info.minId=this.unshiftmsg[this.unshiftmsg.length-1].id;
this.$forceUpdate();
//
var chatlastinfo={
id: this.info.chatId,
@ -815,13 +818,13 @@
sort:"privatechat"
};
this.updateChatList(chatlastinfo);
//
this.readed();
}
// }
},
screendo(scrindex){
this.scrollToView = '';
@ -947,7 +950,7 @@
console.log("inputs");
console.log(e);
var rindex=this.unshiftmsg.length-1;
let data = {
"fromname": this.userName,
"fromuser": this.userid,
@ -960,22 +963,21 @@
"fromtime": new Date(),
"type": e.type
};
if(e.type!=='video'){
this.unshiftmsg.push(data);
rindex=this.unshiftmsg.length-1;
// :id
this.screendo(this.unshiftmsg.length - 1);
}
//
var that = this;
if(e.type=='txt'){
//
if(e.type=='txt'){
//
//
this.onSendWS(data,rindex);
}
else if(e.type=='image'){
else if(e.type=='image'){
//
//
var token= uni.getStorageSync("token");
@ -993,10 +995,10 @@
success: res => {
console.log(res);
uni.hideLoading();
var rdata=JSON.parse(res.data);
var rdata=JSON.parse(res.data);
if(rdata.data&&rdata.data.originUrl){
var url=rdata.data&&rdata.data.originUrl?rdata.data.originUrl:'';
data.content= url;
data.content= url;
//
if (e.type == 'image'&&url) {
this.imgMsg.push(url);
@ -1017,9 +1019,9 @@
console.log(res)
}
});
}
else if(e.type=='audio'){
else if(e.type=='audio'){
//
//
var token= uni.getStorageSync("token");
@ -1064,7 +1066,7 @@
console.log(res)
}
});
}
else if(e.type=='video'){
//
@ -1110,14 +1112,16 @@
console.log(res)
}
});
}
},
//
async onSendWS(datamsg,rindex){
var message=JSON.stringify(datamsg);
var msssagebf=JSON.parse(message);
if(msssagebf.type=='txt'){
msssagebf.content=encodeURIComponent(msssagebf.content);
}
@ -1182,7 +1186,7 @@
send:this.sendinfo
};
this.updateChatList(chatlastinfo);
if(mtype=='5'||mtype=='6'){
//
this.ifsend=false;
@ -1274,7 +1278,7 @@
padding: 10rpx 12rpx !important;
border-radius: 10rpx;
}
.chat {
height: 100%;
@ -1431,7 +1435,7 @@
margin-left: 16rpx;
border-radius: 0rpx 20rpx 20rpx 20rpx;
}
.msg-img {
margin: 0 0 0 20rpx;
}
@ -1447,7 +1451,7 @@
padding-bottom: 4rpx;
margin-right: 10rpx;
}
.voicel{
.voicel{
height: 32rpx;
width: 32rpx;
}
@ -1457,7 +1461,7 @@
height: 32rpx;
background-size: 400%;
}
.msg-product{
display: flex;
flex-direction: column;
@ -1571,9 +1575,9 @@
border:none;
}
}
}
}
}
}
}
@ -1628,7 +1632,7 @@
.ms-img {
margin-right: 16rpx;
}
.msg-img {
margin: 0 20rpx 0 0;
}
@ -1663,7 +1667,7 @@
height: 32rpx;
background-size: 400%;
}
.msg-product{
display: flex;
flex-direction: column;
@ -1777,15 +1781,15 @@
border:none;
}
}
}
}
}
}
}
}
}
.voicePlay {
animation-name: voicePlay;
animation-duration: 1s;
@ -1793,7 +1797,7 @@
animation-iteration-count: infinite;
animation-timing-function: steps(3);
}
@keyframes voicePlay {
0% {
background-position: 0;
@ -1802,7 +1806,7 @@
background-position: 100%;
}
}
.popup{
position: fixed;
bottom: 180rpx;
@ -1907,5 +1911,5 @@
}
}
}
</style>

@ -1,8 +1,8 @@
<template>
<view class="content">
<!-- 聊天内容 -->
<scroll-view class="chat" scroll-y="true" scroll-with-animation="true" :scroll-into-view="scrollToView"
refresher-enabled="true" :refresher-triggered="triggered" :refresher-threshold="100"
<scroll-view class="chat" scroll-y="true" scroll-with-animation="true" :scroll-into-view="scrollToView"
refresher-enabled="true" :refresher-triggered="triggered" :refresher-threshold="100"
refresher-background="#f0f0f0" @refresherpulling="onPulling"
@refresherrefresh="onRefresh" @refresherrestore="onRestore" @refresherabort="onAbort">
<view class="chat-main" :style="{paddingBottom:inputh+'px'}">
@ -16,7 +16,7 @@
<image class="uimg" :src="item.headimg" @error="handleImageError($event,index)" mode="aspectFill"></image>
<text class="uname">{{item.fromname}}</text>
</view>
<view class="message" v-if="item.type == 'txt'">
<!-- 文字 -->
<view class="msg-text">
@ -41,9 +41,9 @@
{{item.time}}
</view>
</view>
<view class="message" v-if="item.type == 'product'">
<view v-if="item.send" class="msg-product" @click="gotoDetail(item)">
<view v-if="item.send.type==3" class="zx">
<view class="zxname">订单咨询</view>
@ -52,7 +52,7 @@
<view class="msg-con">
<image class="img" :src="item.send.pic" mode="aspectFit"></image>
<view class="info">
<view class="name">
<view class="name">
<text class="ntxt">{{item.send.name}}</text>
<text v-if="item.send.spData" v-for="(value, key) in item.send.spData" :key="key" class="txt">
{{ key }}: {{ value }}
@ -64,7 +64,7 @@
购买金额:¥ <text style="font-size: 30rpx;">{{item.send.price}}</text>{{(item.send.unit?'/'+item.send.unit:'')}}
</view>
<view v-else class="price">
¥{{item.send.price+(item.send.unit?'/'+item.send.unit:'')}}
¥{{item.send.price+(item.send.unit?'/'+item.send.unit:'')}}
</view>
<view v-if="item.send.quantity" class="qty">
{{item.send.quantity?'共('+item.send.quantity+(item.send.unit?item.send.unit:'件')+')':''}}
@ -79,9 +79,9 @@
</view>
</view>
</view>
</view>
</view>
<view class="msg-m msg-right" v-if="item.fromuser == userid">
<view class="user-img" >
@ -142,22 +142,22 @@
购买金额:¥ <text style="font-size: 30rpx;">{{item.send.price}}</text>{{(item.send.unit?'/'+item.send.unit:'')}}
</view>
<view v-else class="price">
¥{{item.send.price+(item.send.unit?'/'+item.send.unit:'')}}
¥{{item.send.price+(item.send.unit?'/'+item.send.unit:'')}}
</view>
<view class="qty">
{{item.send.quantity?'共('+item.send.quantity+(item.send.unit?item.send.unit:'件')+')':''}}
</view>
</view>
<button class="btn" type="primary" v-if="item.send.type==1"></button>
<button class="btn" type="primary" v-if="item.send.type==2"></button>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
@ -171,14 +171,14 @@
<text v-if="sendinfo.spData" v-for="(value, key) in sendinfo.spData" :key="key" class="txt">
{{ key }}: {{ value }}
</text>
</view>
</view>
<view class="xq">
<view class="list">
<view v-if="sendinfo.type==3" class="ord">
购买金额:¥ <text style="font-size: 30rpx;">{{sendinfo.price}}</text>{{(sendinfo.unit?'/'+sendinfo.unit:'')}}
</view>
<view v-else class="price">
¥{{sendinfo.price+(sendinfo.unit?'/'+sendinfo.unit:'')}}
¥{{sendinfo.price+(sendinfo.unit?'/'+sendinfo.unit:'')}}
</view>
<view class="qty">
{{sendinfo.quantity?'共('+sendinfo.quantity+(sendinfo.unit?sendinfo.unit:'件')+')':''}}
@ -196,7 +196,7 @@
</view>
<submit @inputs="inputs" @heights="heights"></submit>
<u-toast ref="uToast" />
</view>
</template>
@ -239,8 +239,8 @@
oldTime: new Date(),
inputh: '90',
ifaudio:false, //
socketTask: null, // WebSocket
isConnected: false, // WebSocket
socketTask: null, // WebSocket
isConnected: false, // WebSocket
heartbeatInterval: null, // 20
heartbeatTimeout: 20000, // 20
//
@ -275,7 +275,7 @@
uni.setNavigationBarTitle({
title: title,
});
if(this.info.sendinfo){
this.ifsend=true;
this.sendinfo=this.info.sendinfo;
@ -289,20 +289,20 @@
}
},
onShow() {
//
if(this.heartbeatInterval){
clearInterval(this.heartbeatInterval); //
}
//
this.getchatstore();
// :id
setTimeout(() => {
this.screendo(this.unshiftmsg.length - 1);
}, 500);
},
// onBackPress(options) {
// if (options.from === 'backbutton') {
@ -342,7 +342,7 @@
//
uni.navigateTo({
url: `/pages/order/orderinfo?id=${send.orderId}`
});
});
}
}
},
@ -399,11 +399,11 @@
try{
this.unshiftmsg=[];
//
var data= myCache(this.info.chatId)?myCache(this.info.chatId):[];
var data= myCache(this.info.chatId)?myCache(this.info.chatId):[];
data.forEach((cell,i)=>{
cell["ifaudio"]=false;
//
if (i < this.unshiftmsg.length - 1) {
if (i < this.unshiftmsg.length - 1) {
//
let t = dateTime.spaceTime(this.oldTime, cell.fromtime);
if (t) {
@ -433,7 +433,7 @@
"toname": this.userName, //
"touser": this.userid, //
"content": cont,
"minId":this.info.minId,
"minId":this.info.minId,
"time": 0,
"ifaudio":false,
"fromtime": new Date(),
@ -453,7 +453,7 @@
"toname": this.userName, //
"touser": this.userid, //
"content": cont,
"minId":this.info.minId,
"minId":this.info.minId,
"time": 0,
"ifaudio":false,
"fromtime": new Date(),
@ -461,7 +461,7 @@
}
];
this.$forceUpdate();
}
}
}
else{
// :id
@ -473,10 +473,10 @@
catch(err){
console.log(err);
}
// socket
this.socketinit();
},
// minId
ifpull(){
@ -505,7 +505,7 @@
this.heartbeatInterval = setInterval(() => {
if (this.socketTask) {
this.socketTask.send({
data: JSON.stringify({
data: JSON.stringify({
'cmd': 1,//
'data': {
'accessToken':uni.getStorageSync("token")
@ -519,7 +519,7 @@
socketinit(){
var that = this;
this.socketTask=uni.connectSocket({
url: "wss://www.sanduolantoyoga.com/yoga-imserver/",
url: "ws://192.168.13.9:8528/im",
header: {
// 'content-type': 'application/json',
Authorization: uni.getStorageSync("token"),
@ -539,7 +539,7 @@
uni.onSocketOpen(res => {
console.log('WebSocket连接已打开');
that.isConnected = true;
//
uni.sendSocketMessage({
data: JSON.stringify({
@ -557,17 +557,17 @@
console.log('消息发送失败!')
}
});
//
this.startHeartbeat();
this.startHeartbeat();
});
uni.onSocketMessage(res => {
console.log('收到WebSocket服务器消息');
if(res.data){
let data=JSON.parse(res.data);
console.log(data);
if(data.data){
if(data.data){
data=data.data;
if(data){
if(this.ifadd(data.id)){
@ -587,8 +587,8 @@
"fromname": this.info.chatName,
"fromuser": this.info.friendId,
"headimg": this.info.chatAvatar,
"toname": this.userName,
"touser": this.userid,
"toname": this.userName,
"touser": this.userid,
"content": data.content,
"time": data.type==3?5:0,
"ifaudio":data.type==3? true:false,
@ -599,7 +599,7 @@
"sendId": data.sendId
};
this.setMsglist(mdata)
// socket
var chatlastinfo={
id: this.info.chatId,
@ -641,16 +641,16 @@
clearInterval(that.heartbeatInterval); //
});
},
// WebSocket
closeWebSocket() {
if (this.socket) {
uni.closeSocket();
this.socket = null;
this.isConnected = false;
// WebSocket
closeWebSocket() {
if (this.socket) {
uni.closeSocket();
this.socket = null;
this.isConnected = false;
}
},
async setMsglist(data){
data.send=data.type==6?await this.getProduct(data.content):(data.type==5?await this.getOrder(data.content):null);
data.send=data.type==6?await this.getProduct(data.content):(data.type==5?await this.getOrder(data.content):null);
data.type=data.type==0?'txt':(data.type==1?'image':data.type==3?'audio':(data.type==4?'video':(data.type==5?'order':(data.type==6?'product':'')))),
// console.log("setMsglist",data)
this.unshiftmsg.push(data);
@ -728,7 +728,7 @@
return rets;
}
},
//
async readed() {
const {data: res} = await uni.$http.put('/api/message/private/readed?friendId='+this.info.friendId);
@ -757,7 +757,7 @@
type=cell.type;
cell["ifaudio"]=false;
//
if (i < this.unshiftmsg.length - 1) {
if (i < this.unshiftmsg.length - 1) {
//
let t = dateTime.spaceTime(this.oldTime, cell.fromtime);
if (t) {
@ -771,7 +771,7 @@
}
else if(cell.type==0){
cell.content=decodeURIComponent(cell.content);
}
}
// // 0: 1: 2: 3: 4: 5 6 10, "" 11, " "12, " " 30,""
let mdata = {
"fromname": this.userName,
@ -790,19 +790,19 @@
};
this.unshiftmsg.unshift(mdata);
});
// :id
setTimeout(() => {
that.screendo(that.unshiftmsg.length-1);
}, 100);
//
myCache(this.info.chatId,this.unshiftmsg);
this.info.minId=this.unshiftmsg[this.unshiftmsg.length-1].id;
// Id
// this.savaMaxId({id:this.info.chatId,minId:this.info.minId});
this.$forceUpdate();
// 线
var chatlastinfo={
id: this.info.chatId,
@ -817,13 +817,13 @@
sort:"privatechat"
};
this.updateChatList(chatlastinfo);
//
this.readed();
}
// }
},
screendo(scrindex){
this.scrollToView = '';
@ -949,7 +949,7 @@
console.log("inputs");
console.log(e);
var rindex=this.unshiftmsg.length-1;
let data = {
"fromname": this.userName,
"fromuser": this.userid,
@ -962,22 +962,22 @@
"fromtime": new Date(),
"type": e.type
};
if(e.type!=='video'){
this.unshiftmsg.push(data);
rindex=this.unshiftmsg.length-1;
// :id
this.screendo(this.unshiftmsg.length - 1);
}
//
var that = this;
if(e.type=='txt'){
//
if(e.type=='txt'){
//
//
this.onSendWS(data,rindex);
}
else if(e.type=='image'){
else if(e.type=='image'){
//
//
var token= uni.getStorageSync("token");
@ -995,10 +995,10 @@
success: res => {
console.log(res);
uni.hideLoading();
var rdata=JSON.parse(res.data);
var rdata=JSON.parse(res.data);
if(rdata.data&&rdata.data.originUrl){
var url=rdata.data&&rdata.data.originUrl?rdata.data.originUrl:'';
data.content= url;
data.content= url;
//
if (e.type == 'image'&&url) {
this.imgMsg.push(url);
@ -1019,9 +1019,9 @@
console.log(res)
}
});
}
else if(e.type=='audio'){
else if(e.type=='audio'){
//
//
var token= uni.getStorageSync("token");
@ -1066,7 +1066,7 @@
console.log(res)
}
});
}
else if(e.type=='video'){
//
@ -1112,9 +1112,9 @@
console.log(res)
}
});
}
},
//
async onSendWS(datamsg,rindex){
@ -1184,7 +1184,7 @@
send:this.sendinfo
};
this.updateChatList(chatlastinfo);
if(mtype=='5'||mtype=='6'){
//
this.ifsend=false;
@ -1276,7 +1276,7 @@
padding: 10rpx 12rpx !important;
border-radius: 10rpx;
}
.chat {
height: 100%;
@ -1433,7 +1433,7 @@
margin-left: 16rpx;
border-radius: 0rpx 20rpx 20rpx 20rpx;
}
.msg-img {
margin: 0 0 0 20rpx;
}
@ -1449,7 +1449,7 @@
padding-bottom: 4rpx;
margin-right: 10rpx;
}
.voicel{
.voicel{
height: 32rpx;
width: 32rpx;
}
@ -1459,7 +1459,7 @@
height: 32rpx;
background-size: 400%;
}
.msg-product{
display: flex;
flex-direction: column;
@ -1573,9 +1573,9 @@
border:none;
}
}
}
}
}
}
}
@ -1630,7 +1630,7 @@
.ms-img {
margin-right: 16rpx;
}
.msg-img {
margin: 0 20rpx 0 0;
}
@ -1665,7 +1665,7 @@
height: 32rpx;
background-size: 400%;
}
.msg-product{
display: flex;
flex-direction: column;
@ -1779,15 +1779,15 @@
border:none;
}
}
}
}
}
}
}
}
}
.voicePlay {
animation-name: voicePlay;
animation-duration: 1s;
@ -1795,7 +1795,7 @@
animation-iteration-count: infinite;
animation-timing-function: steps(3);
}
@keyframes voicePlay {
0% {
background-position: 0;
@ -1804,7 +1804,7 @@
background-position: 100%;
}
}
.popup{
position: fixed;
bottom: 180rpx;
@ -1909,5 +1909,5 @@
}
}
}
</style>

@ -1,8 +1,8 @@
<template>
<view class="content">
<!-- 聊天内容 -->
<scroll-view class="chat" scroll-y="true" scroll-with-animation="true" :scroll-into-view="scrollToView"
refresher-enabled="true" :refresher-triggered="triggered" :refresher-threshold="100"
<scroll-view class="chat" scroll-y="true" scroll-with-animation="true" :scroll-into-view="scrollToView"
refresher-enabled="true" :refresher-triggered="triggered" :refresher-threshold="100"
refresher-background="#f0f0f0" @refresherpulling="onPulling"
@refresherrefresh="onRefresh" @refresherrestore="onRestore" @refresherabort="onAbort">
<view class="chat-main" :style="{paddingBottom:inputh+'px'}">
@ -16,7 +16,7 @@
<image class="uimg" :src="item.headimg" @error="handleImageError($event,index)" mode="aspectFill"></image>
<text class="uname">{{item.fromname}}</text>
</view>
<view class="message" v-if="item.type == 'txt'">
<!-- 文字 -->
<view class="msg-text">
@ -41,9 +41,9 @@
{{item.time}}
</view>
</view>
<view class="message" v-if="item.type == 'product'">
<view v-if="item.send" class="msg-product" @click="gotoDetail(item)">
<view v-if="item.send.type==3" class="zx">
<view class="zxname">订单咨询</view>
@ -52,7 +52,7 @@
<view class="msg-con">
<image class="img" :src="item.send.pic" mode="aspectFit"></image>
<view class="info">
<view class="name">
<view class="name">
<text class="ntxt">{{item.send.name}}</text>
<text v-if="item.send.spData" v-for="(value, key) in item.send.spData" :key="key" class="txt">
{{ key }}: {{ value }}
@ -64,7 +64,7 @@
购买金额:¥ <text style="font-size: 30rpx;">{{item.send.price}}</text>{{(item.send.unit?'/'+item.send.unit:'')}}
</view>
<view v-else class="price">
¥{{item.send.price+(item.send.unit?'/'+item.send.unit:'')}}
¥{{item.send.price+(item.send.unit?'/'+item.send.unit:'')}}
</view>
<view v-if="item.send.quantity" class="qty">
{{item.send.quantity?'共('+item.send.quantity+(item.send.unit?item.send.unit:'件')+')':''}}
@ -79,9 +79,9 @@
</view>
</view>
</view>
</view>
</view>
<view class="msg-m msg-right" v-if="item.fromuser == userid">
<view class="user-img" >
@ -94,7 +94,7 @@
<view class="read" @click="openRead(item)">{{item.read?item.read.length:0}}</view>
<view class="noread" @click="openNoRead(item)">{{item.read?3-item.read.length:3}}</view>
</view>
<view class="msg-text">
<text selectable>{{item.content}}</text>
</view>
@ -153,22 +153,22 @@
购买金额:¥ <text style="font-size: 30rpx;">{{item.send.price}}</text>{{(item.send.unit?'/'+item.send.unit:'')}}
</view>
<view v-else class="price">
¥{{item.send.price+(item.send.unit?'/'+item.send.unit:'')}}
¥{{item.send.price+(item.send.unit?'/'+item.send.unit:'')}}
</view>
<view class="qty">
{{item.send.quantity?'共('+item.send.quantity+(item.send.unit?item.send.unit:'件')+')':''}}
</view>
</view>
<button class="btn" type="primary" v-if="item.send.type==1"></button>
<button class="btn" type="primary" v-if="item.send.type==2"></button>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
@ -182,14 +182,14 @@
<text v-if="sendinfo.spData" v-for="(value, key) in sendinfo.spData" :key="key" class="txt">
{{ key }}: {{ value }}
</text>
</view>
</view>
<view class="xq">
<view class="list">
<view v-if="sendinfo.type==3" class="ord">
购买金额:¥ <text style="font-size: 30rpx;">{{sendinfo.price}}</text>{{(sendinfo.unit?'/'+sendinfo.unit:'')}}
</view>
<view v-else class="price">
¥{{sendinfo.price+(sendinfo.unit?'/'+sendinfo.unit:'')}}
¥{{sendinfo.price+(sendinfo.unit?'/'+sendinfo.unit:'')}}
</view>
<view class="qty">
{{sendinfo.quantity?'共('+sendinfo.quantity+(sendinfo.unit?sendinfo.unit:'件')+')':''}}
@ -207,7 +207,7 @@
</view>
<submit @inputs="inputs" @heights="heights"></submit>
<u-toast ref="uToast" />
<!-- 群公告 -->
<uni-popup ref="popup" background-color="#fff">
<view class="popup-content" :class="{ 'popup-height': 'right' }">
@ -249,7 +249,7 @@
</view>
</view>
</uni-popup>
<!-- 未读已读 -->
<uni-popup ref="popupRead" background-color="#fff">
<view class="popup-content" :class="{ 'popup-height': 'right' }">
@ -274,7 +274,7 @@
</view>
</view>
</uni-popup>
</view>
</template>
@ -302,22 +302,22 @@
// chatName: data.name,
// chatAvatar: data.headImage,
// chatTime: timestamp,
// userid: data.ownerId,
// userid: data.ownerId,
// friendId: data.customerService, // userid
// teacherId: data.instructor, // userid
// minId: "", // id
// sort:"groupchat", // privatechat groupchat
// from:"yh", // yh message
// notice: data.notice,
// remarkNickName: data.remarkNickName,
// showNickName: data.showNickName,
// showGroupName: data.showGroupName,
// remarkGroupName: data.remarkGroupName,
// reason: data.reason,
// customerService: data.customerService,
// instructor: data.instructor,
// productId: data.productId,
// productName: data.productName,
// notice: data.notice,
// remarkNickName: data.remarkNickName,
// showNickName: data.showNickName,
// showGroupName: data.showGroupName,
// remarkGroupName: data.remarkGroupName,
// reason: data.reason,
// customerService: data.customerService,
// instructor: data.instructor,
// productId: data.productId,
// productName: data.productName,
// sendinfo:{
// type:1,// 1 2 3
// id:this.product.id,
@ -348,7 +348,7 @@
oldTime: new Date(),
inputh: '90',
ifaudio:false, //
// isConnected: false, // WebSocket
// isConnected: false, // WebSocket
// heartbeatInterval: null, // 20
// heartbeatTimeout: 20000, // 20
//
@ -379,7 +379,7 @@
this.userName=user.nickName?user.nickName:"";
this.userid=user.userid?user.userid:"";
this.userheadimg=user.avatar?user.avatar:require("@/static/image/girl.png");
if(options.data){
//
this.info=JSON.parse(decodeURIComponent(options.data));
@ -389,7 +389,7 @@
uni.setNavigationBarTitle({
title: title,
});
if(this.info.sendinfo){
this.ifsend=true;
this.sendinfo=this.info.sendinfo;
@ -400,18 +400,18 @@
this.sendinfo=null;
this.$forceUpdate();
}
}
},
onShow() {
//
this.getchatstore();
//
this.getrwinfo();
var that=this;
// :id
setTimeout(() => {
@ -421,12 +421,12 @@
that.reconnect();
}
}, 500);
this.getGroupInfo(this.info.groupId);
},
// buttons
onNavigationBarButtonTap: async function(e) {
onNavigationBarButtonTap: async function(e) {
const index = e.index;
if (index === 0) {
this.$refs.popup.open("right");
@ -474,7 +474,7 @@
var that = this;
if (!this.isConnected) {
uni.connectSocket({
url: "wss://www.sanduolantoyoga.com/yoga-imserver/",
url: "ws://192.168.13.9:8528/im",
header: {
// 'content-type': 'application/json',
Authorization: uni.getStorageSync("token"),
@ -559,13 +559,13 @@
}
}
}
}
}
});
}
},
//
async getGroupInfo(id) {
const {data: res} = await uni.$http.get('/api/group/find/'+id);
@ -573,7 +573,7 @@
var data = res.data;
}
},
//
async getrwinfo(){
//
@ -596,7 +596,7 @@
this.$forceUpdate();
}
}
//
if(this.userid==this.info.instructor){
this.info.instructorname=this.userName;
@ -617,7 +617,7 @@
this.$forceUpdate();
}
}
//
if(this.userid==this.info.ownerId){
this.info.ownername=this.userName;
@ -638,7 +638,7 @@
this.$forceUpdate();
}
}
},
gotoDetail(item){
var send=item.send;
@ -659,7 +659,7 @@
//
uni.navigateTo({
url: `/pages/order/orderinfo?id=${send.orderId}`
});
});
}
}
},
@ -719,7 +719,7 @@
data.forEach((cell,i)=>{
cell["ifaudio"]=false;
//
if (i < this.unshiftmsg.length - 1) {
if (i < this.unshiftmsg.length - 1) {
//
let t = dateTime.spaceTime(this.oldTime, cell.fromtime);
if (t) {
@ -735,8 +735,8 @@
cell.content=decodeURIComponent(cell.content);
}
this.unshiftmsg.push(cell);
});
});
if(this.unshiftmsg.length<=0)
{
if(this.info.from=="yh"){
@ -750,7 +750,7 @@
"toname": this.userName, //
"touser": this.userid, //
"content": cont,
"minId":this.info.minId,
"minId":this.info.minId,
"time": 0,
"ifaudio":false,
"fromtime": new Date(),
@ -770,7 +770,7 @@
"toname": this.userName, //
"touser": this.userid, //
"content": cont,
"minId":this.info.minId,
"minId":this.info.minId,
"time": 0,
"ifaudio":false,
"fromtime": new Date(),
@ -792,7 +792,7 @@
catch(err){
console.log(err);
}
// socket
this.socketinit();
},
@ -821,9 +821,9 @@
var headImage="/static/image/girl.png";
const {data: res} = await uni.$http.get("/api/friend/find/"+data.sendId);
if(res.data){
//
//
nickName=res.data.nickName?res.data.nickName:"客服";
headImage=res.data.headImage?res.data.headImage:'/static/image/girl.png';
headImage=res.data.headImage?res.data.headImage:'/static/image/girl.png';
this.msgMerge(data,nickName,headImage);
}
else{
@ -838,13 +838,13 @@
if(data.type == 1) {
this.imgMsg.unshift(data.content)
}
let mdata = {
"fromname": nickName, //
"fromuser": data.sendId, //
"headimg": headImage,//
"toname": this.userName,
"touser": this.userid,
"toname": this.userName,
"touser": this.userid,
"content": data.content,
"readedCount": data.readedCount,
"time": data.type==3?5:0,
@ -861,12 +861,12 @@
setTimeout(() => {
this.screendo(this.unshiftmsg.length-1);
}, 100);
//
myCache(this.info.chatId,this.unshiftmsg);
myCache(this.info.chatId,this.unshiftmsg);
// Id
this.savaMaxId({id:this.info.chatId,minId:data.id});
// socket
var chatlastinfo={
id: this.info.chatId,
@ -980,7 +980,7 @@
type=cell.type;
cell["ifaudio"]=false;
//
if (i < this.unshiftmsg.length - 1) {
if (i < this.unshiftmsg.length - 1) {
//
let t = dateTime.spaceTime(this.oldTime, cell.fromtime);
if (t) {
@ -994,7 +994,7 @@
}
else if(cell.type==0){
cell.content=decodeURIComponent(cell.content);
}
}
// // 0: 1: 2: 3: 4: 5 6 10, "" 11, " "12, " " 30,""
let mdata = {
"fromname": this.userName,
@ -1024,7 +1024,7 @@
this.$forceUpdate();
// Id
this.savaMaxId({id:this.info.chatId,minId:this.info.minId});
// 线
var chatlastinfo={
id: this.info.chatId,
@ -1046,7 +1046,7 @@
// }
//
this.readed();
},
screendo(scrindex){
this.scrollToView = '';
@ -1172,7 +1172,7 @@
console.log("inputs");
console.log(e);
var rindex=this.unshiftmsg.length-1;
let data = {
"fromname": this.userName,
"fromuser": this.userid,
@ -1185,22 +1185,22 @@
"fromtime": new Date(),
"type": e.type
};
if(e.type!=='video'){
this.unshiftmsg.push(data);
rindex=this.unshiftmsg.length-1;
// :id
this.screendo(this.unshiftmsg.length - 1);
}
//
var that = this;
if(e.type=='txt'){
//
if(e.type=='txt'){
//
//
this.onSendWS(data,rindex);
}
else if(e.type=='image'){
else if(e.type=='image'){
//
//
var token= uni.getStorageSync("token");
@ -1218,10 +1218,10 @@
success: res => {
console.log(res);
uni.hideLoading();
var rdata=JSON.parse(res.data);
var rdata=JSON.parse(res.data);
if(rdata.data&&rdata.data.originUrl){
var url=rdata.data&&rdata.data.originUrl?rdata.data.originUrl:'';
data.content= url;
data.content= url;
//
if (e.type == 'image'&&url) {
this.imgMsg.push(url);
@ -1242,9 +1242,9 @@
console.log(res)
}
});
}
else if(e.type=='audio'){
else if(e.type=='audio'){
//
//
var token= uni.getStorageSync("token");
@ -1289,7 +1289,7 @@
console.log(res)
}
});
}
else if(e.type=='video'){
//
@ -1335,9 +1335,9 @@
console.log(res)
}
});
}
},
//
async onSendWS(datamsg,rindex){
@ -1390,7 +1390,7 @@
myCache(this.info.chatId,this.unshiftmsg);
// Id
this.savaMaxId({id:this.info.chatId,minId:rrdata.id});
//
var chatlastinfo={
id:this.info.chatId,
@ -1405,14 +1405,14 @@
teacherId: this.info.teacherId?this.info.teacherId:this.info.instructor, // userid
fromuser: this.info.friendId,
customerService: this.info.customerService,
instructor: this.info.instructor,
ownerId: this.info.ownerId,
instructor: this.info.instructor,
ownerId: this.info.ownerId,
img: this.info.chatAvatar,
sort:"groupchat",
send:this.sendinfo
};
this.updateChatList(chatlastinfo);
if(mtype=='5'||mtype=='6'){
//
this.ifsend=false;
@ -1503,7 +1503,7 @@
this.unshiftmsg[index]["headimg"]= require("@/static/image/girl.png");
this.$forceUpdate();
},
//
openNoRead(item){
this.ifread=false;
@ -1535,7 +1535,7 @@
})
}
});
}
//
var that=this;
@ -1557,7 +1557,7 @@
name:this.info.instructorname
})
}
tmp = this.readinfo.read.some(val => {
return val.id == that.info.ownerId;
});
@ -1600,7 +1600,7 @@
})
}
});
}
//
var that=this;
@ -1622,7 +1622,7 @@
name:this.info.instructorname
})
}
tmp = this.readinfo.read.some(val => {
return val.id == that.info.ownerId;
});
@ -1663,7 +1663,7 @@
padding: 10rpx 12rpx !important;
border-radius: 10rpx;
}
.chat {
height: 100%;
@ -1819,7 +1819,7 @@
margin-left: 16rpx;
border-radius: 0rpx 20rpx 20rpx 20rpx;
}
.msg-img {
margin: 0 0 0 20rpx;
}
@ -1835,7 +1835,7 @@
padding-bottom: 4rpx;
margin-right: 10rpx;
}
.voicel{
.voicel{
height: 32rpx;
width: 32rpx;
}
@ -1845,7 +1845,7 @@
height: 32rpx;
background-size: 400%;
}
.msg-product{
display: flex;
flex-direction: column;
@ -1959,9 +1959,9 @@
border:none;
}
}
}
}
}
}
}
@ -1989,7 +1989,7 @@
justify-content: flex-end;
align-items: flex-end;
}
.feed-imgy{
display: flex;
flex-direction: column;
@ -2022,7 +2022,7 @@
.ms-img {
margin-right: 16rpx;
}
.msg-img {
margin: 0 20rpx 0 0;
}
@ -2057,7 +2057,7 @@
height: 32rpx;
background-size: 400%;
}
.msg-product{
display: flex;
flex-direction: column;
@ -2171,15 +2171,15 @@
border:none;
}
}
}
}
}
}
}
}
}
.voicePlay {
animation-name: voicePlay;
animation-duration: 1s;
@ -2187,7 +2187,7 @@
animation-iteration-count: infinite;
animation-timing-function: steps(3);
}
@keyframes voicePlay {
0% {
background-position: 0;
@ -2196,7 +2196,7 @@
background-position: 100%;
}
}
.popup{
position: fixed;
bottom: 180rpx;
@ -2301,7 +2301,7 @@
}
}
}
.example {
width: 550rpx;
opacity: 1;
@ -2360,7 +2360,7 @@
flex: 1;
}
}
.chatlist{
display: flex;
flex-wrap: wrap;
@ -2403,6 +2403,6 @@
}
}
}
</style>

@ -1,8 +1,8 @@
<template>
<view class="content">
<!-- 聊天内容 -->
<scroll-view class="chat" scroll-y="true" scroll-with-animation="true" :scroll-into-view="scrollToView"
refresher-enabled="true" :refresher-triggered="triggered" :refresher-threshold="100"
<scroll-view class="chat" scroll-y="true" scroll-with-animation="true" :scroll-into-view="scrollToView"
refresher-enabled="true" :refresher-triggered="triggered" :refresher-threshold="100"
refresher-background="#f0f0f0" @refresherpulling="onPulling"
@refresherrefresh="onRefresh" @refresherrestore="onRestore" @refresherabort="onAbort">
<view class="chat-main" :style="{paddingBottom:inputh+'px'}">
@ -16,7 +16,7 @@
<image class="uimg" :src="item.headimg" @error="handleImageError($event,index)"></image>
<text class="uname">{{item.fromname}}</text>
</view>
<view class="message" v-if="item.type == 'txt'">
<!-- 文字 -->
<view class="msg-text">
@ -41,9 +41,9 @@
{{item.time}}
</view>
</view>
<view class="message" v-if="item.type == 'product'">
<view v-if="item.send" class="msg-product" @click="gotoDetail(item)">
<view v-if="item.send.type==3" class="zx">
<view class="zxname">订单咨询</view>
@ -52,7 +52,7 @@
<view class="msg-con">
<image class="img" :src="item.send.pic" mode="aspectFit"></image>
<view class="info">
<view class="name">
<view class="name">
<text class="ntxt">{{item.send.name}}</text>
<text v-if="item.send.spData" v-for="(value, key) in item.send.spData" :key="key" class="txt">
{{ key }}: {{ value }}
@ -64,7 +64,7 @@
购买金额:¥ <text style="font-size: 30rpx;">{{item.send.price}}</text>{{(item.send.unit?'/'+item.send.unit:'')}}
</view>
<view v-else class="price">
¥{{item.send.price+(item.send.unit?'/'+item.send.unit:'')}}
¥{{item.send.price+(item.send.unit?'/'+item.send.unit:'')}}
</view>
<view v-if="item.send.quantity" class="qty">
{{item.send.quantity?'共('+item.send.quantity+(item.send.unit?item.send.unit:'件')+')':''}}
@ -79,9 +79,9 @@
</view>
</view>
</view>
</view>
</view>
<view class="msg-m msg-right" v-if="item.fromuser == userid">
<view class="user-img" >
@ -132,27 +132,27 @@
购买金额:¥ <text style="font-size: 30rpx;">{{item.send.price}}</text>{{(item.send.unit?'/'+item.send.unit:'')}}
</view>
<view v-else class="price">
¥{{item.send.price+(item.send.unit?'/'+item.send.unit:'')}}
¥{{item.send.price+(item.send.unit?'/'+item.send.unit:'')}}
</view>
<view class="qty">
{{item.send.quantity?'共('+item.send.quantity+(item.send.unit?item.send.unit:'件')+')':''}}
</view>
</view>
<button class="btn" type="primary" v-if="item.send.type==1"></button>
<button class="btn" type="primary" v-if="item.send.type==2"></button>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="popup" v-if="ifsend&&sendinfo">
<view class="tipcon" click="gotoXQ()">
<image class="img" :src="sendinfo.pic" mode="aspectFit"></image>
@ -162,14 +162,14 @@
<text v-if="sendinfo.spData" v-for="(value, key) in sendinfo.spData" :key="key" class="txt">
{{ key }}: {{ value }}
</text>
</view>
</view>
<view class="xq">
<view class="list">
<view v-if="sendinfo.type==3" class="ord">
购买金额:¥ <text style="font-size: 30rpx;">{{sendinfo.price}}</text>{{(sendinfo.unit?'/'+sendinfo.unit:'')}}
</view>
<view v-else class="price">
¥{{sendinfo.price+(sendinfo.unit?'/'+sendinfo.unit:'')}}
¥{{sendinfo.price+(sendinfo.unit?'/'+sendinfo.unit:'')}}
</view>
<view class="qty">
{{sendinfo.quantity?'共('+sendinfo.quantity+(sendinfo.unit?sendinfo.unit:'件')+')':''}}
@ -187,7 +187,7 @@
</view>
<submit @inputs="inputs" @heights="heights"></submit>
<u-toast ref="uToast" />
<!-- 普通弹窗 -->
<uni-popup ref="popup" background-color="#fff">
<view class="popup-content" :class="{ 'popup-height': 'right' }">
@ -221,7 +221,7 @@
</view>
</view>
</uni-popup>
</view>
</template>
@ -264,8 +264,8 @@
oldTime: new Date(),
inputh: '90',
ifaudio:false, //
socketTask: null, // WebSocket
isConnected: false, // WebSocket
socketTask: null, // WebSocket
isConnected: false, // WebSocket
//
ifsend: false,
sendinfo:{
@ -298,7 +298,7 @@
uni.setNavigationBarTitle({
title: title,
});
if(this.info.sendinfo){
this.ifsend=true;
this.sendinfo=this.info.sendinfo;
@ -309,9 +309,9 @@
this.sendinfo=null;
this.$forceUpdate();
}
//
this.getchatstore();
this.getchatstore();
}
},
onShow() {
@ -320,7 +320,7 @@
console.log("onShow")
this.screendo(this.unshiftmsg.length - 1);
}, 100);
// socket
this.socketinit();
},
@ -342,7 +342,7 @@
this.closeWebSocket();
},
// buttons
onNavigationBarButtonTap: async function(e) {
onNavigationBarButtonTap: async function(e) {
const index = e.index;
if (index === 0) {
this.$refs.popup.open("right")
@ -351,7 +351,7 @@
// url: `/pages/chat/chatset?data=${data}`
// });
}
},
},
methods: {
gotoDetail(item){
var send=item.send;
@ -372,7 +372,7 @@
//
uni.navigateTo({
url: `/pages/order/orderinfo?id=${send.orderId}`
});
});
}
}
},
@ -432,7 +432,7 @@
data.forEach((cell,i)=>{
cell["ifaudio"]=false;
//
if (i < this.unshiftmsg.length - 1) {
if (i < this.unshiftmsg.length - 1) {
//
let t = dateTime.spaceTime(this.oldTime, cell.fromtime);
if (t) {
@ -449,11 +449,11 @@
}
this.unshiftmsg.push(cell);
});
// //
// var test = {
// "fromname": this.info.chatName, //
// "fromuser": this.userid, //
// "fromuser": this.userid, //
// "headimg": this.info.chatAvatar, //
// "toname": this.userName, //
// "touser": this.info.friendId, //
@ -466,7 +466,7 @@
// };
// this.unshiftmsg.push(test)
// this.$forceUpdate();
if(this.unshiftmsg.length<=0)
{
if(this.info.from=="yh"){
@ -507,7 +507,7 @@
];
this.$forceUpdate();
}
// minId线
if(this.info.minId){
this.readUp(1);
@ -527,7 +527,7 @@
this.readUp(1);
}
}
}
catch(err){
console.log(err);
@ -538,7 +538,7 @@
var that = this;
if (!this.isConnected) {
this.socketTask=uni.connectSocket({
url: "wss://www.sanduolantoyoga.com/yoga-imserver/",
url: "ws://192.168.13.9:8528/im",
header: {
// 'content-type': 'application/json',
Authorization: uni.getStorageSync("token"),
@ -596,8 +596,8 @@
"fromname": this.info.chatName,
"fromuser": this.info.friendId,
"headimg": this.info.chatAvatar,
"toname": this.userName,
"touser": this.userid,
"toname": this.userName,
"touser": this.userid,
"content": data.content,
"time": data.type==3?5:0,
"ifaudio":data.type==3? true:false,
@ -613,10 +613,10 @@
setTimeout(() => {
that.screendo(that.unshiftmsg.length-1);
}, 100);
//
myCache(this.info.chatId,this.unshiftmsg);
myCache(this.info.chatId,this.unshiftmsg);
// socket
var chatlastinfo={
id: this.info.chatId,
@ -631,7 +631,7 @@
sort:"privatechat"
};
this.updateChatList(chatlastinfo);
}
else{
// 10, "" 11, " " 12, " " 30,""
@ -650,12 +650,12 @@
});
}
},
// WebSocket
closeWebSocket() {
if (this.socket) {
uni.closeSocket();
this.socket = null;
this.isConnected = false;
// WebSocket
closeWebSocket() {
if (this.socket) {
uni.closeSocket();
this.socket = null;
this.isConnected = false;
}
},
//
@ -673,7 +673,7 @@
res.data.forEach((cell,i)=>{
cell["ifaudio"]=false;
//
if (i < this.unshiftmsg.length - 1) {
if (i < this.unshiftmsg.length - 1) {
//
let t = dateTime.spaceTime(this.oldTime, cell.fromtime);
if (t) {
@ -687,7 +687,7 @@
}
else if(cell.type==0){
cell.content=decodeURIComponent(cell.content);
}
}
// // 0: 1: 2: 3: 4: 5 6 10, "" 11, " "12, " " 30,""
let mdata = {
"fromname": this.userName,
@ -722,7 +722,7 @@
myCache(this.info.chatId,this.unshiftmsg);
this.info.minId=this.unshiftmsg[this.unshiftmsg.length-1].minId;
this.$forceUpdate();
// 线
var chatlastinfo={
id: this.info.chatId,
@ -737,8 +737,8 @@
sort:"privatechat"
};
this.updateChatList(chatlastinfo);
}
}
},
screendo(scrindex){
this.scrollToView = '';
@ -864,7 +864,7 @@
console.log("inputs");
console.log(e);
var rindex=this.unshiftmsg.length-1;
let data = {
"fromname": this.userName,
"fromuser": this.userid,
@ -877,22 +877,22 @@
"fromtime": new Date(),
"type": e.type
};
if(e.type!=='video'){
this.unshiftmsg.push(data);
rindex=this.unshiftmsg.length-1;
// :id
this.screendo(this.unshiftmsg.length - 1);
}
//
var that = this;
if(e.type=='txt'){
//
if(e.type=='txt'){
//
//
this.onSendWS(data,rindex);
}
else if(e.type=='image'){
else if(e.type=='image'){
//
//
var token= uni.getStorageSync("token");
@ -910,10 +910,10 @@
success: res => {
console.log(res);
uni.hideLoading();
var rdata=JSON.parse(res.data);
var rdata=JSON.parse(res.data);
if(rdata.data&&rdata.data.originUrl){
var url=rdata.data&&rdata.data.originUrl?rdata.data.originUrl:'';
data.content= url;
data.content= url;
//
if (e.type == 'image'&&url) {
this.imgMsg.push(url);
@ -934,9 +934,9 @@
console.log(res)
}
});
}
else if(e.type=='audio'){
else if(e.type=='audio'){
//
//
var token= uni.getStorageSync("token");
@ -976,7 +976,7 @@
console.log(res)
}
});
}
else if(e.type=='video'){
//
@ -1023,9 +1023,9 @@
console.log(res)
}
});
}
},
//
async onSendWS(datamsg,rindex){
@ -1076,7 +1076,7 @@
this.$forceUpdate();
//
myCache(this.info.chatId,this.unshiftmsg);
console.log('this.unshiftmsg',this.unshiftmsg);
console.log('this.unshiftmsg',this.unshiftmsg);
//
var chatlastinfo={
id:this.info.chatId,
@ -1092,7 +1092,7 @@
send:this.sendinfo
};
this.updateChatList(chatlastinfo);
if(mtype=='5'||mtype=='6'){
//
this.ifsend=false;
@ -1160,7 +1160,7 @@
padding: 10rpx 12rpx !important;
border-radius: 10rpx;
}
.chat {
height: 100%;
@ -1312,7 +1312,7 @@
margin-left: 16rpx;
border-radius: 0rpx 20rpx 20rpx 20rpx;
}
.msg-img {
margin: 0 0 0 20rpx;
}
@ -1328,7 +1328,7 @@
padding-bottom: 4rpx;
margin-right: 10rpx;
}
.voicel{
.voicel{
height: 32rpx;
width: 32rpx;
}
@ -1338,7 +1338,7 @@
height: 32rpx;
background-size: 400%;
}
.msg-product{
display: flex;
flex-direction: column;
@ -1451,9 +1451,9 @@
border:none;
}
}
}
}
}
}
}
@ -1491,7 +1491,7 @@
.ms-img {
margin-right: 16rpx;
}
.msg-img {
margin: 0 20rpx 0 0;
}
@ -1526,7 +1526,7 @@
height: 32rpx;
background-size: 400%;
}
.msg-product{
display: flex;
flex-direction: column;
@ -1639,15 +1639,15 @@
border:none;
}
}
}
}
}
}
}
}
}
.voicePlay {
animation-name: voicePlay;
animation-duration: 1s;
@ -1655,7 +1655,7 @@
animation-iteration-count: infinite;
animation-timing-function: steps(3);
}
@keyframes voicePlay {
0% {
background-position: 0;
@ -1664,7 +1664,7 @@
background-position: 100%;
}
}
.popup{
position: fixed;
bottom: 180rpx;
@ -1769,8 +1769,8 @@
}
}
}
.example {
width: 550rpx;
opacity: 1;
@ -1826,5 +1826,5 @@
}
}
}
</style>

@ -1,8 +1,8 @@
<template>
<view class="content">
<!-- 聊天内容 -->
<scroll-view class="chat" scroll-y="true" scroll-with-animation="true" :scroll-into-view="scrollToView"
refresher-enabled="true" :refresher-triggered="triggered" :refresher-threshold="100"
<scroll-view class="chat" scroll-y="true" scroll-with-animation="true" :scroll-into-view="scrollToView"
refresher-enabled="true" :refresher-triggered="triggered" :refresher-threshold="100"
refresher-background="#f0f0f0" @refresherpulling="onPulling"
@refresherrefresh="onRefresh" @refresherrestore="onRestore" @refresherabort="onAbort">
<view class="chat-main" :style="{paddingBottom:inputh+'px'}">
@ -16,7 +16,7 @@
<image class="uimg" :src="item.headimg" @error="handleImageError($event,index)" mode="aspectFill"></image>
<text class="uname">{{item.fromname}}</text>
</view>
<view class="message" v-if="item.type == 'txt'">
<!-- 文字 -->
<view class="msg-text">
@ -41,9 +41,9 @@
{{item.time}}
</view>
</view>
<view class="message" v-if="item.type == 'product'">
<view v-if="item.send" class="msg-product" @click="gotoDetail(item)">
<view v-if="item.send.type==3" class="zx">
<view class="zxname">订单咨询</view>
@ -52,7 +52,7 @@
<view class="msg-con">
<image class="img" :src="item.send.pic" mode="aspectFit"></image>
<view class="info">
<view class="name">
<view class="name">
<text class="ntxt">{{item.send.name}}</text>
<text v-if="item.send.spData" v-for="(value, key) in item.send.spData" :key="key" class="txt">
{{ key }}: {{ value }}
@ -64,7 +64,7 @@
购买金额:¥ <text style="font-size: 30rpx;">{{item.send.price}}</text>{{(item.send.unit?'/'+item.send.unit:'')}}
</view>
<view v-else class="price">
¥{{item.send.price+(item.send.unit?'/'+item.send.unit:'')}}
¥{{item.send.price+(item.send.unit?'/'+item.send.unit:'')}}
</view>
<view v-if="item.send.quantity" class="qty">
{{item.send.quantity?'共('+item.send.quantity+(item.send.unit?item.send.unit:'件')+')':''}}
@ -79,9 +79,9 @@
</view>
</view>
</view>
</view>
</view>
<view class="msg-m msg-right" v-if="item.fromuser == userid">
<view class="user-img" >
@ -144,22 +144,22 @@
购买金额:¥ <text style="font-size: 30rpx;">{{item.send.price}}</text>{{(item.send.unit?'/'+item.send.unit:'')}}
</view>
<view v-else class="price">
¥{{item.send.price+(item.send.unit?'/'+item.send.unit:'')}}
¥{{item.send.price+(item.send.unit?'/'+item.send.unit:'')}}
</view>
<view class="qty">
{{item.send.quantity?'共('+item.send.quantity+(item.send.unit?item.send.unit:'件')+')':''}}
</view>
</view>
<button class="btn" type="primary" v-if="item.send.type==1"></button>
<button class="btn" type="primary" v-if="item.send.type==2"></button>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
@ -173,14 +173,14 @@
<text v-if="sendinfo.spData" v-for="(value, key) in sendinfo.spData" :key="key" class="txt">
{{ key }}: {{ value }}
</text>
</view>
</view>
<view class="xq">
<view class="list">
<view v-if="sendinfo.type==3" class="ord">
购买金额:¥ <text style="font-size: 30rpx;">{{sendinfo.price}}</text>{{(sendinfo.unit?'/'+sendinfo.unit:'')}}
</view>
<view v-else class="price">
¥{{sendinfo.price+(sendinfo.unit?'/'+sendinfo.unit:'')}}
¥{{sendinfo.price+(sendinfo.unit?'/'+sendinfo.unit:'')}}
</view>
<view class="qty">
{{sendinfo.quantity?'共('+sendinfo.quantity+(sendinfo.unit?sendinfo.unit:'件')+')':''}}
@ -198,7 +198,7 @@
</view>
<submit @inputs="inputs" @heights="heights"></submit>
<u-toast ref="uToast" />
<!-- 群公告 -->
<uni-popup ref="popup" background-color="#fff">
<view class="popup-content" :class="{ 'popup-height': 'right' }">
@ -240,7 +240,7 @@
</view>
</view>
</uni-popup>
<!-- 未读已读 -->
<uni-popup ref="popupRead" background-color="#fff">
<view class="popup-content" :class="{ 'popup-height': 'right' }">
@ -265,7 +265,7 @@
</view>
</view>
</uni-popup>
</view>
</template>
@ -293,22 +293,22 @@
// chatName: data.name,
// chatAvatar: data.headImage,
// chatTime: timestamp,
// userid: data.ownerId,
// userid: data.ownerId,
// friendId: data.customerService, // userid
// teacherId: data.instructor, // userid
// minId: "", // id
// sort:"groupchat", // privatechat groupchat
// from:"yh", // yh message
// notice: data.notice,
// remarkNickName: data.remarkNickName,
// showNickName: data.showNickName,
// showGroupName: data.showGroupName,
// remarkGroupName: data.remarkGroupName,
// reason: data.reason,
// customerService: data.customerService,
// instructor: data.instructor,
// productId: data.productId,
// productName: data.productName,
// notice: data.notice,
// remarkNickName: data.remarkNickName,
// showNickName: data.showNickName,
// showGroupName: data.showGroupName,
// remarkGroupName: data.remarkGroupName,
// reason: data.reason,
// customerService: data.customerService,
// instructor: data.instructor,
// productId: data.productId,
// productName: data.productName,
// sendinfo:{
// type:1,// 1 2 3
// id:this.product.id,
@ -339,8 +339,8 @@
oldTime: new Date(),
inputh: '90',
ifaudio:false, //
socketTask: null, // WebSocket
isConnected: false, // WebSocket
socketTask: null, // WebSocket
isConnected: false, // WebSocket
heartbeatInterval: null, // 20
heartbeatTimeout: 20000, // 20
//
@ -380,7 +380,7 @@
uni.setNavigationBarTitle({
title: title,
});
if(this.info.sendinfo){
this.ifsend=true;
this.sendinfo=this.info.sendinfo;
@ -394,25 +394,25 @@
}
},
onShow() {
//
if(this.heartbeatInterval){
clearInterval(this.heartbeatInterval); //
}
//
this.getchatstore();
//
this.getrwinfo();
// :id
setTimeout(() => {
console.log("onShow")
this.screendo(this.unshiftmsg.length - 1);
}, 500);
},
// onBackPress(options) {
// if (options.from === 'backbutton') {
@ -427,12 +427,12 @@
// }
// },
// buttons
onNavigationBarButtonTap: async function(e) {
onNavigationBarButtonTap: async function(e) {
const index = e.index;
if (index === 0) {
this.$refs.popup.open("right");
}
},
},
// beforeDestroy() {
// // console.log('socketbeforeDestroy');
// // WebSocket
@ -488,7 +488,7 @@
//
uni.navigateTo({
url: `/pages/order/orderinfo?id=${send.orderId}`
});
});
}
}
},
@ -548,7 +548,7 @@
data.forEach((cell,i)=>{
cell["ifaudio"]=false;
//
if (i < this.unshiftmsg.length - 1) {
if (i < this.unshiftmsg.length - 1) {
//
let t = dateTime.spaceTime(this.oldTime, cell.fromtime);
if (t) {
@ -564,8 +564,8 @@
cell.content=decodeURIComponent(cell.content);
}
this.unshiftmsg.push(cell);
});
});
if(this.unshiftmsg.length<=0)
{
if(this.info.from=="yh"){
@ -579,7 +579,7 @@
"toname": this.userName, //
"touser": this.userid, //
"content": cont,
"minId":this.info.minId,
"minId":this.info.minId,
"time": 0,
"ifaudio":false,
"fromtime": new Date(),
@ -599,7 +599,7 @@
"toname": this.userName, //
"touser": this.userid, //
"content": cont,
"minId":this.info.minId,
"minId":this.info.minId,
"time": 0,
"ifaudio":false,
"fromtime": new Date(),
@ -619,7 +619,7 @@
catch(err){
console.log(err);
}
// socket
this.socketinit();
},
@ -648,7 +648,7 @@
this.heartbeatInterval = setInterval(() => {
if (this.socketTask) {
this.socketTask.send({
data: JSON.stringify({
data: JSON.stringify({
'cmd': 1,//
'data': {
'accessToken':uni.getStorageSync("token")
@ -659,11 +659,11 @@
}, this.heartbeatTimeout);
},
// WebSocket
closeWebSocket() {
if (this.socket) {
uni.closeSocket();
this.socket = null;
this.isConnected = false;
closeWebSocket() {
if (this.socket) {
uni.closeSocket();
this.socket = null;
this.isConnected = false;
}
},
// ws
@ -671,7 +671,7 @@
var that = this;
if (!this.isConnected) {
this.socketTask=uni.connectSocket({
url: "wss://www.sanduolantoyoga.com/yoga-imserver/",
url: "ws://192.168.13.9:8528/im",
header: {
// 'content-type': 'application/json',
Authorization: uni.getStorageSync("token"),
@ -720,7 +720,7 @@
//
if(data.data){
data=data.data;
//
if(data.groupId==this.info.groupId){
if(this.ifadd(data.id)){
@ -742,7 +742,7 @@
}
}
}
}
}
});
@ -763,9 +763,9 @@
var headImage="/static/image/girl.png";
const {data: res} = await uni.$http.get("/api/friend/find/"+data.sendId);
if(res.data){
//
//
nickName=res.data.nickName?res.data.nickName:"客服";
headImage=res.data.headImage?res.data.headImage:'/static/image/girl.png';
headImage=res.data.headImage?res.data.headImage:'/static/image/girl.png';
this.msgMerge(data,nickName,headImage);
}
else{
@ -773,7 +773,7 @@
}
},
async msgMerge(data,nickName,headImage){
if(data.type==0){
data.content=decodeURIComponent(data.content);
}
@ -781,13 +781,13 @@
if(data.type == 1) {
this.imgMsg.unshift(data.content)
}
let mdata = {
"fromname": nickName, //
"fromuser": data.sendId, //
"headimg": headImage,//
"toname": this.userName,
"touser": this.userid,
"toname": this.userName,
"touser": this.userid,
"content": data.content,
"readedCount": data.readedCount,
"time": data.type==3?5:0,
@ -804,12 +804,12 @@
setTimeout(() => {
this.screendo(this.unshiftmsg.length-1);
}, 100);
//
myCache(this.info.chatId,this.unshiftmsg);
myCache(this.info.chatId,this.unshiftmsg);
// Id
this.savaMaxId({id:this.info.chatId,minId:data.id});
// socket
var chatlastinfo={
id: this.info.chatId,
@ -923,7 +923,7 @@
type=cell.type;
cell["ifaudio"]=false;
//
if (i < this.unshiftmsg.length - 1) {
if (i < this.unshiftmsg.length - 1) {
//
let t = dateTime.spaceTime(this.oldTime, cell.fromtime);
if (t) {
@ -937,7 +937,7 @@
}
else if(cell.type==0){
cell.content=decodeURIComponent(cell.content);
}
}
// // 0: 1: 2: 3: 4: 5 6 10, "" 11, " "12, " " 30,""
let mdata = {
"fromname": this.userName,
@ -972,7 +972,7 @@
this.$forceUpdate();
// Id
this.savaMaxId({id:this.info.chatId,minId:this.info.minId});
// 线
var chatlastinfo={
id: this.info.chatId,
@ -990,12 +990,12 @@
sort:"groupchat"
};
this.updateChatList(chatlastinfo);
//
this.readed();
}
// }
},
screendo(scrindex){
this.scrollToView = '';
@ -1121,7 +1121,7 @@
console.log("inputs");
console.log(e);
var rindex=this.unshiftmsg.length-1;
let data = {
"fromname": this.userName,
"fromuser": this.userid,
@ -1134,22 +1134,22 @@
"fromtime": new Date(),
"type": e.type
};
if(e.type!=='video'){
this.unshiftmsg.push(data);
rindex=this.unshiftmsg.length-1;
// :id
this.screendo(this.unshiftmsg.length - 1);
}
//
var that = this;
if(e.type=='txt'){
//
if(e.type=='txt'){
//
//
this.onSendWS(data,rindex);
}
else if(e.type=='image'){
else if(e.type=='image'){
//
//
var token= uni.getStorageSync("token");
@ -1167,10 +1167,10 @@
success: res => {
console.log(res);
uni.hideLoading();
var rdata=JSON.parse(res.data);
var rdata=JSON.parse(res.data);
if(rdata.data&&rdata.data.originUrl){
var url=rdata.data&&rdata.data.originUrl?rdata.data.originUrl:'';
data.content= url;
data.content= url;
//
if (e.type == 'image'&&url) {
this.imgMsg.push(url);
@ -1191,9 +1191,9 @@
console.log(res)
}
});
}
else if(e.type=='audio'){
else if(e.type=='audio'){
//
//
var token= uni.getStorageSync("token");
@ -1238,7 +1238,7 @@
console.log(res)
}
});
}
else if(e.type=='video'){
//
@ -1284,9 +1284,9 @@
console.log(res)
}
});
}
},
//
async onSendWS(datamsg,rindex){
@ -1339,7 +1339,7 @@
myCache(this.info.chatId,this.unshiftmsg);
// Id
this.savaMaxId({id:this.info.chatId,minId:rrdata.id});
//
var chatlastinfo={
id:this.info.chatId,
@ -1358,7 +1358,7 @@
send:this.sendinfo
};
this.updateChatList(chatlastinfo);
if(mtype=='5'||mtype=='6'){
//
this.ifsend=false;
@ -1452,7 +1452,7 @@
padding: 10rpx 12rpx !important;
border-radius: 10rpx;
}
.chat {
height: 100%;
@ -1608,7 +1608,7 @@
margin-left: 16rpx;
border-radius: 0rpx 20rpx 20rpx 20rpx;
}
.msg-img {
margin: 0 0 0 20rpx;
}
@ -1624,7 +1624,7 @@
padding-bottom: 4rpx;
margin-right: 10rpx;
}
.voicel{
.voicel{
height: 32rpx;
width: 32rpx;
}
@ -1634,7 +1634,7 @@
height: 32rpx;
background-size: 400%;
}
.msg-product{
display: flex;
flex-direction: column;
@ -1748,9 +1748,9 @@
border:none;
}
}
}
}
}
}
}
@ -1772,7 +1772,7 @@
justify-content: flex-end;
align-items: flex-end;
}
.feed-imgy{
display: flex;
flex-direction: column;
@ -1805,7 +1805,7 @@
.ms-img {
margin-right: 16rpx;
}
.msg-img {
margin: 0 20rpx 0 0;
}
@ -1840,7 +1840,7 @@
height: 32rpx;
background-size: 400%;
}
.msg-product{
display: flex;
flex-direction: column;
@ -1954,15 +1954,15 @@
border:none;
}
}
}
}
}
}
}
}
}
.voicePlay {
animation-name: voicePlay;
animation-duration: 1s;
@ -1970,7 +1970,7 @@
animation-iteration-count: infinite;
animation-timing-function: steps(3);
}
@keyframes voicePlay {
0% {
background-position: 0;
@ -1979,7 +1979,7 @@
background-position: 100%;
}
}
.popup{
position: fixed;
bottom: 180rpx;
@ -2084,7 +2084,7 @@
}
}
}
.example {
width: 550rpx;
opacity: 1;
@ -2141,7 +2141,7 @@
flex: 1;
}
}
.chatlist{
display: flex;
flex-wrap: wrap;
@ -2184,6 +2184,6 @@
}
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,339 @@
<template>
<view class="page">
<!-- 页面头部 -->
<view class="page-header" :style="{ paddingTop: geStatusBarHeight() + 8 + 'px' }">
<view class="text-center">消息</view>
<uni-icons type="staff-filled" size="30" color="#ffffff" @click="gotoContacts"></uni-icons>
</view>
<view class="wrapcon">
<!-- 暂无消息 -->
<view v-if="chats.length === 0 && loadStatus === 'nomore'" class="nodata">~</view>
<!-- 列表 -->
<view class="listcell">
<u-swipe-action
ref="swipeRef"
:options="options"
v-for="(chat, index) in chats"
:key="chat.id || index"
style="min-width: 600rpx;"
@click="actionClick(chat, index)"
>
<view class="lcon" @click="gotoChat(chat, index)" @longpress="handleLongPress(index)">
<view class="limg">
<image class="img" :src="chat.headImage || '/static/image/girl.png'" mode="aspectFill"></image>
</view>
<view class="lright">
<view class="lrow">
<view class="pname">{{ chat.showName || '未知' }}</view>
<view class="ptime">{{ changeTime(chat.lastSendTime) }}</view>
</view>
<view class="lrow">
<view class="pnr">
{{ chat.lastContent || '' }}
</view>
<view class="lnum" v-if="chat.unreadCount > 0">
{{ chat.unreadCount }}
</view>
</view>
</view>
</view>
</u-swipe-action>
</view>
<!-- 加载状态 -->
<u-loadmore v-if="chats.length === 0 && loadStatus === 'loading'" :status="loadStatus" :marginTop="20"></u-loadmore>
</view>
<!-- 提示信息弹窗 -->
<uni-popup ref="message" type="message">
<uni-popup-message :type="msgType" :message="messageText" :duration="2000"></uni-popup-message>
</uni-popup>
</view>
</template>
<script>
import { mapState, mapActions, mapGetters } from 'vuex'
import dateTime from '@/common/dateTime.js'
import {myCache} from "../../utils/utils";
export default {
data() {
return {
loadStatus: 'loadmore',
options: [
{
text: '删除',
style: {
backgroundColor: '#ed2a28'
}
}
],
messageText: '',
msgType: 'error'
}
},
computed: {
...mapState('chatStore', ['chats']),
...mapGetters('chatStore', ['isLoading']),
//
unreadCount() {
let count = 0
this.chats.forEach(chat => {
if (!chat.delete) {
count += chat.unreadCount
}
})
return count
},
// loading
loading() {
return this.isLoading()
}
},
watch: {
unreadCount(newVal) {
this.setBadge(newVal)
}
},
onShow() {
console.log("this.chats列表.......",this.chats)
this.loadStatus = 'nomore'
this.setBadge(this.unreadCount)
},
onPullDownRefresh() {
// WebSocket
this.setBadge(this.unreadCount)
setTimeout(() => {
uni.stopPullDownRefresh()
}, 500)
},
methods: {
...mapActions('chatStore', ['removeChat', 'moveTop']),
//
setBadge(count) {
if (count > 0) {
uni.setTabBarBadge({
index: 3, // tabBar
text: count + ''
})
} else {
uni.removeTabBarBadge({
index: 3
})
}
},
//
handleLongPress(index) {
this.$refs.swipeRef[index].open()
},
//
actionClick(chat, index) {
const that = this
uni.showModal({
title: '提示',
content: '确定要删除此聊天记录吗?',
success: res => {
if (res.confirm) {
that.$refs.swipeRef[index].close()
that.removeChat(index) // Vuex action
}
}
})
},
//
gotoChat(chat, index) {
// chat
//
let info = {}
if (chat.type === 'PRIVATE') {
info = {
chatId: chat.id || `privatechat-${chat.targetId}`,
chatName: chat.showName,
chatAvatar: chat.headImage,
friendId: chat.targetId,
minId: chat.hotMinIdx || 0,
sort: 'privatechat',
from: 'message'
}
} else if (chat.type === 'GROUP') {
info = {
chatId: chat.id || `groupchat-${chat.targetId}`,
groupId: chat.targetId,
chatName: chat.showName,
chatAvatar: chat.headImage,
minId: chat.hotMinIdx || 0,
sort: 'groupchat',
from: 'message'
}
}
const data = encodeURIComponent(JSON.stringify(info))
uni.navigateTo({ url: `/pages/chatbox/chat-box?chatIdx=${index}` });
},
//
gotoContacts() {
uni.navigateTo({
url: '/pages/message/contact'
})
},
//
changeTime(time) {
return dateTime.dateTime(time)
},
//
geStatusBarHeight() {
return uni.getSystemInfoSync().statusBarHeight
}
}
}
</script>
<style lang="scss" scoped>
.page {
padding: 0;
position: relative;
background-image: url('@/static/image/bg.jpg');
background-attachment: fixed;
background-size: cover;
background-position: center center;
min-height: calc(100vh - var(--window-top) - var(--window-bottom));
}
.page-header {
position: fixed;
width: 100%;
z-index: 999;
padding: 40rpx 30rpx 30rpx 30rpx;
display: flex;
color: #fff;
font-size: 36rpx;
background-color: #00a89b;
.text-center {
width: 100%;
text-align: center;
}
.edit {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 30rpx;
font-size: 32rpx;
}
}
.wrapcon {
padding: 0;
width: 100%;
position: relative;
margin: 180rpx 0 0 0;
min-height: calc(100vh - 180rpx);
/* #ifdef H5 */
margin: calc(110rpx + var(--window-top)) 0 0 0;
min-height: calc(100vh - var(--window-top) - var(--window-bottom) - 180rpx);
/* #endif */
overflow-y: auto;
}
.listcell {
background: #fff;
margin: 0;
width: 100%;
min-width: 750rpx;
.lcon {
border-bottom: 1rpx solid #eee;
padding-top: 20rpx;
padding-bottom: 20rpx;
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
min-width: 750rpx;
.limg {
width: 100rpx;
height: 100rpx;
margin-right: 20rpx;
margin-left: 20rpx;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: center;
border-radius: 50%;
.img {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
}
}
.lright {
display: flex;
flex: 1;
flex-direction: column;
}
.lrow {
display: flex;
flex-direction: row;
margin-bottom: 20rpx;
margin-right: 20rpx;
}
.pname {
display: flex;
flex: 1;
line-height: 50rpx;
font-size: 32rpx;
font-weight: 600;
color: #000000;
}
.ptime {
line-height: 50rpx;
font-size: 24rpx;
font-weight: 500;
color: #595959;
}
.pnr {
display: flex;
flex: 1;
font-size: 24rpx;
font-weight: 400;
color: #595959;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 40rpx;
max-height: 40rpx;
}
.lnum {
background-color: #de0000;
padding: 0 10rpx;
min-width: 32rpx;
height: 32rpx;
line-height: 32rpx;
color: #fff;
font-size: 24rpx;
border-radius: 28rpx;
text-align: center;
}
}
}
.nodata {
background-image: url('../../static/image/nomsg.png');
background-repeat: no-repeat;
background-position: center center;
background-size: 100%;
height: 376rpx;
width: 256rpx;
color: #00a89b;
display: flex;
align-items: center;
justify-content: flex-end;
flex-direction: column;
padding-bottom: 40rpx;
margin: 100rpx auto;
}
</style>

@ -0,0 +1,174 @@
<template>
<view class="tab-page">
<view v-if="loading" class="chat-loading">
<loading :size="50" :mask="false">
<view>消息接收中...</view>
</loading>
</view>
<view v-if="initializing" class="chat-loading">
<loading :size="50" :mask="false">
<view>正在初始化...</view>
</loading>
</view>
<view class="nav-bar" v-if="showSearch">
<view class="nav-search">
<uni-search-bar focus="true" radius="100" v-model="searchText" cancelButton="none"
placeholder="搜索"></uni-search-bar>
</view>
</view>
<view class="chat-tip" v-if="!loading && chats.length == 0">
温馨提示您现在还没有任何聊天消息快跟您的好友发起聊天吧~
</view>
<scroll-view class="scroll-bar" v-else scroll-with-animation="true" scroll-y="true">
<view v-for="(chat, index) in chats" :key="index">
<long-press-menu v-if="isShowChat(chat)" :items="menu.items" @select="onSelectMenu($event, index)">
<chat-item :chat="chat" :index="index" :active="menu.chatIdx == index"></chat-item>
</long-press-menu>
</view>
</scroll-view>
</view>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex'
export default {
data() {
return {
showSearch: false,
searchText: "",
menu: {
show: false,
style: "",
chatIdx: -1,
isTouchMove: false,
items: [
{
key: 'DELETE',
name: '删除该聊天',
icon: 'trash',
color: '#e64e4e'
},
{
key: 'TOP',
name: '置顶该聊天',
icon: 'arrow-up'
}
]
}
}
},
computed: {
// Vuex state getters
...mapState('chatStore', ['chats']),
...mapGetters('chatStore', ['isLoading']),
//
unreadCount() {
let count = 0
this.chats.forEach(chat => {
if (!chat.delete) {
count += chat.unreadCount
}
})
return count
},
// getter
loading() {
return this.isLoading()
},
//
initializing() {
return !getApp().$vm.isInit
},
// 使
showChats() {
return this.chats.filter(chat =>
!chat.delete && chat.showName && chat.showName.includes(this.searchText)
)
}
},
methods: {
// Vuex actions
...mapActions('chatStore', ['removeChat', 'moveTop']),
onSelectMenu(item, chatIdx) {
switch (item.key) {
case 'DELETE':
this.removeChat(chatIdx)
break
case 'TOP':
this.moveTop(chatIdx)
break
default:
break
}
this.menu.show = false
},
// removeChat moveToTop mapActions
isShowChat(chat) {
if (chat.delete) return false
return !this.searchText || chat.showName.includes(this.searchText)
},
onSearch() {
this.showSearch = !this.showSearch
this.searchText = ""
},
refreshUnreadBadge() {
if (this.unreadCount > 0) {
uni.setTabBarBadge({
index: 0,
text: this.unreadCount + ""
})
} else {
uni.removeTabBarBadge({
index: 0,
complete: () => {}
})
}
}
},
watch: {
unreadCount(newCount) {
this.refreshUnreadBadge()
}
},
onShow() {
this.refreshUnreadBadge()
}
}
</script>
<style lang="scss">
.tab-page {
position: relative;
display: flex;
flex-direction: column;
.chat-tip {
position: absolute;
top: 400rpx;
padding: 50rpx;
line-height: 50rpx;
text-align: center;
color: $im-text-color-lighter;
}
.chat-loading {
display: block;
width: 100%;
height: 120rpx;
background: white;
color: $im-text-color-lighter;
.loading-box {
position: relative;
}
}
.scroll-bar {
flex: 1;
height: 100%;
}
}
</style>

@ -5,32 +5,32 @@
<view class="tip">yoga</view>
<view class="formregion">
<uni-forms :model="form" ref="uForm">
<uni-forms-item prop="phonenumber" >
<uni-easyinput type="number" placeholder="请输入手机号码" v-model="form.phonenumber" @blur="zhhandleBlur" :inputBorder="false" :clearable="false" :placeholderStyle="placeholderStyle"/>
</uni-forms-item>
<uni-forms-item prop="password" >
<uni-easyinput type="password" placeholder="请输入密码" v-model="form.password" @blur="mmhandleBlur" maxlength="12" :inputBorder="false" :clearable="false" :placeholderStyle="placeholderStyle"/>
</uni-forms-item>
<uni-forms-item prop="code" >
<uni-easyinput type="text" placeholder="请输入验证码" v-model.trim="form.code" :inputBorder="false" :clearable="false" :placeholderStyle="placeholderStyle"/>
<image :src="checkimg" class="checkimg" mode="scaleToFill"></image>
<view class="codebtn" @click="getcode">?</view>
</uni-forms-item>
<button type="primary" class="logincla" @click="loginpwdDo"></button>
<view v-if="iflogin" class="loginno">..</view>
<view v-if="iflogin" class="loginno">..</view>
<view class="regcon">
<!-- <view class="regtxt" @click="gotopwd"></view>
<view class="regtxtl"></view> -->
<view class="regtxtr" @click="gotoregister"> </view>
</view>
</uni-forms>
</view>
<u-toast ref="uToast" />
</view>
@ -102,7 +102,7 @@
success: function (res) {
if (res.confirm) {
console.log('用户点击确定');
// #ifdef APP-PLUS
//
plus.device.vendor = 'apple'; // iOS
@ -112,7 +112,7 @@
plus.runtime.openURL('package:' + plus.android.runtimeMainActivity());
}
// #endif
}
}
});
@ -123,20 +123,20 @@
},
//
getsystemInfo(){
const info = uni.getSystemInfoSync();
const info = uni.getSystemInfoSync();
const platform = info.platform; //
if (info.platform === 'ios') {
// ios
console.log('iOS 设备唯一标识:', info.deviceId);
}
}
else{
//
console.log('设备信息ID:',info.deviceId);
}
this.form.registerId=info.deviceId;
this.$forceUpdate();
// const iosApp = info.platform;
console.log('iOS 设备名称:', info.model);
console.log('iOS 系统版本:', info.system);
@ -179,7 +179,7 @@
// duration: 2000
// });
}
},
//
checkMM(mm) {
@ -227,7 +227,7 @@
}
return true;
},
//
chooseLocation(){
var _this=this;
@ -273,7 +273,7 @@
},
//
async loginpwdDo(){
//
if(!this.form.phonenumber){
this.$refs.uToast.show({
@ -356,6 +356,7 @@
// var shoplist=[shop]
// myCache('shoplist',shoplist);
}
getApp().$vm.init(user.userId)
},
async loadshop(){
//
@ -403,7 +404,7 @@
</style>
<style lang="scss" scoped>
::v-deep .u-checkbox__label{
color:#bdbfc1
}
@ -489,7 +490,7 @@
margin-right: 10rpx;
}
}
.loginno{
display: block;
background-color:rgba(137, 150, 95, 0.3);
@ -503,7 +504,7 @@
}
.logincla{
display: block;
background-color:#00A99A;
font-family: $font-family;
width: 100%;
line-height: 2;
}
@ -557,6 +558,6 @@
font-size: 42rpx;
letter-spacing: 4rpx;
padding: 0 0 40rpx 0;
}
</style>

@ -41,35 +41,36 @@
</view>
</view>
</view>
<view class="wrapcon">
<!-- 列表 -->
<!-- 列表 -->
<view v-if="list[tabCurrentIndex].grouplist.length==0 && list[tabCurrentIndex].loadStatus=='nomore'" class="nodata">
暂无数据..
</view>
<view class="listcell">
<view class="lcon" v-for="(info, index) in list[tabCurrentIndex].grouplist" :key="index" @click="gotoGroup(info)">
<view class="limg">
<image class="img" :src="info.friendHeadImage?info.friendHeadImage:(tabCurrentIndex==0?'/static/image/kff.png':(tabCurrentIndex==1?'/static/image/gw.png':(tabCurrentIndex==2?'/static/image/gw.png':(tabCurrentIndex==3?'/static/image/dzh.png':'/static/image/ql.png'))))" mode="aspectFill"></image>
<image class="img" :src="info.headImage?info.headImage:(tabCurrentIndex==0?'/static/image/kff.png':(tabCurrentIndex==1?'/static/image/gw.png':(tabCurrentIndex==2?'/static/image/gw.png':(tabCurrentIndex==3?'/static/image/dzh.png':'/static/image/ql.png'))))" mode="aspectFill"></image>
</view>
<view class="lright">
<view class="pname">{{info.friendNickName}}</view>
<view class="pname">{{info.nickName}}</view>
</view>
</view>
</view>
<u-loadmore v-if="list[tabCurrentIndex].grouplist.length==0 && list[tabCurrentIndex].loadStatus=='loading'"
<u-loadmore v-if="list[tabCurrentIndex].grouplist.length==0 && list[tabCurrentIndex].loadStatus=='loading'"
:status="list[tabCurrentIndex].loadStatus" :marginTop="20"></u-loadmore>
</view>
<!-- 提示信息弹窗 -->
<uni-popup ref="message" type="message">
<uni-popup-message :type="msgType" :message="messageText" :duration="2000"></uni-popup-message>
</uni-popup>
</view>
</template>
<script>
import { mapState, mapActions, mapGetters } from 'vuex'
import { myCache } from '../../utils/utils.js';
import dateTime from '@/common/dateTime.js';
export default {
@ -122,6 +123,9 @@
ifxy:false,
}
},
computed: {
...mapGetters('chatStore', ['findChatIdx']), //
},
onLoad(options) {
var userInfo=myCache('userInfo');
this.userid = userInfo.userId? userInfo.userId:'';
@ -141,7 +145,7 @@
this.$forceUpdate();
}
});
if(this.ifxy){
this.tabCurrentIndex=0;
this.$forceUpdate();
@ -150,7 +154,7 @@
this.tabCurrentIndex=5;
this.$forceUpdate();
}
// 退
if(myCache('myshopid')){}
this.loadData();
@ -160,13 +164,13 @@
// id:"coach",
// name:'',
// id:"customerService",
// name:'',
// name:'',
// id:"adviser",
// name:'',
// name:'',
// id:"storeManager",
// name:'',
// name:'',
// id:"groupchat",
// name:'',
// name:'',
// id:"consumer",
// name:'',
},
@ -182,6 +186,8 @@
this.loadData();
},
methods: {
...mapActions('chatStore', ['openChat']),
//
addContacts(){},
gotoBack(){
@ -205,16 +211,26 @@
// info.customerService=data.customerService;
// info.instructor=data.instructor;
// info.productId=data.productId;
// info.productName=data.productName;
// info.productName=data.productName;
}
},
//
gotoGroup(item){
console.log(item);
var timestamp = new Date().getTime();
if(this.tabCurrentIndex==4){
//
// this.getGroupInfo(item.id);
let chat = {
type: 'GROUP',
targetId: item.id,
showName: item.name,
headImage: item.headImage,
};
this.openChat(chat);
let chatIdx = this.findChatIdx(chat);
uni.navigateTo({ url: `/pages/chatbox/chat-box?chatIdx=${chatIdx}` });
/**
this.getGroupInfo(item.id);
var chatid="groupchat-" + item.id;
var info={
chatId: chatid,
@ -227,34 +243,46 @@
minId: "",
sort:"groupchat", // privatechat groupchat
from:"message", // yh message
notice: item.notice,
notice: item.notice,
remarkNickName: item.remarkNickName,
remarkGroupName: item.remarkGroupName,
showNickName: item.showNickName,
remarkGroupName: item.remarkGroupName,
showNickName: item.showNickName,
showGroupName: item.showGroupName,
reason: item.reason,
customerService: item.customerService,
instructor: item.instructor,
ownerId: item.ownerId,
productId: item.productId,
reason: item.reason,
customerService: item.customerService,
instructor: item.instructor,
ownerId: item.ownerId,
productId: item.productId,
productName: item.productName
}
var data=encodeURIComponent(JSON.stringify(info));
uni.navigateTo({
url: `/pages/chat/groupchat?data=${data}`
});
**/
}
else{
let chat = {
type: 'PRIVATE',
targetId: item.id,
showName: item.nickName,
headImage: item.headImage,
};
this.openChat(chat);
let chatIdx = this.findChatIdx(chat);
console.log("chatIdx",chatIdx)
uni.navigateTo({ url: `/pages/chatbox/chat-box?chatIdx=${chatIdx}` });
//
/**
var info={
chatId: "privatechat-" + this.userid +"-"+ item.friendId,
chatId: "privatechat-" + this.userid +"-"+ item.id,
chatType: (this.tabCurrentIndex==0?"coach":(this.tabCurrentIndex==1?"customerService":(this.tabCurrentIndex==2?"adviser":
(this.tabCurrentIndex==3?"storeManager":(this.tabCurrentIndex==4?"groupchat":(this.tabCurrentIndex==5?"consumer":"")))))),
chatName: item.friendNickName,
chatAvatar: item.friendHeadImage,
chatName: item.nickName,
chatAvatar: item.headImage,
chatTime: timestamp,
userid: this.userid,
friendId: item.friendId, //
userid: this.userid,
friendId: item.id, //
minId: null, // id
sort: this.tabCurrentIndex==4?"groupchat":"privatechat", // privatechat groupchat
from: "message" // yh message
@ -295,7 +323,9 @@
url: `/pages/chat/chat?data=${data}`
});
}
**/
}
},
loadData(){
//
@ -348,14 +378,14 @@
this.list[3].grouplist=res.data.storeManager;
this.$forceUpdate();
}
//
if(res.data&&res.data.consumer&&res.data.consumer.length>0){
this.list[5].grouplist=res.data.consumer;
this.$forceUpdate();
}
}
//
if(this.tabCurrentIndex==4){
const {data: res} = await uni.$http.get('/api/group/list');
@ -364,8 +394,8 @@
if(res.data&&res.data.length>0){
res.data.forEach(cell => {
this.list[this.tabCurrentIndex].grouplist.push({...cell,
friendNickName: cell.name,
friendHeadImage: cell.headImage?cell.headImage:'/static/image/ql.png'
nickName: cell.name,
headImage: cell.headImage?cell.headImage:'/static/image/ql.png'
})
});
this.$forceUpdate();
@ -381,7 +411,7 @@
getNavBarHeight(){
return 45+uni.getSystemInfoSync()['statusBarHeight'];
},
}
}
</script>
@ -401,7 +431,7 @@
width: 100%;
z-index: 999;
background-color: #00a89b;
.edit {
position: absolute;
top: 50%;

@ -1,19 +1,19 @@
<template>
<view class="page">
<view class="page-header" :style="{ paddingTop: geStatusBarHeight()+ 8 + 'px'}">
<view class="text-center">消息</view>
<uni-icons type="staff-filled" size="30" color="#ffffff" @click="$u.throttle(gotoContacts(), 2000)"></uni-icons>
</view>
<view class="wrapcon">
<!-- 列表 -->
<!-- 列表 -->
<view v-if="grouplist.length==0 && loadStatus=='nomore'" class="nodata">~</view>
<view class="listcell">
<u-swipe-action ref="swipeRef" :options="options" v-for="(info, index) in grouplist" :key="info.id" style="min-width: 600rpx;"
@click="$u.throttle(actionClick(info,index), 2000)">
@click="$u.throttle(actionClick(info,index), 2000)">
<view class="lcon" @click="gotoGroup(info,index)" :key="index" @longpress="handleLongPress(info,index)">
<view class="limg">
<image class="img" :src="info.img" mode="aspectFill"></image>
@ -26,7 +26,7 @@
<view class="lrow">
<view class="pnr">
{{ info.type==0?info.content:(info.type==1?"图片":(info.type==2?"文件":(info.type==3?"语音":(info.type==4?"视频":
(info.type==5?"订单咨询":(info.type==6?"商品咨询":info.content))))))}}
(info.type==5?"订单咨询":(info.type==6?"商品咨询":info.content))))))}}
</view>
<view class="lnum" v-if="info.sl>0">
{{ info.sl}}
@ -35,19 +35,19 @@
</view>
</view>
</u-swipe-action>
</view>
<u-loadmore v-if="grouplist.length==0 && loadStatus=='loading'" :status="loadStatus" :marginTop="20"></u-loadmore>
</view>
<!-- 提示信息弹窗 -->
<uni-popup ref="message" type="message">
<uni-popup-message :type="msgType" :message="messageText" :duration="2000"></uni-popup-message>
</uni-popup>
</view>
</template>
@ -84,17 +84,17 @@
onLoad(options) {
},
onShow(){
this.openId = myCache('openId');
var user = myCache('user');
this.userid = user.userid? user.userid:'';
this.userName=user.nickName?user.nickName:"";
this.userheadimg=user.avatar?user.avatar:require("@/static/image/girl.png");
this.phone = user.userphone;
//
this.loadData();
var that=this;
setTimeout(() => {
if(!that.isConnected){
@ -102,12 +102,12 @@
that.reconnect();
}
}, 500);
that.setmsgnum();
setTimeout(() => {
that.setmsgnum();
}, 2000);
},
onPullDownRefresh() {
console.log('onPullDownRefresh');
@ -155,8 +155,9 @@
// ws
socketinit(){
var that = this;
uni.connectSocket({
url: "wss://www.sanduolantoyoga.com/yoga-imserver/",
let socketTask = null;
socketTask=uni.connectSocket({
url: "ws://192.168.13.9:8528/im",
header: {
// 'content-type': 'application/json',
Authorization: uni.getStorageSync("token"),
@ -171,8 +172,8 @@
console.log(res,'socket连接成功complete');
}
});
uni.onSocketOpen(resopen => {
socketTask.onOpen(resopen => {
console.log('WebSocket连接已打开');
this.isConnected = true;
this.$forceUpdate();
@ -192,10 +193,10 @@
console.log('消息发送失败!')
}
});
//
this.startHeartbeat();
// 线
this.grouplist.forEach((cell,i)=>{
if(cell.minId){
@ -203,7 +204,7 @@
this.pullMessage(i,cell);
}
});
// id
var data= myCache("privateMsgMaxId")?myCache("privateMsgMaxId"):[{id:0,minId:0}];
data.forEach((cell)=>{
@ -220,7 +221,7 @@
this.getList(cell);
}
});
var data= myCache("groupMsgMaxId")?myCache("groupMsgMaxId"):[{id:0,minId:0}];
data.forEach((cell)=>{
var ifcz=false;
@ -235,11 +236,11 @@
this.getGroupList(cell);
}
});
});
//
uni.onSocketClose(res => {
socketTask.onClose(res => {
console.log('WebSocket连接已关闭',res);
this.isConnected = false;
this.$forceUpdate();
@ -247,7 +248,7 @@
this.reconnect();
});
//
uni.onSocketError(err => {
socketTask.onError(err => {
console.error('WebSocket连接打开失败请检查', err);
this.isConnected = false;
this.$forceUpdate();
@ -255,7 +256,7 @@
this.reconnect();
});
//
uni.onSocketMessage(res => {
socketTask.onMessage(res => {
console.log('收到WebSocket服务器消息');
if(res.data){
var rs=JSON.parse(res.data);
@ -270,9 +271,9 @@
// 0: 1: 2: 3: 4: 10, "" 11, " "12, " " 30,""
if(data.type==0||data.type==1||data.type==2||data.type==3||data.type==4||data.type==5||data.type==6){
if(data.type==0){
data.content=decodeURIComponent(data.content);
data.content=decodeURIComponent(data.content);
}
var id="privatechat-" + data.recvId +"-" + data.sendId;
var index=this.ifadd(id);
var chatinfo=null;
@ -313,7 +314,7 @@
}
else{
// 10, "" 11, " " 12, " " 30,""
}
}
}
@ -327,11 +328,11 @@
//
// 0: 1: 2: 3: 4: 10, "" 11, " "12, " " 30,""
if(data.type==0||data.type==1||data.type==2||data.type==3||data.type==4||data.type==5||data.type==6){
if(data.type==0){
data.content=decodeURIComponent(data.content);
data.content=decodeURIComponent(data.content);
}
var id="groupchat-" + data.groupId;
var index=this.ifadd(id);
var chatinfo=null;
@ -382,7 +383,7 @@
}
}
});
},
//
setmsgnum(){
@ -402,12 +403,12 @@
}
},
handleLongPress(info,index) {
console.log(index);
console.log(index);
this.$refs.swipeRef[index].open();
},
//
actionClick(info,index) {
console.log(info,index);
console.log(info,index);
var that=this;
uni.showModal({
title: '提示',
@ -425,7 +426,7 @@
this.setmsgnum();
}
}
});
});
},
//
ifadd(id){
@ -483,16 +484,16 @@
minId: chat.minId,
sort:"groupchat", // privatechat groupchat
from:"message", // yh message
notice: chat.notice,
remarkNickName: chat.remarkNickName,
showNickName: chat.showNickName,
showGroupName: chat.showGroupName,
remarkGroupName: chat.remarkGroupName,
reason: chat.reason,
customerService: chat.customerService,
instructor: chat.instructor,
ownerId: chat.ownerId,
productId: chat.productId,
notice: chat.notice,
remarkNickName: chat.remarkNickName,
showNickName: chat.showNickName,
showGroupName: chat.showGroupName,
remarkGroupName: chat.remarkGroupName,
reason: chat.reason,
customerService: chat.customerService,
instructor: chat.instructor,
ownerId: chat.ownerId,
productId: chat.productId,
productName: chat.productName
}
var data=encodeURIComponent(JSON.stringify(info));
@ -590,7 +591,7 @@
img: "",
sort:"privatechat",
ifload:1
};
this.getPrivateInfo(chatlastinfo);
});
@ -755,7 +756,7 @@
myCache("chatlist-"+this.userid,this.grouplist);
//
this.setmsgnum();
//
var msgchat=myCache(info.id);
if(!msgchat){
@ -801,7 +802,7 @@
msgchat.push(firstmsg);
myCache(info.id,msgchat);
}
}
},
async updateChatList(info){
@ -831,7 +832,7 @@
myCache("chatlist-"+this.userid,this.grouplist);
//
this.setmsgnum();
//
var msgchat=myCache(info.id);
if(!msgchat){
@ -879,7 +880,7 @@
myCache(info.id,msgchat);
}
}
},
//
ifchatadd(id,msgid){
@ -944,7 +945,7 @@
getNavBarHeight(){
return 45+uni.getSystemInfoSync()['statusBarHeight'];
},
}
}
</script>

@ -1,15 +1,15 @@
<template>
<view class="page">
<view class="page-header" :style="{ paddingTop: geStatusBarHeight()+ 8 + 'px'}">
<view class="text-center">消息</view>
<uni-icons type="staff-filled" size="30" color="#ffffff" @click="$u.throttle(gotoContacts(), 2000)"></uni-icons>
</view>
<view class="wrapcon">
<!-- 列表 -->
<!-- 列表 -->
<view v-if="grouplist.length==0 && loadStatus=='nomore'" class="nodata">~</view>
<view class="listcell">
<view class="lcon" v-for="(info, index) in grouplist" :key="index" @click="gotoGroup(info)">
<view class="limg">
@ -35,14 +35,14 @@
</view>
</view>
<u-loadmore v-if="grouplist.length==0 && loadStatus=='loading'" :status="loadStatus" :marginTop="20"></u-loadmore>
</view>
<!-- 提示信息弹窗 -->
<uni-popup ref="message" type="message">
<uni-popup-message :type="msgType" :message="messageText" :duration="2000"></uni-popup-message>
</uni-popup>
</view>
</template>
@ -80,7 +80,7 @@
// },
],
socketTask: null,
isConnected: false, // WebSocket
isConnected: false, // WebSocket
socketmsg:[],//
heartbeatInterval: null, // 20
heartbeatTimeout: 20000, // 20
@ -132,9 +132,9 @@
socketinit(){
var that = this;
if (!that.isConnected) {
console.log("wss://www.sanduolantoyoga.com/yoga-imserver/")
console.log("ws://192.168.13.9:8528/im")
this.socketTask=uni.connectSocket({
url: "wss://www.sanduolantoyoga.com/yoga-imserver/",
url: "ws://192.168.13.9:8528/im",
header: {
// 'content-type': 'application/json',
Authorization: uni.getStorageSync("token"),
@ -149,7 +149,7 @@
console.log(res,'socket连接成功complete');
}
});
uni.onSocketOpen(resopen => {
console.log('WebSocket连接已打开');
that.isConnected = true;
@ -169,13 +169,13 @@
console.log('消息发送失败!')
}
});
//
// this.startHeartbeat();
//
// this.sendWebSocketMessage();
});
uni.onSocketMessage(res => {
console.log('收到WebSocket服务器消息', res);
if(res.data){
@ -205,7 +205,7 @@
// };
// if(data.type==0||data.type==1||data.type==3||data.type==4){
// that.socketmsg.push(data);
// that.$forceUpdate();
// that.$forceUpdate();
// }
// else{
// // 10, "" 11, " " 12, " " 30,""
@ -213,13 +213,13 @@
// }
}
});
uni.onSocketClose(res => {
console.log('WebSocket连接已关闭');
that.isConnected = false;
clearInterval(that.heartbeatInterval); //
});
uni.onSocketError(err => {
console.error('WebSocket连接打开失败请检查', err);
clearInterval(that.heartbeatInterval); //
@ -231,7 +231,7 @@
this.heartbeatInterval = setInterval(() => {
if (this.socketTask) {
this.socketTask.send({
data: JSON.stringify({
data: JSON.stringify({
'cmd': 1,//
'data': {
'accessToken':uni.getStorageSync("token")
@ -254,13 +254,13 @@
}
});
},
// WebSocket
// WebSocket
closeWebSocket() {
uni.closeSocket();
this.isConnected = false;
clearInterval(this.heartbeatInterval); //
},
gotoContacts(){
uni.navigateTo({
url: `/pages/message/contact`
@ -331,7 +331,7 @@
getNavBarHeight(){
return 45+uni.getSystemInfoSync()['statusBarHeight'];
},
}
}
</script>

@ -1,18 +1,18 @@
<template>
<view class="page">
<view class="page-header" :style="{ paddingTop: geStatusBarHeight()+ 8 + 'px'}">
<view class="text-center">消息</view>
<uni-icons type="staff-filled" size="30" color="#ffffff" @click="$u.throttle(gotoContacts(), 2000)"></uni-icons>
</view>
<view class="wrapcon">
<!-- 列表 -->
<!-- 列表 -->
<view v-if="grouplist.length==0 && loadStatus=='nomore'" class="nodata">~</view>
<view class="listcell">
<u-swipe-action :options="options" v-for="(info, index) in grouplist" :key="index" @click="$u.throttle(actionClick(info,index), 2000)" >
<u-swipe-action :options="options" v-for="(info, index) in grouplist" :key="index" @click="$u.throttle(actionClick(info,index), 2000)" >
<view class="lcon" @click="gotoGroup(info,index)">
<view class="limg">
<image class="img" :src="info.img" mode="aspectFill"></image>
@ -25,28 +25,28 @@
<view class="lrow">
<view class="pnr">
{{ info.type==0?info.content:(info.type==1?"图片":(info.type==2?"文件":(info.type==3?"语音":(info.type==4?"视频":(info.type==5||info.type==6?
(info.chatType=='customerService'?(info.send&&info.send.type==3?'订单咨询':((info.send&&info.send.type==2?'课程咨询':'商品咨询'))):""):"")))))}}
(info.chatType=='customerService'?(info.send&&info.send.type==3?'订单咨询':((info.send&&info.send.type==2?'课程咨询':'商品咨询'))):""):"")))))}}
</view>
<view class="lnum" v-if="info.sl>0">
{{ info.sl}}
</view>
</view>
</view>
</view>
</view>
</u-swipe-action>
</view>
<u-loadmore v-if="grouplist.length==0 && loadStatus=='loading'" :status="loadStatus" :marginTop="20"></u-loadmore>
</view>
<!-- 提示信息弹窗 -->
<uni-popup ref="message" type="message">
<uni-popup-message :type="msgType" :message="messageText" :duration="2000"></uni-popup-message>
</uni-popup>
</view>
</template>
@ -75,7 +75,7 @@
imgurl:uni.$http.baseUrl,
grouplist:[],
socketTask: null,
isConnected: false, // WebSocket
isConnected: false, // WebSocket
socketmsg:[],//
heartbeatInterval: null, // 20
heartbeatTimeout: 20000, // 20
@ -130,7 +130,7 @@
methods: {
//
actionClick(info,index) {
console.log(info,index);
console.log(info,index);
var that=this;
uni.showModal({
title: '提示',
@ -145,7 +145,7 @@
myCache("chatlist",that.grouplist);
}
}
});
});
},
//
startHeartbeat() {
@ -153,7 +153,7 @@
this.heartbeatInterval = setInterval(() => {
if (this.socketTask) {
this.socketTask.send({
data: JSON.stringify({
data: JSON.stringify({
'cmd': 1,//
'data': {
'accessToken':uni.getStorageSync("token")
@ -167,9 +167,9 @@
socketinit(){
var that = this;
// if (!that.isConnected) {
that.socketTask=uni.connectSocket({
url: "wss://www.sanduolantoyoga.com/yoga-imserver/",
url: "ws://192.168.13.9:8528/im",
header: {
// 'content-type': 'application/json',
Authorization: uni.getStorageSync("token"),
@ -184,7 +184,7 @@
console.log(res,'socket连接成功complete');
}
});
uni.onSocketOpen(resopen => {
console.log('WebSocket连接已打开');
that.isConnected = true;
@ -205,11 +205,11 @@
console.log('消息发送失败!')
}
});
//
this.startHeartbeat();
});
uni.onSocketMessage(res => {
console.log('收到WebSocket服务器消息');
if(res.data){
@ -218,12 +218,12 @@
if(rs.data){
var data=rs.data;
if(data){
if(data.recvId==that.userid){
if(data.recvId==that.userid){
//
//
// 0: 1: 2: 3: 4: 10, "" 11, " "12, " " 30,""
if(data.type==0){
data.content=decodeURIComponent(data.content);
data.content=decodeURIComponent(data.content);
}
// else if(data.type==1){
// data.content="";
@ -255,7 +255,7 @@
that.$forceUpdate();
}
});
if(ifexist==0){
//
// socket
@ -287,14 +287,14 @@
}
}
});
uni.onSocketClose(res => {
console.log('WebSocket连接已关闭',res);
that.isConnected = false;
that.$forceUpdate();
clearInterval(that.heartbeatInterval); //
});
uni.onSocketError(err => {
console.error('WebSocket连接打开失败请检查', err);
that.isConnected = false;
@ -303,13 +303,13 @@
});
// }
},
// WebSocket
// WebSocket
closeWebSocket() {
uni.closeSocket();
this.isConnected = false;
this.$forceUpdate()
},
gotoContacts(){
uni.navigateTo({
url: `/pages/message/contact`
@ -381,7 +381,7 @@
that.reorder();
myCache("chatlist",that.grouplist);
}, 300);
// socket
this.socketinit();
},
@ -555,7 +555,7 @@
getNavBarHeight(){
return 45+uni.getSystemInfoSync()['statusBarHeight'];
},
}
}
</script>

@ -1,19 +1,19 @@
<template>
<view class="page">
<view class="page-header" :style="{ paddingTop: geStatusBarHeight()+ 8 + 'px'}">
<view class="text-center">消息</view>
<uni-icons type="staff-filled" size="30" color="#ffffff" @click="$u.throttle(gotoContacts(), 2000)"></uni-icons>
</view>
<view class="wrapcon">
<!-- 列表 -->
<!-- 列表 -->
<view v-if="grouplist.length==0 && loadStatus=='nomore'" class="nodata">~</view>
<view class="listcell">
<u-swipe-action ref="swipeRef" :options="options" v-for="(info, index) in grouplist" :key="info.id" style="min-width: 600rpx;"
@click="$u.throttle(actionClick(info,index), 2000)">
@click="$u.throttle(actionClick(info,index), 2000)">
<view class="lcon" @click="gotoGroup(info,index)" :key="index" @longpress="handleLongPress(info,index)">
<view class="limg">
<image class="img" :src="info.img" mode="aspectFill"></image>
@ -26,7 +26,7 @@
<view class="lrow">
<view class="pnr">
{{ info.type==0?info.content:(info.type==1?"图片":(info.type==2?"文件":(info.type==3?"语音":(info.type==4?"视频":
(info.type==5?"订单咨询":(info.type==6?"商品咨询":info.content))))))}}
(info.type==5?"订单咨询":(info.type==6?"商品咨询":info.content))))))}}
</view>
<view class="lnum" v-if="info.sl>0">
{{ info.sl}}
@ -35,19 +35,19 @@
</view>
</view>
</u-swipe-action>
</view>
<u-loadmore v-if="grouplist.length==0 && loadStatus=='loading'" :status="loadStatus" :marginTop="20"></u-loadmore>
</view>
<!-- 提示信息弹窗 -->
<uni-popup ref="message" type="message">
<uni-popup-message :type="msgType" :message="messageText" :duration="2000"></uni-popup-message>
</uni-popup>
</view>
</template>
@ -76,7 +76,7 @@
imgurl:uni.$http.baseUrl,
grouplist:[],
socketTask: null,
isConnected: false, // WebSocket
isConnected: false, // WebSocket
socketmsg:[],//
heartbeatInterval: null, // 20
heartbeatTimeout: 20000, // 20
@ -128,12 +128,12 @@
// },
methods: {
handleLongPress(info,index) {
console.log(index);
console.log(index);
this.$refs.swipeRef[index].open();
},
//
actionClick(info,index) {
console.log(info,index);
console.log(info,index);
var that=this;
uni.showModal({
title: '提示',
@ -149,7 +149,7 @@
myCache("chatlist-"+this.userid,that.grouplist);
}
}
});
});
},
//
startHeartbeat() {
@ -157,7 +157,7 @@
this.heartbeatInterval = setInterval(() => {
if (this.socketTask) {
this.socketTask.send({
data: JSON.stringify({
data: JSON.stringify({
'cmd': 1,//
'data': {
'accessToken':uni.getStorageSync("token")
@ -171,7 +171,7 @@
socketinit(){
var that = this;
that.socketTask=uni.connectSocket({
url: "wss://www.sanduolantoyoga.com/yoga-imserver/",
url: "ws://192.168.13.9:8528/im",
header: {
// 'content-type': 'application/json',
Authorization: uni.getStorageSync("token"),
@ -186,7 +186,7 @@
console.log(res,'socket连接成功complete');
}
});
uni.onSocketOpen(resopen => {
console.log('WebSocket连接已打开');
that.isConnected = true;
@ -207,10 +207,10 @@
console.log('消息发送失败!')
}
});
//
this.startHeartbeat();
// 线
this.grouplist.forEach((cell,i)=>{
if(cell.minId){
@ -218,7 +218,7 @@
this.pullMessage(i,cell);
}
});
// id
var data= myCache("privateMsgMaxId")?myCache("privateMsgMaxId"):[{id:0,minId:0}];
data.forEach((cell)=>{
@ -235,7 +235,7 @@
this.getList(cell);
}
});
var data= myCache("groupMsgMaxId")?myCache("groupMsgMaxId"):[{id:0,minId:0}];
data.forEach((cell)=>{
var ifcz=false;
@ -250,9 +250,9 @@
this.getGroupList(cell);
}
});
});
uni.onSocketMessage(res => {
console.log('收到WebSocket服务器消息');
if(res.data){
@ -267,7 +267,7 @@
//
// 0: 1: 2: 3: 4: 10, "" 11, " "12, " " 30,""
if(data.type==0){
data.content=decodeURIComponent(data.content);
data.content=decodeURIComponent(data.content);
}
if(data.type==0||data.type==1||data.type==2||data.type==3||data.type==4||data.type==5||data.type==6){
var ifexist=0,chatinfo=null;
@ -307,7 +307,7 @@
}
else{
// 10, "" 11, " " 12, " " 30,""
}
}
}
@ -321,7 +321,7 @@
//
// 0: 1: 2: 3: 4: 10, "" 11, " "12, " " 30,""
if(data.type==0){
data.content=decodeURIComponent(data.content);
data.content=decodeURIComponent(data.content);
}
if(data.type==0||data.type==1||data.type==2||data.type==3||data.type==4||data.type==5||data.type==6){
var ifexist=0,idx=null;
@ -337,7 +337,7 @@
that.$forceUpdate();
}
});
if(ifexist==0){
//
// socket
@ -365,14 +365,14 @@
}
else{
// 10, "" 11, " " 12, " " 30,""
}
}
}
}
}
});
//
uni.onSocketClose(res => {
console.log('WebSocket连接已关闭',res);
@ -392,7 +392,7 @@
// this.socketinit();
});
},
// WebSocket
// WebSocket
closeWebSocket() {
uni.closeSocket();
this.isConnected = false;
@ -452,15 +452,15 @@
minId: chat.minId,
sort:"groupchat", // privatechat groupchat
from:"message", // yh message
notice: chat.notice,
remarkNickName: chat.remarkNickName,
showNickName: chat.showNickName,
showGroupName: chat.showGroupName,
remarkGroupName: chat.remarkGroupName,
reason: chat.reason,
customerService: chat.customerService,
instructor: chat.instructor,
productId: chat.productId,
notice: chat.notice,
remarkNickName: chat.remarkNickName,
showNickName: chat.showNickName,
showGroupName: chat.showGroupName,
remarkGroupName: chat.remarkGroupName,
reason: chat.reason,
customerService: chat.customerService,
instructor: chat.instructor,
productId: chat.productId,
productName: chat.productName
}
var data=encodeURIComponent(JSON.stringify(info));
@ -489,7 +489,7 @@
that.reorder();
myCache("chatlist-"+this.userid,that.grouplist);
}, 300);
// socket
this.socketinit();
},
@ -873,7 +873,7 @@
//
this.reorder();
myCache("chatlist-"+this.userid,this.grouplist);
var msgchat=myCache(info.id);
console.log(info.id,msgchat);
if(!msgchat){
@ -973,7 +973,7 @@
getNavBarHeight(){
return 45+uni.getSystemInfoSync()['statusBarHeight'];
},
}
}
</script>

@ -1,6 +1,6 @@
<template>
<view class="page">
<!-- 轮播图商品 -->
<view class="container">
<swiper class="product" @change="change" :current="swiperCurrent"
@ -10,7 +10,7 @@
<swiper-item v-for="(item, index) in swiperList" :key="index" class="imgload">
<view class="swiper-item">
<image :src="item.img" mode="aspectFill"></image>
</view>
</view>
</swiper-item>
</swiper>
<!-- 轮播指示点样式修改 -->
@ -22,7 +22,7 @@
</block>
</view>
</view>
<!-- 课程信息 -->
<view class="s-list">
<view class="product-item">
@ -49,7 +49,7 @@
</text>
</view>
<view v-if="product.remarks" class="tag">
{{product.remarks}}
{{product.remarks}}
</view>
</view>
<view class="pjcon">
@ -89,7 +89,7 @@
</view>
</view>
</view>
<!-- 课程详情 -->
<view class="s-list1">
<scroll-view scroll-x style="width: 100%;" scroll-with-animation>
@ -127,7 +127,7 @@
</swiper-item> -->
</swiper>
</view>
<!-- 底部菜单 -->
<view class="submitcon">
<view class="skipbtn">
@ -148,26 +148,27 @@
<button type="primary" class="bbtn" @tap="gobuy($event,1)" data-img="/static/image/cart.png">加入购物车</button>
<button type="primary" class="bbtn1" @tap="gobuy($event,2)"></button>
</view>
<!-- 是否登录 -->
<openlogin ref="loginId" @getPhoneNumber="getPhoneNumber"></openlogin>
<!-- service 客服-->
<service ref="serviceId" @goservice="goservice"></service>
<!-- top 返回顶部-->
<top ref="topId"></top>
<!-- cart 购物车-->
<cart ref="cartId" x="0.18" y="1.08" @addShopCar="addShopCar" :pdata="product" :ptype="buyType"></cart>
<!-- 加入购物车动画 cartx carty 是购物车位置在屏幕位置的比例 例如左上角x0.1 y0.1 右下角 x0.9 y0.9-->
<shopCarAnimation ref="carAnmation" cartx="0.18" carty="1.08"></shopCarAnimation>
</view>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import { myCache,getcartNum,getRemoteFile,getRemoteHtmlFile } from '../../utils/utils.js';
import openlogin from "../components/openlogin.vue";
import top from "../components/top.vue";
@ -192,11 +193,11 @@
url: '../../static/image/index/p6.jpg',
content: '内容 A'
}
],
//
swiperList: [
// {
// {
// id: 0,
// img: '../../static/image/theme/p9.png',
// },
@ -282,6 +283,9 @@
id:''
};
},
computed: {
...mapGetters('chatStore', ['findChatIdx']), //
},
onLoad(option) {
if(option.id&&option.id!=='undefined'){
this.id=option.id;
@ -330,6 +334,7 @@
this.$refs.topId.topData(e.scrollTop);
},
methods: {
...mapActions('chatStore', ['openChat']),
goservice(val){
console.log(val);
},
@ -367,7 +372,7 @@
async collectDo(){
if(this.iflogin()){
if(this.ifcollect){
//
//
uni.showLoading({
title: '操作中...'
});
@ -378,7 +383,7 @@
title: '已取消收藏!',
icon: 'success',
duration: 2000
})
})
this.ifcollect=!this.ifcollect;
this.$forceUpdate();
}
@ -387,7 +392,7 @@
title: res.msg? res.msg:'收藏操作失败!',
icon: 'error',
duration: 2000
})
})
}
}
else{
@ -402,16 +407,16 @@
title: '已加入收藏!',
icon: 'success',
duration: 2000
})
})
this.ifcollect=!this.ifcollect;
this.$forceUpdate();
}
}
else{
uni.showToast({
title: res.msg? res.msg:'收藏操作失败!',
icon: 'error',
duration: 2000
})
})
}
}
}
@ -422,10 +427,10 @@
try{
const {data: res1} = await uni.$http.get('/api/product/detail/'+this.id);
if (res1&&res1.product) {
var product=res1.product;
// (parseFloat(price)).toFixed(2);
//
var pics=product.albumPics?product.albumPics.split(','):[];
pics.forEach((cell,idx)=>{
@ -434,7 +439,7 @@
img:cell?getRemoteFile(cell):'../../static/image/nopic.jpg',
})
});
this.product={
storeNmae:myCache('myshop'),//
id:product.id, // id
@ -444,9 +449,9 @@
unit:product.unit,
weight:product.weight, //
instructor:product.instructor, // id
brandName:product.brandName, //
productCategoryName:product.productCategoryName, //
isCourse:product.isCourse, //
brandName:product.brandName, //
productCategoryName:product.productCategoryName, //
isCourse:product.isCourse, //
price:product.price, //
pic:product.pic?product.pic:'',//
img:product.pic?product.pic:'',//
@ -468,11 +473,11 @@
// pjname:'',
// pjdate:'2026-06-26',
// pjcontent:'',
//
this.sphtml=product.detailMobileHtml?getRemoteHtmlFile(product.detailMobileHtml):'';
this.$forceUpdate();
// productAttr
var productAttr= JSON.parse(product.productAttr);
if(productAttr&&productAttr.length>0){
@ -493,7 +498,7 @@
});
}
this.$forceUpdate();
//
this.selinit();
}
@ -628,7 +633,7 @@
confirmText: '确定',
});
}
}
else{
//
@ -728,44 +733,53 @@
var data=res.data;
var timestamp = new Date().getTime();
var chatid="groupchat-" + data.id;
var info={
chatId: chatid,
groupId: data.id,
chatName: data.name,
chatAvatar: data.headImage?data.headImage:'/static/image/kfr.png',
chatTime: timestamp,
userid: data.ownerId,
friendId: data.customerService, // userid
teacherId: data.instructor?data.instructor:this.product.instructor, // userid chat.instructor?chat.instructor:chat.teacherId
minId: "", // id
sort:"groupchat", // privatechat groupchat
from:"yh", // yh message
notice: data.notice,
remarkNickName: data.remarkNickName,
showNickName: data.showNickName,
showGroupName: data.showGroupName,
remarkGroupName: data.remarkGroupName,
reason: data.reason,
customerService: data.customerService,
instructor: data.instructor,
ownerId: data.ownerId,
productId: data.productId,
productName: data.productName,
sendinfo:{
type:1,// 1 2 3
id:this.product.id,
name:this.product.name,
pic:this.product.pic,
price:this.product.price,
unit: this.product.unit,
brandName:this.product.brandName,
isCourse:this.product.isCourse
}
}
var data=encodeURIComponent(JSON.stringify(info));
uni.navigateTo({
url: `/pages/chat/groupchat?data=${data}`
});
let chat = {
type: 'GROUP',
targetId: data.id,
showName: data.name,
headImage: data.headImage?data.headImage:'/static/image/kfr.png',
};
this.openChat(chat);
let chatIdx = this.findChatIdx(chat);
uni.navigateTo({ url: `/pages/chatbox/chat-box?chatIdx=${chatIdx}` });
// var info={
// chatId: chatid,
// groupId: data.id,
// chatName: data.name,
// chatAvatar: data.headImage?data.headImage:'/static/image/kfr.png',
// chatTime: timestamp,
// userid: data.ownerId,
// friendId: data.customerService, // userid
// teacherId: data.instructor?data.instructor:this.product.instructor, // userid chat.instructor?chat.instructor:chat.teacherId
// minId: "", // id
// sort:"groupchat", // privatechat groupchat
// from:"yh", // yh message
// notice: data.notice,
// remarkNickName: data.remarkNickName,
// showNickName: data.showNickName,
// showGroupName: data.showGroupName,
// remarkGroupName: data.remarkGroupName,
// reason: data.reason,
// customerService: data.customerService,
// instructor: data.instructor,
// ownerId: data.ownerId,
// productId: data.productId,
// productName: data.productName,
// sendinfo:{
// type:1,// 1 2 3
// id:this.product.id,
// name:this.product.name,
// pic:this.product.pic,
// price:this.product.price,
// unit: this.product.unit,
// brandName:this.product.brandName,
// isCourse:this.product.isCourse
// }
// }
// var data=encodeURIComponent(JSON.stringify(info));
// uni.navigateTo({
// url: `/pages/chat/groupchat?data=${data}`
// });
}
else{
uni.showModal({
@ -780,34 +794,43 @@
else{
//
//
var chatid="privatechat-" + this.userid +"-"+ this.product.customerService;
var timestamp = new Date().getTime();
var info={
chatId: chatid,
chatName: nickName,
chatAvatar: headImage,
chatTime: timestamp,
userid: this.userid,
friendId: this.product.customerService, // userid
minId: "", // id
sort:"privatechat", // privatechat groupchat
from:"yh", // yh message
sendinfo:{
type:2,// 1 2 3
id:this.product.id,
name:this.product.name,
pic:this.product.pic,
price:this.product.price,
unit: this.product.unit,
brandName:this.product.brandName,
isCourse:this.product.isCourse
}
}
var data=encodeURIComponent(JSON.stringify(info));
uni.navigateTo({
url: `/pages/chat/chat?data=${data}`
});
let chat = {
type: 'PRIVATE',
targetId: this.product.instructor,
showName: nickName,
headImage: headImage,
};
this.openChat(chat);
let chatIdx = this.findChatIdx(chat);
uni.navigateTo({ url: `/pages/chatbox/chat-box?chatIdx=${chatIdx}` });
// var chatid="privatechat-" + this.userid +"-"+ this.product.customerService;
// var timestamp = new Date().getTime();
// var info={
// chatId: chatid,
// chatName: nickName,
// chatAvatar: headImage,
// chatTime: timestamp,
// userid: this.userid,
// friendId: this.product.customerService, // userid
// minId: "", // id
// sort:"privatechat", // privatechat groupchat
// from:"yh", // yh message
// sendinfo:{
// type:2,// 1 2 3
// id:this.product.id,
// name:this.product.name,
// pic:this.product.pic,
// price:this.product.price,
// unit: this.product.unit,
// brandName:this.product.brandName,
// isCourse:this.product.isCourse
// }
// }
// var data=encodeURIComponent(JSON.stringify(info));
// uni.navigateTo({
// url: `/pages/chat/chat?data=${data}`
// });
}
},
gotoCourse(){
@ -896,7 +919,7 @@
position: absolute;
bottom: 30rpx;
left: 50%;
//
//
transform: translate(-50%, 0);
-webkit-transform: translate(-50%, 0);
z-index: 99;
@ -927,7 +950,7 @@
}
}
}
.tips{
display: flex;
flex-direction: row;
@ -953,7 +976,7 @@
}
}
}
/* 弹框 */
.allcon{
width: 100%;
@ -1249,9 +1272,9 @@
.swiper-item {
height: 100%;
}
.mlist{
width: 100%;
width: 100%;
height: 80rpx;
display: flex;
flex-direction: row;
@ -1483,7 +1506,7 @@
margin-top: 6rpx;
}
}
/* 为你推荐 */
.f-header{
display:flex;

@ -1,6 +1,6 @@
<template>
<view class="page">
<!-- 轮播图商品 -->
<view class="container">
<swiper class="product" @change="change" :current="swiperCurrent"
@ -10,7 +10,7 @@
<swiper-item v-for="(item, index) in swiperList" :key="index" class="imgload">
<view class="swiper-item">
<image :src="item.img" mode="aspectFill"></image>
</view>
</view>
</swiper-item>
</swiper>
<!-- 轮播指示点样式修改 -->
@ -22,7 +22,7 @@
</block>
</view>
</view>
<!-- 标签信息 -->
<view class="tips">
<view class="tip">
@ -34,7 +34,7 @@
<text class="tip-txt">不支持7天无理由退换货</text>
</view>
</view>
<!-- 商品信息 -->
<view class="s-list">
<view class="product-item">
@ -61,12 +61,12 @@
</text>
</view>
</view>
<view class="secon">
<view class="seyg">已选{{product.sels}} {{product.quantity}} {{product.unit}}</view>
<uni-icons type="forward" color="#b3b3b3" size="20"></uni-icons>
</view>
<view class="pjcon">
<view class="pjitem">
<view class="pjyg">评价{{product.pjnum}}</view>
@ -85,7 +85,7 @@
{{product.pjcontent}}
</view>
</view>
<view class="ocen">
<view class="title">商品参数</view>
<!-- <view v-if="product.list.length>0" class="icell" v-for="(cell, ii) in product.list" :key="ii">
@ -105,9 +105,9 @@
<text class="itxt">{{product.productCategoryName}}</text>
</view>
</view>
</view>
<!-- 商品详情 -->
<view class="s-list1">
<scroll-view style="width: 100%;" scroll-with-animation>
@ -139,7 +139,7 @@
</swiper-item>
</swiper>
</view>
<!-- 底部菜单 -->
<view class="submitcon">
<view class="skipbtn">
@ -160,26 +160,27 @@
<button type="primary" class="bbtn" @tap="gobuy($event,1)" data-img="/static/image/cart.png">加入购物车</button>
<button type="primary" class="bbtn1" @tap="gobuy($event,2)"></button>
</view>
<!-- 是否登录 -->
<openlogin ref="loginId" @getPhoneNumber="getPhoneNumber"></openlogin>
<!-- service 客服-->
<service ref="serviceId" @goservice="goservice"></service>
<!-- top 返回顶部-->
<top ref="topId"></top>
<!-- cart 购物车-->
<cart ref="cartId" x="0.18" y="1.08" @addShopCar="addShopCar" :pdata="product" :ptype="buyType"></cart>
<!-- 加入购物车动画 cartx carty 是购物车位置在屏幕位置的比例 例如左上角x0.1 y0.1 右下角 x0.9 y0.9-->
<shopCarAnimation ref="carAnmation" cartx="0.18" carty="1.08"></shopCarAnimation>
</view>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import { myCache,getcartNum,getRemoteFile,getRemoteHtmlFile } from '../../utils/utils.js';
import openlogin from "../components/openlogin.vue";
import top from "../components/top.vue";
@ -199,7 +200,7 @@
ifcollect:false,
//
swiperList: [
// {
// {
// id: 0,
// img: '../../static/image/theme/p1.jpg',
// },
@ -234,6 +235,9 @@
id:'',
};
},
computed: {
...mapGetters('chatStore', ['findChatIdx']), //
},
onLoad(option) {
if(option.id&&option.id!=='undefined'){
this.id=option.id;
@ -282,6 +286,7 @@
this.$refs.topId.topData(e.scrollTop);
},
methods: {
...mapActions('chatStore', ['openChat']),
goservice(val){
console.log(val);
},
@ -320,7 +325,7 @@
async collectDo(){
if(this.iflogin()){
if(this.ifcollect){
//
//
uni.showLoading({
title: '操作中...'
});
@ -331,7 +336,7 @@
title: '已取消收藏!',
icon: 'success',
duration: 2000
})
})
this.ifcollect=!this.ifcollect;
this.$forceUpdate();
}
@ -340,7 +345,7 @@
title: res.msg? res.msg:'收藏操作失败!',
icon: 'error',
duration: 2000
})
})
}
}
else{
@ -355,16 +360,16 @@
title: '已加入收藏!',
icon: 'success',
duration: 2000
})
})
this.ifcollect=!this.ifcollect;
this.$forceUpdate();
}
}
else{
uni.showToast({
title: res.msg? res.msg:'收藏操作失败!',
icon: 'error',
duration: 2000
})
})
}
}
}
@ -376,10 +381,10 @@
const {data: res1} = await uni.$http.get('/api/product/detail/'+this.id);
console.log(res1);
if (res1&&res1.product) {
var product=res1.product;
// (parseFloat(price)).toFixed(2);
//
var pics=product.albumPics?product.albumPics.split(','):[];
pics.forEach((cell,idx)=>{
@ -388,7 +393,7 @@
img:cell?getRemoteFile(cell):'../../static/image/nopic.jpg',
})
});
this.product={
storeNmae:myCache('myshop'),//
id:product.id, // id
@ -398,9 +403,9 @@
unit:product.unit,
weight:product.weight, //
instructor:product.instructor, // id
brandName:product.brandName, //
productCategoryName:product.productCategoryName, //
isCourse:product.isCourse, //
brandName:product.brandName, //
productCategoryName:product.productCategoryName, //
isCourse:product.isCourse, //
price:product.price, //
pic:product.pic?product.pic:'',//
img:product.pic?product.pic:'',//
@ -422,11 +427,11 @@
// pjname:'',
// pjdate:'2026-06-26',
// pjcontent:'',
//
this.sphtml=product.detailMobileHtml?getRemoteHtmlFile(product.detailMobileHtml):'';
this.$forceUpdate();
// productAttr
var productAttr= JSON.parse(product.productAttr);
if(productAttr&&productAttr.length>0){
@ -547,7 +552,7 @@
this.$refs.loginId.open();
}
else{
this.buyType=type;
if(this.product.ifspec=='1'){
//
@ -651,7 +656,7 @@
});
return false;
}
var nickName="客服";
var headImage="/static/image/kfr.png";
uni.showLoading({
@ -674,33 +679,42 @@
// 线
//
//
var timestamp = new Date().getTime();
var chatid="privatechat-" + this.userid +"-"+ this.product.customerService;
var info={
chatId: chatid,
chatName: nickName,
chatAvatar: headImage,
chatTime: timestamp,
userid: this.userid,
friendId: this.product.customerService, // userid
minId: "", // id
sort:"privatechat", // privatechat groupchat
from:"yh", // yh message
sendinfo:{
type:1,// 1 2 3
id:this.product.id,
name:this.product.name,
pic:this.product.pic,
price:this.product.price,
unit: this.product.unit,
brandName:this.product.brandName,
isCourse:this.product.isCourse
}
}
var data=encodeURIComponent(JSON.stringify(info));
uni.navigateTo({
url: `/pages/chat/chat?data=${data}`
});
let chat = {
type: 'PRIVATE',
targetId: this.userid,
showName: nickName,
headImage: headImage?headImage:'/static/image/kf.png',
};
this.openChat(chat);
let chatIdx = this.findChatIdx(chat);
uni.navigateTo({ url: `/pages/chatbox/chat-box?chatIdx=${chatIdx}` });
// var timestamp = new Date().getTime();
// var chatid="privatechat-" + this.userid +"-"+ this.product.customerService;
// var info={
// chatId: chatid,
// chatName: nickName,
// chatAvatar: headImage,
// chatTime: timestamp,
// userid: this.userid,
// friendId: this.product.customerService, // userid
// minId: "", // id
// sort:"privatechat", // privatechat groupchat
// from:"yh", // yh message
// sendinfo:{
// type:1,// 1 2 3
// id:this.product.id,
// name:this.product.name,
// pic:this.product.pic,
// price:this.product.price,
// unit: this.product.unit,
// brandName:this.product.brandName,
// isCourse:this.product.isCourse
// }
// }
// var data=encodeURIComponent(JSON.stringify(info));
// uni.navigateTo({
// url: `/pages/chat/chat?data=${data}`
// });
},
gotoShop(){
//
@ -788,7 +802,7 @@
position: absolute;
bottom: 30rpx;
left: 50%;
//
//
transform: translate(-50%, 0);
-webkit-transform: translate(-50%, 0);
z-index: 99;
@ -819,7 +833,7 @@
}
}
}
.tips{
display: flex;
flex-direction: row;
@ -845,7 +859,7 @@
}
}
}
/* 弹框 */
.allcon{
width: 100%;
@ -1125,9 +1139,9 @@
.swiper-item {
height: 100%;
}
.mlist{
width: 100%;
width: 100%;
height: 80rpx;
display: flex;
flex-direction: row;
@ -1359,7 +1373,7 @@
margin-top: 6rpx;
}
}
/* 为你推荐 */
.f-header{
display:flex;

@ -1,13 +1,13 @@
<template>
<view class="page">
<view class="fixhead" :style="{ paddingTop: geStatusBarHeight() + 'px'}">
<view class="leftarr">
<uni-icons type="back" size="24" color="#fff" @tap="gotoBack"></uni-icons>
</view>
<!-- <view class="htitle"> 老师介绍 </view> -->
</view>
<view class="acon">
<view class="atop">
<swiper class="swiperb" @change="change" :current="swiperCurrent"
@ -19,7 +19,7 @@
<swiper-item v-for="(item, i) in swiperList" :key="i">
<view class="swiper-item">
<image :src="item.ban_img" mode="aspectFill"></image>
</view>
</view>
</swiper-item>
</swiper>
<view class="dots">
@ -42,31 +42,32 @@
<mp-html :content="html" :markdown="true" :lazy-load="true"/>
</view>
</view>
<view class="submitcon">
<button type="primary" class="bbtn" @click="goconsult()"></button>
<button type="primary" class="bbtn" @click="gocourse()"></button>
</view>
<!-- 是否登录 -->
<openlogin ref="loginId" @getPhoneNumber="getPhoneNumber"></openlogin>
<!-- service 客服-->
<service ref="serviceId" @goservice="goservice"></service>
<!-- top 返回顶部-->
<top ref="topId"></top>
</view>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import { myCache,getRemoteFile,getRemoteHtmlFile } from '../../utils/utils.js';
import openlogin from "../components/openlogin.vue";
import top from "../components/top.vue";
import service from "../components/service.vue";
export default {
components: {
openlogin,top,service
@ -89,6 +90,9 @@
avatar:require("@/static/image/i1.png")
};
},
computed: {
...mapGetters('chatStore', ['findChatIdx']), //
},
onLoad(options) {
if(options.id){
this.id=options.id;
@ -129,6 +133,7 @@
this.$refs.topId.topData(e.scrollTop);
},
methods: {
...mapActions('chatStore', ['openChat']),
change(e) {
this.swiperCurrent = e.detail.current;
this.$forceUpdate();
@ -144,7 +149,7 @@
},
goservice(val){
console.log(val);
},
},
iflogin(){
this.openId = myCache('openId');
this.phone = myCache('phone');
@ -227,24 +232,33 @@
// });
try{
//
var chatid="privatechat-" + this.userid +"-"+ this.id;
var timestamp = new Date().getTime();
var info={
chatId: "privatechat-" + this.userid +"-"+ this.id,
chatType: "coach",
chatName: this.teacherName,
chatAvatar: this.avatar,
chatTime: timestamp,
userid: this.userid,
friendId: this.appUserId, // userid
minId: "", // id
sort: "privatechat", // privatechat groupchat
from: "yh" // yh message
}
var data=encodeURIComponent(JSON.stringify(info));
uni.navigateTo({
url: `/pages/chat/chat?data=${data}`
});
let chat = {
type: 'PRIVATE',
targetId: this.appUserId,
showName: this.teacherName,
headImage: this.avatar?this.avatar:'/static/image/kf.png',
};
this.openChat(chat);
let chatIdx = this.findChatIdx(chat);
uni.navigateTo({ url: `/pages/chatbox/chat-box?chatIdx=${chatIdx}` });
// var chatid="privatechat-" + this.userid +"-"+ this.id;
// var timestamp = new Date().getTime();
// var info={
// chatId: "privatechat-" + this.userid +"-"+ this.id,
// chatType: "coach",
// chatName: this.teacherName,
// chatAvatar: this.avatar,
// chatTime: timestamp,
// userid: this.userid,
// friendId: this.appUserId, // userid
// minId: "", // id
// sort: "privatechat", // privatechat groupchat
// from: "yh" // yh message
// }
// var data=encodeURIComponent(JSON.stringify(info));
// uni.navigateTo({
// url: `/pages/chat/chat?data=${data}`
// });
}
catch(e){
console.log(e);
@ -386,7 +400,7 @@
border:none;
}
}
.swiperb {
width: 100%;
height: 660rpx;
@ -401,7 +415,7 @@
position: absolute;
bottom: 20rpx;
left: 50%;
//
//
transform: translate(-50%, 0);
-webkit-transform: translate(-50%, 0);
z-index: 99;
@ -423,5 +437,5 @@
background: #fff;
}
}
</style>

@ -0,0 +1,177 @@
@font-face {
font-family: "iconfont"; /* Project id 4272106 */
src: url('./static/icon/iconfont.ttf?t=1746119818070') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-remove:before {
content: "\e603";
}
.icon-doc:before {
content: "\e61c";
}
.icon-image:before {
content: "\e7f7";
}
.icon-top-message:before {
content: "\e6ff";
}
.icon-setting:before {
content: "\e851";
}
.icon-phone:before {
content: "\e692";
}
.icon-email:before {
content: "\e611";
}
.icon-username:before {
content: "\e60f";
}
.icon-chat-muted:before {
content: "\e634";
}
.icon-chat-unmuted:before {
content: "\ec44";
}
.icon-privacy-protocol:before {
content: "\e70a";
}
.icon-un-register:before {
content: "\e656";
}
.icon-modify-pwd:before {
content: "\e63c";
}
.icon-user-protocol:before {
content: "\e61a";
}
.icon-film:before {
content: "\e66b";
}
.icon-chat:before {
content: "\e624";
}
.icon-delete:before {
content: "\e605";
}
.icon-receipt:before {
content: "\e601";
}
.icon-pause:before {
content: "\e669";
}
.icon-play:before {
content: "\e620";
}
.icon-voice-play:before {
content: "\e675";
}
.icon-chat-video:before {
content: "\e73b";
}
.icon-chat-voice:before {
content: "\e633";
}
.icon-video:before {
content: "\e685";
}
.icon-ok:before {
content: "\e65a";
}
.icon-at:before {
content: "\e7de";
}
.icon-man:before {
content: "\e615";
}
.icon-girl:before {
content: "\e602";
}
.icon-file:before {
content: "\e671";
}
.icon-add:before {
content: "\e66c";
}
.icon-warning-circle-fill:before {
content: "\e848";
}
.icon-loading:before {
content: "\e93d";
}
.icon-camera:before {
content: "\e600";
}
.icon-folder:before {
content: "\e6a0";
}
.icon-microphone:before {
content: "\e63b";
}
.icon-icon_emoji:before {
content: "\e619";
}
.icon-call:before {
content: "\e610";
}
.icon-keyboard:before {
content: "\e679";
}
.icon-voice-circle:before {
content: "\e67f";
}
.icon-picture:before {
content: "\e653";
}
.icon-search:before {
content: "\e648";
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

@ -0,0 +1,751 @@
import { MESSAGE_TYPE, MESSAGE_STATUS } from "@/common/enums.js"
import localForage from 'localforage'
import {myCache} from "../utils/utils";
// 全局缓存变量(用于延迟渲染)
let cacheChats = []
// =================== Vuex 模块 ===================
const chatModule = {
namespaced: true,
state: () => ({
activeChat: null,
privateMsgMaxId: 0,
groupMsgMaxId: 0,
loadingPrivateMsg: false,
loadingGroupMsg: false,
chats: [] // 实际渲染的会话列表(仅前 N 个)
}),
getters: {
// 是否正在加载离线消息
isLoading: (state) => () => {
return state.loadingPrivateMsg || state.loadingGroupMsg
},
// 返回当前应显示的会话列表(加载中返回缓存列表,否则返回真实列表)
curChats: (state, getters) => () => {
if (cacheChats && getters.isLoading()) {
return cacheChats
}
return state.chats
},
// 查找会话索引
findChatIdx: (state, getters) => (chat) => {
const chats = getters.curChats()
for (let idx in chats) {
if (chats[idx].type === chat.type && chats[idx].targetId === chat.targetId) {
return parseInt(idx)
}
}
return -1
},
// 查找会话对象
findChat: (state, getters) => (chat) => {
const idx = getters.findChatIdx(chat)
return idx >= 0 ? getters.curChats()[idx] : null
},
findChatByFriend: (state, getters) => (fid) => {
const chats = getters.curChats()
return chats.find(c => c.type === 'PRIVATE' && c.targetId === fid)
},
findChatByGroup: (state, getters) => (gid) => {
const chats = getters.curChats()
return chats.find(c => c.type === 'GROUP' && c.targetId === gid)
},
// 在指定 chat 中查找消息
findMessage: (state, getters) => (chat, msgInfo) => {
if (!chat) return null
for (let idx in chat.messages) {
const m = chat.messages[idx]
if (msgInfo.id && m.id === msgInfo.id) return m
if (msgInfo.tmpId && m.tmpId && m.tmpId === msgInfo.tmpId) return m
}
return null
}
},
mutations: {
// ---------- 基础 mutations ----------
SET_CHATS(state, chats) {
state.chats = chats
},
SET_PRIVATE_MSG_MAX_ID(state, id) {
state.privateMsgMaxId = id
},
SET_GROUP_MSG_MAX_ID(state, id) {
state.groupMsgMaxId = id
},
SET_LOADING_PRIVATE(state, loading) {
state.loadingPrivateMsg = loading
},
SET_LOADING_GROUP(state, loading) {
state.loadingGroupMsg = loading
},
SET_ACTIVE_CHAT(state, chat) {
state.activeChat = chat
},
// ---------- 会话操作 ----------
ADD_CHAT(state, newChat) {
// 插入到列表头部
state.chats.unshift(newChat)
},
REMOVE_CHAT(state, idx) {
if (idx >= 0 && idx < state.chats.length) {
state.chats.splice(idx, 1)
}
},
MOVE_CHAT_TO_TOP(state, idx) {
if (idx > 0 && idx < state.chats.length) {
const chat = state.chats[idx]
state.chats.splice(idx, 1)
state.chats.unshift(chat)
chat.lastSendTime = new Date().getTime()
chat.stored = false
}
},
UPDATE_CHAT(state, { idx, chat }) {
if (idx >= 0 && idx < state.chats.length) {
state.chats[idx] = chat
}
},
RESET_UNREAD_COUNT(state, { type, targetId }) {
for (let c of state.chats) {
if (c.type === type && c.targetId === targetId) {
c.unreadCount = 0
c.atMe = false
c.atAll = false
c.stored = false
break
}
}
},
// ---------- 消息操作 ----------
UPDATE_MESSAGE(state, { chatIdx, msgInfo }) {
const chat = state.chats[chatIdx]
if (!chat) return
const msg = chat.messages.find(m => (m.id && m.id === msgInfo.id) || (m.tmpId && m.tmpId === msgInfo.tmpId))
if (msg) {
Object.assign(msg, msgInfo)
chat.stored = false
}
},
DELETE_MESSAGE(state, { chatIdx, msgInfo }) {
const chat = state.chats[chatIdx]
if (!chat) return
const idx = chat.messages.findIndex(m => (m.id && m.id === msgInfo.id) || (m.tmpId && m.tmpId === msgInfo.tmpId))
if (idx !== -1) {
chat.messages.splice(idx, 1)
chat.stored = false
}
},
// 更新会话的头像/名称
UPDATE_CHAT_INFO(state, { targetId, type, headImage, showName }) {
for (let c of state.chats) {
if (c.type === type && c.targetId === targetId) {
if (headImage) c.headImage = headImage
if (showName) c.showName = showName
c.stored = false
break
}
}
},
// 更新 chat 的基本信息lastContent, lastSendTime, sendNickName, unreadCount, atMe, atAll
UPDATE_CHAT_SUMMARY(state, { idx, lastContent, lastSendTime, sendNickName, unreadCount, atMe, atAll }) {
const chat = state.chats[idx];
if (!chat) return;
if (lastContent !== undefined) chat.lastContent = lastContent;
if (lastSendTime !== undefined) chat.lastSendTime = lastSendTime;
if (sendNickName !== undefined) chat.sendNickName = sendNickName;
if (unreadCount !== undefined) chat.unreadCount = unreadCount;
if (atMe !== undefined) chat.atMe = atMe;
if (atAll !== undefined) chat.atAll = atAll;
chat.stored = false;
},
// 插入时间戳消息
PUSH_TIME_TIP(state, { idx, sendTime }) {
const chat = state.chats[idx];
if (!chat) return;
chat.messages.push({ sendTime, type: MESSAGE_TYPE.TIP_TIME });
chat.lastTimeTip = sendTime;
chat.stored = false;
},
// 插入消息(支持指定位置)
INSERT_MESSAGE(state, { idx, msg, insertPos }) {
const chat = state.chats[idx];
if (!chat) return;
if (insertPos === undefined || insertPos === chat.messages.length) {
chat.messages.push(msg);
} else {
chat.messages.splice(insertPos, 0, msg);
}
chat.stored = false;
},
// 更新已有的消息(合并属性)
UPDATE_EXISTING_MESSAGE(state, { idx, msgInfo }) {
const chat = state.chats[idx];
if (!chat) return;
const msg = chat.messages.find(m => (m.id && m.id === msgInfo.id) || (m.tmpId && m.tmpId === msgInfo.tmpId));
if (msg) Object.assign(msg, msgInfo);
chat.stored = false;
},
// 删除消息
DELETE_MESSAGE_BY_ID(state, { idx, msgInfo }) {
const chat = state.chats[idx];
if (!chat) return;
const pos = chat.messages.findIndex(m => (m.id && m.id === msgInfo.id) || (m.tmpId && m.tmpId === msgInfo.tmpId));
if (pos !== -1) {
chat.messages.splice(pos, 1);
chat.stored = false;
}
},
// 撤回消息
RECALL_MESSAGE(state, { idx, targetId, recallContent, sendTime, selfSend }) {
const chat = state.chats[idx];
if (!chat) return;
for (let m of chat.messages) {
if (m.id === targetId) {
m.status = MESSAGE_STATUS.RECALL;
m.content = recallContent;
m.type = MESSAGE_TYPE.TIP_TEXT;
// 更新会话最后一条(如果撤回的是最后一条)
if (chat.messages[chat.messages.length - 1] === m) {
chat.lastContent = recallContent;
chat.lastSendTime = sendTime;
chat.sendNickName = '';
}
break;
}
}
// 处理引用消息
for (let m of chat.messages) {
if (m.quoteMessage && m.quoteMessage.id === targetId) {
m.quoteMessage.content = "引用内容已撤回";
m.quoteMessage.status = MESSAGE_STATUS.RECALL;
m.quoteMessage.type = MESSAGE_TYPE.TIP_TEXT;
}
}
chat.stored = false;
},
// 更新会话头像和名称
UPDATE_CHAT_AVATAR_NAME(state, { type, targetId, headImage, showName }) {
for (let chat of state.chats) {
if (chat.type === type && chat.targetId === targetId) {
if (headImage !== undefined) chat.headImage = headImage;
if (showName !== undefined) chat.showName = showName;
chat.stored = false;
break;
}
}
},
// 一次性替换所有会话(用于 refreshChats
REPLACE_CHATS(state, chats) {
state.chats = chats;
// 清空缓存(全局变量)
cacheChats = null;
},
// 更新单个 chat 的 hotMinIdx 和 stored 等
UPDATE_CHAT_META(state, { idx, hotMinIdx, stored }) {
const chat = state.chats[idx];
if (!chat) return;
if (hotMinIdx !== undefined) chat.hotMinIdx = hotMinIdx;
if (stored !== undefined) chat.stored = stored;
},
CLEAR_UNREAD_COUNT(state, idx) {
const chats = state.chats; // 或者通过 getter 获取当前列表
if (idx >= 0 && idx < chats.length) {
const chat = chats[idx];
if (chat) {
chat.unreadCount = 0;
chat.atMe = false;
chat.atAll = false;
chat.stored = false; // 标记需持久化
}
}
}
},
actions: {
// ========== 初始化 ==========
initChats({ commit, state }, chatsData) {
cacheChats = []
commit('SET_CHATS', [])
commit('SET_PRIVATE_MSG_MAX_ID', chatsData.privateMsgMaxId || 0)
commit('SET_GROUP_MSG_MAX_ID', chatsData.groupMsgMaxId || 0)
const allChats = chatsData.chats || []
for (let chat of allChats) {
chat.stored = false
// 深拷贝存入缓存,用于延迟渲染
cacheChats.push(JSON.parse(JSON.stringify(chat)))
// 仅渲染前 15 个会话(可调)
if (state.chats.length < 15) {
// 直接 push 新对象,避免引用缓存
state.chats.push({ ...chat })
}
}
// 防止图片一直处在加载中状态
cacheChats.forEach((chat) => {
chat.messages.forEach((msg) => {
if (msg.loadStatus == "loading") {
msg.loadStatus = "fail"
}
})
})
},
// ========== 打开会话 ==========
openChat({ state, getters, commit, dispatch }, chatInfo) {
// 加载中直接返回(原逻辑)
// if (getters.isLoading()) return
const chats = getters.curChats()
let existingIdx = -1
for (let idx in chats) {
if (chats[idx].type === chatInfo.type && chats[idx].targetId === chatInfo.targetId) {
existingIdx = parseInt(idx)
break
}
}
if (existingIdx !== -1) {
// 已存在 → 移到顶部
dispatch('moveTop', existingIdx)
} else {
// 新建会话
const newChat = {
targetId: chatInfo.targetId,
type: chatInfo.type,
showName: chatInfo.showName,
headImage: chatInfo.headImage,
lastContent: "",
lastSendTime: new Date().getTime(),
unreadCount: 0,
hotMinIdx: 0,
messages: [],
atMe: false,
atAll: false,
stored: false,
delete: false
}
commit('ADD_CHAT', newChat)
// 原逻辑未立即保存,等待后续消息触发保存
dispatch('saveToStorage', { withColdMessage: false })
}
},
activeChat({ commit, getters }, idx) {
const chats = getters.curChats(); // 使用 getter 获取当前列表(支持加载状态)
if (idx >= 0 && idx < chats.length) {
commit('CLEAR_UNREAD_COUNT', idx);
}
},
// ========== 移动会话到顶部 ==========
moveTop({ state, getters, commit, dispatch }, idx) {
if (getters.isLoading()) return
if (idx > 0 && idx < state.chats.length) {
commit('MOVE_CHAT_TO_TOP', idx)
// 移动后保存(原逻辑在 moveTop 最后调用了 saveToStorage
dispatch('saveToStorage', { withColdMessage: false })
}
},
removeChat({ state, getters, commit, dispatch }, idx) {
const chats = getters.curChats()
chats[idx].delete = true
chats[idx].stored = false
dispatch('saveToStorage', { withColdMessage: false })
},
resetUnreadCount({ state, getters, dispatch }, chatInfo) {
const chats = getters.curChats()
for (let idx in chats) {
if (chats[idx].type === chatInfo.type && chats[idx].targetId === chatInfo.targetId) {
chats[idx].unreadCount = 0
chats[idx].atMe = false
chats[idx].atAll = false
chats[idx].stored = false
dispatch('saveToStorage', { withColdMessage: false })
break
}
}
},
readedMessage({ state, getters, dispatch }, pos) {
const chat = getters.findChatByFriend(pos.friendId)
if (!chat) return
chat.messages.forEach((m) => {
let status=Number(m.status)
if (m.id && m.selfSend && status < MESSAGE_STATUS.RECALL) {
// pos.maxId为空表示整个会话已读
if (!pos.maxId || m.id <= pos.maxId) {
m.status = MESSAGE_STATUS.READED
chat.stored = false
}
}
})
dispatch('saveToStorage', { withColdMessage: false })
},
/**
* 删除私聊会话
* @param {Object} context - Vuex action 上下文
* @param {Number|String} userId - 对方用户ID
*/
removePrivateChat({ getters, dispatch }, userId) {
const chats = getters.curChats()
for (let idx in chats) {
if (chats[idx].type === 'PRIVATE' && chats[idx].targetId === userId) {
// 将索引转为数字(因为 for...in 返回字符串)
dispatch('removeChat', parseInt(idx))
break // 一个用户只有一个私聊会话,找到后退出
}
}
},
/**
* 删除群聊会话
* @param {Object} context - Vuex action 上下文
* @param {Number|String} groupId - 群ID
*/
removeGroupChat({ getters, dispatch }, groupId) {
const chats = getters.curChats()
for (let idx in chats) {
if (chats[idx].type === 'GROUP' && chats[idx].targetId === groupId) {
dispatch('removeChat', parseInt(idx))
break
}
}
},
insertMessage({ state, getters, commit, dispatch, rootState }, { msgInfo, chatInfo }) {
const type = chatInfo.type;
// 更新最大消息 ID
if (msgInfo.id && type === "PRIVATE" && msgInfo.id > state.privateMsgMaxId) {
commit('SET_PRIVATE_MSG_MAX_ID', msgInfo.id);
}
if (msgInfo.id && type === "GROUP" && msgInfo.id > state.groupMsgMaxId) {
commit('SET_GROUP_MSG_MAX_ID', msgInfo.id);
}
// 查找会话
const idx = getters.findChatIdx(chatInfo);
if (idx === -1) return;
const chat = state.chats[idx]; // 直接引用
// 检查是否已存在该消息(重发或更新)
const existingMsg = getters.findMessage(chat, msgInfo);
if (existingMsg) {
commit('UPDATE_EXISTING_MESSAGE', { idx, msgInfo });
dispatch('saveToStorage', { withColdMessage: false });
return;
}
let msgInfoType=Number(msgInfo.type)
let msgInfoStatus=Number(msgInfo.status)
// 准备要更新的字段
let lastContent = chat.lastContent;
if (msgInfoType === MESSAGE_TYPE.IMAGE) lastContent = "[图片]";
else if (msgInfoType === MESSAGE_TYPE.FILE) lastContent = "[文件]";
else if (msgInfoType === MESSAGE_TYPE.AUDIO) lastContent = "[语音]";
else if (msgInfoType === MESSAGE_TYPE.ACT_RT_VOICE) lastContent = "[语音通话]";
else if (msgInfoType === MESSAGE_TYPE.ACT_RT_VIDEO) lastContent = "[视频通话]";
else if (msgInfoType === MESSAGE_TYPE.TEXT || msgInfoType === MESSAGE_TYPE.RECALL || msgInfoType === MESSAGE_TYPE.TIP_TEXT) {
lastContent = msgInfo.content;
}
// 更新最后消息摘要
let unreadCount = chat.unreadCount || 0;
let atMe = chat.atMe || false;
let atAll = chat.atAll || false;
// 未读计数
if (!msgInfo.selfSend && msgInfoStatus !== MESSAGE_STATUS.READED &&
msgInfoStatus !== MESSAGE_STATUS.RECALL && msgInfoType !== MESSAGE_TYPE.TIP_TEXT) {
unreadCount++;
}
// @ 处理
if (!msgInfo.selfSend && chat.type === "GROUP" && msgInfo.atUserIds && msgInfoStatus !== MESSAGE_STATUS.READED) {
const userInfo = rootState.user?.userInfo;
if (userInfo) {
if (msgInfo.atUserIds.includes(userInfo.id)) atMe = true;
if (msgInfo.atUserIds.includes(-1)) atAll = true;
}
}
// 更新会话摘要(通过 mutation
commit('UPDATE_CHAT_SUMMARY', {
idx,
lastContent,
lastSendTime: msgInfo.sendTime,
sendNickName: msgInfo.sendNickName,
unreadCount,
atMe,
atAll
});
// 时间戳提示间隔10分钟
if (!chat.lastTimeTip || (chat.lastTimeTip < msgInfo.sendTime - 600 * 1000)) {
commit('PUSH_TIME_TIP', { idx, sendTime: msgInfo.sendTime });
}
// 计算插入位置(防止乱序)
let insertPos = chat.messages.length;
if (msgInfo.id && msgInfo.id > 0) {
for (let i = 0; i < chat.messages.length; i++) {
const m = chat.messages[i];
if (m.id && msgInfo.id < m.id) {
insertPos = i;
console.log(`消息乱序修正: ${chat.messages.length} -> ${insertPos}`);
break;
}
}
}
// 插入消息
commit('INSERT_MESSAGE', { idx, msg: msgInfo, insertPos });
// 标记已存储为 false已在 mutation 中完成)
// 保存到本地
dispatch('saveToStorage', { withColdMessage: false });
},
// ========== 更新消息 ==========
updateMessage({ getters, commit, dispatch }, { msgInfo, chatInfo }) {
const idx = getters.findChatIdx(chatInfo);
if (idx === -1) return;
commit('UPDATE_EXISTING_MESSAGE', { idx, msgInfo });
dispatch('saveToStorage', { withColdMessage: false });
},
// ========== 删除消息 ==========
deleteMessage({ getters, commit, dispatch }, { msgInfo, chatInfo }) {
const idx = getters.findChatIdx(chatInfo);
if (idx === -1) return;
const chat = getters.findChat(chatInfo);
// 找到消息位置,判断是否为冷消息(仅用于记录,不影响存储)
let isCold = false;
for (let i = 0; i < chat.messages.length; i++) {
const m = chat.messages[i];
if ((m.id && m.id === msgInfo.id) || (m.tmpId && m.tmpId === msgInfo.tmpId)) {
isCold = i < chat.hotMinIdx;
break;
}
}
commit('DELETE_MESSAGE_BY_ID', { idx, msgInfo });
dispatch('saveToStorage', { withColdMessage: isCold });
},
// ========== 撤回消息 ==========
recallMessage({ getters, commit, dispatch, rootState }, { msgInfo, chatInfo }) {
const idx = getters.findChatIdx(chatInfo);
if (idx === -1) return;
const chat = getters.findChat(chatInfo);
const targetId = msgInfo.content; // 被撤回的消息ID
// 构建撤回提示内容
const name = msgInfo.selfSend ? '你' : (chat.type === 'PRIVATE' ? '对方' : msgInfo.sendNickName);
const recallContent = name + "撤回了一条消息";
// 判断是否为冷消息
let isCold = false;
for (let i = 0; i < chat.messages.length; i++) {
if (chat.messages[i].id === targetId) {
isCold = i < chat.hotMinIdx;
break;
}
}
commit('RECALL_MESSAGE', { idx, targetId, recallContent, sendTime: msgInfo.sendTime, selfSend: msgInfo.selfSend });
// 更新未读计数(如果撤回的是对方消息且未读)
if (!msgInfo.selfSend && Number(msgInfo.status) !== MESSAGE_STATUS.READED) {
// 需要在 mutation 中增加未读计数?但原逻辑是 chat.unreadCount++,我们可以在 mutation 中处理
// 但为了复用,我们在 action 中手动增加(通过 UPDATE_CHAT_SUMMARY
const chatRef = state.chats[idx];
if (chatRef) {
commit('UPDATE_CHAT_SUMMARY', { idx, unreadCount: (chatRef.unreadCount || 0) + 1 });
}
}
dispatch('saveToStorage', { withColdMessage: isCold });
},
// ========== 更新会话信息(好友) ==========
updateChatFromFriend({ commit, dispatch }, friend) {
commit('UPDATE_CHAT_AVATAR_NAME', {
type: 'PRIVATE',
targetId: friend.id,
headImage: friend.headImage,
showName: friend.nickName
});
// 存储(会影响多个会话,但只更新了头像名称,可延迟保存)
dispatch('saveToStorage', { withColdMessage: false });
},
// ========== 更新会话信息(用户自身) ==========
updateChatFromUser({ commit, dispatch }, user) {
commit('UPDATE_CHAT_AVATAR_NAME', {
type: 'PRIVATE',
targetId: user.id,
headImage: user.headImageThumb,
showName: user.nickName
});
dispatch('saveToStorage', { withColdMessage: false });
},
// ========== 更新会话信息(群组) ==========
updateChatFromGroup({ commit, dispatch }, group) {
commit('UPDATE_CHAT_AVATAR_NAME', {
type: 'GROUP',
targetId: group.id,
headImage: group.headImageThumb,
showName: group.name
});
dispatch('saveToStorage', { withColdMessage: false });
},
setLoadingPrivateMsg({ commit, dispatch, getters }, loading) {
commit('SET_LOADING_PRIVATE', loading);
if (!getters.isLoading()) { // 使用 context.getters模块内局部 getters
dispatch('refreshChats');
}
},
setLoadingGroupMsg({ commit, dispatch, getters }, loading) {
commit('SET_LOADING_GROUP', loading);
if (!getters.isLoading()) { // 同上
dispatch('refreshChats');
}
},
// ========== 刷新会话列表(将缓存数据一次性渲染) ==========
refreshChats({ commit, dispatch }) {
if (!cacheChats) return;
// 排序(按最后消息时间倒序)
cacheChats.sort((a, b) => b.lastSendTime - a.lastSendTime);
// 非 App 端容量限制每个会话最多保留1000条消息
// #ifndef APP-PLUS
cacheChats.forEach(chat => {
if (chat.messages.length > 1000) {
chat.messages = chat.messages.slice(-1000);
}
});
// #endif
// 记录热数据索引(所有消息都视为热数据)
cacheChats.forEach(chat => chat.hotMinIdx = chat.messages.length);
// 一次性替换 state.chats
commit('REPLACE_CHATS', cacheChats);
// 清空缓存变量(已在 mutation 中处理)
// 持久化
dispatch('saveToStorage', { withColdMessage: true });
},
// ========== 保存到本地 ==========
saveToStorage({ state, getters, rootState }, { withColdMessage = false } = {}) {
if (getters.isLoading()) return
const userId = myCache('user').userid;
if (!userId) return
const key = `chats-app-${userId}`
const chatKeys = []
// 按会话为单位存储,
state.chats.forEach(chat => {
// 只存储有改动的会话
const chatKey = `${key}-${chat.type}-${chat.targetId}`
if (!chat.stored) {
if (chat.delete) {
uni.removeStorageSync(chatKey);
} else {
// 存储冷数据
if (withColdMessage) {
const coldChat = { ...chat, messages: chat.messages.slice(0, chat.hotMinIdx) }
uni.setStorageSync(chatKey, coldChat)
}
// 存储热消息
const hotKey = chatKey + '-hot'
const hotChat = { ...chat, messages: chat.messages.slice(chat.hotMinIdx) }
uni.setStorageSync(hotKey, hotChat);
}
chat.stored = true
}
if (!chat.delete) {
chatKeys.push(chatKey)
}
})
const chatsData = {
privateMsgMaxId: state.privateMsgMaxId,
groupMsgMaxId: state.groupMsgMaxId,
chatKeys
}
uni.setStorageSync(key, chatsData)
// 移除已删除的会话
state.chats = state.chats.filter(c => !c.delete)
},
// ========== 加载离线消息 ==========
loadChat({ commit, dispatch, rootState }) {
return new Promise((resolve, reject) => {
var userId = myCache('user').userid;
if (!userId) return resolve()
const key = `chats-app-${userId}`
const chatsData = uni.getStorageSync(`chats-app-${userId}`);
if (chatsData) {
if (chatsData.chatKeys) {
chatsData.chats = [];
chatsData.chatKeys.forEach(key => {
const coldChat = uni.getStorageSync(key);
const hotChat = uni.getStorageSync(key + '-hot');
if (!coldChat && !hotChat) {
return;
}
// 合并冷热消息
const chat = Object.assign({}, coldChat, hotChat);
if (hotChat && coldChat) {
chat.messages = coldChat.messages.concat(hotChat.messages);
}
chatsData.chats.push(chat);
});
}
// 调用 initChats action 初始化数据
dispatch('initChats', chatsData);
}
resolve();
})
},
clear(state){
},
// ... 其他 actions 类似迁移
}
}
export default chatModule

@ -0,0 +1,146 @@
// store/friendStore.js
import { TERMINAL_TYPE } from '../common/enums.js'
const friendModule = {
namespaced: true,
state: () => ({
friends: [],
timer: null
}),
mutations: {
SET_FRIENDS(state, friends) {
friends.forEach(f => {
f.online = false
f.onlineWeb = false
f.onlineApp = false
})
state.friends = friends
},
UPDATE_FRIEND(state, friend) {
const f = state.friends.find(item => item.id === friend.id)
if (f) {
const copy = JSON.parse(JSON.stringify(f))
Object.assign(f, friend)
f.online = copy.online
f.onlineWeb = copy.onlineWeb
f.onlineApp = copy.onlineApp
}
},
REMOVE_FRIEND(state, id) {
state.friends.filter(f => f.id === id).forEach(f => f.deleted = true)
},
ADD_FRIEND(state, friend) {
state.friends.unshift(friend)
},
SET_ONLINE_STATUS(state, onlineTerminals) {
state.friends.forEach(f => {
const userTerminal = onlineTerminals.find(o => o.userId === f.id)
if (userTerminal) {
f.online = true
f.onlineWeb = userTerminal.terminals.indexOf(TERMINAL_TYPE.WEB) >= 0
f.onlineApp = userTerminal.terminals.indexOf(TERMINAL_TYPE.APP) >= 0
} else {
f.online = false
f.onlineWeb = false
f.onlineApp = false
}
})
},
SET_TIMER(state, timer) {
state.timer = timer
},
CLEAR(state) {
if (state.timer) {
clearTimeout(state.timer)
state.timer = null
}
state.friends = []
}
},
actions: {
setFriends({ commit }, friends) {
commit('SET_FRIENDS', friends)
},
updateFriend({ commit }, friend) {
commit('UPDATE_FRIEND', friend)
},
removeFriend({ commit }, id) {
commit('REMOVE_FRIEND', id)
},
addFriend({ commit, state }, friend) {
const exists = state.friends.some(f => f.id === friend.id)
if (exists) {
commit('UPDATE_FRIEND', friend)
} else {
commit('ADD_FRIEND', friend)
}
},
setOnlineStatus({ commit }, onlineTerminals) {
commit('SET_ONLINE_STATUS', onlineTerminals)
},
// 异步刷新在线状态(使用 await uni.$http.get
async refreshOnlineStatus({ state, commit, dispatch }) {
const userIds = state.friends.filter(f => !f.deleted).map(f => f.id)
if (userIds.length === 0) return
// 调用接口GET /user/terminal/online?userIds=xxx
const onlineTerminals = await uni.$http.get('/api/friend/terminal/online', {
userIds: userIds.join(',')
})
dispatch('setOnlineStatus', onlineTerminals.data.data)
// 清除旧定时器
if (state.timer) {
clearTimeout(state.timer)
}
// 设置新定时器30s后再次刷新
const timer = setTimeout(() => {
dispatch('refreshOnlineStatus')
}, 30000)
commit('SET_TIMER', timer)
},
// 清空
clear({ commit, state }) {
if (state.timer) {
clearTimeout(state.timer)
}
commit('CLEAR')
},
// 加载好友列表
async loadFriend({ dispatch }) {
try {
const friends = await uni.$http.get('/api/friend/list')
if (friends.data.data.all){
dispatch('setFriends', friends.data.data.all)
dispatch('refreshOnlineStatus')
}
} catch (error) {
// 可在此处理错误,原代码 reject此处转为 throw
throw error
}
}
},
getters: {
isFriend: (state) => (userId) => {
return state.friends.filter(f => !f.deleted).some(f => f.id === userId)
},
findFriend: (state) => (id) => {
return state.friends.find(f => f.id === id)
}
}
}
export default friendModule

@ -0,0 +1,81 @@
// store/groupStore.js
const groupModule = {
namespaced: true,
state: () => ({
groups: []
}),
mutations: {
SET_GROUPS(state, groups) {
state.groups = groups
},
ADD_GROUP(state, group) {
// 使用 unshift 插入到列表头部(与 Pinia 保持一致)
state.groups.unshift(group)
},
UPDATE_GROUP(state, group) {
const g = state.groups.find(item => item.id === group.id)
if (g) {
Object.assign(g, group)
}
},
REMOVE_GROUP(state, id) {
state.groups.filter(g => g.id === id).forEach(g => g.quit = true)
},
CLEAR_GROUPS(state) {
state.groups = []
}
},
actions: {
// 设置群组列表
setGroups({ commit }, groups) {
commit('SET_GROUPS', groups)
},
// 添加或更新群组
addGroup({ commit, state }, group) {
const exists = state.groups.some(g => g.id === group.id)
if (exists) {
commit('UPDATE_GROUP', group)
} else {
commit('ADD_GROUP', group)
}
},
// 删除群组(逻辑删除)
removeGroup({ commit }, id) {
commit('REMOVE_GROUP', id)
},
// 更新群组信息
updateGroup({ commit }, group) {
commit('UPDATE_GROUP', group)
},
// 清空群组列表
clear({ commit }) {
commit('CLEAR_GROUPS')
},
// 加载群组列表(异步)
async loadGroup({ dispatch }) {
try {
const groups = await uni.$http.get('/api/group/list')
dispatch('setGroups', groups.data.data)
} catch (error) {
// 将错误抛出,让调用方处理
throw error
}
}
},
getters: {
findGroup: (state) => (id) => {
return state.groups.find(g => g.id === id)
}
}
}
export default groupModule

@ -1,5 +1,8 @@
import Vue from 'vue'
import Vuex from 'vuex'
import chatStore from './chatStore'
import friendStore from './friendStore'
import groupStore from './groupStore'
Vue.use(Vuex)
let lifeData = {};
@ -8,7 +11,7 @@ try{
// 尝试获取本地是否存在lifeData变量第一次启动APP时是不存在的
lifeData = uni.getStorageSync('lifeData');
}catch(e){
}
// 需要永久存储且下次APP启动需要取出的在state中的变量名
@ -79,6 +82,11 @@ const store = new Vuex.Store({
// 保存变量到本地,见顶部函数定义
saveLifeData(saveKey, state[saveKey])
}
},
modules: {
chatStore, // 模块名称为 'chatStore'
friendStore,
groupStore
}
})

@ -1,7 +1,8 @@
/**
* uView UIscss"u-"
* 使uniappuni.scss
* uViewcssscss"u-"使
* uViewcssscss"u-"使
*/
@import 'uview-ui/theme.scss';
@import '@/im-var.scss';

Loading…
Cancel
Save