一套简单的web即时通讯——第一版
前言
我们之前已经实现了 WebSocket+Java 私聊、群聊实例,后面我们模仿layer弹窗,封装了一个自己的web弹窗 自定义web弹窗/层:简易风格的msg与可拖放的dialog,生成博客园文章目录弹窗,再后来就产生了将两者结合起来的想法,加上我们之前实现了一套自动生成代码的jpa究极进化版 SpringBoot系列——Spring-Data-JPA(究极进化版) 自动生成单表基础增、删、改、查接口,于是去网上搜索,参考即时通讯系统的消息存储如何建表,看下能不能把我们之前的东西稍微整合一下,将之前的写的东西应用起来学以致用,实现一套简单的web即时通讯,一版一版的升级完善。
第一版功能
1、实现简单的登录/注册
2、将之前的页面改成自定义web弹窗的形式,并且完善群聊、私聊功能
代码编写
目前建了三个表,SQL如下:
/* Navicat Premium Data Transfer Source Server : localhost Source Server Type : MySQL Source Server Version : 50528 Source Host : localhost:3306 Source Schema : test Target Server Type : MySQL Target Server Version : 50528 File Encoding : 65001 Date: 09/05/2019 10:09:11 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for ims_friend -- ---------------------------- DROP TABLE IF EXISTS `ims_friend`; CREATE TABLE `ims_friend` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键', `user_id` int(11) NULL DEFAULT NULL COMMENT '用户id', `friend_id` int(11) NULL DEFAULT NULL COMMENT '好友id', `created_time` datetime NULL DEFAULT NULL COMMENT '创建时间', `updata_time` datetime NULL DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '好友表' ROW_FORMAT = Compact; -- ---------------------------- -- Table structure for ims_friend_message -- ---------------------------- DROP TABLE IF EXISTS `ims_friend_message`; CREATE TABLE `ims_friend_message` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键', `from_user_id` int(11) NULL DEFAULT NULL COMMENT '发消息的人的id', `to_user_id` int(11) NULL DEFAULT NULL COMMENT '收消息的人的id', `content` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '消息内容', `is_read` int(11) NULL DEFAULT NULL COMMENT '是否已读,1是0否', `is_back` int(11) NULL DEFAULT NULL COMMENT '是否撤回,1是0否', `created_time` datetime NULL DEFAULT NULL COMMENT '创建时间', `updata_time` datetime NULL DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '好友消息表' ROW_FORMAT = Compact; -- ---------------------------- -- Table structure for ims_user -- ---------------------------- DROP TABLE IF EXISTS `ims_user`; CREATE TABLE `ims_user` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键', `user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '帐号', `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码', `nick_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵称', `gender` int(11) NULL DEFAULT 0 COMMENT '性别:0为男,1为女', `avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像', `email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '电子邮箱', `phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号码', `sign` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '个性签名', `created_time` datetime NULL DEFAULT NULL COMMENT '创建时间', `updata_time` datetime NULL DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户信息表' ROW_FORMAT = Compact; SET FOREIGN_KEY_CHECKS = 1;
自动生成代码,运行main方法执行
package cn.huanzi.ims.util; import java.io.File; import java.io.FileWriter; import java.io.PrintWriter; import java.sql.*; import java.util.ArrayList; import java.util.List; /** * 自动生成代码 */ public class CodeDOM { /** * 构造参数,出入表名 */ private CodeDOM(String tableName) { this.tableName = tableName; basePackage_ = "cn\\huanzi\\ims\\"; package_ = basePackage_ + StringUtil.camelCaseName(tableName).toLowerCase() + "\\"; //System.getProperty("user.dir") 获取的是项目所在路径 basePath = System.getProperty("user.dir") + "\\src\\main\\java\\" + package_; } /** * 数据连接相关 */ private static final String URL = "jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&characterEncoding=utf-8"; private static final String USERNAME = "root"; private static final String PASSWORD = "123456"; private static final String DRIVERCLASSNAME = "com.mysql.jdbc.Driver"; /** * 表名 */ private String tableName; /** * 基础路径 */ private String basePackage_; private String package_; private String basePath; /** * 创建pojo实体类 */ private void createPojo(List<TableInfo> tableInfos) { File file = FileUtil.createFile(basePath + "pojo\\" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ".java"); StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append( "package " + package_.replaceAll("\\\\", ".") + "pojo;\n" + "\n" + "import lombok.Data;\n" + "import javax.persistence.*;\n" + "import java.io.Serializable;\n" + "import java.util.Date;\n" + "\n" + "@Entity\n" + "@Table(name = \"" + tableName + "\")\n" + "@Data\n" + "public class " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + " implements Serializable {\n" ); //遍历设置属性 for (TableInfo tableInfo : tableInfos) { //主键 if ("PRI".equals(tableInfo.getColumnKey())) { stringBuffer.append(" @Id\n"); } //自增 if ("auto_increment".equals(tableInfo.getExtra())) { stringBuffer.append(" @GeneratedValue(strategy= GenerationType.IDENTITY)\n"); } stringBuffer.append(" private " + StringUtil.typeMapping(tableInfo.getDataType()) + " " + StringUtil.camelCaseName(tableInfo.getColumnName()) + ";//" + tableInfo.getColumnComment() + "\n\n"); } stringBuffer.append("}"); FileUtil.fileWriter(file, stringBuffer); } /** * 创建vo类 */ private void createVo(List<TableInfo> tableInfos) { File file = FileUtil.createFile(basePath + "vo\\" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo.java"); StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append( "package " + package_.replaceAll("\\\\", ".") + "vo;\n" + "\n" + "import lombok.Data;\n" + "import java.io.Serializable;\n" + "import java.util.Date;\n" + "\n" + "@Data\n" + "public class " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo implements Serializable {\n" ); //遍历设置属性 for (TableInfo tableInfo : tableInfos) { stringBuffer.append(" private " + StringUtil.typeMapping(tableInfo.getDataType()) + " " + StringUtil.camelCaseName(tableInfo.getColumnName()) + ";//" + tableInfo.getColumnComment() + "\n\n"); } stringBuffer.append("}"); FileUtil.fileWriter(file, stringBuffer); } /** * 创建repository类 */ private void createRepository(List<TableInfo> tableInfos) { File file = FileUtil.createFile(basePath + "repository\\" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Repository.java"); StringBuffer stringBuffer = new StringBuffer(); String t = "String"; //遍历属性 for (TableInfo tableInfo : tableInfos) { //主键 if ("PRI".equals(tableInfo.getColumnKey())) { t = StringUtil.typeMapping(tableInfo.getDataType()); } } stringBuffer.append( "package " + package_.replaceAll("\\\\", ".") + "repository;\n" + "\n" + "import " + basePackage_.replaceAll("\\\\", ".") + "common.repository.*;\n" + "import " + package_.replaceAll("\\\\", ".") + "pojo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ";\n" + "import org.springframework.stereotype.Repository;\n" + "\n" + "@Repository\n" + "public interface " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Repository extends CommonRepository<" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ", " + t + "> {" ); stringBuffer.append("\n"); stringBuffer.append("}"); FileUtil.fileWriter(file, stringBuffer); } /** * 创建service类 */ private void createService(List<TableInfo> tableInfos) { File file = FileUtil.createFile(basePath + "service\\" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Service.java"); StringBuffer stringBuffer = new StringBuffer(); String t = "String"; //遍历属性 for (TableInfo tableInfo : tableInfos) { //主键 if ("PRI".equals(tableInfo.getColumnKey())) { t = StringUtil.typeMapping(tableInfo.getDataType()); } } stringBuffer.append( "package " + package_.replaceAll("\\\\", ".") + "service;\n" + "\n" + "import " + basePackage_.replaceAll("\\\\", ".") + "common.service.*;\n" + "import " + package_.replaceAll("\\\\", ".") + "pojo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ";\n" + "import " + package_.replaceAll("\\\\", ".") + "vo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo;\n" + "\n" + "public interface " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Service extends CommonService<" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo, " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ", " + t + "> {" ); stringBuffer.append("\n"); stringBuffer.append("}"); FileUtil.fileWriter(file, stringBuffer); //Impl File file1 = FileUtil.createFile(basePath + "service\\" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "ServiceImpl.java"); StringBuffer stringBuffer1 = new StringBuffer(); stringBuffer1.append( "package " + package_.replaceAll("\\\\", ".") + "service;\n" + "\n" + "import " + basePackage_.replaceAll("\\\\", ".") + "common.service.*;\n" + "import " + package_.replaceAll("\\\\", ".") + "pojo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ";\n" + "import " + package_.replaceAll("\\\\", ".") + "vo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo;\n" + "import " + package_.replaceAll("\\\\", ".") + "repository." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Repository;\n" + "import org.springframework.beans.factory.annotation.Autowired;\n" + "import org.springframework.stereotype.Service;\n" + "import org.springframework.transaction.annotation.Transactional;\n" + "import javax.persistence.EntityManager;\n" + "import javax.persistence.PersistenceContext;\n" + "\n" + "@Service\n" + "@Transactional\n" + "public class " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "ServiceImpl extends CommonServiceImpl<" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo, " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ", " + t + "> implements " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Service{" ); stringBuffer1.append("\n\n"); stringBuffer1.append( " @PersistenceContext\n" + " private EntityManager em;\n"); stringBuffer1.append("" + " @Autowired\n" + " private " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Repository " + StringUtil.camelCaseName(tableName) + "Repository;\n"); stringBuffer1.append("}"); FileUtil.fileWriter(file1, stringBuffer1); } /** * 创建controller类 */ private void createController(List<TableInfo> tableInfos) { File file = FileUtil.createFile(basePath + "controller\\" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Controller.java"); StringBuffer stringBuffer = new StringBuffer(); String t = "String"; //遍历属性 for (TableInfo tableInfo : tableInfos) { //主键 if ("PRI".equals(tableInfo.getColumnKey())) { t = StringUtil.typeMapping(tableInfo.getDataType()); } } stringBuffer.append( "package " + package_.replaceAll("\\\\", ".") + "controller;\n" + "\n" + "import " + basePackage_.replaceAll("\\\\", ".") + "common.controller.*;\n" + "import " + package_.replaceAll("\\\\", ".") + "pojo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ";\n" + "import " + package_.replaceAll("\\\\", ".") + "vo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo;\n" + "import " + package_.replaceAll("\\\\", ".") + "service." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Service;\n" + "import org.springframework.beans.factory.annotation.Autowired;\n" + "import org.springframework.web.bind.annotation.*;\n" + "\n" + "@RestController\n" + "@RequestMapping(\"/" + StringUtil.camelCaseName(tableName) + "/\")\n" + "public class " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Controller extends CommonController<" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo, " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ", " + t + "> {" ); stringBuffer.append("\n"); stringBuffer.append("" + " @Autowired\n" + " private " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Service " + StringUtil.camelCaseName(tableName) + "Service;\n"); stringBuffer.append("}"); FileUtil.fileWriter(file, stringBuffer); } /** * 获取表结构信息 * * @return list */ private List<TableInfo> getTableInfo() { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; ArrayList<TableInfo> list = new ArrayList<>(); try { conn = DBConnectionUtil.getConnection(); String sql = "select column_name,data_type,column_comment,column_key,extra from information_schema.columns where table_name=?"; ps = conn.prepareStatement(sql); ps.setString(1, tableName); rs = ps.executeQuery(); while (rs.next()) { TableInfo tableInfo = new TableInfo(); //列名,全部转为小写 tableInfo.setColumnName(rs.getString("column_name").toLowerCase()); //列类型 tableInfo.setDataType(rs.getString("data_type")); //列注释 tableInfo.setColumnComment(rs.getString("column_comment")); //主键 tableInfo.setColumnKey(rs.getString("column_key")); //主键类型 tableInfo.setExtra(rs.getString("extra")); list.add(tableInfo); } } catch (SQLException e) { e.printStackTrace(); } finally { assert rs != null; DBConnectionUtil.close(conn, ps, rs); } return list; } /** * file工具类 */ private static class FileUtil { /** * 创建文件 * * @param pathNameAndFileName 路径跟文件名 * @return File对象 */ private static File createFile(String pathNameAndFileName) { File file = new File(pathNameAndFileName); try { //获取父目录 File fileParent = file.getParentFile(); if (!fileParent.exists()) { fileParent.mkdirs(); } //创建文件 if (!file.exists()) { file.createNewFile(); } } catch (Exception e) { file = null; System.err.println("新建文件操作出错"); e.printStackTrace(); } return file; } /** * 字符流写入文件 * * @param file file对象 * @param stringBuffer 要写入的数据 */ private static void fileWriter(File file, StringBuffer stringBuffer) { //字符流 try { FileWriter resultFile = new FileWriter(file, true);//true,则追加写入 false,则覆盖写入 PrintWriter myFile = new PrintWriter(resultFile); //写入 myFile.println(stringBuffer.toString()); myFile.close(); resultFile.close(); } catch (Exception e) { System.err.println("写入操作出错"); e.printStackTrace(); } } } /** * 字符串处理工具类 */ private static class StringUtil { /** * 数据库类型->JAVA类型 * * @param dbType 数据库类型 * @return JAVA类型 */ private static String typeMapping(String dbType) { String javaType = ""; if ("int|integer".contains(dbType)) { javaType = "Integer"; } else if ("float|double|decimal|real".contains(dbType)) { javaType = "Double"; } else if ("date|time|datetime|timestamp".contains(dbType)) { javaType = "Date"; } else { javaType = "String"; } return javaType; } /** * 驼峰转换为下划线 */ public static String underscoreName(String camelCaseName) { StringBuilder result = new StringBuilder(); if (camelCaseName != null && camelCaseName.length() > 0) { result.append(camelCaseName.substring(0, 1).toLowerCase()); for (int i = 1; i < camelCaseName.length(); i++) { char ch = camelCaseName.charAt(i); if (Character.isUpperCase(ch)) { result.append("_"); result.append(Character.toLowerCase(ch)); } else { result.append(ch); } } } return result.toString(); } /** * 首字母大写 */ public static String captureName(String name) { char[] cs = name.toCharArray(); cs[0] -= 32; return String.valueOf(cs); } /** * 下划线转换为驼峰 */ public static String camelCaseName(String underscoreName) { StringBuilder result = new StringBuilder(); if (underscoreName != null && underscoreName.length() > 0) { boolean flag = false; for (int i = 0; i < underscoreName.length(); i++) { char ch = underscoreName.charAt(i); if ("_".charAt(0) == ch) { flag = true; } else { if (flag) { result.append(Character.toUpperCase(ch)); flag = false; } else { result.append(ch); } } } } return result.toString(); } } /** * JDBC连接数据库工具类 */ private static class DBConnectionUtil { { // 1、加载驱动 try { Class.forName(DRIVERCLASSNAME); } catch (ClassNotFoundException e) { e.printStackTrace(); } } /** * 返回一个Connection连接 * * @return */ public static Connection getConnection() { Connection conn = null; // 2、连接数据库 try { conn = DriverManager.getConnection(URL, USERNAME, PASSWORD); } catch (SQLException e) { e.printStackTrace(); } return conn; } /** * 关闭Connection,Statement连接 * * @param conn * @param stmt */ public static void close(Connection conn, Statement stmt) { try { conn.close(); stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } /** * 关闭Connection,Statement,ResultSet连接 * * @param conn * @param stmt * @param rs */ public static void close(Connection conn, Statement stmt, ResultSet rs) { try { close(conn, stmt); rs.close(); } catch (SQLException e) { e.printStackTrace(); } } } /** * 表结构行信息实体类 */ private class TableInfo { private String columnName; private String dataType; private String columnComment; private String columnKey; private String extra; TableInfo() { } String getColumnName() { return columnName; } void setColumnName(String columnName) { this.columnName = columnName; } String getDataType() { return dataType; } void setDataType(String dataType) { this.dataType = dataType; } String getColumnComment() { return columnComment; } void setColumnComment(String columnComment) { this.columnComment = columnComment; } String getColumnKey() { return columnKey; } void setColumnKey(String columnKey) { this.columnKey = columnKey; } String getExtra() { return extra; } void setExtra(String extra) { this.extra = extra; } } /** * 快速创建,供外部调用,调用之前先设置一下项目的基础路径 */ private String create() { List<TableInfo> tableInfo = getTableInfo(); createPojo(tableInfo); createVo(tableInfo); createRepository(tableInfo); createService(tableInfo); createController(tableInfo); return tableName + " 后台代码生成完毕!"; } public static void main(String[] args) { String[] tables = {"ims_user", "ims_friend", "ims_friend_message"}; for (int i = 0; i < tables.length; i++) { String msg = new CodeDOM(tables[i]).create(); System.out.println(msg); } } }
工程结构
工程是一个springboot项目,跟我们之前一样使用lombok、thymeleaf等
tip.js、tip.css放在static的js、css里面
我们在ims_user表生成的controller、service层新增登录、登出等几个接口
@RestController @RequestMapping("/imsUser/") public class ImsUserController extends CommonController<ImsUserVo, ImsUser, Integer> { @Autowired private ImsUserService imsUserService; /** * 跳转登录、注册页面 */ @RequestMapping("loginPage.html") public ModelAndView loginPage() { return new ModelAndView("login.html"); } /** * 跳转聊天页面 */ @RequestMapping("socketChart/{username}.html") public ModelAndView socketChartPage(@PathVariable String username) { return new ModelAndView("socketChart.html","username",username); } /** * 登录 */ @PostMapping("login") public Result<ImsUserVo> login(ImsUserVo userVo) { //加密后再去对比密文 userVo.setPassword(MD5Util.getMD5(userVo.getPassword())); Result<List<ImsUserVo>> result = list(userVo); if(result.isFlag() && result.getData().size() > 0){ ImsUserVo imsUserVo = result.getData().get(0); //置空隐私信息 imsUserVo.setPassword(null); //add WebSocketServer.loginList WebSocketServer.loginList.add(imsUserVo.getUserName()); return Result.of(imsUserVo); }else{ return Result.of(null,false,"账号或密码错误!"); } } /** * 登出 */ @RequestMapping("logout/{username}") public String loginOut(HttpServletRequest request, @PathVariable String username) { new WebSocketServer().deleteUserByUsername(username); return "退出成功!"; } /** * 获取在线用户 */ @PostMapping("getOnlineList") private List<String> getOnlineList(String username) { List<String> list = new ArrayList<String>(); //遍历webSocketMap for (Map.Entry<String, Session> entry : WebSocketServer.getSessionMap().entrySet()) { if (!entry.getKey().equals(username)) { list.add(entry.getKey()); } } return list; } }
@Service @Transactional public class ImsUserServiceImpl extends CommonServiceImpl<ImsUserVo, ImsUser, Integer> implements ImsUserService{ @PersistenceContext private EntityManager em; @Autowired private ImsUserRepository imsUserRepository; @Override public Result<ImsUserVo> save(ImsUserVo entityVo) { //先查询是否已经存在相同账号 ImsUserVo imsUserVo = new ImsUserVo(); imsUserVo.setUserName(entityVo.getUserName()); if(list(imsUserVo).getData().size() > 0){ return Result.of(null,false,"账号已存在!"); } //存储密文 entityVo.setPassword(MD5Util.getMD5(entityVo.getPassword())); return super.save(entityVo); } }
WebSocketServer也有优化调整
package cn.huanzi.ims.socket; import cn.huanzi.ims.imsuser.service.ImsUserService; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; /** * WebSocket服务 */ @RestController @RequestMapping("/websocket") @ServerEndpoint(value = "/websocket/{username}", configurator = MyEndpointConfigure.class) public class WebSocketServer { /** * 在线人数 */ private static int onlineCount = 0; /** * 在线用户的Map集合,key:用户名,value:Session对象 */ private static Map<String, Session> sessionMap = new HashMap<String, Session>(); /** * 登录用户集合 */ public static List<String> loginList = new ArrayList<>(); public static Map<String, Session> getSessionMap(){ return sessionMap; } /** * 注入其他类(换成自己想注入的对象) */ private ImsUserService imsUserService; /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(Session session, @PathParam("username") String username) { //在webSocketMap新增上线用户 sessionMap.put(username, session); //在线人数加加 WebSocketServer.onlineCount++; //通知除了自己之外的所有人 sendOnlineCount(username, "{'type':'onlineCount','onlineCount':" + WebSocketServer.onlineCount + ",username:'" + username + "'}"); } /** * 连接关闭调用的方法 */ @OnClose public void onClose(Session session) { //下线用户名 String logoutUserName = ""; //从webSocketMap删除下线用户 for (Entry<String, Session> entry : sessionMap.entrySet()) { if (entry.getValue() == session) { sessionMap.remove(entry.getKey()); logoutUserName = entry.getKey(); break; } } deleteUserByUsername(logoutUserName); } /** * 服务器接收到客户端消息时调用的方法 */ @OnMessage public void onMessage(String message, Session session) { try { //JSON字符串转 HashMap HashMap hashMap = new ObjectMapper().readValue(message, HashMap.class); //消息类型 String type = (String) hashMap.get("type"); //来源用户 Map srcUser = (Map) hashMap.get("srcUser"); //目标用户 Map tarUser = (Map) hashMap.get("tarUser"); //如果点击的是自己,那就是群聊 if (srcUser.get("username").equals(tarUser.get("username"))) { //群聊 groupChat(session,hashMap); } else { //私聊 privateChat(session, tarUser, hashMap); } //后期要做消息持久化 } catch (IOException e) { e.printStackTrace(); } } /** * 发生错误时调用 */ @OnError public void onError(Session session, Throwable error) { error.printStackTrace(); } /** * 通知除了自己之外的所有人 */ private void sendOnlineCount(String username, String message) { for (Entry<String, Session> entry : sessionMap.entrySet()) { try { if (entry.getKey() != username) { entry.getValue().getBasicRemote().sendText(message); } } catch (IOException e) { e.printStackTrace(); } } } /** * 私聊 */ private void privateChat(Session session, Map tarUser, HashMap hashMap) throws IOException { //获取目标用户的session Session tarUserSession = sessionMap.get(tarUser.get("username")); //如果不在线则发送“对方不在线”回来源用户 if (tarUserSession == null) { session.getBasicRemote().sendText("{\"type\":\"0\",\"message\":\"对方不在线\"}"); } else { hashMap.put("type", "1"); tarUserSession.getBasicRemote().sendText(new ObjectMapper().writeValueAsString(hashMap)); } } /** * 群聊 */ private void groupChat(Session session, HashMap hashMap) throws IOException { for (Entry<String, Session> entry : sessionMap.entrySet()) { //自己就不用再发送消息了 if (entry.getValue() != session) { hashMap.put("type", "2"); entry.getValue().getBasicRemote().sendText(new ObjectMapper().writeValueAsString(hashMap)); } } } /** 删除用户 */ public void deleteUserByUsername(String username){ //在线人数减减 WebSocketServer.onlineCount--; WebSocketServer.loginList.remove(username); //通知除了自己之外的所有人 sendOnlineCount(username, "{'type':'onlineCount','onlineCount':" + WebSocketServer.onlineCount + ",username:'" + username + "'}"); } }
先看一下我们的自定义web弹窗的js、css,跟之前相比有一点小升级
/* web弹窗 */ .tip-msg { background-color: rgba(61, 61, 61, 0.93); color: #ffffff; opacity: 0; max-width: 200px; position: fixed; text-align: center; line-height: 25px; border-radius: 30px; padding: 5px 15px; display: inline-block; z-index: 10000; } .tip-shade { z-index: 9999; background-color: rgb(0, 0, 0); opacity: 0.6; position: fixed; top: 0; left: 0; width: 100%; height: 100%; } .tip-dialog { z-index: 9999; position: fixed; display: block; background: #e9e9e9; border-radius: 5px; opacity: 0; border: 1px solid #dad8d8; box-shadow: 0px 1px 20px 2px rgb(255, 221, 221); } .tip-title { cursor: move; padding: 5px; position: relative; height: 25px; border-bottom: 1px solid #dad8d8; user-select: none; } .tip-title-text { margin: 0; padding: 0; font-size: 15px; } .tip-title-btn { position: absolute; top: 5px; right: 5px; } .tip-content { padding: 8px; position: relative; word-break: break-all; font-size: 14px; overflow-x: hidden; overflow-y: auto; } .tip-resize { position: absolute; width: 15px; height: 15px; right: 0; bottom: 0; cursor: se-resize; }
/** * 自定义web弹窗/层:简易风格的msg与可拖放的dialog * 依赖jquery */ var tip = { /** * 初始化 */ init: function () { var titleDiv = null;//标题元素 var dialogDiv = null;//窗口元素 var titleDown = false;//是否在标题元素按下鼠标 var resizeDown = false;//是否在缩放元素按下鼠标 var offset = {x: 0, y: 0};//鼠标按下时的坐标系/计算后的坐标 /* 使用 on() 方法添加的事件处理程序适用于当前及未来的元素(比如由脚本创建的新元素)。 问题:事件绑定在div上出现div移动速度跟不上鼠标速度,导致鼠标移动太快时会脱离div,从而无法触发事件。 解决:把事件绑定在document文档上,无论鼠标在怎么移动,始终是在文档范围之内。 */ //鼠标在标题元素按下 $(document).on("mousedown", ".tip-title", function (e) { var event1 = e || window.event; titleDiv = $(this); dialogDiv = titleDiv.parent(); titleDown = true; offset.x = e.clientX - parseFloat(dialogDiv.css("left")); offset.y = e.clientY - parseFloat(dialogDiv.css("top")); }); //鼠标移动 $(document).on("mousemove", function (e) { var event2 = e || window.event; var eveX = event2.clientX; // 获取鼠标相对于浏览器x轴的位置 var eveY = event2.clientY; // 获取鼠标相对于浏览器Y轴的位置 // var height = document.body.clientHeight;//表示HTML文档所在窗口的当前高度; // var width = document.body.clientWidth;//表示HTML文档所在窗口的当前宽度; var height = window.innerHeight;//浏览器窗口的内部高度; var width = window.innerWidth;//浏览器窗口的内部宽度; //在标题元素按下 if (titleDown) { //处理滚动条 if (tip.hasXScrollbar()) { height = height - tip.getScrollbarWidth(); } if (tip.hasYScrollbar()) { width = width - tip.getScrollbarWidth(); } //上边 var top = (eveY - offset.y); if (top <= 0) { top = 0; } if (top >= (height - dialogDiv.height())) { top = height - dialogDiv.height() - 5; } //左边 var left = (eveX - offset.x); if (left <= 0) { left = 0; } if (left >= (width - dialogDiv.width())) { left = width - dialogDiv.width() - 5; } dialogDiv.css({ "top": top + "px", "left": left + "px" }); } //在缩放元素按下 if (resizeDown) { var newWidth = (dialogDiv.resize.width + (eveX - offset.x)); if (dialogDiv.resize.initWidth >= newWidth) { newWidth = dialogDiv.resize.initWidth; } var newHeight = (dialogDiv.resize.height + (eveY - offset.y)); if (dialogDiv.resize.initHeight >= newHeight) { newHeight = dialogDiv.resize.initHeight; } dialogDiv.css("width", newWidth + "px"); dialogDiv.find(".tip-content").css("height", newHeight + "px"); } }); //鼠标弹起 $(document).on("mouseup", function (e) { //清空对象 titleDown = false; resizeDown = false; titleDiv = null; dialogDiv = null; offset = {x: 0, y: 0}; }); //阻止按钮事件冒泡 $(document).on("mousedown", ".tip-title-min,.tip-title-max,.tip-title-close", function (e) { e.stopPropagation();//阻止事件冒泡 }); //最小化 $(document).on("click", ".tip-title-min", function (e) { // var height = document.body.clientHeight;//表示HTML文档所在窗口的当前高度; // var width = document.body.clientWidth;//表示HTML文档所在窗口的当前宽度; var height = window.innerHeight;//浏览器窗口的内部高度; var width = window.innerWidth;//浏览器窗口的内部宽度; var $parent = $(this).parents(".tip-dialog"); //显示浏览器滚动条 document.body.parentNode.style.overflowY = "auto"; //当前是否为最大化 if ($parent[0].isMax) { $parent[0].isMax = false; $parent.css({ "top": $parent[0].topMin, "left": $parent[0].leftMin, "height": $parent[0].heightMin, "width": $parent[0].widthMin }); } //当前是否为最小化 if (!$parent[0].isMin) { $parent[0].isMin = true; $parent[0].bottomMin = $parent.css("bottom"); $parent[0].leftMin = $parent.css("left"); $parent[0].heightMin = $parent.css("height"); $parent[0].widthMin = $parent.css("width"); $parent.css({ "top": "", "bottom": "5px", "left": 0, "height": "30px", "width": "95px" }); $parent.find(".tip-title-text").css("display", "none"); $parent.find(".tip-content").css("display", "none"); } else { $parent[0].isMin = false; $parent.css({ "top": $parent[0].topMin, "bottom": $parent[0].bottomMin, "left": $parent[0].leftMin, "height": $parent[0].heightMin, "width": $parent[0].widthMin }); $parent.find(".tip-title-text").css("display", "block"); $parent.find(".tip-content").css("display", "block"); } }); //最大化 $(document).on("click", ".tip-title-max", function (e) { // var height = document.body.clientHeight;//表示HTML文档所在窗口的当前高度; // var width = document.body.clientWidth;//表示HTML文档所在窗口的当前宽度; var height = window.innerHeight;//浏览器窗口的内部高度; var width = window.innerWidth;//浏览器窗口的内部宽度; var $parent = $(this).parents(".tip-dialog"); //当前是否为最小化 if ($parent[0].isMin) { $parent[0].isMin = false; $parent.css({ "top": $parent[0].topMin, "bottom": $parent[0].bottomMin, "left": $parent[0].leftMin, "height": $parent[0].heightMin, "width": $parent[0].widthMin }); $parent.find(".tip-title h2").css("display", "block"); } //当前是否为最大化 if (!$parent[0].isMax) { //隐藏浏览器滚动条 document.body.parentNode.style.overflowY = "hidden"; $parent[0].isMax = true; $parent[0].topMin = $parent.css("top"); $parent[0].leftMin = $parent.css("left"); $parent[0].heightMin = $parent.css("height"); $parent[0].widthMin = $parent.css("width"); $parent.css({ "top": 0, "left": 0, "height": height - 5 + "px", "width": width - 5 + "px" }); } else { //显示浏览器滚动条 document.body.parentNode.style.overflowY = "auto"; $parent[0].isMax = false; $parent.css({ "top": $parent[0].topMin, "left": $parent[0].leftMin, "height": $parent[0].heightMin, "width": $parent[0].widthMin }); } }); //缩放 $(document).on("mousedown", ".tip-resize", function (e) { var event1 = e || window.event; dialogDiv = $(this).parent(); resizeDown = true; offset.x = e.clientX; offset.y = e.clientY; //点击时的宽高 dialogDiv.resize.width = dialogDiv.width(); dialogDiv.resize.height = dialogDiv.find(".tip-content").height(); }); //关闭 $(document).on("click", ".tip-title-close", function (e) { $(this).parents(".tip-dialog").parent().remove(); //显示浏览器滚动条 document.body.parentNode.style.overflowY = "auto"; }); //点击窗口优先显示 $(document).on("click", ".tip-dialog", function (e) { $(".tip-dialog").css("z-index","9999"); $(this).css("z-index","10000"); }); }, /** * 是否存在X轴方向滚动条 */ hasXScrollbar: function () { return document.body.scrollWidth > (window.innerWidth || document.documentElement.clientWidth); }, /** * 是否存在Y轴方向滚动条 */ hasYScrollbar: function () { return document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight); }, /** * 计算滚动条的宽度 */ getScrollbarWidth: function () { /* 思路:生成一个带滚动条的div,分析得到滚动条长度,然后过河拆桥 */ var scrollDiv = document.createElement("div"); scrollDiv.style.cssText = 'width: 99px; height: 99px; overflow: scroll; position: absolute; top: -9999px;'; document.body.appendChild(scrollDiv); var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth; document.body.removeChild(scrollDiv); return scrollbarWidth; }, /** * tip提示 * tip.msg("哈哈哈哈哈"); * tip.msg({text:"哈哈哈哈哈",time:5000}); */ msg: function (setting) { var time = setting.time || 2000; // 显示时间(毫秒) 默认延迟2秒关闭 var text = setting.text || setting; // 文本内容 //组装HTML var tip = "<div class='tip tip-msg'>" + text + "</div>"; //删除旧tip $(".tip-msg").remove(); //添加到body $("body").append(tip); //获取jq对象 var $tip = $(".tip-msg"); //动画过渡 $tip.animate({opacity: 1}, 500); //计算位置浏览器窗口上下、左右居中 // var height = document.body.clientHeight;//表示HTML文档所在窗口的当前高度; var width = document.body.clientWidth;//表示HTML文档所在窗口的当前宽度; var height = window.innerHeight;//浏览器窗口的内部高度; // var width = window.innerWidth;//浏览器窗口的内部宽度; width = ((width / 2) - ($tip.css("width").replace("px", "") / 2)) / width; height = ((height / 2) - ($tip.css("height").replace("px", "") / 2)) / height; $tip.css({ "top": (height * 100) + "%", "left": (width * 100) + "%" }); //延迟删除 setTimeout(function () { //动画过渡 $tip.animate({opacity: 0}, 500, function () { $tip.remove(); }); }, time); }, /** * 可拖放窗口 * tip.dialog({title:"测试弹窗标题",content:"测试弹窗内容"}); * tip.dialog({title:"测试弹窗标题",class:"myClassName",content:"<h1>测试弹窗内容</h1>",offset: ['100px', '50px'],area:["200px","100px"],shade:0,closeCallBack:function(){console.log('你点击了关闭按钮')}}); */ dialog: function (setting) { var title = setting.title || "这里是标题"; // 标题 var clazz = setting.class || ""; // class var content = setting.content || "这里是内容"; // 内容 var area = setting.area; // 宽高 var offset = setting.offset || "auto"; // 位置 上、左 var shade = setting.shade !== undefined ? setting.shade : 0.7;//遮阴 为0时无遮阴对象 //组装HTML var tip = "<div>\n" + " <!-- 遮阴层 -->\n" + " <div class=\"tip tip-shade\"></div>\n" + " <!-- 主体 -->\n" + " <div class=\"tip tip-dialog " + clazz + "\">\n" + " <!-- 标题 -->\n" + " <div class=\"tip tip-title\">\n" + " <h2 class=\"tip tip-title-text\"></h2>\n" + " <div class=\"tip tip-title-btn\">\n" + " <button class=\"tip tip-title-min\" title=\"最小化\">--</button>\n" + " <button class=\"tip tip-title-max\" title=\"最大化\">O</button>\n" + " <button class=\"tip tip-title-close\" title=\"关闭\">X</button>\n" + " </div>\n" + " </div>\n" + " <!-- 窗口内容 -->\n" + " <div class=\"tip tip-content\"></div>\n" + " <!-- 右下角改变窗口大小 -->\n" + " <div class=\"tip tip-resize\"></div>\n" + " </div>\n" + "</div>"; var $tip = $(tip); //添加到body $("body").append($tip); //设置遮阴 $tip.find(".tip-shade").css("opacity", shade); if (shade === 0) { $tip.find(".tip-shade").css({ "width": "0", "height": "0" }); } //获取dialog对象 $tip = $tip.find(".tip-dialog"); //标题 $tip.find(".tip-title-text").html(title); //内容 $tip.find(".tip-content").append(content); //设置初始宽高 if (area) { $tip.css({ "width": area[0], }); $tip.find(".tip-content").css({ "height": area[1] }); } //动画过渡 $tip.animate({opacity: 1}, 500); //计算位置浏览器窗口上下、左右居中 if (offset === "auto") { // var height = document.body.clientHeight;//表示HTML文档所在窗口的当前高度; var width = document.body.clientWidth;//表示HTML文档所在窗口的当前宽度; var height = window.innerHeight;//浏览器窗口的内部高度; // var width = window.innerWidth;//浏览器窗口的内部宽度; width = ((width / 2) - ($tip.css("width").replace("px", "") / 2)) / width; height = ((height / 2) - ($tip.css("height").replace("px", "") / 2)) / height; $tip.css({ "top": (height * 100) + "%", "left": (width * 100) + "%" }); } else if (Array.isArray(offset)) { $tip.css({ "top": offset[0], "left": offset[1] }); } //初始值宽高 $tip.resize.initWidth = $tip.width(); $tip.resize.initHeight = $tip.find(".tip-content").height(); //绑定关闭回调 if(setting.closeCallBack){ $(".tip-title-close").click(function (e) { setting.closeCallBack(); }); } } }; //初始化 tip.init();
接下来就是登录/注册页面跟聊天页面的HTML、JS的修改,我们先定义一个head.html作为一个公用head在其他地方引入
<!--此页面用于放置页面的公共片段(fragment)--> <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head th:fragment="static"> <head> <script th:inline="javascript"> //项目根路径 // ctx = /*[[@{/}]]*/''; ctx = [[${#request.getContextPath()}]];//应用路径 </script> </head> <head> <!-- jquery --> <script th:src="@{/js/jquery.min.js}"></script> <!-- 自定义web弹窗 CSS、JS 文件 --> <link th:href="@{/css/tip.css}" rel="stylesheet" type="text/css"/> <script th:src="@{/js/tip.js}"></script> </head> <head> <style> body, html { margin: 0; padding: 0; background: #898f92; } </style> </head> <head> <script> /** * 拓展表单对象:用于将对象序列化为JSON对象 */ $.fn.serializeObject = function () { var o = {}; var a = this.serializeArray(); $.each(a, function () { if (o[this.name]) { if (!o[this.name].push) { o[this.name] = [o[this.name]]; } o[this.name].push(this.value || ''); } else { o[this.name] = this.value || ''; } }); return o; }; /** * 表单自动回显 * 依赖jqury * 使用参考:$("#form1").form({"id":"112","username":"ff","password":"111","type":"admin"}); */ $.fn.form = function (data) { var form = $(this); for (var i in data) { var name = i; var value = data[i]; if (name !== "" && value !== "") { valuAtion(name, value); } } function valuAtion(name, value) { if (form.length < 1) { return; } if (form.find("[name='" + name + "']").length < 1) { return; } var input = form.find("[name='" + name + "']")[0]; if ($.inArray(input.type, ["text", "password", "hidden", "select-one", "textarea"]) > -1) { $(input).val(value); } else if (input.type == " " || input.type == "checkbox") { form.find("[name='" + name + "'][value='" + value + "']").attr("checked", true); } } }; /** * 常用工具方法 */ commonUtil = { /** * 获取当前时间,并格式化输出为:2018-05-18 14:21:46 * @returns {string} */ getNowTime: function () { var time = new Date(); var year = time.getFullYear();//获取年 var month = time.getMonth() + 1;//或者月 var day = time.getDate();//或者天 var hour = time.getHours();//获取小时 var minu = time.getMinutes();//获取分钟 var second = time.getSeconds();//或者秒 var data = year + "-"; if (month < 10) { data += "0"; } data += month + "-"; if (day < 10) { data += "0" } data += day + " "; if (hour < 10) { data += "0" } data += hour + ":"; if (minu < 10) { data += "0" } data += minu + ":"; if (second < 10) { data += "0" } data += second; return data; } } </script> </head> </html>
<!DOCTYPE > <html xmlns:th="http://www.thymeleaf.org"> <head> <title>登录/注册</title> <!-- 引入公用部分 --> <script th:replace="head::static"></script> <!-- login CSS、JS 文件 --> <link th:href="@{/css/login.css}" rel="stylesheet" type="text/css"/> </head> <body> <script th:inline="javascript"> </script> <!-- login CSS、JS 文件 --> <script th:src="@{/js/login.js}"></script> </body> </html>
h3 { margin: 0 0 5px 0; text-align: center; } .login { width: 250px; background: #e9e9e9; border-radius: 5px; margin: 0 auto; border: 1px solid #e9e9e9; padding: 10px; } .login > form { margin: 0; } .login:focus-within { border: 1px solid #10b7f3; background: #caefff; } .register { width: 350px; background: #e9e9e9; border-radius: 5px; margin: 0 auto; border: 1px solid #e9e9e9; padding: 10px; display: none; } .register > form { margin: 0; } .register > table,.login > table { margin: 0 auto; } .register:focus-within { border: 1px solid #10b7f3; background: #caefff; }
let $login = "<div class=\"login\">\n" + " <h3>登录</h3>\n" + " <form id=\"loginForm\">\n" + " <table>\n" + " <tr>\n" + " <td>账号:</td>\n" + " <td><input type=\"text\" name=\"userName\"/></td>\n" + " </tr>\n" + " <tr>\n" + " <td>密码:</td>\n" + " <td><input type=\"text\" name=\"password\"/></td>\n" + " </tr>\n" + " <tr>\n" + " <td><a href=\"#\" onclick=\"switchover()\">注册</a></td>\n" + " <td colspan=\"2\"><a href=\"#\" onclick=\"login()\">登录</a></td>\n" + " </tr>\n" + " </table>\n" + " </form>\n" + "</div>"; let $register = "<!-- 注册 -->\n" + "<div class=\"register\">\n" + " <h3>注册</h3>\n" + "\n" + " <form id=\"registerForm\">\n" + " <table>\n" + " <tr>\n" + " <td>账号:</td>\n" + " <td><label for=\"userName\"></label><input type=\"text\" id=\"userName\" name=\"userName\"/></td>\n" + " </tr>\n" + " <tr>\n" + " <td>密码:</td>\n" + " <td><input type=\"text\" id=\"password\" name=\"password\"/></td>\n" + " </tr>\n" + " <tr>\n" + " <td>昵称:</td>\n" + " <td><input type=\"text\" id=\"nickName\" name=\"nickName\"/></td>\n" + " </tr>\n" + " <tr>\n" + " <td>性别:</td>\n" + " <td>\n" + " 男<input type=\"radio\" name=\"gender\" value=\"1\" checked/>\n" + " 女<input type=\"radio\" name=\"gender\" value=\"0\"/>\n" + " </td>\n" + " </tr>\n" + " <tr>\n" + " <td>头像:</td>\n" + " <td><input type=\"text\" id=\"avatar\" name=\"avatar\"/></td>\n" + " </tr>\n" + " <tr>\n" + " <td>电子邮箱:</td>\n" + " <td><input type=\"email\" id=\"email\" name=\"email\"/></td>\n" + " </tr>\n" + " <tr>\n" + " <td>手机号码:</td>\n" + " <td><input type=\"text\" id=\"phone\" name=\"phone\"/></td>\n" + " </tr>\n" + " <tr>\n" + " <td>个性签名:</td>\n" + " <td><textarea id=\"sign\" name=\"sign\"></textarea></td>\n" + " </tr>\n" + " <!-- 两个隐藏时间 -->\n" + " <input type=\"hidden\" id=\"createdTime\" name=\"createdTime\"/>\n" + " <input type=\"hidden\" id=\"updataTime\" name=\"updataTime\"/>\n" + " <tr>\n" + " <td><a href=\"#\" onclick=\"switchover()\">登录</a></td>\n" + " <td colspan=\"2\"><a href=\"#\" onclick=\"register()\">注册</a></td>\n" + " </tr>\n" + " </table>\n" + " </form>\n" + "</div>"; tip.dialog({title: "登录/注册", content: $login + $register, shade: 0}); //切换登录、注册页面 function switchover() { if ($(".login").css("display") === "none") { $(".login").show(); $(".register").hide(); } else { $(".register").show(); $(".login").hide(); } } //提交注册 function register() { let newTime = commonUtil.getNowTime(); $("#createdTime").val(newTime); $("#updataTime").val(newTime); $("#avatar").val("/image/logo.png"); console.log($("#registerForm").serializeObject()); $.post(ctx + "/imsUser/save", $("#registerForm").serializeObject(), function (data) { if (data.flag) { switchover(); } // tip提示 tip.msg(data.msg); }); } //提交登录 function login() { console.log($("#loginForm").serializeObject()); $.post(ctx + "/imsUser/login", $("#loginForm").serializeObject(), function (data) { if (data.flag) { window.location.href = ctx + "/imsUser/socketChart/" + data.data.userName + ".html" } else { // tip提示 tip.msg(data.msg); } }); return false; }
<!DOCTYPE> <!--解决idea thymeleaf 表达式模板报红波浪线--> <!--suppress ALL --> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>聊天页面</title> <!-- 引入公用部分 --> <script th:replace="head::static"></script> <!-- socketChart CSS、JS 文件 --> <link th:href="@{/css/socketChart.css}" rel="stylesheet" type="text/css"/> </head> <body> </body> <script type="text/javascript" th:inline="javascript"> //登录名 var username = /*[[${username}]]*/''; </script> <!-- socketChart CSS、JS 文件 --> <script th:src="@{/js/socketChart.js}"></script> <script type="text/javascript"> //老的浏览器可能根本没有实现 mediaDevices,所以我们可以先设置一个空的对象 if (navigator.mediaDevices === undefined) { navigator.mediaDevices = {}; } //一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia //因为这样可能会覆盖已有的属性。这里我们只会在没有getUserMedia属性的时候添加它。 if (navigator.mediaDevices.getUserMedia === undefined) { navigator.mediaDevices.getUserMedia = function (constraints) { // 首先,如果有getUserMedia的话,就获得它 var getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; // 一些浏览器根本没实现它 - 那么就返回一个error到promise的reject来保持一个统一的接口 if (!getUserMedia) { return Promise.reject(new Error('getUserMedia is not implemented in this browser')); } // 否则,为老的navigator.getUserMedia方法包裹一个Promise return new Promise(function (resolve, reject) { getUserMedia.call(navigator, constraints, resolve, reject); }); } } var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; //开启视频 $(document).on("click", "#videoBut", function (event) { console.log("开始与" + $("#toUserName").text() + "视频聊天..."); var username = $("#toUserName").text(); // 创建PeerConnection实例 (参数为null则没有iceserver,即使没有stunserver和turnserver,仍可在局域网下通讯) var pc = new webkitRTCPeerConnection(null); // 发送ICE候选到其他客户端 pc.onicecandidate = function (event) { if (event.candidate !== null) { //转json字符串 websocket.send(JSON.stringify({ "event": "_ice_candidate", "data": { "candidate": event.candidate } })); websocket.send(JSON.stringify({ "type": "1", "tarUser": {"username": tarUserName}, "srcUser": {"username": srcUserName}, "message": message })); } }; }); </script> </html>
//追加tip页面 let hzGroup = "" + "<div id=\"hz-group\">\n" + " 在线人数:<span id=\"onlineCount\">0</span>\n" + " <!-- 主体 -->\n" + " <div id=\"hz-group-body\">\n" + "\n" + " </div>\n" + " </div>"; tip.dialog({title: "<span id=\"talks\">" + username + "</span>", content: hzGroup, offset: ["10%", "80%"], shade: 0, closeCallBack: function () { console.log("dfasdfasd") window.location.href = ctx + "/imsUser/logout/" + username; }}); //聊天页面 let hzMessage = "" + " <div id=\"hz-message\">\n" + " <!-- 主体 -->\n" + " <div id=\"hz-message-body\">\n" + " </div>\n" + " <!-- 功能条 -->\n" + " <div id=\"\">\n" + " <button>表情</button>\n" + " <button>图片</button>\n" + " <button id=\"videoBut\">视频</button>\n" + " <button onclick=\"send(this)\" style=\"float: right;\">发送</button>\n" + " </div>\n" + " <!-- 输入框 -->\n" + " <div contenteditable=\"true\" id=\"hz-message-input\">\n" + "\n" + " </div>\n" + " </div>"; //消息对象数组 var msgObjArr = []; //websocket对象 var websocket = null; //判断当前浏览器是否支持WebSocket if ('WebSocket' in window) { websocket = new WebSocket("ws://localhost:10086/websocket/" + username); } else { console.error("不支持WebSocket"); } //连接发生错误的回调方法 websocket.onerror = function (e) { console.error("WebSocket连接发生错误"); }; //连接成功建立的回调方法 websocket.onopen = function () { //获取所有在线用户 $.ajax({ type: 'post', url: ctx + "/imsUser/getOnlineList", contentType: 'application/json;charset=utf-8', dataType: 'json', data: {username: username}, success: function (data) { if (data.length) { //列表 for (let i = 0; i < data.length; i++) { var userName = data[i]; var text = "<div class=\"hz-group-list\"><img class='left' style='width: 23px;' src='https://avatars3.githubusercontent.com/u/31408183?s=40&v=4'/><span class='hz-group-list-username'>" + userName + "</span><span id=\"" + userName + "-status\" style='color: #497b0f;;'>[在线]</span><div id=\"hz-badge-" + userName + "\" class='hz-badge'>0</div></div>"; //把自己排在第一个 if (username === userName) { $("#hz-group-body").prepend(text); } else { $("#hz-group-body").append(text); } } //在线人数 $("#onlineCount").text(data.length); } }, error: function (xhr, status, error) { console.log("ajax错误!"); } }); }; //接收到消息的回调方法 websocket.onmessage = function (event) { var messageJson = eval("(" + event.data + ")"); //普通消息(私聊) if (messageJson.type === "1") { //来源用户 var srcUser = messageJson.srcUser; //目标用户 var tarUser = messageJson.tarUser; //消息 var message = messageJson.message; //最加聊天数据 setMessageInnerHTML(srcUser.username, srcUser.username, message); } //普通消息(群聊) if (messageJson.type === "2") { //来源用户 var srcUser = messageJson.srcUser; //目标用户 var tarUser = messageJson.tarUser; //消息 var message = messageJson.message; //最加聊天数据 setMessageInnerHTML(username, tarUser.username, message); } //对方不在线 if (messageJson.type === "0") { //消息 var message = messageJson.message; $("#hz-message-body").append( "<div class=\"hz-message-list\" style='text-align: center;'>" + "<div class=\"hz-message-list-text\">" + "<span>" + message + "</span>" + "</div>" + "</div>"); } //在线人数 if (messageJson.type === "onlineCount") { //取出username var onlineCount = messageJson.onlineCount; var userName = messageJson.username; var oldOnlineCount = $("#onlineCount").text(); //新旧在线人数对比 if (oldOnlineCount < onlineCount) { if ($("#" + userName + "-status").length > 0) { $("#" + userName + "-status").text("[在线]"); $("#" + userName + "-status").css("color", "#497b0f"); } else { $("#hz-group-body").append("<div class=\"hz-group-list\"><span class='hz-group-list-username'>" + userName + "</span><span id=\"" + userName + "-status\" style='color: #497b0f;'>[在线]</span><div id=\"hz-badge-" + userName + "\" class='hz-badge'>0</div></div>"); } } else { //有人下线 $("#" + userName + "-status").text("[离线]"); $("#" + userName + "-status").css("color", "#9c0c0c"); } $("#onlineCount").text(onlineCount); } }; //连接关闭的回调方法 websocket.onclose = function () { //alert("WebSocket连接关闭"); }; //将消息显示在对应聊天窗口 对于接收消息来说这里的toUserName就是来源用户,对于发送来说则相反 function setMessageInnerHTML(srcUserName, msgUserName, message) { //判断 var childrens = $("#hz-group-body").children(".hz-group-list"); var isExist = false; for (var i = 0; i < childrens.length; i++) { var text = $(childrens[i]).find(".hz-group-list-username").text(); if (text == srcUserName) { isExist = true; break; } } if (!isExist) { //追加聊天对象 msgObjArr.push({ toUserName: srcUserName, message: [{username: msgUserName, message: message, date: nowTime()}]//封装数据 }); $("#hz-group-body").append("<div class=\"hz-group-list\"><span class='hz-group-list-username'>" + srcUserName + "</span><span id=\"" + srcUserName + "-status\">[在线]</span><div id=\"hz-badge-" + srcUserName + "\" class='hz-badge'>0</div></div>"); } else { //取出对象 var isExist = false; for (var i = 0; i < msgObjArr.length; i++) { var obj = msgObjArr[i]; if (obj.toUserName == srcUserName) { //保存最新数据 obj.message.push({username: msgUserName, message: message, date: nowTime()}); isExist = true; break; } } if (!isExist) { //追加聊天对象 msgObjArr.push({ toUserName: srcUserName, message: [{username: msgUserName, message: message, date: nowTime()}]//封装数据 }); } } //刚好有打开的是对应的聊天页面 if ($(".tip" + srcUserName).length > 0) { $(".tip" + srcUserName + " #hz-message-body").append( "<div class=\"hz-message-list\">" + "<p class='hz-message-list-username'>" + msgUserName + "</p>" + "<img class='left' style='width: 23px;margin: 0 5px 0 0;' src='https://avatars3.githubusercontent.com/u/31408183?s=40&v=4'/>" + "<div class=\"hz-message-list-text left\">" + "<span>" + message + "</span>" + "</div>" + "<div style=\" clear: both; \"></div>" + "</div>"); } else { //小圆点++ var conut = $("#hz-badge-" + srcUserName).text(); $("#hz-badge-" + srcUserName).text(parseInt(conut) + 1); $("#hz-badge-" + srcUserName).css("opacity", "1"); } } //发送消息 function send(but) { //目标用户名 var tarUserName = $(but).parents(".tip-dialog").find("#toUserName").text(); //登录用户名 var srcUserName = $("#talks").text(); //消息 var message = $(but).parents(".tip-dialog").find("#hz-message-input").html(); websocket.send(JSON.stringify({ "type": "1", "tarUser": {"username": tarUserName}, "srcUser": {"username": srcUserName}, "message": message })); $(".tip" + tarUserName + " #hz-message-body").append( "<div class=\"hz-message-list\">" + "<img class='right' style='width: 23px;margin: 0 0 0 5px;' src='https://avatars3.githubusercontent.com/u/31408183?s=40&v=4'/>" + "<div class=\"hz-message-list-text right\">" + "<span>" + message + "</span>" + "</div>" + "</div>"); $(".tip" + tarUserName + " #hz-message-input").html(""); //取出对象 if (msgObjArr.length > 0) { var isExist = false; for (var i = 0; i < msgObjArr.length; i++) { var obj = msgObjArr[i]; if (obj.toUserName == tarUserName) { //保存最新数据 obj.message.push({username: srcUserName, message: message, date: nowTime()}); isExist = true; break; } } if (!isExist) { //追加聊天对象 msgObjArr.push({ toUserName: tarUserName, message: [{username: srcUserName, message: message, date: nowTime()}]//封装数据[{username:huanzi,message:"你好,我是欢子!",date:2018-04-29 22:48:00}] }); } } else { //追加聊天对象 msgObjArr.push({ toUserName: tarUserName, message: [{username: srcUserName, message: message, date: nowTime()}]//封装数据[{username:huanzi,message:"你好,我是欢子!",date:2018-04-29 22:48:00}] }); } } //监听点击用户 $("body").on("click", ".hz-group-list", function () { var toUserName = $(this).find(".hz-group-list-username").text(); //弹出聊天页面 tip.dialog({ title: "正在与 <span id=\"toUserName\"></span> 聊天", class: "tip" + toUserName, content: hzMessage, shade: 0 }); // $(".hz-group-list").css("background-color", ""); // $(this).css("background-color", "whitesmoke"); $(".tip" + toUserName + " #toUserName").text(toUserName); //清空小圆点 $("#hz-badge-" + toUserName).text("0"); $("#hz-badge-" + toUserName).css("opacity", "0"); if (msgObjArr.length > 0) { for (var i = 0; i < msgObjArr.length; i++) { var obj = msgObjArr[i]; if (obj.toUserName === toUserName) { //追加数据 var messageArr = obj.message; if (messageArr.length > 0) { for (var j = 0; j < messageArr.length; j++) { var msgObj = messageArr[j]; var leftOrRight = "right"; var message = msgObj.message; var msgUserName = msgObj.username; //当聊天窗口与msgUserName的人相同,文字在左边(对方/其他人),否则在右边(自己) if (msgUserName === toUserName) { leftOrRight = "left"; } //但是如果点击的是自己,群聊的逻辑就不太一样了 if (username === toUserName && msgUserName !== toUserName) { leftOrRight = "left"; } if (username === toUserName && msgUserName === toUserName) { leftOrRight = "right"; } var magUserName = leftOrRight === "left" ? "<p class='hz-message-list-username'>" + msgUserName + "</p>" : ""; $(".tip" + toUserName + " #hz-message-body").append( "<div class=\"hz-message-list\">" + magUserName + "<img class='" + leftOrRight + "' style='width: 23px;margin: 0 5px 0 0;' src='https://avatars3.githubusercontent.com/u/31408183?s=40&v=4'/>" + "<div class=\"hz-message-list-text " + leftOrRight + "\">" + "<span>" + message + "</span>" + "</div>" + "<div style=\" clear: both; \"></div>" + "</div>"); } } break; } } } }); //获取当前时间 function nowTime() { var time = new Date(); var year = time.getFullYear();//获取年 var month = time.getMonth() + 1;//或者月 var day = time.getDate();//或者天 var hour = time.getHours();//获取小时 var minu = time.getMinutes();//获取分钟 var second = time.getSeconds();//或者秒 var data = year + "-"; if (month < 10) { data += "0"; } data += month + "-"; if (day < 10) { data += "0" } data += day + " "; if (hour < 10) { data += "0" } data += hour + ":"; if (minu < 10) { data += "0" } data += minu + ":"; if (second < 10) { data += "0" } data += second; return data; }
#hz-main { width: 700px; height: 500px; background-color: red; margin: 0 auto; } #hz-message { width: 500px; float: left; background-color: #B5B5B5; } #hz-message-body { width: 460px; height: 340px; background-color: #E0C4DA; padding: 10px 20px; overflow: auto; } #hz-message-input { width: 500px; height: 99px; background-color: white; overflow: auto; } #hz-group { width: 200px; height: 500px; background-color: rosybrown; float: right; } .hz-message-list { min-height: 30px; margin: 10px 0; } .hz-message-list-text { padding: 7px 13px; border-radius: 15px; width: auto; max-width: 85%; display: inline-block; } .hz-message-list-username { margin: 0 0 0 25px; } .hz-group-body { overflow: auto; } .hz-group-list { padding: 10px; line-height: 23px; } .hz-group-list:hover{ background-color: whitesmoke; } .left { float: left; color: #595a5a; background-color: #ebebeb; } .right { float: right; color: #f7f8f8; background-color: #919292; } .hz-badge { width: 20px; height: 20px; background-color: #FF5722; border-radius: 50%; float: right; color: white; text-align: center; line-height: 20px; font-weight: bold; opacity: 0; }
演示效果
注册、登录、登出
登录三个账号,上下线提示功能
模拟私聊
模拟群聊(目前点击自己头像是群聊频道)
总结
第一版先实现到这里。第二版预计实现消息存储、离线推送,好友分组,好友搜索、添加,以及一些其他优化;欢迎大家指出不足之处!
版权声明
捐献、打赏
支付宝
微信