Commit 79a2a1e90d67cf178d44c735548a95bac8d0ce24
1 parent
e96d6894
Split admin.html's video library into per-channel management
live.html and dance.html already have independent scheduling pools (architecture.md 2.1), distinguished by clips' channel field, but admin.html rendered everything as one flat table with no way to tell the two apart or scope edits to one room. Add a 视频直播间/跳舞直播间 tab switcher that filters the table by channel, lets manual adds pick which room they land in, and lets existing rows be reclassified via the edit form's new channel select (backed by server.js's existing channel support on POST/PATCH /api/admin/library, committed in e96d689 as part of an unrelated concurrent session's work). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
Showing
1 changed file
with
47 additions
and
3 deletions
admin.html
| ... | ... | @@ -23,6 +23,8 @@ |
| 23 | 23 | .row-4 { display: grid; grid-template-columns: 1fr 1fr 1.6fr 100px; gap: 14px; } |
| 24 | 24 | .sort-arrow { font-size: 10px; } |
| 25 | 25 | .count-hint { color: var(--muted); font-size: 12px; margin-bottom: 10px; } |
| 26 | + .channel-tabs { display: flex; gap: 8px; margin-bottom: 14px; } | |
| 27 | + .channel-tabs button.active { background: var(--accent); color: #fff; border-color: var(--accent); } | |
| 26 | 28 | </style> |
| 27 | 29 | </head> |
| 28 | 30 | <body> |
| ... | ... | @@ -37,6 +39,11 @@ |
| 37 | 39 | </header> |
| 38 | 40 | |
| 39 | 41 | <main class="admin-wrap"> |
| 42 | + <div class="channel-tabs"> | |
| 43 | + <button id="tabVideo" class="secondary">视频直播间</button> | |
| 44 | + <button id="tabDance" class="secondary">跳舞直播间</button> | |
| 45 | + </div> | |
| 46 | + | |
| 40 | 47 | <div class="card add-card"> |
| 41 | 48 | <div class="field"><label>新增片段(手动指定视频地址)</label></div> |
| 42 | 49 | <div class="row-4"> |
| ... | ... | @@ -46,6 +53,13 @@ |
| 46 | 53 | <input type="number" id="newDuration" placeholder="时长(秒)" /> |
| 47 | 54 | </div> |
| 48 | 55 | <div class="field" style="margin-top:8px;"> |
| 56 | + <label>归属直播间</label> | |
| 57 | + <select id="newChannel"> | |
| 58 | + <option value="video">视频直播间</option> | |
| 59 | + <option value="dance">跳舞直播间</option> | |
| 60 | + </select> | |
| 61 | + </div> | |
| 62 | + <div class="field" style="margin-top:8px;"> | |
| 49 | 63 | <textarea id="newFullPrompt" placeholder="完整 prompt(可选,留空则用名称)" style="min-height:60px;"></textarea> |
| 50 | 64 | </div> |
| 51 | 65 | <button id="addBtn" style="margin-top:8px;">新增</button> |
| ... | ... | @@ -70,6 +84,7 @@ |
| 70 | 84 | <th data-sort="promptEn">名称(英)</th> |
| 71 | 85 | <th data-sort="fullPrompt">Prompt</th> |
| 72 | 86 | <th>视频地址</th> |
| 87 | + <th>直播间</th> | |
| 73 | 88 | <th>溯源(人物×衣服×参考视频)</th> |
| 74 | 89 | <th data-sort="duration">时长</th> |
| 75 | 90 | <th data-sort="likes">点赞</th> |
| ... | ... | @@ -103,6 +118,13 @@ let danceRefs = []; |
| 103 | 118 | let editingId = null; |
| 104 | 119 | let sortKey = "createdAt"; |
| 105 | 120 | let sortDir = -1; |
| 121 | +// 视频直播间(live.html) / 跳舞直播间(dance.html) 是两条独立的产品线, | |
| 122 | +// 素材各自的排播池互不共享(architecture.md 2.1),所以后台也分开管理。 | |
| 123 | +let activeChannel = "video"; | |
| 124 | + | |
| 125 | +function inChannel(c) { | |
| 126 | + return activeChannel === "dance" ? c.channel === "dance" : c.channel !== "dance"; | |
| 127 | +} | |
| 106 | 128 | |
| 107 | 129 | function escapeHtml(s) { |
| 108 | 130 | const div = document.createElement("div"); |
| ... | ... | @@ -142,7 +164,7 @@ function traceability(c) { |
| 142 | 164 | function filteredSorted() { |
| 143 | 165 | const q = document.getElementById("searchInput").value.trim().toLowerCase(); |
| 144 | 166 | const source = document.getElementById("sourceFilter").value; |
| 145 | - let rows = library.slice(); | |
| 167 | + let rows = library.filter(inChannel); | |
| 146 | 168 | if (q) rows = rows.filter((c) => c.prompt.toLowerCase().includes(q) || (c.promptEn || "").toLowerCase().includes(q) || (c.fullPrompt || "").toLowerCase().includes(q) || c.id.toLowerCase().includes(q)); |
| 147 | 169 | if (source) rows = rows.filter((c) => c.source === source); |
| 148 | 170 | rows.sort((a, b) => { |
| ... | ... | @@ -160,7 +182,10 @@ function videoHref(c) { |
| 160 | 182 | |
| 161 | 183 | function render() { |
| 162 | 184 | const rows = filteredSorted(); |
| 163 | - document.getElementById("countHint").textContent = `共 ${library.length} 条,当前筛选 ${rows.length} 条`; | |
| 185 | + const channelTotal = library.filter(inChannel).length; | |
| 186 | + document.getElementById("countHint").textContent = `本直播间共 ${channelTotal} 条,当前筛选 ${rows.length} 条`; | |
| 187 | + document.getElementById("tabVideo").classList.toggle("active", activeChannel === "video"); | |
| 188 | + document.getElementById("tabDance").classList.toggle("active", activeChannel === "dance"); | |
| 164 | 189 | document.querySelectorAll("th[data-sort]").forEach((th) => { |
| 165 | 190 | th.innerHTML = th.textContent.replace(/ ?[▲▼]$/, "") + (th.dataset.sort === sortKey ? ` <span class="sort-arrow">${sortDir === 1 ? "▲" : "▼"}</span>` : ""); |
| 166 | 191 | }); |
| ... | ... | @@ -175,6 +200,12 @@ function render() { |
| 175 | 200 | <td class="name"><input data-field="promptEn" value="${escapeHtml(c.promptEn || "")}" /></td> |
| 176 | 201 | <td class="prompt"><textarea data-field="fullPrompt">${escapeHtml(c.fullPrompt || "")}</textarea></td> |
| 177 | 202 | <td><input data-field="videoUrl" value="${escapeHtml(c.videoUrl || "")}" placeholder="留空=保持原有播放源" /></td> |
| 203 | + <td> | |
| 204 | + <select data-field="channel"> | |
| 205 | + <option value="video" ${c.channel === "dance" ? "" : "selected"}>视频直播间</option> | |
| 206 | + <option value="dance" ${c.channel === "dance" ? "selected" : ""}>跳舞直播间</option> | |
| 207 | + </select> | |
| 208 | + </td> | |
| 178 | 209 | <td>${escapeHtml(traceability(c))}</td> |
| 179 | 210 | <td>${c.duration ?? "-"}s</td> |
| 180 | 211 | <td>${c.likes || 0}</td> |
| ... | ... | @@ -193,6 +224,7 @@ function render() { |
| 193 | 224 | <td class="name">${escapeHtml(c.promptEn || "")}</td> |
| 194 | 225 | <td class="prompt" title="${escapeHtml(c.fullPrompt || "")}">${escapeHtml((c.fullPrompt || "").slice(0, 80))}${(c.fullPrompt || "").length > 80 ? "…" : ""}</td> |
| 195 | 226 | <td><a class="video-link" href="${videoHref(c)}" target="_blank">${videoHref(c)}</a></td> |
| 227 | + <td>${c.channel === "dance" ? "跳舞直播间" : "视频直播间"}</td> | |
| 196 | 228 | <td>${escapeHtml(traceability(c))}</td> |
| 197 | 229 | <td>${c.duration ?? "-"}s</td> |
| 198 | 230 | <td>${c.likes || 0}</td> |
| ... | ... | @@ -248,6 +280,17 @@ document.querySelectorAll("th[data-sort]").forEach((th) => { |
| 248 | 280 | document.getElementById("searchInput").addEventListener("input", render); |
| 249 | 281 | document.getElementById("sourceFilter").addEventListener("change", render); |
| 250 | 282 | |
| 283 | +document.getElementById("tabVideo").addEventListener("click", () => { | |
| 284 | + activeChannel = "video"; | |
| 285 | + document.getElementById("newChannel").value = "video"; | |
| 286 | + render(); | |
| 287 | +}); | |
| 288 | +document.getElementById("tabDance").addEventListener("click", () => { | |
| 289 | + activeChannel = "dance"; | |
| 290 | + document.getElementById("newChannel").value = "dance"; | |
| 291 | + render(); | |
| 292 | +}); | |
| 293 | + | |
| 251 | 294 | document.getElementById("addBtn").addEventListener("click", async () => { |
| 252 | 295 | const errorBox = document.getElementById("addError"); |
| 253 | 296 | errorBox.hidden = true; |
| ... | ... | @@ -256,6 +299,7 @@ document.getElementById("addBtn").addEventListener("click", async () => { |
| 256 | 299 | const videoUrl = document.getElementById("newVideoUrl").value.trim(); |
| 257 | 300 | const fullPrompt = document.getElementById("newFullPrompt").value.trim(); |
| 258 | 301 | const duration = Number(document.getElementById("newDuration").value) || null; |
| 302 | + const channel = document.getElementById("newChannel").value; | |
| 259 | 303 | if (!prompt || !videoUrl) { |
| 260 | 304 | errorBox.hidden = false; |
| 261 | 305 | errorBox.textContent = "名称和视频地址必填"; |
| ... | ... | @@ -264,7 +308,7 @@ document.getElementById("addBtn").addEventListener("click", async () => { |
| 264 | 308 | const res = await fetch("/api/admin/library", { |
| 265 | 309 | method: "POST", |
| 266 | 310 | headers: { "Content-Type": "application/json" }, |
| 267 | - body: JSON.stringify({ prompt, promptEn, videoUrl, fullPrompt, duration }), | |
| 311 | + body: JSON.stringify({ prompt, promptEn, videoUrl, fullPrompt, duration, channel }), | |
| 268 | 312 | }); |
| 269 | 313 | if (res.ok) { |
| 270 | 314 | document.getElementById("newPrompt").value = ""; | ... | ... |