赞
踩
主要在SpringBoot项目的pom文件基础上,加上下面的
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId> <!-- Use 'netty-all' for 4.0 or above -->
<scope>compile</scope>
</dependency>
package com.ydy.netty.config;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
public class ChannelGroupConfig {
//存储每一个客户端接入进来的对象
public static ChannelGroup group = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
}
初始化配置类。使用了多线程启动,如果主线程启动的话,后面的controller调用会阻塞在那。连接端口使用的8888端口。
使用@PostConstruct注解,在项目加载完所有bean之后,执行该方法
package com.ydy.netty.config; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.concurrent.*; @Component public class StartWebSocket { private static Logger LOGGER = LoggerFactory.getLogger(StartWebSocket.class); private static ExecutorService executor = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(10)); @PostConstruct public static void initNetty(){ LOGGER.info("初始化netty,主线程开始"); executor.execute(new Runnable() { @Override public void run() { action(); } }); LOGGER.info("初始化netty,主线程结束"); } public static void action(){ EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); EventLoopGroup workGroup = new NioEventLoopGroup(); try { //开启服务端 ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(eventLoopGroup,workGroup); serverBootstrap.channel(NioServerSocketChannel.class); serverBootstrap.childHandler(new WebSocketChannelHandler()); LOGGER.info("服务端开启等待客户端连接.."); Channel channel = serverBootstrap.bind(8888).sync().channel(); channel.closeFuture().sync(); }catch (Exception e){ e.printStackTrace(); }finally { //退出程序 eventLoopGroup.shutdownGracefully(); workGroup.shutdownGracefully(); } } }
package com.ydy.netty.config; import com.ydy.netty.socket.WebSocketHandler; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.stream.ChunkedWriteHandler; public class WebSocketChannelHandler extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline pipeline = socketChannel.pipeline(); pipeline.addLast("http-codec",new HttpServerCodec()); pipeline.addLast("aggregator",new HttpObjectAggregator(65536)); pipeline.addLast("http-chunked",new ChunkedWriteHandler()); pipeline.addLast("handler",new WebSocketHandler()); } }
用channelUserMap 存储所有的连接用户键为:用户的code,值为:通道对象
当有用户发送来连接通道请求(和发送信息的请发方式不同),就把该用户加入进去,并查询该用户的所有好友,如果好友存在map中,就拿出该用户的通道。向里面写入XX已上线或者已下线通知。
该类如法自动注入bean对象,所以引入了SpringUtils
广播式发送消息就不需要map通过key拿到特定channel对象,直接写信息就行了。
package com.ydy.netty.socket; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.ydy.common.model.ChatRecord; import com.ydy.common.model.UserFriend; import com.ydy.common.util.JsonUtil; import com.ydy.common.vo.UserFriendVo; import com.ydy.netty.config.ChannelGroupConfig; import com.ydy.netty.service.NettyService; import com.ydy.netty.util.SpringUtils; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.websocketx.*; import io.netty.util.CharsetUtil; import org.apache.commons.collections4.CollectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.Timestamp; import java.util.Collection; import java.util.List; import java.util.concurrent.ConcurrentHashMap; public class WebSocketHandler extends SimpleChannelInboundHandler<Object> { //用户id=>channel示例 //可以通过用户的唯一标识保存用户的channel //这样就可以发送给指定的用户 public static ConcurrentHashMap<String, Channel> channelUserMap = new ConcurrentHashMap<>(); private WebSocketServerHandshaker webSocketServerHandshaker; private static Logger LOGGER = LoggerFactory.getLogger(WebSocketHandler.class); private NettyService nettyService; /** * 每当服务端收到新的客户端连接时,客户端的channel存入ChannelGroup列表中,并通知列表中其他客户端channel * @param ctx * @throws Exception */ @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { //获取连接的channel LOGGER.info("handlerAdded,连接channel{},连接id{}",ctx.channel(),ctx.channel().id()); ChannelGroupConfig.group.add(ctx.channel()); } /** *每当服务端断开客户端连接时,客户端的channel从ChannelGroup中移除,并通知列表中其他客户端channel * @param ctx * @throws Exception */ @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { Channel channel = ctx.channel(); //从服务端的channelGroup中移除当前离开的客户端 ChannelGroupConfig.group.remove(channel); //获得删除channle对应的userCode String removeUserCode = ""; for (String userCode : channelUserMap.keySet()) { Channel userChannel = channelUserMap.get(userCode); if(userChannel.equals(channel)){ removeUserCode = userCode; break; } } //从服务端的channelMap中移除当前离开的客户端 Collection<Channel> col = channelUserMap.values(); while(true == col.contains(channel)) { col.remove(ctx.channel()); LOGGER.info("handlerRemoved,netty客户端连接删除成功!,删除channel:{},channelId:{}",ctx.channel(),ctx.channel().id()); } //通知好友上线下线通知 sendFriendMsgLoginOrOut(removeUserCode,"notice","下线了"); } /** * * @Title: sendFriendMsgLoginOrOut * @author: dy.yin 2021/4/22 10:49 * @param: [removeUserCode] * @return: void * @throws */ private void sendFriendMsgLoginOrOut(String userCode,String type,String message) { //查询该用户好友 nettyService = SpringUtils.getBean(NettyService.class); List<UserFriendVo> friendList = nettyService.getUserFriendsList(userCode); for (UserFriendVo friend : friendList) { String friendCode = friend.getFriendCode(); String userName = friend.getUserName(); if(channelUserMap.containsKey(friendCode)){ channelUserMap.get(friendCode).writeAndFlush(new TextWebSocketFrame(userName + message)); } } } /** * 服务端监听到客户端活动 * @param ctx * @throws Exception */ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { LOGGER.info("channelActive,netty与客户端建立连接,通道开启!channel{}连接,连接id{}",ctx.channel(),ctx.channel().id()); } /** * 服务端监听到客户端不活动 * @param ctx * @throws Exception */ @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { LOGGER.info("channelInactive,netty与客户端断开连接,通道关闭!channel:{},channelId:{}",ctx.channel(),ctx.channel().id()); } //工程出现异常的时候调用 @Override public void exceptionCaught(ChannelHandlerContext context, Throwable throwable)throws Exception{ LOGGER.info("exceptionCaught,抛出异常,异常信息{},异常信息channel:{},channelId:{}",throwable.getLocalizedMessage(),context.channel(),context.channel().id()); handlerRemoved(context); context.close(); } @Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) throws Exception { //处理客户端向服务端发起的http握手请求 if (o instanceof FullHttpRequest){ LOGGER.info("http连接请求"); handHttpRequest(channelHandlerContext,(FullHttpRequest) o); }else if (o instanceof WebSocketFrame){//处理websocket链接业务 LOGGER.info("websocket信息请求"); handWebSocketFrame(channelHandlerContext,(WebSocketFrame) o); } } /** * 处理客户端与服务端之间的websocket业务 * @param context * @param webSocketFrame */ private void handWebSocketFrame(ChannelHandlerContext context,WebSocketFrame webSocketFrame){ if (webSocketFrame instanceof CloseWebSocketFrame){//判断是否是关闭websocket的指令 webSocketServerHandshaker.close(context.channel(),(CloseWebSocketFrame) webSocketFrame.retain()); } if (webSocketFrame instanceof PingWebSocketFrame){//判断是否是ping消息 context.channel().write(new PongWebSocketFrame(webSocketFrame.content().retain())); return; } if (!(webSocketFrame instanceof TextWebSocketFrame)){//判断是否是二进制消息 System.out.println("不支持二进制消息"); throw new RuntimeException(this.getClass().getName()); } //获取客户端向服务端发送的消息 String text = ((TextWebSocketFrame) webSocketFrame ).text(); LOGGER.info("服务端收到客户端的消息:" + text); ChatRecord chatRecord = exchangeChatMessage(context.channel(),text); //接收信息的userCode String toCode = chatRecord.getToCode(); //判断发送的code是否是群聊code List<String> listCode = nettyService.queryGroupChatUsers(toCode); if(CollectionUtils.isNotEmpty(listCode)){ //群聊 给群里的每个人都发 listCode.forEach(v->{ //服务端向好友客户端发送消息 if(channelUserMap.containsKey(v) && !v.equals(chatRecord.getFromCode())){ channelUserMap.get(v).writeAndFlush(new TextWebSocketFrame(JsonUtil.getJson(chatRecord))); } }); }else{ //单聊 //服务端向好友客户端发送消息 if(channelUserMap.containsKey(toCode)){ channelUserMap.get(toCode).writeAndFlush(new TextWebSocketFrame(JsonUtil.getJson(chatRecord))); } } } /** * 发送的信息转换 * @Title: exchangeChatMessage * @author: dy.yin 2021/4/22 13:02 * @param: [channel, text] * @return: java.util.Map<java.lang.String,java.lang.Object> * @throws */ private ChatRecord exchangeChatMessage(Channel channel, String text) { JSONObject chatRecordJson = JSONObject.parseObject(text); ChatRecord chatRecord = JSON.toJavaObject(chatRecordJson,ChatRecord.class); chatRecord.setMessageTime(new Timestamp(System.currentTimeMillis())); nettyService = SpringUtils.getBean(NettyService.class); nettyService.insertChatRecord(chatRecord); return chatRecord; } /** * 处理客户端向服务端发起http握手请求业务 * @param context * @param fullHttpRequest */ private void handHttpRequest(ChannelHandlerContext context,FullHttpRequest fullHttpRequest){ LOGGER.info("请求连接的channel{},id为{}",context.channel(),context.channel().id()); //判断是否http握手请求 if (!fullHttpRequest.getDecoderResult().isSuccess() ||!("websocket".equals(fullHttpRequest.headers().get("Upgrade")))){ sendHttpResponse(context,fullHttpRequest, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST)); return; } nettyService = SpringUtils.getBean(NettyService.class); String webSocketUrl = nettyService.getWebSocketUrl(); WebSocketServerHandshakerFactory webSocketServerHandshakerFactory = new WebSocketServerHandshakerFactory(webSocketUrl,null,false); webSocketServerHandshaker = webSocketServerHandshakerFactory.newHandshaker(fullHttpRequest); if (webSocketServerHandshaker == null){ WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(context.channel()); }else{ webSocketServerHandshaker.handshake(context.channel(),fullHttpRequest); } //把token解析成用户Code Channel channel = context.channel(); String uri = fullHttpRequest.getUri(); String userCode = uri.substring(uri.lastIndexOf("?")+1,uri.length()); channelUserMap.put(userCode,channel); sendFriendMsgLoginOrOut(userCode,"notice","上线了"); } /** * 服务端想客户端发送响应消息 * @param context * @param fullHttpRequest * @param defaultFullHttpResponse */ private void sendHttpResponse(ChannelHandlerContext context, FullHttpRequest fullHttpRequest, DefaultFullHttpResponse defaultFullHttpResponse){ if (defaultFullHttpResponse.getStatus().code() != 200){ ByteBuf buf = Unpooled.copiedBuffer(defaultFullHttpResponse.getStatus().toString(), CharsetUtil.UTF_8); defaultFullHttpResponse.content().writeBytes(buf); buf.release(); } //服务端向客户端发送数据 ChannelFuture future = context.channel().writeAndFlush(defaultFullHttpResponse); if (defaultFullHttpResponse.getStatus().code() !=200){ future.addListener(ChannelFutureListener.CLOSE); } } }
package com.ydy.netty.util; import org.springframework.aop.framework.AopContext; import org.springframework.beans.BeansException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.stereotype.Component; @Component public class SpringUtils implements BeanFactoryPostProcessor { /** Spring应用上下文环境 */ private static ConfigurableListableBeanFactory beanFactory; @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { SpringUtils.beanFactory = beanFactory; } /** * 获取对象 * * @param name * @return Object 一个以所给名字注册的bean的实例 * @throws BeansException * */ @SuppressWarnings("unchecked") public static <T> T getBean(String name) throws BeansException { return (T) beanFactory.getBean(name); } /** * 获取类型为requiredType的对象 * * @param clz * @return * @throws BeansException * */ public static <T> T getBean(Class<T> clz) throws BeansException { T result = (T) beanFactory.getBean(clz); return result; } /** * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true * * @param name * @return boolean */ public static boolean containsBean(String name) { return beanFactory.containsBean(name); } /** * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) * * @param name * @return boolean * @throws NoSuchBeanDefinitionException * */ public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { return beanFactory.isSingleton(name); } /** * @param name * @return Class 注册对象的类型 * @throws NoSuchBeanDefinitionException * */ public static Class<?> getType(String name) throws NoSuchBeanDefinitionException { return beanFactory.getType(name); } /** * 如果给定的bean名字在bean定义中有别名,则返回这些别名 * * @param name * @return * @throws NoSuchBeanDefinitionException * */ public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { return beanFactory.getAliases(name); } /** * 获取aop代理对象 * * @param invoker * @return */ @SuppressWarnings("unchecked") public static <T> T getAopProxy(T invoker) { return (T) AopContext.currentProxy(); } }
文中用的redis主要是获取配置的请求路径,测试可以写死,不用配置:
package com.ydy.common.model; import java.sql.Timestamp; public class ChatRecord { private Integer id; private String fromCode; private String fromName; private String mappingCode; private String toCode; private String fromHeadImage; private String message; private Timestamp messageTime; private String showTime; public ChatRecord() { } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getFromCode() { return fromCode; } public void setFromCode(String fromCode) { this.fromCode = fromCode; } public String getToCode() { return toCode; } public void setToCode(String toCode) { this.toCode = toCode; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public Timestamp getMessageTime() { return messageTime; } public void setMessageTime(Timestamp messageTime) { this.messageTime = messageTime; } public String getFromHeadImage() { return fromHeadImage; } public void setFromHeadImage(String fromHeadImage) { this.fromHeadImage = fromHeadImage; } public String getFromName() { return fromName; } public void setFromName(String fromName) { this.fromName = fromName; } public String getMappingCode() { return mappingCode; } public void setMappingCode(String mappingCode) { this.mappingCode = mappingCode; } public String getShowTime() { return showTime; } public void setShowTime(String showTime) { this.showTime = showTime; } @Override public String toString() { return "ChatRecord{" + "id=" + id + ", fromCode='" + fromCode + '\'' + ", fromName='" + fromName + '\'' + ", mappingCode='" + mappingCode + '\'' + ", toCode='" + toCode + '\'' + ", fromHeadImage='" + fromHeadImage + '\'' + ", message='" + message + '\'' + ", messageTime=" + messageTime + '}'; } }
package com.ydy.common.util; import org.codehaus.jackson.JsonGenerationException; import org.codehaus.jackson.JsonParseException; import org.codehaus.jackson.map.JsonMappingException; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.type.TypeReference; import java.io.IOException; import java.util.Map; public class JsonUtil { private final static ObjectMapper objectMapper = new ObjectMapper(); private static String objectToJson(Object object) { ObjectMapper om = new ObjectMapper(); String json = ""; try { try { json = om.writeValueAsString(object); } catch (JsonGenerationException e) { e.printStackTrace(); } catch (JsonMappingException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } catch (Exception e) { e.printStackTrace(); } return json; } public static String getJson(Object obj) { return objectToJson(obj); } public static <T> T jsonToObject(String json, TypeReference<T> typeReference) { ObjectMapper mapper = new ObjectMapper(); try { return mapper.readValue(json, typeReference); } catch (JsonParseException e) { } catch (JsonMappingException e) { } catch (IOException e) { } return null; } }
<template> <div class="chat_body"> <el-container > <!--左边--> <div class="left-aside"> <!--头像--> <el-row slot="reference"> <div class="head-portrait"><img :src="headPortrait" /></div> </el-row> </div> <!--中间--> <el-aside width="250px"> <!--聊天列表头部--> <div class="chat-record"> <div class="chat-record-input"> <el-input v-model="inputQuery" size="mini" prefix-icon="el-icon-search" placeholder="搜索" clearable></el-input> </div> <div class="chat-record-add"> <a href="#" style="line-height: 25px" title="发起群聊" @click="addGroupChatDialog">✚</a> </div> </div> <!--聊天记录列表--> <div v-for="(item,index) in friendChatList" :key="index" style="margin-top :20px;"> <el-row @click.native="clickChatRecord(item)"> <el-col :span="6"> <!-- <el-badge :value="2" class="item">--> <div class="chat-portrait"><img :src="item.friendPortrait" /></div> <!-- </el-badge>--> </el-col> <el-col :span="18"> <el-row> <el-col :span="15" class="chat-nickName">{{item.friendName}}</el-col> <el-col :span="9">{{item.showTime}}</el-col> </el-row> <div class="chat-newMsg">{{item.message}}</div> </el-col> </el-row> </div> </el-aside> <!--右边--> <el-main> <el-row class="main-title"> <el-col :span="21">{{mainTitle}}</el-col> <el-col :span="3" @click.native="lookDetail"> <a href="#" class="el-icon-more" aria-hidden="true" title="聊天信息"></a> </el-col> </el-row> <div class="main-msg"> <div v-for="(item,index) in chatMsgRecord" :key="index"> <el-row v-if="item.fromCode != fromCode"> <el-row><span class="message-time">{{item.showTime}}</span></el-row> <el-col :span="3"> <div class="msg-portrait-left"><img :src="item.fromHeadImage" /></div> </el-col> <el-col :span="21"> <div class="chat-message-left-nickName">{{item.fromName}}</div> <div class="chat-msg-left"> <span class="msg-detail">{{item.message}}</span> </div> </el-col> </el-row> <el-row v-if="item.fromCode == fromCode"> <el-row> <span class="message-time">{{item.showTime}}</span> </el-row> <el-col :span="21"> <div class="chat-message-right-nickName">{{item.fromName}}</div> <div class="chat-msg-right"> <span class="msg-detail">{{item.message}}</span> </div> </el-col> <el-col :span="3"> <div class="msg-portrait-right"><img :src="headPortrait" /></div> </el-col> </el-row> </div> </div> <div class ="main-chat-input"> <!--工具栏--> <el-popover placement="top-start" width="400" trigger="click" class="emoBox"> <div class="emotionList"> <a href="javascript:void(0);" @click="getEmo(index)" v-for="(item,index) in faceList" :key="index" class="emotionItem">{{item}}</a> </div> <el-button class="emotionSelect" slot="reference"> <i class="el-icon-picture-outline-round" aria-hidden="true" title="表情"></i> </el-button> </el-popover> <el-button class="emotionSelect"> <i class="el-icon-folder-opened" aria-hidden="true" title="发送文件"></i> </el-button> <el-button class="emotionSelect"> <i class="el-icon-chat-dot-round" aria-hidden="true" title="聊天记录"></i> </el-button> <!--输入框--> <el-input type="textarea" :rows="7" v-model="textarea" resize="none" border="none" @keyup.enter.native="sendMsg" id="textarea" > </el-input> </div> <el-button size="mini" style="float:right" @click="sendMsg">发送(S)</el-button> </el-main> </el-container> <!--创建群聊弹框--> <el-dialog :visible.sync="groupChatDialog" :close-on-click-modal="false" :append-to-body="true"> <div style="height: 300px;"> <div class="add-chatGroup-left" > <el-input v-model="inputQuery" size="mini" prefix-icon="el-icon-search" placeholder="搜索" clearable></el-input> <div v-for="(item,index) in friendList" :key="index" style="margin-top :20px;"> <el-row> <el-col :span="6"> <div class="chat-portrait"><img :src="item.friendPortrait" /></div> </el-col> <el-col :span="15"> <div style="height: 35px;line-height: 35px;font-size: 18px;font-weight: bold">{{item.friendName}}</div> </el-col> <el-col :span="2"> <div> <el-checkbox @change="groupChatCheckChange($event,item)"></el-checkbox> </div> </el-col> </el-row> </div> </div> <div class="add-chatGroup-right"> <div> <span style="margin-right: 100px;">{{checkGroupChatTitle}}</span> <el-button size="mini" type="success" @click="addGroupChat">确认</el-button> <el-button size="mini" type="info">取消</el-button> </div> <div v-for="tag in checkChatUsers" :key="tag.friendCode" style="margin-top :20px;"> <el-row closable :disable-transitions="false" @close="handleCloseTag(tag.friendCode)"> <el-col :span="6"> <div class="chat-portrait"><img :src="tag.friendPortrait" /></div> </el-col> <el-col :span="10"> <div style="height: 35px;line-height: 35px;font-size: 18px;font-weight: bold">{{tag.friendName}}</div> </el-col> </el-row> </div> </div> </div> </el-dialog> <!--群聊和个人详细信息--> <el-drawer title="我是标题" :visible.sync="drawer" :with-header="false"> <span>我来啦!</span> </el-drawer> </div> </template> <script> const appData=require("../../assets/json/emoji.json")//引入存放emoji表情的json文件 export default { components:{ }, data() { return { dialogVisible :true, //搜索框输入 inputQuery:'', //登录客户头像 headPortrait:sessionStorage.getItem("headPortrait"), //聊天记录表 friendChatList:[], //聊天 mainTitle:'', //信息 textarea:'', //接收人UserCode toCode :'', //发送人userCode fromCode:sessionStorage.getItem("userCode"), fromName:sessionStorage.getItem("nickName"), //通信url webSocketUrl:sessionStorage.getItem("webSocketUrl"), //当前聊天对象的聊天记录 chatMsgRecord :[], //所有的聊天记录 chatRecord:'', //当前聊天的code组装key currentChatKey:'', /********/ //群聊弹框 groupChatDialog:false, //选择的好友 checkChatUsers:[], //勾选好友抬头 checkGroupChatTitle:'请勾选需要添加的联系人', //可添加为群聊的好友 friendList:[], /*表情包*/ faceList:[],//表情包数据 content:'', /*群聊详细信息*/ drawer:false, } }, methods:{ //查看聊天详细信息 lookDetail(){ this.drawer = true; }, //获取表情包,放入输入框 getEmo(index){ let textArea = document.getElementById('textarea'); //将选中的表情插入到输入文本的光标之后 function changeSelectedText(obj, str) { if (window.getSelection) { // 非IE浏览器 textArea.setRangeText(str); // 在未选中文本的情况下,重新设置光标位置 textArea.selectionStart += str.length; textArea.focus() } else if (document.selection) { // IE浏览器 obj.focus(); var sel = document.selection.createRange(); sel.text = str; } } changeSelectedText(textArea,this.faceList[index]); this.content=textArea.value;// 要同步data中的数据 return; }, //添加群聊好友 addGroupChat(){ if(this.checkChatUsers.length < 2){ this.$msg.success("请选择2个及以上好友!"); return; } console.log(this.checkChatUsers); this.groupChatDialog = false; //提交 this.$api.addGroupChat(this.checkChatUsers).then(res => { }).catch(err => { this.$commsgbox.alert(err); }); }, //勾选好友 groupChatCheckChange(checked,item){ if(checked == true){ console.log("勾选:"+item.friendCode); this.checkChatUsers.push(item); }else{ console.log("取消:"+item.friendCode); for(var index in this.checkChatUsers){ if(this.checkChatUsers[index].friendCode === item.friendCode){ this.checkChatUsers.splice(index, 1); } } } if(this.checkChatUsers.length > 0 ){ this.checkGroupChatTitle = "已选择了"+this.checkChatUsers.length+"个联系人"; }else{ this.checkGroupChatTitle = '请勾选需要添加的联系人'; } }, //群聊弹框 addGroupChatDialog(){ let req = {"userCode":this.fromCode}; this.$api.getFriendList(req).then(res => { this.friendList = res.data.data; this.groupChatDialog = true; this.checkChatUsers = []; }).catch(err => { this.$commsgbox.alert(err); }); }, /***********************/ //查询聊天列表 getChatFriendsList(){ let req = {"userCode":this.fromCode}; this.$api.getChatFriendsList(req).then(res => { this.friendChatList = res.data.data; }).catch(err => { this.$commsgbox.alert(err); }); }, //点击好友列表,开始聊天 clickChatRecord(item){ let that = this; //聊天信息展示的抬头显示好友昵称 that.mainTitle = item.friendName; //好友的code为收信人code that.toCode = item.friendCode; //找到mappingCode let mappingCode = item.mappingCode; //给当前聊天纤细对象赋值 if(that.chatRecord != undefined && mappingCode in that.chatRecord){ that.chatMsgRecord = that.chatRecord[mappingCode]; }else{ that.chatMsgRecord = []; } //更新当前聊天对象祝贺key that.currentChatKey = mappingCode; //信息下拉滚条置到底部 that.setScrollToEnd(); //好友列表重新排序 TODO }, //发送信息 sendMsg () { let that = this; //信息输入框内容 let msg = that.textarea; //发送人userCode let fromCode = that.fromCode; //接收人userCode let toCode = that.toCode; //当前聊天组合的mappingCode let mappingCode = that.currentChatKey; //判断信息不为空 if(msg.trim()===''){ that.$commsgbox.alert("不能发送空白信息!"); return; } //判断收信人是否选择 if(toCode===''){ that.$commsgbox.alert("请选择要发送信息的好友!"); return; } //组装发送的信息体 let req = { "fromCode":fromCode, "fromName":this.fromName, "mappingCode":mappingCode, "toCode":toCode, "message":msg, "fromHeadImage":this.headPortrait, "showTime":this.$comfunc.getHHmm() }; //把组装的发送的信息添加到当前聊天对象的聊天信息集合中 that.chatMsgRecord.push(req); //把对象转为字符串传输 let agentData = JSON.stringify(req); //websocket发送信息 that.webSocketSendMessage(agentData); //更新好友列表的最新信息 that.friendChatList.forEach(function(val,index){ let friendCode = val.friendCode; if(toCode === friendCode){ //更新信息和时间 val.message = msg; val.showTime = that.$comfunc.getHHmm(); return; } }); //信息输入框置空 that.textarea = ""; //聊天详细信息一直位于底部 that.setScrollToEnd(); }, //websocket发送信息 webSocketSendMessage(agentData){ let that = this; //若是ws开启状态 if (that.websock.readyState === that.websock.OPEN) { that.websocketSend(agentData); } // 若是 正在开启状态,则等待300毫秒 else if (that.websock.readyState === that.websock.CONNECTING) { setTimeout(function () { that.websocketSend(agentData); }, 300); } // 若未开启 ,则等待500毫秒 else { that.initWebSocket(); setTimeout(function () { that.websocketSend(agentData) }, 500); } }, //数据发送 websocketSend(agentData){ let that = this; console.log("发送的信息:"+agentData); that.websock.send(agentData); }, //关闭 websocketClose(e){ console.log("connection closed (" + e.code + ")"); }, //设置div的下拉条始终在底部 setScrollToEnd(){ let that = this; that.$nextTick(()=> { let box = that.$el.querySelector(".main-msg") box.scrollTop = box.scrollHeight }); }, //监听服务端返回信息数据接收 websocketOnmessage(e){ let that = this; let reData = e.data; console.log("接收到的信息为:"+ reData); //好友上线下线信息 if(that.showFriendNoticeMessage(reData)){return;} //json转换 reData = JSON.parse(reData); /** * 对数据做处理,处理逻辑为: * 1、如果收到的信息为当前聊天对象的信息,直接把值付给当前聊天信息对象 * 2、如果收到的信息不是当前聊天对象的,找到该对象的聊天信息,然后把信息加进去 * 3、更新聊天列表的时间和信息 * @type {string} */ that.handleReceiveMessage(reData); //聊天详细信息一直位于底部 that.setScrollToEnd(); }, //好友上下线消息提醒 showFriendNoticeMessage(reData){ let that = this; if(reData.indexOf("{") == -1){ console.log("message提示:"+reData); that.$msg.success(reData); return true; } }, //处理接收到的信息 handleReceiveMessage(reData){ let that = this; //聊天组code let mappingCode = reData.mappingCode; //1、判断如果发送的信息为当前聊天对象,直接拼接信息 if(that.currentChatKey === mappingCode){ console.log("聊天对象为当前对象"); that.chatMsgRecord.push(reData); }else{ //2、如果不是当前聊天的对象,拼接到对应list,然后重新放入map中 console.log("聊天对象为好友列表对象"); if(mappingCode in that.chatRecord){ let tmpChatMsgRecord = that.chatRecord[mappingCode]; tmpChatMsgRecord.push(reData); that.chatRecord[mappingCode] = tmpChatMsgRecord; }else{ let tmpChatMsgRecord =[]; tmpChatMsgRecord.push(reData); that.chatRecord[mappingCode] = tmpChatMsgRecord; } } //3、更新聊天列表的时间和信息 console.log("更新好友列表信息"); that.friendChatList.forEach(function(val,index){ let code = val.mappingCode; //找到聊天列表中与当前接收到的信息为同一人的对象 if(code == mappingCode){ //更新信息和时间 val.message = reData.message; val.showTime = that.$comfunc.getHHmm(); } }); }, //查询聊天界面信息 getChatInfo(){ let that = this; //连接websocket的userCode let req = {"userCode":that.fromCode}; that.$api.getChatInfo(req).then(res => { //好友聊天列表 that.friendChatList = res.data.data.friendChatList; //所有的聊天记录 that.chatRecord = res.data.data.chatRecord; }).catch(err => { that.$commsgbox.alert(err); }); }, //初始化websocket initWebSocket(){ //ws地址 const wsUri = this.webSocketUrl +"?"+this.fromCode; this.websock = new WebSocket(wsUri); //绑定message响应 this.websock.onmessage = this.websocketOnmessage; //绑定关闭响应 this.websock.onclose = this.websocketClose; }, //初始化加载表情包列表 initEmoji(){ for (let i in appData){//读取json文件保存数据给数组 this.faceList.push(appData[i].char); } }, }, created() { //初始化websocket组件和方法 this.initWebSocket(); //初始化查询个人信息 好友列表 和所有的聊天信息 this.getChatInfo(); }, mounted() { this.initEmoji(); }, filters: { time:function(time){ return this.$comfunc.timeFormat(time); }, } } </script> <style scoped> .message-time{ background-color: #DADADA; padding:1px 0px; } .add-chatGroup-left{ width:350px; float: left; height: 300px; overflow-y: auto; /*右边框*/ border-width: 0 1px 0 0; border-style: solid; border-color: black; } .add-chatGroup-right{ width: 400px; float: left; height:300px; overflow-y: auto; } .chat-message-left-nickName{ text-align: left; margin: 0px 10px; } .chat-message-right-nickName{ text-align: right; margin: 0px 10px; } .msg-detail{ padding: 0px 15px; } .chat-msg-right{ text-align: center; min-height:35px; height:max-content; line-height: 35px; margin: 5px 10px; background-color: #9EEA6A; border-radius:5px; width:max-content; float:right; max-width:250px; word-wrap: break-word; } .msg-portrait-right{ width:35px; height: 35px; margin-right:20px; margin-top:10px; } .msg-portrait-right img{ display: block; width: 35px; height: 35px; } .chat-msg-left{ text-align: center; min-height:35px; height:max-content; line-height: 35px; margin: 5px 10px; background-color: #FFFFFF; border-radius:5px; width:max-content; max-width:250px; word-wrap: break-word; } .msg-portrait-left{ width:35px; height: 35px; margin: 10px 15px; } .msg-portrait-left img{ display: block; width: 35px; height: 35px; } .main-chat-input{ height: 155px; background-color: #ffffff; } .main-msg{ height:250px; overflow-y: auto; } .chat-newMsg{ text-align: left; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .chat-nickName{ text-align: left; font-size: 16px; font-weight: bold; width: 100px; float: left; } .chat-msgTime{ text-align: right; font-size: 12px; width: 20px; } .chat-record-add{ float: right; width: 25px; height: 25px; margin-right: 10px; margin-top:20px; background-color: #DCD9D8; } .head-portrait{ margin:20px 12px; } .head-portrait img { display: block; width: 35px; height: 35px; } a{ text-decoration:none; } a:hover{color: black} input { background-color:transparent; } .chat-record-input{ width: 175px; margin-left: 10px; line-height: 65px; float: left; } .left-aside{ width: 60px; background-color: #28292C; } .chat_body{ height: 500px; border: #99a9bf solid 1px; } .el-container{ height: 500px; margin-bottom: 40px; } .el-aside { background-color: #EEEAE8; } .el-main { background-color: #F5F5F5; padding: 0px; } .chat-record{ height: 65px; width:230px; } .main-title{ height: 65px; border-bottom: #99a9bf solid 1px; font-size: 16px; font-weight: bold; text-align: left; line-height: 65px; padding-left: 25px; } /*表情*/ .emotionSelect{ border: none; padding:5px 10px; float:left; } .emotionList{ display: flex; flex-wrap: wrap; padding:5px; } .emotionItem{ width:10%; font-size:20px; text-align:center; } /*包含以下四种的链接*/ .emotionItem { text-decoration: none; } /*正常的未被访问过的链接*/ .emotionItem:link { text-decoration: none; } /*已经访问过的链接*/ .emotionItem:visited { text-decoration: none; } /*鼠标划过(停留)的链接*/ .emotionItem:hover { text-decoration: none; } /* 正在点击的链接*/ .emotionItem:active { text-decoration: none; } </style> <style lang="scss"> /* el-popover是和app同级的,所以scoped的局部属性设置无效 */ /* 需要设置全局style */ .el-popover{ height:200px; width:300px; overflow-y:auto; } </style>
<template> <div> <div class="userDetail"> <!--头像--> <div class="headPortraitImage" title = "个人头像"> <span class="span-title">个人头像</span> <el-upload action="#" list-type="picture-card" :auto-upload="false" :file-list="headPortraitList" accept=".png,.jpg,.gif,.jpeg"> <i slot="default" class="el-icon-plus"></i> <div slot="file" slot-scope="{file}"> <span v-if="file.userCode"><img class="el-upload-list__item-thumbnail" :src="file.headPortrait" alt="file.fileName"></span> <span v-if="!file.userCode"><img class="el-upload-list__item-thumbnail" :src="file.url" alt=""></span> <span class="el-upload-list__item-actions"> <span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)"> <i class="el-icon-zoom-in"></i> </span> <span v-if="!file.userCode" class="el-upload-list__item-delete" @click="handleUpload(file)"> <i class="el-icon-upload"></i> </span> </span> </div> </el-upload> <el-dialog :visible.sync="dialogVisible"> <img width="100%" :src="headPortrait" alt=""> </el-dialog> </div> </div> <div class="can-add-friends" title="可添加好友列表"> <span class="span-title">可添加好友列表</span> <div v-for="(item,index) in canAddFriendList" :key="index" style="margin-top :20px;"> <el-row> <el-col :span="3"> <div class="chat-portrait"><img :src="item.friendPortrait" /></div> </el-col> <el-col :span="10"> <div>{{item.friendCode}}</div> </el-col> <el-col :span="3"> <div class="chat-nickName">{{item.friendName}}</div> </el-col> <el-col :span="3"> <el-button size="mini" @click="addFriend(item)">添加</el-button> </el-col> </el-row> </div> </div> <div class="friends-add-request" title="好友添加申请列表"> <span class="span-title">好友添加申请列表</span> <div v-for="(item,index) in friendAddRequestList" :key="index" style="margin-top :20px;"> <el-row> <el-col :span="3"> <div class="chat-portrait"><img :src="item.friendPortrait" /></div> </el-col> <el-col :span="10"> <div>{{item.friendCode}}</div> </el-col> <el-col :span="3"> <div class="chat-nickName">{{item.friendName}}</div> </el-col> <el-col :span="3"> <el-button size="mini" @click="agreeAddFriend(item)">同意</el-button> </el-col> </el-row> </div> </div> </div> </template> <script> export default { components:{ }, data() { return { //能够添加为好友的列表 canAddFriendList:[], //好友添加申请 friendAddRequestList:[], //发送人userCode fromCode:sessionStorage.getItem("userCode"), //头像地址 headPortrait:'', headPortraitList:[], dialogVisible:false, labelPosition:'left', } }, methods: { //查询好友添加申请 queryAddFriendRequestList(){ let req = {"userCode": this.fromCode}; this.$api.queryAddFriendRequest(req).then(res => { this.friendAddRequestList = res.data.data; }).catch(err => { this.$commsgbox.alert(err); }); }, //同意好友添加 agreeAddFriend(item){ let req = Object.assign(item,{"userCode": this.fromCode}); this.$api.agreeAddFriend(req).then(res => { this.queryAddFriendRequestList(); }).catch(err => { this.$commsgbox.alert(err); }); }, //添加好友提交 addFriend(item) { console.log("添加的好友信息" + item); let req = {"userCode": this.fromCode, "friendCode": item.friendCode}; this.$api.addFriend(req).then(res => { this.getCanAddFriendList(); }).catch(err => { this.$commsgbox.alert(err); }); }, //查询可添加的好友列表 getCanAddFriendList() { console.log("添加好友"); let req = {"userCode": this.fromCode}; this.$api.getCanAddFriendList(req).then(res => { this.canAddFriendList = res.data.data; }).catch(err => { this.$commsgbox.alert(err); }); }, //查看头像 handlePictureCardPreview(file) { console.log(file); if (!file.userCode) { this.headPortrait = file.url; } else { this.headPortrait = file.headPortrait; } this.dialogVisible = true; }, handleUpload(file){ let that = this; console.log(file); let formData = new FormData(); formData.append('file',file.raw); formData.append("userCode", this.fromCode); formData.append("nickName","尹家村帅勇"); that.$api.headPortraitImageUpload(formData).then(res => { let user = res.data.data; let headPortrait = res.data.data.headPortrait; this.headPortrait = headPortrait; sessionStorage.setItem("headPortrait",headPortrait); that.headPortraitList = []; that.headPortraitList.push(user); }).catch(err => { that.$commsgbox.alert(err); }); }, //查询界面信息 getChatUserInfo(){ let req = {"userCode": this.fromCode}; this.$api.getChatUserInfo(req).then(res => { //个人详细信息 let user = res.data.data.user; this.headPortraitList.push(user); //可添加好友列表 this.canAddFriendList = res.data.data.canAddFriendList; //待同意好友列表 this.friendAddRequestList = res.data.data.friendAddRequestList; }).catch(err => { this.$commsgbox.alert(err); }); } }, created() { this.getChatUserInfo(); }, mounted() { }, } </script> <style> .chat-portrait img{ display: block; width: 40px; height: 40px; } .chat-portrait{ width:40px; height: 40px; margin-left: 10px; } .chat-nickName{ text-align: center; font-size: 16px; font-weight: bold; } .userDetail{ height:100px; } .can-add-friends{ height: 200px; /*background-color: #9EEA6A;*/ } .friends-add-request{ height: 200px; /*background-color: #d27468;*/ } /**********************/ .el-upload{ width: 80px; height: 80px; line-height: 80px; } .el-upload el-upload--picture-card{ height: 80px; height: 80px; } .el-upload-list--picture-card .el-upload-list__item{ width: 80px; height: 80px; line-height: 80px; } .el-upload-list--picture-card .el-upload-list__item-thumbnail{ width: 80px; height: 80px; line-height: 80px; } .avatar{ width: 80px; height: 80px; } .headPortraitImage{ width: 300px; padding-left: 200px; } .span-title{ font-weight: bold; font-size: 16px; } </style>
<template> <div> <el-row :gutter="12"> <el-col :span="12"> <el-card shadow="hover" class="box-card"> <chat ref="chat"></chat> </el-card> </el-col> <el-col :span="12"> <el-card shadow="hover" class="box-card"> <chat-info ref="chatInfo"></chat-info> </el-card> </el-col> </el-row> </div> </template> <script> import chat from './chat.vue'; import chatInfo from './chatInfo.vue'; export default { components:{ chat, chatInfo, }, data() { return { } }, methods:{ }, created() { }, mounted() { }, } </script> <style scoped> .box-card{ height: 550px; } </style>
gitee地址: gitee。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。