增加团队管理页面
This commit is contained in:
parent
97e6b30e65
commit
68bc9757e4
195
frontend/src/pages/Deploy/Server/List/components/ServerTable.tsx
Normal file
195
frontend/src/pages/Deploy/Server/List/components/ServerTable.tsx
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
TestTube,
|
||||||
|
Pencil,
|
||||||
|
Trash2,
|
||||||
|
Loader2,
|
||||||
|
Activity,
|
||||||
|
AlertCircle,
|
||||||
|
HelpCircle,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import type { ServerResponse } from '../types';
|
||||||
|
import { ServerStatusLabels, OsTypeLabels } from '../types';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
import 'dayjs/locale/zh-cn';
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
|
interface ServerTableProps {
|
||||||
|
servers: ServerResponse[];
|
||||||
|
loading: boolean;
|
||||||
|
onTest: (server: ServerResponse) => void;
|
||||||
|
onEdit: (server: ServerResponse) => void;
|
||||||
|
onDelete: (server: ServerResponse) => void;
|
||||||
|
isTesting?: (serverId: number) => boolean;
|
||||||
|
getOsIcon: (osType?: string) => React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ServerTable: React.FC<ServerTableProps> = ({
|
||||||
|
servers,
|
||||||
|
loading,
|
||||||
|
onTest,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
isTesting,
|
||||||
|
getOsIcon,
|
||||||
|
}) => {
|
||||||
|
const formatTime = (time?: string) => {
|
||||||
|
if (!time) return '-';
|
||||||
|
return dayjs(time).fromNow();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusIcon = (status?: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'ONLINE':
|
||||||
|
return <Activity className="h-4 w-4 text-green-600 dark:text-green-400" />;
|
||||||
|
case 'OFFLINE':
|
||||||
|
return <AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400" />;
|
||||||
|
default:
|
||||||
|
return <HelpCircle className="h-4 w-4 text-gray-600 dark:text-gray-400" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table minWidth="1200px">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead width="180px">服务器名称</TableHead>
|
||||||
|
<TableHead width="150px">IP地址</TableHead>
|
||||||
|
<TableHead width="120px">状态</TableHead>
|
||||||
|
<TableHead width="100px">操作系统</TableHead>
|
||||||
|
<TableHead width="80px">CPU核心</TableHead>
|
||||||
|
<TableHead width="100px">内存(GB)</TableHead>
|
||||||
|
<TableHead width="100px">磁盘(GB)</TableHead>
|
||||||
|
<TableHead width="120px">SSH认证</TableHead>
|
||||||
|
<TableHead width="140px">分类</TableHead>
|
||||||
|
<TableHead width="150px">最后连接</TableHead>
|
||||||
|
<TableHead sticky width="140px" className="text-right">操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{servers.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={11} className="text-center py-8">
|
||||||
|
<span className="text-muted-foreground">暂无服务器数据</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
servers.map((server) => (
|
||||||
|
<TableRow key={server.id}>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium text-foreground">{server.serverName}</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="font-mono text-sm">{server.hostIp}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getStatusIcon(server.status)}
|
||||||
|
<Badge className={
|
||||||
|
server.status === 'ONLINE' ? 'bg-green-500/10 text-green-700 border-green-500/30 dark:text-green-400' :
|
||||||
|
server.status === 'OFFLINE' ? 'bg-red-500/10 text-red-700 border-red-500/30 dark:text-red-400' :
|
||||||
|
'bg-gray-500/10 text-gray-700 border-gray-500/30 dark:text-gray-400'
|
||||||
|
}>
|
||||||
|
{ServerStatusLabels[server.status]?.label || server.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getOsIcon(server.osType)}
|
||||||
|
<span className="text-sm">{OsTypeLabels[server.osType]?.label || server.osType}</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-sm">{server.cpuCores || '-'}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-sm">{server.memorySize || '-'}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-sm">{server.diskSize || '-'}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{server.authType === 'PASSWORD' ? '密码' : '密钥'}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{server.categoryName || '未分类'}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-sm text-muted-foreground">{formatTime(server.lastConnectTime)}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sticky className="text-right">
|
||||||
|
<div className="flex items-center justify-end gap-1">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onTest(server)}
|
||||||
|
disabled={isTesting?.(server.id)}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
{isTesting?.(server.id) ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<TestTube className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>测试连接</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onEdit(server)}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<Pencil className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>编辑</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onDelete(server)}
|
||||||
|
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>删除</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -11,6 +11,8 @@ import {
|
|||||||
Filter,
|
Filter,
|
||||||
Search,
|
Search,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
|
Grid3x3,
|
||||||
|
List,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
@ -39,16 +41,25 @@ import {
|
|||||||
} from '@/components/ui/alert-dialog';
|
} from '@/components/ui/alert-dialog';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import { DataTablePagination } from '@/components/ui/pagination';
|
import { DataTablePagination } from '@/components/ui/pagination';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
import type { ServerResponse, ServerCategoryResponse, ServerStatus, OsType } from './types';
|
import type { ServerResponse, ServerCategoryResponse, ServerStatus, OsType } from './types';
|
||||||
import { ServerStatusLabels, OsTypeLabels } from './types';
|
import { ServerStatusLabels, OsTypeLabels } from './types';
|
||||||
import { getServers, getServerCategories, deleteServer, testServerConnection } from './service';
|
import { getServers, getServerCategories, deleteServer, testServerConnection } from './service';
|
||||||
import { CategoryManageDialog } from './components/CategoryManageDialog';
|
import { CategoryManageDialog } from './components/CategoryManageDialog';
|
||||||
import { ServerEditDialog } from './components/ServerEditDialog';
|
import { ServerEditDialog } from './components/ServerEditDialog';
|
||||||
import { ServerCard } from './components/ServerCard';
|
import { ServerCard } from './components/ServerCard';
|
||||||
|
import { ServerTable } from './components/ServerTable';
|
||||||
|
|
||||||
const ServerList: React.FC = () => {
|
const ServerList: React.FC = () => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
// 视图模式
|
||||||
|
type ViewMode = 'grid' | 'table';
|
||||||
|
const [viewMode, setViewMode] = useState<ViewMode>('grid');
|
||||||
|
|
||||||
|
// 搜索关键字
|
||||||
|
const [searchKeyword, setSearchKeyword] = useState<string>('');
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const [categories, setCategories] = useState<ServerCategoryResponse[]>([]);
|
const [categories, setCategories] = useState<ServerCategoryResponse[]>([]);
|
||||||
const [servers, setServers] = useState<ServerResponse[]>([]);
|
const [servers, setServers] = useState<ServerResponse[]>([]);
|
||||||
@ -253,9 +264,21 @@ const ServerList: React.FC = () => {
|
|||||||
setSelectedCategoryId(undefined);
|
setSelectedCategoryId(undefined);
|
||||||
setSelectedStatus(undefined);
|
setSelectedStatus(undefined);
|
||||||
setSelectedOsType(undefined);
|
setSelectedOsType(undefined);
|
||||||
|
setSearchKeyword('');
|
||||||
setPageIndex(0); // 重置到第一页
|
setPageIndex(0); // 重置到第一页
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 客户端搜索过滤(在加载数据后进行)
|
||||||
|
const filteredServers = servers.filter(server => {
|
||||||
|
if (!searchKeyword.trim()) return true;
|
||||||
|
const keyword = searchKeyword.toLowerCase();
|
||||||
|
return (
|
||||||
|
server.serverName.toLowerCase().includes(keyword) ||
|
||||||
|
server.hostIp.includes(keyword) ||
|
||||||
|
(server.hostname && server.hostname.toLowerCase().includes(keyword))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// 监听筛选条件和分页变化
|
// 监听筛选条件和分页变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadServers();
|
loadServers();
|
||||||
@ -266,239 +289,295 @@ const ServerList: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="h-screen flex flex-col overflow-hidden">
|
<div className="h-screen flex flex-col overflow-hidden bg-background">
|
||||||
<div className="flex-shrink-0 space-y-6 p-6">
|
{/* 顶部区域 - 统计和标题 */}
|
||||||
{/* 统计卡片 */}
|
<div className="flex-shrink-0 space-y-4 p-6">
|
||||||
<div className="grid gap-4 md:grid-cols-4">
|
{/* 标题栏 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">服务器管理</h1>
|
||||||
|
<p className="text-muted-foreground mt-1">管理和监控服务器资源,支持SSH连接和分类管理</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" onClick={() => setCategoryDialogOpen(true)}>
|
||||||
|
<Settings className="h-4 w-4 mr-2" />
|
||||||
|
分类管理
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setEditingServer(null);
|
||||||
|
setEditDialogOpen(true);
|
||||||
|
}}
|
||||||
|
className="bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
新增服务器
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 统计卡片 - 更紧凑的布局 */}
|
||||||
|
<div className="grid gap-3 md:grid-cols-4">
|
||||||
<Card className="bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-950/20 dark:to-indigo-950/20 border-blue-200 dark:border-blue-900">
|
<Card className="bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-950/20 dark:to-indigo-950/20 border-blue-200 dark:border-blue-900">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardContent className="p-4">
|
||||||
<CardTitle className="text-sm font-medium">总服务器</CardTitle>
|
<div className="flex items-center justify-between">
|
||||||
<Server className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
<div>
|
||||||
</CardHeader>
|
<p className="text-xs text-muted-foreground font-medium">总服务器</p>
|
||||||
<CardContent>
|
<p className="text-2xl font-bold text-blue-600 dark:text-blue-400 mt-1">{stats.total}</p>
|
||||||
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">{stats.total}</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-1">所有服务器</p>
|
<Server className="h-8 w-8 text-blue-600 dark:text-blue-400 opacity-50" />
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-950/20 dark:to-emerald-950/20 border-green-200 dark:border-green-900">
|
<Card className="bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-950/20 dark:to-emerald-950/20 border-green-200 dark:border-green-900">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardContent className="p-4">
|
||||||
<CardTitle className="text-sm font-medium">在线</CardTitle>
|
<div className="flex items-center justify-between">
|
||||||
<Activity className="h-4 w-4 text-green-600 dark:text-green-400" />
|
<div>
|
||||||
</CardHeader>
|
<p className="text-xs text-muted-foreground font-medium">在线</p>
|
||||||
<CardContent>
|
<p className="text-2xl font-bold text-green-600 dark:text-green-400 mt-1">{stats.online}</p>
|
||||||
<div className="text-2xl font-bold text-green-600 dark:text-green-400">{stats.online}</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-1">运行中</p>
|
<Activity className="h-8 w-8 text-green-600 dark:text-green-400 opacity-50" />
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="bg-gradient-to-br from-red-50 to-rose-50 dark:from-red-950/20 dark:to-rose-950/20 border-red-200 dark:border-red-900">
|
<Card className="bg-gradient-to-br from-red-50 to-rose-50 dark:from-red-950/20 dark:to-rose-950/20 border-red-200 dark:border-red-900">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardContent className="p-4">
|
||||||
<CardTitle className="text-sm font-medium">离线</CardTitle>
|
<div className="flex items-center justify-between">
|
||||||
<AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400" />
|
<div>
|
||||||
</CardHeader>
|
<p className="text-xs text-muted-foreground font-medium">离线</p>
|
||||||
<CardContent>
|
<p className="text-2xl font-bold text-red-600 dark:text-red-400 mt-1">{stats.offline}</p>
|
||||||
<div className="text-2xl font-bold text-red-600 dark:text-red-400">{stats.offline}</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-1">不可用</p>
|
<AlertCircle className="h-8 w-8 text-red-600 dark:text-red-400 opacity-50" />
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="bg-gradient-to-br from-gray-50 to-slate-50 dark:from-gray-950/20 dark:to-slate-950/20 border-gray-200 dark:border-gray-800">
|
<Card className="bg-gradient-to-br from-gray-50 to-slate-50 dark:from-gray-950/20 dark:to-slate-950/20 border-gray-200 dark:border-gray-800">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardContent className="p-4">
|
||||||
<CardTitle className="text-sm font-medium">未知</CardTitle>
|
<div className="flex items-center justify-between">
|
||||||
<HelpCircle className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
<div>
|
||||||
</CardHeader>
|
<p className="text-xs text-muted-foreground font-medium">未知</p>
|
||||||
<CardContent>
|
<p className="text-2xl font-bold text-gray-600 dark:text-gray-400 mt-1">{stats.unknown}</p>
|
||||||
<div className="text-2xl font-bold text-gray-600 dark:text-gray-400">{stats.unknown}</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-1">状态未知</p>
|
<HelpCircle className="h-8 w-8 text-gray-600 dark:text-gray-400 opacity-50" />
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 服务器管理卡片 - 可滚动区域 */}
|
{/* 内容区域 - 可滚动 */}
|
||||||
<div className="flex-1 overflow-hidden px-6 pb-6">
|
<div className="flex-1 overflow-hidden px-6 pb-6">
|
||||||
<Card className="h-full flex flex-col">
|
<Card className="h-full flex flex-col">
|
||||||
|
{/* 搜索和视图切换栏 */}
|
||||||
<CardHeader className="flex-shrink-0">
|
<CardHeader className="flex-shrink-0">
|
||||||
<div className="flex items-center justify-between">
|
<div className="space-y-4">
|
||||||
<div>
|
{/* 搜索框 */}
|
||||||
<CardTitle className="text-2xl">服务器管理</CardTitle>
|
|
||||||
<CardDescription className="mt-2">
|
|
||||||
管理和监控服务器资源,支持SSH连接和分类管理
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="outline" onClick={() => setCategoryDialogOpen(true)}>
|
<div className="flex-1">
|
||||||
<Settings className="h-4 w-4 mr-2" />
|
<Input
|
||||||
分类管理
|
placeholder="搜索服务器名称、IP或主机名..."
|
||||||
</Button>
|
value={searchKeyword}
|
||||||
<Button
|
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||||
onClick={() => {
|
className="h-10"
|
||||||
setEditingServer(null);
|
/>
|
||||||
setEditDialogOpen(true);
|
</div>
|
||||||
|
<div className="flex items-center gap-1 bg-muted/50 rounded-lg p-1">
|
||||||
|
<Button
|
||||||
|
variant={viewMode === 'grid' ? 'default' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setViewMode('grid')}
|
||||||
|
className="h-8 px-3"
|
||||||
|
>
|
||||||
|
<Grid3x3 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={viewMode === 'table' ? 'default' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setViewMode('table')}
|
||||||
|
className="h-8 px-3"
|
||||||
|
>
|
||||||
|
<List className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 筛选栏 */}
|
||||||
|
<div className="flex items-center gap-3 p-4 bg-muted/30 rounded-lg flex-wrap">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Filter className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-medium">筛选:</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={tempCategoryId?.toString() || 'all'}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setTempCategoryId(value === 'all' ? undefined : Number(value));
|
||||||
}}
|
}}
|
||||||
className="bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700"
|
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<SelectTrigger className="w-[180px]">
|
||||||
新增服务器
|
<SelectValue placeholder="全部分类" />
|
||||||
</Button>
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">全部分类</SelectItem>
|
||||||
|
{categories.map((cat) => (
|
||||||
|
<SelectItem key={cat.id} value={cat.id.toString()}>
|
||||||
|
{cat.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={tempStatus || 'all'}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setTempStatus(value === 'all' ? undefined : (value as ServerStatus));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[140px]">
|
||||||
|
<SelectValue placeholder="全部状态" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">全部状态</SelectItem>
|
||||||
|
{Object.entries(ServerStatusLabels).map(([key, value]) => (
|
||||||
|
<SelectItem key={key} value={key}>
|
||||||
|
{value.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={tempOsType || 'all'}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setTempOsType(value === 'all' ? undefined : (value as OsType));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[140px]">
|
||||||
|
<SelectValue placeholder="全部系统" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">全部系统</SelectItem>
|
||||||
|
{Object.entries(OsTypeLabels).map(([key, value]) => (
|
||||||
|
<SelectItem key={key} value={key}>
|
||||||
|
{value.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<div className="flex gap-2 ml-auto">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleReset}
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-4 w-4 mr-1.5" />
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSearch}
|
||||||
|
>
|
||||||
|
<Search className="h-4 w-4 mr-1.5" />
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<Separator className="flex-shrink-0" />
|
<Separator className="flex-shrink-0" />
|
||||||
|
|
||||||
|
{/* 内容区域 */}
|
||||||
<ScrollArea className="flex-1">
|
<ScrollArea className="flex-1">
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
{/* 筛选栏 */}
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-3 mb-6 p-4 bg-muted/30 rounded-lg">
|
{/* 加载状态 - 骨架屏 */}
|
||||||
<div className="flex items-center gap-2">
|
{loading && servers.length === 0 && (
|
||||||
<Filter className="h-4 w-4 text-muted-foreground" />
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
<span className="text-sm font-medium">筛选:</span>
|
{Array.from({ length: 8 }).map((_, index) => (
|
||||||
</div>
|
<Card key={index} className="overflow-hidden">
|
||||||
|
<CardContent className="p-6 space-y-4">
|
||||||
<Select
|
<div className="flex items-start gap-4">
|
||||||
value={tempCategoryId?.toString() || 'all'}
|
<Skeleton className="h-14 w-14 rounded-lg" />
|
||||||
onValueChange={(value) => {
|
<div className="flex-1 space-y-2">
|
||||||
setTempCategoryId(value === 'all' ? undefined : Number(value));
|
<Skeleton className="h-5 w-32" />
|
||||||
}}
|
<Skeleton className="h-4 w-24" />
|
||||||
>
|
</div>
|
||||||
<SelectTrigger className="w-[180px]">
|
|
||||||
<SelectValue placeholder="全部分类" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">全部分类</SelectItem>
|
|
||||||
{categories.map((cat) => (
|
|
||||||
<SelectItem key={cat.id} value={cat.id.toString()}>
|
|
||||||
{cat.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
value={tempStatus || 'all'}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
setTempStatus(value === 'all' ? undefined : (value as ServerStatus));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[140px]">
|
|
||||||
<SelectValue placeholder="全部状态" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">全部状态</SelectItem>
|
|
||||||
{Object.entries(ServerStatusLabels).map(([key, value]) => (
|
|
||||||
<SelectItem key={key} value={key}>
|
|
||||||
{value.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
value={tempOsType || 'all'}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
setTempOsType(value === 'all' ? undefined : (value as OsType));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[140px]">
|
|
||||||
<SelectValue placeholder="全部系统" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">全部系统</SelectItem>
|
|
||||||
{Object.entries(OsTypeLabels).map(([key, value]) => (
|
|
||||||
<SelectItem key={key} value={key}>
|
|
||||||
{value.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<div className="flex gap-2 ml-auto">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleReset}
|
|
||||||
>
|
|
||||||
<RotateCcw className="h-4 w-4 mr-1.5" />
|
|
||||||
重置
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={handleSearch}
|
|
||||||
>
|
|
||||||
<Search className="h-4 w-4 mr-1.5" />
|
|
||||||
查询
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 服务器卡片网格 */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* 加载状态 - 骨架屏 */}
|
|
||||||
{loading && servers.length === 0 && (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
||||||
{Array.from({ length: 8 }).map((_, index) => (
|
|
||||||
<Card key={index} className="overflow-hidden">
|
|
||||||
<CardContent className="p-6 space-y-4">
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<Skeleton className="h-14 w-14 rounded-lg" />
|
|
||||||
<div className="flex-1 space-y-2">
|
|
||||||
<Skeleton className="h-5 w-32" />
|
|
||||||
<Skeleton className="h-4 w-24" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="space-y-3">
|
||||||
<div className="space-y-3">
|
<Skeleton className="h-4 w-full" />
|
||||||
<Skeleton className="h-4 w-full" />
|
<Skeleton className="h-4 w-3/4" />
|
||||||
<Skeleton className="h-4 w-3/4" />
|
<Skeleton className="h-4 w-1/2" />
|
||||||
<Skeleton className="h-4 w-1/2" />
|
</div>
|
||||||
</div>
|
<div className="pt-4 border-t">
|
||||||
<div className="pt-4 border-t">
|
<Skeleton className="h-9 w-full" />
|
||||||
<Skeleton className="h-9 w-full" />
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
</CardContent>
|
</Card>
|
||||||
</Card>
|
))}
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 空状态 */}
|
|
||||||
{!loading && servers.length === 0 && (
|
|
||||||
<div className="flex flex-col items-center justify-center py-24 text-muted-foreground">
|
|
||||||
<Server className="h-16 w-16 mb-4 opacity-20" />
|
|
||||||
<p className="text-lg font-medium">暂无服务器</p>
|
|
||||||
<p className="text-sm mt-2">点击上方"新增服务器"按钮添加服务器</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 服务器列表 */}
|
|
||||||
{!loading && servers.length > 0 && (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
||||||
{servers.map((server) => (
|
|
||||||
<ServerCard
|
|
||||||
key={server.id}
|
|
||||||
server={server}
|
|
||||||
onTest={handleTestConnection}
|
|
||||||
onEdit={handleEdit}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
isTesting={testingServerId === server.id}
|
|
||||||
getOsIcon={getOsIcon}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 分页 */}
|
|
||||||
{!loading && servers.length > 0 && (
|
|
||||||
<div className="mt-6 flex items-center justify-between">
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
共 {totalElements} 条记录,第 {pageIndex + 1} / {Math.ceil(totalElements / pageSize)} 页
|
|
||||||
</div>
|
</div>
|
||||||
<DataTablePagination
|
)}
|
||||||
pageIndex={pageIndex}
|
|
||||||
pageSize={pageSize}
|
{/* 空状态 */}
|
||||||
pageCount={Math.ceil(totalElements / pageSize)}
|
{!loading && filteredServers.length === 0 && (
|
||||||
onPageChange={handlePageChange}
|
<div className="flex flex-col items-center justify-center py-24 text-muted-foreground">
|
||||||
|
<Server className="h-16 w-16 mb-4 opacity-20" />
|
||||||
|
<p className="text-lg font-medium">暂无服务器</p>
|
||||||
|
<p className="text-sm mt-2">
|
||||||
|
{servers.length === 0 ? '点击上方"新增服务器"按钮添加服务器' : '没有匹配搜索条件的服务器'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 网格视图 */}
|
||||||
|
{!loading && filteredServers.length > 0 && viewMode === 'grid' && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
|
{filteredServers.map((server) => (
|
||||||
|
<ServerCard
|
||||||
|
key={server.id}
|
||||||
|
server={server}
|
||||||
|
onTest={handleTestConnection}
|
||||||
|
onEdit={handleEdit}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
isTesting={testingServerId === server.id}
|
||||||
|
getOsIcon={getOsIcon}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 表格视图 */}
|
||||||
|
{!loading && filteredServers.length > 0 && viewMode === 'table' && (
|
||||||
|
<ServerTable
|
||||||
|
servers={filteredServers}
|
||||||
|
loading={loading}
|
||||||
|
onTest={handleTestConnection}
|
||||||
|
onEdit={handleEdit}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
isTesting={(serverId) => testingServerId === serverId}
|
||||||
|
getOsIcon={getOsIcon}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
{/* 分页 */}
|
||||||
|
{!loading && servers.length > 0 && (
|
||||||
|
<div className="mt-6 flex items-center justify-between">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
共 {totalElements} 条记录,第 {pageIndex + 1} / {Math.ceil(totalElements / pageSize)} 页
|
||||||
|
{searchKeyword && filteredServers.length > 0 && (
|
||||||
|
<span>,搜索结果 {filteredServers.length} 条</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<DataTablePagination
|
||||||
|
pageIndex={pageIndex}
|
||||||
|
pageSize={pageSize}
|
||||||
|
pageCount={Math.ceil(totalElements / pageSize)}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user