feat(web): include removed in visible stats; cover bang badge
站内展示/统计统一 VISIBLE(含 removed):热度、UP 列表、总览 active_ups/初导入计数、说明页粉丝覆盖、黑名单视频数。采集队列仍 只 active/unfaved。失效片封面右上角感叹号徽标,点击/tap 提示 「该视频已失效」;手动/候选入库仍可复活 removed→active。
This commit is contained in:
1 parent
6c73904b25
commit
0c660c6474
7 files changed
+122
-17
No files matched your search
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -13,6 +13,13 @@ export function cover(item, className = "thumb") {
|
||||
loading="lazy" referrerpolicy="no-referrer"></div>`;
|
||||
}
|
||||
|
||||
|
||||
export function removedCoverMark(item) {
|
||||
if (item?.status !== "removed") return "";
|
||||
return `<button type="button" class="cover-removed-mark" data-removed-tip
|
||||
title="该视频已失效" aria-label="该视频已失效">!</button>`;
|
||||
}
|
||||
|
||||
export function rankBadge(rank, className = "rank-badge") {
|
||||
if (!rank) return "";
|
||||
const top = rank <= 3 ? ` rank-${rank}` : "";
|
||||
@@ -73,6 +80,7 @@ export function videoCard(item, {
|
||||
<div class="video-cover">
|
||||
${cover(item, "video-thumb")}
|
||||
${durationBadge}
|
||||
${removedCoverMark(item)}
|
||||
</div>
|
||||
<div class="video-card-body">
|
||||
<h3 class="video-title">${title}</h3>
|
||||
@@ -150,6 +158,7 @@ export function mediaCard(item, { rank = null, highlight = "" } = {}) {
|
||||
<div class="media-cover">
|
||||
${cover(item, "media-thumb")}
|
||||
${durationBadge}
|
||||
${removedCoverMark(item)}
|
||||
</div>
|
||||
<span class="media-title">${escapeHtml(item.title || "(无标题)")}</span>
|
||||
<span class="media-byline">
|
||||
|
||||
@@ -388,6 +388,32 @@ a.live-chip:hover { border-color: color-mix(in srgb, var(--accent) 55%, var(--bo
|
||||
background: color-mix(in srgb, var(--accent) 70%, transparent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.detail-cover-wrap { position: relative; }
|
||||
.cover-removed-mark {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--warning) 82%, #1a1208);
|
||||
color: #fff8f0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
cursor: help;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
.cover-removed-mark:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.cover-duration {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { api } from "../api.js";
|
||||
import {
|
||||
badge, centerActiveChips, cover, emptyState, metricChips, metricGrid,
|
||||
removedCoverMark,
|
||||
sectionHeader, videoCard,
|
||||
} from "../components.js";
|
||||
import { lineChart, themeColor } from "../charts.js";
|
||||
@@ -53,7 +54,10 @@ export async function renderVideo(root, { params, query }) {
|
||||
</header>
|
||||
|
||||
<div class="detail-hero">
|
||||
${cover(detail, "hero-cover")}
|
||||
<div class="detail-cover-wrap">
|
||||
${cover(detail, "hero-cover")}
|
||||
${removedCoverMark(detail)}
|
||||
</div>
|
||||
<div class="detail-hero-body">
|
||||
<h1>${escapeHtml(detail.title)}</h1>
|
||||
<div class="detail-meta">
|
||||
|
||||
Reference in new issue
Block a user