Fix detail modal progress bars not updating
Docker Build and Push / build-and-push (cuda, 11.8) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 12.6) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 12.8) (push) Has been skipped
Docker Build and Push / check-secrets (push) Successful in 4s
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
Docker Build and Push / build-and-push (directml, latest) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
Build Windows CUDA 11.8 / build (push) Has been cancelled
Build Windows CUDA 12.6 / build (push) Has been cancelled
Build Windows CUDA 12.8 / build (push) Has been cancelled
Build Windows DirectML / build (push) Has been cancelled

Split openDetail (full render once) from renderDetail (partial update only).
- openDetail: renders static content + placeholder divs, loads logs once
- renderDetail (called on task_update): only patches status badge, phase bars,
  duration, error, action buttons — does NOT replace innerHTML or reload logs
This eliminates the flickering and actually makes progress bars respond to
WebSocket updates.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
liuchen
2026-06-27 15:21:35 +08:00
co-authored by Claude Haiku 4.5
parent da119826f5
commit b81f6afd0f
+72 -51
View File
@@ -1003,31 +1003,13 @@ function progressCell(t) {
function openDetail(id) {
S.detailTaskId = id;
renderDetail(id);
openModal('modal-detail');
}
function renderDetail(id) {
const t = S.tasks[id];
if (!t) return;
const { cls, label } = taskStatus(t);
const el = document.getElementById('detailStatus');
el.className = `badge ${cls}`;
el.innerHTML = `<span class="bdot"></span>${label}`;
// 与列表完全一致的格式函数
const fmtTime = ts => ts ? new Date(ts*1000).toLocaleString('zh-CN', {year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '-';
const fmtDur = () => {
if (!t.started_at) return '-';
const end = t.completed_at || (Date.now()/1000);
const s = Math.round(end - t.started_at);
const m = Math.floor(s/60), sec = s%60;
const str = m > 0 ? `${m}m ${sec}s` : `${sec}s`;
return t.completed_at ? str : `<span style="color:var(--warn)">${str}…</span>`;
};
const modeLabel = { 'sttn-auto':'STTN 自动','sttn-det':'STTN','lama':'LaMa','propainter':'ProPainter','opencv':'OpenCV' };
// 全量渲染一次,含静态内容 + 占位 div(id 固定供后续局部更新)
const body = document.getElementById('detailBody');
body.innerHTML = `
<div class="item">
@@ -1039,47 +1021,86 @@ function renderDetail(id) {
<span class="subtle">创建时间</span><span class="subtle">开始处理</span><span class="subtle">耗时</span>
<span style="font-size:13px">${fmtTime(t.created_at)}</span>
<span style="font-size:13px">${fmtTime(t.started_at)}</span>
<span style="font-size:13px">${fmtDur()}</span>
<span id="detailDur" style="font-size:13px">-</span>
</div>
<div class="subtle" style="margin-top:6px">算法: ${modeLabel[t.inpaint_mode]||t.inpaint_mode} · 检测: ${t.detect_mode==='smart'?'水印模式':'文字 OCR'}</div>
${t.error ? `<div class="badge bad" style="align-self:start;word-break:break-all;margin-top:4px">${esc(t.error)}</div>` : ''}
${t.status==='downloading' ? `
<div style="margin-top:8px">
<div style="font-size:11px;color:var(--muted);margin-bottom:4px">下载进度 ${t.download_progress||0}%</div>
<div class="prog-track"><div class="prog-fill dl" style="width:${t.download_progress||0}%"></div></div>
</div>
` : ''}
${t.status==='processing' ? `
<div style="margin-top:8px;display:grid;gap:5px">
<div class="phase-row"><span class="phase-label">检测</span><div class="phase-track"><div class="phase-fill detect" style="width:${t.detect_pct||0}%"></div></div><span class="phase-pct">${t.detect_pct||0}%</span></div>
<div class="phase-row"><span class="phase-label">处理</span><div class="phase-track"><div class="phase-fill process" style="width:${t.process_pct||0}%"></div></div><span class="phase-pct">${t.process_pct||0}%</span></div>
<div class="phase-row"><span class="phase-label">合并</span><div class="phase-track"><div class="phase-fill merge" style="width:${t.merge_pct||0}%"></div></div><span class="phase-pct">${t.merge_pct||0}%</span></div>
</div>
` : ''}
<div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap">
${t.cache_path ? `
<button class="button secondary" style="min-height:32px;padding:0 14px;font-size:13px"
onclick="playVideo('${t.id}', 'source')">▶ 原视频</button>
` : ''}
${t.status==='done' ? `
<button class="button" style="min-height:32px;padding:0 14px;font-size:13px"
onclick="playVideo('${t.id}', 'output')">▶ 处理结果</button>
<a href="/api/tasks/${t.id}/download" download
class="button secondary" style="min-height:32px;padding:0 14px;font-size:13px;text-decoration:none">↓ 下载</a>
` : ''}
<div id="detailError"></div>
<div id="detailProgress" style="margin-top:8px"></div>
<div id="detailActions" style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap">
${t.cache_path ? `<button class="button secondary" style="min-height:32px;padding:0 14px;font-size:13px" onclick="playVideo('${t.id}', 'source')">▶ 原视频</button>` : ''}
</div>
</div>
<div>
<h3 style="font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);margin-bottom:8px">日志</h3>
<div id="detailLogs" style="
background:#f7f9fc;border:1px solid var(--line);border-radius:var(--radius-sm);
padding:12px 14px;max-height:240px;overflow-y:auto;
font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:1.7;
"><div style="color:var(--muted)">加载中…</div></div>
<div id="detailLogs" style="background:#f7f9fc;border:1px solid var(--line);border-radius:var(--radius-sm);padding:12px 14px;max-height:240px;overflow-y:auto;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:1.7;">
<div style="color:var(--muted)">加载中…</div>
</div>
</div>
`;
// 异步从 DB 加载日志
// 局部刷新动态内容
updateDetailDynamic(t);
// 异步加载日志(只加载一次)
loadDetailLogs(id);
openModal('modal-detail');
}
// 只更新会变化的部分:状态、进度条、耗时、错误、操作按钮
function updateDetailDynamic(t) {
const { cls, label } = taskStatus(t);
const statusEl = document.getElementById('detailStatus');
if (statusEl) { statusEl.className = `badge ${cls}`; statusEl.innerHTML = `<span class="bdot"></span>${label}`; }
const durEl = document.getElementById('detailDur');
if (durEl) {
if (!t.started_at) {
durEl.innerHTML = '-';
} else {
const end = t.completed_at || (Date.now()/1000);
const s = Math.round(end - t.started_at);
const m = Math.floor(s/60), sec = s%60;
const str = m > 0 ? `${m}m ${sec}s` : `${sec}s`;
durEl.innerHTML = t.completed_at ? str : `<span style="color:var(--warn)">${str}…</span>`;
}
}
const errEl = document.getElementById('detailError');
if (errEl) errEl.innerHTML = t.error ? `<div class="badge bad" style="align-self:start;word-break:break-all;margin-top:4px">${esc(t.error)}</div>` : '';
const progEl = document.getElementById('detailProgress');
if (progEl) {
if (t.status === 'downloading') {
const p = t.download_progress||0;
progEl.innerHTML = `<div style="font-size:11px;color:var(--muted);margin-bottom:4px">下载进度 ${p}%</div><div class="prog-track"><div class="prog-fill dl" style="width:${p}%"></div></div>`;
} else if (t.status === 'processing') {
progEl.innerHTML = `
<div style="display:grid;gap:5px">
<div class="phase-row"><span class="phase-label">检测</span><div class="phase-track"><div class="phase-fill detect" style="width:${t.detect_pct||0}%"></div></div><span class="phase-pct">${t.detect_pct||0}%</span></div>
<div class="phase-row"><span class="phase-label">处理</span><div class="phase-track"><div class="phase-fill process" style="width:${t.process_pct||0}%"></div></div><span class="phase-pct">${t.process_pct||0}%</span></div>
<div class="phase-row"><span class="phase-label">合并</span><div class="phase-track"><div class="phase-fill merge" style="width:${t.merge_pct||0}%"></div></div><span class="phase-pct">${t.merge_pct||0}%</span></div>
</div>`;
} else {
progEl.innerHTML = '';
}
}
const actEl = document.getElementById('detailActions');
if (actEl && t.status === 'done') {
// 只在完成时补充按钮,避免重复加
if (!actEl.querySelector('.btn-output')) {
actEl.insertAdjacentHTML('beforeend', `
<button class="button btn-output" style="min-height:32px;padding:0 14px;font-size:13px" onclick="playVideo('${t.id}', 'output')">▶ 处理结果</button>
<a href="/api/tasks/${t.id}/download" download class="button secondary" style="min-height:32px;padding:0 14px;font-size:13px;text-decoration:none">↓ 下载</a>
`);
}
}
}
// task_update 时只做局部刷新,不重建整个 body
function renderDetail(id) {
const t = S.tasks[id];
if (!t) return;
updateDetailDynamic(t);
}
async function loadDetailLogs(taskId) {