Spaces:
Running
Running
| <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 & 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()">▶ 系統登入</button> | |
| <div class="error-msg" id="errorMsg">⚠ 帳號、密碼或驗證碼錯誤,請重試</div> | |
| <div class="login-footer"> | |
| <span class="status-dot"></span>系統服務正常 | MOMO & PChome 已就緒 | 🔒 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()">⏻ 返回登入畫面</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 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;"> | |
| 🛒 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;"> | |
| 💻 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> | |