Websocket-科幻风聊天室

 前序

嗯...这个聊天室是我老早之前的一个项目(论坛)里的了

当时没用websocket,而是给每个聊天房(User-a和User-b)单独安排一个json文件来储存之间的信息

后面再做另一个项目(卡牌对战)时需要用到websocket,但这又有什么关系呢。主要还是我前几天闲着没事干,准备再更新些内容,毕竟是对战游戏,我觉得怎么也得有个好友之间相关的功能么

索性就爆改一下下,不过也是真累,中间遇到一堆问题。

预览见下:

正文

简述-Websocket

首先简述下websocket。websocket就是一种网络通信协议,其支持双工通信,能够实时且双向交互。其低延迟,高持久,加密通信保安全的特点

你可以这么理解:

HTTP:你如果要跟你的朋友聊天,那就需要去依次打电话

websocket:你与你的朋友都聚在一起,想聊啥就聊啥

环境

首先你需要安装 Node.js,在官网下载并安装适合你操作系统的版本 Node.js 官网 

然后创建一个新的项目文件夹,并初始化一个新Node.js项目

npm init

 然后使用 npm 安装 websocket :npm install ws

npm install ws

接着在项目文件夹中创建一系列相关的文件。这里我先说中心服务器文件-websocket.js

在websocket.js里,加入监听连接/消息以及关闭事件

先引入ws模块,接着我们需要去创建一个websocket服务器实例并让其监听8080端口 

定义一个集合以便存储其所有来连接的websocket客户端

我们需要为服务器新增一个事件监听器,监听connection事件。每当有新的客户端连接到服务器时,就会触发callback;将其连接添加进集合中

光这一个仍然不够,我们还需要添加监听器:

我们为WebSocket客户端添加一个事件监听器,监听message事件。当服务器接收到客户端发送的消息时,就会触发本事件

当收到消息时,我们遍历集合中所有客户端。相对每个客户端来说,检查它是否不是当前接收消息的客户端,同时确保其连接状态为打开

如果条件满足,将接收到的消息发给当前遍历到的客户端;因而,消息就会被广播给除了发送者之外的所有客户端

最后一步,为WebSocket客户端再添加一个事件监听器,监听close事件;当客户端关闭连接时,会触发本事件,然后从集合中删除此客户端

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
const clients = new Set();
wss.on('connection', function connection(ws) {
  clients.add(ws);
  ws.on('message', function incoming(message) {
    clients.forEach(client => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(message);
      }
    });
  });
  ws.on('close', () => {
    clients.delete(ws);
  });
});

聊天室

首先,我们构思下聊天室的样子(准确来说应该是私信或好友聊天)

如此,我们大概可以得出一份设计图

左栏好友列表,右栏iframe

那么这样该如何去写呢

我们先创建index.html和iframe.html文件

页面结构

我们思考,这样的一个页面应该有哪些元素?

根据设计图可以知道,左栏为好友(列表)右栏为两人聊天页面(iframe)

所以页面的布局就为两栏布局

接着再详细划分每一个元素,左栏涉及到好友的头像,以及好友的名称

右栏为iframe,一个大容器内分两个块级元素,块1内容为聊天记录部分,块2为发送消息部分;

聊天记录部分我们要管理其每一条消息,头像+气泡框(内内容);发送消息部分,左边一个‘图片’按钮,右边textarea+发送按钮

那么如何去管理好友列表呢?我们可以左栏添加一个监听右键事件,当右键时就弹出一个菜单

<div class="im-container">
    <div class="im-left-panel" id="contactsList">
    </div>
    <div class="im-right-panel">
        <iframe id="chat-frame" src="about:blank"></iframe>
    </div>
</div>
<body oncontextmenu="showMenu(event)">
<div class="contextmenu" id="context">
    <ul>
        <li><a href="javascript:about_sx();">必看</a></li>
        <li><a href="javascript:add_talking();">添加会话</a> </li>
        <li><a href="javascript:deltalking();">删除会话</a> </li>
        <li><a href="javascript:iframedy();">删除聊天记录</a> </li>
    </ul>

这里我设置了一个‘必看’菜单项,那么当其被点击时会发生什么呢?既然是‘必看’了,那肯定是通知或敬告之类的,所以我们就可以再创建一个模态框,用来放置通知或敬告信息。

一个模态框可以有什么?header:标题;body:内容;footer:按钮->callback

<div id="welcomeModal2" class="modal2">
  <div class="modal2-background"></div>
  <div class="modal2-content">
    <div class="modal2-header">
      <span class="close2">&times;</span>
      <h2>必看</h2>
    </div>
    <div class="modal2-body">
      <p>交友需谨慎,切莫透漏隐私信息</br>By.熙鹤</p>
    </div>
    <div class="modal2-footer">
      <button class="button-jump2" id="acceptButton2">朕已阅</button>
    </div>
  </div>
</div>

iframe.html 

<div id="welcomeModal" class="modal">
  <div class="modal-background"></div>
  <div class="modal-content">
    <div class="modal-header">
      <span class="close">&times;</span>
      <h2 style="color: white;">私信-必看</h2>
    </div>
    <div class="modal-body">
      <p style="color:wheat;">谨防诈骗,多留一个心眼</p>
    </div>
    <div class="modal-footer">
      <button class="button-jump" id="acceptButton">我同意</button>
    </div>
  </div>
</div>
<div class="container">
  <div class="chat" id="chat"></div>
  <div class="input-container">
  <div id="toolbar">
    <button id="insertImageLink" class="ql-image-link" title="插入图片">
        <i class="fas fa-image"></i>
    </button>
</div>
<textarea id="message"></textarea>
    <button id="send">发送</button>
  </div>
</div>

样式

一个好的样式能给用户带来更多的视觉感和冲击感

在这我将以科幻风而述

再次思考?科幻风...科幻风有哪些特征?

色彩上->冷色调、霓虹色调来渲染这种高科技感

光效上->渐变、阴影、反射制造立体感,增加视觉冲击力

字体上->未来字体

主题上->多用宇宙元素/暗色主题

动效上->滚动效果、hover、focus

左栏上,背景色我们可以采用暗色主题,以偏商务风,更沉稳也有气势感;其每一个好友的容器背景色我们则采用明灰色

右栏上,与暗色相对,使用霓虹色调,与暗黑进行鲜明对比;同时内部大容器添加发光效果(阴影)+圆角,块级元素同样圆角。气泡框圆角+巧克力色,button蔚蓝色,添加hover以及focus放大效果

IM-布局1:管理好友列表

大容器(.im-container

我们将其高度设置为100%

然后使用display: flex;创建flex布局,使得元素能灵活在纵向排列

背景色由前面内容可知,暗黑主题,所以我们可以采用#0d1117和#fff

左栏(侧栏).im-left-panel

为了保证每个好友(一行)头像+名称的横向占位不会超出左栏,所以我们找一个适合的宽值,比如:250px

但我们想一想我们总需要将左栏与右栏区分,所以我们可以加入一个border-right,来区分左右栏

为了保证高度能在好友过多时不溢出,所以我们允许其垂直滚动overflow-y: auto;

为了加强其层次感,我们再添加背景渐变:linear-gradient

右栏 (.im-right-panel)

为了保证其iframe占满剩下空间,我们使用flex-grow: 1;确保

我们使用radial-gradient,使其产生从中心向外辐射的视效

好友项 (.contact-item)

我们使用display: flex;align-items: center;来让布局水平、垂直皆居中

为了保证其每一个好友之间有距离,我们使用margin-bottompadding

为了增强互动性:我们使用cursorhover

最后我们使用transition来添加平滑过渡的一个效果

其它(other)

头像需要保持宽高比,同时限制其宽高

去除列表项默认标记li


.im-container {
    display: flex;
    height: 100vh;
    margin: 0;
    background: #0d1117; 
    color: #fff; 
    overflow: hidden; 
}


.im-left-panel {
    width: 250px;
    border-right: 1px solid #3e4c5a; 
    padding: 10px;
    overflow-y: auto;
    background: linear-gradient(45deg, #0d1117, #1e2327); 
}


.im-right-panel {
    flex-grow: 1;
    padding: 10px;
    overflow-y: auto;
    background: radial-gradient(circle, rgba(255,255,255,0.1), #0d1117); 
}


.contact-item {
    display: flex;
    align-items: center;
    margin-bottom: 10px;
    cursor: pointer;
    padding: 10px;
    border-radius: 10px; 
    background: rgba(255, 255, 255, 0.1); 
    transition: transform 0.2s, box-shadow 0.2s; 
}

.contact-item:hover {
    transform: translateY(-5px); 
    box-shadow: 0 10px 20px rgba(255, 255, 255, 0.2); 
}

.avatar {
    width: 40px;
    height: 40px;
    border-radius: 20px;
    margin-right: 10px;
    background-size: cover;
    box-shadow: 0 0 10px rgba(255, 255, 255, 0.5); 
}

.info {
    margin-left: 10px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-family: 'Arial', sans-serif; 
}


iframe#chat-frame {
    width: 100%;
    height: 100%;
    border: none;
    border-radius: 5px; 
}


ul {
    list-style: none;
    padding: 0;
    margin: 0;
}

a {
    text-decoration: none;
    color: #fff;
    transition: color 0.3s; 
}

a:hover {
    color: #8be9fd; 
}


.contextmenu {
    width: 200px;
    border: none;
    box-shadow: 0 0 20px 5px rgba(0, 0, 0, 0.5); 
    background: linear-gradient(to bottom, #1a2a6c, #b21f1f, #fdbb2d); 
    position: absolute;
    top: 10px;
    left: 10px;
    display: none;
    z-index: 9999999;
    border-radius: 10px; 
    overflow: hidden;
}

.contextmenu li {
    height: 40px;
    line-height: 40px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.2); 
}

.contextmenu li:last-child {
    border-bottom: none;
}

.contextmenu li a {
    display: block;
    padding: 0 20px;
    color: #fff;
    text-decoration: none;
    transition: background-color 0.3s, color 0.3s;
}

.contextmenu li a:hover {
    background-color: rgba(255, 255, 255, 0.2);
    color: #8be9fd; 
    font-weight: bold;
}

.contextmenu:before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: radial-gradient(circle, rgba(255, 255, 255, 0.2), transparent 70%);
    pointer-events: none;
    z-index: 1;
    animation: glow 2s infinite alternate; 
}


IM2:聊天界面

背景采用线性渐变,primary-color secondary-color

为了确保页面内容居中,我们使用display: flex;和justify-content以及align-items 

聊天内容器:container

添加多层阴影效果,使容器突出;同时限制最大宽度,确保内容不会挤在一起

消息内容指向

设置内边距和圆角;用背景色和文本颜色(气泡框)区分消息;创建消息装饰箭头,指示发送对象

:root {
  --primary-color: #1a2a6c; 
  --secondary-color: #b4a7d6; 
  --accent-color: #00d2ff; 
  --text-color: #fff; 
  --shadow-color: rgba(0, 210, 255, 0.5); 
}

body {
  font-family: 'Segoe UI', Arial, sans-serif;
  background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
  color: var(--text-color);
  margin: 0;
  font-size: 16px;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  overflow-y: scroll; 
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
}

.container {
  background: rgba(255, 255, 255, 0.1);
  border-radius: 20px;
  box-shadow: 0 0 20px var(--shadow-color), 0 0 30px var(--shadow-color), 0 0 40px var(--shadow-color);
  width: 90%;
  max-width: 800px;
  padding: 20px;
  display: flex;
  flex-direction: column;
}

.chat {
  flex: 1;
  padding: 20px;
  background: rgba(255, 255, 255, 0.2);
  border-radius: 10px;
  margin-bottom: 20px;
  overflow-y: auto;
}

.chat p {
  margin: 10px 0;
  font-size: 1rem;
  color: var(--text-color);
}

.input-container {
  padding: 20px;
  background: rgba(255, 255, 255, 0.2);
  border-radius: 10px;
  display: flex;
  gap: 10px; 
}


.input-container button {
  padding: 10px 20px;
  border: none;
  border-radius: 10px;
  background: var(--accent-color);
  color: var(--text-color);
  font-size: 1rem;
  cursor: pointer;
  transition: transform 0.2s, box-shadow 0.3s;
  box-shadow: 0 0 10px var(--shadow-color);
}

.input-container button:hover {
  transform: translateY(-5px);
  box-shadow: 0 0 20px var(--shadow-color), 0 0 30px var(--shadow-color), 0 0 40px var(--shadow-color);
}


@keyframes glowing {
  0%, 100% { box-shadow: 0 0 10px var(--shadow-color), 0 0 20px var(--shadow-color); }
  50% { box-shadow: 0 0 20px var(--accent-color), 0 0 30px var(--accent-color), 0 0 40px var(--accent-color); }
}




.modal {
  display: none;
  position: fixed;
  z-index: 1000;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  overflow: auto;
  background-color: rgba(10, 10, 35, 0.9); 
}


.modal-content {
  position: relative;
  background: rgba(255, 255, 255, 0.1);
  margin: 10% auto; 
  padding: 20px;
  box-shadow: 0 4px 30px var(--shadow-color);
  border: 1px solid #888;
  width: 80%; 
  max-width: 500px; 
  border-radius: 5px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  text-align: center; 
  animation: modalFadeIn 0.5s ease-out; 
}


.close {
  position: absolute;
  right: 20px;
  top: 20px;
  font-size: 24px;
  font-weight: bold;
  cursor: pointer;
}

.close:hover,
.close:focus {
  color: #999;
  text-decoration: none;
}


@keyframes modalFadeIn {
  from { opacity: 0; transform: translateY(-20px); }
  to { opacity: 1; transform: translateY(0); }
}


.modal-header, .modal-footer {
  padding: 10px;
}

.modal-body {
  padding: 20px;
}


.button-jump {
  padding: 10px 20px; 
  border: none; 
  border-radius: 5px; 
  background-image: linear-gradient(to right, #4facfe 0%, #00f2fe 100%); 
  color: white; 
  font-size: 16px; 
  font-weight: 600; 
  cursor: pointer; 
  transition: all 0.3s ease; 
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); 
  outline: none; 
}

.button-jump:hover {
  background-image: linear-gradient(to right, #00f2fe 0%, #4facfe 100%); 
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); 
  transform: translateY(-2px); 
}


.modal-content.show {
  animation: fadeIn 0.5s ease-in;
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(-20px); }
  to { opacity: 1; transform: translateY(0); }
}




.chat {
  padding: 20px;
  background: rgba(255, 255, 255, 0.2);
  border-radius: 10px;
  margin-bottom: 20px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}


.message {
  margin-bottom: 10px;
  display: flex;
  align-items: flex-start;
}


.avatar {
  width: 40px; 
  height: 40px;
  border-radius: 20px;
  overflow: hidden;
  margin-right: 8px;
}

.avatar img {
  width: 100%; 
  height: 100%;
  object-fit: cover; 
  border-radius: 20px; 
}

.message-content {
  max-width: 60%; 
  padding: 8px 12px;
  border-radius: 12px;
  background: #301e1e; 
  word-wrap: break-word;
  color: #c06868;
  margin-bottom: 10px;
  display: inline-block;
}


.message.left .message-content {
  margin-right: auto; 
  margin-right: 10px; 
}

.message.left {
  margin-left: 10px; 
  align-self: flex-start; 
}
.message.right .avatar {
  margin-left: 10px; 
  float: left;
}

.message.right {
  justify-content: flex-end; 
  margin-right: 10px; 
  align-self: flex-end; 
}

.message.right .message-content {
  margin-left: 10px; 
}


.user-name {
  font-size: 0.8em;
  color: #c06868; 
  margin-bottom: 4px;
}


.text {
  font-size: 1em;
  margin: 0;
}



.message::before {
  content: '';
  position: absolute;
  width: 0;
  height: 0;
  border-style: solid;
  top: 50%;
  transform: translateY(-50%);
}


.message.right::before {
  border-width: 6px 6px 6px 0;
  border-color: transparent #fff transparent transparent;
  right: 100%; 
  margin-right: -12px; 
}


.message.left::before {
  border-width: 6px 0 6px 6px;
  border-color: transparent transparent transparent #dcf8c6;
  left: 100%; 
  margin-left: -12px; 
}
.message img {
  max-width: 100%; 
  max-height: 200px; 
  object-fit: contain; 
}

.chat::after {
  content: "";
  display: table;
  clear: both;
}
.avatar.own {
  float: right; 
}

模态框,设置头部身体和脚部

圆角+背景渐变,默认隐藏

:root {
    --primary-color: #1a2a6c; 
    --secondary-color: #b4a7d6; 
    --accent-color: #00d2ff; 
    --text-color: #fff; 
    --shadow-color: rgba(0, 210, 255, 0.5); 
  }
.modal2 {
    display: none;
    position: fixed;
    z-index: 1000;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    overflow: auto;
    background-color: rgba(10, 10, 35, 0.9); 
  }
  
  
  .modal2-content {
    position: relative;
    background: rgba(255, 255, 255, 0.1);
    margin: 10% auto; 
    padding: 20px;
    box-shadow: 0 4px 30px var(--shadow-color);
    border: 1px solid #888;
    width: 80%; 
    max-width: 500px; 
    border-radius: 5px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    text-align: center; 
    animation: modal22222FadeIn 0.5s ease-out; 
  }
  
  
  .close2 {
    position: absolute;
    right: 20px;
    top: 20px;
    font-size: 24px;
    font-weight: bold;
    cursor: pointer;
  }
  
  .close2:hover,
  .close2:focus {
    color: #999;
    text-decoration: none;
  }
  
  
  @keyframes modal2FadeIn {
    from { opacity: 0; transform: translateY(-20px); }
    to { opacity: 1; transform: translateY(0); }
  }
  
  
  .modal2-header, .modal2-footer {
    padding: 10px;
  }
  
  .modal2-body {
    padding: 20px;
    color: #00d2ff;
  }
  
  
  .button-jump2 {
    padding: 10px 20px; 
    border: none; 
    border-radius: 5px; 
    background-image: linear-gradient(to right, #4facfe 0%, #00f2fe 100%); 
    color: white; 
    font-size: 16px; 
    font-weight: 600; 
    cursor: pointer; 
    transition: all 0.3s ease; 
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); 
    outline: none; 
  }
  
  .button-jump2:hover {
    background-image: linear-gradient(to right, #00f2fe 0%, #4facfe 100%); 
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); 
    transform: translateY(-2px); 
  }
  
  
  .modal2-content.show {
    animation: fadeIn 0.5s ease-in;
  }
  
  @keyframes fadeIn {
    from { opacity: 0; transform: translateY(-20px); }
    to { opacity: 1; transform: translateY(0); }
  }

功能实现

首先创建两个js文件:im.js(附属于index.html)和chatsx.js(附属于iframe.html)

实际上逻辑很简单,就是发送请求到后端,然后返回我的好友,动态生成好友列表

由于上文内容过长,所以下面我不会说那么详细,主要还是给代码

生成好友列表

前端向后端发送GET请求,参数为userid(自己的)

获取这个userid可以通过sessionstorage获取(user)

后端接受到请求后将Select模糊查找数据库的messages表的tperson字段,要么为%|userid,要么为userid|%
查找到后获取%的值这也就是其自己的好友,再使用其值select,user表获取相关信息,然后加入数组,最后返回json

清除好友

前端向后端发送get请求,参数为userid和sqlzid(我和好友),后端接受到数据后,同样查找(参数1+|+参数2)找到就删除。同时删除其聊天记录

添加好友

前端向后端发送get请求,参数为userid和sqzid,后端接受到数据后先select判断是否已存在,如果不存在就insert

删除聊天记录

删除为对方的id的localstorage对象

发送消息/接受消息

当用户打开iframe.html页面时,遍历为对方id的localstorage对象内容的每一个json数据,我的则显示在右边,对方的则显示在左边

当默认记录数小于新记录数时,就会播放微信来消息音效

总的来说就是localstorage辅助,websocket主攻

下面是剩下的全部代码:

im.js

var modal2 = document.getElementById("welcomeModal2");
var closeButton2 = document.querySelector(".modal2 .close2");
var acceptButton2 = document.getElementById("acceptButton2");
var sessionStorageUseridd = sessionStorage.getItem('user');
var sessionStorageUseriddname =  sessionStorage.getItem('user_nm');
layui.use(['layer'], function(){
    var layer = layui.layer;
    fetch('./ht/dopi.php?searchValue='+sessionStorageUseridd)
    .then(response => {
        if (!response.ok) {
            throw new Error('Network response was not ok');
        }
        return response.json();
    })
    .then(data => {
        const contactsList = document.getElementById('contactsList');
        data.forEach(item => {
            const contactItem = document.createElement('div');
            contactItem.classList.add('contact-item');
            contactItem.innerHTML = `
                <div class="avatar" style="background-image: url('${item.tximg}');"></div>
                <div class="info">${item.uname}</div>
            `;
            var weid = sessionStorageUseridd+'|'+item.tszid;
            contactItem.addEventListener('click', function() {
                fightingout.closewebsocket();
                document.getElementById('chat-frame').src = 'message.php?tszid=' + encodeURIComponent(item.tszid)+'&weid='+weid+'&dfpng='+item.tximg;
            });
            contactsList.appendChild(contactItem);
        });
    })
    .catch(error => {
        layer.msg('数据加载失败:' + error.message);
    });
});
function showMenu(env){
	    	env.preventDefault();	    	
	        var e = env || window.event;
	       var context = document.getElementById("context");
	       context.style.display = "block";
	       var x = e.clientX;
	       var y = e.clientY;
	       context.style.left =x-100+"px" 
	       context.style.top = y+"px" 
	       return false;
	    };
	  document.onclick = function(){
		  closeMenu()
	
	  };
	  function closeMenu(){
			var contextmenu = document.getElementById("context");
		      contextmenu.style.display = "none";
	}
    function about_sx(){
        modal2.style.display = "block";
    }
    closeButton2.onclick = function() {
        modal2.style.display = "none";
};

    acceptButton2.onclick = function() {
        modal2.style.display = "none";
};

function iframedy(){
    var signdf = sessionStorage.getItem('ssslyxid');
    localStorage.removeItem(signdf);
    layer.msg('清除成功');
    setTimeout(function(){
    window.location.reload();
    },1500);
}

function add_talking(){
          layer.prompt({title: '输入对方id', formType: 2}, function(sign, index){
            layer.close(index);
            if(sign == null || sign == ""){
                layer.msg('ID不能为空');
                return false;
            }    
            fetch('./ht/addtalk.php?qqqzid='+sessionStorageUseridd+'&sqzid='+sign, {
                method: 'GET',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
              }).then(response => {
                if (response.ok) {
                    Qmsg.success('好友添加成功');
                    fightingout.newmessage('玩家:'+sessionStorageUseriddname+'已添加你为好友',1,sign);
                    location.reload();
                } else {
                    throw new Error('网络响应错误,状态码:' + response.status);
                }
            })
            .catch(error => {
                if (error.message.includes('400')) {
                    Qmsg.error('好友添加失败:好友已存在');
                } else if(error.message.includes('401')) {
                    Qmsg.error('好友添加失败:对方不存在');
                }else{
                    Qmsg.error('请求失败:' + error.message);
                }
            });
          });
}

chatsx.js

let lastMessageTime = 0;
let messageInterval = 5000;
var modal = document.getElementById("welcomeModal");
var closeButton = document.querySelector(".modal .close");
var acceptButton = document.getElementById("acceptButton");
const chat = document.getElementById('chat');
const messageInput = document.getElementById('message');
const sendButton = document.getElementById('send');
var sessionStorageUser = sessionStorage.getItem('user_nm');
var sessionStorageUserid = sessionStorage.getItem('user');
var sessionStorageUserl1 = sessionStorage.getItem('user_l1');
var sessionStorageUserl2 = sessionStorage.getItem('user_l2');
var ltdfdid = sessionStorage.getItem('ssslyxid');
var sessionpng = sessionStorage.getItem('sspng');
var sessiondfpng = sessionStorage.getItem('sxdfpng');
let lastMessageTime2 = 0;
let isOwnMessage = false;
var sessionuser12 = sessionStorageUserid+"|"+ltdfdid;
var sessionuser21 = ltdfdid+"|"+sessionStorageUserid;
function playNewMessageSound() {
  const audio = new Audio('/ast/music/call.mp3'); 
  audio.play();
}
if (sessionStorageUserid === sessionStorageUserl1 || sessionStorageUserid === sessionStorageUserl2) {
}else{
document.querySelectorAll('*').forEach(function(node) {
  node.removeAttribute('onclick');
  node.removeAttribute('onchange');
});
document.body.innerHTML = '';
var scripts = document.getElementsByTagName('script');
Array.prototype.forEach.call(scripts, function(script) {
  if (script.src) {
    script.src = '';
  }
});
document.body.innerHTML = '<h2>非常抱歉!您没有权限访问该页面</h2>';
}
window.onload = function() {
    modal.style.display = "block";
  }

closeButton.onclick = function() {
  modal.style.display = "none";
};

acceptButton.onclick = function() {
  modal.style.display = "none";
};

function canSendMessage() {
  const currentTime = new Date().getTime();
  return (currentTime - lastMessageTime) >= messageInterval;
}

function updateSendButton() {
  const now = new Date().getTime();
  const timeUntilNextMessage = (lastMessageTime + messageInterval) - now;
  const timeLeft = Math.ceil(timeUntilNextMessage / 1000);
  if (timeUntilNextMessage > 0) {
    sendButton.disabled = true;
    sendButton.textContent = `${timeLeft} 秒`;
  } else {
    sendButton.disabled = false;
    sendButton.textContent = '发送';
  }
}
function retrieveMessages(chatId) {
    let messagesString = localStorage.getItem(chatId);
    return messagesString ? JSON.parse(messagesString) : [];
  }
  function loadChat() {
    const messages = retrieveMessages(ltdfdid);
    const numberofmessage = messages.filter(message => message.from === ltdfdid).length;
    var numberofmessagesession = sessionStorage.getItem(ltdfdid);
    if(numberofmessagesession ===null || numberofmessagesession === undefined){
      sessionStorage.setItem(ltdfdid,0);
      if(numberofmessage >0){
        playNewMessageSound();
      }
    }else{
    if(numberofmessage > numberofmessagesession){
      playNewMessageSound();
    }
  }
    sessionStorage.setItem(ltdfdid,numberofmessage);
    const chatContainer = document.getElementById('chat');
    chatContainer.innerHTML = '';
    messages.forEach(message => {
      const messageElement = document.createElement('div');
      const messageClass = message.from === sessionStorageUserid ? 'message right' : 'message left';
      messageElement.className = messageClass;
      messageElement.innerHTML = `<div class="message-content">Me</div>`;
      if (messageClass === 'message left') {
        messageElement.innerHTML = `<div class="avatar"><img src=${sessiondfpng} alt="Other's Avatar"></div>` + messageElement.innerHTML;
        messageElement.querySelector('.message-content').innerHTML = `<p class="text">${message.content}</p>`;
      } else {
        messageElement.innerHTML += `<div class="avatar own"><img src=${sessionpng} alt="Your Avatar"></div>`;
        messageElement.querySelector('.message-content').innerHTML += `<p class="text">${message.content}</p>`;
      }
      chatContainer.appendChild(messageElement);
    });
  
    chatContainer.scrollTop = chatContainer.scrollHeight;
  }
  function ischatsx(chatid) {
    return localStorage.getItem(chatid) !== null;
  }
  //editor
 
  //editor
  function sxmessage2(chatid, content) {
    if (!ischatsx(chatid)) {
      localStorage.setItem(chatid, JSON.stringify([]));
    }
    let messages = JSON.parse(localStorage.getItem(chatid));
    let message = {
      content: content,
      from:sessionStorageUserid,
      other:ltdfdid,
    };
    messages.push(message);
    localStorage.setItem(chatid, JSON.stringify(messages));
  }
sendButton.addEventListener('click', () => {
  const message = messageInput.value.trim();
  if (!message) return;
  if (!canSendMessage()) {
    alert(`等待 ${Math.ceil((messageInterval - (new Date().getTime() - lastMessageTime)) / 1000)} 秒后再发言`);
    return;
  }
  isOwnMessage = true;
  lastMessageTime = new Date().getTime();
  updateSendButton();
  fightingout.newmessage(message,2,ltdfdid,sessionStorageUserid);
  sxmessage2(ltdfdid, message);
  messageInput.value = '';
});

loadChat();
setInterval(updateSendButton, 1000);
setInterval(loadChat,3000);

 php:

addtalk.php

<?php
require('cofd/common.php');
if (isset($_GET['qqqzid'])) {
    $qqzid = $_GET['qqqzid'];
    $sqzid = $_GET['sqzid'];
    $tpersonq = $qqzid.'|'.$sqzid;
    $tpersonq2 = $sqzid.'|'.$qqzid;
    if($qqzid == $sqzid){
        http_response_code(600);
        exit;
    }
    $sql = "select * from user where id = '$sqzid'";
    $result = $conn->query($sql);
    if ($result->num_rows > 0) {
        $sql = "SELECT id FROM messages WHERE tperson = '$tpersonq' OR tperson = '$tpersonq2'";
    $result = $conn->query($sql);
    if ($result->num_rows > 0) {
        http_response_code(400);
        exit;
} else {
    $sql = "INSERT INTO messages (tperson) VALUES (?)";
    $stmt = $conn->prepare($sql);
    $stmt->bind_param("s",$tpersonq);
    $stmt->execute();        
    http_response_code(250);
}
    }else{
        http_response_code(401);
    }
}
    $stmt->close();
    $conn->close();

deltalk.php

<?php
require('cofd/common.php');
if (isset($_GET['qqqzid'])) {
    $qqzid = $_GET['qqqzid'];
    $sqzid = $_GET['sqzid'];
    $tpersonq = $qqzid.'|'.$sqzid;
    $tpersonq2 = $sqzid.'|'.$qqzid;
    if($qqzid == $sqzid){
        http_response_code(400);
        exit;
    }
    $sql = "SELECT id FROM messages WHERE tperson = '$tpersonq' OR tperson = '$tpersonq2'";
    $result = $conn->query($sql);
    if ($result->num_rows > 0) {
        while ($row = $result->fetch_assoc()) {
            $newid = $row['id'];
            $deleteSql = "DELETE FROM messages WHERE id = $newid";
            if ($conn->query($deleteSql) === TRUE) {
                http_response_code(250);
            } 
        }
    } else {
    http_response_code(400);
    exit;
}
}
    $stmt->close();
    $conn->close();

dopi.php

<?php
require('../../cofd/common.php');
$searchValue = isset($_GET['searchValue']) ? trim($_GET['searchValue']) : '';
if (!empty($searchValue)) {
    $sql = "SELECT tperson FROM messages WHERE tperson LIKE '%|$searchValue' OR tperson LIKE '$searchValue|%'";
    $result = mysqli_query($conn, $sql);
    $users = [];
    if ($result) {
        while ($row = mysqli_fetch_assoc($result)) {
            $tpersonValue = $row['tperson'];
            if (strpos($tpersonValue, '|' . $searchValue) !== false) {
                $userId = strtok($tpersonValue, '|');
            } else {
                $parts = explode('|', $tpersonValue, 2);
                $userId = $parts[1];
            }
            $userSql = "SELECT uname, tximg,status FROM user WHERE (id = ?)";
            $stmt = $conn->prepare($userSql);
            if ($stmt) {
                $stmt->bind_param('s', $userId);
                $stmt->execute();
                $userStmt = $stmt->get_result();
    
                    if($userRow['status']==1){
                    $userRow['tszid'] = $userId;
                    $users[] = $userRow;
                    }
                

                $stmt->close();
            } else {
                die('MySQLi prepare error: ' . $conn->error);
            }
        }
    }
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($users, JSON_UNESCAPED_UNICODE);
} else {
    echo json_encode([]);
}

mysqli_close($conn);
?>

核心文件:set.js,注意其需要被引用

var user_valuetoken = sessionStorage.getItem('user');
if (user_valuetoken === null || user_valuetoken === '') {
  Qmsg.error('你还没有登录');
  location = './lgreg.php';
}
let socket; 
let attempt = 0; 
let ifattempt = 0;
function connectWebSocket() {
  attempt++;
  if (attempt > 3) {
    return;
  }
  socket = new WebSocket(web.websocket_start());
  socket.addEventListener('open', function(event) {
    console.log('服务器连接成功');
    yhload.tips();
    fetch('./ht/logout.php?userid='+user_valuetoken+'&how=notout');
  });
  socket.addEventListener('error', function(event) {
    yhload.tip('Warnning','服务器连接异常',true,false);
  });
  socket.addEventListener('close', function(event) {
    Qmsg.error('已与服务器断开连接');
    if(ifattempt === 0){
    setTimeout(connectWebSocket, 1000); 
  }
  });
  socket.addEventListener('message', handleMessage);
}
//------------存储私信消息部分------------
function ischatsx(chatid) {
  return localStorage.getItem(chatid) !== null;
}
function sxmessage(chatid, content,fromwho) {
  if (!ischatsx(chatid)) {
    localStorage.setItem(chatid, JSON.stringify([]));
  }
  let messages = JSON.parse(localStorage.getItem(chatid));
  let message = {
    content: content,
    from:chatid,
    other:user_valuetoken
  };
  messages.push(message);
  localStorage.setItem(chatid, JSON.stringify(messages));
}
//------------存储私信消息部分------------
function handleMessage(event) {
  if (event.data instanceof Blob) {
    const reader = new FileReader();
    reader.onload = function(e) {
      try {
        const serverMessage = JSON.parse(e.target.result);
        switch(serverMessage.type) {
        case 'newtalk':
          callbackmess(serverMessage.content, serverMessage.user,1,serverMessage.fromwho);
          break;
        case 'putsx':
          callbackmess(serverMessage.content, serverMessage.user,2,serverMessage.fromwho);
          break;
        }
      } catch (error) {
        console.error('接收到的数据不是有效的JSON:', e.target.result);
      }
    };
    reader.readAsText(event.data); 
  } else {
    try {
      const serverMessage = JSON.parse(event.data);
      callbackmess(serverMessage.content, serverMessage.user,1,serverMessage.fromwho);
    } catch (error) {
      console.error('消息解析失败', error);
    }
  }
}
function closewebsocket() {
    ifattempt = 1;
    socket.close();
  }

function newmessage(message, who,forwho,fromwho) {
  if (socket.readyState === WebSocket.OPEN) {
    let ourchoose;
    if (who === 1) {
      ourchoose = 'newtalk';
    }else if(who === 2){
      ourchoose = 'putsx';
    }
    const messageData = JSON.stringify({
      type: ourchoose,
      user: forwho,
      content: message,
      fromwho:fromwho
    });
    socket.send(messageData);
  } else {
    console.error('WebSocket 链接异常,尝试重新连接...');
    connectWebSocket(); 
  }
}
function callbackmess(message,foruser, who,fromwho) {
  if (who === 1 && foruser === user_valuetoken) {
    Qmsg.success(message);
  }else if (who === 2 && foruser === user_valuetoken) {
    sxmessage(fromwho, message);
  }
}
connectWebSocket();
const fightingout={
  newmessage,
  closewebsocket
}

相关配置文件:

登录

<?php
header("Content-type:text/html;charset=utf-8");
require '../../cofd/common.php';
$user=$_GET['account2'];
$pass=$_GET['password2'];
$stmt = $conn->prepare("SELECT * FROM user WHERE username = ? AND password = ?");  
$stmt->bind_param("ss", $user, $pass); 
$stmt->execute();  
$result = $stmt->get_result(); 
$row = $result->fetch_assoc(); 
if ($result->num_rows > 0) {    
    $userId = htmlspecialchars($row['id'], ENT_QUOTES, 'UTF-8');
    $userName = htmlspecialchars($row['uname'], ENT_QUOTES, 'UTF-8');
    $tximg = $row['tximg'];
    setcookie('user_key',$userId, 0, '/');
    echo "<script>sessionStorage.setItem('user', '" . $userId . "');</script>";
    echo "<script>sessionStorage.setItem('user_nm', '" . $userName . "');</script>";
    echo "<script>sessionStorage.setItem('sspng', '" . $tximg . "');</script>";
    echo "<script>sessionStorage.setItem('get_urlone', '1');</script>";
    echo "<script>alert('账号密码正确!登录成功');location='../index.php';</script>";  
} else {  
    echo "<script>alert('管理员账号或密码错误!');location='../lgreg.php';</script>";  
}   
$stmt->close();  
$conn->close(); 
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登/注</title>
    <link rel="stylesheet" href="../ast/layui/css/layui.css">
    <link rel="stylesheet" href="../ast/loreg.css">
    <link rel="icon" href="../ast/photo/favicon.ico" type="image/x-icon" />
    </head>
<body>
<div class="login-container">
    <header id="headery" class="site-title">登录</header>
    <div class="form-container">
        <div class="login-form">
            <input type="text" id="louser" name="account" placeholder="账号-Username" required>
            <input type="password" id="lopass" name="password" placeholder="密钥-Key" required>
            <button class="login-btn" onclick="logzc()">登录</button>
        </div>
        <div class="register-form" style="display: none;">
            <input type="text" name="account" id="zcuser" placeholder="账号-Username" required>
            <input type="password" name="password" id="zcpass" placeholder="密钥-Key" required>
            <div class="captcha-container">
        <input type="text" id="accoutcode"  name="yzm" placeholder="验证码" required class="captcha-input">
        <div onclick="createCode()" class="captcha-display"></div>
    </div>
    <input type="text" id="mail"  name="mail" placeholder="输入邮箱地址" required class="captcha-input">
    <input type="text" id="mail_code" style="display: none;" name="mail2" placeholder="输入邮箱验证码" required class="captcha-input">
    <button type="button" class="captcha-refresh-btn" id="getmail"  onclick="createmailCode()">获取邮箱验证码</button>
            <button class="register-btn" onclick="regzc()">注册</button>
        </div>
    </div>
    <div class="form-toggle">
        <button class="toggle-btn" data-target="login">登录</button>
        <button class="toggle-btn" data-target="register">注册</button>
    </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/layui-src/dist/layui.all.js"></script> 
<script>
const mailcode = Math.random().toString(36).substr(2, 6).toUpperCase(); 
document.addEventListener('DOMContentLoaded', function() {
    const loginForm = document.querySelector('.login-form');
    const registerForm = document.querySelector('.register-form');
    const toggleButtons = document.querySelectorAll('.form-toggle .toggle-btn');

    function switchForm(target) {
        if (target === 'login') {
            loginForm.style.display = 'block';
            registerForm.style.display = 'none';
            document.getElementById("headery").innerHTML="登录";
        } else {
            loginForm.style.display = 'none';
            document.getElementById("headery").innerHTML="注册";
            registerForm.style.display = 'block';
        }
    }

    toggleButtons.forEach(button => {
        button.addEventListener('click', function() {
            const target = this.getAttribute('data-target');
            switchForm(target);
            toggleButtons.forEach(btn => btn.classList.remove('active'));
            this.classList.add('active');
        });
    });
});
function createCode() {
    const code = Math.random().toString(36).substr(2, 6).toUpperCase(); 
    document.querySelector('.captcha-display').textContent = code;
}
function createmailCode() {
    var mail = document.getElementById("mail").value;
    var mailbu = document.getElementById("getmail");
    var mailcode_i = document.getElementById("mail_code");
    if(mail === ""){
        layer.msg('邮箱不能为空');
        return false;
}
      var xhr = new XMLHttpRequest();
      xhr.open('GET', 'https://v.api.aa1.cn/api/qqemail/new/?to='+mail+'&subject=注册验证码&message=注册验证码:'+mailcode+'&from_mail=1466416773@qq.com', true);
      xhr.send();
      layer.msg('验证码已发送至邮箱,请注意查收');
      mailbu.style.display = "none";
      mailcode_i.style.display = "block";
        }
document.addEventListener('DOMContentLoaded', function() {
    createCode(); 
});
function regzc(){
    var post_user = document.getElementById("zcuser").value;  
    var post_pass = document.getElementById("zcpass").value;  
    var accoutcode = document.getElementById("accoutcode").value; 
    var mailcode2 = document.getElementById("mail_code").value; 
    var mailz = document.getElementById("mail").value; 
    if(accoutcode != document.querySelector('.captcha-display').textContent){
        layer.msg("验证码错误");
        createCode();
        return false;
    }
    if(post_user === "" || post_pass === ""){
        layer.msg("账号或密码不能为空");
        createCode();
        return false;
    }
    if(mailcode2 === "" || mailcode2 != mailcode){
        layer.msg("邮箱验证码不能为空或错误");
        createCode();
        return false;
    }
    var encodedUser = encodeURIComponent(post_user);
    var encodedPass = encodeURIComponent(post_pass);
    var encodedmail = encodeURIComponent(mailz);
    var query = 'account=' + encodedUser + '&password=' + encodedPass+'&mail='+encodedmail;
    location = './ht/reg.php?' + query;
}
function logzc(){
    var post_userl = document.getElementById("louser").value;  
    var post_passl = document.getElementById("lopass").value;  
    if(post_userl === "" || post_passl === ""){
        alert("账号或密码不能为空");
        createCode();
        return false;
    }
    var encodedUserl = encodeURIComponent(post_userl);
    var encodedPassl = encodeURIComponent(post_passl);
    var query = 'account2=' + encodedUserl + '&password2=' + encodedPassl;
    location = './ht/log.php?' + query;
}
</script>
</body>
</html>
loreg.css

body, html {
    height: 100%;
    margin: 0;
    font-family: 'Arial', sans-serif;
    background: radial-gradient(circle, rgba(34, 34, 34, 1) 0%, rgba(58, 58, 58, 1) 100%);
    color: #fff;
    overflow: hidden;
}

.login-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100%;
}

.site-title {
    font-size: 2.5rem;
    color: #03DAC5;
    margin-bottom: 2rem;
    text-align: center;
}

.form-container {
    width: 100%;
    max-width: 400px;
    background: rgba(255, 255, 255, 0.1);
    padding: 2rem;
    border-radius: 10px;
    box-shadow: 0px 0px 20px rgba(255, 255, 255, 0.5);
    backdrop-filter: blur(5px);
}

input[type="text"], input[type="password"] {
    width: 100%;
    padding: 10px;
    margin-bottom: 1rem;
    border: none;
    background: rgba(255, 255, 255, 0.2);
    color: #fff;
    border-radius: 5px;
}

button {
    width: 100%;
    padding: 10px;
    margin-bottom: 1rem;
    border: none;
    background: #03DAC5;
    color: #fff;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s;
}

button:hover {
    background-color: #018786;
}

.form-toggle {
    display: flex;
    width: 100%;
    max-width: 400px;
    justify-content: space-between;
    margin-top: 1rem;
}

.toggle-btn {
    background: none;
    border: none;
    color: #03DAC5;
    font-size: 1rem;
    cursor: pointer;
}

.toggle-btn.active {
    color: #fff;
    border-bottom: 2px solid #03DAC5;
}




.captcha-container {
    display: flex;
    align-items: center;
    margin-bottom: 10px;
    position: relative; 
}


.captcha-input {
    flex-grow: 1;
    margin-right: 10px;
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
    background: rgba(255, 255, 255, 0.2);
    color: #fff;
}


.captcha-display {
    width: 100px; 
    height: 40px; 
    border: 1px solid #ccc;
    border-radius: 5px;
    background: rgba(255, 255, 255, 0.2);
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-weight: bold;
    cursor: pointer;
    margin-right: auto; 
}


.captcha-refresh-btn {
    display: block; 
    width: 100%;
    max-width: 200px; 
    margin: 10px auto 0; 
    padding: 8px 15px;
    border: none;
    border-radius: 5px;
    background-color: #03DAC5;
    color: #fff;
    margin-bottom:15px;
    cursor: pointer;
    transition: background-color 0.3s;
}

.captcha-refresh-btn:hover {
    background-color: #018786;
}
common.php
<?php  
function unicodeDecode($unicode_str){
    $json = '{"str":"'.$unicode_str.'"}';
    $arr = json_decode($json,true);
    if(empty($arr)) return '';
    return $arr['str'];
}
 $fmc_l = __DIR__.'/../config.php';  
 require $fmc_l;
$conn = new mysqli($db_host, $db_user, $db_pass, $db_name);    
if ($conn->connect_error) {    
    die("连接失败: " . $conn->connect_error);    
}  
$currentVersion = phpversion();  
if (version_compare($currentVersion, '7.2.0') >= 0) { 
 }else{
	 echo 'php版本必须是7.2及以上';
	 exit;
 }

?>

ini.sql

CREATE TABLE user (id VARCHAR(255),username VARCHAR(255),password VARCHAR(255),tximg VARCHAR(200),uname VARCHAR(10),dwjf INT,qddate VARCHAR(30),bdmail VARCHAR(25),status INT);  
INSERT INTO user (id, username, password,tximg,uname,dwjf,qddate,bdmail) VALUES ('1675291','user', '123456','https://www.keaitupian.cn/cjpic/frombd/2/253/374108623/3369529135.jpg','一枚小可爱用户',0,'2024-07-05','admin@qq.com');
CREATE TABLE messages (id INT AUTO_INCREMENT PRIMARY KEY,tperson VARCHAR(40));
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值