feat(web): add mobile-first read-only dashboard frontend
This commit is contained in:
1 parent
257fbe009b
commit
cbba405271
18 files changed
+2628
-2
No files matched your search
@@ -0,0 +1,123 @@
|
||||
import { api } from "../api.js";
|
||||
import { emptyState, sectionHeader } from "../components.js";
|
||||
import {
|
||||
escapeHtml, formatCount, formatDateTime, formatNumber,
|
||||
} from "../format.js";
|
||||
|
||||
function taskRows(tasks) {
|
||||
const entries = Object.entries(tasks || {});
|
||||
if (!entries.length) return emptyState("暂无任务记录", "运行一次同步、快照或发现后会写入最近结果。");
|
||||
return `<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>任务</th><th>结果</th><th>完成时间</th><th>摘要 / 错误</th></tr></thead>
|
||||
<tbody>
|
||||
${entries.map(([name, record]) => {
|
||||
const summary = record.summary && typeof record.summary === "object"
|
||||
? Object.entries(record.summary)
|
||||
.filter(([key]) => ["added", "removed", "candidates", "notified", "ok", "failed", "total", "completed", "backup_error"].includes(key))
|
||||
.map(([key, value]) => `${key}=${value}`).join(" ")
|
||||
: "";
|
||||
return `<tr>
|
||||
<td>${escapeHtml(name)}</td>
|
||||
<td>${record.ok ? "成功" : `<span class="danger">失败</span>`}</td>
|
||||
<td>${escapeHtml(formatDateTime(record.finished_at))}</td>
|
||||
<td class="summary-cell">${escapeHtml(summary || record.error || "—")}</td>
|
||||
</tr>`;
|
||||
}).join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export async function renderSystem(root) {
|
||||
const system = await api("/api/system");
|
||||
const health = system.snapshot_health || { total: 0, ok: 0, failed: 0 };
|
||||
const counts = system.counts || {};
|
||||
const next = system.next_runs || {};
|
||||
|
||||
root.innerHTML = `<section class="page system-page">
|
||||
<header class="page-head">
|
||||
<div>
|
||||
<h1>系统状态</h1>
|
||||
<p class="page-sub">数据 ${escapeHtml(system.data_date || "—")} · 快照 ${formatCount(system.snapshot_days)} 天</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="mini-stats">
|
||||
<div><span>采集成功</span><strong>${formatCount(health.ok)}</strong></div>
|
||||
<div><span>采集失败</span><strong class="${health.failed ? "danger" : ""}">${formatCount(health.failed)}</strong></div>
|
||||
<div><span>在库</span><strong>${formatCount(counts.active)}</strong></div>
|
||||
<div><span>已取消收藏</span><strong>${formatCount(counts.unfaved)}</strong></div>
|
||||
<div><span>已失效</span><strong>${formatCount(counts.removed)}</strong></div>
|
||||
<div><span>已移出库</span><strong>${formatCount(counts.dropped)}</strong></div>
|
||||
</div>
|
||||
|
||||
<section class="panel">
|
||||
${sectionHeader("最近任务")}
|
||||
${taskRows(system.tasks)}
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
${sectionHeader("预计下次运行")}
|
||||
<div class="status-grid">
|
||||
<div><span>收藏夹同步</span><strong>${escapeHtml(formatDateTime(next.folder_sync))}</strong></div>
|
||||
<div><span>搜索发现</span><strong>${escapeHtml(formatDateTime(next.discover))}</strong></div>
|
||||
<div><span>每日快照</span><strong>${escapeHtml(formatDateTime(next.snapshot))}</strong></div>
|
||||
<div><span>调度参数</span><strong>${system.schedule.discover_minutes} / ${system.schedule.folder_sync_minutes} 分钟</strong></div>
|
||||
</div>
|
||||
<p class="section-note">预计时间按配置和最近任务完成时间推算;实际以常驻进程为准。</p>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
${sectionHeader("收藏夹容量")}
|
||||
${(system.folders || []).map((folder) => `
|
||||
<div class="capacity-row">
|
||||
<div class="capacity-label">
|
||||
<span>${escapeHtml(folder.title || String(folder.year))}</span>
|
||||
<strong>${formatCount(folder.media_count)} / ${formatCount(folder.capacity)}</strong>
|
||||
</div>
|
||||
<div class="capacity-track"><span style="width:${Math.min(100, folder.ratio * 100)}%"
|
||||
class="${folder.warning ? "warning" : ""}"></span></div>
|
||||
<p class="section-note">最近同步 ${escapeHtml(formatDateTime(folder.last_ids_at))}</p>
|
||||
</div>`).join("") || emptyState("未配置收藏夹")}
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
${sectionHeader("授权与备份")}
|
||||
<div class="status-grid">
|
||||
<div><span>登录态</span><strong>${system.auth.ok === "1" ? "正常" : "异常"}</strong></div>
|
||||
<div><span>登录信息</span><strong>${escapeHtml(system.auth.message || "—")}</strong></div>
|
||||
<div><span>最近检查</span><strong>${escapeHtml(formatDateTime(system.auth.last_check))}</strong></div>
|
||||
<div><span>最近备份</span><strong>${system.backup.ok === "1" ? "成功" : "未完成"}</strong></div>
|
||||
<div><span>备份错误</span><strong class="${system.backup.error ? "danger" : ""}">${escapeHtml(system.backup.error || "—")}</strong></div>
|
||||
<div><span>Telegram</span><strong>${system.telegram_enabled ? "已启用" : "未启用"}</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
${(system.errors || []).length ? `<section class="panel">
|
||||
${sectionHeader("最近错误")}
|
||||
${system.errors.map((error) => `<div class="error-row">
|
||||
<strong>${escapeHtml(error.task)}</strong>
|
||||
<span>${escapeHtml(error.error || "未知错误")}</span>
|
||||
</div>`).join("")}
|
||||
</section>` : ""}
|
||||
|
||||
<section class="panel">
|
||||
${sectionHeader("UP 黑名单")}
|
||||
<p class="section-note">只影响未来搜索发现;已入库视频继续追踪。Web 只读,移除请使用 CLI 或数据库。</p>
|
||||
${(system.blacklist || []).length ? `<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>mid</th><th>昵称</th><th>原因</th><th>在库视频</th></tr></thead>
|
||||
<tbody>
|
||||
${system.blacklist.map((item) => `<tr>
|
||||
<td>${item.mid}</td>
|
||||
<td>${escapeHtml(item.name || "—")}</td>
|
||||
<td>${escapeHtml(item.reason || "—")}</td>
|
||||
<td>${formatNumber(item.video_count || 0)}</td>
|
||||
</tr>`).join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>` : emptyState("黑名单为空")}
|
||||
</section>
|
||||
</section>`;
|
||||
}
|
||||
Reference in new issue
Block a user