前序
嗯...这个聊天室是我老早之前的一个项目(论坛)里的了
当时没用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">×</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">×</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-bottom
和padding
为了增强互动性:我们使用cursor
和hover
最后我们使用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));