/* 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 `
`;
}
/** 原位更新卡片易变部分:状态徽章 / 进度条宽度 / 计数文本。 */
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);
}
});
}