'use strict';
(function () {
const state = {
initialized: false,
loading: false,
root: '',
tasks: [],
filter: 'all',
timer: null,
};
const STATUS_LABELS = {
queued: '排队中',
submitted: '已提交',
running: '进行中',
success: '已完成',
failed: '失败',
canceled: '已取消',
timeout: '已超时',
};
const KIND_LABELS = {
logo: 'LOGO',
banner: '海报',
signage: '招牌',
sticker: '贴纸',
dish: '菜品',
};
function el(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
function formatTime(value) {
if (!value) return '';
const date = new Date(value);
const pad = (n) => String(n).padStart(2, '0');
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
function formatBytes(size) {
if (!Number.isFinite(size) || size <= 0) return '';
if (size < 1024 * 1024) return `${Math.round(size / 1024)} KB`;
return `${(size / 1024 / 1024).toFixed(1)} MB`;
}
function progressValue(task) {
const parsed = Number.parseInt(String(task.progress || '').replace(/[^\d]/g, ''), 10);
if (task.status === 'success') return 100;
if (Number.isFinite(parsed)) return Math.min(100, Math.max(0, parsed));
return task.status === 'queued' ? 0 : 35;
}
function getFilteredTasks() {
if (state.filter === 'all') return state.tasks;
if (state.filter === 'running') {
return state.tasks.filter((task) => task.status === 'running' || task.status === 'submitted');
}
return state.tasks.filter((task) => task.status === state.filter);
}
function setCount(id, value) {
document.getElementById(id).textContent = String(value || 0);
}
function renderSummary() {
const counts = { all: 0, running: 0, queued: 0, success: 0, failed: 0 };
state.tasks.forEach((task) => {
counts.all += 1;
if (task.status === 'running' || task.status === 'submitted') counts.running += 1;
else if (task.status === 'queued') counts.queued += 1;
else if (task.status === 'success') counts.success += 1;
else if (task.status === 'failed' || task.status === 'timeout') counts.failed += 1;
});
setCount('taskCountAll', counts.all);
setCount('taskCountRunning', counts.running);
setCount('taskCountQueued', counts.queued);
setCount('taskCountSuccess', counts.success);
setCount('taskCountFailed', counts.failed);
document.getElementById('taskSummary').textContent = state.loading
? '正在加载任务…'
: `共 ${counts.all} 个任务 · ${counts.running} 个进行中 · ${counts.failed} 个失败`;
}
function buildCrawlCard(task) {
const card = el('article', `task-card status-${task.status}`);
const main = el('div', 'task-main');
const titleRow = el('div', 'task-title-row');
const title = el('div', 'task-title');
title.textContent = task.name || '门店采集';
const badge = el('span', `badge task-badge ${task.status}`);
badge.textContent = { running: '进行中', success: '已完成', failed: '已停止/失败' }[task.status] || task.status;
titleRow.append(title, badge);
const progress = el('div', 'task-progress');
const bar = el('span', 'task-progress-bar');
bar.style.width = `${progressValue(task)}%`;
progress.appendChild(bar);
const meta = el('div', 'task-meta');
meta.textContent = [
task.detail || '',
task.createdAt ? `开始 ${formatTime(task.createdAt)}` : '',
task.finishedAt ? `结束 ${formatTime(task.finishedAt)}` : '',
].filter(Boolean).join(' · ');
main.append(titleRow, progress, meta);
if (task.warning) main.appendChild(el('div', 'task-notice warning', task.warning));
const actions = el('div', 'task-actions');
const right = el('div', 'task-status-detail');
right.textContent = task.progress || '';
actions.appendChild(right);
card.append(main, actions);
return card;
}
function buildTaskCard(task) {
if (task.type === 'crawl') return buildCrawlCard(task);
const card = el('article', `task-card status-${task.status}`);
const main = el('div', 'task-main');
const titleRow = el('div', 'task-title-row');
const title = el('div', 'task-title');
title.textContent = `${task.shopName || '未知门店'} · ${KIND_LABELS[task.kind] || task.name || task.kind}`;
const badge = el('span', `badge task-badge ${task.status}`);
badge.textContent = STATUS_LABELS[task.status] || task.status;
titleRow.append(title, badge);
const meta = el('div', 'task-meta');
meta.textContent = [
task.model || '默认模型',
formatTime(task.createdAt),
task.cost !== null && task.cost !== undefined ? `费用 ${task.cost}` : '',
task.providerTaskId ? `任务 ${String(task.providerTaskId).slice(0, 10)}…` : '',
].filter(Boolean).join(' · ');
const progress = el('div', 'task-progress');
const bar = el('span', 'task-progress-bar');
bar.style.width = `${progressValue(task)}%`;
progress.appendChild(bar);
main.append(titleRow, progress, meta);
if (task.warning) main.appendChild(el('div', 'task-notice warning', task.warning));
if (task.error) main.appendChild(el('div', 'task-notice error', task.error));
const actions = el('div', 'task-actions');
const right = el('div', 'task-status-detail');
right.textContent = `${task.progress || progressValue(task) + '%'}${task.fileSizeBytes ? ` · ${formatBytes(task.fileSizeBytes)}` : ''}`;
actions.appendChild(right);
if (task.resultUrl) {
const open = el('a', 'btn btn-ghost btn-small icon-btn', '查看结果');
open.innerHTML = '';
open.title = '查看结果';
open.setAttribute('aria-label', '查看结果');
open.href = task.resultUrl;
open.target = '_blank';
open.rel = 'noopener noreferrer';
actions.appendChild(open);
}
card.append(main, actions);
return card;
}
function renderTasks() {
const list = document.getElementById('taskList');
const tasks = getFilteredTasks();
list.innerHTML = '';
list.classList.toggle('empty', tasks.length === 0);
if (!tasks.length) {
const empty = el('div', 'task-empty');
const icon = el('div', 'task-empty-icon');
icon.innerHTML = '';
const text = el('p', '', state.loading ? '正在加载任务' : '暂无匹配任务');
const help = el('small', '', state.loading ? '请稍候' : '提交生成任务或开始门店采集后会自动显示在这里');
empty.append(icon, text, help);
list.appendChild(empty);
return;
}
tasks.forEach((task) => list.appendChild(buildTaskCard(task)));
}
function render() {
renderSummary();
renderTasks();
}
async function loadTasks() {
if (state.loading) return;
state.loading = true;
render();
try {
if (!state.root) {
const rootResponse = await fetch('/api/default-root');
const rootData = await rootResponse.json();
state.root = rootData.root;
}
const url = `/api/tasks?root=${encodeURIComponent(state.root)}`;
const response = await fetch(url);
const data = await response.json();
if (!response.ok) throw new Error(data.error || '任务列表加载失败');
state.tasks = data.tasks || [];
} catch (error) {
state.tasks = [];
document.getElementById('taskSummary').textContent = error.message;
} finally {
state.loading = false;
render();
}
}
function bindEvents() {
document.getElementById('btnRefreshTasks').addEventListener('click', loadTasks);
document.getElementById('taskFilters').addEventListener('click', (event) => {
const button = event.target.closest('[data-filter]');
if (!button) return;
state.filter = button.dataset.filter;
document.querySelectorAll('#taskFilters .history-filter').forEach((item) => {
item.classList.toggle('active', item === button);
});
renderTasks();
});
}
window.shanTasksInit = function () {
if (!state.initialized) {
state.initialized = true;
bindEvents();
}
loadTasks();
if (!state.timer) {
state.timer = setInterval(() => {
if (document.getElementById('viewTasks').classList.contains('active')) loadTasks();
}, 3000);
}
};
})();