Commit 3ef2f7b09d440551f8cf14e4b061a8a12e9fa32b

Authored by yibo
1 parent 79a2a1e9

Add per-clip video preview modal to admin.html

Browsing a clip previously meant opening /lib/video/:id in a new tab
via the raw address link. Add a 预览 button per row that opens a
native <dialog> with an inline <video> player instead — closes on
Esc, backdrop click, or its own close button, and clears the src on
close so playback/buffering stops.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
Showing 1 changed file with 21 additions and 0 deletions
admin.html
@@ -25,6 +25,9 @@ @@ -25,6 +25,9 @@
25 .count-hint { color: var(--muted); font-size: 12px; margin-bottom: 10px; } 25 .count-hint { color: var(--muted); font-size: 12px; margin-bottom: 10px; }
26 .channel-tabs { display: flex; gap: 8px; margin-bottom: 14px; } 26 .channel-tabs { display: flex; gap: 8px; margin-bottom: 14px; }
27 .channel-tabs button.active { background: var(--accent); color: #fff; border-color: var(--accent); } 27 .channel-tabs button.active { background: var(--accent); color: #fff; border-color: var(--accent); }
  28 + #previewDialog { padding: 0; border: none; border-radius: var(--radius); background: #000; }
  29 + #previewDialog::backdrop { background: rgba(0,0,0,.6); }
  30 + #previewDialog video { display: block; max-width: 80vw; max-height: 80vh; }
28 </style> 31 </style>
29 </head> 32 </head>
30 <body> 33 <body>
@@ -84,6 +87,7 @@ @@ -84,6 +87,7 @@
84 <th data-sort="promptEn">名称(英)</th> 87 <th data-sort="promptEn">名称(英)</th>
85 <th data-sort="fullPrompt">Prompt</th> 88 <th data-sort="fullPrompt">Prompt</th>
86 <th>视频地址</th> 89 <th>视频地址</th>
  90 + <th>预览</th>
87 <th>直播间</th> 91 <th>直播间</th>
88 <th>溯源(人物×衣服×参考视频)</th> 92 <th>溯源(人物×衣服×参考视频)</th>
89 <th data-sort="duration">时长</th> 93 <th data-sort="duration">时长</th>
@@ -99,6 +103,10 @@ @@ -99,6 +103,10 @@
99 </div> 103 </div>
100 </main> 104 </main>
101 105
  106 +<dialog id="previewDialog">
  107 + <video id="previewVideo" controls autoplay></video>
  108 +</dialog>
  109 +
102 <script> 110 <script>
103 // Redirect to login whenever any /api/admin/* call comes back 401 (T6 auth 111 // Redirect to login whenever any /api/admin/* call comes back 401 (T6 auth
104 // gate) — a single interceptor instead of touching every fetch call site. 112 // gate) — a single interceptor instead of touching every fetch call site.
@@ -200,6 +208,7 @@ function render() { @@ -200,6 +208,7 @@ function render() {
200 <td class="name"><input data-field="promptEn" value="${escapeHtml(c.promptEn || "")}" /></td> 208 <td class="name"><input data-field="promptEn" value="${escapeHtml(c.promptEn || "")}" /></td>
201 <td class="prompt"><textarea data-field="fullPrompt">${escapeHtml(c.fullPrompt || "")}</textarea></td> 209 <td class="prompt"><textarea data-field="fullPrompt">${escapeHtml(c.fullPrompt || "")}</textarea></td>
202 <td><input data-field="videoUrl" value="${escapeHtml(c.videoUrl || "")}" placeholder="留空=保持原有播放源" /></td> 210 <td><input data-field="videoUrl" value="${escapeHtml(c.videoUrl || "")}" placeholder="留空=保持原有播放源" /></td>
  211 + <td><button class="secondary" data-preview="${c.id}">▶ 预览</button></td>
203 <td> 212 <td>
204 <select data-field="channel"> 213 <select data-field="channel">
205 <option value="video" ${c.channel === "dance" ? "" : "selected"}>视频直播间</option> 214 <option value="video" ${c.channel === "dance" ? "" : "selected"}>视频直播间</option>
@@ -224,6 +233,7 @@ function render() { @@ -224,6 +233,7 @@ function render() {
224 <td class="name">${escapeHtml(c.promptEn || "")}</td> 233 <td class="name">${escapeHtml(c.promptEn || "")}</td>
225 <td class="prompt" title="${escapeHtml(c.fullPrompt || "")}">${escapeHtml((c.fullPrompt || "").slice(0, 80))}${(c.fullPrompt || "").length > 80 ? "…" : ""}</td> 234 <td class="prompt" title="${escapeHtml(c.fullPrompt || "")}">${escapeHtml((c.fullPrompt || "").slice(0, 80))}${(c.fullPrompt || "").length > 80 ? "…" : ""}</td>
226 <td><a class="video-link" href="${videoHref(c)}" target="_blank">${videoHref(c)}</a></td> 235 <td><a class="video-link" href="${videoHref(c)}" target="_blank">${videoHref(c)}</a></td>
  236 + <td><button class="secondary" data-preview="${c.id}">▶ 预览</button></td>
227 <td>${c.channel === "dance" ? "跳舞直播间" : "视频直播间"}</td> 237 <td>${c.channel === "dance" ? "跳舞直播间" : "视频直播间"}</td>
228 <td>${escapeHtml(traceability(c))}</td> 238 <td>${escapeHtml(traceability(c))}</td>
229 <td>${c.duration ?? "-"}s</td> 239 <td>${c.duration ?? "-"}s</td>
@@ -257,6 +267,12 @@ function render() { @@ -257,6 +267,12 @@ function render() {
257 } 267 }
258 }) 268 })
259 ); 269 );
  270 + tbody.querySelectorAll("[data-preview]").forEach((b) =>
  271 + b.addEventListener("click", () => {
  272 + previewVideo.src = videoHref(rows.find((c) => c.id === b.dataset.preview));
  273 + previewDialog.showModal();
  274 + })
  275 + );
260 tbody.querySelectorAll("[data-del]").forEach((b) => 276 tbody.querySelectorAll("[data-del]").forEach((b) =>
261 b.addEventListener("click", async () => { 277 b.addEventListener("click", async () => {
262 if (!confirm("确认删除这条片段?(如果来自 H3 Console 会同步从那边删除)")) return; 278 if (!confirm("确认删除这条片段?(如果来自 H3 Console 会同步从那边删除)")) return;
@@ -277,6 +293,11 @@ document.querySelectorAll(&quot;th[data-sort]&quot;).forEach((th) =&gt; { @@ -277,6 +293,11 @@ document.querySelectorAll(&quot;th[data-sort]&quot;).forEach((th) =&gt; {
277 }); 293 });
278 }); 294 });
279 295
  296 +const previewDialog = document.getElementById("previewDialog");
  297 +const previewVideo = document.getElementById("previewVideo");
  298 +previewDialog.addEventListener("click", (e) => { if (e.target === previewDialog) previewDialog.close(); });
  299 +previewDialog.addEventListener("close", () => { previewVideo.pause(); previewVideo.src = ""; });
  300 +
280 document.getElementById("searchInput").addEventListener("input", render); 301 document.getElementById("searchInput").addEventListener("input", render);
281 document.getElementById("sourceFilter").addEventListener("change", render); 302 document.getElementById("sourceFilter").addEventListener("change", render);
282 303