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
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:
co-authored by
Claude Haiku 4.5
parent
da119826f5
commit
b81f6afd0f
+72
-51
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user