2026-02-22 23:33:00 +08:00
|
|
|
// FunConnect Client - Renderer Logic
|
|
|
|
|
const $ = (sel) => document.querySelector(sel);
|
|
|
|
|
const $$ = (sel) => document.querySelectorAll(sel);
|
|
|
|
|
|
|
|
|
|
let isConnected = false;
|
|
|
|
|
let currentServerUrl = '';
|
2026-02-22 23:38:41 +08:00
|
|
|
let appSettings = {};
|
|
|
|
|
|
|
|
|
|
// Load settings on startup
|
|
|
|
|
(async () => {
|
|
|
|
|
appSettings = await window.funmc.getSettings();
|
|
|
|
|
if (appSettings.serverUrl) $('#server-url').value = appSettings.serverUrl;
|
|
|
|
|
if (appSettings.localPort) $('#join-local-port').value = appSettings.localPort;
|
|
|
|
|
showRecentServers();
|
|
|
|
|
})();
|
2026-02-22 23:33:00 +08:00
|
|
|
|
|
|
|
|
// ===== Window Controls =====
|
|
|
|
|
$('#btn-min').addEventListener('click', () => window.funmc.minimize());
|
|
|
|
|
$('#btn-max').addEventListener('click', () => window.funmc.maximize());
|
|
|
|
|
$('#btn-close').addEventListener('click', () => window.funmc.close());
|
|
|
|
|
|
|
|
|
|
// ===== Navigation =====
|
|
|
|
|
$$('.nav-item').forEach(item => {
|
|
|
|
|
item.addEventListener('click', () => {
|
|
|
|
|
const page = item.dataset.page;
|
|
|
|
|
$$('.nav-item').forEach(n => n.classList.remove('active'));
|
|
|
|
|
item.classList.add('active');
|
|
|
|
|
$$('.page').forEach(p => p.classList.remove('active'));
|
|
|
|
|
$(`#page-${page}`).classList.add('active');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ===== Connect Page =====
|
|
|
|
|
$('#btn-connect').addEventListener('click', async () => {
|
|
|
|
|
const url = $('#server-url').value.trim();
|
|
|
|
|
if (!url) {
|
|
|
|
|
showMsg('#connect-msg', '请输入服务器地址', 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const btn = $('#btn-connect');
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
btn.innerHTML = '<span class="spinner"></span> 连接中...';
|
|
|
|
|
showMsg('#connect-msg', '', '');
|
|
|
|
|
|
|
|
|
|
const result = await window.funmc.connectServer(url);
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
isConnected = true;
|
|
|
|
|
currentServerUrl = url;
|
|
|
|
|
updateServerStatus(true);
|
|
|
|
|
showMsg('#connect-msg', '连接成功!', 'success');
|
|
|
|
|
showServerInfo(result.data);
|
2026-02-22 23:38:41 +08:00
|
|
|
// Save to recent servers and settings
|
|
|
|
|
await window.funmc.addRecentServer(url);
|
|
|
|
|
await window.funmc.setSettings({ serverUrl: url });
|
|
|
|
|
appSettings.serverUrl = url;
|
|
|
|
|
showRecentServers();
|
2026-02-22 23:33:00 +08:00
|
|
|
} else {
|
|
|
|
|
isConnected = false;
|
|
|
|
|
updateServerStatus(false);
|
|
|
|
|
showMsg('#connect-msg', '连接失败: ' + result.error, 'error');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
btn.textContent = '连接服务器';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function showServerInfo(data) {
|
|
|
|
|
const grid = $('#server-info-grid');
|
|
|
|
|
grid.innerHTML = '';
|
|
|
|
|
const items = [
|
|
|
|
|
{ label: '节点名称', value: data.nodeName || 'N/A' },
|
|
|
|
|
{ label: '节点ID', value: data.nodeId || 'N/A' },
|
|
|
|
|
{ label: '运行模式', value: data.isMaster ? '主节点' : '工作节点' },
|
|
|
|
|
{ label: '运行状态', value: data.status === 'ok' ? '正常' : '异常' },
|
|
|
|
|
{ label: '运行时间', value: formatUptime(data.uptime || 0) },
|
|
|
|
|
{ label: '版本', value: 'v1.0.0' },
|
|
|
|
|
];
|
|
|
|
|
items.forEach(item => {
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.className = 'info-item';
|
|
|
|
|
div.innerHTML = `<div class="label">${item.label}</div><div class="value">${item.value}</div>`;
|
|
|
|
|
grid.appendChild(div);
|
|
|
|
|
});
|
|
|
|
|
$('#server-info').classList.remove('hidden');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateServerStatus(online) {
|
|
|
|
|
const dot = $('#server-status .status-dot');
|
|
|
|
|
const text = $('#server-status .status-text');
|
|
|
|
|
if (online) {
|
|
|
|
|
dot.className = 'status-dot online';
|
|
|
|
|
text.textContent = '已连接';
|
|
|
|
|
} else {
|
|
|
|
|
dot.className = 'status-dot offline';
|
|
|
|
|
text.textContent = '未连接';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== Rooms Page =====
|
|
|
|
|
$('#btn-refresh-rooms').addEventListener('click', loadRooms);
|
|
|
|
|
|
|
|
|
|
async function loadRooms() {
|
|
|
|
|
if (!isConnected) {
|
|
|
|
|
$('#rooms-list').innerHTML = '<div class="empty-state"><span class="empty-icon">🔗</span><p>请先连接服务器</p></div>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$('#rooms-list').innerHTML = '<div class="empty-state"><span class="spinner"></span><p>加载中...</p></div>';
|
|
|
|
|
|
|
|
|
|
const result = await window.funmc.listRooms();
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
$('#rooms-list').innerHTML = `<div class="empty-state"><span class="empty-icon">❌</span><p>${result.error}</p></div>`;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rooms = result.data.rooms || [];
|
|
|
|
|
if (rooms.length === 0) {
|
|
|
|
|
$('#rooms-list').innerHTML = '<div class="empty-state"><span class="empty-icon">🎮</span><p>暂无在线房间</p></div>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$('#rooms-list').innerHTML = rooms.map(room => `
|
|
|
|
|
<div class="room-card" data-id="${room.id}">
|
|
|
|
|
<div class="room-left">
|
|
|
|
|
<div class="room-icon">🎮</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div class="room-name">${escapeHtml(room.name)} ${room.password ? '🔒' : ''}</div>
|
|
|
|
|
<div class="room-meta">房间号: ${room.id} | 房主: ${escapeHtml(room.hostName)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="room-right">
|
|
|
|
|
<span class="room-players">👥 ${room.currentPlayers}/${room.maxPlayers}</span>
|
|
|
|
|
<span class="room-version">${room.gameEdition === 'java' ? 'Java' : '基岩'} ${room.gameVersion}</span>
|
|
|
|
|
<button class="room-btn-join" onclick="quickJoin('${room.id}')">加入</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 23:38:41 +08:00
|
|
|
async function quickJoin(roomId) {
|
2026-02-22 23:33:00 +08:00
|
|
|
// Navigate to join page and fill in room ID
|
|
|
|
|
$$('.nav-item').forEach(n => n.classList.remove('active'));
|
|
|
|
|
document.querySelector('[data-page="join"]').classList.add('active');
|
|
|
|
|
$$('.page').forEach(p => p.classList.remove('active'));
|
|
|
|
|
$('#page-join').classList.add('active');
|
|
|
|
|
$('#join-room-id').value = roomId;
|
2026-02-22 23:38:41 +08:00
|
|
|
if (appSettings.localPort) $('#join-local-port').value = appSettings.localPort;
|
2026-02-22 23:33:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== Host Page =====
|
|
|
|
|
$('#btn-host').addEventListener('click', async () => {
|
|
|
|
|
if (!isConnected) {
|
|
|
|
|
showMsg('#host-msg', '请先连接服务器', 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const name = $('#host-name').value.trim();
|
|
|
|
|
const port = parseInt($('#host-port').value);
|
|
|
|
|
const version = $('#host-version').value.trim();
|
|
|
|
|
const edition = $('#host-edition').value;
|
|
|
|
|
const maxPlayers = parseInt($('#host-maxplayers').value);
|
|
|
|
|
const password = $('#host-password').value;
|
|
|
|
|
|
|
|
|
|
if (!name) {
|
|
|
|
|
showMsg('#host-msg', '请输入房间名称', 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const btn = $('#btn-host');
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
btn.innerHTML = '<span class="spinner"></span> 创建中...';
|
|
|
|
|
showMsg('#host-msg', '', '');
|
|
|
|
|
|
|
|
|
|
const result = await window.funmc.hostRoom({
|
|
|
|
|
serverUrl: currentServerUrl,
|
|
|
|
|
roomName: name,
|
|
|
|
|
localMcPort: port,
|
|
|
|
|
gameVersion: version,
|
|
|
|
|
gameEdition: edition,
|
|
|
|
|
maxPlayers: maxPlayers,
|
|
|
|
|
password: password || undefined,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
showMsg('#host-msg', '', '');
|
|
|
|
|
$('#host-room-id').textContent = result.data.room.id;
|
|
|
|
|
$('#host-result').classList.remove('hidden');
|
|
|
|
|
} else {
|
|
|
|
|
showMsg('#host-msg', '创建失败: ' + result.error, 'error');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
btn.textContent = '创建房间';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$('#btn-copy-room').addEventListener('click', () => {
|
|
|
|
|
const roomId = $('#host-room-id').textContent;
|
|
|
|
|
navigator.clipboard.writeText(roomId).then(() => {
|
|
|
|
|
$('#btn-copy-room').textContent = '已复制!';
|
|
|
|
|
setTimeout(() => { $('#btn-copy-room').textContent = '复制房间号'; }, 2000);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ===== Join Page =====
|
|
|
|
|
$('#btn-join').addEventListener('click', async () => {
|
|
|
|
|
const roomId = $('#join-room-id').value.trim();
|
|
|
|
|
const host = $('#join-host').value.trim();
|
|
|
|
|
const port = parseInt($('#join-port').value);
|
|
|
|
|
const localPort = parseInt($('#join-local-port').value);
|
|
|
|
|
|
|
|
|
|
if (!roomId) {
|
|
|
|
|
showMsg('#join-msg', '请输入房间号', 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!host) {
|
|
|
|
|
showMsg('#join-msg', '请输入中继服务器地址', 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const btn = $('#btn-join');
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
btn.innerHTML = '<span class="spinner"></span> 连接中...';
|
|
|
|
|
showMsg('#join-msg', '', '');
|
|
|
|
|
|
|
|
|
|
const result = await window.funmc.joinRoom({
|
|
|
|
|
serverHost: host,
|
|
|
|
|
serverPort: port,
|
|
|
|
|
roomId: roomId,
|
|
|
|
|
localPort: localPort,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
showMsg('#join-msg', '', '');
|
|
|
|
|
$('#join-active-port').textContent = String(localPort);
|
|
|
|
|
$('#join-status').classList.remove('hidden');
|
|
|
|
|
$('#btn-join').classList.add('hidden');
|
|
|
|
|
$('#btn-leave').classList.remove('hidden');
|
|
|
|
|
} else {
|
|
|
|
|
showMsg('#join-msg', '加入失败: ' + result.error, 'error');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
btn.textContent = '加入房间';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$('#btn-leave').addEventListener('click', async () => {
|
|
|
|
|
await window.funmc.disconnect();
|
|
|
|
|
$('#join-status').classList.add('hidden');
|
|
|
|
|
$('#btn-join').classList.remove('hidden');
|
|
|
|
|
$('#btn-leave').classList.add('hidden');
|
|
|
|
|
showMsg('#join-msg', '已断开连接', 'success');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ===== Relay Status Events =====
|
|
|
|
|
window.funmc.onRelayStatus((data) => {
|
|
|
|
|
if (data.status === 'disconnected') {
|
|
|
|
|
$('#join-status').classList.add('hidden');
|
|
|
|
|
$('#btn-join').classList.remove('hidden');
|
|
|
|
|
$('#btn-leave').classList.add('hidden');
|
|
|
|
|
showMsg('#join-msg', '连接已断开', 'error');
|
|
|
|
|
} else if (data.status === 'error') {
|
|
|
|
|
showMsg('#join-msg', '连接错误: ' + data.error, 'error');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ===== Auto-refresh rooms when navigating =====
|
|
|
|
|
document.querySelector('[data-page="rooms"]').addEventListener('click', () => {
|
|
|
|
|
if (isConnected) loadRooms();
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-22 23:38:41 +08:00
|
|
|
// ===== Settings Page =====
|
|
|
|
|
document.querySelector('[data-page="settings"]').addEventListener('click', loadSettings);
|
|
|
|
|
|
|
|
|
|
async function loadSettings() {
|
|
|
|
|
appSettings = await window.funmc.getSettings();
|
|
|
|
|
$('#settings-name').value = appSettings.playerName || '';
|
|
|
|
|
$('#settings-port').value = appSettings.localPort || 25566;
|
|
|
|
|
$('#settings-autoreconnect').checked = appSettings.autoReconnect !== false;
|
|
|
|
|
$('#settings-tray').checked = appSettings.minimizeToTray !== false;
|
|
|
|
|
|
|
|
|
|
const list = $('#settings-recent-list');
|
|
|
|
|
const recent = appSettings.recentServers || [];
|
|
|
|
|
if (recent.length === 0) {
|
|
|
|
|
list.innerHTML = '<div class="recent-list-empty">暂无记录</div>';
|
|
|
|
|
} else {
|
|
|
|
|
list.innerHTML = recent.map(url => `
|
|
|
|
|
<div class="recent-list-item">
|
|
|
|
|
<span class="url">${escapeHtml(url)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$('#btn-save-settings').addEventListener('click', async () => {
|
|
|
|
|
const settings = {
|
|
|
|
|
playerName: $('#settings-name').value.trim() || 'Player',
|
|
|
|
|
localPort: parseInt($('#settings-port').value) || 25566,
|
|
|
|
|
autoReconnect: $('#settings-autoreconnect').checked,
|
|
|
|
|
minimizeToTray: $('#settings-tray').checked,
|
|
|
|
|
};
|
|
|
|
|
await window.funmc.setSettings(settings);
|
|
|
|
|
Object.assign(appSettings, settings);
|
|
|
|
|
$('#join-local-port').value = settings.localPort;
|
|
|
|
|
showMsg('#settings-msg', '设置已保存', 'success');
|
|
|
|
|
setTimeout(() => showMsg('#settings-msg', '', ''), 2000);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function showRecentServers() {
|
|
|
|
|
const settings = await window.funmc.getSettings();
|
|
|
|
|
const recent = settings.recentServers || [];
|
|
|
|
|
const container = $('#recent-servers');
|
|
|
|
|
if (recent.length === 0) {
|
|
|
|
|
container.classList.add('hidden');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
container.classList.remove('hidden');
|
|
|
|
|
container.innerHTML = recent.map(url => `
|
|
|
|
|
<div class="recent-item" data-url="${escapeHtml(url)}">${escapeHtml(url)}</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
container.querySelectorAll('.recent-item').forEach(item => {
|
|
|
|
|
item.addEventListener('click', () => {
|
|
|
|
|
$('#server-url').value = item.dataset.url;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 08:21:09 +08:00
|
|
|
// ===== Room Detail Modal =====
|
|
|
|
|
let currentModalRoomId = null;
|
|
|
|
|
let modalRefreshTimer = null;
|
|
|
|
|
|
|
|
|
|
function openRoomModal(roomId) {
|
|
|
|
|
currentModalRoomId = roomId;
|
|
|
|
|
$('#modal-overlay').classList.remove('hidden');
|
|
|
|
|
refreshModalRoom();
|
|
|
|
|
modalRefreshTimer = setInterval(refreshModalRoom, 5000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeRoomModal() {
|
|
|
|
|
currentModalRoomId = null;
|
|
|
|
|
$('#modal-overlay').classList.add('hidden');
|
|
|
|
|
if (modalRefreshTimer) { clearInterval(modalRefreshTimer); modalRefreshTimer = null; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$('#modal-close').addEventListener('click', closeRoomModal);
|
|
|
|
|
$('#modal-overlay').addEventListener('click', (e) => {
|
|
|
|
|
if (e.target === $('#modal-overlay')) closeRoomModal();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function refreshModalRoom() {
|
|
|
|
|
if (!currentModalRoomId || !isConnected) return;
|
|
|
|
|
const result = await window.funmc.getRoomDetail(currentModalRoomId);
|
|
|
|
|
if (!result || !result.success) return;
|
|
|
|
|
const room = result.data.room;
|
|
|
|
|
const players = result.data.players || [];
|
|
|
|
|
|
|
|
|
|
$('#modal-room-name').textContent = `${room.name} ${room.password ? '🔒' : ''}`;
|
|
|
|
|
$('#modal-room-info').innerHTML = [
|
|
|
|
|
{ l: '房间号', v: room.id },
|
|
|
|
|
{ l: '房主', v: room.hostName },
|
|
|
|
|
{ l: '版本', v: `${room.gameEdition === 'java' ? 'Java' : '基岩'} ${room.gameVersion}` },
|
|
|
|
|
{ l: '玩家', v: `${room.currentPlayers}/${room.maxPlayers}` },
|
|
|
|
|
].map(i => `<div class="modal-info-item"><div class="label">${i.l}</div><div class="value">${i.v}</div></div>`).join('');
|
|
|
|
|
|
|
|
|
|
$('#modal-player-count').textContent = `(${players.length})`;
|
|
|
|
|
if (players.length === 0) {
|
|
|
|
|
$('#modal-player-list').innerHTML = '<div class="player-empty">暂无玩家在线</div>';
|
|
|
|
|
} else {
|
|
|
|
|
$('#modal-player-list').innerHTML = players.map(p => `
|
|
|
|
|
<div class="player-item">
|
|
|
|
|
<div>
|
|
|
|
|
<span class="player-name">👤 ${escapeHtml(p.name)}</span>
|
|
|
|
|
<span class="player-time">${formatTimeSince(p.joinedAt)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="btn-kick" onclick="kickPlayer('${currentModalRoomId}','${p.id}')">踢出</button>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function kickPlayer(roomId, playerId) {
|
|
|
|
|
if (!confirm('确定要踢出该玩家吗?')) return;
|
|
|
|
|
const result = await window.funmc.kickPlayer(roomId, playerId);
|
|
|
|
|
if (result && result.success) {
|
|
|
|
|
refreshModalRoom();
|
|
|
|
|
} else {
|
|
|
|
|
alert('踢出失败: ' + (result?.error || '未知错误'));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$('#modal-join-btn').addEventListener('click', () => {
|
|
|
|
|
if (currentModalRoomId) {
|
|
|
|
|
closeRoomModal();
|
|
|
|
|
quickJoin(currentModalRoomId);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$('#modal-delete-btn').addEventListener('click', async () => {
|
|
|
|
|
if (!currentModalRoomId) return;
|
|
|
|
|
if (!confirm('确定要删除此房间吗?此操作不可撤销。')) return;
|
|
|
|
|
const result = await window.funmc.deleteRoom(currentModalRoomId);
|
|
|
|
|
if (result && result.success) {
|
|
|
|
|
closeRoomModal();
|
|
|
|
|
loadRooms();
|
|
|
|
|
} else {
|
|
|
|
|
alert('删除失败: ' + (result?.error || '未知错误'));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Chat in modal
|
|
|
|
|
$('#modal-chat-send').addEventListener('click', sendChatMessage);
|
|
|
|
|
$('#modal-chat-input').addEventListener('keydown', (e) => {
|
|
|
|
|
if (e.key === 'Enter') sendChatMessage();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function sendChatMessage() {
|
|
|
|
|
const input = $('#modal-chat-input');
|
|
|
|
|
const msg = input.value.trim();
|
|
|
|
|
if (!msg || !currentModalRoomId) return;
|
|
|
|
|
input.value = '';
|
|
|
|
|
const sender = appSettings.playerName || 'Player';
|
|
|
|
|
appendChatMessage(sender, msg);
|
|
|
|
|
await window.funmc.sendChat(currentModalRoomId, sender, msg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendChatMessage(sender, text, time) {
|
|
|
|
|
const container = $('#modal-chat-messages');
|
|
|
|
|
const t = time || new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.className = 'chat-msg';
|
|
|
|
|
div.innerHTML = `<span class="chat-sender">${escapeHtml(sender)}</span> <span class="chat-text">${escapeHtml(text)}</span><span class="chat-time">${t}</span>`;
|
|
|
|
|
container.appendChild(div);
|
|
|
|
|
container.scrollTop = container.scrollHeight;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Make room cards open modal
|
|
|
|
|
document.addEventListener('click', (e) => {
|
|
|
|
|
const card = e.target.closest('.room-card');
|
|
|
|
|
if (card && !e.target.classList.contains('room-btn-join')) {
|
|
|
|
|
openRoomModal(card.dataset.id);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-22 23:33:00 +08:00
|
|
|
// ===== Utilities =====
|
|
|
|
|
function showMsg(selector, text, type) {
|
|
|
|
|
const el = $(selector);
|
|
|
|
|
el.textContent = text;
|
|
|
|
|
el.className = 'msg' + (type ? ' ' + type : '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function escapeHtml(str) {
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.textContent = str;
|
|
|
|
|
return div.innerHTML;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatUptime(seconds) {
|
|
|
|
|
const h = Math.floor(seconds / 3600);
|
|
|
|
|
const m = Math.floor((seconds % 3600) / 60);
|
|
|
|
|
const s = Math.floor(seconds % 60);
|
|
|
|
|
if (h > 0) return `${h}时${m}分${s}秒`;
|
|
|
|
|
if (m > 0) return `${m}分${s}秒`;
|
|
|
|
|
return `${s}秒`;
|
|
|
|
|
}
|
2026-02-23 08:21:09 +08:00
|
|
|
|
|
|
|
|
function formatTimeSince(dateStr) {
|
|
|
|
|
const ms = Date.now() - new Date(dateStr).getTime();
|
|
|
|
|
const m = Math.floor(ms / 60000);
|
|
|
|
|
if (m < 1) return '刚刚';
|
|
|
|
|
if (m < 60) return `${m}分钟前`;
|
|
|
|
|
return `${Math.floor(m / 60)}小时前`;
|
|
|
|
|
}
|