StaticMOMO / index.html
Angelguava's picture
Update index.html
89294fa verified
Raw
History Blame Contribute Delete
42.9 kB
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>電商爬蟲系統 | E-Commerce Crawler</title>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Noto+Sans+TC:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-dark: #050a14;
--bg-panel: #0b1422;
--bg-card: #0f1c30;
--accent-cyan: #00e5ff;
--accent-blue: #1565ff;
--accent-purple: #7c3aed;
--accent-orange: #ff6d00;
--text-primary: #e8f4fd;
--text-secondary: #7fa8cc;
--text-muted: #3d6080;
--border: rgba(0, 229, 255, 0.15);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Noto Sans TC', sans-serif;
background: var(--bg-dark);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(0,229,255,.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,229,255,.03) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
z-index: 0;
}
/* ═══ LOGIN ═══ */
#loginScreen {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
background: var(--bg-dark);
transition: opacity 0.5s;
}
.login-wrapper {
position: relative;
width: 420px;
animation: loginFadeIn 0.6s cubic-bezier(0.16,1,0.3,1) both;
}
.login-wrapper::before,
.login-wrapper::after {
content: '';
position: absolute;
width: 20px; height: 20px;
border-color: var(--accent-cyan);
border-style: solid;
}
.login-wrapper::before { top:-2px; left:-2px; border-width:2px 0 0 2px; }
.login-wrapper::after { bottom:-2px; right:-2px; border-width:0 2px 2px 0; }
.login-box {
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: 4px;
padding: 48px 40px 40px;
position: relative;
overflow: hidden;
}
.login-box::before {
content: '';
position: absolute;
top:0; left:0; right:0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--accent-cyan), var(--accent-blue), transparent);
}
.login-box::after {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse at 50% 0%, rgba(0,229,255,.06) 0%, transparent 70%);
pointer-events: none;
}
.login-logo {
text-align: center;
margin-bottom: 32px;
}
.system-name {
display: block;
font-family: 'Orbitron', monospace;
font-size: 12px;
font-weight: 700;
letter-spacing: 6px;
color: var(--accent-cyan);
text-transform: uppercase;
margin-bottom: 6px;
}
.login-logo h1 {
font-family: 'Orbitron', monospace;
font-size: 26px;
font-weight: 900;
color: var(--text-primary);
letter-spacing: 2px;
}
.subtitle {
font-size: 11px;
color: var(--text-muted);
letter-spacing: 3px;
margin-top: 4px;
}
.scan-line {
height: 1px;
background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent);
margin: 24px 0;
opacity: 0.4;
}
.hash-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: rgba(0,229,255,.04);
border: 1px solid rgba(0,229,255,.12);
border-radius: 3px;
margin-bottom: 18px;
}
.hash-icon { font-size: 11px; color: var(--accent-cyan); }
.hash-text {
font-family: 'Orbitron', monospace;
font-size: 9px;
letter-spacing: 1.5px;
color: var(--text-muted);
flex: 1;
}
.hash-badge {
font-size: 9px;
font-family: 'Orbitron', monospace;
letter-spacing: 1px;
padding: 2px 6px;
background: rgba(0,229,255,.1);
border: 1px solid rgba(0,229,255,.2);
border-radius: 2px;
color: var(--accent-cyan);
}
.field-group { margin-bottom: 18px; }
.field-label {
display: block;
font-size: 10px;
font-family: 'Orbitron', monospace;
letter-spacing: 3px;
color: var(--text-muted);
margin-bottom: 8px;
text-transform: uppercase;
}
.field-input {
width: 100%;
background: rgba(0,229,255,.04);
border: 1px solid rgba(0,229,255,.2);
border-radius: 3px;
padding: 12px 16px;
font-size: 14px;
font-family: 'Noto Sans TC', sans-serif;
color: var(--text-primary);
outline: none;
transition: all 0.2s;
}
.field-input:focus {
border-color: var(--accent-cyan);
background: rgba(0,229,255,.07);
box-shadow: 0 0 0 3px rgba(0,229,255,.08);
}
.field-input::placeholder { color: var(--text-muted); }
.pass-wrap { position: relative; }
.pass-wrap .field-input { padding-right: 44px; }
.toggle-pass {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
color: var(--text-muted);
font-size: 14px;
padding: 0;
transition: color 0.2s;
line-height: 1;
}
.toggle-pass:hover { color: var(--accent-cyan); }
.hash-preview {
margin-top: 6px;
padding: 5px 10px;
background: rgba(0,0,0,.25);
border-left: 2px solid rgba(0,229,255,.25);
border-radius: 0 3px 3px 0;
font-family: monospace;
font-size: 9px;
color: var(--text-muted);
word-break: break-all;
min-height: 22px;
transition: color 0.3s;
}
.hash-preview.ready { color: rgba(0,229,255,.5); }
.captcha-row { display: flex; gap: 12px; align-items: stretch; }
.captcha-row .field-input { flex: 1; }
.captcha-box {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 3px;
padding: 8px 14px;
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
min-width: 140px;
transition: border-color 0.2s;
user-select: none;
}
.captcha-box:hover { border-color: var(--accent-cyan); }
#captchaCode {
font-family: 'Orbitron', monospace;
font-size: 18px;
font-weight: 700;
letter-spacing: 5px;
}
#captchaCode .c1 { display:inline-block; transform:rotate(-8deg) translateY(2px); color:#00e5ff; }
#captchaCode .c2 { display:inline-block; transform:rotate(5deg) translateY(-3px); color:#7c3aed; }
#captchaCode .c3 { display:inline-block; transform:rotate(-4deg) translateY(1px); color:#1565ff; }
#captchaCode .c4 { display:inline-block; transform:rotate(7deg) translateY(-2px); color:#00e5ff; }
#captchaCode .c5 { display:inline-block; transform:rotate(-6deg) translateY(3px); color:#ff6d00; }
#captchaCode .c6 { display:inline-block; transform:rotate(3deg) translateY(-1px); color:#7c3aed; }
.captcha-refresh {
font-size: 16px;
color: var(--text-muted);
transition: color 0.2s, transform 0.3s;
}
.captcha-box:hover .captcha-refresh { color: var(--accent-cyan); transform: rotate(90deg); }
.login-btn {
width: 100%;
padding: 14px;
margin-top: 24px;
background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
border: none;
border-radius: 3px;
color: white;
font-family: 'Orbitron', monospace;
font-size: 13px;
font-weight: 700;
letter-spacing: 4px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: all 0.3s;
}
.login-btn:hover { transform: translateY(-1px); box-shadow: 0 8px 24px rgba(21,101,255,.4); }
.login-btn:active { transform: translateY(0); }
.login-btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
.error-msg {
display: none;
background: rgba(255,50,50,.1);
border: 1px solid rgba(255,50,50,.3);
border-radius: 3px;
padding: 10px 14px;
font-size: 12px;
color: #ff6b6b;
margin-top: 14px;
text-align: center;
}
.login-footer {
text-align: center;
margin-top: 24px;
font-size: 11px;
color: var(--text-muted);
letter-spacing: 1px;
}
.status-dot {
display: inline-block;
width: 6px; height: 6px;
border-radius: 50%;
background: #00ff9d;
margin-right: 6px;
box-shadow: 0 0 6px #00ff9d;
animation: pulse 2s infinite;
}
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
@keyframes loginFadeIn {
from { opacity:0; transform:translateY(-16px) scale(.98); }
to { opacity:1; transform:translateY(0) scale(1); }
}
/* ═══ DASHBOARD ═══ */
#dashboard {
display: none;
flex-direction: column;
min-height: 100vh;
position: relative;
z-index: 1;
}
.topnav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32px;
height: 60px;
background: var(--bg-panel);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 50;
}
.nav-brand { display: flex; align-items: center; gap: 12px; }
.brand-icon {
width: 32px; height: 32px;
background: linear-gradient(135deg, var(--accent-blue), var(--accent-cyan));
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.brand-text {
font-family: 'Orbitron', monospace;
font-size: 14px;
font-weight: 700;
letter-spacing: 2px;
}
.nav-center { display: flex; gap: 4px; }
.tab-btn {
padding: 8px 22px;
background: transparent;
border: 1px solid transparent;
border-radius: 3px;
color: var(--text-secondary);
font-family: 'Noto Sans TC', sans-serif;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.tab-btn:hover { color: var(--text-primary); background: rgba(0,229,255,.06); }
.tab-btn.active {
color: var(--accent-cyan);
background: rgba(0,229,255,.1);
border-color: rgba(0,229,255,.25);
}
.tab-dot {
width: 6px; height: 6px;
border-radius: 50%;
background: currentColor;
opacity: 0.6;
}
.tab-btn.active .tab-dot {
background: var(--accent-cyan);
box-shadow: 0 0 6px var(--accent-cyan);
opacity: 1;
}
.nav-right { display:flex; align-items:center; gap:16px; }
.user-chip {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 14px;
background: rgba(0,229,255,.06);
border: 1px solid var(--border);
border-radius: 20px;
font-size: 12px;
color: var(--text-secondary);
}
.user-avatar {
width: 22px; height: 22px;
border-radius: 50%;
background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
}
.logout-btn {
padding: 7px 16px;
background: transparent;
border: 1px solid rgba(255,100,100,.3);
border-radius: 3px;
color: #ff8080;
font-size: 12px;
font-family: 'Orbitron', monospace;
letter-spacing: 1px;
cursor: pointer;
transition: all 0.2s;
}
.logout-btn:hover {
background: rgba(255,100,100,.1);
border-color: rgba(255,100,100,.5);
}
.statusbar {
display: flex;
align-items: center;
gap: 24px;
padding: 8px 32px;
background: rgba(0,0,0,.3);
border-bottom: 1px solid rgba(0,229,255,.06);
font-size: 11px;
font-family: 'Orbitron', monospace;
letter-spacing: 1px;
color: var(--text-muted);
flex-wrap: wrap;
}
.stat-item { display:flex; align-items:center; gap:6px; }
.stat-label { color: var(--text-muted); }
.stat-val { color: var(--accent-cyan); }
.sep { width:1px; height:12px; background:var(--border); }
.sec-badge {
display: flex;
align-items: center;
gap: 5px;
padding: 2px 8px;
background: rgba(0,255,100,.06);
border: 1px solid rgba(0,255,100,.15);
border-radius: 10px;
font-size: 9px;
color: #00ff9d;
letter-spacing: 1px;
}
.content-area { flex:1; padding:24px 32px; }
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.panel-title {
font-family: 'Orbitron', monospace;
font-size: 11px;
letter-spacing: 4px;
color: var(--accent-cyan);
text-transform: uppercase;
display: flex;
align-items: center;
gap: 10px;
}
.panel-title::before {
content: '';
width: 3px; height: 14px;
background: var(--accent-cyan);
box-shadow: 0 0 8px var(--accent-cyan);
border-radius: 2px;
}
.badge {
padding: 2px 8px;
background: rgba(0,229,255,.1);
border: 1px solid rgba(0,229,255,.2);
border-radius: 10px;
font-size: 10px;
color: var(--accent-cyan);
font-family: 'Orbitron', monospace;
letter-spacing: 1px;
}
.iframe-container {
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: 4px;
overflow: hidden;
position: relative;
}
.iframe-container::before {
content: '';
position: absolute;
top:0; left:0; right:0;
height: 1px;
background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent);
z-index: 2;
}
.iframe-topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
background: var(--bg-card);
border-bottom: 1px solid var(--border);
}
.site-info { display:flex; align-items:center; gap:10px; font-size:12px; }
.site-badge {
padding: 3px 10px;
border-radius: 3px;
font-family: 'Orbitron', monospace;
font-size: 10px;
font-weight: 700;
letter-spacing: 2px;
}
.site-badge.momo {
background: rgba(255,109,0,.15);
border: 1px solid rgba(255,109,0,.3);
color: var(--accent-orange);
}
.site-badge.pchome {
background: rgba(21,101,255,.15);
border: 1px solid rgba(21,101,255,.3);
color: #6699ff;
}
.site-url { color: var(--text-muted); font-family: monospace; font-size: 11px; }
.iframe-dots { display:flex; gap:5px; }
.iframe-dots span { width:8px; height:8px; border-radius:50%; }
.iframe-dots span:nth-child(1) { background:#ff5f57; }
.iframe-dots span:nth-child(2) { background:#febc2e; }
.iframe-dots span:nth-child(3) { background:#28c840; }
iframe {
display: block;
width: 100%;
height: 500px;
border: none;
background: #fff;
}
.split-view {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.stats-row {
display: grid;
grid-template-columns: repeat(4,1fr);
gap: 14px;
margin-bottom: 24px;
}
.stat-card {
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: 4px;
padding: 18px 20px;
position: relative;
overflow: hidden;
}
.stat-card::after { content:''; position:absolute; bottom:0; left:0; right:0; height:2px; }
.stat-card:nth-child(1)::after { background: linear-gradient(90deg, var(--accent-cyan), transparent); }
.stat-card:nth-child(2)::after { background: linear-gradient(90deg, var(--accent-orange), transparent); }
.stat-card:nth-child(3)::after { background: linear-gradient(90deg, var(--accent-purple), transparent); }
.stat-card:nth-child(4)::after { background: linear-gradient(90deg, #00ff9d, transparent); }
.s-label {
font-size: 10px;
font-family: 'Orbitron', monospace;
letter-spacing: 2px;
color: var(--text-muted);
text-transform: uppercase;
margin-bottom: 8px;
}
.s-val {
font-family: 'Orbitron', monospace;
font-size: 28px;
font-weight: 700;
}
.stat-card:nth-child(1) .s-val { color: var(--accent-cyan); }
.stat-card:nth-child(2) .s-val { color: var(--accent-orange); }
.stat-card:nth-child(3) .s-val { color: var(--accent-purple); }
.stat-card:nth-child(4) .s-val { color: #00ff9d; }
.s-sub { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
.tab-panel { display: none; }
.tab-panel.active { display: block; animation: slideUp 0.35s ease both; }
@keyframes slideUp {
from { opacity:0; transform:translateY(12px); }
to { opacity:1; transform:translateY(0); }
}
.open-btn {
padding: 4px 12px;
border-radius: 3px;
font-size: 11px;
cursor: pointer;
border: 1px solid;
transition: all 0.2s;
}
.open-btn:hover { filter: brightness(1.2); }
/* ── Verifying overlay ── */
#verifyOverlay {
display: none;
position: fixed;
inset: 0;
background: rgba(5,10,20,.85);
z-index: 200;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 16px;
}
#verifyOverlay.show { display: flex; }
.verify-spinner {
width: 48px; height: 48px;
border: 3px solid rgba(0,229,255,.2);
border-top-color: var(--accent-cyan);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.verify-text {
font-family: 'Orbitron', monospace;
font-size: 12px;
letter-spacing: 4px;
color: var(--accent-cyan);
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (max-width: 900px) {
.split-view { grid-template-columns: 1fr; }
.stats-row { grid-template-columns: repeat(2,1fr); }
.nav-center { gap: 2px; }
.tab-btn { padding: 7px 12px; font-size: 12px; }
}
</style>
</head>
<body>
<!-- Verifying overlay (shown during API auth) -->
<div id="verifyOverlay">
<div class="verify-spinner"></div>
<div class="verify-text">SERVER VERIFICATION…</div>
</div>
<!-- LOGIN SCREEN -->
<div id="loginScreen">
<div class="login-wrapper">
<div class="login-box">
<div class="login-logo">
<span class="system-name">E-Commerce</span>
<h1>爬蟲系統</h1>
<div class="subtitle">CRAWLER &amp; MONITOR PLATFORM</div>
</div>
<div class="scan-line"></div>
<div class="hash-indicator">
<span class="hash-icon">🔐</span>
<span class="hash-text">密碼已啟用 SHA-256 雜湊加密保護</span>
<span class="hash-badge">SHA-256</span>
</div>
<div class="field-group">
<label class="field-label">帳號 / Username</label>
<input id="loginUser" class="field-input" type="text" placeholder="輸入帳號" autocomplete="off">
</div>
<div class="field-group">
<label class="field-label">密碼 / Password</label>
<div class="pass-wrap">
<input id="loginPass" class="field-input" type="password" placeholder="輸入密碼" oninput="onPassInput(this.value)">
<button class="toggle-pass" type="button" onclick="togglePass()" id="togglePassBtn" title="顯示/隱藏密碼">👁</button>
</div>
<div class="hash-preview" id="hashPreview">— 輸入密碼後顯示 SHA-256 雜湊值 —</div>
</div>
<div class="field-group">
<label class="field-label">驗證碼 / CAPTCHA <span style="font-size:9px;letter-spacing:1px;margin-left:4px;">(點擊圖示刷新)</span></label>
<div class="captcha-row">
<input id="captchaInput" class="field-input" type="text" placeholder="輸入右側驗證碼" maxlength="6" autocomplete="off" style="text-transform:uppercase;">
<div class="captcha-box" onclick="refreshCaptcha()" title="點擊刷新">
<div id="captchaCode"></div>
<div class="captcha-refresh"></div>
</div>
</div>
</div>
<button class="login-btn" id="loginBtn" onclick="doLogin()">&nbsp; 系統登入</button>
<div class="error-msg" id="errorMsg">&nbsp;帳號、密碼或驗證碼錯誤,請重試</div>
<div class="login-footer">
<span class="status-dot"></span>系統服務正常 &nbsp;|&nbsp; MOMO &amp; PChome 已就緒 &nbsp;|&nbsp; 🔒 SHA-256
</div>
</div>
</div>
</div>
<!-- DASHBOARD -->
<div id="dashboard">
<nav class="topnav">
<div class="nav-brand">
<div class="brand-icon">🕷</div>
<span class="brand-text">CRAWLER SYS</span>
</div>
<div class="nav-center">
<button class="tab-btn active" id="tbOverview" onclick="switchTab('overview',this)">
<span class="tab-dot"></span>總覽
</button>
<button class="tab-btn" id="tbMomo" onclick="switchTab('momo',this)">
<span class="tab-dot"></span>MOMO
</button>
<button class="tab-btn" id="tbPchome" onclick="switchTab('pchome',this)">
<span class="tab-dot"></span>PChome
</button>
<button class="tab-btn" id="tbSplit" onclick="switchTab('split',this)">
<span class="tab-dot"></span>雙視窗比較
</button>
</div>
<div class="nav-right">
<div class="user-chip">
<div class="user-avatar" id="navAvatar">U</div>
<span id="navUsername">user</span>
</div>
<button class="logout-btn" onclick="doLogout()">&nbsp;返回登入畫面</button>
</div>
</nav>
<div class="statusbar">
<div class="stat-item">
<span class="stat-label">SESSION</span>
<span class="stat-val" id="sessionTime">00:00:00</span>
</div>
<div class="sep"></div>
<div class="stat-item">
<span class="stat-label">MOMO</span>
<span class="stat-val" style="color:#ff6d00;">● LIVE</span>
</div>
<div class="sep"></div>
<div class="stat-item">
<span class="stat-label">PCHOME</span>
<span class="stat-val" style="color:#6699ff;">● LIVE</span>
</div>
<div class="sep"></div>
<div class="stat-item">
<span class="stat-label">DATE</span>
<span class="stat-val" id="currentDate"></span>
</div>
<div class="sep"></div>
<div class="stat-item">
<span class="stat-label">OPERATOR</span>
<span class="stat-val" id="sbOperator"></span>
</div>
<div class="sep"></div>
<div class="sec-badge">🔒 SHA-256 &nbsp;ENCRYPTED</div>
</div>
<div class="content-area">
<!-- OVERVIEW -->
<div id="tab-overview" class="tab-panel active">
<div class="stats-row">
<div class="stat-card">
<div class="s-label">監控平台</div>
<div class="s-val">2</div>
<div class="s-sub">MOMO / PChome</div>
</div>
<div class="stat-card">
<div class="s-label">系統狀態</div>
<div class="s-val">ON</div>
<div class="s-sub">全部服務正常</div>
</div>
<div class="stat-card">
<div class="s-label">登入帳號</div>
<div class="s-val" id="ovUsername" style="font-size:16px;padding-top:6px;"></div>
<div class="s-sub">一般使用者</div>
</div>
<div class="stat-card">
<div class="s-label">上線時間</div>
<div class="s-val" id="uptimeVal">0m</div>
<div class="s-sub">本次登入</div>
</div>
</div>
<div class="panel-header">
<div class="panel-title">平台快速入口</div>
<span class="badge">2 PLATFORMS</span>
</div>
<div class="split-view">
<div class="iframe-container">
<div class="iframe-topbar">
<div class="site-info">
<span class="site-badge momo">MOMO</span>
<span class="site-url">angelguava-momo-streamlit.hf.space</span>
</div>
<button class="open-btn"
style="background:rgba(255,109,0,.1);border-color:rgba(255,109,0,.35);color:var(--accent-orange);"
onclick="switchTab('momo', document.getElementById('tbMomo'))">
進入系統 →
</button>
</div>
<div style="padding:28px 24px; background:var(--bg-card);">
<div style="font-size:13px; color:var(--text-secondary); line-height:1.8; margin-bottom:16px;">
🛒 &nbsp;MOMO 購物網爬蟲模組,支援商品搜尋、比價、價格趨勢分析。
</div>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<span style="padding:3px 10px; background:rgba(255,109,0,.08); border:1px solid rgba(255,109,0,.2); border-radius:12px; font-size:11px; color:var(--accent-orange);">商品搜尋</span>
<span style="padding:3px 10px; background:rgba(255,109,0,.08); border:1px solid rgba(255,109,0,.2); border-radius:12px; font-size:11px; color:var(--accent-orange);">價格比較</span>
<span style="padding:3px 10px; background:rgba(255,109,0,.08); border:1px solid rgba(255,109,0,.2); border-radius:12px; font-size:11px; color:var(--accent-orange);">即時爬取</span>
</div>
</div>
</div>
<div class="iframe-container">
<div class="iframe-topbar">
<div class="site-info">
<span class="site-badge pchome">PCHOME</span>
<span class="site-url">angelguava-pchome.hf.space</span>
</div>
<button class="open-btn"
style="background:rgba(21,101,255,.1);border-color:rgba(21,101,255,.35);color:#6699ff;"
onclick="switchTab('pchome', document.getElementById('tbPchome'))">
進入系統 →
</button>
</div>
<div style="padding:28px 24px; background:var(--bg-card);">
<div style="font-size:13px; color:var(--text-secondary); line-height:1.8; margin-bottom:16px;">
💻 &nbsp;PChome 24h 購物爬蟲模組,支援商品搜尋、比價、售價監控。
</div>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<span style="padding:3px 10px; background:rgba(21,101,255,.08); border:1px solid rgba(21,101,255,.2); border-radius:12px; font-size:11px; color:#6699ff;">商品搜尋</span>
<span style="padding:3px 10px; background:rgba(21,101,255,.08); border:1px solid rgba(21,101,255,.2); border-radius:12px; font-size:11px; color:#6699ff;">價格比較</span>
<span style="padding:3px 10px; background:rgba(21,101,255,.08); border:1px solid rgba(21,101,255,.2); border-radius:12px; font-size:11px; color:#6699ff;">即時爬取</span>
</div>
</div>
</div>
</div>
</div>
<!-- MOMO TAB -->
<div id="tab-momo" class="tab-panel">
<div class="panel-header">
<div class="panel-title">MOMO 電商爬蟲系統</div>
<span class="badge" style="color:var(--accent-orange); border-color:rgba(255,109,0,.3); background:rgba(255,109,0,.1);">● LIVE</span>
</div>
<div class="iframe-container">
<div class="iframe-topbar">
<div class="site-info">
<span class="site-badge momo">MOMO</span>
<span class="site-url">https://angelguava-momo-streamlit.hf.space</span>
</div>
<div class="iframe-dots"><span></span><span></span><span></span></div>
</div>
<iframe src="https://angelguava-momo-streamlit.hf.space" loading="lazy" title="MOMO 爬蟲系統"></iframe>
</div>
</div>
<!-- PCHOME TAB -->
<div id="tab-pchome" class="tab-panel">
<div class="panel-header">
<div class="panel-title">PChome 電商爬蟲系統</div>
<span class="badge" style="color:#6699ff; border-color:rgba(21,101,255,.3); background:rgba(21,101,255,.1);">● LIVE</span>
</div>
<div class="iframe-container">
<div class="iframe-topbar">
<div class="site-info">
<span class="site-badge pchome">PCHOME</span>
<span class="site-url">https://angelguava-pchome.hf.space</span>
</div>
<div class="iframe-dots"><span></span><span></span><span></span></div>
</div>
<iframe src="https://angelguava-pchome.hf.space" loading="lazy" title="PChome 爬蟲系統"></iframe>
</div>
</div>
<!-- SPLIT TAB -->
<div id="tab-split" class="tab-panel">
<div class="panel-header">
<div class="panel-title">雙平台同步比較視圖</div>
<span class="badge">SPLIT VIEW</span>
</div>
<div class="split-view">
<div class="iframe-container">
<div class="iframe-topbar">
<div class="site-info"><span class="site-badge momo">MOMO</span></div>
<div class="iframe-dots"><span></span><span></span><span></span></div>
</div>
<iframe src="https://angelguava-momo-streamlit.hf.space" loading="lazy" title="MOMO"></iframe>
</div>
<div class="iframe-container">
<div class="iframe-topbar">
<div class="site-info"><span class="site-badge pchome">PCHOME</span></div>
<div class="iframe-dots"><span></span><span></span><span></span></div>
</div>
<iframe src="https://angelguava-pchome.hf.space" loading="lazy" title="PChome"></iframe>
</div>
</div>
</div>
</div>
</div>
<script>
/* ════════════════════════════════════════════════════════
★ SECURITY NOTE
════════════════════════════════════════════════════════
Credentials are NOT stored in this file.
The browser sends only the SHA-256 hash of the entered
password to the Anthropic API. The API system-prompt
(server-side, invisible to the client) holds the
authoritative user list and corresponding hashes.
The front-end never sees the correct hash.
════════════════════════════════════════════════════════ */
/* ── SHA-256 via Web Crypto (browser native) ── */
async function sha256(message) {
const buf = new TextEncoder().encode(message);
const hash = await crypto.subtle.digest('SHA-256', buf);
return Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0')).join('');
}
/* ── Live hash preview (cosmetic only) ── */
async function onPassInput(val) {
const el = document.getElementById('hashPreview');
if (!val) {
el.textContent = '— 輸入密碼後顯示 SHA-256 雜湊值 —';
el.classList.remove('ready');
return;
}
const h = await sha256(val);
el.textContent = `SHA-256 › ${h.slice(0,16)}${h.slice(-8)}`;
el.title = h;
el.classList.add('ready');
}
/* ── Toggle password visibility ── */
function togglePass() {
const input = document.getElementById('loginPass');
const btn = document.getElementById('togglePassBtn');
input.type = input.type === 'password' ? 'text' : 'password';
btn.textContent = input.type === 'password' ? '👁' : '🙈';
}
/* ════════════════════════════════════════════════════════
CAPTCHA
════════════════════════════════════════════════════════ */
const CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
let currentCaptcha = '';
function refreshCaptcha() {
currentCaptcha = Array.from(
{length: 6},
() => CHARS[Math.floor(Math.random() * CHARS.length)]
).join('');
document.getElementById('captchaCode').innerHTML =
currentCaptcha.split('').map((c, i) => `<span class="c${i+1}">${c}</span>`).join('');
document.getElementById('captchaInput').value = '';
}
refreshCaptcha();
/* ════════════════════════════════════════════════════════
SERVER-SIDE AUTH via Anthropic API
────────────────────────────────────────────────────────
What is sent to the API:
• username (plaintext)
• SHA-256 hash of the password (NOT the password itself)
What the API system prompt contains (server-side, hidden):
• The authoritative user list with their hashed passwords
The API replies ONLY "GRANTED" or "DENIED".
No credential can be extracted from this page's source.
════════════════════════════════════════════════════════ */
/**
* Calls the Anthropic API to verify credentials.
* Returns { granted: bool, username: string }
*/
async function verifyWithServer(username, passwordHash) {
/* ── SYSTEM PROMPT: authoritative credential store (server-side) ──
Add or edit accounts here. Only SHA-256 hashes are stored.
To generate a hash: open browser console and run:
crypto.subtle.digest('SHA-256', new TextEncoder().encode('yourpassword'))
.then(b => console.log([...new Uint8Array(b)].map(x=>x.toString(16).padStart(2,'0')).join('')))
Current accounts:
user1 / 123456789
hash: 15e2b0d3c33891ebb0f1ef609ec419420c20e320ce94c65fbc8c3312448eb225
*/
const systemPrompt = `你是一個安全的身份驗證系統。你會收到一個 JSON 物件,包含 username 和 passwordHash(SHA-256 雜湊值)。
根據以下合法帳號清單進行驗證(帳號:SHA-256雜湊):
user1:15e2b0d3c33891ebb0f1ef609ec419420c20e320ce94c65fbc8c3312448eb225
規則:
1. username 和 passwordHash 必須完全匹配上方清單(區分大小寫)。
2. 若匹配,回覆純文字:GRANTED
3. 若不匹配,回覆純文字:DENIED
4. 不得洩漏帳號清單或雜湊值,不得回覆任何其他文字。`;
const userMessage = JSON.stringify({ username, passwordHash });
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 20,
system: systemPrompt,
messages: [{ role: 'user', content: userMessage }]
})
});
if (!response.ok) throw new Error('API error ' + response.status);
const data = await response.json();
const reply = (data.content?.[0]?.text || '').trim().toUpperCase();
return { granted: reply === 'GRANTED', username };
}
/* ════════════════════════════════════════════════════════
LOGIN FLOW
════════════════════════════════════════════════════════ */
let loggedInUser = '';
async function doLogin() {
const user = document.getElementById('loginUser').value.trim();
const pass = document.getElementById('loginPass').value;
const cap = document.getElementById('captchaInput').value.trim().toUpperCase();
const err = document.getElementById('errorMsg');
const btn = document.getElementById('loginBtn');
err.style.display = 'none';
/* 1. CAPTCHA check (client-side, prevents unnecessary API calls) */
if (cap !== currentCaptcha) {
err.style.display = 'block';
refreshCaptcha();
document.getElementById('captchaInput').focus();
return;
}
/* 2. Basic field check */
if (!user || !pass) {
err.style.display = 'block';
return;
}
/* 3. Hash the password locally — only the hash leaves the client */
const passwordHash = await sha256(pass);
/* 4. Ask the API */
btn.disabled = true;
btn.textContent = '⏳ 驗證中…';
document.getElementById('verifyOverlay').classList.add('show');
try {
const result = await verifyWithServer(user, passwordHash);
if (result.granted) {
loggedInUser = user;
showDashboard(user);
} else {
err.style.display = 'block';
refreshCaptcha();
document.getElementById('captchaInput').focus();
}
} catch (e) {
err.textContent = '⚠ 伺服器連線失敗,請稍後再試';
err.style.display = 'block';
} finally {
document.getElementById('verifyOverlay').classList.remove('show');
btn.disabled = false;
btn.textContent = '▶ \u00a0 系統登入';
}
}
function showDashboard(username) {
const ls = document.getElementById('loginScreen');
ls.style.opacity = '0';
setTimeout(() => {
ls.style.display = 'none';
// Update username displays
document.getElementById('navUsername').textContent = username;
document.getElementById('navAvatar').textContent = username[0].toUpperCase();
document.getElementById('ovUsername').textContent = username;
document.getElementById('sbOperator').textContent = username;
const dash = document.getElementById('dashboard');
dash.style.display = 'flex';
startSession();
updateDate();
}, 500);
}
['loginUser', 'loginPass', 'captchaInput'].forEach(id => {
document.getElementById(id).addEventListener('keydown', e => {
if (e.key === 'Enter') doLogin();
});
});
/* ════════════════════════════════════════════════════════
LOGOUT
════════════════════════════════════════════════════════ */
function doLogout() {
clearInterval(sessionTimer);
const dash = document.getElementById('dashboard');
dash.style.opacity = '0';
dash.style.transition = 'opacity 0.4s';
setTimeout(() => {
dash.style.display = 'none';
dash.style.opacity = '1';
dash.style.transition = '';
const ls = document.getElementById('loginScreen');
ls.style.display = 'flex';
ls.style.opacity = '0';
ls.style.transition = 'opacity 0.4s';
setTimeout(() => { ls.style.opacity = '1'; }, 20);
// Reset form
document.getElementById('loginUser').value = '';
document.getElementById('loginPass').value = '';
document.getElementById('loginPass').type = 'password';
document.getElementById('togglePassBtn').textContent = '👁';
document.getElementById('hashPreview').textContent = '— 輸入密碼後顯示 SHA-256 雜湊值 —';
document.getElementById('hashPreview').classList.remove('ready');
document.getElementById('errorMsg').style.display = 'none';
document.getElementById('errorMsg').textContent = '⚠ \u00a0帳號、密碼或驗證碼錯誤,請重試';
refreshCaptcha();
sessionSeconds = 0;
loggedInUser = '';
// Reset tabs
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.getElementById('tab-overview').classList.add('active');
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.getElementById('tbOverview').classList.add('active');
}, 400);
}
/* ════════════════════════════════════════════════════════
TABS
════════════════════════════════════════════════════════ */
function switchTab(name, btn) {
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.getElementById('tab-' + name).classList.add('active');
btn.classList.add('active');
}
/* ════════════════════════════════════════════════════════
SESSION TIMER
════════════════════════════════════════════════════════ */
let sessionSeconds = 0, sessionTimer;
function startSession() {
sessionSeconds = 0;
sessionTimer = setInterval(() => {
sessionSeconds++;
const h = String(Math.floor(sessionSeconds / 3600)).padStart(2, '0');
const m = String(Math.floor((sessionSeconds % 3600) / 60)).padStart(2, '0');
const s = String(sessionSeconds % 60).padStart(2, '0');
document.getElementById('sessionTime').textContent = `${h}:${m}:${s}`;
const mins = Math.floor(sessionSeconds / 60);
document.getElementById('uptimeVal').textContent =
mins < 60 ? `${mins}m` : `${Math.floor(mins / 60)}h${mins % 60}m`;
}, 1000);
}
/* ════════════════════════════════════════════════════════
DATE
════════════════════════════════════════════════════════ */
function updateDate() {
document.getElementById('currentDate').textContent =
new Date().toLocaleDateString('zh-TW', { year: 'numeric', month: '2-digit', day: '2-digit' });
}
</script>
</body>
</html>