Files
FunConnect/client/renderer/app.js
FunMC 80fe5e6e6e feat: v1.2.0 房间详情/聊天/踢人 + 速率限制 + WebSocket增强
Server:
- API速率限制中间件 (120 req/min per IP, X-RateLimit headers)
- 房间聊天API: POST /rooms/:id/chat
- 认证中间件放行公开GET路由和房间join
- WebSocket: 房间订阅/取消订阅 (subscribe/unsubscribe)
- WebSocket: 房间聊天广播 (chat -> broadcastToRoom)
- WebSocket: 房间事件通知 (roomCreated/Deleted/playerJoined/Left)

Client:
- 房间详情弹窗: 点击房间卡片打开
  - 房间信息网格 (房间号/房主/版本/人数)
  - 在线玩家列表 (5秒自动刷新)
  - 踢出玩家 (确认对话框)
  - 房间聊天 (实时发送/显示)
  - 加入房间 / 删除房间按钮
- 连接状态指示器动画 (online/offline/connecting)
- 房间卡片hover效果
- 版本更新到 v1.2.0
- ApiClient: 新增 getRoomDetail/kickPlayer/sendChat
- Preload: 新增对应IPC方法
- Main: 新增 rooms:detail/kick/chat handlers
2026-02-23 08:21:09 +08:00

474 lines
16 KiB
JavaScript

// FunConnect Client - Renderer Logic
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => document.querySelectorAll(sel);
let isConnected = false;
let currentServerUrl = '';
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();
})();
// ===== 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);
// Save to recent servers and settings
await window.funmc.addRecentServer(url);
await window.funmc.setSettings({ serverUrl: url });
appSettings.serverUrl = url;
showRecentServers();
} 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('');
}
async function quickJoin(roomId) {
// 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;
if (appSettings.localPort) $('#join-local-port').value = appSettings.localPort;
}
// ===== 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();
});
// ===== 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;
});
});
}
// ===== 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);
}
});
// ===== 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}`;
}
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)}小时前`;
}