refactor: 重写系统设置模块,新增维护模式与登录策略功能

- 移除大量无实际效果的冗余配置项
- 新增维护模式拦截逻辑,支持管理员豁免
- 重构登录失败次数限制,从配置动态读取阈值
- 新增站点Logo支持与配置管理
- 优化用户管理页面数据拉取逻辑
- 新增端口批量创建范围模式功能
- 更新依赖包版本与缓存策略
This commit is contained in:
zhang1106
2026-06-12 14:16:29 +08:00
parent 170997ef61
commit 85d6b2c633
19 changed files with 1699 additions and 1600 deletions
+40 -27
View File
@@ -9,9 +9,11 @@
"version": "2.0.0",
"dependencies": {
"@ant-design/icons": "^6.1.0",
"@ant-design/icons-svg": "^4.4.2",
"@react-three/drei": "^9.122.0",
"@react-three/fiber": "^8.18.0",
"@types/dagre": "^0.7.54",
"all": "^0.0.0",
"antd": "^5.8.6",
"axios": "^1.13.6",
"dagre": "^0.8.5",
@@ -143,7 +145,7 @@
},
"node_modules/@ant-design/icons-svg": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz",
"resolved": "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz",
"integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==",
"license": "MIT"
},
@@ -281,6 +283,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -629,6 +632,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -672,6 +676,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -693,6 +698,7 @@
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
"integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@emotion/memoize": "^0.9.0"
}
@@ -1767,6 +1773,7 @@
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz",
"integrity": "sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.17.8",
"@types/react-reconciler": "^0.26.7",
@@ -2371,8 +2378,7 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -2733,6 +2739,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz",
"integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -2763,6 +2770,7 @@
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz",
"integrity": "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@dimforge/rapier3d-compat": "~0.12.0",
"@tweenjs/tween.js": "~23.1.3",
@@ -2908,7 +2916,6 @@
"integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/parser": "^7.29.0",
"@vue/shared": "3.5.29",
@@ -2923,7 +2930,6 @@
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"engines": {
"node": ">=0.12"
},
@@ -2936,8 +2942,7 @@
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.29",
@@ -2945,7 +2950,6 @@
"integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-core": "3.5.29",
"@vue/shared": "3.5.29"
@@ -2975,8 +2979,7 @@
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.29",
@@ -2984,7 +2987,6 @@
"integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.29",
"@vue/shared": "3.5.29"
@@ -2996,7 +2998,6 @@
"integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/shared": "3.5.29"
}
@@ -3007,7 +3008,6 @@
"integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.29",
"@vue/shared": "3.5.29"
@@ -3019,7 +3019,6 @@
"integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.29",
"@vue/runtime-core": "3.5.29",
@@ -3033,7 +3032,6 @@
"integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-ssr": "3.5.29",
"@vue/shared": "3.5.29"
@@ -3047,8 +3045,7 @@
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz",
"integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@webgpu/types": {
"version": "0.1.69",
@@ -3062,6 +3059,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -3115,13 +3113,18 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/all": {
"version": "0.0.0",
"resolved": "https://registry.npmmirror.com/all/-/all-0.0.0.tgz",
"integrity": "sha512-0oKlfNVv2d+d7c1gwjGspzgbwot47PGQ4b3v1ccx4mR8l9P/Y6E6Dr/yE8lNT63EcAKEbHo6UG3odDpC/NQcKw==",
"license": "MIT"
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
}
@@ -3521,6 +3524,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -3997,6 +4001,7 @@
"resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@@ -4127,7 +4132,8 @@
"version": "1.11.19",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/debug": {
"version": "4.4.3",
@@ -4242,8 +4248,7 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/dom-helpers": {
"version": "5.2.1",
@@ -4542,6 +4547,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -4602,6 +4608,7 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -5965,6 +5972,7 @@
"integrity": "sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@acemir/cssom": "^0.9.28",
"@asamuzakjp/dom-selector": "^6.7.6",
@@ -6179,7 +6187,6 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -6721,6 +6728,7 @@
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -6750,7 +6758,6 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -6766,7 +6773,6 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -6779,8 +6785,7 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/promise-worker-transferable": {
"version": "1.0.4",
@@ -7453,6 +7458,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -7477,6 +7483,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -8424,6 +8431,7 @@
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
@@ -8450,7 +8458,8 @@
"version": "0.183.2",
"resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
"integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/three-mesh-bvh": {
"version": "0.7.8",
@@ -8973,6 +8982,7 @@
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
@@ -9001,6 +9011,7 @@
"integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.18.10",
"postcss": "^8.4.27",
@@ -9620,6 +9631,7 @@
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -10025,6 +10037,7 @@
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+2
View File
@@ -14,9 +14,11 @@
},
"dependencies": {
"@ant-design/icons": "^6.1.0",
"@ant-design/icons-svg": "^4.4.2",
"@react-three/drei": "^9.122.0",
"@react-three/fiber": "^8.18.0",
"@types/dagre": "^0.7.54",
"all": "^0.0.0",
"antd": "^5.8.6",
"axios": "^1.13.6",
"dagre": "^0.8.5",
+14 -2
View File
@@ -48,6 +48,7 @@ import {
} from 'react-router-dom';
import { useAuth } from './hooks/useAuth';
import { useConfig } from './hooks/useConfig';
import { useSiteLogo } from './stores/configStore';
import { useDesignTokens } from './hooks/useDesignTokens';
import useIdleTimeout from './hooks/useIdleTimeout';
import { SWRConfig, swrConfig } from './hooks/useSWR';
@@ -153,6 +154,7 @@ const AppLayout = ({ children }) => {
const [idleConfig, setIdleConfig] = useState(DEFAULT_IDLE_CONFIG);
const { user, logout } = useAuth();
const { config } = useConfig();
const siteLogo = useSiteLogo();
const navigate = useNavigate();
const location = useLocation();
const designTokens = useDesignTokens();
@@ -409,14 +411,24 @@ const AppLayout = ({ children }) => {
width: '36px',
height: '36px',
borderRadius: designTokens.borderRadius.small,
background: designTokens.colors.primary.gradient,
background: siteLogo ? 'transparent' : designTokens.colors.primary.gradient,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
overflow: 'hidden',
}}
>
<CloudServerOutlined style={{ fontSize: '18px', color: '#ffffff' }} />
{siteLogo ? (
<img
src={siteLogo}
alt="Logo"
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
onError={e => { e.target.style.display = 'none'; }}
/>
) : (
<CloudServerOutlined style={{ fontSize: '18px', color: '#ffffff' }} />
)}
</div>
{!collapsed && (
<div>
+6
View File
@@ -36,6 +36,12 @@ axios.interceptors.response.use(
}
}
}
// 维护模式拦截:503 + MAINTENANCE_MODE code
if (error.response?.status === 503 && error.response?.data?.code === 'MAINTENANCE_MODE') {
if (maintenanceCallback) {
maintenanceCallback(true);
}
}
return Promise.reject(error);
}
);
@@ -24,8 +24,14 @@ function MaintenanceBanner() {
return;
}
setReason(maintenanceData.reason || '系统维护中');
setStartTime(maintenanceData.startTime);
// 支持布尔值(从503拦截器)或对象(从其他来源)
if (typeof maintenanceData === 'boolean' && maintenanceData) {
setReason('系统维护中,仅管理员可访问');
setStartTime(new Date().toISOString());
} else if (typeof maintenanceData === 'object') {
setReason(maintenanceData.reason || '系统维护中');
setStartTime(maintenanceData.startTime);
}
setVisible(true);
};
+345 -86
View File
@@ -1,4 +1,4 @@
import React, { useState, useCallback, useEffect, useRef } from 'react';
import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react';
import {
Modal,
Form,
@@ -14,6 +14,7 @@ import {
Divider,
Row,
Col,
Segmented,
} from 'antd';
import {
PlusOutlined,
@@ -23,6 +24,7 @@ import {
TagOutlined,
FileTextOutlined,
CheckCircleOutlined,
SwapOutlined,
} from '@ant-design/icons';
import axios from 'axios';
import { designTokens } from '../config/theme';
@@ -31,6 +33,24 @@ import CloseButton from './CloseButton';
const { Option } = Select;
const { TextArea } = Input;
/**
* 从端口名中提取前缀和末尾数字
* @param {string} portName - 端口名称,如 "1/0/48" 或 "eth0/1"
* @returns {{ prefix: string, num: number } | null} 前缀和数字,无数字则返回 null
*/
function extractPrefixAndNum(portName) {
if (!portName || typeof portName !== 'string') return null;
const match = portName.match(/^(.*?)(\d+)$/);
if (!match) return null;
return { prefix: match[1], num: parseInt(match[2], 10) };
}
/**
* 快捷模式解析端口范围(单输入框格式)
* 支持完整格式 "1/0/1-1/0/48" 和简写格式 "1/0/1-48"
* @param {string} portName - 端口范围字符串
* @returns {{ isRange: boolean, prefix: string, startNum: number, endNum: number, portCount: number, ports: string[], error?: string } | null}
*/
function parsePortRange(portName) {
if (!portName || typeof portName !== 'string') {
return null;
@@ -42,54 +62,135 @@ function parsePortRange(portName) {
return null;
}
const [startPart, endPart] = trimmed.split('-').map(s => s.trim());
// 用最后一个 - 分割,解决多 - 问题(如 eth-0-1-eth-0-48
const lastDashIdx = trimmed.lastIndexOf('-');
const startPart = trimmed.substring(0, lastDashIdx).trim();
const endPart = trimmed.substring(lastDashIdx + 1).trim();
if (!startPart || !endPart) {
return null;
return { isRange: false, prefix: '', startNum: 0, endNum: 0, portCount: 0, ports: [], error: '格式不完整,- 前后都需要有内容' };
}
const startNumMatch = startPart.match(/(\d+)$/);
const endNumMatch = endPart.match(/(\d+)$/);
const startParsed = extractPrefixAndNum(startPart);
const endParsed = extractPrefixAndNum(endPart);
if (!startNumMatch || !endNumMatch) {
return null;
if (!startParsed) {
return { isRange: false, prefix: '', startNum: 0, endNum: 0, portCount: 0, ports: [], error: `起始端口名 "${startPart}" 末尾无数字,无法解析范围` };
}
const startNum = parseInt(startNumMatch[1], 10);
const endNum = parseInt(endNumMatch[1], 10);
// 简写格式:endPart 只有数字(如 "1/0/1-48"),endParsed.prefix 为空
if (!endParsed) {
return { isRange: false, prefix: '', startNum: 0, endNum: 0, portCount: 0, ports: [], error: `结束端口名 "${endPart}" 末尾无数字,无法解析范围` };
}
if (startNum >= endNum || endNum - startNum > 1000) {
return null;
let prefix;
let startNum = startParsed.num;
let endNum = endParsed.num;
if (endParsed.prefix === '') {
// 简写格式:1/0/1-48,结束部分只有数字,前缀沿用起始部分
prefix = startParsed.prefix;
} else {
// 完整格式:1/0/1-1/0/48,校验前缀一致
if (startParsed.prefix !== endParsed.prefix) {
return { isRange: false, prefix: '', startNum: 0, endNum: 0, portCount: 0, ports: [], error: `前后端口名前缀不一致:"${startParsed.prefix}" vs "${endParsed.prefix}"` };
}
prefix = startParsed.prefix;
}
if (startNum >= endNum) {
return { isRange: false, prefix: '', startNum: 0, endNum: 0, portCount: 0, ports: [], error: `起始数字 ${startNum} 必须小于结束数字 ${endNum}` };
}
if (endNum - startNum > 1000) {
return { isRange: false, prefix: '', startNum: 0, endNum: 0, portCount: 0, ports: [], error: '单次最多创建1000个端口' };
}
const startIdx = startPart.length - startNumMatch[0].length;
const prefix = startPart.substring(0, startIdx);
const portCount = endNum - startNum + 1;
const ports = [];
for (let i = 0; i < portCount; i++) {
const num = startNum + i;
ports.push(`${prefix}${num}`);
ports.push(`${prefix}${startNum + i}`);
}
return {
isRange: true,
prefix,
startNum,
endNum,
portCount,
ports,
};
return { isRange: true, prefix, startNum, endNum, portCount, ports };
}
function generatePortNames(portName) {
const result = parsePortRange(portName);
if (result && result.isRange) {
return result.ports;
/**
* 范围模式解析(双输入框:起始端口名 + 结束端口名)
* @param {string} startPortName - 起始端口名
* @param {string} endPortName - 结束端口名
* @returns {{ isRange: boolean, prefix: string, startNum: number, endNum: number, portCount: number, ports: string[], error?: string }}
*/
function parseRangeMode(startPortName, endPortName) {
if (!startPortName || !endPortName) {
return { isRange: false, prefix: '', startNum: 0, endNum: 0, portCount: 0, ports: [], error: '请填写起始和结束端口名' };
}
return [portName];
const startParsed = extractPrefixAndNum(startPortName.trim());
const endParsed = extractPrefixAndNum(endPortName.trim());
if (!startParsed) {
return { isRange: false, prefix: '', startNum: 0, endNum: 0, portCount: 0, ports: [], error: `起始端口名 "${startPortName}" 末尾无数字` };
}
if (!endParsed) {
return { isRange: false, prefix: '', startNum: 0, endNum: 0, portCount: 0, ports: [], error: `结束端口名 "${endPortName}" 末尾无数字` };
}
if (startParsed.prefix !== endParsed.prefix) {
return { isRange: false, prefix: '', startNum: 0, endNum: 0, portCount: 0, ports: [], error: `前后端口名前缀不一致:"${startParsed.prefix || '(空)'}" vs "${endParsed.prefix || '(空)'}"` };
}
if (startParsed.num >= endParsed.num) {
return { isRange: false, prefix: '', startNum: 0, endNum: 0, portCount: 0, ports: [], error: `起始数字 ${startParsed.num} 必须小于结束数字 ${endParsed.num}` };
}
if (endParsed.num - startParsed.num > 1000) {
return { isRange: false, prefix: '', startNum: 0, endNum: 0, portCount: 0, ports: [], error: '单次最多创建1000个端口' };
}
const prefix = startParsed.prefix;
const startNum = startParsed.num;
const endNum = endParsed.num;
const portCount = endNum - startNum + 1;
const ports = [];
for (let i = 0; i < portCount; i++) {
ports.push(`${prefix}${startNum + i}`);
}
return { isRange: true, prefix, startNum, endNum, portCount, ports };
}
/**
* 根据模式和输入生成端口名列表
* @param {string} mode - 'range' 或 'quick'
* @param {object} params - { portName } 或 { startPortName, endPortName }
* @returns {{ ports: string[], error: string | null, parseResult: object | null }}
*/
function generatePortNames(mode, params) {
if (mode === 'range') {
const { startPortName, endPortName } = params;
if (!startPortName && !endPortName) {
return { ports: [], error: null, parseResult: null };
}
if (!startPortName || !endPortName) {
// 只填了一个,暂不报错,返回空
return { ports: [], error: null, parseResult: null };
}
const result = parseRangeMode(startPortName, endPortName);
if (result.isRange) {
return { ports: result.ports, error: null, parseResult: result };
}
return { ports: [], error: result.error, parseResult: result };
}
// 快捷模式(仅单端口,不支持范围)
const { portName } = params;
if (!portName) {
return { ports: [], error: null, parseResult: null };
}
// 快捷模式直接作为单端口处理
return { ports: [portName.trim()], error: null, parseResult: null };
}
function PortCreateModal({
@@ -107,12 +208,18 @@ function PortCreateModal({
const [previewPorts, setPreviewPorts] = useState([]);
const [showPreview, setShowPreview] = useState(false);
const [nicList, setNicList] = useState([]);
const [portMode, setPortMode] = useState('quick'); // 'range' | 'quick'
const [parseError, setParseError] = useState(null);
const [parseResult, setParseResult] = useState(null);
const prevVisibleRef = useRef(false);
useEffect(() => {
if (visible && !prevVisibleRef.current) {
setPreviewPorts([]);
setShowPreview(false);
setPortMode('quick');
setParseError(null);
setParseResult(null);
form.resetFields();
if (networkCard) {
@@ -144,25 +251,84 @@ function PortCreateModal({
}
};
const handlePortNameChange = useCallback(e => {
const value = e.target.value;
const ports = generatePortNames(value);
/**
* 更新端口预览和解析状态
* @param {string} mode - 当前模式
* @param {object} params - 解析参数
*/
const updatePortPreview = useCallback((mode, params) => {
const result = generatePortNames(mode, params);
if (ports.length > 1) {
setPreviewPorts(ports.slice(0, 20));
setParseError(result.error);
setParseResult(result.parseResult);
if (result.ports.length > 1) {
setPreviewPorts(result.ports.slice(0, 20));
setShowPreview(true);
} else if (result.ports.length === 1) {
setPreviewPorts([]);
setShowPreview(false);
} else {
setPreviewPorts([]);
setShowPreview(false);
}
}, []);
/** 快捷模式:端口名输入变化 */
const handleQuickPortNameChange = useCallback(e => {
const value = e.target.value;
updatePortPreview('quick', { portName: value });
}, [updatePortPreview]);
/** 范围模式:起始/结束端口名输入变化 */
const handleRangePortNameChange = useCallback(() => {
const startPortName = form.getFieldValue('startPortName');
const endPortName = form.getFieldValue('endPortName');
updatePortPreview('range', { startPortName, endPortName });
}, [form, updatePortPreview]);
/** 模式切换处理 */
const handleModeChange = useCallback((newMode) => {
setPortMode(newMode);
setParseError(null);
setParseResult(null);
setPreviewPorts([]);
setShowPreview(false);
// 清空端口名相关字段
form.setFieldsValue({ portName: undefined, startPortName: undefined, endPortName: undefined });
}, [form]);
/** 获取当前生成的端口名列表 */
const getCurrentPortNames = useCallback(() => {
if (portMode === 'range') {
const startPortName = form.getFieldValue('startPortName');
const endPortName = form.getFieldValue('endPortName');
return generatePortNames('range', { startPortName, endPortName });
}
const portName = form.getFieldValue('portName');
return generatePortNames('quick', { portName });
}, [portMode, form]);
const handleSubmit = useCallback(async () => {
try {
const values = await form.validateFields();
setLoading(true);
const portNames = generatePortNames(values.portName);
// 获取端口名列表
const { ports: portNames, error: genError } = getCurrentPortNames();
if (genError) {
message.error(genError);
setLoading(false);
return;
}
if (!portNames || portNames.length === 0) {
message.error('请输入端口名称');
setLoading(false);
return;
}
const finalNicId = disableNicChange && defaultNicId ? defaultNicId : values.nicId || null;
if (portNames.length === 1) {
@@ -203,6 +369,8 @@ function PortCreateModal({
form.resetFields();
setPreviewPorts([]);
setShowPreview(false);
setParseError(null);
setParseResult(null);
onSuccess?.();
onClose();
} catch (error) {
@@ -214,22 +382,36 @@ function PortCreateModal({
} finally {
setLoading(false);
}
}, [device, form, onClose, onSuccess]);
}, [device, form, onClose, onSuccess, getCurrentPortNames, disableNicChange, defaultNicId]);
const handleCancel = useCallback(() => {
form.resetFields();
setPreviewPorts([]);
setShowPreview(false);
setParseError(null);
setParseResult(null);
onClose();
}, [form, onClose]);
const handleValuesChange = changedValues => {
if (changedValues.portName) {
handlePortNameChange({ target: { value: changedValues.portName } });
if (portMode === 'quick' && changedValues.portName) {
handleQuickPortNameChange({ target: { value: changedValues.portName } });
}
if (portMode === 'range' && (changedValues.startPortName !== undefined || changedValues.endPortName !== undefined)) {
handleRangePortNameChange();
}
};
const portCount = previewPorts.length || (form.getFieldValue('portName') && !showPreview ? 1 : 0);
const portCount = useMemo(() => {
if (parseResult?.portCount) return parseResult.portCount;
if (previewPorts.length > 0) return previewPorts.length;
// 单端口情况
if (portMode === 'quick') {
const portName = form.getFieldValue('portName');
if (portName && !parseError) return 1;
}
return 0;
}, [parseResult, previewPorts, portMode, form, parseError]);
const selectedNic = nicList.find(nic => nic.nicId === form.getFieldValue('nicId'));
const styles = {
@@ -429,12 +611,28 @@ function PortCreateModal({
message="格式说明"
description={
<div style={{ fontSize: '11px', lineHeight: '1.6' }}>
<div>
<strong>单个端口</strong>eth0/1gigabitethernet1/0/1
</div>
<div>
<strong>端口范围</strong>1/0/1-1/0/48创建 1/0/1 1/0/48 共48个端口
</div>
{portMode === 'range' ? (
<>
<div>
<strong>范围模式</strong>分别输入起始和结束端口名系统自动生成端口序列
</div>
<div>
<strong>规则</strong>起始和结束端口名的前缀必须一致且起始数字需小于结束数字
</div>
<div>
<strong>示例</strong>起始 1/0/1结束 1/0/48 创建 1/0/1 1/0/48 48 个端口
</div>
</>
) : (
<>
<div>
<strong>单端口模式</strong>每次创建一个端口如需批量创建请切换至范围模式
</div>
<div>
<strong>示例</strong>eth0/1GigabitEthernet0/0/11/0/24
</div>
</>
)}
</div>
}
type="info"
@@ -448,52 +646,114 @@ function PortCreateModal({
<div style={styles.sectionTitle}>
<TagOutlined style={{ color: designTokens.colors.primary.main }} />
端口标识
<div style={{ marginLeft: 'auto' }}>
<Segmented
size="small"
value={portMode}
onChange={handleModeChange}
options={[
{ label: '单端口', value: 'quick' },
{ label: '范围模式', value: 'range' },
]}
/>
</div>
</div>
<Form.Item
name="portName"
rules={[
{ required: true, message: '请输入端口名称' },
{
pattern: /^[\w\/:\-]+$/,
message: '端口名称格式不正确',
},
{
validator: (_, value) => {
if (!value) return Promise.resolve();
const ports = generatePortNames(value);
if (ports.length > 1000) {
return Promise.reject(new Error('单次最多创建1000个端口'));
{portMode === 'range' ? (
<>
<Form.Item
name="startPortName"
label={<span style={styles.fieldLabel}>起始端口名</span>}
rules={[
{ required: true, message: '请输入起始端口名' },
{
pattern: /^[\w\/:]+$/,
message: '端口名称格式不正确',
},
]}
>
<Input
size="small"
placeholder="例如: 1/0/1"
prefix={
<TagOutlined
style={{ color: designTokens.colors.neutral[400], fontSize: '12px' }}
/>
}
return Promise.resolve();
},
},
]}
>
<Input
size="small"
placeholder="例如: eth0/1 或 1/0/1-1/0/48"
prefix={
<TagOutlined
style={{ color: designTokens.colors.neutral[400], fontSize: '12px' }}
style={{ borderRadius: '6px' }}
onChange={handleRangePortNameChange}
/>
}
style={{ borderRadius: '6px' }}
suffix={
<Tooltip title="支持单个端口或端口范围(如 1/0/1-1/0/48">
<InfoCircleOutlined
style={{ color: designTokens.colors.neutral[400], fontSize: '11px' }}
</Form.Item>
<Form.Item
name="endPortName"
label={<span style={styles.fieldLabel}>结束端口名</span>}
rules={[
{ required: true, message: '请输入结束端口名' },
{
pattern: /^[\w\/:]+$/,
message: '端口名称格式不正确',
},
]}
>
<Input
size="small"
placeholder="例如: 1/0/48"
prefix={
<TagOutlined
style={{ color: designTokens.colors.neutral[400], fontSize: '12px' }}
/>
}
style={{ borderRadius: '6px' }}
onChange={handleRangePortNameChange}
/>
</Form.Item>
</>
) : (
<Form.Item
name="portName"
rules={[
{ required: true, message: '请输入端口名称' },
{
pattern: /^[\w\/:]+$/,
message: '端口名称格式不正确',
},
]}
>
<Input
size="small"
placeholder="例如: eth0/1 或 1/0/24"
prefix={
<TagOutlined
style={{ color: designTokens.colors.neutral[400], fontSize: '12px' }}
/>
</Tooltip>
}
/>
</Form.Item>
}
style={{ borderRadius: '6px' }}
suffix={
<Tooltip title="输入单个端口名称,批量创建请切换至范围模式">
<InfoCircleOutlined
style={{ color: designTokens.colors.neutral[400], fontSize: '11px' }}
/>
</Tooltip>
}
/>
</Form.Item>
)}
{showPreview && (
{/* 解析失败提示 */}
{parseError && (
<Alert
message={parseError}
type="error"
showIcon
style={{ marginBottom: '8px', borderRadius: '6px', fontSize: '12px' }}
/>
)}
{showPreview && parseResult && (
<div style={styles.previewCard}>
<div style={styles.previewTitle}>
<ThunderboltOutlined />
将创建 {previewPorts.length} 个端口
将创建 {parseResult.portCount} 个端口
</div>
<div style={styles.previewTags}>
{previewPorts.map((port, index) => (
@@ -501,15 +761,14 @@ function PortCreateModal({
{port}
</Tag>
))}
{parsePortRange(form.getFieldValue('portName'))?.portCount >
previewPorts.length && (
{parseResult.portCount > previewPorts.length && (
<Tag
style={{
...styles.previewTag,
background: designTokens.colors.neutral[100],
}}
>
... {parsePortRange(form.getFieldValue('portName'))?.portCount}
... {parseResult.portCount}
</Tag>
)}
</div>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+10 -5
View File
@@ -48,11 +48,6 @@ const UserManagement = () => {
const [passwordForm] = Form.useForm();
const fileInputRef = useRef(null);
useEffect(() => {
fetchUsers();
fetchRoles();
}, [pagination.current]);
const fetchUsers = useCallback(async () => {
setLoading(true);
try {
@@ -89,6 +84,16 @@ const UserManagement = () => {
}
}, []);
// 分页或 Tab 切换时刷新用户列表(依赖 fetchUsers,由其内部 useCallback 依赖自动驱动)
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
// 角色列表仅在挂载时拉取一次,避免重复请求
useEffect(() => {
fetchRoles();
}, []);
const handleAdd = useCallback(() => {
setEditingUser(null);
form.resetFields();
+3 -7
View File
@@ -8,15 +8,10 @@ import axios from 'axios';
const defaultConfig = {
site_name: '机柜管理系统',
site_logo: '',
primary_color: '#667eea',
secondary_color: '#764ba2',
sidebar_collapsed: false,
compact_mode: false,
animation_enabled: true,
language: 'zh-CN',
timezone: 'Asia/Shanghai',
date_format: 'YYYY-MM-DD',
session_timeout: 30,
idle_timeout: 30,
max_login_attempts: 5,
maintenance_mode: false,
};
@@ -84,5 +79,6 @@ export const useConfigStore = create((set, get) => ({
export const useConfig = () => useConfigStore((state) => state.config);
export const useConfigLoading = () => useConfigStore((state) => state.loading);
export const useSiteName = () => useConfigStore((state) => state.config.site_name);
export const useSiteLogo = () => useConfigStore((state) => state.config.site_logo);
export const usePrimaryColor = () => useConfigStore((state) => state.config.primary_color);
export const useSecondaryColor = () => useConfigStore((state) => state.config.secondary_color);