From 374b13804120915c61408e5df015b28ab9d536eb Mon Sep 17 00:00:00 2001 From: kaireewu Date: Mon, 2 Nov 2026 12:19:30 +0800 Subject: [PATCH] feat(ui): densify home and ranking video cards Drop nested home panels, overlay rank and duration on covers, and keep metrics fully visible by moving dates onto the author row. --- reaction_board/web/static/components.js | 48 +++++-- reaction_board/web/static/style.css | 179 ++++++++++++++++-------- reaction_board/web/static/views/home.js | 24 ++-- 3 files changed, 165 insertions(+), 86 deletions(-) diff --git a/reaction_board/web/static/components.js b/reaction_board/web/static/components.js index 2112530..311eac0 100644 --- a/reaction_board/web/static/components.js +++ b/reaction_board/web/static/components.js @@ -13,10 +13,10 @@ export function cover(item, className = "thumb") { loading="lazy" referrerpolicy="no-referrer">`; } -export function rankBadge(rank) { +export function rankBadge(rank, className = "rank-badge") { if (!rank) return ""; const top = rank <= 3 ? ` rank-${rank}` : ""; - return `${rank}`; + return `${rank}`; } export function avatar(item, className = "avatar") { @@ -51,19 +51,25 @@ export function videoCard(item, { const upLink = upMid ? `${up}` : up; const status = showStatus && item.status ? `${escapeHtml(statusLabel(item.status))}` : ""; + const duration = formatDuration(item.duration); + const durationBadge = duration === "—" + ? "" + : `${escapeHtml(duration)}`; return ` - + ${rankBadge(rank)} - ${cover(item, "video-thumb")} +
+ ${cover(item, "video-thumb")} + ${durationBadge} +

${title}

-
- ${upLink} - ${escapeHtml(formatDate(item.pubdate))} - ${escapeHtml(formatDuration(item.duration))} - ${status} +
+ ${status} ${escapeHtml(primaryValue ?? "—")}${escapeHtml(label)} @@ -88,7 +94,9 @@ export function upCard(item, { return ` ${rankBadge(rank)} - ${avatar(item, "up-avatar")} +
+ ${avatar(item, "up-avatar")} +

${escapeHtml(item.up_name || `UP ${item.up_mid}`)}

@@ -120,12 +128,22 @@ export function metricGrid(item) {
`; } -export function mediaCard(item, { rank = null, meta = "", highlight = "" } = {}) { +export function mediaCard(item, { rank = null, highlight = "" } = {}) { + const duration = formatDuration(item.duration); + const durationBadge = duration === "—" + ? "" + : `${escapeHtml(duration)}`; return `
- ${rank === null || rank === undefined ? "" : `${escapeHtml(String(rank))}`} - ${cover(item, "media-thumb")} - ${escapeHtml(item.title || "(无标题)")} - ${meta ? `${meta}` : ""} + ${rankBadge(rank)} +
+ ${cover(item, "media-thumb")} + ${durationBadge} +
+ ${escapeHtml(item.title || "(无标题)")} + ${highlight ? `${highlight}` : ""}
`; } diff --git a/reaction_board/web/static/style.css b/reaction_board/web/static/style.css index 7c3bffc..a9c9c8b 100644 --- a/reaction_board/web/static/style.css +++ b/reaction_board/web/static/style.css @@ -297,6 +297,7 @@ h1, h2, h3, strong, b { font-weight: 600; } margin-bottom: 8px; } .section-head h2 { font-size: 16px; font-weight: 600; } +.home-block { min-width: 0; } .section-note { color: var(--muted); font-size: 12px; margin: 0 0 12px; } .warning-text { color: var(--warning); } .danger { color: var(--danger); } @@ -343,8 +344,7 @@ a.live-chip:hover { border-color: color-mix(in srgb, var(--accent) 55%, var(--bo .video-card, .up-card { position: relative; display: grid; - grid-template-columns: 24px 96px minmax(0, 1fr); - align-items: start; + align-items: stretch; gap: 12px; padding: 10px; border: 1px solid var(--border); @@ -353,50 +353,75 @@ a.live-chip:hover { border-color: color-mix(in srgb, var(--accent) 55%, var(--bo transition: border-color 0.15s cubic-bezier(0.22, 1, 0.36, 1), background-color 0.15s cubic-bezier(0.22, 1, 0.36, 1); } +.video-card { grid-template-columns: 128px minmax(0, 1fr); } +.up-card { grid-template-columns: 64px minmax(0, 1fr); align-items: center; } .video-card:hover, .up-card:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); background: color-mix(in srgb, var(--accent) 5%, var(--surface)); } -.video-card.no-rank, .up-card.no-rank { grid-template-columns: 96px minmax(0, 1fr); } -.video-card.no-rank .video-thumb, .up-card.no-rank .video-thumb { grid-column: 1; } -.video-card.no-rank .video-card-body, .up-card.no-rank .up-card-body { grid-column: 2; } +.video-cover, .media-cover, .up-cover { position: relative; overflow: hidden; } +.video-cover, .media-cover { border-radius: 6px; } +.up-cover { width: 64px; height: 64px; } .rank-badge { - grid-column: 1; - grid-row: 1; - align-self: start; + position: absolute; + z-index: 1; + top: 0; + left: 0; display: grid; place-items: center; - width: 24px; - height: 24px; - border-radius: 6px; - background: transparent; - color: var(--muted); + min-width: 28px; + height: 22px; + padding: 0 7px; + border-radius: inherit; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + background: rgba(0, 0, 0, 0.54); + color: #fff; font-size: 13px; font-weight: 600; + line-height: 1; + pointer-events: none; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55); +} +.rank-badge.rank-1 { + background: color-mix(in srgb, var(--accent) 70%, transparent); + color: #fff; +} +.cover-duration { + position: absolute; + z-index: 1; + left: 0; + right: 0; + bottom: 0; + display: flex; + justify-content: flex-end; + align-items: center; + padding: 4px 6px; + background: rgba(0, 0, 0, 0.4); + color: #fff; + font-size: 11px; + font-weight: 400; + line-height: 1; + pointer-events: none; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55); } -.rank-badge.rank-1 { background: var(--accent); color: var(--accent-contrast); } -.rank-badge.rank-2, .rank-badge.rank-3 { background: var(--surface-2); color: var(--text); } .video-thumb, .hero-cover, .media-thumb, .candidate-thumb { position: relative; overflow: hidden; border-radius: 6px; background: var(--surface-2); } -.video-thumb { - grid-column: 2; - grid-row: 1; - width: 96px; - aspect-ratio: 16 / 9; -} +.video-thumb { width: 100%; aspect-ratio: 16 / 9; } .video-thumb img, .hero-cover img, .media-thumb img, .candidate-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; } .cover-placeholder { display: grid; place-items: center; color: var(--muted); font-size: 11px; } .cover-placeholder img.image-error { opacity: 0; } -.video-card-body, .up-card-body { grid-column: 3; grid-row: 1; min-width: 0; } +.video-card-body, .up-card-body { + display: flex; + flex-direction: column; + min-width: 0; +} .up-avatar { - grid-column: 2; - grid-row: 1; - justify-self: center; - align-self: center; width: 64px; height: 64px; border-radius: 50%; @@ -409,7 +434,6 @@ a.live-chip:hover { border-color: color-mix(in srgb, var(--accent) 55%, var(--bo font-weight: 600; } .up-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; } -.up-card.no-rank > .up-avatar { grid-column: 1; } .avatar-placeholder { text-transform: uppercase; } .video-title { display: -webkit-box; @@ -420,8 +444,26 @@ a.live-chip:hover { border-color: color-mix(in srgb, var(--accent) 55%, var(--bo line-height: 1.35; font-weight: 600; } -/* 标题固定占两行高度(line-height 1.35 × 2),让单行与两行标题的行高一致。 */ -.video-card .video-title, .media-title { min-height: 2.7em; } +/* 标题固定占两行高度,让单行与两行标题的卡片高度一致。 */ +.video-card .video-title { min-height: 2.7em; } +.media-title { min-height: 2.8em; } +.video-byline { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 8px; + margin-top: 4px; + min-width: 0; +} +.video-up { + min-width: 0; + flex: 1 1 auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--muted); + font-size: 12px; +} .video-meta { display: flex; flex-wrap: wrap; @@ -434,12 +476,24 @@ a.live-chip:hover { border-color: color-mix(in srgb, var(--accent) 55%, var(--bo display: flex; flex-wrap: wrap; align-items: baseline; - gap: 4px 10px; + gap: 4px 8px; margin-top: 6px; color: var(--muted); font-size: 11px; + min-width: 0; } -.primary-stat { display: inline-flex; align-items: baseline; gap: 4px; color: var(--text); } +.video-card .video-stats { + flex-wrap: nowrap; + margin-top: auto; + padding-top: 8px; +} +.video-date { + flex: 0 0 auto; + color: var(--muted); + font-size: 12px; + white-space: nowrap; +} +.primary-stat { display: inline-flex; flex: 0 0 auto; align-items: baseline; gap: 4px; color: var(--text); white-space: nowrap; } .primary-stat b { font-size: 15px; font-weight: 600; } .primary-stat small { color: var(--muted); font-size: 11px; } .secondary-stat { white-space: nowrap; } @@ -451,12 +505,13 @@ a.live-chip:hover { border-color: color-mix(in srgb, var(--accent) 55%, var(--bo /* 统一的响应式媒体网格:移动端 2 列(图片自动缩放),PC 端 3 列。 */ .media-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; } @media (min-width: 900px) { .media-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } } -/* 统一的竖排媒体卡:图 + 两行截断标题 + 作者/数据行 + 高亮数据行。 */ +/* 竖排媒体卡:封面(时长)+ 两行标题 + 单行 UP + 日期/指标底栏。 */ .media-card { position: relative; display: flex; flex-direction: column; - gap: 6px; + height: 100%; + gap: 8px; padding: 10px; border: 1px solid var(--border); border-radius: 16px; @@ -468,33 +523,45 @@ a.live-chip:hover { border-color: color-mix(in srgb, var(--accent) 55%, var(--bo border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); background: color-mix(in srgb, var(--accent) 5%, var(--surface)); } -.media-rank { - position: absolute; - top: 10px; - left: 10px; - z-index: 1; - display: grid; - place-items: center; - width: 24px; - height: 24px; - border-radius: 6px; - background: rgba(0, 0, 0, 0.62); - color: #fff; - font-size: 11px; - font-weight: 600; -} .media-thumb { width: 100%; aspect-ratio: 16 / 9; } .media-title { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; - font-size: 13px; - line-height: 1.35; + font-size: 12px; + line-height: 1.4; + font-weight: 400; +} +.media-byline { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 8px; + min-width: 0; +} +.media-up { + min-width: 0; + flex: 1 1 auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--muted); + font-size: 11px; +} +.media-date { + flex: 0 0 auto; + color: var(--muted); + font-size: 11px; + white-space: nowrap; +} +.media-highlight { + margin-top: auto; + color: var(--accent-strong); + font-size: 11px; font-weight: 600; + white-space: nowrap; } -.media-meta { color: var(--muted); font-size: 11px; } -.media-highlight { color: var(--accent-strong); font-size: 11px; font-weight: 600; } .status-grid { display: grid; @@ -928,9 +995,7 @@ a.live-chip:hover { border-color: color-mix(in srgb, var(--accent) 55%, var(--bo .up-stats { grid-template-columns: repeat(5, minmax(0, 1fr)); } .detail-hero { flex-direction: row; align-items: flex-start; } .hero-cover { width: 300px; flex: 0 0 300px; } - .video-thumb { width: 112px; } - .video-card, .up-card { grid-template-columns: 24px 112px minmax(0, 1fr); } - .video-card.no-rank, .up-card.no-rank { grid-template-columns: 112px minmax(0, 1fr); } + .video-card { grid-template-columns: 144px minmax(0, 1fr); } .secondary-stat.optional { display: inline; } } @@ -983,9 +1048,7 @@ a.live-chip:hover { border-color: color-mix(in srgb, var(--accent) 55%, var(--bo } @media (max-width: 380px) { - .video-card, .up-card { grid-template-columns: 20px 80px minmax(0, 1fr); gap: 8px; } - .video-card.no-rank, .up-card.no-rank { grid-template-columns: 80px minmax(0, 1fr); } - .video-thumb { width: 80px; } + .video-card { grid-template-columns: 104px minmax(0, 1fr); gap: 8px; } .metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .candidate-card { grid-template-columns: 80px minmax(0, 1fr); } .candidate-thumb { width: 80px; } diff --git a/reaction_board/web/static/views/home.js b/reaction_board/web/static/views/home.js index 05acf2a..8753661 100644 --- a/reaction_board/web/static/views/home.js +++ b/reaction_board/web/static/views/home.js @@ -1,8 +1,8 @@ import { api } from "../api.js"; import { emptyState, mediaCard } from "../components.js"; import { - escapeAttr, escapeHtml, formatCount, formatDate, formatDelta, - formatDuration, formatPercent, formatScore, + escapeAttr, escapeHtml, formatCount, formatDelta, + formatPercent, formatScore, } from "../format.js"; import { canAccessProtected } from "../session.js"; @@ -19,7 +19,6 @@ function heatGrid(items) { return `
${items.map((item, index) => mediaCard(item, { rank: index + 1, - meta: `${escapeHtml(item.up_name || "未知 UP")} · ${formatCount(item.metrics?.view)} 播放`, highlight: `热度 ${formatScore(item.heat)} · 前 ${Math.max(0, 100 - (item.heat_percentile || 0)).toFixed(0)}%`, })).join("")}
`; @@ -27,19 +26,18 @@ function heatGrid(items) { function publishedCard(item) { return mediaCard(item, { - meta: `${escapeHtml(item.up_name || "未知 UP")} · ${escapeHtml(formatDate(item.pubdate))} · ${escapeHtml(formatDuration(item.duration))}`, highlight: `${formatCount(item.metrics?.view)} 播放 · 互动 ${formatPercent(item.interaction_rate)}`, }); } function growthCard(item, index) { const delta = item.growth?.delta; - const deltaText = delta === null || delta === undefined ? "" : ` · ${formatDelta(delta)}`; - return mediaCard(item, { - rank: index + 1, - meta: `${escapeHtml(item.up_name || "未知 UP")} · ${escapeHtml(formatDate(item.pubdate))} · ${escapeHtml(formatDuration(item.duration))}`, - highlight: `${formatCount(item.metrics?.view)} 播放${deltaText}`, - }); + let highlight = `${formatCount(item.metrics?.view)} 播放`; + if (delta !== null && delta !== undefined) { + const tone = Number(delta) >= 0 ? "up" : "down"; + highlight += ` ${formatDelta(delta)}`; + } + return mediaCard(item, { rank: index + 1, highlight }); } export async function renderHome(root) { @@ -96,7 +94,7 @@ export async function renderHome(root) { 请在 Telegram 中处理 → ` : ""} -
+

最近发布

查看全部 @@ -108,7 +106,7 @@ export async function renderHome(root) {
-
+

增长动态 · ${escapeHtml(growthLabel)}

增长榜 @@ -118,7 +116,7 @@ export async function renderHome(root) { : emptyState(growthEmptyTitle, growthEmptyHint)}
-
+

热度速览

完整榜单