Springboot +Netty+Vue实现聊天(单聊+创建群聊并聊天)

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

猜你喜欢

转载自blog.csdn.net/qq_40917075/article/details/116142446