import React, { useState, useEffect } from 'react'; import { Plus, Search, ArrowUpDown, Trash2, KeyRound, ChevronLeft, ChevronRight, ChevronsRight, Server, RefreshCw } from 'lucide-react'; import JobCard from './JobCard'; import { fetchJobs, prioritizeJob, deleteJob, restoreJob, resetGpu } from '../api'; import { getToken, getH3Base } from '../utils'; import { useToast } from './Toast'; import Modal from './Modal'; export default function Sidebar({ selectedId, onSelectTask, onCreateTask, onRerunTask, backendInfo, onRefreshBackends, onManageToken, }) { const { addToast } = useToast(); const [jobs, setJobs] = useState([]); const [totalJobs, setTotalJobs] = useState(0); const [page, setPage] = useState(1); const [searchQ, setSearchQ] = useState(''); const [statusFilter, setStatusFilter] = useState(''); const [kindFilter, setKindFilter] = useState(''); const [sortOrder, setSortOrder] = useState('desc'); const [viewingTrash, setViewingTrash] = useState(false); const [trashJobs, setTrashJobs] = useState([]); const [loading, setLoading] = useState(false); // 模态弹窗状态 const [confirmModal, setConfirmModal] = useState({ isOpen: false, title: '', content: '', isDanger: false, onConfirm: null }); // 刷新任务列表 const loadJobs = async (targetPage = page) => { setLoading(true); try { if (viewingTrash) { const data = await fetchJobs({ trash: true }); setTrashJobs(data.jobs || []); } else { const data = await fetchJobs({ page: targetPage, limit: 20, q: searchQ.trim(), status: statusFilter, kind: kindFilter, sort: sortOrder, }); setJobs(data.jobs || []); setTotalJobs(data.total || 0); } } catch (e) { console.error(e); } finally { setLoading(false); } }; useEffect(() => { loadJobs(1); setPage(1); }, [searchQ, statusFilter, kindFilter, sortOrder, viewingTrash]); // 定时每 5 秒轮询主列表 useEffect(() => { const timer = setInterval(() => { if (!viewingTrash) loadJobs(page); }, 5000); return () => clearInterval(timer); }, [page, searchQ, statusFilter, kindFilter, sortOrder, viewingTrash]); const handlePrioritize = async (id) => { try { const res = await prioritizeJob(id); addToast(`任务 #${id.substring(0, 8)} 已插队到第 ${res.queue_position} 位`, 'success'); loadJobs(page); } catch (e) { addToast('插队失败: ' + e.message, 'error'); } }; const handleDelete = async (id) => { try { await deleteJob(id); addToast('已将任务移入回收站', 'info'); if (selectedId === id) onCreateTask(); loadJobs(page); } catch (e) { addToast('删除失败: ' + e.message, 'error'); } }; const handleRestore = async (id) => { try { await restoreJob(id); addToast('任务已从回收站恢复', 'success'); loadJobs(); } catch (e) { addToast('恢复失败: ' + e.message, 'error'); } }; const handleResetGpu = () => { setConfirmModal({ isOpen: true, title: '重置整个服务的 GPU 显存', content: '确定要触发硬重启?\n这会打断当前正在执行的任务(如果有),并重启后端的 Ray 与推理进程以回收显存泄露。\n大约需要耗时 15-20 秒,重置完成前无法提交新任务。', isDanger: true, onConfirm: async () => { setConfirmModal((prev) => ({ ...prev, isOpen: false })); try { await resetGpu(); addToast('已触发 GPU 重启,正在回收显存…', 'info'); if (onRefreshBackends) onRefreshBackends(); } catch (e) { addToast('重置失败: ' + e.message, 'error'); } }, }); }; const totalPages = Math.max(1, Math.ceil(totalJobs / 20)); // 跳到指定页(越界/非数字时夹到 1..totalPages) const gotoPage = (v) => { const n = Math.min(totalPages, Math.max(1, parseInt(v, 10) || 1)); setPage(n); loadJobs(n); }; return ( ); }