f9a06069b1
首次提交:抖音 Cookie 项目初始化 - Flask 应用主体 - 项目依赖配置 - 前端模板 - .gitignore 配置 @
1082 lines
37 KiB
HTML
1082 lines
37 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>抖音 Cookie 扫码提取器</title>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
|
<style>
|
|
:root {
|
|
--bg: #0f172a;
|
|
--card-bg: #1e293b;
|
|
--input-bg: #0f172a;
|
|
--text: #e2e8f0;
|
|
--text-secondary: #94a3b8;
|
|
--accent: #818cf8;
|
|
--accent-hover: #6366f1;
|
|
--accent-glow: rgba(129, 140, 248, 0.3);
|
|
--success: #34d399;
|
|
--error: #fb7185;
|
|
--warning: #facc15;
|
|
--radius: 16px;
|
|
--shadow: 0 20px 40px rgba(0,0,0,0.4);
|
|
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
position: relative;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
body::before {
|
|
content: '';
|
|
position: fixed;
|
|
top: -50%;
|
|
left: -50%;
|
|
width: 200%;
|
|
height: 200%;
|
|
background: radial-gradient(circle at 30% 50%, rgba(129, 140, 248, 0.05) 0%, transparent 50%),
|
|
radial-gradient(circle at 70% 80%, rgba(192, 132, 252, 0.05) 0%, transparent 50%);
|
|
animation: rotateBg 30s linear infinite;
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
}
|
|
|
|
@keyframes rotateBg {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.container {
|
|
width: 100%;
|
|
max-width: 720px;
|
|
position: relative;
|
|
z-index: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
|
|
.card {
|
|
background: var(--card-bg);
|
|
border-radius: var(--radius);
|
|
padding: 28px;
|
|
box-shadow: var(--shadow);
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255,255,255,0.06);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.card:hover {
|
|
border-color: rgba(255,255,255,0.12);
|
|
box-shadow: 0 25px 50px rgba(0,0,0,0.5);
|
|
}
|
|
|
|
/* 状态显示区 */
|
|
.status-panel {
|
|
background: rgba(0,0,0,0.3);
|
|
border-radius: 12px;
|
|
padding: 14px 18px;
|
|
margin-bottom: 16px;
|
|
border: 1px solid rgba(255,255,255,0.06);
|
|
}
|
|
|
|
.status-panel .status-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.status-panel .status-label {
|
|
font-size: 0.8rem;
|
|
color: var(--text-secondary);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.status-panel .status-label .dot {
|
|
display: inline-block;
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
animation: pulse 1.5s ease-in-out infinite;
|
|
}
|
|
|
|
.dot.idle { background: var(--text-secondary); animation: none; }
|
|
.dot.loading { background: var(--warning); }
|
|
.dot.scanning { background: #38bdf8; }
|
|
.dot.ready { background: var(--success); }
|
|
.dot.error { background: var(--error); animation: none; }
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 0.4; transform: scale(0.8); }
|
|
50% { opacity: 1; transform: scale(1.2); }
|
|
}
|
|
|
|
.status-panel .status-text {
|
|
font-size: 0.95rem;
|
|
color: var(--text);
|
|
font-weight: 500;
|
|
word-break: break-all;
|
|
min-height: 1.5em;
|
|
}
|
|
|
|
.status-panel .status-time {
|
|
font-size: 0.7rem;
|
|
color: var(--text-secondary);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
/* 进度条 */
|
|
.progress-bar {
|
|
width: 100%;
|
|
height: 4px;
|
|
background: rgba(255,255,255,0.06);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
margin-top: 10px;
|
|
display: none;
|
|
}
|
|
|
|
.progress-bar .progress-fill {
|
|
height: 100%;
|
|
width: 0%;
|
|
background: linear-gradient(90deg, var(--accent), var(--success));
|
|
border-radius: 4px;
|
|
transition: width 0.5s ease;
|
|
}
|
|
|
|
.progress-bar.active { display: block; }
|
|
|
|
/* 统计面板 */
|
|
.stats-float {
|
|
background: var(--card-bg);
|
|
border-radius: var(--radius);
|
|
padding: 16px 24px;
|
|
border: 1px solid rgba(255,255,255,0.06);
|
|
backdrop-filter: blur(10px);
|
|
display: flex;
|
|
justify-content: space-around;
|
|
align-items: center;
|
|
box-shadow: var(--shadow);
|
|
transition: var(--transition);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.stats-float::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 2px;
|
|
background: linear-gradient(90deg, transparent, var(--accent), transparent);
|
|
animation: shimmer 3s infinite;
|
|
}
|
|
|
|
@keyframes shimmer {
|
|
0% { transform: translateX(-100%); }
|
|
100% { transform: translateX(100%); }
|
|
}
|
|
|
|
.stat-item {
|
|
text-align: center;
|
|
flex: 1;
|
|
padding: 4px 0;
|
|
position: relative;
|
|
}
|
|
|
|
.stat-item:not(:last-child)::after {
|
|
content: '';
|
|
position: absolute;
|
|
right: 0;
|
|
top: 20%;
|
|
height: 60%;
|
|
width: 1px;
|
|
background: rgba(255,255,255,0.06);
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
background: linear-gradient(135deg, #c084fc, #818cf8);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.7rem;
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.stat-item .stat-icon {
|
|
font-size: 1.1rem;
|
|
margin-right: 6px;
|
|
-webkit-text-fill-color: initial;
|
|
color: var(--accent);
|
|
}
|
|
|
|
.header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.header-icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
background: linear-gradient(135deg, rgba(192, 132, 252, 0.2), rgba(129, 140, 248, 0.2));
|
|
border-radius: 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.6rem;
|
|
flex-shrink: 0;
|
|
border: 1px solid rgba(255,255,255,0.06);
|
|
}
|
|
|
|
h1 {
|
|
font-size: 1.6rem;
|
|
font-weight: 700;
|
|
background: linear-gradient(135deg, #c084fc, #818cf8);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.subtitle {
|
|
color: var(--text-secondary);
|
|
font-size: 0.85rem;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.main-layout {
|
|
display: flex;
|
|
gap: 16px;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.left-panel {
|
|
flex: 2;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.right-panel {
|
|
flex: 1;
|
|
min-width: 200px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.btn {
|
|
padding: 12px 20px;
|
|
border-radius: 12px;
|
|
border: none;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
font-size: 0.95rem;
|
|
position: relative;
|
|
overflow: hidden;
|
|
width: 100%;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(135deg, #818cf8, #6366f1);
|
|
color: white;
|
|
box-shadow: 0 4px 15px rgba(129, 140, 248, 0.3);
|
|
}
|
|
|
|
.btn-primary:hover:not(:disabled) {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 25px rgba(129, 140, 248, 0.4);
|
|
}
|
|
|
|
.btn-primary:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
transform: none !important;
|
|
}
|
|
|
|
.btn-danger {
|
|
background: linear-gradient(135deg, #fb7185, #e11d48);
|
|
color: white;
|
|
box-shadow: 0 4px 15px rgba(251, 113, 133, 0.3);
|
|
}
|
|
|
|
.btn-danger:hover:not(:disabled) {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 25px rgba(251, 113, 133, 0.4);
|
|
}
|
|
|
|
.btn-success {
|
|
background: linear-gradient(135deg, #34d399, #10b981);
|
|
color: white;
|
|
box-shadow: 0 4px 15px rgba(52, 211, 153, 0.3);
|
|
}
|
|
|
|
.btn-success:hover:not(:disabled) {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 25px rgba(52, 211, 153, 0.4);
|
|
}
|
|
|
|
.input-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.input-group label {
|
|
font-size: 0.8rem;
|
|
color: var(--text-secondary);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.input-group input {
|
|
padding: 10px 14px;
|
|
background: var(--input-bg);
|
|
border: 1px solid rgba(255,255,255,0.08);
|
|
border-radius: 10px;
|
|
color: var(--text);
|
|
font-size: 0.9rem;
|
|
outline: none;
|
|
transition: var(--transition);
|
|
width: 100%;
|
|
}
|
|
|
|
.input-group input:focus {
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 3px var(--accent-glow);
|
|
}
|
|
|
|
.input-group input::placeholder {
|
|
color: var(--text-secondary);
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.input-group .proxy-status {
|
|
font-size: 0.75rem;
|
|
color: var(--text-secondary);
|
|
padding: 4px 8px;
|
|
border-radius: 6px;
|
|
background: rgba(255,255,255,0.03);
|
|
}
|
|
|
|
.input-group .proxy-status.active {
|
|
color: var(--success);
|
|
background: rgba(52, 211, 153, 0.1);
|
|
}
|
|
|
|
.qr-container {
|
|
text-align: center;
|
|
padding: 16px;
|
|
background: rgba(0,0,0,0.2);
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(255,255,255,0.04);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.qr-wrapper {
|
|
position: relative;
|
|
display: inline-block;
|
|
}
|
|
|
|
.qr-wrapper img {
|
|
max-width: 220px;
|
|
border-radius: 12px;
|
|
border: 2px solid rgba(255,255,255,0.08);
|
|
padding: 6px;
|
|
background: white;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.qr-wrapper img:hover {
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
.qr-scan-line {
|
|
position: absolute;
|
|
left: 10%;
|
|
right: 10%;
|
|
height: 2px;
|
|
background: linear-gradient(90deg, transparent, var(--accent), transparent);
|
|
animation: scanLine 2s ease-in-out infinite;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
@keyframes scanLine {
|
|
0% { top: 10%; opacity: 0; }
|
|
50% { opacity: 1; }
|
|
100% { top: 90%; opacity: 0; }
|
|
}
|
|
|
|
.status-badge {
|
|
display: inline-block;
|
|
padding: 5px 16px;
|
|
border-radius: 50px;
|
|
font-size: 0.8rem;
|
|
font-weight: 500;
|
|
margin-top: 10px;
|
|
animation: fadeInUp 0.4s ease;
|
|
}
|
|
|
|
@keyframes fadeInUp {
|
|
from { opacity: 0; transform: translateY(10px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.status-loading {
|
|
background: rgba(250,204,21,0.15);
|
|
color: #facc15;
|
|
border: 1px solid rgba(250,204,21,0.2);
|
|
}
|
|
.status-scanning {
|
|
background: rgba(56,189,248,0.15);
|
|
color: #38bdf8;
|
|
border: 1px solid rgba(56,189,248,0.2);
|
|
}
|
|
.status-success {
|
|
background: rgba(52,211,153,0.15);
|
|
color: #34d399;
|
|
border: 1px solid rgba(52,211,153,0.2);
|
|
}
|
|
.status-error {
|
|
background: rgba(251,113,133,0.15);
|
|
color: #fb7185;
|
|
border: 1px solid rgba(251,113,133,0.2);
|
|
}
|
|
|
|
.code-box {
|
|
background: #0d1117;
|
|
border-radius: 12px;
|
|
padding: 14px;
|
|
position: relative;
|
|
margin-top: 4px;
|
|
border: 1px solid rgba(255,255,255,0.05);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.code-box:hover {
|
|
border-color: rgba(255,255,255,0.1);
|
|
}
|
|
|
|
.code-box pre {
|
|
margin: 0;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
font-size: 0.75rem;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
color: #c9d1d9;
|
|
padding-right: 50px;
|
|
}
|
|
|
|
.code-box pre::-webkit-scrollbar {
|
|
width: 3px;
|
|
}
|
|
|
|
.code-box pre::-webkit-scrollbar-thumb {
|
|
background: var(--accent);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.copy-btn {
|
|
position: absolute;
|
|
top: 8px;
|
|
right: 8px;
|
|
background: rgba(255,255,255,0.05);
|
|
border: 1px solid rgba(255,255,255,0.08);
|
|
border-radius: 6px;
|
|
padding: 4px 10px;
|
|
color: #c9d1d9;
|
|
cursor: pointer;
|
|
font-size: 0.7rem;
|
|
transition: var(--transition);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.copy-btn:hover {
|
|
background: rgba(129, 140, 248, 0.15);
|
|
border-color: var(--accent);
|
|
color: var(--accent);
|
|
}
|
|
|
|
.copy-btn.copied {
|
|
background: rgba(52, 211, 153, 0.15);
|
|
border-color: var(--success);
|
|
color: var(--success);
|
|
}
|
|
|
|
.toast {
|
|
position: fixed;
|
|
bottom: 30px;
|
|
right: 30px;
|
|
background: var(--card-bg);
|
|
border-left: 4px solid var(--success);
|
|
padding: 14px 22px;
|
|
border-radius: 12px;
|
|
box-shadow: 0 10px 40px rgba(0,0,0,0.6);
|
|
transform: translateX(120%);
|
|
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
z-index: 999;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255,255,255,0.06);
|
|
min-width: 200px;
|
|
}
|
|
|
|
.toast.show { transform: translateX(0); }
|
|
.toast.error { border-left-color: var(--error); }
|
|
|
|
.toast-icon {
|
|
font-size: 1.3rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.toast-msg {
|
|
font-size: 0.9rem;
|
|
color: var(--text);
|
|
}
|
|
|
|
.hidden { display: none; }
|
|
|
|
.btn-group {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.btn-group .btn {
|
|
flex: 1;
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.main-layout {
|
|
flex-direction: column;
|
|
}
|
|
.right-panel {
|
|
min-width: unset;
|
|
}
|
|
.container { padding: 0; }
|
|
.card { padding: 20px; }
|
|
.stats-float { flex-wrap: wrap; gap: 8px; padding: 12px 16px; }
|
|
.stat-item { flex: 1 1 50%; }
|
|
.stat-item:not(:last-child)::after { display: none; }
|
|
.stat-number { font-size: 1.2rem; }
|
|
h1 { font-size: 1.3rem; }
|
|
.header-icon { width: 40px; height: 40px; font-size: 1.3rem; }
|
|
.qr-wrapper img { max-width: 150px; }
|
|
.toast { right: 16px; left: 16px; bottom: 16px; min-width: auto; }
|
|
.btn { padding: 10px 16px; font-size: 0.85rem; }
|
|
.btn-group { flex-direction: column; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<!-- 浮动统计面板 -->
|
|
<div class="stats-float" id="statsPanel">
|
|
<div class="stat-item">
|
|
<div class="stat-number">
|
|
<span class="stat-icon">🔄</span>
|
|
<span id="statTotal">0</span>
|
|
</div>
|
|
<div class="stat-label">总提取次数</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-number">
|
|
<span class="stat-icon">✅</span>
|
|
<span id="statSuccess">0</span>
|
|
</div>
|
|
<div class="stat-label">成功次数</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-number">
|
|
<span class="stat-icon">⏱️</span>
|
|
<span id="statAvgTime">0s</span>
|
|
</div>
|
|
<div class="stat-label">平均耗时</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-number">
|
|
<span class="stat-icon">📊</span>
|
|
<span id="statRate">0%</span>
|
|
</div>
|
|
<div class="stat-label">成功率</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 主卡片 -->
|
|
<div class="card">
|
|
<div class="header">
|
|
<div class="header-icon">🍪</div>
|
|
<div>
|
|
<h1>抖音 Cookie 提取器</h1>
|
|
<div class="subtitle">扫码登录 · 一键提取 JSON 格式 Cookie</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 状态显示面板 -->
|
|
<div class="status-panel">
|
|
<div class="status-header">
|
|
<div class="status-label">
|
|
<span class="dot idle" id="statusDot"></span>
|
|
<span id="statusLabel">就绪</span>
|
|
</div>
|
|
<span style="font-size:0.7rem;color:var(--text-secondary);" id="statusTime">-</span>
|
|
</div>
|
|
<div class="status-text" id="statusText">等待操作...</div>
|
|
<div class="progress-bar" id="progressBar">
|
|
<div class="progress-fill" id="progressFill"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 左右布局 -->
|
|
<div class="main-layout">
|
|
<!-- 左侧:主要操作区 -->
|
|
<div class="left-panel">
|
|
<div class="btn-group">
|
|
<button class="btn btn-primary" id="startBtn" onclick="startQR()">
|
|
<i class="fas fa-qrcode"></i> 获取二维码
|
|
</button>
|
|
<button class="btn btn-danger" id="resetBtn" onclick="resetSession()" style="flex:0.5;">
|
|
<i class="fas fa-undo"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 二维码展示区 -->
|
|
<div id="qrContainer" class="qr-container hidden">
|
|
<div class="qr-wrapper">
|
|
<img id="qrImage" src="" alt="抖音登录二维码">
|
|
<div class="qr-scan-line" id="scanLine"></div>
|
|
</div>
|
|
<div id="statusBadge" class="status-badge"></div>
|
|
<div style="margin-top:10px;font-size:0.8rem;color:var(--text-secondary);">
|
|
<i class="fas fa-download"></i>
|
|
<a href="/qrcode.png" target="_blank" style="color:var(--accent);">下载二维码</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cookie 结果区 -->
|
|
<div id="resultContainer" class="hidden">
|
|
<div class="code-box">
|
|
<button class="copy-btn" onclick="copyCookies()">
|
|
<i class="fas fa-copy"></i> 复制
|
|
</button>
|
|
<pre><code id="cookieCode" class="language-json"></code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 右侧:代理配置 -->
|
|
<div class="right-panel">
|
|
<div class="input-group">
|
|
<label>
|
|
<i class="fas fa-globe"></i> 代理 API
|
|
<span style="font-weight:400;color:var(--text-secondary);font-size:0.7rem;">(选填)</span>
|
|
</label>
|
|
<input type="text" id="proxyApi" placeholder="https://api.example.com/proxy" autocomplete="off">
|
|
<div class="proxy-status" id="proxyStatus">
|
|
<i class="fas fa-circle" style="font-size:0.5rem;color:var(--text-secondary);"></i>
|
|
默认:服务器 IP
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-success" id="testProxyBtn" onclick="testProxy()" style="font-size:0.85rem;padding:8px 12px;">
|
|
<i class="fas fa-plug"></i> 测试代理
|
|
</button>
|
|
<div style="font-size:0.7rem;color:var(--text-secondary);text-align:center;padding:4px;">
|
|
<i class="fas fa-info-circle"></i> 留空则使用服务器默认 IP
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toast 通知 -->
|
|
<div id="toast" class="toast">
|
|
<span class="toast-icon" id="toastIcon">✅</span>
|
|
<span class="toast-msg" id="toastMsg"></span>
|
|
</div>
|
|
|
|
<script>
|
|
let pollingInterval = null;
|
|
let statusPollingInterval = null;
|
|
let startTime = null;
|
|
let isQrReady = false;
|
|
|
|
let stats = {
|
|
total: parseInt(localStorage.getItem('cookieStats_total') || '0'),
|
|
success: parseInt(localStorage.getItem('cookieStats_success') || '0'),
|
|
totalTime: parseInt(localStorage.getItem('cookieStats_totalTime') || '0')
|
|
};
|
|
|
|
const startBtn = document.getElementById('startBtn');
|
|
const resetBtn = document.getElementById('resetBtn');
|
|
const proxyInput = document.getElementById('proxyApi');
|
|
const proxyStatus = document.getElementById('proxyStatus');
|
|
const qrContainer = document.getElementById('qrContainer');
|
|
const qrImage = document.getElementById('qrImage');
|
|
const statusBadge = document.getElementById('statusBadge');
|
|
const resultContainer = document.getElementById('resultContainer');
|
|
const cookieCode = document.getElementById('cookieCode');
|
|
const toast = document.getElementById('toast');
|
|
const toastMsg = document.getElementById('toastMsg');
|
|
const toastIcon = document.getElementById('toastIcon');
|
|
const scanLine = document.getElementById('scanLine');
|
|
|
|
// 状态显示元素
|
|
const statusDot = document.getElementById('statusDot');
|
|
const statusLabel = document.getElementById('statusLabel');
|
|
const statusText = document.getElementById('statusText');
|
|
const statusTime = document.getElementById('statusTime');
|
|
const progressBar = document.getElementById('progressBar');
|
|
const progressFill = document.getElementById('progressFill');
|
|
|
|
// ========== 状态更新 ==========
|
|
function updateStatus(status, message, progress) {
|
|
// 更新点颜色
|
|
statusDot.className = 'dot';
|
|
if (status === 'idle') {
|
|
statusDot.classList.add('idle');
|
|
statusLabel.textContent = '就绪';
|
|
progressBar.classList.remove('active');
|
|
} else if (status === 'loading') {
|
|
statusDot.classList.add('loading');
|
|
statusLabel.textContent = '⏳ 启动中';
|
|
progressBar.classList.add('active');
|
|
} else if (status === 'scanning') {
|
|
statusDot.classList.add('scanning');
|
|
statusLabel.textContent = '📱 扫码中';
|
|
progressBar.classList.add('active');
|
|
} else if (status === 'ready') {
|
|
statusDot.classList.add('ready');
|
|
statusLabel.textContent = '✅ 已就绪';
|
|
progressBar.classList.remove('active');
|
|
} else if (status === 'error') {
|
|
statusDot.classList.add('error');
|
|
statusLabel.textContent = '❌ 错误';
|
|
progressBar.classList.remove('active');
|
|
} else if (status === 'success') {
|
|
statusDot.classList.add('ready');
|
|
statusLabel.textContent = '🎉 成功';
|
|
progressBar.classList.remove('active');
|
|
}
|
|
|
|
statusText.textContent = message || '等待操作...';
|
|
statusTime.textContent = new Date().toLocaleTimeString();
|
|
|
|
if (progress !== undefined) {
|
|
progressFill.style.width = Math.min(progress, 100) + '%';
|
|
}
|
|
}
|
|
|
|
// ========== 轮询后端状态 ==========
|
|
async function pollStatus() {
|
|
try {
|
|
const resp = await fetch('/api/status');
|
|
const data = await resp.json();
|
|
|
|
if (data.status === 'idle') {
|
|
updateStatus('idle', data.message || '就绪');
|
|
} else if (data.status === 'loading') {
|
|
updateStatus('loading', data.message || '正在启动浏览器...');
|
|
} else if (data.status === 'scanning') {
|
|
updateStatus('scanning', data.message || '等待扫码...');
|
|
} else if (data.status === 'qr_ready') {
|
|
updateStatus('ready', '✅ 二维码已生成,请扫码');
|
|
} else if (data.status === 'success') {
|
|
updateStatus('success', '🎉 登录成功!');
|
|
} else if (data.status === 'error') {
|
|
updateStatus('error', '❌ ' + data.message);
|
|
}
|
|
} catch (e) {
|
|
// 忽略轮询错误
|
|
}
|
|
}
|
|
|
|
// ========== 开始状态轮询 ==========
|
|
function startStatusPolling() {
|
|
if (statusPollingInterval) clearInterval(statusPollingInterval);
|
|
statusPollingInterval = setInterval(pollStatus, 2000);
|
|
pollStatus(); // 立即执行一次
|
|
}
|
|
|
|
// ========== 重置会话 ==========
|
|
async function resetSession() {
|
|
try {
|
|
const resp = await fetch('/api/reset', { method: 'POST' });
|
|
const data = await resp.json();
|
|
showToast(data.message || '已重置');
|
|
if (pollingInterval) {
|
|
clearInterval(pollingInterval);
|
|
pollingInterval = null;
|
|
}
|
|
isQrReady = false;
|
|
qrContainer.classList.add('hidden');
|
|
resultContainer.classList.add('hidden');
|
|
startBtn.disabled = false;
|
|
startBtn.innerHTML = '<i class="fas fa-qrcode"></i> 获取二维码';
|
|
updateStatus('idle', '已重置,可以重新开始');
|
|
} catch (e) {
|
|
showToast('重置失败,请重启服务', true);
|
|
}
|
|
}
|
|
|
|
// ========== 代理输入监听 ==========
|
|
proxyInput.addEventListener('input', function() {
|
|
if (this.value.trim()) {
|
|
proxyStatus.innerHTML = `<i class="fas fa-circle" style="font-size:0.5rem;color:var(--accent);"></i> 已配置代理`;
|
|
proxyStatus.className = 'proxy-status active';
|
|
} else {
|
|
proxyStatus.innerHTML = `<i class="fas fa-circle" style="font-size:0.5rem;color:var(--text-secondary);"></i> 默认:服务器 IP`;
|
|
proxyStatus.className = 'proxy-status';
|
|
}
|
|
});
|
|
|
|
// ========== 测试代理 ==========
|
|
async function testProxy() {
|
|
const proxyApi = proxyInput.value.trim();
|
|
if (!proxyApi) {
|
|
showToast('请先输入代理 API 地址', true);
|
|
return;
|
|
}
|
|
|
|
const btn = document.getElementById('testProxyBtn');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 测试中...';
|
|
|
|
try {
|
|
const response = await fetch('/api/test_proxy', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({ proxy_api: proxyApi })
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'success') {
|
|
showToast(`✅ 代理测试成功!IP: ${data.ip || '获取成功'}`);
|
|
proxyStatus.innerHTML = `<i class="fas fa-circle" style="font-size:0.5rem;color:var(--success);"></i> ✅ 代理可用`;
|
|
proxyStatus.className = 'proxy-status active';
|
|
} else {
|
|
showToast(`❌ 代理测试失败: ${data.message}`, true);
|
|
proxyStatus.innerHTML = `<i class="fas fa-circle" style="font-size:0.5rem;color:var(--error);"></i> ❌ 代理不可用`;
|
|
proxyStatus.className = 'proxy-status';
|
|
}
|
|
} catch (e) {
|
|
showToast('❌ 网络错误,请检查服务是否运行', true);
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="fas fa-plug"></i> 测试代理';
|
|
}
|
|
}
|
|
|
|
// ========== 更新统计 ==========
|
|
function updateStats() {
|
|
document.getElementById('statTotal').textContent = stats.total;
|
|
document.getElementById('statSuccess').textContent = stats.success;
|
|
const avg = stats.success > 0 ? Math.round(stats.totalTime / stats.success) : 0;
|
|
document.getElementById('statAvgTime').textContent = avg > 0 ? `${avg}s` : '--';
|
|
const rate = stats.total > 0 ? Math.round((stats.success / stats.total) * 100) : 0;
|
|
document.getElementById('statRate').textContent = `${rate}%`;
|
|
}
|
|
|
|
function saveStats() {
|
|
localStorage.setItem('cookieStats_total', stats.total.toString());
|
|
localStorage.setItem('cookieStats_success', stats.success.toString());
|
|
localStorage.setItem('cookieStats_totalTime', stats.totalTime.toString());
|
|
}
|
|
|
|
// ========== Toast ==========
|
|
function showToast(message, isError = false) {
|
|
toastMsg.textContent = message;
|
|
toastIcon.textContent = isError ? '❌' : '✅';
|
|
toast.classList.remove('error');
|
|
if (isError) toast.classList.add('error');
|
|
toast.classList.add('show');
|
|
clearTimeout(toast._timeout);
|
|
toast._timeout = setTimeout(() => toast.classList.remove('show'), 4000);
|
|
}
|
|
|
|
// ========== 获取二维码 ==========
|
|
async function startQR() {
|
|
const proxyApi = proxyInput.value.trim();
|
|
|
|
// 先检查是否已有任务
|
|
try {
|
|
const statusResp = await fetch('/api/status');
|
|
const statusData = await statusResp.json();
|
|
if (statusData.status === 'loading' || statusData.status === 'scanning') {
|
|
showToast('⏳ 已有任务正在执行,请等待...', true);
|
|
return;
|
|
}
|
|
} catch (e) {}
|
|
|
|
startBtn.disabled = true;
|
|
startBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 加载中...';
|
|
qrContainer.classList.add('hidden');
|
|
resultContainer.classList.add('hidden');
|
|
scanLine.style.display = 'block';
|
|
isQrReady = false;
|
|
updateStatus('loading', '正在启动浏览器...');
|
|
startTime = Date.now();
|
|
|
|
try {
|
|
const response = await fetch('/api/start_qr', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({ proxy_api: proxyApi })
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'qr_ready') {
|
|
qrImage.src = data.qr_image;
|
|
qrContainer.classList.remove('hidden');
|
|
isQrReady = true;
|
|
updateStatus('ready', '✅ 二维码已生成,请用抖音扫码');
|
|
startBtn.innerHTML = '<i class="fas fa-qrcode"></i> 获取二维码';
|
|
startBtn.disabled = false;
|
|
|
|
// 开始轮询登录状态
|
|
startPolling();
|
|
stats.total++;
|
|
updateStats();
|
|
saveStats();
|
|
|
|
if (proxyApi) {
|
|
proxyStatus.innerHTML = `<i class="fas fa-circle" style="font-size:0.5rem;color:var(--success);"></i> ✅ 代理已启用`;
|
|
proxyStatus.className = 'proxy-status active';
|
|
}
|
|
|
|
showToast('✅ 二维码已生成,请扫码登录');
|
|
} else {
|
|
updateStatus('error', '❌ ' + (data.message || '获取二维码失败'));
|
|
startBtn.disabled = false;
|
|
startBtn.innerHTML = '<i class="fas fa-qrcode"></i> 获取二维码';
|
|
showToast(data.message || '获取二维码失败', true);
|
|
scanLine.style.display = 'none';
|
|
}
|
|
} catch (e) {
|
|
console.error('启动错误:', e);
|
|
updateStatus('error', '❌ 连接服务器失败');
|
|
startBtn.disabled = false;
|
|
startBtn.innerHTML = '<i class="fas fa-qrcode"></i> 获取二维码';
|
|
showToast('⚠️ 无法连接到服务器,请确保服务已启动', true);
|
|
scanLine.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// ========== 轮询登录状态 ==========
|
|
function startPolling() {
|
|
if (pollingInterval) clearInterval(pollingInterval);
|
|
pollingInterval = setInterval(checkLogin, 2000);
|
|
}
|
|
|
|
async function checkLogin() {
|
|
try {
|
|
const resp = await fetch('/api/check_login');
|
|
const data = await resp.json();
|
|
|
|
if (data.status === 'success') {
|
|
clearInterval(pollingInterval);
|
|
pollingInterval = null;
|
|
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
updateStatus('success', `🎉 登录成功!耗时 ${elapsed}s`);
|
|
displayCookies(data.cookies);
|
|
showToast(`🎉 Cookie 提取成功!耗时 ${elapsed}s`);
|
|
startBtn.disabled = false;
|
|
startBtn.innerHTML = '<i class="fas fa-qrcode"></i> 获取二维码';
|
|
scanLine.style.display = 'none';
|
|
isQrReady = false;
|
|
|
|
stats.success++;
|
|
stats.totalTime += elapsed;
|
|
updateStats();
|
|
saveStats();
|
|
} else if (data.status === 'error') {
|
|
clearInterval(pollingInterval);
|
|
pollingInterval = null;
|
|
updateStatus('error', '❌ ' + data.message);
|
|
showToast(data.message, true);
|
|
startBtn.disabled = false;
|
|
startBtn.innerHTML = '<i class="fas fa-qrcode"></i> 获取二维码';
|
|
scanLine.style.display = 'none';
|
|
isQrReady = false;
|
|
} else if (data.status === 'scanning') {
|
|
updateStatus('scanning', '⏳ 等待手机确认登录...');
|
|
}
|
|
} catch (e) {
|
|
console.error('轮询错误', e);
|
|
}
|
|
}
|
|
|
|
// ========== 显示 Cookie ==========
|
|
function displayCookies(cookies) {
|
|
const jsonStr = JSON.stringify(cookies, null, 2);
|
|
cookieCode.textContent = jsonStr;
|
|
resultContainer.classList.remove('hidden');
|
|
hljs.highlightElement(cookieCode);
|
|
}
|
|
|
|
// ========== 复制 Cookie ==========
|
|
function copyCookies() {
|
|
const text = cookieCode.textContent;
|
|
const btn = document.querySelector('.copy-btn');
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
btn.classList.add('copied');
|
|
btn.innerHTML = '<i class="fas fa-check"></i> 已复制';
|
|
showToast('📋 已复制到剪贴板');
|
|
setTimeout(() => {
|
|
btn.classList.remove('copied');
|
|
btn.innerHTML = '<i class="fas fa-copy"></i> 复制';
|
|
}, 2000);
|
|
}).catch(() => showToast('复制失败', true));
|
|
}
|
|
|
|
// ========== 页面关闭清理 ==========
|
|
window.addEventListener('beforeunload', () => {
|
|
if (pollingInterval) clearInterval(pollingInterval);
|
|
if (statusPollingInterval) clearInterval(statusPollingInterval);
|
|
});
|
|
|
|
// ========== 初始化 ==========
|
|
updateStats();
|
|
updateStatus('idle', '就绪,点击"获取二维码"开始');
|
|
startStatusPolling();
|
|
</script>
|
|
<script>hljs.highlightAll();</script>
|
|
</body>
|
|
</html> |