Springboot +Netty+Vue实现聊天(单聊+创建群聊并聊天)
Springboot +Netty+Vue实现简单的单对单聊天后台项目结构pom文件主要在SpringBoot项目的pom文件基础上,加上下面的<dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId> <!-- Use 'netty-all'
·
Springboot +Netty+Vue实现简单的单对单聊天
后台
项目结构
pom文件
主要在SpringBoot项目的pom文件基础上,加上下面的
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId> <!-- Use 'netty-all' for 4.0 or above -->
<scope>compile</scope>
</dependency>
目录展示的后端代码
ChannelGroupConfig
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);
}
StartWebSocket
初始化配置类。使用了多线程启动,如果主线程启动的话,后面的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();
}
}
}
WebSocketChannelHandler
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());
}
}
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);
}
}
}
SpringUtils
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主要是获取配置的请求路径,测试可以写死,不用配置:
ChatRecord 聊天实例对象
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 +
'}';
}
}
JsonUtil
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。
更多推荐
已为社区贡献1条内容
所有评论(0)