diff --git a/index.html b/index.html
index 3e7fdc3..67eb9f7 100644
--- a/index.html
+++ b/index.html
@@ -51,6 +51,83 @@
}
.header h1 { font-size: 22px; font-weight: 700; letter-spacing: 1px; }
.header p { font-size: 13px; opacity: 0.85; margin-top: 4px; }
+ .header .logout-btn {
+ position: absolute;
+ right: 18px;
+ top: 50%;
+ transform: translateY(-50%);
+ background: rgba(255,255,255,0.18);
+ color: white;
+ border: 1px solid rgba(255,255,255,0.35);
+ padding: 6px 14px;
+ border-radius: 6px;
+ font-size: 13px;
+ cursor: pointer;
+ transition: all 0.2s;
+ }
+ .header .logout-btn:hover { background: rgba(255,255,255,0.32); }
+
+ /* ============================================================
+ Login Overlay (BIZ-110)
+ ============================================================ */
+ .login-overlay {
+ display: none;
+ position: fixed;
+ inset: 0;
+ background: linear-gradient(135deg, #e74c3c 0%, #c0392b 50%, #8e44ad 100%);
+ z-index: 9999;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+ }
+ .login-overlay.show { display: flex; }
+ .login-card {
+ background: white;
+ border-radius: 16px;
+ padding: 36px 32px;
+ width: 100%;
+ max-width: 400px;
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
+ text-align: center;
+ }
+ .login-title { font-size: 22px; font-weight: 700; color: var(--text); margin-bottom: 6px; }
+ .login-subtitle { font-size: 13px; color: var(--text-light); margin-bottom: 22px; }
+ .login-hint { font-size: 13px; color: var(--text-light); margin-bottom: 12px; }
+ .login-input {
+ width: 100%;
+ padding: 12px 14px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ font-size: 15px;
+ outline: none;
+ transition: border-color 0.2s;
+ margin-bottom: 12px;
+ }
+ .login-input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(231,76,60,0.1); }
+ .login-btn {
+ width: 100%;
+ padding: 12px 24px;
+ background: linear-gradient(135deg, #e74c3c, #c0392b);
+ color: white;
+ border: none;
+ border-radius: 8px;
+ font-size: 15px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s;
+ }
+ .login-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(231,76,60,0.3); }
+ .login-btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
+ .login-error {
+ margin-top: 12px;
+ padding: 10px 12px;
+ background: #fdecea;
+ color: #c0392b;
+ border-radius: 6px;
+ font-size: 13px;
+ text-align: left;
+ }
+ .login-foot { font-size: 12px; color: var(--text-light); margin-top: 16px; }
/* ============================================================
Navigation Tabs
@@ -728,6 +805,23 @@
+
+
+
+
🔐 登录验证
+
双色球号码生成系统
+
请输入访问令牌(LOTTO_API_TOKEN)以继续
+
+
+
+
+
+
+
+
+
@@ -737,6 +831,7 @@ Toast
Header
============================================================ -->
@@ -902,6 +997,74 @@ const state = {
searchTimer: null,
};
+// ============================================================
+// Login Page (BIZ-110)
+// ============================================================
+function showLoginPage(reason) {
+ const overlay = document.getElementById('loginOverlay');
+ if (!overlay) return;
+ overlay.classList.add('show');
+ // 隐藏主内容
+ const main = document.getElementById('mainApp');
+ if (main) main.style.display = 'none';
+ // 提示信息
+ const hint = document.getElementById('loginHint');
+ if (hint) {
+ hint.textContent = reason || '请输入访问令牌(LOTTO_API_TOKEN)以继续';
+ }
+ // 自动聚焦输入框
+ const input = document.getElementById('loginTokenInput');
+ if (input) {
+ input.value = '';
+ setTimeout(() => input.focus(), 50);
+ }
+}
+
+function showMainPage() {
+ const overlay = document.getElementById('loginOverlay');
+ if (overlay) overlay.classList.remove('show');
+ const main = document.getElementById('mainApp');
+ if (main) main.style.display = '';
+}
+
+async function handleLoginSubmit() {
+ const input = document.getElementById('loginTokenInput');
+ const btn = document.getElementById('loginSubmitBtn');
+ const errEl = document.getElementById('loginError');
+ if (!input || !btn) return;
+ const token = (input.value || '').trim();
+ if (!token) {
+ if (errEl) {
+ errEl.textContent = '令牌不能为空';
+ errEl.style.display = 'block';
+ }
+ return;
+ }
+ if (errEl) errEl.style.display = 'none';
+ btn.disabled = true;
+ const oldText = btn.textContent;
+ btn.textContent = '⏳ 验证中...';
+ try {
+ // 试探一下 /api/status 验证 token(BIZ-110:/api/status 不走 require_auth,
+ // 因此用 /api/records 探测,且带回业务数据)
+ setToken(token);
+ await api('/api/records?page=1&page_size=1');
+ // 验证成功
+ showMainPage();
+ showToast('✅ 登录成功', 'success');
+ // 重新加载当前页面数据
+ if (state.currentPage) switchPage(state.currentPage);
+ } catch (e) {
+ setToken('');
+ if (errEl) {
+ errEl.textContent = '令牌无效:' + (e.message || '验证失败');
+ errEl.style.display = 'block';
+ }
+ btn.disabled = false;
+ btn.textContent = oldText;
+ }
+}
+
// ============================================================
// Toast
// ============================================================
@@ -939,21 +1102,76 @@ function switchPage(page) {
}
// ============================================================
-// API Helper
+// Auth (BIZ-110)
+// ============================================================
+const AUTH_STORAGE_KEY = 'lotto_api_token';
+
+function getToken() {
+ try {
+ return localStorage.getItem(AUTH_STORAGE_KEY) || '';
+ } catch (e) {
+ return '';
+ }
+}
+
+function setToken(t) {
+ try {
+ if (t) localStorage.setItem(AUTH_STORAGE_KEY, t);
+ else localStorage.removeItem(AUTH_STORAGE_KEY);
+ } catch (e) { /* localStorage 不可用(隐私模式)—忽略 */ }
+}
+
+function isLoggedIn() {
+ return !!getToken();
+}
+
+function logout(reason) {
+ setToken('');
+ showToast(reason || '已退出登录', 'info');
+ showLoginPage();
+}
+
+// ============================================================
+// API Helper (BIZ-110: 注入 Bearer token)
// ============================================================
async function api(path, options = {}) {
+ const headers = { 'Content-Type': 'application/json', ...(options.headers || {}) };
+ const token = getToken();
+ if (token) headers['Authorization'] = 'Bearer ' + token;
+
+ let res;
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;
+ res = await fetch(path, { ...options, headers });
} catch (e) {
- showToast(e.message, 'error');
+ showToast('网络错误:' + e.message, 'error');
throw e;
}
+
+ // 401 → 清 token + 跳登录页
+ if (res.status === 401) {
+ let msg = '未授权访问';
+ try {
+ const j = await res.json();
+ if (j && j.error) msg = j.error;
+ } catch (_) { /* 忽略 JSON 解析错误 */ }
+ setToken('');
+ showLoginPage(msg);
+ throw new Error(msg);
+ }
+
+ let data;
+ try {
+ data = await res.json();
+ } catch (e) {
+ showToast('响应解析失败', 'error');
+ throw e;
+ }
+
+ if (!data.success) {
+ showToast(data.error || '请求失败', 'error');
+ throw new Error(data.error || '请求失败');
+ }
+ return data.data;
}
// ============================================================
@@ -1544,7 +1762,18 @@ async function compareRecord(id) {
// ============================================================
// Init
// ============================================================
-loadStatsOverview();
+function bootstrap() {
+ if (!isLoggedIn()) {
+ showLoginPage();
+ return;
+ }
+ showMainPage();
+ loadStatsOverview();
+}
+
+bootstrap();
+
+