Files
nodecrypt/web/main.js
T
2025-05-05 16:30:37 +08:00

507 lines
17 KiB
JavaScript

import config from './config.js';
// 多房间状态管理
let roomsData = [];
let activeRoomIndex = -1;
function getNewRoomData() {
return { room: '', userList: [], userMap: {}, myId: null, myName: '', chat: null, messages: [] };
}
// 切换房间并恢复上下文,更新 UI
function switchRoom(index) {
if (index < 0 || index >= roomsData.length) return;
activeRoomIndex = index;
const rd = roomsData[index];
// 同步更新sidebar用户名和头像
const sidebarUsername = document.getElementById('sidebar-username');
if (sidebarUsername) sidebarUsername.textContent = rd.myName;
const avatarEl = document.getElementById('sidebar-user-avatar');
if (avatarEl) avatarEl.innerHTML = getSvgAvatar(rd.myName, 44);
renderRooms(index);
renderMainHeader();
renderUserList();
renderChatArea();
}
// 渲染当前房间消息区
function renderChatArea() {
const chatArea = document.getElementById('chat-area');
if (!chatArea) return;
if (activeRoomIndex < 0 || !roomsData[activeRoomIndex]) {
chatArea.innerHTML = '';
return;
}
chatArea.innerHTML = '';
roomsData[activeRoomIndex].messages.forEach(m => {
if (m.type === 'me') addMsg(m.text, true);
else addOtherMsg(m.text, m.name, m.avatar, true);
});
}
// 根据字符串生成固定颜色
function stringToColor(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
// 生成明亮的色相,避免太灰
const h = Math.abs(hash) % 360;
return `hsl(${h}, 70%, 75%)`;
}
// 生成 Dicebear Micah 风格头像,背景色与用户名相关
function getSvgAvatar(text, size = 42) {
const seed = encodeURIComponent(text || 'User');
const bg = stringToColor(text || 'User');
return `<img src="https://api.dicebear.com/9.x/micah/svg?seed=${seed}&size=${size}&baseColor=ac6651,f9c9b6" alt="avatar" width="${size}" height="${size}" style="border-radius:50%;background:${bg};object-fit:cover;" />`;
}
// 获取用户名首字母(前2位大写)
function getInitials(name) {
if (!name) return '?';
return name.slice(0, 2).toUpperCase();
}
// 简单转义,防止XSS
function escapeHTML(str) {
return str.replace(/[&<>"']/g, function (c) {
return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;','\'':'&#39;'}[c];
});
}
// 渲染在线用户列表(右侧Members)
function renderUserList() {
const userListEl = document.getElementById('member-list');
userListEl.innerHTML = '';
const rd = roomsData[activeRoomIndex];
if (!rd) return;
let me = rd.userList.find(u => u.clientId === rd.myId);
let others = rd.userList.filter(u => u.clientId !== rd.myId);
if (me) userListEl.appendChild(createUserItem(me, true));
others.forEach(u => userListEl.appendChild(createUserItem(u, false)));
renderMainHeader(); // 在线用户变化时刷新
}
function createUserItem(user, isMe) {
let div = document.createElement('div');
div.className = 'member' + (isMe ? ' me' : '');
div.innerHTML = `
<span class="avatar">${getSvgAvatar(user.username, 38)}</span>
<div class="member-info">
<div class="member-name">${escapeHTML(user.username)}${isMe ? ' (我)' : ''}</div>
</div>
`;
// 不可点击
return div;
}
// 处理服务器推送的在线用户列表
function handleClientList(idx, list, selfId) {
const rd = roomsData[idx];
if (!rd) return;
rd.userList = list;
rd.userMap = {};
list.forEach(u => { rd.userMap[u.clientId] = u; });
rd.myId = selfId;
if (activeRoomIndex === idx) renderUserList();
}
// 新用户上线或资料变更
function handleClientSecured(idx, user) {
const rd = roomsData[idx];
if (!rd) return;
let uidx = rd.userList.findIndex(u => u.clientId === user.clientId);
if (uidx === -1) {
rd.userList.push(user);
} else {
rd.userList[uidx] = user;
}
rd.userMap[user.clientId] = user;
if (activeRoomIndex === idx) renderUserList();
}
// 用户下线
function handleClientLeft(idx, clientId) {
const rd = roomsData[idx];
if (!rd) return;
rd.userList = rd.userList.filter(u => u.clientId !== clientId);
delete rd.userMap[clientId];
if (activeRoomIndex === idx) renderUserList();
}
function setStatus(text) {
let statusBar = document.getElementById('status-bar');
if (!statusBar) {
statusBar = document.createElement('div');
statusBar.id = 'status-bar';
statusBar.style = 'color:green;padding:4px 10px;font-size:13px;';
document.body.appendChild(statusBar);
}
statusBar.innerText = text;
}
function addMsg(text, isHistory = false) {
// 只有新消息才记录
if (!isHistory && activeRoomIndex >= 0) roomsData[activeRoomIndex].messages.push({ type: 'me', text });
const chatArea = document.getElementById('chat-area');
if (!chatArea) return;
const div = document.createElement('div');
div.className = 'bubble me';
const now = new Date();
const time = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
div.innerHTML = `<span class="bubble-content">${text}</span><span class="bubble-meta">${time}</span>`;
chatArea.appendChild(div);
chatArea.scrollTop = chatArea.scrollHeight;
}
function addOtherMsg(msg, name = 'Anonymous', avatar = '', isHistory = false) {
// 只有新消息才记录
if (!isHistory && activeRoomIndex >= 0) roomsData[activeRoomIndex].messages.push({ type: 'other', text: msg, name, avatar });
const chatArea = document.getElementById('chat-area');
if (!chatArea) return;
const bubbleWrap = document.createElement('div');
bubbleWrap.className = 'bubble-other-wrap';
bubbleWrap.innerHTML = `
<span class="bubble-other-avatar">${getSvgAvatar(avatar || name, 32)}</span>
<div class="bubble-other-main">
<div class="bubble other">
<div class="bubble-other-name">${name}</div>
<span class="bubble-content">${msg.replace(/\n/g, '<br>')}</span>
<span class="bubble-meta">${(new Date()).getHours().toString().padStart(2, '0')}:${(new Date()).getMinutes().toString().padStart(2, '0')}</span>
</div>
</div>
`;
chatArea.appendChild(bubbleWrap);
chatArea.scrollTop = chatArea.scrollHeight;
}
// 渲染房间列表(动态多房间)
function renderRooms(activeId = 0) {
const roomList = document.getElementById('room-list');
roomList.innerHTML = '';
roomsData.forEach((rd, i) => {
const div = document.createElement('div');
div.className = 'room' + (i === activeId ? ' active' : '');
div.innerHTML = `
<div class="info">
<div class="title">#${escapeHTML(rd.room)}</div>
</div>
`;
div.onclick = () => switchRoom(i);
roomList.appendChild(div);
});
}
// 登录表单提交处理
function loginFormHandler(modal) {
return function(e) {
e.preventDefault();
const username = document.getElementById('username').value.trim();
const room = document.getElementById('room').value.trim();
const password = document.getElementById('password').value.trim();
joinRoom(username, room, password, modal);
};
}
function joinRoom(username, room, password, modal = null) {
// 生成房间数据并切换
const newRd = getNewRoomData();
newRd.room = room;
newRd.myName = username;
roomsData.push(newRd);
const idx = roomsData.length - 1;
switchRoom(idx);
// 更新侧边栏
const sidebarUsername = document.getElementById('sidebar-username');
if (sidebarUsername) sidebarUsername.textContent = username;
const avatarEl = document.getElementById('sidebar-user-avatar');
if (avatarEl) avatarEl.innerHTML = getSvgAvatar(username, 44);
// 隐藏登录界面或关闭modal
if (modal) modal.remove();
else document.getElementById('login-container').style.display = 'none';
document.getElementById('chat-container').style.display = '';
setStatus('Connecting...');
// 初始化 ChatCrypt
const callbacks = {
onServerClosed: () => setStatus('Node connection closed'),
onServerSecured: () => setStatus('Secure connection to node'),
onClientSecured: (user) => handleClientSecured(idx, user),
onClientList: (list, selfId) => handleClientList(idx, list, selfId),
onClientLeft: (clientId) => handleClientLeft(idx, clientId),
onClientMessage: (msg) => {
if (msg.username === newRd.myName) return;
roomsData[idx].messages.push({ type: 'other', text: msg.data, name: msg.username, avatar: msg.username });
if (activeRoomIndex === idx) renderChatArea();
}
};
const chatInst = new window.ChatCrypt(config, callbacks);
chatInst.setCredentials(username, room, password);
chatInst.connect();
roomsData[idx].chat = chatInst;
}
// 打开新房间登录模态
function openLoginModal() {
const loginContainer = document.getElementById('login-container');
// 创建一个新的modal,内容copy(不是复用)初始登录页面
const modal = document.createElement('div');
modal.className = 'login-modal';
// 复制登录表单内容
modal.innerHTML = `
<div class="login-modal-bg"></div>
<div class="login-modal-card">
<button class="login-modal-close" style="position:absolute;right:10px;top:10px;font-size:22px;background:none;border:none;cursor:pointer;">&times;</button>
<h1>Enter a Node</h1>
<form id="login-form-modal">
<div class="input-group">
<label for="username-modal">Username</label>
<input id="username-modal" type="text" autocomplete="username" required="">
</div>
<div class="input-group">
<label for="room-modal">Node Name</label>
<input id="room-modal" type="text" required="">
</div>
<div class="input-group">
<label for="password-modal">Node Password <span class="optional">(optional)</span></label>
<input id="password-modal" type="password" autocomplete="off">
</div>
<button type="submit" class="login-btn">ENTER</button>
</form>
</div>
`;
document.body.appendChild(modal);
// 关闭按钮
modal.querySelector('.login-modal-close').onclick = () => modal.remove();
// 表单提交逻辑,复用loginFormHandler但传入modal
const form = modal.querySelector('#login-form-modal');
form.addEventListener('submit', function(e) {
e.preventDefault();
// 获取输入
const username = document.getElementById('username-modal').value.trim();
const room = document.getElementById('room-modal').value.trim();
const password = document.getElementById('password-modal').value.trim();
joinRoom(username, room, password, modal);
});
}
// 渲染主面板房间头部(UI占位)
function renderMainHeader() {
const rd = roomsData[activeRoomIndex];
let roomName = rd ? rd.room : 'Room';
let onlineCount = rd && rd.userList ? rd.userList.length : 0;
if (rd && !rd.userList.some(u => u.clientId === rd.myId)) {
onlineCount += 1;
}
document.getElementById("main-header").innerHTML = `
<div style="display: flex; align-items: center;">
<div class="group-title" style="font-size: 1.22em; font-weight: bold;">#${escapeHTML(roomName)}</div>
<span style="margin-left:10px;font-size:13px;color:#888;">${onlineCount} members</span>
</div>
<div class="main-header-actions">
<button class="more-btn" id="more-btn" aria-label="More">
<span class="more-btn-dot"></span>
<span class="more-btn-dot"></span>
<span class="more-btn-dot"></span>
</button>
<div class="more-menu" id="more-menu">
<div class="more-menu-item" data-action="share">Share</div>
<div class="more-menu-item" data-action="exit">Quit</div>
</div>
</div>
`;
setupMoreBtnMenu();
}
// 清空消息区(UI占位)
function clearChat() {
document.getElementById("chat-area").innerHTML = "";
}
// 右侧tab切换
function setupTabs() {
const tabs = document.getElementById("member-tabs").children;
for (let i = 0; i < tabs.length; i++) {
tabs[i].onclick = function() {
for (let j = 0; j < tabs.length; j++) tabs[j].classList.remove("active");
this.classList.add("active");
// 这里只做UI切换,实际功能由你开发
}
}
}
// Telegram风格输入框占位符逻辑
function autoGrowInput() {
const input = document.querySelector('.input-message-input');
if (!input) return;
input.style.height = 'auto';
input.style.height = input.scrollHeight + 'px';
}
function setupInputPlaceholder() {
const input = document.querySelector('.input-message-input');
const placeholder = document.querySelector('.input-field-placeholder');
function checkEmpty() {
// 判断内容是否为空(忽略所有空白、<br>、&nbsp;等)
const html = input.innerHTML.replace(/<br\s*\/?>(\s*)?/gi, '').replace(/&nbsp;/g, '').replace(/\u200B/g, '').trim();
if (html === '') {
placeholder.style.opacity = '1';
} else {
placeholder.style.opacity = '0';
}
autoGrowInput();
}
input.addEventListener('input', checkEmpty);
input.addEventListener('blur', checkEmpty);
input.addEventListener('focus', checkEmpty);
// 初始化
checkEmpty();
autoGrowInput();
}
// 更多按钮菜单交互
function setupMoreBtnMenu() {
const btn = document.getElementById('more-btn');
const menu = document.getElementById('more-menu');
if (!btn || !menu) return;
let menuRect = null;
let animating = false;
let isMouseMoveBound = false;
function onMouseMove(ev) {
if (!menuRect) return;
const mx = ev.clientX, my = ev.clientY;
const btnRect = btn.getBoundingClientRect();
const safe = 100;
const inMenu =
mx >= menuRect.left - safe && mx <= menuRect.right + safe &&
my >= menuRect.top - safe && my <= menuRect.bottom + safe;
const inBtn =
mx >= btnRect.left - safe && mx <= btnRect.right + safe &&
my >= btnRect.top - safe && my <= btnRect.bottom + safe;
if (!inMenu && !inBtn) {
closeMenu();
}
}
function bindMouseMove() {
if (!isMouseMoveBound) {
window.addEventListener('mousemove', onMouseMove);
isMouseMoveBound = true;
}
}
function unbindMouseMove() {
if (isMouseMoveBound) {
window.removeEventListener('mousemove', onMouseMove);
isMouseMoveBound = false;
}
}
function openMenu() {
menu.classList.remove('close');
menu.classList.add('open');
menu.style.display = 'block';
menuRect = menu.getBoundingClientRect();
bindMouseMove();
}
function closeMenu() {
if (animating) return;
animating = true;
menu.classList.remove('open');
menu.classList.add('close');
setTimeout(() => {
if (menu.classList.contains('close')) menu.style.display = 'none';
animating = false;
unbindMouseMove();
}, 180);
menuRect = null;
}
btn.onclick = function(e) {
e.stopPropagation();
if (menu.classList.contains('open')) {
closeMenu();
} else {
openMenu();
}
};
menu.onclick = function(e) {
if (e.target.classList.contains('more-menu-item')) {
if (e.target.dataset.action === 'share') {
alert('分享功能待实现');
} else if (e.target.dataset.action === 'exit') {
// 退出当前房间
if (activeRoomIndex >= 0 && roomsData[activeRoomIndex]) {
// 优先彻底销毁连接
const chatInst = roomsData[activeRoomIndex].chat;
if (chatInst && typeof chatInst.destruct === 'function') {
chatInst.destruct();
} else if (chatInst && typeof chatInst.disconnect === 'function') {
chatInst.disconnect();
}
roomsData[activeRoomIndex].chat = null;
// 移除房间
roomsData.splice(activeRoomIndex, 1);
if (roomsData.length > 0) {
// 切换到第一个房间
switchRoom(0);
} else {
// 没有房间了,回到登录
location.reload();
}
}
}
closeMenu();
}
};
document.addEventListener('click', function hideMenu(ev) {
if (!menu.contains(ev.target) && ev.target !== btn) {
closeMenu();
}
});
// 动画结束后允许再次触发
menu.addEventListener('animationend', function(e) {
animating = false;
});
menu.addEventListener('transitionend', function(e) {
animating = false;
});
}
window.addEventListener('DOMContentLoaded', () => {
const loginContainer = document.getElementById('login-container');
const chatContainer = document.getElementById('chat-container');
const loginForm = document.getElementById('login-form');
if (loginForm) {
loginForm.addEventListener('submit', loginFormHandler(null));
}
// 绑定“进入新房间”按钮
const joinBtn = document.querySelector('.join-room');
if (joinBtn) joinBtn.onclick = openLoginModal;
setupInputPlaceholder();
setupMoreBtnMenu();
// 自动聚焦到输入框
const input = document.querySelector('.input-message-input');
if (input) {
input.focus();
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
const text = input.innerText.trim();
// 只用当前房间的 chat 实例
if (text && roomsData[activeRoomIndex]?.chat) {
roomsData[activeRoomIndex].chat.sendChannelMessage('text', text);
addMsg(text);
input.innerText = '';
input.dispatchEvent(new Event('input'));
}
}
});
}
});
// 初始化
renderRooms(activeRoomIndex);
renderMainHeader();
renderUserList();
setupTabs();
renderChatArea();