dance-rooms.html 4.28 KB
<!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>
  /* 不设 max-width 上限——大屏上让 4 列真正铺满整个窗口宽度,而不是
     卡在一个固定宽度中间留白;gap/padding 用 vw 让间距也跟着屏幕缩放。 */
  .rooms-wrap { margin: 0 auto; padding: 24px 3vw 60px; }
  /* 一行 4 个,固定列数(不是 auto-fill)——行数随房间数量自然增长 */
  .rooms-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1.6vw; margin-top: 16px; }
  .room-card { display: block; text-decoration: none; }
  /* 用 aspect-ratio 代替固定像素高度,卡片跟着列宽等比例缩放,大屏铺满
     后图片不会被拉伸/裁得比小屏更奇怪。 */
  .room-card .media { position: relative; width: 100%; aspect-ratio: 4 / 5; 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>