const $ = (selector) => document.querySelector(selector) const fields = (name) => document.querySelectorAll(`[data-field="${name}"]`) const setField = (name, value) => fields(name).forEach((node) => { node.textContent = String(value) }) const button = $('#draw-button') const stage = $('.draw-stage') const statusLine = $('#draw-status') const rankingBody = $('#ranking-body') const recordList = $('#record-list') const creditMode = $('#credit-mode') const dialog = $('#winner-dialog') let state = null let drawing = false function money(value, digits = 2) { return Number(value).toLocaleString('en-US', { minimumFractionDigits: digits, maximumFractionDigits: digits }) } function compact(value) { return new Intl.NumberFormat('zh-CN', { notation: 'compact', maximumFractionDigits: 1 }).format(Number(value)) } function localTime(value) { return new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }).format(new Date(value)) } function renderRanking(ranking) { const rows = ranking?.rows ?? [] if (!rows.length) { rankingBody.innerHTML = '当前窗口暂无用量' return } rankingBody.innerHTML = rows.map((row) => ` ${String(row.rank).padStart(2, '0')} ${escapeHtml(row.displayName)} $${money(row.actualCost, 3)} ${compact(row.requests)} 次${compact(row.tokens)} tokens `).join('') } function renderRecords(records) { if (!records?.length) { recordList.innerHTML = '
  • 第一位幸运用户,会是谁?
  • ' return } recordList.innerHTML = records.map((record, index) => `
  • #${String(index + 1).padStart(2, '0')} ${escapeHtml(record.winnerDisplayName)}${localTime(record.drawnAt)} · ${record.eligibleCount} 人等概率 $${money(record.prizeAmountUsd, 0)}${creditLabel(record.creditStatus)}
  • `).join('') } function creditLabel(status) { return ({ succeeded: '已充值', dry_run: '模拟充值', disabled: '充值未开启', pending: '充值待确认', failed: '充值失败' })[status] ?? status } function escapeHtml(value) { const node = document.createElement('span') node.textContent = String(value) return node.innerHTML } function render(next) { state = next setField('active-hours', next.activeWithinHours) setField('prize', money(next.prizeAmountUsd, 0)) setField('remaining', next.remainingDraws) setField('eligible', next.eligibleUserCount ?? '—') setField('daily-grant', next.dailyGrantCount) setField('next-grant', localTime(next.nextGrantAt)) setField('ranking-label', next.rankingWindowDays === 1 ? '今日' : `近 ${next.rankingWindowDays} 日`) setField('ranking-total', `$${money(next.ranking?.totals?.actualCost ?? 0)}`) renderRanking(next.ranking) renderRecords(next.records) const autoEnabled = next.automaticCredit?.enabled === true creditMode.textContent = autoEnabled ? (next.automaticCredit.mode === 'live' ? '自动充值已开启' : '自动充值模拟中') : '自动充值暂未开启' button.disabled = drawing || Number(next.remainingDraws) < 1 || Number(next.eligibleUserCount) < 1 statusLine.textContent = Number(next.remainingDraws) < 1 ? '今天的机会已用完,明早 06:00 再来' : `${next.eligibleUserCount} 名候选人已就位` } async function requestJson(url, options = {}) { const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 12000) try { const response = await fetch(url, { ...options, signal: controller.signal }) const raw = await response.text() let data = null try { data = raw ? JSON.parse(raw) : null } catch { /* handled below */ } if (!response.ok || !data?.ok) throw new Error(data?.error ?? `服务暂不可用(HTTP ${response.status})`) return data } catch (error) { if (error instanceof DOMException && error.name === 'AbortError') throw new Error('请求超时,请稍后重试') throw error } finally { clearTimeout(timeout) } } async function loadState() { const data = await requestJson('/api/public/state', { cache: 'no-store' }) render(data) } function showStateError(error) { const message = error instanceof Error ? error.message : String(error) statusLine.textContent = `${message},正在重试…` rankingBody.innerHTML = '服务暂时不可用,正在重试…' creditMode.textContent = '连接中断' button.disabled = true } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)) } async function draw() { if (drawing) return drawing = true button.disabled = true stage.classList.add('is-drawing') const phrases = ['锁定活跃用户…', '打乱候选顺序…', '正在碰撞好运…', '马上揭晓!'] let phraseIndex = 0 const ticker = setInterval(() => { statusLine.textContent = phrases[phraseIndex % phrases.length] phraseIndex += 1 }, 620) try { const request = requestJson('/api/public/draw', { method: 'POST', headers: { 'content-type': 'application/json' } }) const [, data] = await Promise.all([sleep(2900), request]) $('#winner-name').textContent = data.record.winnerDisplayName $('#winner-prize').textContent = money(data.record.prizeAmountUsd, 0) $('#winner-meta').textContent = `${data.record.eligibleCount} 人等概率 · ${creditLabel(data.record.creditStatus)}` dialog.showModal() await loadState() } catch (error) { statusLine.textContent = error instanceof Error ? error.message : String(error) await loadState().catch((stateError) => { state = null showStateError(stateError) setTimeout(boot, 5000) }) } finally { clearInterval(ticker) drawing = false stage.classList.remove('is-drawing') button.disabled = !state || Number(state.remainingDraws) < 1 || Number(state.eligibleUserCount) < 1 } } button.addEventListener('click', draw) $('#winner-close').addEventListener('click', () => dialog.close()) dialog.addEventListener('click', (event) => { if (event.target === dialog) dialog.close() }) async function boot() { try { await loadState() } catch (error) { showStateError(error) setTimeout(boot, 5000) } } boot() setInterval(() => { if (!drawing && !document.hidden) loadState().catch(showStateError) }, 30000)