Commit 27392c5d7e987fd32f0b203fdea4541380c8c200
1 parent
4a799a9e
新建任务表单:参考图/视频/音频/首尾帧支持单个删除
之前上传选错/传多了只能清空整个表单重来。每个预览缩略图右上角加一个×删除按钮,点击从 已选列表移除;首尾帧删除时顺带清空对应file input的值,不然重选同一个文件浏览器不会触发 change事件。setThumbs统一改成从uploaded状态重新渲染的renderRefThumbs/renderFl2vaThumbs/ renderTags,删除后重渲染逻辑一致。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Showing
1 changed file
with
56 additions
and
20 deletions
h3-console/static/index.html
| ... | ... | @@ -52,6 +52,11 @@ |
| 52 | 52 | .thumbrow img { height: 60px; border-radius: 4px; } |
| 53 | 53 | .thumbrow video { height: 220px; max-width: 100%; border-radius: 4px; } |
| 54 | 54 | .thumbrow audio { height: 32px; width: 220px; } |
| 55 | + .thumb-wrap { position: relative; display: inline-block; } | |
| 56 | + .thumb-del { position: absolute; top: -6px; right: -6px; width: 20px; height: 20px; padding: 0; | |
| 57 | + border-radius: 50%; border: none; background: #c33; color: #fff; font-size: 13px; line-height: 1; | |
| 58 | + cursor: pointer; } | |
| 59 | + .thumb-del:hover { background: #a22; } | |
| 55 | 60 | .status-done { color: #2a7; } .status-failed { color: #c33; } .status-running { color: #a70; } .status-queued { color: #777; } .status-cancelled { color: #999; } |
| 56 | 61 | .err { color: #c33; font-size: 13px; margin-top: 8px; white-space: pre-wrap; font-family: ui-monospace, Menlo, Consolas, monospace; |
| 57 | 62 | max-height: 400px; overflow-y: auto; background: #fff5f5; border: 1px solid #f3c2c2; border-radius: 6px; padding: 10px; } |
| ... | ... | @@ -322,40 +327,76 @@ async function uploadFile(file) { |
| 322 | 327 | return await r.json(); |
| 323 | 328 | } |
| 324 | 329 | |
| 330 | +// 每个预览项都包一层 .thumb-wrap 挂一个 × 删除按钮——上传上去选错了/传多了, | |
| 331 | +// 之前只能整个表单清空重来,2026-08-24 用户要求能单个删。 | |
| 332 | +function wrapWithDelete(mediaEl, onDelete) { | |
| 333 | + const wrap = document.createElement('span'); | |
| 334 | + wrap.className = 'thumb-wrap'; | |
| 335 | + const del = document.createElement('button'); | |
| 336 | + del.type = 'button'; del.className = 'thumb-del'; del.textContent = '×'; del.title = '删除'; | |
| 337 | + del.onclick = onDelete; | |
| 338 | + wrap.appendChild(mediaEl); wrap.appendChild(del); | |
| 339 | + return wrap; | |
| 340 | +} | |
| 341 | + | |
| 342 | +function renderFl2vaThumbs() { | |
| 343 | + const box = document.getElementById('fl2va-thumbs'); box.innerHTML = ''; | |
| 344 | + [['fl2va-first', '首帧'], ['fl2va-last', '尾帧']].forEach(([key, label]) => { | |
| 345 | + const u = uploaded[key]; | |
| 346 | + if (!u) return; | |
| 347 | + const img = document.createElement('img'); img.src = u; img.title = label; | |
| 348 | + box.appendChild(wrapWithDelete(img, () => { | |
| 349 | + uploaded[key] = null; | |
| 350 | + document.getElementById(key).value = ''; // 清掉file input,不然重选同一个文件不会触发change | |
| 351 | + renderFl2vaThumbs(); | |
| 352 | + })); | |
| 353 | + }); | |
| 354 | +} | |
| 355 | + | |
| 325 | 356 | async function uploadTo(inputId) { |
| 326 | 357 | const el = document.getElementById(inputId); |
| 327 | 358 | if (!el.files[0]) return; |
| 328 | 359 | try { |
| 329 | 360 | const res = await uploadFile(el.files[0]); |
| 330 | 361 | uploaded[inputId] = res.url; |
| 331 | - const thumbs = document.getElementById('fl2va-thumbs'); | |
| 332 | - const img = document.createElement('img'); img.src = res.url; thumbs.appendChild(img); | |
| 362 | + renderFl2vaThumbs(); | |
| 333 | 363 | } catch (e) { document.getElementById('fl2va-err').textContent = '上传失败: ' + e.message; } |
| 334 | 364 | } |
| 335 | 365 | |
| 366 | +function renderRefThumbs() { | |
| 367 | + const box = document.getElementById('ref2va-thumbs'); box.innerHTML = ''; | |
| 368 | + uploaded['ref2va-refs'].forEach((u, i) => { | |
| 369 | + const img = document.createElement('img'); img.src = u; | |
| 370 | + box.appendChild(wrapWithDelete(img, () => { | |
| 371 | + uploaded['ref2va-refs'].splice(i, 1); | |
| 372 | + renderRefThumbs(); | |
| 373 | + })); | |
| 374 | + }); | |
| 375 | +} | |
| 376 | + | |
| 336 | 377 | async function uploadMulti() { |
| 337 | 378 | const el = document.getElementById('ref2va-refs'); |
| 338 | - const thumbs = document.getElementById('ref2va-thumbs'); | |
| 339 | 379 | for (const f of el.files) { |
| 340 | 380 | try { |
| 341 | 381 | const res = await uploadFile(f); |
| 342 | 382 | uploaded['ref2va-refs'].push(res.url); |
| 343 | - const img = document.createElement('img'); img.src = res.url; thumbs.appendChild(img); | |
| 344 | 383 | } catch (e) { document.getElementById('ref2va-err').textContent = '上传失败: ' + e.message; } |
| 345 | 384 | } |
| 385 | + renderRefThumbs(); | |
| 346 | 386 | } |
| 347 | 387 | |
| 348 | 388 | // 参考视频/独立音频参考共用——不像参考图没有上限,这两个都封顶3个(跟节点协议一致), |
| 349 | -// 视频/音频不方便像图片一样直接<img>预览,用文件名文字标签代替。 | |
| 350 | -// elId 里带 "video"/"audio" 判断放哪种播放器——跟生成结果的视频一样直接能播放预览, | |
| 351 | -// 不再只显示文件名。 | |
| 352 | -function renderTags(elId, urls) { | |
| 389 | +// elId 里带 "video"/"audio" 判断放哪种播放器——跟生成结果的视频一样直接能播放预览。 | |
| 390 | +function renderTags(elId, stateKey) { | |
| 353 | 391 | const box = document.getElementById(elId); box.innerHTML = ''; |
| 354 | 392 | const isVideo = elId.includes('video'); |
| 355 | - urls.forEach(u => { | |
| 393 | + uploaded[stateKey].forEach((u, i) => { | |
| 356 | 394 | const el = document.createElement(isVideo ? 'video' : 'audio'); |
| 357 | 395 | el.src = u; el.controls = true; |
| 358 | - box.appendChild(el); | |
| 396 | + box.appendChild(wrapWithDelete(el, () => { | |
| 397 | + uploaded[stateKey].splice(i, 1); | |
| 398 | + renderTags(elId, stateKey); | |
| 399 | + })); | |
| 359 | 400 | }); |
| 360 | 401 | } |
| 361 | 402 | |
| ... | ... | @@ -369,7 +410,7 @@ async function uploadMultiTagged(inputId, stateKey, tagsElId) { |
| 369 | 410 | uploaded[stateKey].push(res.url); |
| 370 | 411 | } catch (e) { errBox.textContent = '上传失败: ' + e.message; } |
| 371 | 412 | } |
| 372 | - renderTags(tagsElId, uploaded[stateKey]); | |
| 413 | + renderTags(tagsElId, stateKey); | |
| 373 | 414 | } |
| 374 | 415 | |
| 375 | 416 | // 分辨率/时长/Turbo/步数/Seed 五个字段,三个 tab 结构完全一样,抽出来一份读取逻辑。 |
| ... | ... | @@ -430,11 +471,6 @@ async function submitJob(kind) { |
| 430 | 471 | // 重跑:不直接提交,而是把这条任务当初的 req 灌回"新建任务"表单(对应的 tab、参数、 |
| 431 | 472 | // 已上传的图片 URL 都原样带过去),跳到 /newtask.html,用户看一眼确认无误后自己点提交—— |
| 432 | 473 | // 跟正常新建任务走的是同一条路径、同一次人工确认,不是绕过表单直接后台再发一次请求。 |
| 433 | -function setThumbs(elId, urls) { | |
| 434 | - const box = document.getElementById(elId); box.innerHTML = ''; | |
| 435 | - urls.forEach(u => { if (!u) return; const img = document.createElement('img'); img.src = u; box.appendChild(img); }); | |
| 436 | -} | |
| 437 | - | |
| 438 | 474 | function prefillCreate(req) { |
| 439 | 475 | const kind = (req.first_frame_url || req.last_frame_url) ? 'fl2va' |
| 440 | 476 | : ((req.reference_image_urls || []).length || (req.reference_video_urls || []).length || (req.reference_audio_urls || []).length) ? 'ref2va' : 'text'; |
| ... | ... | @@ -474,14 +510,14 @@ function prefillCreate(req) { |
| 474 | 510 | if (kind === 'fl2va') { |
| 475 | 511 | uploaded['fl2va-first'] = req.first_frame_url || null; |
| 476 | 512 | uploaded['fl2va-last'] = req.last_frame_url || null; |
| 477 | - setThumbs('fl2va-thumbs', [req.first_frame_url, req.last_frame_url]); | |
| 513 | + renderFl2vaThumbs(); | |
| 478 | 514 | } else if (kind === 'ref2va') { |
| 479 | 515 | uploaded['ref2va-refs'] = (req.reference_image_urls || []).slice(); |
| 480 | 516 | uploaded['ref2va-videos'] = (req.reference_video_urls || []).slice(); |
| 481 | 517 | uploaded['ref2va-audios'] = (req.reference_audio_urls || []).slice(); |
| 482 | - setThumbs('ref2va-thumbs', uploaded['ref2va-refs']); | |
| 483 | - renderTags('ref2va-video-tags', uploaded['ref2va-videos']); | |
| 484 | - renderTags('ref2va-audio-tags', uploaded['ref2va-audios']); | |
| 518 | + renderRefThumbs(); | |
| 519 | + renderTags('ref2va-video-tags', 'ref2va-videos'); | |
| 520 | + renderTags('ref2va-audio-tags', 'ref2va-audios'); | |
| 485 | 521 | } |
| 486 | 522 | |
| 487 | 523 | showCreate(); | ... | ... |