From 0c660c647415f4418dd42ac33f761ac57e15d3f0 Mon Sep 17 00:00:00 2001 From: bot-ci Date: Tue, 3 Nov 2026 23:45:44 +0800 Subject: [PATCH] feat(web): include removed in visible stats; cover bang badge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 站内展示/统计统一 VISIBLE(含 removed):热度、UP 列表、总览 active_ups/初导入计数、说明页粉丝覆盖、黑名单视频数。采集队列仍 只 active/unfaved。失效片封面右上角感叹号徽标,点击/tap 提示 「该视频已失效」;手动/候选入库仍可复活 removed→active。 --- reaction_board/web/queries.py | 15 ++++----- reaction_board/web/static/app.js | 9 ++++++ reaction_board/web/static/components.js | 9 ++++++ reaction_board/web/static/style.css | 26 +++++++++++++++ reaction_board/web/static/views/video.js | 6 +++- tests/test_removed_cover_mark.py | 34 ++++++++++++++++++++ tests/test_web_queries.py | 40 ++++++++++++++++++------ 7 files changed, 122 insertions(+), 17 deletions(-) create mode 100644 tests/test_removed_cover_mark.py diff --git a/reaction_board/web/queries.py b/reaction_board/web/queries.py index 0b72c1a..59ba2fa 100644 --- a/reaction_board/web/queries.py +++ b/reaction_board/web/queries.py @@ -44,9 +44,10 @@ from ..timeutil import ( ) from .cache import DASHBOARD_CACHE, TTL_FANS, TTL_VIDEOS, build_cache_key -TRACKED_STATUSES = ("active", "unfaved") -# 站内列表可见:含原站失效 removed(继续展示、badge 可标 status-removed);不含人工 dropped +TRACKED_STATUSES = ("active", "unfaved") # 采集队列口径;站内展示勿用 +# 站内展示/统计:含原站失效 removed(继续展示);不含人工 dropped VISIBLE_STATUSES = ("active", "unfaved", "removed") +_VISIBLE_IN = ",".join(f"'{s}'" for s in VISIBLE_STATUSES) VIDEO_SORTS = { "heat", "view", "like", "coin", "favorite", "share", "reply", "danmaku", "interaction", "growth", "growth_rate", "added", "published", @@ -342,7 +343,7 @@ class DashboardQueries: def _videos( conn: sqlite3.Connection, *, - statuses: Sequence[str] = TRACKED_STATUSES, + statuses: Sequence[str] = VISIBLE_STATUSES, folder_id: int | None = None, q: str | None = None, q_fields: Sequence[str] | None = None, @@ -611,7 +612,7 @@ class DashboardQueries: if first_seen_ts is not None: initial_import_count = int(conn.execute( "SELECT COUNT(*) FROM video WHERE first_seen = ? " - "AND status IN ('active','unfaved')", + f"AND status IN ({_VISIBLE_IN})", (int(first_seen_ts),), ).fetchone()[0]) @@ -682,7 +683,7 @@ class DashboardQueries: "total": sum(by_status.values()), "active_ups": int(conn.execute( "SELECT COUNT(DISTINCT up_mid) FROM video " - "WHERE status IN ('active','unfaved') AND up_mid IS NOT NULL" + f"WHERE status IN ({_VISIBLE_IN}) AND up_mid IS NOT NULL" ).fetchone()[0]), "candidates_pending": candidate_pending, "candidates_total": candidate_total, @@ -1755,7 +1756,7 @@ class DashboardQueries: SELECT COUNT(*) AS total, SUM(CASE WHEN up_follower IS NOT NULL THEN 1 ELSE 0 END) AS covered FROM video - WHERE status IN ('active', 'unfaved') AND up_mid IS NOT NULL + WHERE status IN ('active', 'unfaved', 'removed') AND up_mid IS NOT NULL """ ).fetchone() follower_total = int(follower_row["total"] or 0) @@ -1967,7 +1968,7 @@ class DashboardQueries: """ SELECT b.*, (SELECT COUNT(*) FROM video v WHERE v.up_mid = b.mid - AND v.status IN ('active','unfaved')) AS video_count + AND v.status IN ('active','unfaved','removed')) AS video_count FROM up_blacklist b ORDER BY b.created_at, b.mid """ ).fetchall() diff --git a/reaction_board/web/static/app.js b/reaction_board/web/static/app.js index 67a17a3..50d059d 100644 --- a/reaction_board/web/static/app.js +++ b/reaction_board/web/static/app.js @@ -134,6 +134,15 @@ function toast(message, kind = "info") { }, 2600); } + +document.addEventListener("click", (event) => { + const mark = event.target.closest("[data-removed-tip]"); + if (!mark) return; + event.preventDefault(); + event.stopPropagation(); + toast("该视频已失效"); +}, true); + window.addEventListener("error", (event) => { const target = event.target; if (target instanceof HTMLImageElement) { diff --git a/reaction_board/web/static/components.js b/reaction_board/web/static/components.js index 327221c..18bcb82 100644 --- a/reaction_board/web/static/components.js +++ b/reaction_board/web/static/components.js @@ -13,6 +13,13 @@ export function cover(item, className = "thumb") { loading="lazy" referrerpolicy="no-referrer">`; } + +export function removedCoverMark(item) { + if (item?.status !== "removed") return ""; + return ``; +} + export function rankBadge(rank, className = "rank-badge") { if (!rank) return ""; const top = rank <= 3 ? ` rank-${rank}` : ""; @@ -73,6 +80,7 @@ export function videoCard(item, {
${cover(item, "video-thumb")} ${durationBadge} + ${removedCoverMark(item)}

${title}

@@ -150,6 +158,7 @@ export function mediaCard(item, { rank = null, highlight = "" } = {}) {
${cover(item, "media-thumb")} ${durationBadge} + ${removedCoverMark(item)}
${escapeHtml(item.title || "(无标题)")}