/* DCTS 格式化与转义工具(权威定义) * * 集中所有纯函数:HTML 转义、时间/时长格式化、状态徽章。 * 历史上 escapeHtml 定义在 api.js、各 fmt* 散落在 workflowDetail.js,改一处要全局搜, * 现统一到本模块。api.js 通过 re-export 保持向后兼容。 * * SQLite 约定:datetime('now') 形如 'YYYY-MM-DD HH:MM:SS'(UTC,无时区标记), * 解析前需规范化为 ISO。所有时间解析函数共用 parseIso/parseTsMs。 * * 语义与后端契约对齐(crates/server/src/db.rs): * - 网格点状态:pending/queued/running/converged/failed/canceled * - success_method(收敛途径):cold_run / seed_step */ /** * 安全 HTML 转义,防止存储型 XSS。 * 顺序敏感:& 必须先转义,否则后续转义产生的 & 会被二次转义。 * @param {unknown} s * @returns {string} */ export function escapeHtml(s) { if (s == null) return ''; return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** 规范化为 Date(解析失败返回 null)。SQLite 'YYYY-MM-DD HH:MM:SS' 视为 UTC。 */ export function parseIso(iso) { if (!iso) return null; const normalized = iso.includes('T') ? iso : iso.replace(' ', 'T') + 'Z'; const d = new Date(normalized); return Number.isNaN(d.getTime()) ? null : d; } /** 同 parseIso 但返回 epoch 毫秒(解析失败为 NaN)。供进度曲线按真实时间铺横轴。 */ export function parseTsMs(iso) { if (!iso) return NaN; const normalized = iso.includes('T') ? iso : iso.replace(' ', 'T') + 'Z'; return new Date(normalized).getTime(); } /** SQLite datetime('now') 为 'YYYY-MM-DD HH:MM:SS'(UTC 无时区标记),规范化后解析。 */ export function fmtTime(iso) { if (!iso) return '—'; const normalized = iso.includes('T') ? iso : iso.replace(' ', 'T') + 'Z'; const d = new Date(normalized); if (Number.isNaN(d.getTime())) return escapeHtml(iso); return d.toLocaleTimeString('zh-CN'); } /** 紧凑日期时间「MM-DD HH:MM」:带日期(跨天运行不至于看不出是哪天完成的), * 隐去年份与秒(活跃网格数据不跨年,秒级精度放 title)。定宽 11 字符,列内对齐。 */ export function fmtDateTime(iso) { const d = parseIso(iso); if (!d) return iso ? escapeHtml(iso) : '—'; const p = (n) => String(n).padStart(2, '0'); return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`; } /** 完整时间戳「YYYY-MM-DD HH:MM:SS」,供单元格 title 悬停查看精确时刻。 */ export function fmtFullTs(iso) { const d = parseIso(iso); if (!d) return iso || ''; const p = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`; } /** 工作流级时长格式化:「N 分钟」或「N 小时 M 分」(控制条 ETA 用)。null/非有限值/≤0 返回占位符。 */ export function fmtDuration(sec) { // ≤0 视为无数据(与 fmtDur 同口径):失败任务上报的 last_elapsed_sec 可能是 0.0 // (runner 无 summary 时),显示"0 分钟"会误导,应展示占位符。 if (sec == null || !Number.isFinite(sec) || Number(sec) <= 0) return '—'; const m = Math.round(sec / 60); if (m < 60) return `${m} 分钟`; const h = Math.floor(m / 60); return `${h} 小时 ${m % 60} 分`; } /** 网格点级紧凑时长(表格单元格用):42s / 12m 30s / 1h 20m。 * ≤0 视为无数据(tlusty 单点不可能 0 秒完成)。 */ export function fmtDur(sec) { if (sec == null || sec === '') return '—'; const n = Number(sec); if (!Number.isFinite(n) || n <= 0) return '—'; const s = Math.round(n); if (s < 60) return `${s}s`; const m = Math.floor(s / 60); if (m < 60) return `${m}m${s % 60 ? ` ${s % 60}s` : ''}`; const h = Math.floor(m / 60); return `${h}h${m % 60 ? ` ${m % 60}m` : ''}`; } /** 相对收敛度指数记数(点表/尝试表 max_relc 单元格)。空值/非法值返回占位符。 */ export function fmtRelc(v) { if (v == null || v === '') return '—'; const n = Number(v); return Number.isFinite(n) ? n.toExponential(2) : '—'; } /** CSV 字段转义:含逗号/引号/换行时用双引号包裹并翻倍内部引号。 */ export function csvEscape(v) { const s = String(v ?? ''); return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; } /** 心跳时间容错:字段缺失或非法时间戳一律显示占位符,避免渲染出 "Invalid Date"。 */ export function fmtClock(ts) { if (!ts) return '—'; const d = new Date(ts); if (Number.isNaN(d.getTime())) return '—'; return d.toLocaleTimeString('zh-CN'); } // ===== 状态徽章(返回 HTML 字符串,供 innerHTML 注入) ===== /** 工作流状态映射:[中文标签, badge class]。后端可发 idle/initializing/running/paused/completed。 */ export const STATUS_MAP = { running: ['运行中', 'online'], completed: ['已完成', 'completed'], initializing: ['初始化中', 'warning'], paused: ['已暂停', 'secondary'], idle: ['闲置', 'secondary'], }; /** 工作流状态徽章 HTML。 */ export function statusBadge(status) { const [cn, cls] = STATUS_MAP[status] || ['闲置', 'secondary']; return `${cn}`; } /** 网格点状态映射:[中文标签, badge class]。与后端 grid_points.status 对齐。 */ export const POINT_STATUS_MAP = { converged: ['已收敛', 'online'], failed: ['失败', 'danger'], running: ['运行中', 'warning'], queued: ['排队中', 'info'], pending: ['未开始', 'secondary'], }; /** 网格点状态徽章 HTML。 */ export function pointStatusBadge(status) { const [cn, cls] = POINT_STATUS_MAP[status] || ['未知', 'secondary']; return `${cn}`; } /** 网格点收敛途径徽章 HTML(success_method:cold_run / seed_step / 策略名)。 */ export function pointMethodBadge(method) { if (method === 'cold_run') return '冷启动'; if (method === 'seed_step') return '种子步进'; // 其它策略名(如 synspec-only 任务归因的 "standard")原样展示,让光谱归因可见。 if (method) return `${escapeHtml(method)}`; return ''; }