diff --git a/frontend/src/components/FormDesigner/utils/validationHelper.ts b/frontend/src/components/FormDesigner/utils/validationHelper.ts index 64bda08c..0a96a182 100644 --- a/frontend/src/components/FormDesigner/utils/validationHelper.ts +++ b/frontend/src/components/FormDesigner/utils/validationHelper.ts @@ -29,7 +29,17 @@ export const convertValidationRules = (validationRules?: ValidationRule[]): Rule break; case 'pattern': if (rule.value) { - antdRule.pattern = new RegExp(rule.value); + // 只支持标准正则字面量格式:/pattern/flags + const regexMatch = rule.value.match(/^\/(.+)\/([gimsuy]*)$/); + if (regexMatch) { + try { + antdRule.pattern = new RegExp(regexMatch[1], regexMatch[2]); + } catch (error) { + console.error('正则表达式语法错误:', rule.value, error); + } + } else { + console.warn('正则表达式格式错误,请使用标准格式:/pattern/flags,例如 /^\\d+$/i'); + } } break; case 'min': diff --git a/frontend/src/pages/Resource/Server/List/components/ServerCard.tsx b/frontend/src/pages/Resource/Server/List/components/ServerCard.tsx index 83f3dac5..cea72c3b 100644 --- a/frontend/src/pages/Resource/Server/List/components/ServerCard.tsx +++ b/frontend/src/pages/Resource/Server/List/components/ServerCard.tsx @@ -9,11 +9,13 @@ import { Pencil, Trash2, Loader2, + ChevronDown, } from 'lucide-react'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { Skeleton } from '@/components/ui/skeleton'; import type { ServerResponse } from '../types'; import { ServerStatusLabels, OsTypeLabels } from '../types'; import dayjs from 'dayjs'; @@ -32,189 +34,261 @@ interface ServerCardProps { getOsIcon: (osType?: string) => React.ReactNode; } -export const ServerCard: React.FC = ({ - server, - onTest, - onEdit, - onDelete, - isTesting, - getOsIcon, -}) => { +export const ServerCard: React.FC = ({ server, onTest, onEdit, onDelete, isTesting, getOsIcon }) => { + const [expanded, setExpanded] = React.useState(false); + const formatTime = (time?: string) => { if (!time) return '-'; return dayjs(time).fromNow(); }; - return ( - - - {/* 顶部:状态和分类 */} -
-
-
- - {(server.status && ServerStatusLabels[server.status]?.label) || server.status || '-'} - -
- {server.categoryName && ( - - {server.categoryName} - - )} -
+ const renderValue = ( + content: React.ReactNode, + skeletonWidth: string, + options?: { skeleton?: boolean; skeletonHeight?: string; fallback?: React.ReactNode } + ) => { + if (content === undefined || content === null || content === '' || content === false) { + if (options?.skeleton) { + return ( + + ); + } + return options?.fallback ?? null; + } + return content; + }; - {/* 中间:服务器名称和基本信息 */} -
-
-
- {getOsIcon(server.osType)} -
+ return ( + + + {/* 基础信息 */} +
+
+ {server.osType ? ( +
{getOsIcon(server.osType)}
+ ) : ( + + )}
-
-

- {server.serverName} -

-
+
+
+
+ {renderValue( + + {(server.status && ServerStatusLabels[server.status]?.label) || server.status} + , + 'w-16' + )} + {server.categoryName && ( + + {server.categoryName} + + )} +
+ {renderValue( +

+ {server.serverName} +

, + 'w-32', + { skeleton: true } + )} +
- {server.hostIp} + {renderValue( + {server.hostIp}, + 'w-24', + { skeleton: true } + )}
{server.hostname && ( -
- {server.hostname} -
+ {server.hostname} )} {server.osType && ( -
- {(server.osType && OsTypeLabels[server.osType]?.label) || server.osType || '-'} + + {(server.osType && OsTypeLabels[server.osType]?.label) || server.osType} {server.osVersion && ` ${server.osVersion}`} -
+ )}
+ +
+ + {/* 快速信息 */} +
+
+ + {server.lastConnectTime ? ( + 最后连接 {formatTime(server.lastConnectTime)} + ) : ( + 暂无连接记录 + )} +
+
+ + + + + 测试连接 + + + + + + 编辑 + + + + + + 删除 + +
- {/* SSH 和认证方式 - 在一行显示 */} -
- SSH: {server.sshUser || 'root'}:{server.sshPort || 22} - {server.authType && ( - - {server.authType === 'PASSWORD' ? '密码' : '密钥'} - - )} -
- - {/* 硬件配置 - 紧凑显示 */} - {(server.cpuCores || server.memorySize || server.diskSize) && ( -
- {server.cpuCores && ( -
- - {server.cpuCores}核 -
- )} - {server.memorySize && ( -
- - {server.memorySize}GB -
- )} - {server.diskSize && ( -
- - {server.diskSize}GB -
- )} -
- )} - - {/* 标签 - 可选显示 */} - {server.tags && (() => { - try { - const tags = JSON.parse(server.tags); - return Array.isArray(tags) && tags.length > 0 && ( -
- {tags.slice(0, 2).map((tag, index) => ( - - {tag} - - ))} - {tags.length > 2 && +{tags.length - 2}} -
- ); - } catch { - return null; - } - })()} - - {/* 描述 - 始终显示 */} - {server.description && ( -

- {server.description} -

- )} - - {/* 最后连接时间 */} - {server.lastConnectTime && ( -
- - 最后连接 {formatTime(server.lastConnectTime)} -
- )} - - {/* 操作按钮 - Hover 时才显示 */} -
- - - - - 测试连接 - - - - - - 编辑 - - - - - - 删除 - -
+
+
+
+ +
+ {renderValue( + server.memorySize ? ( + {server.memorySize}GB + ) : null, + 'w-10', + { skeleton: true, skeletonHeight: 'h-4' } + )} +
+
+
+ +
+ {renderValue( + server.diskSize ? ( + {server.diskSize}GB + ) : null, + 'w-10', + { skeleton: true, skeletonHeight: 'h-4' } + )} +
+
+ + {server.tags && (() => { + try { + const tags = JSON.parse(server.tags); + if (Array.isArray(tags) && tags.length > 0) { + return ( +
+ {tags.slice(0, 3).map((tag: string, index: number) => ( + + {tag} + + ))} + {tags.length > 3 && ( + +{tags.length - 3} + )} +
+ ); + } + } catch { + return null; + } + return null; + })()} + + {server.description ? ( +

{server.description}

+ ) : null} +
+ )}
);