(() => {
'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 ? `${features.map((feature) => `- ${escapeHTML(feature)}
`).join('')}
` : ''}`
}).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) => `#${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)} | |
`).join('')}
`
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 = `| 时间 | 动作 | 结果 | 用户 ID | 请求 ID |
${items.slice().reverse().map((item) => `| ${formatDate(item.time)} | ${escapeHTML(item.action)} | ${statusLabel(item.result)} | ${escapeHTML(item.user_id ?? '-')} | ${escapeHTML(item.request_id || '-')} |
`).join('')}
` }
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()
})()