Browse Source

若干优化

master
xsx 12 months ago
parent
commit
39f0523557
  1. 8
      im-platform/src/main/java/com/bx/implatform/contant/Constant.java
  2. 4
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java
  3. 17
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java
  4. 2
      im-platform/src/main/java/com/bx/implatform/vo/GroupInviteVO.java
  5. 44
      im-uniapp/App.vue
  6. 100
      im-uniapp/common/wssocket.js
  7. 4
      im-uniapp/manifest.json
  8. 7
      im-uniapp/pages/chat/chat.vue
  9. 26
      im-uniapp/pages/login/login.vue
  10. 38
      im-uniapp/pages/register/register.vue
  11. 8
      im-web/src/api/emotion.js
  12. 18
      im-web/src/assets/style/im.scss
  13. 66
      im-web/src/components/chat/ChatBox.vue
  14. 11
      im-web/src/components/chat/ChatInput.vue
  15. 7
      im-web/src/components/chat/ChatItem.vue
  16. 2
      im-web/src/components/chat/ChatMessageItem.vue
  17. 2
      im-web/src/components/common/Emotion.vue
  18. 16
      im-web/src/view/Friend.vue
  19. 893
      im-web/src/view/Group.vue
  20. 48
      im-web/src/view/Home.vue

8
im-platform/src/main/java/com/bx/implatform/contant/Constant.java

@ -16,13 +16,13 @@ public final class Constant {
public static final Long MAX_FILE_SIZE = 20 * 1024 * 1024L; public static final Long MAX_FILE_SIZE = 20 * 1024 * 1024L;
/** /**
* 群聊最大人数 * 人数上限
*/ */
public static final Long MAX_GROUP_MEMBER = 10000L; public static final Long MAX_LARGE_GROUP_MEMBER = 10000L;
/** /**
* 回执消息限制最大人数 * 普通群人数上限
*/ */
public static final Long LARGE_GROUP_MEMBER = 500L; public static final Long MAX_NORMAL_GROUP_MEMBER = 500L;
} }

4
im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java

@ -65,9 +65,9 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
} }
// 群聊成员列表 // 群聊成员列表
List<Long> userIds = groupMemberService.findUserIdsByGroupId(group.getId()); List<Long> userIds = groupMemberService.findUserIdsByGroupId(group.getId());
if (dto.getReceipt() && userIds.size() > Constant.LARGE_GROUP_MEMBER) { if (dto.getReceipt() && userIds.size() > Constant.MAX_LARGE_GROUP_MEMBER) {
// 大群的回执消息过于消耗资源,不允许发送 // 大群的回执消息过于消耗资源,不允许发送
throw new GlobalException(String.format("当前群聊大于%s人,不支持发送回执消息", Constant.LARGE_GROUP_MEMBER)); throw new GlobalException(String.format("当前群聊大于%s人,不支持发送回执消息", Constant.MAX_LARGE_GROUP_MEMBER));
} }
// 不用发给自己 // 不用发给自己
userIds = userIds.stream().filter(id -> !session.getUserId().equals(id)).collect(Collectors.toList()); userIds = userIds.stream().filter(id -> !session.getUserId().equals(id)).collect(Collectors.toList());

17
im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java

@ -123,7 +123,8 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId); String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
redisTemplate.delete(key); redisTemplate.delete(key);
// 推送解散群聊提示 // 推送解散群聊提示
this.sendTipMessage(groupId, userIds, String.format("'%s'解散了群聊", session.getNickName())); String content = String.format("'%s'解散了群聊", session.getNickName());
this.sendTipMessage(groupId, userIds, content, true);
// 推送同步消息 // 推送同步消息
this.sendDelGroupMessage(groupId, userIds, false); this.sendDelGroupMessage(groupId, userIds, false);
log.info("删除群聊,群聊id:{},群聊名称:{}", group.getId(), group.getName()); log.info("删除群聊,群聊id:{},群聊名称:{}", group.getId(), group.getName());
@ -142,7 +143,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId); String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
redisTemplate.opsForHash().delete(key, userId.toString()); redisTemplate.opsForHash().delete(key, userId.toString());
// 推送退出群聊提示 // 推送退出群聊提示
this.sendTipMessage(groupId, List.of(userId), "您已退出群聊"); this.sendTipMessage(groupId, List.of(userId), "您已退出群聊", false);
// 推送同步消息 // 推送同步消息
this.sendDelGroupMessage(groupId, Lists.newArrayList(), true); this.sendDelGroupMessage(groupId, Lists.newArrayList(), true);
log.info("退出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId); log.info("退出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId);
@ -164,7 +165,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId); String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
redisTemplate.opsForHash().delete(key, userId.toString()); redisTemplate.opsForHash().delete(key, userId.toString());
// 推送踢出群聊提示 // 推送踢出群聊提示
this.sendTipMessage(groupId, List.of(userId), "您已被移出群聊"); this.sendTipMessage(groupId, List.of(userId), "您已被移出群聊", false);
// 推送同步消息 // 推送同步消息
this.sendDelGroupMessage(groupId, List.of(userId), false); this.sendDelGroupMessage(groupId, List.of(userId), false);
log.info("踢出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId); log.info("踢出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId);
@ -234,8 +235,8 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
// 群聊人数校验 // 群聊人数校验
List<GroupMember> members = groupMemberService.findByGroupId(vo.getGroupId()); List<GroupMember> members = groupMemberService.findByGroupId(vo.getGroupId());
long size = members.stream().filter(m -> !m.getQuit()).count(); long size = members.stream().filter(m -> !m.getQuit()).count();
if (vo.getFriendIds().size() + size > Constant.MAX_GROUP_MEMBER) { if (vo.getFriendIds().size() + size > Constant.MAX_LARGE_GROUP_MEMBER) {
throw new GlobalException("群聊人数不能大于" + Constant.MAX_GROUP_MEMBER + "人"); throw new GlobalException("群聊人数不能大于" + Constant.MAX_LARGE_GROUP_MEMBER + "人");
} }
// 找出好友信息 // 找出好友信息
List<Friend> friends = friendsService.findByFriendIds(vo.getFriendIds()); List<Friend> friends = friendsService.findByFriendIds(vo.getFriendIds());
@ -267,7 +268,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
List<Long> userIds = groupMemberService.findUserIdsByGroupId(vo.getGroupId()); List<Long> userIds = groupMemberService.findUserIdsByGroupId(vo.getGroupId());
String memberNames = groupMembers.stream().map(GroupMember::getShowNickName).collect(Collectors.joining(",")); String memberNames = groupMembers.stream().map(GroupMember::getShowNickName).collect(Collectors.joining(","));
String content = String.format("'%s'邀请'%s'加入了群聊", session.getNickName(), memberNames); String content = String.format("'%s'邀请'%s'加入了群聊", session.getNickName(), memberNames);
this.sendTipMessage(vo.getGroupId(), userIds, content); this.sendTipMessage(vo.getGroupId(), userIds, content, true);
log.info("邀请进入群聊,群聊id:{},群聊名称:{},被邀请用户id:{}", group.getId(), group.getName(), log.info("邀请进入群聊,群聊id:{},群聊名称:{},被邀请用户id:{}", group.getId(), group.getName(),
vo.getFriendIds()); vo.getFriendIds());
} }
@ -287,7 +288,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
}).sorted((m1, m2) -> m2.getOnline().compareTo(m1.getOnline())).collect(Collectors.toList()); }).sorted((m1, m2) -> m2.getOnline().compareTo(m1.getOnline())).collect(Collectors.toList());
} }
private void sendTipMessage(Long groupId, List<Long> recvIds, String content) { private void sendTipMessage(Long groupId, List<Long> recvIds, String content, Boolean sendToAll) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
// 消息入库 // 消息入库
GroupMessage message = new GroupMessage(); GroupMessage message = new GroupMessage();
@ -298,7 +299,7 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
message.setSendNickName(session.getNickName()); message.setSendNickName(session.getNickName());
message.setGroupId(groupId); message.setGroupId(groupId);
message.setSendId(session.getUserId()); message.setSendId(session.getUserId());
message.setRecvIds(CommaTextUtils.asText(recvIds)); message.setRecvIds(sendToAll ? "" : CommaTextUtils.asText(recvIds));
groupMessageMapper.insert(message); groupMessageMapper.insert(message);
// 推送 // 推送
GroupMessageVO msgInfo = BeanUtils.copyProperties(message, GroupMessageVO.class); GroupMessageVO msgInfo = BeanUtils.copyProperties(message, GroupMessageVO.class);

2
im-platform/src/main/java/com/bx/implatform/vo/GroupInviteVO.java

@ -3,6 +3,7 @@ package com.bx.implatform.vo;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data; import lombok.Data;
import java.util.List; import java.util.List;
@ -15,6 +16,7 @@ public class GroupInviteVO {
@Schema(description = "群id") @Schema(description = "群id")
private Long groupId; private Long groupId;
@Size(max = 50, message = "一次最多只能邀请50位用户")
@NotEmpty(message = "群id不可为空") @NotEmpty(message = "群id不可为空")
@Schema(description = "好友id列表不可为空") @Schema(description = "好友id列表不可为空")
private List<Long> friendIds; private List<Long> friendIds;

44
im-uniapp/App.vue

@ -17,6 +17,7 @@ export default {
}, },
methods: { methods: {
init() { init() {
this.reconnecting = false;
this.isExit = false; this.isExit = false;
// //
this.loadStore().then(() => { this.loadStore().then(() => {
@ -30,20 +31,17 @@ export default {
}, },
initWebSocket() { initWebSocket() {
let loginInfo = uni.getStorageSync("loginInfo") let loginInfo = uni.getStorageSync("loginInfo")
wsApi.init();
wsApi.connect(UNI_APP.WS_URL, loginInfo.accessToken); wsApi.connect(UNI_APP.WS_URL, loginInfo.accessToken);
wsApi.onConnect(() => { wsApi.onConnect(() => {
//
if (this.reconnecting) { if (this.reconnecting) {
this.reconnecting = false; //
uni.showToast({ this.onReconnectWs();
title: "已重新连接", } else {
icon: 'none' // 线
}) this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId);
this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId);
} }
// 线
this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId);
this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId);
}); });
wsApi.onMessage((cmd, msgInfo) => { wsApi.onMessage((cmd, msgInfo) => {
if (cmd == 2) { if (cmd == 2) {
@ -371,12 +369,11 @@ export default {
// //
this.reconnecting = true; this.reconnecting = true;
// token // token
this.reloadUserInfo().then((userInfo) => { this.userStore.loadUser().then((userInfo) => {
uni.showToast({ uni.showToast({
title: '连接已断开,尝试重新连接...', title: '连接已断开,尝试重新连接...',
icon: 'none', icon: 'none'
}) })
this.userStore.setUserInfo(userInfo);
// //
let loginInfo = uni.getStorageSync("loginInfo") let loginInfo = uni.getStorageSync("loginInfo")
wsApi.reconnect(UNI_APP.WS_URL, loginInfo.accessToken); wsApi.reconnect(UNI_APP.WS_URL, loginInfo.accessToken);
@ -387,10 +384,23 @@ export default {
}, 5000) }, 5000)
}) })
}, },
reloadUserInfo() { onReconnectWs() {
return http({ this.reconnecting = false;
url: '/user/self', //
method: 'GET' const promises = [];
promises.push(this.friendStore.loadFriend());
promises.push(this.groupStore.loadGroup());
Promise.all(promises).then(() => {
uni.showToast({
title: "已重新连接",
icon: 'none'
})
// 线
this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId);
this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId);
}).catch((e) => {
console.log(e);
this.exit();
}) })
}, },
closeSplashscreen(delay) { closeSplashscreen(delay) {

100
im-uniapp/common/wssocket.js

@ -1,20 +1,33 @@
let wsurl = "";
let accessToken = ""; let accessToken = "";
let messageCallBack = null; let messageCallBack = null;
let closeCallBack = null; let closeCallBack = null;
let connectCallBack = null; let connectCallBack = null;
let isConnect = false; //连接标识 避免重复连接 let isConnect = false; //连接标识 避免重复连接
let rec = null; let rec = null;
let isInit = false;
let lastConnectTime = new Date(); // 最后一次连接时间 let lastConnectTime = new Date(); // 最后一次连接时间
let socketTask = null;
let init = () => { let connect = (wsurl, token) => {
// 防止重复初始化 accessToken = token;
if (isInit) { if (isConnect) {
return; return;
} }
isInit = true; lastConnectTime = new Date();
uni.onSocketOpen((res) => { 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连接已打开"); console.log("WebSocket连接已打开");
isConnect = true; isConnect = true;
// 发送登录命令 // 发送登录命令
@ -24,12 +37,12 @@ let init = () => {
accessToken: accessToken accessToken: accessToken
} }
}; };
uni.sendSocketMessage({ socketTask.send({
data: JSON.stringify(loginInfo) data: JSON.stringify(loginInfo)
}); });
}) })
uni.onSocketMessage((res) => { socketTask.onMessage((res) => {
let sendInfo = JSON.parse(res.data) let sendInfo = JSON.parse(res.data)
if (sendInfo.cmd == 0) { if (sendInfo.cmd == 0) {
heartCheck.start() heartCheck.start()
@ -45,54 +58,31 @@ let init = () => {
} }
}) })
uni.onSocketClose((res) => { socketTask.onClose((res) => {
console.log('WebSocket连接关闭') console.log('WebSocket连接关闭')
isConnect = false; isConnect = false;
closeCallBack && closeCallBack(res); closeCallBack && closeCallBack(res);
}) })
uni.onSocketError((e) => { socketTask.onError((e) => {
console.log(e) console.log(e)
isConnect = false; isConnect = false;
// APP 应用切出超过一定时间(约1分钟)会触发报错,此处回调给应用进行重连 // APP 应用切出超过一定时间(约1分钟)会触发报错,此处回调给应用进行重连
closeCallBack && closeCallBack({ code: 1006 }); closeCallBack && closeCallBack({ code: 1006 });
}) })
};
let connect = (url, token) => {
wsurl = url;
accessToken = token;
if (isConnect) {
return;
}
lastConnectTime = new Date();
uni.connectSocket({
url: wsurl,
success: (res) => {
console.log("websocket连接成功");
},
fail: (e) => {
console.log(e);
console.log("websocket连接失败,10s后重连");
setTimeout(() => {
connect();
}, 10000)
}
});
} }
//定义重连函数 //定义重连函数
let reconnect = (wsurl, accessToken) => { let reconnect = (wsurl, accessToken) => {
console.log("尝试重新连接"); console.log("尝试重新连接");
if (isConnect) { if (isConnect) {
//如果已经连上就不在重连了
return; return;
} }
// 延迟10秒重连 避免过多次过频繁请求重连 // 延迟10秒重连 避免过多次过频繁请求重连
let timeDiff = new Date().getTime() - lastConnectTime.getTime() let timeDiff = new Date().getTime() - lastConnectTime.getTime()
let delay = timeDiff < 10000 ? 10000 - timeDiff : 0; let delay = timeDiff < 10000 ? 10000 - timeDiff : 0;
rec && clearTimeout(rec); rec && clearTimeout(rec);
rec = setTimeout(function () { rec = setTimeout(function() {
connect(wsurl, accessToken); connect(wsurl, accessToken);
}, delay); }, delay);
}; };
@ -102,7 +92,7 @@ let close = (code) => {
if (!isConnect) { if (!isConnect) {
return; return;
} }
uni.closeSocket({ socketTask.close({
code: code, code: code,
complete: (res) => { complete: (res) => {
console.log("关闭websocket连接"); console.log("关闭websocket连接");
@ -115,39 +105,28 @@ let close = (code) => {
}; };
//心跳设置 // 心跳设置
var heartCheck = { let heartCheck = {
timeout: 10000, //每段时间发送一次心跳包 这里设置为30s timeout: 20000, // 每段时间发送一次心跳包 这里设置为20s
timeoutObj: null, //延时发送消息对象(启动心跳新建这个对象,收到消息后重置对象) timeoutObj: null, // 延时发送消息对象(启动心跳新建这个对象,收到消息后重置对象)
start: function () { start: function() {
if (isConnect) { if (isConnect) {
console.log('发送WebSocket心跳') console.log('发送WebSocket心跳')
let heartBeat = { let heartBeat = {
cmd: 1, cmd: 1,
data: {} data: {}
}; };
uni.sendSocketMessage({ sendMessage(JSON.stringify(heartBeat))
data: JSON.stringify(heartBeat),
fail(res) {
console.log(res);
}
})
} }
}, },
reset: function () { reset: function() {
clearTimeout(this.timeoutObj); clearTimeout(this.timeoutObj);
this.timeoutObj = setTimeout(function () { this.timeoutObj = setTimeout(() => heartCheck.start(), this.timeout);
heartCheck.start();
}, this.timeout);
} }
};
} let sendMessage = (message) => {
socketTask.send({ data: message })
// 实际调用的方法
function sendMessage(agentData) {
uni.sendSocketMessage({
data: agentData
})
} }
let onConnect = (callback) => { let onConnect = (callback) => {
@ -155,19 +134,18 @@ let onConnect = (callback) => {
} }
function onMessage(callback) { let onMessage = (callback) => {
messageCallBack = callback; messageCallBack = callback;
} }
function onClose(callback) { let onClose = (callback) => {
closeCallBack = callback; closeCallBack = callback;
} }
// 将方法暴露出去 // 将方法暴露出去
export { export {
init,
connect, connect,
reconnect, reconnect,
close, close,

4
im-uniapp/manifest.json

@ -2,8 +2,8 @@
"name" : "盒子IM", "name" : "盒子IM",
"appid" : "__UNI__69DD57A", "appid" : "__UNI__69DD57A",
"description" : "", "description" : "",
"versionName" : "3.1.0", "versionName" : "3.4.0",
"versionCode" : 3100, "versionCode" : 3400,
"transformPx" : false, "transformPx" : false,
/* 5+App */ /* 5+App */
"app-plus" : { "app-plus" : {

7
im-uniapp/pages/chat/chat.vue

@ -76,6 +76,12 @@ export default {
moveToTop(chatIdx) { moveToTop(chatIdx) {
this.chatStore.moveTop(chatIdx); this.chatStore.moveTop(chatIdx);
}, },
isShowChat(chat) {
if (chat.delete) {
return false;
}
return !this.searchText || chat.showName.includes(this.searchText)
},
onSearch() { onSearch() {
this.showSearch = !this.showSearch; this.showSearch = !this.showSearch;
this.searchText = ""; this.searchText = "";
@ -146,7 +152,6 @@ export default {
width: 100%; width: 100%;
height: 120rpx; height: 120rpx;
background: white; background: white;
color: $im-text-color-lighter; color: $im-text-color-lighter;
.loading-box { .loading-box {

26
im-uniapp/pages/login/login.vue

@ -1,15 +1,17 @@
<template> <template>
<view class="login"> <view class="login">
<view class="title">欢迎登录</view> <view class="title">欢迎登录</view>
<uni-forms :modelValue="loginForm" :rules="rules" validate-trigger="bind"> <view class="form">
<uni-forms-item name="userName"> <uni-forms :modelValue="loginForm" :rules="rules" validate-trigger="bind">
<uni-easyinput type="text" v-model="loginForm.userName" prefix-icon="person" placeholder="用户名" /> <uni-forms-item name="userName">
</uni-forms-item> <uni-easyinput type="text" v-model="loginForm.userName" prefix-icon="person" placeholder="用户名" />
<uni-forms-item name="password"> </uni-forms-item>
<uni-easyinput type="password" v-model="loginForm.password" prefix-icon="locked" placeholder="密码" /> <uni-forms-item name="password">
</uni-forms-item> <uni-easyinput type="password" v-model="loginForm.password" prefix-icon="locked" placeholder="密码" />
<button class="btn-submit" @click="submit" type="primary">登录</button> </uni-forms-item>
</uni-forms> <button class="btn-submit" @click="submit" type="primary">登录</button>
</uni-forms>
</view>
<navigator class="nav-register" url="/pages/register/register"> <navigator class="nav-register" url="/pages/register/register">
没有账号,前往注册 没有账号,前往注册
</navigator> </navigator>
@ -69,10 +71,10 @@ export default {
} }
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.login { .login {
.title { .title {
padding-top: 150rpx; padding-top: 250rpx;
padding-bottom: 50rpx; padding-bottom: 50rpx;
color: $im-color-primary; color: $im-color-primary;
text-align: center; text-align: center;
@ -80,7 +82,7 @@ export default {
font-weight: bold; font-weight: bold;
} }
.uni-forms { .form {
padding: 50rpx; padding: 50rpx;
.btn-submit { .btn-submit {

38
im-uniapp/pages/register/register.vue

@ -1,21 +1,23 @@
<template> <template>
<view class="register"> <view class="register">
<view class="title">欢迎注册</view> <view class="title">欢迎注册</view>
<uni-forms ref="form" :modelValue="dataForm" :rules="rules" validate-trigger="bind" label-width="80px"> <view class="form">
<uni-forms-item name="userName" label="用户名"> <uni-forms ref="form" :modelValue="dataForm" :rules="rules" validate-trigger="bind" label-width="80px">
<uni-easyinput type="text" v-model="dataForm.userName" placeholder="用户名" /> <uni-forms-item name="userName" label="用户名">
</uni-forms-item> <uni-easyinput type="text" v-model="dataForm.userName" placeholder="用户名" />
<uni-forms-item name="nickName" label="昵称"> </uni-forms-item>
<uni-easyinput type="text" v-model="dataForm.nickName" placeholder="昵称" /> <uni-forms-item name="nickName" label="昵称">
</uni-forms-item> <uni-easyinput type="text" v-model="dataForm.nickName" placeholder="昵称" />
<uni-forms-item name="password" label="密码"> </uni-forms-item>
<uni-easyinput type="password" v-model="dataForm.password" placeholder="密码" /> <uni-forms-item name="password" label="密码">
</uni-forms-item> <uni-easyinput type="password" v-model="dataForm.password" placeholder="密码" />
<uni-forms-item name="corfirmPassword" label="确认密码"> </uni-forms-item>
<uni-easyinput type="password" v-model="dataForm.corfirmPassword" placeholder="确认密码" /> <uni-forms-item name="corfirmPassword" label="确认密码">
</uni-forms-item> <uni-easyinput type="password" v-model="dataForm.corfirmPassword" placeholder="确认密码" />
<button class="btn-submit" @click="submit" type="primary">注册并登录</button> </uni-forms-item>
</uni-forms> <button class="btn-submit" @click="submit" type="primary">注册并登录</button>
</uni-forms>
</view>
<navigator class="nav-login" url="/pages/login/login"> <navigator class="nav-login" url="/pages/login/login">
返回登录页面 返回登录页面
</navigator> </navigator>
@ -111,10 +113,10 @@ export default {
} }
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.register { .register {
.title { .title {
padding-top: 150rpx; padding-top: 250rpx;
padding-bottom: 50rpx; padding-bottom: 50rpx;
color: $im-color-primary; color: $im-color-primary;
text-align: center; text-align: center;
@ -122,7 +124,7 @@ export default {
font-weight: 600; font-weight: 600;
} }
.uni-forms { .form {
padding: 50rpx; padding: 50rpx;
.btn-submit { .btn-submit {

8
im-web/src/api/emotion.js

@ -7,19 +7,19 @@ const emoTextList = ['憨笑', '媚眼', '开心', '坏笑', '可怜', '爱心',
]; ];
let transform = (content) => { let transform = (content, extClass) => {
return content.replace(/\#[\u4E00-\u9FA5]{1,3}\;/gi, textToImg); return content.replace(/\#[\u4E00-\u9FA5]{1,3}\;/gi, (text) => textToImg(text, extClass));
} }
// 将匹配结果替换表情图片 // 将匹配结果替换表情图片
let textToImg = (emoText) => { let textToImg = (emoText, extClass) => {
let word = emoText.replace(/\#|\;/gi, ''); let word = emoText.replace(/\#|\;/gi, '');
let idx = emoTextList.indexOf(word); let idx = emoTextList.indexOf(word);
if (idx == -1) { if (idx == -1) {
return emoText; return emoText;
} }
let url = require(`@/assets/emoji/${idx}.gif`); let url = require(`@/assets/emoji/${idx}.gif`);
return `<img src="${url}" style="width:32px;height:32px;vertical-align:bottom;"/>` return `<img src="${url}" class="${extClass}" />`
} }
let textToUrl = (emoText) => { let textToUrl = (emoText) => {

18
im-web/src/assets/style/im.scss

@ -89,3 +89,21 @@ section {
} }
} }
.emoji-large {
width: 32px;
height: 32px;
vertical-align: bottom;
}
.emoji-normal {
width: 26px;
height: 26px;
vertical-align: bottom;
}
.emoji-small {
width: 20px;
height: 20px;
vertical-align: bottom;
}

66
im-web/src/components/chat/ChatBox.vue

@ -819,72 +819,6 @@ export default {
height: 100%; height: 100%;
background-color: white !important; background-color: white !important;
.send-text-area {
box-sizing: border-box;
padding: 5px;
width: 100%;
flex: 1;
resize: none;
font-size: 16px;
outline: none;
text-align: left;
line-height: 30px;
&:before {
content: attr(placeholder);
color: gray;
}
.at {
color: blue;
font-weight: 600;
}
.receipt {
color: darkblue;
font-size: 15px;
font-weight: 600;
}
.emo {
width: 30px;
height: 30px;
vertical-align: bottom;
}
}
.send-image-area {
text-align: left;
border: #53a0e7 solid 1px;
.send-image-box {
position: relative;
display: inline-block;
.send-image {
max-height: 180px;
border: 1px solid #ccc;
border-radius: 2%;
margin: 2px;
}
.send-image-close {
position: absolute;
padding: 3px;
right: 7px;
top: 7px;
color: white;
cursor: pointer;
font-size: 15px;
font-weight: 600;
background-color: #aaa;
border-radius: 50%;
border: 1px solid #ccc;
}
}
}
.send-btn-area { .send-btn-area {
padding: 10px; padding: 10px;
position: absolute; position: absolute;

11
im-web/src/components/chat/ChatInput.vue

@ -249,7 +249,7 @@ export default {
}, },
insertEmoji(emojiText) { insertEmoji(emojiText) {
let emojiElement = document.createElement('img'); let emojiElement = document.createElement('img');
emojiElement.className = 'chat-emoji no-text'; emojiElement.className = 'emoji-normal no-text';
emojiElement.dataset.emojiCode = emojiText; emojiElement.dataset.emojiCode = emojiText;
emojiElement.src = this.$emo.textToUrl(emojiText); emojiElement.src = this.$emo.textToUrl(emojiText);
@ -482,7 +482,7 @@ export default {
bottom: 0; bottom: 0;
outline: none; outline: none;
padding: 5px; padding: 5px;
line-height: 1.5; line-height: 26px;
font-size: var(--im-font-size); font-size: var(--im-font-size);
text-align: left; text-align: left;
overflow-y: auto; overflow-y: auto;
@ -504,13 +504,6 @@ export default {
cursor: pointer; cursor: pointer;
} }
.chat-emoji {
width: 30px;
height: 30px;
vertical-align: top;
cursor: pointer;
}
.chat-file-container { .chat-file-container {
max-width: 65%; max-width: 65%;
padding: 10px; padding: 10px;

7
im-web/src/components/chat/ChatItem.vue

@ -16,7 +16,7 @@
<div class="chat-content"> <div class="chat-content">
<div class="chat-at-text">{{ atText }}</div> <div class="chat-at-text">{{ atText }}</div>
<div class="chat-send-name" v-show="isShowSendName">{{ chat.sendNickName + ':&nbsp;' }}</div> <div class="chat-send-name" v-show="isShowSendName">{{ chat.sendNickName + ':&nbsp;' }}</div>
<div class="chat-content-text" v-html="$emo.transform(chat.lastContent)"></div> <div class="chat-content-text" v-html="$emo.transform(chat.lastContent,'emoji-small')"></div>
</div> </div>
</div> </div>
<right-menu v-show="rightMenu.show" :pos="rightMenu.pos" :items="rightMenu.items" <right-menu v-show="rightMenu.show" :pos="rightMenu.pos" :items="rightMenu.items"
@ -216,11 +216,6 @@ export default {
font-size: var(--im-font-size-small); font-size: var(--im-font-size-small);
color: var(--im-text-color-light); color: var(--im-text-color-light);
img {
width: 20px !important;
height: 20px !important;
vertical-align: bottom;
}
} }
} }

2
im-web/src/components/chat/ChatMessageItem.vue

@ -207,7 +207,7 @@ export default {
htmlText() { htmlText() {
let color = this.msgInfo.selfSend ? 'white' : ''; let color = this.msgInfo.selfSend ? 'white' : '';
let text = this.$url.replaceURLWithHTMLLinks(this.msgInfo.content, color) let text = this.$url.replaceURLWithHTMLLinks(this.msgInfo.content, color)
return this.$emo.transform(text) return this.$emo.transform(text,'emoji-normal')
} }
} }
} }

2
im-web/src/components/common/Emotion.vue

@ -4,7 +4,7 @@
<el-scrollbar style="height: 220px"> <el-scrollbar style="height: 220px">
<div class="emotion-item-list"> <div class="emotion-item-list">
<div class="emotion-item" v-for="(emoText, i) in $emo.emoTextList" :key="i" <div class="emotion-item" v-for="(emoText, i) in $emo.emoTextList" :key="i"
@click="onClickEmo(emoText)" v-html="$emo.textToImg(emoText)"> @click="onClickEmo(emoText)" v-html="$emo.textToImg(emoText,'emoji-large')">
</div> </div>
</div> </div>
</el-scrollbar> </el-scrollbar>

16
im-web/src/view/Friend.vue

@ -13,8 +13,9 @@
<div v-for="(friends, i) in friendValues" :key="i"> <div v-for="(friends, i) in friendValues" :key="i">
<div class="index-title">{{ friendKeys[i] }}</div> <div class="index-title">{{ friendKeys[i] }}</div>
<div v-for="(friend) in friends" :key="friend.id"> <div v-for="(friend) in friends" :key="friend.id">
<friend-item :friend="friend" :active="friend.id === activeFriend.id" @chat="onSendMessage(friend)" <friend-item :friend="friend" :active="friend.id === activeFriend.id"
@delete="onDelFriend(friend)" @click.native="onActiveItem(friend)"> @chat="onSendMessage(friend)" @delete="onDelFriend(friend)"
@click.native="onActiveItem(friend)">
</friend-item> </friend-item>
</div> </div>
<div v-if="i < friendValues.length - 1" class="divider"></div> <div v-if="i < friendValues.length - 1" class="divider"></div>
@ -184,10 +185,10 @@ export default {
// //
let map = new Map(); let map = new Map();
this.friendStore.friends.forEach((f) => { this.friendStore.friends.forEach((f) => {
if (f.deleted || (this.searchText && !f.showNickName.includes(this.searchText))) { if (f.deleted || (this.searchText && !f.nickName.includes(this.searchText))) {
return; return;
} }
let letter = this.firstLetter(f.showNickName).toUpperCase(); let letter = this.firstLetter(f.nickName).toUpperCase();
// # // #
if (!this.isEnglish(letter)) { if (!this.isEnglish(letter)) {
letter = "#" letter = "#"
@ -246,6 +247,13 @@ export default {
.friend-list-items { .friend-list-items {
flex: 1; flex: 1;
.index-title {
text-align: left;
font-size: var(--im-larger-size-larger);
padding: 5px 15px;
color: var(--im-text-color-light);
}
} }
} }

893
im-web/src/view/Group.vue

@ -1,92 +1,96 @@
<template> <template>
<el-container class="group-page"> <el-container class="group-page">
<el-aside width="260px" class="group-list-box"> <el-aside width="260px" class="group-list-box">
<div class="group-list-header"> <div class="group-list-header">
<el-input class="search-text" size="small" placeholder="搜索" v-model="searchText"> <el-input class="search-text" size="small" placeholder="搜索" v-model="searchText">
<i class="el-icon-search el-input__icon" slot="prefix"> </i> <i class="el-icon-search el-input__icon" slot="prefix"> </i>
</el-input> </el-input>
<el-button plain class="add-btn" icon="el-icon-plus" title="创建群聊" @click="onCreateGroup()"></el-button> <el-button plain class="add-btn" icon="el-icon-plus" title="创建群聊" @click="onCreateGroup()"></el-button>
</div> </div>
<el-scrollbar class="group-list-items"> <el-scrollbar class="group-list-items">
<div v-for="(groups, i) in groupValues" :key="i"> <div v-for="(groups, i) in groupValues" :key="i">
<div class="index-title">{{ groupKeys[i] }}</div> <div class="index-title">{{ groupKeys[i] }}</div>
<div v-for="group in groups" :key="group.id"> <div v-for="group in groups" :key="group.id">
<group-item :group="group" :active="group.id == activeGroup.id" @click.native="onActiveItem(group)"> <group-item :group="group" :active="group.id == activeGroup.id"
</group-item> @click.native="onActiveItem(group)">
</div> </group-item>
<div v-if="i < groupValues.length - 1" class="divider"></div> </div>
</div> <div v-if="i < groupValues.length - 1" class="divider"></div>
</el-scrollbar> </div>
</el-aside> </el-scrollbar>
<el-container class="group-box"> </el-aside>
<div class="group-header" v-show="activeGroup.id"> <el-container class="group-box">
{{ activeGroup.showGroupName }}({{ groupMembers.length }}) <div class="group-header" v-show="activeGroup.id">
</div> {{ activeGroup.showGroupName }}({{ groupMembers.length }})
<div class="group-container"> </div>
<div v-show="activeGroup.id"> <div class="group-container">
<div class="group-info"> <div v-show="activeGroup.id">
<div> <div class="group-info">
<file-upload v-show="isOwner" class="avatar-uploader" :action="imageAction" :showLoading="true" <div>
:maxSize="maxSize" @success="onUploadSuccess" <file-upload v-show="isOwner" class="avatar-uploader" :action="imageAction"
:fileTypes="['image/jpeg', 'image/png', 'image/jpg', 'image/webp']"> :showLoading="true" :maxSize="maxSize" @success="onUploadSuccess"
<img v-if="activeGroup.headImage" :src="activeGroup.headImage" class="avatar"> :fileTypes="['image/jpeg', 'image/png', 'image/jpg', 'image/webp']">
<i v-else class="el-icon-plus avatar-uploader-icon"></i> <img v-if="activeGroup.headImage" :src="activeGroup.headImage" class="avatar">
</file-upload> <i v-else class="el-icon-plus avatar-uploader-icon"></i>
<head-image v-show="!isOwner" class="avatar" :size="160" :url="activeGroup.headImage" </file-upload>
:name="activeGroup.showGroupName" radius="10%"> <head-image v-show="!isOwner" class="avatar" :size="160" :url="activeGroup.headImage"
</head-image> :name="activeGroup.showGroupName" radius="10%">
<el-button class="send-btn" icon="el-icon-position" type="primary" @click="onSendMessage()">发消息 </head-image>
</el-button> <el-button class="send-btn" icon="el-icon-position" type="primary"
</div> @click="onSendMessage()">发消息
<el-form class="group-form" label-width="130px" :model="activeGroup" :rules="rules" size="small" </el-button>
ref="groupForm"> </div>
<el-form-item label="群聊名称" prop="name"> <el-form class="group-form" label-width="130px" :model="activeGroup" :rules="rules" size="small"
<el-input v-model="activeGroup.name" :disabled="!isOwner" maxlength="20"></el-input> ref="groupForm">
</el-form-item> <el-form-item label="群聊名称" prop="name">
<el-form-item label="群主"> <el-input v-model="activeGroup.name" :disabled="!isOwner" maxlength="20"></el-input>
<el-input :value="ownerName" disabled></el-input> </el-form-item>
</el-form-item> <el-form-item label="群主">
<el-form-item label="群名备注"> <el-input :value="ownerName" disabled></el-input>
<el-input v-model="activeGroup.remarkGroupName" :placeholder="activeGroup.name" </el-form-item>
maxlength="20"></el-input> <el-form-item label="群名备注">
</el-form-item> <el-input v-model="activeGroup.remarkGroupName" :placeholder="activeGroup.name"
<el-form-item label="我在本群的昵称"> maxlength="20"></el-input>
<el-input v-model="activeGroup.remarkNickName" maxlength="20" </el-form-item>
:placeholder="$store.state.userStore.userInfo.nickName"></el-input> <el-form-item label="我在本群的昵称">
</el-form-item> <el-input v-model="activeGroup.remarkNickName" maxlength="20"
<el-form-item label="群公告"> :placeholder="$store.state.userStore.userInfo.nickName"></el-input>
<el-input v-model="activeGroup.notice" :disabled="!isOwner" type="textarea" :rows="3" maxlength="1024" </el-form-item>
placeholder="群主未设置"></el-input> <el-form-item label="群公告">
</el-form-item> <el-input v-model="activeGroup.notice" :disabled="!isOwner" type="textarea" :rows="3"
<div> maxlength="1024" placeholder="群主未设置"></el-input>
<el-button type="warning" v-show="isOwner" @click="onInviteMember()">邀请</el-button> </el-form-item>
<el-button type="success" @click="onSaveGroup()">保存</el-button> <div>
<el-button type="danger" v-show="!isOwner" @click="onQuit()">退出</el-button> <el-button type="warning" v-show="isOwner" @click="onInviteMember()">邀请</el-button>
<el-button type="danger" v-show="isOwner" @click="onDissolve()">解散</el-button> <el-button type="success" @click="onSaveGroup()">保存</el-button>
</div> <el-button type="danger" v-show="!isOwner" @click="onQuit()">退出</el-button>
</el-form> <el-button type="danger" v-show="isOwner" @click="onDissolve()">解散</el-button>
</div> </div>
<el-divider content-position="center"></el-divider> </el-form>
<el-scrollbar ref="scrollbar" :style="'height: ' + scrollHeight + 'px'"> </div>
<div class="group-member-list"> <el-divider content-position="center"></el-divider>
<div class="group-invite"> <el-scrollbar ref="scrollbar" :style="'height: ' + scrollHeight + 'px'">
<div class="invite-member-btn" title="邀请好友进群聊" @click="onInviteMember()"> <div class="group-member-list">
<i class="el-icon-plus"></i> <div class="group-invite">
</div> <div class="invite-member-btn" title="邀请好友进群聊" @click="onInviteMember()">
<div class="invite-member-text">邀请</div> <i class="el-icon-plus"></i>
<add-group-member :visible="showAddGroupMember" :groupId="activeGroup.id" :members="groupMembers" </div>
@reload="loadGroupMembers" @close="onCloseAddGroupMember"></add-group-member> <div class="invite-member-text">邀请</div>
</div> <add-group-member :visible="showAddGroupMember" :groupId="activeGroup.id"
<div v-for="(member, idx) in showMembers" :key="member.id"> :members="groupMembers" @reload="loadGroupMembers"
<group-member v-if="idx < showMaxIdx" class="group-member" :member="member" @close="onCloseAddGroupMember"></add-group-member>
:showDel="isOwner && member.userId != activeGroup.ownerId" @del="onKick"></group-member> </div>
</div> <div v-for="(member, idx) in showMembers" :key="member.id">
</div> <group-member v-if="idx < showMaxIdx" class="group-member" :member="member"
</el-scrollbar> :showDel="isOwner && member.userId != activeGroup.ownerId"
</div> @del="onKick"></group-member>
</div> </div>
</el-container> </div>
</el-container> </el-scrollbar>
</div>
</div>
</el-container>
</el-container>
</template> </template>
@ -99,387 +103,394 @@ import HeadImage from '../components/common/HeadImage.vue';
import { pinyin } from 'pinyin-pro'; import { pinyin } from 'pinyin-pro';
export default { export default {
name: "group", name: "group",
components: { components: {
GroupItem, GroupItem,
GroupMember, GroupMember,
FileUpload, FileUpload,
AddGroupMember, AddGroupMember,
HeadImage HeadImage
}, },
data() { data() {
return { return {
searchText: "", searchText: "",
maxSize: 5 * 1024 * 1024, maxSize: 5 * 1024 * 1024,
activeGroup: {}, activeGroup: {},
groupMembers: [], groupMembers: [],
showAddGroupMember: false, showAddGroupMember: false,
showMaxIdx: 150, showMaxIdx: 150,
rules: { rules: {
name: [{ name: [{
required: true, required: true,
message: '请输入群聊名称', message: '请输入群聊名称',
trigger: 'blur' trigger: 'blur'
}] }]
} }
}; };
}, },
methods: { methods: {
onCreateGroup() { onCreateGroup() {
this.$prompt('请输入群聊名称', '创建群聊', { this.$prompt('请输入群聊名称', '创建群聊', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
inputPattern: /\S/, inputPattern: /\S/,
inputErrorMessage: '请输入群聊名称' inputErrorMessage: '请输入群聊名称'
}).then((o) => { }).then((o) => {
let userInfo = this.$store.state.userStore.userInfo; let userInfo = this.$store.state.userStore.userInfo;
let data = { let data = {
name: o.value name: o.value
} }
this.$http({ this.$http({
url: `/group/create?groupName=${o.value}`, url: `/group/create?groupName=${o.value}`,
method: 'post', method: 'post',
data: data data: data
}).then((group) => { }).then((group) => {
this.$store.commit("addGroup", group); this.$store.commit("addGroup", group);
}) })
}) })
}, },
onActiveItem(group) { onActiveItem(group) {
this.showMaxIdx = 150; this.showMaxIdx = 150;
// store // store
this.activeGroup = JSON.parse(JSON.stringify(group)); this.activeGroup = JSON.parse(JSON.stringify(group));
// //
this.loadGroupMembers(); this.loadGroupMembers();
}, },
onInviteMember() { onInviteMember() {
this.showAddGroupMember = true; this.showAddGroupMember = true;
}, },
onCloseAddGroupMember() { onCloseAddGroupMember() {
this.showAddGroupMember = false; this.showAddGroupMember = false;
}, },
onUploadSuccess(data) { onUploadSuccess(data) {
this.activeGroup.headImage = data.originUrl; this.activeGroup.headImage = data.originUrl;
this.activeGroup.headImageThumb = data.thumbUrl; this.activeGroup.headImageThumb = data.thumbUrl;
}, },
onSaveGroup() { onSaveGroup() {
this.$refs['groupForm'].validate((valid) => { this.$refs['groupForm'].validate((valid) => {
if (valid) { if (valid) {
let vo = this.activeGroup; let vo = this.activeGroup;
this.$http({ this.$http({
url: "/group/modify", url: "/group/modify",
method: "put", method: "put",
data: vo data: vo
}).then((group) => { }).then((group) => {
this.$store.commit("updateGroup", group); this.$store.commit("updateGroup", group);
this.$message.success("修改成功"); this.$message.success("修改成功");
}) })
} }
}); });
}, },
onDissolve() { onDissolve() {
this.$confirm(`确认要解散'${this.activeGroup.name}'吗?`, '确认解散?', { this.$confirm(`确认要解散'${this.activeGroup.name}'吗?`, '确认解散?', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
this.$http({ this.$http({
url: `/group/delete/${this.activeGroup.id}`, url: `/group/delete/${this.activeGroup.id}`,
method: 'delete' method: 'delete'
}).then(() => { }).then(() => {
this.$message.success(`群聊'${this.activeGroup.name}'已解散`); this.$message.success(`群聊'${this.activeGroup.name}'已解散`);
this.$store.commit("removeGroup", this.activeGroup.id); this.$store.commit("removeGroup", this.activeGroup.id);
this.reset(); this.reset();
}); });
}) })
}, },
onKick(member) { onKick(member) {
this.$confirm(`确定将成员'${member.showNickName}'移出群聊吗?`, '确认移出?', { this.$confirm(`确定将成员'${member.showNickName}'移出群聊吗?`, '确认移出?', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
this.$http({ this.$http({
url: `/group/kick/${this.activeGroup.id}`, url: `/group/kick/${this.activeGroup.id}`,
method: 'delete', method: 'delete',
params: { params: {
userId: member.userId userId: member.userId
} }
}).then(() => { }).then(() => {
this.$message.success(`已将${member.showNickName}移出群聊`); this.$message.success(`已将${member.showNickName}移出群聊`);
member.quit = true; member.quit = true;
}); });
}) })
}, },
onQuit() { onQuit() {
this.$confirm(`确认退出'${this.activeGroup.showGroupName}',并清空聊天记录吗?`, '确认退出?', { this.$confirm(`确认退出'${this.activeGroup.showGroupName}',并清空聊天记录吗?`, '确认退出?', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
this.$http({ this.$http({
url: `/group/quit/${this.activeGroup.id}`, url: `/group/quit/${this.activeGroup.id}`,
method: 'delete' method: 'delete'
}).then(() => { }).then(() => {
this.$message.success(`您已退出'${this.activeGroup.name}'`); this.$message.success(`您已退出'${this.activeGroup.name}'`);
this.$store.commit("removeGroup", this.activeGroup.id); this.$store.commit("removeGroup", this.activeGroup.id);
this.$store.commit("removeGroupChat", this.activeGroup.id); this.$store.commit("removeGroupChat", this.activeGroup.id);
this.reset(); this.reset();
}); });
}) })
}, },
onSendMessage() { onSendMessage() {
let chat = { let chat = {
type: 'GROUP', type: 'GROUP',
targetId: this.activeGroup.id, targetId: this.activeGroup.id,
showName: this.activeGroup.showGroupName, showName: this.activeGroup.showGroupName,
headImage: this.activeGroup.headImage, headImage: this.activeGroup.headImage,
}; };
this.$store.commit("openChat", chat); this.$store.commit("openChat", chat);
this.$store.commit("activeChat", 0); this.$store.commit("activeChat", 0);
this.$router.push("/home/chat"); this.$router.push("/home/chat");
}, },
onScroll(e) { onScroll(e) {
const scrollbar = e.target; const scrollbar = e.target;
// //
if (scrollbar.scrollTop + scrollbar.clientHeight >= scrollbar.scrollHeight - 30) { if (scrollbar.scrollTop + scrollbar.clientHeight >= scrollbar.scrollHeight - 30) {
if (this.showMaxIdx < this.showMembers.length) { if (this.showMaxIdx < this.showMembers.length) {
this.showMaxIdx += 50; this.showMaxIdx += 50;
} }
} }
}, },
loadGroupMembers() { loadGroupMembers() {
this.$http({ this.$http({
url: `/group/members/${this.activeGroup.id}`, url: `/group/members/${this.activeGroup.id}`,
method: "get" method: "get"
}).then((members) => { }).then((members) => {
this.groupMembers = members; this.groupMembers = members;
}) })
}, },
reset() { reset() {
this.activeGroup = {}; this.activeGroup = {};
this.groupMembers = []; this.groupMembers = [];
}, },
firstLetter(strText) { firstLetter(strText) {
// 使pinyin-pro // 使pinyin-pro
let pinyinOptions = { let pinyinOptions = {
toneType: 'none', // toneType: 'none', //
type: 'normal' // type: 'normal' //
}; };
let pyText = pinyin(strText, pinyinOptions); let pyText = pinyin(strText, pinyinOptions);
return pyText[0]; return pyText[0];
}, },
isEnglish(character) { isEnglish(character) {
return /^[A-Za-z]+$/.test(character); return /^[A-Za-z]+$/.test(character);
} }
}, },
computed: { computed: {
groupStore() { groupStore() {
return this.$store.state.groupStore; return this.$store.state.groupStore;
}, },
ownerName() { ownerName() {
let member = this.groupMembers.find((m) => m.userId == this.activeGroup.ownerId); let member = this.groupMembers.find((m) => m.userId == this.activeGroup.ownerId);
return member && member.showNickName; return member && member.showNickName;
}, },
isOwner() { isOwner() {
return this.activeGroup.ownerId == this.$store.state.userStore.userInfo.id; return this.activeGroup.ownerId == this.$store.state.userStore.userInfo.id;
}, },
imageAction() { imageAction() {
return `/image/upload`; return `/image/upload`;
}, },
groupMap() { groupMap() {
// //
let map = new Map(); let map = new Map();
this.groupStore.groups.forEach((g) => { this.groupStore.groups.forEach((g) => {
if (g.quit || (this.searchText && !g.showGroupName.includes(this.searchText))) { if (g.quit || (this.searchText && !g.showGroupName.includes(this.searchText))) {
return; return;
} }
let letter = this.firstLetter(g.showGroupName).toUpperCase(); let letter = this.firstLetter(g.showGroupName).toUpperCase();
// # // #
if (!this.isEnglish(letter)) { if (!this.isEnglish(letter)) {
letter = "#" letter = "#"
} }
if (map.has(letter)) { if (map.has(letter)) {
map.get(letter).push(g); map.get(letter).push(g);
} else { } else {
map.set(letter, [g]); map.set(letter, [g]);
} }
}) })
// //
let arrayObj = Array.from(map); let arrayObj = Array.from(map);
arrayObj.sort((a, b) => { arrayObj.sort((a, b) => {
// # // #
if (a[0] == '#' || b[0] == '#') { if (a[0] == '#' || b[0] == '#') {
return b[0].localeCompare(a[0]) return b[0].localeCompare(a[0])
} }
return a[0].localeCompare(b[0]) return a[0].localeCompare(b[0])
}) })
map = new Map(arrayObj.map(i => [i[0], i[1]])); map = new Map(arrayObj.map(i => [i[0], i[1]]));
return map; return map;
}, },
groupKeys() { groupKeys() {
return Array.from(this.groupMap.keys()); return Array.from(this.groupMap.keys());
}, },
groupValues() { groupValues() {
return Array.from(this.groupMap.values()); return Array.from(this.groupMap.values());
}, },
showMembers() { showMembers() {
return this.groupMembers.filter((m) => !m.quit) return this.groupMembers.filter((m) => !m.quit)
}, },
scrollHeight() { scrollHeight() {
return Math.min(300, 80 + this.showMembers.length / 10 * 80); return Math.min(300, 80 + this.showMembers.length / 10 * 80);
} }
}, },
mounted() { mounted() {
let scrollWrap = this.$refs.scrollbar.$el.querySelector('.el-scrollbar__wrap'); let scrollWrap = this.$refs.scrollbar.$el.querySelector('.el-scrollbar__wrap');
scrollWrap.addEventListener('scroll', this.onScroll); scrollWrap.addEventListener('scroll', this.onScroll);
} }
} }
</script> </script>
<style lang="scss"> <style lang="scss">
.group-page { .group-page {
.group-list-box { .group-list-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: var(--im-background); background: var(--im-background);
.group-list-header { .group-list-header {
height: 50px; height: 50px;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 8px; padding: 0 8px;
.add-btn { .add-btn {
padding: 5px !important; padding: 5px !important;
margin: 5px; margin: 5px;
font-size: 16px; font-size: 16px;
border-radius: 50%; border-radius: 50%;
} }
} }
.group-list-items { .group-list-items {
flex: 1; flex: 1;
}
}
.group-box { .index-title {
display: flex; text-align: left;
flex-direction: column; font-size: var(--im-larger-size-larger);
padding: 5px 15px;
color: var(--im-text-color-light);
}
}
}
.group-header { .group-box {
display: flex; display: flex;
justify-content: space-between; flex-direction: column;
padding: 0 12px;
line-height: 50px;
font-size: var(--im-font-size-larger);
border-bottom: var(--im-border);
}
.el-divider--horizontal { .group-header {
margin: 16px 0; display: flex;
} justify-content: space-between;
padding: 0 12px;
line-height: 50px;
font-size: var(--im-font-size-larger);
border-bottom: var(--im-border);
}
.group-container { .el-divider--horizontal {
overflow: auto; margin: 16px 0;
padding: 20px; }
flex: 1;
.group-info { .group-container {
display: flex; overflow: auto;
padding: 5px 20px; padding: 20px;
flex: 1;
.group-form { .group-info {
flex: 1; display: flex;
padding-left: 40px; padding: 5px 20px;
max-width: 700px;
}
.avatar-uploader { .group-form {
--width: 160px; flex: 1;
text-align: left; padding-left: 40px;
max-width: 700px;
}
.el-upload { .avatar-uploader {
border: 1px dashed #d9d9d9 !important; --width: 160px;
border-radius: 6px; text-align: left;
cursor: pointer;
position: relative;
overflow: hidden;
}
.el-upload:hover { .el-upload {
border-color: #409EFF; border: 1px dashed #d9d9d9 !important;
} border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader-icon { .el-upload:hover {
font-size: 28px; border-color: #409EFF;
color: #8c939d; }
width: var(--width);
height: var(--width);
line-height: var(--width);
text-align: center;
}
.avatar { .avatar-uploader-icon {
width: var(--width); font-size: 28px;
height: var(--width); color: #8c939d;
display: block; width: var(--width);
} height: var(--width);
} line-height: var(--width);
text-align: center;
}
.send-btn { .avatar {
margin-top: 12px; width: var(--width);
} height: var(--width);
} display: block;
}
}
.group-member-list { .send-btn {
padding: 0 12px; margin-top: 12px;
display: flex; }
align-items: center; }
flex-wrap: wrap;
text-align: center;
.group-member { .group-member-list {
margin-right: 5px; padding: 0 12px;
} display: flex;
align-items: center;
flex-wrap: wrap;
text-align: center;
.group-invite { .group-member {
display: flex; margin-right: 5px;
flex-direction: column; }
align-items: center;
width: 60px;
.invite-member-btn { .group-invite {
width: 38px; display: flex;
height: 38px; flex-direction: column;
line-height: 38px; align-items: center;
border: var(--im-border); width: 60px;
font-size: 14px;
cursor: pointer;
box-sizing: border-box;
&:hover { .invite-member-btn {
border: #aaaaaa solid 1px; width: 38px;
} height: 38px;
} line-height: 38px;
border: var(--im-border);
font-size: 14px;
cursor: pointer;
box-sizing: border-box;
.invite-member-text { &:hover {
font-size: var(--im-font-size-smaller); border: #aaaaaa solid 1px;
text-align: center; }
width: 100%; }
height: 30px;
line-height: 30px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden
}
}
} .invite-member-text {
} font-size: var(--im-font-size-smaller);
text-align: center;
width: 100%;
height: 30px;
line-height: 30px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden
}
}
}
}
}
}
} }
</style> </style>

48
im-web/src/view/Home.vue

@ -81,7 +81,8 @@ export default {
return { return {
showSettingDialog: false, showSettingDialog: false,
lastPlayAudioTime: new Date().getTime() - 1000, lastPlayAudioTime: new Date().getTime() - 1000,
isFullscreen: true isFullscreen: true,
reconnecting: false
} }
}, },
methods: { methods: {
@ -99,9 +100,13 @@ export default {
// ws // ws
this.$wsApi.connect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken")); this.$wsApi.connect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken"));
this.$wsApi.onConnect(() => { this.$wsApi.onConnect(() => {
// 线 if (this.reconnecting) {
this.pullPrivateOfflineMessage(this.$store.state.chatStore.privateMsgMaxId); this.onReconnectWs();
this.pullGroupOfflineMessage(this.$store.state.chatStore.groupMsgMaxId); } else {
// 线
this.pullPrivateOfflineMessage(this.$store.state.chatStore.privateMsgMaxId);
this.pullGroupOfflineMessage(this.$store.state.chatStore.groupMsgMaxId);
}
}); });
this.$wsApi.onMessage((cmd, msgInfo) => { this.$wsApi.onMessage((cmd, msgInfo) => {
if (cmd == 2) { if (cmd == 2) {
@ -130,15 +135,44 @@ export default {
console.log(e); console.log(e);
if (e.code != 3000) { if (e.code != 3000) {
// 线 // 线
this.$message.error("连接断开,正在尝试重新连接..."); this.reconnectWs();
this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem(
"accessToken"));
} }
}); });
}).catch((e) => { }).catch((e) => {
console.log("初始化失败", e); console.log("初始化失败", e);
}) })
}, },
reconnectWs() {
//
this.reconnecting = true;
// token
this.$store.dispatch("loadUser").then(() => {
// 线
this.$message.error("连接断开,正在尝试重新连接...");
this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem(
"accessToken"));
}).catch(() => {
// 10s
setTimeout(() => this.reconnectWs(), 10000)
})
},
onReconnectWs() {
//
this.reconnecting = false;
//
const promises = [];
promises.push(this.$store.dispatch("loadFriend"));
promises.push(this.$store.dispatch("loadGroup"));
Promise.all(promises).then(() => {
// 线
this.pullPrivateOfflineMessage(this.$store.state.chatStore.privateMsgMaxId);
this.pullGroupOfflineMessage(this.$store.state.chatStore.groupMsgMaxId);
this.$message.success("重新连接成功");
}).catch(() => {
this.$message.error("初始化失败");
this.onExit();
})
},
pullPrivateOfflineMessage(minId) { pullPrivateOfflineMessage(minId) {
this.$store.commit("loadingPrivateMsg", true) this.$store.commit("loadingPrivateMsg", true)
this.$http({ this.$http({

Loading…
Cancel
Save