feat: 清除全部mock数据,Ansible/日志/告警中心/角色权限接真实API;移除SQL审计(不属于项目定位)

This commit is contained in:
attacker committed 2026-06-15 01:59:28 +08:00
1 parent 73b5a48df9
commit df697e0b25
12 files changed
+593 -903

No files matched your search

+12 -71
View File
@@ -1,4 +1,5 @@
import { useState, useCallback } from 'react';
import { toast } from 'sonner';
import { cn } from '@/lib/utils';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -62,82 +63,13 @@ export function BatchImport() {
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
// 模拟文件解析
generateMockRecords();
toast.info('文件解析功能开发中');
}, []);
const generateMockRecords = () => {
const mockRecords: ImportRecord[] = [
{
id: '1',
hostname: 'web-server-03',
ip: '192.168.1.111',
os: 'CentOS 7.9',
cpu: '16',
memory: '64',
disk: '500',
region: '华北-北京',
tags: 'web,production',
status: 'valid',
},
{
id: '2',
hostname: 'app-server-03',
ip: '192.168.1.112',
os: 'Ubuntu 22.04 LTS',
cpu: '32',
memory: '128',
disk: '1000',
region: '华东-上海',
tags: 'app,production',
status: 'valid',
},
{
id: '3',
hostname: 'cache-server-02',
ip: '192.168.1.113',
os: 'Ubuntu 20.04 LTS',
cpu: '16',
memory: '64',
disk: '200',
region: '华南-广州',
tags: 'cache,redis',
status: 'valid',
},
{
id: '4',
hostname: '',
ip: '192.168.1.114',
os: 'CentOS 8.5',
cpu: '8',
memory: '32',
disk: '500',
region: '华北-北京',
tags: 'test',
status: 'invalid',
message: '主机名不能为空',
},
{
id: '5',
hostname: 'invalid-server',
ip: 'invalid-ip',
os: 'Ubuntu 22.04 LTS',
cpu: '16',
memory: '64',
disk: '500',
region: '华东-上海',
tags: 'dev',
status: 'invalid',
message: 'IP地址格式不正确',
},
];
setRecords(mockRecords);
};
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
generateMockRecords();
toast.info('文件解析功能开发中');
}
};
@@ -238,6 +170,15 @@ export function BatchImport() {
</CardContent>
</Card>
{/* Empty State */}
{records.length === 0 && (
<div className="flex flex-col items-center justify-center py-12 text-slate-500">
<Server className="h-10 w-10 mb-3 opacity-30" />
<p>暂无导入记录</p>
<p className="text-xs mt-1">上传文件后将在此预览数据</p>
</div>
)}
{/* Import Stats */}
{records.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
+223 -335
View File
@@ -1,9 +1,8 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
Select,
SelectContent,
@@ -23,135 +22,73 @@ import {
Server,
Plus,
Search,
MapPin,
Zap,
Grid3X3,
List,
MapPin,
Cpu,
MemoryStick,
HardDrive,
Network,
Router,
Shield,
Database,
Monitor,
Edit,
Trash2,
Eye,
ArrowUp,
ArrowDown,
Edit,
} from 'lucide-react';
import { cabinets, devices } from '@/mocks/data';
import type { Cabinet, Device } from '@/types';
import { serverApi } from '@/services/api';
import type { Server as ServerType } from '@/types';
export function CabinetView() {
const [viewMode, setViewMode] = useState<'room' | 'cabinet' | 'list'>('room');
const [selectedRoom, setSelectedRoom] = useState<string>('all');
const [selectedCabinet, setSelectedCabinet] = useState<Cabinet | null>(null);
const [selectedDevice, setSelectedDevice] = useState<Device | null>(null);
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [searchQuery, setSearchQuery] = useState('');
const [servers, setServers] = useState<ServerType[]>([]);
const [loading, setLoading] = useState(true);
const [selectedServer, setSelectedServer] = useState<ServerType | null>(null);
const rooms = Array.from(new Set(cabinets.map((c) => c.room)));
const filteredCabinets = cabinets.filter((cabinet) => {
const matchesRoom = selectedRoom === 'all' || cabinet.room === selectedRoom;
const matchesSearch = cabinet.name.toLowerCase().includes(searchQuery.toLowerCase());
return matchesRoom && matchesSearch;
// 拉取服务器列表 // Logan.Li
const fetchServers = async () => {
setLoading(true);
try {
const resp = await serverApi.getServers({ page: 1, page_size: 200 });
setServers((resp?.list ?? []) as ServerType[]);
} catch {
setServers([]);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchServers();
}, []);
const filteredServers = servers.filter((srv) => {
const matchesStatus = statusFilter === 'all' || srv.status === statusFilter;
const matchesSearch =
srv.hostname?.toLowerCase().includes(searchQuery.toLowerCase()) ||
srv.ip?.toLowerCase().includes(searchQuery.toLowerCase());
return matchesStatus && matchesSearch;
});
const getCabinetDevices = (cabinetId: string) => {
return devices.filter((d) => d.cabinetId === cabinetId).sort((a, b) => b.startU - a.startU);
};
const getDeviceIcon = (type: Device['type']) => {
const icons = {
server: <Server className="h-4 w-4" />,
switch: <Network className="h-4 w-4" />,
router: <Router className="h-4 w-4" />,
firewall: <Shield className="h-4 w-4" />,
storage: <Database className="h-4 w-4" />,
other: <Monitor className="h-4 w-4" />,
const getStatusColor = (status: string) => {
const colors: Record<string, string> = {
running: 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10',
stopped: 'border-red-500/30 text-red-400 bg-red-500/10',
maintenance: 'border-amber-500/30 text-amber-400 bg-amber-500/10',
offline: 'border-slate-500/30 text-slate-400 bg-slate-500/10',
};
return icons[type];
return colors[status] ?? colors.offline;
};
const getDeviceColor = (type: Device['type']) => {
const colors = {
server: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
switch: 'bg-purple-500/20 text-purple-400 border-purple-500/30',
router: 'bg-cyan-500/20 text-cyan-400 border-cyan-500/30',
firewall: 'bg-red-500/20 text-red-400 border-red-500/30',
storage: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30',
other: 'bg-slate-500/20 text-slate-400 border-slate-500/30',
const getStatusText = (status: string) => {
const map: Record<string, string> = {
running: '运行中',
stopped: '已停止',
maintenance: '维护中',
offline: '离线',
};
return colors[type];
return map[status] ?? status;
};
const getStatusColor = (status: Cabinet['status']) => {
const colors = {
active: 'border-emerald-500/50 bg-emerald-500/5',
empty: 'border-slate-600 bg-slate-800/30',
disabled: 'border-slate-700 bg-slate-800/50 opacity-50',
};
return colors[status];
};
// 渲染机柜正面视图
const renderCabinetFront = (cabinet: Cabinet) => {
const cabinetDevices = getCabinetDevices(cabinet.id);
const uSlots: (Device | null)[] = new Array(cabinet.totalU).fill(null);
cabinetDevices.forEach((device) => {
for (let i = 0; i < device.sizeU; i++) {
const uIndex = cabinet.totalU - device.startU - i;
if (uIndex >= 0 && uIndex < cabinet.totalU) {
uSlots[uIndex] = device;
}
}
});
return (
<div className="flex gap-4">
{/* U位编号 */}
<div className="flex flex-col text-xs text-slate-500">
{Array.from({ length: cabinet.totalU }, (_, i) => (
<div key={i} className="h-6 flex items-center justify-end pr-2">
{cabinet.totalU - i}U
</div>
))}
</div>
{/* 机柜 */}
<div className="flex-1 border-2 border-slate-700 rounded-lg overflow-hidden bg-slate-900">
{uSlots.map((device, index) => {
const uNumber = cabinet.totalU - index;
if (device) {
const isFirstU = uNumber === device.startU + device.sizeU - 1;
if (!isFirstU) return null;
return (
<div
key={`device-${device.id}`}
className={`border-b border-slate-800 flex items-center px-3 cursor-pointer hover:opacity-80 transition-opacity ${getDeviceColor(device.type)}`}
style={{ height: `${device.sizeU * 24}px` }}
onClick={() => setSelectedDevice(device)}
>
{getDeviceIcon(device.type)}
<span className="ml-2 text-sm font-medium truncate">{device.name}</span>
<span className="ml-auto text-xs opacity-70">{device.ip}</span>
</div>
);
}
return (
<div
key={`empty-${index}`}
className="h-6 border-b border-slate-800/50 hover:bg-slate-800/30 transition-colors"
/>
);
})}
</div>
</div>
);
};
const getUsageColor = (usage: number) =>
usage > 80 ? 'bg-red-400' : usage > 60 ? 'bg-amber-400' : 'bg-emerald-400';
return (
<div className="space-y-6">
@@ -159,18 +96,12 @@ export function CabinetView() {
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-100">机房机柜视图</h1>
<p className="text-sm text-slate-500 mt-1">数据中心机房与机柜设备管理</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800">
<MapPin className="h-4 w-4 mr-2" />
机房管理
</Button>
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
<Plus className="h-4 w-4 mr-2" />
添加设备
</Button>
<p className="text-sm text-slate-500 mt-1">数据中心服务器资产概览</p>
</div>
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
<Plus className="h-4 w-4 mr-2" />
添加设备
</Button>
</div>
{/* Stats Cards */}
@@ -179,36 +110,10 @@ export function CabinetView() {
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">机房数</p>
<p className="text-2xl font-bold text-slate-100">{rooms.length}</p>
<p className="text-sm text-slate-500">服务器总数</p>
<p className="text-2xl font-bold text-slate-100">{servers.length}</p>
</div>
<div className="p-3 rounded-lg bg-blue-500/10 text-blue-400">
<MapPin className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">机柜数</p>
<p className="text-2xl font-bold text-slate-100">{cabinets.length}</p>
</div>
<div className="p-3 rounded-lg bg-purple-500/10 text-purple-400">
<Grid3X3 className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">设备数</p>
<p className="text-2xl font-bold text-emerald-400">{devices.length}</p>
</div>
<div className="p-3 rounded-lg bg-emerald-500/10 text-emerald-400">
<Server className="h-5 w-5" />
</div>
</div>
@@ -218,13 +123,43 @@ export function CabinetView() {
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">总功率</p>
<p className="text-sm text-slate-500">运行中</p>
<p className="text-2xl font-bold text-emerald-400">
{servers.filter((s) => s.status === 'running').length}
</p>
</div>
<div className="p-3 rounded-lg bg-emerald-500/10 text-emerald-400">
<Grid3X3 className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">维护中</p>
<p className="text-2xl font-bold text-amber-400">
{cabinets.reduce((acc, c) => acc + c.powerUsed, 0).toFixed(1)}kW
{servers.filter((s) => s.status === 'maintenance').length}
</p>
</div>
<div className="p-3 rounded-lg bg-amber-500/10 text-amber-400">
<Zap className="h-5 w-5" />
<Cpu className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">离线</p>
<p className="text-2xl font-bold text-slate-400">
{servers.filter((s) => s.status === 'offline' || s.status === 'stopped').length}
</p>
</div>
<div className="p-3 rounded-lg bg-slate-500/10 text-slate-400">
<HardDrive className="h-5 w-5" />
</div>
</div>
</CardContent>
@@ -237,44 +172,34 @@ export function CabinetView() {
<div className="relative w-64">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
<Input
placeholder="搜索机柜或设备..."
placeholder="搜索主机名或 IP..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
/>
</div>
<Select value={selectedRoom} onValueChange={setSelectedRoom}>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-40 h-9 bg-slate-800/50 border-slate-700 text-slate-200">
<SelectValue placeholder="选择机房" />
<SelectValue placeholder="选择状态" />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
<SelectItem value="all">全部机房</SelectItem>
{rooms.map((room) => (
<SelectItem key={room} value={room}>
{room}
</SelectItem>
))}
<SelectItem value="all">全部状态</SelectItem>
<SelectItem value="running">运行中</SelectItem>
<SelectItem value="stopped">已停止</SelectItem>
<SelectItem value="maintenance">维护中</SelectItem>
<SelectItem value="offline">离线</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-1 bg-slate-800 rounded-lg p-1">
<Button
variant={viewMode === 'room' ? 'secondary' : 'ghost'}
variant={viewMode === 'grid' ? 'secondary' : 'ghost'}
size="sm"
className="h-7"
onClick={() => setViewMode('room')}
>
<MapPin className="h-4 w-4 mr-1" />
机房视图
</Button>
<Button
variant={viewMode === 'cabinet' ? 'secondary' : 'ghost'}
size="sm"
className="h-7"
onClick={() => setViewMode('cabinet')}
onClick={() => setViewMode('grid')}
>
<Grid3X3 className="h-4 w-4 mr-1" />
机柜视图
网格视图
</Button>
<Button
variant={viewMode === 'list' ? 'secondary' : 'ghost'}
@@ -289,176 +214,143 @@ export function CabinetView() {
</div>
{/* View Content */}
{viewMode === 'room' && (
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
{filteredCabinets.map((cabinet) => {
const usagePercent = (cabinet.usedU / cabinet.totalU) * 100;
const powerPercent = (cabinet.powerUsed / cabinet.powerCapacity) * 100;
return (
<Card
key={cabinet.id}
className={`bg-slate-900 border-2 cursor-pointer hover:shadow-lg transition-all ${getStatusColor(cabinet.status)}`}
onClick={() => setSelectedCabinet(cabinet)}
>
<CardContent className="p-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-slate-200">{cabinet.name}</span>
{cabinet.status === 'active' && (
<div className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
)}
</div>
<div className="space-y-2">
<div>
<div className="flex justify-between text-xs text-slate-500 mb-1">
<span>U位</span>
<span>{cabinet.usedU}/{cabinet.totalU}</span>
</div>
<div className="h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${
usagePercent > 80 ? 'bg-red-400' : usagePercent > 60 ? 'bg-amber-400' : 'bg-emerald-400'
}`}
style={{ width: `${usagePercent}%` }}
/>
</div>
{loading ? (
<div className="flex items-center justify-center py-16">
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
</div>
) : filteredServers.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 text-slate-500">
<MapPin className="h-10 w-10 mb-3 opacity-40" />
<p className="text-sm">暂无服务器数据</p>
</div>
) : viewMode === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{filteredServers.map((srv) => (
<Card
key={srv.id}
className="bg-slate-900 border-slate-800 cursor-pointer hover:border-slate-700 transition-all"
onClick={() => setSelectedServer(srv)}
>
<CardContent className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="p-2 rounded-lg bg-blue-500/10">
<Server className="h-4 w-4 text-blue-400" />
</div>
<div>
<div className="flex justify-between text-xs text-slate-500 mb-1">
<span>功率</span>
<span>{cabinet.powerUsed}/{cabinet.powerCapacity}kW</span>
</div>
<div className="h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${
powerPercent > 80 ? 'bg-red-400' : powerPercent > 60 ? 'bg-amber-400' : 'bg-emerald-400'
}`}
style={{ width: `${powerPercent}%` }}
/>
</div>
<p className="text-sm font-medium text-slate-200">{srv.hostname || '--'}</p>
<p className="text-xs text-slate-500">{srv.ip || '--'}</p>
</div>
</div>
<div className="mt-3 flex items-center gap-1">
<Badge variant="outline" className={getStatusColor(srv.status)}>
{getStatusText(srv.status)}
</Badge>
</div>
<div className="space-y-2">
<div>
<div className="flex justify-between text-xs text-slate-500 mb-1">
<span className="flex items-center gap-1">
<Cpu className="h-3 w-3" /> CPU
</span>
<span>{Math.round(srv.cpuUsage ?? 0)}%</span>
</div>
<div className="h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${getUsageColor(srv.cpuUsage ?? 0)}`}
style={{ width: `${srv.cpuUsage ?? 0}%` }}
/>
</div>
</div>
<div>
<div className="flex justify-between text-xs text-slate-500 mb-1">
<span className="flex items-center gap-1">
<MemoryStick className="h-3 w-3" /> 内存
</span>
<span>{Math.round(srv.memoryUsage ?? 0)}%</span>
</div>
<div className="h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${getUsageColor(srv.memoryUsage ?? 0)}`}
style={{ width: `${srv.memoryUsage ?? 0}%` }}
/>
</div>
</div>
<div>
<div className="flex justify-between text-xs text-slate-500 mb-1">
<span className="flex items-center gap-1">
<HardDrive className="h-3 w-3" /> 磁盘
</span>
<span>{Math.round(srv.diskUsage ?? 0)}%</span>
</div>
<div className="h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${getUsageColor(srv.diskUsage ?? 0)}`}
style={{ width: `${srv.diskUsage ?? 0}%` }}
/>
</div>
</div>
</div>
<div className="mt-3 flex flex-wrap items-center gap-1">
<Badge variant="outline" className="text-xs border-slate-700 text-slate-400">
{srv.cpu ?? 0} 核
</Badge>
<Badge variant="outline" className="text-xs border-slate-700 text-slate-400">
{srv.memory ?? 0} GB
</Badge>
{srv.region && (
<Badge variant="outline" className="text-xs border-slate-700 text-slate-400">
{getCabinetDevices(cabinet.id).length} 设备
{srv.region}
</Badge>
</div>
</CardContent>
</Card>
);
})}
</div>
)}
{viewMode === 'cabinet' && selectedCabinet && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<Card className="bg-slate-900 border-slate-800">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-slate-100">{selectedCabinet.name}</CardTitle>
<CardDescription className="text-slate-500">
{selectedCabinet.room} · {selectedCabinet.usedU}/{selectedCabinet.totalU}U · {selectedCabinet.powerUsed}/{selectedCabinet.powerCapacity}kW
</CardDescription>
</div>
<Button variant="ghost" size="sm" onClick={() => setSelectedCabinet(null)}>
返回
</Button>
</div>
</CardHeader>
<CardContent>
{renderCabinetFront(selectedCabinet)}
</CardContent>
</Card>
</div>
<div>
<Card className="bg-slate-900 border-slate-800">
<CardHeader>
<CardTitle className="text-slate-100 text-base">机柜信息</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-xs text-slate-500 mb-1">机柜编号</p>
<p className="text-sm text-slate-200">{selectedCabinet.name}</p>
</div>
<div>
<p className="text-xs text-slate-500 mb-1">所属机房</p>
<p className="text-sm text-slate-200">{selectedCabinet.room}</p>
</div>
<div>
<p className="text-xs text-slate-500 mb-1">位置</p>
<p className="text-sm text-slate-200">第{selectedCabinet.row}排 第{selectedCabinet.column}列</p>
</div>
<div>
<p className="text-xs text-slate-500 mb-1">状态</p>
<Badge variant="outline" className={
selectedCabinet.status === 'active'
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: 'border-slate-500/30 text-slate-400 bg-slate-500/10'
}>
{selectedCabinet.status === 'active' ? '使用中' : selectedCabinet.status === 'empty' ? '空闲' : '停用'}
</Badge>
</div>
)}
</div>
</CardContent>
</Card>
</div>
))}
</div>
)}
{viewMode === 'list' && (
) : (
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-0">
<div className="border-t border-slate-800">
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">设备名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">类型</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">机柜</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">U位</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">主机名</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">IP地址</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">系统</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">区域</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">配置</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">状态</th>
<th className="px-4 py-3 text-right text-xs font-medium text-slate-400">操作</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{devices.map((device) => (
<tr key={device.id} className="hover:bg-slate-800/50">
{filteredServers.map((srv) => (
<tr key={srv.id} className="hover:bg-slate-800/50">
<td className="px-4 py-3">
<div className="flex items-center gap-2">
{getDeviceIcon(device.type)}
<span className="text-sm font-medium text-slate-200">{device.name}</span>
<Server className="h-4 w-4 text-blue-400" />
<span className="text-sm font-medium text-slate-200">{srv.hostname || '--'}</span>
</div>
</td>
<td className="px-4 py-3 text-sm text-slate-300">{srv.ip || '--'}</td>
<td className="px-4 py-3 text-sm text-slate-300">{srv.os || '--'}</td>
<td className="px-4 py-3 text-sm text-slate-300">{srv.region || '--'}</td>
<td className="px-4 py-3 text-sm text-slate-400">
{srv.cpu ?? 0}核 / {srv.memory ?? 0}GB
</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-slate-700 text-slate-400 text-xs">
{device.type}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-slate-300">
{cabinets.find((c) => c.id === device.cabinetId)?.name}
</td>
<td className="px-4 py-3 text-sm text-slate-300">
{device.startU}U - {device.startU + device.sizeU - 1}U ({device.sizeU}U)
</td>
<td className="px-4 py-3 text-sm text-slate-300">{device.ip || '--'}</td>
<td className="px-4 py-3">
<Badge variant="outline" className={
device.status === 'online'
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: device.status === 'offline'
? 'border-red-500/30 text-red-400 bg-red-500/10'
: 'border-amber-500/30 text-amber-400 bg-amber-500/10'
}>
{device.status === 'online' ? '在线' : device.status === 'offline' ? '离线' : '维护中'}
<Badge variant="outline" className={getStatusColor(srv.status)}>
{getStatusText(srv.status)}
</Badge>
</td>
<td className="px-4 py-3 text-right">
<div className="flex items-center justify-end gap-1">
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-slate-100">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-slate-100"
onClick={() => setSelectedServer(srv)}
>
<Eye className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-slate-100">
@@ -475,43 +367,39 @@ export function CabinetView() {
</Card>
)}
{/* Device Detail Dialog */}
<Dialog open={!!selectedDevice} onOpenChange={() => setSelectedDevice(null)}>
{/* Server Detail Dialog */}
<Dialog open={!!selectedServer} onOpenChange={() => setSelectedServer(null)}>
<DialogContent className="bg-slate-900 border-slate-800">
<DialogHeader>
<DialogTitle className="text-slate-100 flex items-center gap-2">
{selectedDevice && getDeviceIcon(selectedDevice.type)}
{selectedDevice?.name}
<Server className="h-5 w-5 text-blue-400" />
{selectedServer?.hostname}
</DialogTitle>
<DialogDescription className="text-slate-500">
{selectedDevice?.type} · {selectedDevice?.assetNo}
{selectedServer?.ip} · {selectedServer?.os}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4 p-4 rounded-lg bg-slate-800/50">
<div>
<p className="text-xs text-slate-500 mb-1">序列号</p>
<p className="text-sm text-slate-200">{selectedDevice?.sn || '--'}</p>
<p className="text-xs text-slate-500 mb-1">IP 地址</p>
<p className="text-sm text-slate-200">{selectedServer?.ip || '--'}</p>
</div>
<div>
<p className="text-xs text-slate-500 mb-1">资产编号</p>
<p className="text-sm text-slate-200">{selectedDevice?.assetNo || '--'}</p>
<p className="text-xs text-slate-500 mb-1">操作系统</p>
<p className="text-sm text-slate-200">{selectedServer?.os || '--'}</p>
</div>
<div>
<p className="text-xs text-slate-500 mb-1">IP地址</p>
<p className="text-sm text-slate-200">{selectedDevice?.ip || '--'}</p>
<p className="text-xs text-slate-500 mb-1">配置</p>
<p className="text-sm text-slate-200">
{selectedServer?.cpu ?? 0} 核 / {selectedServer?.memory ?? 0} GB
</p>
</div>
<div>
<p className="text-xs text-slate-500 mb-1">状态</p>
<Badge variant="outline" className={
selectedDevice?.status === 'online'
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: selectedDevice?.status === 'offline'
? 'border-red-500/30 text-red-400 bg-red-500/10'
: 'border-amber-500/30 text-amber-400 bg-amber-500/10'
}>
{selectedDevice?.status === 'online' ? '在线' : selectedDevice?.status === 'offline' ? '离线' : '维护中'}
<Badge variant="outline" className={selectedServer ? getStatusColor(selectedServer.status) : ''}>
{selectedServer ? getStatusText(selectedServer.status) : ''}
</Badge>
</div>
</div>
@@ -3,14 +3,14 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import {
Dialog,
DialogContent,
DialogHeader,
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import {
import {
Cloud,
Key,
CheckCircle,
@@ -35,21 +35,13 @@ const cloudProviders = [
{ id: 'azure', name: 'Azure', color: 'bg-blue-600', icon: 'AZ' },
];
// Mock cloud servers data
const mockCloudServers = [
{ id: 'ecs-001', name: 'i-bp1a2b3c4d5e6f7g8', hostname: 'web-server-01', ip: '47.98.123.45', region: 'cn-hangzhou', status: 'running', os: 'CentOS 7.9', spec: '2核4G' },
{ id: 'ecs-002', name: 'i-bp2b3c4d5e6f7g8h9', hostname: 'web-server-02', ip: '47.98.123.46', region: 'cn-hangzhou', status: 'running', os: 'CentOS 7.9', spec: '2核4G' },
{ id: 'ecs-003', name: 'i-bp3c4d5e6f7g8h9i0', hostname: 'app-server-01', ip: '47.98.123.47', region: 'cn-shanghai', status: 'stopped', os: 'Ubuntu 20.04', spec: '4核8G' },
{ id: 'ecs-004', name: 'i-bp4d5e6f7g8h9i0j1', hostname: 'db-server-01', ip: '47.98.123.48', region: 'cn-beijing', status: 'running', os: 'CentOS 8.2', spec: '8核16G' },
{ id: 'ecs-005', name: 'i-bp5e6f7g8h9i0j1k2', hostname: 'cache-server-01', ip: '47.98.123.49', region: 'cn-hangzhou', status: 'running', os: 'CentOS 7.9', spec: '2核8G' },
];
export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
const [cloudAccounts, setCloudAccounts] = useState<any[]>([]);
const [step, setStep] = useState(1);
const [selectedProvider, setSelectedProvider] = useState<string>('');
const [selectedAccount, setSelectedAccount] = useState<string>('');
const [selectedServers, setSelectedServers] = useState<string[]>([]);
const [cloudServers, setCloudServers] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
const loadAccounts = async () => {
@@ -73,7 +65,11 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
if (step < 3) {
if (step === 2) {
setIsLoading(true);
setTimeout(() => setIsLoading(false), 1000);
// 云主机发现功能尚未实现,暂以空列表展示
setTimeout(() => {
setCloudServers([]);
setIsLoading(false);
}, 600);
}
setStep(step + 1);
}
@@ -94,10 +90,10 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
};
const handleSelectAll = () => {
if (selectedServers.length === mockCloudServers.length) {
if (selectedServers.length === cloudServers.length && cloudServers.length > 0) {
setSelectedServers([]);
} else {
setSelectedServers(mockCloudServers.map(s => s.id));
setSelectedServers(cloudServers.map(s => s.id));
}
};
@@ -241,8 +237,8 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
<div className="flex items-center justify-between">
<p className="text-slate-400 text-sm">请选择要导入的云主机</p>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300" onClick={handleSelectAll}>
{selectedServers.length === mockCloudServers.length ? '取消全选' : '全选'}
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300" onClick={handleSelectAll} disabled={cloudServers.length === 0}>
{selectedServers.length === cloudServers.length && cloudServers.length > 0 ? '取消全选' : '全选'}
</Button>
<Badge className="bg-blue-500/20 text-blue-400 border-0">
已选 {selectedServers.length} 台
@@ -261,8 +257,8 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
<thead className="bg-slate-800 sticky top-0">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-slate-400 w-10">
<Checkbox
checked={selectedServers.length === mockCloudServers.length && mockCloudServers.length > 0}
<Checkbox
checked={selectedServers.length === cloudServers.length && cloudServers.length > 0}
onCheckedChange={handleSelectAll}
/>
</th>
@@ -275,35 +271,45 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{mockCloudServers.map((server) => (
<tr
key={server.id}
className={`hover:bg-slate-800/50 ${selectedServers.includes(server.id) ? 'bg-blue-500/10' : ''}`}
>
<td className="px-3 py-2">
<Checkbox
checked={selectedServers.includes(server.id)}
onCheckedChange={() => handleServerToggle(server.id)}
/>
</td>
<td className="px-3 py-2 text-sm text-slate-300">{server.name}</td>
<td className="px-3 py-2 text-sm text-slate-200">{server.hostname}</td>
<td className="px-3 py-2 text-sm text-slate-400">{server.ip}</td>
<td className="px-3 py-2 text-sm text-slate-400">{server.region}</td>
<td className="px-3 py-2 text-sm text-slate-400">{server.spec}</td>
<td className="px-3 py-2">
<Badge
variant="outline"
className={server.status === 'running'
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: 'border-slate-500/30 text-slate-400 bg-slate-500/10'
}
>
{server.status === 'running' ? '运行中' : '已停止'}
</Badge>
{cloudServers.length === 0 ? (
<tr>
<td colSpan={7} className="px-3 py-12 text-center">
<Server className="h-10 w-10 mx-auto mb-3 text-slate-600" />
<p className="text-slate-500">暂无云主机</p>
<p className="text-xs text-slate-600 mt-1">云主机发现功能尚未实现,请手动添加服务器</p>
</td>
</tr>
))}
) : (
cloudServers.map((server) => (
<tr
key={server.id}
className={`hover:bg-slate-800/50 ${selectedServers.includes(server.id) ? 'bg-blue-500/10' : ''}`}
>
<td className="px-3 py-2">
<Checkbox
checked={selectedServers.includes(server.id)}
onCheckedChange={() => handleServerToggle(server.id)}
/>
</td>
<td className="px-3 py-2 text-sm text-slate-300">{server.name}</td>
<td className="px-3 py-2 text-sm text-slate-200">{server.hostname}</td>
<td className="px-3 py-2 text-sm text-slate-400">{server.ip}</td>
<td className="px-3 py-2 text-sm text-slate-400">{server.region}</td>
<td className="px-3 py-2 text-sm text-slate-400">{server.spec}</td>
<td className="px-3 py-2">
<Badge
variant="outline"
className={server.status === 'running'
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: 'border-slate-500/30 text-slate-400 bg-slate-500/10'
}
>
{server.status === 'running' ? '运行中' : '已停止'}
</Badge>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
@@ -20,7 +20,13 @@ import {
ChevronRight,
ChevronLeft,
} from 'lucide-react';
import { regions } from '@/mocks/data';
import { regionApi } from '@/services/api';
interface RegionOption {
id: string;
name: string;
code: string;
}
interface ExcelServerImportProps {
open: boolean;
@@ -32,13 +38,10 @@ const connectionMethods = [
{ id: 'key', name: '密钥认证', description: '使用SSH密钥连接' },
];
const mockPreviewData = [
{ row: 2, hostname: 'web-server-01', ip: '192.168.1.101', port: 22, username: 'root', status: 'valid', message: '验证通过' },
{ row: 3, hostname: 'web-server-02', ip: '192.168.1.102', port: 22, username: 'root', status: 'valid', message: '验证通过' },
{ row: 4, hostname: 'db-server-01', ip: '192.168.1.201', port: 22, username: 'admin', status: 'warning', message: 'IP格式正确' },
{ row: 5, hostname: '', ip: '192.168.1.999', port: 22, username: 'root', status: 'error', message: '主机名不能为空' },
{ row: 6, hostname: 'cache-server-01', ip: '192.168.1.103', port: 22, username: 'root', status: 'valid', message: '验证通过' },
];
type PreviewRow = {
row: number; hostname: string; ip: string; port: number;
username: string; status: string; message: string;
};
export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
const [step, setStep] = useState(1);
@@ -46,14 +49,40 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
const [connectionMethod, setConnectionMethod] = useState<string>('password');
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [isUploading, setIsUploading] = useState(false);
const [previewData, setPreviewData] = useState<typeof mockPreviewData>([]);
const [previewData, setPreviewData] = useState<PreviewRow[]>([]);
const [regions, setRegions] = useState<RegionOption[]>([]);
// 对话框打开时拉取区域列表 // Logan.Li
useEffect(() => {
if (!open) return;
let cancelled = false;
(async () => {
try {
const resp = await regionApi.list();
if (cancelled) return;
const list = resp?.list ?? [];
setRegions(
list.map((r: any) => ({
id: String(r.id ?? ''),
name: r.name ?? '',
code: r.code ?? '',
}))
);
} catch {
if (!cancelled) setRegions([]);
}
})();
return () => {
cancelled = true;
};
}, [open]);
const handleNext = () => {
if (step === 2 && selectedFile) {
setIsUploading(true);
setTimeout(() => {
setIsUploading(false);
setPreviewData(mockPreviewData);
setPreviewData([]); // 真实解析由后端处理
setStep(step + 1);
}, 1500);
} else {