128 lines
3.8 KiB
JavaScript
128 lines
3.8 KiB
JavaScript
async function api(path, opts) {
|
||
const r = await fetch(path, opts);
|
||
const j = await r.json().catch(() => ({}));
|
||
if (!r.ok) {
|
||
const err = new Error(j.error || r.statusText);
|
||
if (j.detail) err.detail = j.detail;
|
||
if (j.uploadUrl) err.uploadUrl = j.uploadUrl;
|
||
throw err;
|
||
}
|
||
return j;
|
||
}
|
||
|
||
/** 右下角 Toast;type: ok | err | warn | info */
|
||
function showToast(message, type) {
|
||
if (!message) return;
|
||
let root = document.getElementById('toast-root');
|
||
if (!root) {
|
||
root = document.createElement('div');
|
||
root.id = 'toast-root';
|
||
root.className = 'toast-root';
|
||
document.body.appendChild(root);
|
||
}
|
||
const el = document.createElement('div');
|
||
el.className = 'toast ' + (type || 'info');
|
||
el.textContent = message;
|
||
root.appendChild(el);
|
||
requestAnimationFrame(() => el.classList.add('show'));
|
||
setTimeout(() => {
|
||
el.classList.remove('show');
|
||
setTimeout(() => el.remove(), 300);
|
||
}, 4500);
|
||
}
|
||
|
||
function showInlineError(el, message) {
|
||
if (!el) return;
|
||
el.innerHTML = '<div class="flash err">' + esc(message) + '</div>';
|
||
}
|
||
|
||
function firstLine(s) {
|
||
if (!s) return '';
|
||
const i = s.indexOf('\n');
|
||
return i >= 0 ? s.slice(0, i) : s;
|
||
}
|
||
|
||
function qs(name) {
|
||
return new URLSearchParams(location.search).get(name) || '';
|
||
}
|
||
|
||
/** 状态徽章:text 为中文标签,code 为原始值(用于配色) */
|
||
function badge(code, text) {
|
||
const display = text != null && text !== '' ? text : (code || '-');
|
||
const s = (code || display || '').toLowerCase();
|
||
let cls = 'info';
|
||
if (s === 'success' || s === 'done') cls = 'ok';
|
||
else if (s === 'failed' || s === 'fail' || s === 'error') cls = 'err';
|
||
else if (s === 'skipped' || s === 'warning' || s === 'waiting' || s === 'pending') cls = 'warn';
|
||
else if (s === 'running' || s === 'pulling') cls = 'info';
|
||
return `<span class="badge ${cls}">${esc(display)}</span>`;
|
||
}
|
||
|
||
function esc(s) {
|
||
const d = document.createElement('div');
|
||
d.textContent = s == null ? '' : String(s);
|
||
return d.innerHTML;
|
||
}
|
||
|
||
function fmtTime(nt) {
|
||
if (!nt || !nt.Valid || !nt.Time) return '-';
|
||
try { return new Date(nt.Time).toLocaleString(); } catch { return nt.Time; }
|
||
}
|
||
|
||
function fmtTimeAny(t) {
|
||
if (!t) return '-';
|
||
if (typeof t === 'string') {
|
||
try { return new Date(t).toLocaleString(); } catch { return t; }
|
||
}
|
||
return fmtTime(t);
|
||
}
|
||
|
||
async function loadMeta() {
|
||
try {
|
||
const m = await api('/api/meta');
|
||
const el = document.getElementById('meta-footer');
|
||
if (el) {
|
||
el.innerHTML = `日志目录: ${esc(m.logDir)} | MySQL: ${m.mysqlOk ? '<span class="badge ok">已连接</span>' : '<span class="badge err">' + esc(m.mysqlError || '未连接') + '</span>'}`;
|
||
}
|
||
return m;
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function fillStepSelect(sel, options, includeAll) {
|
||
if (!sel || !options) return;
|
||
sel.innerHTML = options.map(o =>
|
||
`<option value="${esc(o.value)}">${esc(o.label)}</option>`
|
||
).join('');
|
||
}
|
||
|
||
const MAIN_NAV = [
|
||
{ href: '/', id: 'home', label: '首页' },
|
||
{ href: '/config', id: 'config', label: '配置' },
|
||
{ href: '/fileauth', id: 'fileauth', label: '上传凭证' },
|
||
{ href: '/upload', id: 'upload', label: '上传' },
|
||
{ href: '/supervise-step', id: 'supervise-step', label: '处方核销' },
|
||
{ href: '/runs', id: 'runs', label: '流水' },
|
||
{ href: '/logs', id: 'logs', label: '日志' },
|
||
{ href: '/test', id: 'test', label: '测试' },
|
||
];
|
||
|
||
function renderMainNav(activeId) {
|
||
const el = document.getElementById('main-nav');
|
||
if (!el) return;
|
||
el.innerHTML = MAIN_NAV.map(n => {
|
||
const cur = n.id === activeId ? ' style="border-color:var(--accent)"' : '';
|
||
return `<a href="${n.href}"${cur}>${esc(n.label)}</a>`;
|
||
}).join('');
|
||
}
|
||
|
||
async function retryRecord(id, onOk) {
|
||
await api('/api/runs/retry', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id: Number(id) }),
|
||
});
|
||
if (onOk) onOk();
|
||
}
|