Files
nodecrypt/web/main.js
T
2025-05-04 17:41:57 +08:00

337 lines
11 KiB
JavaScript

import config from './config.js';
// 生成简易SVG头像
function getSvgAvatar(text, size = 42) {
const color = "#30a8f7";
// 只取第一个字母大写
const letter = text ? text[0].toUpperCase() : "?";
return `
<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
<circle cx="${size/2}" cy="${size/2}" r="${size/2}" fill="${color}"/>
<text x="50%" y="57%" text-anchor="middle" dominant-baseline="middle" font-size="${size/2}" fill="#fff" font-family="Arial, sans-serif">${letter}</text>
</svg>
`.replace(/\n/g, '');
}
let chat = null;
let myName = '';
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) {
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 = '匿名', 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;
}
// 占位房间和成员数量,实际项目请用你的数据替换
const ROOM_COUNT = 4;
const MEMBER_COUNT = 8;
// 渲染房间列表(UI占位)
function renderRooms(activeId = 0) {
const roomList = document.getElementById("room-list");
roomList.innerHTML = "";
for (let i = 0; i < ROOM_COUNT; i++) {
const div = document.createElement("div");
div.className = "room" + (i === activeId ? " active" : "");
div.innerHTML = `
<span class="avatar">${getSvgAvatar("R"+(i+1), 42)}</span>
<div class="info">
<div class="title">Room ${i+1}</div>
<div class="lastmsg"></div>
</div>
<div class="time"></div>
`;
div.onclick = () => {
renderRooms(i);
renderMainHeader(i);
clearChat();
};
roomList.appendChild(div);
}
}
// 渲染主面板房间头部(UI占位)
function renderMainHeader(roomId = 0) {
document.getElementById("main-header").innerHTML = `
<div style="display: flex; align-items: center;">
<span class="avatar">${getSvgAvatar("R"+(roomId+1), 40)}</span>
<div class="group-title">Room ${roomId+1}</div>
<span style="margin-left:10px;font-size:13px;color:#888;">${MEMBER_COUNT} members</span>
</div>
<div class="main-header-actions">
<button class="more-btn" id="more-btn" aria-label="更多选项">
<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">分享</div>
<div class="more-menu-item" data-action="exit">退出</div>
</div>
</div>
`;
setupMoreBtnMenu(); // 每次渲染都重新绑定事件
}
// 清空消息区(UI占位)
function clearChat() {
document.getElementById("chat-area").innerHTML = "";
}
// 渲染成员列表(UI占位)
function renderMembers() {
const list = document.getElementById("member-list");
list.innerHTML = "";
for (let i = 0; i < MEMBER_COUNT; i++) {
const div = document.createElement("div");
div.className = "member";
div.innerHTML = `
<span class="avatar">${getSvgAvatar("U"+(i+1), 38)}</span>
<div class="member-info">
<div class="member-name">User${i+1}</div>
<div class="member-status"></div>
</div>
`;
list.appendChild(div);
}
}
// 右侧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 closeTimer = null;
let animating = false;
function openMenu() {
if (animating) return;
menu.classList.remove('close');
menu.classList.add('open');
menu.style.display = 'block';
menuRect = menu.getBoundingClientRect();
}
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;
}, 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') {
alert('退出功能待实现');
}
closeMenu();
}
};
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();
window.removeEventListener('mousemove', onMouseMove);
}
}
menu.addEventListener('mouseenter', function() {
window.addEventListener('mousemove', onMouseMove);
});
menu.addEventListener('mouseleave', function() {
window.addEventListener('mousemove', onMouseMove);
});
btn.addEventListener('mouseleave', function() {
window.addEventListener('mousemove', onMouseMove);
});
btn.addEventListener('click', function() {
if (menu.classList.contains('open')) {
window.addEventListener('mousemove', onMouseMove);
}
});
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', 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();
if (!username || !room) {
alert('请输入用户名和房间名');
return;
}
myName = username;
loginContainer.style.display = 'none';
chatContainer.style.display = '';
const sidebarUsername = document.getElementById('sidebar-username');
if (sidebarUsername) sidebarUsername.textContent = username;
const avatar = document.getElementById('sidebar-user-avatar');
if (avatar) avatar.innerHTML = getSvgAvatar(username, 44);
setStatus('正在连接...');
// 初始化 ChatCrypt
const callbacks = {
onServerClosed: () => setStatus('服务器连接关闭'),
onServerSecured: () => setStatus('与服务器安全连接已建立'),
onClientSecured: () => {}, // 不显示“与xxx建立安全连接”
onClientList: () => {}, // 不显示“当前在线”
onClientMessage: (msg) => {
if (msg.username === myName) return;
addOtherMsg(msg.data, msg.username, msg.username);
}
};
chat = new window.ChatCrypt(config, callbacks);
chat.setCredentials(username, room, password);
chat.connect();
});
}
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();
if (text && chat) {
chat.sendChannelMessage('text', text);
addMsg(text);
input.innerText = '';
input.dispatchEvent(new Event('input'));
}
}
});
}
});
// 初始化
renderRooms();
renderMainHeader();
renderMembers();
setupTabs();
clearChat();