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,6 +52,11 @@ | ||
| 52 | .thumbrow img { height: 60px; border-radius: 4px; } | 52 | .thumbrow img { height: 60px; border-radius: 4px; } |
| 53 | .thumbrow video { height: 220px; max-width: 100%; border-radius: 4px; } | 53 | .thumbrow video { height: 220px; max-width: 100%; border-radius: 4px; } |
| 54 | .thumbrow audio { height: 32px; width: 220px; } | 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 | .status-done { color: #2a7; } .status-failed { color: #c33; } .status-running { color: #a70; } .status-queued { color: #777; } .status-cancelled { color: #999; } | 60 | .status-done { color: #2a7; } .status-failed { color: #c33; } .status-running { color: #a70; } .status-queued { color: #777; } .status-cancelled { color: #999; } |
| 56 | .err { color: #c33; font-size: 13px; margin-top: 8px; white-space: pre-wrap; font-family: ui-monospace, Menlo, Consolas, monospace; | 61 | .err { color: #c33; font-size: 13px; margin-top: 8px; white-space: pre-wrap; font-family: ui-monospace, Menlo, Consolas, monospace; |
| 57 | max-height: 400px; overflow-y: auto; background: #fff5f5; border: 1px solid #f3c2c2; border-radius: 6px; padding: 10px; } | 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,40 +327,76 @@ async function uploadFile(file) { | ||
| 322 | return await r.json(); | 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 | async function uploadTo(inputId) { | 356 | async function uploadTo(inputId) { |
| 326 | const el = document.getElementById(inputId); | 357 | const el = document.getElementById(inputId); |
| 327 | if (!el.files[0]) return; | 358 | if (!el.files[0]) return; |
| 328 | try { | 359 | try { |
| 329 | const res = await uploadFile(el.files[0]); | 360 | const res = await uploadFile(el.files[0]); |
| 330 | uploaded[inputId] = res.url; | 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 | } catch (e) { document.getElementById('fl2va-err').textContent = '上传失败: ' + e.message; } | 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 | async function uploadMulti() { | 377 | async function uploadMulti() { |
| 337 | const el = document.getElementById('ref2va-refs'); | 378 | const el = document.getElementById('ref2va-refs'); |
| 338 | - const thumbs = document.getElementById('ref2va-thumbs'); | ||
| 339 | for (const f of el.files) { | 379 | for (const f of el.files) { |
| 340 | try { | 380 | try { |
| 341 | const res = await uploadFile(f); | 381 | const res = await uploadFile(f); |
| 342 | uploaded['ref2va-refs'].push(res.url); | 382 | uploaded['ref2va-refs'].push(res.url); |
| 343 | - const img = document.createElement('img'); img.src = res.url; thumbs.appendChild(img); | ||
| 344 | } catch (e) { document.getElementById('ref2va-err').textContent = '上传失败: ' + e.message; } | 383 | } catch (e) { document.getElementById('ref2va-err').textContent = '上传失败: ' + e.message; } |
| 345 | } | 384 | } |
| 385 | + renderRefThumbs(); | ||
| 346 | } | 386 | } |
| 347 | 387 | ||
| 348 | // 参考视频/独立音频参考共用——不像参考图没有上限,这两个都封顶3个(跟节点协议一致), | 388 | // 参考视频/独立音频参考共用——不像参考图没有上限,这两个都封顶3个(跟节点协议一致), |
| 349 | -// 视频/音频不方便像图片一样直接<img>预览,用文件名文字标签代替。 | ||
| 350 | -// elId 里带 "video"/"audio" 判断放哪种播放器——跟生成结果的视频一样直接能播放预览, | ||
| 351 | -// 不再只显示文件名。 | ||
| 352 | -function renderTags(elId, urls) { | 389 | +// elId 里带 "video"/"audio" 判断放哪种播放器——跟生成结果的视频一样直接能播放预览。 |
| 390 | +function renderTags(elId, stateKey) { | ||
| 353 | const box = document.getElementById(elId); box.innerHTML = ''; | 391 | const box = document.getElementById(elId); box.innerHTML = ''; |
| 354 | const isVideo = elId.includes('video'); | 392 | const isVideo = elId.includes('video'); |
| 355 | - urls.forEach(u => { | 393 | + uploaded[stateKey].forEach((u, i) => { |
| 356 | const el = document.createElement(isVideo ? 'video' : 'audio'); | 394 | const el = document.createElement(isVideo ? 'video' : 'audio'); |
| 357 | el.src = u; el.controls = true; | 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,7 +410,7 @@ async function uploadMultiTagged(inputId, stateKey, tagsElId) { | ||
| 369 | uploaded[stateKey].push(res.url); | 410 | uploaded[stateKey].push(res.url); |
| 370 | } catch (e) { errBox.textContent = '上传失败: ' + e.message; } | 411 | } catch (e) { errBox.textContent = '上传失败: ' + e.message; } |
| 371 | } | 412 | } |
| 372 | - renderTags(tagsElId, uploaded[stateKey]); | 413 | + renderTags(tagsElId, stateKey); |
| 373 | } | 414 | } |
| 374 | 415 | ||
| 375 | // 分辨率/时长/Turbo/步数/Seed 五个字段,三个 tab 结构完全一样,抽出来一份读取逻辑。 | 416 | // 分辨率/时长/Turbo/步数/Seed 五个字段,三个 tab 结构完全一样,抽出来一份读取逻辑。 |
| @@ -430,11 +471,6 @@ async function submitJob(kind) { | @@ -430,11 +471,6 @@ async function submitJob(kind) { | ||
| 430 | // 重跑:不直接提交,而是把这条任务当初的 req 灌回"新建任务"表单(对应的 tab、参数、 | 471 | // 重跑:不直接提交,而是把这条任务当初的 req 灌回"新建任务"表单(对应的 tab、参数、 |
| 431 | // 已上传的图片 URL 都原样带过去),跳到 /newtask.html,用户看一眼确认无误后自己点提交—— | 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 | function prefillCreate(req) { | 474 | function prefillCreate(req) { |
| 439 | const kind = (req.first_frame_url || req.last_frame_url) ? 'fl2va' | 475 | const kind = (req.first_frame_url || req.last_frame_url) ? 'fl2va' |
| 440 | : ((req.reference_image_urls || []).length || (req.reference_video_urls || []).length || (req.reference_audio_urls || []).length) ? 'ref2va' : 'text'; | 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,14 +510,14 @@ function prefillCreate(req) { | ||
| 474 | if (kind === 'fl2va') { | 510 | if (kind === 'fl2va') { |
| 475 | uploaded['fl2va-first'] = req.first_frame_url || null; | 511 | uploaded['fl2va-first'] = req.first_frame_url || null; |
| 476 | uploaded['fl2va-last'] = req.last_frame_url || null; | 512 | uploaded['fl2va-last'] = req.last_frame_url || null; |
| 477 | - setThumbs('fl2va-thumbs', [req.first_frame_url, req.last_frame_url]); | 513 | + renderFl2vaThumbs(); |
| 478 | } else if (kind === 'ref2va') { | 514 | } else if (kind === 'ref2va') { |
| 479 | uploaded['ref2va-refs'] = (req.reference_image_urls || []).slice(); | 515 | uploaded['ref2va-refs'] = (req.reference_image_urls || []).slice(); |
| 480 | uploaded['ref2va-videos'] = (req.reference_video_urls || []).slice(); | 516 | uploaded['ref2va-videos'] = (req.reference_video_urls || []).slice(); |
| 481 | uploaded['ref2va-audios'] = (req.reference_audio_urls || []).slice(); | 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 | showCreate(); | 523 | showCreate(); |