13a259b0f8
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
1172 lines
41 KiB
HTML
1172 lines
41 KiB
HTML
<!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> </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>
|