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,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("th[data-sort]").forEach((th) => { | @@ -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 | 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 |