diff --git a/frontend/src/pages/Deploy/Server/List/components/ServerTable.tsx b/frontend/src/pages/Deploy/Server/List/components/ServerTable.tsx new file mode 100644 index 00000000..ac4a33e5 --- /dev/null +++ b/frontend/src/pages/Deploy/Server/List/components/ServerTable.tsx @@ -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 = ({ + 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 ; + case 'OFFLINE': + return ; + default: + return ; + } + }; + + return ( + + + + 服务器名称 + IP地址 + 状态 + 操作系统 + CPU核心 + 内存(GB) + 磁盘(GB) + SSH认证 + 分类 + 最后连接 + 操作 + + + + {servers.length === 0 ? ( + + + 暂无服务器数据 + + + ) : ( + servers.map((server) => ( + + +
+ {server.serverName} +
+
+ + {server.hostIp} + + +
+ {getStatusIcon(server.status)} + + {ServerStatusLabels[server.status]?.label || server.status} + +
+
+ +
+ {getOsIcon(server.osType)} + {OsTypeLabels[server.osType]?.label || server.osType} +
+
+ + {server.cpuCores || '-'} + + + {server.memorySize || '-'} + + + {server.diskSize || '-'} + + + + {server.authType === 'PASSWORD' ? '密码' : '密钥'} + + + + + {server.categoryName || '未分类'} + + + + {formatTime(server.lastConnectTime)} + + +
+ + + + + 测试连接 + + + + + + 编辑 + + + + + + 删除 + +
+
+
+ )) + )} +
+
+ ); +}; diff --git a/frontend/src/pages/Deploy/Server/List/index.tsx b/frontend/src/pages/Deploy/Server/List/index.tsx index 33af81b5..0dceba04 100644 --- a/frontend/src/pages/Deploy/Server/List/index.tsx +++ b/frontend/src/pages/Deploy/Server/List/index.tsx @@ -11,6 +11,8 @@ import { Filter, Search, RotateCcw, + Grid3x3, + List, } from 'lucide-react'; import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; @@ -39,16 +41,25 @@ import { } from '@/components/ui/alert-dialog'; import { useToast } from '@/components/ui/use-toast'; import { DataTablePagination } from '@/components/ui/pagination'; +import { Input } from '@/components/ui/input'; import type { ServerResponse, ServerCategoryResponse, ServerStatus, OsType } from './types'; import { ServerStatusLabels, OsTypeLabels } from './types'; import { getServers, getServerCategories, deleteServer, testServerConnection } from './service'; import { CategoryManageDialog } from './components/CategoryManageDialog'; import { ServerEditDialog } from './components/ServerEditDialog'; import { ServerCard } from './components/ServerCard'; +import { ServerTable } from './components/ServerTable'; const ServerList: React.FC = () => { const { toast } = useToast(); + // 视图模式 + type ViewMode = 'grid' | 'table'; + const [viewMode, setViewMode] = useState('grid'); + + // 搜索关键字 + const [searchKeyword, setSearchKeyword] = useState(''); + // 状态管理 const [categories, setCategories] = useState([]); const [servers, setServers] = useState([]); @@ -60,12 +71,12 @@ const ServerList: React.FC = () => { const [selectedCategoryId, setSelectedCategoryId] = useState(); const [selectedStatus, setSelectedStatus] = useState(); const [selectedOsType, setSelectedOsType] = useState(); - + // 筛选条件 - 临时输入的(未提交) const [tempCategoryId, setTempCategoryId] = useState(); const [tempStatus, setTempStatus] = useState(); const [tempOsType, setTempOsType] = useState(); - + const [categoryDialogOpen, setCategoryDialogOpen] = useState(false); const [editDialogOpen, setEditDialogOpen] = useState(false); const [editingServer, setEditingServer] = useState(null); @@ -253,9 +264,21 @@ const ServerList: React.FC = () => { setSelectedCategoryId(undefined); setSelectedStatus(undefined); setSelectedOsType(undefined); + setSearchKeyword(''); 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(() => { loadServers(); @@ -266,239 +289,295 @@ const ServerList: React.FC = () => { return ( -
-
- {/* 统计卡片 */} -
+
+ {/* 顶部区域 - 统计和标题 */} +
+ {/* 标题栏 */} +
+
+

服务器管理

+

管理和监控服务器资源,支持SSH连接和分类管理

+
+
+ + +
+
+ + {/* 统计卡片 - 更紧凑的布局 */} +
- - 总服务器 - - - -
{stats.total}
-

所有服务器

+ +
+
+

总服务器

+

{stats.total}

+
+ +
- - 在线 - - - -
{stats.online}
-

运行中

+ +
+
+

在线

+

{stats.online}

+
+ +
- - 离线 - - - -
{stats.offline}
-

不可用

+ +
+
+

离线

+

{stats.offline}

+
+ +
- - 未知 - - - -
{stats.unknown}
-

状态未知

+ +
+
+

未知

+

{stats.unknown}

+
+ +
-
- {/* 服务器管理卡片 - 可滚动区域 */} + {/* 内容区域 - 可滚动 */}
+ {/* 搜索和视图切换栏 */} -
-
- 服务器管理 - - 管理和监控服务器资源,支持SSH连接和分类管理 - -
+
+ {/* 搜索框 */}
- - + +
+
+ + {/* 筛选栏 */} +
+
+ + 筛选: +
+ + + + + + + +
+ + +
+ + + {/* 内容区域 */} - {/* 筛选栏 */} -
-
- - 筛选: -
- - - - - - - -
- - -
-
- - {/* 服务器卡片网格 */} -
- {/* 加载状态 - 骨架屏 */} - {loading && servers.length === 0 && ( -
- {Array.from({ length: 8 }).map((_, index) => ( - - -
- -
- - +
+ {/* 加载状态 - 骨架屏 */} + {loading && servers.length === 0 && ( +
+ {Array.from({ length: 8 }).map((_, index) => ( + + +
+ +
+ + +
-
-
- - - -
-
- -
- - - ))} -
- )} - - {/* 空状态 */} - {!loading && servers.length === 0 && ( -
- -

暂无服务器

-

点击上方"新增服务器"按钮添加服务器

-
- )} - - {/* 服务器列表 */} - {!loading && servers.length > 0 && ( -
- {servers.map((server) => ( - - ))} -
- )} - - {/* 分页 */} - {!loading && servers.length > 0 && ( -
-
- 共 {totalElements} 条记录,第 {pageIndex + 1} / {Math.ceil(totalElements / pageSize)} 页 +
+ + + +
+
+ +
+ + + ))}
- + +

暂无服务器

+

+ {servers.length === 0 ? '点击上方"新增服务器"按钮添加服务器' : '没有匹配搜索条件的服务器'} +

+
+ )} + + {/* 网格视图 */} + {!loading && filteredServers.length > 0 && viewMode === 'grid' && ( +
+ {filteredServers.map((server) => ( + + ))} +
+ )} + + {/* 表格视图 */} + {!loading && filteredServers.length > 0 && viewMode === 'table' && ( + testingServerId === serverId} + getOsIcon={getOsIcon} /> -
- )} -
+ )} + + {/* 分页 */} + {!loading && servers.length > 0 && ( +
+
+ 共 {totalElements} 条记录,第 {pageIndex + 1} / {Math.ceil(totalElements / pageSize)} 页 + {searchKeyword && filteredServers.length > 0 && ( + ,搜索结果 {filteredServers.length} 条 + )} +
+ +
+ )} +