Commit 3ef2f7b09d440551f8cf14e4b061a8a12e9fa32b
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 | 25 | .count-hint { color: var(--muted); font-size: 12px; margin-bottom: 10px; } |
| 26 | 26 | .channel-tabs { display: flex; gap: 8px; margin-bottom: 14px; } |
| 27 | 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 | 31 | </style> |
| 29 | 32 | </head> |
| 30 | 33 | <body> |
| ... | ... | @@ -84,6 +87,7 @@ |
| 84 | 87 | <th data-sort="promptEn">名称(英)</th> |
| 85 | 88 | <th data-sort="fullPrompt">Prompt</th> |
| 86 | 89 | <th>视频地址</th> |
| 90 | + <th>预览</th> | |
| 87 | 91 | <th>直播间</th> |
| 88 | 92 | <th>溯源(人物×衣服×参考视频)</th> |
| 89 | 93 | <th data-sort="duration">时长</th> |
| ... | ... | @@ -99,6 +103,10 @@ |
| 99 | 103 | </div> |
| 100 | 104 | </main> |
| 101 | 105 | |
| 106 | +<dialog id="previewDialog"> | |
| 107 | + <video id="previewVideo" controls autoplay></video> | |
| 108 | +</dialog> | |
| 109 | + | |
| 102 | 110 | <script> |
| 103 | 111 | // Redirect to login whenever any /api/admin/* call comes back 401 (T6 auth |
| 104 | 112 | // gate) — a single interceptor instead of touching every fetch call site. |
| ... | ... | @@ -200,6 +208,7 @@ function render() { |
| 200 | 208 | <td class="name"><input data-field="promptEn" value="${escapeHtml(c.promptEn || "")}" /></td> |
| 201 | 209 | <td class="prompt"><textarea data-field="fullPrompt">${escapeHtml(c.fullPrompt || "")}</textarea></td> |
| 202 | 210 | <td><input data-field="videoUrl" value="${escapeHtml(c.videoUrl || "")}" placeholder="留空=保持原有播放源" /></td> |
| 211 | + <td><button class="secondary" data-preview="${c.id}">▶ 预览</button></td> | |
| 203 | 212 | <td> |
| 204 | 213 | <select data-field="channel"> |
| 205 | 214 | <option value="video" ${c.channel === "dance" ? "" : "selected"}>视频直播间</option> |
| ... | ... | @@ -224,6 +233,7 @@ function render() { |
| 224 | 233 | <td class="name">${escapeHtml(c.promptEn || "")}</td> |
| 225 | 234 | <td class="prompt" title="${escapeHtml(c.fullPrompt || "")}">${escapeHtml((c.fullPrompt || "").slice(0, 80))}${(c.fullPrompt || "").length > 80 ? "…" : ""}</td> |
| 226 | 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 | 237 | <td>${c.channel === "dance" ? "跳舞直播间" : "视频直播间"}</td> |
| 228 | 238 | <td>${escapeHtml(traceability(c))}</td> |
| 229 | 239 | <td>${c.duration ?? "-"}s</td> |
| ... | ... | @@ -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 | 276 | tbody.querySelectorAll("[data-del]").forEach((b) => |
| 261 | 277 | b.addEventListener("click", async () => { |
| 262 | 278 | if (!confirm("确认删除这条片段?(如果来自 H3 Console 会同步从那边删除)")) return; |
| ... | ... | @@ -277,6 +293,11 @@ document.querySelectorAll("th[data-sort]").forEach((th) => { |
| 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 | 301 | document.getElementById("searchInput").addEventListener("input", render); |
| 281 | 302 | document.getElementById("sourceFilter").addEventListener("change", render); |
| 282 | 303 | ... | ... |