简要
该文章只是作者从自己开发的代码中截取的一部分,只是做一个参考;实际上需要自己在该代码基础上进行调整和优化,有疑问可以在评论区进行提问
一、数据库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);
}
}
Time工具类:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
public class Time {
private final static String timeFormat1 = "yyyy-MM-dd HH:mm:ss";
private final static String timeFormat2 = "yyyy/MM/dd HH:mm:ss";
private final static String timeFormat3 = "yyyy-MM-dd";
private final static String timeFormat4 = "yyyy/MM/dd";
/**
* 获取当前时间
* @return
*/
public static String getTime(String timeFormat) {
SimpleDateFormat df = new SimpleDateFormat(timeFormat);//设置日期格式
return df.format(new Date());
}
/**
* 获取当前时间默认时间格式
* @return
*/
public static String getTime() {
SimpleDateFormat df = new SimpleDateFormat(timeFormat1);
return df.format(new Date());
}
/**
* 字符串转Date
* @param time
* @param timeFormat
* @return
*/
public static Date strToDate(String time,String timeFormat) {
try {
return new SimpleDateFormat(timeFormat).parse(time);
}catch (ParseException e) {
e.printStackTrace();
}
return null;
}
/**
* 字符串转Date(默认格式)
* @param time
* @return
*/
public static Date strToDate(String time) {
try {
return new SimpleDateFormat(timeFormat1).parse(time);
}catch (ParseException e) {
e.printStackTrace();
}
return null;
}
/**
* Date转字符串(默认格式)
* @param time
* @return
*/
public static String dateToStr(Date time) {
SimpleDateFormat df = new SimpleDateFormat(timeFormat1);
return df.format(time);
}
/**
* Date转字符串指定时间格式
* @param time
* @param timeFormat
* @return
*/
public static String dateToStr(Date time,String timeFormat) {
SimpleDateFormat df = new SimpleDateFormat(timeFormat);
return df.format(time);
}
/**
* 获取时间戳
* @return
*/
public static long getTimeStamp() {
//其他两种方式
//long time3 = new Date().getTime();
//long time1 = System.currentTimeMillis();
return Calendar.getInstance().getTimeInMillis();
}
/**
*时间转化为时间戳
* @param date
* @return
* @throws ParseException
*/
public static long changeStamp(String date, String timeFormat) throws ParseException {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(timeFormat);
Date time= simpleDateFormat.parse(date);
return time.getTime();
}
/**
*时间转化为时间戳(默认格式)
* @param date
* @return
* @throws ParseException
*/
public static long changeStamp(String date) throws ParseException {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(timeFormat1);
Date time= simpleDateFormat.parse(date);
return time.getTime();
}
/**
*时间戳转化为时间
* @param stamp
* @return
*/
public static String changeTime(String stamp, String timeFormat) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(timeFormat);
Date date = new Date(Long.parseLong(stamp));
return simpleDateFormat.format(date);
}
/**
*时间戳转化为时间(默认格式)
* @param stamp
* @return
*/
public static String changeTime(String stamp) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(timeFormat1);
Date date = new Date(Long.parseLong(stamp));
return simpleDateFormat.format(date);
}
}
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"
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();