dance-rooms.html
3.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>换装跳舞直播间列表</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Shrikhand&display=swap" />
<link rel="stylesheet" href="live.css" />
<link rel="stylesheet" href="dance.css" />
<style>
.rooms-wrap { max-width: 1080px; margin: 0 auto; padding: 18px 14px 60px; }
/* 一行 3 个,固定列数(不是 auto-fill)——行数随房间数量自然增长 */
.rooms-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; margin-top: 16px; }
.room-card { display: block; text-decoration: none; }
.room-card .media { position: relative; width: 100%; height: 280px; border-radius: 8px; overflow: hidden; background: #000; }
.room-card .media img, .room-card .media video { width: 100%; height: 100%; object-fit: cover; display: block; }
.room-card .name { font: bold 15px Tahoma, sans-serif; margin-top: 8px; }
.room-card .watching { position: static; display: inline-flex; margin-top: 4px; font-size: 11px; padding: 2px 8px; }
.rooms-empty { text-align: center; color: var(--muted); margin-top: 60px; font-size: 13px; font-weight: bold; }
@media (max-width: 700px) { .rooms-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 440px) { .rooms-grid { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<div class="channel">
<header class="topbar">
<h1 class="logo">换装跳舞直播间</h1>
<div class="spacer"></div>
<a class="topbar-pill" href="live.html">AI 短剧频道 →</a>
</header>
<div class="rooms-wrap">
<div id="roomsGrid" class="rooms-grid"></div>
<div id="roomsEmpty" class="rooms-empty" hidden>还没有开播的人物,去后台生成几条视频吧</div>
</div>
</div>
<script>
function escapeHtml(s) {
const div = document.createElement("div");
div.textContent = s || "";
return div.innerHTML;
}
// Lazy-load each card's preview video only once it's actually scrolled into
// view — same reasoning as admin.html's library preview: loading every
// room's video at once would hammer /lib/video (a fresh H3 Console fetch
// per clip) for no benefit off-screen.
const previewObserver = new IntersectionObserver(
(entries) => {
for (const e of entries) {
if (!e.isIntersecting) continue;
const v = e.target;
v.src = v.dataset.src;
v.play().catch(() => {});
previewObserver.unobserve(v);
}
},
{ rootMargin: "200px" }
);
let renderedIds = null;
async function loadRooms() {
const rooms = await fetch("/api/dance/rooms").then((r) => r.json());
document.getElementById("roomsEmpty").hidden = rooms.length > 0;
const ids = rooms.map((r) => r.id).join(",");
if (ids === renderedIds) {
// same set of rooms already on screen — just refresh the viewer counts
// in place, don't touch/reload the <video> elements
rooms.forEach((r) => {
const el = document.querySelector(`[data-watching="${r.id}"]`);
if (el) el.textContent = `👀 ${r.watching}`;
});
return;
}
renderedIds = ids;
const grid = document.getElementById("roomsGrid");
grid.innerHTML = rooms
.map(
(r) => `
<a class="room-card wardrobe-card" href="dance.html?characterId=${r.id}">
<div class="media">
${
r.previewVideoId
? `<video muted loop playsinline preload="none" poster="${escapeHtml(r.imageUrl)}" data-src="/lib/video/${r.previewVideoId}"></video>`
: `<img src="${escapeHtml(r.imageUrl)}" />`
}
</div>
<div class="name">${escapeHtml(r.name)}</div>
<div class="watching" data-watching="${r.id}">👀 ${r.watching}</div>
</a>`
)
.join("");
grid.querySelectorAll("video[data-src]").forEach((v) => previewObserver.observe(v));
}
loadRooms();
setInterval(loadRooms, 5000);
</script>
</body>
</html>