Files
douyin_cookie_yunsya/templates/index.html
T
root f9a06069b1 @
首次提交:抖音 Cookie 项目初始化

- Flask 应用主体
- 项目依赖配置
- 前端模板
- .gitignore 配置
@
2026-06-24 22:20:04 +08:00

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>