373 lines
13 KiB
HTML
373 lines
13 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>抖音一键采集器</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0f1020;
|
|
--panel: #1a1b2e;
|
|
--border: #2c2e4a;
|
|
--text: #e6e6f0;
|
|
--muted: #9aa0c0;
|
|
--accent: #8b5cf6;
|
|
--accent2: #a855f7;
|
|
--ok: #34d399;
|
|
--warn: #fbbf24;
|
|
--err: #f87171;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
background: radial-gradient(1200px 600px at 70% -10%, #2a1c4a55, transparent), var(--bg);
|
|
color: var(--text);
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
|
|
line-height: 1.6;
|
|
padding: 32px 16px 60px;
|
|
}
|
|
.wrap { max-width: 880px; margin: 0 auto; }
|
|
h1 { font-size: 26px; margin: 0 0 6px; }
|
|
.sub { color: var(--muted); font-size: 14px; margin: 0 0 24px; }
|
|
.pill {
|
|
font-size: 11px; color: var(--muted); border: 1px solid var(--border);
|
|
padding: 2px 10px; border-radius: 999px; vertical-align: middle;
|
|
}
|
|
.card {
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: 14px;
|
|
padding: 22px 24px;
|
|
margin-bottom: 18px;
|
|
}
|
|
.card-accent { border-color: #5b3f8a; }
|
|
.steps { margin: 0; padding-left: 20px; color: var(--muted); font-size: 14px; }
|
|
.steps li { margin: 6px 0; }
|
|
.steps strong { color: var(--text); }
|
|
.spinner-wrap { text-align: center; padding: 28px 0 12px; }
|
|
.spinner {
|
|
width: 48px; height: 48px; margin: 0 auto 16px;
|
|
border: 3px solid var(--border);
|
|
border-top-color: var(--accent);
|
|
border-radius: 50%;
|
|
animation: spin .8s linear infinite;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
.status-msg { text-align: center; color: var(--muted); font-size: 14px; min-height: 24px; }
|
|
.checks { display: grid; gap: 8px; margin: 16px 0; }
|
|
.chk { display: flex; align-items: center; gap: 10px; font-size: 14px; }
|
|
.dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
|
|
.ok .dot { background: var(--ok); } .ok { color: var(--ok); }
|
|
.warn .dot { background: var(--warn); } .warn { color: var(--warn); }
|
|
.err .dot { background: var(--err); } .err { color: var(--err); }
|
|
textarea {
|
|
width: 100%; min-height: 220px; resize: vertical;
|
|
background: #0c0d1a; color: #cbd5e1;
|
|
border: 1px solid var(--border); border-radius: 10px;
|
|
padding: 14px; font-family: ui-monospace, Consolas, monospace; font-size: 12px;
|
|
outline: none; line-height: 1.5;
|
|
}
|
|
textarea:focus { border-color: var(--accent); }
|
|
button {
|
|
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
|
color: #fff; border: none; border-radius: 10px;
|
|
padding: 12px 24px; font-size: 15px; font-weight: 600; cursor: pointer;
|
|
}
|
|
button:hover { opacity: .92; }
|
|
button.ghost {
|
|
background: transparent; border: 1px solid var(--border); color: var(--muted);
|
|
font-weight: 500; font-size: 13px; padding: 10px 16px;
|
|
}
|
|
button.big { padding: 14px 32px; font-size: 16px; }
|
|
.row { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; margin-top: 14px; }
|
|
.hint {
|
|
background: #1e1b3a; border: 1px solid #3b2f6b; border-radius: 10px;
|
|
padding: 12px 16px; font-size: 13px; color: #cbd5e1; margin-top: 16px;
|
|
}
|
|
.hint ol { margin: 8px 0 0; padding-left: 18px; }
|
|
.banner-ok {
|
|
background: linear-gradient(135deg, #1c3b2e, #1a2b24);
|
|
border: 1px solid #2f5b39; border-radius: 12px;
|
|
padding: 16px 20px; margin-bottom: 18px;
|
|
}
|
|
.banner-ok h2 { margin: 0 0 4px; font-size: 18px; color: var(--ok); }
|
|
.banner-warn h2 { color: var(--warn); }
|
|
.banner-err h2 { color: var(--err); }
|
|
.hidden { display: none !important; }
|
|
.toast {
|
|
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
|
|
background: #111; border: 1px solid var(--accent); color: #fff;
|
|
padding: 10px 20px; border-radius: 10px; opacity: 0; transition: opacity .2s;
|
|
pointer-events: none; font-size: 14px; z-index: 99;
|
|
}
|
|
.toast.show { opacity: 1; }
|
|
.uid-tag {
|
|
display: inline-block; background: #2a1c4a; border: 1px solid #5b3f8a;
|
|
padding: 2px 10px; border-radius: 8px; font-size: 13px; color: #c4b5fd;
|
|
margin-left: 8px;
|
|
}
|
|
.steps-bar {
|
|
display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 18px;
|
|
}
|
|
.step-item {
|
|
flex: 1; min-width: 120px; text-align: center; padding: 10px 8px;
|
|
border-radius: 10px; border: 1px solid var(--border); color: var(--muted);
|
|
font-size: 13px; background: #121325;
|
|
}
|
|
.step-item.active {
|
|
border-color: var(--accent); color: var(--text);
|
|
background: linear-gradient(135deg, #2a1c4a, #1a1b2e);
|
|
}
|
|
.step-item.done { border-color: #2f5b39; color: var(--ok); }
|
|
.log-box {
|
|
margin-top: 16px; max-height: 140px; overflow: auto;
|
|
background: #0c0d1a; border: 1px solid var(--border); border-radius: 10px;
|
|
padding: 10px 12px; font-size: 12px; color: #94a3b8;
|
|
font-family: ui-monospace, Consolas, monospace; line-height: 1.5;
|
|
}
|
|
.log-box div { margin: 2px 0; }
|
|
.top-bar {
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
flex-wrap: wrap; gap: 10px; margin-bottom: 8px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<div class="top-bar">
|
|
<div>
|
|
<h1 style="margin:0;">抖音一键采集器 <span class="pill">无痕窗口 · 不上传</span></h1>
|
|
<p class="sub" style="margin:8px 0 0;">登录与复制凭证均在同一无痕 Chrome 内完成。<span id="runTag" class="pill" style="margin-left:6px;"></span></p>
|
|
</div>
|
|
<button class="ghost" id="btnQuit" title="关闭采集器">退出</button>
|
|
</div>
|
|
|
|
<div class="steps-bar" id="stepsBar">
|
|
<div class="step-item active" data-step="1">① 启动</div>
|
|
<div class="step-item" data-step="2">② 扫码登录</div>
|
|
<div class="step-item" data-step="3">③ 自动采集</div>
|
|
<div class="step-item" data-step="4">④ 复制凭证</div>
|
|
</div>
|
|
|
|
<!-- 采集中 -->
|
|
<div id="phaseWaiting" class="card card-accent">
|
|
<div class="spinner-wrap">
|
|
<div class="spinner"></div>
|
|
<p class="status-msg" id="waitMsg">正在启动…</p>
|
|
</div>
|
|
<ol class="steps">
|
|
<li>切换到<strong>「② 抖音登录」</strong>标签页扫码登录</li>
|
|
<li>登录后会<strong>自动打开私信页</strong>,等消息列表出现即可</li>
|
|
<li>回到<strong>本标签页(① 凭证采集)</strong>复制凭证,去客服系统粘贴</li>
|
|
</ol>
|
|
<div class="row" style="justify-content:center;margin-top:20px;">
|
|
<button class="big" id="btnStart">我已打开私信页,立即采集</button>
|
|
</div>
|
|
<p style="text-align:center;color:var(--muted);font-size:12px;margin:10px 0 0;">一般无需点击,约 15 秒后会自动开始</p>
|
|
<div class="log-box" id="logBox"></div>
|
|
</div>
|
|
|
|
<!-- 采集完成 -->
|
|
<div id="phaseDone" class="hidden">
|
|
<div id="banner" class="banner-ok">
|
|
<h2 id="bannerTitle">✓ 采集完成</h2>
|
|
<p id="bannerSub" style="margin:0;font-size:14px;color:#cbd5e1;"></p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div style="font-weight:600;margin-bottom:8px;">凭证检查</div>
|
|
<div class="checks" id="checks"></div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:10px;">
|
|
<span style="font-weight:600;">凭证 JSON <span id="uidTag" class="uid-tag hidden"></span></span>
|
|
<div class="row" style="margin:0;">
|
|
<button class="big" id="btnCopy">📋 一键复制凭证</button>
|
|
<button class="ghost" id="btnSelect">全选</button>
|
|
</div>
|
|
</div>
|
|
<textarea id="output" readonly placeholder="凭证将显示在这里…"></textarea>
|
|
<div class="hint">
|
|
<strong>下一步:</strong>
|
|
<ol>
|
|
<li>点击「一键复制凭证」(凭证齐全时会自动复制)</li>
|
|
<li>打开客服系统 → <strong>账号管理</strong> → 编辑账号 → 粘贴到 Cookie 框 → 保存 → 启动托管</li>
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 失败 -->
|
|
<div id="phaseError" class="card hidden" style="border-color:#7f1d1d;">
|
|
<h2 style="margin:0 0 8px;color:var(--err);">采集失败</h2>
|
|
<p id="errorMsg" style="color:var(--muted);margin:0;"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toast" id="toast"></div>
|
|
|
|
<script>
|
|
const $ = id => document.getElementById(id);
|
|
|
|
function toast(msg) {
|
|
const t = $('toast');
|
|
t.textContent = msg;
|
|
t.classList.add('show');
|
|
clearTimeout(t._t);
|
|
t._t = setTimeout(() => t.classList.remove('show'), 2000);
|
|
}
|
|
|
|
function renderChecks(list) {
|
|
$('checks').innerHTML = (list || []).map(c =>
|
|
`<div class="chk ${c.s}"><span class="dot"></span>${c.t}</div>`
|
|
).join('');
|
|
}
|
|
|
|
function renderSteps(step) {
|
|
const n = Number(step) || 1;
|
|
document.querySelectorAll('.step-item').forEach(el => {
|
|
const s = Number(el.dataset.step);
|
|
el.classList.remove('active', 'done');
|
|
if (s < n) el.classList.add('done');
|
|
else if (s === n) el.classList.add('active');
|
|
});
|
|
}
|
|
|
|
function renderLogs(lines) {
|
|
const box = $('logBox');
|
|
if (!box) return;
|
|
const list = lines || [];
|
|
box.innerHTML = list.slice(-12).map(l => `<div>${l.replace(/</g,'<')}</div>`).join('') || '<div style="opacity:.5">等待日志…</div>';
|
|
box.scrollTop = box.scrollHeight;
|
|
}
|
|
|
|
function copyText(text) {
|
|
if (!text) { toast('没有可复制的内容'); return; }
|
|
navigator.clipboard.writeText(text).then(() => toast('✓ 已复制到剪贴板,可去系统粘贴'))
|
|
.catch(() => {
|
|
$('output').select();
|
|
document.execCommand('copy');
|
|
toast('✓ 已复制到剪贴板');
|
|
});
|
|
}
|
|
|
|
$('btnCopy').onclick = () => copyText($('output').value);
|
|
$('btnSelect').onclick = () => { $('output').select(); };
|
|
|
|
async function requestStart() {
|
|
const btn = $('btnStart');
|
|
btn.disabled = true;
|
|
btn.textContent = '正在开始…';
|
|
try {
|
|
const r = await fetch('/api/start', { method: 'POST' });
|
|
const data = await r.json();
|
|
if (data.ok) {
|
|
toast('已开始采集');
|
|
btn.textContent = '采集中…';
|
|
} else {
|
|
btn.disabled = false;
|
|
btn.textContent = '我已打开私信页,立即采集';
|
|
toast('请稍候,采集器还在启动');
|
|
}
|
|
} catch (e) {
|
|
btn.disabled = false;
|
|
btn.textContent = '我已打开私信页,立即采集';
|
|
toast('连接失败,请稍候或切换到「② 抖音登录」标签页');
|
|
}
|
|
}
|
|
$('btnStart').onclick = requestStart;
|
|
|
|
async function requestQuit() {
|
|
try { await fetch('/api/quit', { method: 'POST' }); } catch (e) {}
|
|
toast('正在退出…');
|
|
setTimeout(() => window.close(), 800);
|
|
}
|
|
$('btnQuit').onclick = requestQuit;
|
|
|
|
function showDone(data) {
|
|
$('phaseWaiting').classList.add('hidden');
|
|
$('phaseDone').classList.remove('hidden');
|
|
renderSteps(4);
|
|
renderChecks(data.checks || []);
|
|
|
|
const json = data.result_text || '';
|
|
$('output').value = json;
|
|
|
|
const ok = data.ok_core;
|
|
const banner = $('banner');
|
|
banner.className = ok ? 'banner-ok' : 'banner-ok banner-warn';
|
|
$('bannerTitle').textContent = ok ? '✓ 采集完成,凭证齐全' : '⚠ 采集完成,部分凭证缺失';
|
|
$('bannerSub').textContent = data.summary || '';
|
|
|
|
if (data.my_uid) {
|
|
$('uidTag').textContent = 'UID: ' + data.my_uid;
|
|
$('uidTag').classList.remove('hidden');
|
|
}
|
|
|
|
// 凭证齐全时自动复制
|
|
if (ok && json) {
|
|
setTimeout(() => copyText(json), 400);
|
|
}
|
|
}
|
|
|
|
function showError(msg) {
|
|
$('phaseWaiting').classList.add('hidden');
|
|
$('phaseError').classList.remove('hidden');
|
|
$('errorMsg').textContent = msg || '未知错误';
|
|
}
|
|
|
|
let lastRunId = '';
|
|
|
|
function resetUiForNewRun() {
|
|
$('phaseWaiting').classList.remove('hidden');
|
|
$('phaseDone').classList.add('hidden');
|
|
$('phaseError').classList.add('hidden');
|
|
$('waitMsg').textContent = '正在启动…';
|
|
renderSteps(1);
|
|
renderLogs([]);
|
|
const btn = $('btnStart');
|
|
if (btn) { btn.disabled = false; btn.textContent = '我已打开私信页,立即采集'; }
|
|
}
|
|
|
|
async function poll() {
|
|
try {
|
|
const r = await fetch('/api/status', { cache: 'no-store' });
|
|
const data = await r.json();
|
|
if (data.run_id && data.run_id !== lastRunId) {
|
|
lastRunId = data.run_id;
|
|
resetUiForNewRun();
|
|
}
|
|
if (data.port) {
|
|
$('runTag').textContent = '会话 ' + (data.run_id || '') + ' · 端口 ' + data.port;
|
|
}
|
|
renderSteps(data.step);
|
|
renderLogs(data.logs);
|
|
if (data.phase === 'waiting') {
|
|
$('waitMsg').textContent = data.message || '等待登录并采集…';
|
|
} else if (data.phase === 'collecting') {
|
|
$('waitMsg').textContent = data.message || '正在采集凭证…';
|
|
} else if (data.phase === 'done') {
|
|
showDone(data);
|
|
return;
|
|
} else if (data.phase === 'error') {
|
|
const msg = data.message || '未知错误';
|
|
if (/cursor-sandbox-cache/i.test(msg)) {
|
|
showError(msg + '\n\n请关闭本页后重新双击「一键采集.bat」。');
|
|
} else {
|
|
showError(msg);
|
|
}
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
$('waitMsg').textContent = '正在连接采集器…';
|
|
}
|
|
setTimeout(poll, 800);
|
|
}
|
|
|
|
poll();
|
|
</script>
|
|
</body>
|
|
</html>
|