diff --git a/web/package-lock.json b/web/package-lock.json index 5105dce..9fa9662 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@ant-design/pro-components": "^2.8.10", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "^4.1.14", "@tanstack/react-query": "^5.90.6", @@ -1605,6 +1606,32 @@ } } }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -1635,6 +1662,21 @@ } } }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dismissable-layer": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", @@ -1662,6 +1704,75 @@ } } }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-id": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", @@ -1680,6 +1791,46 @@ } } }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", @@ -1783,6 +1934,37 @@ } } }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -3366,6 +3548,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3797,6 +3991,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4301,6 +4501,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -5917,6 +6126,53 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "7.9.4", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz", @@ -5955,6 +6211,28 @@ "react-dom": ">=18" } }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/reactcss": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", @@ -6562,6 +6840,49 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", diff --git a/web/package.json b/web/package.json index 10da54a..cc68c12 100644 --- a/web/package.json +++ b/web/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@ant-design/pro-components": "^2.8.10", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "^4.1.14", "@tanstack/react-query": "^5.90.6", diff --git a/web/src/components/CyberCard.tsx b/web/src/components/CyberCard.tsx index c919d1c..5524ce4 100644 --- a/web/src/components/CyberCard.tsx +++ b/web/src/components/CyberCard.tsx @@ -12,21 +12,21 @@ const CyberCard = ({className, children, animation, hover}: Props) => { return (
- {/* 装饰性边框 */} + {/* 装饰性边框 - 仅在暗色模式下显示 */}
+ className="hidden dark:block absolute top-0 left-0 w-2 h-2 border-t-2 border-l-2 border-cyan-500/30 group-hover:border-cyan-400 transition-colors">
+ className="hidden dark:block absolute top-0 right-0 w-2 h-2 border-t-2 border-r-2 border-cyan-500/30 group-hover:border-cyan-400 transition-colors">
+ className="hidden dark:block absolute bottom-0 left-0 w-2 h-2 border-b-2 border-l-2 border-cyan-500/30 group-hover:border-cyan-400 transition-colors">
+ className="hidden dark:block absolute bottom-0 right-0 w-2 h-2 border-b-2 border-r-2 border-cyan-500/30 group-hover:border-cyan-400 transition-colors"> {animation &&
+ className="hidden dark:block absolute inset-0 bg-gradient-to-b from-transparent via-cyan-500/5 to-transparent -translate-y-full group-hover:translate-y-full transition-transform duration-1000 ease-in-out pointer-events-none"/> }
diff --git a/web/src/components/PublicHeader.tsx b/web/src/components/PublicHeader.tsx index 2c853b7..2d3f896 100644 --- a/web/src/components/PublicHeader.tsx +++ b/web/src/components/PublicHeader.tsx @@ -1,5 +1,6 @@ import {useEffect, useState} from 'react'; -import {Activity, LogIn, Menu, ServerIcon, Settings, X, Sun, Moon, Monitor} from 'lucide-react'; +import {Activity, LogIn, Menu, ServerIcon, Settings, X, Sun, Moon, Monitor, ChevronDown} from 'lucide-react'; +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import {getCurrentUser} from '../api/auth'; import {Link, useLocation} from "react-router-dom"; import {useTheme, type Theme} from '../contexts/ThemeContext'; @@ -11,27 +12,6 @@ const PublicHeader = () => { const {theme, setTheme} = useTheme(); let location = useLocation(); - // 主题切换函数 - const cycleTheme = () => { - const themes: Theme[] = ['light', 'dark', 'auto']; - const currentIndex = themes.indexOf(theme); - const nextIndex = (currentIndex + 1) % themes.length; - setTheme(themes[nextIndex]); - }; - - // 获取当前主题图标 - const getThemeIcon = () => { - switch (theme) { - case 'light': - return Sun; - case 'dark': - return Moon; - case 'auto': - return Monitor; - } - }; - - const ThemeIcon = getThemeIcon(); useEffect(() => { // 检查本地是否有 token @@ -75,11 +55,19 @@ const PublicHeader = () => { let leftName = ''; let rightName = ''; - + if (systemName) { - const mid = Math.floor(systemName.length / 2); - leftName = systemName.substring(0, mid); - rightName = systemName.substring(mid); + // 优先在空格处分割 + const spaceIndex = systemName.indexOf(' '); + if (spaceIndex > 0) { + leftName = systemName.substring(0, spaceIndex); + rightName = systemName.substring(spaceIndex); // 保留空格 + } else { + // 如果没有空格,从中间分割 + const mid = Math.floor(systemName.length / 2); + leftName = systemName.substring(0, mid); + rightName = systemName.substring(mid); + } } return ( @@ -120,16 +108,16 @@ const PublicHeader = () => { ))} @@ -137,7 +125,7 @@ const PublicHeader = () => {
{/* Desktop Right Section */} -
+
{currentTime.toLocaleTimeString()} {
- {/* 主题切换按钮 - Desktop */} - + {/* 主题切换下拉框 - Desktop */} + + + + + + + + setTheme('auto')} + > + + 跟随系统 + {theme === 'auto' && ✓} + + setTheme('light')} + > + + 浅色主题 + {theme === 'light' && ✓} + + setTheme('dark')} + > + + 暗黑主题 + {theme === 'dark' && ✓} + + + + {/* 登录/管理后台按钮 - Desktop */} {isLoggedIn ? ( @@ -168,7 +195,7 @@ const PublicHeader = () => { ) : ( @@ -210,8 +237,8 @@ const PublicHeader = () => { className={` flex items-center gap-3 p-4 rounded-lg border transition-all ${activeTab === tab.id - ? 'bg-cyan-500/20 border-cyan-500/80 text-cyan-500' - : 'bg-cyan-500/5 border-slate-200 dark:border-cyan-500/30 text-slate-600 dark:text-slate-400 hover:bg-cyan-500/10 hover:border-cyan-500/50' + ? 'bg-blue-50 dark:bg-cyan-500/20 border-blue-500 dark:border-cyan-500/80 text-blue-600 dark:text-cyan-500' + : 'bg-slate-50/50 dark:bg-cyan-500/5 border-slate-200 dark:border-cyan-500/30 text-slate-600 dark:text-slate-400 hover:bg-blue-50 dark:hover:bg-cyan-500/10 hover:border-blue-300 dark:hover:border-cyan-500/50' } `} > @@ -223,21 +250,59 @@ const PublicHeader = () => { {/* Divider */}
- {/* Mobile Theme Toggle Button */} - + {/* Mobile Theme Toggle DropdownMenu */} + + + + + + + + setTheme('auto')} + > + + 跟随系统 + {theme === 'auto' && ✓} + + setTheme('light')} + > + + 浅色主题 + {theme === 'light' && ✓} + + setTheme('dark')} + > + + 暗黑主题 + {theme === 'dark' && ✓} + + + + {/* Mobile Login/Admin Button */} {isLoggedIn ? (
管理后台 @@ -246,7 +311,7 @@ const PublicHeader = () => { 登录 diff --git a/web/src/components/ServerCard.tsx b/web/src/components/ServerCard.tsx index d9f5444..548df35 100644 --- a/web/src/components/ServerCard.tsx +++ b/web/src/components/ServerCard.tsx @@ -1,8 +1,22 @@ import type {FC} from 'react'; -import {Activity, AlertTriangle, ArrowDown, ArrowUp, Calendar, Clock, Cpu, HardDrive, MemoryStick, Network, Thermometer} from 'lucide-react'; +import { + Activity, + AlertTriangle, + ArrowDown, + ArrowUp, + Calendar, + Clock, + Cpu, + HardDrive, + MemoryStick, + Network, + Thermometer +} from 'lucide-react'; import type {Agent, LatestMetrics} from '@/types'; import CompactResourceBar from '@/components/CompactResourceBar'; import {formatBytes, formatSpeed, formatUptime} from '@/utils/util'; +import CyberCard from "@/components/CyberCard.tsx"; +import {Link} from "react-router-dom"; interface AgentWithMetrics extends Agent { metrics?: LatestMetrics; @@ -10,7 +24,6 @@ interface AgentWithMetrics extends Agent { interface ServerCardProps { server: AgentWithMetrics; - onClick: (serverId: string) => void; } const calculateNetworkSpeed = (metrics?: LatestMetrics) => { @@ -38,7 +51,7 @@ const getTemperatures = (metrics?: LatestMetrics) => { return metrics.temperature.sort((a, b) => a.type.localeCompare(b.type)); }; -const ServerCard: FC = ({server, onClick}) => { +const ServerCard: FC = ({server}) => { const isOnline = server.status === 1; const cpuUsage = server.metrics?.cpu?.usagePercent ?? 0; const memoryUsage = server.metrics?.memory?.usagePercent ?? 0; @@ -52,178 +65,162 @@ const ServerCard: FC = ({server, onClick}) => { const netConn = server.metrics?.networkConnection; return ( -
onClick(server.id)} - onKeyDown={(event) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - onClick(server.id); - } - }} - className="group bg-[#0f1016]/80 backdrop-blur-md border border-cyan-500/20 shadow-[0_0_15px_rgba(6,182,212,0.05)] hover:border-cyan-500/50 hover:shadow-[0_0_25px_rgba(6,182,212,0.15)] hover:bg-[#0f1016]/90 transition-all duration-300 cursor-pointer overflow-hidden relative" - > - {/* 装饰性边框 */} -
-
-
-
-
- -
- {/* 顶部:名称和状态 */} -
-
-
- {server.name || server.hostname} + + +
+ {/* 顶部:名称和状态 */} +
+
+
+ {server.name || server.hostname} +
+
+ {server.os} + + {server.arch} +
-
- {server.os} - - {server.arch} -
-
- {server.tags && server.tags.length > 0 && ( -
- {server.tags.slice(0, 2).map(tag => ( - + {server.tags && server.tags.length > 0 && ( +
+ {server.tags.slice(0, 2).map(tag => ( + #{tag} - ))} - {server.tags.length > 2 && ( - + ))} + {server.tags.length > 2 && ( + +{server.tags.length - 2} - )} -
- )} -
- - {isOnline && server.metrics?.host && ( -
-
- - {formatUptime(server.metrics.host.uptime)} -
- -
- - {server.metrics.host.procs} 进程 -
-
- )} - - {/* 资源使用情况 */} - {isOnline ? ( -
- - - - {temperatures.length > 0 && ( -
- - {temperatures.map((temp, index) => ( - - {temp.temperature?.toFixed(1)}°C - {temp.type} - {index < temperatures.length - 1 && |} - - ))} + )}
)}
- ) : ( -
- - 离线 -
- )} - {/* 网络和流量 */} -
-
-
- + {isOnline && server.metrics?.host && ( +
+
+ + {formatUptime(server.metrics.host.uptime)} +
+ +
+ + {server.metrics.host.procs} 进程 +
+
+ )} + + {/* 资源使用情况 */} + {isOnline ? ( +
+ + + + {temperatures.length > 0 && ( +
+ + {temperatures.map((temp, index) => ( + + {temp.temperature?.toFixed(1)}°C + {temp.type} + {index < temperatures.length - 1 && + |} + + ))} +
+ )} +
+ ) : ( +
+ + 离线 +
+ )} + + {/* 网络和流量 */} +
+
+
+ {formatSpeed(download)} - + - {formatSpeed(upload)} + {formatSpeed(upload)} +
+ {server.expireTime > 0 && ( +
+ + {new Date(server.expireTime).toLocaleDateString('zh-CN')} +
+ )}
- {server.expireTime > 0 && ( -
- - {new Date(server.expireTime).toLocaleDateString('zh-CN')} + {isOnline && netConn && ( +
+ + + {netConn.established || 0} + 已建立 + + + + {netConn.listen || 0} + 监听 + + + + {netConn.closeWait || 0} + 等待关闭 +
)}
- {isOnline && netConn && ( -
- - - {netConn.established || 0} - 已建立 - - - - {netConn.listen || 0} - 监听 - - - - {netConn.closeWait || 0} - 等待关闭 - + + {/* 流量限制 */} + {server.trafficLimit > 0 && ( +
+
+ 流量使用 + {Math.round((server.trafficUsed || 0) / server.trafficLimit * 100)}% +
+
+
+
)}
- - {/* 流量限制 */} - {server.trafficLimit > 0 && ( -
-
- 流量使用 - {Math.round((server.trafficUsed || 0) / server.trafficLimit * 100)}% -
-
-
-
-
- )} -
-
+ + ); }; diff --git a/web/src/components/common/LittleStatCard.tsx b/web/src/components/common/LittleStatCard.tsx index 1f686eb..725a04d 100644 --- a/web/src/components/common/LittleStatCard.tsx +++ b/web/src/components/common/LittleStatCard.tsx @@ -9,10 +9,10 @@ const LittleStatCard = ({ }) => (
-

{label}

-

{value}

+

{label}

+

{value}

); diff --git a/web/src/components/monitor/AgentStatsTable.tsx b/web/src/components/monitor/AgentStatsTable.tsx index 6ea8804..396fd91 100644 --- a/web/src/components/monitor/AgentStatsTable.tsx +++ b/web/src/components/monitor/AgentStatsTable.tsx @@ -18,7 +18,7 @@ interface AgentStatsTableProps { export const AgentStatsTable = ({monitorStats, monitorType}: AgentStatsTableProps) => { if (monitorStats.length === 0) { return ( -
+

暂无探针数据

); @@ -27,42 +27,42 @@ export const AgentStatsTable = ({monitorStats, monitorType}: AgentStatsTableProp return (
-

探针监控详情

-

各探针的当前状态和统计数据

+

探针监控详情

+

各探针的当前状态和统计数据

- - + - - - {monitorType === 'https' && ( - )} - - + {monitorStats.map((stat, index) => { const color = AGENT_COLORS[index % AGENT_COLORS.length]; return ( + className="hover:bg-slate-100 dark:hover:bg-cyan-950/20 transition-colors"> - {monitorType === 'https' && ( @@ -99,7 +99,7 @@ export const AgentStatsTable = ({monitorStats, monitorType}: AgentStatsTableProp daysLeft={stat.certDaysLeft} /> ) : ( - - + - )} )} @@ -114,7 +114,7 @@ export const AgentStatsTable = ({monitorStats, monitorType}: AgentStatsTableProp ) : ( - - + - )} diff --git a/web/src/components/monitor/MonitorCard.tsx b/web/src/components/monitor/MonitorCard.tsx index 38faf0c..9970aad 100644 --- a/web/src/components/monitor/MonitorCard.tsx +++ b/web/src/components/monitor/MonitorCard.tsx @@ -78,14 +78,14 @@ const MonitorCard = ({monitor, displayMode}: {
+ className="p-2.5 bg-gray-100 dark:bg-cyan-950/30 border border-slate-200 dark:border-cyan-500/20 rounded-lg flex-shrink-0">
-

+

{monitor.name}

-
+
{monitor.target}
@@ -98,24 +98,24 @@ const MonitorCard = ({monitor, displayMode}: { {/* 指标信息 */}
-

+

{displayLabel} {monitor.agentCount > 0 && ( + className="bg-slate-200 dark:bg-slate-700 text-xs px-1.5 rounded-full text-slate-700 dark:text-cyan-300"> {monitor.agentCount} 节点 )}

200 ? 'text-amber-400 drop-shadow-[0_0_8px_rgba(251,191,36,0.5)]' : 'text-white drop-shadow-[0_0_8px_rgba(34,211,238,0.5)]'}`}> - {displayValue}ms + className={`text-xl font-bold flex items-baseline gap-1 ${displayValue > 200 ? 'text-amber-600 dark:text-amber-400 drop-shadow-[0_0_8px_rgba(251,191,36,0.3)] dark:drop-shadow-[0_0_8px_rgba(251,191,36,0.5)]' : 'text-slate-800 dark:text-white drop-shadow-none dark:drop-shadow-[0_0_8px_rgba(34,211,238,0.5)]'}`}> + {displayValue}ms
{monitor.type === 'https' && monitor.certExpiryTime ? ( <> -

SSL 证书

+

SSL 证书

) : ( <> -

上次检测

-

+

上次检测

+

{formatDateTime(monitor.lastCheckTime)}

diff --git a/web/src/components/monitor/MonitorHero.tsx b/web/src/components/monitor/MonitorHero.tsx index 41c4824..1853ea9 100644 --- a/web/src/components/monitor/MonitorHero.tsx +++ b/web/src/components/monitor/MonitorHero.tsx @@ -22,7 +22,7 @@ export const MonitorHero = ({monitor, onBack}: MonitorHeroProps) => {
+
探针名称 + 状态 + 响应时间 + 最后检测 + 证书信息 + 错误信息
- - + + {stat.agentName || stat.agentId.substring(0, 8)}
@@ -82,13 +82,13 @@ export const AgentStatsTable = ({monitorStats, monitorType}: AgentStatsTableProp
- - + + {formatTime(stat.responseTime)}
+ {formatDateTime(stat.checkedAt)}