Files
host_message/templates/chat.html
2026-02-13 12:33:43 +08:00

6058 lines
192 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>叠加态 - 局域网聊天室</title>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v6.4.0/css/all.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css">
<link rel="icon" href="/image/logo_w.ico" type="image/x-icon">
<style>
:root {
--primary-color: #4a90e2;
--secondary-color: #357abd;
--background-color: #f5f7fa;
--message-sent: #dcf8c6;
--message-received: #fff;
--sidebar-width: 280px;
--header-height: 60px;
--border-radius: 12px;
--shadow-color: rgba(0, 0, 0, 0.1);
--transition-speed: 0.3s;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--background-color);
height: 100vh;
overflow: hidden;
}
.app-container {
display: flex;
height: 100vh;
position: relative;
}
/* 侧边栏样式 */
.sidebar {
width: var(--sidebar-width);
background: white;
height: 100%;
position: fixed;
left: calc(-1 * var(--sidebar-width));
transition: transform var(--transition-speed);
z-index: 1000;
box-shadow: 2px 0 10px var(--shadow-color);
display: flex;
flex-direction: column;
}
.sidebar.visible {
transform: translateX(var(--sidebar-width));
}
.sidebar-header {
padding: 20px;
background: var(--primary-color);
color: white;
font-size: 1.2em;
display: flex;
align-items: center;
justify-content: space-between;
}
.chat-modes {
padding: 15px;
}
.mode-item {
padding: 12px 15px;
margin-bottom: 8px;
border-radius: var(--border-radius);
cursor: pointer;
transition: all var(--transition-speed);
display: flex;
align-items: center;
gap: 10px;
}
.mode-item:hover {
background: rgba(74, 144, 226, 0.1);
}
.mode-item.active {
background: var(--primary-color);
color: white;
}
.mode-item i {
font-size: 1.2em;
}
/* 主聊天区域 */
.chat-main {
flex: 1;
margin-left: 0;
transition: margin var(--transition-speed);
display: flex;
flex-direction: column;
height: 100vh;
background: white;
}
.chat-header {
height: var(--header-height);
background: white;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
padding: 0 20px;
position: relative;
}
.menu-toggle {
background: none;
border: none;
font-size: 1.5em;
color: var(--primary-color);
cursor: pointer;
padding: 10px;
margin-right: 15px;
transition: transform var(--transition-speed);
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
}
.menu-toggle:hover {
transform: scale(1.1);
}
.chat-title {
font-size: 1.2em;
font-weight: 500;
}
.connection-status {
margin-left: auto;
display: flex;
align-items: center;
gap: 10px;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: #ccc;
transition: background var(--transition-speed);
}
.status-indicator.connected {
background: #4caf50;
}
/* 消息区域 */
.chat-messages {
flex: 1;
padding: 20px;
overflow-y: auto;
background: #e5ddd5;
display: flex;
flex-direction: column;
gap: 10px;
position: relative;
}
.message {
position: relative;
padding: 12px 15px;
border-radius: 12px;
word-wrap: break-word;
margin: 0;
min-width: 120px;
}
.message.sent {
background: linear-gradient(135deg, var(--message-sent) 0%, #c5e1a5 100%);
margin-left: auto;
border-bottom-right-radius: 5px;
}
.message.received {
background: linear-gradient(135deg, var(--message-received) 0%, #f5f5f5 100%);
margin-right: auto;
border-bottom-left-radius: 5px;
}
.message-content {
padding-top: 8px;
font-size: 1em;
line-height: 1.4;
}
.message-info {
display: flex;
align-items: center;
font-size: 0.75em;
margin-top: 4px;
padding-top: 4px;
border-top: 1px solid rgba(0,0,0,0.05);
color: #666;
}
.info-item {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 6px;
border-radius: 10px;
background: rgba(0,0,0,0.04);
margin-right: 6px;
}
.info-item:last-child {
margin-right: 0;
}
.info-item i {
font-size: 0.9em;
}
.message.sent .message-info {
color: #558b2f;
}
.message.received .message-info {
color: #666;
}
/* 消息时间悬浮效果 */
.message-time {
transition: all 0.3s ease;
}
.message:hover .message-time {
opacity: 1;
}
/* 消息气泡尾<E6B3A1><E5B0BE><EFBFBD><EFBFBD><EFBFBD> */
.message::after {
content: '';
position: absolute;
bottom: 0;
width: 12px;
height: 12px;
}
.message.sent::after {
right: -6px;
background: linear-gradient(135deg, var(--message-sent) 0%, #c5e1a5 100%);
transform: skewX(45deg);
border-bottom-right-radius: 5px;
}
.message.received::after {
left: -6px;
background: linear-gradient(135deg, var(--message-received) 0%, #f5f5f5 100%);
transform: skewX(-45deg);
border-bottom-left-radius: 5px;
}
/* 优化聊天区域滚动条 */
.chat-messages::-webkit-scrollbar {
width: 6px;
}
.chat-messages::-webkit-scrollbar-track {
background: rgba(0,0,0,0.05);
border-radius: 3px;
}
.chat-messages::-webkit-scrollbar-thumb {
background: rgba(0,0,0,0.2);
border-radius: 3px;
}
.chat-messages::-webkit-scrollbar-thumb:hover {
background: rgba(0,0,0,0.3);
}
/* 输入区域 */
.chat-input-container {
padding: 15px 20px;
background: white;
border-top: 1px solid #eee;
display: flex;
gap: 15px;
align-items: center;
}
.chat-input {
flex: 1;
padding: 12px 20px;
border: 1px solid #ddd;
border-radius: 24px;
outline: none;
font-size: 1em;
transition: border-color var(--transition-speed);
}
.chat-input:focus {
border-color: var(--primary-color);
}
.send-btn {
padding: 12px 24px;
background: var(--primary-color);
color: white;
border: none;
border-radius: 24px;
cursor: pointer;
transition: all var(--transition-speed);
display: flex;
align-items: center;
gap: 8px;
}
.send-btn:hover {
background: var(--secondary-color);
transform: translateY(-1px);
}
.send-btn:active {
transform: translateY(1px);
}
/* 动画效果 */
@keyframes messageSlide {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 私<><E7A781><EFBFBD><E8BE93>框 */
.private-chat-input {
padding: 15px;
border-bottom: 1px solid #eee;
display: none;
}
.private-chat-input.active {
display: block;
}
.private-chat-input input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: var(--border-radius);
outline: none;
}
/* 移动端适配 */
@media (max-width: 768px) {
.sidebar {
width: 100%;
max-width: 320px;
}
.chat-main {
width: 100%;
}
.message {
max-width: 85%;
}
.chat-input-container {
padding: 10px;
}
.send-btn {
padding: 12px 20px;
}
}
/* 触摸设备优化 */
@media (hover: none) {
.mode-item:active {
background: rgba(74, 144, 226, 0.1);
}
.send-btn:active {
background: var(--secondary-color);
}
}
/* 在原有样式的基础上添加 */
.icon-fallback {
display: inline-block;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
}
/* 修改菜单图标样式 */
.menu-toggle {
/* 原有样式保持不变 */
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
}
/* 添加汉堡菜单的回退样式 */
.menu-icon {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 20px;
height: 16px;
}
.menu-icon span {
display: block;
width: 100%;
height: 2px;
background-color: var(--primary-color);
transition: all 0.3s;
}
/* 发送按钮图标样式优化 */
.send-btn i {
font-size: 1.2em;
display: flex;
align-items: center;
justify-content: center;
}
/* 在原有样式基础上添加 */
.message {
margin: 20px 0; /* 增加消息间距 */
}
/* IP 头像样式 */
.avatar {
width: 35px;
height: 35px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: white;
font-size: 1.2em;
margin-right: 10px;
}
/* 消息容器布局优化 */
.message-container {
position: relative;
display: flex;
align-items: flex-start;
margin: 12px 0;
max-width: 85%;
}
.message-container.sent {
flex-direction: row-reverse;
margin-left: auto;
}
.message-container.received {
margin-right: auto;
}
/* 优化头像样式 */
.avatar {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: white;
font-size: 1.1em;
flex-shrink: 0;
}
.message-container.sent .avatar {
margin-left: 8px;
}
.message-container.received .avatar {
margin-right: 8px;
}
/* 优化消息内容包装器 */
.message-content-wrapper {
flex: 1;
min-width: 200px;
max-width: calc(100% - 40px);
}
/* 优化消息气泡样式 */
.message {
padding: 10px 12px;
border-radius: 12px;
position: relative;
word-wrap: break-word;
margin: 0;
min-width: 120px;
}
.message.sent {
background: linear-gradient(135deg, var(--message-sent) 0%, #c5e1a5 100%);
border-bottom-right-radius: 4px;
}
.message.received {
background: linear-gradient(135deg, var(--message-received) 0%, #f5f5f5 100%);
border-bottom-left-radius: 4px;
}
/* 优化用户标签样式 */
.user-tag {
font-size: 0.8em;
color: #666;
margin-bottom: 4px;
position: absolute;
top: -18px;
}
.message-container.sent .user-tag {
right: 45px; /* 调整"我"的位置,与头像对齐 */
}
.message-container.received .user-tag {
left: 45px;
}
/* 添加复制按钮样式 */
.message-actions {
position: absolute;
right: -30px;
top: 50%;
transform: translateY(-50%);
opacity: 0;
transition: opacity 0.2s ease;
}
.message-container.received .message-actions {
left: -30px;
right: auto;
}
.message-container:hover .message-actions {
opacity: 1;
}
/* 优化消息容器样式 */
.message-container {
position: relative;
display: flex;
align-items: flex-start;
margin: 12px 0;
max-width: 85%;
}
/* 优化复制按钮样式 */
.copy-btn {
position: absolute;
width: 32px;
height: 32px;
border-radius: 50%;
background: white;
border: 1px solid rgba(0, 0, 0, 0.1);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 0.3s ease;
z-index: 2;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
top: 50%;
transform: translateY(-50%);
}
/* 发送消息的复制按钮位置 */
.message-container.sent .copy-btn {
left: -40px; /* 调整到消息外侧 */
}
/* 接收消息的复制按钮位置 */
.message-container.received .copy-btn {
right: -40px; /* 调整到<E695B4><E588B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><E5A496> */
}
.message-container:hover .copy-btn {
opacity: 1;
}
.copy-btn:hover {
background: white;
transform: translateY(-50%) scale(1.1);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.copy-btn i {
color: var(--primary-color);
font-size: 1.1em;
}
/* 复制成功提示优化 */
.copy-tooltip {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 0.9em;
z-index: 1000;
pointer-events: none;
opacity: 0;
transition: all 0.3s ease;
}
.copy-tooltip.show {
opacity: 1;
transform: translate(-50%, -10px);
}
/* 响应式布局优化 */
@media (max-width: 768px) {
.message-container {
max-width: 95%;
}
.message-content-wrapper {
min-width: 160px;
}
.message {
min-width: 100px;
}
.info-item {
font-size: 0.8em;
padding: 1px 4px;
}
}
/* 超小屏幕优化 */
@media (max-width: 480px) {
.message-info {
flex-direction: column;
align-items: flex-end;
gap: 2px;
}
.info-item {
width: auto;
}
.message-content-wrapper {
min-width: 120px;
}
}
/* 添加消息内容最大宽度制 */
.message-content {
font-size: 1em;
line-height: 1.4;
margin-bottom: 4px;
word-break: break-word;
}
/* 在样式部分添加收回按钮的样式 */
.sidebar-toggle {
position: absolute;
right: -15px;
top: 50%;
transform: translateY(-50%);
width: 30px;
height: 60px;
background: var(--primary-color);
border: none;
border-radius: 0 30px 30px 0;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: 2px 0 5px rgba(0,0,0,0.1);
z-index: 1001;
}
.sidebar-toggle:hover {
background: var(--secondary-color);
width: 35px;
}
.sidebar-toggle i {
transition: transform 0.3s ease;
}
.sidebar.visible .sidebar-toggle i {
transform: rotate(180deg);
}
/* 优化侧边栏样式 */
.sidebar {
width: var(--sidebar-width);
background: white;
height: 100%;
position: fixed;
left: calc(-1 * var(--sidebar-width));
transition: transform var(--transition-speed);
z-index: 1000;
box-shadow: 2px 0 10px var(--shadow-color);
display: flex;
flex-direction: column;
}
/* 优化头部状态栏样式 */
.connection-status {
display: flex;
align-items: center;
gap: 12px;
background: rgba(0, 0, 0, 0.05);
padding: 6px 12px;
border-radius: 20px;
}
.ip-display {
display: flex;
align-items: center;
gap: 5px;
padding-left: 8px;
border-left: 1px solid rgba(0, 0, 0, 0.1);
color: var(--primary-color);
}
/* 修改消息中 IP 显示样式 */
.info-item.ip-info {
background: rgba(74, 144, 226, 0.1);
color: var(--primary-color);
}
.message.sent .info-item.ip-info {
background: rgba(85, 139, 47, 0.1);
color: #558b2f;
}
.sidebar-actions {
padding: 15px;
border-bottom: 1px solid #eee;
}
.action-btn {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 15px;
background: var(--primary-color);
color: white;
text-decoration: none;
border-radius: var(--border-radius);
transition: all 0.3s ease;
}
.action-btn:hover {
background: var(--secondary-color);
transform: translateY(-2px);
}
.action-btn i {
font-size: 1.2em;
}
/* 聊天列表样式 */
.chat-list {
flex: 1;
display: flex;
flex-direction: column;
border-top: 1px solid #eee;
overflow: hidden;
}
.chat-list-header {
padding: 15px;
font-weight: bold;
background: #f8f9fa;
border-bottom: 1px solid #eee;
}
.chat-list-content {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.chat-item {
display: flex;
align-items: center;
padding: 10px;
border-radius: var(--border-radius);
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 5px;
position: relative;
}
.chat-item:hover {
background: rgba(74, 144, 226, 0.1);
}
.chat-item.active {
background: var(--primary-color);
color: white;
}
.chat-item-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.2em;
}
.chat-item-info {
flex: 1;
}
.chat-item-name {
font-weight: 500;
margin-bottom: 3px;
}
.chat-item-last-msg {
font-size: 0.8em;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-item-badge {
position: absolute;
right: 10px;
top: 10px;
background: #f44336;
color: white;
width: 18px;
height: 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7em;
opacity: 0;
transform: scale(0);
transition: all 0.3s ease;
}
.chat-item-badge.show {
opacity: 1;
transform: scale(1);
}
/* 消息分组样式 */
.message-date-divider {
text-align: center;
margin: 20px 0;
position: relative;
}
.message-date-divider::before {
content: '';
position: absolute;
left: 0;
top: 50%;
width: 100%;
height: 1px;
background: rgba(0,0,0,0.1);
}
.message-date-divider span {
background: #e5ddd5;
padding: 0 10px;
color: #666;
font-size: 0.8em;
position: relative;
z-index: 1;
}
.chat-item-actions {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
display: none;
gap: 5px;
}
.chat-item:hover .chat-item-actions {
display: flex;
}
.chat-delete-btn {
background: #dc3545;
color: white;
border: none;
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
opacity: 0.8;
}
.chat-delete-btn:hover {
opacity: 1;
transform: scale(1.1);
}
/* 确聊天项有足够的右边距来容按钮 */
.chat-item {
padding-right: 45px;
}
/* Toast 提示样式 */
.toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px 20px;
border-radius: 20px;
font-size: 0.9em;
z-index: 1000;
opacity: 0;
transition: opacity 0.3s ease;
}
.toast.show {
opacity: 1;
}
/* 添加确认对话框样式 */
.confirm-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
z-index: 1001;
max-width: 300px;
width: 90%;
}
.confirm-dialog-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
.confirm-dialog-buttons button {
padding: 8px 15px;
border: none;
border-radius: 5px;
cursor: pointer;
}
.confirm-dialog-buttons .confirm-btn {
background: #dc3545;
color: white;
}
.confirm-dialog-buttons .cancel-btn {
background: #6c757d;
color: white;
}
/* 修改导航栏和消息气泡的移动端样式 */
@media (max-width: 768px) {
/* 导航栏优化 */
.chat-header {
padding: 0 15px;
height: 50px;
justify-content: space-between;
}
/* 移动端 IP 显示优化 */
.connection-status {
font-size: 0.85em;
padding: 4px 10px;
background: rgba(74, 144, 226, 0.1);
border-radius: 15px;
display: flex;
align-items: center;
gap: 6px;
}
.ip-display {
display: flex !important; /* 强制显示 IP */
border: none;
padding: 0;
color: var(--primary-color);
}
/* 左侧栏按钮优化 */
.mode-item {
margin: 8px 12px;
padding: 12px 16px;
border-radius: 12px;
background: #f8f9fa;
transition: all 0.3s ease;
}
.mode-item.active {
background: var(--primary-color);
transform: scale(1.02);
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.2);
}
.mode-item i {
font-size: 1.3em;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
margin-right: 12px;
}
/* 聊天列表项优化 */
.chat-item {
margin: 8px 12px;
padding: 12px;
border-radius: 12px;
background: #f8f9fa;
transition: all 0.3s ease;
}
.chat-item:active {
transform: scale(0.98);
background: rgba(74, 144, 226, 0.1);
}
/* 返回按钮优化 */
.action-btn {
margin: 12px 15px;
padding: 14px;
border-radius: 12px;
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.2);
transition: all 0.3s ease;
}
.action-btn:active {
transform: scale(0.98);
box-shadow: 0 1px 4px rgba(74, 144, 226, 0.1);
}
/* 侧边栏头部优化 */
.sidebar-header {
padding: 20px 15px;
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
display: flex;
align-items: center;
justify-content: space-between;
min-height: 70px;
}
.sidebar-header span {
font-size: 1.1em;
font-weight: 500;
}
/* 分类标题优化 */
.chat-list-header {
padding: 15px;
font-size: 0.9em;
color: #666;
background: transparent;
font-weight: 600;
letter-spacing: 0.5px;
}
/* 滚动条优化 */
.chat-list-content::-webkit-scrollbar {
width: 4px;
}
.chat-list-content::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1);
border-radius: 2px;
}
/* 添加分割线 */
.chat-modes {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
padding-bottom: 10px;
}
/* 优化动画效果 */
.sidebar {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.sidebar.visible {
transform: translateX(100%);
}
/* 未读消息提示优化 */
.chat-item-badge {
background: #ff4757;
font-weight: 600;
box-shadow: 0 2px 4px rgba(255, 71, 87, 0.3);
}
/* IP地址显示优化 */
#currentIp {
font-size: 0.9em;
opacity: 0.9;
background: rgba(255, 255, 255, 0.2);
padding: 4px 10px;
border-radius: 10px;
}
}
/* 超小屏幕优化 */
@media (max-width: 360px) {
.mode-item {
margin: 6px 10px;
padding: 10px 14px;
}
.chat-item {
margin: 6px 10px;
padding: 10px;
}
.action-btn {
margin: 10px;
padding: 12px;
}
.chat-item-avatar {
width: 35px;
height: 35px;
}
}
/* 添加触摸反馈 */
@media (hover: none) {
.mode-item:active,
.chat-item:active,
.action-btn:active {
transform: scale(0.98);
}
}
/* 添加侧边栏遮罩 */
.sidebar-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
opacity: 0;
transition: opacity 0.3s ease;
}
.sidebar-overlay.visible {
display: block;
opacity: 1;
}
/* 优化消息时间显示 */
.message-time {
font-size: 0.9em;
color: rgba(0, 0, 0, 0.5);
}
.message.sent .message-time {
color: rgba(0, 0, 0, 0.4);
}
/* 移动端侧边栏优化 */
@media (max-width: 768px) {
/* 侧边栏基础样式 */
.sidebar {
width: 100%;
max-width: 100%;
height: 100vh;
position: fixed;
left: -100%;
top: 0;
background: white;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1000;
display: flex;
flex-direction: column;
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
}
.sidebar.visible {
transform: translateX(100%);
}
/* 侧边栏头部 */
.sidebar-header {
padding: 16px;
background: var(--primary-color);
color: white;
font-size: 1.1em;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 60px;
}
/* 聊天模式选择器 */
.chat-modes {
padding: 12px;
}
.mode-item {
padding: 15px;
margin-bottom: 6px;
border-radius: 12px;
font-size: 1em;
display: flex;
align-items: center;
gap: 12px;
touch-action: manipulation;
}
.mode-item i {
font-size: 1.2em;
width: 24px;
text-align: center;
}
/* 聊天列表 */
.chat-list {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.chat-list-header {
padding: 12px 16px;
font-size: 0.95em;
font-weight: 500;
background: #f8f9fa;
border-bottom: 1px solid #eee;
}
.chat-list-content {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding: 8px;
}
/* 聊天项样式 */
.chat-item {
padding: 12px;
margin-bottom: 4px;
border-radius: 12px;
display: flex;
align-items: center;
gap: 12px;
position: relative;
touch-action: manipulation;
}
.chat-item-avatar {
width: 45px;
height: 45px;
border-radius: 50%;
flex-shrink: 0;
}
.chat-item-info {
flex: 1;
min-width: 0;
}
.chat-item-name {
font-size: 0.95em;
margin-bottom: 4px;
font-weight: 500;
}
.chat-item-last-msg {
font-size: 0.85em;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 返回按钮 */
.action-btn {
margin: 12px;
padding: 14px;
border-radius: 12px;
font-size: 0.95em;
}
/* 遮罩层优化 */
.sidebar-overlay {
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(2px);
}
/* 未读消息提示 */
.chat-item-badge {
width: 20px;
height: 20px;
font-size: 0.8em;
right: 8px;
top: 50%;
transform: translateY(-50%) scale(0);
}
.chat-item-badge.show {
transform: translateY(-50%) scale(1);
}
/* 添加滑动手势支持 */
.sidebar {
touch-action: pan-y pinch-zoom;
}
/* 优化触摸反馈 */
.mode-item:active,
.chat-item:active {
background: rgba(74, 144, 226, 0.1);
}
/* 安全区域适配 */
@supports (padding: max(0px)) {
.sidebar {
padding-top: max(16px, env(safe-area-inset-top));
padding-bottom: max(16px, env(safe-area-inset-bottom));
padding-left: max(16px, env(safe-area-inset-left));
padding-right: max(16px, env(safe-area-inset-right));
}
}
}
/* 超小屏幕优化 */
@media (max-width: 360px) {
.mode-item {
padding: 12px;
font-size: 0.9em;
}
.chat-item-avatar {
width: 40px;
height: 40px;
}
.chat-item-name {
font-size: 0.9em;
}
.chat-item-last-msg {
font-size: 0.8em;
}
}
.file-message {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: rgba(74, 144, 226, 0.05);
border-radius: 12px;
margin: 8px 0;
border: 1px solid rgba(74, 144, 226, 0.1);
transition: all 0.3s ease;
}
.file-message:hover {
background: rgba(74, 144, 226, 0.08);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.file-icon-wrapper {
width: 48px;
height: 48px;
border-radius: 8px;
background: var(--primary-color);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.file-icon-wrapper i {
font-size: 24px;
color: white;
}
.file-info {
flex: 1;
min-width: 0;
}
.file-info .file-name {
font-weight: 500;
font-size: 0.95em;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #333;
}
.file-details {
display: flex;
gap: 8px;
align-items: center;
}
.file-details .file-size {
font-size: 0.8em;
color: #666;
}
.file-details .file-type {
font-size: 0.75em;
background: var(--primary-color);
color: white;
padding: 2px 6px;
border-radius: 4px;
font-weight: 500;
}
.image-message {
position: relative;
max-width: 350px;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
}
.image-message:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.image-message img {
width: 100%;
height: auto;
display: block;
transition: transform 0.3s ease;
}
.image-message:hover img {
transform: scale(1.02);
}
/* 图片悬停下载按钮 */
.image-download-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
backdrop-filter: blur(2px);
pointer-events: none;
}
.image-message:hover .image-download-overlay {
opacity: 1;
pointer-events: auto;
}
/* 移动端图片点击状态 */
.image-message.mobile-clicked .image-download-overlay {
opacity: 1;
pointer-events: auto;
}
.image-download-btn {
width: 60px;
height: 60px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.image-download-btn:hover {
background: white;
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
}
.image-download-btn i {
font-size: 24px;
color: var(--primary-color);
}
/* 图片信息显示 */
.image-info-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
color: white;
padding: 12px;
transform: translateY(100%);
transition: transform 0.3s ease;
}
.image-message:hover .image-info-overlay,
.image-message.mobile-clicked .image-info-overlay {
transform: translateY(0);
}
.image-info-overlay .file-name {
font-size: 0.9em;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.image-info-overlay .file-size {
font-size: 0.8em;
opacity: 0.8;
}
.drag-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: none;
align-items: center;
justify-content: center;
flex-direction: column;
z-index: 1000;
}
.drag-overlay.active {
display: flex;
}
.drag-overlay i {
font-size: 48px;
color: var(--primary-color);
margin-bottom: 16px;
}
.drag-overlay h3 {
color: var(--primary-color);
font-size: 1.2em;
}
/* 优化复制按钮样式 */
.copy-btn {
position: absolute;
width: 32px;
height: 32px;
border-radius: 50%;
background: white;
border: 1px solid rgba(0, 0, 0, 0.1);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 0.3s ease;
z-index: 2;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
top: 50%;
transform: translateY(-50%);
}
/* 发送消息的复制按钮位置 */
.message-container.sent .copy-btn {
left: -40px; /* 调整到消息外侧 */
}
/* 接收消息的复制按钮位置 */
.message-container.received .copy-btn {
right: -40px; /* 调整到外 */
}
.message-container:hover .copy-btn {
opacity: 1;
}
.copy-btn:hover {
background: white;
transform: translateY(-50%) scale(1.1);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.copy-btn i {
color: var(--primary-color);
font-size: 1.1em;
}
/* 移动端适配 */
@media (max-width: 768px) {
.copy-btn {
opacity: 0.8; /* 移动端保持半透明可见 */
width: 28px;
height: 28px;
}
.message-container.sent .copy-btn {
left: -34px;
}
.message-container.received .copy-btn {
right: -34px;
}
/* 增加按钮点击区域 */
.copy-btn::before {
content: '';
position: absolute;
top: -8px;
right: -8px;
bottom: -8px;
left: -8px;
}
}
/* 优化复制成功提示 */
.copy-toast {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px 20px;
border-radius: 20px;
font-size: 14px;
z-index: 1000;
opacity: 0;
transition: all 0.3s ease;
pointer-events: none;
}
.copy-toast.show {
opacity: 1;
transform: translate(-50%, -10px);
}
/* 在现有的 .copy-btn 样式基础上添加 */
.copy-btn.download-btn {
background: var(--primary-color);
color: white;
}
.copy-btn.download-btn:hover {
background: var(--secondary-color);
color: white;
transform: translateY(-50%) scale(1.1);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.copy-btn.download-btn i {
color: white;
}
/* 添加上传按钮样式 */
.upload-btn {
background: none;
border: none;
color: var(--primary-color);
cursor: pointer;
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.upload-btn:hover {
transform: scale(1.1);
}
.upload-btn i {
font-size: 1.5em;
}
/* 隐藏文件输入框 */
.file-input {
display: none;
}
/* 添加系统消息样式 */
.system-message {
display: flex;
justify-content: center;
margin: 15px 0;
}
.system-content {
background: rgba(74, 144, 226, 0.1);
color: var(--primary-color);
padding: 8px 16px;
border-radius: 15px;
font-size: 0.9em;
display: flex;
align-items: center;
gap: 6px;
}
.system-content i {
font-size: 1em;
}
/* 历史消息样式 */
.history-message {
opacity: 1;
}
/* 历史消息中的图片不变灰 */
.history-message .image-message,
.history-message .media-message {
opacity: 1;
}
.history-badge {
background: rgba(255, 152, 0, 0.2);
color: #ff9800;
padding: 2px 6px;
border-radius: 8px;
font-size: 0.7em;
margin-left: 6px;
font-weight: 500;
}
.history-message .message {
border-left: 3px solid rgba(255, 152, 0, 0.3);
padding-left: 15px;
}
/* 但是文本消息保持历史样式 */
.history-message .message-content {
opacity: 0.8;
}
/* 优化历史消息在移动端的显示 */
@media (max-width: 768px) {
.history-badge {
font-size: 0.65em;
padding: 1px 4px;
}
.system-content {
font-size: 0.85em;
padding: 6px 12px;
}
}
/* 媒体消息通用样式 */
.media-message {
max-width: 400px;
border-radius: 12px;
overflow: hidden;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin: 8px 0;
position: relative;
transition: all 0.3s ease;
}
.media-message:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
/* 音频消息特殊样式 - 更宽的容器 */
.audio-message {
max-width: 450px;
min-width: 350px;
}
.media-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: rgba(74, 144, 226, 0.05);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
position: relative;
}
.media-header i {
font-size: 24px;
color: var(--primary-color);
width: 32px;
text-align: center;
}
.media-title {
flex: 1;
min-width: 0;
}
.media-title .file-name {
font-weight: 500;
font-size: 0.95em;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.media-title .file-size {
font-size: 0.8em;
color: #666;
}
/* 媒体播放器样式 */
.media-player {
width: 100%;
outline: none;
background: #000;
}
.audio-message .media-player {
height: 60px;
width: 100%;
background: #f8f9fa;
}
.video-message .media-player {
max-height: 300px;
background: #000;
}
/* 音频消息悬停下载按钮 */
.audio-download-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 80px;
background: linear-gradient(90deg, transparent, rgba(74, 144, 226, 0.1));
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.audio-message:hover .audio-download-overlay {
opacity: 1;
pointer-events: auto;
}
.audio-download-btn {
width: 50px;
height: 50px;
border-radius: 50%;
background: var(--primary-color);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.3);
}
.audio-download-btn:hover {
background: var(--secondary-color);
transform: scale(1.1);
box-shadow: 0 4px 16px rgba(74, 144, 226, 0.4);
}
.audio-download-btn i {
font-size: 20px;
color: white;
}
/* 视频消息悬停下载按钮 */
.video-download-overlay {
position: absolute;
top: 8px;
right: 8px;
opacity: 0;
transition: opacity 0.3s ease;
z-index: 10;
}
.video-message:hover .video-download-overlay {
opacity: 1;
}
.video-download-btn {
width: 44px;
height: 44px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.7);
border: 2px solid white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.video-download-btn:hover {
background: rgba(0, 0, 0, 0.9);
transform: scale(1.1);
}
.video-download-btn i {
font-size: 18px;
color: white;
}
/* 响应式设计 */
@media (max-width: 768px) {
.media-message {
max-width: 100%;
}
/* 移动端图片消息优化 */
.image-message {
max-width: 100%;
margin: 12px 0;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.image-message img {
width: 100%;
height: auto;
display: block;
}
/* 移动端默认隐藏下载按钮,点击后才显示 */
.image-download-overlay {
opacity: 0;
pointer-events: none;
background: rgba(0, 0, 0, 0.6);
}
/* 移动端点击状态显示下载按钮 */
.image-message.mobile-clicked .image-download-overlay {
opacity: 1;
pointer-events: auto;
}
.image-download-btn {
width: 50px;
height: 50px;
background: rgba(255, 255, 255, 0.95);
}
.image-download-btn i {
font-size: 20px;
}
/* 移动端音频消息优化 */
.audio-message {
max-width: 100%;
min-width: 300px;
margin: 12px 0;
}
/* 移动端音频下载按钮始终可见 */
.audio-download-overlay {
opacity: 1;
pointer-events: auto;
width: 70px;
background: linear-gradient(90deg, transparent, rgba(74, 144, 226, 0.08));
}
.audio-download-btn {
width: 44px;
height: 44px;
}
.audio-download-btn i {
font-size: 18px;
}
/* 移动端音频播放器优化 */
.audio-message .media-player {
height: 70px;
padding: 8px;
background: #f8f9fa;
border-radius: 0 0 12px 12px;
}
/* 移动端媒体头部优化 */
.audio-message .media-header {
padding: 14px 16px;
background: rgba(74, 144, 226, 0.08);
}
.audio-message .media-header i {
font-size: 26px;
width: 36px;
color: var(--primary-color);
}
.audio-message .media-title .file-name {
font-size: 1em;
font-weight: 600;
margin-bottom: 4px;
}
.audio-message .media-title .file-size {
font-size: 0.85em;
color: #666;
font-weight: 500;
}
/* 移动端视频下载按钮 */
.video-download-overlay {
opacity: 1;
top: 12px;
right: 12px;
}
.video-download-btn {
width: 40px;
height: 40px;
background: rgba(0, 0, 0, 0.8);
border: 2px solid rgba(255, 255, 255, 0.9);
}
.video-download-btn i {
font-size: 16px;
}
.video-message .media-player {
max-height: 250px;
}
.media-header {
padding: 10px;
}
.file-message {
padding: 10px;
}
.file-icon-wrapper {
width: 40px;
height: 40px;
}
.file-icon-wrapper i {
font-size: 20px;
}
/* 移动端图片信息优化 */
.image-info-overlay {
transform: translateY(100%);
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
padding: 8px 12px;
}
.image-info-overlay .file-name {
font-size: 0.85em;
}
.image-info-overlay .file-size {
font-size: 0.75em;
}
/* 移动端图片点击提示 */
.image-click-hint {
position: absolute;
top: 8px;
left: 8px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.75em;
opacity: 0.9;
transition: opacity 0.3s ease;
pointer-events: none;
font-weight: 500;
backdrop-filter: blur(4px);
}
.image-message.mobile-clicked .image-click-hint {
opacity: 0;
pointer-events: none;
}
/* 移动端触摸优化 */
.image-download-btn:active,
.audio-download-btn:active,
.video-download-btn:active {
transform: scale(0.95);
}
/* 移动端文件消息优化 */
.file-message {
padding: 10px;
margin: 8px 0;
}
.file-icon-wrapper {
width: 40px;
height: 40px;
}
.file-icon-wrapper i {
font-size: 20px;
}
.file-info .file-name {
font-size: 0.9em;
}
.file-details .file-size {
font-size: 0.75em;
}
.file-details .file-type {
font-size: 0.7em;
padding: 1px 4px;
}
}
/* 播放器控件优化 */
.media-player::-webkit-media-controls-panel {
background-color: rgba(255, 255, 255, 0.9);
}
.audio-message .media-player::-webkit-media-controls-panel {
background-color: #f8f9fa;
}
/* 音频播放器进度条优化 */
.audio-message .media-player::-webkit-media-controls-timeline {
background-color: rgba(74, 144, 226, 0.2);
border-radius: 2px;
margin: 0 8px;
}
.audio-message .media-player::-webkit-media-controls-current-time-display,
.audio-message .media-player::-webkit-media-controls-time-remaining-display {
color: var(--primary-color);
font-size: 12px;
}
/* 音频播放按钮优化 */
.audio-message .media-player::-webkit-media-controls-play-button {
background-color: var(--primary-color);
border-radius: 50%;
}
/* 音频消息容器优化 */
.audio-message {
transition: all 0.3s ease;
}
.audio-message:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* 加载状态 */
.media-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: #666;
}
.media-loading i {
animation: spin 1s linear infinite;
margin-right: 8px;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* 超小屏幕优化 */
@media (max-width: 480px) {
.audio-message {
min-width: 100px;
margin: 6px 0;
}
.audio-message .media-header {
padding: 12px 14px;
}
.audio-message .media-header i {
font-size: 24px;
width: 32px;
}
.audio-message .media-title .file-name {
font-size: 0.95em;
margin-bottom: 3px;
}
.audio-message .media-title .file-size {
font-size: 0.8em;
}
.audio-download-overlay {
width: 60px;
}
.audio-download-btn {
width: 40px;
height: 40px;
}
.audio-download-btn i {
font-size: 16px;
}
.audio-message .media-player {
height: 65px;
padding: 6px;
}
.image-download-btn {
width: 44px;
height: 44px;
}
.image-download-btn i {
font-size: 18px;
}
.video-download-btn {
width: 36px;
height: 36px;
}
.video-download-btn i {
font-size: 14px;
}
/* 超小屏幕播放器控件 */
.audio-message .media-player::-webkit-media-controls-play-button {
width: 42px;
height: 42px;
margin: 0 8px 0 6px;
}
.audio-message .media-player::-webkit-media-controls-timeline {
min-width: 100px;
margin: 0 6px;
}
.audio-message .media-player::-webkit-media-controls-current-time-display,
.audio-message .media-player::-webkit-media-controls-time-remaining-display {
font-size: 13px;
margin: 0 3px;
}
.audio-message .media-player::-webkit-media-controls-volume-slider {
width: 50px;
margin: 0 6px;
}
.audio-message .media-player::-webkit-media-controls-mute-button {
width: 28px;
height: 28px;
margin: 0 3px;
}
/* 超小屏幕文件消息优化 */
.file-message {
padding: 8px;
gap: 8px;
}
.file-icon-wrapper {
width: 36px;
height: 36px;
}
.file-icon-wrapper i {
font-size: 18px;
}
.file-info .file-name {
font-size: 0.85em;
}
.file-details .file-size {
font-size: 0.7em;
}
.file-details .file-type {
font-size: 0.65em;
padding: 1px 3px;
}
}
/* 管理员设置样式 */
.admin-settings {
padding: 15px;
border-bottom: 1px solid #eee;
background: #f8f9fa;
}
.admin-settings-header {
font-size: 0.9em;
font-weight: 600;
color: #666;
margin-bottom: 10px;
letter-spacing: 0.5px;
}
.admin-input-container {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 8px;
}
.admin-ip-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 0.9em;
outline: none;
transition: border-color 0.3s ease;
}
.admin-ip-input:focus {
border-color: var(--primary-color);
}
.admin-set-btn {
width: 36px;
height: 36px;
border: none;
border-radius: 6px;
background: var(--primary-color);
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.admin-set-btn:hover {
background: var(--secondary-color);
transform: scale(1.05);
}
.admin-set-btn i {
font-size: 1.1em;
}
.admin-status {
padding: 6px 10px;
border-radius: 15px;
background: rgba(0, 0, 0, 0.05);
text-align: center;
}
.admin-status-text {
font-size: 0.8em;
color: #666;
font-weight: 500;
}
.admin-status.admin-set .admin-status-text {
color: #28a745;
}
.admin-status.is-admin .admin-status-text {
color: #dc3545;
font-weight: 600;
}
/* 管理员功能样式 */
.admin-functions {
padding: 15px;
border-bottom: 1px solid #eee;
background: linear-gradient(135deg, #fff5f5 0%, #fff0f0 100%);
}
.admin-functions-header {
font-size: 0.9em;
font-weight: 600;
color: #dc3545;
margin-bottom: 10px;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 6px;
}
.admin-functions-header::before {
content: "👑";
font-size: 1.2em;
}
.admin-function-btn {
width: 100%;
padding: 12px 15px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
color: white;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
font-size: 0.9em;
font-weight: 500;
transition: all 0.3s ease;
margin-bottom: 8px;
}
.admin-function-btn:hover {
background: linear-gradient(135deg, #c82333 0%, #bd2130 100%);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
}
.admin-function-btn:active {
transform: translateY(0);
}
.admin-function-btn i {
font-size: 1.2em;
}
.clear-all-btn {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
}
.clear-all-btn:hover {
background: linear-gradient(135deg, #bd2130 0%, #a71e2a 100%);
}
/* 移动端管理员样式优化 */
@media (max-width: 768px) {
.admin-settings {
margin: 0 12px;
border-radius: 12px;
background: #f8f9fa;
}
.admin-input-container {
gap: 10px;
}
.admin-ip-input {
padding: 10px 14px;
font-size: 16px; /* 防止iOS缩放 */
}
.admin-set-btn {
width: 40px;
height: 40px;
}
.admin-functions {
margin: 0 12px;
border-radius: 12px;
background: linear-gradient(135deg, #fff5f5 0%, #fff0f0 100%);
}
.admin-function-btn {
padding: 14px 16px;
font-size: 1em;
border-radius: 10px;
}
.admin-function-btn:active {
transform: scale(0.98);
}
}
/* 超小屏幕优化 */
@media (max-width: 360px) {
.mode-item {
padding: 12px;
font-size: 0.9em;
}
.chat-item-avatar {
width: 40px;
height: 40px;
}
.chat-item-name {
font-size: 0.9em;
}
.chat-item-last-msg {
font-size: 0.8em;
}
}
.file-message {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: rgba(74, 144, 226, 0.05);
border-radius: 12px;
margin: 8px 0;
border: 1px solid rgba(74, 144, 226, 0.1);
transition: all 0.3s ease;
}
.file-message:hover {
background: rgba(74, 144, 226, 0.08);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.file-icon-wrapper {
width: 48px;
height: 48px;
border-radius: 8px;
background: var(--primary-color);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.file-icon-wrapper i {
font-size: 24px;
color: white;
}
.file-info {
flex: 1;
min-width: 0;
}
.file-info .file-name {
font-weight: 500;
font-size: 0.95em;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #333;
}
.file-details {
display: flex;
gap: 8px;
align-items: center;
}
.file-details .file-size {
font-size: 0.8em;
color: #666;
}
.file-details .file-type {
font-size: 0.75em;
background: var(--primary-color);
color: white;
padding: 2px 6px;
border-radius: 4px;
font-weight: 500;
}
.image-message {
position: relative;
max-width: 350px;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
}
.image-message:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.image-message img {
width: 100%;
height: auto;
display: block;
transition: transform 0.3s ease;
}
.image-message:hover img {
transform: scale(1.02);
}
/* 图片悬停下载按钮 */
.image-download-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
backdrop-filter: blur(2px);
pointer-events: none;
}
.image-message:hover .image-download-overlay {
opacity: 1;
pointer-events: auto;
}
/* 移动端图片点击状态 */
.image-message.mobile-clicked .image-download-overlay {
opacity: 1;
pointer-events: auto;
}
.image-download-btn {
width: 60px;
height: 60px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.image-download-btn:hover {
background: white;
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
}
.image-download-btn i {
font-size: 24px;
color: var(--primary-color);
}
/* 图片信息显示 */
.image-info-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
color: white;
padding: 12px;
transform: translateY(100%);
transition: transform 0.3s ease;
}
.image-message:hover .image-info-overlay,
.image-message.mobile-clicked .image-info-overlay {
transform: translateY(0);
}
.image-info-overlay .file-name {
font-size: 0.9em;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.image-info-overlay .file-size {
font-size: 0.8em;
opacity: 0.8;
}
.drag-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: none;
align-items: center;
justify-content: center;
flex-direction: column;
z-index: 1000;
}
.drag-overlay.active {
display: flex;
}
.drag-overlay i {
font-size: 48px;
color: var(--primary-color);
margin-bottom: 16px;
}
.drag-overlay h3 {
color: var(--primary-color);
font-size: 1.2em;
}
/* 优化复制按钮样式 */
.copy-btn {
position: absolute;
width: 32px;
height: 32px;
border-radius: 50%;
background: white;
border: 1px solid rgba(0, 0, 0, 0.1);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 0.3s ease;
z-index: 2;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
top: 50%;
transform: translateY(-50%);
}
/* 发送消息的复制按钮位置 */
.message-container.sent .copy-btn {
left: -40px; /* 调整到消息外侧 */
}
/* 接收消息的复制按钮位置 */
.message-container.received .copy-btn {
right: -40px; /* 调整到外 */
}
.message-container:hover .copy-btn {
opacity: 1;
}
.copy-btn:hover {
background: white;
transform: translateY(-50%) scale(1.1);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.copy-btn i {
color: var(--primary-color);
font-size: 1.1em;
}
/* 移动端适配 */
@media (max-width: 768px) {
.copy-btn {
opacity: 0.8; /* 移动端保持半透明可见 */
width: 28px;
height: 28px;
}
.message-container.sent .copy-btn {
left: -34px;
}
.message-container.received .copy-btn {
right: -34px;
}
/* 增加按钮点击区域 */
.copy-btn::before {
content: '';
position: absolute;
top: -8px;
right: -8px;
bottom: -8px;
left: -8px;
}
}
/* 优化复制成功提示 */
.copy-toast {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px 20px;
border-radius: 20px;
font-size: 14px;
z-index: 1000;
opacity: 0;
transition: all 0.3s ease;
pointer-events: none;
}
.copy-toast.show {
opacity: 1;
transform: translate(-50%, -10px);
}
/* 在现有的 .copy-btn 样式基础上添加 */
.copy-btn.download-btn {
background: var(--primary-color);
color: white;
}
.copy-btn.download-btn:hover {
background: var(--secondary-color);
color: white;
transform: translateY(-50%) scale(1.1);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.copy-btn.download-btn i {
color: white;
}
/* 添加上传按钮样式 */
.upload-btn {
background: none;
border: none;
color: var(--primary-color);
cursor: pointer;
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.upload-btn:hover {
transform: scale(1.1);
}
.upload-btn i {
font-size: 1.5em;
}
/* 隐藏文件输入框 */
.file-input {
display: none;
}
/* 添加系统消息样式 */
.system-message {
display: flex;
justify-content: center;
margin: 15px 0;
}
.system-content {
background: rgba(74, 144, 226, 0.1);
color: var(--primary-color);
padding: 8px 16px;
border-radius: 15px;
font-size: 0.9em;
display: flex;
align-items: center;
gap: 6px;
}
.system-content i {
font-size: 1em;
}
/* 历史消息样式 */
.history-message {
opacity: 1;
}
/* 历史消息中的图片不变灰 */
.history-message .image-message,
.history-message .media-message {
opacity: 1;
}
.history-badge {
background: rgba(255, 152, 0, 0.2);
color: #ff9800;
padding: 2px 6px;
border-radius: 8px;
font-size: 0.7em;
margin-left: 6px;
font-weight: 500;
}
.history-message .message {
border-left: 3px solid rgba(255, 152, 0, 0.3);
padding-left: 15px;
}
/* 但是文本消息保持历史样式 */
.history-message .message-content {
opacity: 0.8;
}
/* 优化历史消息在移动端的显示 */
@media (max-width: 768px) {
.history-badge {
font-size: 0.65em;
padding: 1px 4px;
}
.system-content {
font-size: 0.85em;
padding: 6px 12px;
}
}
/* 媒体消息通用样式 */
.media-message {
max-width: 400px;
border-radius: 12px;
overflow: hidden;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin: 8px 0;
position: relative;
transition: all 0.3s ease;
}
.media-message:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
/* 音频消息特殊样式 - 更宽的容器 */
.audio-message {
max-width: 450px;
min-width: 350px;
}
.media-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: rgba(74, 144, 226, 0.05);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
position: relative;
}
.media-header i {
font-size: 24px;
color: var(--primary-color);
width: 32px;
text-align: center;
}
.media-title {
flex: 1;
min-width: 0;
}
.media-title .file-name {
font-weight: 500;
font-size: 0.95em;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.media-title .file-size {
font-size: 0.8em;
color: #666;
}
/* 媒体播放器样式 */
.media-player {
width: 100%;
outline: none;
background: #000;
}
.audio-message .media-player {
height: 60px;
width: 100%;
background: #f8f9fa;
}
.video-message .media-player {
max-height: 300px;
background: #000;
}
/* 音频消息悬停下载按钮 */
.audio-download-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 80px;
background: linear-gradient(90deg, transparent, rgba(74, 144, 226, 0.1));
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.audio-message:hover .audio-download-overlay {
opacity: 1;
pointer-events: auto;
}
.audio-download-btn {
width: 50px;
height: 50px;
border-radius: 50%;
background: var(--primary-color);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.3);
}
.audio-download-btn:hover {
background: var(--secondary-color);
transform: scale(1.1);
box-shadow: 0 4px 16px rgba(74, 144, 226, 0.4);
}
.audio-download-btn i {
font-size: 20px;
color: white;
}
/* 视频消息悬停下载按钮 */
.video-download-overlay {
position: absolute;
top: 8px;
right: 8px;
opacity: 0;
transition: opacity 0.3s ease;
z-index: 10;
}
.video-message:hover .video-download-overlay {
opacity: 1;
}
.video-download-btn {
width: 44px;
height: 44px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.7);
border: 2px solid white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.video-download-btn:hover {
background: rgba(0, 0, 0, 0.9);
transform: scale(1.1);
}
.video-download-btn i {
font-size: 18px;
color: white;
}
/* 响应式设计 */
@media (max-width: 768px) {
.media-message {
max-width: 100%;
}
/* 移动端图片消息优化 */
.image-message {
max-width: 100%;
margin: 12px 0;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.image-message img {
width: 100%;
height: auto;
display: block;
}
/* 移动端默认隐藏下载按钮,点击后才显示 */
.image-download-overlay {
opacity: 0;
pointer-events: none;
background: rgba(0, 0, 0, 0.6);
}
/* 移动端点击状态显示下载按钮 */
.image-message.mobile-clicked .image-download-overlay {
opacity: 1;
pointer-events: auto;
}
.image-download-btn {
width: 50px;
height: 50px;
background: rgba(255, 255, 255, 0.95);
}
.image-download-btn i {
font-size: 20px;
}
/* 移动端音频消息优化 */
.audio-message {
max-width: 100%;
min-width: 300px;
margin: 12px 0;
}
/* 移动端音频下载按钮始终可见 */
.audio-download-overlay {
opacity: 1;
pointer-events: auto;
width: 70px;
background: linear-gradient(90deg, transparent, rgba(74, 144, 226, 0.08));
}
.audio-download-btn {
width: 44px;
height: 44px;
}
.audio-download-btn i {
font-size: 18px;
}
/* 移动端音频播放器优化 */
.audio-message .media-player {
height: 70px;
padding: 8px;
background: #f8f9fa;
border-radius: 0 0 12px 12px;
}
/* 移动端媒体头部优化 */
.audio-message .media-header {
padding: 14px 16px;
background: rgba(74, 144, 226, 0.08);
}
.audio-message .media-header i {
font-size: 26px;
width: 36px;
color: var(--primary-color);
}
.audio-message .media-title .file-name {
font-size: 1em;
font-weight: 600;
margin-bottom: 4px;
}
.audio-message .media-title .file-size {
font-size: 0.85em;
color: #666;
font-weight: 500;
}
/* 移动端视频下载按钮 */
.video-download-overlay {
opacity: 1;
top: 12px;
right: 12px;
}
.video-download-btn {
width: 40px;
height: 40px;
background: rgba(0, 0, 0, 0.8);
border: 2px solid rgba(255, 255, 255, 0.9);
}
.video-download-btn i {
font-size: 16px;
}
.video-message .media-player {
max-height: 250px;
}
.media-header {
padding: 10px;
}
.file-message {
padding: 10px;
}
.file-icon-wrapper {
width: 40px;
height: 40px;
}
.file-icon-wrapper i {
font-size: 20px;
}
/* 移动端图片信息优化 */
.image-info-overlay {
transform: translateY(100%);
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
padding: 8px 12px;
}
.image-info-overlay .file-name {
font-size: 0.85em;
}
.image-info-overlay .file-size {
font-size: 0.75em;
}
/* 移动端图片点击提示 */
.image-click-hint {
position: absolute;
top: 8px;
left: 8px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.75em;
opacity: 0.9;
transition: opacity 0.3s ease;
pointer-events: none;
font-weight: 500;
backdrop-filter: blur(4px);
}
.image-message.mobile-clicked .image-click-hint {
opacity: 0;
pointer-events: none;
}
/* 移动端触摸优化 */
.image-download-btn:active,
.audio-download-btn:active,
.video-download-btn:active {
transform: scale(0.95);
}
/* 移动端文件消息优化 */
.file-message {
padding: 10px;
margin: 8px 0;
}
.file-icon-wrapper {
width: 40px;
height: 40px;
}
.file-icon-wrapper i {
font-size: 20px;
}
.file-info .file-name {
font-size: 0.9em;
}
.file-details .file-size {
font-size: 0.75em;
}
.file-details .file-type {
font-size: 0.7em;
padding: 1px 4px;
}
}
/* 播放器控件优化 */
.media-player::-webkit-media-controls-panel {
background-color: rgba(255, 255, 255, 0.9);
}
.audio-message .media-player::-webkit-media-controls-panel {
background-color: #f8f9fa;
}
/* 音频播放器进度条优化 */
.audio-message .media-player::-webkit-media-controls-timeline {
background-color: rgba(74, 144, 226, 0.2);
border-radius: 2px;
margin: 0 8px;
}
.audio-message .media-player::-webkit-media-controls-current-time-display,
.audio-message .media-player::-webkit-media-controls-time-remaining-display {
color: var(--primary-color);
font-size: 12px;
}
/* 音频播放按钮优化 */
.audio-message .media-player::-webkit-media-controls-play-button {
background-color: var(--primary-color);
border-radius: 50%;
}
/* 音频消息容器优化 */
.audio-message {
transition: all 0.3s ease;
}
.audio-message:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* 加载状态 */
.media-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: #666;
}
.media-loading i {
animation: spin 1s linear infinite;
margin-right: 8px;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* 超小屏幕优化 */
@media (max-width: 480px) {
.audio-message {
min-width: 100px;
margin: 6px 0;
}
.audio-message .media-header {
padding: 12px 14px;
}
.audio-message .media-header i {
font-size: 24px;
width: 32px;
}
.audio-message .media-title .file-name {
font-size: 0.95em;
margin-bottom: 3px;
}
.audio-message .media-title .file-size {
font-size: 0.8em;
}
.audio-download-overlay {
width: 60px;
}
.audio-download-btn {
width: 40px;
height: 40px;
}
.audio-download-btn i {
font-size: 16px;
}
.audio-message .media-player {
height: 65px;
padding: 6px;
}
.image-download-btn {
width: 44px;
height: 44px;
}
.image-download-btn i {
font-size: 18px;
}
.video-download-btn {
width: 36px;
height: 36px;
}
.video-download-btn i {
font-size: 14px;
}
/* 超小屏幕播放器控件 */
.audio-message .media-player::-webkit-media-controls-play-button {
width: 42px;
height: 42px;
margin: 0 8px 0 6px;
}
.audio-message .media-player::-webkit-media-controls-timeline {
min-width: 100px;
margin: 0 6px;
}
.audio-message .media-player::-webkit-media-controls-current-time-display,
.audio-message .media-player::-webkit-media-controls-time-remaining-display {
font-size: 13px;
margin: 0 3px;
}
.audio-message .media-player::-webkit-media-controls-volume-slider {
width: 50px;
margin: 0 6px;
}
.audio-message .media-player::-webkit-media-controls-mute-button {
width: 28px;
height: 28px;
margin: 0 3px;
}
/* 超小屏幕文件消息优化 */
.file-message {
padding: 8px;
gap: 8px;
}
.file-icon-wrapper {
width: 36px;
height: 36px;
}
.file-icon-wrapper i {
font-size: 18px;
}
.file-info .file-name {
font-size: 0.85em;
}
.file-details .file-size {
font-size: 0.7em;
}
.file-details .file-type {
font-size: 0.65em;
padding: 1px 3px;
}
}
/* 管理员设置样式 */
.admin-settings {
padding: 15px;
border-bottom: 1px solid #eee;
background: #f8f9fa;
}
.admin-settings-header {
font-size: 0.9em;
font-weight: 600;
color: #666;
margin-bottom: 10px;
letter-spacing: 0.5px;
}
.admin-input-container {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 8px;
}
.admin-ip-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 0.9em;
outline: none;
transition: border-color 0.3s ease;
}
.admin-ip-input:focus {
border-color: var(--primary-color);
}
.admin-set-btn {
width: 36px;
height: 36px;
border: none;
border-radius: 6px;
background: var(--primary-color);
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.admin-set-btn:hover {
background: var(--secondary-color);
transform: scale(1.05);
}
.admin-set-btn i {
font-size: 1.1em;
}
.admin-status {
padding: 6px 10px;
border-radius: 15px;
background: rgba(0, 0, 0, 0.05);
text-align: center;
}
.admin-status-text {
font-size: 0.8em;
color: #666;
font-weight: 500;
}
.admin-status.admin-set .admin-status-text {
color: #28a745;
}
.admin-status.is-admin .admin-status-text {
color: #dc3545;
font-weight: 600;
}
/* 管理员功能样式 */
.admin-functions {
padding: 15px;
border-bottom: 1px solid #eee;
background: linear-gradient(135deg, #fff5f5 0%, #fff0f0 100%);
}
.admin-functions-header {
font-size: 0.9em;
font-weight: 600;
color: #dc3545;
margin-bottom: 10px;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 6px;
}
.admin-functions-header::before {
content: "👑";
font-size: 1.2em;
}
.admin-function-btn {
width: 100%;
padding: 12px 15px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
color: white;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
font-size: 0.9em;
font-weight: 500;
transition: all 0.3s ease;
margin-bottom: 8px;
}
.admin-function-btn:hover {
background: linear-gradient(135deg, #c82333 0%, #bd2130 100%);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
}
.admin-function-btn:active {
transform: translateY(0);
}
.admin-function-btn i {
font-size: 1.2em;
}
.clear-all-btn {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
}
.clear-all-btn:hover {
background: linear-gradient(135deg, #bd2130 0%, #a71e2a 100%);
}
/* 移动端管理员样式优化 */
@media (max-width: 768px) {
.admin-settings {
margin: 0 12px;
border-radius: 12px;
background: #f8f9fa;
}
.admin-input-container {
gap: 10px;
}
.admin-ip-input {
padding: 10px 14px;
font-size: 16px; /* 防止iOS缩放 */
}
.admin-set-btn {
width: 40px;
height: 40px;
}
.admin-functions {
margin: 0 12px;
border-radius: 12px;
background: linear-gradient(135deg, #fff5f5 0%, #fff0f0 100%);
}
.admin-function-btn {
padding: 14px 16px;
font-size: 1em;
border-radius: 10px;
}
.admin-function-btn:active {
transform: scale(0.98);
}
}
/* 超小屏幕图片消息优化 */
.image-message {
margin: 8px 0;
border-radius: 10px;
}
.image-download-btn {
width: 44px;
height: 44px;
}
.image-download-btn i {
font-size: 18px;
}
.image-click-hint {
font-size: 0.7em;
padding: 3px 6px;
top: 6px;
left: 6px;
}
.image-info-overlay {
padding: 6px 10px;
}
.image-info-overlay .file-name {
font-size: 0.8em;
}
.image-info-overlay .file-size {
font-size: 0.7em;
}
/* 超小屏幕播放器控件 */
.audio-message .media-player::-webkit-media-controls-play-button {
width: 42px;
height: 42px;
margin: 0 8px 0 6px;
}
</style>
</head>
<body>
<div class="app-container">
<!-- 侧边栏 -->
<div class="sidebar" id="sidebar">
<button class="sidebar-toggle" onclick="toggleSidebar()">
<i class="bi bi-chevron-right"></i>
</button>
<div class="sidebar-header">
<span>聊天模式</span>
<span id="currentIp"></span>
</div>
<div class="sidebar-actions">
<a href="/" class="action-btn">
<i class="bi bi-file-earmark-arrow-up"></i>
<span>返回文件传输</span>
</a>
</div>
<!-- 管理员设置区域 -->
<div class="admin-settings">
<div class="admin-settings-header">管理员信息</div>
<div class="admin-status" id="adminStatus">
<span class="admin-status-text">正在检查管理员权限...</span>
</div>
</div>
<!-- 管理员功能区域(仅管理员可见) -->
<div class="admin-functions" id="adminFunctions" style="display: none;">
<div class="admin-functions-header">管理员功能</div>
<button class="admin-function-btn clear-all-btn" onclick="clearAllChatHistory()">
<i class="bi bi-trash3"></i>
<span>清空所有历史记录</span>
</button>
</div>
<div class="chat-modes">
<div class="mode-item active" data-mode="group">
<i class="bi bi-people-fill"></i>
<span>群聊</span>
</div>
<div class="mode-item" data-mode="private">
<i class="bi bi-person-fill"></i>
<span>私聊</span>
</div>
</div>
<div class="chat-list" id="chatList">
<div class="chat-list-header">聊天列表</div>
<div class="chat-list-content" id="chatListContent">
<!-- 聊天列表将通过 JavaScript 动态添加 -->
</div>
</div>
</div>
<!-- 主聊天区域 -->
<div class="chat-main">
<div class="chat-header">
<button class="menu-toggle" onclick="toggleSidebar()">
<div class="menu-icon">
<span></span>
<span></span>
<span></span>
</div>
</button>
<div class="chat-title-container">
<img src="/image/logo2.png" alt="Logo" style="height: 24px; width: auto; margin-right: 8px; vertical-align: middle;">
<span class="chat-title" id="chatTitle">内网群聊</span>
</div>
<div class="connection-status">
<span class="status-indicator" id="connectionIndicator"></span>
<span>在线</span>
<span class="ip-display">
<i class="bi bi-pc-display"></i>
<span id="headerIp"></span>
</span>
</div>
</div>
<div class="chat-messages" id="messageContainer">
<div class="drag-overlay" id="dragOverlay">
<i class="bi bi-cloud-upload"></i>
<h3>释放发送文件</h3>
</div>
<!-- 现有的消息内容 -->
</div>
<div class="chat-input-container">
<input type="file" id="fileInput" class="file-input">
<button class="upload-btn" onclick="document.getElementById('fileInput').click()">
<i class="bi bi-paperclip"></i>
</button>
<input type="text" class="chat-input" id="messageInput" placeholder="输入消息...">
<button class="send-btn" onclick="sendMessage()">
<i class="bi bi-send-fill"></i>
<span>发送</span>
</button>
</div>
</div>
</div>
<!-- 在 body 开始处添加遮罩层 -->
<div class="sidebar-overlay" id="sidebarOverlay" onclick="toggleSidebar()"></div>
<script>
let ws = null;
let currentMode = 'group';
let targetIp = '';
let myIp = '';
let sidebarVisible = false;
// 添加聊天管理相关变量
let chatHistory = {
group: [],
private: {}
};
let unreadMessages = {};
let currentChatTarget = null;
let isLoadingHistory = false; // 添加历史加载状态标识
// 添加IP名称映射相关变量
let ipNameMapping = {};
let ipAvatarMapping = {};
let availableUsers = [];
// 管理员相关变量
let adminIp = null;
let isAdmin = false;
// 初始化 WebSocket 连接
async function initializeChat() {
try {
// 获取真实IP
const response = await fetch('/get_ip');
const data = await response.json();
myIp = data.ip;
console.log('My IP:', myIp);
// 获取IP名称映射
await fetchIpNameMapping();
// 更新显示(使用名称)
const myName = getDisplayName(myIp);
document.getElementById('currentIp').textContent = `本机: ${myName}${myName !== myIp ? ` (${myIp})` : ''}`;
document.getElementById('headerIp').textContent = myName;
// 建立WebSocket连接
connectWebSocket();
initializeChatList();
// 从本地存储恢复聊天历史
loadLocalChatHistory();
// 初始化管理员功能
initializeAdminFeatures();
} catch (error) {
console.error('初始化失败:', error);
}
}
function connectWebSocket() {
ws = new WebSocket(`ws://${window.location.host}/ws`);
ws.onopen = function() {
console.log('WebSocket连接已建立');
document.getElementById('connectionIndicator').classList.add('connected');
};
ws.onmessage = function(event) {
console.log('收到消息:', event.data);
const data = JSON.parse(event.data);
// 处理系统消息
if (data.type === 'system') {
addSystemMessage(data.message);
return;
}
// 处理管理员操作消息
if (data.type === 'admin_action') {
if (data.action === 'clear_all_history') {
// 如果收到管理员清空历史的通知且不是自己发送的
if (data.admin_ip !== myIp) {
// 清空本地历史
chatHistory.group = [];
chatHistory.private = {};
unreadMessages = {};
// 清空显示
const messageContainer = document.getElementById('messageContainer');
messageContainer.innerHTML = '';
// 重新初始化聊天列表
initializeChatList();
// 切换到群聊模式
currentMode = 'group';
currentChatTarget = null;
document.getElementById('chatTitle').textContent = '内网群聊';
updateModeButtons('group');
addSystemMessage(`📢 ${data.message}`);
showToast('管理员已清空所有聊天记录');
}
}
return;
}
// 处理历史消息开始和结束标识
if (data.type === 'history_start') {
isLoadingHistory = true;
console.log('开始加载历史消息');
addSystemMessage(data.message);
return;
}
if (data.type === 'history_end') {
isLoadingHistory = false;
console.log('历史消息加载结束');
addSystemMessage(data.message);
// 保存到本地存储
saveLocalChatHistory();
// 滚动到最新消息
setTimeout(() => {
const messageContainer = document.getElementById('messageContainer');
messageContainer.scrollTop = messageContainer.scrollHeight;
}, 100);
return;
}
// 如果是自己发送的消息且已经显示过,并且不是历史消息,直接返回
if (data.ip === myIp && data.alreadyDisplayed && !data.is_history) {
console.log('跳过已显示的消息');
return;
}
// 处理历史消息的存储
const isHistory = data.is_history || isLoadingHistory;
console.log('处理消息:', {
message: data.message,
ip: data.ip,
isHistory: isHistory,
currentMode: currentMode,
isLoadingHistory: isLoadingHistory
});
// 处理私聊消息
if (data.targetIp) {
const chatPartner = data.ip === myIp ? data.targetIp : data.ip;
// 存储到私聊历史
if (!chatHistory.private[chatPartner]) {
chatHistory.private[chatPartner] = [];
}
// 检查是否已存在相同消息(避免重复)
const isDuplicate = chatHistory.private[chatPartner].some(msg =>
msg.timestamp === data.timestamp &&
msg.message === data.message &&
msg.ip === data.ip
);
if (!isDuplicate) {
chatHistory.private[chatPartner].push(data);
// 按时间排序
chatHistory.private[chatPartner].sort((a, b) =>
(a.timestamp || 0) - (b.timestamp || 0)
);
}
// 如果是当前聊天对象,显示消息
if (currentMode === 'private' && currentChatTarget === chatPartner) {
if (!isDuplicate) {
addMessage(data, isHistory);
}
} else if (!isHistory) {
// 如果不是历史消息且不是当前聊天,添加未读提醒
updateChatList(chatPartner, data.message);
addUnreadBadge(chatPartner);
}
} else {
// 处理群聊消息
const isDuplicate = chatHistory.group.some(msg =>
msg.timestamp === data.timestamp &&
msg.message === data.message &&
msg.ip === data.ip
);
if (!isDuplicate) {
chatHistory.group.push(data);
// 按时间排序
chatHistory.group.sort((a, b) =>
(a.timestamp || 0) - (b.timestamp || 0)
);
// 限制历史消息数量
if (chatHistory.group.length > 200) {
chatHistory.group = chatHistory.group.slice(-200);
}
}
// 在群聊模式下始终显示消息(包括历史消息)
if (currentMode === 'group') {
if (!isDuplicate || isHistory) {
console.log('显示群聊消息:', data, '是否历史:', isHistory);
addMessage(data, isHistory);
}
}
}
};
ws.onclose = function() {
console.log('WebSocket连接已关闭');
document.getElementById('connectionIndicator').classList.remove('connected');
setTimeout(connectWebSocket, 3000);
};
ws.onerror = function(error) {
console.error('WebSocket错误:', error);
};
}
// 本地存储相关函数
function saveLocalChatHistory() {
try {
const historyData = {
group: chatHistory.group,
private: chatHistory.private,
lastSaved: new Date().toISOString()
};
localStorage.setItem(`chatHistory_${myIp}`, JSON.stringify(historyData));
console.log('聊天历史已保存到本地存储');
} catch (error) {
console.error('保存本地聊天历史失败:', error);
}
}
function loadLocalChatHistory() {
try {
const savedHistory = localStorage.getItem(`chatHistory_${myIp}`);
if (savedHistory) {
const historyData = JSON.parse(savedHistory);
chatHistory.group = historyData.group || [];
chatHistory.private = historyData.private || {};
console.log('从本地存储加载聊天历史:', {
群聊消息: chatHistory.group.length,
私聊对话: Object.keys(chatHistory.private).length
});
// 更新聊天列表
Object.keys(chatHistory.private).forEach(ip => {
if (ip !== myIp && chatHistory.private[ip].length > 0) {
const lastMessage = chatHistory.private[ip][chatHistory.private[ip].length - 1];
updateChatList(ip, lastMessage.message || '文件消息');
}
});
}
} catch (error) {
console.error('加载本地聊天历史失败:', error);
}
}
// 清理本地存储的历史数据
function clearLocalChatHistory() {
try {
localStorage.removeItem(`chatHistory_${myIp}`);
chatHistory.group = [];
chatHistory.private = {};
console.log('本地聊天历史已清空');
} catch (error) {
console.error('清空本地历史失败:', error);
}
}
// 添加颜色生成函数
function generateColor(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
const colors = [
'#4CAF50', '#2196F3', '#9C27B0', '#FF9800',
'#E91E63', '#3F51B5', '#009688', '#795548'
];
return colors[Math.abs(hash) % colors.length];
}
// 创建头像元素的统一函数
function createAvatarElement(ip, size = '32px', fontSize = '1.1em') {
const avatar = generateAvatar(ip);
if (avatar.type === 'image') {
// 生成回退emoji
const emojis = ['🌟', '🎯', '🎨', '🎭', '🎪', '🎫', '🎮', '🎲', '🎸', '🎺',
'🎭', '🎪', '🎨', '🎯', '🌟', '🎲', '🎮', '🎫', '🎸', '🎺'];
const fallbackEmoji = emojis[ip.split('.')[3] % emojis.length];
return `
<div style="
width: ${size};
height: ${size};
border-radius: 50%;
background-color: ${avatar.color};
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: ${fontSize};
position: relative;
overflow: hidden;
">
<img src="${avatar.src}" alt="头像"
style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<span style="display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; align-items: center; justify-content: center;">${fallbackEmoji}</span>
</div>
`;
} else {
return `
<div style="
width: ${size};
height: ${size};
border-radius: 50%;
background-color: ${avatar.color};
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: ${fontSize};
">
${avatar.icon}
</div>
`;
}
}
//// / 生成用户头像
function generateAvatar(ip) {
// 如果有配置的头像,优先使用配置的头像
if (ipAvatarMapping[ip]) {
return {
type: 'image',
src: ipAvatarMapping[ip],
color: generateColor(ip)
};
}
// 使用固定的 emoji 数组作为头像
const emojis = ['🌟', '🎯', '🎨', '🎭', '🎪', '🎫', '🎮', '🎲', '🎸', '🎺',
'🎭', '🎪', '🎨', '🎯', '🌟', '🎲', '🎮', '🎫', '🎸', '🎺'];
const icon = emojis[ip.split('.')[3] % emojis.length];
return {
type: 'emoji',
icon: icon,
color: generateColor(ip)
};
}
// 修改消息显示函数
function addMessage(data, isHistory = false) {
const messageContainer = document.getElementById('messageContainer');
// 检查是否已存在相同消息(避免重复显示)
const existingMessages = messageContainer.querySelectorAll('.message-container');
for (let existingMsg of existingMessages) {
const existingTimestamp = existingMsg.getAttribute('data-timestamp');
const existingIp = existingMsg.getAttribute('data-ip');
const existingContent = existingMsg.querySelector('.message-content');
if (existingTimestamp == data.timestamp &&
existingIp === data.ip &&
existingContent &&
existingContent.textContent.includes(data.message)) {
console.log('跳过重复消息:', data);
return;
}
}
const messageWrapper = document.createElement('div');
const isCurrentUser = data.ip === myIp;
const avatar = generateAvatar(data.ip);
messageWrapper.className = `message-container ${isCurrentUser ? 'sent' : 'received'}`;
messageWrapper.setAttribute('data-timestamp', data.timestamp);
messageWrapper.setAttribute('data-ip', data.ip);
// 如果是历史消息,添加特殊样式
if (isHistory) {
messageWrapper.classList.add('history-message');
}
let messageContent = '';
if (data.type === 'file') {
try {
const fileData = JSON.parse(data.message);
console.log('解析文件数据:', fileData); // 添加调试日志
// 验证文件数据完整性
if (!fileData.fileName || !fileData.filePath) {
throw new Error('文件数据不完整');
}
const encodedFileData = encodeURIComponent(JSON.stringify(fileData));
const fileExtension = fileData.fileName.toLowerCase().split('.').pop();
const detectedFileType = getFileType(fileData.fileName, fileData.fileType || '');
const fileIcon = getFileIcon(detectedFileType, fileExtension);
// 根据文件类型显示不同的内容
if (detectedFileType === 'image' || fileData.isImage) {
messageContent = `
<div class="media-message image-message" onclick="handleImageClick(this, event)">
<img src="${fileData.filePath}" alt="${fileData.fileName}"
onclick="event.stopPropagation(); window.open('${fileData.filePath}', '_blank')"
loading="lazy">
<div class="image-click-hint">点击图片下载</div>
<div class="image-download-overlay">
<button class="image-download-btn" onclick="event.stopPropagation(); handleFileAction('${encodedFileData}')">
<i class="bi bi-download"></i>
</button>
</div>
<div class="image-info-overlay">
<div class="file-name">${fileData.fileName}</div>
<div class="file-size">${formatFileSize(fileData.fileSize)}</div>
</div>
</div>
`;
} else if (detectedFileType === 'audio') {
messageContent = `
<div class="media-message audio-message">
<div class="media-header">
<i class="${fileIcon}"></i>
<div class="media-title">
<div class="file-name">${fileData.fileName}</div>
<div class="file-size">${formatFileSize(fileData.fileSize)}</div>
</div>
</div>
<audio controls preload="metadata" class="media-player">
<source src="${fileData.filePath}" type="${fileData.fileType || 'audio/mpeg'}">
您的浏览器不支持音频播放。
</audio>
<div class="audio-download-overlay">
<button class="audio-download-btn" onclick="handleFileAction('${encodedFileData}')">
<i class="bi bi-download"></i>
</button>
</div>
</div>
`;
} else if (detectedFileType === 'video') {
messageContent = `
<div class="media-message video-message">
<div class="media-header">
<i class="${fileIcon}"></i>
<div class="media-title">
<div class="file-name">${fileData.fileName}</div>
<div class="file-size">${formatFileSize(fileData.fileSize)}</div>
</div>
</div>
<video controls preload="metadata" class="media-player">
<source src="${fileData.filePath}" type="${fileData.fileType || 'video/mp4'}">
您的浏览器不支持视频播放。
</video>
<div class="video-download-overlay">
<button class="video-download-btn" onclick="handleFileAction('${encodedFileData}')">
<i class="bi bi-download"></i>
</button>
</div>
</div>
`;
} else {
// 其他文件类型的通用显示
messageContent = `
<div class="file-message">
<div class="file-icon-wrapper">
<i class="${fileIcon}"></i>
</div>
<div class="file-info">
<div class="file-name">${fileData.fileName}</div>
<div class="file-details">
<span class="file-size">${formatFileSize(fileData.fileSize)}</span>
<span class="file-type">${detectedFileType.toUpperCase()}</span>
</div>
</div>
<button class="copy-btn download-btn" onclick="handleFileAction('${encodedFileData}')">
<i class="bi bi-download"></i>
</button>
</div>
`;
}
} catch (error) {
console.error('文件消息处理失败:', error);
messageContent = `
<div class="message-content error">
<i class="bi bi-exclamation-triangle"></i>
文件消息处理失败: ${error.message}
</div>
`;
}
} else {
messageContent = `
<div class="message-content">
${data.message}
<button class="copy-btn" onclick="copyMessage('${data.message.replace(/'/g, "\\'")}')">
<i class="bi bi-clipboard"></i>
</button>
</div>
`;
}
// 添加历史消息标识
const historyBadge = isHistory ? '<span class="history-badge">历史</span>' : '';
// 获取用户显示名称
const senderName = getDisplayName(data.ip);
const userDisplayText = isCurrentUser ? '我' : senderName;
messageWrapper.innerHTML = `
<div class="user-tag">${userDisplayText} ${historyBadge}</div>
<div class="avatar" style="background-color: transparent !important; padding: 0;">
${createAvatarElement(data.ip, '32px', '1.1em')}
</div>
<div class="message-content-wrapper">
<div class="message ${isCurrentUser ? 'sent' : 'received'}">
${messageContent}
<div class="message-info">
<div class="info-item ip-info">
<i class="bi bi-pc-display"></i>
<span>${senderName}</span>
</div>
<div class="info-item">
<i class="bi bi-clock"></i>
<span class="message-time">${formatTime(data.timestamp)}</span>
</div>
</div>
</div>
</div>
`;
messageContainer.appendChild(messageWrapper);
// 如果不是历史消息,滚动到底部
if (!isHistory && !isLoadingHistory) {
setTimeout(() => {
messageContainer.scrollTop = messageContainer.scrollHeight;
}, 50);
}
}
// 添加系统消息显示函数
function addSystemMessage(message) {
const messageContainer = document.getElementById('messageContainer');
const systemMessage = document.createElement('div');
systemMessage.className = 'system-message';
systemMessage.innerHTML = `
<div class="system-content">
<i class="bi bi-info-circle"></i>
<span>${message}</span>
</div>
`;
messageContainer.appendChild(systemMessage);
messageContainer.scrollTop = messageContainer.scrollHeight;
}
// 添加时间格式化函数
function formatTime(timestamp) {
const date = new Date(timestamp);
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const seconds = date.getSeconds().toString().padStart(2, '0');
return `${hours}:${minutes}:${seconds}`;
}
// 发送消息时保存到历史
function sendMessage() {
const messageInput = document.getElementById('messageInput');
const message = messageInput.value.trim();
if (!message || !ws || ws.readyState !== WebSocket.OPEN) {
return;
}
const messageData = {
message: message,
timestamp: new Date().getTime(),
alreadyDisplayed: true // 标记消息已在发送者端显示
};
if (currentMode === 'private' && currentChatTarget) {
messageData.targetIp = currentChatTarget;
}
try {
// 发送消息
ws.send(JSON.stringify(messageData));
messageInput.value = '';
// 添加到本地显示和历史记录
const localMessage = {
...messageData,
ip: myIp
};
// 存储消息到本地历史
if (currentMode === 'private' && currentChatTarget) {
if (!chatHistory.private[currentChatTarget]) {
chatHistory.private[currentChatTarget] = [];
}
chatHistory.private[currentChatTarget].push(localMessage);
// 保持时间排序
chatHistory.private[currentChatTarget].sort((a, b) =>
(a.timestamp || 0) - (b.timestamp || 0)
);
// 更新聊天列表
updateChatList(currentChatTarget, message);
} else {
chatHistory.group.push(localMessage);
// 保持时间排序
chatHistory.group.sort((a, b) =>
(a.timestamp || 0) - (b.timestamp || 0)
);
// 限制历史消息数量
if (chatHistory.group.length > 200) {
chatHistory.group = chatHistory.group.slice(-200);
}
}
// 显示消息
addMessage(localMessage);
// 滚动到最新消息
const messageContainer = document.getElementById('messageContainer');
messageContainer.scrollTop = messageContainer.scrollHeight;
// 保存到本地存储
saveLocalChatHistory();
} catch (error) {
console.error('发送消息失败:', error);
showToast('发送消息失败');
}
}
function handleKeyPress(event) {
if (event.key === 'Enter') {
sendMessage();
}
}
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebarOverlay');
const toggleIcon = sidebar.querySelector('.sidebar-toggle i');
sidebarVisible = !sidebarVisible;
sidebar.classList.toggle('visible');
overlay.classList.toggle('visible');
// 更新箭头方向
if (sidebarVisible) {
toggleIcon.classList.remove('bi-chevron-right');
toggleIcon.classList.add('bi-chevron-left');
document.body.style.overflow = 'hidden'; // 防止背景滚动
} else {
toggleIcon.classList.remove('bi-chevron-left');
toggleIcon.classList.add('bi-chevron-right');
document.body.style.overflow = '';
}
}
function switchMode(mode) {
// 清空消息容器
const messageContainer = document.getElementById('messageContainer');
messageContainer.innerHTML = '';
if (mode === 'private') {
// 私聊模式 - 显示用户选择界面
showPrivateChatSelector();
return; // 不立即切换模式,等用户选择后再切换
} else {
// 群聊模式
switchToGroupChat();
}
// 保存状态到本地存储
saveLocalChatHistory();
// 滚动到最新消息
setTimeout(() => {
messageContainer.scrollTop = messageContainer.scrollHeight;
}, 100);
}
// 添加更新模式按钮状态的函数
function updateModeButtons(activeMode) {
document.querySelectorAll('.mode-item').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`[data-mode="${activeMode}"]`).classList.add('active');
}
// 修改事件监听器绑定
document.addEventListener('DOMContentLoaded', function() {
// 为群聊和私聊按钮添加点击事件
document.querySelectorAll('.mode-item').forEach(item => {
item.addEventListener('click', function(e) {
e.preventDefault(); // 防止事件冒泡
const mode = this.getAttribute('data-mode');
switchMode(mode);
});
});
// 点击主聊天区域时关闭侧边栏(移动端)
document.querySelector('.chat-main').addEventListener('click', function(e) {
if (sidebarVisible && window.innerWidth <= 768) {
if (!e.target.closest('.menu-toggle')) {
toggleSidebar();
}
}
});
// 为输入框添加回车键事件监听
const messageInput = document.getElementById('messageInput');
messageInput.addEventListener('keypress', function(event) {
if (event.key === 'Enter') {
sendMessage();
}
});
});
// 优化复制功能
async function copyMessage(text) {
try {
// 使用 Clipboard API
await navigator.clipboard.writeText(text);
showCopyToast('复制成功');
} catch (err) {
// 降级方案
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
textarea.style.top = '0';
document.body.appendChild(textarea);
try {
// 选择文本
if (navigator.userAgent.match(/ipad|iphone/i)) {
// iOS 设备特殊处理
const range = document.createRange();
range.selectNodeContents(textarea);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
textarea.setSelectionRange(0, 999999);
} else {
textarea.select();
}
// 执行复制
const successful = document.execCommand('copy');
if (successful) {
showCopyToast('复制成功');
} else {
showCopyToast('复制失败,请手动复制');
}
} catch (err) {
showCopyToast('复制失败,请手动复制');
} finally {
document.body.removeChild(textarea);
}
}
}
// 优化提示显示
function showCopyToast(message) {
let toast = document.querySelector('.copy-toast');
if (!toast) {
toast = document.createElement('div');
toast.className = 'copy-toast';
document.body.appendChild(toast);
}
// 清除之前的定时器
if (toast.timeoutId) {
clearTimeout(toast.timeoutId);
}
toast.textContent = message;
toast.classList.add('show');
// 设置新的定时器
toast.timeoutId = setTimeout(() => {
toast.classList.remove('show');
}, 2000);
}
// 在添加消息时的HTML模板中修改复制按钮的位置
function createMessageHTML(message, isSent) {
return `
<div class="message-container ${isSent ? 'sent' : 'received'}">
<button class="copy-btn" aria-label="复制消息"
onclick="event.stopPropagation(); copyMessage('${message.text.replace(/'/g, "\\'")}')">
<i class="bi bi-clipboard"></i>
</button>
<div class="message ${isSent ? 'sent' : 'received'}">
<!-- 其他消息内容 -->
</div>
</div>
`;
}
// 在 body 标签末尾添加提示元素
document.body.insertAdjacentHTML('beforeend', '<div class="copy-tooltip"></div>');
// 初始化
initializeChat();
document.getElementById('messageInput').focus();
// 添加窗口大小变化监听
window.addEventListener('resize', function() {
const sidebar = document.getElementById('sidebar');
const toggleIcon = sidebar.querySelector('.sidebar-toggle i');
if (window.innerWidth > 768) {
// 在桌面端保持侧边栏状态
if (!sidebarVisible) {
toggleIcon.classList.remove('bi-chevron-left');
toggleIcon.classList.add('bi-chevron-right');
}
} else {
// 在移动端自动隐藏边栏
sidebarVisible = false;
sidebar.classList.remove('visible');
toggleIcon.classList.remove('bi-chevron-left');
toggleIcon.classList.add('bi-chevron-right');
}
});
// 初始化聊天列表
function initializeChatList() {
const chatListContent = document.getElementById('chatListContent');
chatListContent.innerHTML = `
<div class="chat-item active" data-chat="group">
<div class="chat-item-avatar" style="background: var(--primary-color)">
<i class="bi bi-people-fill"></i>
</div>
<div class="chat-item-info">
<div class="chat-item-name">群聊</div>
<div class="chat-item-last-msg">所有人的聊天室</div>
</div>
</div>
`;
// 为群聊项添加点击事件监听器
const groupChatItem = chatListContent.querySelector('.chat-item[data-chat="group"]');
if (groupChatItem) {
groupChatItem.addEventListener('click', () => {
switchToGroupChat();
});
}
// 设置默认为群聊模式,并且不清空消息容器
currentMode = 'group';
currentChatTarget = null;
document.getElementById('chatTitle').textContent = '内网群聊';
updateModeButtons('group');
}
// 添加或更新聊天项
function updateChatList(ip, lastMessage) {
if (ip === myIp) return;
const chatListContent = document.getElementById('chatListContent');
let chatItem = document.querySelector(`.chat-item[data-chat="${ip}"]`);
if (!chatItem) {
const displayName = getDisplayName(ip);
chatItem = document.createElement('div');
chatItem.className = 'chat-item';
chatItem.setAttribute('data-chat', ip);
chatItem.innerHTML = `
<div class="chat-item-avatar" style="background-color: transparent !important; padding: 0;">
${createAvatarElement(ip, '40px', '1.2em')}
</div>
<div class="chat-item-info">
<div class="chat-item-name">${displayName}</div>
<div class="chat-item-last-msg"></div>
</div>
<div class="chat-item-badge">1</div>
<div class="chat-item-actions">
<button class="chat-delete-btn" onclick="deleteChatHistory('${ip}', event)">
<i class="bi bi-trash"></i>
</button>
</div>
`;
chatItem.addEventListener('click', () => switchToPrivateChat(ip));
chatListContent.appendChild(chatItem);
}
// 更新最后一条消息
chatItem.querySelector('.chat-item-last-msg').textContent = lastMessage;
}
// 切换到私聊
function switchToPrivateChat(targetIp) {
currentMode = 'private';
currentChatTarget = targetIp;
// 更新UI
const targetName = getDisplayName(targetIp);
document.getElementById('chatTitle').textContent = `${targetName} 的私聊`;
updateModeButtons('private');
// 清除未读消息提醒
clearUnreadBadge(targetIp);
// 清空并显示私聊消息
const messageContainer = document.getElementById('messageContainer');
messageContainer.innerHTML = '';
// 显示私聊历史
if (chatHistory.private[targetIp] && chatHistory.private[targetIp].length > 0) {
const sortedMessages = [...chatHistory.private[targetIp]].sort((a, b) =>
(a.timestamp || 0) - (b.timestamp || 0)
);
sortedMessages.forEach(msg => {
addMessage(msg, true); // 标记为历史消息
});
// 添加分隔线
addSystemMessage('以上为历史消息');
}
// 更新聊天列表状态
updateChatItemStatus(targetIp);
// 滚动到最新消息
setTimeout(() => {
messageContainer.scrollTop = messageContainer.scrollHeight;
}, 100);
// 自动关闭侧边栏(移动端)
if (window.innerWidth <= 768 && sidebarVisible) {
toggleSidebar();
}
}
// 切换到群聊
function switchToGroupChat() {
currentMode = 'group';
currentChatTarget = null;
// 更新UI
document.getElementById('chatTitle').textContent = '内网群聊';
updateModeButtons('group');
// 清空并显示群聊消息
const messageContainer = document.getElementById('messageContainer');
messageContainer.innerHTML = '';
// 显示群聊历史
displayGroupHistory();
// 更新聊天列表状态
updateChatItemStatus('group');
// 自动关闭侧边栏(移动端)
if (window.innerWidth <= 768 && sidebarVisible) {
toggleSidebar();
}
}
// 显示聊天记录
function displayChatHistory(targetIp) {
const messageContainer = document.getElementById('messageContainer');
messageContainer.innerHTML = '';
const messages = chatHistory.private[targetIp] || [];
messages.forEach(data => addMessage(data, false));
}
// 更新聊天项状态
function updateChatItemStatus(activeIp) {
document.querySelectorAll('.chat-item').forEach(item => {
item.classList.remove('active');
if (item.getAttribute('data-chat') === activeIp) {
item.classList.add('active');
}
});
}
// 添加未读消息提醒
function addUnreadBadge(ip) {
if (currentChatTarget !== ip) {
unreadMessages[ip] = (unreadMessages[ip] || 0) + 1;
const badge = document.querySelector(`.chat-item[data-chat="${ip}"] .chat-item-badge`);
if (badge) {
badge.textContent = unreadMessages[ip];
badge.classList.add('show');
// 添加闪烁动画
badge.style.animation = 'none';
badge.offsetHeight; // 触发重绘
badge.style.animation = 'badgePulse 1s infinite';
}
}
}
// 清除未读消息提醒
function clearUnreadBadge(ip) {
unreadMessages[ip] = 0;
const badge = document.querySelector(`.chat-item[data-chat="${ip}"] .chat-item-badge`);
if (badge) {
badge.classList.remove('show');
}
}
// 添加提示音函数
function playNotificationSound() {
const audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBkCY2e/GdSgFKHzK8N2NOwgXZLnv6KJQDgtMpOL0uWYeBj2V1/LJeCoFJHfH8uCRPwgUYbbx7KVTDghIoOD3vGkhBTqS1fPMeywFIHPE9OOVQwgRXrTz76hWDgZEnN75v20jBTeP0/XPfi4FHW/B9eaYRwgOWrH18qtZDgRBmd/8wm8lBTSM0fbSgjAFGWu+9+mcSwgLV6/39K5cDwM+lt//xXInBTGJz/fVhTIFGGi7+OyeTggJVK34961fDwE7k+D/yHUpBS2GzPjYiDQFF2W4+e+hUQgHUaz6+LBiEAE4j97/y3crBSqDyvrbiTYFFWK2+vGjUwgFTqr7+rNkEAA1jd3/znktBSeBx/vdizgFE1+z+/SmVggDTKj8/LVmEQAyi9v/0HsuBSR/xfzfjDoFEV2x/PanWAgBSab9/bhpEQAwh9n/03wtBSF8w/3ikz0FD1qu/fmqWwj/R6T+/7pqEgAuhdf/1X4vBR56wf7klD8FDVis/vutXQj9RaL//71sEwAshNb/14AxBRx3v//nlUEFDFaq//2vXwj8Q6D//79uFAAqgdT/2YIyBRp1vf/qlkMFC1Wo//+yYQj6QZ7//8FwFQAof9P/24QzBRhyuv/sl0UFCVOm//+0Ywj5P5z//8NyFgAmfdH/3YY1BRZwuP/umEYFCFGk//+2ZQj3PZr//8V0FwAkfM//34g2BRRutv/wmUgFBlCj//+4Zwj2O5j//8d1GQAie87/4Yo3BRJstP/ymUkFBU6h//+6aQj1OZb//8l3GgAgec3/44w5BRBqsv/0mkoFA0yf//+8awj0N5X//8t4GwAfeM3/5Y46BQ9osP/2m0wFAkqe//++bQjyNZP//817HQAdd8z/55A7BQ1mr//4nE0FAUid//7AcAjxM5H//899HgAcdcv/6ZE9BQtlrf/6nU8F/0ac//7BcgjwMY///9F/HwAadMv/65M+BQpjq//8nlAF/kSa//7DdAjuL47//9OAIQAZc8r/7JRABQhiqf/+n1IF/EKY//7FdgjuLYz//9WCIgAXcsn/7pZBBQdgp///oFMF+0CW//7HeAjtK4r//9eEIwAWcMj/8JdDBQVfpv//oVUF+T6U//7JegjsKYj//9mGJAAVb8f/8plEBQRdpP//o1cF+DyS//7LfAjrJ4b//9uIJQATbcb/9JpGBQJbo///pVgF9zqQ//7NfgjqJYX//92JJgASbMX/9pxHBQFZof//plkF9jiO//7PgAjpI4P//9+LJwARasT/+J1JBQBYn///qFsF9TaM//7RggjnIYH//+GNKQAPacP/+p9KBf9Wnf//qVwF9DSK//7ThAjmH3///+OPKgAOaML//KBMBf5UnP//q14F8zKI//7VhgjlHn3//+WQLAANZsH//qJNBfxSm///rWAF8jCG//7XiAjkHHv//+eSLQALZcD//6NOBftQmf//rmEF8S6E//7ZigjjGnn//+mULgAKY7///6VQBfpOl///sGMF8CyC//7bjAjiGHf//+uWMAAJYr7//6dRBflMlf//smQF7yqA//7djgihFnX//+2YMQAHYb3//6lTBfhKk///s2YF7iiA//7fkAigFHP//++aMgAGX7z//6tUBfdIkf//tWgF7SZ+//7hlAifEnH///GcNAAFXrr//61WBfZGj///tmkF7CR8//7jlgieDG///fOfNQAEXLn//69XBfVEjf//uGsF6yJ6//7lmAidCm3//fWhNwACW7j//7FZBfRCi///um0F6iB4//7nmgicCGv//fenOAACWbb//7NaBfNAif//vG8F6R52//7pnAibBmn//fmpOgAAWLX//7VcBfI+h///vnAF6Bx0//7rngiaBGf//fusPAAAVrP//7deBfE8hf//wHIF5xpy//7toQiZAmX//f2uPQAAVbL//7lgBfA6g///wnQF5hhw//7vpQiYAGP//f+xPwAAU7D//7phBe84gf//xHYF5RZu//7xpwiXAGH//f+zQQAA[base64 audio data]').play();
}
// 添加闪烁动画样式
const style = document.createElement('style');
style.textContent = `
@keyframes badgePulse {
0% { transform: scale(1); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
`;
document.head.appendChild(style);
// 添加状态检查函数
function checkCurrentMode() {
console.log('Current Mode:', currentMode);
console.log('Current Target:', currentChatTarget);
console.log('Chat History:', chatHistory);
}
// 添加显示群聊历史的函数
function displayGroupHistory() {
const messageContainer = document.getElementById('messageContainer');
messageContainer.innerHTML = '';
// 按时间顺序显示群聊消息
if (chatHistory.group && chatHistory.group.length > 0) {
// 对消息按时间戳排序
const sortedMessages = [...chatHistory.group].sort((a, b) =>
(a.timestamp || 0) - (b.timestamp || 0)
);
sortedMessages.forEach(msg => {
addMessage(msg, true); // 标记为历史消息
});
// 添加分隔线
addSystemMessage('以上为历史消息');
}
// 滚动到最新消息
setTimeout(() => {
messageContainer.scrollTop = messageContainer.scrollHeight;
}, 100);
}
// 添加删除聊天历史的函数
function deleteChatHistory(ip, event) {
// 阻止事件冒泡,避免触发聊天项的点击事件
event.stopPropagation();
if (!confirm(`确定要删除与 ${ip} 的聊天记录吗?删除后将无法恢复。`)) {
return;
}
// 删除聊天历史
delete chatHistory.private[ip];
delete unreadMessages[ip];
// 从聊天列表中移除
const chatItem = document.querySelector(`.chat-item[data-chat="${ip}"]`);
if (chatItem) {
chatItem.remove();
}
// 如果当前正在查看这个聊天,切换到群聊
if (currentMode === 'private' && currentChatTarget === ip) {
switchMode('group');
}
showToast('聊天记录已删除');
}
// 添加 Toast 提示函数
function showToast(message) {
let toast = document.querySelector('.toast');
if (!toast) {
toast = document.createElement('div');
toast.className = 'toast';
document.body.appendChild(toast);
}
toast.textContent = message;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
// 添加屏幕旋转处理
window.addEventListener('orientationchange', function() {
// 关闭侧边栏
if (sidebarVisible) {
toggleSidebar();
}
// 重新计算高度
setTimeout(() => {
const messages = document.querySelector('.chat-messages');
messages.scrollTop = messages.scrollHeight;
}, 100);
});
// 添加触摸手势支持
let touchStartX = 0;
let touchEndX = 0;
document.addEventListener('touchstart', e => {
touchStartX = e.touches[0].clientX;
});
document.addEventListener('touchend', e => {
touchEndX = e.changedTouches[0].clientX;
handleSwipe();
});
function handleSwipe() {
const swipeDistance = touchEndX - touchStartX;
const threshold = 100; // 滑动阈值
if (Math.abs(swipeDistance) > threshold) {
if (swipeDistance > 0 && !sidebarVisible) {
// 向右滑动,打开侧边栏
toggleSidebar();
} else if (swipeDistance < 0 && sidebarVisible) {
// 向左滑动,关闭侧边栏
toggleSidebar();
}
}
}
// 修改拖拽相关事件处理
const messageContainer = document.getElementById('messageContainer');
const dragOverlay = document.getElementById('dragOverlay');
// 防止默认行为
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// 显示拖拽覆盖层
function highlight(e) {
preventDefaults(e);
dragOverlay.classList.add('active');
}
// 隐藏拖拽覆盖层
function unhighlight(e) {
preventDefaults(e);
dragOverlay.classList.remove('active');
}
// 添加文件验证函数
function validateFile(file) {
// 最大文件大小(例如 500MB增加限制以支持更大的音视频文件
const MAX_FILE_SIZE = 500 * 1024 * 1024;
// 检查文件大小
if (file.size > MAX_FILE_SIZE) {
throw new Error(`文件大小超过限制 (最大 ${formatFileSize(MAX_FILE_SIZE)})`);
}
// 移除文件类型限制,支持所有格式
// 所有文件类型都被允许
return true;
}
// 添加文件类型检测函数
function getFileType(filename, mimeType) {
const ext = filename.toLowerCase().split('.').pop();
// 音频文件类型
const audioExtensions = ['mp3', 'wav', 'ogg', 'aac', 'm4a', 'flac', 'wma'];
const audioMimeTypes = ['audio/'];
// 视频文件类型
const videoExtensions = ['mp4', 'webm', 'ogg', 'avi', 'mov', 'wmv', 'flv', 'mkv', '3gp'];
const videoMimeTypes = ['video/'];
// 图片文件类型
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'];
const imageMimeTypes = ['image/'];
// 文档文件类型
const docExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'rtf'];
// 代码文件类型
const codeExtensions = ['js', 'html', 'css', 'py', 'java', 'cpp', 'c', 'php', 'rb', 'go', 'rs', 'ts', 'jsx', 'tsx', 'vue', 'json', 'xml', 'sql'];
// 压缩文件类型
const archiveExtensions = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2'];
if (audioExtensions.includes(ext) || audioMimeTypes.some(type => mimeType.startsWith(type))) {
return 'audio';
} else if (videoExtensions.includes(ext) || videoMimeTypes.some(type => mimeType.startsWith(type))) {
return 'video';
} else if (imageExtensions.includes(ext) || imageMimeTypes.some(type => mimeType.startsWith(type))) {
return 'image';
} else if (docExtensions.includes(ext)) {
return 'document';
} else if (codeExtensions.includes(ext)) {
return 'code';
} else if (archiveExtensions.includes(ext)) {
return 'archive';
} else {
return 'file';
}
}
// 获取文件图标
function getFileIcon(fileType, extension) {
const iconMap = {
'audio': 'bi-music-note-beamed',
'video': 'bi-play-circle',
'image': 'bi-image',
'document': 'bi-file-earmark-text',
'code': 'bi-file-earmark-code',
'archive': 'bi-file-earmark-zip',
'file': 'bi-file-earmark'
};
// 特殊文件类型的图标
const extIconMap = {
'pdf': 'bi-file-earmark-pdf',
'doc': 'bi-file-earmark-word',
'docx': 'bi-file-earmark-word',
'xls': 'bi-file-earmark-excel',
'xlsx': 'bi-file-earmark-excel',
'ppt': 'bi-file-earmark-ppt',
'pptx': 'bi-file-earmark-ppt',
'txt': 'bi-file-earmark-text',
'py': 'bi-file-earmark-code',
'js': 'bi-file-earmark-code',
'html': 'bi-file-earmark-code',
'css': 'bi-file-earmark-code'
};
return extIconMap[extension] || iconMap[fileType] || 'bi-file-earmark';
}
// 修改文件拖放处理
async function handleDrop(e) {
e.preventDefault();
e.stopPropagation();
unhighlight(e);
const files = e.dataTransfer.files;
if (!files || files.length === 0) return;
const file = files[0];
try {
// 验证文件
validateFile(file);
if (isUploading) return;
isUploading = true;
await handleFileUpload(file);
} catch (error) {
console.error('文件处理失败:', error);
showToast(error.message);
} finally {
isUploading = false;
}
}
// 修改事件监听器绑定方式
function initializeDropZone() {
const messageContainer = document.getElementById('messageContainer');
// 移除现有的事件监听器(如果有)
messageContainer.removeEventListener('dragenter', handleDragEnter);
messageContainer.removeEventListener('dragover', handleDragOver);
messageContainer.removeEventListener('dragleave', handleDragLeave);
messageContainer.removeEventListener('drop', handleDrop);
// 添加新的事件监听器
messageContainer.addEventListener('dragenter', handleDragEnter, false);
messageContainer.addEventListener('dragover', handleDragOver, false);
messageContainer.addEventListener('dragleave', handleDragLeave, false);
messageContainer.addEventListener('drop', handleDrop, false);
}
// 拖拽事件处理函数
function handleDragEnter(e) {
e.preventDefault();
e.stopPropagation();
highlight(e);
}
function handleDragOver(e) {
e.preventDefault();
e.stopPropagation();
highlight(e);
}
function handleDragLeave(e) {
e.preventDefault();
e.stopPropagation();
// 检查是否真的离开了容器
const rect = e.target.getBoundingClientRect();
const x = e.clientX;
const y = e.clientY;
if (x <= rect.left || x >= rect.right || y <= rect.top || y >= rect.bottom) {
unhighlight(e);
}
}
// 添加上传状态标志
let isUploading = false;
// 在页面加载完成后初始化
document.addEventListener('DOMContentLoaded', initializeDropZone);
// 处理文件上传和发送
async function handleFileUpload(file) {
const formData = new FormData();
formData.append('file', file);
try {
// 添加文件信息日志
console.log('准备上传文件:', {
name: file.name,
type: file.type,
size: file.size
});
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
// 添加响应状态日志
console.log('上传响应状态:', response.status);
if (!response.ok) {
const errorText = await response.text();
console.error('上传失败响应:', errorText);
throw new Error(`上传失败: ${response.status} ${errorText}`);
}
const result = await response.json();
console.log('上传成功结果:', result);
// 检测文件类型
const detectedFileType = getFileType(file.name, file.type);
const fileExtension = file.name.toLowerCase().split('.').pop();
// 构建增强的文件数据
const fileData = {
fileName: file.name,
fileSize: file.size,
filePath: result.path,
fileType: file.type,
fileExtension: fileExtension,
detectedType: detectedFileType,
isImage: detectedFileType === 'image',
isAudio: detectedFileType === 'audio',
isVideo: detectedFileType === 'video',
uploadTime: new Date().toISOString()
};
const messageData = {
type: 'file',
message: JSON.stringify(fileData),
timestamp: new Date().getTime(),
alreadyDisplayed: true
};
if (currentMode === 'private') {
messageData.targetIp = currentChatTarget;
}
// 发送WebSocket消息
ws.send(JSON.stringify(messageData));
// 本地显示和保存
const localMessage = {
...messageData,
ip: myIp
};
// 保存到历史记录
if (currentMode === 'private' && currentChatTarget) {
if (!chatHistory.private[currentChatTarget]) {
chatHistory.private[currentChatTarget] = [];
}
chatHistory.private[currentChatTarget].push(localMessage);
// 根据文件类型显示不同的最后消息提示
let lastMsgHint = '文件消息';
if (detectedFileType === 'image') lastMsgHint = '图片';
else if (detectedFileType === 'audio') lastMsgHint = '音频';
else if (detectedFileType === 'video') lastMsgHint = '视频';
else if (detectedFileType === 'document') lastMsgHint = '文档';
updateChatList(currentChatTarget, lastMsgHint);
} else {
chatHistory.group.push(localMessage);
}
addMessage(localMessage);
saveLocalChatHistory();
// 滚动到最新消息
const messageContainer = document.getElementById('messageContainer');
messageContainer.scrollTop = messageContainer.scrollHeight;
} catch (error) {
console.error('文件上传失败详细信息:', error);
showToast('文件上传失败:' + error.message);
throw error;
}
}
function formatFileSize(bytes) {
if (bytes < 1024) {
return bytes + ' B';
} else if (bytes < 1024 * 1024) {
return (bytes / 1024).toFixed(2) + ' KB';
} else if (bytes < 1024 * 1024 * 1024) {
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
} else {
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
}
}
// 添加文件信息复制功能
async function copyFileInfo(fileName, fileSize) {
const text = `文件名:${fileName}\n文件大小:${fileSize}`;
await copyMessage(text);
}
// 添加触摸反馈
document.addEventListener('DOMContentLoaded', function() {
// 为所有复制按钮添加触摸反馈
document.addEventListener('click', function(e) {
if (e.target.closest('.copy-btn')) {
const btn = e.target.closest('.copy-btn');
btn.style.transform = 'scale(0.95)';
setTimeout(() => {
btn.style.transform = '';
}, 200);
}
});
// 防止触摸事件穿透
document.querySelectorAll('.copy-btn').forEach(btn => {
btn.addEventListener('touchstart', e => {
e.preventDefault();
}, { passive: false });
});
});
// 添加文件操作处理函数
async function handleFileAction(messageData) {
try {
// 解码并解析文件数据
const decodedData = decodeURIComponent(messageData);
const fileData = JSON.parse(decodedData);
console.log('Downloading file:', fileData); // 添加调试日志
// 验证必要的文件信息是否存在
if (!fileData.filePath || !fileData.fileName) {
throw new Error('文件信息不完整');
}
// 创建下载链接
const link = document.createElement('a');
link.href = fileData.filePath;
link.download = fileData.fileName;
// 触发下载
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showToast('开始下载文件');
} catch (error) {
console.error('文件下载失败:', error);
showToast('文件下载失败: ' + error.message);
}
}
// 初始化文件上传相关事件
function initializeFileUpload() {
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
validateFile(file);
if (isUploading) return;
isUploading = true;
await handleFileUpload(file);
} catch (error) {
console.error('文件处理失败:', error);
showToast(error.message);
} finally {
isUploading = false;
fileInput.value = ''; // 清空文件输入框,允许重复选择相同文件
}
});
}
// 在页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
initializeDropZone();
initializeFileUpload();
});
// 管理员功能实现
// 设置管理员IP
function setAdminIp() {
const adminIpInput = document.getElementById('adminIpInput');
const inputIp = adminIpInput.value.trim();
if (!inputIp) {
// 清空管理员设置
adminIp = null;
localStorage.removeItem('adminIp');
updateAdminStatus();
showToast('已清空管理员设置');
return;
}
// 验证IP格式
if (!isValidIp(inputIp)) {
showToast('请输入有效的IP地址');
return;
}
adminIp = inputIp;
localStorage.setItem('adminIp', adminIp);
updateAdminStatus();
showToast(`管理员已设置为: ${adminIp}`);
adminIpInput.value = '';
}
// 验证IP地址格式
function isValidIp(ip) {
const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (!ipRegex.test(ip)) return false;
const parts = ip.split('.');
return parts.every(part => {
const num = parseInt(part, 10);
return num >= 0 && num <= 255;
});
}
// 更新管理员状态显示
function updateAdminStatus() {
const adminStatus = document.getElementById('adminStatus');
const adminFunctions = document.getElementById('adminFunctions');
const statusText = adminStatus.querySelector('.admin-status-text');
// 重置样式
adminStatus.className = 'admin-status';
if (!adminIp) {
statusText.textContent = '未设置管理员';
adminFunctions.style.display = 'none';
isAdmin = false;
} else if (myIp === adminIp) {
statusText.textContent = '您是管理员';
adminStatus.classList.add('is-admin');
adminFunctions.style.display = 'block';
isAdmin = true;
} else {
statusText.textContent = `管理员: ${adminIp}`;
adminStatus.classList.add('admin-set');
adminFunctions.style.display = 'none';
isAdmin = false;
}
}
// 加载管理员设置
function loadAdminSettings() {
const savedAdminIp = localStorage.getItem('adminIp');
if (savedAdminIp) {
adminIp = savedAdminIp;
updateAdminStatus();
}
}
// 清空所有聊天历史记录
async function clearAllChatHistory() {
// 先验证管理员权限
const hasPermission = await verifyAdminPermission();
if (!hasPermission) {
return;
}
// 确认对话框
const confirmed = confirm(
'⚠️ 警告:此操作将永久删除所有聊天记录!\n' +
'包括:\n' +
'• 所有群聊消息\n' +
'• 所有私聊对话\n' +
'• 本地存储的历史记录\n' +
'• 服务器端的历史文件\n\n' +
'此操作无法撤销,确定要继续吗?'
);
if (!confirmed) return;
// 二次确认
const doubleConfirmed = confirm(
'🔴 最后确认:您确定要删除所有聊天记录吗?\n' +
'这将清空服务器上的所有历史数据。'
);
if (!doubleConfirmed) return;
try {
// 先清空本地数据
chatHistory.group = [];
chatHistory.private = {};
unreadMessages = {};
// 清空本地存储
if (myIp) {
localStorage.removeItem(`chatHistory_${myIp}`);
}
// 清空所有用户的本地存储清理可能存在的其他IP记录
Object.keys(localStorage).forEach(key => {
if (key.startsWith('chatHistory_')) {
localStorage.removeItem(key);
}
});
// 清空当前显示的消息
const messageContainer = document.getElementById('messageContainer');
messageContainer.innerHTML = '';
// 重新初始化聊天列表
initializeChatList();
// 切换到群聊模式
currentMode = 'group';
currentChatTarget = null;
document.getElementById('chatTitle').textContent = '群聊';
updateModeButtons('group');
// 发送WebSocket消息通知其他用户并清空服务器历史
if (ws && ws.readyState === WebSocket.OPEN) {
const systemMessage = {
type: 'admin_action',
action: 'clear_all_history',
admin_ip: myIp,
message: `管理员 ${myIp} 已清空所有聊天记录`,
timestamp: new Date().getTime()
};
try {
ws.send(JSON.stringify(systemMessage));
} catch (error) {
console.warn('发送WebSocket通知失败:', error);
}
}
// 同时调用后端API确保服务器端历史也被清空
fetch('/admin/clear_history', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('服务器端历史记录已清空:', data);
addSystemMessage('✅ 所有聊天记录已被管理员彻底清空(包括服务器历史)');
showToast('✅ 所有聊天记录已成功清空(包括服务器端)');
} else {
console.error('服务器端清空失败:', data.message);
addSystemMessage('⚠️ 本地记录已清空,但服务器端清空可能失败');
showToast('⚠️ 本地清空成功,服务器端可能需要手动处理');
}
})
.catch(error => {
console.error('调用服务器清空API失败:', error);
addSystemMessage('⚠️ 本地记录已清空,但服务器端清空失败');
showToast('⚠️ 本地清空成功,但服务器端清空失败');
});
addSystemMessage('🔄 正在清空服务器历史记录...');
// 调用后端API清空服务器端历史包含权限验证
const response = await fetch('/admin/clear_history', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
if (data.success) {
console.log('服务器端历史记录已清空:', data);
addSystemMessage('✅ 所有聊天记录已被管理员彻底清空(包括服务器历史)');
showToast('✅ 所有聊天记录已成功清空(包括服务器端)');
// 发送WebSocket消息通知其他用户服务器端已经广播了这里不再重复发送
console.log('管理员操作:所有聊天记录已清空');
} else {
console.error('服务器端清空失败:', data.message);
addSystemMessage('⚠️ 本地记录已清空,但服务器端清空失败');
showToast('⚠️ 本地清空成功,服务器端清空失败');
}
} else {
const error = await response.json();
console.error('服务器权限验证失败:', error);
addSystemMessage('❌ 权限验证失败,操作被拒绝');
showToast('❌ 权限不足:' + (error.detail || '操作被拒绝'));
}
} catch (error) {
console.error('清空聊天记录失败:', error);
addSystemMessage('❌ 清空操作失败');
showToast('❌ 清空操作失败: ' + error.message);
}
}
// 管理员权限检查装饰器
function requireAdmin(fn) {
return function(...args) {
if (!isAdmin) {
showToast('此功能仅限管理员使用');
return;
}
return fn.apply(this, args);
};
}
// 初始化管理员相关功能
async function initializeAdminFeatures() {
// 从后端获取管理员信息
await fetchAdminInfo();
}
// 管理员功能实现
// 从后端获取管理员信息
async function fetchAdminInfo() {
try {
const response = await fetch('/admin/info');
const data = await response.json();
adminIp = data.admin_ip;
isAdmin = data.is_admin;
console.log('管理员信息:', {
adminIp: adminIp,
isAdmin: isAdmin,
clientIp: data.client_ip
});
updateAdminStatus();
return data;
} catch (error) {
console.error('获取管理员信息失败:', error);
showToast('获取管理员信息失败');
return null;
}
}
// 更新管理员状态显示
function updateAdminStatus() {
const adminStatus = document.getElementById('adminStatus');
const adminFunctions = document.getElementById('adminFunctions');
const statusText = adminStatus.querySelector('.admin-status-text');
// 重置样式
adminStatus.className = 'admin-status';
if (!adminIp) {
statusText.textContent = '未配置管理员';
adminFunctions.style.display = 'none';
isAdmin = false;
} else if (isAdmin) {
statusText.innerHTML = `
<i class="bi bi-shield-check"></i>
您是管理员 (${adminIp})
`;
adminStatus.classList.add('is-admin');
adminFunctions.style.display = 'block';
} else {
statusText.innerHTML = `
<i class="bi bi-shield"></i>
管理员: ${adminIp}
`;
adminStatus.classList.add('admin-set');
adminFunctions.style.display = 'none';
}
}
// 验证管理员权限
async function verifyAdminPermission() {
if (!isAdmin) {
showToast('只有管理员才能执行此操作');
return false;
}
try {
const response = await fetch('/admin/verify');
if (response.ok) {
const data = await response.json();
return data.verified;
} else {
const error = await response.json();
showToast(error.detail || '权限验证失败');
return false;
}
} catch (error) {
console.error('权限验证失败:', error);
showToast('权限验证失败');
return false;
}
}
// 获取IP名称映射
async function fetchIpNameMapping() {
try {
const response = await fetch('/get_ip_names');
const data = await response.json();
ipNameMapping = data.ip_vs_name || {};
ipAvatarMapping = data.ip_vs_avatar || {};
console.log('IP名称映射已加载:', ipNameMapping);
console.log('IP头像映射已加载:', ipAvatarMapping);
// 更新可用用户列表
availableUsers = Object.keys(ipNameMapping).filter(ip => ip !== myIp);
// 更新所有已显示的IP为名称
updateDisplayedNames();
return data;
} catch (error) {
console.error('获取IP名称映射失败:', error);
return null;
}
}
// 获取显示名称名称优先如果没有映射则显示IP
function getDisplayName(ip) {
const name = ipNameMapping[ip];
return name ? `${name} (${ip})` : ip;
}
// 获取简短显示名称(用于头像等小空间)
function getShortDisplayName(ip) {
const name = getDisplayName(ip);
if (name === ip) {
return ip;
}
// 如果是中文名称返回前2个字符
if (/[\u4e00-\u9fa5]/.test(name)) {
return name.substring(0, 2);
}
// 如果是英文名称返回前3个字符
return name.substring(0, 3);
}
// 更新已显示的所有IP为名称
function updateDisplayedNames() {
// 更新消息中的IP显示
document.querySelectorAll('.ip-info span').forEach(span => {
const ip = span.textContent;
if (ipNameMapping[ip]) {
span.textContent = getDisplayName(ip);
}
});
// 更新用户标签
document.querySelectorAll('.user-tag').forEach(tag => {
const text = tag.textContent;
const ipMatch = text.match(/用户 ([\d.]+)/);
if (ipMatch) {
const ip = ipMatch[1];
const name = getDisplayName(ip);
if (name !== ip) {
tag.innerHTML = tag.innerHTML.replace(`用户 ${ip}`, name);
}
}
});
// 更新聊天列表中的名称
document.querySelectorAll('.chat-item').forEach(item => {
const ip = item.getAttribute('data-chat');
if (ip !== 'group' && ipNameMapping[ip]) {
const nameElement = item.querySelector('.chat-item-name');
if (nameElement) {
nameElement.textContent = getDisplayName(ip);
}
}
});
// 更新当前聊天标题
if (currentMode === 'private' && currentChatTarget) {
const targetName = getDisplayName(currentChatTarget);
document.getElementById('chatTitle').textContent = `${targetName} 的私聊`;
}
// 更新头部IP显示
const headerIp = document.getElementById('headerIp');
if (headerIp) {
headerIp.textContent = getDisplayName(myIp);
}
// 更新侧边栏当前IP显示
const currentIpElement = document.getElementById('currentIp');
if (currentIpElement) {
const myName = getDisplayName(myIp);
currentIpElement.textContent = `本机: ${myName}${myName !== myIp ? ` (${myIp})` : ''}`;
}
}
// 创建私聊用户选择对话框
function showPrivateChatSelector() {
// 如果没有可用用户直接提示输入IP
if (availableUsers.length === 0) {
const targetIpInput = prompt('没有预设用户请输入目标IP地址');
if (targetIpInput && targetIpInput.trim()) {
startPrivateChat(targetIpInput.trim());
}
return;
}
// 创建选择对话框
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
`;
const dialog = document.createElement('div');
dialog.style.cssText = `
background: white;
border-radius: 16px;
padding: 24px;
max-width: 400px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
`;
let dialogHTML = `
<div style="margin-bottom: 20px;">
<h3 style="margin: 0 0 8px 0; color: var(--primary-color); font-size: 1.2em;">
<i class="bi bi-person-plus" style="margin-right: 8px;"></i>
选择聊天对象
</h3>
<p style="margin: 0; color: #666; font-size: 0.9em;">选择一个用户开始私聊</p>
</div>
<div style="margin-bottom: 20px;">
`;
// 添加预设用户列表
availableUsers.forEach(ip => {
const name = getDisplayName(ip);
dialogHTML += `
<div class="user-selector-item" onclick="selectPrivateChatUser('${ip}')" style="
display: flex;
align-items: center;
padding: 12px;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 8px;
border: 1px solid #eee;
" onmouseover="this.style.background='#f8f9fa'" onmouseout="this.style.background='transparent'">
<div style="margin-right: 12px;">
${createAvatarElement(ip, '40px', '1.2em')}
</div>
<div style="flex: 1;">
<div style="font-weight: 500; margin-bottom: 2px;">${name}</div>
<div style="font-size: 0.8em; color: #666;">${ip}</div>
</div>
<i class="bi bi-chevron-right" style="color: #ccc;"></i>
</div>
`;
});
dialogHTML += `
</div>
<div style="border-top: 1px solid #eee; padding-top: 16px;">
<button onclick="showCustomIpInput()" style="
width: 100%;
padding: 12px;
border: 1px solid var(--primary-color);
background: transparent;
color: var(--primary-color);
border-radius: 8px;
cursor: pointer;
margin-bottom: 8px;
transition: all 0.3s ease;
" onmouseover="this.style.background='rgba(74, 144, 226, 0.1)'" onmouseout="this.style.background='transparent'">
<i class="bi bi-plus-circle" style="margin-right: 8px;"></i>
输入其他IP地址
</button>
<button onclick="closePrivateChatSelector()" style="
width: 100%;
padding: 12px;
border: none;
background: #f8f9fa;
color: #666;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
" onmouseover="this.style.background='#e9ecef'" onmouseout="this.style.background='#f8f9fa'">
取消
</button>
</div>
`;
dialog.innerHTML = dialogHTML;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
// 保存引用用于关闭
window.privateChatSelectorOverlay = overlay;
}
// 选择私聊用户
function selectPrivateChatUser(ip) {
closePrivateChatSelector();
startPrivateChat(ip);
}
// 显示自定义IP输入
function showCustomIpInput() {
const targetIpInput = prompt('请输入目标IP地址');
if (targetIpInput && targetIpInput.trim()) {
closePrivateChatSelector();
startPrivateChat(targetIpInput.trim());
}
}
// 关闭私聊选择器
function closePrivateChatSelector() {
if (window.privateChatSelectorOverlay) {
document.body.removeChild(window.privateChatSelectorOverlay);
window.privateChatSelectorOverlay = null;
}
}
// 开始私聊
function startPrivateChat(targetIp) {
if (targetIp === myIp) {
showToast('不能与自己私聊');
return;
}
currentMode = 'private';
currentChatTarget = targetIp;
// 更新UI
const targetName = getDisplayName(targetIp);
document.getElementById('chatTitle').textContent = `${targetName} 的私聊`;
updateModeButtons('private');
// 添加或更新聊天列表
updateChatList(targetIp, '开始私聊');
// 清空并显示私聊消息
const messageContainer = document.getElementById('messageContainer');
messageContainer.innerHTML = '';
if (chatHistory.private[targetIp] && chatHistory.private[targetIp].length > 0) {
const sortedMessages = [...chatHistory.private[targetIp]].sort((a, b) =>
(a.timestamp || 0) - (b.timestamp || 0)
);
sortedMessages.forEach(msg => {
addMessage(msg, true);
});
addSystemMessage('以上为历史消息');
}
// 更新聊天项状态
updateChatItemStatus(targetIp);
// 自动关闭侧边栏(移动端)
if (window.innerWidth <= 768 && sidebarVisible) {
toggleSidebar();
}
// 滚动到最新消息
setTimeout(() => {
messageContainer.scrollTop = messageContainer.scrollHeight;
}, 100);
}
// 检测是否为移动设备
function isMobileDevice() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
window.innerWidth <= 768;
}
// 处理图片点击事件
function handleImageClick(imageElement, event) {
// 阻止事件冒泡
event.stopPropagation();
// 只在移动端执行点击逻辑
if (!isMobileDevice()) {
return;
}
// 切换点击状态
const isClicked = imageElement.classList.contains('mobile-clicked');
// 先移除所有其他图片的点击状态
document.querySelectorAll('.image-message.mobile-clicked').forEach(img => {
if (img !== imageElement) {
img.classList.remove('mobile-clicked');
}
});
if (isClicked) {
// 如果已经点击过,再次点击则隐藏下载按钮
imageElement.classList.remove('mobile-clicked');
} else {
// 显示下载按钮和信息
imageElement.classList.add('mobile-clicked');
// 3秒后自动隐藏
setTimeout(() => {
imageElement.classList.remove('mobile-clicked');
}, 3000);
}
}
// 添加全局点击事件监听,点击其他地方时隐藏所有图片下载按钮
document.addEventListener('click', function(event) {
if (!isMobileDevice()) return;
// 如果点击的不是图片消息相关元素,则隐藏所有下载按钮
if (!event.target.closest('.image-message')) {
document.querySelectorAll('.image-message.mobile-clicked').forEach(img => {
img.classList.remove('mobile-clicked');
});
}
});
</script>
</body>
</html>