Browse Source

群聊@功能(完成)

master
xsx 2 years ago
parent
commit
a0debd09a0
  1. 9
      README.md
  2. 3
      im-ui/src/components/chat/ChatBox.vue
  3. 7
      im-ui/src/view/Login.vue
  4. 33
      im-uniapp/common/wssocket.js
  5. 176
      im-uniapp/components/chat-at-box/chat-at-box.vue
  6. 2
      im-uniapp/components/chat-message-item/chat-message-item.vue
  7. 6
      im-uniapp/components/head-image/head-image.vue
  8. 115
      im-uniapp/pages/chat/chat-box.vue
  9. 4
      im-uniapp/pages/group/group-invite.vue
  10. 6
      im-uniapp/static/icon/iconfont.css
  11. BIN
      im-uniapp/static/icon/iconfont.ttf
  12. 1
      im-uniapp/store/chatStore.js

9
README.md

@ -13,14 +13,13 @@
#### 近期更新
发布2.0版本,本次更新主要是加入了uniapp版本:
发布2.0版本,本次更新加入了uniapp版本:
- 支持移动端和web端同时在线,多端消息同步
- 目前仅兼容h5和微信小程序,后续会继续兼容更多终端类型
- 页面风格优化:表情包更新、自动生成文字头像等
感兴趣的小伙伴,可在下方扫码体验
- 聊天窗口加入已读未读显示
- 群聊加入@功能
- 界面风格升级,表情包更新、生成文字头像等
#### 在线体验

3
im-ui/src/components/chat/ChatBox.vue

@ -219,7 +219,6 @@
},
createSendText() {
let sendText = ""
console.log(this.$refs.editBox.childNodes);
this.$refs.editBox.childNodes.forEach((node) => {
if (node.nodeName == "#text") {
sendText += node.textContent;
@ -233,10 +232,8 @@
},
createAtUserIds() {
let ids = [];
console.log(this.$refs.editBox.childNodes);
this.$refs.editBox.childNodes.forEach((node) => {
if (node.nodeName == "SPAN") {
console.log(node);
ids.push(node.dataset.id);
}
})

7
im-ui/src/view/Login.vue

@ -17,6 +17,13 @@
<li>修改拉取离线消息机制:用户登录后,自动从服务器同步最近1个月的消息</li>
</ul>
</div>
<div>
<h3>最近更新(2023-11-12)</h3>
<ul>
<li>群聊加入@功能</li>
<li>聊天输入框支持显示emoji表情</li>
</ul>
</div>
<div>
<h3>项目依旧完全开源可内网部署如果项目对您有帮助,请帮忙点个star:</h3>
</div>

33
im-uniapp/common/wssocket.js

@ -4,9 +4,14 @@ let messageCallBack = null;
let closeCallBack = null;
let isConnect = false; //连接标识 避免重复连接
let rec = null;
let isInit = false;
let init = () => {
// 防止重复初始化
if (isInit) {
return;
}
isInit = true;
uni.onSocketOpen((res) => {
console.log("WebSocket连接已打开");
isConnect = true;
@ -21,7 +26,7 @@ let init = () => {
data: JSON.stringify(loginInfo)
});
})
uni.onSocketMessage((res) => {
let sendInfo = JSON.parse(res.data)
if (sendInfo.cmd == 0) {
@ -32,17 +37,17 @@ let init = () => {
heartCheck.reset();
} else {
// 其他消息转发出去
console.log("接收到消息",sendInfo);
console.log("接收到消息", sendInfo);
messageCallBack && messageCallBack(sendInfo.cmd, sendInfo.data)
}
})
uni.onSocketClose((res) => {
console.log('WebSocket连接关闭')
isConnect = false; //断开后修改标识
closeCallBack && closeCallBack(res);
})
uni.onSocketError((e) => {
console.log(e)
isConnect = false; //连接断开修改标识
@ -53,7 +58,7 @@ let init = () => {
})
};
let connect = (url, token)=>{
let connect = (url, token) => {
wsurl = url;
accessToken = token;
if (isConnect) {
@ -67,23 +72,23 @@ let connect = (url, token)=>{
fail: (e) => {
console.log(e);
console.log("websocket连接失败,10s后重连");
setTimeout(()=>{
setTimeout(() => {
connect();
},10000)
}, 10000)
}
});
}
//定义重连函数
let reconnect = (wsurl,accessToken) => {
let reconnect = (wsurl, accessToken) => {
console.log("尝试重新连接");
if (isConnect){
if (isConnect) {
//如果已经连上就不在重连了
return;
return;
}
rec && clearTimeout(rec);
rec = setTimeout(function() { // 延迟15秒重连 避免过多次过频繁请求重连
connect(wsurl,accessToken);
connect(wsurl, accessToken);
}, 15000);
};
@ -98,8 +103,8 @@ let close = () => {
console.log("关闭websocket连接");
isConnect = false;
},
fail:(e)=>{
console.log("关闭websocket连接失败",e);
fail: (e) => {
console.log("关闭websocket连接失败", e);
}
});
};

176
im-uniapp/components/chat-at-box/chat-at-box.vue

@ -0,0 +1,176 @@
<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 showMembers" v-show="m.checked" class="at-user-item">
<head-image :name="m.aliasName" :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">
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true">
<view v-for="m in showMembers" v-show="m.aliasName.startsWith(searchText)"
:key="m.userId">
<view class="member-item" @click="onSwitchChecked(m)">
<head-image :name="m.aliasName" :online="m.online" :url="m.headImage"
:size="90"></head-image>
<view class="member-name">{{ m.aliasName}}</view>
<view class="member-checked">
<radio :checked="m.checked" @click.stop="onSwitchChecked(m)" />
</view>
</view>
</view>
</scroll-view>
</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.$store.state.userStore.userInfo.id;
if(this.ownerId == userId){
this.showMembers.push({
userId:-1,
aliasName: "全体成员"
})
}
this.members.forEach((m) => {
if(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() {
let ids = [];
this.showMembers.forEach((m) => {
if (m.checked) {
ids.push(m.userId);
}
})
return ids;
}
}
}
</script>
<style lang="scss" scoped>
.chat-at-box {
position: relative;
border: #dddddd solid 1rpx;
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: 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;
}
}
.scroll-bar {
height: 800rpx;
}
}
}
</style>

2
im-uniapp/components/chat-message-item/chat-message-item.vue

@ -7,7 +7,7 @@
<view class="chat-msg-normal" v-if="msgInfo.type>=0 && msgInfo.type<10"
:class="{'chat-msg-mine':msgInfo.selfSend}">
<head-image class="avatar" :id="msgInfo.sendId" :url="headImage" :name="showName" :size="80"></head-image>
<head-image class="avatar" @longpress="$emit('longPressHead')" :id="msgInfo.sendId" :url="headImage" :name="showName" :size="80"></head-image>
<view class="chat-msg-content" @longpress="onShowMenu($event)">
<view v-if="msgInfo.groupId && !msgInfo.selfSend" class="chat-msg-top">
<text>{{showName}}</text>

6
im-uniapp/components/head-image/head-image.vue

@ -1,5 +1,5 @@
<template>
<view class="head-image" @click="showUserInfo($event)">
<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">
@ -26,7 +26,7 @@
},
size: {
type: Number,
default: 50
default: 20
},
url: {
type: String
@ -77,6 +77,8 @@
position: relative;
overflow: hidden;
border-radius: 10%;
border: 1px solid #ccc;
vertical-align: bottom;
}
.avatar-text {

115
im-uniapp/pages/chat/chat-box.vue

@ -1,5 +1,5 @@
<template>
<view class=" page chat-box">
<view class="page chat-box">
<view class="header">
<uni-icons class="btn-side left" type="back" size="30" @click="onNavBack()"></uni-icons>
<text class="title">{{title}}</text>
@ -7,26 +7,37 @@
</view>
<view class="chat-msg" @click="switchChatTabBox('none',true)">
<scroll-view class="scroll-box" scroll-y="true" @scrolltoupper="onScrollToTop"
:scroll-into-view="'chat-item-'+scrollMsgIdx">
:scroll-into-view="'chat-item-'+scrollMsgIdx">
<view v-for="(msgInfo,idx) in chat.messages" :key="idx">
<chat-message-item v-if="idx>=showMinIdx" :headImage="headImage(msgInfo)" :showName="showName(msgInfo)"
@recall="onRecallMessage" @delete="onDeleteMessage" @download="onDownloadFile"
:id="'chat-item-'+idx" :msgInfo="msgInfo">
<chat-message-item v-if="idx>=showMinIdx" :headImage="headImage(msgInfo)"
:showName="showName(msgInfo)" @recall="onRecallMessage" @delete="onDeleteMessage"
@longPressHead="onLongPressHead(msgInfo)"
@download="onDownloadFile" :id="'chat-item-'+idx" :msgInfo="msgInfo">
</chat-message-item>
</view>
</scroll-view>
</view>
<view v-if="atUserIds.length>0" class="chat-at-bar" @click="openAtBox()">
<view class="iconfont icon-at">:&nbsp;</view>
<scroll-view v-if="atUserIds.length>0" class="chat-at-scroll-box" scroll-x="true" scroll-left="120">
<view class="chat-at-items">
<view v-for="m in atUserItems" class="chat-at-item">
<head-image :name="m.aliasName" :url="m.headImage" :size="50"></head-image>
</view>
</view>
</scroll-view>
</view>
<view class="send-bar">
<view class="iconfont icon-voice-circle" @click="showTip()"></view>
<view class="send-text">
<textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false"
:adjust-position="false" @confirm="sendTextMessage()" @keyboardheightchange="onKeyboardheightchange"
confirm-type="send" confirm-hold :hold-keyboard="true"></textarea>
</view>
<view v-if="chat.type=='GROUP'" class="iconfont icon-at" @click="openAtBox()"></view>
<view class="iconfont icon-icon_emoji" @click="switchChatTabBox('emo',true)"></view>
<view v-show="sendText==''" class="iconfont icon-add" @click="switchChatTabBox('tools',true)">
<view v-if="sendText==''" class="iconfont icon-add" @click="switchChatTabBox('tools',true)">
</view>
<button v-show="sendText!=''" class="btn-send" type="primary" @touchend.prevent="sendTextMessage()"
<button v-if="sendText!=''||atUserIds.length>0" class="btn-send" type="primary" @touchend.prevent="sendTextMessage()"
size="mini">发送</button>
</view>
@ -53,7 +64,6 @@
</file-upload>
<view class="tool-name">文件</view>
</view>
<view class="chat-tools-item" @click="showTip()">
<view class="tool-icon iconfont icon-microphone"></view>
<view class="tool-name">语音输入</view>
@ -62,9 +72,7 @@
<view class="tool-icon iconfont icon-call"></view>
<view class="tool-name">呼叫</view>
</view>
</view>
<scroll-view v-if="chatTabBox==='emo'" class="chat-emotion" scroll-y="true">
<view class="emotion-item-list">
<image class="emotion-item" :title="emoText" :src="$emo.textToPath(emoText)"
@ -74,7 +82,8 @@
</scroll-view>
<view v-if="showKeyBoard"></view>
</view>
<chat-at-box ref="atBox" :ownerId="group.ownerId" :members="groupMembers"
@complete="onAtComplete"></chat-at-box>
</view>
</template>
@ -92,6 +101,7 @@
chatTabBox: 'none',
showKeyBoard: false,
keyboardHeight: 322,
atUserIds: [],
showMinIdx: 0 // showMinIdx
}
},
@ -102,6 +112,18 @@
icon: "none"
})
},
openAtBox() {
this.$refs.atBox.init(this.atUserIds);
this.$refs.atBox.open();
},
onAtComplete(atUserIds) {
this.atUserIds = atUserIds;
},
onLongPressHead(msgInfo){
if(!msgInfo.selfSend && this.chat.type=="GROUP" && this.atUserIds.indexOf(msgInfo.sendId)<0){
this.atUserIds.push(msgInfo.sendId);
}
},
headImage(msgInfo) {
if (this.chat.type == 'GROUP') {
let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
@ -117,17 +139,19 @@
} else {
return msgInfo.selfSend ? this.mine.nickName : this.chat.showName
}
},
sendTextMessage() {
if (!this.sendText.trim()) {
if (!this.sendText.trim() && this.atUserIds.length==0) {
return uni.showToast({
title: "不能发送空白信息",
icon: "none"
});
}
let atText = this.createAtText()
let msgInfo = {
content: this.sendText,
content: this.sendText + atText,
atUserIds: this.atUserIds,
type: 0
}
// id
@ -148,8 +172,20 @@
}).finally(() => {
//
this.scrollToBottom();
// @
this.atUserIds = [];
});
},
createAtText() {
let atText = "";
this.atUserIds.forEach((id) => {
let member = this.groupMembers.find((m)=>m.userId==id);
if (member) {
atText += ` @${member.aliasName}`;
}
})
return atText;
},
fillTargetId(msgInfo, targetId) {
if (this.chat.type == "GROUP") {
msgInfo.groupId = targetId;
@ -175,7 +211,7 @@
return;
}
this.$nextTick(() => {
console.log("scrollToMsgIdx",this.scrollMsgIdx)
console.log("scrollToMsgIdx", this.scrollMsgIdx)
this.scrollMsgIdx = idx;
});
@ -450,6 +486,20 @@
},
unreadCount() {
return this.chat.unreadCount;
},
atUserItems(){
let atUsers = [];
this.atUserIds.forEach((id)=>{
if(id==-1){
atUsers.push({id:-1,aliasName:"全体成员"})
return;
}
let member = this.groupMembers.find((m)=>m.userId==id);
if(member){
atUsers.push(member);
}
})
return atUsers;
}
},
watch: {
@ -543,6 +593,34 @@
}
}
.chat-at-bar {
display: flex;
align-items: center;
padding: 0 10rpx;
border: #dddddd solid 1px;
.icon-at {
font-size: 35rpx;
color: darkblue;
font-weight: 600;
}
.chat-at-scroll-box {
flex: 1;
width: 80%;
.chat-at-items {
display: flex;
align-items: center;
height: 70rpx;
.chat-at-item {
padding: 0 3rpx;
}
}
}
}
.send-bar {
display: flex;
align-items: center;
@ -552,7 +630,7 @@
background-color: white;
.iconfont {
font-size: 70rpx;
font-size: 60rpx;
margin: 3rpx;
}
@ -571,7 +649,6 @@
.send-text-area {
width: 100%;
}
}
.btn-send {
@ -586,7 +663,6 @@
background-color: whitesmoke;
.chat-tools {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
@ -609,7 +685,6 @@
height: 60rpx;
line-height: 60rpx;
font-size: 25rpx;
}
}
}

4
im-uniapp/pages/group/group-invite.vue

@ -17,9 +17,7 @@
<radio :checked="friend.checked" :disabled="friend.disabled" @click.stop="onSwitchChecked(friend)"/>
</view>
</view>
</view>
</scroll-view>
</view>

6
im-uniapp/static/icon/iconfont.css

@ -1,6 +1,6 @@
@font-face {
font-family: "iconfont"; /* Project id 4272106 */
src: url('iconfont.ttf?t=1697348383625') format('truetype');
src: url('iconfont.ttf?t=1699795609670') format('truetype');
}
.iconfont {
@ -11,6 +11,10 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-at:before {
content: "\e7de";
}
.icon-man:before {
content: "\e615";
}

BIN
im-uniapp/static/icon/iconfont.ttf

Binary file not shown.

1
im-uniapp/store/chatStore.js

@ -158,6 +158,7 @@ export default {
chat.atAll = true;
}
}
// 记录消息的最大id
if (msgInfo.id && type == "PRIVATE" && msgInfo.id > state.privateMsgMaxId) {
state.privateMsgMaxId = msgInfo.id;

Loading…
Cancel
Save