Commit 27392c5d7e987fd32f0b203fdea4541380c8c200

Authored by yibo
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();
... ...