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,74 @@
|
||||
import { api } from "../api.js";
|
||||
import { cover, emptyState, sectionHeader } from "../components.js";
|
||||
import {
|
||||
escapeAttr, escapeHtml, formatCount, formatDate, formatDuration,
|
||||
} from "../format.js";
|
||||
|
||||
function candidateCard(item) {
|
||||
const score = Number(item.score || 0);
|
||||
return `<article class="candidate-card">
|
||||
${cover(item, "candidate-thumb")}
|
||||
<div class="candidate-body">
|
||||
<div class="candidate-top">
|
||||
<span class="score-badge ${score >= 4 ? "high" : score >= 2 ? "mid" : "low"}">score ${score}</span>
|
||||
${item.notified_at ? `<span class="badge subtle">已通知</span>` : `<span class="badge warning">未通知</span>`}
|
||||
<span class="badge subtle">剩余 ${item.days_left} 天</span>
|
||||
</div>
|
||||
<h3 class="video-title">${escapeHtml(item.title || "(无标题)")}</h3>
|
||||
<div class="video-meta">
|
||||
<span>${escapeHtml(item.up_name || "未知 UP")}</span>
|
||||
<span>发布 ${escapeHtml(formatDate(item.pubdate))}</span>
|
||||
<span>${escapeHtml(formatDuration(item.duration))}</span>
|
||||
<span>${escapeHtml(item.channel || "")}</span>
|
||||
</div>
|
||||
<div class="video-stats">
|
||||
<span class="secondary-stat">播放 ${formatCount(item.play)}</span>
|
||||
<span class="secondary-stat">点赞 ${formatCount(item.like)}</span>
|
||||
<span class="secondary-stat">收藏 ${formatCount(item.favorite)}</span>
|
||||
<span class="secondary-stat">弹幕 ${formatCount(item.danmaku)}</span>
|
||||
<span class="secondary-stat">评论 ${formatCount(item.review)}</span>
|
||||
</div>
|
||||
<div class="candidate-actions">
|
||||
${item.url ? `<a class="button ghost" href="${escapeAttr(item.url)}" target="_blank" rel="noreferrer">打开 B 站</a>` : ""}
|
||||
<span class="readonly-note">请在 Telegram 中收藏 / 忽略 / 拉黑</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
export async function renderCandidates(root) {
|
||||
const data = await api("/api/candidates");
|
||||
const pending = data.items.filter((item) => item.decision === null);
|
||||
const decided = data.items.filter((item) => item.decision !== null);
|
||||
|
||||
root.innerHTML = `<section class="page candidates-page">
|
||||
<header class="page-head">
|
||||
<div>
|
||||
<h1>候选池</h1>
|
||||
<p class="page-sub">${formatCount(data.pending)} 条待确认 · ${formatCount(data.total)} 条总计 · 保留 ${data.ttl_days} 天</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="notice-row">
|
||||
<strong>Web 看板只读</strong>
|
||||
<span>${data.telegram_enabled ? "请在 Telegram 消息中处理候选。" : "Telegram 未启用,请使用 CLI 或数据库处理候选。"}</span>
|
||||
</div>
|
||||
|
||||
<section class="panel">
|
||||
${sectionHeader("待确认")}
|
||||
${pending.length ? `<div class="candidate-list">${pending.map(candidateCard).join("")}</div>`
|
||||
: emptyState("没有待确认候选", "搜索发现达到阈值的视频会出现在这里,同时发送 Telegram 通知。")}
|
||||
</section>
|
||||
|
||||
${decided.length ? `<details class="panel details-panel">
|
||||
<summary>已处理候选(${decided.length})</summary>
|
||||
<div class="candidate-list">
|
||||
${decided.map((item) => `<div class="decided-row">
|
||||
<span class="badge subtle">${escapeHtml(item.decision)}</span>
|
||||
<a href="${escapeAttr(item.url || "#")}" target="_blank" rel="noreferrer">${escapeHtml(item.title)}</a>
|
||||
<span>${escapeHtml(item.up_name || "")}</span>
|
||||
</div>`).join("")}
|
||||
</div>
|
||||
</details>` : ""}
|
||||
</section>`;
|
||||
}
|
||||
Reference in new issue
Block a user