(() => { 'use strict' const state = { csrf: '', plans: [], subscriptions: null, subscriptionPage: 1, subscriptionPageSize: 50, captcha: { enabled: false, provider: '' } } const $ = (selector) => document.querySelector(selector) const loginView = $('#login-view') const appView = $('#app-view') const loginForm = $('#login-form') const basePath = (document.body.dataset.basePath || '').replace(/\/$/, '') const route = (path) => `${basePath}${path}` function showError(message, target = $('#app-error')) { target.textContent = message || '请求失败' target.hidden = !message } function clearError(target = $('#app-error')) { target.textContent = ''; target.hidden = true } async function request(path, options = {}) { const headers = new Headers(options.headers || {}) headers.set('Accept', 'application/json') if (options.body && !headers.has('Content-Type')) headers.set('Content-Type', 'application/json') if (state.csrf && options.method && options.method !== 'GET') headers.set('X-CSRF-Token', state.csrf) const response = await fetch(route(path), { ...options, headers, credentials: 'same-origin' }) const payload = await response.json().catch(() => ({})) if (!response.ok) { if (response.status === 401) { state.csrf = ''; loginView.hidden = false; appView.hidden = true } throw new Error(payload.error || '请求失败') } return payload } function unwrap(payload) { return payload && payload.code === 0 && Object.prototype.hasOwnProperty.call(payload, 'data') ? payload.data : payload } function formatNumber(value) { if (value === null || value === undefined || value === '') return '-' const number = Number(value) return Number.isFinite(number) ? number.toLocaleString('zh-CN', { maximumFractionDigits: 6 }) : String(value) } function formatDate(value) { if (!value) return '-' const date = new Date(value) return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString('zh-CN', { dateStyle: 'medium', timeStyle: 'short' }) } function escapeHTML(value) { return String(value ?? '').replace(/[&<>'"]/g, (character) => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[character])) } function statusLabel(value) { const safe = String(value || 'unknown'); return `${escapeHTML(safe)}` } function setView(name) { document.querySelectorAll('.tab').forEach((button) => button.classList.toggle('active', button.dataset.view === name)) document.querySelectorAll('.view').forEach((view) => { view.hidden = view.id !== `view-${name}` }) if (name === 'plans' && !state.plans.length) loadPlans() if (name === 'subscriptions' && !state.subscriptions) loadSubscriptions() if (name === 'audit') loadAudit() } async function login(event) { event.preventDefault(); clearError($('#login-error')) const form = new FormData(loginForm); const button = loginForm.querySelector('button[type="submit"]'); button.disabled = true try { const captchaToken = String(form.get('captcha_token') || '').trim() if (state.captcha.enabled && !captchaToken) throw new Error('请先完成安全验证并填写验证结果') let payload = await request('/login', { method: 'POST', body: JSON.stringify({ email: form.get('email'), password: form.get('password'), turnstile_token: captchaToken || undefined, tencent_captcha_ticket: state.captcha.provider === 'tencent' ? captchaToken || undefined : undefined, tencent_captcha_randstr: state.captcha.provider === 'tencent' ? String(form.get('captcha_randstr') || '').trim() || undefined : undefined }) }) if (payload.requires_2fa) { const code = window.prompt('请输入管理员 2FA 验证码') if (!code) throw new Error('需要 2FA 验证码') payload = await request('/login/2fa', { method: 'POST', body: JSON.stringify({ pending_token: payload.pending_token, totp_code: code }) }) } if (!payload.ok || !payload.csrf_token) throw new Error('登录响应无效') state.csrf = payload.csrf_token; loginView.hidden = true; appView.hidden = false const user = payload.user || {}; $('#operator').textContent = user.email || user.username || `管理员 #${user.id || '-'}` await Promise.all([loadStatus(), loadOverview()]) } catch (error) { showError(error.message, $('#login-error')) } finally { button.disabled = false } } async function loadPlans() { try { const payload = unwrap(await request('/api/plans')); state.plans = Array.isArray(payload) ? payload : []; $('#plan-count').textContent = formatNumber(state.plans.length); renderPlans(); return true } catch (error) { showError(error.message); return false } } async function loadSubscriptions() { try { const params = new URLSearchParams(); const form = new FormData($('#subscription-filter')) const userID = String(form.get('user_id') || '').trim(); const status = String(form.get('status') || '') if (userID && !/^[1-9][0-9]*$/.test(userID)) throw new Error('用户 ID 必须是正整数') let endpoint = '/api/subscriptions' if (userID && !status) endpoint = `/api/users/${encodeURIComponent(userID)}/subscriptions` if (endpoint === '/api/subscriptions') { if (userID) params.set('user_id', userID); if (status) params.set('status', status); params.set('page', String(state.subscriptionPage)); params.set('page_size', String(state.subscriptionPageSize)) } const payload = unwrap(await request(`${endpoint}${params.toString() ? `?${params.toString()}` : ''}`)) || {}; state.subscriptions = payload $('#subscription-count').textContent = formatNumber(Array.isArray(payload) ? payload.length : (payload.total ?? payload.items?.length ?? 0)); renderSubscriptions(payload); return true } catch (error) { showError(error.message); return false } } async function loadHealth() { try { const response = await fetch(route('/healthz'), { credentials: 'same-origin', headers: { Accept: 'application/json' } }); return response.ok } catch { return false } } async function loadOverview() { clearError(); $('#sync-time').textContent = '同步中' const results = await Promise.all([loadHealth(), loadPlans(), loadSubscriptions()]) const healthy = results.every(Boolean); const degraded = results.some(Boolean) setCoreStatus(healthy ? 'ok' : degraded ? 'degraded' : 'bad') $('#sync-time').textContent = degraded ? `最近同步:${formatDate(new Date().toISOString())}` : '同步失败' } async function loadAudit() { try { const payload = await request('/api/audit'); renderAudit(payload.items || []) } catch (error) { showError(error.message) } } async function loadStatus() { try { const payload = await request('/api/status'); $('#credential-status').textContent = payload.credential_state === 'server_managed' ? '服务端托管(不回显)' : '不可用' } catch { $('#credential-status').textContent = '不可用' } } async function loadCaptchaConfig() { try { const payload = await request('/api/captcha-config'); state.captcha = payload const panel = $('#captcha-panel') if (!payload.enabled) { panel.hidden = true; return } panel.hidden = false; $('#captcha-label').textContent = `${payload.provider || '安全'}验证` const descriptions = { geetest: 'Core 已启用 GeeTest。请在官方验证组件完成挑战后,将返回的 JSON 验证结果粘贴到此处。', turnstile: 'Core 已启用 Cloudflare Turnstile。请完成挑战后填写返回的验证结果。', tencent: 'Core 已启用腾讯验证码。请填写 ticket,并在下方填写 randstr。', aliyun: 'Core 已启用阿里云验证码。请填写 captchaVerifyParam。' } $('#captcha-help').textContent = descriptions[payload.provider] || '请完成 Core 配置的验证码后填写验证结果。' $('#captcha-randstr-row').hidden = payload.provider !== 'tencent' } catch { $('#captcha-panel').hidden = true } } async function loadBalance(userID) { if (!/^[1-9][0-9]*$/.test(userID)) throw new Error('用户 ID 必须是正整数') const payload = unwrap(await request(`/api/users/${encodeURIComponent(userID)}`)) || {}; $('#user-balance').textContent = formatNumber(payload.balance) } async function bootstrap() { try { const payload = await request('/api/me'); state.csrf = payload.csrf_token || '' if (!state.csrf || !payload.user) throw new Error('session unavailable') loginView.hidden = true; appView.hidden = false const user = payload.user; $('#operator').textContent = user.email || user.username || `管理员 #${user.id || '-'}` await Promise.all([loadCaptchaConfig(), loadStatus(), loadOverview()]) } catch { loginView.hidden = false; appView.hidden = true; await loadCaptchaConfig() } } function setCoreStatus(stateName) { const element = $('#core-status'); const labels = { ok: '已连接', degraded: '部分可用', bad: '不可用' }; element.textContent = labels[stateName] || '检查中'; element.className = `status ${stateName === 'ok' ? 'ok' : stateName === 'bad' ? 'bad' : ''}` } function parseFeatures(features) { if (!features) return []; if (Array.isArray(features)) return features; if (typeof features !== 'string') return []; try { const parsed = JSON.parse(features); return Array.isArray(parsed) ? parsed : [] } catch { return features.split(/[,\n]/).map((item) => item.trim()).filter(Boolean) } } function renderPlans() { const container = $('#plans-list'); if (!state.plans.length) { container.innerHTML = '

暂无套餐数据

'; return } container.innerHTML = state.plans.map((plan) => { const groups = (plan.included_groups || []).map((group) => `${escapeHTML(group.name || group.id)}`).join('') || '未返回分组' const features = parseFeatures(plan.features) return `

${escapeHTML(plan.name || plan.product_name || `套餐 #${plan.id}`)}

${escapeHTML(plan.description || '')}

${plan.for_sale ? '可售' : '下架'}
${formatNumber(plan.price)} ${escapeHTML(plan.currency || '')}
有效期
${formatNumber(plan.validity_days)} ${escapeHTML(plan.validity_unit || '天')}
周期额度
${formatNumber(plan.cycle_quota_usd)}
总额度
${formatNumber(plan.total_quota_usd)}
实例上限
${formatNumber(plan.max_subscriptions_per_user)}
${groups}
${features.length ? `` : ''}
` }).join('') } function renderSubscriptions(payload) { const container = $('#subscriptions-list'); const items = Array.isArray(payload) ? payload : (payload.items || []); const pagination = $('#subscription-pagination') if (Array.isArray(payload) || !payload.pages) pagination.hidden = true else { pagination.hidden = false; $('#subscription-page-info').textContent = `第 ${payload.page || state.subscriptionPage} / ${payload.pages} 页,共 ${formatNumber(payload.total)} 条`; $('#subscription-prev').disabled = (payload.page || state.subscriptionPage) <= 1; $('#subscription-next').disabled = (payload.page || state.subscriptionPage) >= payload.pages } if (!items.length) { container.innerHTML = '

暂无订阅数据

'; return } container.innerHTML = `${items.map((item) => ``).join('')}
订阅实例用户套餐状态有效期周期用量
#${escapeHTML(item.id)}${escapeHTML(item.user?.username || item.user?.email || item.user_id || '-')}${escapeHTML(item.plan_name || item.plan_id || '-')}${statusLabel(item.status)}${formatDate(item.starts_at)}
至 ${formatDate(item.expires_at)}
${formatNumber(item.cycle_usage_usd)} / ${formatNumber(item.cycle_quota_usd)}
` container.querySelectorAll('.detail-button').forEach((button) => button.addEventListener('click', () => showDetail(button.dataset.id))) } async function showDetail(id) { try { const payload = unwrap(await request(`/api/subscriptions/${encodeURIComponent(id)}`)); $('#detail-content').textContent = JSON.stringify(payload, null, 2); $('#detail-dialog').showModal() } catch (error) { showError(error.message) } } function renderAudit(items) { const container = $('#audit-list'); if (!items.length) { container.innerHTML = '

暂无操作记录

'; return }; container.innerHTML = `${items.slice().reverse().map((item) => ``).join('')}
时间动作结果用户 ID请求 ID
${formatDate(item.time)}${escapeHTML(item.action)}${statusLabel(item.result)}${escapeHTML(item.user_id ?? '-')}${escapeHTML(item.request_id || '-')}
` } loginForm.addEventListener('submit', login) $('#logout').addEventListener('click', async () => { try { await request('/logout', { method: 'POST' }) } catch { /* session is cleared locally */ } state.csrf = ''; loginView.hidden = false; appView.hidden = true; loginForm.reset() }) $('#refresh-all').addEventListener('click', loadOverview) $('#subscription-filter').addEventListener('submit', (event) => { event.preventDefault(); state.subscriptionPage = 1; state.subscriptions = null; loadSubscriptions() }) $('#subscription-prev').addEventListener('click', () => { if (state.subscriptionPage > 1) { state.subscriptionPage -= 1; loadSubscriptions() } }) $('#subscription-next').addEventListener('click', () => { const pages = Number(state.subscriptions?.pages || 0); if (state.subscriptionPage < pages) { state.subscriptionPage += 1; loadSubscriptions() } }) $('#balance-form').addEventListener('submit', async (event) => { event.preventDefault(); clearError(); const userID = String(new FormData(event.currentTarget).get('user_id') || '').trim(); try { await loadBalance(userID) } catch (error) { showError(error.message) } }) $('#close-detail').addEventListener('click', () => $('#detail-dialog').close()) document.querySelectorAll('.tab').forEach((button) => button.addEventListener('click', () => setView(button.dataset.view))) document.querySelectorAll('.reload').forEach((button) => button.addEventListener('click', () => { if (button.dataset.target === 'plans') loadPlans(); if (button.dataset.target === 'subscriptions') { state.subscriptions = null; loadSubscriptions() }; if (button.dataset.target === 'audit') loadAudit() })) void bootstrap() })()