import { useState, useEffect } from 'react'; import { cn } from '@/lib/utils'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Checkbox } from '@/components/ui/checkbox'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Search, Plus, MoreHorizontal, Terminal, Trash2, Edit, Download, Upload, Server as ServerIcon, Cloud, FileSpreadsheet, RefreshCw, } from 'lucide-react'; import { serverApi } from '@/services/api'; import { ServerStatsCards } from './ServerStatsCards'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { toast } from 'sonner'; import type { Server as ServerType } from '@/types'; import { CloudServerImport } from './CloudServerImport'; import { TerminalDialog } from './TerminalDialog'; import { ExcelServerImport } from './ExcelServerImport'; import { ServerForm, EMPTY_FORM, type ServerFormData } from './ServerForm'; export function ServerManager() { const confirm = useConfirm(); const [servers, setServers] = useState([]); const [loading, setLoading] = useState(true); const [selectedServers, setSelectedServers] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); const [cloudImportOpen, setCloudImportOpen] = useState(false); const [excelImportOpen, setExcelImportOpen] = useState(false); // 创建/编辑表单状态 const [formOpen, setFormOpen] = useState(false); const [editingId, setEditingId] = useState(null); const [formData, setFormData] = useState(EMPTY_FORM); const [formSaving, setFormSaving] = useState(false); const [detailsOpen, setDetailsOpen] = useState(false); // 终端 Dialog 状态 const [terminalOpen, setTerminalOpen] = useState(false); const [terminalServer, setTerminalServer] = useState<{ id: number; name: string; ip: string } | null>(null); const openTerminal = (server: any) => { setTerminalServer({ id: server.id, name: server.hostname, ip: server.ip }); setTerminalOpen(true); }; const loadData = async () => { try { setLoading(true); const res: any = await serverApi.getServers(); setServers(res?.list || []); } catch (e) { console.error('加载服务器列表失败:', e); setServers([]); } finally { setLoading(false); } }; useEffect(() => { loadData(); }, []); // 解析标签:后端 tags 可能是 JSON 字符串、逗号分隔字符串或数组 const getTags = (server: any): string[] => { const tags = server.tags; if (!tags) return []; if (Array.isArray(tags)) return tags; try { const parsed = JSON.parse(tags); return Array.isArray(parsed) ? parsed : [String(tags)]; } catch { return String(tags) .split(',') .map((t: string) => t.trim()) .filter(Boolean); } }; const filteredServers = servers.filter((server) => { const matchesSearch = (server.hostname || '').toLowerCase().includes(searchQuery.toLowerCase()) || (server.ip || '').includes(searchQuery) || (server.public_ip || '').includes(searchQuery); const matchesStatus = statusFilter === 'all' || server.status === statusFilter; return matchesSearch && matchesStatus; }); const toggleSelectAll = () => { if (selectedServers.length === filteredServers.length) { setSelectedServers([]); } else { setSelectedServers(filteredServers.map((s) => s.id)); } }; const toggleSelect = (id: string) => { if (selectedServers.includes(id)) { setSelectedServers(selectedServers.filter((s) => s !== id)); } else { setSelectedServers([...selectedServers, id]); } }; const handleDelete = async (id: string) => { const server = servers.find((s) => String(s.id) === String(id)); const ok = await confirm({ title: '删除服务器', description: `确定删除「${server?.hostname || id}」吗?此操作不可撤销。`, confirmText: '删除', variant: 'destructive', }); if (!ok) return; try { await serverApi.deleteServer(id); toast.success('服务器已删除'); loadData(); } catch (e) { console.error('删除服务器失败:', e); toast.error('删除失败'); } }; // 批量删除 const handleBatchDelete = async () => { if (selectedServers.length === 0) return; const ok = await confirm({ title: '批量删除服务器', description: `确定删除选中的 ${selectedServers.length} 台服务器吗?此操作不可撤销。`, confirmText: '批量删除', variant: 'destructive', }); if (!ok) return; try { await serverApi.batchDelete(selectedServers.map((id) => Number(id))); toast.success(`已删除 ${selectedServers.length} 台服务器`); setSelectedServers([]); loadData(); } catch (e) { console.error('批量删除失败:', e); toast.error('批量删除失败'); } }; // 打开创建表单 const openCreateForm = () => { setEditingId(null); setFormData(EMPTY_FORM); setDetailsOpen(false); setFormOpen(true); }; // 打开编辑表单 const openEditForm = (server: any) => { setEditingId(server.id); setFormData({ hostname: server.hostname || '', ip: server.ip || '', public_ip: server.public_ip || '', port: String(server.port || 22), username: server.username || 'root', auth_type: server.auth_type || 'password', password: '', os: server.os || '', cpu: String(server.cpu || ''), memory: server.memory ? String(Math.round(server.memory / 1024)) : '', // MB → GB disk: String(server.disk || ''), status: server.status || 'running', tags: server.tags || '', description: server.description || '', }); setDetailsOpen(false); setFormOpen(true); }; // 保存表单 const handleSave = async () => { if (!formData.hostname || !formData.ip) return; try { setFormSaving(true); const payload: Record = { hostname: formData.hostname, ip: formData.ip, public_ip: formData.public_ip || undefined, port: Number(formData.port) || 22, username: formData.username || 'root', auth_type: formData.auth_type || 'password', os: formData.os, cpu: Number(formData.cpu) || 0, memory: (Number(formData.memory) || 0) * 1024, // GB → MB disk: Number(formData.disk) || 0, status: formData.status, tags: formData.tags || undefined, description: formData.description || undefined, }; // 密码仅在填写时提交 if (formData.password) { payload.password = formData.password; } if (editingId) { await serverApi.updateServer(editingId, payload); } else { await serverApi.createServer(payload); } setFormOpen(false); loadData(); } catch (e) { console.error('保存服务器失败:', e); } finally { setFormSaving(false); } }; // 导出 CSV — 将当前服务器列表导出为 CSV 文件 // Logan.Li const handleExport = () => { if (filteredServers.length === 0) { toast.error('没有可导出的数据'); return; } const headers = ['主机名', 'IP地址', '公网IP', '端口', '操作系统', 'CPU(核)', '内存(GB)', '磁盘(GB)', '状态', '标签']; const rows = filteredServers.map((s) => { const memGB = s.memory ? Math.round(s.memory / 1024) : 0; const tags = getTags(s).join(';'); return [s.hostname || '', s.ip || '', s.public_ip || '', s.port || 22, s.os || '', s.cpu || 0, memGB, s.disk || 0, s.status || '', tags]; }); const csv = [headers, ...rows] .map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(',')) .join('\n'); const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `servers_${new Date().toISOString().slice(0, 10)}.csv`; a.click(); URL.revokeObjectURL(url); toast.success(`已导出 ${filteredServers.length} 台服务器`); }; const getStatusBadge = (status: ServerType['status']) => { switch (status) { case 'running': return (
运行中 ); case 'stopped': return ( 已停止 ); case 'maintenance': return (
维护中 ); case 'offline': return ( 离线 ); default: return null; } }; const colSpanCount = 8; return (
{/* Page Header */}

服务器管理

管理云平台服务器资产

选择导入方式 setCloudImportOpen(true)} > 云主机导入 从云平台同步 setExcelImportOpen(true)} > Excel导入 批量导入服务器
{/* Stats Cards */} {/* Filters & Table */} {/* Toolbar */}
setSearchQuery(e.target.value)} className="pl-9 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]" />
{selectedServers.length > 0 && (
已选择 {selectedServers.length} 项
)}
0 } onCheckedChange={toggleSelectAll} className="border-[var(--dg-border)]" /> 主机名 IP地址 公网IP 状态 配置 使用率 标签 操作 {loading ? ( 加载中... ) : filteredServers.length === 0 ? ( 暂无服务器数据 ) : ( filteredServers.map((server) => { const cpuUsage = Number(server.cpu_usage || 0); const memoryUsage = Number(server.memory_usage || 0); const tags = getTags(server); const memGB = server.memory ? Math.round(server.memory / 1024) : 0; return ( toggleSelect(server.id)} className="border-[var(--dg-border)]" />

{server.hostname}

{server.os}

{server.ip} {server.public_ip ? ( {server.public_ip} ) : ( - )} {getStatusBadge(server.status)}
{server.cpu}核 / {memGB}GB / {server.disk}GB
CPU
80 ? 'bg-red-400' : cpuUsage > 60 ? 'bg-amber-400' : 'bg-emerald-400' )} style={{ width: `${cpuUsage}%` }} />
{cpuUsage.toFixed(1)}%
内存
80 ? 'bg-red-400' : memoryUsage > 60 ? 'bg-amber-400' : 'bg-emerald-400' )} style={{ width: `${memoryUsage}%` }} />
{memoryUsage.toFixed(1)}%
{tags.slice(0, 3).map((tag) => ( {tag} ))} {tags.length > 3 && ( +{tags.length - 3} )}
操作 openEditForm(server)} > 编辑 openTerminal(server)} > 终端 handleDelete(server.id)} > 删除
); }) )}
{/* 创建/编辑服务器 Dialog */} {/* Import Dialogs */} { setCloudImportOpen(false); loadData(); }} /> { setExcelImportOpen(false); loadData(); }} /> {/* 终端 Dialog */}
); }