0


springBoot+webSocket+uniapp实现实时聊天功能

简要

该文章只是作者从自己开发的代码中截取的一部分,只是做一个参考;实际上需要自己在该代码基础上进行调整和优化,有疑问可以在评论区进行提问

一、数据库ER设计

    聊天功能主要涉及到两张表,message和user表,message用来存信息,user表用来关联用户信息,主要是拿来取用户昵称以及头像

message表创建:

CREATE TABLE `chat_message` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '自增主键id',
  `send_user_id` varchar(20) NOT NULL COMMENT '发送用户id',
  `accept_user_id` varchar(20) NOT NULL COMMENT '接手用户id',
  `type` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '消息类型(图片:img,视频:video,文本:text)',
  `content` text COMMENT '发送内容',
  `readed` int NOT NULL DEFAULT '0' COMMENT '是否阅读',
  `delete` int NOT NULL DEFAULT '0' COMMENT '是否删除',
  `send_time` datetime NOT NULL COMMENT '发送时间',
  PRIMARY KEY (`id`),
  KEY `user_id` (`send_user_id`,`accept_user_id`) USING BTREE COMMENT 'userId索引'
) ENGINE=InnoDB AUTO_INCREMENT=160 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

user表创建:

CREATE TABLE `wx_user` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '自增id',
  `user_id` varchar(20) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '用户ID',
  `nickName` varchar(20) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '昵称',
  `headImg` varchar(150) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '头像链接',
  `phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '电话',
  `openid` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT 'openID',
  `unionid` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT 'unionID',
  `status` int NOT NULL DEFAULT '0' COMMENT '状态 0:使用中 1:冻结中 2:长时间未使用',
  `created_time` datetime DEFAULT NULL COMMENT '创建时间',
  `modefied_time` datetime DEFAULT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `user_id` (`user_id`),
  UNIQUE KEY `openID` (`openid`) USING BTREE,
  UNIQUE KEY `unionID` (`unionid`) USING BTREE,
  UNIQUE KEY `phone` (`phone`) USING BTREE,
  KEY `id` (`id`,`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;

二、后端接口设计

entity:

Message.java

package com.example.wxapi.entity.MessageEntity;

import com.example.wxapi.entity.UserEntity.WxUser;
import lombok.Data;

@Data
public class ChatMessage {
    private Integer id;
    private String sendUserId;
    private String acceptUserId;
    private String type;
    private String content;
    private Integer soundTIme;
    private String sendTime;
    private Integer readedNum;
    private WxUser wxUser;
}

WxUser.java

package com.example.wxapi.entity.UserEntity;

import lombok.Data;

@Data
public class WxUser {
    private Integer id;
    private String userId;
    private String nickName;
    private String headImg;
    private String phone;
    private String openid;
    private String unionid;
    private Integer status;
    private String createdTime;
    private String modefiedTime;
}

mapper层:

MessageMapper.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.wxapi.dao.personIndexMapper.MessageMapper">

    <!--    最新信息-->
    <resultMap id="newMsgMap" type="com.example.wxapi.entity.MessageEntity.ChatMessage">
        <result column="acceptUserId" jdbcType="BIGINT" property="acceptUserId" />
        <result column="content" jdbcType="VARCHAR" property="content" />
        <result column="type" jdbcType="VARCHAR" property="type" />
        <result column="sendTime" jdbcType="VARCHAR" property="sendTime" />
        <result column="readedNum" jdbcType="BIGINT" property="readedNum" />
        <association property="wxUser" javaType="com.example.wxapi.entity.UserEntity.WxUser">
            <result column="nickName" jdbcType="VARCHAR" property="nickName" />
            <result column="headImg" jdbcType="VARCHAR" property="headImg" />
        </association>
    </resultMap>

<!--    聊天信息-->
    <resultMap id="chatMsgMap" type="com.example.wxapi.entity.MessageEntity.ChatMessage">
        <id column="id" jdbcType="BIGINT" property="id" />
        <result column="sendUserId" jdbcType="BIGINT" property="sendUserId" />
        <result column="acceptUserId" jdbcType="BIGINT" property="acceptUserId" />
        <result column="content" jdbcType="VARCHAR" property="content" />
        <result column="type" jdbcType="VARCHAR" property="type" />
        <result column="soundTime" jdbcType="BIGINT" property="soundTIme" />
        <result column="sendTime" jdbcType="VARCHAR" property="sendTime" />
        <association property="wxUser" javaType="com.example.wxapi.entity.UserEntity.WxUser">
            <result column="nickName" jdbcType="VARCHAR" property="nickName" />
            <result column="headImg" jdbcType="VARCHAR" property="headImg" />
        </association>
    </resultMap>

    <!--    获取聊天信息-->
    <select id="getChatMessage" parameterType="com.example.wxapi.entity.MessageEntity.ChatMessage" resultMap="chatMsgMap" timeout="10">
        SELECT
            bcm.id,
            bcm.send_user_id sendUserId,
            bcm.accept_user_id acceptUserId,
            wx.nickName,
            wx.headImg,
            bcm.content,
            bcm.sound_time soundTime,
            bcm.type,
            bcm.send_time sendTime
        FROM
            base.chat_message bcm left JOIN base.wx_user wx on bcm.send_user_id = wx.user_id
        WHERE
            ( bcm.send_user_id = #{sendUserId} AND bcm.accept_user_id = #{acceptUserId} AND bcm.`delete` = 0 )
           OR ( bcm.send_user_id = #{acceptUserId} AND bcm.accept_user_id = #{sendUserId} AND bcm.`delete` = 0 )
        ORDER BY
            bcm.send_time DESC
    </select>

    <!--    最新聊天信息-->
    <select id="getFriendMsgList" parameterType="String" resultMap="newMsgMap" timeout="10">
        SELECT
            tmp.userId acceptUserId,
            wu.nickName,
            wu.headImg,
            cm.content,
            cm.type,
            cm.send_time sendTime,
            ( SELECT count( * ) FROM base.chat_message WHERE send_user_id = tmp.userId AND readed = 0 ) readedNum
        FROM
            (
                SELECT
                    max( allMsg.msgId ) msgId,
                    allMsg.userId userId
                FROM
                    (
                        ( SELECT max( id ) msgId, send_user_id userId FROM base.chat_message WHERE accept_user_id = #{userId} GROUP BY send_user_id ) UNION ALL
                        ( SELECT max( id ) msgId, accept_user_id userId FROM base.chat_message WHERE send_user_id = #{userId} GROUP BY accept_user_id )
                    ) allMsg
                GROUP BY
                    allMsg.userId
            ) tmp
                INNER JOIN base.chat_message cm ON cm.id = tmp.msgId
                INNER JOIN base.wx_user wu ON wu.user_id = tmp.userId
        ORDER BY cm.send_time DESC
    </select>

<!--    获取所有聊天未读消息数量-->
    <select id="getAllNoReadMsgNum" parameterType="String" resultType="int" timeout="10">
        SELECT
            COUNT( * ) noReadNum
        FROM
            base.chat_message
        WHERE
            accept_user_id = #{userId}
          AND readed = 0
    </select>

<!--    已读消息-->
    <update id="readedMsg" parameterType="String" timeout="10">
        UPDATE base.chat_message
        SET readed = 1
        WHERE
            accept_user_id = #{acceptUserId}
          AND send_user_id = #{sendUserId}
    </update>

<!--    删除信息-->
    <update id="delMsg" parameterType="int" timeout="10">
        UPDATE base.chat_message SET `delete` = 1 WHERE id = #{msgId}
    </update>

    <!--    发送消息-->
    <insert id="sendMsg" parameterType="com.example.wxapi.entity.MessageEntity.ChatMessage" timeout="10">
        INSERT INTO base.chat_message
        <trim prefix="(" suffix=")" suffixOverrides=",">
            send_user_id,
            accept_user_id,
            <if test="content!=null">
                content,
            </if>
            <if test="soundTime!=null">
                sound_time,
            </if>
            `type`,
            send_time
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            #{sendUserId},
            #{acceptUserId},
            <if test="content!=null">
                #{content},
            </if>
            <if test="soundTime!=null">
                #{soundTime},
            </if>
            #{type},
            #{sendTime}
        </trim>
    </insert>

</mapper>

dao层:

MessageMapper.java

package com.example.wxapi.dao.personIndexMapper;

import com.example.wxapi.entity.MessageEntity.ChatMessage;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface MessageMapper {

    List<ChatMessage> getFriendMsgList(String userId);

    List<ChatMessage> getChatMessage(String sendUserId,String acceptUserId);

    Boolean sendMsg(String sendUserId,String acceptUserId,String content,String type,Integer soundTime,String sendTime);

    Boolean readedMsg(String sendUserId,String acceptUserId);

    int getAllNoReadMsgNum(String userId);

    Boolean delMsg(int msgId);

}

service层:

MessageService.java

package com.example.wxapi.service.personService;

import com.example.wxapi.entity.MessageEntity.ChatMessage;
import com.example.wxapi.entity.MessageEntity.SystemMessage;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;
import java.util.Map;

public interface MessageService {

    Map<String,Object> getChatMessage(String sendUserId,String acceptUserId, int pageNum, int pageSize);

    Map<String,Object> sendMsg(ChatMessage chatMessage);

    Map<String,Object> sendFileMsg(String sendUserId, String acceptUserId, String type, Integer time, MultipartFile file);

    Map<String,Object> getFriendMsgList(String userId,int pageNum,int pageSize);

    Boolean readedMsg(String sendUserId,String acceptUserId);

    int getAllNoReadMsgNum(String userId);

}

注入类 MessageServiceImpl.java

package com.example.wxapi.service.implement.personServiceImpl;

import com.example.wxapi.component.UploadFile;
import com.example.wxapi.dao.personIndexMapper.MessageMapper;
import com.example.wxapi.entity.MessageEntity.ChatMessage;
import com.example.wxapi.service.personService.MessageService;
import com.example.wxapi.tools.Time;
import com.example.wxapi.webSocket.WebSocketServer;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
@Slf4j
public class MessageServiceImpl implements MessageService {

    @Resource
    private MessageMapper messageMapper;

    @Resource
    private WebSocketServer webSocketServer;

    /**
     * 获取聊天好友信息
     * @param userId
     * @param pageNum
     * @param pageSize
     * @return
     */
    @Override
    public Map<String, Object> getFriendMsgList(String userId, int pageNum, int pageSize) {
        Map<String,Object> resData = new HashMap<>();
        PageHelper.startPage(pageNum,pageSize);
        PageInfo<ChatMessage> info = new PageInfo<>(messageMapper.getFriendMsgList(userId));
        resData.put("pagesNum",info.getPages());
        resData.put("totalNum",info.getTotal());
        resData.put("size",info.getSize());
        resData.put("data", info.getList());
        return resData;
    }

    /**
     * 获取聊天信息
     * @param sendUserId
     * @param acceptUserId
     * @param pageNum
     * @param pageSize
     * @return
     */
    @Override
    public Map<String, Object> getChatMessage(String sendUserId,String acceptUserId,int pageNum,int pageSize) {
        Map<String,Object> resData = new HashMap<>();
        PageHelper.startPage(pageNum,pageSize);
        PageInfo<ChatMessage> info = new PageInfo<>(messageMapper.getChatMessage(sendUserId,acceptUserId));
        resData.put("pagesNum",info.getPages());
        resData.put("totalNum",info.getTotal());
        resData.put("size",info.getSize());
        resData.put("data", info.getList());
        return resData;
    }

    /**
     * 发送消息
     * @param chatMessage
     * @return
     */
    @Override
    public Map<String,Object> sendMsg(ChatMessage chatMessage) {
        Map<String,Object> repData = new HashMap<>();
        if(messageMapper.sendMsg(chatMessage.getSendUserId(), chatMessage.getAcceptUserId(), chatMessage.getContent(), chatMessage.getType(), chatMessage.getSoundTIme(),Time.getTime("yyyy-MM-dd HH:mm:ss"))) {
            try {
                List<Map<String,Object>> newMsg = (List<Map<String, Object>>) this.getChatMessage(chatMessage.getSendUserId(), chatMessage.getAcceptUserId(),1,1).get("data");
                webSocketServer.send(chatMessage.getSendUserId(), chatMessage.getAcceptUserId(), newMsg.get(0));
                repData.put("status",true);
                repData.put("returnMsg",newMsg.get(0));
            } catch (IOException e) {
                log.info("发送失败!");
            }
        }
        return repData;
    }

    /**
     * 发送聊天文件
     * @param sendUserId
     * @param acceptUserId
     * @param type
     * @param file
     * @return
     */
    @Override
    public Map<String, Object> sendFileMsg(String sendUserId, String acceptUserId, String type, Integer time, MultipartFile file) {
        Map<String,Object> repData = new HashMap<>();
        Map<String,Object> res = UploadFile.doRemoteUpload(file,"/file/");
        if ((Boolean) res.get("status")) {
            if (messageMapper.sendMsg(sendUserId,acceptUserId,(String) res.get("fileUrl"),type,time,Time.getTime("yyyy-MM-dd HH:mm:ss"))) {
                List<Map<String,Object>> newMsg = (List<Map<String, Object>>) this.getChatMessage(sendUserId,acceptUserId,1,1).get("data");
                try {
                    webSocketServer.send(sendUserId, acceptUserId, newMsg.get(0));
                }catch (IOException e) {
                    log.info("发送失败!");
                }
                repData.put("status",true);
                repData.put("returnMsg",newMsg.get(0));
            }
        }else {
            repData.put("status",false);
        }
        return repData;
    }

    /**iu i
     * 已读消息
     * @param sendUserId
     * @param acceptUserId
     * @return
     */
    @Override
    public Boolean readedMsg(String sendUserId, String acceptUserId) {
        return messageMapper.readedMsg(sendUserId,acceptUserId);
    }

    /**
     * 所有未读数
     * @param userId
     * @return
     */
    @Override
    public int getAllNoReadMsgNum(String userId) {
        return messageMapper.getAllNoReadMsgNum(userId);
    }

}

controller层:

MsgApi.java

package com.example.wxapi.controller.personIndexApi;

import com.example.wxapi.dao.personIndexMapper.MessageMapper;
import com.example.wxapi.entity.MessageEntity.ChatMessage;
import com.example.wxapi.global.JsonResult;
import com.example.wxapi.service.implement.personServiceImpl.MessageServiceImpl;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
import java.util.Objects;

/**
*   JsonResult 为自定义json序列化方法,用自己的方法即可
*
**/

@RestController
@RequestMapping("/msg")
public class MsgApi {

    @Resource
    private MessageServiceImpl messageService;

    @Resource
    private MessageMapper messageMapper;

    /**
     * 获取好友列表API
     * @param userId
     * @param pageNum
     * @param pageSize
     * @return
     */
   
    @PostMapping("/getFriendMsgList")
    public JsonResult getFriendMsgList(@RequestParam(value = "userId") String userId,
                                       @RequestParam(value = "pageNum") int pageNum,
                                       @RequestParam(value = "pageSize") int pageSize) {
        return JsonResult.success(messageService.getFriendMsgList(userId,pageNum,pageSize));
    }

    /**
     * 获取聊天信息API
     * @param sendUserId
     * @param acceptUserId
     * @param pageNum
     * @param pageSize
     * @return
     */

    
    @PostMapping("/getChatMessage")
    public JsonResult getChatMessage(@RequestParam(value = "sendUserId") String sendUserId,
                                     @RequestParam(value = "acceptUserId") String acceptUserId,
                                     @RequestParam(value = "pageNum") int pageNum,
                                     @RequestParam(value = "pageSize") int pageSize) {
        return JsonResult.success(messageService.getChatMessage(sendUserId,acceptUserId,pageNum,pageSize));
    }

    /**
     * 发送消息API
     * @param chatMessage
     * @return
     */
    
    @PostMapping("sendMsg")
    public JsonResult sendMsg(@RequestBody ChatMessage chatMessage) {
        Map<String,Object> repData = messageService.sendMsg(chatMessage);
        if ((Boolean) repData.get("status"))
            return JsonResult.success(repData.get("returnMsg"));
        return JsonResult.fail();
    }

    /**
     * 发送聊天文件API
     * @param sendUserId
     * @param acceptUserId
     * @param type
     * @param time
     * @param file
     * @return
     */
    
    @PostMapping("/sendFileMsg")
    public JsonResult sendFileMsg(@RequestParam(value = "sendUserId") String sendUserId,
                                  @RequestParam(value = "acceptUserId") String acceptUserId,
                                  @RequestParam(value = "type") String type,
                                  @RequestParam(value = "time", required = false) Integer time,
                                  @RequestParam(value = "file")MultipartFile file
                                  ) {
        Map<String,Object> resData = messageService.sendFileMsg(sendUserId,acceptUserId,type,time,file);
        if ((Boolean) resData.get("status"))
            return JsonResult.success(resData.get("returnMsg"));
        else
            return JsonResult.fail("发送失败!");
    }

    /**
     * 已读消息API
     * @param sendUserId
     * @param acceptUserId
     * @return
     */
    
    @GetMapping("/readedMsg")
    public JsonResult readedMsg(@RequestParam("sendUserId") String sendUserId,
                                @RequestParam("acceptUserId") String acceptUserId) {
        if (messageService.readedMsg(sendUserId,acceptUserId))
            return JsonResult.success();
        return JsonResult.fail(200,"已读失败");
    }

    /**
     * 获取所有消息未读数API
     * @param userId
     * @return
     */
    
    @GetMapping("/getAllNoReadMsgNum")
    public JsonResult getAllNoReadMsgNum(@RequestParam("userId") String userId) {
        return JsonResult.success(messageService.getAllNoReadMsgNum(userId));
    }

    /**
     * 删除消息
     * @param msgId
     * @return
     */
    
    @DeleteMapping("/delMsg")
    public JsonResult delMsg(int msgId) {
        if (messageMapper.delMsg(msgId))
            return JsonResult.success();
        return JsonResult.fail();
    }
}

最重要的东西来了,Websocket服务

WebSocketServer.java

package com.example.wxapi.webSocket;

import com.alibaba.fastjson2.JSON;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@ServerEndpoint("/websocket/{uid}")
@Component
public class WebSocketServer {

    private static int onlineCount = 0;

    private Session session;

    private String uid;

    private static final ConcurrentHashMap<Object,WebSocketServer> webSocketMap = new ConcurrentHashMap<>();

    /**
     * 连接
     * @param session
     * @param uid
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("uid") String uid) {
        this.session = session;
        this.uid = uid;
        if(webSocketMap.containsKey(uid)) {
            webSocketMap.remove(uid);
            webSocketMap.put(uid,this);
        }else {
            webSocketMap.put(uid,this);
            onlineCount++;
        }

        log.info("用户:{} 连接成功,当前在线人数:{}",uid,onlineCount);
    }

    /**
     * 关闭连接
     */
    @OnClose
    public void onClose() {
        if(webSocketMap.containsKey(uid))
            onlineCount--;
        webSocketMap.remove(uid);
        log.info("用户:{} 已退出连接,当前在线人数:{}",uid,onlineCount);
    }

    /**
     * 监听消息
     * @param content
     * @param session
     */
    @OnMessage
    public void OnMessage(String content,Session session) {

        log.info("用户:{} 发送内容:{}",uid,content);
    }

    /**
     * 服务推送消息
     * @param content
     * @throws IOException
     */
    public void sendMessage(String content) throws IOException {
        this.session.getBasicRemote().sendText(content);
    }

    /**
     * 发送消息
     * @param uid
     * @param toUid
     * @param content
     * @throws IOException
     */
    public void send(String uid,String toUid,Object content) throws IOException {
        if(webSocketMap.containsKey(toUid)) {
            Map<String,Object> msgInfo = new HashMap<>();
            msgInfo.put("sender",uid);
            msgInfo.put("acceptor",toUid);
            msgInfo.put("msg",content);
            webSocketMap.get(toUid).sendMessage(JSON.toJSONString(msgInfo));
            log.info("用户:{} 向用户: {} 发送了信息:{}",uid,toUid,content);
        }
        else {
            log.info("用户:{} 没在线",toUid);
        }
    }

}

上传文件到文件服务器的uploadFile方法

UploadFile.java

package com.example.wxapi.component;

import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.WebResource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;

@Component
@Slf4j
public class UploadFile {

    //远程文件服务器地址
    private static final String FILE_URL="http://xxxxxxxx"

    public static Map<String,Object> doRemoteUpload(MultipartFile File,String fileType){
        Map<String,Object> map = new HashMap<>();
        //文件服务器url
        String path = FILE_URL;
        //为上传到服务器的文件取名,使用UUID防止文件名重复
        String type= Objects.requireNonNull(File.getOriginalFilename()).substring(File.getOriginalFilename().lastIndexOf("."));
        String fileNicKName= UUID.randomUUID() +type;
        String fileName = File.getOriginalFilename();
        String fileUrl = path + fileType + fileNicKName;
        try{
            //使用Jersey客户端上传文件
            Client client = Client.create();
            WebResource webResource = client.resource(path + fileType + URLEncoder.encode(fileNicKName, StandardCharsets.UTF_8));
            webResource.put(File.getBytes());
            map.put("status",true);
            map.put("fileName",fileName);
            map.put("fileUrl",fileUrl);
            log.info("文件名:{}  =======> 文件上传路径:  {}",fileName,fileUrl);
        }catch(Exception e){
            e.printStackTrace();
           map.put("status",false);
           map.put("Msg","上传失败!");
        }
        return map;
    }

}

三、前端页面设计

聊天页面

chatIndex.vue

<template>
    <view class="chat">
        <scroll-view
            class="scroll-view"
            :style="{height: `${windowHeight-inputHeight}rpx`}"
            id="scrollview"
            scroll-y="true" 
            :scroll-top="scrollTop"
            @scrolltoupper="topRefresh"
            @click="touchClose"
        >
            <view id="msglistview" class="chat-body">
                <u-loading-icon v-if="loading" />
                <view v-for="(item,index) in msgList" :key="index">
                    <view class="msg-time" v-if="item.isShowTime">
                        {{changeTime(item.sendTime)}}
                    </view>                
                    <view class="item self" v-if="item.sendUserId == userId">
                        <view class="msg-menu menu-right" :style="{display: showBoxId==item.id?'block':'none'}">
                            <view class="tr-icon tr-icon-right"></view>
                            <ChatMsgMenu :msgUserId="item.sendUserId" :msgId="item.id" :content="item.content" :msgSortId="index" :time="item.sendTime" @cancelMsg="cancelMsg"/>
                        </view>                        
                        <view  @longpress="showBoxId=item.id">
                            <view class="content-text right" v-if="item.type=='text'">
                                {{item.content}}
                            </view>
                            <view class="content-text right" v-else-if="item.type=='voice'">
                                <view style="display: flex;" @click="playSound(item.content)">
                                    <text>{{ item.soundTIme }}''</text>
                                    <image style="width: 42rpx;height: 42rpx;" :src="imgConf.replayChange"/>
                                </view>
                            </view>
                            <view class="content-img" v-else-if="item.type=='img'">
                                <image class="img-style" :src="item.content"  mode="widthFix" :lazy-load="true"/>
                            </view>
                            <view class="content-video" v-else>
                                <video class="video-style" :src="item.content" />
                            </view>
                        </view>                        
                        <image class="avatar" :src="item.wxUser.headImg" />
                    </view>
                    <view class="item Ai" v-else>
                        <image class="avatar" :src="item.wxUser.headImg" />
                        <view  @longpress="showBoxId=item.id">
                            <view class="content-text left" v-if="item.type=='text'">
                                {{item.content}}
                            </view>
                            <view class="content-text le  ft" v-else-if="item.type=='voice'">
                                <view style="display: flex;" @click="playSound(item.content)">
                                    <text>{{ item.soundTIme }}''</text>
                                    <image style="width: 42rpx;height: 42rpx;" :src="imgConf.replayChange"/>
                                </view>
                            </view>
                            <view class="content-img" v-else-if="item.type=='img'">
                                <image class="img-style" :src="item.content"  mode="widthFix" :lazy-load="true"/>
                            </view>
                            <view class="content-video" v-else>
                                <video class="video-style" :src="item.content" />
                            </view>
                        </view>
                        <view class="msg-menu menu-left" :style="{display: showBoxId==item.id?'block':'none'}">
                            <view class="tr-icon tr-icon-left"></view>
                            <ChatMsgMenu :msgUserId="item.sendUserId" :msgId="item.id" :content="item.content" :msgSortId="index" :time="item.sendTime" @cancelMsg="cancelMsg"/>
                        </view>
                    </view>
                </view>
            </view>
        </scroll-view>
        <view class="chat-bottom" :style="{height: `${inputHeight}rpx`}">
            <view class="send-msg" :style="{bottom:`${keyboardHeight}rpx`}">
                <view class="uni-textarea">
                    <image class="icon-style" :src="changeLogUrl" @click="changeInputType"/>
                    <view class="out_textarea_box">
                        <textarea
                            placeholder-class="textarea_placeholder"
                            :style="{textAlign:(textareaConf.disabled?'center':'')}"
                            v-model="chatMsg"
                            maxlength="250"
                            confirm-type="send"
                            auto-height
                            :placeholder="textareaConf.text"
                            :show-confirm-bar="false"
                            :adjust-position="false"
                            :disabled="textareaConf.disabled"
                            @confirm="handleSend"
                            @linechange="sendHeight"
                            @focus="focus" @blur="blur"
                            @touchstart="handleTouchStart"
                            @touchmove="handleTouchMove"
                            @touchend="handleTouchEnd"
                           />
                    </view>                                     
                    <image class="icon-style" :src="imgConf.emoji" @click="handleSend"/>
                    <image class="icon-style" :src="imgConf.more" @click="moreMenu"/>
                </view>
            </view>
            <view :style="{display:showMoreMenu?'block':'none'}" class="more-menu">
                <view class="inner-box">
                    <view class="menu" @click="sendFile('choose','')">
                        <view>
                            <image class="i-style" :src="imgConf.sendphoto"></image>
                            <view class="t-style">照片</view>
                        </view>
                    </view>
                    <view class="menu" @click="sendFile('shoot','')">
                        <view>
                            <image class="i-style" :src="imgConf.takePhoto"></image>
                            <view class="t-style">拍摄</view>
                        </view>
                    </view>
                </view>
            </view>
        </view>
        <view class="voice-mask" v-show="voice.mask">
            <view class="inner-mask">
                <view class="voice-progress-box" :style="{width:`${progressNum}`+'rpx'}">
                    <view class="third-icon"/>
                    <view class="progress-num">
                        {{ voice.length }}s
                    </view>
                </view>
                <view class="cancel-btn" :class="{cancelBtn:voice.cancel}">
                    <image style="width: 60rpx;height: 60rpx;" src="http://116.205.133.116:8080/static/app/logo/publicLogo/cancelSend.png"></image>
                </view>
                <view class="show-tips">
                    上滑取消发送
                </view>
                <view class="bottom-area">
                    <image class="img-style" :src="imgConf.voiceBtn" />
                </view>
            </view>
        </view>
    </view>
</template>
<script>
import timeMethod from "@/tools/timeMethod.js";
import time from "@/tools/timeMethod.js"
import conf from "@/config/properties.js"
export default{
    data() {
        return {
            imgConf: { //页面icon图片地址配置
                emoji: "http://xxx/static/app/logo/publicLogo/emoji.png",
                more: "http://xxx/static/app/logo/publicLogo/more.png",
                sendphoto: "http://xx/static/app/logo/publicLogo/sendPhoto.png",
                sendVideo: "http://xxx/static/app/logo/publicLogo/video.png",
                takePhoto: "http://xxx/static/app/logo/publicLogo/takePhoto.png",
                voiceBtn: "http://xxx/static/app/logo/publicLogo/voiceBtn.png",
                keyboard: "http://xxx/static/app/logo/publicLogo/keyborad.png",
                speak: "http://xxx/static/app/logo/publicLogo/speak.png",
                replayChange: "http://xxx/static/app/logo/publicLogo/replay.png",
                replay: "http://xxx/static/app/logo/publicLogo/replay.png",
                replaing: "http://xxx/static/app/logo/publicLogo/replaing.png"
            
            },
            changeLogUrl: "http://xx/static/app/logo/publicLogo/speak.png",
            loading: false,
            keyboardHeight:0,
            bottomHeight: 0,
            scrollTop: 0,
            chatMsg: "",
            userId: "",
            userHeadImg: "",
            toUserId: "",
            toUserHeadImg: "",
            pageSize: 20,
            pageNum: 1,
            returnPageNum: "",
            msgList: [],
            judgeScrollToBottom: true,
            startTime: "",
            msgID: 0,
            showBoxId: "",
            showBoxUserId: "",
            showMoreMenu: false,
            textareaConf: {
                disabled: false,
                text: ""
            },
            voice: {
                mask: false,
                length: 0,
                cancel: false,
                startX: "",
                startY: "",
                timer: "",
                recordInstance: "",
                finished: false
            },
            msgConf: {
                showTimeSpace: 120  //消息隔多长时间才展示
            }
        }
    },
    updated(){
        //页面更新时调用聊天消息定位到最底部
        if (this.judgeScrollToBottom) {
            this.scrollToBottom();
        }
    },
    computed: {
        windowHeight() {
            return this.rpxTopx(uni.getSystemInfoSync().windowHeight);
        },
        
        // 键盘弹起来的高度+发送框高度
        inputHeight(){
            return this.bottomHeight+this.keyboardHeight;
        },
        
        //语音进度条
        progressNum() {
            return this.voice.length*2 + 250;
        }

    },
    onLoad(e){
        //监听键盘高度
        uni.onKeyboardHeightChange(res => {
            this.keyboardHeight = this.rpxTopx(res.height);
            if(this.keyboardHeight<=0) {
                this.keyboardHeight = 0;
                this.showMoreMenu = false;
            }
        });
        //获取自己userId
        this.userId = uni.getStorageSync("userId");
        //获取好友UserId
        this.toUserId = e.userId;
        //创建录音实例
        this.voice.recordInstance = uni.getRecorderManager();
        this.webSocket();                    
        this.getUserInfo();
        this.getMessage();
        this.readedMsg();
        
    },
    onUnload() {
        //关闭socket
        uni.closeSocket({
            code: 200,
            success() {
                console.log("正常关闭")
            }
        })
    },
    methods: {
        
        //websocket实例
        webSocket() {    
            //消息监听
            uni.onSocketMessage((res)=>{
                let data = JSON.parse(res.data);
                this.msgList.push(data.msg);
            })
        },
        
        
        //下拉刷新
        topRefresh() {
            if (this.pageNum<this.returnPageNum) {
                this.pageNum++;
                this.judgeScrollToBottom = false;
                this.loading = true;
                this.getMessage();
            }
        },
        
        //获取消息列表
        getMessage() {
            this.$request("/msg/getChatMessage","POST",
            {"sendUserId": this.userId,
            "acceptUserId": this.toUserId,
            "pageSize": this.pageSize,
            "pageNum": this.pageNum},{"Content-Type":"application/x-www-form-urlencoded"}).then(res=>{
                this.returnPageNum = res.data.data.pagesNum;
                this.showMsgTime(res.data.data.data);        
                this.loading = false;
            })
        },
        
        //消息时间展示
        showMsgTime(data) {
            data.forEach(e=>{
                e.isShowTime = false;  //时间显示打标
                e.sendTime = timeMethod.timeFormat(e.sendTime,"T");
                this.msgID++;  //消息id计数,定位消息list的索引
                if (this.startTime!="") {  //第一条消息前面没时间,排出掉
                    if (Math.abs(timeMethod.calculateTime(e.sendTime,this.startTime))/1000 > this.msgConf.showTimeSpace) {    //计算消息时间间隔大于120秒                    
                        this.msgList.slice(0 - this.msgID)[0].isShowTime = true;  //注入打标数据
                    }
                }
                this.startTime = e.sendTime; //每次循环记住该条消息时间,用于计算消息之间时间间隔
                this.msgList.unshift(e); //处理好数据后push进消息list
            })            
            //消息列表最上面一条显示时间
            if (this.pageNum == this.returnPageNum) {
                this.msgList[0].isShowTime = true;
            }
        },
        
        //时间转变
        changeTime(time) {
            let space = (new Date(timeMethod.timeFormat(time,"T")) - new Date(timeMethod.getNowTime().split("T")[0]+"T00:00:00"))/(1000*60*60*24);
            let Time =timeMethod.timeFormat(time," ").split(" ");
            let week = timeMethod.getDateToWeek(time);
            //当天
            if (space > 0 && space < 1) {
                return Time[1].slice(0,5);
            } 
            //昨天
            else if (space > -1 && space < 0) {
                return "昨天 " + Time[1].slice(0,5);
            } 
            //星期
            else if (space < -1 && Math.abs(space) < timeMethod.getDateToWeek(timeMethod.getNowTime()).weekID - 1) {
                return week.weekName + " " + Time[1].slice(0,5);
            }
            //日期
            else {
                return Time[0].slice(5,10) + " " + Time[1].slice(0,5);
            }
        },
        
        //获取用户信息
        getUserInfo() {
            this.$request("/sys/getUserName","POST",{"userId":this.userId,"toUserId":this.toUserId},
            {"Content-Type":"application/x-www-form-urlencoded"}).then(res=>{
                let data = res.data.data;
                this.userHeadImg = data[0].headImg;
                this.toUserHeadImg = data[1].headImg;
                uni.setNavigationBarTitle({
                    title: data[1].nickName
                })
            })
        },
        
        //输入框聚焦
        focus(){
            this.scrollToBottom();
        },
        
        //输入框取消聚焦
        blur(){
            this.scrollToBottom();
        },
        
        // px转换成rpx
        rpxTopx(px){
            let deviceWidth = uni.getSystemInfoSync().windowWidth;
            let rpx = ( 750 / deviceWidth ) * Number(px);
            return Math.floor(rpx);
        },
        
        // 监视聊天发送栏高度
        sendHeight(){
            setTimeout(()=>{
                let query = uni.createSelectorQuery();
                query.select('.send-msg').boundingClientRect();
                query.exec(res =>{
                    this.bottomHeight = this.rpxTopx(res[0].height);
                })
            },200)
        },
        
        // 滚动至聊天底部
        scrollToBottom(e){
            setTimeout(()=>{
                let query = uni.createSelectorQuery().in(this);
                query.select('#scrollview').boundingClientRect();
                query.select('#msglistview').boundingClientRect();
                query.exec((res) =>{
                    if(res[1].height > res[0].height){
                        this.scrollTop = this.rpxTopx(res[1].height - res[0].height);
                    }
                })
            },200);
        },
        
        // 发送消息
        handleSend() {
            this.judgeScrollToBottom = true;
            this.pageNum = 1;
            //如果消息不为空
            if(this.chatMsg.length!==0){
                this.$request("/msg/sendMsg","POST",{
                    "sendUserId":this.userId,
                    "acceptUserId":this.toUserId,
                    "type": "text",
                    "content":this.chatMsg}).then(res=>{
                    if (res.data.status=="ok") {
                        this.msgList.push(res.data.data);
                        this.chatMsg = "";
                    }
                })
            }
        },
        
        //接收消息或发送消息时间显示
        showTime() {
            let time = timeMethod.getNowTime();
            if (timeMethod.calculateTime(time,this.msgList.slice(-1)[0].sendTime)/1000 > this.msgConf.showTimeSpace) {
                return true;
            }else {
                return false;    
            }                 
        },
        
        //已读消息
        readedMsg() {
            this.$request("/msg/readedMsg","GET",{"sendUserId":this.toUserId,"acceptUserId":this.userId})
        },
        
        //语音播放
        playSound(url) {
            this.imgConf.replayChange = this.imgConf.replaing;
            let music = null;
            music = uni.createInnerAudioContext(); 
            music.src = url;
            music.play(); 
            music.onEnded(()=>{
                music = null;
                this.imgConf.replayChange = this.imgConf.replay;
            })
        },
        
        //msglist索引
        cancelMsg(id) {
            Array.prototype.remove = function (dx) {
                if (isNaN(dx) || dx > this.length) { return false; }
                for (var i = 0, n = 0; i < this.length; i++) {
                    if (this[i] != this[dx]) {
                        this[n++] = this[i]
                    }
                }
                this.length -= 1
            }
            this.msgList.remove(id);
        },
        
        //语音图标切换
        changeInputType() {
            if (this.changeLogUrl == this.imgConf.speak) {
                this.changeLogUrl = this.imgConf.keyboard;
                this.textareaConf.disabled = true;
                this.textareaConf.text = "按住说话";
                this.chatMsg = "";
            } else {
                this.changeLogUrl = this.imgConf.speak;
                this.textareaConf.disabled = false;
                this.textareaConf.text = "";
            }
        },
        
        //全局点击关闭
        touchClose() {
            this.showBoxId = "";
            this.showMoreMenu = false;
            this.keyboardHeight = 0;
        },
        
        //更多菜单
        moreMenu() {
             this.keyboardHeight = 300;
            let timer = setTimeout(()=>{this.showMoreMenu = true;},100);
        },
        
        
        // 开始录制语音
        handleTouchStart(e){
            var that = this;
            if (this.textareaConf.disabled) {
                that.voice.finished = false; //手指离开按钮打标
                uni.getSetting({
                    success(res) {
                        if (res.authSetting['scope.record']===undefined) {
                            console.log("第一次授权")
                        } else if (!res.authSetting['scope.record']) {
                            uni.showToast({
                                icon: "none",
                                title: "点击右上角···进入设置开启麦克风授权!",
                                duration: 2000
                            })
                        } else {                        
                            that.voice.recordInstance.start();
                            that.voice.mask = true;
                            that.voice.isRecord = true;
                            that.voice.length = 1;
                            that.voice.startX = e.touches[0].pageX;
                            that.voice.startY = e.touches[0].pageY;
                            that.voice.timer = setInterval(() => {
                                that.voice.length += 1;
                                if(that.voice.length >= 60) {
                                    clearInterval(that.voice.timer);
                                    that.handleTouchEnd();
                                }
                            },1000)    
                            //判断先结束按钮但是录制才开始时不会结束录制的条件;因为获取授权这儿存在延时;所以结束录制时可能还没开始录制
                            if (that.voice.finished && that.voice.mask) {
                                that.handleTouchEnd();
                            }
                        }
                    }
                })
            }            
        },
        
        
        // 语音录制时滑动事件
        handleTouchMove(e){
            if (this.textareaConf.disabled) {
                if (this.voice.startY - e.touches[0].pageY >100) {
                    this.voice.cancel = true;
                }else {
                    this.voice.cancel = false;
                }
            }
        },
        
        // 语音录制结束
        handleTouchEnd(){
            if (this.textareaConf.disabled) {
                this.voice.finished = true;
                this.voice.mask = false;
                clearInterval(this.voice.timer);
                this.voice.recordInstance.stop();
                this.voice.recordInstance.onStop((res) => {
                    const message = {
                        voice:res.tempFilePath,
                        length:this.voice.length
                    }
                    if (!this.voice.cancel) {
                        if (this.voice.length>1) {
                            this.sendFile("voice",message);
                        } else {
                            uni.showToast({
                                icon: 'none',
                                title: "语音时间太短",
                                duration: 1000
                            })
                        }
                    }else {
                        this.voice.cancel = false;
                    }
                })                                                    
            }
        },
        
        //发送文件
        sendFile(type,data) {
            var that = this;
            if (type=="choose") {
                uni.chooseMedia({
                    count: 1,
                    mediaType: ['image', 'video'],
                    sourceType: ['album'],
                    maxDuration: 30,
                    success(res) {
                        let type = 'img';
                        if (res.tempFiles[0].fileType=='image') {
                            type = 'img'
                        } else {
                            type = 'video'
                        }
                        that.uploadFile(res.tempFiles[0].tempFilePath,type)
                    }
                })    
            } else if (type=="shoot") {
                uni.chooseMedia({
                    count: 1,
                    mediaType: ['image', 'video'],
                    sourceType: ['camera'],
                    maxDuration: 30,
                    success(res) {
                        let type = 'img';
                        if (res.tempFiles[0].fileType=='image') {
                            type = 'img'
                        } else {
                            type = 'video'
                        }
                        that.uploadFile(res.tempFiles[0].tempFilePath,type)
                    }
                })    
            } else {
                that.uploadFile(data.voice,'voice')
            }
        },
        
        uploadFile(path,type) {
            var that = this;
            let data = {"sendUserId":this.userId,"acceptUserId":this.toUserId,"type":type};
            if (type=='voice') {
                data = {"sendUserId":this.userId,"acceptUserId":this.toUserId,"type":type,"time":this.voice.length};
            }
            uni.uploadFile({
                url: "http://127.0.0.1:8080" + "/msg/sendFileMsg",
                filePath: path,
                name: 'file',
                formData: data,
                header: {"token": this.$store.state.token},
                success(res) {
                    let newMsg = JSON.parse(res.data)
                    that.msgList.push(newMsg.data)
                }
            })
        }
        
        
    }
}
</script>
<style lang="scss">
    
$chatContentbgc: #C2DCFF;
$sendBtnbgc: #4F7DF5;

center {
    display: flex;
    align-items: center;
    justify-content: center;
}
 
/* 聊天消息 */
.chat {
    
.topTabbar {
    width: 100%;
    height: 90rpx;
    line-height: 90rpx;
    display: flex;
    margin-top: 80rpx;
    justify-content: space-between;

.icon {
    margin-left: 20rpx;
}

.text {
    margin: auto;
    font-size: 16px;
    font-weight: 700;
}

.button {
    width: 10%;
    margin: auto 20rpx auto 0rpx;
    }
}

.scroll-view {
::-webkit-scrollbar {
    display: none;
    width: 0 !important;
    height: 0 !important;
    -webkit-appearance: none;
    background: transparent;
    color: transparent;
    z-index: 0;
}
            
background-color: #F6F6F6;
    
.chat-body {
    display: flex;
    flex-direction: column;
    padding-top: 23rpx;
    
        
.self {
    justify-content: flex-end;
    position: relative;
}

.Ai {
    position: relative;
}

.item {
    display: flex;
    padding: 23rpx 30rpx;

.right {
    background-color: $chatContentbgc;
}

.left {
    background-color: #FFFFFF;
}

.right::after {
    position: absolute;
    display: inline-block;
    content: '';
    width: 0;
    height: 0;
    left: 100%;
    top: 10px;
    border: 12rpx solid transparent;
    border-left: 12rpx solid $chatContentbgc;
}

.left::after {
    position: absolute;
    display: inline-block;
    content: '';
    width: 0;
    height: 0;
    top: 10px;
    right: 100%;
    border: 12rpx solid transparent;
    border-right: 12rpx solid #FFFFFF;
}

.content-text {
    position: relative;
    max-width: 486rpx;
    border-radius: 8rpx;
    word-wrap: break-word;
    padding: 24rpx 24rpx;
    margin: 0 24rpx;
    border-radius: 5px;
    font-size: 32rpx;
    font-family: PingFang SC;
    font-weight: 500;
    color: #333333;
    line-height: 42rpx;    
}

.content-img {
    margin: 0 24rpx;
}

.content-video {
    margin: 0 24rpx;
}

.img-style {
    width: 400rpx;
    height: auto;
    border-radius: 10rpx;
}

.video-style {
    width: 400rpx;
    height: 400rpx;
}

.avatar {
    display: flex;
    justify-content: center;
    width: 78rpx;
    height: 78rpx;
    background: $sendBtnbgc;
    border-radius: 50rpx;
    overflow: hidden;
    
image {
    align-self: center;
}
    }
    
.msg-menu {
    min-width: 100rpx;
    height: 100rpx;
    display: none;
    background: #383838;
    position: absolute;
    border-radius: 10rpx;
    z-index: 100;
    
.tr-icon {
    position: absolute;
    top: 100rpx;
    width: 0;
    height: 0;
    border: 15rpx solid transparent;
    border-top: 15rpx solid #383838;
}

.tr-icon-left {
    left: 15rpx;
}

.tr-icon-right {
    right: 15rpx;
}
    }
    
.menu-left {
    top: -100rpx;
    left: 120rpx;
    
}

.menu-right {
    top: -100rpx;
    right: 120rpx;
    }

}
    }
    
.msg-time {
    font-size: 24rpx;
    text-align: center;
    color: #737373;
}
}
 
.chat-bottom {
    width: 100%;
    height: auto;
    background: #F4F5F7;
    transition: all 0.25s ease;
    
.send-msg {
    display: flex;
    align-items: flex-end;
    padding: 16rpx 30rpx;
    width: 100%;
    min-height: 150rpx;
    position: fixed;
    bottom: 0;
    background: #fff;
    transition: all 0.25s ease;

.uni-textarea {
    width: 100%;
    padding-bottom: 40rpx;
    display: flex;
    align-items: center;
    
.icon-style {
    width: 60rpx;
    height: 60rpx;
    padding: 0rpx 10rpx ;
}

.out_textarea_box {
    width:65%;
    min-height: 80rpx;
    max-height: 200rpx;
    border-radius: 40rpx;
    background: #f1f1f1;
    display: flex;
    justify-content: center;
    align-items: center;
    
textarea {
    width:86%;
    min-height: 42rpx;
    max-height: 200rpx;
    background: #f1f1f1;
    font-size: 32rpx;
    font-family: PingFang SC;
    color: #333333;
    }
}
    }
}
    }
    
.more-menu {
    width: 100%;
    min-height: 300rpx;
    margin-top: 150rpx;
    display: none;    
    position: fixed;
    bottom: 0rpx;

.inner-box {
    width: 98%;
    height: 280rpx;
    margin: 10rpx 1%;
    display: flex;
    
.menu {
    width: 120rpx;
    height: 130rpx;
    background: #ffffff;
    margin: 20rpx;
    border-radius: 20rpx;
    display: flex;
    align-items: center;
    justify-content: center;

.i-style {
    width: 80rpx;
    height: 80rpx;
}
.t-style {
    font-size: 22rpx;
    font-weight: 600;
    text-align: center;
    }
}
    }
}

.voice-mask{
    position:fixed;
    top:0;
    right:0;
    bottom:0;
    left:0;
    background-color: rgba(0,0,0,0.8);
    
.inner-mask {
    display: flex;
    flex-direction: column;
    align-items: center;
    
.voice-progress-box {
    min-width: 250rpx;
    height: 150rpx;
    margin-top: 60%;
    border-radius: 50rpx;
    background: #4df861;
    position: relative;
    @extend center;
    
.third-icon {
    width: 0;
    height: 0;
    border: 15rpx solid transparent;
    border-top: 15rpx solid #4df861;
    position: absolute;
    top: 100%;
    left: 45%;
    }
    
.progress-num {
    font-size: 50rpx;
    font-weight: 600;
    }
}

.cancel-btn {
    width: 120rpx;
    height: 120rpx;
    clip-path: circle();
    margin-top: 50%;
    background: #080808;
    @extend center;
}

.cancelBtn {
    width: 150rpx;
    height: 150rpx;
    
}

.show-tips {
    width: 100%;
    margin-top: 80rpx;
    text-align: center;
    color: white;
    animation: 4s opacity2 1s infinite; 
    font-size: 30rpx;
    font-weight: 400;
    font-family: sans-serif;
}

@keyframes opacity2{
    0%{opacity:0}
    50%{opacity:.8;}
    100%{opacity:0;}
}

.bottom-area {
    position: fixed;
    bottom: 0rpx;
    width: 100%;
    height:190rpx;
    border-top: #BABABB 8rpx solid;
    border-radius: 300rpx 300rpx 0 0;
    background-image: linear-gradient(#949794,#e1e3e1);
    @extend center;
    
.img-style {
    width: 50rpx;
    height: 50rpx;
}
    }    
}
    }
}
view,button,text,input,textarea {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

::-webkit-scrollbar {
    display: none;
    width: 0 !important;
    height: 0 !important;
    -webkit-appearance: none;
    background: transparent;
}
</style>

timeMethod.js方法 聊天界面需要引入

class TimeMethod {
    
    constructor() {}
    
    //日期格式化
    addZero(data) {
        if (parseInt(data) < 10) {
            return "0" + String(data);
        }
        return data;
    }    
    
    /**
     * 获取当前日期
     */
    getNowTime() {
        var myDate = new Date();
        let year = myDate.getFullYear();
        let mouth = this.addZero(myDate.getMonth());
        let day = this.addZero(myDate.getDate());
        let hour = this.addZero(myDate.getHours());
        let minute = this.addZero(myDate.getMinutes());
        let second = this.addZero(myDate.getSeconds());
        return year + '-' + String((parseInt(mouth)+1)) + '-' + day + 'T' + hour+ ':' + minute+ ':' + second
    }
    
    /**
     * @param {Object} timestamp
     * @param {Object} type
     * 时间戳转时间
     */
    timestampToTime(timestamp,type) {
            if(String(timestamp).length===10) {
                //时间戳为10位需*1000
                var date = new Date(timestamp * 1000);
            }else {
                var date = new Date(timestamp);
            }
            var Y = date.getFullYear() + '-';    
            var M = (date.getMonth()+1 < 10 ? '0'+(date.getMonth()+1) : date.getMonth()+1) + '-';    
            var D = date.getDate() + ' ';    
            var h = date.getHours() + ':';    
            var m = date.getMinutes() + ':';    
            var s = date.getSeconds();
            if(type==="date") {
                return Y+M+D;
            }else {
                return Y+M+D+h+m+s;
            }
        }
        
        
    /**
     * @param {Object} time
     * 时间转时间戳
     */
    timeToTimestamp(time) {
        //精确到秒,毫秒用000代替 :Date.parse(date); 
        return new Date(time).getTime(); 
    }
    
    
    /**
     * @param {Object} startTime
     * @param {Object} endTime
     * 日期计算
     */
    calculateTime(startTime,endTime) {
        return new Date(startTime) - new Date(endTime)
    }
    
    /**
     * @param {Object} time
     * 日期转星期
     */
    getDateToWeek(time) {
        let weekArrayList = [
        {"weekID":7,"weekName":"星期日"},
        {"weekID":1,"weekName":"星期一"},
        {"weekID":2,"weekName":"星期二"},
        {"weekID":3,"weekName":"星期三"},
        {"weekID":4,"weekName":"星期四"},
        {"weekID":5,"weekName":"星期五"},
        {"weekID":6,"weekName":"星期六"}];
        return weekArrayList[new Date(time).getDay()]
    }
    
    /**
     * @param {Object} date
     *  yyyy-MM-dd HH:mm:ss转为   yyyy-MM-ddTHH:mm:ss
     */
    timeFormat(date,type) {
        if (type == "T")
            return date.replace(" ","T")
        else
            return date.replace("T"," ")
    }
    
    /**
     * @param {Object} time
     * 定时器
     */
    timeSleep(time) {
        return new Promise((resolve)=>setTimeout(resolve,time))
    }

}

export default new TimeMethod();

消息组件 ChatMsgMenu.vue 使用uniapp 的 easycom模式 直接引用

<template>
    <view class="menu-box">
        <view  v-for="item,index in menuList" :key="item.id">
            <view class="menu-box-inner" v-if="msgUserId==userId || !item.isShowSelf" @click="clickMenu(item.type,content)">
                <image class="menu-icon" :src="item.icon" />
                <view class="text-style">{{ item.name }}</view>                                    
            </view>
        </view>
    </view>
</template>

<script>
export default {
    props: {
        msgUserId: {
            type: String,
            default: ""
        },
        msgId: {
            type: Number,
            default: 0
        },
        msgSortId: {
            type: Number,
            default: 0
        },
        content: {
            type: String,
            default: ""
        }
        ,time: {
            type: String,
            default: ""
        }
    },
    data() {
        return {
            menuList: [
                {"id":1,"isShowSelf":true,"name":"撤回","type":"cancel","icon":"http://xxx/static/app/logo/publicLogo/cancel.png"},
                {"id":2,"isShowSelf":false,"name":"复制","type":"copy","icon":"http://xxx/static/app/logo/publicLogo/copy.png"},
                {"id":3,"isShowSelf":false,"name":"引用","type":"quote","icon":"http://xxx/static/app/logo/publicLogo/quote.png"}
            ],
            userId: uni.getStorageSync("userId")
        }
    },
    methods: {
        
        //点击菜单
        clickMenu(type,text) {
            switch(type) {
                case "cancel":
                    this.delMsg();
                    break;
                case "copy":
                    this.copyText(text)
                    break;
                default:
                    console.log(type)
            }
        },
        
        //撤回消息
        delMsg() {
            this.$request("/msg/delMsg","DELETE",{"msgId":this.msgId},{"Content-Type":"application/x-www-form-urlencoded"}).then(res=>{
                if (res.data.status=="ok") {
                    uni.showToast({
                        icon: "none",
                        title: "消息已撤回",
                        duration: 1000
                    })    
                    this.$emit("cancelMsg",this.msgSortId);
                }
            })
        },
        
        //复制信息
        copyText(text) {
            wx.setClipboardData({
                data: text,
                success(res) {
                     wx.getClipboardData({
                         success(res) {
                             uni.showToast({
                                 icon: 'none',
                                title: "复制成功",
                                duration: 1000
                             })
                         }
                     })
                }
            })
        }
    }
}
</script>

<style lang="scss">
center {
    display: flex;
    align-items: center;
    justify-content: center;
}    

.menu-box {
    min-width: 100rpx;
    height: 100rpx;
    @extend center;
    
.menu-box-inner {
    width: 90rpx;
    @extend center;
    flex-direction: column;
    
.menu-icon {
    width: 45rpx;
    height: 45rpx;
}

.text-style {
    font-size: 23rpx;
    color: #e6e6e6;
}
    }
}
</style>

最重要的webSocket.js,这个需要直接挂在main.js上 在里面直接导入 import "@/webSocket/webSocket.js"就行

class WebSocket {
    
    
    constructor() {
        let userId =  uni.getStorageSync("userId")
        if (userId.length!==0) {
            this.connect(userId);
        }
    }
    
    /**
     * 连接
     */
    connect(userId) {
        uni.connectSocket({
            url: `ws://127.0.0.1:8080/${userId}`,
            header: {
                'content-type': 'application/json'
            },
            method: 'GET',
            success() {
                console.log("socket连接成功!");
            },
            fail() {
                console.log("socket连接失败!");
            }
        })
        
        uni.onSocketOpen(res=>{
            console.log("监测到已连接上websocket")
        })
        
        uni.onSocketError(res=>{
            console.log(res)
            console.log("监测到连接websocket错误")
        })
        
        uni.onSocketClose(res=>{
            console.log("监测到连接websocket已关闭")
        })
    }
    
}

export default new WebSocket();

本文转载自: https://blog.csdn.net/qq_53266140/article/details/133912056
版权归原作者 二九筒 所有, 如有侵权,请联系我们删除。

“springBoot+webSocket+uniapp实现实时聊天功能”的评论:

还没有评论