diff --git a/README.md b/README.md index 6214483..035ee04 100644 --- a/README.md +++ b/README.md @@ -80,3 +80,25 @@ SITE_2=name="master-2",backend_url="wss://m2.example.com",token="xyz789" 一个 `SITE_n` 都没设的话脚本啥也不干 直接用仓库里那份 `config.json` 本地 `npm run dev` 走的是 vite 直接起 也不会触发这个脚本 可以只有一个 `SITE` 不强制 `SITE_2` `SITE_3` 之类的 + +# 🎉 近期核心更新与深度优化 + +近期针对性能、高频检测支持以及 UI 质感进行了大刀阔斧的重构,带来“秒开”与“仪表盘级别”的监控体验: + +### 🚀 极致的性能与加载体验 +- ** IndexedDB 刷新秒开**:突破了传统的 `localStorage` 容量限制,引入原生 IndexedDB 持久化存储全量 24h 历史 TCPing 数据。现在每次刷新页面或重新打开浏览器,无需任何请求等待,**历史监控图表瞬间“秒开”铺满**。 +- **无缝增量渲染**:在界面秒开的基础上,后台以“请求一个、渲染一个”的瀑布流方式静默拉取缺失的数据段并无缝拼合,彻底告别旧版漫长的干等白屏。 +- **高并发节点保护**:重写数据拉取逻辑为按顺序单节点并发查询(Concurrency=1),有效避免几十台节点瞬间发起几百个 Websocket 请求导致后端卡死或触发限流。 +- **支持 20 秒高频检测**:将单节点 TCPing 拉取上限扩容至 `15000` 条。即使节点每 20 秒执行一次 Ping 检测(一日上万条记录),也能轻松拉满 24 小时的完整热力图,彻底解决右侧数据缺失或只有几小时记录的 Bug。 + +### 🎨 仪表盘级别的赛博质感 +- **细粒度网格进度条**:废弃了传统的单一直线进度条,CPU / 内存 / 磁盘的使用率现在采用 **20 格分离式点阵风格**(类似 TCPing 热力图),极具赛博科幻仪表盘质感。 +- **动态健康阈值色彩**: + - `< 70%` 正常状态:黑夜模式为纯白,白天模式为纯黑,带来极简的高对比度。 + - `70% - 100%` 高负载:平滑的琥珀色到橙色警告渐变。 + - `> 100%` 超频爆表:当 CPU 使用率破表时,激发 **专属高亮鲜红 + 红色光晕扩散特效**,监控一眼可见。 +- **全局日夜间双主题适配**: + - 白天模式(Light):去除了深色模式下的朦胧透明感,全面启用 `slate-50` 纯净白灰底色及高对比度石板灰字体,界面锐利、通透明亮。 + - 黑夜模式(Dark):保持深邃暗黑的极客赛博风格。 +- **24h 在线率前置**:无需点进详情页,在首页卡片上直接渲染 24h 历史存活状态长条。 +- **精准掉线计算**:优化在线率统计算法,仅对“完全掉线(100% 丢包)”的时段进行离线惩罚,间歇性的偶发丢包不再会导致服务器被误判为离线并标红,更贴合实际生产环境。 diff --git a/index.html b/index.html index edd8033..983bbca 100644 --- a/index.html +++ b/index.html @@ -14,12 +14,12 @@ } } catch (_) {} - +
- + diff --git a/package-lock.json b/package-lock.json index 8463175..274fe7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nodeget-statusshow", - "version": "1.3.2", + "version": "1.4.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nodeget-statusshow", - "version": "1.3.2", + "version": "1.4.3", "dependencies": { "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", @@ -799,9 +799,9 @@ } }, "node_modules/bare-events": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", - "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.3.tgz", + "integrity": "sha512-HdUm8EMQBLaJvGUdidNNbqpA1kYkwNcb+MYxkxCLAPJGQzlv9J0C24h8V65Z4c5GLd/JEALDvpFCQgpLJqc0zw==", "dev": true, "license": "Apache-2.0", "peerDependencies": { diff --git a/package.json b/package.json index f113589..7c42615 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build": "vite build", "postbuild": "node scripts/build-template-config.mjs && node scripts/build-filelist.mjs && node scripts/build-zip.mjs && node scripts/build-config.mjs", "preview": "vite preview", + "deploy:cf": "npm run build && npx wrangler deploy", "typecheck": "tsc -p tsconfig.json" }, "dependencies": { diff --git a/public/linux-logo-icon/download.html b/public/linux-logo-icon/download.html new file mode 100644 index 0000000..5544314 --- /dev/null +++ b/public/linux-logo-icon/download.html @@ -0,0 +1,202 @@ + + + + + + + NodeGet Theme Downloader + + + + + + +
+

NodeGet Theme Downloader

+ Home +
+
+

+ 这是一个通用的下载器,用于提取当前网站所实用的 NodeGet 主题,下载后修改 nodeget-theme.json 即可将压缩包部署到任意静态储存服务。 +

+ + +
+
+ + + + + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index b67dbea..d8efd9f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,12 +11,13 @@ import { NodeTable } from './components/NodeTable' import { NodeDetail } from './components/NodeDetail' import { TagFilter } from './components/TagFilter' import { RegionFilter } from './components/RegionFilter' +import { useFleetTcpPing } from './hooks/useFleetTcpPing' +import { deriveUsage, displayName } from './utils/derive' +import type { Sort, View } from './types' const WorldMap = lazy(() => import('./components/WorldMap').then(m => ({ default: m.WorldMap })), ) -import { deriveUsage, displayName } from './utils/derive' -import type { Sort, View } from './types' const DEFAULT_LOGO = `${import.meta.env.BASE_URL}logo.png` const VIEW_KEY = 'nodeget.view' @@ -40,7 +41,7 @@ const num = (v?: number) => (Number.isFinite(v) ? (v as number) : -Infinity) export function App() { const { config, error: configError } = useConfig() - const { nodes, errors, pool } = useNodes(config) + const { nodes, errors, loading, pool } = useNodes(config) const [view, setView] = useState(initialView) const [sort, setSort] = useState(initialSort) @@ -109,9 +110,7 @@ export function App() { const list = useMemo(() => { let arr = [...nodes.values()].filter(n => !n.meta?.hidden) if (activeTag) arr = arr.filter(n => n.meta?.tags?.includes(activeTag)) - if (activeRegion) { - arr = arr.filter(n => n.meta?.region?.trim().toUpperCase() === activeRegion) - } + if (activeRegion) arr = arr.filter(n => n.meta?.region?.trim().toUpperCase() === activeRegion) const q = query.trim().toLowerCase() if (q) { @@ -134,7 +133,6 @@ export function App() { } const rank = new Map(regions.list.map((r, i) => [r.code, i])) - return arr.sort((a, b) => { if (a.online !== b.online) return a.online ? -1 : 1 @@ -151,22 +149,35 @@ export function App() { const ar = rank.get(a.meta?.region?.trim().toUpperCase() || '') ?? Infinity const br = rank.get(b.meta?.region?.trim().toUpperCase() || '') ?? Infinity cmp = ar - br + } else if (sort === 'default') { + cmp = (a.meta?.order ?? 0) - (b.meta?.order ?? 0) } - else if (sort === 'default') cmp = (a.meta?.order ?? 0) - (b.meta?.order ?? 0) return cmp || displayName(a).localeCompare(displayName(b)) }) }, [nodes, query, activeTag, activeRegion, sort, regions]) - const selectedNode = selected ? nodes.get(selected) || null : null + const selectedNode = selected + ? nodes.get(selected) || [...nodes.values()].find(n => n.uuid === selected) || null + : null + const siteName = (config as any)?.user_preferences?.site_name ?? (config as any)?.site_name ?? '未设置站点' + const siteLogo = (config as any)?.user_preferences?.site_logo ?? (config as any)?.site_logo ?? DEFAULT_LOGO + const siteFooter = (config as any)?.user_preferences?.footer ?? (config as any)?.footer + const fleetTcpPing = useFleetTcpPing(pool, list) if (configError) { return (
- + - 加载 config.json 失败 - {String(configError.message || configError)} + 加载配置失败 + +

{String(configError.message || configError)}

+

+ 本地调试可设置 NODEGET_MOCK=true 使用 Mock 数据,或复制 + .env.example .env.local 后填入真实配置。 +

+
) @@ -175,12 +186,11 @@ export function App() { if (!config) { return (
- 加载中… + 加载中...
) } - const logo = config.user_preferences.site_logo || DEFAULT_LOGO const empty = list.length === 0 const hasErrors = errors.length > 0 @@ -188,8 +198,8 @@ export function App() {
-
+
{!empty && ( } - {empty && !hasErrors && ( + {empty && !hasErrors && loading && (
- 连接后端中… + 正在连接后端...
)} - {empty && hasErrors && ( -
暂无节点
+ {empty && (!loading || hasErrors) && ( +
+ 暂无可展示节点 +
)} {!empty && view === 'cards' && (
{list.map(n => ( - + ))}
)} @@ -232,7 +251,7 @@ export function App() { - 加载地图中… + 正在加载地图...
} > @@ -241,7 +260,7 @@ export function App() { )} {hasErrors && ( - + {errors.length} 个后端错误 @@ -258,7 +277,11 @@ export function App() { )} -