/* DCTS Dashboard Workflows Component */ import { escapeHtml } from '../utils/format.js'; import { ICONS } from '../utils/icons.js'; /** 工作流卡片骨架:纯展示卡(名称/描述/状态/进度)。 * * 卡片下方的「YAML配置/启动/暂停/删除」按钮已移除(见 docs/task_engine_decoupling_design.md * §6.1)。所有操作(启动/停止/删除/保存配置)收敛至工作流详情页顶部的内嵌阶段配置面板 * (wfEnginePanel.js)。首页卡片仅作状态概览与入口。 */ function buildCardSkeleton({ name, href, desc, statusCn, statusClass, hasProgress, pct, countsText }) { return `

${name}

${desc}

${statusCn}
${countsText}
`; } /** 原位更新卡片易变部分:状态徽章 / 进度条宽度 / 计数文本。 */ function updateCardInPlace(card, { statusCn, statusClass, hasProgress, pct, countsText }) { // 状态徽章 const badge = card.querySelector('[data-wf-badge]'); if (badge) { if (badge.textContent !== statusCn) badge.textContent = statusCn; if (!badge.classList.contains(statusClass)) { badge.className = `status-badge ${statusClass}`; badge.setAttribute('data-wf-badge', ''); } } // 进度条 const progWrap = card.querySelector('[data-wf-progress]'); if (progWrap) { const shouldShow = hasProgress; const isHidden = progWrap.classList.contains('hidden'); if (shouldShow && isHidden) progWrap.classList.remove('hidden'); else if (!shouldShow && !isHidden) progWrap.classList.add('hidden'); if (shouldShow) { const fill = progWrap.querySelector('[data-wf-fill]'); const wStr = `${pct}%`; if (fill && fill.style.width !== wStr) fill.style.width = wStr; const counts = progWrap.querySelector('[data-wf-counts]'); if (counts && counts.textContent !== countsText) counts.textContent = countsText; } } } export function renderWorkflowsSkeleton() { const container = document.getElementById('workflows-list'); if (!container) return; container.innerHTML = Array.from({ length: 2 }).map(() => `
`).join(''); } export function renderWorkflows(workflows) { const container = document.getElementById('workflows-list'); const wfMetric = document.getElementById('val-total-workflows'); const wfSub = document.getElementById('val-active-wf'); if (!container) return; if (wfMetric) wfMetric.textContent = workflows ? workflows.length : 0; if (!workflows || workflows.length === 0) { if (wfSub) wfSub.textContent = '暂无已注册工作流'; const emptyHtml = `
${ICONS.layers({ size: 36, cls: 'empty-state-icon' })}

暂无恒星大气网格工作流配置

`; if (container.innerHTML.trim() !== emptyHtml.trim()) { container.innerHTML = emptyHtml; } return; } const runningCount = workflows.filter(w => w.status === 'running').length; if (wfSub) { wfSub.textContent = runningCount > 0 ? `${runningCount} 个工作流运行中` : '集群待命'; } const existingCards = new Map(); container.querySelectorAll('.workflow-card[data-wf-card-name]').forEach(card => { existingCards.set(card.getAttribute('data-wf-card-name'), card); }); if (container.querySelector('.skeleton-card, .empty-cell')) { container.innerHTML = ''; existingCards.clear(); } const updatedCards = []; workflows.forEach(wf => { // 状态徽章全映射:后端可发 idle/initializing/running/paused/completed let statusCn = '闲置'; let statusClass = 'secondary'; if (wf.status === 'running') { statusCn = '运行中'; statusClass = 'online'; } else if (wf.status === 'completed') { statusCn = '已完成'; statusClass = 'completed'; } else if (wf.status === 'initializing') { statusCn = '初始化中'; statusClass = 'warning'; } else if (wf.status === 'paused') { statusCn = '已暂停'; statusClass = 'secondary'; } const name = escapeHtml(wf.name); const href = `#/workflows/${encodeURIComponent(wf.name)}`; const desc = escapeHtml(wf.description || '无描述'); // 内联进度(来自列表接口内联 stats;未启动的工作流 stats=null → 不渲染) const s = wf.stats; const hasProgress = s && s.total > 0; const pct = hasProgress ? Math.min(100, Math.max(0, Math.round((s.converged / s.total) * 100))) : 0; const countsText = hasProgress ? `${s.converged}/${s.total} 收敛 · ${s.seed_step_converged} 种子步进 · ${s.failed} 失败 · ${s.running} 运行` : ''; let card = existingCards.get(name); if (card) { // 原位更新:状态徽章 / 进度条 / 计数文本——各自 diff,避免整卡 innerHTML 重建 updateCardInPlace(card, { statusCn, statusClass, hasProgress, pct, countsText }); existingCards.delete(name); } else { card = document.createElement('div'); card.className = 'workflow-card'; card.setAttribute('data-wf-card-name', name); card.innerHTML = buildCardSkeleton({ name, href, desc, statusCn, statusClass, hasProgress, pct, countsText }); } updatedCards.push(card); }); existingCards.forEach(card => card.remove()); updatedCards.forEach((card, idx) => { if (container.children[idx] !== card) { container.insertBefore(card, container.children[idx] || null); } }); }