Download templates/admin.html from cacode/dyspark: direct link, hf CLI and curl.
- Browser
- Download file 18.6 kB
-
https://huggingface.co/spaces/cacode/dyspark/resolve/main/templates/admin.html
- Command line
-
hf download hf://spaces/cacode/dyspark/templates/admin.html
-
curl -L -o admin.html https://huggingface.co/spaces/cacode/dyspark/resolve/main/templates/admin.html
18.6 kB
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>DouYin Spark Helper - Admin 控制台</title> | |
| <link rel="stylesheet" href="/static/style.css"> | |
| </head> | |
| <body class="dash-body"> | |
| <header class="topbar"> | |
| <div> | |
| <h1>Admin 后台管理</h1> | |
| <p>用户管理 + 定时任务总览</p> | |
| </div> | |
| <div class="top-actions"> | |
| <button id="retryAllFailedBtn" class="btn ghost">一键重试失败任务</button> | |
| <button id="refreshBtn" class="btn ghost">刷新</button> | |
| <button id="logoutBtn" class="btn ghost">退出登录</button> | |
| </div> | |
| </header> | |
| <main class="container"> | |
| <section class="panel"> | |
| <h2>用户与任务总览</h2> | |
| <p id="summary" class="muted">加载中...</p> | |
| <div id="dbAlert" class="alert warning" style="display:none;"></div> | |
| <div class="table-wrap"> | |
| <table class="admin-table"> | |
| <thead> | |
| <tr> | |
| <th>发起用户</th> | |
| <th>唯一标识</th> | |
| <th>注册时间</th> | |
| <th>定时状态</th> | |
| <th>发送时间</th> | |
| <th>消息内容</th> | |
| <th>接收方</th> | |
| <th>下次执行</th> | |
| <th>最近状态</th> | |
| <th>操作</th> | |
| </tr> | |
| </thead> | |
| <tbody id="adminBody"> | |
| <tr><td colspan="10">暂无数据</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| <p id="adminMsg" class="msg"></p> | |
| </section> | |
| <section id="detailPanel" class="panel" style="display:none;"> | |
| <div class="panel-header"> | |
| <h2 id="detailTitle">任务详情</h2> | |
| <div class="top-actions"> | |
| <button id="detailRetryBtn" class="btn" style="display:none;">重试失败任务</button> | |
| <button id="detailRefreshBtn" class="btn">刷新详情</button> | |
| </div> | |
| </div> | |
| <div id="detailMeta" class="detail-grid"></div> | |
| <div class="detail-split"> | |
| <article class="detail-card"> | |
| <h3>消息内容</h3> | |
| <pre id="detailMessage" class="detail-message">-</pre> | |
| </article> | |
| <article class="detail-card"> | |
| <h3>接收方(去重后)</h3> | |
| <pre id="detailTargets" class="detail-message">-</pre> | |
| </article> | |
| </div> | |
| <article class="detail-card"> | |
| <h3>账号明细</h3> | |
| <div class="table-wrap"> | |
| <table class="admin-table detail-history"> | |
| <thead> | |
| <tr> | |
| <th>账号昵称</th> | |
| <th>unique_id</th> | |
| <th>目标数</th> | |
| <th>cookies 数</th> | |
| <th>目标列表</th> | |
| </tr> | |
| </thead> | |
| <tbody id="detailAccountsBody"> | |
| <tr><td colspan="5">暂无数据</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </article> | |
| <article class="detail-card"> | |
| <h3>执行历史</h3> | |
| <div class="table-wrap"> | |
| <table class="admin-table detail-history"> | |
| <thead> | |
| <tr> | |
| <th>触发方式</th> | |
| <th>开始时间</th> | |
| <th>结束时间</th> | |
| <th>状态</th> | |
| <th>耗时</th> | |
| <th>信息</th> | |
| </tr> | |
| </thead> | |
| <tbody id="detailHistoryBody"> | |
| <tr><td colspan="6">暂无记录</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </article> | |
| <article class="detail-card"> | |
| <h3>任务日志</h3> | |
| <pre id="detailLogs" class="detail-logbox">暂无日志。</pre> | |
| </article> | |
| </section> | |
| </main> | |
| <script> | |
| const adminBody = document.getElementById("adminBody"); | |
| const summary = document.getElementById("summary"); | |
| const retryAllFailedBtn = document.getElementById("retryAllFailedBtn"); | |
| const dbAlert = document.getElementById("dbAlert"); | |
| const adminMsg = document.getElementById("adminMsg"); | |
| const detailPanel = document.getElementById("detailPanel"); | |
| const detailTitle = document.getElementById("detailTitle"); | |
| const detailMeta = document.getElementById("detailMeta"); | |
| const detailMessage = document.getElementById("detailMessage"); | |
| const detailTargets = document.getElementById("detailTargets"); | |
| const detailAccountsBody = document.getElementById("detailAccountsBody"); | |
| const detailHistoryBody = document.getElementById("detailHistoryBody"); | |
| const detailLogs = document.getElementById("detailLogs"); | |
| const detailRetryBtn = document.getElementById("detailRetryBtn"); | |
| let currentDetailUser = null; | |
| let dbConnected = true; | |
| function setMsg(msg, isError = false) { | |
| adminMsg.textContent = msg || ""; | |
| adminMsg.style.color = isError ? "#c0392b" : "#146356"; | |
| } | |
| async function requestJSON(url, options = {}) { | |
| const resp = await fetch(url, { | |
| credentials: "same-origin", | |
| headers: { "Content-Type": "application/json", ...(options.headers || {}) }, | |
| ...options, | |
| }); | |
| if (resp.status === 401) { | |
| window.location.href = "/admin"; | |
| throw new Error("登录已失效,请重新登录。"); | |
| } | |
| const data = await resp.json(); | |
| if (!resp.ok || data.ok === false) { | |
| throw new Error(data.message || "请求失败"); | |
| } | |
| return data; | |
| } | |
| function escapeHtml(value) { | |
| return String(value ?? "") | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">") | |
| .replace(/\"/g, """) | |
| .replace(/'/g, "'"); | |
| } | |
| function renderDbStatus(status, fallbackMessage = "") { | |
| dbConnected = !status || status.connected !== false; | |
| if (dbConnected) { | |
| dbAlert.style.display = "none"; | |
| dbAlert.textContent = ""; | |
| return; | |
| } | |
| const parts = ["当前无法连接 SQL 服务器。"]; | |
| if (fallbackMessage) { | |
| parts.push(fallbackMessage); | |
| } else if (status && status.last_error) { | |
| parts.push(status.last_error); | |
| } | |
| if (status && status.last_checked_at && status.last_checked_at !== "-") { | |
| parts.push(`最近检查:${status.last_checked_at}`); | |
| } | |
| if (status && status.last_ok_at && status.last_ok_at !== "-") { | |
| parts.push(`最近成功连接:${status.last_ok_at}`); | |
| } | |
| dbAlert.textContent = parts.join(" "); | |
| dbAlert.style.display = "block"; | |
| } | |
| function clearDetailPanel() { | |
| currentDetailUser = null; | |
| detailPanel.style.display = "none"; | |
| detailRetryBtn.style.display = "none"; | |
| detailRetryBtn.dataset.user = ""; | |
| detailTitle.textContent = "任务详情"; | |
| detailMeta.innerHTML = ""; | |
| detailMessage.textContent = "-"; | |
| detailTargets.textContent = "-"; | |
| detailLogs.textContent = "暂无日志。"; | |
| detailAccountsBody.innerHTML = '<tr><td colspan="5">暂无数据</td></tr>'; | |
| detailHistoryBody.innerHTML = '<tr><td colspan="6">暂无记录</td></tr>'; | |
| } | |
| function renderRows(users) { | |
| if (!users || users.length === 0) { | |
| adminBody.innerHTML = '<tr><td colspan="10">暂无用户</td></tr>'; | |
| clearDetailPanel(); | |
| return; | |
| } | |
| adminBody.innerHTML = users.map((u) => { | |
| if (u.error) { | |
| return ` | |
| <tr> | |
| <td>${escapeHtml(u.username)}</td> | |
| <td>${escapeHtml(u.unique_id || "-")}</td> | |
| <td>${escapeHtml(u.created_at || "-")}</td> | |
| <td colspan="6" style="color:#c0392b;">数据异常:${escapeHtml(u.error)}</td> | |
| <td> | |
| <div class="admin-actions"> | |
| <button class="btn admin-delete-user" data-user="${escapeHtml(u.username)}">删用户</button> | |
| </div> | |
| </td> | |
| </tr> | |
| `; | |
| } | |
| const targets = Array.isArray(u.targets) ? u.targets : []; | |
| const targetText = targets.length ? targets.join(" / ") : "-"; | |
| const statusText = u.is_running ? "运行中" : (u.last_status || "-"); | |
| const retryAction = u.can_retry | |
| ? `<button class="btn admin-retry-task" data-user="${escapeHtml(u.username)}">重试失败</button>` | |
| : ""; | |
| return ` | |
| <tr> | |
| <td>${escapeHtml(u.username)}</td> | |
| <td>${escapeHtml(u.unique_id)}</td> | |
| <td>${escapeHtml(u.created_at)}</td> | |
| <td>${u.scheduler_enabled ? "启用" : "禁用"}</td> | |
| <td>${escapeHtml(u.schedule_time)} (${escapeHtml(u.schedule_timezone)})</td> | |
| <td class="ellipsis-cell" title="${escapeHtml(u.message_template || "")}">${escapeHtml(u.message_template || "-")}</td> | |
| <td class="ellipsis-cell" title="${escapeHtml(targetText)}">${escapeHtml(targetText)}</td> | |
| <td>${escapeHtml(u.next_run || "-")}</td> | |
| <td>${escapeHtml(statusText)}</td> | |
| <td> | |
| <div class="admin-actions"> | |
| <button class="btn admin-view-task" data-user="${escapeHtml(u.username)}">详情/日志</button> | |
| ${retryAction} | |
| <button class="btn admin-del-task" data-user="${escapeHtml(u.username)}">删任务</button> | |
| <button class="btn admin-delete-user" data-user="${escapeHtml(u.username)}">删用户</button> | |
| </div> | |
| </td> | |
| </tr> | |
| `; | |
| }).join(""); | |
| document.querySelectorAll(".admin-view-task").forEach((btn) => { | |
| btn.addEventListener("click", async () => { | |
| const username = btn.dataset.user; | |
| await loadTaskDetail(username, true); | |
| }); | |
| }); | |
| document.querySelectorAll(".admin-del-task").forEach((btn) => { | |
| btn.addEventListener("click", async () => { | |
| const username = btn.dataset.user; | |
| if (!confirm(`确认删除用户 ${username} 的定时任务?`)) return; | |
| setMsg("正在删除任务..."); | |
| try { | |
| const data = await requestJSON(`/api/admin/tasks/${encodeURIComponent(username)}/delete`, { | |
| method: "POST", | |
| body: "{}", | |
| }); | |
| setMsg(data.message || "已删除任务"); | |
| await loadOverview(); | |
| if (currentDetailUser === username) { | |
| await loadTaskDetail(username, false); | |
| } | |
| } catch (err) { | |
| setMsg(err.message, true); | |
| } | |
| }); | |
| }); | |
| document.querySelectorAll(".admin-retry-task").forEach((btn) => { | |
| btn.addEventListener("click", async () => { | |
| const username = btn.dataset.user; | |
| await retryFailedTask(username); | |
| }); | |
| }); | |
| document.querySelectorAll(".admin-delete-user").forEach((btn) => { | |
| btn.addEventListener("click", async () => { | |
| const username = btn.dataset.user; | |
| if (!confirm(`确认删除用户 ${username}?该操作不可恢复。`)) return; | |
| setMsg("正在删除用户..."); | |
| try { | |
| const data = await requestJSON(`/api/admin/users/${encodeURIComponent(username)}`, { | |
| method: "DELETE", | |
| }); | |
| setMsg(data.message || "用户已删除"); | |
| if (currentDetailUser === username) { | |
| clearDetailPanel(); | |
| } | |
| await loadOverview(); | |
| } catch (err) { | |
| setMsg(err.message, true); | |
| } | |
| }); | |
| }); | |
| } | |
| function renderDetail(task) { | |
| currentDetailUser = task.username; | |
| detailPanel.style.display = "block"; | |
| detailTitle.textContent = `任务详情 · ${task.username}`; | |
| const runtime = task.runtime || {}; | |
| const config = task.config || {}; | |
| const targets = Array.isArray(task.targets) ? task.targets : []; | |
| detailMeta.innerHTML = ` | |
| <div class="detail-item"><span>发起用户</span><strong>${escapeHtml(task.username || "-")}</strong></div> | |
| <div class="detail-item"><span>唯一标识</span><strong>${escapeHtml(task.unique_id || "-")}</strong></div> | |
| <div class="detail-item"><span>注册时间</span><strong>${escapeHtml(task.created_at || "-")}</strong></div> | |
| <div class="detail-item"><span>定时状态</span><strong>${task.scheduler_enabled ? "启用" : "禁用"}</strong></div> | |
| <div class="detail-item"><span>发送时间</span><strong>${escapeHtml(task.schedule_time || "-")} (${escapeHtml(task.schedule_timezone || "-")})</strong></div> | |
| <div class="detail-item"><span>下一次执行</span><strong>${escapeHtml(runtime.next_run || "-")}</strong></div> | |
| <div class="detail-item"><span>最近状态</span><strong>${runtime.is_running ? "运行中" : escapeHtml(runtime.last_status || "-")}</strong></div> | |
| <div class="detail-item"><span>最近开始</span><strong>${escapeHtml(runtime.last_start || "-")}</strong></div> | |
| <div class="detail-item"><span>账号数 / 目标数</span><strong>${runtime.account_count || 0} / ${runtime.target_count || 0}</strong></div> | |
| <div class="detail-item"><span>并发设置</span><strong>multiTask=${config.multiTask ? "true" : "false"}, taskCount=${config.taskCount || 1}</strong></div> | |
| `; | |
| detailMessage.textContent = task.message_template || "-"; | |
| detailTargets.textContent = targets.length ? targets.join("\n") : "-"; | |
| detailLogs.textContent = task.logs || "暂无日志。"; | |
| detailRetryBtn.dataset.user = task.username || ""; | |
| detailRetryBtn.style.display = task.can_retry ? "inline-flex" : "none"; | |
| const accounts = Array.isArray(task.accounts) ? task.accounts : []; | |
| if (!accounts.length) { | |
| detailAccountsBody.innerHTML = '<tr><td colspan="5">暂无账号明细</td></tr>'; | |
| } else { | |
| detailAccountsBody.innerHTML = accounts.map((item) => { | |
| const t = Array.isArray(item.targets) ? item.targets : []; | |
| return ` | |
| <tr> | |
| <td>${escapeHtml(item.username || "-")}</td> | |
| <td>${escapeHtml(item.unique_id || "-")}</td> | |
| <td>${item.target_count || 0}</td> | |
| <td>${item.cookie_count || 0}</td> | |
| <td>${escapeHtml(t.join(" / ") || "-")}</td> | |
| </tr> | |
| `; | |
| }).join(""); | |
| } | |
| const history = Array.isArray(task.history) ? task.history : []; | |
| if (!history.length) { | |
| detailHistoryBody.innerHTML = '<tr><td colspan="6">暂无记录</td></tr>'; | |
| } else { | |
| detailHistoryBody.innerHTML = history.map((row) => ` | |
| <tr> | |
| <td>${escapeHtml(row.trigger || "-")}</td> | |
| <td>${escapeHtml(row.start || "-")}</td> | |
| <td>${escapeHtml(row.end || "-")}</td> | |
| <td>${escapeHtml(row.status || "-")}</td> | |
| <td>${escapeHtml(row.duration || "-")}</td> | |
| <td>${escapeHtml(row.message || "-")}</td> | |
| </tr> | |
| `).join(""); | |
| } | |
| } | |
| async function loadTaskDetail(username, showMessage = false) { | |
| try { | |
| const data = await requestJSON(`/api/admin/tasks/${encodeURIComponent(username)}?log_limit=1200`); | |
| renderDetail(data.task || {}); | |
| if (showMessage) { | |
| setMsg(`已加载 ${username} 的任务详情与日志。`); | |
| } | |
| } catch (err) { | |
| setMsg(err.message, true); | |
| } | |
| } | |
| async function retryAllFailedTasks() { | |
| if (!dbConnected) { | |
| setMsg("SQL 连接异常,暂时无法批量重试失败任务。", true); | |
| return; | |
| } | |
| setMsg("正在批量重试失败任务..."); | |
| try { | |
| const data = await requestJSON("/api/admin/tasks/retry-failed", { | |
| method: "POST", | |
| body: "{}", | |
| }); | |
| setMsg(data.message || "已开始批量重试失败任务"); | |
| await loadOverview(); | |
| if (currentDetailUser && dbConnected) { | |
| await loadTaskDetail(currentDetailUser, false); | |
| } | |
| } catch (err) { | |
| setMsg(err.message, true); | |
| } | |
| } | |
| async function retryFailedTask(username) { | |
| if (!username) { | |
| setMsg("未找到可重试的任务用户。", true); | |
| return; | |
| } | |
| setMsg("正在重试失败任务..."); | |
| try { | |
| const data = await requestJSON(`/api/admin/tasks/${encodeURIComponent(username)}/retry`, { | |
| method: "POST", | |
| body: "{}", | |
| }); | |
| setMsg(data.message || "已开始重试失败任务"); | |
| await loadOverview(); | |
| if (currentDetailUser === username && dbConnected) { | |
| await loadTaskDetail(username, false); | |
| } | |
| } catch (err) { | |
| setMsg(err.message, true); | |
| } | |
| } | |
| async function loadOverview() { | |
| const data = await requestJSON("/api/admin/overview"); | |
| renderDbStatus(data.db_status || null, data.message || ""); | |
| summary.textContent = dbConnected | |
| ? `共 ${data.task_count || 0} 个用户任务。` | |
| : "SQL 连接异常,当前未能加载用户任务。"; | |
| renderRows(data.users || []); | |
| if (!dbConnected) { | |
| clearDetailPanel(); | |
| } | |
| return data; | |
| } | |
| retryAllFailedBtn.addEventListener("click", async () => { | |
| await retryAllFailedTasks(); | |
| }); | |
| document.getElementById("refreshBtn").addEventListener("click", async () => { | |
| try { | |
| await loadOverview(); | |
| if (dbConnected && currentDetailUser) { | |
| await loadTaskDetail(currentDetailUser, false); | |
| } | |
| } catch (err) { | |
| setMsg(err.message, true); | |
| } | |
| }); | |
| document.getElementById("detailRefreshBtn").addEventListener("click", async () => { | |
| if (!currentDetailUser) { | |
| setMsg("请先在上方选择一个任务。", true); | |
| return; | |
| } | |
| await loadTaskDetail(currentDetailUser, true); | |
| }); | |
| detailRetryBtn.addEventListener("click", async () => { | |
| await retryFailedTask(detailRetryBtn.dataset.user || currentDetailUser); | |
| }); | |
| document.getElementById("logoutBtn").addEventListener("click", async () => { | |
| await fetch("/api/logout", { method: "POST", credentials: "same-origin" }); | |
| window.location.href = "/admin"; | |
| }); | |
| loadOverview().catch((err) => setMsg(err.message, true)); | |
| setInterval(async () => { | |
| try { | |
| await loadOverview(); | |
| if (dbConnected && currentDetailUser) { | |
| await loadTaskDetail(currentDetailUser, false); | |
| } | |
| } catch (_) { | |
| } | |
| }, 8000); | |
| </script> | |
| </body> | |
| </html> | |