DaBiao / static /webapp.html
ljx77qaq's picture
fix: route author posts to persistent forum topics
6106143
Raw History Blame Contribute Delete
14.4 kB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover,user-scalable=no">
<title>NikNok 抖音管理员</title>
<link rel="stylesheet" href="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/Inter/3.19.11/Inter.var.min.css">
<style>
:root {
--bg: #0c0d10; --bg2: #16181d; --bg3: #1e2128; --line: #282c34;
--fg: #e8eaed; --dim: #9aa0aa; --acc: #ff4d6d; --acc2: #4d8dff; --ok: #31c48d; --warn: #f7b731;
}
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
body { margin: 0; background: var(--bg); color: var(--fg); font: 15px/1.6 -apple-system, Inter, "PingFang SC", sans-serif; min-height: 100vh; display: flex; flex-direction: column; }
header { padding: 16px 20px; background: var(--bg2); border-bottom: 1px solid var(--line); font-weight: 700; font-size: 17px; display: flex; align-items: center; gap: 8px; }
header .badge { font-size: 11px; background: var(--acc); color: #fff; padding: 2px 8px; border-radius: 999px; font-weight: 500; }
main { flex: 1; padding: 16px; max-width: 800px; width: 100%; margin: 0 auto; overflow-y: auto; }
.card { background: var(--bg2); border: 1px solid var(--line); border-radius: 14px; padding: 16px; margin-bottom: 16px; }
.card h3 { margin-top: 0; font-size: 15px; color: var(--fg); margin-bottom: 10px; border-bottom: 1px solid var(--line); padding-bottom: 8px; display: flex; justify-content: space-between; align-items: center; }
.card h3 .btn { font-weight: 500; padding: 4px 10px; font-size: 12px; }
.btn { background: var(--acc); color: #fff; border: 0; border-radius: 8px; padding: 8px 14px; font-size: 14px; cursor: pointer; }
.btn.ghost { background: var(--bg3); color: var(--fg); border: 1px solid var(--line); }
.btn.sm { padding: 4px 10px; font-size: 12px; }
.btn.ok { background: var(--ok); }
.btn.warn { background: var(--warn); color: #333; }
.btn.block { display: block; width: 100%; text-align: center; margin-top: 10px; box-sizing: border-box; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; }
.item { background: var(--bg3); border-radius: 10px; padding: 12px; text-align: center; border: 1px solid var(--line); }
.item img { width: 64px; height: 64px; border-radius: 50%; margin-bottom: 8px; object-fit: cover; background: #333; }
.item .name { font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 4px; }
.item .cnt { font-size: 12px; color: var(--dim); margin-bottom: 8px; }
.item .acts { display: flex; gap: 4px; justify-content: center; }
.item .acts .btn { flex: 1; padding: 4px 0; }
nav { display: flex; background: var(--bg2); border-top: 1px solid var(--line); height: 60px; }
nav .tab { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; font-size: 12px; color: var(--dim); cursor: pointer; text-decoration: none; }
nav .tab.active { color: var(--acc2); }
nav .tab .icon { font-size: 18px; }
input, textarea { width: 100%; background: var(--bg3); border: 1px solid var(--line); color: var(--fg); border-radius: 8px; padding: 10px; font-size: 14px; box-sizing: border-box; font-family: inherit; }
textarea { height: 100px; resize: none; }
.page { display: none; }
.page.active { display: block; }
.loader { text-align: center; padding: 24px 0; color: var(--dim); font-size: 13px; }
.stats { display: flex; gap: 8px; font-size: 13px; background: var(--bg3); padding: 8px; border-radius: 8px; margin-bottom: 10px; }
.status { font-size: 12px; padding: 2px 6px; border-radius: 4px; font-weight: 600; }
.status.running { background: var(--acc2); color: #fff; }
.status.completed { background: var(--ok); color: #fff; }
.status.error { background: var(--acc); color: #fff; }
.status.pending, .status.stopped { background: var(--dim); color: #111; }
.toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: var(--bg2); border: 1px solid var(--line); padding: 10px 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.5); z-index: 1000; opacity: 0; transition: opacity 0.3s; pointer-events: none; }
</style>
</head>
<body id="app">
<header>
<span>🎛️ 抖音控制中心</span>
<span class="badge">WebApp 版</span>
</header>
<main>
<!-- ==================== 首页/博主管理 ==================== -->
<div id="page-bloggers" class="page active">
<div class="card">
<div style="display:flex; gap:8px; margin-bottom:10px;">
<input id="sync_target" placeholder="TG 频道 ID(如 @channel 或 -100xx)" onchange="saveChannel()">
<button class="btn" style="white-space:nowrap" onclick="saveChannel()">保存频道</button>
</div>
<div class="stats">
<div>👥 关注数: <span id="following_count">-</span></div>
<div>✅ 已同步: <span id="synced_count">0</span></div>
<div>⚙️ 状态: <span id="cookie_status">加载中</span></div>
</div>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
<h3 style="margin-bottom:0">全部关注博主</h3>
<button class="btn ghost sm" onclick="refreshFollowings()">🔄 刷新列表</button>
</div>
<div id="bloggers_list" class="grid">
<div class="loader">正在拼命加载关注列表...</div>
</div>
</div>
</div>
<!-- ==================== 同步任务 ==================== -->
<div id="page-tasks" class="page">
<div class="card">
<h3>⚡ 后台同步任务</h3>
<div id="tasks_list">
<div class="loader">加载任务中...</div>
</div>
</div>
</div>
<!-- ==================== Cookie/设置 ==================== -->
<div id="page-cookie" class="page">
<div class="card">
<h3>🔑 覆盖默认 Cookie</h3>
<div style="margin-bottom:10px; font-size:13px; color:var(--dim);">当前已配置的 Cookie 决定关注抓取权限,定期过期更新:</div>
<textarea id="cookie_input" placeholder="在此处粘贴您的 Cookie(支持整句复制)"></textarea>
<button class="btn block" onclick="saveCookie()">💾 保存并验证</button>
<button class="btn ghost block" style="margin-top:8px;color:#ff9aab;border-color:#5c2b3a" onclick="clearCookie()">🗑️ 清空 Cookie</button>
<div style="margin-top:16px; padding-top:16px; border-top:1px dashed var(--line); font-size:13px; color:var(--dim); line-height:1.8;">
<b>如何获取 Cookie:</b><br>
1. 手机自带浏览器 / Alook 打开 <a href="https://www.douyin.com" style="color:var(--acc2)" target="_blank">douyin.com</a> 登录。<br>
2. 在浏览器打开 <i>设置 ➔ 开发者工具 ➔ Cookie</i> 点击【全部复制】。<br>
3. 返回本页面粘贴到上面点击保存。
</div>
</div>
</div>
</main>
<nav>
<a class="tab active" data-page="bloggers" onclick="switchPage('bloggers')">
<span class="icon">🏠</span>
<span>博主列表</span>
</a>
<a class="tab" data-page="tasks" onclick="switchPage('tasks')">
<span class="icon">⚡</span>
<span>同步频道</span>
</a>
<a class="tab" data-page="cookie" onclick="switchPage('cookie')">
<span class="icon">🔑</span>
<span>Cookie配置</span>
</a>
</nav>
<div class="toast" id="toast"></div>
<script>
let tgChatId = "123456"; // WebApp fallback chat_id
try {
const tg = window.Telegram?.WebApp;
if (tg) {
tg.expand();
tgChatId = String(tg.initDataUnsafe?.user?.id || tgChatId);
}
} catch(e) {}
// Basic UI helpers
const $ = s => document.querySelector(s);
const toast = (text, isErr=false) => {
const el = $("#toast");
el.textContent = text;
el.style.opacity = 1;
el.style.color = isErr ? "#ff9aab" : "#31c48d";
setTimeout(()=>{ el.style.opacity = 0 }, 2800);
};
function switchPage(name) {
document.querySelectorAll(".page").forEach(p => p.classList.remove("active"));
document.querySelectorAll("nav .tab").forEach(t => t.classList.remove("active"));
$(`#page-${name}`).classList.add("active");
document.querySelector(`nav .tab[data-page="${name}"]`).classList.add("active");
if(name === "bloggers") loadBloggers();
if(name === "tasks") loadTasks();
}
// ==================== API CALLS ====================
const API = {
get: (url) => fetch(url).then(r => r.json()),
post: (url, data) => fetch(url, { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify(data||{}) }).then(r => r.json()),
del: (url) => fetch(url, { method: "DELETE" }).then(r => r.json()),
};
// 1. Cookie & Auth Status
function getCookieStatus() {
API.get("/api/webapp/cookie").then(r => {
if (!r.has_cookie) {
$("#cookie_status").innerHTML = "<span style='color:#ff9aab'>未配置 Cookie</span>";
} else {
const color = r.valid ? "#31c48d" : "#ff9aab";
$("#cookie_status").innerHTML = `<span style='color:${color}'>${r.status}</span>`;
}
});
}
function saveCookie() {
const val = $("#cookie_input").value.trim();
if(!val) return toast("请输入 Cookie 后再保存", true);
API.post("/api/webapp/cookie", { cookie: val }).then(r => {
if (r.ok) {
toast(`验证完毕: ${r.status}`);
getCookieStatus();
setTimeout(() => switchPage("bloggers"), 1000);
} else toast(r.error || "保存失败", true);
});
}
function clearCookie() {
if(!confirm("确认要清除当前的 Cookie 吗?清除后无法爬取关注博主")) return;
API.del("/api/webapp/cookie").then(()=>{
toast("Cookie 已清空");
getCookieStatus();
});
}
// 2. Channel settings
function loadChannel() {
API.get(`/api/webapp/channel?chat_id=${tgChatId}`).then(r => {
$("#sync_target").value = r.channel || "";
});
}
function saveChannel() {
const val = $("#sync_target").value.trim();
API.post(`/api/webapp/channel?chat_id=${tgChatId}`, { channel: val }).then(()=>{
toast("目标频道绑定成功!");
});
}
// 3. Bloggers Followings
let page = 0, limit = 15;
function loadBloggers(offset=0, append=false) {
if (!append) $("#bloggers_list").innerHTML = '<div class="loader">正在加载...</div>';
API.get(`/api/webapp/following?chat_id=${tgChatId}&offset=${offset}&limit=${limit}`).then(r => {
if (!r.ok) { $("#bloggers_list").innerHTML = "出错了"; return; }
if (r.items.length === 0) {
$("#bloggers_list").innerHTML = "<div class='loader' style='grid-column:1/-1;color:var(--dim)'>没有获取到强行关注,请配置Cookie</div>";
return;
}
let html = append ? $("#bloggers_list").innerHTML : "";
// clean up loader if present
if(!append) html = "";
r.items.forEach(f => {
const uid_short = f.uid || f.sec_uid.slice(8, 20);
html += `
<div class="item">
<img src="https://p11.douyinpic.com${ (f.avatar_url || '').includes('http') ? '' : ''}${f.avatar_url.replace('http://', 'https://').replace('https://p11.douyinpic.com', '')}" onerror="this.src='https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png'">
<div class="name" title="${f.nickname}">${f.nickname}</div>
<div class="cnt">作品: ${f.aweme_count || 0}个</div>
<div class="acts">
<button class="btn sm" onclick="startSync('${f.sec_uid}', '${f.nickname}')">🚀 搬运</button>
</div>
</div>`;
});
if (html) {
$("#bloggers_list").innerHTML = html;
}
$("#following_count").innerText = r.total || 0;
});
}
function refreshFollowings() {
loadBloggers(0, false);
toast("已重置列表同步");
}
// 4. Tasks Sync
function loadTasks() {
API.get(`/api/webapp/tasks?chat_id=${tgChatId}`).then(r => {
if(!r.ok) return;
if(!r.tasks || r.tasks.length === 0) {
$("#tasks_list").innerHTML = `<div class="loader" style="color:var(--dim)">暂无任何运行任务</div>`;
return;
}
let html = "";
r.tasks.forEach(t => {
let cls = "completed";
if (t.status === "running" || t.status === "pending") cls = "running";
else if (t.status === "error") cls = "error";
else if (t.status === "stopped") cls = "stopped";
html += `
<div class="item" style="margin-bottom:8px; text-align:left;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;">
<b>👤 ${t.author_name}</b>
<span class="status ${cls}">${t.status}</span>
</div>
<div style="font-size:12px;color:var(--dim)">
频道: <code>${t.target_channel}</code> | 任务ID: ${t.task_id}<br>
进度: ${t.synced_posts} 已同步 / ${t.failed_posts} 失败 / 总共 ${t.total_posts} 作品
</div>
${(t.status === 'running' || t.status === 'pending') ? `<button class="btn warn sm block" style="margin-top:8px;" onclick="stopTask('${t.task_id}')">⏹️ 终止该任务</button>` : ''}
</div>`;
});
$("#tasks_list").innerHTML = html;
});
}
function startSync(sec_uid, name) {
const ch = $("#sync_target").value.trim();
if (!ch) {
if(!confirm("您未配置默认搬运频道,将直接发送至 Web/Telegram当前群组。继续吗?")) return;
}
API.post(`/api/webapp/sync/start?chat_id=${tgChatId}`, { sec_uid: sec_uid, mode: "all" }).then(r => {
if (r.ok) {
toast(`已启动任务:搬运“${name}”的作品!`);
switchPage("tasks");
} else {
toast("错误:" + r.error, true);
}
});
}
function stopTask(id) {
if(!confirm("确认停止该任务?")) return;
API.post(`/api/webapp/sync/stop/${id}?chat_id=${tgChatId}`, {}).then(r => {
toast("停止信号已发出");
loadTasks();
});
}
// Init
getCookieStatus();
loadChannel();
loadBloggers(0);
</script>
</body>
</html>