Files
Lottery/index.html
T
vincent 13a259b0f8 chore: initial commit — existing lottoData codebase
Files:
- lottery.py (1189 lines) — DoubleColorBallGenerator core engine
- fetch_data.py (131 lines) — history data fetcher from 55128.cn
- web_executor.py (216 lines) — data fetch Web console (Flask :5000)
- app.py (505 lines) — number generation Web service (Flask :8085)
- index.html (1171 lines) — frontend SPA
- web_console.html (323 lines) — fetch console frontend
- deploy/ — systemd service + cron script + logs

BIZ-74 architecture review baseline
2026-07-03 16:39:21 +08:00

1172 lines
41 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>🎯 双色球号码生成系统</title>
<style>
/* ============================================================
CSS Variables & Reset
============================================================ */
:root {
--primary: #e74c3c;
--primary-dark: #c0392b;
--blue: #3498db;
--blue-dark: #2980b9;
--bg: #f5f6fa;
--card-bg: #ffffff;
--text: #2c3e50;
--text-light: #7f8c8d;
--border: #e0e0e0;
--shadow: 0 2px 12px rgba(0,0,0,0.08);
--radius: 12px;
--nav-height: 60px;
--mobile-nav-height: 56px;
--red-ball: #e74c3c;
--blue-ball: #3498db;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
padding-bottom: 80px;
}
/* ============================================================
Header
============================================================ */
.header {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 50%, #8e44ad 100%);
color: white;
padding: 20px 24px;
text-align: center;
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
.header h1 { font-size: 22px; font-weight: 700; letter-spacing: 1px; }
.header p { font-size: 13px; opacity: 0.85; margin-top: 4px; }
/* ============================================================
Navigation Tabs
============================================================ */
.nav-tabs {
display: flex;
background: var(--card-bg);
border-bottom: 1px solid var(--border);
position: sticky;
top: 68px;
z-index: 99;
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
}
.nav-tab {
flex: 1;
padding: 14px 8px;
text-align: center;
font-size: 14px;
font-weight: 500;
color: var(--text-light);
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.2s;
user-select: none;
}
.nav-tab:hover { color: var(--text); background: #fafafa; }
.nav-tab.active {
color: var(--primary);
border-bottom-color: var(--primary);
font-weight: 600;
}
.nav-tab .icon { margin-right: 4px; }
/* ============================================================
Container & Pages
============================================================ */
.container { max-width: 960px; margin: 0 auto; padding: 16px; }
.page { display: none; }
.page.active { display: block; }
/* ============================================================
Card
============================================================ */
.card {
background: var(--card-bg);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 20px;
margin-bottom: 16px;
}
.card-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border);
}
/* ============================================================
Form Controls
============================================================ */
.form-group {
margin-bottom: 14px;
}
.form-group label {
display: block;
font-size: 14px;
font-weight: 500;
margin-bottom: 6px;
color: var(--text);
}
.form-row {
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: flex-end;
}
.form-row .form-group { flex: 1; min-width: 120px; margin-bottom: 0; }
select, input[type="number"] {
width: 100%;
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 15px;
color: var(--text);
background: white;
transition: border-color 0.2s;
outline: none;
}
select:focus, input[type="number"]:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(231,76,60,0.1);
}
/* ============================================================
Buttons
============================================================ */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 12px 28px;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
user-select: none;
}
.btn:active { transform: scale(0.97); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.btn-primary {
background: linear-gradient(135deg, #e74c3c, #c0392b);
color: white;
}
.btn-primary:hover:not(:disabled) { box-shadow: 0 4px 15px rgba(231,76,60,0.4); }
.btn-secondary {
background: var(--bg);
color: var(--text);
border: 1px solid var(--border);
}
.btn-secondary:hover:not(:disabled) { background: #e8e8e8; }
.btn-blue {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
}
.btn-blue:hover:not(:disabled) { box-shadow: 0 4px 15px rgba(52,152,219,0.4); }
.btn-sm {
padding: 6px 14px;
font-size: 13px;
}
.btn-danger {
background: #e74c3c;
color: white;
}
.btn-danger:hover:not(:disabled) { background: #c0392b; }
.btn-group {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 16px;
}
/* ============================================================
Ball Display
============================================================ */
.ball {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
color: white;
font-size: 14px;
font-weight: 700;
margin: 2px;
box-shadow: inset 0 -2px 4px rgba(0,0,0,0.2);
}
.ball-red { background: radial-gradient(circle at 35% 35%, #ff6b6b, var(--red-ball)); }
.ball-blue { background: radial-gradient(circle at 35% 35%, #5dade2, var(--blue-ball)); }
.ball-lg {
width: 44px;
height: 44px;
font-size: 17px;
}
/* ============================================================
Ticket Display
============================================================ */
.ticket {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
border-bottom: 1px solid #f0f0f0;
flex-wrap: wrap;
}
.ticket:last-child { border-bottom: none; }
.ticket-index {
font-size: 12px;
color: var(--text-light);
min-width: 36px;
font-weight: 500;
}
.ticket-info {
font-size: 12px;
color: var(--text-light);
margin-left: auto;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.ticket-info span {
background: #f0f0f0;
padding: 2px 8px;
border-radius: 4px;
}
/* ============================================================
Table
============================================================ */
.table-wrap {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
th, td {
padding: 10px 8px;
text-align: center;
border-bottom: 1px solid var(--border);
white-space: nowrap;
}
th {
background: #f8f9fa;
font-weight: 600;
color: var(--text-light);
font-size: 12px;
position: sticky;
top: 0;
}
tr:hover { background: #fafafa; }
/* ============================================================
Pagination
============================================================ */
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 16px;
flex-wrap: wrap;
}
.pagination button {
padding: 6px 14px;
border: 1px solid var(--border);
border-radius: 6px;
background: white;
cursor: pointer;
font-size: 13px;
color: var(--text);
}
.pagination button:hover:not(:disabled) { background: var(--bg); }
.pagination button:disabled { opacity: 0.4; cursor: not-allowed; }
.pagination .page-info {
font-size: 13px;
color: var(--text-light);
}
/* ============================================================
Loading & Toast
============================================================ */
.loading {
text-align: center;
padding: 40px 20px;
color: var(--text-light);
}
.spinner {
display: inline-block;
width: 32px;
height: 32px;
border: 3px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 10px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.toast {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
z-index: 999;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
max-width: 90%;
text-align: center;
}
.toast.show { opacity: 1; }
.toast.success { background: #27ae60; }
.toast.error { background: #e74c3c; }
/* ============================================================
Stats Grid
============================================================ */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
}
.stat-item {
background: #f8f9fa;
padding: 14px;
border-radius: 8px;
text-align: center;
}
.stat-item .label {
font-size: 12px;
color: var(--text-light);
margin-bottom: 4px;
}
.stat-item .value {
font-size: 18px;
font-weight: 700;
color: var(--text);
}
.stat-item .sub {
font-size: 12px;
color: var(--text-light);
margin-top: 4px;
}
.ball-list {
display: flex;
flex-wrap: wrap;
gap: 4px;
justify-content: center;
}
/* ============================================================
Records
============================================================ */
.record-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
flex-wrap: wrap;
gap: 8px;
}
.record-item:last-child { border-bottom: none; }
.record-meta { flex: 1; min-width: 150px; }
.record-meta .title { font-size: 14px; font-weight: 500; }
.record-meta .desc { font-size: 12px; color: var(--text-light); margin-top: 2px; }
.record-actions { display: flex; gap: 6px; }
.empty-state {
text-align: center;
padding: 40px 20px;
color: var(--text-light);
}
.empty-state .icon { font-size: 48px; margin-bottom: 12px; }
/* ============================================================
Search Bar
============================================================ */
.search-bar {
display: flex;
gap: 10px;
margin-bottom: 16px;
}
.search-bar input {
flex: 1;
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 14px;
outline: none;
}
.search-bar input:focus { border-color: var(--primary); }
/* ============================================================
Mobile Bottom Nav
============================================================ */
.mobile-nav {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--card-bg);
border-top: 1px solid var(--border);
z-index: 100;
box-shadow: 0 -2px 10px rgba(0,0,0,0.08);
}
.mobile-nav-items {
display: flex;
}
.mobile-nav-item {
flex: 1;
padding: 8px 4px;
text-align: center;
font-size: 11px;
color: var(--text-light);
cursor: pointer;
transition: color 0.2s;
user-select: none;
}
.mobile-nav-item .m-icon { font-size: 22px; display: block; margin-bottom: 2px; }
.mobile-nav-item.active { color: var(--primary); font-weight: 600; }
/* ============================================================
Responsive
============================================================ */
@media (max-width: 768px) {
.header h1 { font-size: 18px; }
.header p { font-size: 12px; }
.nav-tabs { display: none; }
.mobile-nav { display: block; }
body { padding-bottom: 70px; }
.container { padding: 12px; }
.card { padding: 14px; }
.ball { width: 30px; height: 30px; font-size: 12px; }
.ball-lg { width: 36px; height: 36px; font-size: 14px; }
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.ticket { padding: 8px 10px; }
.ticket-info { margin-left: 0; width: 100%; justify-content: flex-start; }
.form-row { flex-direction: column; }
.form-row .form-group { min-width: auto; }
.btn { padding: 10px 20px; font-size: 14px; }
.record-actions { width: 100%; justify-content: flex-end; }
}
@media (max-width: 480px) {
.ball { width: 26px; height: 26px; font-size: 11px; }
.ball-lg { width: 32px; height: 32px; font-size: 13px; }
.stats-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; }
.stat-item { padding: 10px; }
.stat-item .value { font-size: 16px; }
}
/* ============================================================
Scrollbar
============================================================ */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #aaa; }
/* ============================================================
Result Section
============================================================ */
.result-section { display: none; }
.result-section.show { display: block; }
.result-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
flex-wrap: wrap;
gap: 8px;
}
.result-header .title { font-size: 16px; font-weight: 600; }
.result-header .badge {
background: #27ae60;
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
/* ============================================================
History Table Mobile Card View
============================================================ */
@media (max-width: 600px) {
.history-table thead { display: none; }
.history-table tr {
display: block;
padding: 12px;
border-bottom: 1px solid var(--border);
}
.history-table td {
display: inline-block;
border: none;
padding: 2px 4px;
font-size: 12px;
}
.history-table td:first-child {
display: block;
font-weight: 600;
font-size: 14px;
margin-bottom: 4px;
}
}
</style>
</head>
<body>
<!-- ============================================================
Toast
============================================================ -->
<div class="toast" id="toast"></div>
<!-- ============================================================
Header
============================================================ -->
<div class="header">
<h1>🎯 双色球号码生成系统</h1>
<p>基于历史数据分析 · 支持 PC 端和移动端</p>
</div>
<!-- ============================================================
Desktop Navigation
============================================================ -->
<div class="nav-tabs" id="desktopNav">
<div class="nav-tab active" data-page="generate" onclick="switchPage('generate')">
<span class="icon">🎲</span> 号码生成
</div>
<div class="nav-tab" data-page="history" onclick="switchPage('history')">
<span class="icon">📊</span> 历史数据
</div>
<div class="nav-tab" data-page="records" onclick="switchPage('records')">
<span class="icon">📋</span> 生成记录
</div>
<div class="nav-tab" data-page="stats" onclick="switchPage('stats')">
<span class="icon">📈</span> 统计分析
</div>
</div>
<!-- ============================================================
Page: 号码生成
============================================================ -->
<div class="container">
<div class="page active" id="page-generate">
<div class="card">
<div class="card-title">🎲 生成双色球号码</div>
<div class="form-row">
<div class="form-group">
<label>生成策略</label>
<select id="strategy">
<option value="advanced">高级策略(基于历史数据分析)</option>
<option value="basic">基础策略(完全随机)</option>
</select>
</div>
<div class="form-group">
<label>生成注数</label>
<input type="number" id="numTickets" value="10" min="1" max="1000">
</div>
<div class="form-group" style="flex:0 0 auto;">
<label>&nbsp;</label>
<button class="btn btn-primary" id="generateBtn" onclick="generateTickets()">
🚀 立即生成
</button>
</div>
</div>
</div>
<!-- 生成结果 -->
<div class="card result-section" id="resultSection">
<div class="result-header">
<div>
<span class="title">📋 生成结果</span>
<span class="badge" id="resultBadge">10 注</span>
</div>
<div class="btn-group" style="margin:0;">
<button class="btn btn-blue btn-sm" id="downloadBtn" onclick="downloadResult()">📥 下载 Excel</button>
<button class="btn btn-secondary btn-sm" onclick="generateTickets()">🔄 重新生成</button>
</div>
</div>
<div id="resultContent"></div>
</div>
<!-- 统计概览 -->
<div class="card" id="statsOverview">
<div class="card-title">📊 数据概览</div>
<div id="statsOverviewContent">
<div class="loading"><div class="spinner"></div><div>加载统计数据中...</div></div>
</div>
</div>
</div>
<!-- ============================================================
Page: 历史数据
============================================================ -->
<div class="page" id="page-history">
<div class="card">
<div class="card-title">📊 双色球历史开奖数据</div>
<div class="search-bar">
<input type="text" id="historySearch" placeholder="搜索期号或日期..." oninput="debounceSearch()">
<button class="btn btn-secondary btn-sm" onclick="loadHistory(1)">🔍 搜索</button>
</div>
<div id="historyContent">
<div class="loading"><div class="spinner"></div><div>加载历史数据中...</div></div>
</div>
<div class="pagination" id="historyPagination"></div>
</div>
</div>
<!-- ============================================================
Page: 生成记录
============================================================ -->
<div class="page" id="page-records">
<div class="card">
<div class="card-title">📋 生成记录</div>
<div id="recordsContent">
<div class="loading"><div class="spinner"></div><div>加载记录中...</div></div>
</div>
<div class="pagination" id="recordsPagination"></div>
</div>
</div>
<!-- ============================================================
Page: 统计分析
============================================================ -->
<div class="page" id="page-stats">
<div class="card">
<div class="card-title">📈 号码统计分析</div>
<div id="statsContent">
<div class="loading"><div class="spinner"></div><div>加载统计中...</div></div>
</div>
</div>
</div>
</div>
<!-- ============================================================
Mobile Bottom Navigation
============================================================ -->
<div class="mobile-nav">
<div class="mobile-nav-items">
<div class="mobile-nav-item active" data-page="generate" onclick="switchPage('generate')">
<span class="m-icon">🎲</span> 生成
</div>
<div class="mobile-nav-item" data-page="history" onclick="switchPage('history')">
<span class="m-icon">📊</span> 历史
</div>
<div class="mobile-nav-item" data-page="records" onclick="switchPage('records')">
<span class="m-icon">📋</span> 记录
</div>
<div class="mobile-nav-item" data-page="stats" onclick="switchPage('stats')">
<span class="m-icon">📈</span> 统计
</div>
</div>
</div>
<!-- ============================================================
JavaScript
============================================================ -->
<script>
// ============================================================
// State
// ============================================================
const state = {
currentPage: 'generate',
lastResult: null,
historyPage: 1,
recordsPage: 1,
searchTimer: null,
};
// ============================================================
// Toast
// ============================================================
function showToast(msg, type = '') {
const t = document.getElementById('toast');
t.textContent = msg;
t.className = 'toast show ' + type;
clearTimeout(t._hide);
t._hide = setTimeout(() => t.classList.remove('show'), 2500);
}
// ============================================================
// Page Switching
// ============================================================
function switchPage(page) {
state.currentPage = page;
// Desktop tabs
document.querySelectorAll('.nav-tab').forEach(el => {
el.classList.toggle('active', el.dataset.page === page);
});
// Mobile tabs
document.querySelectorAll('.mobile-nav-item').forEach(el => {
el.classList.toggle('active', el.dataset.page === page);
});
// Pages
document.querySelectorAll('.page').forEach(el => {
el.classList.toggle('active', el.id === 'page-' + page);
});
// Load data on demand
if (page === 'history') loadHistory(state.historyPage);
if (page === 'records') loadRecords(state.recordsPage);
if (page === 'stats') loadStatsPage();
}
// ============================================================
// API Helper
// ============================================================
async function api(path, options = {}) {
try {
const res = await fetch(path, {
headers: { 'Content-Type': 'application/json', ...options.headers },
...options
});
const data = await res.json();
if (!data.success) throw new Error(data.error || '请求失败');
return data.data;
} catch (e) {
showToast(e.message, 'error');
throw e;
}
}
// ============================================================
// Generate Tickets
// ============================================================
async function generateTickets() {
const btn = document.getElementById('generateBtn');
const numTickets = parseInt(document.getElementById('numTickets').value) || 10;
const strategy = document.getElementById('strategy').value;
if (numTickets < 1 || numTickets > 1000) {
showToast('注数必须在 1-1000 之间', 'error');
return;
}
btn.disabled = true;
btn.textContent = '⏳ 生成中...';
document.getElementById('resultSection').classList.remove('show');
try {
const data = await api('/api/generate', {
method: 'POST',
body: JSON.stringify({ num_tickets: numTickets, strategy })
});
state.lastResult = data;
renderResult(data);
document.getElementById('resultSection').classList.add('show');
showToast(`✅ 成功生成 ${data.total} 注号码`, 'success');
} catch (e) {
// error already shown by api()
} finally {
btn.disabled = false;
btn.textContent = '🚀 立即生成';
}
}
function renderResult(data) {
const container = document.getElementById('resultContent');
const badge = document.getElementById('resultBadge');
badge.textContent = `${data.total} 注`;
if (!data.tickets || data.tickets.length === 0) {
container.innerHTML = '<div class="empty-state"><div class="icon">😅</div><div>未生成号码</div></div>';
return;
}
let html = '<div class="table-wrap"><table><thead><tr>' +
'<th>序号</th><th>红球</th><th>蓝球</th><th>和值</th><th>奇偶比</th><th>大小比</th><th>跨度</th>' +
'</tr></thead><tbody>';
const displayCount = Math.min(data.tickets.length, 50);
for (let i = 0; i < displayCount; i++) {
const t = data.tickets[i];
const redBalls = t.reds.map(r => `<span class="ball ball-red">${String(r).padStart(2,'0')}</span>`).join('');
const blueBall = `<span class="ball ball-blue">${String(t.blue).padStart(2,'0')}</span>`;
html += `<tr>
<td>${String(t.index).padStart(3,'0')}</td>
<td style="white-space:nowrap;">${redBalls}</td>
<td>${blueBall}</td>
<td>${t.sum_value}</td>
<td>${t.odd_even}</td>
<td>${t.size_ratio}</td>
<td>${t.span}</td>
</tr>`;
}
if (data.total > 50) {
html += `<tr><td colspan="7" style="color:var(--text-light);font-size:13px;">... 仅显示前 50 注,完整数据请下载 Excel 查看</td></tr>`;
}
html += '</tbody></table></div>';
container.innerHTML = html;
// Update download button
document.getElementById('downloadBtn').onclick = () => {
window.open(data.download_url, '_blank');
};
}
function downloadResult() {
if (state.lastResult && state.lastResult.download_url) {
window.open(state.lastResult.download_url, '_blank');
} else {
showToast('没有可下载的文件', 'error');
}
}
// ============================================================
// History Data
// ============================================================
async function loadHistory(page) {
state.historyPage = page;
const container = document.getElementById('historyContent');
const pagination = document.getElementById('historyPagination');
const search = document.getElementById('historySearch').value;
container.innerHTML = '<div class="loading"><div class="spinner"></div><div>加载中...</div></div>';
pagination.innerHTML = '';
try {
const data = await api(`/api/history?page=${page}&page_size=20&search=${encodeURIComponent(search)}`);
renderHistory(data, container, pagination);
} catch (e) {
container.innerHTML = '<div class="empty-state"><div class="icon">😅</div><div>暂无历史数据</div></div>';
}
}
function renderHistory(data, container, pagination) {
if (!data.records || data.records.length === 0) {
container.innerHTML = '<div class="empty-state"><div class="icon">📭</div><div>暂无数据</div></div>';
pagination.innerHTML = '';
return;
}
// Find ball columns
const ballCols = [];
for (let i = 1; i <= 6; i++) {
const name = `红球 ${i}`;
if (data.columns.includes(name)) ballCols.push(name);
}
const blueCol = data.columns.includes('蓝球') ? '蓝球' : null;
let html = '<div class="table-wrap"><table class="history-table"><thead><tr>';
const displayCols = ['期号', '开奖日期', ...ballCols];
if (blueCol) displayCols.push(blueCol);
// Add extra useful columns
const extraCols = data.columns.filter(c =>
!displayCols.includes(c) &&
!['特别号', '奖池'].includes(c)
);
displayCols.push(...extraCols);
displayCols.forEach(c => { html += `<th>${c}</th>`; });
html += '</tr></thead><tbody>';
data.records.forEach(rec => {
html += '<tr>';
displayCols.forEach(col => {
let val = rec[col];
if (val === null || val === undefined) val = '-';
if (ballCols.includes(col) || col === blueCol) {
const isBlue = col === blueCol;
val = `<span class="ball ${isBlue ? 'ball-blue' : 'ball-red'}">${String(val).padStart(2,'0')}</span>`;
}
html += `<td>${val}</td>`;
});
html += '</tr>';
});
html += '</tbody></table></div>';
container.innerHTML = html;
// Pagination
const totalPages = Math.ceil(data.total / data.page_size);
if (totalPages > 1) {
let p = '';
p += `<button ${data.page <= 1 ? 'disabled' : ''} onclick="loadHistory(${data.page - 1})"> 上一页</button>`;
p += `<span class="page-info">第 ${data.page}/${totalPages} 页 (共 ${data.total} 条)</span>`;
p += `<button ${data.page >= totalPages ? 'disabled' : ''} onclick="loadHistory(${data.page + 1})">下一页 </button>`;
pagination.innerHTML = p;
}
}
function debounceSearch() {
clearTimeout(state.searchTimer);
state.searchTimer = setTimeout(() => loadHistory(1), 500);
}
// ============================================================
// Generation Records
// ============================================================
async function loadRecords(page) {
state.recordsPage = page;
const container = document.getElementById('recordsContent');
const pagination = document.getElementById('recordsPagination');
container.innerHTML = '<div class="loading"><div class="spinner"></div><div>加载中...</div></div>';
pagination.innerHTML = '';
try {
const data = await api(`/api/records?page=${page}&page_size=20`);
renderRecords(data, container, pagination);
} catch (e) {
container.innerHTML = '<div class="empty-state"><div class="icon">📭</div><div>暂无生成记录</div></div>';
}
}
function renderRecords(data, container, pagination) {
if (!data.records || data.records.length === 0) {
container.innerHTML = '<div class="empty-state"><div class="icon">📭</div><div>暂无生成记录</div></div>';
pagination.innerHTML = '';
return;
}
let html = '';
data.records.forEach(rec => {
const fileSize = rec.filesize ? (rec.filesize / 1024).toFixed(1) + ' KB' : '未知';
html += `<div class="record-item">
<div class="record-meta">
<div class="title">${rec.strategy} · ${rec.num_tickets} 注</div>
<div class="desc">${rec.created_at} · ${fileSize}</div>
</div>
<div class="record-actions">
<button class="btn btn-blue btn-sm" onclick="window.open('/api/download/${rec.filename}', '_blank')">📥 下载</button>
<button class="btn btn-danger btn-sm" onclick="deleteRecord('${rec.id}')">🗑️ 删除</button>
</div>
</div>`;
});
container.innerHTML = html;
// Pagination
const totalPages = Math.ceil(data.total / data.page_size);
if (totalPages > 1) {
let p = '';
p += `<button ${data.page <= 1 ? 'disabled' : ''} onclick="loadRecords(${data.page - 1})"> 上一页</button>`;
p += `<span class="page-info">第 ${data.page}/${totalPages} 页 (共 ${data.total} 条)</span>`;
p += `<button ${data.page >= totalPages ? 'disabled' : ''} onclick="loadRecords(${data.page + 1})">下一页 </button>`;
pagination.innerHTML = p;
}
}
async function deleteRecord(id) {
if (!confirm('确定要删除这条记录吗?')) return;
try {
await api(`/api/records/${id}`, { method: 'DELETE' });
showToast('✅ 记录已删除', 'success');
loadRecords(state.recordsPage);
} catch (e) {}
}
// ============================================================
// Statistics Page
// ============================================================
async function loadStatsPage() {
const container = document.getElementById('statsContent');
container.innerHTML = '<div class="loading"><div class="spinner"></div><div>加载统计中...</div></div>';
try {
const data = await api('/api/statistics');
renderStatsPage(data, container);
} catch (e) {
container.innerHTML = '<div class="empty-state"><div class="icon">😅</div><div>暂无统计数据</div></div>';
}
}
function renderStatsPage(data, container) {
let html = '';
// History count
if (data.history_count) {
html += `<div class="stats-grid" style="margin-bottom:16px;">
<div class="stat-item">
<div class="label">历史开奖期数</div>
<div class="value">${data.history_count}</div>
</div>
</div>`;
}
// Hot red balls
if (data.hot_reds) {
html += `<div class="card-title" style="margin-top:16px;">🔥 红球热号 TOP 15</div>
<div class="ball-list">`;
data.hot_reds.forEach(r => {
html += `<span class="ball ball-red ball-lg">${String(r).padStart(2,'0')}</span>`;
});
html += '</div>';
}
// Cold red balls
if (data.cold_reds) {
html += `<div class="card-title" style="margin-top:16px;">❄️ 红球冷号 TOP 15</div>
<div class="ball-list">`;
data.cold_reds.forEach(r => {
html += `<span class="ball ball-red ball-lg">${String(r).padStart(2,'0')}</span>`;
});
html += '</div>';
}
// Hot blue balls
if (data.hot_blues) {
html += `<div class="card-title" style="margin-top:16px;">🔥 蓝球热号 TOP 8</div>
<div class="ball-list">`;
data.hot_blues.forEach(b => {
html += `<span class="ball ball-blue ball-lg">${String(b).padStart(2,'0')}</span>`;
});
html += '</div>';
}
// Stats grid
html += '<div class="stats-grid" style="margin-top:16px;">';
if (data.common_odd_even) {
html += `<div class="stat-item">
<div class="label">最常见奇偶比</div>
<div class="value">${data.common_odd_even}</div>
</div>`;
}
if (data.common_size_ratio) {
html += `<div class="stat-item">
<div class="label">最常见大小比</div>
<div class="value">${data.common_size_ratio}</div>
</div>`;
}
if (data.sum_range) {
html += `<div class="stat-item">
<div class="label">和值范围</div>
<div class="value">${data.sum_range.min} - ${data.sum_range.max}</div>
<div class="sub">平均 ${data.sum_range.mean?.toFixed(1)}</div>
</div>`;
}
if (data.span_range) {
html += `<div class="stat-item">
<div class="label">跨度范围</div>
<div class="value">${data.span_range.min} - ${data.span_range.max}</div>
<div class="sub">平均 ${data.span_range.mean?.toFixed(1)}</div>
</div>`;
}
html += '</div>';
container.innerHTML = html;
}
// ============================================================
// Stats Overview (on generate page)
// ============================================================
async function loadStatsOverview() {
const container = document.getElementById('statsOverviewContent');
try {
const data = await api('/api/statistics');
let html = '<div class="stats-grid">';
if (data.history_count) {
html += `<div class="stat-item">
<div class="label">历史期数</div>
<div class="value">${data.history_count}</div>
</div>`;
}
if (data.common_odd_even) {
html += `<div class="stat-item">
<div class="label">常见奇偶比</div>
<div class="value">${data.common_odd_even}</div>
</div>`;
}
if (data.common_size_ratio) {
html += `<div class="stat-item">
<div class="label">常见大小比</div>
<div class="value">${data.common_size_ratio}</div>
</div>`;
}
if (data.sum_range) {
html += `<div class="stat-item">
<div class="label">和值范围</div>
<div class="value">${data.sum_range.min}-${data.sum_range.max}</div>
</div>`;
}
if (data.span_range) {
html += `<div class="stat-item">
<div class="label">跨度范围</div>
<div class="value">${data.span_range.min}-${data.span_range.max}</div>
</div>`;
}
html += '</div>';
// Hot balls preview
if (data.hot_reds) {
html += `<div style="margin-top:12px;">
<div style="font-size:13px;font-weight:500;margin-bottom:8px;">🔥 红球热号 TOP 10</div>
<div class="ball-list">`;
data.hot_reds.slice(0, 10).forEach(r => {
html += `<span class="ball ball-red">${String(r).padStart(2,'0')}</span>`;
});
html += '</div></div>';
}
if (data.hot_blues) {
html += `<div style="margin-top:12px;">
<div style="font-size:13px;font-weight:500;margin-bottom:8px;">🔥 蓝球热号 TOP 5</div>
<div class="ball-list">`;
data.hot_blues.slice(0, 5).forEach(b => {
html += `<span class="ball ball-blue">${String(b).padStart(2,'0')}</span>`;
});
html += '</div></div>';
}
container.innerHTML = html;
} catch (e) {
container.innerHTML = '<div class="empty-state"><div class="icon">😅</div><div>无法加载统计数据</div></div>';
}
}
// ============================================================
// Init
// ============================================================
loadStatsOverview();
</script>
</body>
</html>