diff --git a/h3-console/static/index.html b/h3-console/static/index.html
index af95841..af84735 100644
--- a/h3-console/static/index.html
+++ b/h3-console/static/index.html
@@ -52,6 +52,11 @@
.thumbrow img { height: 60px; border-radius: 4px; }
.thumbrow video { height: 220px; max-width: 100%; border-radius: 4px; }
.thumbrow audio { height: 32px; width: 220px; }
+ .thumb-wrap { position: relative; display: inline-block; }
+ .thumb-del { position: absolute; top: -6px; right: -6px; width: 20px; height: 20px; padding: 0;
+ border-radius: 50%; border: none; background: #c33; color: #fff; font-size: 13px; line-height: 1;
+ cursor: pointer; }
+ .thumb-del:hover { background: #a22; }
.status-done { color: #2a7; } .status-failed { color: #c33; } .status-running { color: #a70; } .status-queued { color: #777; } .status-cancelled { color: #999; }
.err { color: #c33; font-size: 13px; margin-top: 8px; white-space: pre-wrap; font-family: ui-monospace, Menlo, Consolas, monospace;
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) {
return await r.json();
}
+// 每个预览项都包一层 .thumb-wrap 挂一个 × 删除按钮——上传上去选错了/传多了,
+// 之前只能整个表单清空重来,2026-08-24 用户要求能单个删。
+function wrapWithDelete(mediaEl, onDelete) {
+ const wrap = document.createElement('span');
+ wrap.className = 'thumb-wrap';
+ const del = document.createElement('button');
+ del.type = 'button'; del.className = 'thumb-del'; del.textContent = '×'; del.title = '删除';
+ del.onclick = onDelete;
+ wrap.appendChild(mediaEl); wrap.appendChild(del);
+ return wrap;
+}
+
+function renderFl2vaThumbs() {
+ const box = document.getElementById('fl2va-thumbs'); box.innerHTML = '';
+ [['fl2va-first', '首帧'], ['fl2va-last', '尾帧']].forEach(([key, label]) => {
+ const u = uploaded[key];
+ if (!u) return;
+ const img = document.createElement('img'); img.src = u; img.title = label;
+ box.appendChild(wrapWithDelete(img, () => {
+ uploaded[key] = null;
+ document.getElementById(key).value = ''; // 清掉file input,不然重选同一个文件不会触发change
+ renderFl2vaThumbs();
+ }));
+ });
+}
+
async function uploadTo(inputId) {
const el = document.getElementById(inputId);
if (!el.files[0]) return;
try {
const res = await uploadFile(el.files[0]);
uploaded[inputId] = res.url;
- const thumbs = document.getElementById('fl2va-thumbs');
- const img = document.createElement('img'); img.src = res.url; thumbs.appendChild(img);
+ renderFl2vaThumbs();
} catch (e) { document.getElementById('fl2va-err').textContent = '上传失败: ' + e.message; }
}
+function renderRefThumbs() {
+ const box = document.getElementById('ref2va-thumbs'); box.innerHTML = '';
+ uploaded['ref2va-refs'].forEach((u, i) => {
+ const img = document.createElement('img'); img.src = u;
+ box.appendChild(wrapWithDelete(img, () => {
+ uploaded['ref2va-refs'].splice(i, 1);
+ renderRefThumbs();
+ }));
+ });
+}
+
async function uploadMulti() {
const el = document.getElementById('ref2va-refs');
- const thumbs = document.getElementById('ref2va-thumbs');
for (const f of el.files) {
try {
const res = await uploadFile(f);
uploaded['ref2va-refs'].push(res.url);
- const img = document.createElement('img'); img.src = res.url; thumbs.appendChild(img);
} catch (e) { document.getElementById('ref2va-err').textContent = '上传失败: ' + e.message; }
}
+ renderRefThumbs();
}
// 参考视频/独立音频参考共用——不像参考图没有上限,这两个都封顶3个(跟节点协议一致),
-// 视频/音频不方便像图片一样直接
预览,用文件名文字标签代替。
-// elId 里带 "video"/"audio" 判断放哪种播放器——跟生成结果的视频一样直接能播放预览,
-// 不再只显示文件名。
-function renderTags(elId, urls) {
+// elId 里带 "video"/"audio" 判断放哪种播放器——跟生成结果的视频一样直接能播放预览。
+function renderTags(elId, stateKey) {
const box = document.getElementById(elId); box.innerHTML = '';
const isVideo = elId.includes('video');
- urls.forEach(u => {
+ uploaded[stateKey].forEach((u, i) => {
const el = document.createElement(isVideo ? 'video' : 'audio');
el.src = u; el.controls = true;
- box.appendChild(el);
+ box.appendChild(wrapWithDelete(el, () => {
+ uploaded[stateKey].splice(i, 1);
+ renderTags(elId, stateKey);
+ }));
});
}
@@ -369,7 +410,7 @@ async function uploadMultiTagged(inputId, stateKey, tagsElId) {
uploaded[stateKey].push(res.url);
} catch (e) { errBox.textContent = '上传失败: ' + e.message; }
}
- renderTags(tagsElId, uploaded[stateKey]);
+ renderTags(tagsElId, stateKey);
}
// 分辨率/时长/Turbo/步数/Seed 五个字段,三个 tab 结构完全一样,抽出来一份读取逻辑。
@@ -430,11 +471,6 @@ async function submitJob(kind) {
// 重跑:不直接提交,而是把这条任务当初的 req 灌回"新建任务"表单(对应的 tab、参数、
// 已上传的图片 URL 都原样带过去),跳到 /newtask.html,用户看一眼确认无误后自己点提交——
// 跟正常新建任务走的是同一条路径、同一次人工确认,不是绕过表单直接后台再发一次请求。
-function setThumbs(elId, urls) {
- const box = document.getElementById(elId); box.innerHTML = '';
- urls.forEach(u => { if (!u) return; const img = document.createElement('img'); img.src = u; box.appendChild(img); });
-}
-
function prefillCreate(req) {
const kind = (req.first_frame_url || req.last_frame_url) ? 'fl2va'
: ((req.reference_image_urls || []).length || (req.reference_video_urls || []).length || (req.reference_audio_urls || []).length) ? 'ref2va' : 'text';
@@ -474,14 +510,14 @@ function prefillCreate(req) {
if (kind === 'fl2va') {
uploaded['fl2va-first'] = req.first_frame_url || null;
uploaded['fl2va-last'] = req.last_frame_url || null;
- setThumbs('fl2va-thumbs', [req.first_frame_url, req.last_frame_url]);
+ renderFl2vaThumbs();
} else if (kind === 'ref2va') {
uploaded['ref2va-refs'] = (req.reference_image_urls || []).slice();
uploaded['ref2va-videos'] = (req.reference_video_urls || []).slice();
uploaded['ref2va-audios'] = (req.reference_audio_urls || []).slice();
- setThumbs('ref2va-thumbs', uploaded['ref2va-refs']);
- renderTags('ref2va-video-tags', uploaded['ref2va-videos']);
- renderTags('ref2va-audio-tags', uploaded['ref2va-audios']);
+ renderRefThumbs();
+ renderTags('ref2va-video-tags', 'ref2va-videos');
+ renderTags('ref2va-audio-tags', 'ref2va-audios');
}
showCreate();