Files

126 lines
6.6 KiB
JavaScript

export const stateNames = {
queued: '排队中', running: '运行中', succeeded: '进程成功', failed: '失败',
cancelled: '已取消', timed_out: '已超时', interrupted: '已中断',
ready: '可运行', needs_configuration: '待配置', missing: '源文件缺失'
};
export const active = run => ['queued', 'running'].includes(run.state);
export const enc = value => encodeURIComponent(value);
export const scriptUrl = id => `/api/scripts/${enc(id)}`;
export const runUrl = id => `/api/runs/${enc(id)}`;
export const date = value => value ? new Date(value).toLocaleString('zh-CN', {hour12: false}) : '—';
export function bytes(value) {
if (!Number.isFinite(Number(value))) return '—';
const size = Number(value);
if (size < 1024) return `${size} B`;
if (size < 1048576) return `${(size / 1024).toFixed(1)} KB`;
return `${(size / 1048576).toFixed(1)} MB`;
}
export function el(tag, attributes = {}, ...children) {
const node = document.createElement(tag);
for (const [key, value] of Object.entries(attributes)) {
if (key === 'class') node.className = value;
else if (key.startsWith('on')) node.addEventListener(key.slice(2), value);
else if (key === 'text') node.textContent = value;
else if (['checked', 'disabled', 'hidden', 'selected', 'value'].includes(key)) node[key] = value;
else if (value !== undefined && value !== null) node.setAttribute(key, value);
}
for (const child of children.flat(Infinity)) {
if (child !== null && child !== undefined) node.append(child instanceof Node ? child : document.createTextNode(String(child)));
}
return node;
}
export const button = (text, onClick, style = 'secondary') => el('button', {type: 'button', class: `button ${style}`, onclick: onClick}, text);
export const link = (text, href, className = '') => el('a', {href, class: className}, text);
export const badge = (state, text = stateNames[state] || state) => el('span', {class: `badge ${state}`}, text);
export const note = (text, tone = '') => el('div', {class: `notice ${tone}`}, text);
export const empty = (title, detail) => el('div', {class: 'empty'}, el('span', {class: 'empty-icon', 'aria-hidden': 'true'}, '▤'), el('h3', {}, title), el('p', {}, detail));
export const loading = (text = '正在加载…') => el('div', {class: 'loading', role: 'status'}, text);
let fieldCounter = 0;
export function field(text, control, hint = '') {
const hintId = `field-hint-${++fieldCounter}`;
control.setAttribute('aria-label', text);
if (hint) control.setAttribute('aria-describedby', hintId);
return el('label', {class: 'field'}, el('span', {class: 'field-label'}, text), control, hint ? el('small', {id: hintId}, hint) : null);
}
export const input = (attributes = {}) => el('input', {class: 'input', ...attributes});
export const textarea = (attributes = {}) => el('textarea', {class: 'input', rows: 3, ...attributes});
export const select = (label, options, value = '') => el('select', {class: 'input', 'aria-label': label}, options.map(([id, text]) => el('option', {value: id, selected: String(id) === String(value)}, text)));
export function heading(title, subtitle, actions = []) {
return el('div', {class: 'page-heading'}, el('div', {}, el('h1', {}, title), el('p', {}, subtitle)), el('div', {class: 'actions'}, actions));
}
export function kv(items) {
return el('dl', {class: 'metadata'}, items.map(([label, value]) => [el('dt', {}, label), el('dd', {}, value ?? '—')]));
}
export function errorBox(error, retry) {
return el('div', {class: 'notice danger', role: 'alert'}, el('span', {}, error.message || '请求失败,请稍后重试。'), retry ? button('重试', retry, 'small') : null);
}
export async function api(path, options = {}, signal) {
const controller = new AbortController();
const abort = () => controller.abort();
if (signal?.aborted) controller.abort();
else signal?.addEventListener('abort', abort, {once: true});
const timer = setTimeout(abort, options.body instanceof FormData ? 120000 : 20000);
const headers = {...options.headers};
if (options.method && options.method !== 'GET') headers['X-XAS-Console'] = '1';
let body = options.body;
if (body && !(body instanceof FormData)) {
headers['Content-Type'] = 'application/json';
body = JSON.stringify(body);
}
try {
const response = await fetch(path, {...options, body, headers, signal: controller.signal, credentials: 'same-origin'});
const data = await response.json().catch(() => null);
if (!response.ok) throw new Error(typeof data?.detail === 'string' ? data.detail : `请求未完成(HTTP ${response.status})。`);
if (data === null) throw new Error('服务器返回了无法读取的数据。');
return data;
} catch (error) {
if (signal?.aborted) throw error;
if (error.name === 'AbortError') throw new Error('请求超时,操作结果尚未确认。请先核对记录;运行请求重试会复用原编号。');
if (error instanceof TypeError) throw new Error('无法连接控制台,请检查网络后重试。');
throw error;
} finally {
clearTimeout(timer);
signal?.removeEventListener('abort', abort);
}
}
let toastTimer;
export function toast(text) {
const target = document.querySelector('#toast');
target.textContent = text;
target.hidden = false;
clearTimeout(toastTimer);
toastTimer = setTimeout(() => { target.hidden = true; }, 5000);
}
export function dialog(title, content) {
const close = button('关闭', () => panel.close(), 'small');
close.setAttribute('aria-label', '关闭对话框');
const panel = el('dialog', {class: 'dialog', 'aria-label': title}, el('div', {class: 'dialog-header'}, el('h2', {}, title), close), content);
panel.addEventListener('close', () => panel.remove(), {once: true});
document.body.append(panel);
panel.showModal();
return panel;
}
export function pageContext(main) {
const controller = new AbortController();
const timers = new Set();
const context = {
main, signal: controller.signal,
get alive() { return !controller.signal.aborted; },
show(...nodes) { if (context.alive) { main.replaceChildren(...nodes.filter(node => node !== null && node !== undefined)); main.setAttribute('aria-busy', 'false'); } },
async repeat(callback, interval = 2500) {
if (!context.alive) return;
if (document.visibilityState === 'visible' && await callback() === false) return;
if (context.alive) {
const timer = setTimeout(() => { timers.delete(timer); context.repeat(callback, interval); }, interval);
timers.add(timer);
}
},
dispose() {
controller.abort();
timers.forEach(clearTimeout);
document.querySelectorAll('dialog').forEach(panel => panel.close());
}
};
return context;
}