(function () { const body = document.body; const user = (() => { try { return JSON.parse(body.dataset.user || 'null'); } catch { return null; } })(); const statusEl = document.getElementById('status'); const usersTable = document.getElementById('users-table'); const roomsTable = document.getElementById('rooms-table'); const cardsTable = document.getElementById('cards-table'); const oauthForm = document.getElementById('linuxdo-oauth-form'); const ROLE_OPTIONS = ['player', 'moderator', 'admin']; const setStatus = (text) => { statusEl.textContent = text || ''; }; const getJson = async (url) => { const res = await fetch(url); return res.json(); }; const patchJson = async (url, payload) => { const res = await fetch(url, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); return res.json(); }; const deleteJson = async (url) => { const res = await fetch(url, { method: 'DELETE' }); return res.json(); }; const formatDate = (isoText) => { const raw = String(isoText || ''); if (!raw) return '-'; try { return new Date(raw).toLocaleString(); } catch { return raw; } }; const makeTable = (headers) => { const table = document.createElement('table'); table.className = 'admin-table'; const thead = document.createElement('thead'); const tr = document.createElement('tr'); headers.forEach((h) => { const th = document.createElement('th'); th.textContent = h; tr.appendChild(th); }); thead.appendChild(tr); table.appendChild(thead); const tbody = document.createElement('tbody'); table.appendChild(tbody); return { table, tbody }; }; const renderUsers = (rows) => { usersTable.textContent = ''; const { table, tbody } = makeTable(['用户名', '权限组', '创建时间', '操作']); rows.forEach((row) => { const tr = document.createElement('tr'); const tdUsername = document.createElement('td'); tdUsername.textContent = row.username || '-'; tr.appendChild(tdUsername); const tdRole = document.createElement('td'); const select = document.createElement('select'); ROLE_OPTIONS.forEach((role) => { const option = document.createElement('option'); option.value = role; option.textContent = role; if (role === row.role) option.selected = true; select.appendChild(option); }); tdRole.appendChild(select); tr.appendChild(tdRole); const tdCreated = document.createElement('td'); tdCreated.textContent = formatDate(row.created_at); tr.appendChild(tdCreated); const tdAction = document.createElement('td'); const saveBtn = document.createElement('button'); saveBtn.type = 'button'; saveBtn.className = 'secondary-btn'; saveBtn.textContent = '保存权限'; saveBtn.addEventListener('click', async () => { setStatus(`正在更新 ${row.username} 的权限...`); try { const data = await patchJson(`/api/admin/users/${row.user_id}/role`, { role: select.value }); if (!data.ok) { setStatus(`更新失败: ${data.error || '未知错误'}`); return; } setStatus(`已更新 ${row.username} 权限为 ${select.value}`); loadAll(); } catch (error) { setStatus(`网络错误: ${error}`); } }); tdAction.appendChild(saveBtn); tr.appendChild(tdAction); tbody.appendChild(tr); }); usersTable.appendChild(table); }; const renderRooms = (rows) => { roomsTable.textContent = ''; const { table, tbody } = makeTable(['房间码', '剧本卡', '状态', '玩家数', '最近更新', '操作']); rows.forEach((row) => { const tr = document.createElement('tr'); const tdRoom = document.createElement('td'); tdRoom.textContent = row.room_id || '-'; tr.appendChild(tdRoom); const tdCard = document.createElement('td'); tdCard.textContent = row.card_title || row.game_card_id || '-'; tr.appendChild(tdCard); const tdStatus = document.createElement('td'); tdStatus.textContent = row.status || '-'; tr.appendChild(tdStatus); const tdCount = document.createElement('td'); tdCount.textContent = String(row.player_count ?? 0); tr.appendChild(tdCount); const tdUpdated = document.createElement('td'); tdUpdated.textContent = formatDate(row.updated_at); tr.appendChild(tdUpdated); const tdAction = document.createElement('td'); const delBtn = document.createElement('button'); delBtn.type = 'button'; delBtn.className = 'ghost-btn'; delBtn.textContent = '强制结束并删除'; delBtn.addEventListener('click', async () => { if (!confirm(`确认删除房间 ${row.room_id} 吗?`)) return; setStatus(`正在删除房间 ${row.room_id}...`); try { const data = await deleteJson(`/api/admin/rooms/${row.room_id}`); if (!data.ok) { setStatus(`删除失败: ${data.error || '未知错误'}`); return; } setStatus(`房间 ${row.room_id} 已删除。`); loadAll(); } catch (error) { setStatus(`网络错误: ${error}`); } }); tdAction.appendChild(delBtn); tr.appendChild(tdAction); tbody.appendChild(tr); }); roomsTable.appendChild(table); }; const renderCards = (rows) => { cardsTable.textContent = ''; const { table, tbody } = makeTable(['标题', '作者', '官方', '创建时间', '操作']); rows.forEach((row) => { const tr = document.createElement('tr'); const tdTitle = document.createElement('td'); tdTitle.textContent = row.title || '-'; tr.appendChild(tdTitle); const tdCreator = document.createElement('td'); tdCreator.textContent = row.creator_display_name || '-'; tr.appendChild(tdCreator); const tdOfficial = document.createElement('td'); tdOfficial.textContent = row.is_official ? '是' : '否'; tr.appendChild(tdOfficial); const tdCreated = document.createElement('td'); tdCreated.textContent = formatDate(row.created_at); tr.appendChild(tdCreated); const tdAction = document.createElement('td'); const editBtn = document.createElement('a'); editBtn.className = 'link-btn'; editBtn.href = `/cards/${row.card_id}/edit`; editBtn.textContent = '编辑'; tdAction.appendChild(editBtn); const toggleBtn = document.createElement('button'); toggleBtn.type = 'button'; toggleBtn.className = 'secondary-btn'; toggleBtn.textContent = row.is_official ? '设为玩家卡' : '设为官方卡'; toggleBtn.style.marginTop = '0.4rem'; toggleBtn.addEventListener('click', async () => { setStatus(`正在更新游戏卡「${row.title}」...`); try { const data = await patchJson(`/api/admin/cards/${row.card_id}`, { is_official: !row.is_official, }); if (!data.ok) { setStatus(`更新失败: ${data.error || '未知错误'}`); return; } setStatus(`游戏卡「${row.title}」已更新。`); loadAll(); } catch (error) { setStatus(`网络错误: ${error}`); } }); tdAction.appendChild(toggleBtn); const delBtn = document.createElement('button'); delBtn.type = 'button'; delBtn.className = 'ghost-btn'; delBtn.style.marginTop = '0.4rem'; delBtn.textContent = '删除游戏卡'; delBtn.addEventListener('click', async () => { if (!confirm(`确认删除游戏卡「${row.title}」吗?`)) return; setStatus(`正在删除游戏卡「${row.title}」...`); try { const data = await deleteJson(`/api/admin/cards/${row.card_id}`); if (!data.ok) { setStatus(`删除失败: ${data.error || '未知错误'}`); return; } setStatus(`游戏卡「${row.title}」已删除。`); loadAll(); } catch (error) { setStatus(`网络错误: ${error}`); } }); tdAction.appendChild(delBtn); tr.appendChild(tdAction); tbody.appendChild(tr); }); cardsTable.appendChild(table); }; const loadLinuxdoOauth = async () => { if (!oauthForm) return true; const data = await getJson('/api/admin/settings/oauth/linuxdo'); if (!data.ok) { setStatus(data.error || '加载 OAuth 配置失败'); return false; } const cfg = data.config || {}; document.getElementById('linuxdo-enabled').value = cfg.enabled ? 'true' : 'false'; document.getElementById('linuxdo-client-id').value = cfg.client_id || ''; document.getElementById('linuxdo-client-secret').value = ''; document.getElementById('linuxdo-authorize-url').value = cfg.authorize_url || ''; document.getElementById('linuxdo-token-url').value = cfg.token_url || ''; document.getElementById('linuxdo-userinfo-url').value = cfg.userinfo_url || ''; document.getElementById('linuxdo-scope').value = cfg.scope || ''; return true; }; if (oauthForm) { oauthForm.addEventListener('submit', async (event) => { event.preventDefault(); setStatus('正在保存 OAuth 配置...'); const payload = { enabled: document.getElementById('linuxdo-enabled').value === 'true', client_id: document.getElementById('linuxdo-client-id').value.trim(), client_secret: document.getElementById('linuxdo-client-secret').value, authorize_url: document.getElementById('linuxdo-authorize-url').value.trim(), token_url: document.getElementById('linuxdo-token-url').value.trim(), userinfo_url: document.getElementById('linuxdo-userinfo-url').value.trim(), scope: document.getElementById('linuxdo-scope').value.trim(), }; if (!payload.client_secret) { delete payload.client_secret; } try { const data = await patchJson('/api/admin/settings/oauth/linuxdo', payload); if (!data.ok) { setStatus(data.error || '保存失败'); return; } document.getElementById('linuxdo-client-secret').value = ''; setStatus('OAuth 配置已保存。'); } catch (error) { setStatus(`网络错误: ${error}`); } }); } const loadAll = async () => { if (!user || user.role !== 'admin') { window.location.href = '/'; return; } setStatus('正在加载后台数据...'); try { const [usersResp, roomsResp, cardsResp, oauthOk] = await Promise.all([ getJson('/api/admin/users'), getJson('/api/admin/rooms'), getJson('/api/admin/cards'), loadLinuxdoOauth(), ]); if (!usersResp.ok || !roomsResp.ok || !cardsResp.ok || !oauthOk) { const msg = usersResp.error || roomsResp.error || cardsResp.error || '加载失败'; setStatus(msg); return; } renderUsers(usersResp.users || []); renderRooms(roomsResp.rooms || []); renderCards(cardsResp.cards || []); setStatus('后台数据已更新。'); } catch (error) { setStatus(`网络错误: ${error}`); } }; loadAll(); })();