${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 || "(无标题)")}
+
+ ${escapeHtml(item.up_name || "未知 UP")}
+ ${escapeHtml(formatDate(item.pubdate))}
+
${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)}
-