import { useEffect, useState } from 'react' import toast from 'react-hot-toast' import { Activity, Copy } from 'lucide-react' import { tasksAPI, type BackgroundTask, type TasksSnapshot } from '../api/tasks' import { TorrentTaskTable, TranscodeTaskTable } from './TaskRuntimeTables' const metricLabels: Record = { organized: '新增', replaced: '替换', reclassified: '纠偏', skipped: '跳过', errors: '错误', scans: '扫描库', scan_visited: '访问', scan_added: '入库', scan_updated: '更新', scan_removed: '移除', scan_errors: '扫描错误', scrapes: '刮削库', scrape_matched: '匹配', scrape_processed: '刮削处理', scrape_skipped: '刮削跳过', scrape_errors: '刮削错误', skip_already_organized: '已在目标', skip_duplicate_in_library: '已入库去重', skip_target_file_exists: '目标已存在', skip_sample_trailer_clip: '样片过滤', skip_duplicate_exists: '重复跳过', skip_target_exists: '目标已存在', visited: '访问', added: '入库', updated: '更新', probed: '探测', local_metadata: '本地元数据', removed: '移除', matched: '匹配', processed: '处理', queued: '排队', } function formatMetrics(metrics?: Record): string { if (!metrics) return '' return Object.entries(metrics) .filter(([, value]) => Number.isFinite(value) && value !== 0) .map(([key, value]) => `${metricLabels[key] ?? key} ${value}`) .join(' · ') } function hasTaskIssues(task: BackgroundTask): boolean { return Boolean(task.metrics?.errors || task.metrics?.scan_errors || task.metrics?.scrape_errors) } function statusBadge(task: BackgroundTask) { if (task.status === 'failed') { return failed } if (hasTaskIssues(task)) { return issues } if (task.status === 'completed') { return done } return running } function taskCopyText(task: BackgroundTask): string { const lines = [ `任务: ${task.name}`, `状态: ${task.status}${hasTaskIssues(task) ? ' (issues)' : ''}`, `阶段: ${task.stage || '-'}`, `来源: ${task.source_path || '-'}`, `目标: ${task.dest_path || '-'}`, `消息: ${task.error || task.message || '-'}`, ] const metrics = formatMetrics(task.metrics) if (metrics) lines.push(`指标: ${metrics}`) if (task.details?.length) { lines.push('详情:') lines.push(...task.details) } return lines.join('\n') } async function copyTask(task: BackgroundTask) { try { await navigator.clipboard.writeText(taskCopyText(task)) toast.success('任务详情已复制') } catch { toast.error('复制失败,请手动选中详情文本') } } function BackgroundTaskTable({ tasks, empty }: { tasks: BackgroundTask[]; empty: string }) { if (tasks.length === 0) return

{empty}

return ( {tasks.map((task) => ( ))}
任务 阶段 状态 结果 时间
{task.name}
{task.source_path || task.dest_path || task.message || '-'}
{task.stage || '-'} {statusBadge(task)}
{task.error || task.message || '-'}
{formatMetrics(task.metrics) && (
{formatMetrics(task.metrics)}
)} {task.details && task.details.length > 0 && (
{task.details.map((line, index) => (
{line}
))}
)}
{new Date(task.finished_at || task.updated_at || task.started_at).toLocaleTimeString()}
) } // TasksPage shows everything the backend is doing right now: ffmpeg // transcodes + qBittorrent downloads. Refreshes every 3 s. export function TasksPage() { const [snap, setSnap] = useState(null) useEffect(() => { let cancelled = false const tick = () => tasksAPI.snapshot().then((s) => { if (!cancelled) setSnap(s) }) void tick() const id = window.setInterval(tick, 3_000) return () => { cancelled = true window.clearInterval(id) } }, []) if (!snap) return

加载中…

const torrents = snap.torrents ?? [] const background = snap.background_tasks ?? { active: [], recent: [] } return (

实时任务

整理 / 重命名 / 入库 / 刮削任务

运行中

最近完成

转码任务

下载任务

) }