Download static/webapp.html from ljx77qaq/DaBiao: direct link, hf CLI and curl.
- Browser
- Download file 14.4 kB
-
https://huggingface.co/spaces/ljx77qaq/DaBiao/resolve/main/static/webapp.html
- Command line
-
hf download hf://spaces/ljx77qaq/DaBiao/static/webapp.html
-
curl -L -o webapp.html https://huggingface.co/spaces/ljx77qaq/DaBiao/resolve/main/static/webapp.html
14.4 kB
| <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> | |