chore: 统一代码风格并配置ESLint和Prettier
配置ESLint和Prettier规则 添加前端和后端的忽略文件 统一代码格式和缩进 修复代码风格问题
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
// 根配置不直接检查文件,而是作为项目入口
|
||||||
|
// 实际检查由 frontend/ 和 backend/ 各自的配置处理
|
||||||
|
ignorePatterns: ['frontend/**', 'backend/**', 'node_modules/**', 'dist/**'],
|
||||||
|
overrides: []
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# 依赖
|
||||||
|
node_modules/
|
||||||
|
frontend/node_modules/
|
||||||
|
backend/node_modules/
|
||||||
|
|
||||||
|
# 构建输出
|
||||||
|
dist/
|
||||||
|
frontend/dist/
|
||||||
|
backend/dist/
|
||||||
|
|
||||||
|
# 日志
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# 数据库
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
|
||||||
|
# 上传文件
|
||||||
|
backend/uploads/
|
||||||
|
|
||||||
|
# 其他
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# 构建输出
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# 依赖
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# 日志
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# 数据库
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
|
||||||
|
# 上传文件
|
||||||
|
uploads/
|
||||||
|
|
||||||
|
# 其他
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
es2021: true,
|
||||||
|
jest: true
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:prettier/recommended'
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module'
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
||||||
|
'no-console': ['warn', { allow: ['warn', 'error', 'info'] }],
|
||||||
|
'no-undef': 'error',
|
||||||
|
'no-unreachable': 'error',
|
||||||
|
'no-unused-expressions': 'error',
|
||||||
|
'eqeqeq': ['error', 'always'],
|
||||||
|
'curly': ['error', 'all'],
|
||||||
|
'no-var': 'error',
|
||||||
|
'prefer-const': 'error'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
Generated
+2631
-3
File diff suppressed because it is too large
Load Diff
+12
-1
@@ -7,7 +7,11 @@
|
|||||||
"dev": "nodemon server.js",
|
"dev": "nodemon server.js",
|
||||||
"create-indexes": "node create_indexes.js",
|
"create-indexes": "node create_indexes.js",
|
||||||
"check-indexes": "node create_indexes.js check",
|
"check-indexes": "node create_indexes.js check",
|
||||||
"drop-indexes": "node create_indexes.js drop"
|
"drop-indexes": "node create_indexes.js drop",
|
||||||
|
"lint": "eslint . --ext js --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"lint:fix": "eslint . --ext js --fix",
|
||||||
|
"format": "prettier --write \"**/*.js\"",
|
||||||
|
"format:check": "prettier --check \"**/*.js\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.4",
|
"axios": "^1.13.4",
|
||||||
@@ -31,8 +35,15 @@
|
|||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"eslint": "^9.39.2",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-prettier": "^5.5.5",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.5.0",
|
||||||
"jest": "^30.2.0",
|
"jest": "^30.2.0",
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^3.0.1",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
"supertest": "^7.1.4"
|
"supertest": "^7.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# 构建输出
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# 依赖
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Vite 缓存
|
||||||
|
.vite/
|
||||||
|
|
||||||
|
# 日志
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# 其他
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:react/jsx-runtime',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
'plugin:prettier/recommended'
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||||
|
settings: { react: { version: '18.2' } },
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react/jsx-no-target-blank': 'off',
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true }
|
||||||
|
],
|
||||||
|
'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
||||||
|
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||||
|
'react/prop-types': 'off',
|
||||||
|
'react/display-name': 'off'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
Generated
+2934
-2
File diff suppressed because it is too large
Load Diff
+12
-1
@@ -6,7 +6,11 @@
|
|||||||
"start": "vite",
|
"start": "vite",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"lint:fix": "eslint . --ext js,jsx --fix",
|
||||||
|
"format": "prettier --write \"src/**/*.{js,jsx,css,json}\"",
|
||||||
|
"format:check": "prettier --check \"src/**/*.{js,jsx,css,json}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^6.1.0",
|
"@ant-design/icons": "^6.1.0",
|
||||||
@@ -27,7 +31,14 @@
|
|||||||
"@testing-library/react": "^16.3.1",
|
"@testing-library/react": "^16.3.1",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@vitejs/plugin-react": "^4.0.3",
|
"@vitejs/plugin-react": "^4.0.3",
|
||||||
|
"eslint": "^9.39.2",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-prettier": "^5.5.5",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.5.0",
|
||||||
"jsdom": "^27.3.0",
|
"jsdom": "^27.3.0",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
"terser": "^5.44.1",
|
"terser": "^5.44.1",
|
||||||
"vite": "^4.4.9",
|
"vite": "^4.4.9",
|
||||||
"vitest": "^4.0.16"
|
"vitest": "^4.0.16"
|
||||||
|
|||||||
+351
-137
@@ -1,7 +1,49 @@
|
|||||||
import React, { useState, Suspense, lazy } from 'react';
|
import React, { useState, Suspense, lazy } from 'react';
|
||||||
import { Layout, Menu, theme, Button, Dropdown, Avatar, message, Space, Divider, ConfigProvider as AntdConfigProvider } from 'antd';
|
import {
|
||||||
import { BarChartOutlined, DatabaseOutlined, CloudServerOutlined, MenuUnfoldOutlined, MenuFoldOutlined, EyeOutlined, BuildOutlined, HomeOutlined, ShoppingCartOutlined, InboxOutlined, ImportOutlined, FileTextOutlined, UserOutlined, LogoutOutlined, HistoryOutlined, AuditOutlined, ToolOutlined, ScheduleOutlined, SettingOutlined, ApiOutlined, PartitionOutlined, CodepenOutlined } from '@ant-design/icons';
|
Layout,
|
||||||
import { BrowserRouter as Router, Routes, Route, Link, Navigate, useLocation, useNavigate } from 'react-router-dom';
|
Menu,
|
||||||
|
theme,
|
||||||
|
Button,
|
||||||
|
Dropdown,
|
||||||
|
Avatar,
|
||||||
|
message,
|
||||||
|
Space,
|
||||||
|
Divider,
|
||||||
|
ConfigProvider as AntdConfigProvider,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
BarChartOutlined,
|
||||||
|
DatabaseOutlined,
|
||||||
|
CloudServerOutlined,
|
||||||
|
MenuUnfoldOutlined,
|
||||||
|
MenuFoldOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
BuildOutlined,
|
||||||
|
HomeOutlined,
|
||||||
|
ShoppingCartOutlined,
|
||||||
|
InboxOutlined,
|
||||||
|
ImportOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
LogoutOutlined,
|
||||||
|
HistoryOutlined,
|
||||||
|
AuditOutlined,
|
||||||
|
ToolOutlined,
|
||||||
|
ScheduleOutlined,
|
||||||
|
SettingOutlined,
|
||||||
|
ApiOutlined,
|
||||||
|
PartitionOutlined,
|
||||||
|
CodepenOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import {
|
||||||
|
BrowserRouter as Router,
|
||||||
|
Routes,
|
||||||
|
Route,
|
||||||
|
Link,
|
||||||
|
Navigate,
|
||||||
|
useLocation,
|
||||||
|
useNavigate,
|
||||||
|
} from 'react-router-dom';
|
||||||
import { useAuth } from './context/AuthContext';
|
import { useAuth } from './context/AuthContext';
|
||||||
import { ConfigProvider, useConfig } from './context/ConfigContext';
|
import { ConfigProvider, useConfig } from './context/ConfigContext';
|
||||||
import { Scene3DProvider } from './context/Scene3DContext';
|
import { Scene3DProvider } from './context/Scene3DContext';
|
||||||
@@ -31,30 +73,34 @@ const PortManagement = lazy(() => import('./pages/PortManagement'));
|
|||||||
const { Header, Content, Sider } = Layout;
|
const { Header, Content, Sider } = Layout;
|
||||||
|
|
||||||
const PageLoading = () => (
|
const PageLoading = () => (
|
||||||
<div style={{
|
<div
|
||||||
display: 'flex',
|
style={{
|
||||||
flexDirection: 'column',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
height: '100vh',
|
alignItems: 'center',
|
||||||
background: '#f5f5f5',
|
height: '100vh',
|
||||||
gap: '16px'
|
background: '#f5f5f5',
|
||||||
}}>
|
gap: '16px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Spin size="large" />
|
<Spin size="large" />
|
||||||
<span style={{ color: '#8c8c8c', fontSize: '14px' }}>正在加载页面...</span>
|
<span style={{ color: '#8c8c8c', fontSize: '14px' }}>正在加载页面...</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const AuthLoading = () => (
|
const AuthLoading = () => (
|
||||||
<div style={{
|
<div
|
||||||
display: 'flex',
|
style={{
|
||||||
flexDirection: 'column',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
height: '100vh',
|
alignItems: 'center',
|
||||||
background: '#f5f5f5',
|
height: '100vh',
|
||||||
gap: '16px'
|
background: '#f5f5f5',
|
||||||
}}>
|
gap: '16px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Spin size="large" />
|
<Spin size="large" />
|
||||||
<span style={{ color: '#8c8c8c', fontSize: '14px' }}>正在加载认证状态...</span>
|
<span style={{ color: '#8c8c8c', fontSize: '14px' }}>正在加载认证状态...</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -101,9 +147,21 @@ const AppLayout = ({ children }) => {
|
|||||||
if (path === '/') return 'dashboard';
|
if (path === '/') return 'dashboard';
|
||||||
if (path.startsWith('/visualization-3d')) return 'visualization-3d';
|
if (path.startsWith('/visualization-3d')) return 'visualization-3d';
|
||||||
if (path.startsWith('/rooms') || path.startsWith('/racks')) return 'room-management';
|
if (path.startsWith('/rooms') || path.startsWith('/racks')) return 'room-management';
|
||||||
if (path.startsWith('/devices') || path.startsWith('/fields') || path.startsWith('/cables') || path.startsWith('/ports')) return 'asset-management';
|
if (
|
||||||
|
path.startsWith('/devices') ||
|
||||||
|
path.startsWith('/fields') ||
|
||||||
|
path.startsWith('/cables') ||
|
||||||
|
path.startsWith('/ports')
|
||||||
|
)
|
||||||
|
return 'asset-management';
|
||||||
if (path.startsWith('/consumables')) return 'consumables-management';
|
if (path.startsWith('/consumables')) return 'consumables-management';
|
||||||
if (path.startsWith('/users') || path.startsWith('/login-history') || path.startsWith('/operation-logs') || path.startsWith('/settings')) return 'system-management';
|
if (
|
||||||
|
path.startsWith('/users') ||
|
||||||
|
path.startsWith('/login-history') ||
|
||||||
|
path.startsWith('/operation-logs') ||
|
||||||
|
path.startsWith('/settings')
|
||||||
|
)
|
||||||
|
return 'system-management';
|
||||||
if (path.startsWith('/tickets')) return 'ticket-management';
|
if (path.startsWith('/tickets')) return 'ticket-management';
|
||||||
return 'dashboard';
|
return 'dashboard';
|
||||||
};
|
};
|
||||||
@@ -218,22 +276,22 @@ const AppLayout = ({ children }) => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'system-management',
|
key: 'system-management',
|
||||||
icon: <UserOutlined style={{ fontSize: '18px' }} />,
|
icon: <UserOutlined style={{ fontSize: '18px' }} />,
|
||||||
label: '系统管理',
|
label: '系统管理',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
key: 'users',
|
key: 'users',
|
||||||
icon: <UserOutlined style={{ fontSize: '16px' }} />,
|
icon: <UserOutlined style={{ fontSize: '16px' }} />,
|
||||||
label: <Link to="/users">用户管理</Link>,
|
label: <Link to="/users">用户管理</Link>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'system-settings',
|
key: 'system-settings',
|
||||||
icon: <SettingOutlined style={{ fontSize: '16px' }} />,
|
icon: <SettingOutlined style={{ fontSize: '16px' }} />,
|
||||||
label: <Link to="/settings">系统设置</Link>,
|
label: <Link to="/settings">系统设置</Link>,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -252,56 +310,66 @@ const AppLayout = ({ children }) => {
|
|||||||
left: 0,
|
left: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
zIndex: 100
|
zIndex: 100,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{
|
<div
|
||||||
display: 'flex',
|
style={{
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: collapsed ? 'center' : 'flex-start',
|
|
||||||
padding: collapsed ? '16px 0' : '16px 20px',
|
|
||||||
borderBottom: `1px solid ${designTokens.colors.sidebar.border}`,
|
|
||||||
minHeight: '64px',
|
|
||||||
background: designTokens.colors.sidebar.bg
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
width: '36px',
|
|
||||||
height: '36px',
|
|
||||||
borderRadius: designTokens.borderRadius.small,
|
|
||||||
background: designTokens.colors.primary.gradient,
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: collapsed ? 'center' : 'flex-start',
|
||||||
flexShrink: 0
|
padding: collapsed ? '16px 0' : '16px 20px',
|
||||||
}}>
|
borderBottom: `1px solid ${designTokens.colors.sidebar.border}`,
|
||||||
|
minHeight: '64px',
|
||||||
|
background: designTokens.colors.sidebar.bg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '36px',
|
||||||
|
height: '36px',
|
||||||
|
borderRadius: designTokens.borderRadius.small,
|
||||||
|
background: designTokens.colors.primary.gradient,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<CloudServerOutlined style={{ fontSize: '18px', color: '#ffffff' }} />
|
<CloudServerOutlined style={{ fontSize: '18px', color: '#ffffff' }} />
|
||||||
</div>
|
</div>
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<div>
|
<div>
|
||||||
<div style={{
|
<div
|
||||||
fontSize: '15px',
|
style={{
|
||||||
fontWeight: '600',
|
fontSize: '15px',
|
||||||
color: designTokens.colors.primary.main,
|
fontWeight: '600',
|
||||||
lineHeight: 1.2
|
color: designTokens.colors.primary.main,
|
||||||
}}>
|
lineHeight: 1.2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{config.site_name || 'IDC管理'}
|
{config.site_name || 'IDC管理'}
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div
|
||||||
fontSize: '11px',
|
style={{
|
||||||
color: designTokens.colors.sidebar.text,
|
fontSize: '11px',
|
||||||
marginTop: '2px'
|
color: designTokens.colors.sidebar.text,
|
||||||
}}>
|
marginTop: '2px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
数据中心管理平台
|
数据中心管理平台
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{
|
<div
|
||||||
padding: collapsed ? '12px 0' : '12px 8px',
|
style={{
|
||||||
overflowY: 'auto',
|
padding: collapsed ? '12px 0' : '12px 8px',
|
||||||
flex: 1
|
overflowY: 'auto',
|
||||||
}}>
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Menu
|
<Menu
|
||||||
mode="inline"
|
mode="inline"
|
||||||
selectedKeys={[getSelectedKey()]}
|
selectedKeys={[getSelectedKey()]}
|
||||||
@@ -309,16 +377,18 @@ const AppLayout = ({ children }) => {
|
|||||||
style={{
|
style={{
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
borderRight: 0,
|
borderRight: 0,
|
||||||
fontSize: '14px'
|
fontSize: '14px',
|
||||||
}}
|
}}
|
||||||
items={menuItems}
|
items={menuItems}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{
|
<div
|
||||||
padding: '12px',
|
style={{
|
||||||
borderTop: `1px solid ${designTokens.colors.sidebar.border}`
|
padding: '12px',
|
||||||
}}>
|
borderTop: `1px solid ${designTokens.colors.sidebar.border}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||||
@@ -332,48 +402,60 @@ const AppLayout = ({ children }) => {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: collapsed ? 'center' : 'flex-start',
|
justifyContent: collapsed ? 'center' : 'flex-start',
|
||||||
gap: '8px'
|
gap: '8px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!collapsed && <span style={{ fontSize: '13px', color: designTokens.colors.text.secondary }}>收起菜单</span>}
|
{!collapsed && (
|
||||||
|
<span style={{ fontSize: '13px', color: designTokens.colors.text.secondary }}>
|
||||||
|
收起菜单
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Sider>
|
</Sider>
|
||||||
|
|
||||||
<Layout style={{
|
<Layout
|
||||||
marginLeft: collapsed ? 72 : 240,
|
style={{
|
||||||
transition: 'margin-left 0.2s ease'
|
marginLeft: collapsed ? 72 : 240,
|
||||||
}}>
|
transition: 'margin-left 0.2s ease',
|
||||||
<Header style={{
|
}}
|
||||||
padding: '0 24px',
|
>
|
||||||
height: 64,
|
<Header
|
||||||
background: designTokens.colors.background.primary,
|
style={{
|
||||||
display: 'flex',
|
padding: '0 24px',
|
||||||
justifyContent: 'flex-end',
|
height: 64,
|
||||||
alignItems: 'center',
|
background: designTokens.colors.background.primary,
|
||||||
boxShadow: designTokens.shadows.small,
|
display: 'flex',
|
||||||
position: 'sticky',
|
justifyContent: 'flex-end',
|
||||||
top: 0,
|
alignItems: 'center',
|
||||||
zIndex: 99,
|
boxShadow: designTokens.shadows.small,
|
||||||
overflow: 'visible'
|
position: 'sticky',
|
||||||
}}>
|
top: 0,
|
||||||
|
zIndex: 99,
|
||||||
|
overflow: 'visible',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{user && (
|
{user && (
|
||||||
<div style={{
|
<div
|
||||||
display: 'flex',
|
style={{
|
||||||
alignItems: 'center',
|
|
||||||
gap: '12px',
|
|
||||||
height: '100%'
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '8px',
|
gap: '12px',
|
||||||
padding: '4px 12px',
|
height: '100%',
|
||||||
background: designTokens.colors.background.secondary,
|
}}
|
||||||
borderRadius: designTokens.borderRadius.medium,
|
>
|
||||||
height: '40px',
|
<div
|
||||||
boxSizing: 'border-box'
|
style={{
|
||||||
}}>
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
padding: '4px 12px',
|
||||||
|
background: designTokens.colors.background.secondary,
|
||||||
|
borderRadius: designTokens.borderRadius.medium,
|
||||||
|
height: '40px',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: designTokens.colors.primary.main,
|
backgroundColor: designTokens.colors.primary.main,
|
||||||
@@ -382,15 +464,19 @@ const AppLayout = ({ children }) => {
|
|||||||
height: 32,
|
height: 32,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center'
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
icon={<UserOutlined style={{ fontSize: '14px' }} />}
|
icon={<UserOutlined style={{ fontSize: '14px' }} />}
|
||||||
/>
|
/>
|
||||||
<span style={{
|
<span
|
||||||
color: designTokens.colors.text.primary,
|
style={{
|
||||||
fontSize: 14,
|
color: designTokens.colors.text.primary,
|
||||||
fontWeight: 500
|
fontSize: 14,
|
||||||
}}>{user.username}</span>
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{user.username}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
@@ -401,7 +487,7 @@ const AppLayout = ({ children }) => {
|
|||||||
padding: '8px 12px',
|
padding: '8px 12px',
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
borderRadius: designTokens.borderRadius.small,
|
borderRadius: designTokens.borderRadius.small,
|
||||||
fontSize: 13
|
fontSize: 13,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
退出
|
退出
|
||||||
@@ -414,7 +500,7 @@ const AppLayout = ({ children }) => {
|
|||||||
padding: 24,
|
padding: 24,
|
||||||
margin: 0,
|
margin: 0,
|
||||||
minHeight: 'calc(100vh - 64px)',
|
minHeight: 'calc(100vh - 64px)',
|
||||||
background: designTokens.colors.background.secondary
|
background: designTokens.colors.background.secondary,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -433,24 +519,152 @@ const ThemeConfig = () => {
|
|||||||
<Suspense fallback={<PageLoading />}>
|
<Suspense fallback={<PageLoading />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/" element={<PrivateRoute><Dashboard /></PrivateRoute>} />
|
<Route
|
||||||
<Route path="/devices" element={<PrivateRoute><DeviceManagement /></PrivateRoute>} />
|
path="/"
|
||||||
<Route path="/racks" element={<PrivateRoute><RackManagement /></PrivateRoute>} />
|
element={
|
||||||
<Route path="/rooms" element={<PrivateRoute><RoomManagement /></PrivateRoute>} />
|
<PrivateRoute>
|
||||||
<Route path="/fields" element={<PrivateRoute><DeviceFieldManagement /></PrivateRoute>} />
|
<Dashboard />
|
||||||
<Route path="/visualization-3d" element={<PrivateRoute><Scene3DProvider><Rack3DVisualization /></Scene3DProvider></PrivateRoute>} />
|
</PrivateRoute>
|
||||||
<Route path="/consumables" element={<PrivateRoute><ConsumableManagement /></PrivateRoute>} />
|
}
|
||||||
<Route path="/consumables-categories" element={<PrivateRoute><CategoryManagement /></PrivateRoute>} />
|
/>
|
||||||
<Route path="/consumables-stats" element={<PrivateRoute><ConsumableStatistics /></PrivateRoute>} />
|
<Route
|
||||||
<Route path="/consumables-logs" element={<PrivateRoute><ConsumableLogs /></PrivateRoute>} />
|
path="/devices"
|
||||||
<Route path="/users" element={<PrivateRoute><UserManagement /></PrivateRoute>} />
|
element={
|
||||||
<Route path="/tickets" element={<PrivateRoute><TicketManagement /></PrivateRoute>} />
|
<PrivateRoute>
|
||||||
<Route path="/ticket-categories" element={<PrivateRoute><TicketCategoryManagement /></PrivateRoute>} />
|
<DeviceManagement />
|
||||||
<Route path="/ticket-statistics" element={<PrivateRoute><TicketStatistics /></PrivateRoute>} />
|
</PrivateRoute>
|
||||||
<Route path="/ticket-fields" element={<PrivateRoute><TicketFieldManagement /></PrivateRoute>} />
|
}
|
||||||
<Route path="/settings" element={<PrivateRoute><SystemSettings /></PrivateRoute>} />
|
/>
|
||||||
<Route path="/cables" element={<PrivateRoute><CableManagement /></PrivateRoute>} />
|
<Route
|
||||||
<Route path="/ports" element={<PrivateRoute><PortManagement /></PrivateRoute>} />
|
path="/racks"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<RackManagement />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/rooms"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<RoomManagement />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/fields"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<DeviceFieldManagement />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/visualization-3d"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<Scene3DProvider>
|
||||||
|
<Rack3DVisualization />
|
||||||
|
</Scene3DProvider>
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/consumables"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<ConsumableManagement />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/consumables-categories"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<CategoryManagement />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/consumables-stats"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<ConsumableStatistics />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/consumables-logs"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<ConsumableLogs />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/users"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<UserManagement />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/tickets"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<TicketManagement />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/ticket-categories"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<TicketCategoryManagement />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/ticket-statistics"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<TicketStatistics />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/ticket-fields"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<TicketFieldManagement />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/settings"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<SystemSettings />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/cables"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<CableManagement />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/ports"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<PortManagement />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
+77
-80
@@ -9,7 +9,7 @@ const cacheManager = (() => {
|
|||||||
return `${method}:${url}:${paramsStr}`;
|
return `${method}:${url}:${paramsStr}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isExpired = (key) => {
|
const isExpired = key => {
|
||||||
const timestamp = cacheTimestamps.get(key);
|
const timestamp = cacheTimestamps.get(key);
|
||||||
if (!timestamp) return true;
|
if (!timestamp) return true;
|
||||||
const ttl = config.get(key)?.ttl || defaultTTL;
|
const ttl = config.get(key)?.ttl || defaultTTL;
|
||||||
@@ -34,7 +34,7 @@ const cacheManager = (() => {
|
|||||||
return key;
|
return key;
|
||||||
};
|
};
|
||||||
|
|
||||||
const invalidate = (url) => {
|
const invalidate = url => {
|
||||||
const keysToDelete = [];
|
const keysToDelete = [];
|
||||||
cache.forEach((_, key) => {
|
cache.forEach((_, key) => {
|
||||||
if (key.includes(url)) {
|
if (key.includes(url)) {
|
||||||
@@ -49,7 +49,7 @@ const cacheManager = (() => {
|
|||||||
return keysToDelete.length;
|
return keysToDelete.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
const invalidatePattern = (pattern) => {
|
const invalidatePattern = pattern => {
|
||||||
const regex = new RegExp(pattern);
|
const regex = new RegExp(pattern);
|
||||||
const keysToDelete = [];
|
const keysToDelete = [];
|
||||||
cache.forEach((_, key) => {
|
cache.forEach((_, key) => {
|
||||||
@@ -78,7 +78,7 @@ const cacheManager = (() => {
|
|||||||
const getStats = () => {
|
const getStats = () => {
|
||||||
return {
|
return {
|
||||||
size: cache.size,
|
size: cache.size,
|
||||||
keys: Array.from(cache.keys())
|
keys: Array.from(cache.keys()),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -90,37 +90,29 @@ const cacheManager = (() => {
|
|||||||
clear,
|
clear,
|
||||||
setTTL,
|
setTTL,
|
||||||
getStats,
|
getStats,
|
||||||
defaultTTL
|
defaultTTL,
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const cacheInterceptor = (api) => {
|
const cacheInterceptor = api => {
|
||||||
const requestCache = new Set();
|
const requestCache = new Set();
|
||||||
const pendingRequests = new Map();
|
const pendingRequests = new Map();
|
||||||
|
|
||||||
api.interceptors.request.use(
|
api.interceptors.request.use(
|
||||||
(config) => {
|
config => {
|
||||||
if (config.method?.toLowerCase() === 'get') {
|
if (config.method?.toLowerCase() === 'get') {
|
||||||
const cacheKey = cacheManager.generateKey(
|
const cacheKey = cacheManager.generateKey(config.method, config.url, config.params);
|
||||||
config.method,
|
|
||||||
config.url,
|
|
||||||
config.params
|
|
||||||
);
|
|
||||||
|
|
||||||
if (requestCache.has(cacheKey)) {
|
if (requestCache.has(cacheKey)) {
|
||||||
config.adapter = () => {
|
config.adapter = () => {
|
||||||
const cachedData = cacheManager.get(
|
const cachedData = cacheManager.get(config.method, config.url, config.params);
|
||||||
config.method,
|
|
||||||
config.url,
|
|
||||||
config.params
|
|
||||||
);
|
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
data: cachedData,
|
data: cachedData,
|
||||||
status: 200,
|
status: 200,
|
||||||
statusText: 'OK',
|
statusText: 'OK',
|
||||||
headers: {},
|
headers: {},
|
||||||
config
|
config,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
requestCache.delete(cacheKey);
|
requestCache.delete(cacheKey);
|
||||||
@@ -130,11 +122,11 @@ const cacheInterceptor = (api) => {
|
|||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => Promise.reject(error)
|
error => Promise.reject(error)
|
||||||
);
|
);
|
||||||
|
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => {
|
response => {
|
||||||
if (response.config.method?.toLowerCase() === 'get') {
|
if (response.config.method?.toLowerCase() === 'get') {
|
||||||
const cacheKey = cacheManager.generateKey(
|
const cacheKey = cacheManager.generateKey(
|
||||||
response.config.method,
|
response.config.method,
|
||||||
@@ -151,7 +143,7 @@ const cacheInterceptor = (api) => {
|
|||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
(error) => {
|
error => {
|
||||||
if (error.config) {
|
if (error.config) {
|
||||||
const cacheKey = cacheManager.generateKey(
|
const cacheKey = cacheManager.generateKey(
|
||||||
error.config.method,
|
error.config.method,
|
||||||
@@ -178,108 +170,113 @@ export const cachedAPI = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
post: (url, data) => api.post(url, data).then(data => {
|
post: (url, data) =>
|
||||||
cacheManager.invalidate(url);
|
api.post(url, data).then(data => {
|
||||||
return data;
|
cacheManager.invalidate(url);
|
||||||
}),
|
return data;
|
||||||
|
}),
|
||||||
|
|
||||||
put: (url, data) => api.put(url, data).then(data => {
|
put: (url, data) =>
|
||||||
cacheManager.invalidate(url);
|
api.put(url, data).then(data => {
|
||||||
return data;
|
cacheManager.invalidate(url);
|
||||||
}),
|
return data;
|
||||||
|
}),
|
||||||
|
|
||||||
delete: (url) => api.delete(url).then(data => {
|
delete: url =>
|
||||||
cacheManager.invalidate(url);
|
api.delete(url).then(data => {
|
||||||
return data;
|
cacheManager.invalidate(url);
|
||||||
}),
|
return data;
|
||||||
|
}),
|
||||||
|
|
||||||
invalidate: (url) => cacheManager.invalidate(url),
|
invalidate: url => cacheManager.invalidate(url),
|
||||||
|
|
||||||
invalidatePattern: (pattern) => cacheManager.invalidatePattern(pattern),
|
invalidatePattern: pattern => cacheManager.invalidatePattern(pattern),
|
||||||
|
|
||||||
clearCache: () => cacheManager.clear(),
|
clearCache: () => cacheManager.clear(),
|
||||||
|
|
||||||
setCacheTTL: (url, ttl) => cacheManager.setTTL(url, ttl),
|
setCacheTTL: (url, ttl) => cacheManager.setTTL(url, ttl),
|
||||||
|
|
||||||
getCacheStats: () => cacheManager.getStats()
|
getCacheStats: () => cacheManager.getStats(),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deviceAPI = {
|
export const deviceAPI = {
|
||||||
list: (params) => cachedAPI.get('/devices', params),
|
list: params => cachedAPI.get('/devices', params),
|
||||||
get: (deviceId) => cachedAPI.get(`/devices/${deviceId}`),
|
get: deviceId => cachedAPI.get(`/devices/${deviceId}`),
|
||||||
create: (data) => cachedAPI.post('/devices', data),
|
create: data => cachedAPI.post('/devices', data),
|
||||||
update: (deviceId, data) => cachedAPI.put(`/api/devices/${deviceId}`, data),
|
update: (deviceId, data) => cachedAPI.put(`/api/devices/${deviceId}`, data),
|
||||||
delete: (deviceId) => cachedAPI.delete(`/api/devices/${deviceId}`),
|
delete: deviceId => cachedAPI.delete(`/api/devices/${deviceId}`),
|
||||||
batchOffline: (data) => cachedAPI.post('/devices/batch-offline', data),
|
batchOffline: data => cachedAPI.post('/devices/batch-offline', data),
|
||||||
batchDelete: (data) => cachedAPI.delete('/devices/batch-delete', { data }),
|
batchDelete: data => cachedAPI.delete('/devices/batch-delete', { data }),
|
||||||
export: (params) => api.get('/devices/export', { params, responseType: 'blob' }),
|
export: params => api.get('/devices/export', { params, responseType: 'blob' }),
|
||||||
import: (formData) => api.post('/devices/import', formData, {
|
import: formData =>
|
||||||
headers: { 'Content-Type': 'multipart/form-data' }
|
api.post('/devices/import', formData, {
|
||||||
})
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const rackAPI = {
|
export const rackAPI = {
|
||||||
list: (params) => cachedAPI.get('/racks', params),
|
list: params => cachedAPI.get('/racks', params),
|
||||||
get: (rackId) => cachedAPI.get(`/racks/${rackId}`),
|
get: rackId => cachedAPI.get(`/racks/${rackId}`),
|
||||||
create: (data) => cachedAPI.post('/racks', data),
|
create: data => cachedAPI.post('/racks', data),
|
||||||
update: (rackId, data) => cachedAPI.put(`/racks/${rackId}`, data),
|
update: (rackId, data) => cachedAPI.put(`/racks/${rackId}`, data),
|
||||||
delete: (rackId) => cachedAPI.delete(`/racks/${rackId}`),
|
delete: rackId => cachedAPI.delete(`/racks/${rackId}`),
|
||||||
import: (formData) => api.post('/racks/import', formData, {
|
import: formData =>
|
||||||
headers: { 'Content-Type': 'multipart/form-data' }
|
api.post('/racks/import', formData, {
|
||||||
})
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const roomAPI = {
|
export const roomAPI = {
|
||||||
list: (params) => cachedAPI.get('/rooms', params),
|
list: params => cachedAPI.get('/rooms', params),
|
||||||
get: (roomId) => cachedAPI.get(`/rooms/${roomId}`),
|
get: roomId => cachedAPI.get(`/rooms/${roomId}`),
|
||||||
create: (data) => cachedAPI.post('/rooms', data),
|
create: data => cachedAPI.post('/rooms', data),
|
||||||
update: (roomId, data) => cachedAPI.put(`/rooms/${roomId}`, data),
|
update: (roomId, data) => cachedAPI.put(`/rooms/${roomId}`, data),
|
||||||
delete: (roomId) => cachedAPI.delete(`/rooms/${roomId}`)
|
delete: roomId => cachedAPI.delete(`/rooms/${roomId}`),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deviceFieldAPI = {
|
export const deviceFieldAPI = {
|
||||||
list: () => cachedAPI.get('/deviceFields'),
|
list: () => cachedAPI.get('/deviceFields'),
|
||||||
get: (fieldId) => cachedAPI.get(`/deviceFields/${fieldId}`),
|
get: fieldId => cachedAPI.get(`/deviceFields/${fieldId}`),
|
||||||
create: (data) => cachedAPI.post('/deviceFields', data),
|
create: data => cachedAPI.post('/deviceFields', data),
|
||||||
update: (fieldId, data) => cachedAPI.put(`/deviceFields/${fieldId}`, data),
|
update: (fieldId, data) => cachedAPI.put(`/deviceFields/${fieldId}`, data),
|
||||||
delete: (fieldId) => cachedAPI.delete(`/deviceFields/${fieldId}`),
|
delete: fieldId => cachedAPI.delete(`/deviceFields/${fieldId}`),
|
||||||
updateConfig: (data) => cachedAPI.post('/deviceFields/config', data)
|
updateConfig: data => cachedAPI.post('/deviceFields/config', data),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const consumableAPI = {
|
export const consumableAPI = {
|
||||||
list: (params) => cachedAPI.get('/consumables', params),
|
list: params => cachedAPI.get('/consumables', params),
|
||||||
get: (consumableId) => cachedAPI.get(`/consumables/${consumableId}`),
|
get: consumableId => cachedAPI.get(`/consumables/${consumableId}`),
|
||||||
create: (data) => cachedAPI.post('/consumables', data),
|
create: data => cachedAPI.post('/consumables', data),
|
||||||
update: (consumableId, data) => cachedAPI.put(`/consumables/${consumableId}`, data),
|
update: (consumableId, data) => cachedAPI.put(`/consumables/${consumableId}`, data),
|
||||||
delete: (consumableId) => cachedAPI.delete(`/consumables/${consumableId}`),
|
delete: consumableId => cachedAPI.delete(`/consumables/${consumableId}`),
|
||||||
import: (data) => cachedAPI.post('/consumables/import', data),
|
import: data => cachedAPI.post('/consumables/import', data),
|
||||||
quickInOut: (data) => cachedAPI.post('/consumables/quick-inout', data),
|
quickInOut: data => cachedAPI.post('/consumables/quick-inout', data),
|
||||||
getStatistics: () => cachedAPI.get('/consumables/statistics/summary'),
|
getStatistics: () => cachedAPI.get('/consumables/statistics/summary'),
|
||||||
getLowStock: () => cachedAPI.get('/consumables/low-stock')
|
getLowStock: () => cachedAPI.get('/consumables/low-stock'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const consumableCategoryAPI = {
|
export const consumableCategoryAPI = {
|
||||||
list: (params) => cachedAPI.get('/consumable-categories', params),
|
list: params => cachedAPI.get('/consumable-categories', params),
|
||||||
getList: (params) => cachedAPI.get('/consumable-categories/list', params),
|
getList: params => cachedAPI.get('/consumable-categories/list', params),
|
||||||
create: (data) => cachedAPI.post('/consumable-categories', data),
|
create: data => cachedAPI.post('/consumable-categories', data),
|
||||||
update: (id, data) => cachedAPI.put(`/consumable-categories/${id}`, data),
|
update: (id, data) => cachedAPI.put(`/consumable-categories/${id}`, data),
|
||||||
delete: (id) => cachedAPI.delete(`/consumable-categories/${id}`)
|
delete: id => cachedAPI.delete(`/consumable-categories/${id}`),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const consumableLogAPI = {
|
export const consumableLogAPI = {
|
||||||
list: (params) => cachedAPI.get('/consumables/logs', params),
|
list: params => cachedAPI.get('/consumables/logs', params),
|
||||||
create: (data) => cachedAPI.post('/consumables/logs', data),
|
create: data => cachedAPI.post('/consumables/logs', data),
|
||||||
export: (params) => api.get('/consumables/logs/export', { params, responseType: 'blob' }),
|
export: params => api.get('/consumables/logs/export', { params, responseType: 'blob' }),
|
||||||
import: (data) => cachedAPI.post('/consumables/logs/import', data)
|
import: data => cachedAPI.post('/consumables/logs/import', data),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ticketCategoryAPI = {
|
export const ticketCategoryAPI = {
|
||||||
list: (params) => cachedAPI.get('/ticket-categories', params),
|
list: params => cachedAPI.get('/ticket-categories', params),
|
||||||
create: (data) => cachedAPI.post('/ticket-categories', data),
|
create: data => cachedAPI.post('/ticket-categories', data),
|
||||||
update: (code, data) => cachedAPI.put(`/ticket-categories/${code}`, data),
|
update: (code, data) => cachedAPI.put(`/ticket-categories/${code}`, data),
|
||||||
delete: (code) => cachedAPI.delete(`/ticket-categories/${code}`),
|
delete: code => cachedAPI.delete(`/ticket-categories/${code}`),
|
||||||
getTree: () => cachedAPI.get('/ticket-categories/tree'),
|
getTree: () => cachedAPI.get('/ticket-categories/tree'),
|
||||||
init: () => cachedAPI.post('/ticket-categories/init')
|
init: () => cachedAPI.post('/ticket-categories/init'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export { cacheManager };
|
export { cacheManager };
|
||||||
|
|||||||
+41
-41
@@ -6,12 +6,12 @@ const api = axios.create({
|
|||||||
baseURL: API_BASE_URL,
|
baseURL: API_BASE_URL,
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
api.interceptors.request.use(
|
api.interceptors.request.use(
|
||||||
(config) => {
|
config => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
@@ -31,16 +31,16 @@ api.interceptors.request.use(
|
|||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
error => {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => {
|
response => {
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
(error) => {
|
error => {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
const { status, data } = error.response;
|
const { status, data } = error.response;
|
||||||
|
|
||||||
@@ -72,82 +72,82 @@ api.interceptors.response.use(
|
|||||||
|
|
||||||
export const authAPI = {
|
export const authAPI = {
|
||||||
checkAdmin: () => api.get('/auth/check-admin'),
|
checkAdmin: () => api.get('/auth/check-admin'),
|
||||||
register: (data) => api.post('/auth/register', data),
|
register: data => api.post('/auth/register', data),
|
||||||
login: (data) => api.post('/auth/login', data),
|
login: data => api.post('/auth/login', data),
|
||||||
unlock: (data) => api.post('/auth/unlock', data),
|
unlock: data => api.post('/auth/unlock', data),
|
||||||
getProfile: () => api.get('/auth/profile'),
|
getProfile: () => api.get('/auth/profile'),
|
||||||
updateProfile: (data) => api.put('/auth/profile', data),
|
updateProfile: data => api.put('/auth/profile', data),
|
||||||
changePassword: (data) => api.put('/auth/password', data)
|
changePassword: data => api.put('/auth/password', data),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userAPI = {
|
export const userAPI = {
|
||||||
list: (params) => api.get('/users', { params }),
|
list: params => api.get('/users', { params }),
|
||||||
all: () => api.get('/users/all'),
|
all: () => api.get('/users/all'),
|
||||||
get: (userId) => api.get(`/users/${userId}`),
|
get: userId => api.get(`/users/${userId}`),
|
||||||
create: (data) => api.post('/users', data),
|
create: data => api.post('/users', data),
|
||||||
update: (userId, data) => api.put(`/users/${userId}`, data),
|
update: (userId, data) => api.put(`/users/${userId}`, data),
|
||||||
resetPassword: (userId, data) => api.put(`/users/${userId}/password`, data),
|
resetPassword: (userId, data) => api.put(`/users/${userId}/password`, data),
|
||||||
delete: (userId) => api.delete(`/users/${userId}`),
|
delete: userId => api.delete(`/users/${userId}`),
|
||||||
uploadAvatar: (userId, file) => {
|
uploadAvatar: (userId, file) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('avatar', file);
|
formData.append('avatar', file);
|
||||||
return api.post(`/users/${userId}/avatar`, formData, {
|
return api.post(`/users/${userId}/avatar`, formData, {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' }
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
deleteAvatar: (userId) => api.delete(`/users/${userId}/avatar`),
|
deleteAvatar: userId => api.delete(`/users/${userId}/avatar`),
|
||||||
approve: (userId) => api.put(`/users/${userId}/approve`),
|
approve: userId => api.put(`/users/${userId}/approve`),
|
||||||
reject: (userId) => api.put(`/users/${userId}/reject`)
|
reject: userId => api.put(`/users/${userId}/reject`),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const roleAPI = {
|
export const roleAPI = {
|
||||||
list: (params) => api.get('/roles', { params }),
|
list: params => api.get('/roles', { params }),
|
||||||
all: () => api.get('/roles/all'),
|
all: () => api.get('/roles/all'),
|
||||||
get: (roleId) => api.get(`/roles/${roleId}`),
|
get: roleId => api.get(`/roles/${roleId}`),
|
||||||
create: (data) => api.post('/roles', data),
|
create: data => api.post('/roles', data),
|
||||||
update: (roleId, data) => api.put(`/roles/${roleId}`, data),
|
update: (roleId, data) => api.put(`/roles/${roleId}`, data),
|
||||||
delete: (roleId) => api.delete(`/roles/${roleId}`),
|
delete: roleId => api.delete(`/roles/${roleId}`),
|
||||||
initRoles: () => api.post('/roles/init-roles')
|
initRoles: () => api.post('/roles/init-roles'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loginHistoryAPI = {
|
export const loginHistoryAPI = {
|
||||||
list: (params) => api.get('/login-history', { params }),
|
list: params => api.get('/login-history', { params }),
|
||||||
getByUser: (userId, params) => api.get(`/login-history/user/${userId}`, { params }),
|
getByUser: (userId, params) => api.get(`/login-history/user/${userId}`, { params }),
|
||||||
delete: (id) => api.delete(`/login-history/${id}`),
|
delete: id => api.delete(`/login-history/${id}`),
|
||||||
clear: (data) => api.delete('/login-history', { data })
|
clear: data => api.delete('/login-history', { data }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const operationLogAPI = {
|
export const operationLogAPI = {
|
||||||
list: (params) => api.get('/operation-logs', { params }),
|
list: params => api.get('/operation-logs', { params }),
|
||||||
getActions: () => api.get('/operation-logs/actions'),
|
getActions: () => api.get('/operation-logs/actions'),
|
||||||
getModules: () => api.get('/operation-logs/modules'),
|
getModules: () => api.get('/operation-logs/modules'),
|
||||||
delete: (id) => api.delete(`/operation-logs/${id}`),
|
delete: id => api.delete(`/operation-logs/${id}`),
|
||||||
clear: (data) => api.delete('/operation-logs', { data })
|
clear: data => api.delete('/operation-logs', { data }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ticketAPI = {
|
export const ticketAPI = {
|
||||||
list: (params) => api.get('/tickets', { params }),
|
list: params => api.get('/tickets', { params }),
|
||||||
get: (ticketId) => api.get(`/tickets/${ticketId}`),
|
get: ticketId => api.get(`/tickets/${ticketId}`),
|
||||||
create: (data) => api.post('/tickets', data),
|
create: data => api.post('/tickets', data),
|
||||||
update: (ticketId, data) => api.put(`/tickets/${ticketId}`, data),
|
update: (ticketId, data) => api.put(`/tickets/${ticketId}`, data),
|
||||||
delete: (ticketId) => api.delete(`/tickets/${ticketId}`),
|
delete: ticketId => api.delete(`/tickets/${ticketId}`),
|
||||||
assign: (ticketId, data) => api.put(`/tickets/${ticketId}/assign`, data),
|
assign: (ticketId, data) => api.put(`/tickets/${ticketId}/assign`, data),
|
||||||
transfer: (ticketId, data) => api.put(`/tickets/${ticketId}/transfer`, data),
|
transfer: (ticketId, data) => api.put(`/tickets/${ticketId}/transfer`, data),
|
||||||
process: (ticketId, data) => api.put(`/tickets/${ticketId}/process`, data),
|
process: (ticketId, data) => api.put(`/tickets/${ticketId}/process`, data),
|
||||||
close: (ticketId, data) => api.put(`/tickets/${ticketId}/close`, data),
|
close: (ticketId, data) => api.put(`/tickets/${ticketId}/close`, data),
|
||||||
reopen: (ticketId, data) => api.put(`/tickets/${ticketId}/reopen`, data),
|
reopen: (ticketId, data) => api.put(`/tickets/${ticketId}/reopen`, data),
|
||||||
getOperations: (ticketId) => api.get(`/tickets/${ticketId}/operations`),
|
getOperations: ticketId => api.get(`/tickets/${ticketId}/operations`),
|
||||||
getStatistics: (params) => api.get('/tickets/statistics', { params })
|
getStatistics: params => api.get('/tickets/statistics', { params }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ticketCategoryAPI = {
|
export const ticketCategoryAPI = {
|
||||||
list: (params) => api.get('/ticket-categories', { params }),
|
list: params => api.get('/ticket-categories', { params }),
|
||||||
get: (code) => api.get(`/ticket-categories/${code}`),
|
get: code => api.get(`/ticket-categories/${code}`),
|
||||||
create: (data) => api.post('/ticket-categories', data),
|
create: data => api.post('/ticket-categories', data),
|
||||||
update: (code, data) => api.put(`/ticket-categories/${code}`, data),
|
update: (code, data) => api.put(`/ticket-categories/${code}`, data),
|
||||||
delete: (code) => api.delete(`/ticket-categories/${code}`),
|
delete: code => api.delete(`/ticket-categories/${code}`),
|
||||||
tree: () => api.get('/ticket-categories/tree'),
|
tree: () => api.get('/ticket-categories/tree'),
|
||||||
init: () => api.post('/ticket-categories/init')
|
init: () => api.post('/ticket-categories/init'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,12 +5,12 @@ import * as THREE from 'three';
|
|||||||
export const LOD_LEVELS = {
|
export const LOD_LEVELS = {
|
||||||
HIGH: 0,
|
HIGH: 0,
|
||||||
MEDIUM: 1,
|
MEDIUM: 1,
|
||||||
LOW: 2
|
LOW: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LOD_DISTANCES = {
|
export const LOD_DISTANCES = {
|
||||||
HIGH: 5,
|
HIGH: 5,
|
||||||
MEDIUM: 10
|
MEDIUM: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
const createSimplifiedDeviceMesh = (device, uHeight, rackDepth, deviceColor, statusColor) => {
|
const createSimplifiedDeviceMesh = (device, uHeight, rackDepth, deviceColor, statusColor) => {
|
||||||
@@ -34,7 +34,7 @@ const createSimplifiedDeviceMesh = (device, uHeight, rackDepth, deviceColor, sta
|
|||||||
<boxGeometry args={[chassisWidth, height - 0.002, chassisDepth]} />
|
<boxGeometry args={[chassisWidth, height - 0.002, chassisDepth]} />
|
||||||
<meshStandardMaterial color="#333333" roughness={0.9} metalness={0.3} />
|
<meshStandardMaterial color="#333333" roughness={0.9} metalness={0.3} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<mesh position={[panelWidth/2 - 0.03, halfHeight - 0.02, frontZ + 0.01]}>
|
<mesh position={[panelWidth / 2 - 0.03, halfHeight - 0.02, frontZ + 0.01]}>
|
||||||
<circleGeometry args={[0.006, 16]} />
|
<circleGeometry args={[0.006, 16]} />
|
||||||
<meshBasicMaterial color={statusColor} toneMapped={false} />
|
<meshBasicMaterial color={statusColor} toneMapped={false} />
|
||||||
</mesh>
|
</mesh>
|
||||||
@@ -65,9 +65,9 @@ const createMediumDeviceMesh = (device, uHeight, rackDepth, deviceColor, statusC
|
|||||||
<meshStandardMaterial color={deviceColor} roughness={0.8} metalness={0.1} />
|
<meshStandardMaterial color={deviceColor} roughness={0.8} metalness={0.1} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<mesh position={[0, 0, frontZ - panelDepth - chassisDepth / 2]}>
|
<mesh position={[0, 0, frontZ - panelDepth - chassisDepth / 2]}>
|
||||||
<boxGeometry args={[chassisWidth, height - 0.002, chassisDepth]} />
|
<boxGeometry args={[chassisWidth, height - 0.002, chassisDepth]} />
|
||||||
<meshStandardMaterial color="#333333" roughness={0.9} metalness={0.3} />
|
<meshStandardMaterial color="#333333" roughness={0.9} metalness={0.3} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<group position={[-0.18, 0, frontZ + 0.006]}>
|
<group position={[-0.18, 0, frontZ + 0.006]}>
|
||||||
<mesh position={[-0.02, 0, 0]}>
|
<mesh position={[-0.02, 0, 0]}>
|
||||||
<boxGeometry args={[0.04, height - 0.01, 0.002]} />
|
<boxGeometry args={[0.04, height - 0.01, 0.002]} />
|
||||||
@@ -86,7 +86,7 @@ const createMediumDeviceMesh = (device, uHeight, rackDepth, deviceColor, statusC
|
|||||||
</mesh>
|
</mesh>
|
||||||
))}
|
))}
|
||||||
</group>
|
</group>
|
||||||
<mesh position={[panelWidth/2 - 0.03, halfHeight - 0.02, frontZ + 0.01]}>
|
<mesh position={[panelWidth / 2 - 0.03, halfHeight - 0.02, frontZ + 0.01]}>
|
||||||
<circleGeometry args={[0.006, 16]} />
|
<circleGeometry args={[0.006, 16]} />
|
||||||
<meshBasicMaterial color={statusColor} toneMapped={false} />
|
<meshBasicMaterial color={statusColor} toneMapped={false} />
|
||||||
</mesh>
|
</mesh>
|
||||||
@@ -102,7 +102,7 @@ const LODManager = ({
|
|||||||
deviceColor,
|
deviceColor,
|
||||||
statusColor,
|
statusColor,
|
||||||
children,
|
children,
|
||||||
level = LOD_LEVELS.HIGH
|
level = LOD_LEVELS.HIGH,
|
||||||
}) => {
|
}) => {
|
||||||
const groupRef = useRef();
|
const groupRef = useRef();
|
||||||
const highDetailRef = useRef();
|
const highDetailRef = useRef();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const RackModel = ({
|
|||||||
onAddNic,
|
onAddNic,
|
||||||
onAddPort,
|
onAddPort,
|
||||||
tooltipFields,
|
tooltipFields,
|
||||||
deviceSlideEnabled = true
|
deviceSlideEnabled = true,
|
||||||
}) => {
|
}) => {
|
||||||
const width = 0.6;
|
const width = 0.6;
|
||||||
const depth = 1.0;
|
const depth = 1.0;
|
||||||
@@ -33,21 +33,21 @@ const RackModel = ({
|
|||||||
storage: '#8b5cf6',
|
storage: '#8b5cf6',
|
||||||
default: '#3b82f6',
|
default: '#3b82f6',
|
||||||
status: {
|
status: {
|
||||||
running: '#10b981',
|
running: '#10b981',
|
||||||
warning: '#f59e0b',
|
warning: '#f59e0b',
|
||||||
error: '#ef4444',
|
error: '#ef4444',
|
||||||
offline: '#6b7280'
|
offline: '#6b7280',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDeviceColor = (type) => {
|
const getDeviceColor = type => {
|
||||||
const t = type?.toLowerCase() || '';
|
const t = type?.toLowerCase() || '';
|
||||||
if (t.includes('server') || t.includes('服务器')) return colors.server;
|
if (t.includes('server') || t.includes('服务器')) return colors.server;
|
||||||
if (t.includes('switch') || t.includes('交换机')) return colors.switch;
|
if (t.includes('switch') || t.includes('交换机')) return colors.switch;
|
||||||
if (t.includes('router') || t.includes('路由器')) return colors.router;
|
if (t.includes('router') || t.includes('路由器')) return colors.router;
|
||||||
if (t.includes('firewall') || t.includes('防火墙')) return colors.firewall;
|
if (t.includes('firewall') || t.includes('防火墙')) return colors.firewall;
|
||||||
if (t.includes('storage') || t.includes('存储')) return colors.storage;
|
if (t.includes('storage') || t.includes('存储')) return colors.storage;
|
||||||
return colors.default;
|
return colors.default;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 设备组的Y偏移量(与下方设备渲染的偏移一致)
|
// 设备组的Y偏移量(与下方设备渲染的偏移一致)
|
||||||
@@ -62,7 +62,7 @@ const RackModel = ({
|
|||||||
const yPos = (u - 1) * uHeight + uHeight / 2 + deviceGroupOffset;
|
const yPos = (u - 1) * uHeight + uHeight / 2 + deviceGroupOffset;
|
||||||
|
|
||||||
// 创建数字纹理 - 白色数字在深色背景上更清晰
|
// 创建数字纹理 - 白色数字在深色背景上更清晰
|
||||||
const createNumberTexture = (num) => {
|
const createNumberTexture = num => {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = 128;
|
canvas.width = 128;
|
||||||
canvas.height = 128;
|
canvas.height = 128;
|
||||||
@@ -90,9 +90,9 @@ const RackModel = ({
|
|||||||
const planeGeometry = new THREE.PlaneGeometry(planeSize, planeSize);
|
const planeGeometry = new THREE.PlaneGeometry(planeSize, planeSize);
|
||||||
|
|
||||||
// 前侧柱子的位置: [-width/2 + postWidth/2, y, depth/2 - postWidth/2]
|
// 前侧柱子的位置: [-width/2 + postWidth/2, y, depth/2 - postWidth/2]
|
||||||
const leftPostX = -width/2 + postWidth/2;
|
const leftPostX = -width / 2 + postWidth / 2;
|
||||||
const rightPostX = width/2 - postWidth/2;
|
const rightPostX = width / 2 - postWidth / 2;
|
||||||
const frontPostZ = depth/2 - postWidth/2;
|
const frontPostZ = depth / 2 - postWidth / 2;
|
||||||
|
|
||||||
// 刻度线颜色:每5U使用醒目的黄色,其他使用灰色
|
// 刻度线颜色:每5U使用醒目的黄色,其他使用灰色
|
||||||
const tickColor = isMajorU ? '#fbbf24' : '#6b7280';
|
const tickColor = isMajorU ? '#fbbf24' : '#6b7280';
|
||||||
@@ -102,7 +102,7 @@ const RackModel = ({
|
|||||||
<group key={`u-label-${u}`}>
|
<group key={`u-label-${u}`}>
|
||||||
{/* 左前侧柱子上的U位标识 - 贴在柱子正侧面(面向前方) */}
|
{/* 左前侧柱子上的U位标识 - 贴在柱子正侧面(面向前方) */}
|
||||||
<mesh
|
<mesh
|
||||||
position={[leftPostX, yPos, frontPostZ + postWidth/2 + 0.001]}
|
position={[leftPostX, yPos, frontPostZ + postWidth / 2 + 0.001]}
|
||||||
geometry={planeGeometry}
|
geometry={planeGeometry}
|
||||||
>
|
>
|
||||||
<meshBasicMaterial
|
<meshBasicMaterial
|
||||||
@@ -114,7 +114,7 @@ const RackModel = ({
|
|||||||
</mesh>
|
</mesh>
|
||||||
{/* 右前侧柱子上的U位标识 - 贴在柱子正侧面(面向前方) */}
|
{/* 右前侧柱子上的U位标识 - 贴在柱子正侧面(面向前方) */}
|
||||||
<mesh
|
<mesh
|
||||||
position={[rightPostX, yPos, frontPostZ + postWidth/2 + 0.001]}
|
position={[rightPostX, yPos, frontPostZ + postWidth / 2 + 0.001]}
|
||||||
geometry={planeGeometry}
|
geometry={planeGeometry}
|
||||||
>
|
>
|
||||||
<meshBasicMaterial
|
<meshBasicMaterial
|
||||||
@@ -125,12 +125,12 @@ const RackModel = ({
|
|||||||
/>
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
{/* 左前侧柱子上的刻度线 */}
|
{/* 左前侧柱子上的刻度线 */}
|
||||||
<mesh position={[leftPostX, yPos, frontPostZ + postWidth/2 + 0.002]}>
|
<mesh position={[leftPostX, yPos, frontPostZ + postWidth / 2 + 0.002]}>
|
||||||
<boxGeometry args={[postWidth, tickHeight, 0.001]} />
|
<boxGeometry args={[postWidth, tickHeight, 0.001]} />
|
||||||
<meshBasicMaterial color={tickColor} />
|
<meshBasicMaterial color={tickColor} />
|
||||||
</mesh>
|
</mesh>
|
||||||
{/* 右前侧柱子上的刻度线 */}
|
{/* 右前侧柱子上的刻度线 */}
|
||||||
<mesh position={[rightPostX, yPos, frontPostZ + postWidth/2 + 0.002]}>
|
<mesh position={[rightPostX, yPos, frontPostZ + postWidth / 2 + 0.002]}>
|
||||||
<boxGeometry args={[postWidth, tickHeight, 0.001]} />
|
<boxGeometry args={[postWidth, tickHeight, 0.001]} />
|
||||||
<meshBasicMaterial color={tickColor} />
|
<meshBasicMaterial color={tickColor} />
|
||||||
</mesh>
|
</mesh>
|
||||||
@@ -142,53 +142,47 @@ const RackModel = ({
|
|||||||
|
|
||||||
// 生成机柜框架
|
// 生成机柜框架
|
||||||
const frame = useMemo(() => {
|
const frame = useMemo(() => {
|
||||||
const materialProps = { color: "#333", roughness: 0.5, metalness: 0.8 };
|
const materialProps = { color: '#333', roughness: 0.5, metalness: 0.8 };
|
||||||
const postArgs = [postWidth, height, postWidth];
|
const postArgs = [postWidth, height, postWidth];
|
||||||
const topBottomArgs = [width + 0.02, 0.02, depth + 0.02];
|
const topBottomArgs = [width + 0.02, 0.02, depth + 0.02];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
<mesh position={[-width/2 + postWidth/2, height/2, -depth/2 + postWidth/2]}>
|
<mesh position={[-width / 2 + postWidth / 2, height / 2, -depth / 2 + postWidth / 2]}>
|
||||||
<boxGeometry args={postArgs} />
|
<boxGeometry args={postArgs} />
|
||||||
<meshStandardMaterial {...materialProps} />
|
<meshStandardMaterial {...materialProps} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<mesh position={[width/2 - postWidth/2, height/2, -depth/2 + postWidth/2]}>
|
<mesh position={[width / 2 - postWidth / 2, height / 2, -depth / 2 + postWidth / 2]}>
|
||||||
<boxGeometry args={postArgs} />
|
<boxGeometry args={postArgs} />
|
||||||
<meshStandardMaterial {...materialProps} />
|
<meshStandardMaterial {...materialProps} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<mesh position={[-width/2 + postWidth/2, height/2, depth/2 - postWidth/2]}>
|
<mesh position={[-width / 2 + postWidth / 2, height / 2, depth / 2 - postWidth / 2]}>
|
||||||
<boxGeometry args={postArgs} />
|
<boxGeometry args={postArgs} />
|
||||||
<meshStandardMaterial {...materialProps} />
|
<meshStandardMaterial {...materialProps} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<mesh position={[width/2 - postWidth/2, height/2, depth/2 - postWidth/2]}>
|
<mesh position={[width / 2 - postWidth / 2, height / 2, depth / 2 - postWidth / 2]}>
|
||||||
<boxGeometry args={postArgs} />
|
<boxGeometry args={postArgs} />
|
||||||
<meshStandardMaterial {...materialProps} />
|
<meshStandardMaterial {...materialProps} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
<mesh position={[0, height, 0]}>
|
<mesh position={[0, height, 0]}>
|
||||||
<boxGeometry args={topBottomArgs} />
|
<boxGeometry args={topBottomArgs} />
|
||||||
<meshStandardMaterial {...materialProps} />
|
<meshStandardMaterial {...materialProps} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<mesh position={[0, 0, 0]}>
|
<mesh position={[0, 0, 0]}>
|
||||||
<boxGeometry args={topBottomArgs} />
|
<boxGeometry args={topBottomArgs} />
|
||||||
<meshStandardMaterial {...materialProps} />
|
<meshStandardMaterial {...materialProps} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{[-1, 1].map((side) => (
|
{[-1, 1].map(side => (
|
||||||
<mesh key={`side-${side}`} position={[side * (width/2 - 0.005), height/2, 0]}>
|
<mesh key={`side-${side}`} position={[side * (width / 2 - 0.005), height / 2, 0]}>
|
||||||
<boxGeometry args={[0.01, height - 0.04, depth - 0.04]} />
|
<boxGeometry args={[0.01, height - 0.04, depth - 0.04]} />
|
||||||
<meshStandardMaterial
|
<meshStandardMaterial color="#2d3748" roughness={0.4} metalness={0.7} side={2} />
|
||||||
color="#2d3748"
|
</mesh>
|
||||||
roughness={0.4}
|
|
||||||
metalness={0.7}
|
|
||||||
side={2}
|
|
||||||
/>
|
|
||||||
</mesh>
|
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* U位刻度标识 */}
|
{/* U位刻度标识 */}
|
||||||
{uLabels}
|
{uLabels}
|
||||||
|
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
}, [width, height, depth, postWidth, uLabels]);
|
}, [width, height, depth, postWidth, uLabels]);
|
||||||
@@ -198,42 +192,42 @@ const RackModel = ({
|
|||||||
{frame}
|
{frame}
|
||||||
|
|
||||||
<group position={[0, 0.1, 0]}>
|
<group position={[0, 0.1, 0]}>
|
||||||
{devices.map((device) => {
|
{devices.map(device => {
|
||||||
const uStart = device.position || device.u_position || 1;
|
const uStart = device.position || device.u_position || 1;
|
||||||
const uSize = device.height || device.u_height || 1;
|
const uSize = device.height || device.u_height || 1;
|
||||||
const yPos = (uStart - 1) * uHeight + (uSize * uHeight) / 2;
|
const yPos = (uStart - 1) * uHeight + (uSize * uHeight) / 2;
|
||||||
|
|
||||||
const deviceColor = getDeviceColor(device.type);
|
const deviceColor = getDeviceColor(device.type);
|
||||||
const statusColor = colors.status[device.status] || colors.status.running;
|
const statusColor = colors.status[device.status] || colors.status.running;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LODManager
|
<LODManager
|
||||||
key={device.id}
|
key={device.id}
|
||||||
device={device}
|
device={device}
|
||||||
uHeight={uHeight}
|
uHeight={uHeight}
|
||||||
rackDepth={depth}
|
rackDepth={depth}
|
||||||
position={[0, yPos, 0]}
|
position={[0, yPos, 0]}
|
||||||
deviceColor={deviceColor}
|
deviceColor={deviceColor}
|
||||||
statusColor={statusColor}
|
statusColor={statusColor}
|
||||||
level={LOD_LEVELS.HIGH}
|
level={LOD_LEVELS.HIGH}
|
||||||
>
|
>
|
||||||
<DeviceModel
|
<DeviceModel
|
||||||
device={device}
|
device={device}
|
||||||
uHeight={uHeight}
|
uHeight={uHeight}
|
||||||
rackDepth={depth}
|
rackDepth={depth}
|
||||||
position={[0, 0, 0]}
|
position={[0, 0, 0]}
|
||||||
isSelected={selectedDeviceId === device.id}
|
isSelected={selectedDeviceId === device.id}
|
||||||
onClick={onDeviceClick}
|
onClick={onDeviceClick}
|
||||||
onPointerOver={onDeviceHover}
|
onPointerOver={onDeviceHover}
|
||||||
onPointerOut={onDeviceLeave}
|
onPointerOut={onDeviceLeave}
|
||||||
onEdit={onEditDevice}
|
onEdit={onEditDevice}
|
||||||
onAddNic={onAddNic}
|
onAddNic={onAddNic}
|
||||||
onAddPort={onAddPort}
|
onAddPort={onAddPort}
|
||||||
tooltipFields={tooltipFields}
|
tooltipFields={tooltipFields}
|
||||||
slideEnabled={deviceSlideEnabled}
|
slideEnabled={deviceSlideEnabled}
|
||||||
/>
|
/>
|
||||||
</LODManager>
|
</LODManager>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import React, { Suspense, useMemo, useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
|
import React, {
|
||||||
|
Suspense,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useEffect,
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
} from 'react';
|
||||||
import { Canvas, useFrame, useThree } from '@react-three/fiber';
|
import { Canvas, useFrame, useThree } from '@react-three/fiber';
|
||||||
import { OrbitControls, PerspectiveCamera, Environment } from '@react-three/drei';
|
import { OrbitControls, PerspectiveCamera, Environment } from '@react-three/drei';
|
||||||
const envMapUrl = '/assets/3d/env.hdr';
|
const envMapUrl = '/assets/3d/env.hdr';
|
||||||
@@ -22,7 +29,7 @@ const Controls = ({ rack, onControlsReady }) => {
|
|||||||
const { camera } = useThree();
|
const { camera } = useThree();
|
||||||
// 机柜中心点(中轴线)
|
// 机柜中心点(中轴线)
|
||||||
const rackHeight = rack?.height || 45;
|
const rackHeight = rack?.height || 45;
|
||||||
const targetY = rackHeight * 0.04445 / 2 + 0.5;
|
const targetY = (rackHeight * 0.04445) / 2 + 0.5;
|
||||||
const fixedTarget = useMemo(() => new THREE.Vector3(0, targetY, 0), [targetY]);
|
const fixedTarget = useMemo(() => new THREE.Vector3(0, targetY, 0), [targetY]);
|
||||||
|
|
||||||
// 根据机柜高度计算合适的相机距离限制
|
// 根据机柜高度计算合适的相机距离限制
|
||||||
@@ -48,7 +55,7 @@ const Controls = ({ rack, onControlsReady }) => {
|
|||||||
camera.position.copy(initialCameraPosition);
|
camera.position.copy(initialCameraPosition);
|
||||||
controlsRef.current.target.copy(fixedTarget);
|
controlsRef.current.target.copy(fixedTarget);
|
||||||
controlsRef.current.update();
|
controlsRef.current.update();
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,107 +83,122 @@ const Controls = ({ rack, onControlsReady }) => {
|
|||||||
enableZoom={true}
|
enableZoom={true}
|
||||||
enableRotate={true}
|
enableRotate={true}
|
||||||
mouseButtons={{
|
mouseButtons={{
|
||||||
LEFT: 0, // 左键旋转
|
LEFT: 0, // 左键旋转
|
||||||
MIDDLE: 1, // 中键平移
|
MIDDLE: 1, // 中键平移
|
||||||
RIGHT: 2 // 右键平移
|
RIGHT: 2, // 右键平移
|
||||||
}}
|
}}
|
||||||
touches={{
|
touches={{
|
||||||
ONE: 1,
|
ONE: 1,
|
||||||
TWO: 2
|
TWO: 2,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Scene = forwardRef(({ rack, tooltipFields, onDeviceClick, onDeviceHover, onDeviceLeave }, ref) => {
|
const Scene = forwardRef(
|
||||||
// 从 Context 获取3D场景状态
|
({ rack, tooltipFields, onDeviceClick, onDeviceHover, onDeviceLeave }, ref) => {
|
||||||
const {
|
// 从 Context 获取3D场景状态
|
||||||
devices,
|
const { devices, selectedDevice, deviceSlideEnabled } = useScene3D();
|
||||||
selectedDevice,
|
|
||||||
deviceSlideEnabled
|
|
||||||
} = useScene3D();
|
|
||||||
|
|
||||||
// 用于存储 controls API
|
// 用于存储 controls API
|
||||||
const controlsApiRef = useRef(null);
|
const controlsApiRef = useRef(null);
|
||||||
|
|
||||||
// 使用 useImperativeHandle 暴露重置方法给父组件
|
// 使用 useImperativeHandle 暴露重置方法给父组件
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
resetView: () => {
|
resetView: () => {
|
||||||
if (controlsApiRef.current) {
|
if (controlsApiRef.current) {
|
||||||
controlsApiRef.current.reset();
|
controlsApiRef.current.reset();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 使用 useMemo 稳定 props 引用
|
// 使用 useMemo 稳定 props 引用
|
||||||
const rackModelProps = useMemo(() => ({
|
const rackModelProps = useMemo(
|
||||||
rack,
|
() => ({
|
||||||
devices,
|
rack,
|
||||||
selectedDeviceId: selectedDevice?.id,
|
devices,
|
||||||
onDeviceClick,
|
selectedDeviceId: selectedDevice?.id,
|
||||||
onDeviceLeave,
|
onDeviceClick,
|
||||||
onDeviceHover,
|
onDeviceLeave,
|
||||||
tooltipFields,
|
onDeviceHover,
|
||||||
deviceSlideEnabled
|
tooltipFields,
|
||||||
}), [rack, devices, selectedDevice, onDeviceClick, onDeviceLeave, onDeviceHover, tooltipFields, deviceSlideEnabled]);
|
deviceSlideEnabled,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
rack,
|
||||||
|
devices,
|
||||||
|
selectedDevice,
|
||||||
|
onDeviceClick,
|
||||||
|
onDeviceLeave,
|
||||||
|
onDeviceHover,
|
||||||
|
tooltipFields,
|
||||||
|
deviceSlideEnabled,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
// 根据机柜高度动态计算相机初始位置
|
// 根据机柜高度动态计算相机初始位置
|
||||||
const rackHeight = rack?.height || 45;
|
const rackHeight = rack?.height || 45;
|
||||||
const rackHeightMeters = rackHeight * 0.04445;
|
const rackHeightMeters = rackHeight * 0.04445;
|
||||||
// 相机位置:确保能完整看到机柜,高度随机柜高度调整
|
// 相机位置:确保能完整看到机柜,高度随机柜高度调整
|
||||||
const cameraPosition = useMemo(() => {
|
const cameraPosition = useMemo(() => {
|
||||||
const baseHeight = 2;
|
const baseHeight = 2;
|
||||||
const heightFactor = rackHeightMeters * 0.6;
|
const heightFactor = rackHeightMeters * 0.6;
|
||||||
const distance = Math.max(3, rackHeightMeters * 1.2);
|
const distance = Math.max(3, rackHeightMeters * 1.2);
|
||||||
return [distance * 0.7, baseHeight + heightFactor * 0.3, distance];
|
return [distance * 0.7, baseHeight + heightFactor * 0.3, distance];
|
||||||
}, [rackHeightMeters]);
|
}, [rackHeightMeters]);
|
||||||
|
|
||||||
// 相机目标点(机柜中心)
|
// 相机目标点(机柜中心)
|
||||||
const cameraTarget = useMemo(() => {
|
const cameraTarget = useMemo(() => {
|
||||||
return [0, rackHeightMeters / 2 + 0.5, 0];
|
return [0, rackHeightMeters / 2 + 0.5, 0];
|
||||||
}, [rackHeightMeters]);
|
}, [rackHeightMeters]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Canvas
|
<Canvas
|
||||||
shadows
|
shadows
|
||||||
dpr={deviceDpr}
|
dpr={deviceDpr}
|
||||||
performance={{ min: 0.5 }}
|
performance={{ min: 0.5 }}
|
||||||
gl={{
|
gl={{
|
||||||
antialias: true, // 对所有设备开启抗锯齿提升清晰度
|
antialias: true, // 对所有设备开启抗锯齿提升清晰度
|
||||||
alpha: true, // 必须开启alpha以支持透明背景
|
alpha: true, // 必须开启alpha以支持透明背景
|
||||||
powerPreference: 'high-performance'
|
powerPreference: 'high-performance',
|
||||||
}}
|
}}
|
||||||
style={{ background: 'transparent' }}
|
style={{ background: 'transparent' }}
|
||||||
>
|
>
|
||||||
<PerspectiveCamera makeDefault position={cameraPosition} fov={45} />
|
<PerspectiveCamera makeDefault position={cameraPosition} fov={45} />
|
||||||
|
|
||||||
<ambientLight intensity={0.5} color="#ffffff" />
|
<ambientLight intensity={0.5} color="#ffffff" />
|
||||||
<pointLight position={[5, 8, 5]} intensity={2} color="#ffffff" castShadow />
|
<pointLight position={[5, 8, 5]} intensity={2} color="#ffffff" castShadow />
|
||||||
<directionalLight
|
<directionalLight
|
||||||
position={[10, 10, 5]}
|
position={[10, 10, 5]}
|
||||||
intensity={1}
|
intensity={1}
|
||||||
castShadow
|
castShadow
|
||||||
shadow-mapSize={[2048, 2048]}
|
shadow-mapSize={[2048, 2048]}
|
||||||
shadow-camera-far={20}
|
shadow-camera-far={20}
|
||||||
shadow-camera-left={-10}
|
shadow-camera-left={-10}
|
||||||
shadow-camera-right={10}
|
shadow-camera-right={10}
|
||||||
shadow-camera-top={10}
|
shadow-camera-top={10}
|
||||||
shadow-camera-bottom={-10}
|
shadow-camera-bottom={-10}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<Environment files={envMapUrl} blur={0.5} resolution={256} background={false} />
|
<Environment files={envMapUrl} blur={0.5} resolution={256} background={false} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
{/* Models */}
|
{/* Models */}
|
||||||
<group position={[0, 0, 0]}>
|
<group position={[0, 0, 0]}>
|
||||||
<RackModel {...rackModelProps} />
|
<RackModel {...rackModelProps} />
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
{/* Controls - 使用独立组件保持旋转中心固定 */}
|
{/* Controls - 使用独立组件保持旋转中心固定 */}
|
||||||
<Controls rack={rack} onControlsReady={(api) => { controlsApiRef.current = api; }} />
|
<Controls
|
||||||
</Canvas>
|
rack={rack}
|
||||||
);
|
onControlsReady={api => {
|
||||||
});
|
controlsApiRef.current = api;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Canvas>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default Scene;
|
export default Scene;
|
||||||
@@ -3,27 +3,62 @@ import { MATERIAL_CONFIGS, MATERIAL_TYPES } from './constants.js';
|
|||||||
|
|
||||||
export const createDeviceChassisMaterial = (options = {}) => {
|
export const createDeviceChassisMaterial = (options = {}) => {
|
||||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.DEVICE_CHASSIS];
|
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.DEVICE_CHASSIS];
|
||||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} {...options} />;
|
return (
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={config.color}
|
||||||
|
metalness={config.metalness}
|
||||||
|
roughness={config.roughness}
|
||||||
|
{...options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createDevicePanelMaterial = (options = {}) => {
|
export const createDevicePanelMaterial = (options = {}) => {
|
||||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.DEVICE_PANEL];
|
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.DEVICE_PANEL];
|
||||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} {...options} />;
|
return (
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={config.color}
|
||||||
|
metalness={config.metalness}
|
||||||
|
roughness={config.roughness}
|
||||||
|
{...options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createDevicePanelSelectedMaterial = (options = {}) => {
|
export const createDevicePanelSelectedMaterial = (options = {}) => {
|
||||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.DEVICE_PANEL_SELECTED];
|
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.DEVICE_PANEL_SELECTED];
|
||||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} {...options} />;
|
return (
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={config.color}
|
||||||
|
metalness={config.metalness}
|
||||||
|
roughness={config.roughness}
|
||||||
|
{...options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createDriveTrayMaterial = (options = {}) => {
|
export const createDriveTrayMaterial = (options = {}) => {
|
||||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.DRIVE_TRAY];
|
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.DRIVE_TRAY];
|
||||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} {...options} />;
|
return (
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={config.color}
|
||||||
|
metalness={config.metalness}
|
||||||
|
roughness={config.roughness}
|
||||||
|
{...options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createDriveTrayHandleMaterial = (options = {}) => {
|
export const createDriveTrayHandleMaterial = (options = {}) => {
|
||||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.DRIVE_TRAY_HANDLE];
|
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.DRIVE_TRAY_HANDLE];
|
||||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} {...options} />;
|
return (
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={config.color}
|
||||||
|
metalness={config.metalness}
|
||||||
|
roughness={config.roughness}
|
||||||
|
{...options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createLedIndicatorMaterial = (color = '#10b981', options = {}) => {
|
export const createLedIndicatorMaterial = (color = '#10b981', options = {}) => {
|
||||||
@@ -37,32 +72,78 @@ export const createLedErrorMaterial = (options = {}) => {
|
|||||||
|
|
||||||
export const createSfpPortMaterial = (options = {}) => {
|
export const createSfpPortMaterial = (options = {}) => {
|
||||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.SFP_PORT];
|
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.SFP_PORT];
|
||||||
return <meshPhysicalMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} transparent opacity={config.opacity} {...options} />;
|
return (
|
||||||
|
<meshPhysicalMaterial
|
||||||
|
color={config.color}
|
||||||
|
metalness={config.metalness}
|
||||||
|
roughness={config.roughness}
|
||||||
|
transparent
|
||||||
|
opacity={config.opacity}
|
||||||
|
{...options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createRj45PortMaterial = (options = {}) => {
|
export const createRj45PortMaterial = (options = {}) => {
|
||||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.RJ45_PORT];
|
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.RJ45_PORT];
|
||||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} {...options} />;
|
return (
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={config.color}
|
||||||
|
metalness={config.metalness}
|
||||||
|
roughness={config.roughness}
|
||||||
|
{...options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createVentHoleMaterial = (options = {}) => {
|
export const createVentHoleMaterial = (options = {}) => {
|
||||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.VENT_HOLE];
|
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.VENT_HOLE];
|
||||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} {...options} />;
|
return (
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={config.color}
|
||||||
|
metalness={config.metalness}
|
||||||
|
roughness={config.roughness}
|
||||||
|
{...options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createBackPanelMaterial = (options = {}) => {
|
export const createBackPanelMaterial = (options = {}) => {
|
||||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.BACK_PANEL];
|
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.BACK_PANEL];
|
||||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} transparent opacity={config.opacity} {...options} />;
|
return (
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={config.color}
|
||||||
|
metalness={config.metalness}
|
||||||
|
roughness={config.roughness}
|
||||||
|
transparent
|
||||||
|
opacity={config.opacity}
|
||||||
|
{...options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createPsuModuleMaterial = (options = {}) => {
|
export const createPsuModuleMaterial = (options = {}) => {
|
||||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.PSU_MODULE];
|
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.PSU_MODULE];
|
||||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} {...options} />;
|
return (
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={config.color}
|
||||||
|
metalness={config.metalness}
|
||||||
|
roughness={config.roughness}
|
||||||
|
{...options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createFanModuleMaterial = (options = {}) => {
|
export const createFanModuleMaterial = (options = {}) => {
|
||||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.FAN_MODULE];
|
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.FAN_MODULE];
|
||||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} {...options} />;
|
return (
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={config.color}
|
||||||
|
metalness={config.metalness}
|
||||||
|
roughness={config.roughness}
|
||||||
|
{...options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createConsolePortMaterial = (color = '#facc15') => {
|
export const createConsolePortMaterial = (color = '#facc15') => {
|
||||||
|
|||||||
@@ -3,32 +3,79 @@ import { MATERIAL_CONFIGS, MATERIAL_TYPES } from './constants.js';
|
|||||||
|
|
||||||
export const createRackFrameMaterial = (options = {}) => {
|
export const createRackFrameMaterial = (options = {}) => {
|
||||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.RACK_FRAME];
|
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.RACK_FRAME];
|
||||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} envMapIntensity={config.envMapIntensity} {...options} />;
|
return (
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={config.color}
|
||||||
|
metalness={config.metalness}
|
||||||
|
roughness={config.roughness}
|
||||||
|
envMapIntensity={config.envMapIntensity}
|
||||||
|
{...options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createRailMaterial = (options = {}) => {
|
export const createRailMaterial = (options = {}) => {
|
||||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.RAIL];
|
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.RAIL];
|
||||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} envMapIntensity={config.envMapIntensity} {...options} />;
|
return (
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={config.color}
|
||||||
|
metalness={config.metalness}
|
||||||
|
roughness={config.roughness}
|
||||||
|
envMapIntensity={config.envMapIntensity}
|
||||||
|
{...options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createRailHoleMaterial = (options = {}) => {
|
export const createRailHoleMaterial = (options = {}) => {
|
||||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.RAIL_HOLE];
|
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.RAIL_HOLE];
|
||||||
return <meshBasicMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} {...options} />;
|
return (
|
||||||
|
<meshBasicMaterial
|
||||||
|
color={config.color}
|
||||||
|
metalness={config.metalness}
|
||||||
|
roughness={config.roughness}
|
||||||
|
{...options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createSidePanelMaterial = (options = {}) => {
|
export const createSidePanelMaterial = (options = {}) => {
|
||||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.SIDE_PANEL];
|
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.SIDE_PANEL];
|
||||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} envMapIntensity={config.envMapIntensity} {...options} />;
|
return (
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={config.color}
|
||||||
|
metalness={config.metalness}
|
||||||
|
roughness={config.roughness}
|
||||||
|
envMapIntensity={config.envMapIntensity}
|
||||||
|
{...options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createTopPlateMaterial = (options = {}) => {
|
export const createTopPlateMaterial = (options = {}) => {
|
||||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.TOP_PLATE];
|
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.TOP_PLATE];
|
||||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} envMapIntensity={config.envMapIntensity} {...options} />;
|
return (
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={config.color}
|
||||||
|
metalness={config.metalness}
|
||||||
|
roughness={config.roughness}
|
||||||
|
envMapIntensity={config.envMapIntensity}
|
||||||
|
{...options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createBottomPlateMaterial = (options = {}) => {
|
export const createBottomPlateMaterial = (options = {}) => {
|
||||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.BOTTOM_PLATE];
|
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.BOTTOM_PLATE];
|
||||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} envMapIntensity={config.envMapIntensity} {...options} />;
|
return (
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={config.color}
|
||||||
|
metalness={config.metalness}
|
||||||
|
roughness={config.roughness}
|
||||||
|
envMapIntensity={config.envMapIntensity}
|
||||||
|
{...options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createTextLabelMaterial = (color = '#ffffff') => {
|
export const createTextLabelMaterial = (color = '#ffffff') => {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { MATERIAL_CONFIGS, MATERIAL_TYPES } from './constants.js';
|
import { MATERIAL_CONFIGS, MATERIAL_TYPES } from './constants.js';
|
||||||
|
|
||||||
export const getMaterialConfig = (type) => {
|
export const getMaterialConfig = type => {
|
||||||
return MATERIAL_CONFIGS[type] || null;
|
return MATERIAL_CONFIGS[type] || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMaterialType = (type) => {
|
export const getMaterialType = type => {
|
||||||
return MATERIAL_TYPES[type] || null;
|
return MATERIAL_TYPES[type] || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
|
|||||||
form.resetFields();
|
form.resetFields();
|
||||||
if (sourceDevice) {
|
if (sourceDevice) {
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
sourceDeviceId: sourceDevice.deviceId || sourceDevice.id,
|
sourceDeviceId: sourceDevice.deviceId || sourceDevice.id,
|
||||||
});
|
});
|
||||||
fetchDevicePorts(sourceDevice.deviceId || sourceDevice.id, 'source');
|
fetchDevicePorts(sourceDevice.deviceId || sourceDevice.id, 'source');
|
||||||
}
|
}
|
||||||
@@ -63,12 +63,12 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSourceDeviceChange = (deviceId) => {
|
const handleSourceDeviceChange = deviceId => {
|
||||||
form.setFieldsValue({ sourcePort: undefined });
|
form.setFieldsValue({ sourcePort: undefined });
|
||||||
fetchDevicePorts(deviceId, 'source');
|
fetchDevicePorts(deviceId, 'source');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTargetDeviceChange = (deviceId) => {
|
const handleTargetDeviceChange = deviceId => {
|
||||||
form.setFieldsValue({ targetPort: undefined });
|
form.setFieldsValue({ targetPort: undefined });
|
||||||
fetchDevicePorts(deviceId, 'target');
|
fetchDevicePorts(deviceId, 'target');
|
||||||
};
|
};
|
||||||
@@ -85,18 +85,15 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
|
|||||||
let payload = { ...values };
|
let payload = { ...values };
|
||||||
|
|
||||||
// If Source is NOT Switch AND Target IS Switch, swap them
|
// If Source is NOT Switch AND Target IS Switch, swap them
|
||||||
if (sourceDev && targetDev &&
|
if (sourceDev && targetDev && sourceDev.type !== 'switch' && targetDev.type === 'switch') {
|
||||||
sourceDev.type !== 'switch' &&
|
payload = {
|
||||||
targetDev.type === 'switch') {
|
...values,
|
||||||
|
sourceDeviceId: values.targetDeviceId,
|
||||||
payload = {
|
sourcePort: values.targetPort,
|
||||||
...values,
|
targetDeviceId: values.sourceDeviceId,
|
||||||
sourceDeviceId: values.targetDeviceId,
|
targetPort: values.sourcePort,
|
||||||
sourcePort: values.targetPort,
|
};
|
||||||
targetDeviceId: values.sourceDeviceId,
|
console.log('Swapped source/target to ensure Switch is Source');
|
||||||
targetPort: values.sourcePort
|
|
||||||
};
|
|
||||||
console.log('Swapped source/target to ensure Switch is Source');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await axios.post('/api/cables', payload);
|
await axios.post('/api/cables', payload);
|
||||||
@@ -130,122 +127,125 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
|
|||||||
>
|
>
|
||||||
<Form form={form} layout="vertical">
|
<Form form={form} layout="vertical">
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||||
{/* Source Side */}
|
{/* Source Side */}
|
||||||
<div style={{ padding: '12px', background: '#f9f9f9', borderRadius: '8px' }}>
|
<div style={{ padding: '12px', background: '#f9f9f9', borderRadius: '8px' }}>
|
||||||
<div style={{ marginBottom: 12, fontWeight: 500, color: '#666' }}>源设备 (起点)</div>
|
<div style={{ marginBottom: 12, fontWeight: 500, color: '#666' }}>源设备 (起点)</div>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="sourceDeviceId"
|
name="sourceDeviceId"
|
||||||
label="设备"
|
label="设备"
|
||||||
rules={[{ required: true, message: '请选择源设备' }]}
|
rules={[{ required: true, message: '请选择源设备' }]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
placeholder="选择源设备"
|
placeholder="选择源设备"
|
||||||
showSearch
|
showSearch
|
||||||
filterOption={(input, option) =>
|
filterOption={(input, option) =>
|
||||||
(option?.children ?? '').toLowerCase().includes(input.toLowerCase())
|
(option?.children ?? '').toLowerCase().includes(input.toLowerCase())
|
||||||
}
|
}
|
||||||
onChange={handleSourceDeviceChange}
|
onChange={handleSourceDeviceChange}
|
||||||
loading={fetchingDevices}
|
loading={fetchingDevices}
|
||||||
disabled={!!sourceDevice} // Lock source device if provided
|
disabled={!!sourceDevice} // Lock source device if provided
|
||||||
>
|
>
|
||||||
{devices.map(d => (
|
{devices.map(d => (
|
||||||
<Option key={d.deviceId} value={d.deviceId}>{d.name}</Option>
|
<Option key={d.deviceId} value={d.deviceId}>
|
||||||
))}
|
{d.name}
|
||||||
</Select>
|
</Option>
|
||||||
</Form.Item>
|
))}
|
||||||
<Form.Item
|
</Select>
|
||||||
name="sourcePort"
|
</Form.Item>
|
||||||
label="端口"
|
<Form.Item
|
||||||
rules={[{ required: true, message: '请选择源端口' }]}
|
name="sourcePort"
|
||||||
>
|
label="端口"
|
||||||
<Select placeholder="选择源端口" showSearch>
|
rules={[{ required: true, message: '请选择源端口' }]}
|
||||||
{sourcePorts.map(p => (
|
>
|
||||||
<Option key={p.portId} value={p.portName}>
|
<Select placeholder="选择源端口" showSearch>
|
||||||
{p.portName} ({p.portType})
|
{sourcePorts.map(p => (
|
||||||
</Option>
|
<Option key={p.portId} value={p.portName}>
|
||||||
))}
|
{p.portName} ({p.portType})
|
||||||
</Select>
|
</Option>
|
||||||
</Form.Item>
|
))}
|
||||||
</div>
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Target Side */}
|
{/* Target Side */}
|
||||||
<div style={{ padding: '12px', background: '#f9f9f9', borderRadius: '8px' }}>
|
<div style={{ padding: '12px', background: '#f9f9f9', borderRadius: '8px' }}>
|
||||||
<div style={{ marginBottom: 12, fontWeight: 500, color: '#666' }}>目标设备 (终点)</div>
|
<div style={{ marginBottom: 12, fontWeight: 500, color: '#666' }}>目标设备 (终点)</div>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="targetDeviceId"
|
name="targetDeviceId"
|
||||||
label="设备"
|
label="设备"
|
||||||
rules={[{ required: true, message: '请选择目标设备' }]}
|
rules={[{ required: true, message: '请选择目标设备' }]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
placeholder="选择目标设备"
|
placeholder="选择目标设备"
|
||||||
showSearch
|
showSearch
|
||||||
filterOption={(input, option) =>
|
filterOption={(input, option) =>
|
||||||
(option?.children ?? '').toLowerCase().includes(input.toLowerCase())
|
(option?.children ?? '').toLowerCase().includes(input.toLowerCase())
|
||||||
}
|
}
|
||||||
onChange={handleTargetDeviceChange}
|
onChange={handleTargetDeviceChange}
|
||||||
loading={fetchingDevices}
|
loading={fetchingDevices}
|
||||||
>
|
>
|
||||||
{devices.filter(d => d.deviceId !== form.getFieldValue('sourceDeviceId')).map(d => (
|
{devices
|
||||||
<Option key={d.deviceId} value={d.deviceId}>{d.name}</Option>
|
.filter(d => d.deviceId !== form.getFieldValue('sourceDeviceId'))
|
||||||
))}
|
.map(d => (
|
||||||
</Select>
|
<Option key={d.deviceId} value={d.deviceId}>
|
||||||
</Form.Item>
|
{d.name}
|
||||||
<Form.Item
|
</Option>
|
||||||
name="targetPort"
|
))}
|
||||||
label="端口"
|
</Select>
|
||||||
rules={[{ required: true, message: '请选择目标端口' }]}
|
</Form.Item>
|
||||||
>
|
<Form.Item
|
||||||
<Select
|
name="targetPort"
|
||||||
placeholder="选择目标端口"
|
label="端口"
|
||||||
showSearch
|
rules={[{ required: true, message: '请选择目标端口' }]}
|
||||||
disabled={!form.getFieldValue('targetDeviceId')}
|
>
|
||||||
>
|
<Select
|
||||||
{targetPorts.map(p => (
|
placeholder="选择目标端口"
|
||||||
<Option key={p.portId} value={p.portName}>
|
showSearch
|
||||||
{p.portName} ({p.portType})
|
disabled={!form.getFieldValue('targetDeviceId')}
|
||||||
</Option>
|
>
|
||||||
))}
|
{targetPorts.map(p => (
|
||||||
</Select>
|
<Option key={p.portId} value={p.portName}>
|
||||||
</Form.Item>
|
{p.portName} ({p.portType})
|
||||||
</div>
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginTop: 16 }}>
|
<div style={{ marginTop: 16 }}>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 16 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 16 }}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="cableType"
|
name="cableType"
|
||||||
label="线缆类型"
|
label="线缆类型"
|
||||||
initialValue="ethernet"
|
initialValue="ethernet"
|
||||||
rules={[{ required: true }]}
|
rules={[{ required: true }]}
|
||||||
>
|
>
|
||||||
<Select>
|
<Select>
|
||||||
<Option value="ethernet">网线</Option>
|
<Option value="ethernet">网线</Option>
|
||||||
<Option value="fiber">光纤</Option>
|
<Option value="fiber">光纤</Option>
|
||||||
<Option value="copper">铜缆</Option>
|
<Option value="copper">铜缆</Option>
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name="status"
|
|
||||||
label="状态"
|
|
||||||
initialValue="normal"
|
|
||||||
rules={[{ required: true }]}
|
|
||||||
>
|
|
||||||
<Select>
|
|
||||||
<Option value="normal">正常</Option>
|
|
||||||
<Option value="fault">故障</Option>
|
|
||||||
<Option value="disconnected">未连接</Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name="cableLength"
|
|
||||||
label="长度 (米)"
|
|
||||||
>
|
|
||||||
<Input type="number" min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
<Form.Item name="description" label="描述">
|
|
||||||
<Input.TextArea rows={2} />
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="status"
|
||||||
|
label="状态"
|
||||||
|
initialValue="normal"
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<Select>
|
||||||
|
<Option value="normal">正常</Option>
|
||||||
|
<Option value="fault">故障</Option>
|
||||||
|
<Option value="disconnected">未连接</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="cableLength" label="长度 (米)">
|
||||||
|
<Input type="number" min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<Form.Item name="description" label="描述">
|
||||||
|
<Input.TextArea rows={2} />
|
||||||
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,6 +1,24 @@
|
|||||||
import React, { useState, useCallback, useMemo } from 'react';
|
import React, { useState, useCallback, useMemo } from 'react';
|
||||||
import { Drawer, Tabs, Tag, Space, Typography, Empty, Card, Tooltip, Button, Popconfirm } from 'antd';
|
import {
|
||||||
import { ApiOutlined, CloudServerOutlined, EnvironmentOutlined, EditOutlined, PlusCircleOutlined, DeleteOutlined } from '@ant-design/icons';
|
Drawer,
|
||||||
|
Tabs,
|
||||||
|
Tag,
|
||||||
|
Space,
|
||||||
|
Typography,
|
||||||
|
Empty,
|
||||||
|
Card,
|
||||||
|
Tooltip,
|
||||||
|
Button,
|
||||||
|
Popconfirm,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
ApiOutlined,
|
||||||
|
CloudServerOutlined,
|
||||||
|
EnvironmentOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
PlusCircleOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import NetworkCardPanel from './NetworkCardPanel';
|
import NetworkCardPanel from './NetworkCardPanel';
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
@@ -10,25 +28,38 @@ const designTokens = {
|
|||||||
primary: '#667eea',
|
primary: '#667eea',
|
||||||
success: '#10b981',
|
success: '#10b981',
|
||||||
error: '#ef4444',
|
error: '#ef4444',
|
||||||
warning: '#f59e0b'
|
warning: '#f59e0b',
|
||||||
},
|
},
|
||||||
spacing: {
|
spacing: {
|
||||||
sm: 8,
|
sm: 8,
|
||||||
md: 16
|
md: 16,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function DeviceDetailDrawer({ device, visible, onClose, cables, onRefreshCables, onEdit, onAddNic, onAddPort, onAddCable, onDeleteCable, tooltipFields, refreshTrigger }) {
|
function DeviceDetailDrawer({
|
||||||
|
device,
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
cables,
|
||||||
|
onRefreshCables,
|
||||||
|
onEdit,
|
||||||
|
onAddNic,
|
||||||
|
onAddPort,
|
||||||
|
onAddCable,
|
||||||
|
onDeleteCable,
|
||||||
|
tooltipFields,
|
||||||
|
refreshTrigger,
|
||||||
|
}) {
|
||||||
const [activeTab, setActiveTab] = useState('ports');
|
const [activeTab, setActiveTab] = useState('ports');
|
||||||
|
|
||||||
const deviceCables = useMemo(() => {
|
const deviceCables = useMemo(() => {
|
||||||
if (!device || !cables) return [];
|
if (!device || !cables) return [];
|
||||||
return cables.filter(c =>
|
return cables.filter(
|
||||||
c.sourceDeviceId === device.deviceId || c.targetDeviceId === device.deviceId
|
c => c.sourceDeviceId === device.deviceId || c.targetDeviceId === device.deviceId
|
||||||
);
|
);
|
||||||
}, [device, cables]);
|
}, [device, cables]);
|
||||||
|
|
||||||
const getStatusTag = useCallback((status) => {
|
const getStatusTag = useCallback(status => {
|
||||||
const config = {
|
const config = {
|
||||||
running: { color: 'success', text: '运行中' },
|
running: { color: 'success', text: '运行中' },
|
||||||
normal: { color: 'success', text: '正常' },
|
normal: { color: 'success', text: '正常' },
|
||||||
@@ -36,13 +67,13 @@ function DeviceDetailDrawer({ device, visible, onClose, cables, onRefreshCables,
|
|||||||
error: { color: 'error', text: '故障' },
|
error: { color: 'error', text: '故障' },
|
||||||
fault: { color: 'error', text: '故障' },
|
fault: { color: 'error', text: '故障' },
|
||||||
offline: { color: 'default', text: '离线' },
|
offline: { color: 'default', text: '离线' },
|
||||||
maintenance: { color: 'processing', text: '维护中' }
|
maintenance: { color: 'processing', text: '维护中' },
|
||||||
};
|
};
|
||||||
const { color, text } = config[status] || { color: 'default', text: status };
|
const { color, text } = config[status] || { color: 'default', text: status };
|
||||||
return <Tag color={color}>{text}</Tag>;
|
return <Tag color={color}>{text}</Tag>;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getDeviceTypeName = useCallback((type) => {
|
const getDeviceTypeName = useCallback(type => {
|
||||||
const typeMap = {
|
const typeMap = {
|
||||||
server: '服务器',
|
server: '服务器',
|
||||||
switch: '交换机',
|
switch: '交换机',
|
||||||
@@ -50,41 +81,53 @@ function DeviceDetailDrawer({ device, visible, onClose, cables, onRefreshCables,
|
|||||||
storage: '存储设备',
|
storage: '存储设备',
|
||||||
firewall: '防火墙',
|
firewall: '防火墙',
|
||||||
ups: 'UPS',
|
ups: 'UPS',
|
||||||
pdu: 'PDU'
|
pdu: 'PDU',
|
||||||
};
|
};
|
||||||
return typeMap[type?.toLowerCase()] || type || '未知设备';
|
return typeMap[type?.toLowerCase()] || type || '未知设备';
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const renderFieldValue = useCallback((field, device) => {
|
const renderFieldValue = useCallback(
|
||||||
|
(field, device) => {
|
||||||
const fieldKey = field.field;
|
const fieldKey = field.field;
|
||||||
if (fieldKey === 'status') return getStatusTag(device.status);
|
if (fieldKey === 'status') return getStatusTag(device.status);
|
||||||
|
|
||||||
// 优先从device对象获取值,如果没有则从customFields中获取
|
// 优先从device对象获取值,如果没有则从customFields中获取
|
||||||
let value = device[fieldKey];
|
let value = device[fieldKey];
|
||||||
if ((value === undefined || value === null) && device.customFields && typeof device.customFields === 'object') {
|
if (
|
||||||
value = device.customFields[fieldKey];
|
(value === undefined || value === null) &&
|
||||||
|
device.customFields &&
|
||||||
|
typeof device.customFields === 'object'
|
||||||
|
) {
|
||||||
|
value = device.customFields[fieldKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fieldKey === 'type') value = getDeviceTypeName(value);
|
if (fieldKey === 'type') value = getDeviceTypeName(value);
|
||||||
else if (fieldKey === 'position') value = `U${device.position} ${device.height ? `(${device.height}U)` : ''}`;
|
else if (fieldKey === 'position')
|
||||||
|
value = `U${device.position} ${device.height ? `(${device.height}U)` : ''}`;
|
||||||
|
|
||||||
return <Text strong style={{ fontSize: '14px' }}>{value !== undefined && value !== null ? value : '-'}</Text>;
|
return (
|
||||||
}, [getStatusTag, getDeviceTypeName]);
|
<Text strong style={{ fontSize: '14px' }}>
|
||||||
|
{value !== undefined && value !== null ? value : '-'}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[getStatusTag, getDeviceTypeName]
|
||||||
|
);
|
||||||
|
|
||||||
const displayFields = useMemo(() => {
|
const displayFields = useMemo(() => {
|
||||||
if (tooltipFields && Object.keys(tooltipFields).length > 0) {
|
if (tooltipFields && Object.keys(tooltipFields).length > 0) {
|
||||||
return Object.values(tooltipFields).filter(f => f.enabled);
|
return Object.values(tooltipFields).filter(f => f.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default fallback fields if no config
|
// Default fallback fields if no config
|
||||||
return [
|
return [
|
||||||
{ field: 'deviceId', label: '设备ID' },
|
{ field: 'deviceId', label: '设备ID' },
|
||||||
{ field: 'type', label: '设备类型' },
|
{ field: 'type', label: '设备类型' },
|
||||||
{ field: 'status', label: '设备状态' },
|
{ field: 'status', label: '设备状态' },
|
||||||
{ field: 'position', label: '位置' },
|
{ field: 'position', label: '位置' },
|
||||||
{ field: 'ipAddress', label: 'IP地址' },
|
{ field: 'ipAddress', label: 'IP地址' },
|
||||||
{ field: 'brand', label: '品牌' }
|
{ field: 'brand', label: '品牌' },
|
||||||
];
|
];
|
||||||
}, [tooltipFields]);
|
}, [tooltipFields]);
|
||||||
|
|
||||||
const tabItems = [
|
const tabItems = [
|
||||||
@@ -103,7 +146,7 @@ function DeviceDetailDrawer({ device, visible, onClose, cables, onRefreshCables,
|
|||||||
onRefresh={onRefreshCables}
|
onRefresh={onRefreshCables}
|
||||||
refreshTrigger={refreshTrigger}
|
refreshTrigger={refreshTrigger}
|
||||||
/>
|
/>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'cables',
|
key: 'cables',
|
||||||
@@ -138,38 +181,64 @@ function DeviceDetailDrawer({ device, visible, onClose, cables, onRefreshCables,
|
|||||||
<div style={{ marginBottom: designTokens.spacing.sm }}>
|
<div style={{ marginBottom: designTokens.spacing.sm }}>
|
||||||
<Space direction="vertical" size={4}>
|
<Space direction="vertical" size={4}>
|
||||||
<div>
|
<div>
|
||||||
<Text type="secondary" style={{ fontSize: '12px' }}>源设备:</Text>
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||||
|
源设备:
|
||||||
|
</Text>
|
||||||
<div style={{ fontWeight: 500 }}>
|
<div style={{ fontWeight: 500 }}>
|
||||||
{cable.sourceDevice?.name || '-'}
|
{cable.sourceDevice?.name || '-'}
|
||||||
<Tag color="blue" style={{ marginLeft: '8px' }}>{cable.sourcePort}</Tag>
|
<Tag color="blue" style={{ marginLeft: '8px' }}>
|
||||||
|
{cable.sourcePort}
|
||||||
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Text type="secondary" style={{ fontSize: '12px' }}>目标设备:</Text>
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||||
|
目标设备:
|
||||||
|
</Text>
|
||||||
<div style={{ fontWeight: 500 }}>
|
<div style={{ fontWeight: 500 }}>
|
||||||
{cable.targetDevice?.name || '-'}
|
{cable.targetDevice?.name || '-'}
|
||||||
<Tag color="green" style={{ marginLeft: '8px' }}>{cable.targetPort}</Tag>
|
<Tag color="green" style={{ marginLeft: '8px' }}>
|
||||||
|
{cable.targetPort}
|
||||||
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Tag color={cable.status === 'normal' ? 'success' : cable.status === 'fault' ? 'error' : 'default'}>
|
<Tag
|
||||||
{cable.status === 'normal' ? '正常' : cable.status === 'fault' ? '故障' : '未连接'}
|
color={
|
||||||
|
cable.status === 'normal'
|
||||||
|
? 'success'
|
||||||
|
: cable.status === 'fault'
|
||||||
|
? 'error'
|
||||||
|
: 'default'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{cable.status === 'normal'
|
||||||
|
? '正常'
|
||||||
|
: cable.status === 'fault'
|
||||||
|
? '故障'
|
||||||
|
: '未连接'}
|
||||||
</Tag>
|
</Tag>
|
||||||
<Tag color="purple">
|
<Tag color="purple">
|
||||||
{cable.cableType === 'ethernet' ? '网线' : cable.cableType === 'fiber' ? '光纤' : '铜缆'}
|
{cable.cableType === 'ethernet'
|
||||||
|
? '网线'
|
||||||
|
: cable.cableType === 'fiber'
|
||||||
|
? '光纤'
|
||||||
|
: '铜缆'}
|
||||||
</Tag>
|
</Tag>
|
||||||
{cable.cableLength && (
|
{cable.cableLength && <Tag color="orange">{cable.cableLength}m</Tag>}
|
||||||
<Tag color="orange">
|
|
||||||
{cable.cableLength}m
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
{cable.description && (
|
{cable.description && (
|
||||||
<div style={{ marginTop: designTokens.spacing.sm, fontSize: '12px', color: '#666' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: designTokens.spacing.sm,
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#666',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{cable.description}
|
{cable.description}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -178,8 +247,8 @@ function DeviceDetailDrawer({ device, visible, onClose, cables, onRefreshCables,
|
|||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!device) return null;
|
if (!device) return null;
|
||||||
@@ -189,11 +258,15 @@ function DeviceDetailDrawer({ device, visible, onClose, cables, onRefreshCables,
|
|||||||
title={
|
title={
|
||||||
<Space style={{ maxWidth: '280px', overflow: 'hidden' }}>
|
<Space style={{ maxWidth: '280px', overflow: 'hidden' }}>
|
||||||
<CloudServerOutlined style={{ color: designTokens.colors.primary, flexShrink: 0 }} />
|
<CloudServerOutlined style={{ color: designTokens.colors.primary, flexShrink: 0 }} />
|
||||||
<span style={{
|
<span
|
||||||
overflow: 'hidden',
|
style={{
|
||||||
textOverflow: 'ellipsis',
|
overflow: 'hidden',
|
||||||
whiteSpace: 'nowrap'
|
textOverflow: 'ellipsis',
|
||||||
}}>设备详情 - {device.name}</span>
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
设备详情 - {device.name}
|
||||||
|
</span>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
placement="right"
|
placement="right"
|
||||||
@@ -206,42 +279,51 @@ function DeviceDetailDrawer({ device, visible, onClose, cables, onRefreshCables,
|
|||||||
<Button icon={<EditOutlined />} onClick={() => onEdit?.(device)} />
|
<Button icon={<EditOutlined />} onClick={() => onEdit?.(device)} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="添加网卡">
|
<Tooltip title="添加网卡">
|
||||||
<Button icon={<PlusCircleOutlined />} onClick={() => onAddNic?.(device)}>加网卡</Button>
|
<Button icon={<PlusCircleOutlined />} onClick={() => onAddNic?.(device)}>
|
||||||
|
加网卡
|
||||||
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="添加端口">
|
<Tooltip title="添加端口">
|
||||||
<Button icon={<ApiOutlined />} onClick={() => onAddPort?.(device)}>加端口</Button>
|
<Button icon={<ApiOutlined />} onClick={() => onAddPort?.(device)}>
|
||||||
|
加端口
|
||||||
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="添加接线">
|
<Tooltip title="添加接线">
|
||||||
<Button icon={<EnvironmentOutlined />} onClick={() => onAddCable?.(device)}>加接线</Button>
|
<Button icon={<EnvironmentOutlined />} onClick={() => onAddCable?.(device)}>
|
||||||
|
加接线
|
||||||
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
styles={{ body: { padding: '16px 20px', overflow: 'auto' } }}
|
styles={{ body: { padding: '16px 20px', overflow: 'auto' } }}
|
||||||
>
|
>
|
||||||
<div className="device-info-section" style={{ marginBottom: '20px' }}>
|
<div className="device-info-section" style={{ marginBottom: '20px' }}>
|
||||||
<Title level={5} style={{ margin: '0 0 12px 0', color: '#1e293b' }}>基本信息</Title>
|
<Title level={5} style={{ margin: '0 0 12px 0', color: '#1e293b' }}>
|
||||||
<div className="info-grid" style={{
|
基本信息
|
||||||
display: 'grid',
|
</Title>
|
||||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
<div
|
||||||
gap: '12px',
|
className="info-grid"
|
||||||
background: '#f8fafc',
|
style={{
|
||||||
padding: '16px',
|
display: 'grid',
|
||||||
borderRadius: '10px'
|
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||||
}}>
|
gap: '12px',
|
||||||
|
background: '#f8fafc',
|
||||||
|
padding: '16px',
|
||||||
|
borderRadius: '10px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{displayFields.map(field => (
|
{displayFields.map(field => (
|
||||||
<div className="info-item" key={field.field}>
|
<div className="info-item" key={field.field}>
|
||||||
<Text type="secondary" style={{ fontSize: '12px', display: 'block' }}>{field.label}</Text>
|
<Text type="secondary" style={{ fontSize: '12px', display: 'block' }}>
|
||||||
|
{field.label}
|
||||||
|
</Text>
|
||||||
{renderFieldValue(field, device)}
|
{renderFieldValue(field, device)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs
|
<Tabs activeKey={activeTab} onChange={setActiveTab} items={tabItems} />
|
||||||
activeKey={activeTab}
|
|
||||||
onChange={setActiveTab}
|
|
||||||
items={tabItems}
|
|
||||||
/>
|
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ const { TextArea } = Input;
|
|||||||
const designTokens = {
|
const designTokens = {
|
||||||
colors: {
|
colors: {
|
||||||
primary: {
|
primary: {
|
||||||
main: '#667eea'
|
main: '#667eea',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
medium: '10px'
|
medium: '10px',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) {
|
function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) {
|
||||||
@@ -33,7 +33,7 @@ function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) {
|
|||||||
description: values.description,
|
description: values.description,
|
||||||
model: values.model,
|
model: values.model,
|
||||||
manufacturer: values.manufacturer,
|
manufacturer: values.manufacturer,
|
||||||
status: values.status
|
status: values.status,
|
||||||
});
|
});
|
||||||
|
|
||||||
message.success('网卡创建成功');
|
message.success('网卡创建成功');
|
||||||
@@ -77,7 +77,7 @@ function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) {
|
|||||||
form={form}
|
form={form}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
initialValues={{
|
initialValues={{
|
||||||
status: 'normal'
|
status: 'normal',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -92,24 +92,15 @@ function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) {
|
|||||||
}
|
}
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: '请输入网卡名称' },
|
{ required: true, message: '请输入网卡名称' },
|
||||||
{ max: 50, message: '名称不能超过50个字符' }
|
{ max: 50, message: '名称不能超过50个字符' },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder="例如: 网卡1、eth0、LAN1" />
|
<Input placeholder="例如: 网卡1、eth0、LAN1" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Space style={{ display: 'flex', width: '100%' }}>
|
<Space style={{ display: 'flex', width: '100%' }}>
|
||||||
<Form.Item
|
<Form.Item name="slotNumber" label="插槽编号" style={{ flex: 1 }}>
|
||||||
name="slotNumber"
|
<InputNumber placeholder="可选" min={1} max={100} style={{ width: '100%' }} />
|
||||||
label="插槽编号"
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
>
|
|
||||||
<InputNumber
|
|
||||||
placeholder="可选"
|
|
||||||
min={1}
|
|
||||||
max={100}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -128,27 +119,16 @@ function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) {
|
|||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<Space style={{ display: 'flex', width: '100%' }}>
|
<Space style={{ display: 'flex', width: '100%' }}>
|
||||||
<Form.Item
|
<Form.Item name="manufacturer" label="制造商" style={{ flex: 1 }}>
|
||||||
name="manufacturer"
|
|
||||||
label="制造商"
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
>
|
|
||||||
<Input placeholder="如: Intel、Realtek、Broadcom" />
|
<Input placeholder="如: Intel、Realtek、Broadcom" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item name="model" label="型号" style={{ flex: 1 }}>
|
||||||
name="model"
|
|
||||||
label="型号"
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
>
|
|
||||||
<Input placeholder="如: X520-DA2" />
|
<Input placeholder="如: X520-DA2" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item name="description" label="描述">
|
||||||
name="description"
|
|
||||||
label="描述"
|
|
||||||
>
|
|
||||||
<TextArea rows={2} placeholder="请输入描述信息(可选)" />
|
<TextArea rows={2} placeholder="请输入描述信息(可选)" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { Table, Button, Space, Tag, Tooltip, Popconfirm, Empty, Spin, Badge, Collapse, Card } from 'antd';
|
import {
|
||||||
import { PlusOutlined, DeleteOutlined, ReloadOutlined, ApiOutlined, CloudServerOutlined, FolderOutlined } from '@ant-design/icons';
|
Table,
|
||||||
|
Button,
|
||||||
|
Space,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
Popconfirm,
|
||||||
|
Empty,
|
||||||
|
Spin,
|
||||||
|
Badge,
|
||||||
|
Collapse,
|
||||||
|
Card,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
ApiOutlined,
|
||||||
|
CloudServerOutlined,
|
||||||
|
FolderOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import PortCreateModal from './PortCreateModal';
|
import PortCreateModal from './PortCreateModal';
|
||||||
import NetworkCardCreateModal from './NetworkCardCreateModal';
|
import NetworkCardCreateModal from './NetworkCardCreateModal';
|
||||||
@@ -10,12 +29,12 @@ const { Panel } = Collapse;
|
|||||||
const designTokens = {
|
const designTokens = {
|
||||||
colors: {
|
colors: {
|
||||||
primary: {
|
primary: {
|
||||||
main: '#667eea'
|
main: '#667eea',
|
||||||
},
|
},
|
||||||
success: '#10b981',
|
success: '#10b981',
|
||||||
error: '#ef4444',
|
error: '#ef4444',
|
||||||
warning: '#f59e0b'
|
warning: '#f59e0b',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
||||||
@@ -34,7 +53,7 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const [cardsResponse, networkCardsResponse] = await Promise.all([
|
const [cardsResponse, networkCardsResponse] = await Promise.all([
|
||||||
axios.get(`/api/network-cards/device/${deviceId}/with-ports`),
|
axios.get(`/api/network-cards/device/${deviceId}/with-ports`),
|
||||||
axios.get(`/api/network-cards/device/${deviceId}`)
|
axios.get(`/api/network-cards/device/${deviceId}`),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const cardsData = cardsResponse.data || [];
|
const cardsData = cardsResponse.data || [];
|
||||||
@@ -58,27 +77,35 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
|||||||
fetchData();
|
fetchData();
|
||||||
}, [fetchData, refreshTrigger]);
|
}, [fetchData, refreshTrigger]);
|
||||||
|
|
||||||
const handleDeleteCard = useCallback(async (card) => {
|
const handleDeleteCard = useCallback(
|
||||||
try {
|
async card => {
|
||||||
await axios.delete(`/api/network-cards/${card.nicId}`);
|
try {
|
||||||
import('antd').then(({ message }) => message.success('网卡删除成功'));
|
await axios.delete(`/api/network-cards/${card.nicId}`);
|
||||||
fetchData();
|
import('antd').then(({ message }) => message.success('网卡删除成功'));
|
||||||
onRefresh?.();
|
fetchData();
|
||||||
} catch (error) {
|
onRefresh?.();
|
||||||
import('antd').then(({ message }) => message.error(error.response?.data?.error || '网卡删除失败'));
|
} catch (error) {
|
||||||
}
|
import('antd').then(({ message }) =>
|
||||||
}, [fetchData, onRefresh]);
|
message.error(error.response?.data?.error || '网卡删除失败')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[fetchData, onRefresh]
|
||||||
|
);
|
||||||
|
|
||||||
const handleDeletePort = useCallback(async (port) => {
|
const handleDeletePort = useCallback(
|
||||||
try {
|
async port => {
|
||||||
await axios.delete(`/api/device-ports/${port.portId}`);
|
try {
|
||||||
import('antd').then(({ message }) => message.success('端口删除成功'));
|
await axios.delete(`/api/device-ports/${port.portId}`);
|
||||||
fetchData();
|
import('antd').then(({ message }) => message.success('端口删除成功'));
|
||||||
onRefresh?.();
|
fetchData();
|
||||||
} catch (error) {
|
onRefresh?.();
|
||||||
import('antd').then(({ message }) => message.error('端口删除失败'));
|
} catch (error) {
|
||||||
}
|
import('antd').then(({ message }) => message.error('端口删除失败'));
|
||||||
}, [fetchData, onRefresh]);
|
}
|
||||||
|
},
|
||||||
|
[fetchData, onRefresh]
|
||||||
|
);
|
||||||
|
|
||||||
const handleCreateCardSuccess = useCallback(() => {
|
const handleCreateCardSuccess = useCallback(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
@@ -90,7 +117,7 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
|||||||
onRefresh?.();
|
onRefresh?.();
|
||||||
}, [fetchData, onRefresh]);
|
}, [fetchData, onRefresh]);
|
||||||
|
|
||||||
const handleExpand = (nicId) => {
|
const handleExpand = nicId => {
|
||||||
setExpandedCards(prev => {
|
setExpandedCards(prev => {
|
||||||
if (prev.includes(nicId)) {
|
if (prev.includes(nicId)) {
|
||||||
return prev.filter(id => id !== nicId);
|
return prev.filter(id => id !== nicId);
|
||||||
@@ -99,27 +126,27 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusTag = (status) => {
|
const getStatusTag = status => {
|
||||||
const config = {
|
const config = {
|
||||||
free: { color: 'success', text: '空闲' },
|
free: { color: 'success', text: '空闲' },
|
||||||
occupied: { color: 'processing', text: '占用' },
|
occupied: { color: 'processing', text: '占用' },
|
||||||
fault: { color: 'error', text: '故障' },
|
fault: { color: 'error', text: '故障' },
|
||||||
normal: { color: 'success', text: '正常' },
|
normal: { color: 'success', text: '正常' },
|
||||||
warning: { color: 'warning', text: '警告' },
|
warning: { color: 'warning', text: '警告' },
|
||||||
offline: { color: 'default', text: '离线' }
|
offline: { color: 'default', text: '离线' },
|
||||||
};
|
};
|
||||||
const { color, text } = config[status] || { color: 'default', text: status };
|
const { color, text } = config[status] || { color: 'default', text: status };
|
||||||
return <Tag color={color}>{text}</Tag>;
|
return <Tag color={color}>{text}</Tag>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTypeTag = (type) => {
|
const getTypeTag = type => {
|
||||||
const config = {
|
const config = {
|
||||||
'RJ45': { color: 'blue', text: 'RJ45' },
|
RJ45: { color: 'blue', text: 'RJ45' },
|
||||||
'SFP': { color: 'green', text: 'SFP' },
|
SFP: { color: 'green', text: 'SFP' },
|
||||||
'SFP+': { color: 'cyan', text: 'SFP+' },
|
'SFP+': { color: 'cyan', text: 'SFP+' },
|
||||||
'SFP28': { color: 'purple', text: 'SFP28' },
|
SFP28: { color: 'purple', text: 'SFP28' },
|
||||||
'QSFP': { color: 'orange', text: 'QSFP' },
|
QSFP: { color: 'orange', text: 'QSFP' },
|
||||||
'QSFP28': { color: 'red', text: 'QSFP28' }
|
QSFP28: { color: 'red', text: 'QSFP28' },
|
||||||
};
|
};
|
||||||
const { color, text } = config[type] || { color: 'default', text: type };
|
const { color, text } = config[type] || { color: 'default', text: type };
|
||||||
return <Tag color={color}>{text}</Tag>;
|
return <Tag color={color}>{text}</Tag>;
|
||||||
@@ -132,34 +159,34 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
|||||||
dataIndex: 'portName',
|
dataIndex: 'portName',
|
||||||
key: 'portName',
|
key: 'portName',
|
||||||
width: 120,
|
width: 120,
|
||||||
render: (text) => <span style={{ fontWeight: 500 }}>{text}</span>
|
render: text => <span style={{ fontWeight: 500 }}>{text}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '类型',
|
title: '类型',
|
||||||
dataIndex: 'portType',
|
dataIndex: 'portType',
|
||||||
key: 'portType',
|
key: 'portType',
|
||||||
width: 80,
|
width: 80,
|
||||||
render: (type) => getTypeTag(type)
|
render: type => getTypeTag(type),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '速率',
|
title: '速率',
|
||||||
dataIndex: 'portSpeed',
|
dataIndex: 'portSpeed',
|
||||||
key: 'portSpeed',
|
key: 'portSpeed',
|
||||||
width: 70
|
width: 70,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
key: 'status',
|
key: 'status',
|
||||||
width: 70,
|
width: 70,
|
||||||
render: (status) => getStatusTag(status)
|
render: status => getStatusTag(status),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'VLAN',
|
title: 'VLAN',
|
||||||
dataIndex: 'vlanId',
|
dataIndex: 'vlanId',
|
||||||
key: 'vlanId',
|
key: 'vlanId',
|
||||||
width: 60,
|
width: 60,
|
||||||
render: (vlanId) => vlanId || '-'
|
render: vlanId => vlanId || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
@@ -178,8 +205,8 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Space>
|
</Space>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -194,30 +221,41 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderCardHeader = (card) => {
|
const renderCardHeader = card => {
|
||||||
const stats = card.stats || { free: 0, occupied: 0, fault: 0, total: 0 };
|
const stats = card.stats || { free: 0, occupied: 0, fault: 0, total: 0 };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
<div style={{
|
<div
|
||||||
width: '36px',
|
style={{
|
||||||
height: '36px',
|
width: '36px',
|
||||||
borderRadius: '8px',
|
height: '36px',
|
||||||
background: card.isUngrouped
|
borderRadius: '8px',
|
||||||
? 'linear-gradient(135deg, #94a3b8 0%, #64748b 100%)'
|
background: card.isUngrouped
|
||||||
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
? 'linear-gradient(135deg, #94a3b8 0%, #64748b 100%)'
|
||||||
display: 'flex',
|
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
alignItems: 'center',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
color: '#fff'
|
justifyContent: 'center',
|
||||||
}}>
|
color: '#fff',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{card.isUngrouped ? <FolderOutlined /> : <CloudServerOutlined />}
|
{card.isUngrouped ? <FolderOutlined /> : <CloudServerOutlined />}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontWeight: 600, fontSize: '14px', color: '#1e293b' }}>
|
<div style={{ fontWeight: 600, fontSize: '14px', color: '#1e293b' }}>
|
||||||
{card.name}
|
{card.name}
|
||||||
{card.slotNumber && <span style={{ color: '#94a3b8', marginLeft: 8 }}>插槽 {card.slotNumber}</span>}
|
{card.slotNumber && (
|
||||||
|
<span style={{ color: '#94a3b8', marginLeft: 8 }}>插槽 {card.slotNumber}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '12px', color: '#64748b' }}>
|
<div style={{ fontSize: '12px', color: '#64748b' }}>
|
||||||
{card.description || (card.isUngrouped ? '未分配到网卡的端口' : '网卡')}
|
{card.description || (card.isUngrouped ? '未分配到网卡的端口' : '网卡')}
|
||||||
@@ -248,7 +286,7 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
|||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={(e) => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setSelectedCard(card);
|
setSelectedCard(card);
|
||||||
setCreatePortModalVisible(true);
|
setCreatePortModalVisible(true);
|
||||||
@@ -270,26 +308,35 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalStats = cards.reduce((acc, card) => {
|
const totalStats = cards.reduce(
|
||||||
const stats = card.stats || {};
|
(acc, card) => {
|
||||||
acc.total += stats.total || 0;
|
const stats = card.stats || {};
|
||||||
acc.free += stats.free || 0;
|
acc.total += stats.total || 0;
|
||||||
acc.occupied += stats.occupied || 0;
|
acc.free += stats.free || 0;
|
||||||
acc.fault += stats.fault || 0;
|
acc.occupied += stats.occupied || 0;
|
||||||
return acc;
|
acc.fault += stats.fault || 0;
|
||||||
}, { total: 0, free: 0, occupied: 0, fault: 0 });
|
return acc;
|
||||||
|
},
|
||||||
|
{ total: 0, free: 0, occupied: 0, fault: 0 }
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="network-card-panel">
|
<div className="network-card-panel">
|
||||||
<div className="panel-header" style={{
|
<div
|
||||||
display: 'flex',
|
className="panel-header"
|
||||||
justifyContent: 'space-between',
|
style={{
|
||||||
alignItems: 'center',
|
display: 'flex',
|
||||||
marginBottom: '16px'
|
justifyContent: 'space-between',
|
||||||
}}>
|
alignItems: 'center',
|
||||||
|
marginBottom: '16px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="stats" style={{ display: 'flex', gap: '24px' }}>
|
<div className="stats" style={{ display: 'flex', gap: '24px' }}>
|
||||||
<Space size={16}>
|
<Space size={16}>
|
||||||
<Badge count={networkCards.length} style={{ backgroundColor: designTokens.colors.primary.main }} />
|
<Badge
|
||||||
|
count={networkCards.length}
|
||||||
|
style={{ backgroundColor: designTokens.colors.primary.main }}
|
||||||
|
/>
|
||||||
<span style={{ color: '#64748b', fontSize: '13px' }}>个网卡</span>
|
<span style={{ color: '#64748b', fontSize: '13px' }}>个网卡</span>
|
||||||
<Badge count={totalStats.total} style={{ backgroundColor: '#667eea' }} />
|
<Badge count={totalStats.total} style={{ backgroundColor: '#667eea' }} />
|
||||||
<span style={{ color: '#64748b', fontSize: '13px' }}>个端口</span>
|
<span style={{ color: '#64748b', fontSize: '13px' }}>个端口</span>
|
||||||
@@ -334,11 +381,11 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
|||||||
) : (
|
) : (
|
||||||
<Collapse
|
<Collapse
|
||||||
activeKey={expandedCards}
|
activeKey={expandedCards}
|
||||||
onChange={(keys) => setExpandedCards(keys)}
|
onChange={keys => setExpandedCards(keys)}
|
||||||
expandIconPosition="end"
|
expandIconPosition="end"
|
||||||
style={{ background: 'transparent' }}
|
style={{ background: 'transparent' }}
|
||||||
>
|
>
|
||||||
{cards.map((card) => (
|
{cards.map(card => (
|
||||||
<Panel
|
<Panel
|
||||||
key={card.nicId}
|
key={card.nicId}
|
||||||
header={renderCardHeader(card)}
|
header={renderCardHeader(card)}
|
||||||
@@ -346,7 +393,7 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
|||||||
background: '#fff',
|
background: '#fff',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
marginBottom: '8px',
|
marginBottom: '8px',
|
||||||
border: '1px solid #e2e8f0'
|
border: '1px solid #e2e8f0',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{card.ports && card.ports.length > 0 ? (
|
{card.ports && card.ports.length > 0 ? (
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||||
import { Modal, Form, Input, Select, InputNumber, message, Space, Button, Tooltip, Alert, Tag } from 'antd';
|
import {
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
Space,
|
||||||
|
Button,
|
||||||
|
Tooltip,
|
||||||
|
Alert,
|
||||||
|
Tag,
|
||||||
|
} from 'antd';
|
||||||
import { PlusOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
import { PlusOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
@@ -10,12 +22,12 @@ const designTokens = {
|
|||||||
colors: {
|
colors: {
|
||||||
primary: {
|
primary: {
|
||||||
main: '#667eea',
|
main: '#667eea',
|
||||||
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
medium: '10px'
|
medium: '10px',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function parsePortRange(portName) {
|
function parsePortRange(portName) {
|
||||||
@@ -64,7 +76,7 @@ function parsePortRange(portName) {
|
|||||||
startNum,
|
startNum,
|
||||||
endNum,
|
endNum,
|
||||||
portCount,
|
portCount,
|
||||||
ports
|
ports,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +128,7 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePortNameChange = useCallback((e) => {
|
const handlePortNameChange = useCallback(e => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
const ports = generatePortNames(value);
|
const ports = generatePortNames(value);
|
||||||
|
|
||||||
@@ -145,7 +157,7 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
|||||||
portSpeed: values.portSpeed,
|
portSpeed: values.portSpeed,
|
||||||
vlanId: values.vlanId,
|
vlanId: values.vlanId,
|
||||||
status: values.status,
|
status: values.status,
|
||||||
description: values.description
|
description: values.description,
|
||||||
});
|
});
|
||||||
message.success('端口创建成功');
|
message.success('端口创建成功');
|
||||||
} else {
|
} else {
|
||||||
@@ -158,7 +170,7 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
|||||||
portSpeed: values.portSpeed,
|
portSpeed: values.portSpeed,
|
||||||
vlanId: values.vlanId,
|
vlanId: values.vlanId,
|
||||||
status: values.status,
|
status: values.status,
|
||||||
description: values.description
|
description: values.description,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await axios.post('/api/device-ports/batch', { ports: portsData });
|
await axios.post('/api/device-ports/batch', { ports: portsData });
|
||||||
@@ -196,9 +208,7 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
|||||||
<Space>
|
<Space>
|
||||||
<PlusOutlined style={{ color: designTokens.colors.primary.main }} />
|
<PlusOutlined style={{ color: designTokens.colors.primary.main }} />
|
||||||
<span>新增端口 - {device?.name || '设备'}</span>
|
<span>新增端口 - {device?.name || '设备'}</span>
|
||||||
{portCount > 1 && (
|
{portCount > 1 && <Tag color="blue">{portCount} 个端口</Tag>}
|
||||||
<Tag color="blue">{portCount} 个端口</Tag>
|
|
||||||
)}
|
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
open={visible}
|
open={visible}
|
||||||
@@ -217,18 +227,11 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
|||||||
initialValues={{
|
initialValues={{
|
||||||
portType: 'RJ45',
|
portType: 'RJ45',
|
||||||
portSpeed: '1G',
|
portSpeed: '1G',
|
||||||
status: 'free'
|
status: 'free',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Form.Item name="deviceId" label="设备">
|
||||||
name="deviceId"
|
<Input value={device?.name} disabled placeholder={device?.deviceId} />
|
||||||
label="设备"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={device?.name}
|
|
||||||
disabled
|
|
||||||
placeholder={device?.deviceId}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -263,7 +266,10 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
|||||||
label={
|
label={
|
||||||
<Space>
|
<Space>
|
||||||
端口名称
|
端口名称
|
||||||
<Tooltip title="支持单个端口(如 eth0/1)或端口范围(如 1/0/1-1/0/48)" mouseEnterDelay={0.5}>
|
<Tooltip
|
||||||
|
title="支持单个端口(如 eth0/1)或端口范围(如 1/0/1-1/0/48)"
|
||||||
|
mouseEnterDelay={0.5}
|
||||||
|
>
|
||||||
<InfoCircleOutlined style={{ color: '#999' }} />
|
<InfoCircleOutlined style={{ color: '#999' }} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -272,7 +278,7 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
|||||||
{ required: true, message: '请输入端口名称' },
|
{ required: true, message: '请输入端口名称' },
|
||||||
{
|
{
|
||||||
pattern: /^[\w\/:\-]+$/,
|
pattern: /^[\w\/:\-]+$/,
|
||||||
message: '端口名称格式不正确'
|
message: '端口名称格式不正确',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
validator: (_, value) => {
|
validator: (_, value) => {
|
||||||
@@ -282,13 +288,13 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
|||||||
return Promise.reject(new Error('单次最多创建1000个端口'));
|
return Promise.reject(new Error('单次最多创建1000个端口'));
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
placeholder="例如: eth0/1 或 1/0/1-1/0/48"
|
placeholder="例如: eth0/1 或 1/0/1-1/0/48"
|
||||||
onChange={(e) => {
|
onChange={e => {
|
||||||
// 确保 Form 值更新
|
// 确保 Form 值更新
|
||||||
form.setFieldValue('portName', e.target.value);
|
form.setFieldValue('portName', e.target.value);
|
||||||
handlePortNameChange(e);
|
handlePortNameChange(e);
|
||||||
@@ -303,9 +309,12 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
|||||||
<div style={{ marginTop: 8 }}>
|
<div style={{ marginTop: 8 }}>
|
||||||
<Space wrap size={4}>
|
<Space wrap size={4}>
|
||||||
{previewPorts.map((port, index) => (
|
{previewPorts.map((port, index) => (
|
||||||
<Tag key={index} color="blue">{port}</Tag>
|
<Tag key={index} color="blue">
|
||||||
|
{port}
|
||||||
|
</Tag>
|
||||||
))}
|
))}
|
||||||
{previewPorts.length < parsePortRange(form.getFieldValue('portName'))?.portCount && (
|
{previewPorts.length <
|
||||||
|
parsePortRange(form.getFieldValue('portName'))?.portCount && (
|
||||||
<Tag color="default">...等</Tag>
|
<Tag color="default">...等</Tag>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -352,17 +361,8 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
|||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<Space style={{ display: 'flex', width: '100%' }}>
|
<Space style={{ display: 'flex', width: '100%' }}>
|
||||||
<Form.Item
|
<Form.Item name="vlanId" label="VLAN ID" style={{ flex: 1 }}>
|
||||||
name="vlanId"
|
<InputNumber placeholder="1-4094" min={1} max={4094} style={{ width: '100%' }} />
|
||||||
label="VLAN ID"
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
>
|
|
||||||
<InputNumber
|
|
||||||
placeholder="1-4094"
|
|
||||||
min={1}
|
|
||||||
max={4094}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -379,25 +379,30 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item name="description" label="描述">
|
||||||
name="description"
|
|
||||||
label="描述"
|
|
||||||
>
|
|
||||||
<TextArea rows={2} placeholder="请输入描述信息(可选)" />
|
<TextArea rows={2} placeholder="请输入描述信息(可选)" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<div style={{
|
<div
|
||||||
background: '#f5f5f5',
|
style={{
|
||||||
padding: '12px 16px',
|
background: '#f5f5f5',
|
||||||
borderRadius: '8px',
|
padding: '12px 16px',
|
||||||
fontSize: '12px',
|
borderRadius: '8px',
|
||||||
color: '#666'
|
fontSize: '12px',
|
||||||
}}>
|
color: '#666',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<strong>格式说明:</strong>
|
<strong>格式说明:</strong>
|
||||||
<ul style={{ margin: '8px 0 0 0', paddingLeft: '20px' }}>
|
<ul style={{ margin: '8px 0 0 0', paddingLeft: '20px' }}>
|
||||||
<li>单个端口:<code>eth0/1</code>、<code>gigabitethernet1/0/1</code></li>
|
<li>
|
||||||
<li>端口范围:<code>1/0/1-1/0/48</code>(创建 1/0/1 到 1/0/48 共48个端口)</li>
|
单个端口:<code>eth0/1</code>、<code>gigabitethernet1/0/1</code>
|
||||||
<li>简单范围:<code>eth1-eth24</code>(创建 eth1 到 eth24 共24个端口)</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
端口范围:<code>1/0/1-1/0/48</code>(创建 1/0/1 到 1/0/48 共48个端口)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
简单范围:<code>eth1-eth24</code>(创建 eth1 到 eth24 共24个端口)
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { Table, Button, Space, Tag, Tooltip, Popconfirm, Empty, Spin, Badge } from 'antd';
|
import { Table, Button, Space, Tag, Tooltip, Popconfirm, Empty, Spin, Badge } from 'antd';
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined, ApiOutlined } from '@ant-design/icons';
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
ApiOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import PortCreateModal from './PortCreateModal';
|
import PortCreateModal from './PortCreateModal';
|
||||||
|
|
||||||
const designTokens = {
|
const designTokens = {
|
||||||
colors: {
|
colors: {
|
||||||
primary: {
|
primary: {
|
||||||
main: '#667eea'
|
main: '#667eea',
|
||||||
},
|
},
|
||||||
success: '#10b981',
|
success: '#10b981',
|
||||||
error: '#ef4444',
|
error: '#ef4444',
|
||||||
warning: '#f59e0b'
|
warning: '#f59e0b',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function PortManagementPanel({ deviceId, deviceName, onRefresh }) {
|
function PortManagementPanel({ deviceId, deviceName, onRefresh }) {
|
||||||
@@ -38,40 +44,43 @@ function PortManagementPanel({ deviceId, deviceName, onRefresh }) {
|
|||||||
fetchPorts();
|
fetchPorts();
|
||||||
}, [fetchPorts]);
|
}, [fetchPorts]);
|
||||||
|
|
||||||
const handleDelete = useCallback(async (port) => {
|
const handleDelete = useCallback(
|
||||||
try {
|
async port => {
|
||||||
await axios.delete(`/api/device-ports/${port.portId}`);
|
try {
|
||||||
import('antd').then(({ message }) => message.success('端口删除成功'));
|
await axios.delete(`/api/device-ports/${port.portId}`);
|
||||||
fetchPorts();
|
import('antd').then(({ message }) => message.success('端口删除成功'));
|
||||||
onRefresh?.();
|
fetchPorts();
|
||||||
} catch (error) {
|
onRefresh?.();
|
||||||
import('antd').then(({ message }) => message.error('端口删除失败'));
|
} catch (error) {
|
||||||
}
|
import('antd').then(({ message }) => message.error('端口删除失败'));
|
||||||
}, [fetchPorts, onRefresh]);
|
}
|
||||||
|
},
|
||||||
|
[fetchPorts, onRefresh]
|
||||||
|
);
|
||||||
|
|
||||||
const handleCreateSuccess = useCallback(() => {
|
const handleCreateSuccess = useCallback(() => {
|
||||||
fetchPorts();
|
fetchPorts();
|
||||||
onRefresh?.();
|
onRefresh?.();
|
||||||
}, [fetchPorts, onRefresh]);
|
}, [fetchPorts, onRefresh]);
|
||||||
|
|
||||||
const getStatusTag = (status) => {
|
const getStatusTag = status => {
|
||||||
const config = {
|
const config = {
|
||||||
free: { color: 'success', text: '空闲' },
|
free: { color: 'success', text: '空闲' },
|
||||||
occupied: { color: 'processing', text: '占用' },
|
occupied: { color: 'processing', text: '占用' },
|
||||||
fault: { color: 'error', text: '故障' }
|
fault: { color: 'error', text: '故障' },
|
||||||
};
|
};
|
||||||
const { color, text } = config[status] || { color: 'default', text: status };
|
const { color, text } = config[status] || { color: 'default', text: status };
|
||||||
return <Tag color={color}>{text}</Tag>;
|
return <Tag color={color}>{text}</Tag>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTypeTag = (type) => {
|
const getTypeTag = type => {
|
||||||
const config = {
|
const config = {
|
||||||
'RJ45': { color: 'blue', text: 'RJ45' },
|
RJ45: { color: 'blue', text: 'RJ45' },
|
||||||
'SFP': { color: 'green', text: 'SFP' },
|
SFP: { color: 'green', text: 'SFP' },
|
||||||
'SFP+': { color: 'cyan', text: 'SFP+' },
|
'SFP+': { color: 'cyan', text: 'SFP+' },
|
||||||
'SFP28': { color: 'purple', text: 'SFP28' },
|
SFP28: { color: 'purple', text: 'SFP28' },
|
||||||
'QSFP': { color: 'orange', text: 'QSFP' },
|
QSFP: { color: 'orange', text: 'QSFP' },
|
||||||
'QSFP28': { color: 'red', text: 'QSFP28' }
|
QSFP28: { color: 'red', text: 'QSFP28' },
|
||||||
};
|
};
|
||||||
const { color, text } = config[type] || { color: 'default', text: type };
|
const { color, text } = config[type] || { color: 'default', text: type };
|
||||||
return <Tag color={color}>{text}</Tag>;
|
return <Tag color={color}>{text}</Tag>;
|
||||||
@@ -83,38 +92,38 @@ function PortManagementPanel({ deviceId, deviceName, onRefresh }) {
|
|||||||
dataIndex: 'portName',
|
dataIndex: 'portName',
|
||||||
key: 'portName',
|
key: 'portName',
|
||||||
width: 120,
|
width: 120,
|
||||||
render: (text) => (
|
render: text => (
|
||||||
<Tooltip title={text}>
|
<Tooltip title={text}>
|
||||||
<span style={{ fontWeight: 500 }}>{text}</span>
|
<span style={{ fontWeight: 500 }}>{text}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '类型',
|
title: '类型',
|
||||||
dataIndex: 'portType',
|
dataIndex: 'portType',
|
||||||
key: 'portType',
|
key: 'portType',
|
||||||
width: 90,
|
width: 90,
|
||||||
render: (type) => getTypeTag(type)
|
render: type => getTypeTag(type),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '速率',
|
title: '速率',
|
||||||
dataIndex: 'portSpeed',
|
dataIndex: 'portSpeed',
|
||||||
key: 'portSpeed',
|
key: 'portSpeed',
|
||||||
width: 80
|
width: 80,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
key: 'status',
|
key: 'status',
|
||||||
width: 80,
|
width: 80,
|
||||||
render: (status) => getStatusTag(status)
|
render: status => getStatusTag(status),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'VLAN',
|
title: 'VLAN',
|
||||||
dataIndex: 'vlanId',
|
dataIndex: 'vlanId',
|
||||||
key: 'vlanId',
|
key: 'vlanId',
|
||||||
width: 70,
|
width: 70,
|
||||||
render: (vlanId) => vlanId || '-'
|
render: vlanId => vlanId || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
@@ -129,18 +138,13 @@ function PortManagementPanel({ deviceId, deviceName, onRefresh }) {
|
|||||||
okText="确定"
|
okText="确定"
|
||||||
cancelText="取消"
|
cancelText="取消"
|
||||||
>
|
>
|
||||||
<Button
|
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
danger
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
>
|
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Space>
|
</Space>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const freeCount = ports.filter(p => p.status === 'free').length;
|
const freeCount = ports.filter(p => p.status === 'free').length;
|
||||||
@@ -169,11 +173,7 @@ function PortManagementPanel({ deviceId, deviceName, onRefresh }) {
|
|||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button icon={<ReloadOutlined />} onClick={fetchPorts} size="small">
|
||||||
icon={<ReloadOutlined />}
|
|
||||||
onClick={fetchPorts}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -182,7 +182,7 @@ function PortManagementPanel({ deviceId, deviceName, onRefresh }) {
|
|||||||
onClick={() => setCreateModalVisible(true)}
|
onClick={() => setCreateModalVisible(true)}
|
||||||
style={{
|
style={{
|
||||||
background: designTokens.colors.primary.gradient,
|
background: designTokens.colors.primary.gradient,
|
||||||
border: 'none'
|
border: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
新增端口
|
新增端口
|
||||||
|
|||||||
@@ -2,14 +2,22 @@ import React, { useState } from 'react';
|
|||||||
import { Tooltip, Badge, Divider, Pagination } from 'antd';
|
import { Tooltip, Badge, Divider, Pagination } from 'antd';
|
||||||
import { LinkOutlined, SwapRightOutlined, AimOutlined, NodeIndexOutlined } from '@ant-design/icons';
|
import { LinkOutlined, SwapRightOutlined, AimOutlined, NodeIndexOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onPortClick, compact = false }) => {
|
const PortPanel = ({
|
||||||
|
ports,
|
||||||
|
deviceName,
|
||||||
|
deviceId,
|
||||||
|
cables = [],
|
||||||
|
devices = [],
|
||||||
|
onPortClick,
|
||||||
|
compact = false,
|
||||||
|
}) => {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(48); // 默认每页48个端口
|
const [pageSize, setPageSize] = useState(48); // 默认每页48个端口
|
||||||
|
|
||||||
// 按端口名称排序(升序)
|
// 按端口名称排序(升序)
|
||||||
const sortedPorts = [...ports].sort((a, b) => {
|
const sortedPorts = [...ports].sort((a, b) => {
|
||||||
// 尝试按数字部分排序,支持格式如:1/0/1, eth0/1, GigabitEthernet1/0/1 等
|
// 尝试按数字部分排序,支持格式如:1/0/1, eth0/1, GigabitEthernet1/0/1 等
|
||||||
const extractNumbers = (str) => {
|
const extractNumbers = str => {
|
||||||
const matches = str.match(/\d+/g);
|
const matches = str.match(/\d+/g);
|
||||||
return matches ? matches.map(Number) : [];
|
return matches ? matches.map(Number) : [];
|
||||||
};
|
};
|
||||||
@@ -35,7 +43,7 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
|||||||
const paginatedPorts = sortedPorts.slice(startIndex, endIndex);
|
const paginatedPorts = sortedPorts.slice(startIndex, endIndex);
|
||||||
|
|
||||||
// 获取端口状态颜色
|
// 获取端口状态颜色
|
||||||
const getPortStatusColor = (status) => {
|
const getPortStatusColor = status => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'free':
|
case 'free':
|
||||||
return '#6b7280'; // 灰色 - 空闲
|
return '#6b7280'; // 灰色 - 空闲
|
||||||
@@ -51,7 +59,7 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 获取端口状态文本
|
// 获取端口状态文本
|
||||||
const getPortStatusText = (status) => {
|
const getPortStatusText = status => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'free':
|
case 'free':
|
||||||
return '空闲';
|
return '空闲';
|
||||||
@@ -67,7 +75,7 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 获取端口类型图标 - 使用更真实的端口符号
|
// 获取端口类型图标 - 使用更真实的端口符号
|
||||||
const getPortTypeIcon = (portType) => {
|
const getPortTypeIcon = portType => {
|
||||||
switch (portType) {
|
switch (portType) {
|
||||||
case 'RJ45':
|
case 'RJ45':
|
||||||
return '⬡'; // 六边形表示网口
|
return '⬡'; // 六边形表示网口
|
||||||
@@ -84,7 +92,7 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 获取简化端口显示名称(只显示数字)
|
// 获取简化端口显示名称(只显示数字)
|
||||||
const getPortDisplayName = (portName) => {
|
const getPortDisplayName = portName => {
|
||||||
// 提取最后的数字
|
// 提取最后的数字
|
||||||
const match = portName.match(/(\d+)$/);
|
const match = portName.match(/(\d+)$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
@@ -95,36 +103,37 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 获取线缆类型文本
|
// 获取线缆类型文本
|
||||||
const getCableTypeText = (cableType) => {
|
const getCableTypeText = cableType => {
|
||||||
const typeMap = {
|
const typeMap = {
|
||||||
'ethernet': '网线',
|
ethernet: '网线',
|
||||||
'fiber': '光纤',
|
fiber: '光纤',
|
||||||
'copper': '铜缆',
|
copper: '铜缆',
|
||||||
'power': '电源线'
|
power: '电源线',
|
||||||
};
|
};
|
||||||
return typeMap[cableType] || cableType || '未知';
|
return typeMap[cableType] || cableType || '未知';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取线缆类型颜色
|
// 获取线缆类型颜色
|
||||||
const getCableTypeColor = (cableType) => {
|
const getCableTypeColor = cableType => {
|
||||||
const colorMap = {
|
const colorMap = {
|
||||||
'ethernet': '#52c41a',
|
ethernet: '#52c41a',
|
||||||
'fiber': '#1890ff',
|
fiber: '#1890ff',
|
||||||
'copper': '#faad14',
|
copper: '#faad14',
|
||||||
'power': '#ff4d4f'
|
power: '#ff4d4f',
|
||||||
};
|
};
|
||||||
return colorMap[cableType] || '#999';
|
return colorMap[cableType] || '#999';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 查找端口关联的接线
|
// 查找端口关联的接线
|
||||||
const findPortCable = (port) => {
|
const findPortCable = port => {
|
||||||
if (!cables || cables.length === 0) return null;
|
if (!cables || cables.length === 0) return null;
|
||||||
|
|
||||||
return cables.find(cable =>
|
return cables.find(
|
||||||
(cable.sourceDeviceId === deviceId && cable.sourcePortId === port.portId) ||
|
cable =>
|
||||||
(cable.targetDeviceId === deviceId && cable.targetPortId === port.portId) ||
|
(cable.sourceDeviceId === deviceId && cable.sourcePortId === port.portId) ||
|
||||||
(cable.sourceDeviceId === deviceId && cable.sourcePort === port.portName) ||
|
(cable.targetDeviceId === deviceId && cable.targetPortId === port.portId) ||
|
||||||
(cable.targetDeviceId === deviceId && cable.targetPort === port.portName)
|
(cable.sourceDeviceId === deviceId && cable.sourcePort === port.portName) ||
|
||||||
|
(cable.targetDeviceId === deviceId && cable.targetPort === port.portName)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -132,9 +141,10 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
|||||||
const getPeerInfo = (cable, currentPort) => {
|
const getPeerInfo = (cable, currentPort) => {
|
||||||
if (!cable) return null;
|
if (!cable) return null;
|
||||||
|
|
||||||
const isSource = cable.sourceDeviceId === deviceId ||
|
const isSource =
|
||||||
(cable.sourcePortId && cable.sourcePortId === currentPort.portId) ||
|
cable.sourceDeviceId === deviceId ||
|
||||||
cable.sourcePort === currentPort.portName;
|
(cable.sourcePortId && cable.sourcePortId === currentPort.portId) ||
|
||||||
|
cable.sourcePort === currentPort.portName;
|
||||||
|
|
||||||
if (isSource) {
|
if (isSource) {
|
||||||
// 当前是源端,返回目标端信息
|
// 当前是源端,返回目标端信息
|
||||||
@@ -143,7 +153,7 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
|||||||
deviceName: targetDevice?.name || cable.targetDeviceId,
|
deviceName: targetDevice?.name || cable.targetDeviceId,
|
||||||
deviceId: cable.targetDeviceId,
|
deviceId: cable.targetDeviceId,
|
||||||
portName: cable.targetPort || cable.targetPortId,
|
portName: cable.targetPort || cable.targetPortId,
|
||||||
direction: 'out'
|
direction: 'out',
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// 当前是目标端,返回源端信息
|
// 当前是目标端,返回源端信息
|
||||||
@@ -152,37 +162,60 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
|||||||
deviceName: sourceDevice?.name || cable.sourceDeviceId,
|
deviceName: sourceDevice?.name || cable.sourceDeviceId,
|
||||||
deviceId: cable.sourceDeviceId,
|
deviceId: cable.sourceDeviceId,
|
||||||
portName: cable.sourcePort || cable.sourcePortId,
|
portName: cable.sourcePort || cable.sourcePortId,
|
||||||
direction: 'in'
|
direction: 'in',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染端口详情提示
|
// 渲染端口详情提示
|
||||||
const renderPortTooltip = (port) => {
|
const renderPortTooltip = port => {
|
||||||
const cable = findPortCable(port);
|
const cable = findPortCable(port);
|
||||||
const peerInfo = cable ? getPeerInfo(cable, port) : null;
|
const peerInfo = cable ? getPeerInfo(cable, port) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '8px 4px', minWidth: '220px' }}>
|
<div style={{ padding: '8px 4px', minWidth: '220px' }}>
|
||||||
{/* 端口基本信息 */}
|
{/* 端口基本信息 */}
|
||||||
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8, borderBottom: '1px solid rgba(255,255,255,0.2)', paddingBottom: 4 }}>
|
<div
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: 14,
|
||||||
|
marginBottom: 8,
|
||||||
|
borderBottom: '1px solid rgba(255,255,255,0.2)',
|
||||||
|
paddingBottom: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<NodeIndexOutlined style={{ marginRight: 6 }} />
|
<NodeIndexOutlined style={{ marginRight: 6 }} />
|
||||||
{port.portName}
|
{port.portName}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 12, lineHeight: '1.8' }}>
|
<div style={{ fontSize: 12, lineHeight: '1.8' }}>
|
||||||
<div><span style={{ opacity: 0.7 }}>端口类型:</span> {port.portType}</div>
|
<div>
|
||||||
<div><span style={{ opacity: 0.7 }}>端口速率:</span> {port.portSpeed}</div>
|
<span style={{ opacity: 0.7 }}>端口类型:</span> {port.portType}
|
||||||
<div><span style={{ opacity: 0.7 }}>状态:</span>
|
</div>
|
||||||
<span style={{
|
<div>
|
||||||
color: getPortStatusColor(port.status),
|
<span style={{ opacity: 0.7 }}>端口速率:</span> {port.portSpeed}
|
||||||
marginLeft: 4,
|
</div>
|
||||||
fontWeight: 500
|
<div>
|
||||||
}}>
|
<span style={{ opacity: 0.7 }}>状态:</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: getPortStatusColor(port.status),
|
||||||
|
marginLeft: 4,
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{getPortStatusText(port.status)}
|
{getPortStatusText(port.status)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{port.vlanId && <div><span style={{ opacity: 0.7 }}>VLAN:</span> {port.vlanId}</div>}
|
{port.vlanId && (
|
||||||
{port.description && <div><span style={{ opacity: 0.7 }}>描述:</span> {port.description}</div>}
|
<div>
|
||||||
|
<span style={{ opacity: 0.7 }}>VLAN:</span> {port.vlanId}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{port.description && (
|
||||||
|
<div>
|
||||||
|
<span style={{ opacity: 0.7 }}>描述:</span> {port.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 接线信息 */}
|
{/* 接线信息 */}
|
||||||
@@ -196,45 +229,49 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
|||||||
<div style={{ fontSize: 12, lineHeight: '1.8' }}>
|
<div style={{ fontSize: 12, lineHeight: '1.8' }}>
|
||||||
{/* 线缆类型和长度 */}
|
{/* 线缆类型和长度 */}
|
||||||
<div style={{ marginBottom: 6 }}>
|
<div style={{ marginBottom: 6 }}>
|
||||||
<span style={{
|
<span
|
||||||
display: 'inline-block',
|
style={{
|
||||||
padding: '2px 8px',
|
display: 'inline-block',
|
||||||
borderRadius: '4px',
|
padding: '2px 8px',
|
||||||
background: getCableTypeColor(cable.cableType) + '20',
|
borderRadius: '4px',
|
||||||
color: getCableTypeColor(cable.cableType),
|
background: getCableTypeColor(cable.cableType) + '20',
|
||||||
fontSize: '11px',
|
color: getCableTypeColor(cable.cableType),
|
||||||
fontWeight: 500
|
fontSize: '11px',
|
||||||
}}>
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{getCableTypeText(cable.cableType)}
|
{getCableTypeText(cable.cableType)}
|
||||||
</span>
|
</span>
|
||||||
{cable.cableLength && (
|
{cable.cableLength && (
|
||||||
<span style={{ marginLeft: 8, opacity: 0.8 }}>
|
<span style={{ marginLeft: 8, opacity: 0.8 }}>{cable.cableLength}m</span>
|
||||||
{cable.cableLength}m
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 连接方向 */}
|
{/* 连接方向 */}
|
||||||
<div style={{
|
<div
|
||||||
display: 'flex',
|
style={{
|
||||||
alignItems: 'center',
|
display: 'flex',
|
||||||
gap: '8px',
|
alignItems: 'center',
|
||||||
padding: '8px',
|
gap: '8px',
|
||||||
background: 'rgba(255,255,255,0.05)',
|
padding: '8px',
|
||||||
borderRadius: '6px',
|
background: 'rgba(255,255,255,0.05)',
|
||||||
marginTop: '8px'
|
borderRadius: '6px',
|
||||||
}}>
|
marginTop: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<div style={{
|
<div
|
||||||
width: '32px',
|
style={{
|
||||||
height: '32px',
|
width: '32px',
|
||||||
borderRadius: '50%',
|
height: '32px',
|
||||||
background: peerInfo.direction === 'out' ? '#52c41a20' : '#1890ff20',
|
borderRadius: '50%',
|
||||||
display: 'flex',
|
background: peerInfo.direction === 'out' ? '#52c41a20' : '#1890ff20',
|
||||||
alignItems: 'center',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
fontSize: '14px'
|
justifyContent: 'center',
|
||||||
}}>
|
fontSize: '14px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{peerInfo.direction === 'out' ? '📤' : '📥'}
|
{peerInfo.direction === 'out' ? '📤' : '📥'}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '10px', marginTop: '2px', opacity: 0.6 }}>
|
<div style={{ fontSize: '10px', marginTop: '2px', opacity: 0.6 }}>
|
||||||
@@ -243,15 +280,9 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<div style={{ fontWeight: 500, color: '#fff' }}>
|
<div style={{ fontWeight: 500, color: '#fff' }}>{peerInfo.deviceName}</div>
|
||||||
{peerInfo.deviceName}
|
<div style={{ fontSize: '11px', opacity: 0.7 }}>端口: {peerInfo.portName}</div>
|
||||||
</div>
|
<div style={{ fontSize: '10px', opacity: 0.5 }}>ID: {peerInfo.deviceId}</div>
|
||||||
<div style={{ fontSize: '11px', opacity: 0.7 }}>
|
|
||||||
端口: {peerInfo.portName}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '10px', opacity: 0.5 }}>
|
|
||||||
ID: {peerInfo.deviceId}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -285,34 +316,40 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div
|
||||||
background: 'linear-gradient(145deg, #1e293b 0%, #0f172a 100%)',
|
style={{
|
||||||
borderRadius: compact ? '12px' : '16px',
|
background: 'linear-gradient(145deg, #1e293b 0%, #0f172a 100%)',
|
||||||
padding: compact ? '16px' : '24px',
|
borderRadius: compact ? '12px' : '16px',
|
||||||
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.1)',
|
padding: compact ? '16px' : '24px',
|
||||||
border: '1px solid rgba(255, 255, 255, 0.1)'
|
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.1)',
|
||||||
}}>
|
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{/* 设备标题 - compact 模式下隐藏 */}
|
{/* 设备标题 - compact 模式下隐藏 */}
|
||||||
{!compact && (
|
{!compact && (
|
||||||
<div style={{
|
<div
|
||||||
display: 'flex',
|
style={{
|
||||||
alignItems: 'center',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
alignItems: 'center',
|
||||||
marginBottom: '20px',
|
justifyContent: 'space-between',
|
||||||
paddingBottom: '16px',
|
marginBottom: '20px',
|
||||||
borderBottom: '1px solid rgba(255, 255, 255, 0.1)'
|
paddingBottom: '16px',
|
||||||
}}>
|
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
<div style={{
|
<div
|
||||||
width: '40px',
|
style={{
|
||||||
height: '40px',
|
width: '40px',
|
||||||
borderRadius: '10px',
|
height: '40px',
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
borderRadius: '10px',
|
||||||
display: 'flex',
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
alignItems: 'center',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
fontSize: '20px'
|
justifyContent: 'center',
|
||||||
}}>
|
fontSize: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
🔌
|
🔌
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -328,33 +365,39 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
|||||||
{/* 状态图例 */}
|
{/* 状态图例 */}
|
||||||
<div style={{ display: 'flex', gap: '16px' }}>
|
<div style={{ display: 'flex', gap: '16px' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||||
<div style={{
|
<div
|
||||||
width: '8px',
|
style={{
|
||||||
height: '8px',
|
width: '8px',
|
||||||
borderRadius: '50%',
|
height: '8px',
|
||||||
background: '#6b7280',
|
borderRadius: '50%',
|
||||||
boxShadow: '0 0 8px #6b7280'
|
background: '#6b7280',
|
||||||
}} />
|
boxShadow: '0 0 8px #6b7280',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<span style={{ fontSize: '12px', color: 'rgba(255,255,255,0.6)' }}>空闲</span>
|
<span style={{ fontSize: '12px', color: 'rgba(255,255,255,0.6)' }}>空闲</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||||
<div style={{
|
<div
|
||||||
width: '8px',
|
style={{
|
||||||
height: '8px',
|
width: '8px',
|
||||||
borderRadius: '50%',
|
height: '8px',
|
||||||
background: '#10b981',
|
borderRadius: '50%',
|
||||||
boxShadow: '0 0 8px #10b981'
|
background: '#10b981',
|
||||||
}} />
|
boxShadow: '0 0 8px #10b981',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<span style={{ fontSize: '12px', color: 'rgba(255,255,255,0.6)' }}>已连接</span>
|
<span style={{ fontSize: '12px', color: 'rgba(255,255,255,0.6)' }}>已连接</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||||
<div style={{
|
<div
|
||||||
width: '8px',
|
style={{
|
||||||
height: '8px',
|
width: '8px',
|
||||||
borderRadius: '50%',
|
height: '8px',
|
||||||
background: '#ef4444',
|
borderRadius: '50%',
|
||||||
boxShadow: '0 0 8px #ef4444'
|
background: '#ef4444',
|
||||||
}} />
|
boxShadow: '0 0 8px #ef4444',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<span style={{ fontSize: '12px', color: 'rgba(255,255,255,0.6)' }}>故障</span>
|
<span style={{ fontSize: '12px', color: 'rgba(255,255,255,0.6)' }}>故障</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -362,16 +405,18 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 端口网格 - 固定每行24个端口 */}
|
{/* 端口网格 - 固定每行24个端口 */}
|
||||||
<div style={{
|
<div
|
||||||
display: 'grid',
|
style={{
|
||||||
gridTemplateColumns: 'repeat(24, 1fr)',
|
display: 'grid',
|
||||||
gap: '8px',
|
gridTemplateColumns: 'repeat(24, 1fr)',
|
||||||
padding: '16px',
|
gap: '8px',
|
||||||
background: 'rgba(0, 0, 0, 0.3)',
|
padding: '16px',
|
||||||
borderRadius: '12px',
|
background: 'rgba(0, 0, 0, 0.3)',
|
||||||
border: '1px solid rgba(255, 255, 255, 0.05)'
|
borderRadius: '12px',
|
||||||
}}>
|
border: '1px solid rgba(255, 255, 255, 0.05)',
|
||||||
{paginatedPorts.map((port) => {
|
}}
|
||||||
|
>
|
||||||
|
{paginatedPorts.map(port => {
|
||||||
const statusColor = getPortStatusColor(port.status);
|
const statusColor = getPortStatusColor(port.status);
|
||||||
const isClickable = onPortClick && port.status !== 'disabled';
|
const isClickable = onPortClick && port.status !== 'disabled';
|
||||||
const cable = findPortCable(port);
|
const cable = findPortCable(port);
|
||||||
@@ -384,7 +429,7 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
|||||||
color="#1e293b"
|
color="#1e293b"
|
||||||
overlayStyle={{
|
overlayStyle={{
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
border: '1px solid rgba(255, 255, 255, 0.1)'
|
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -397,69 +442,79 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
|||||||
cursor: isClickable ? 'pointer' : 'not-allowed',
|
cursor: isClickable ? 'pointer' : 'not-allowed',
|
||||||
transition: 'all 0.2s ease',
|
transition: 'all 0.2s ease',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
minWidth: '0'
|
minWidth: '0',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* LED 指示灯 - 在端口上方 */}
|
{/* LED 指示灯 - 在端口上方 */}
|
||||||
<div style={{
|
<div
|
||||||
width: '6px',
|
style={{
|
||||||
height: '6px',
|
width: '6px',
|
||||||
borderRadius: '50%',
|
height: '6px',
|
||||||
background: statusColor,
|
borderRadius: '50%',
|
||||||
boxShadow: `0 0 6px ${statusColor}, 0 0 12px ${statusColor}50`,
|
background: statusColor,
|
||||||
marginBottom: '4px',
|
boxShadow: `0 0 6px ${statusColor}, 0 0 12px ${statusColor}50`,
|
||||||
animation: port.status === 'fault' ? 'pulse 1.5s infinite' : 'none'
|
marginBottom: '4px',
|
||||||
}} />
|
animation: port.status === 'fault' ? 'pulse 1.5s infinite' : 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 端口主体 - 矩形样式 */}
|
{/* 端口主体 - 矩形样式 */}
|
||||||
<div style={{
|
<div
|
||||||
width: '100%',
|
style={{
|
||||||
aspectRatio: '1 / 1.2',
|
width: '100%',
|
||||||
background: 'linear-gradient(180deg, #2a3441 0%, #1e2530 100%)',
|
aspectRatio: '1 / 1.2',
|
||||||
border: `2px solid ${statusColor}`,
|
background: 'linear-gradient(180deg, #2a3441 0%, #1e2530 100%)',
|
||||||
borderRadius: '2px',
|
border: `2px solid ${statusColor}`,
|
||||||
display: 'flex',
|
borderRadius: '2px',
|
||||||
alignItems: 'center',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
position: 'relative',
|
justifyContent: 'center',
|
||||||
boxShadow: `inset 0 1px 0 rgba(255,255,255,0.1), 0 2px 4px rgba(0,0,0,0.3)`
|
position: 'relative',
|
||||||
}}>
|
boxShadow: `inset 0 1px 0 rgba(255,255,255,0.1), 0 2px 4px rgba(0,0,0,0.3)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{/* 端口内部图标 */}
|
{/* 端口内部图标 */}
|
||||||
<div style={{
|
<div
|
||||||
fontSize: '10px',
|
style={{
|
||||||
color: statusColor,
|
fontSize: '10px',
|
||||||
opacity: 0.8
|
color: statusColor,
|
||||||
}}>
|
opacity: 0.8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{getPortTypeIcon(port.portType)}
|
{getPortTypeIcon(port.portType)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 接线指示标记 */}
|
{/* 接线指示标记 */}
|
||||||
{cable && (
|
{cable && (
|
||||||
<div style={{
|
<div
|
||||||
position: 'absolute',
|
style={{
|
||||||
top: '1px',
|
position: 'absolute',
|
||||||
right: '1px',
|
top: '1px',
|
||||||
width: '4px',
|
right: '1px',
|
||||||
height: '4px',
|
width: '4px',
|
||||||
borderRadius: '50%',
|
height: '4px',
|
||||||
background: getCableTypeColor(cable.cableType),
|
borderRadius: '50%',
|
||||||
boxShadow: `0 0 3px ${getCableTypeColor(cable.cableType)}`
|
background: getCableTypeColor(cable.cableType),
|
||||||
}} />
|
boxShadow: `0 0 3px ${getCableTypeColor(cable.cableType)}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 端口名称 - 在端口下方 */}
|
{/* 端口名称 - 在端口下方 */}
|
||||||
<div style={{
|
<div
|
||||||
fontSize: '9px',
|
style={{
|
||||||
fontWeight: 500,
|
fontSize: '9px',
|
||||||
color: 'rgba(255, 255, 255, 0.7)',
|
fontWeight: 500,
|
||||||
textAlign: 'center',
|
color: 'rgba(255, 255, 255, 0.7)',
|
||||||
marginTop: '3px',
|
textAlign: 'center',
|
||||||
whiteSpace: 'nowrap',
|
marginTop: '3px',
|
||||||
overflow: 'hidden',
|
whiteSpace: 'nowrap',
|
||||||
textOverflow: 'ellipsis',
|
overflow: 'hidden',
|
||||||
maxWidth: '100%'
|
textOverflow: 'ellipsis',
|
||||||
}}>
|
maxWidth: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{getPortDisplayName(port.portName)}
|
{getPortDisplayName(port.portName)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -470,13 +525,15 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
|||||||
|
|
||||||
{/* 分页 */}
|
{/* 分页 */}
|
||||||
{totalPorts > pageSize && (
|
{totalPorts > pageSize && (
|
||||||
<div style={{
|
<div
|
||||||
display: 'flex',
|
style={{
|
||||||
justifyContent: 'center',
|
display: 'flex',
|
||||||
padding: '16px 0 0 0',
|
justifyContent: 'center',
|
||||||
borderTop: '1px solid rgba(255, 255, 255, 0.1)',
|
padding: '16px 0 0 0',
|
||||||
marginTop: '16px'
|
borderTop: '1px solid rgba(255, 255, 255, 0.1)',
|
||||||
}}>
|
marginTop: '16px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Pagination
|
<Pagination
|
||||||
current={currentPage}
|
current={currentPage}
|
||||||
total={totalPorts}
|
total={totalPorts}
|
||||||
@@ -487,11 +544,11 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
|||||||
}}
|
}}
|
||||||
showSizeChanger
|
showSizeChanger
|
||||||
showQuickJumper
|
showQuickJumper
|
||||||
showTotal={(total) => `共 ${total} 个端口`}
|
showTotal={total => `共 ${total} 个端口`}
|
||||||
pageSizeOptions={['24', '48', '96']}
|
pageSizeOptions={['24', '48', '96']}
|
||||||
size="small"
|
size="small"
|
||||||
style={{
|
style={{
|
||||||
color: 'rgba(255, 255, 255, 0.8)'
|
color: 'rgba(255, 255, 255, 0.8)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,14 +9,16 @@ const ProtectedRoute = ({ children, requiredPermission }) => {
|
|||||||
|
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div
|
||||||
display: 'flex',
|
style={{
|
||||||
flexDirection: 'column',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
height: '100vh',
|
alignItems: 'center',
|
||||||
gap: '16px'
|
height: '100vh',
|
||||||
}}>
|
gap: '16px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Spin size="large" />
|
<Spin size="large" />
|
||||||
<span style={{ color: '#8c8c8c', fontSize: '14px' }}>加载中...</span>
|
<span style={{ color: '#8c8c8c', fontSize: '14px' }}>加载中...</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
DesktopOutlined,
|
DesktopOutlined,
|
||||||
UsbOutlined,
|
UsbOutlined,
|
||||||
MonitorOutlined,
|
MonitorOutlined,
|
||||||
SettingOutlined
|
SettingOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import PortPanel from './PortPanel';
|
import PortPanel from './PortPanel';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
@@ -23,8 +23,8 @@ const designTokens = {
|
|||||||
error: '#ef4444',
|
error: '#ef4444',
|
||||||
warning: '#f59e0b',
|
warning: '#f59e0b',
|
||||||
metal: { light: '#9ca3af', DEFAULT: '#6b7280', dark: '#4b5563' },
|
metal: { light: '#9ca3af', DEFAULT: '#6b7280', dark: '#4b5563' },
|
||||||
slot: { empty: '#d1d5db', occupied: '#3b82f6' }
|
slot: { empty: '#d1d5db', occupied: '#3b82f6' },
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,7 +44,7 @@ const ServerBackplanePanel = ({
|
|||||||
cables,
|
cables,
|
||||||
allDevices,
|
allDevices,
|
||||||
onPortClick,
|
onPortClick,
|
||||||
onManageNetworkCards
|
onManageNetworkCards,
|
||||||
}) => {
|
}) => {
|
||||||
const [cards, setCards] = useState([]);
|
const [cards, setCards] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -82,9 +82,20 @@ const ServerBackplanePanel = ({
|
|||||||
const name = (card.name || '').toLowerCase();
|
const name = (card.name || '').toLowerCase();
|
||||||
|
|
||||||
// 判断网卡类型
|
// 判断网卡类型
|
||||||
if (name.includes('idrac') || name.includes('ilo') || name.includes('bmc') || name.includes('mgmt') || name.includes('管理')) {
|
if (
|
||||||
|
name.includes('idrac') ||
|
||||||
|
name.includes('ilo') ||
|
||||||
|
name.includes('bmc') ||
|
||||||
|
name.includes('mgmt') ||
|
||||||
|
name.includes('管理')
|
||||||
|
) {
|
||||||
management.push({ ...card, type: 'management' });
|
management.push({ ...card, type: 'management' });
|
||||||
} else if (slotNum === 0 || name.includes('onboard') || name.includes('板载') || name.includes('内置')) {
|
} else if (
|
||||||
|
slotNum === 0 ||
|
||||||
|
name.includes('onboard') ||
|
||||||
|
name.includes('板载') ||
|
||||||
|
name.includes('内置')
|
||||||
|
) {
|
||||||
onboard.push({ ...card, type: 'onboard' });
|
onboard.push({ ...card, type: 'onboard' });
|
||||||
} else {
|
} else {
|
||||||
expansionSlots.push({ ...card, type: 'expansion', slotIndex: slotNum });
|
expansionSlots.push({ ...card, type: 'expansion', slotIndex: slotNum });
|
||||||
@@ -115,7 +126,7 @@ const ServerBackplanePanel = ({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '6px',
|
gap: '6px',
|
||||||
border: '2px solid #4b5563',
|
border: '2px solid #4b5563',
|
||||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.1)'
|
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.1)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ fontSize: '10px', color: '#9ca3af', fontWeight: 600 }}>MGMT</div>
|
<div style={{ fontSize: '10px', color: '#9ca3af', fontWeight: 600 }}>MGMT</div>
|
||||||
@@ -134,7 +145,7 @@ const ServerBackplanePanel = ({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'all 0.2s',
|
transition: 'all 0.2s',
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.3)'
|
boxShadow: '0 2px 4px rgba(0,0,0,0.3)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SettingOutlined style={{ fontSize: 16, color: '#10b981' }} />
|
<SettingOutlined style={{ fontSize: 16, color: '#10b981' }} />
|
||||||
@@ -152,7 +163,7 @@ const ServerBackplanePanel = ({
|
|||||||
border: '2px dashed #6b7280',
|
border: '2px dashed #6b7280',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center'
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusOutlined style={{ fontSize: 14, color: '#6b7280' }} />
|
<PlusOutlined style={{ fontSize: 14, color: '#6b7280' }} />
|
||||||
@@ -160,11 +171,47 @@ const ServerBackplanePanel = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 其他接口占位 */}
|
{/* 其他接口占位 */}
|
||||||
<div style={{ width: '48px', height: '24px', background: '#1f2937', borderRadius: '2px', border: '1px solid #4b5563' }}>
|
<div
|
||||||
<Text style={{ fontSize: '8px', color: '#6b7280', display: 'block', textAlign: 'center', lineHeight: '22px' }}>VGA</Text>
|
style={{
|
||||||
|
width: '48px',
|
||||||
|
height: '24px',
|
||||||
|
background: '#1f2937',
|
||||||
|
borderRadius: '2px',
|
||||||
|
border: '1px solid #4b5563',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: '8px',
|
||||||
|
color: '#6b7280',
|
||||||
|
display: 'block',
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: '22px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
VGA
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ width: '48px', height: '16px', background: '#1f2937', borderRadius: '2px', border: '1px solid #4b5563' }}>
|
<div
|
||||||
<Text style={{ fontSize: '8px', color: '#6b7280', display: 'block', textAlign: 'center', lineHeight: '14px' }}>USB</Text>
|
style={{
|
||||||
|
width: '48px',
|
||||||
|
height: '16px',
|
||||||
|
background: '#1f2937',
|
||||||
|
borderRadius: '2px',
|
||||||
|
border: '1px solid #4b5563',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: '8px',
|
||||||
|
color: '#6b7280',
|
||||||
|
display: 'block',
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: '14px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
USB
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -182,11 +229,20 @@ const ServerBackplanePanel = ({
|
|||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
padding: '12px',
|
padding: '12px',
|
||||||
border: '2px solid #6b7280',
|
border: '2px solid #6b7280',
|
||||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.1)'
|
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.1)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
<div
|
||||||
<Text style={{ fontSize: '11px', color: '#d1d5db', fontWeight: 600 }}>板载网卡 (Onboard)</Text>
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: '11px', color: '#d1d5db', fontWeight: 600 }}>
|
||||||
|
板载网卡 (Onboard)
|
||||||
|
</Text>
|
||||||
{onboardCard && (
|
{onboardCard && (
|
||||||
<Badge
|
<Badge
|
||||||
count={onboardCard.ports?.length || 0}
|
count={onboardCard.ports?.length || 0}
|
||||||
@@ -204,18 +260,21 @@ const ServerBackplanePanel = ({
|
|||||||
padding: '12px',
|
padding: '12px',
|
||||||
border: `2px solid ${onboardCard.ports?.length > 0 ? designTokens.colors.primary.main : '#6b7280'}`,
|
border: `2px solid ${onboardCard.ports?.length > 0 ? designTokens.colors.primary.main : '#6b7280'}`,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'all 0.2s'
|
transition: 'all 0.2s',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 4个RJ45端口布局 */}
|
{/* 4个RJ45端口布局 */}
|
||||||
<div style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}>
|
<div style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}>
|
||||||
{[0, 1, 2, 3].map((idx) => {
|
{[0, 1, 2, 3].map(idx => {
|
||||||
const port = onboardCard.ports?.[idx];
|
const port = onboardCard.ports?.[idx];
|
||||||
const hasPort = !!port;
|
const hasPort = !!port;
|
||||||
const isOccupied = hasPort && port.status === 'occupied';
|
const isOccupied = hasPort && port.status === 'occupied';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip key={idx} title={hasPort ? `${port.portName} - ${port.status}` : '未配置'}>
|
<Tooltip
|
||||||
|
key={idx}
|
||||||
|
title={hasPort ? `${port.portName} - ${port.status}` : '未配置'}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: '40px',
|
width: '40px',
|
||||||
@@ -224,15 +283,18 @@ const ServerBackplanePanel = ({
|
|||||||
? 'linear-gradient(180deg, #374151 0%, #1f2937 100%)'
|
? 'linear-gradient(180deg, #374151 0%, #1f2937 100%)'
|
||||||
: '#374151',
|
: '#374151',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
border: `2px solid ${hasPort
|
border: `2px solid ${
|
||||||
? (isOccupied ? designTokens.colors.success : designTokens.colors.metal.light)
|
hasPort
|
||||||
: '#4b5563'
|
? isOccupied
|
||||||
|
? designTokens.colors.success
|
||||||
|
: designTokens.colors.metal.light
|
||||||
|
: '#4b5563'
|
||||||
}`,
|
}`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
position: 'relative'
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* LED指示灯 */}
|
{/* LED指示灯 */}
|
||||||
@@ -241,13 +303,11 @@ const ServerBackplanePanel = ({
|
|||||||
width: '4px',
|
width: '4px',
|
||||||
height: '4px',
|
height: '4px',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
background: hasPort
|
background: hasPort ? (isOccupied ? '#10b981' : '#6b7280') : '#374151',
|
||||||
? (isOccupied ? '#10b981' : '#6b7280')
|
|
||||||
: '#374151',
|
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '2px',
|
top: '2px',
|
||||||
right: '2px',
|
right: '2px',
|
||||||
boxShadow: isOccupied ? '0 0 4px #10b981' : 'none'
|
boxShadow: isOccupied ? '0 0 4px #10b981' : 'none',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span style={{ fontSize: '8px', color: '#9ca3af' }}>⬡</span>
|
<span style={{ fontSize: '8px', color: '#9ca3af' }}>⬡</span>
|
||||||
@@ -273,7 +333,7 @@ const ServerBackplanePanel = ({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
gap: '8px'
|
gap: '8px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusOutlined style={{ fontSize: 20, color: '#6b7280' }} />
|
<PlusOutlined style={{ fontSize: 20, color: '#6b7280' }} />
|
||||||
@@ -303,13 +363,23 @@ const ServerBackplanePanel = ({
|
|||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
padding: '12px',
|
padding: '12px',
|
||||||
border: '2px solid #6b7280',
|
border: '2px solid #6b7280',
|
||||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.1)'
|
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.1)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Text style={{ fontSize: '11px', color: '#d1d5db', fontWeight: 600 }}>PCIe 扩展插槽</Text>
|
<Text style={{ fontSize: '11px', color: '#d1d5db', fontWeight: 600 }}>PCIe 扩展插槽</Text>
|
||||||
<Space size={4}>
|
<Space size={4}>
|
||||||
<Badge count={expansionSlots.length} style={{ backgroundColor: designTokens.colors.primary.main }} />
|
<Badge
|
||||||
|
count={expansionSlots.length}
|
||||||
|
style={{ backgroundColor: designTokens.colors.primary.main }}
|
||||||
|
/>
|
||||||
<Text style={{ fontSize: '10px', color: '#9ca3af' }}>/{totalSlots}</Text>
|
<Text style={{ fontSize: '10px', color: '#9ca3af' }}>/{totalSlots}</Text>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
@@ -318,7 +388,9 @@ const ServerBackplanePanel = ({
|
|||||||
{slots.map(({ slotNumber, card }) => (
|
{slots.map(({ slotNumber, card }) => (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
key={slotNumber}
|
key={slotNumber}
|
||||||
title={card ? `${card.name} (${card.ports?.length || 0}口)` : `插槽 ${slotNumber} (空闲)`}
|
title={
|
||||||
|
card ? `${card.name} (${card.ports?.length || 0}口)` : `插槽 ${slotNumber} (空闲)`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onClick={() => card && setSelectedSlot(card)}
|
onClick={() => card && setSelectedSlot(card)}
|
||||||
@@ -337,15 +409,24 @@ const ServerBackplanePanel = ({
|
|||||||
padding: '6px',
|
padding: '6px',
|
||||||
cursor: card ? 'pointer' : 'default',
|
cursor: card ? 'pointer' : 'default',
|
||||||
transition: 'all 0.2s',
|
transition: 'all 0.2s',
|
||||||
boxShadow: card ? '0 2px 8px rgba(59, 130, 246, 0.3)' : 'none'
|
boxShadow: card ? '0 2px 8px rgba(59, 130, 246, 0.3)' : 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ fontSize: '9px', color: '#6b7280', fontWeight: 600 }}>Slot {slotNumber}</Text>
|
<Text style={{ fontSize: '9px', color: '#6b7280', fontWeight: 600 }}>
|
||||||
|
Slot {slotNumber}
|
||||||
|
</Text>
|
||||||
|
|
||||||
{card ? (
|
{card ? (
|
||||||
<>
|
<>
|
||||||
<CloudServerOutlined style={{ fontSize: 20, color: '#3b82f6' }} />
|
<CloudServerOutlined style={{ fontSize: 20, color: '#3b82f6' }} />
|
||||||
<div style={{ display: 'flex', gap: '2px', flexWrap: 'wrap', justifyContent: 'center' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '2px',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{card.ports?.slice(0, 4).map((port, idx) => (
|
{card.ports?.slice(0, 4).map((port, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
@@ -354,19 +435,30 @@ const ServerBackplanePanel = ({
|
|||||||
height: '8px',
|
height: '8px',
|
||||||
borderRadius: '1px',
|
borderRadius: '1px',
|
||||||
background: port.status === 'occupied' ? '#10b981' : '#6b7280',
|
background: port.status === 'occupied' ? '#10b981' : '#6b7280',
|
||||||
boxShadow: port.status === 'occupied' ? '0 0 2px #10b981' : 'none'
|
boxShadow: port.status === 'occupied' ? '0 0 2px #10b981' : 'none',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{card.ports?.length > 4 && (
|
{card.ports?.length > 4 && (
|
||||||
<Text style={{ fontSize: '8px', color: '#9ca3af' }}>+{card.ports.length - 4}</Text>
|
<Text style={{ fontSize: '8px', color: '#9ca3af' }}>
|
||||||
|
+{card.ports.length - 4}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Text style={{ fontSize: '8px', color: '#9ca3af' }}>{card.ports?.length || 0}口</Text>
|
<Text style={{ fontSize: '8px', color: '#9ca3af' }}>
|
||||||
|
{card.ports?.length || 0}口
|
||||||
|
</Text>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div style={{ width: '40px', height: '40px', border: '2px dashed #4b5563', borderRadius: '4px' }} />
|
<div
|
||||||
|
style={{
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
border: '2px dashed #4b5563',
|
||||||
|
borderRadius: '4px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Text style={{ fontSize: '8px', color: '#6b7280' }}>空闲</Text>
|
<Text style={{ fontSize: '8px', color: '#6b7280' }}>空闲</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -390,11 +482,13 @@ const ServerBackplanePanel = ({
|
|||||||
border: '2px solid #4b5563',
|
border: '2px solid #4b5563',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: '8px'
|
gap: '8px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ fontSize: '10px', color: '#9ca3af', fontWeight: 600, textAlign: 'center' }}>电源</Text>
|
<Text style={{ fontSize: '10px', color: '#9ca3af', fontWeight: 600, textAlign: 'center' }}>
|
||||||
{[1, 2].map((psu) => (
|
电源
|
||||||
|
</Text>
|
||||||
|
{[1, 2].map(psu => (
|
||||||
<div
|
<div
|
||||||
key={psu}
|
key={psu}
|
||||||
style={{
|
style={{
|
||||||
@@ -406,7 +500,7 @@ const ServerBackplanePanel = ({
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: '4px'
|
gap: '4px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ThunderboltOutlined style={{ fontSize: 20, color: '#10b981' }} />
|
<ThunderboltOutlined style={{ fontSize: 20, color: '#10b981' }} />
|
||||||
@@ -417,7 +511,7 @@ const ServerBackplanePanel = ({
|
|||||||
height: '6px',
|
height: '6px',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
background: '#10b981',
|
background: '#10b981',
|
||||||
boxShadow: '0 0 6px #10b981'
|
boxShadow: '0 0 6px #10b981',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -445,17 +539,24 @@ const ServerBackplanePanel = ({
|
|||||||
padding: '8px 12px',
|
padding: '8px 12px',
|
||||||
background: '#f8fafc',
|
background: '#f8fafc',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
border: '1px solid #e2e8f0'
|
border: '1px solid #e2e8f0',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space>
|
||||||
<Badge count={cards.filter(c => !c.isUngrouped).length} style={{ backgroundColor: designTokens.colors.primary.main }} />
|
<Badge
|
||||||
<Text type="secondary" style={{ fontSize: '13px' }}>个网卡</Text>
|
count={cards.filter(c => !c.isUngrouped).length}
|
||||||
|
style={{ backgroundColor: designTokens.colors.primary.main }}
|
||||||
|
/>
|
||||||
|
<Text type="secondary" style={{ fontSize: '13px' }}>
|
||||||
|
个网卡
|
||||||
|
</Text>
|
||||||
<Badge
|
<Badge
|
||||||
count={cards.reduce((acc, card) => acc + (card.ports?.length || 0), 0)}
|
count={cards.reduce((acc, card) => acc + (card.ports?.length || 0), 0)}
|
||||||
style={{ backgroundColor: '#667eea' }}
|
style={{ backgroundColor: '#667eea' }}
|
||||||
/>
|
/>
|
||||||
<Text type="secondary" style={{ fontSize: '13px' }}>个端口</Text>
|
<Text type="secondary" style={{ fontSize: '13px' }}>
|
||||||
|
个端口
|
||||||
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
<Space>
|
||||||
<Button size="small" icon={<ReloadOutlined />} onClick={fetchData}>
|
<Button size="small" icon={<ReloadOutlined />} onClick={fetchData}>
|
||||||
@@ -480,7 +581,7 @@ const ServerBackplanePanel = ({
|
|||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
border: '3px solid #374151',
|
border: '3px solid #374151',
|
||||||
boxShadow: 'inset 0 2px 4px rgba(0,0,0,0.2), 0 4px 12px rgba(0,0,0,0.3)'
|
boxShadow: 'inset 0 2px 4px rgba(0,0,0,0.2), 0 4px 12px rgba(0,0,0,0.3)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 服务器标识 */}
|
{/* 服务器标识 */}
|
||||||
@@ -492,7 +593,7 @@ const ServerBackplanePanel = ({
|
|||||||
marginBottom: '12px',
|
marginBottom: '12px',
|
||||||
padding: '6px 12px',
|
padding: '6px 12px',
|
||||||
background: 'rgba(0,0,0,0.3)',
|
background: 'rgba(0,0,0,0.3)',
|
||||||
borderRadius: '4px'
|
borderRadius: '4px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DesktopOutlined style={{ fontSize: 14, color: '#9ca3af', marginRight: 8 }} />
|
<DesktopOutlined style={{ fontSize: 14, color: '#9ca3af', marginRight: 8 }} />
|
||||||
@@ -536,16 +637,34 @@ const ServerBackplanePanel = ({
|
|||||||
>
|
>
|
||||||
{selectedSlot && (
|
{selectedSlot && (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ marginBottom: '16px', padding: '12px', background: '#f8fafc', borderRadius: '8px' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: '16px',
|
||||||
|
padding: '12px',
|
||||||
|
background: '#f8fafc',
|
||||||
|
borderRadius: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||||
<Text type="secondary">类型: {selectedSlot.type === 'onboard' ? '板载网卡' : selectedSlot.type === 'management' ? '管理口' : '扩展网卡'}</Text>
|
<Text type="secondary">
|
||||||
{selectedSlot.description && <Text type="secondary">描述: {selectedSlot.description}</Text>}
|
类型:{' '}
|
||||||
|
{selectedSlot.type === 'onboard'
|
||||||
|
? '板载网卡'
|
||||||
|
: selectedSlot.type === 'management'
|
||||||
|
? '管理口'
|
||||||
|
: '扩展网卡'}
|
||||||
|
</Text>
|
||||||
|
{selectedSlot.description && (
|
||||||
|
<Text type="secondary">描述: {selectedSlot.description}</Text>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<Text type="secondary">端口统计: </Text>
|
<Text type="secondary">端口统计: </Text>
|
||||||
<Space size={8}>
|
<Space size={8}>
|
||||||
<Tag color="success">空闲: {selectedSlot.stats?.free || 0}</Tag>
|
<Tag color="success">空闲: {selectedSlot.stats?.free || 0}</Tag>
|
||||||
<Tag color="processing">占用: {selectedSlot.stats?.occupied || 0}</Tag>
|
<Tag color="processing">占用: {selectedSlot.stats?.occupied || 0}</Tag>
|
||||||
{selectedSlot.stats?.fault > 0 && <Tag color="error">故障: {selectedSlot.stats.fault}</Tag>}
|
{selectedSlot.stats?.fault > 0 && (
|
||||||
|
<Tag color="error">故障: {selectedSlot.stats.fault}</Tag>
|
||||||
|
)}
|
||||||
<Tag color="blue">总计: {selectedSlot.ports?.length || 0}</Tag>
|
<Tag color="blue">总计: {selectedSlot.ports?.length || 0}</Tag>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { Button, Empty, Spin, Badge, Typography, Space, Checkbox, Tooltip } from 'antd';
|
import { Button, Empty, Spin, Badge, Typography, Space, Checkbox, Tooltip } from 'antd';
|
||||||
import { DownOutlined, UpOutlined, EyeOutlined, EyeInvisibleOutlined, PlusOutlined, CloudServerOutlined } from '@ant-design/icons';
|
import {
|
||||||
|
DownOutlined,
|
||||||
|
UpOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
EyeInvisibleOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
CloudServerOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import ServerBackplanePanel from './ServerBackplanePanel';
|
import ServerBackplanePanel from './ServerBackplanePanel';
|
||||||
import PortPanel from './PortPanel';
|
import PortPanel from './PortPanel';
|
||||||
|
|
||||||
@@ -29,7 +36,7 @@ const VirtualDeviceList = ({
|
|||||||
onAddPort,
|
onAddPort,
|
||||||
onManageNetworkCards,
|
onManageNetworkCards,
|
||||||
initialVisibleCount = 5,
|
initialVisibleCount = 5,
|
||||||
loadMoreCount = 5
|
loadMoreCount = 5,
|
||||||
}) => {
|
}) => {
|
||||||
const [visibleCount, setVisibleCount] = useState(initialVisibleCount);
|
const [visibleCount, setVisibleCount] = useState(initialVisibleCount);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -54,10 +61,10 @@ const VirtualDeviceList = ({
|
|||||||
const options = {
|
const options = {
|
||||||
root: null,
|
root: null,
|
||||||
rootMargin: '100px',
|
rootMargin: '100px',
|
||||||
threshold: 0.1
|
threshold: 0.1,
|
||||||
};
|
};
|
||||||
|
|
||||||
observerRef.current = new IntersectionObserver((entries) => {
|
observerRef.current = new IntersectionObserver(entries => {
|
||||||
entries.forEach(entry => {
|
entries.forEach(entry => {
|
||||||
if (entry.isIntersecting && !loading && visibleCount < devices.length) {
|
if (entry.isIntersecting && !loading && visibleCount < devices.length) {
|
||||||
loadMore();
|
loadMore();
|
||||||
@@ -110,10 +117,10 @@ const VirtualDeviceList = ({
|
|||||||
setShowAll(false);
|
setShowAll(false);
|
||||||
}, [devices]);
|
}, [devices]);
|
||||||
|
|
||||||
const toggleDeviceExpand = (deviceId) => {
|
const toggleDeviceExpand = deviceId => {
|
||||||
setExpandedDevices(prev => ({
|
setExpandedDevices(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[deviceId]: !prev[deviceId]
|
[deviceId]: !prev[deviceId],
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -121,33 +128,29 @@ const VirtualDeviceList = ({
|
|||||||
const hasMore = visibleCount < devices.length;
|
const hasMore = visibleCount < devices.length;
|
||||||
|
|
||||||
if (devices.length === 0) {
|
if (devices.length === 0) {
|
||||||
return (
|
return <Empty description="暂无设备数据" style={{ padding: '60px 0' }} />;
|
||||||
<Empty
|
|
||||||
description="暂无设备数据"
|
|
||||||
style={{ padding: '60px 0' }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
<div ref={containerRef} style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||||
{/* 控制栏 */}
|
{/* 控制栏 */}
|
||||||
<div style={{
|
<div
|
||||||
display: 'flex',
|
style={{
|
||||||
justifyContent: 'space-between',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
justifyContent: 'space-between',
|
||||||
padding: '12px 16px',
|
alignItems: 'center',
|
||||||
background: '#f8fafc',
|
padding: '12px 16px',
|
||||||
borderRadius: '8px',
|
background: '#f8fafc',
|
||||||
border: '1px solid #e2e8f0'
|
borderRadius: '8px',
|
||||||
}}>
|
border: '1px solid #e2e8f0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Space align="center">
|
<Space align="center">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
<Text strong style={{ fontSize: '14px' }}>设备列表</Text>
|
<Text strong style={{ fontSize: '14px' }}>
|
||||||
<Badge
|
设备列表
|
||||||
count={devices.length}
|
</Text>
|
||||||
style={{ backgroundColor: '#667eea' }}
|
<Badge count={devices.length} style={{ backgroundColor: '#667eea' }} />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||||
显示 {visibleDevices.length} / {devices.length}
|
显示 {visibleDevices.length} / {devices.length}
|
||||||
@@ -166,7 +169,7 @@ const VirtualDeviceList = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 设备面板列表 */}
|
{/* 设备面板列表 */}
|
||||||
{visibleDevices.map((device) => {
|
{visibleDevices.map(device => {
|
||||||
const deviceId = device.deviceId;
|
const deviceId = device.deviceId;
|
||||||
const data = groupedPorts[deviceId] || { device, ports: [] };
|
const data = groupedPorts[deviceId] || { device, ports: [] };
|
||||||
const isExpanded = expandedDevices[deviceId];
|
const isExpanded = expandedDevices[deviceId];
|
||||||
@@ -181,7 +184,7 @@ const VirtualDeviceList = ({
|
|||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
transition: 'all 0.3s ease'
|
transition: 'all 0.3s ease',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 设备标题栏 */}
|
{/* 设备标题栏 */}
|
||||||
@@ -195,31 +198,37 @@ const VirtualDeviceList = ({
|
|||||||
background: isExpanded ? '#f1f5f9' : '#fff',
|
background: isExpanded ? '#f1f5f9' : '#fff',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
borderBottom: isExpanded ? '1px solid #e2e8f0' : 'none',
|
borderBottom: isExpanded ? '1px solid #e2e8f0' : 'none',
|
||||||
transition: 'background 0.2s'
|
transition: 'background 0.2s',
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={e => {
|
||||||
e.currentTarget.style.background = '#f1f5f9';
|
e.currentTarget.style.background = '#f1f5f9';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={e => {
|
||||||
if (!isExpanded) {
|
if (!isExpanded) {
|
||||||
e.currentTarget.style.background = '#fff';
|
e.currentTarget.style.background = '#fff';
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
<div style={{
|
<div
|
||||||
width: '40px',
|
style={{
|
||||||
height: '40px',
|
width: '40px',
|
||||||
borderRadius: '10px',
|
height: '40px',
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
borderRadius: '10px',
|
||||||
display: 'flex',
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
alignItems: 'center',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
fontSize: '20px'
|
justifyContent: 'center',
|
||||||
}}>
|
fontSize: '20px',
|
||||||
{device.type?.toLowerCase()?.includes('server') ? '🖥️' :
|
}}
|
||||||
device.type?.toLowerCase()?.includes('switch') ? '🔀' :
|
>
|
||||||
device.type?.toLowerCase()?.includes('router') ? '🌐' : '📦'}
|
{device.type?.toLowerCase()?.includes('server')
|
||||||
|
? '🖥️'
|
||||||
|
: device.type?.toLowerCase()?.includes('switch')
|
||||||
|
? '🔀'
|
||||||
|
: device.type?.toLowerCase()?.includes('router')
|
||||||
|
? '🌐'
|
||||||
|
: '📦'}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontWeight: 600, fontSize: '15px', color: '#1e293b' }}>
|
<div style={{ fontWeight: 600, fontSize: '15px', color: '#1e293b' }}>
|
||||||
@@ -257,13 +266,13 @@ const VirtualDeviceList = ({
|
|||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<CloudServerOutlined />}
|
icon={<CloudServerOutlined />}
|
||||||
onClick={(e) => {
|
onClick={e => {
|
||||||
e.stopPropagation(); // 防止触发折叠
|
e.stopPropagation(); // 防止触发折叠
|
||||||
onManageNetworkCards && onManageNetworkCards(device);
|
onManageNetworkCards && onManageNetworkCards(device);
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
border: 'none'
|
border: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
网卡管理
|
网卡管理
|
||||||
@@ -275,13 +284,13 @@ const VirtualDeviceList = ({
|
|||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={(e) => {
|
onClick={e => {
|
||||||
e.stopPropagation(); // 防止触发折叠
|
e.stopPropagation(); // 防止触发折叠
|
||||||
onAddPort && onAddPort(device);
|
onAddPort && onAddPort(device);
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||||
border: 'none'
|
border: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
添加端口
|
添加端口
|
||||||
@@ -318,7 +327,9 @@ const VirtualDeviceList = ({
|
|||||||
cables={cables}
|
cables={cables}
|
||||||
allDevices={allDevices}
|
allDevices={allDevices}
|
||||||
onPortClick={onPortClick}
|
onPortClick={onPortClick}
|
||||||
onManageNetworkCards={() => onManageNetworkCards && onManageNetworkCards(device)}
|
onManageNetworkCards={() =>
|
||||||
|
onManageNetworkCards && onManageNetworkCards(device)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -334,15 +345,13 @@ const VirtualDeviceList = ({
|
|||||||
style={{
|
style={{
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
padding: '20px',
|
padding: '20px',
|
||||||
color: '#64748b'
|
color: '#64748b',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Spin size="small" tip="加载更多设备..." />
|
<Spin size="small" tip="加载更多设备..." />
|
||||||
) : (
|
) : (
|
||||||
<Text type="secondary">
|
<Text type="secondary">向下滚动加载更多 ({devices.length - visibleCount} 个设备)</Text>
|
||||||
向下滚动加载更多 ({devices.length - visibleCount} 个设备)
|
|
||||||
</Text>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,49 +9,49 @@ export const designTokens = {
|
|||||||
main: '#667eea',
|
main: '#667eea',
|
||||||
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
light: '#8b9ff0',
|
light: '#8b9ff0',
|
||||||
dark: '#4f5db8'
|
dark: '#4f5db8',
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
main: '#10b981',
|
main: '#10b981',
|
||||||
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||||
light: '#34d399',
|
light: '#34d399',
|
||||||
dark: '#047857'
|
dark: '#047857',
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
main: '#f59e0b',
|
main: '#f59e0b',
|
||||||
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||||||
light: '#fbbf24',
|
light: '#fbbf24',
|
||||||
dark: '#b45309'
|
dark: '#b45309',
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
main: '#ef4444',
|
main: '#ef4444',
|
||||||
gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
|
gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
|
||||||
light: '#f87171',
|
light: '#f87171',
|
||||||
dark: '#b91c1c'
|
dark: '#b91c1c',
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
primary: '#1e293b',
|
primary: '#1e293b',
|
||||||
secondary: '#64748b',
|
secondary: '#64748b',
|
||||||
tertiary: '#94a3b8',
|
tertiary: '#94a3b8',
|
||||||
inverse: '#ffffff'
|
inverse: '#ffffff',
|
||||||
},
|
},
|
||||||
background: {
|
background: {
|
||||||
primary: '#ffffff',
|
primary: '#ffffff',
|
||||||
secondary: '#f8fafc',
|
secondary: '#f8fafc',
|
||||||
tertiary: '#f1f5f9',
|
tertiary: '#f1f5f9',
|
||||||
dark: '#1e293b'
|
dark: '#1e293b',
|
||||||
},
|
},
|
||||||
border: {
|
border: {
|
||||||
light: '#e2e8f0',
|
light: '#e2e8f0',
|
||||||
medium: '#cbd5e1',
|
medium: '#cbd5e1',
|
||||||
dark: '#94a3b8'
|
dark: '#94a3b8',
|
||||||
},
|
},
|
||||||
device: {
|
device: {
|
||||||
server: '#3b82f6',
|
server: '#3b82f6',
|
||||||
switch: '#22c55e',
|
switch: '#22c55e',
|
||||||
router: '#f59e0b',
|
router: '#f59e0b',
|
||||||
storage: '#8b5cf6',
|
storage: '#8b5cf6',
|
||||||
other: '#64748b'
|
other: '#64748b',
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
normal: '#10b981',
|
normal: '#10b981',
|
||||||
@@ -60,35 +60,35 @@ export const designTokens = {
|
|||||||
error: '#ef4444',
|
error: '#ef4444',
|
||||||
fault: '#ef4444',
|
fault: '#ef4444',
|
||||||
offline: '#6b7280',
|
offline: '#6b7280',
|
||||||
maintenance: '#3b82f6'
|
maintenance: '#3b82f6',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
shadows: {
|
shadows: {
|
||||||
small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||||
medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
||||||
large: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
|
large: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
|
||||||
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
|
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
|
||||||
glow: '0 0 20px rgba(102, 126, 234, 0.3)'
|
glow: '0 0 20px rgba(102, 126, 234, 0.3)',
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
small: '6px',
|
small: '6px',
|
||||||
medium: '10px',
|
medium: '10px',
|
||||||
large: '16px',
|
large: '16px',
|
||||||
xl: '24px',
|
xl: '24px',
|
||||||
round: '50%'
|
round: '50%',
|
||||||
},
|
},
|
||||||
transitions: {
|
transitions: {
|
||||||
fast: '150ms cubic-bezier(0.4, 0, 0.2, 1)',
|
fast: '150ms cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
normal: '300ms cubic-bezier(0.4, 0, 0.2, 1)',
|
normal: '300ms cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
slow: '500ms cubic-bezier(0.4, 0, 0.2, 1)'
|
slow: '500ms cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
},
|
},
|
||||||
spacing: {
|
spacing: {
|
||||||
xs: '4px',
|
xs: '4px',
|
||||||
sm: '8px',
|
sm: '8px',
|
||||||
md: '16px',
|
md: '16px',
|
||||||
lg: '24px',
|
lg: '24px',
|
||||||
xl: '32px'
|
xl: '32px',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default designTokens;
|
export default designTokens;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const PAGINATION_CONFIG = {
|
|||||||
// 显示快速跳转
|
// 显示快速跳转
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
// 显示总数
|
// 显示总数
|
||||||
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`
|
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 搜索防抖延迟(毫秒)
|
// 搜索防抖延迟(毫秒)
|
||||||
@@ -21,7 +21,7 @@ export const DEBOUNCE_DELAY = 300;
|
|||||||
// 表格滚动配置
|
// 表格滚动配置
|
||||||
export const TABLE_SCROLL_CONFIG = {
|
export const TABLE_SCROLL_CONFIG = {
|
||||||
x: 'max-content',
|
x: 'max-content',
|
||||||
y: 'calc(100vh - 400px)'
|
y: 'calc(100vh - 400px)',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 默认设备字段配置
|
// 默认设备字段配置
|
||||||
@@ -32,7 +32,7 @@ export const DEFAULT_DEVICE_FIELDS = [
|
|||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
visible: true,
|
visible: true,
|
||||||
editable: false
|
editable: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'name',
|
fieldName: 'name',
|
||||||
@@ -40,7 +40,7 @@ export const DEFAULT_DEVICE_FIELDS = [
|
|||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
visible: true,
|
visible: true,
|
||||||
editable: true
|
editable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'type',
|
fieldName: 'type',
|
||||||
@@ -54,8 +54,8 @@ export const DEFAULT_DEVICE_FIELDS = [
|
|||||||
{ value: 'switch', label: '交换机' },
|
{ value: 'switch', label: '交换机' },
|
||||||
{ value: 'router', label: '路由器' },
|
{ value: 'router', label: '路由器' },
|
||||||
{ value: 'storage', label: '存储设备' },
|
{ value: 'storage', label: '存储设备' },
|
||||||
{ value: 'other', label: '其他设备' }
|
{ value: 'other', label: '其他设备' },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'model',
|
fieldName: 'model',
|
||||||
@@ -63,7 +63,7 @@ export const DEFAULT_DEVICE_FIELDS = [
|
|||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
required: false,
|
required: false,
|
||||||
visible: true,
|
visible: true,
|
||||||
editable: true
|
editable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'serialNumber',
|
fieldName: 'serialNumber',
|
||||||
@@ -71,7 +71,7 @@ export const DEFAULT_DEVICE_FIELDS = [
|
|||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
visible: true,
|
visible: true,
|
||||||
editable: true
|
editable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'rackId',
|
fieldName: 'rackId',
|
||||||
@@ -79,7 +79,7 @@ export const DEFAULT_DEVICE_FIELDS = [
|
|||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
visible: true,
|
visible: true,
|
||||||
editable: true
|
editable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'position',
|
fieldName: 'position',
|
||||||
@@ -87,7 +87,7 @@ export const DEFAULT_DEVICE_FIELDS = [
|
|||||||
fieldType: 'number',
|
fieldType: 'number',
|
||||||
required: true,
|
required: true,
|
||||||
visible: true,
|
visible: true,
|
||||||
editable: true
|
editable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'height',
|
fieldName: 'height',
|
||||||
@@ -95,7 +95,7 @@ export const DEFAULT_DEVICE_FIELDS = [
|
|||||||
fieldType: 'number',
|
fieldType: 'number',
|
||||||
required: true,
|
required: true,
|
||||||
visible: true,
|
visible: true,
|
||||||
editable: true
|
editable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'powerConsumption',
|
fieldName: 'powerConsumption',
|
||||||
@@ -103,7 +103,7 @@ export const DEFAULT_DEVICE_FIELDS = [
|
|||||||
fieldType: 'number',
|
fieldType: 'number',
|
||||||
required: false,
|
required: false,
|
||||||
visible: true,
|
visible: true,
|
||||||
editable: true
|
editable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'ipAddress',
|
fieldName: 'ipAddress',
|
||||||
@@ -111,7 +111,7 @@ export const DEFAULT_DEVICE_FIELDS = [
|
|||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
required: false,
|
required: false,
|
||||||
visible: true,
|
visible: true,
|
||||||
editable: true
|
editable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'status',
|
fieldName: 'status',
|
||||||
@@ -124,8 +124,8 @@ export const DEFAULT_DEVICE_FIELDS = [
|
|||||||
{ value: 'running', label: '运行中' },
|
{ value: 'running', label: '运行中' },
|
||||||
{ value: 'maintenance', label: '维护中' },
|
{ value: 'maintenance', label: '维护中' },
|
||||||
{ value: 'offline', label: '离线' },
|
{ value: 'offline', label: '离线' },
|
||||||
{ value: 'fault', label: '故障' }
|
{ value: 'fault', label: '故障' },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'purchaseDate',
|
fieldName: 'purchaseDate',
|
||||||
@@ -133,7 +133,7 @@ export const DEFAULT_DEVICE_FIELDS = [
|
|||||||
fieldType: 'date',
|
fieldType: 'date',
|
||||||
required: false,
|
required: false,
|
||||||
visible: true,
|
visible: true,
|
||||||
editable: true
|
editable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'warrantyExpiry',
|
fieldName: 'warrantyExpiry',
|
||||||
@@ -141,7 +141,7 @@ export const DEFAULT_DEVICE_FIELDS = [
|
|||||||
fieldType: 'date',
|
fieldType: 'date',
|
||||||
required: false,
|
required: false,
|
||||||
visible: true,
|
visible: true,
|
||||||
editable: true
|
editable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'description',
|
fieldName: 'description',
|
||||||
@@ -149,8 +149,8 @@ export const DEFAULT_DEVICE_FIELDS = [
|
|||||||
fieldType: 'textarea',
|
fieldType: 'textarea',
|
||||||
required: false,
|
required: false,
|
||||||
visible: true,
|
visible: true,
|
||||||
editable: true
|
editable: true,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 基础字段名称列表(用于导入导出时排除自定义字段)
|
// 基础字段名称列表(用于导入导出时排除自定义字段)
|
||||||
@@ -168,7 +168,7 @@ export const BASE_FIELD_NAMES = [
|
|||||||
'status',
|
'status',
|
||||||
'purchaseDate',
|
'purchaseDate',
|
||||||
'warrantyExpiry',
|
'warrantyExpiry',
|
||||||
'description'
|
'description',
|
||||||
];
|
];
|
||||||
|
|
||||||
// 系统字段列表(不可编辑)
|
// 系统字段列表(不可编辑)
|
||||||
@@ -186,7 +186,7 @@ export const IMPORT_CONFIG = {
|
|||||||
// 单次最大导入条数
|
// 单次最大导入条数
|
||||||
maxImportCount: 5000,
|
maxImportCount: 5000,
|
||||||
// 编码格式
|
// 编码格式
|
||||||
encoding: 'gbk'
|
encoding: 'gbk',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 导出配置
|
// 导出配置
|
||||||
@@ -196,7 +196,7 @@ export const EXPORT_CONFIG = {
|
|||||||
// 日期格式
|
// 日期格式
|
||||||
dateFormat: 'YYYY-MM-DD_HH-mm-ss',
|
dateFormat: 'YYYY-MM-DD_HH-mm-ss',
|
||||||
// 支持的导出格式
|
// 支持的导出格式
|
||||||
formats: ['xlsx', 'csv']
|
formats: ['xlsx', 'csv'],
|
||||||
};
|
};
|
||||||
|
|
||||||
// 模态框配置
|
// 模态框配置
|
||||||
@@ -210,7 +210,7 @@ export const MODAL_CONFIG = {
|
|||||||
// 导入模态框宽度
|
// 导入模态框宽度
|
||||||
importModalWidth: 600,
|
importModalWidth: 600,
|
||||||
// 导出模态框宽度
|
// 导出模态框宽度
|
||||||
exportModalWidth: 500
|
exportModalWidth: 500,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 统计卡片配置
|
// 统计卡片配置
|
||||||
@@ -220,7 +220,7 @@ export const STATS_CONFIG = {
|
|||||||
// 显示的维护中设备数量上限
|
// 显示的维护中设备数量上限
|
||||||
maxMaintenanceDisplay: 99,
|
maxMaintenanceDisplay: 99,
|
||||||
// 显示的故障设备数量上限
|
// 显示的故障设备数量上限
|
||||||
maxFaultDisplay: 99
|
maxFaultDisplay: 99,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 设备类型选项(用于筛选)
|
// 设备类型选项(用于筛选)
|
||||||
@@ -230,7 +230,7 @@ export const DEVICE_TYPE_OPTIONS = [
|
|||||||
{ value: 'switch', label: '交换机' },
|
{ value: 'switch', label: '交换机' },
|
||||||
{ value: 'router', label: '路由器' },
|
{ value: 'router', label: '路由器' },
|
||||||
{ value: 'storage', label: '存储设备' },
|
{ value: 'storage', label: '存储设备' },
|
||||||
{ value: 'other', label: '其他设备' }
|
{ value: 'other', label: '其他设备' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 设备状态选项(用于筛选)
|
// 设备状态选项(用于筛选)
|
||||||
@@ -239,7 +239,7 @@ export const DEVICE_STATUS_OPTIONS = [
|
|||||||
{ value: 'running', label: '运行中' },
|
{ value: 'running', label: '运行中' },
|
||||||
{ value: 'maintenance', label: '维护中' },
|
{ value: 'maintenance', label: '维护中' },
|
||||||
{ value: 'offline', label: '离线' },
|
{ value: 'offline', label: '离线' },
|
||||||
{ value: 'fault', label: '故障' }
|
{ value: 'fault', label: '故障' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 表格列宽配置
|
// 表格列宽配置
|
||||||
@@ -258,13 +258,13 @@ export const COLUMN_WIDTH_CONFIG = {
|
|||||||
purchaseDate: 110,
|
purchaseDate: 110,
|
||||||
warrantyExpiry: 110,
|
warrantyExpiry: 110,
|
||||||
description: 200,
|
description: 200,
|
||||||
action: 150
|
action: 150,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 空状态配置
|
// 空状态配置
|
||||||
export const EMPTY_STATE_CONFIG = {
|
export const EMPTY_STATE_CONFIG = {
|
||||||
description: '暂无设备数据',
|
description: '暂无设备数据',
|
||||||
image: 'https://gw.alipayobjects.com/zos/antfincdn/ZHrcdLPrvN/empty.svg'
|
image: 'https://gw.alipayobjects.com/zos/antfincdn/ZHrcdLPrvN/empty.svg',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 操作按钮配置
|
// 操作按钮配置
|
||||||
@@ -272,7 +272,7 @@ export const ACTION_BUTTON_CONFIG = {
|
|||||||
// 批量操作阈值(超过此数量显示确认对话框)
|
// 批量操作阈值(超过此数量显示确认对话框)
|
||||||
batchConfirmThreshold: 10,
|
batchConfirmThreshold: 10,
|
||||||
// 批量删除确认消息
|
// 批量删除确认消息
|
||||||
batchDeleteConfirmMessage: (count) => `确定要删除选中的 ${count} 个设备吗?此操作不可恢复。`,
|
batchDeleteConfirmMessage: count => `确定要删除选中的 ${count} 个设备吗?此操作不可恢复。`,
|
||||||
// 单个删除确认消息
|
// 单个删除确认消息
|
||||||
singleDeleteConfirmMessage: (name) => `确定要删除设备 "${name}" 吗?此操作不可恢复。`
|
singleDeleteConfirmMessage: name => `确定要删除设备 "${name}" 吗?此操作不可恢复。`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export const AuthProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const register = async (userData) => {
|
const register = async userData => {
|
||||||
try {
|
try {
|
||||||
const response = await authAPI.register(userData);
|
const response = await authAPI.register(userData);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
@@ -123,13 +123,13 @@ export const AuthProvider = ({ children }) => {
|
|||||||
setUser(null);
|
setUser(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const updateUser = (newUserData) => {
|
const updateUser = newUserData => {
|
||||||
const updatedUser = { ...user, ...newUserData };
|
const updatedUser = { ...user, ...newUserData };
|
||||||
setUser(updatedUser);
|
setUser(updatedUser);
|
||||||
localStorage.setItem('user', JSON.stringify(updatedUser));
|
localStorage.setItem('user', JSON.stringify(updatedUser));
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasPermission = (permission) => {
|
const hasPermission = permission => {
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -144,14 +144,10 @@ export const AuthProvider = ({ children }) => {
|
|||||||
logout,
|
logout,
|
||||||
updateUser,
|
updateUser,
|
||||||
hasPermission,
|
hasPermission,
|
||||||
checkAdmin: () => authAPI.checkAdmin()
|
checkAdmin: () => authAPI.checkAdmin(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||||
<AuthContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</AuthContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AuthContext;
|
export default AuthContext;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const ConfigProvider = ({ children }) => {
|
|||||||
date_format: 'YYYY-MM-DD',
|
date_format: 'YYYY-MM-DD',
|
||||||
session_timeout: 30,
|
session_timeout: 30,
|
||||||
max_login_attempts: 5,
|
max_login_attempts: 5,
|
||||||
maintenance_mode: false
|
maintenance_mode: false,
|
||||||
});
|
});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ export const ConfigProvider = ({ children }) => {
|
|||||||
|
|
||||||
setConfig(prev => ({
|
setConfig(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
...configValues
|
...configValues,
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载系统配置失败:', error);
|
console.error('加载系统配置失败:', error);
|
||||||
@@ -50,10 +50,10 @@ export const ConfigProvider = ({ children }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 更新配置
|
// 更新配置
|
||||||
const updateConfig = (newConfig) => {
|
const updateConfig = newConfig => {
|
||||||
setConfig(prev => ({
|
setConfig(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
...newConfig
|
...newConfig,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ export const Scene3DProvider = ({ children }) => {
|
|||||||
const [loadingDevices, setLoadingDevices] = useState(false);
|
const [loadingDevices, setLoadingDevices] = useState(false);
|
||||||
|
|
||||||
// 使用 useCallback 稳定回调函数
|
// 使用 useCallback 稳定回调函数
|
||||||
const selectDevice = useCallback((device) => {
|
const selectDevice = useCallback(device => {
|
||||||
setSelectedDevice(device);
|
setSelectedDevice(device);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const hoverDevice = useCallback((device) => {
|
const hoverDevice = useCallback(device => {
|
||||||
setHoveredDevice(device);
|
setHoveredDevice(device);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -29,72 +29,84 @@ export const Scene3DProvider = ({ children }) => {
|
|||||||
setDeviceSlideEnabled(prev => !prev);
|
setDeviceSlideEnabled(prev => !prev);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setDeviceSlide = useCallback((enabled) => {
|
const setDeviceSlide = useCallback(enabled => {
|
||||||
setDeviceSlideEnabled(enabled);
|
setDeviceSlideEnabled(enabled);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const updateDevices = useCallback((newDevices) => {
|
const updateDevices = useCallback(newDevices => {
|
||||||
setDevices(newDevices);
|
setDevices(newDevices);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const updateRacks = useCallback((newRacks) => {
|
const updateRacks = useCallback(newRacks => {
|
||||||
setRacks(newRacks);
|
setRacks(newRacks);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const selectRack = useCallback((rack) => {
|
const selectRack = useCallback(rack => {
|
||||||
setSelectedRack(rack);
|
setSelectedRack(rack);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const updateDeviceCables = useCallback((cables) => {
|
const updateDeviceCables = useCallback(cables => {
|
||||||
setDeviceCables(cables);
|
setDeviceCables(cables);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setLoading = useCallback((loading) => {
|
const setLoading = useCallback(loading => {
|
||||||
setLoadingDevices(loading);
|
setLoadingDevices(loading);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 使用 useMemo 缓存 context value,避免不必要的重渲染
|
// 使用 useMemo 缓存 context value,避免不必要的重渲染
|
||||||
const value = useMemo(() => ({
|
const value = useMemo(
|
||||||
// 状态
|
() => ({
|
||||||
devices,
|
// 状态
|
||||||
selectedDevice,
|
devices,
|
||||||
hoveredDevice,
|
selectedDevice,
|
||||||
deviceSlideEnabled,
|
hoveredDevice,
|
||||||
selectedRack,
|
deviceSlideEnabled,
|
||||||
racks,
|
selectedRack,
|
||||||
deviceCables,
|
racks,
|
||||||
loadingDevices,
|
deviceCables,
|
||||||
// 方法
|
loadingDevices,
|
||||||
selectDevice,
|
// 方法
|
||||||
hoverDevice,
|
selectDevice,
|
||||||
toggleDeviceSlide,
|
hoverDevice,
|
||||||
setDeviceSlide,
|
toggleDeviceSlide,
|
||||||
updateDevices,
|
setDeviceSlide,
|
||||||
updateRacks,
|
updateDevices,
|
||||||
selectRack,
|
updateRacks,
|
||||||
updateDeviceCables,
|
selectRack,
|
||||||
setLoading,
|
updateDeviceCables,
|
||||||
// 直接设置状态的方法(用于兼容现有代码)
|
setLoading,
|
||||||
setDevices,
|
// 直接设置状态的方法(用于兼容现有代码)
|
||||||
setSelectedDevice,
|
setDevices,
|
||||||
setHoveredDevice,
|
setSelectedDevice,
|
||||||
setDeviceSlideEnabled,
|
setHoveredDevice,
|
||||||
setSelectedRack,
|
setDeviceSlideEnabled,
|
||||||
setRacks,
|
setSelectedRack,
|
||||||
setDeviceCables,
|
setRacks,
|
||||||
setLoadingDevices,
|
setDeviceCables,
|
||||||
}), [
|
setLoadingDevices,
|
||||||
devices, selectedDevice, hoveredDevice, deviceSlideEnabled,
|
}),
|
||||||
selectedRack, racks, deviceCables, loadingDevices,
|
[
|
||||||
selectDevice, hoverDevice, toggleDeviceSlide, setDeviceSlide,
|
devices,
|
||||||
updateDevices, updateRacks, selectRack, updateDeviceCables, setLoading
|
selectedDevice,
|
||||||
]);
|
hoveredDevice,
|
||||||
|
deviceSlideEnabled,
|
||||||
return (
|
selectedRack,
|
||||||
<Scene3DContext.Provider value={value}>
|
racks,
|
||||||
{children}
|
deviceCables,
|
||||||
</Scene3DContext.Provider>
|
loadingDevices,
|
||||||
|
selectDevice,
|
||||||
|
hoverDevice,
|
||||||
|
toggleDeviceSlide,
|
||||||
|
setDeviceSlide,
|
||||||
|
updateDevices,
|
||||||
|
updateRacks,
|
||||||
|
selectRack,
|
||||||
|
updateDeviceCables,
|
||||||
|
setLoading,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return <Scene3DContext.Provider value={value}>{children}</Scene3DContext.Provider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 自定义 Hook
|
// 自定义 Hook
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const useDesignTokens = () => {
|
|||||||
primary: {
|
primary: {
|
||||||
main: primaryColor,
|
main: primaryColor,
|
||||||
gradient: `linear-gradient(135deg, ${primaryColor} 0%, ${secondaryColor} 100%)`,
|
gradient: `linear-gradient(135deg, ${primaryColor} 0%, ${secondaryColor} 100%)`,
|
||||||
light: '#8b9ff0'
|
light: '#8b9ff0',
|
||||||
},
|
},
|
||||||
success: { main: '#10b981' },
|
success: { main: '#10b981' },
|
||||||
warning: { main: '#f59e0b' },
|
warning: { main: '#f59e0b' },
|
||||||
@@ -26,15 +26,15 @@ export const useDesignTokens = () => {
|
|||||||
text: {
|
text: {
|
||||||
primary: '#1e293b',
|
primary: '#1e293b',
|
||||||
secondary: '#64748b',
|
secondary: '#64748b',
|
||||||
inverse: '#ffffff'
|
inverse: '#ffffff',
|
||||||
},
|
},
|
||||||
background: {
|
background: {
|
||||||
primary: '#ffffff',
|
primary: '#ffffff',
|
||||||
secondary: '#f8fafc',
|
secondary: '#f8fafc',
|
||||||
dark: '#1e293b'
|
dark: '#1e293b',
|
||||||
},
|
},
|
||||||
border: {
|
border: {
|
||||||
light: '#e2e8f0'
|
light: '#e2e8f0',
|
||||||
},
|
},
|
||||||
sidebar: {
|
sidebar: {
|
||||||
bg: '#ffffff',
|
bg: '#ffffff',
|
||||||
@@ -43,23 +43,23 @@ export const useDesignTokens = () => {
|
|||||||
text: '#475569',
|
text: '#475569',
|
||||||
textHover: primaryColor,
|
textHover: primaryColor,
|
||||||
textActive: primaryColor,
|
textActive: primaryColor,
|
||||||
border: '#e2e8f0'
|
border: '#e2e8f0',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
shadows: {
|
shadows: {
|
||||||
small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||||
medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
||||||
large: '0 10px 15px -3px rgba(0, 0, 0, 0.1)'
|
large: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
small: '6px',
|
small: '6px',
|
||||||
medium: '10px'
|
medium: '10px',
|
||||||
},
|
},
|
||||||
spacing: {
|
spacing: {
|
||||||
sm: '8px',
|
sm: '8px',
|
||||||
md: '16px',
|
md: '16px',
|
||||||
lg: '24px'
|
lg: '24px',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}, [config?.primary_color, config?.secondary_color]);
|
}, [config?.primary_color, config?.secondary_color]);
|
||||||
|
|
||||||
@@ -76,9 +76,13 @@ function hexToRgb(hex) {
|
|||||||
const cleanHex = hex.replace('#', '');
|
const cleanHex = hex.replace('#', '');
|
||||||
|
|
||||||
// 处理简写格式 (如: #fff)
|
// 处理简写格式 (如: #fff)
|
||||||
const fullHex = cleanHex.length === 3
|
const fullHex =
|
||||||
? cleanHex.split('').map(c => c + c).join('')
|
cleanHex.length === 3
|
||||||
: cleanHex;
|
? cleanHex
|
||||||
|
.split('')
|
||||||
|
.map(c => c + c)
|
||||||
|
.join('')
|
||||||
|
: cleanHex;
|
||||||
|
|
||||||
const r = parseInt(fullHex.substring(0, 2), 16);
|
const r = parseInt(fullHex.substring(0, 2), 16);
|
||||||
const g = parseInt(fullHex.substring(2, 4), 16);
|
const g = parseInt(fullHex.substring(2, 4), 16);
|
||||||
|
|||||||
+13
-7
@@ -5,9 +5,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
font-family:
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell',
|
||||||
sans-serif;
|
'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
background-color: #f5f7fa;
|
background-color: #f5f7fa;
|
||||||
@@ -149,19 +149,24 @@ body {
|
|||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-input, .ant-select-selector, .ant-picker {
|
.ant-input,
|
||||||
|
.ant-select-selector,
|
||||||
|
.ant-picker {
|
||||||
border-radius: 8px !important;
|
border-radius: 8px !important;
|
||||||
border: 1px solid #d9d9d9 !important;
|
border: 1px solid #d9d9d9 !important;
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important;
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-input:hover, .ant-select-selector:hover, .ant-picker:hover {
|
.ant-input:hover,
|
||||||
|
.ant-select-selector:hover,
|
||||||
|
.ant-picker:hover {
|
||||||
border-color: #40a9ff !important;
|
border-color: #40a9ff !important;
|
||||||
box-shadow: 0 2px 6px rgba(24, 144, 255, 0.15) !important;
|
box-shadow: 0 2px 6px rgba(24, 144, 255, 0.15) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-input:focus, .ant-input-focused,
|
.ant-input:focus,
|
||||||
|
.ant-input-focused,
|
||||||
.ant-select-focused .ant-select-selector,
|
.ant-select-focused .ant-select-selector,
|
||||||
.ant-picker-focused {
|
.ant-picker-focused {
|
||||||
border-color: #1890ff !important;
|
border-color: #1890ff !important;
|
||||||
@@ -188,7 +193,8 @@ body {
|
|||||||
box-shadow: 0 2px 6px rgba(24, 144, 255, 0.15) !important;
|
box-shadow: 0 2px 6px rgba(24, 144, 255, 0.15) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-input-affix-wrapper:focus, .ant-input-affix-wrapper-focused {
|
.ant-input-affix-wrapper:focus,
|
||||||
|
.ant-input-affix-wrapper-focused {
|
||||||
border-color: #1890ff !important;
|
border-color: #1890ff !important;
|
||||||
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.15) !important;
|
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.15) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,35 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { Table, Button, Modal, Form, Input, Select, message, Card, Space, Popconfirm, Tag, Tooltip, Collapse, Empty, Spin, Upload, Progress, Checkbox } from 'antd';
|
import {
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ReloadOutlined, ExportOutlined, ImportOutlined, DownloadOutlined, UploadOutlined as UploadIcon } from '@ant-design/icons';
|
Table,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
message,
|
||||||
|
Card,
|
||||||
|
Space,
|
||||||
|
Popconfirm,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
Collapse,
|
||||||
|
Empty,
|
||||||
|
Spin,
|
||||||
|
Upload,
|
||||||
|
Progress,
|
||||||
|
Checkbox,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
SearchOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
ExportOutlined,
|
||||||
|
ImportOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
|
UploadOutlined as UploadIcon,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import Papa from 'papaparse';
|
import Papa from 'papaparse';
|
||||||
@@ -14,35 +43,35 @@ const designTokens = {
|
|||||||
main: '#667eea',
|
main: '#667eea',
|
||||||
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
light: '#8b9ff0',
|
light: '#8b9ff0',
|
||||||
dark: '#4f5db8'
|
dark: '#4f5db8',
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
main: '#10b981',
|
main: '#10b981',
|
||||||
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||||
light: '#34d399',
|
light: '#34d399',
|
||||||
dark: '#047857'
|
dark: '#047857',
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
main: '#f59e0b',
|
main: '#f59e0b',
|
||||||
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||||||
light: '#fbbf24',
|
light: '#fbbf24',
|
||||||
dark: '#b45309'
|
dark: '#b45309',
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
main: '#ef4444',
|
main: '#ef4444',
|
||||||
gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
|
gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
|
||||||
light: '#f87171',
|
light: '#f87171',
|
||||||
dark: '#b91c1c'
|
dark: '#b91c1c',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
small: '6px',
|
small: '6px',
|
||||||
medium: '10px',
|
medium: '10px',
|
||||||
large: '16px'
|
large: '16px',
|
||||||
},
|
},
|
||||||
shadows: {
|
shadows: {
|
||||||
medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)'
|
medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function CableManagement() {
|
function CableManagement() {
|
||||||
@@ -55,7 +84,7 @@ function CableManagement() {
|
|||||||
const [filters, setFilters] = useState({
|
const [filters, setFilters] = useState({
|
||||||
switchDeviceId: '',
|
switchDeviceId: '',
|
||||||
status: 'all',
|
status: 'all',
|
||||||
cableType: 'all'
|
cableType: 'all',
|
||||||
});
|
});
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const [editingCable, setEditingCable] = useState(null);
|
const [editingCable, setEditingCable] = useState(null);
|
||||||
@@ -88,7 +117,7 @@ function CableManagement() {
|
|||||||
if (!grouped[switchId]) {
|
if (!grouped[switchId]) {
|
||||||
grouped[switchId] = {
|
grouped[switchId] = {
|
||||||
switch: cable.sourceDevice,
|
switch: cable.sourceDevice,
|
||||||
cables: []
|
cables: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
grouped[switchId].cables.push(cable);
|
grouped[switchId].cables.push(cable);
|
||||||
@@ -128,7 +157,7 @@ function CableManagement() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchDevicePorts = useCallback(async (deviceId) => {
|
const fetchDevicePorts = useCallback(async deviceId => {
|
||||||
if (!deviceId) {
|
if (!deviceId) {
|
||||||
setDevicePorts(prev => ({ ...prev, [deviceId]: [] }));
|
setDevicePorts(prev => ({ ...prev, [deviceId]: [] }));
|
||||||
return;
|
return;
|
||||||
@@ -156,7 +185,7 @@ function CableManagement() {
|
|||||||
setFilters({
|
setFilters({
|
||||||
switchDeviceId: '',
|
switchDeviceId: '',
|
||||||
status: 'all',
|
status: 'all',
|
||||||
cableType: 'all'
|
cableType: 'all',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -166,7 +195,7 @@ function CableManagement() {
|
|||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (cable) => {
|
const handleEdit = cable => {
|
||||||
setEditingCable(cable);
|
setEditingCable(cable);
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
sourceDeviceId: cable.sourceDeviceId,
|
sourceDeviceId: cable.sourceDeviceId,
|
||||||
@@ -176,12 +205,12 @@ function CableManagement() {
|
|||||||
cableType: cable.cableType,
|
cableType: cable.cableType,
|
||||||
cableLength: cable.cableLength,
|
cableLength: cable.cableLength,
|
||||||
status: cable.status,
|
status: cable.status,
|
||||||
description: cable.description
|
description: cable.description,
|
||||||
});
|
});
|
||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (cableId) => {
|
const handleDelete = async cableId => {
|
||||||
try {
|
try {
|
||||||
await axios.delete(`/api/cables/${cableId}`);
|
await axios.delete(`/api/cables/${cableId}`);
|
||||||
message.success('删除成功');
|
message.success('删除成功');
|
||||||
@@ -192,7 +221,7 @@ function CableManagement() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteSwitch = async (switchId) => {
|
const handleDeleteSwitch = async switchId => {
|
||||||
try {
|
try {
|
||||||
await axios.delete(`/api/devices/${switchId}`);
|
await axios.delete(`/api/devices/${switchId}`);
|
||||||
message.success('删除设备成功');
|
message.success('删除设备成功');
|
||||||
@@ -228,7 +257,7 @@ function CableManagement() {
|
|||||||
sourceDeviceId: values.sourceDeviceId,
|
sourceDeviceId: values.sourceDeviceId,
|
||||||
sourcePort: values.sourcePort,
|
sourcePort: values.sourcePort,
|
||||||
targetDeviceId: values.targetDeviceId,
|
targetDeviceId: values.targetDeviceId,
|
||||||
targetPort: values.targetPort
|
targetPort: values.targetPort,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (checkResponse.data.hasConflict) {
|
if (checkResponse.data.hasConflict) {
|
||||||
@@ -247,10 +276,12 @@ function CableManagement() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.response?.status === 409) {
|
if (error.response?.status === 409) {
|
||||||
// 冲突错误
|
// 冲突错误
|
||||||
setConflictInfo([{
|
setConflictInfo([
|
||||||
type: 'unknown',
|
{
|
||||||
existingCable: error.response.data.existingCable
|
type: 'unknown',
|
||||||
}]);
|
existingCable: error.response.data.existingCable,
|
||||||
|
},
|
||||||
|
]);
|
||||||
setPendingSubmitValues(values);
|
setPendingSubmitValues(values);
|
||||||
setConflictModalVisible(true);
|
setConflictModalVisible(true);
|
||||||
} else {
|
} else {
|
||||||
@@ -269,7 +300,7 @@ function CableManagement() {
|
|||||||
|
|
||||||
await axios.post('/api/cables', {
|
await axios.post('/api/cables', {
|
||||||
...pendingSubmitValues,
|
...pendingSubmitValues,
|
||||||
force: true
|
force: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
message.success('接线已强制接管并创建成功');
|
message.success('接线已强制接管并创建成功');
|
||||||
@@ -291,12 +322,12 @@ function CableManagement() {
|
|||||||
setImportProgress({ current: 0, total: 0 });
|
setImportProgress({ current: 0, total: 0 });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileUpload = (info) => {
|
const handleFileUpload = info => {
|
||||||
const { file } = info;
|
const { file } = info;
|
||||||
setImportFileList([file]);
|
setImportFileList([file]);
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = async (e) => {
|
reader.onload = async e => {
|
||||||
try {
|
try {
|
||||||
const data = e.target.result;
|
const data = e.target.result;
|
||||||
let parsedData = [];
|
let parsedData = [];
|
||||||
@@ -310,9 +341,9 @@ function CableManagement() {
|
|||||||
Papa.parse(data, {
|
Papa.parse(data, {
|
||||||
header: true,
|
header: true,
|
||||||
skipEmptyLines: true,
|
skipEmptyLines: true,
|
||||||
complete: (results) => {
|
complete: results => {
|
||||||
parsedData = results.data;
|
parsedData = results.data;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
message.error('不支持的文件格式,请上传 .xlsx 或 .csv 文件');
|
message.error('不支持的文件格式,请上传 .xlsx 或 .csv 文件');
|
||||||
@@ -331,7 +362,7 @@ function CableManagement() {
|
|||||||
reader.readAsBinaryString(file);
|
reader.readAsBinaryString(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateImportData = async (data) => {
|
const validateImportData = async data => {
|
||||||
const validatedData = [];
|
const validatedData = [];
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
@@ -399,15 +430,15 @@ function CableManagement() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const cableTypeMap = {
|
const cableTypeMap = {
|
||||||
'网线': 'ethernet',
|
网线: 'ethernet',
|
||||||
'光纤': 'fiber',
|
光纤: 'fiber',
|
||||||
'铜缆': 'copper'
|
铜缆: 'copper',
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
'正常': 'normal',
|
正常: 'normal',
|
||||||
'故障': 'fault',
|
故障: 'fault',
|
||||||
'未连接': 'disconnected'
|
未连接: 'disconnected',
|
||||||
};
|
};
|
||||||
|
|
||||||
const cablesData = importPreview.map((row, index) => ({
|
const cablesData = importPreview.map((row, index) => ({
|
||||||
@@ -419,7 +450,7 @@ function CableManagement() {
|
|||||||
cableType: cableTypeMap[row['线缆类型']] || 'ethernet',
|
cableType: cableTypeMap[row['线缆类型']] || 'ethernet',
|
||||||
cableLength: row['线缆长度(米)'],
|
cableLength: row['线缆长度(米)'],
|
||||||
status: statusMap[row['状态']] || 'normal',
|
status: statusMap[row['状态']] || 'normal',
|
||||||
description: row['描述']
|
description: row['描述'],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const response = await axios.post('/api/cables/batch', { cables: cablesData });
|
const response = await axios.post('/api/cables/batch', { cables: cablesData });
|
||||||
@@ -449,15 +480,15 @@ function CableManagement() {
|
|||||||
const handleDownloadTemplate = () => {
|
const handleDownloadTemplate = () => {
|
||||||
const templateData = [
|
const templateData = [
|
||||||
{
|
{
|
||||||
'源设备ID': 'DEV001',
|
源设备ID: 'DEV001',
|
||||||
'源设备端口': 'eth0/1',
|
源设备端口: 'eth0/1',
|
||||||
'目标设备ID': 'DEV002',
|
目标设备ID: 'DEV002',
|
||||||
'目标设备端口': 'eth0',
|
目标设备端口: 'eth0',
|
||||||
'线缆类型': '网线',
|
线缆类型: '网线',
|
||||||
'线缆长度(米)': '5',
|
'线缆长度(米)': '5',
|
||||||
'状态': '正常',
|
状态: '正常',
|
||||||
'描述': '示例接线'
|
描述: '示例接线',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const worksheet = XLSX.utils.json_to_sheet(templateData);
|
const worksheet = XLSX.utils.json_to_sheet(templateData);
|
||||||
@@ -466,21 +497,21 @@ function CableManagement() {
|
|||||||
XLSX.writeFile(workbook, '接线导入模板.xlsx');
|
XLSX.writeFile(workbook, '接线导入模板.xlsx');
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusTag = (status) => {
|
const getStatusTag = status => {
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
normal: { color: 'success', text: '正常' },
|
normal: { color: 'success', text: '正常' },
|
||||||
fault: { color: 'error', text: '故障' },
|
fault: { color: 'error', text: '故障' },
|
||||||
disconnected: { color: 'default', text: '未连接' }
|
disconnected: { color: 'default', text: '未连接' },
|
||||||
};
|
};
|
||||||
const config = statusMap[status] || { color: 'default', text: status };
|
const config = statusMap[status] || { color: 'default', text: status };
|
||||||
return <Tag color={config.color}>{config.text}</Tag>;
|
return <Tag color={config.color}>{config.text}</Tag>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCableTypeTag = (type) => {
|
const getCableTypeTag = type => {
|
||||||
const typeMap = {
|
const typeMap = {
|
||||||
'网线': { color: 'blue', text: '网线' },
|
网线: { color: 'blue', text: '网线' },
|
||||||
'光纤': { color: 'green', text: '光纤' },
|
光纤: { color: 'green', text: '光纤' },
|
||||||
'铜缆': { color: 'orange', text: '铜缆' }
|
铜缆: { color: 'orange', text: '铜缆' },
|
||||||
};
|
};
|
||||||
const config = typeMap[type] || { color: 'default', text: type };
|
const config = typeMap[type] || { color: 'default', text: type };
|
||||||
return <Tag color={config.color}>{config.text}</Tag>;
|
return <Tag color={config.color}>{config.text}</Tag>;
|
||||||
@@ -494,7 +525,7 @@ function CableManagement() {
|
|||||||
return {
|
return {
|
||||||
status: cable.status,
|
status: cable.status,
|
||||||
text: cable.status === 'normal' ? '已连接' : cable.status === 'fault' ? '故障' : '未连接',
|
text: cable.status === 'normal' ? '已连接' : cable.status === 'fault' ? '故障' : '未连接',
|
||||||
color: cable.status === 'normal' ? 'success' : cable.status === 'fault' ? 'error' : 'default'
|
color: cable.status === 'normal' ? 'success' : cable.status === 'fault' ? 'error' : 'default',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -503,31 +534,31 @@ function CableManagement() {
|
|||||||
title: '端口名称',
|
title: '端口名称',
|
||||||
dataIndex: 'portName',
|
dataIndex: 'portName',
|
||||||
key: 'portName',
|
key: 'portName',
|
||||||
width: 120
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '端口类型',
|
title: '端口类型',
|
||||||
dataIndex: 'portType',
|
dataIndex: 'portType',
|
||||||
key: 'portType',
|
key: 'portType',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (type) => {
|
render: type => {
|
||||||
const typeMap = {
|
const typeMap = {
|
||||||
'RJ45': { color: 'blue', text: 'RJ45' },
|
RJ45: { color: 'blue', text: 'RJ45' },
|
||||||
'SFP': { color: 'green', text: 'SFP' },
|
SFP: { color: 'green', text: 'SFP' },
|
||||||
'SFP+': { color: 'cyan', text: 'SFP+' },
|
'SFP+': { color: 'cyan', text: 'SFP+' },
|
||||||
'SFP28': { color: 'purple', text: 'SFP28' },
|
SFP28: { color: 'purple', text: 'SFP28' },
|
||||||
'QSFP': { color: 'orange', text: 'QSFP' },
|
QSFP: { color: 'orange', text: 'QSFP' },
|
||||||
'QSFP28': { color: 'red', text: 'QSFP28' }
|
QSFP28: { color: 'red', text: 'QSFP28' },
|
||||||
};
|
};
|
||||||
const config = typeMap[type] || { color: 'default', text: type };
|
const config = typeMap[type] || { color: 'default', text: type };
|
||||||
return <Tag color={config.color}>{config.text}</Tag>;
|
return <Tag color={config.color}>{config.text}</Tag>;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '端口速率',
|
title: '端口速率',
|
||||||
dataIndex: 'portSpeed',
|
dataIndex: 'portSpeed',
|
||||||
key: 'portSpeed',
|
key: 'portSpeed',
|
||||||
width: 100
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '连接状态',
|
title: '连接状态',
|
||||||
@@ -537,7 +568,7 @@ function CableManagement() {
|
|||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
const status = getPortConnectionStatus(record.portName, record.switchData);
|
const status = getPortConnectionStatus(record.portName, record.switchData);
|
||||||
return <Tag color={status.color}>{status.text}</Tag>;
|
return <Tag color={status.color}>{status.text}</Tag>;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '目标设备',
|
title: '目标设备',
|
||||||
@@ -553,7 +584,7 @@ function CableManagement() {
|
|||||||
<div style={{ fontSize: 12, color: '#999' }}>{cable.targetPort}</div>
|
<div style={{ fontSize: 12, color: '#999' }}>{cable.targetPort}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '线缆类型',
|
title: '线缆类型',
|
||||||
@@ -564,7 +595,7 @@ function CableManagement() {
|
|||||||
const cable = record.switchData.cables.find(c => c.sourcePort === record.portName);
|
const cable = record.switchData.cables.find(c => c.sourcePort === record.portName);
|
||||||
if (!cable) return '-';
|
if (!cable) return '-';
|
||||||
return getCableTypeTag(cable.cableType);
|
return getCableTypeTag(cable.cableType);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '长度(米)',
|
title: '长度(米)',
|
||||||
@@ -575,7 +606,7 @@ function CableManagement() {
|
|||||||
const cable = record.switchData.cables.find(c => c.sourcePort === record.portName);
|
const cable = record.switchData.cables.find(c => c.sourcePort === record.portName);
|
||||||
if (!cable) return '-';
|
if (!cable) return '-';
|
||||||
return cable.cableLength ? `${cable.cableLength}m` : '-';
|
return cable.cableLength ? `${cable.cableLength}m` : '-';
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
@@ -602,12 +633,7 @@ function CableManagement() {
|
|||||||
okText="确定"
|
okText="确定"
|
||||||
cancelText="取消"
|
cancelText="取消"
|
||||||
>
|
>
|
||||||
<Button
|
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
danger
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
>
|
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
@@ -615,8 +641,8 @@ function CableManagement() {
|
|||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -625,7 +651,7 @@ function CableManagement() {
|
|||||||
style={{
|
style={{
|
||||||
borderRadius: designTokens.borderRadius.large,
|
borderRadius: designTokens.borderRadius.large,
|
||||||
boxShadow: designTokens.shadows.medium,
|
boxShadow: designTokens.shadows.medium,
|
||||||
marginBottom: 16
|
marginBottom: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
@@ -634,7 +660,7 @@ function CableManagement() {
|
|||||||
placeholder="选择交换机"
|
placeholder="选择交换机"
|
||||||
style={{ width: 200 }}
|
style={{ width: 200 }}
|
||||||
value={filters.switchDeviceId || undefined}
|
value={filters.switchDeviceId || undefined}
|
||||||
onChange={(value) => setFilters(prev => ({ ...prev, switchDeviceId: value }))}
|
onChange={value => setFilters(prev => ({ ...prev, switchDeviceId: value }))}
|
||||||
allowClear
|
allowClear
|
||||||
showSearch
|
showSearch
|
||||||
filterOption={(input, option) => {
|
filterOption={(input, option) => {
|
||||||
@@ -655,7 +681,7 @@ function CableManagement() {
|
|||||||
placeholder="线缆类型"
|
placeholder="线缆类型"
|
||||||
style={{ width: 120 }}
|
style={{ width: 120 }}
|
||||||
value={filters.cableType}
|
value={filters.cableType}
|
||||||
onChange={(value) => setFilters(prev => ({ ...prev, cableType: value }))}
|
onChange={value => setFilters(prev => ({ ...prev, cableType: value }))}
|
||||||
>
|
>
|
||||||
<Option value="all">全部</Option>
|
<Option value="all">全部</Option>
|
||||||
<Option value="ethernet">网线</Option>
|
<Option value="ethernet">网线</Option>
|
||||||
@@ -667,7 +693,7 @@ function CableManagement() {
|
|||||||
placeholder="连接状态"
|
placeholder="连接状态"
|
||||||
style={{ width: 120 }}
|
style={{ width: 120 }}
|
||||||
value={filters.status}
|
value={filters.status}
|
||||||
onChange={(value) => setFilters(prev => ({ ...prev, status: value }))}
|
onChange={value => setFilters(prev => ({ ...prev, status: value }))}
|
||||||
>
|
>
|
||||||
<Option value="all">全部</Option>
|
<Option value="all">全部</Option>
|
||||||
<Option value="normal">已连接</Option>
|
<Option value="normal">已连接</Option>
|
||||||
@@ -710,9 +736,7 @@ function CableManagement() {
|
|||||||
批量导入
|
批量导入
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button icon={<ExportOutlined />}>
|
<Button icon={<ExportOutlined />}>导出</Button>
|
||||||
导出
|
|
||||||
</Button>
|
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -733,26 +757,37 @@ function CableManagement() {
|
|||||||
const switchDevice = switchData.switch;
|
const switchDevice = switchData.switch;
|
||||||
const switchPorts = devicePorts[switchId] || [];
|
const switchPorts = devicePorts[switchId] || [];
|
||||||
const connectedCount = switchData.cables.filter(c => c.status === 'normal').length;
|
const connectedCount = switchData.cables.filter(c => c.status === 'normal').length;
|
||||||
const disconnectedCount = switchData.cables.filter(c => c.status === 'disconnected').length;
|
const disconnectedCount = switchData.cables.filter(
|
||||||
|
c => c.status === 'disconnected'
|
||||||
|
).length;
|
||||||
const faultCount = switchData.cables.filter(c => c.status === 'fault').length;
|
const faultCount = switchData.cables.filter(c => c.status === 'fault').length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel
|
<Panel
|
||||||
key={switchId}
|
key={switchId}
|
||||||
header={
|
header={
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
<div style={{
|
<div
|
||||||
width: '40px',
|
style={{
|
||||||
height: '40px',
|
width: '40px',
|
||||||
borderRadius: designTokens.borderRadius.medium,
|
height: '40px',
|
||||||
background: designTokens.colors.primary.gradient,
|
borderRadius: designTokens.borderRadius.medium,
|
||||||
display: 'flex',
|
background: designTokens.colors.primary.gradient,
|
||||||
alignItems: 'center',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
color: '#fff',
|
justifyContent: 'center',
|
||||||
fontSize: '18px'
|
color: '#fff',
|
||||||
}}>
|
fontSize: '18px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
🔀
|
🔀
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -792,12 +827,7 @@ function CableManagement() {
|
|||||||
okText="确定"
|
okText="确定"
|
||||||
cancelText="取消"
|
cancelText="取消"
|
||||||
>
|
>
|
||||||
<Button
|
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
danger
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
>
|
|
||||||
删除设备
|
删除设备
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
@@ -808,7 +838,7 @@ function CableManagement() {
|
|||||||
columns={portColumns}
|
columns={portColumns}
|
||||||
dataSource={switchPorts.map(port => ({
|
dataSource={switchPorts.map(port => ({
|
||||||
...port,
|
...port,
|
||||||
switchData: switchData
|
switchData: switchData,
|
||||||
}))}
|
}))}
|
||||||
rowKey="portId"
|
rowKey="portId"
|
||||||
pagination={false}
|
pagination={false}
|
||||||
@@ -849,7 +879,7 @@ function CableManagement() {
|
|||||||
const searchText = `${device.name} ${device.deviceId}`.toLowerCase();
|
const searchText = `${device.name} ${device.deviceId}`.toLowerCase();
|
||||||
return searchText.indexOf(input.toLowerCase()) >= 0;
|
return searchText.indexOf(input.toLowerCase()) >= 0;
|
||||||
}}
|
}}
|
||||||
onChange={(value) => {
|
onChange={value => {
|
||||||
fetchDevicePorts(value);
|
fetchDevicePorts(value);
|
||||||
form.setFieldsValue({ sourcePort: undefined });
|
form.setFieldsValue({ sourcePort: undefined });
|
||||||
}}
|
}}
|
||||||
@@ -874,7 +904,8 @@ function CableManagement() {
|
|||||||
const ports = devicePorts[form.getFieldValue('sourceDeviceId')] || [];
|
const ports = devicePorts[form.getFieldValue('sourceDeviceId')] || [];
|
||||||
const port = ports.find(p => p.portName === option.value);
|
const port = ports.find(p => p.portName === option.value);
|
||||||
if (!port) return false;
|
if (!port) return false;
|
||||||
const searchText = `${port.portName} ${port.portType} ${port.portSpeed}`.toLowerCase();
|
const searchText =
|
||||||
|
`${port.portName} ${port.portType} ${port.portSpeed}`.toLowerCase();
|
||||||
return searchText.indexOf(input.toLowerCase()) >= 0;
|
return searchText.indexOf(input.toLowerCase()) >= 0;
|
||||||
}}
|
}}
|
||||||
disabled={!form.getFieldValue('sourceDeviceId')}
|
disabled={!form.getFieldValue('sourceDeviceId')}
|
||||||
@@ -901,7 +932,7 @@ function CableManagement() {
|
|||||||
const searchText = `${device.name} ${device.deviceId}`.toLowerCase();
|
const searchText = `${device.name} ${device.deviceId}`.toLowerCase();
|
||||||
return searchText.indexOf(input.toLowerCase()) >= 0;
|
return searchText.indexOf(input.toLowerCase()) >= 0;
|
||||||
}}
|
}}
|
||||||
onChange={(value) => {
|
onChange={value => {
|
||||||
fetchDevicePorts(value);
|
fetchDevicePorts(value);
|
||||||
form.setFieldsValue({ targetPort: undefined });
|
form.setFieldsValue({ targetPort: undefined });
|
||||||
}}
|
}}
|
||||||
@@ -926,7 +957,8 @@ function CableManagement() {
|
|||||||
const ports = devicePorts[form.getFieldValue('targetDeviceId')] || [];
|
const ports = devicePorts[form.getFieldValue('targetDeviceId')] || [];
|
||||||
const port = ports.find(p => p.portName === option.value);
|
const port = ports.find(p => p.portName === option.value);
|
||||||
if (!port) return false;
|
if (!port) return false;
|
||||||
const searchText = `${port.portName} ${port.portType} ${port.portSpeed}`.toLowerCase();
|
const searchText =
|
||||||
|
`${port.portName} ${port.portType} ${port.portSpeed}`.toLowerCase();
|
||||||
return searchText.indexOf(input.toLowerCase()) >= 0;
|
return searchText.indexOf(input.toLowerCase()) >= 0;
|
||||||
}}
|
}}
|
||||||
disabled={!form.getFieldValue('targetDeviceId')}
|
disabled={!form.getFieldValue('targetDeviceId')}
|
||||||
@@ -952,10 +984,7 @@ function CableManagement() {
|
|||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item name="cableLength" label="线缆长度(米)">
|
||||||
name="cableLength"
|
|
||||||
label="线缆长度(米)"
|
|
||||||
>
|
|
||||||
<Input type="number" placeholder="请输入线缆长度" />
|
<Input type="number" placeholder="请输入线缆长度" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@@ -972,10 +1001,7 @@ function CableManagement() {
|
|||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item name="description" label="描述">
|
||||||
name="description"
|
|
||||||
label="描述"
|
|
||||||
>
|
|
||||||
<Input.TextArea rows={3} placeholder="请输入描述" />
|
<Input.TextArea rows={3} placeholder="请输入描述" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -994,11 +1020,7 @@ function CableManagement() {
|
|||||||
<Button key="cancel" onClick={() => setImportModalVisible(false)}>
|
<Button key="cancel" onClick={() => setImportModalVisible(false)}>
|
||||||
取消
|
取消
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button
|
<Button key="download" icon={<DownloadOutlined />} onClick={handleDownloadTemplate}>
|
||||||
key="download"
|
|
||||||
icon={<DownloadOutlined />}
|
|
||||||
onClick={handleDownloadTemplate}
|
|
||||||
>
|
|
||||||
下载模板
|
下载模板
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button
|
<Button
|
||||||
@@ -1011,7 +1033,7 @@ function CableManagement() {
|
|||||||
style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
|
style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
|
||||||
>
|
>
|
||||||
开始导入
|
开始导入
|
||||||
</Button>
|
</Button>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
@@ -1033,10 +1055,10 @@ function CableManagement() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '12px', marginBottom: 16 }}>
|
<div style={{ display: 'flex', gap: '12px', marginBottom: 16 }}>
|
||||||
<Checkbox checked={skipExisting} onChange={(e) => setSkipExisting(e.target.checked)}>
|
<Checkbox checked={skipExisting} onChange={e => setSkipExisting(e.target.checked)}>
|
||||||
跳过已存在的接线
|
跳过已存在的接线
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Checkbox checked={updateExisting} onChange={(e) => setUpdateExisting(e.target.checked)}>
|
<Checkbox checked={updateExisting} onChange={e => setUpdateExisting(e.target.checked)}>
|
||||||
更新已存在的接线
|
更新已存在的接线
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
@@ -1044,13 +1066,16 @@ function CableManagement() {
|
|||||||
{importPreview.length > 0 && (
|
{importPreview.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Text strong>数据预览(前10条)</Text>
|
<Text strong>数据预览(前10条)</Text>
|
||||||
<Button
|
<Button size="small" icon={<DownloadOutlined />} onClick={handleDownloadTemplate}>
|
||||||
size="small"
|
|
||||||
icon={<DownloadOutlined />}
|
|
||||||
onClick={handleDownloadTemplate}
|
|
||||||
>
|
|
||||||
下载模板
|
下载模板
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1060,46 +1085,46 @@ function CableManagement() {
|
|||||||
title: '源设备ID',
|
title: '源设备ID',
|
||||||
dataIndex: '源设备ID',
|
dataIndex: '源设备ID',
|
||||||
key: 'sourceDeviceId',
|
key: 'sourceDeviceId',
|
||||||
width: 150
|
width: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '源设备端口',
|
title: '源设备端口',
|
||||||
dataIndex: '源设备端口',
|
dataIndex: '源设备端口',
|
||||||
key: 'sourcePort',
|
key: 'sourcePort',
|
||||||
width: 120
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '目标设备ID',
|
title: '目标设备ID',
|
||||||
dataIndex: '目标设备ID',
|
dataIndex: '目标设备ID',
|
||||||
key: 'targetDeviceId',
|
key: 'targetDeviceId',
|
||||||
width: 150
|
width: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '目标设备端口',
|
title: '目标设备端口',
|
||||||
dataIndex: '目标设备端口',
|
dataIndex: '目标设备端口',
|
||||||
key: 'targetPort',
|
key: 'targetPort',
|
||||||
width: 120
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '线缆类型',
|
title: '线缆类型',
|
||||||
dataIndex: '线缆类型',
|
dataIndex: '线缆类型',
|
||||||
key: 'cableType',
|
key: 'cableType',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (type) => getCableTypeTag(type)
|
render: type => getCableTypeTag(type),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: '状态',
|
dataIndex: '状态',
|
||||||
key: 'status',
|
key: 'status',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (status) => getStatusTag(status)
|
render: status => getStatusTag(status),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '描述',
|
title: '描述',
|
||||||
dataIndex: '描述',
|
dataIndex: '描述',
|
||||||
key: 'description',
|
key: 'description',
|
||||||
ellipsis: true
|
ellipsis: true,
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
dataSource={importPreview.slice(0, 10)}
|
dataSource={importPreview.slice(0, 10)}
|
||||||
rowKey={(record, index) => index}
|
rowKey={(record, index) => index}
|
||||||
@@ -1126,7 +1151,7 @@ function CableManagement() {
|
|||||||
status="active"
|
status="active"
|
||||||
strokeColor={{
|
strokeColor={{
|
||||||
'0%': designTokens.colors.primary.main,
|
'0%': designTokens.colors.primary.main,
|
||||||
'100%': designTokens.colors.success.main
|
'100%': designTokens.colors.success.main,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div style={{ marginTop: 8 }}>
|
<div style={{ marginTop: 8 }}>
|
||||||
@@ -1135,7 +1160,8 @@ function CableManagement() {
|
|||||||
</Text>
|
</Text>
|
||||||
{importProgress.current > 0 && (
|
{importProgress.current > 0 && (
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
预计剩余时间:{Math.ceil((importProgress.total - importProgress.current) / 5)} 秒
|
预计剩余时间:{Math.ceil((importProgress.total - importProgress.current) / 5)}{' '}
|
||||||
|
秒
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1165,14 +1191,9 @@ function CableManagement() {
|
|||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button
|
<Button key="force" type="primary" danger onClick={handleForceSubmit}>
|
||||||
key="force"
|
|
||||||
type="primary"
|
|
||||||
danger
|
|
||||||
onClick={handleForceSubmit}
|
|
||||||
>
|
|
||||||
强制接管
|
强制接管
|
||||||
</Button>
|
</Button>,
|
||||||
]}
|
]}
|
||||||
width={600}
|
width={600}
|
||||||
>
|
>
|
||||||
@@ -1190,7 +1211,11 @@ function CableManagement() {
|
|||||||
>
|
>
|
||||||
<div style={{ marginBottom: 8 }}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
<Tag color="error">
|
<Tag color="error">
|
||||||
{conflict.type === 'source' ? '源端口' : conflict.type === 'target' ? '目标端口' : '端口'}
|
{conflict.type === 'source'
|
||||||
|
? '源端口'
|
||||||
|
: conflict.type === 'target'
|
||||||
|
? '目标端口'
|
||||||
|
: '端口'}
|
||||||
</Tag>
|
</Tag>
|
||||||
<span style={{ fontWeight: 500, marginLeft: 8 }}>{conflict.port}</span>
|
<span style={{ fontWeight: 500, marginLeft: 8 }}>{conflict.port}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1199,11 +1224,15 @@ function CableManagement() {
|
|||||||
<div>当前连接:</div>
|
<div>当前连接:</div>
|
||||||
<div style={{ marginTop: 4, paddingLeft: 12 }}>
|
<div style={{ marginTop: 4, paddingLeft: 12 }}>
|
||||||
<div>
|
<div>
|
||||||
源设备:{conflict.existingCable.sourceDevice?.name || conflict.existingCable.sourceDeviceId}
|
源设备:
|
||||||
|
{conflict.existingCable.sourceDevice?.name ||
|
||||||
|
conflict.existingCable.sourceDeviceId}
|
||||||
({conflict.existingCable.sourcePort})
|
({conflict.existingCable.sourcePort})
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 2 }}>
|
<div style={{ marginTop: 2 }}>
|
||||||
目标设备:{conflict.existingCable.targetDevice?.name || conflict.existingCable.targetDeviceId}
|
目标设备:
|
||||||
|
{conflict.existingCable.targetDevice?.name ||
|
||||||
|
conflict.existingCable.targetDeviceId}
|
||||||
({conflict.existingCable.targetPort})
|
({conflict.existingCable.targetPort})
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 2 }}>
|
<div style={{ marginTop: 2 }}>
|
||||||
@@ -1214,7 +1243,15 @@ function CableManagement() {
|
|||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
<div style={{ marginTop: 16, padding: 12, background: '#fff7ed', borderRadius: 6, border: '1px solid #fed7aa' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 16,
|
||||||
|
padding: 12,
|
||||||
|
background: '#fff7ed',
|
||||||
|
borderRadius: 6,
|
||||||
|
border: '1px solid #fed7aa',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span style={{ color: '#ea580c' }}>💡</span>
|
<span style={{ color: '#ea580c' }}>💡</span>
|
||||||
<span style={{ marginLeft: 8, color: '#9a3412' }}>
|
<span style={{ marginLeft: 8, color: '#9a3412' }}>
|
||||||
点击"强制接管"将断开原有连接并创建新接线。此操作不可恢复!
|
点击"强制接管"将断开原有连接并创建新接线。此操作不可恢复!
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Table, Button, Modal, Form, Input, InputNumber, Select, message, Card, Space, Popconfirm, Tag } from 'antd';
|
import {
|
||||||
|
Table,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
Select,
|
||||||
|
message,
|
||||||
|
Card,
|
||||||
|
Space,
|
||||||
|
Popconfirm,
|
||||||
|
Tag,
|
||||||
|
} from 'antd';
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
|
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
@@ -15,7 +28,7 @@ function CategoryManagement() {
|
|||||||
current: 1,
|
current: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
showTotal: (total) => `共 ${total} 条记录`
|
showTotal: total => `共 ${total} 条记录`,
|
||||||
});
|
});
|
||||||
const [keyword, setKeyword] = useState('');
|
const [keyword, setKeyword] = useState('');
|
||||||
const [status, setStatus] = useState('all');
|
const [status, setStatus] = useState('all');
|
||||||
@@ -24,7 +37,7 @@ function CategoryManagement() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await axios.get('/api/consumable-categories', {
|
const response = await axios.get('/api/consumable-categories', {
|
||||||
params: { page, pageSize, keyword, status }
|
params: { page, pageSize, keyword, status },
|
||||||
});
|
});
|
||||||
setCategories(response.data.categories);
|
setCategories(response.data.categories);
|
||||||
setPagination(prev => ({ ...prev, current: page, pageSize, total: response.data.total }));
|
setPagination(prev => ({ ...prev, current: page, pageSize, total: response.data.total }));
|
||||||
@@ -56,7 +69,7 @@ function CategoryManagement() {
|
|||||||
setEditingCategory(null);
|
setEditingCategory(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (values) => {
|
const handleSubmit = async values => {
|
||||||
try {
|
try {
|
||||||
if (editingCategory) {
|
if (editingCategory) {
|
||||||
await axios.put(`/api/consumable-categories/${editingCategory.id}`, values);
|
await axios.put(`/api/consumable-categories/${editingCategory.id}`, values);
|
||||||
@@ -69,12 +82,14 @@ function CategoryManagement() {
|
|||||||
fetchCategories();
|
fetchCategories();
|
||||||
setEditingCategory(null);
|
setEditingCategory(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error.response?.data?.error || (editingCategory ? '分类更新失败' : '分类创建失败'));
|
message.error(
|
||||||
|
error.response?.data?.error || (editingCategory ? '分类更新失败' : '分类创建失败')
|
||||||
|
);
|
||||||
console.error('提交失败:', error);
|
console.error('提交失败:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id) => {
|
const handleDelete = async id => {
|
||||||
try {
|
try {
|
||||||
await axios.delete(`/api/consumable-categories/${id}`);
|
await axios.delete(`/api/consumable-categories/${id}`);
|
||||||
message.success('删除成功');
|
message.success('删除成功');
|
||||||
@@ -90,44 +105,44 @@ function CategoryManagement() {
|
|||||||
title: 'ID',
|
title: 'ID',
|
||||||
dataIndex: 'id',
|
dataIndex: 'id',
|
||||||
key: 'id',
|
key: 'id',
|
||||||
width: 80
|
width: 80,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '分类名称',
|
title: '分类名称',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
width: 150
|
width: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '描述',
|
title: '描述',
|
||||||
dataIndex: 'description',
|
dataIndex: 'description',
|
||||||
key: 'description',
|
key: 'description',
|
||||||
width: 200,
|
width: 200,
|
||||||
render: (value) => value || '-'
|
render: value => value || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '排序',
|
title: '排序',
|
||||||
dataIndex: 'sortOrder',
|
dataIndex: 'sortOrder',
|
||||||
key: 'sortOrder',
|
key: 'sortOrder',
|
||||||
width: 80
|
width: 80,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
key: 'status',
|
key: 'status',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (value) => (
|
render: value => (
|
||||||
<Tag color={value === 'active' ? 'green' : 'red'}>
|
<Tag color={value === 'active' ? 'green' : 'red'}>
|
||||||
{value === 'active' ? '启用' : '停用'}
|
{value === 'active' ? '启用' : '停用'}
|
||||||
</Tag>
|
</Tag>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '创建时间',
|
title: '创建时间',
|
||||||
dataIndex: 'createdAt',
|
dataIndex: 'createdAt',
|
||||||
key: 'createdAt',
|
key: 'createdAt',
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (value) => value ? new Date(value).toLocaleString('zh-CN') : '-'
|
render: value => (value ? new Date(value).toLocaleString('zh-CN') : '-'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
@@ -135,26 +150,40 @@ function CategoryManagement() {
|
|||||||
width: 150,
|
width: 150,
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="primary" icon={<EditOutlined />} size="small" onClick={() => showModal(record)}>编辑</Button>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
size="small"
|
||||||
|
onClick={() => showModal(record)}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.id)}>
|
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.id)}>
|
||||||
<Button danger icon={<DeleteOutlined />} size="small">删除</Button>
|
<Button danger icon={<DeleteOutlined />} size="small">
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Space>
|
</Space>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Card title="耗材分类管理" extra={
|
<Card
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => showModal()}>添加分类</Button>
|
title="耗材分类管理"
|
||||||
}>
|
extra={
|
||||||
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => showModal()}>
|
||||||
|
添加分类
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Card size="small" style={{ marginBottom: 16 }}>
|
<Card size="small" style={{ marginBottom: 16 }}>
|
||||||
<Space>
|
<Space>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder="搜索分类名称、描述"
|
placeholder="搜索分类名称、描述"
|
||||||
style={{ width: 300 }}
|
style={{ width: 300 }}
|
||||||
onSearch={(value) => setKeyword(value)}
|
onSearch={value => setKeyword(value)}
|
||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
<Select value={status} onChange={setStatus} style={{ width: 120 }}>
|
<Select value={status} onChange={setStatus} style={{ width: 120 }}>
|
||||||
@@ -172,7 +201,7 @@ function CategoryManagement() {
|
|||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onChange={(pagination) => fetchCategories(pagination.current, pagination.pageSize)}
|
onChange={pagination => fetchCategories(pagination.current, pagination.pageSize)}
|
||||||
scroll={{ x: 1000 }}
|
scroll={{ x: 1000 }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -185,7 +214,14 @@ function CategoryManagement() {
|
|||||||
width={500}
|
width={500}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||||
<Form.Item name="name" label="分类名称" rules={[{ required: true, message: '请输入分类名称' }, { max: 50, message: '分类名称不能超过50个字符' }]}>
|
<Form.Item
|
||||||
|
name="name"
|
||||||
|
label="分类名称"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入分类名称' },
|
||||||
|
{ max: 50, message: '分类名称不能超过50个字符' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Input placeholder="请输入分类名称" />
|
<Input placeholder="请输入分类名称" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="description" label="描述">
|
<Form.Item name="description" label="描述">
|
||||||
@@ -202,7 +238,9 @@ function CategoryManagement() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="primary" htmlType="submit">{editingCategory ? '更新' : '创建'}</Button>
|
<Button type="primary" htmlType="submit">
|
||||||
|
{editingCategory ? '更新' : '创建'}
|
||||||
|
</Button>
|
||||||
<Button onClick={handleCancel}>取消</Button>
|
<Button onClick={handleCancel}>取消</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -1,6 +1,34 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Table, Card, Space, Select, DatePicker, Input, Tag, Button, message, Modal, Upload, Radio, Dropdown, Form, Tooltip, Timeline } from 'antd';
|
import {
|
||||||
import { HistoryOutlined, SearchOutlined, FileTextOutlined, DownloadOutlined, UploadOutlined, FileExcelOutlined, FileOutlined, DownOutlined, EditOutlined, EyeOutlined } from '@ant-design/icons';
|
Table,
|
||||||
|
Card,
|
||||||
|
Space,
|
||||||
|
Select,
|
||||||
|
DatePicker,
|
||||||
|
Input,
|
||||||
|
Tag,
|
||||||
|
Button,
|
||||||
|
message,
|
||||||
|
Modal,
|
||||||
|
Upload,
|
||||||
|
Radio,
|
||||||
|
Dropdown,
|
||||||
|
Form,
|
||||||
|
Tooltip,
|
||||||
|
Timeline,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
HistoryOutlined,
|
||||||
|
SearchOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
|
UploadOutlined,
|
||||||
|
FileExcelOutlined,
|
||||||
|
FileOutlined,
|
||||||
|
DownOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
@@ -15,7 +43,7 @@ function ConsumableLogs() {
|
|||||||
const [filters, setFilters] = useState({
|
const [filters, setFilters] = useState({
|
||||||
operationType: 'all',
|
operationType: 'all',
|
||||||
consumableId: '',
|
consumableId: '',
|
||||||
dateRange: null
|
dateRange: null,
|
||||||
});
|
});
|
||||||
const [importModalVisible, setImportModalVisible] = useState(false);
|
const [importModalVisible, setImportModalVisible] = useState(false);
|
||||||
const [importType, setImportType] = useState('excel');
|
const [importType, setImportType] = useState('excel');
|
||||||
@@ -65,7 +93,7 @@ function ConsumableLogs() {
|
|||||||
fetchLogs(1, pagination.pageSize);
|
fetchLogs(1, pagination.pageSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOperationTag = (type) => {
|
const getOperationTag = type => {
|
||||||
const config = {
|
const config = {
|
||||||
in: { color: 'green', text: '入库' },
|
in: { color: 'green', text: '入库' },
|
||||||
out: { color: 'red', text: '出库' },
|
out: { color: 'red', text: '出库' },
|
||||||
@@ -73,7 +101,7 @@ function ConsumableLogs() {
|
|||||||
update: { color: 'orange', text: '更新' },
|
update: { color: 'orange', text: '更新' },
|
||||||
delete: { color: 'magenta', text: '删除' },
|
delete: { color: 'magenta', text: '删除' },
|
||||||
adjust: { color: 'purple', text: '调整' },
|
adjust: { color: 'purple', text: '调整' },
|
||||||
import: { color: 'cyan', text: '导入' }
|
import: { color: 'cyan', text: '导入' },
|
||||||
};
|
};
|
||||||
const { color, text } = config[type] || { color: 'default', text: type };
|
const { color, text } = config[type] || { color: 'default', text: type };
|
||||||
return <Tag color={color}>{text}</Tag>;
|
return <Tag color={color}>{text}</Tag>;
|
||||||
@@ -86,27 +114,27 @@ function ConsumableLogs() {
|
|||||||
key: 'createdAt',
|
key: 'createdAt',
|
||||||
width: 180,
|
width: 180,
|
||||||
sorter: (a, b) => new Date(a.createdAt) - new Date(b.createdAt),
|
sorter: (a, b) => new Date(a.createdAt) - new Date(b.createdAt),
|
||||||
render: (date) => dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
render: date => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '耗材ID',
|
title: '耗材ID',
|
||||||
dataIndex: 'consumableId',
|
dataIndex: 'consumableId',
|
||||||
key: 'consumableId',
|
key: 'consumableId',
|
||||||
width: 150,
|
width: 150,
|
||||||
render: (value) => <code>{value}</code>
|
render: value => <code>{value}</code>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '耗材名称',
|
title: '耗材名称',
|
||||||
dataIndex: 'consumableName',
|
dataIndex: 'consumableName',
|
||||||
key: 'consumableName',
|
key: 'consumableName',
|
||||||
width: 150
|
width: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作类型',
|
title: '操作类型',
|
||||||
dataIndex: 'operationType',
|
dataIndex: 'operationType',
|
||||||
key: 'operationType',
|
key: 'operationType',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (type) => getOperationTag(type)
|
render: type => getOperationTag(type),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '变动数量',
|
title: '变动数量',
|
||||||
@@ -114,46 +142,49 @@ function ConsumableLogs() {
|
|||||||
key: 'quantity',
|
key: 'quantity',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (value, record) => (
|
render: (value, record) => (
|
||||||
<span style={{
|
<span
|
||||||
color: value > 0 ? '#52c41a' : value < 0 ? '#ff4d4f' : '#888',
|
style={{
|
||||||
fontWeight: 'bold'
|
color: value > 0 ? '#52c41a' : value < 0 ? '#ff4d4f' : '#888',
|
||||||
}}>
|
fontWeight: 'bold',
|
||||||
{value > 0 ? '+' : ''}{value}
|
}}
|
||||||
|
>
|
||||||
|
{value > 0 ? '+' : ''}
|
||||||
|
{value}
|
||||||
</span>
|
</span>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作前库存',
|
title: '操作前库存',
|
||||||
dataIndex: 'previousStock',
|
dataIndex: 'previousStock',
|
||||||
key: 'previousStock',
|
key: 'previousStock',
|
||||||
width: 100
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作后库存',
|
title: '操作后库存',
|
||||||
dataIndex: 'currentStock',
|
dataIndex: 'currentStock',
|
||||||
key: 'currentStock',
|
key: 'currentStock',
|
||||||
width: 100
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作人',
|
title: '操作人',
|
||||||
dataIndex: 'operator',
|
dataIndex: 'operator',
|
||||||
key: 'operator',
|
key: 'operator',
|
||||||
width: 120
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '原因',
|
title: '原因',
|
||||||
dataIndex: 'reason',
|
dataIndex: 'reason',
|
||||||
key: 'reason',
|
key: 'reason',
|
||||||
width: 150,
|
width: 150,
|
||||||
render: (value) => value || '-'
|
render: value => value || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '备注',
|
title: '备注',
|
||||||
dataIndex: 'notes',
|
dataIndex: 'notes',
|
||||||
key: 'notes',
|
key: 'notes',
|
||||||
width: 200,
|
width: 200,
|
||||||
render: (value) => value || '-',
|
render: value => value || '-',
|
||||||
ellipsis: true
|
ellipsis: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
@@ -181,8 +212,8 @@ function ConsumableLogs() {
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleExport = async (currentFilters = filters) => {
|
const handleExport = async (currentFilters = filters) => {
|
||||||
@@ -201,7 +232,7 @@ function ConsumableLogs() {
|
|||||||
|
|
||||||
const response = await axios.get('/api/consumables/logs/export', {
|
const response = await axios.get('/api/consumables/logs/export', {
|
||||||
params,
|
params,
|
||||||
responseType: 'blob'
|
responseType: 'blob',
|
||||||
});
|
});
|
||||||
|
|
||||||
const blob = new Blob([response.data], { type: 'text/csv;charset=utf-8;' });
|
const blob = new Blob([response.data], { type: 'text/csv;charset=utf-8;' });
|
||||||
@@ -232,20 +263,20 @@ function ConsumableLogs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await axios.get('/api/consumables/logs', {
|
const response = await axios.get('/api/consumables/logs', {
|
||||||
params: { ...params, page: 1, pageSize: 10000 }
|
params: { ...params, page: 1, pageSize: 10000 },
|
||||||
});
|
});
|
||||||
|
|
||||||
const exportData = response.data.logs.map(log => ({
|
const exportData = response.data.logs.map(log => ({
|
||||||
'时间': dayjs(log.createdAt).format('YYYY-MM-DD HH:mm:ss'),
|
时间: dayjs(log.createdAt).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
'耗材ID': log.consumableId,
|
耗材ID: log.consumableId,
|
||||||
'耗材名称': log.consumableName,
|
耗材名称: log.consumableName,
|
||||||
'操作类型': getOperationTypeText(log.operationType),
|
操作类型: getOperationTypeText(log.operationType),
|
||||||
'变动数量': log.quantity,
|
变动数量: log.quantity,
|
||||||
'操作前库存': log.previousStock,
|
操作前库存: log.previousStock,
|
||||||
'操作后库存': log.currentStock,
|
操作后库存: log.currentStock,
|
||||||
'操作人': log.operator,
|
操作人: log.operator,
|
||||||
'原因': log.reason || '',
|
原因: log.reason || '',
|
||||||
'备注': log.notes || ''
|
备注: log.notes || '',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const ws = XLSX.utils.json_to_sheet(exportData);
|
const ws = XLSX.utils.json_to_sheet(exportData);
|
||||||
@@ -260,24 +291,24 @@ function ConsumableLogs() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOperationTypeText = (type) => {
|
const getOperationTypeText = type => {
|
||||||
const map = {
|
const map = {
|
||||||
'in': '入库',
|
in: '入库',
|
||||||
'out': '出库',
|
out: '出库',
|
||||||
'create': '创建',
|
create: '创建',
|
||||||
'update': '更新',
|
update: '更新',
|
||||||
'delete': '删除',
|
delete: '删除',
|
||||||
'adjust': '调整',
|
adjust: '调整',
|
||||||
'import': '导入'
|
import: '导入',
|
||||||
};
|
};
|
||||||
return map[type] || type;
|
return map[type] || type;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImport = async (file) => {
|
const handleImport = async file => {
|
||||||
setImporting(true);
|
setImporting(true);
|
||||||
try {
|
try {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = async (e) => {
|
reader.onload = async e => {
|
||||||
try {
|
try {
|
||||||
let logItems = [];
|
let logItems = [];
|
||||||
|
|
||||||
@@ -303,7 +334,7 @@ function ConsumableLogs() {
|
|||||||
|
|
||||||
const response = await axios.post('/api/consumables/logs/import', {
|
const response = await axios.post('/api/consumables/logs/import', {
|
||||||
logs: logItems,
|
logs: logItems,
|
||||||
operator: '前端导入'
|
operator: '前端导入',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.success > 0) {
|
if (response.data.success > 0) {
|
||||||
@@ -338,16 +369,16 @@ function ConsumableLogs() {
|
|||||||
const downloadTemplate = () => {
|
const downloadTemplate = () => {
|
||||||
const template = [
|
const template = [
|
||||||
{
|
{
|
||||||
'耗材ID': 'CON123456',
|
耗材ID: 'CON123456',
|
||||||
'耗材名称': '示例耗材',
|
耗材名称: '示例耗材',
|
||||||
'操作类型': '入库',
|
操作类型: '入库',
|
||||||
'变动数量': 10,
|
变动数量: 10,
|
||||||
'操作前库存': 100,
|
操作前库存: 100,
|
||||||
'操作后库存': 110,
|
操作后库存: 110,
|
||||||
'操作人': '管理员',
|
操作人: '管理员',
|
||||||
'原因': '示例原因',
|
原因: '示例原因',
|
||||||
'备注': '示例备注'
|
备注: '示例备注',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const ws = XLSX.utils.json_to_sheet(template);
|
const ws = XLSX.utils.json_to_sheet(template);
|
||||||
@@ -359,18 +390,18 @@ function ConsumableLogs() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 编辑日志
|
// 编辑日志
|
||||||
const handleEdit = (record) => {
|
const handleEdit = record => {
|
||||||
setCurrentLog(record);
|
setCurrentLog(record);
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
reason: record.reason,
|
reason: record.reason,
|
||||||
notes: record.notes,
|
notes: record.notes,
|
||||||
modificationReason: ''
|
modificationReason: '',
|
||||||
});
|
});
|
||||||
setEditModalVisible(true);
|
setEditModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 提交编辑
|
// 提交编辑
|
||||||
const handleEditSubmit = async (values) => {
|
const handleEditSubmit = async values => {
|
||||||
if (!currentLog) return;
|
if (!currentLog) return;
|
||||||
|
|
||||||
setEditLoading(true);
|
setEditLoading(true);
|
||||||
@@ -379,7 +410,7 @@ function ConsumableLogs() {
|
|||||||
reason: values.reason,
|
reason: values.reason,
|
||||||
notes: values.notes,
|
notes: values.notes,
|
||||||
operator: values.operator || '管理员',
|
operator: values.operator || '管理员',
|
||||||
modificationReason: values.modificationReason
|
modificationReason: values.modificationReason,
|
||||||
});
|
});
|
||||||
|
|
||||||
message.success('日志修改成功');
|
message.success('日志修改成功');
|
||||||
@@ -393,7 +424,7 @@ function ConsumableLogs() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 查看修改历史
|
// 查看修改历史
|
||||||
const handleViewHistory = async (record) => {
|
const handleViewHistory = async record => {
|
||||||
setCurrentLog(record);
|
setCurrentLog(record);
|
||||||
setHistoryModalVisible(true);
|
setHistoryModalVisible(true);
|
||||||
setHistoryLoading(true);
|
setHistoryLoading(true);
|
||||||
@@ -425,12 +456,12 @@ function ConsumableLogs() {
|
|||||||
placeholder="搜索耗材ID"
|
placeholder="搜索耗材ID"
|
||||||
style={{ width: 200 }}
|
style={{ width: 200 }}
|
||||||
allowClear
|
allowClear
|
||||||
onSearch={(value) => handleFilterChange('consumableId', value)}
|
onSearch={value => handleFilterChange('consumableId', value)}
|
||||||
prefix={<SearchOutlined />}
|
prefix={<SearchOutlined />}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={filters.operationType}
|
value={filters.operationType}
|
||||||
onChange={(value) => handleFilterChange('operationType', value)}
|
onChange={value => handleFilterChange('operationType', value)}
|
||||||
style={{ width: 120 }}
|
style={{ width: 120 }}
|
||||||
>
|
>
|
||||||
<Option value="all">全部类型</Option>
|
<Option value="all">全部类型</Option>
|
||||||
@@ -444,7 +475,7 @@ function ConsumableLogs() {
|
|||||||
</Select>
|
</Select>
|
||||||
<RangePicker
|
<RangePicker
|
||||||
value={filters.dateRange}
|
value={filters.dateRange}
|
||||||
onChange={(dates) => handleFilterChange('dateRange', dates)}
|
onChange={dates => handleFilterChange('dateRange', dates)}
|
||||||
placeholder={['开始日期', '结束日期']}
|
placeholder={['开始日期', '结束日期']}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@@ -463,15 +494,15 @@ function ConsumableLogs() {
|
|||||||
key: 'csv',
|
key: 'csv',
|
||||||
icon: <FileOutlined />,
|
icon: <FileOutlined />,
|
||||||
label: '导出CSV',
|
label: '导出CSV',
|
||||||
onClick: () => handleExport(filters)
|
onClick: () => handleExport(filters),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'excel',
|
key: 'excel',
|
||||||
icon: <FileExcelOutlined />,
|
icon: <FileExcelOutlined />,
|
||||||
label: '导出Excel',
|
label: '导出Excel',
|
||||||
onClick: () => handleExportExcel(filters)
|
onClick: () => handleExportExcel(filters),
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button icon={<DownloadOutlined />}>
|
<Button icon={<DownloadOutlined />}>
|
||||||
@@ -491,11 +522,11 @@ function ConsumableLogs() {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{
|
pagination={{
|
||||||
...pagination,
|
...pagination,
|
||||||
showTotal: (total) => `共 ${total} 条记录`,
|
showTotal: total => `共 ${total} 条记录`,
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
showQuickJumper: true
|
showQuickJumper: true,
|
||||||
}}
|
}}
|
||||||
onChange={(pagination) => fetchLogs(pagination.current, pagination.pageSize)}
|
onChange={pagination => fetchLogs(pagination.current, pagination.pageSize)}
|
||||||
scroll={{ x: 1500 }}
|
scroll={{ x: 1500 }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -513,7 +544,7 @@ function ConsumableLogs() {
|
|||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
value={importType}
|
value={importType}
|
||||||
onChange={(e) => setImportType(e.target.value)}
|
onChange={e => setImportType(e.target.value)}
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
>
|
>
|
||||||
<Radio.Button value="excel">Excel文件</Radio.Button>
|
<Radio.Button value="excel">Excel文件</Radio.Button>
|
||||||
@@ -561,21 +592,11 @@ function ConsumableLogs() {
|
|||||||
confirmLoading={editLoading}
|
confirmLoading={editLoading}
|
||||||
width={600}
|
width={600}
|
||||||
>
|
>
|
||||||
<Form
|
<Form form={form} layout="vertical" onFinish={handleEditSubmit}>
|
||||||
form={form}
|
<Form.Item label="操作原因" name="reason">
|
||||||
layout="vertical"
|
|
||||||
onFinish={handleEditSubmit}
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
label="操作原因"
|
|
||||||
name="reason"
|
|
||||||
>
|
|
||||||
<Input.TextArea rows={2} placeholder="请输入操作原因" />
|
<Input.TextArea rows={2} placeholder="请输入操作原因" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label="备注" name="notes">
|
||||||
label="备注"
|
|
||||||
name="notes"
|
|
||||||
>
|
|
||||||
<Input.TextArea rows={3} placeholder="请输入备注信息" />
|
<Input.TextArea rows={3} placeholder="请输入备注信息" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -585,10 +606,7 @@ function ConsumableLogs() {
|
|||||||
>
|
>
|
||||||
<Input.TextArea rows={2} placeholder="请输入修改原因(必填)" />
|
<Input.TextArea rows={2} placeholder="请输入修改原因(必填)" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label="修改人" name="operator">
|
||||||
label="修改人"
|
|
||||||
name="operator"
|
|
||||||
>
|
|
||||||
<Input placeholder="请输入修改人姓名" />
|
<Input placeholder="请输入修改人姓名" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -623,21 +641,38 @@ function ConsumableLogs() {
|
|||||||
<Tag color={getOperationTag(item.operationType).props.color}>
|
<Tag color={getOperationTag(item.operationType).props.color}>
|
||||||
{getOperationTag(item.operationType).props.children}
|
{getOperationTag(item.operationType).props.children}
|
||||||
</Tag>
|
</Tag>
|
||||||
{item.modifiedBy && (
|
{item.modifiedBy && <Tag color="orange">已修改</Tag>}
|
||||||
<Tag color="orange">已修改</Tag>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 12, color: '#666' }}>
|
<div style={{ fontSize: 12, color: '#666' }}>
|
||||||
<p><strong>耗材:</strong> {item.consumableName} ({item.consumableId})</p>
|
<p>
|
||||||
<p><strong>操作人:</strong> {item.operator}</p>
|
<strong>耗材:</strong> {item.consumableName} ({item.consumableId})
|
||||||
{item.reason && <p><strong>原因:</strong> {item.reason}</p>}
|
</p>
|
||||||
{item.notes && <p><strong>备注:</strong> {item.notes}</p>}
|
<p>
|
||||||
|
<strong>操作人:</strong> {item.operator}
|
||||||
|
</p>
|
||||||
|
{item.reason && (
|
||||||
|
<p>
|
||||||
|
<strong>原因:</strong> {item.reason}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{item.notes && (
|
||||||
|
<p>
|
||||||
|
<strong>备注:</strong> {item.notes}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{item.modifiedBy && (
|
{item.modifiedBy && (
|
||||||
<>
|
<>
|
||||||
<p><strong>修改人:</strong> {item.modifiedBy}</p>
|
<p>
|
||||||
<p><strong>修改时间:</strong> {dayjs(item.modifiedAt).format('YYYY-MM-DD HH:mm:ss')}</p>
|
<strong>修改人:</strong> {item.modifiedBy}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>修改时间:</strong>{' '}
|
||||||
|
{dayjs(item.modifiedAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</p>
|
||||||
{item.modificationReason && (
|
{item.modificationReason && (
|
||||||
<p><strong>修改原因:</strong> {item.modificationReason}</p>
|
<p>
|
||||||
|
<strong>修改原因:</strong> {item.modificationReason}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,32 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||||
import { Table, Button, Modal, Form, Input, Select, InputNumber, message, Card, Space, Popconfirm, Upload, Table as AntTable, Progress, Checkbox } from 'antd';
|
import {
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ExportOutlined, ImportOutlined, UploadOutlined, FileExcelOutlined, InboxOutlined } from '@ant-design/icons';
|
Table,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
Card,
|
||||||
|
Space,
|
||||||
|
Popconfirm,
|
||||||
|
Upload,
|
||||||
|
Table as AntTable,
|
||||||
|
Progress,
|
||||||
|
Checkbox,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
SearchOutlined,
|
||||||
|
ExportOutlined,
|
||||||
|
ImportOutlined,
|
||||||
|
UploadOutlined,
|
||||||
|
FileExcelOutlined,
|
||||||
|
InboxOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
@@ -16,7 +42,7 @@ function ConsumableManagement() {
|
|||||||
current: 1,
|
current: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
showTotal: (total) => `共 ${total} 条记录`
|
showTotal: total => `共 ${total} 条记录`,
|
||||||
});
|
});
|
||||||
const [keyword, setKeyword] = useState('');
|
const [keyword, setKeyword] = useState('');
|
||||||
const [category, setCategory] = useState('all');
|
const [category, setCategory] = useState('all');
|
||||||
@@ -34,21 +60,24 @@ function ConsumableManagement() {
|
|||||||
const [stockForm] = Form.useForm();
|
const [stockForm] = Form.useForm();
|
||||||
const [maxStockUnlimited, setMaxStockUnlimited] = useState(false);
|
const [maxStockUnlimited, setMaxStockUnlimited] = useState(false);
|
||||||
|
|
||||||
const fetchConsumables = useCallback(async (page = 1, pageSize = 10) => {
|
const fetchConsumables = useCallback(
|
||||||
try {
|
async (page = 1, pageSize = 10) => {
|
||||||
setLoading(true);
|
try {
|
||||||
const response = await axios.get('/api/consumables', {
|
setLoading(true);
|
||||||
params: { page, pageSize, keyword, category, status }
|
const response = await axios.get('/api/consumables', {
|
||||||
});
|
params: { page, pageSize, keyword, category, status },
|
||||||
setConsumables(response.data.consumables);
|
});
|
||||||
setPagination(prev => ({ ...prev, current: page, pageSize, total: response.data.total }));
|
setConsumables(response.data.consumables);
|
||||||
} catch (error) {
|
setPagination(prev => ({ ...prev, current: page, pageSize, total: response.data.total }));
|
||||||
message.error('获取耗材列表失败');
|
} catch (error) {
|
||||||
console.error('获取耗材列表失败:', error);
|
message.error('获取耗材列表失败');
|
||||||
} finally {
|
console.error('获取耗材列表失败:', error);
|
||||||
setLoading(false);
|
} finally {
|
||||||
}
|
setLoading(false);
|
||||||
}, [keyword, category, status]);
|
}
|
||||||
|
},
|
||||||
|
[keyword, category, status]
|
||||||
|
);
|
||||||
|
|
||||||
const fetchCategories = useCallback(async () => {
|
const fetchCategories = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -64,92 +93,132 @@ function ConsumableManagement() {
|
|||||||
fetchCategories();
|
fetchCategories();
|
||||||
}, [fetchConsumables, fetchCategories]);
|
}, [fetchConsumables, fetchCategories]);
|
||||||
|
|
||||||
const showModal = useCallback((consumable = null) => {
|
const showModal = useCallback(
|
||||||
setEditingConsumable(consumable);
|
(consumable = null) => {
|
||||||
if (consumable) {
|
setEditingConsumable(consumable);
|
||||||
const isUnlimited = consumable.maxStock === 0 || consumable.maxStock === null || consumable.maxStock === undefined;
|
if (consumable) {
|
||||||
setMaxStockUnlimited(isUnlimited);
|
const isUnlimited =
|
||||||
form.setFieldsValue({
|
consumable.maxStock === 0 ||
|
||||||
...consumable,
|
consumable.maxStock === null ||
|
||||||
maxStock: isUnlimited ? undefined : consumable.maxStock
|
consumable.maxStock === undefined;
|
||||||
});
|
setMaxStockUnlimited(isUnlimited);
|
||||||
} else {
|
form.setFieldsValue({
|
||||||
setMaxStockUnlimited(true);
|
...consumable,
|
||||||
form.resetFields();
|
maxStock: isUnlimited ? undefined : consumable.maxStock,
|
||||||
form.setFieldsValue({
|
});
|
||||||
unit: '个',
|
} else {
|
||||||
currentStock: 0,
|
setMaxStockUnlimited(true);
|
||||||
minStock: 0,
|
form.resetFields();
|
||||||
status: 'active',
|
form.setFieldsValue({
|
||||||
unitPrice: 0
|
unit: '个',
|
||||||
});
|
currentStock: 0,
|
||||||
}
|
minStock: 0,
|
||||||
setModalVisible(true);
|
status: 'active',
|
||||||
}, [form]);
|
unitPrice: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setModalVisible(true);
|
||||||
|
},
|
||||||
|
[form]
|
||||||
|
);
|
||||||
|
|
||||||
const handleCancel = useCallback(() => {
|
const handleCancel = useCallback(() => {
|
||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
setEditingConsumable(null);
|
setEditingConsumable(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSubmit = useCallback(async (values) => {
|
const handleSubmit = useCallback(
|
||||||
try {
|
async values => {
|
||||||
const submitData = {
|
try {
|
||||||
...values,
|
const submitData = {
|
||||||
maxStock: maxStockUnlimited ? 0 : values.maxStock,
|
...values,
|
||||||
unitPrice: values.unitPrice || 0
|
maxStock: maxStockUnlimited ? 0 : values.maxStock,
|
||||||
};
|
unitPrice: values.unitPrice || 0,
|
||||||
if (editingConsumable) {
|
};
|
||||||
await axios.put(`/api/consumables/${editingConsumable.consumableId}`, submitData);
|
if (editingConsumable) {
|
||||||
message.success('耗材更新成功');
|
await axios.put(`/api/consumables/${editingConsumable.consumableId}`, submitData);
|
||||||
} else {
|
message.success('耗材更新成功');
|
||||||
await axios.post('/api/consumables', {
|
} else {
|
||||||
...submitData,
|
await axios.post('/api/consumables', {
|
||||||
consumableId: `CON${Date.now()}`
|
...submitData,
|
||||||
});
|
consumableId: `CON${Date.now()}`,
|
||||||
message.success('耗材创建成功');
|
});
|
||||||
|
message.success('耗材创建成功');
|
||||||
|
}
|
||||||
|
setModalVisible(false);
|
||||||
|
fetchConsumables();
|
||||||
|
setEditingConsumable(null);
|
||||||
|
} catch (error) {
|
||||||
|
message.error(editingConsumable ? '耗材更新失败' : '耗材创建失败');
|
||||||
|
console.error('提交失败:', error);
|
||||||
}
|
}
|
||||||
setModalVisible(false);
|
},
|
||||||
fetchConsumables();
|
[editingConsumable, fetchConsumables, maxStockUnlimited]
|
||||||
setEditingConsumable(null);
|
);
|
||||||
} catch (error) {
|
|
||||||
message.error(editingConsumable ? '耗材更新失败' : '耗材创建失败');
|
|
||||||
console.error('提交失败:', error);
|
|
||||||
}
|
|
||||||
}, [editingConsumable, fetchConsumables, maxStockUnlimited]);
|
|
||||||
|
|
||||||
const handleDelete = useCallback(async (consumableId) => {
|
const handleDelete = useCallback(
|
||||||
try {
|
async consumableId => {
|
||||||
await axios.delete(`/api/consumables/${consumableId}`);
|
try {
|
||||||
message.success('删除成功');
|
await axios.delete(`/api/consumables/${consumableId}`);
|
||||||
fetchConsumables();
|
message.success('删除成功');
|
||||||
} catch (error) {
|
fetchConsumables();
|
||||||
message.error('删除失败');
|
} catch (error) {
|
||||||
console.error('删除失败:', error);
|
message.error('删除失败');
|
||||||
}
|
console.error('删除失败:', error);
|
||||||
}, [fetchConsumables]);
|
}
|
||||||
|
},
|
||||||
|
[fetchConsumables]
|
||||||
|
);
|
||||||
|
|
||||||
const handleSearch = useCallback((value) => {
|
const handleSearch = useCallback(value => {
|
||||||
setKeyword(value);
|
setKeyword(value);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const exportToCSV = (data, filename) => {
|
const exportToCSV = (data, filename) => {
|
||||||
const headers = ['耗材ID', '名称', '分类', '单位', '当前库存', '最小库存', '最大库存', '单价', '供应商', '存放位置', '状态'];
|
const headers = [
|
||||||
const keys = ['consumableId', 'name', 'category', 'unit', 'currentStock', 'minStock', 'maxStock', 'unitPrice', 'supplier', 'location', 'status'];
|
'耗材ID',
|
||||||
|
'名称',
|
||||||
|
'分类',
|
||||||
|
'单位',
|
||||||
|
'当前库存',
|
||||||
|
'最小库存',
|
||||||
|
'最大库存',
|
||||||
|
'单价',
|
||||||
|
'供应商',
|
||||||
|
'存放位置',
|
||||||
|
'状态',
|
||||||
|
];
|
||||||
|
const keys = [
|
||||||
|
'consumableId',
|
||||||
|
'name',
|
||||||
|
'category',
|
||||||
|
'unit',
|
||||||
|
'currentStock',
|
||||||
|
'minStock',
|
||||||
|
'maxStock',
|
||||||
|
'unitPrice',
|
||||||
|
'supplier',
|
||||||
|
'location',
|
||||||
|
'status',
|
||||||
|
];
|
||||||
|
|
||||||
const csvContent = [
|
const csvContent = [
|
||||||
headers.join(','),
|
headers.join(','),
|
||||||
...data.map(row => keys.map(key => {
|
...data.map(row =>
|
||||||
let value = row[key];
|
keys
|
||||||
if (key === 'unitPrice') value = `¥${parseFloat(value || 0).toFixed(2)}`;
|
.map(key => {
|
||||||
if (key === 'status') value = value === 'active' ? '启用' : '停用';
|
let value = row[key];
|
||||||
if (value === null || value === undefined) value = '';
|
if (key === 'unitPrice') value = `¥${parseFloat(value || 0).toFixed(2)}`;
|
||||||
const str = String(value);
|
if (key === 'status') value = value === 'active' ? '启用' : '停用';
|
||||||
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
if (value === null || value === undefined) value = '';
|
||||||
return `"${str.replace(/"/g, '""')}"`;
|
const str = String(value);
|
||||||
}
|
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
||||||
return str;
|
return `"${str.replace(/"/g, '""')}"`;
|
||||||
}).join(','))
|
}
|
||||||
|
return str;
|
||||||
|
})
|
||||||
|
.join(',')
|
||||||
|
),
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
@@ -166,7 +235,7 @@ function ConsumableManagement() {
|
|||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/consumables', {
|
const response = await axios.get('/api/consumables', {
|
||||||
params: { keyword, category, status, pageSize: 1000 }
|
params: { keyword, category, status, pageSize: 1000 },
|
||||||
});
|
});
|
||||||
const consumables = response.data.consumables;
|
const consumables = response.data.consumables;
|
||||||
exportToCSV(consumables, `consumables_${new Date().toISOString().split('T')[0]}.csv`);
|
exportToCSV(consumables, `consumables_${new Date().toISOString().split('T')[0]}.csv`);
|
||||||
@@ -177,7 +246,7 @@ function ConsumableManagement() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseCSV = (text) => {
|
const parseCSV = text => {
|
||||||
const lines = text.trim().split('\n');
|
const lines = text.trim().split('\n');
|
||||||
if (lines.length < 2) return [];
|
if (lines.length < 2) return [];
|
||||||
|
|
||||||
@@ -215,11 +284,11 @@ function ConsumableManagement() {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileChange = (info) => {
|
const handleFileChange = info => {
|
||||||
const file = info.fileList[info.fileList.length - 1];
|
const file = info.fileList[info.fileList.length - 1];
|
||||||
if (file && file.originFileObj) {
|
if (file && file.originFileObj) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = e => {
|
||||||
const text = e.target.result;
|
const text = e.target.result;
|
||||||
const parsedData = parseCSV(text);
|
const parsedData = parseCSV(text);
|
||||||
setImportPreview(parsedData.slice(0, 10));
|
setImportPreview(parsedData.slice(0, 10));
|
||||||
@@ -257,7 +326,7 @@ function ConsumableManagement() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = async (e) => {
|
reader.onload = async e => {
|
||||||
const text = e.target.result;
|
const text = e.target.result;
|
||||||
setImportProgress(10);
|
setImportProgress(10);
|
||||||
setImportPhase('正在读取文件...');
|
setImportPhase('正在读取文件...');
|
||||||
@@ -317,7 +386,7 @@ function ConsumableManagement() {
|
|||||||
imported: 0,
|
imported: 0,
|
||||||
failed: 0,
|
failed: 0,
|
||||||
errors: [{ row: '-', error: '文件读取失败,请检查文件是否损坏' }],
|
errors: [{ row: '-', error: '文件读取失败,请检查文件是否损坏' }],
|
||||||
message: '文件读取失败'
|
message: '文件读取失败',
|
||||||
});
|
});
|
||||||
message.error('文件读取失败');
|
message.error('文件读取失败');
|
||||||
};
|
};
|
||||||
@@ -335,7 +404,7 @@ function ConsumableManagement() {
|
|||||||
if (data.errors && Array.isArray(data.errors) && data.errors.length > 0) {
|
if (data.errors && Array.isArray(data.errors) && data.errors.length > 0) {
|
||||||
errorDetails = data.errors.map((err, index) => ({
|
errorDetails = data.errors.map((err, index) => ({
|
||||||
row: err.row || index + 1,
|
row: err.row || index + 1,
|
||||||
error: err.error || err.message || '数据格式错误'
|
error: err.error || err.message || '数据格式错误',
|
||||||
}));
|
}));
|
||||||
errorMessage = `导入失败,共发现 ${errorDetails.length} 处数据错误`;
|
errorMessage = `导入失败,共发现 ${errorDetails.length} 处数据错误`;
|
||||||
} else if (data.message) {
|
} else if (data.message) {
|
||||||
@@ -359,7 +428,7 @@ function ConsumableManagement() {
|
|||||||
imported: 0,
|
imported: 0,
|
||||||
failed: 0,
|
failed: 0,
|
||||||
errors: errorDetails,
|
errors: errorDetails,
|
||||||
message: errorMessage
|
message: errorMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
message.error(errorMessage);
|
message.error(errorMessage);
|
||||||
@@ -368,7 +437,8 @@ function ConsumableManagement() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const downloadTemplate = () => {
|
const downloadTemplate = () => {
|
||||||
const template = '耗材ID,名称,分类,单位,当前库存,最小库存,最大库存,单价,供应商,存放位置,描述,状态\n,测试耗材,办公用品,个,100,10,500,5.00,XX公司,A柜-01层,测试数据,active';
|
const template =
|
||||||
|
'耗材ID,名称,分类,单位,当前库存,最小库存,最大库存,单价,供应商,存放位置,描述,状态\n,测试耗材,办公用品,个,100,10,500,5.00,XX公司,A柜-01层,测试数据,active';
|
||||||
const blob = new Blob([template], { type: 'text/csv;charset=utf-8;' });
|
const blob = new Blob([template], { type: 'text/csv;charset=utf-8;' });
|
||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
@@ -380,163 +450,208 @@ function ConsumableManagement() {
|
|||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showStockModal = useCallback((record, type) => {
|
const showStockModal = useCallback(
|
||||||
setStockRecord(record);
|
(record, type) => {
|
||||||
setStockType(type);
|
setStockRecord(record);
|
||||||
stockForm.setFieldsValue({
|
setStockType(type);
|
||||||
consumableId: record.consumableId,
|
stockForm.setFieldsValue({
|
||||||
consumableName: record.name,
|
consumableId: record.consumableId,
|
||||||
quantity: 1,
|
consumableName: record.name,
|
||||||
reason: '',
|
quantity: 1,
|
||||||
notes: ''
|
reason: '',
|
||||||
});
|
notes: '',
|
||||||
setStockModalVisible(true);
|
});
|
||||||
}, [stockForm]);
|
setStockModalVisible(true);
|
||||||
|
},
|
||||||
|
[stockForm]
|
||||||
|
);
|
||||||
|
|
||||||
const handleStockCancel = useCallback(() => {
|
const handleStockCancel = useCallback(() => {
|
||||||
setStockModalVisible(false);
|
setStockModalVisible(false);
|
||||||
setStockRecord(null);
|
setStockRecord(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleStockSubmit = useCallback(async (values) => {
|
const handleStockSubmit = useCallback(
|
||||||
try {
|
async values => {
|
||||||
const response = await axios.post('/api/consumables/quick-inout', {
|
try {
|
||||||
consumableId: stockRecord.consumableId,
|
const response = await axios.post('/api/consumables/quick-inout', {
|
||||||
type: stockType,
|
consumableId: stockRecord.consumableId,
|
||||||
quantity: values.quantity,
|
type: stockType,
|
||||||
operator: values.operator || '系统管理员',
|
quantity: values.quantity,
|
||||||
reason: values.reason,
|
operator: values.operator || '系统管理员',
|
||||||
notes: values.notes
|
reason: values.reason,
|
||||||
});
|
notes: values.notes,
|
||||||
message.success(`${stockType === 'in' ? '入库' : '出库'}操作成功`);
|
});
|
||||||
setStockModalVisible(false);
|
message.success(`${stockType === 'in' ? '入库' : '出库'}操作成功`);
|
||||||
fetchConsumables();
|
setStockModalVisible(false);
|
||||||
} catch (error) {
|
fetchConsumables();
|
||||||
message.error(error.response?.data?.error || `${stockType === 'in' ? '入库' : '出库'}操作失败`);
|
} catch (error) {
|
||||||
console.error('操作失败:', error);
|
message.error(
|
||||||
}
|
error.response?.data?.error || `${stockType === 'in' ? '入库' : '出库'}操作失败`
|
||||||
}, [stockRecord, stockType, fetchConsumables]);
|
|
||||||
|
|
||||||
const columns = useMemo(() => [
|
|
||||||
{
|
|
||||||
title: '名称',
|
|
||||||
dataIndex: 'name',
|
|
||||||
key: 'name',
|
|
||||||
width: 150
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '分类',
|
|
||||||
dataIndex: 'category',
|
|
||||||
key: 'category',
|
|
||||||
width: 120
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '单位',
|
|
||||||
dataIndex: 'unit',
|
|
||||||
key: 'unit',
|
|
||||||
width: 80
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '当前库存',
|
|
||||||
dataIndex: 'currentStock',
|
|
||||||
key: 'currentStock',
|
|
||||||
width: 100,
|
|
||||||
render: (value, record) => {
|
|
||||||
const isLow = value <= record.minStock;
|
|
||||||
return (
|
|
||||||
<span style={{ color: isLow ? '#ff4d4f' : '#52c41a', fontWeight: 'bold' }}>
|
|
||||||
{value}
|
|
||||||
</span>
|
|
||||||
);
|
);
|
||||||
|
console.error('操作失败:', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
[stockRecord, stockType, fetchConsumables]
|
||||||
title: '最小库存',
|
);
|
||||||
dataIndex: 'minStock',
|
|
||||||
key: 'minStock',
|
const columns = useMemo(
|
||||||
width: 100
|
() => [
|
||||||
},
|
{
|
||||||
{
|
title: '名称',
|
||||||
title: '最大库存',
|
dataIndex: 'name',
|
||||||
dataIndex: 'maxStock',
|
key: 'name',
|
||||||
key: 'maxStock',
|
width: 150,
|
||||||
width: 100,
|
},
|
||||||
render: (value) => value === 0 || value === null || value === undefined ? '无限制' : value
|
{
|
||||||
},
|
title: '分类',
|
||||||
{
|
dataIndex: 'category',
|
||||||
title: '单价(元)',
|
key: 'category',
|
||||||
dataIndex: 'unitPrice',
|
width: 120,
|
||||||
key: 'unitPrice',
|
},
|
||||||
width: 100,
|
{
|
||||||
render: (value) => `¥${parseFloat(value || 0).toFixed(2)}`
|
title: '单位',
|
||||||
},
|
dataIndex: 'unit',
|
||||||
{
|
key: 'unit',
|
||||||
title: '供应商',
|
width: 80,
|
||||||
dataIndex: 'supplier',
|
},
|
||||||
key: 'supplier',
|
{
|
||||||
width: 150,
|
title: '当前库存',
|
||||||
render: (value) => value || '-'
|
dataIndex: 'currentStock',
|
||||||
},
|
key: 'currentStock',
|
||||||
{
|
width: 100,
|
||||||
title: '位置',
|
render: (value, record) => {
|
||||||
dataIndex: 'location',
|
const isLow = value <= record.minStock;
|
||||||
key: 'location',
|
return (
|
||||||
width: 120,
|
<span style={{ color: isLow ? '#ff4d4f' : '#52c41a', fontWeight: 'bold' }}>
|
||||||
render: (value) => value || '-'
|
{value}
|
||||||
},
|
</span>
|
||||||
{
|
);
|
||||||
title: '描述',
|
},
|
||||||
dataIndex: 'description',
|
},
|
||||||
key: 'description',
|
{
|
||||||
width: 200,
|
title: '最小库存',
|
||||||
render: (value) => value || '-',
|
dataIndex: 'minStock',
|
||||||
ellipsis: true
|
key: 'minStock',
|
||||||
},
|
width: 100,
|
||||||
{
|
},
|
||||||
title: '状态',
|
{
|
||||||
dataIndex: 'status',
|
title: '最大库存',
|
||||||
key: 'status',
|
dataIndex: 'maxStock',
|
||||||
width: 100,
|
key: 'maxStock',
|
||||||
render: (value) => (
|
width: 100,
|
||||||
<span style={{ color: value === 'active' ? '#52c41a' : '#ff4d4f' }}>
|
render: value => (value === 0 || value === null || value === undefined ? '无限制' : value),
|
||||||
{value === 'active' ? '启用' : '停用'}
|
},
|
||||||
</span>
|
{
|
||||||
)
|
title: '单价(元)',
|
||||||
},
|
dataIndex: 'unitPrice',
|
||||||
{
|
key: 'unitPrice',
|
||||||
title: '操作',
|
width: 100,
|
||||||
key: 'action',
|
render: value => `¥${parseFloat(value || 0).toFixed(2)}`,
|
||||||
width: 200,
|
},
|
||||||
render: (_, record) => (
|
{
|
||||||
<Space>
|
title: '供应商',
|
||||||
<Button type="primary" icon={<EditOutlined />} size="small" onClick={() => showModal(record)}>编辑</Button>
|
dataIndex: 'supplier',
|
||||||
<Button type="default" icon={<InboxOutlined />} size="small" style={{ background: '#f6ffed', borderColor: '#b7eb8f', color: '#52c41a' }} onClick={() => showStockModal(record, 'in')}>入库</Button>
|
key: 'supplier',
|
||||||
<Button type="default" icon={<ExportOutlined />} size="small" style={{ background: '#fff2f0', borderColor: '#ffccc7', color: '#ff4d4f' }} onClick={() => showStockModal(record, 'out')}>出库</Button>
|
width: 150,
|
||||||
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.consumableId)}>
|
render: value => value || '-',
|
||||||
<Button danger icon={<DeleteOutlined />} size="small">删除</Button>
|
},
|
||||||
</Popconfirm>
|
{
|
||||||
</Space>
|
title: '位置',
|
||||||
)
|
dataIndex: 'location',
|
||||||
}
|
key: 'location',
|
||||||
], [showModal, showStockModal, handleDelete]);
|
width: 120,
|
||||||
|
render: value => value || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '描述',
|
||||||
|
dataIndex: 'description',
|
||||||
|
key: 'description',
|
||||||
|
width: 200,
|
||||||
|
render: value => value || '-',
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
width: 100,
|
||||||
|
render: value => (
|
||||||
|
<span style={{ color: value === 'active' ? '#52c41a' : '#ff4d4f' }}>
|
||||||
|
{value === 'active' ? '启用' : '停用'}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 200,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
size="small"
|
||||||
|
onClick={() => showModal(record)}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
icon={<InboxOutlined />}
|
||||||
|
size="small"
|
||||||
|
style={{ background: '#f6ffed', borderColor: '#b7eb8f', color: '#52c41a' }}
|
||||||
|
onClick={() => showStockModal(record, 'in')}
|
||||||
|
>
|
||||||
|
入库
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
icon={<ExportOutlined />}
|
||||||
|
size="small"
|
||||||
|
style={{ background: '#fff2f0', borderColor: '#ffccc7', color: '#ff4d4f' }}
|
||||||
|
onClick={() => showStockModal(record, 'out')}
|
||||||
|
>
|
||||||
|
出库
|
||||||
|
</Button>
|
||||||
|
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.consumableId)}>
|
||||||
|
<Button danger icon={<DeleteOutlined />} size="small">
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[showModal, showStockModal, handleDelete]
|
||||||
|
);
|
||||||
|
|
||||||
const previewColumns = [
|
const previewColumns = [
|
||||||
{ title: '名称', dataIndex: '名称', key: 'name', width: 120 },
|
{ title: '名称', dataIndex: '名称', key: 'name', width: 120 },
|
||||||
{ title: '分类', dataIndex: '分类', key: 'category', width: 100 },
|
{ title: '分类', dataIndex: '分类', key: 'category', width: 100 },
|
||||||
{ title: '单位', dataIndex: '单位', key: 'unit', width: 80 },
|
{ title: '单位', dataIndex: '单位', key: 'unit', width: 80 },
|
||||||
{ title: '当前库存', dataIndex: '当前库存', key: 'currentStock', width: 90 },
|
{ title: '当前库存', dataIndex: '当前库存', key: 'currentStock', width: 90 },
|
||||||
{ title: '单价', dataIndex: '单价', key: 'unitPrice', width: 80 }
|
{ title: '单价', dataIndex: '单价', key: 'unitPrice', width: 80 },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Card title="耗材管理" extra={
|
<Card
|
||||||
<Space>
|
title="耗材管理"
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => showModal()}>添加耗材</Button>
|
extra={
|
||||||
<Button icon={<ImportOutlined />} onClick={showImportModal}>导入</Button>
|
<Space>
|
||||||
<Button icon={<ExportOutlined />} onClick={handleExport}>导出</Button>
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => showModal()}>
|
||||||
</Space>
|
添加耗材
|
||||||
}>
|
</Button>
|
||||||
|
<Button icon={<ImportOutlined />} onClick={showImportModal}>
|
||||||
|
导入
|
||||||
|
</Button>
|
||||||
|
<Button icon={<ExportOutlined />} onClick={handleExport}>
|
||||||
|
导出
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Card size="small" style={{ marginBottom: 16 }}>
|
<Card size="small" style={{ marginBottom: 16 }}>
|
||||||
<Space>
|
<Space>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
@@ -548,7 +663,9 @@ function ConsumableManagement() {
|
|||||||
<Select value={category} onChange={setCategory} style={{ width: 150 }}>
|
<Select value={category} onChange={setCategory} style={{ width: 150 }}>
|
||||||
<Option value="all">所有分类</Option>
|
<Option value="all">所有分类</Option>
|
||||||
{categories.map(cat => (
|
{categories.map(cat => (
|
||||||
<Option key={cat.id} value={cat.name}>{cat.name}</Option>
|
<Option key={cat.id} value={cat.name}>
|
||||||
|
{cat.name}
|
||||||
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
<Select value={status} onChange={setStatus} style={{ width: 120 }}>
|
<Select value={status} onChange={setStatus} style={{ width: 120 }}>
|
||||||
@@ -565,7 +682,7 @@ function ConsumableManagement() {
|
|||||||
rowKey="consumableId"
|
rowKey="consumableId"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onChange={(pagination) => fetchConsumables(pagination.current, pagination.pageSize)}
|
onChange={pagination => fetchConsumables(pagination.current, pagination.pageSize)}
|
||||||
scroll={{ x: 1300 }}
|
scroll={{ x: 1300 }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -581,31 +698,47 @@ function ConsumableManagement() {
|
|||||||
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
|
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
|
||||||
<Input placeholder="请输入耗材名称" />
|
<Input placeholder="请输入耗材名称" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="category" label="分类" rules={[{ required: true, message: '请选择分类' }]}>
|
<Form.Item
|
||||||
<Select
|
name="category"
|
||||||
placeholder="请选择分类"
|
label="分类"
|
||||||
allowClear
|
rules={[{ required: true, message: '请选择分类' }]}
|
||||||
>
|
>
|
||||||
|
<Select placeholder="请选择分类" allowClear>
|
||||||
{categories.map(cat => (
|
{categories.map(cat => (
|
||||||
<Option key={cat.id} value={cat.name}>{cat.name}</Option>
|
<Option key={cat.id} value={cat.name}>
|
||||||
|
{cat.name}
|
||||||
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="unit" label="单位" rules={[{ required: true, message: '请输入单位' }]} initialValue="个">
|
<Form.Item
|
||||||
|
name="unit"
|
||||||
|
label="单位"
|
||||||
|
rules={[{ required: true, message: '请输入单位' }]}
|
||||||
|
initialValue="个"
|
||||||
|
>
|
||||||
<Input placeholder="如: 个、盒、卷、箱" />
|
<Input placeholder="如: 个、盒、卷、箱" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Space style={{ width: '100%' }}>
|
<Space style={{ width: '100%' }}>
|
||||||
<Form.Item name="currentStock" label="当前库存" rules={[{ required: true, message: '请输入当前库存' }]}>
|
<Form.Item
|
||||||
|
name="currentStock"
|
||||||
|
label="当前库存"
|
||||||
|
rules={[{ required: true, message: '请输入当前库存' }]}
|
||||||
|
>
|
||||||
<InputNumber min={0} style={{ width: '100%' }} />
|
<InputNumber min={0} style={{ width: '100%' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="minStock" label="最小库存" rules={[{ required: true, message: '请输入最小库存' }]}>
|
<Form.Item
|
||||||
|
name="minStock"
|
||||||
|
label="最小库存"
|
||||||
|
rules={[{ required: true, message: '请输入最小库存' }]}
|
||||||
|
>
|
||||||
<InputNumber min={0} style={{ width: '100%' }} />
|
<InputNumber min={0} style={{ width: '100%' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="最大库存">
|
<Form.Item label="最大库存">
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={maxStockUnlimited}
|
checked={maxStockUnlimited}
|
||||||
onChange={(e) => {
|
onChange={e => {
|
||||||
setMaxStockUnlimited(e.target.checked);
|
setMaxStockUnlimited(e.target.checked);
|
||||||
if (e.target.checked) {
|
if (e.target.checked) {
|
||||||
form.setFieldsValue({ maxStock: undefined });
|
form.setFieldsValue({ maxStock: undefined });
|
||||||
@@ -615,7 +748,11 @@ function ConsumableManagement() {
|
|||||||
无限制
|
无限制
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
{!maxStockUnlimited && (
|
{!maxStockUnlimited && (
|
||||||
<Form.Item name="maxStock" noStyle rules={[{ required: true, message: '请输入最大库存' }]}>
|
<Form.Item
|
||||||
|
name="maxStock"
|
||||||
|
noStyle
|
||||||
|
rules={[{ required: true, message: '请输入最大库存' }]}
|
||||||
|
>
|
||||||
<InputNumber min={0} style={{ width: '100%' }} placeholder="请输入最大库存" />
|
<InputNumber min={0} style={{ width: '100%' }} placeholder="请输入最大库存" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
@@ -623,7 +760,13 @@ function ConsumableManagement() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Space>
|
</Space>
|
||||||
<Form.Item name="unitPrice" label="单价(元)">
|
<Form.Item name="unitPrice" label="单价(元)">
|
||||||
<InputNumber min={0} step={0.01} precision={2} style={{ width: '100%' }} placeholder="请输入单价" />
|
<InputNumber
|
||||||
|
min={0}
|
||||||
|
step={0.01}
|
||||||
|
precision={2}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="请输入单价"
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="supplier" label="供应商">
|
<Form.Item name="supplier" label="供应商">
|
||||||
<Input placeholder="请输入供应商" />
|
<Input placeholder="请输入供应商" />
|
||||||
@@ -642,7 +785,9 @@ function ConsumableManagement() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="primary" htmlType="submit">{editingConsumable ? '更新' : '创建'}</Button>
|
<Button type="primary" htmlType="submit">
|
||||||
|
{editingConsumable ? '更新' : '创建'}
|
||||||
|
</Button>
|
||||||
<Button onClick={handleCancel}>取消</Button>
|
<Button onClick={handleCancel}>取消</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -659,17 +804,14 @@ function ConsumableManagement() {
|
|||||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||||
<Card size="small" style={{ background: '#f5f5f5' }}>
|
<Card size="small" style={{ background: '#f5f5f5' }}>
|
||||||
<Space>
|
<Space>
|
||||||
<Button icon={<FileExcelOutlined />} onClick={downloadTemplate}>下载模板</Button>
|
<Button icon={<FileExcelOutlined />} onClick={downloadTemplate}>
|
||||||
|
下载模板
|
||||||
|
</Button>
|
||||||
<span style={{ color: '#888', fontSize: 12 }}>请下载模板后填写数据再导入</span>
|
<span style={{ color: '#888', fontSize: 12 }}>请下载模板后填写数据再导入</span>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Upload
|
<Upload accept=".csv" maxCount={1} beforeUpload={() => false} onChange={handleFileChange}>
|
||||||
accept=".csv"
|
|
||||||
maxCount={1}
|
|
||||||
beforeUpload={() => false}
|
|
||||||
onChange={handleFileChange}
|
|
||||||
>
|
|
||||||
<Button icon={<UploadOutlined />}>选择CSV文件</Button>
|
<Button icon={<UploadOutlined />}>选择CSV文件</Button>
|
||||||
</Upload>
|
</Upload>
|
||||||
|
|
||||||
@@ -717,7 +859,11 @@ function ConsumableManagement() {
|
|||||||
<Form.Item name="consumableName" label="耗材名称">
|
<Form.Item name="consumableName" label="耗材名称">
|
||||||
<Input disabled />
|
<Input disabled />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="quantity" label="数量" rules={[{ required: true, message: '请输入数量' }]}>
|
<Form.Item
|
||||||
|
name="quantity"
|
||||||
|
label="数量"
|
||||||
|
rules={[{ required: true, message: '请输入数量' }]}
|
||||||
|
>
|
||||||
<InputNumber min={1} style={{ width: '100%' }} placeholder="请输入数量" />
|
<InputNumber min={1} style={{ width: '100%' }} placeholder="请输入数量" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="operator" label="操作人">
|
<Form.Item name="operator" label="操作人">
|
||||||
@@ -731,7 +877,9 @@ function ConsumableManagement() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="primary" htmlType="submit">{stockType === 'in' ? '确认入库' : '确认出库'}</Button>
|
<Button type="primary" htmlType="submit">
|
||||||
|
{stockType === 'in' ? '确认入库' : '确认出库'}
|
||||||
|
</Button>
|
||||||
<Button onClick={handleStockCancel}>取消</Button>
|
<Button onClick={handleStockCancel}>取消</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -1,6 +1,29 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Card, Row, Col, Statistic, Table, Tag, DatePicker, Space, Select, Progress, message, Button } from 'antd';
|
import {
|
||||||
import { InboxOutlined, ExportOutlined, WarningOutlined, DollarOutlined, ShoppingCartOutlined, ExclamationCircleOutlined, PlusOutlined, BarChartOutlined, DownloadOutlined } from '@ant-design/icons';
|
Card,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Statistic,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
DatePicker,
|
||||||
|
Space,
|
||||||
|
Select,
|
||||||
|
Progress,
|
||||||
|
message,
|
||||||
|
Button,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
InboxOutlined,
|
||||||
|
ExportOutlined,
|
||||||
|
WarningOutlined,
|
||||||
|
DollarOutlined,
|
||||||
|
ShoppingCartOutlined,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
BarChartOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
@@ -12,60 +35,60 @@ const designTokens = {
|
|||||||
primary: {
|
primary: {
|
||||||
main: '#667eea',
|
main: '#667eea',
|
||||||
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
light: '#8b9ff0'
|
light: '#8b9ff0',
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
main: '#10b981',
|
main: '#10b981',
|
||||||
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
|
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
main: '#f59e0b',
|
main: '#f59e0b',
|
||||||
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)'
|
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
main: '#ef4444',
|
main: '#ef4444',
|
||||||
gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)'
|
gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
primary: '#1e293b',
|
primary: '#1e293b',
|
||||||
secondary: '#64748b',
|
secondary: '#64748b',
|
||||||
tertiary: '#94a3b8',
|
tertiary: '#94a3b8',
|
||||||
inverse: '#ffffff'
|
inverse: '#ffffff',
|
||||||
},
|
},
|
||||||
background: {
|
background: {
|
||||||
primary: '#ffffff',
|
primary: '#ffffff',
|
||||||
secondary: '#f8fafc',
|
secondary: '#f8fafc',
|
||||||
tertiary: '#f1f5f9'
|
tertiary: '#f1f5f9',
|
||||||
},
|
},
|
||||||
border: {
|
border: {
|
||||||
light: '#e2e8f0',
|
light: '#e2e8f0',
|
||||||
medium: '#cbd5e1',
|
medium: '#cbd5e1',
|
||||||
dark: '#94a3b8'
|
dark: '#94a3b8',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
shadows: {
|
shadows: {
|
||||||
small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||||
medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
|
medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
|
||||||
large: '0 10px 15px -3px rgba(0, 0, 0, 0.1)'
|
large: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
small: '6px',
|
small: '6px',
|
||||||
medium: '10px',
|
medium: '10px',
|
||||||
large: '16px'
|
large: '16px',
|
||||||
},
|
},
|
||||||
spacing: {
|
spacing: {
|
||||||
xs: '4px',
|
xs: '4px',
|
||||||
sm: '8px',
|
sm: '8px',
|
||||||
md: '16px',
|
md: '16px',
|
||||||
lg: '24px',
|
lg: '24px',
|
||||||
xl: '32px'
|
xl: '32px',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const pageContainerStyle = {
|
const pageContainerStyle = {
|
||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
background: designTokens.colors.background.secondary,
|
background: designTokens.colors.background.secondary,
|
||||||
padding: designTokens.spacing.lg
|
padding: designTokens.spacing.lg,
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerStyle = {
|
const headerStyle = {
|
||||||
@@ -74,7 +97,7 @@ const headerStyle = {
|
|||||||
background: designTokens.colors.background.primary,
|
background: designTokens.colors.background.primary,
|
||||||
borderRadius: designTokens.borderRadius.large,
|
borderRadius: designTokens.borderRadius.large,
|
||||||
boxShadow: designTokens.shadows.small,
|
boxShadow: designTokens.shadows.small,
|
||||||
border: `1px solid ${designTokens.colors.border.light}`
|
border: `1px solid ${designTokens.colors.border.light}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const titleRowStyle = {
|
const titleRowStyle = {
|
||||||
@@ -83,7 +106,7 @@ const titleRowStyle = {
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
marginBottom: designTokens.spacing.md,
|
marginBottom: designTokens.spacing.md,
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
gap: designTokens.spacing.md
|
gap: designTokens.spacing.md,
|
||||||
};
|
};
|
||||||
|
|
||||||
const titleStyle = {
|
const titleStyle = {
|
||||||
@@ -92,13 +115,13 @@ const titleStyle = {
|
|||||||
gap: designTokens.spacing.sm,
|
gap: designTokens.spacing.sm,
|
||||||
fontSize: '20px',
|
fontSize: '20px',
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: designTokens.colors.text.primary
|
color: designTokens.colors.text.primary,
|
||||||
};
|
};
|
||||||
|
|
||||||
const statsRowStyle = {
|
const statsRowStyle = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: designTokens.spacing.md,
|
gap: designTokens.spacing.md,
|
||||||
flexWrap: 'wrap'
|
flexWrap: 'wrap',
|
||||||
};
|
};
|
||||||
|
|
||||||
const statCardStyle = {
|
const statCardStyle = {
|
||||||
@@ -108,22 +131,22 @@ const statCardStyle = {
|
|||||||
boxShadow: designTokens.shadows.small,
|
boxShadow: designTokens.shadows.small,
|
||||||
border: `1px solid ${designTokens.colors.border.light}`,
|
border: `1px solid ${designTokens.colors.border.light}`,
|
||||||
minWidth: '180px',
|
minWidth: '180px',
|
||||||
flex: 1
|
flex: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const statCardTextStyle = {
|
const statCardTextStyle = {
|
||||||
fontSize: '13px',
|
fontSize: '13px',
|
||||||
color: designTokens.colors.text.secondary,
|
color: designTokens.colors.text.secondary,
|
||||||
marginBottom: designTokens.spacing.xs
|
marginBottom: designTokens.spacing.xs,
|
||||||
};
|
};
|
||||||
|
|
||||||
const statCardValueStyle = {
|
const statCardValueStyle = {
|
||||||
fontSize: '28px',
|
fontSize: '28px',
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: designTokens.colors.text.primary
|
color: designTokens.colors.text.primary,
|
||||||
};
|
};
|
||||||
|
|
||||||
const statCardIconStyle = (color) => ({
|
const statCardIconStyle = color => ({
|
||||||
fontSize: '28px',
|
fontSize: '28px',
|
||||||
color: color,
|
color: color,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -132,7 +155,7 @@ const statCardIconStyle = (color) => ({
|
|||||||
width: '48px',
|
width: '48px',
|
||||||
height: '48px',
|
height: '48px',
|
||||||
borderRadius: designTokens.borderRadius.medium,
|
borderRadius: designTokens.borderRadius.medium,
|
||||||
background: `${color}12`
|
background: `${color}12`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const panelStyle = {
|
const panelStyle = {
|
||||||
@@ -140,7 +163,7 @@ const panelStyle = {
|
|||||||
borderRadius: designTokens.borderRadius.large,
|
borderRadius: designTokens.borderRadius.large,
|
||||||
boxShadow: designTokens.shadows.small,
|
boxShadow: designTokens.shadows.small,
|
||||||
border: `1px solid ${designTokens.colors.border.light}`,
|
border: `1px solid ${designTokens.colors.border.light}`,
|
||||||
overflow: 'hidden'
|
overflow: 'hidden',
|
||||||
};
|
};
|
||||||
|
|
||||||
const panelHeaderStyle = {
|
const panelHeaderStyle = {
|
||||||
@@ -148,7 +171,7 @@ const panelHeaderStyle = {
|
|||||||
borderBottom: `1px solid ${designTokens.colors.border.light}`,
|
borderBottom: `1px solid ${designTokens.colors.border.light}`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between'
|
justifyContent: 'space-between',
|
||||||
};
|
};
|
||||||
|
|
||||||
const panelTitleStyle = {
|
const panelTitleStyle = {
|
||||||
@@ -157,11 +180,11 @@ const panelTitleStyle = {
|
|||||||
color: designTokens.colors.text.primary,
|
color: designTokens.colors.text.primary,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: designTokens.spacing.sm
|
gap: designTokens.spacing.sm,
|
||||||
};
|
};
|
||||||
|
|
||||||
const panelBodyStyle = {
|
const panelBodyStyle = {
|
||||||
padding: designTokens.spacing.lg
|
padding: designTokens.spacing.lg,
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionButtonStyle = {
|
const actionButtonStyle = {
|
||||||
@@ -171,7 +194,7 @@ const actionButtonStyle = {
|
|||||||
fontSize: '13px',
|
fontSize: '13px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: designTokens.spacing.xs
|
gap: designTokens.spacing.xs,
|
||||||
};
|
};
|
||||||
|
|
||||||
const primaryActionStyle = {
|
const primaryActionStyle = {
|
||||||
@@ -179,13 +202,19 @@ const primaryActionStyle = {
|
|||||||
background: designTokens.colors.primary.gradient,
|
background: designTokens.colors.primary.gradient,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
color: '#ffffff',
|
color: '#ffffff',
|
||||||
boxShadow: designTokens.shadows.small
|
boxShadow: designTokens.shadows.small,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ConsumableStatistics() {
|
function ConsumableStatistics() {
|
||||||
const [summary, setSummary] = useState({ total: 0, lowStock: 0, totalValue: 0, byCategory: [] });
|
const [summary, setSummary] = useState({ total: 0, lowStock: 0, totalValue: 0, byCategory: [] });
|
||||||
const [lowStockItems, setLowStockItems] = useState([]);
|
const [lowStockItems, setLowStockItems] = useState([]);
|
||||||
const [stats, setStats] = useState({ inCount: 0, outCount: 0, inQuantity: 0, outQuantity: 0, recentRecords: [] });
|
const [stats, setStats] = useState({
|
||||||
|
inCount: 0,
|
||||||
|
outCount: 0,
|
||||||
|
inQuantity: 0,
|
||||||
|
outQuantity: 0,
|
||||||
|
recentRecords: [],
|
||||||
|
});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [dateRange, setDateRange] = useState([]);
|
const [dateRange, setDateRange] = useState([]);
|
||||||
const [categoryFilter, setCategoryFilter] = useState(null);
|
const [categoryFilter, setCategoryFilter] = useState(null);
|
||||||
@@ -228,11 +257,7 @@ function ConsumableStatistics() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await Promise.all([
|
await Promise.all([fetchSummary(), fetchLowStock(), fetchInOutStats()]);
|
||||||
fetchSummary(),
|
|
||||||
fetchLowStock(),
|
|
||||||
fetchInOutStats()
|
|
||||||
]);
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
loadData();
|
loadData();
|
||||||
@@ -248,96 +273,97 @@ function ConsumableStatistics() {
|
|||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
width: 150,
|
width: 150,
|
||||||
render: (text) => (
|
render: text => (
|
||||||
<span style={{ fontWeight: '500', color: designTokens.colors.text.primary }}>
|
<span style={{ fontWeight: '500', color: designTokens.colors.text.primary }}>{text}</span>
|
||||||
{text}
|
),
|
||||||
</span>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '分类',
|
title: '分类',
|
||||||
dataIndex: 'category',
|
dataIndex: 'category',
|
||||||
key: 'category',
|
key: 'category',
|
||||||
width: 120,
|
width: 120,
|
||||||
render: (category) => (
|
render: category => (
|
||||||
<Tag style={{
|
<Tag
|
||||||
border: 'none',
|
style={{
|
||||||
borderRadius: designTokens.borderRadius.small,
|
border: 'none',
|
||||||
background: `${designTokens.colors.primary.main}15`,
|
borderRadius: designTokens.borderRadius.small,
|
||||||
color: designTokens.colors.primary.main,
|
background: `${designTokens.colors.primary.main}15`,
|
||||||
fontWeight: '500'
|
color: designTokens.colors.primary.main,
|
||||||
}}>
|
fontWeight: '500',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{category}
|
{category}
|
||||||
</Tag>
|
</Tag>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '当前库存',
|
title: '当前库存',
|
||||||
dataIndex: 'currentStock',
|
dataIndex: 'currentStock',
|
||||||
key: 'currentStock',
|
key: 'currentStock',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (value) => (
|
render: value => (
|
||||||
<span style={{
|
<span
|
||||||
color: designTokens.colors.error.main,
|
style={{
|
||||||
fontWeight: '600',
|
color: designTokens.colors.error.main,
|
||||||
background: `${designTokens.colors.error.main}12`,
|
fontWeight: '600',
|
||||||
padding: `2px ${designTokens.spacing.sm}`,
|
background: `${designTokens.colors.error.main}12`,
|
||||||
borderRadius: designTokens.borderRadius.small
|
padding: `2px ${designTokens.spacing.sm}`,
|
||||||
}}>
|
borderRadius: designTokens.borderRadius.small,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{value}
|
{value}
|
||||||
</span>
|
</span>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '最小库存',
|
title: '最小库存',
|
||||||
dataIndex: 'minStock',
|
dataIndex: 'minStock',
|
||||||
key: 'minStock',
|
key: 'minStock',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (value) => (
|
render: value => <span style={{ color: designTokens.colors.text.secondary }}>{value}</span>,
|
||||||
<span style={{ color: designTokens.colors.text.secondary }}>
|
|
||||||
{value}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '单位',
|
title: '单位',
|
||||||
dataIndex: 'unit',
|
dataIndex: 'unit',
|
||||||
key: 'unit',
|
key: 'unit',
|
||||||
width: 80,
|
width: 80,
|
||||||
render: (value) => (
|
render: value => <span style={{ color: designTokens.colors.text.tertiary }}>{value}</span>,
|
||||||
<span style={{ color: designTokens.colors.text.tertiary }}>
|
|
||||||
{value}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '充足率',
|
title: '充足率',
|
||||||
key: 'rate',
|
key: 'rate',
|
||||||
width: 140,
|
width: 140,
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
const rate = Math.min(100, Math.round((record.currentStock / (record.maxStock || 100)) * 100));
|
const rate = Math.min(
|
||||||
|
100,
|
||||||
|
Math.round((record.currentStock / (record.maxStock || 100)) * 100)
|
||||||
|
);
|
||||||
const status = rate < 30 ? 'exception' : rate < 60 ? 'active' : 'success';
|
const status = rate < 30 ? 'exception' : rate < 60 ? 'active' : 'success';
|
||||||
return (
|
return (
|
||||||
<Progress
|
<Progress
|
||||||
percent={rate}
|
percent={rate}
|
||||||
size="small"
|
size="small"
|
||||||
status={status}
|
status={status}
|
||||||
strokeColor={status === 'exception' ? designTokens.colors.error.main : status === 'active' ? designTokens.colors.warning.main : designTokens.colors.success.main}
|
strokeColor={
|
||||||
|
status === 'exception'
|
||||||
|
? designTokens.colors.error.main
|
||||||
|
: status === 'active'
|
||||||
|
? designTokens.colors.warning.main
|
||||||
|
: designTokens.colors.success.main
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '供应商',
|
title: '供应商',
|
||||||
dataIndex: 'supplier',
|
dataIndex: 'supplier',
|
||||||
key: 'supplier',
|
key: 'supplier',
|
||||||
width: 120,
|
width: 120,
|
||||||
render: (value) => (
|
render: value => (
|
||||||
<span style={{ color: designTokens.colors.text.secondary }}>
|
<span style={{ color: designTokens.colors.text.secondary }}>{value || '-'}</span>
|
||||||
{value || '-'}
|
),
|
||||||
</span>
|
},
|
||||||
)
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const recentColumns = [
|
const recentColumns = [
|
||||||
@@ -346,41 +372,41 @@ function ConsumableStatistics() {
|
|||||||
dataIndex: 'createdAt',
|
dataIndex: 'createdAt',
|
||||||
key: 'createdAt',
|
key: 'createdAt',
|
||||||
width: 170,
|
width: 170,
|
||||||
render: (date) => (
|
render: date => (
|
||||||
<span style={{ color: designTokens.colors.text.secondary }}>
|
<span style={{ color: designTokens.colors.text.secondary }}>
|
||||||
{dayjs(date).format('YYYY-MM-DD HH:mm')}
|
{dayjs(date).format('YYYY-MM-DD HH:mm')}
|
||||||
</span>
|
</span>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '耗材名称',
|
title: '耗材名称',
|
||||||
dataIndex: ['Consumable', 'name'],
|
dataIndex: ['Consumable', 'name'],
|
||||||
key: 'consumableName',
|
key: 'consumableName',
|
||||||
width: 140,
|
width: 140,
|
||||||
render: (text) => (
|
render: text => <span style={{ fontWeight: '500' }}>{text}</span>,
|
||||||
<span style={{ fontWeight: '500' }}>
|
|
||||||
{text}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '类型',
|
title: '类型',
|
||||||
dataIndex: 'type',
|
dataIndex: 'type',
|
||||||
key: 'type',
|
key: 'type',
|
||||||
width: 90,
|
width: 90,
|
||||||
render: (type) => (
|
render: type => (
|
||||||
<Tag
|
<Tag
|
||||||
style={{
|
style={{
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: designTokens.borderRadius.small,
|
borderRadius: designTokens.borderRadius.small,
|
||||||
background: type === 'in' ? `${designTokens.colors.success.main}15` : `${designTokens.colors.error.main}15`,
|
background:
|
||||||
color: type === 'in' ? designTokens.colors.success.main : designTokens.colors.error.main,
|
type === 'in'
|
||||||
fontWeight: '500'
|
? `${designTokens.colors.success.main}15`
|
||||||
|
: `${designTokens.colors.error.main}15`,
|
||||||
|
color:
|
||||||
|
type === 'in' ? designTokens.colors.success.main : designTokens.colors.error.main,
|
||||||
|
fontWeight: '500',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{type === 'in' ? '入库' : '出库'}
|
{type === 'in' ? '入库' : '出库'}
|
||||||
</Tag>
|
</Tag>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '数量',
|
title: '数量',
|
||||||
@@ -388,36 +414,38 @@ function ConsumableStatistics() {
|
|||||||
key: 'quantity',
|
key: 'quantity',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (value, record) => (
|
render: (value, record) => (
|
||||||
<span style={{
|
<span
|
||||||
color: record.type === 'in' ? designTokens.colors.success.main : designTokens.colors.error.main,
|
style={{
|
||||||
fontWeight: '600'
|
color:
|
||||||
}}>
|
record.type === 'in'
|
||||||
{record.type === 'in' ? '+' : '-'}{value}
|
? designTokens.colors.success.main
|
||||||
|
: designTokens.colors.error.main,
|
||||||
|
fontWeight: '600',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{record.type === 'in' ? '+' : '-'}
|
||||||
|
{value}
|
||||||
</span>
|
</span>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作人',
|
title: '操作人',
|
||||||
dataIndex: 'operator',
|
dataIndex: 'operator',
|
||||||
key: 'operator',
|
key: 'operator',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (value) => (
|
render: value => (
|
||||||
<span style={{ color: designTokens.colors.text.secondary }}>
|
<span style={{ color: designTokens.colors.text.secondary }}>{value || '-'}</span>
|
||||||
{value || '-'}
|
),
|
||||||
</span>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '原因',
|
title: '原因',
|
||||||
dataIndex: 'reason',
|
dataIndex: 'reason',
|
||||||
key: 'reason',
|
key: 'reason',
|
||||||
width: 150,
|
width: 150,
|
||||||
render: (value) => (
|
render: value => (
|
||||||
<span style={{ color: designTokens.colors.text.secondary }}>
|
<span style={{ color: designTokens.colors.text.secondary }}>{value || '-'}</span>
|
||||||
{value || '-'}
|
),
|
||||||
</span>
|
},
|
||||||
)
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const categories = summary.byCategory?.map(item => item.category) || [];
|
const categories = summary.byCategory?.map(item => item.category) || [];
|
||||||
@@ -441,7 +469,9 @@ function ConsumableStatistics() {
|
|||||||
onChange={setCategoryFilter}
|
onChange={setCategoryFilter}
|
||||||
>
|
>
|
||||||
{categories.map(cat => (
|
{categories.map(cat => (
|
||||||
<Option key={cat} value={cat}>{cat}</Option>
|
<Option key={cat} value={cat}>
|
||||||
|
{cat}
|
||||||
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
<RangePicker
|
<RangePicker
|
||||||
@@ -468,7 +498,9 @@ function ConsumableStatistics() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ ...statCardStyle, borderLeft: `4px solid ${designTokens.colors.error.main}` }}>
|
<div
|
||||||
|
style={{ ...statCardStyle, borderLeft: `4px solid ${designTokens.colors.error.main}` }}
|
||||||
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: designTokens.spacing.md }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: designTokens.spacing.md }}>
|
||||||
<div style={statCardIconStyle(designTokens.colors.error.main)}>
|
<div style={statCardIconStyle(designTokens.colors.error.main)}>
|
||||||
<WarningOutlined />
|
<WarningOutlined />
|
||||||
@@ -482,7 +514,12 @@ function ConsumableStatistics() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ ...statCardStyle, borderLeft: `4px solid ${designTokens.colors.success.main}` }}>
|
<div
|
||||||
|
style={{
|
||||||
|
...statCardStyle,
|
||||||
|
borderLeft: `4px solid ${designTokens.colors.success.main}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: designTokens.spacing.md }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: designTokens.spacing.md }}>
|
||||||
<div style={statCardIconStyle(designTokens.colors.success.main)}>
|
<div style={statCardIconStyle(designTokens.colors.success.main)}>
|
||||||
<DollarOutlined />
|
<DollarOutlined />
|
||||||
@@ -496,15 +533,35 @@ function ConsumableStatistics() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ ...statCardStyle, borderLeft: `4px solid ${netQuantity >= 0 ? designTokens.colors.success.main : designTokens.colors.error.main}` }}>
|
<div
|
||||||
|
style={{
|
||||||
|
...statCardStyle,
|
||||||
|
borderLeft: `4px solid ${netQuantity >= 0 ? designTokens.colors.success.main : designTokens.colors.error.main}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: designTokens.spacing.md }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: designTokens.spacing.md }}>
|
||||||
<div style={statCardIconStyle(netQuantity >= 0 ? designTokens.colors.success.main : designTokens.colors.error.main)}>
|
<div
|
||||||
|
style={statCardIconStyle(
|
||||||
|
netQuantity >= 0
|
||||||
|
? designTokens.colors.success.main
|
||||||
|
: designTokens.colors.error.main
|
||||||
|
)}
|
||||||
|
>
|
||||||
<InboxOutlined />
|
<InboxOutlined />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={statCardTextStyle}>净入库量</div>
|
<div style={statCardTextStyle}>净入库量</div>
|
||||||
<div style={{ ...statCardValueStyle, color: netQuantity >= 0 ? designTokens.colors.success.main : designTokens.colors.error.main }}>
|
<div
|
||||||
{netQuantity >= 0 ? '+' : ''}{netQuantity}
|
style={{
|
||||||
|
...statCardValueStyle,
|
||||||
|
color:
|
||||||
|
netQuantity >= 0
|
||||||
|
? designTokens.colors.success.main
|
||||||
|
: designTokens.colors.error.main,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{netQuantity >= 0 ? '+' : ''}
|
||||||
|
{netQuantity}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -525,39 +582,81 @@ function ConsumableStatistics() {
|
|||||||
<div style={{ padding: designTokens.spacing.lg }}>
|
<div style={{ padding: designTokens.spacing.lg }}>
|
||||||
<Row gutter={designTokens.spacing.md}>
|
<Row gutter={designTokens.spacing.md}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<div style={{
|
<div
|
||||||
background: `${designTokens.colors.success.main}08`,
|
style={{
|
||||||
borderRadius: designTokens.borderRadius.medium,
|
background: `${designTokens.colors.success.main}08`,
|
||||||
padding: designTokens.spacing.lg,
|
borderRadius: designTokens.borderRadius.medium,
|
||||||
textAlign: 'center',
|
padding: designTokens.spacing.lg,
|
||||||
border: `1px solid ${designTokens.colors.success.main}30`
|
textAlign: 'center',
|
||||||
}}>
|
border: `1px solid ${designTokens.colors.success.main}30`,
|
||||||
<div style={{ fontSize: '13px', color: designTokens.colors.text.secondary, marginBottom: designTokens.spacing.sm }}>
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '13px',
|
||||||
|
color: designTokens.colors.text.secondary,
|
||||||
|
marginBottom: designTokens.spacing.sm,
|
||||||
|
}}
|
||||||
|
>
|
||||||
入库次数
|
入库次数
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '32px', fontWeight: '600', color: designTokens.colors.success.main }}>
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '32px',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: designTokens.colors.success.main,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{stats.inCount}
|
{stats.inCount}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '20px', fontWeight: '500', color: designTokens.colors.success.main, marginTop: designTokens.spacing.xs }}>
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '20px',
|
||||||
|
fontWeight: '500',
|
||||||
|
color: designTokens.colors.success.main,
|
||||||
|
marginTop: designTokens.spacing.xs,
|
||||||
|
}}
|
||||||
|
>
|
||||||
+{stats.inQuantity}
|
+{stats.inQuantity}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<div style={{
|
<div
|
||||||
background: `${designTokens.colors.error.main}08`,
|
style={{
|
||||||
borderRadius: designTokens.borderRadius.medium,
|
background: `${designTokens.colors.error.main}08`,
|
||||||
padding: designTokens.spacing.lg,
|
borderRadius: designTokens.borderRadius.medium,
|
||||||
textAlign: 'center',
|
padding: designTokens.spacing.lg,
|
||||||
border: `1px solid ${designTokens.colors.error.main}30`
|
textAlign: 'center',
|
||||||
}}>
|
border: `1px solid ${designTokens.colors.error.main}30`,
|
||||||
<div style={{ fontSize: '13px', color: designTokens.colors.text.secondary, marginBottom: designTokens.spacing.sm }}>
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '13px',
|
||||||
|
color: designTokens.colors.text.secondary,
|
||||||
|
marginBottom: designTokens.spacing.sm,
|
||||||
|
}}
|
||||||
|
>
|
||||||
出库次数
|
出库次数
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '32px', fontWeight: '600', color: designTokens.colors.error.main }}>
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '32px',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: designTokens.colors.error.main,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{stats.outCount}
|
{stats.outCount}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '20px', fontWeight: '500', color: designTokens.colors.error.main, marginTop: designTokens.spacing.xs }}>
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '20px',
|
||||||
|
fontWeight: '500',
|
||||||
|
color: designTokens.colors.error.main,
|
||||||
|
marginTop: designTokens.spacing.xs,
|
||||||
|
}}
|
||||||
|
>
|
||||||
-{stats.outQuantity}
|
-{stats.outQuantity}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -579,7 +678,14 @@ function ConsumableStatistics() {
|
|||||||
<div style={{ padding: designTokens.spacing.lg }}>
|
<div style={{ padding: designTokens.spacing.lg }}>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: designTokens.spacing.sm }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: designTokens.spacing.sm }}>
|
||||||
{summary.byCategory?.map((item, index) => {
|
{summary.byCategory?.map((item, index) => {
|
||||||
const colors = [designTokens.colors.primary.main, designTokens.colors.success.main, designTokens.colors.warning.main, '#8b5cf6', '#06b6d4', '#ec4899'];
|
const colors = [
|
||||||
|
designTokens.colors.primary.main,
|
||||||
|
designTokens.colors.success.main,
|
||||||
|
designTokens.colors.warning.main,
|
||||||
|
'#8b5cf6',
|
||||||
|
'#06b6d4',
|
||||||
|
'#ec4899',
|
||||||
|
];
|
||||||
const color = colors[index % colors.length];
|
const color = colors[index % colors.length];
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -591,15 +697,17 @@ function ConsumableStatistics() {
|
|||||||
border: `1px solid ${color}30`,
|
border: `1px solid ${color}30`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: designTokens.spacing.sm
|
gap: designTokens.spacing.sm,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{
|
<div
|
||||||
width: '8px',
|
style={{
|
||||||
height: '8px',
|
width: '8px',
|
||||||
borderRadius: '50%',
|
height: '8px',
|
||||||
background: color
|
borderRadius: '50%',
|
||||||
}} />
|
background: color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<span style={{ color: designTokens.colors.text.secondary, fontSize: '13px' }}>
|
<span style={{ color: designTokens.colors.text.secondary, fontSize: '13px' }}>
|
||||||
{item.category}
|
{item.category}
|
||||||
</span>
|
</span>
|
||||||
@@ -610,7 +718,12 @@ function ConsumableStatistics() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{(!summary.byCategory || summary.byCategory.length === 0) && (
|
{(!summary.byCategory || summary.byCategory.length === 0) && (
|
||||||
<div style={{ color: designTokens.colors.text.tertiary, padding: designTokens.spacing.lg }}>
|
<div
|
||||||
|
style={{
|
||||||
|
color: designTokens.colors.text.tertiary,
|
||||||
|
padding: designTokens.spacing.lg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
暂无分类数据
|
暂无分类数据
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -628,13 +741,15 @@ function ConsumableStatistics() {
|
|||||||
<ExclamationCircleOutlined style={{ color: designTokens.colors.error.main }} />
|
<ExclamationCircleOutlined style={{ color: designTokens.colors.error.main }} />
|
||||||
低库存预警
|
低库存预警
|
||||||
</div>
|
</div>
|
||||||
<Tag style={{
|
<Tag
|
||||||
border: 'none',
|
style={{
|
||||||
borderRadius: designTokens.borderRadius.small,
|
border: 'none',
|
||||||
background: `${designTokens.colors.error.main}15`,
|
borderRadius: designTokens.borderRadius.small,
|
||||||
color: designTokens.colors.error.main,
|
background: `${designTokens.colors.error.main}15`,
|
||||||
fontWeight: '500'
|
color: designTokens.colors.error.main,
|
||||||
}}>
|
fontWeight: '500',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{lowStockItems.length}项
|
{lowStockItems.length}项
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+682
-395
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,32 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { Table, Button, Modal, Form, Input, Select, message, Card, Space, InputNumber, Switch, Tag, Statistic, Tooltip } from 'antd';
|
import {
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined, AppstoreOutlined, FontSizeOutlined, NumberOutlined, CheckCircleOutlined, CalendarOutlined, FileTextOutlined, LockOutlined } from '@ant-design/icons';
|
Table,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
message,
|
||||||
|
Card,
|
||||||
|
Space,
|
||||||
|
InputNumber,
|
||||||
|
Switch,
|
||||||
|
Tag,
|
||||||
|
Statistic,
|
||||||
|
Tooltip,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
AppstoreOutlined,
|
||||||
|
FontSizeOutlined,
|
||||||
|
NumberOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
CalendarOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
LockOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const { Option = Select.Option } = Select;
|
const { Option = Select.Option } = Select;
|
||||||
@@ -11,35 +37,35 @@ const designTokens = {
|
|||||||
main: '#667eea',
|
main: '#667eea',
|
||||||
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
light: '#8b9ff0',
|
light: '#8b9ff0',
|
||||||
dark: '#4f5db8'
|
dark: '#4f5db8',
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
main: '#10b981',
|
main: '#10b981',
|
||||||
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
|
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
main: '#f59e0b',
|
main: '#f59e0b',
|
||||||
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)'
|
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
main: '#ef4444',
|
main: '#ef4444',
|
||||||
gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)'
|
gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
primary: '#1e293b',
|
primary: '#1e293b',
|
||||||
secondary: '#64748b',
|
secondary: '#64748b',
|
||||||
tertiary: '#94a3b8',
|
tertiary: '#94a3b8',
|
||||||
inverse: '#ffffff'
|
inverse: '#ffffff',
|
||||||
},
|
},
|
||||||
background: {
|
background: {
|
||||||
primary: '#ffffff',
|
primary: '#ffffff',
|
||||||
secondary: '#f8fafc',
|
secondary: '#f8fafc',
|
||||||
tertiary: '#f1f5f9'
|
tertiary: '#f1f5f9',
|
||||||
},
|
},
|
||||||
border: {
|
border: {
|
||||||
light: '#e2e8f0',
|
light: '#e2e8f0',
|
||||||
medium: '#cbd5e1',
|
medium: '#cbd5e1',
|
||||||
dark: '#94a3b8'
|
dark: '#94a3b8',
|
||||||
},
|
},
|
||||||
fieldType: {
|
fieldType: {
|
||||||
string: '#3b82f6',
|
string: '#3b82f6',
|
||||||
@@ -47,44 +73,44 @@ const designTokens = {
|
|||||||
boolean: '#f59e0b',
|
boolean: '#f59e0b',
|
||||||
select: '#8b5cf6',
|
select: '#8b5cf6',
|
||||||
date: '#06b6d4',
|
date: '#06b6d4',
|
||||||
textarea: '#64748b'
|
textarea: '#64748b',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
shadows: {
|
shadows: {
|
||||||
small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||||
medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
||||||
large: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
|
large: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
|
||||||
glow: '0 0 20px rgba(102, 126, 234, 0.15)'
|
glow: '0 0 20px rgba(102, 126, 234, 0.15)',
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
small: '6px',
|
small: '6px',
|
||||||
medium: '10px',
|
medium: '10px',
|
||||||
large: '16px'
|
large: '16px',
|
||||||
},
|
},
|
||||||
transitions: {
|
transitions: {
|
||||||
fast: '150ms cubic-bezier(0.4, 0, 0.2, 1)',
|
fast: '150ms cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
normal: '300ms cubic-bezier(0.4, 0, 0.2, 1)'
|
normal: '300ms cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
},
|
},
|
||||||
spacing: {
|
spacing: {
|
||||||
xs: '4px',
|
xs: '4px',
|
||||||
sm: '8px',
|
sm: '8px',
|
||||||
md: '16px',
|
md: '16px',
|
||||||
lg: '24px',
|
lg: '24px',
|
||||||
xl: '32px'
|
xl: '32px',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const pageContainerStyle = {
|
const pageContainerStyle = {
|
||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
background: designTokens.colors.background.secondary,
|
background: designTokens.colors.background.secondary,
|
||||||
padding: designTokens.spacing.lg
|
padding: designTokens.spacing.lg,
|
||||||
};
|
};
|
||||||
|
|
||||||
const titleRowStyle = {
|
const titleRowStyle = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
marginBottom: designTokens.spacing.lg
|
marginBottom: designTokens.spacing.lg,
|
||||||
};
|
};
|
||||||
|
|
||||||
const titleStyle = {
|
const titleStyle = {
|
||||||
@@ -93,7 +119,7 @@ const titleStyle = {
|
|||||||
gap: designTokens.spacing.sm,
|
gap: designTokens.spacing.sm,
|
||||||
fontSize: '20px',
|
fontSize: '20px',
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: designTokens.colors.text.primary
|
color: designTokens.colors.text.primary,
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionButtonStyle = {
|
const actionButtonStyle = {
|
||||||
@@ -103,7 +129,7 @@ const actionButtonStyle = {
|
|||||||
fontSize: '13px',
|
fontSize: '13px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: designTokens.spacing.xs
|
gap: designTokens.spacing.xs,
|
||||||
};
|
};
|
||||||
|
|
||||||
const primaryActionStyle = {
|
const primaryActionStyle = {
|
||||||
@@ -111,7 +137,7 @@ const primaryActionStyle = {
|
|||||||
background: designTokens.colors.primary.gradient,
|
background: designTokens.colors.primary.gradient,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
color: '#ffffff',
|
color: '#ffffff',
|
||||||
boxShadow: designTokens.shadows.small
|
boxShadow: designTokens.shadows.small,
|
||||||
};
|
};
|
||||||
|
|
||||||
const tableCardStyle = {
|
const tableCardStyle = {
|
||||||
@@ -119,34 +145,34 @@ const tableCardStyle = {
|
|||||||
borderRadius: designTokens.borderRadius.large,
|
borderRadius: designTokens.borderRadius.large,
|
||||||
boxShadow: designTokens.shadows.small,
|
boxShadow: designTokens.shadows.small,
|
||||||
border: `1px solid ${designTokens.colors.border.light}`,
|
border: `1px solid ${designTokens.colors.border.light}`,
|
||||||
overflow: 'hidden'
|
overflow: 'hidden',
|
||||||
};
|
};
|
||||||
|
|
||||||
const tableStyle = {
|
const tableStyle = {
|
||||||
background: designTokens.colors.background.primary
|
background: designTokens.colors.background.primary,
|
||||||
};
|
};
|
||||||
|
|
||||||
const titleIconStyle = {
|
const titleIconStyle = {
|
||||||
color: designTokens.colors.primary.main
|
color: designTokens.colors.primary.main,
|
||||||
};
|
};
|
||||||
|
|
||||||
const modalTitleStyle = {
|
const modalTitleStyle = {
|
||||||
fontWeight: '600'
|
fontWeight: '600',
|
||||||
};
|
};
|
||||||
|
|
||||||
const formLabelStyle = {
|
const formLabelStyle = {
|
||||||
fontWeight: '500'
|
fontWeight: '500',
|
||||||
};
|
};
|
||||||
|
|
||||||
const tableCellStyle = {
|
const tableCellStyle = {
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
color: designTokens.colors.text.primary
|
color: designTokens.colors.text.primary,
|
||||||
};
|
};
|
||||||
|
|
||||||
const typeTagStyle = {
|
const typeTagStyle = {
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: designTokens.borderRadius.small,
|
borderRadius: designTokens.borderRadius.small,
|
||||||
fontWeight: '500'
|
fontWeight: '500',
|
||||||
};
|
};
|
||||||
|
|
||||||
const orderBadgeStyle = {
|
const orderBadgeStyle = {
|
||||||
@@ -154,44 +180,44 @@ const orderBadgeStyle = {
|
|||||||
padding: '2px 8px',
|
padding: '2px 8px',
|
||||||
borderRadius: designTokens.borderRadius.small,
|
borderRadius: designTokens.borderRadius.small,
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
fontWeight: '500'
|
fontWeight: '500',
|
||||||
};
|
};
|
||||||
|
|
||||||
const editButtonStyle = {
|
const editButtonStyle = {
|
||||||
color: designTokens.colors.primary.main,
|
color: designTokens.colors.primary.main,
|
||||||
height: '28px',
|
height: '28px',
|
||||||
padding: '0 8px'
|
padding: '0 8px',
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteButtonStyle = {
|
const deleteButtonStyle = {
|
||||||
height: '28px',
|
height: '28px',
|
||||||
padding: '0 8px'
|
padding: '0 8px',
|
||||||
};
|
};
|
||||||
|
|
||||||
const formRowStyle = {
|
const formRowStyle = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: designTokens.spacing.md
|
gap: designTokens.spacing.md,
|
||||||
};
|
};
|
||||||
|
|
||||||
const formItemFlexStyle = {
|
const formItemFlexStyle = {
|
||||||
flex: 1
|
flex: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const textAreaStyle = {
|
const textAreaStyle = {
|
||||||
fontFamily: 'monospace'
|
fontFamily: 'monospace',
|
||||||
};
|
};
|
||||||
|
|
||||||
const modalBodyStyle = {
|
const modalBodyStyle = {
|
||||||
padding: designTokens.spacing.lg
|
padding: designTokens.spacing.lg,
|
||||||
};
|
};
|
||||||
|
|
||||||
const formActionsStyle = {
|
const formActionsStyle = {
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
textAlign: 'right'
|
textAlign: 'right',
|
||||||
};
|
};
|
||||||
|
|
||||||
const modalStyle = {
|
const modalStyle = {
|
||||||
borderRadius: designTokens.borderRadius.large
|
borderRadius: designTokens.borderRadius.large,
|
||||||
};
|
};
|
||||||
|
|
||||||
const FIELD_TYPE_MAP = {
|
const FIELD_TYPE_MAP = {
|
||||||
@@ -200,7 +226,7 @@ const FIELD_TYPE_MAP = {
|
|||||||
boolean: { text: '布尔值', color: designTokens.colors.fieldType.boolean },
|
boolean: { text: '布尔值', color: designTokens.colors.fieldType.boolean },
|
||||||
select: { text: '下拉选择', color: designTokens.colors.fieldType.select },
|
select: { text: '下拉选择', color: designTokens.colors.fieldType.select },
|
||||||
date: { text: '日期', color: designTokens.colors.fieldType.date },
|
date: { text: '日期', color: designTokens.colors.fieldType.date },
|
||||||
textarea: { text: '多行文本', color: designTokens.colors.fieldType.textarea }
|
textarea: { text: '多行文本', color: designTokens.colors.fieldType.textarea },
|
||||||
};
|
};
|
||||||
|
|
||||||
const FIELD_TYPE_OPTIONS = [
|
const FIELD_TYPE_OPTIONS = [
|
||||||
@@ -209,7 +235,7 @@ const FIELD_TYPE_OPTIONS = [
|
|||||||
{ value: 'boolean', label: '布尔值' },
|
{ value: 'boolean', label: '布尔值' },
|
||||||
{ value: 'select', label: '下拉选择' },
|
{ value: 'select', label: '下拉选择' },
|
||||||
{ value: 'date', label: '日期' },
|
{ value: 'date', label: '日期' },
|
||||||
{ value: 'textarea', label: '多行文本' }
|
{ value: 'textarea', label: '多行文本' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function DeviceFieldManagement() {
|
function DeviceFieldManagement() {
|
||||||
@@ -224,7 +250,7 @@ function DeviceFieldManagement() {
|
|||||||
pageSizeOptions: ['10', '20', '50', '100'],
|
pageSizeOptions: ['10', '20', '50', '100'],
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
showQuickJumper: true,
|
showQuickJumper: true,
|
||||||
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条 / 共 ${total} 条`
|
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条 / 共 ${total} 条`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchFields = async () => {
|
const fetchFields = async () => {
|
||||||
@@ -249,7 +275,7 @@ function DeviceFieldManagement() {
|
|||||||
if (field) {
|
if (field) {
|
||||||
const fieldData = {
|
const fieldData = {
|
||||||
...field,
|
...field,
|
||||||
options: field.options ? JSON.stringify(field.options, null, 2) : ''
|
options: field.options ? JSON.stringify(field.options, null, 2) : '',
|
||||||
};
|
};
|
||||||
form.setFieldsValue(fieldData);
|
form.setFieldsValue(fieldData);
|
||||||
} else {
|
} else {
|
||||||
@@ -263,11 +289,11 @@ function DeviceFieldManagement() {
|
|||||||
setEditingField(null);
|
setEditingField(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (values) => {
|
const handleSubmit = async values => {
|
||||||
try {
|
try {
|
||||||
const fieldData = {
|
const fieldData = {
|
||||||
...values,
|
...values,
|
||||||
options: values.options ? JSON.parse(values.options) : null
|
options: values.options ? JSON.parse(values.options) : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (editingField) {
|
if (editingField) {
|
||||||
@@ -287,7 +313,7 @@ function DeviceFieldManagement() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (fieldId) => {
|
const handleDelete = async fieldId => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '确认删除',
|
title: '确认删除',
|
||||||
content: '确定要删除这个字段吗?',
|
content: '确定要删除这个字段吗?',
|
||||||
@@ -303,126 +329,149 @@ function DeviceFieldManagement() {
|
|||||||
message.error('字段删除失败');
|
message.error('字段删除失败');
|
||||||
console.error('字段删除失败:', error);
|
console.error('字段删除失败:', error);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFieldTypeIcon = (type) => {
|
const getFieldTypeIcon = type => {
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
string: <FontSizeOutlined />,
|
string: <FontSizeOutlined />,
|
||||||
number: <NumberOutlined />,
|
number: <NumberOutlined />,
|
||||||
boolean: <CheckCircleOutlined />,
|
boolean: <CheckCircleOutlined />,
|
||||||
select: <AppstoreOutlined />,
|
select: <AppstoreOutlined />,
|
||||||
date: <CalendarOutlined />,
|
date: <CalendarOutlined />,
|
||||||
textarea: <FileTextOutlined />
|
textarea: <FileTextOutlined />,
|
||||||
};
|
};
|
||||||
return iconMap[type] || <FontSizeOutlined />;
|
return iconMap[type] || <FontSizeOutlined />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = useMemo(() => [
|
const columns = useMemo(
|
||||||
{
|
() => [
|
||||||
title: '字段名称',
|
{
|
||||||
dataIndex: 'fieldName',
|
title: '字段名称',
|
||||||
key: 'fieldName',
|
dataIndex: 'fieldName',
|
||||||
width: 150,
|
key: 'fieldName',
|
||||||
render: (text, record) => (
|
width: 150,
|
||||||
<Space>
|
render: (text, record) => (
|
||||||
<span style={tableCellStyle}>{text}</span>
|
<Space>
|
||||||
{record.isSystem && (
|
<span style={tableCellStyle}>{text}</span>
|
||||||
<Tooltip title="系统字段,不可删除">
|
{record.isSystem && (
|
||||||
<LockOutlined style={{ color: '#f59e0b', fontSize: '14px' }} />
|
<Tooltip title="系统字段,不可删除">
|
||||||
</Tooltip>
|
<LockOutlined style={{ color: '#f59e0b', fontSize: '14px' }} />
|
||||||
)}
|
</Tooltip>
|
||||||
</Space>
|
)}
|
||||||
)
|
</Space>
|
||||||
},
|
),
|
||||||
{
|
},
|
||||||
title: '显示名称',
|
{
|
||||||
dataIndex: 'displayName',
|
title: '显示名称',
|
||||||
key: 'displayName',
|
dataIndex: 'displayName',
|
||||||
width: 120,
|
key: 'displayName',
|
||||||
},
|
width: 120,
|
||||||
{
|
},
|
||||||
title: '字段类型',
|
{
|
||||||
dataIndex: 'fieldType',
|
title: '字段类型',
|
||||||
key: 'fieldType',
|
dataIndex: 'fieldType',
|
||||||
width: 110,
|
key: 'fieldType',
|
||||||
render: (type) => {
|
width: 110,
|
||||||
const config = FIELD_TYPE_MAP[type] || { text: type, color: designTokens.colors.text.tertiary };
|
render: type => {
|
||||||
return (
|
const config = FIELD_TYPE_MAP[type] || {
|
||||||
<Tag style={{ ...typeTagStyle, background: `${config.color}15`, color: config.color }}>
|
text: type,
|
||||||
{getFieldTypeIcon(type)}
|
color: designTokens.colors.text.tertiary,
|
||||||
<span style={{ marginLeft: '4px' }}>{config.text}</span>
|
};
|
||||||
</Tag>
|
return (
|
||||||
);
|
<Tag style={{ ...typeTagStyle, background: `${config.color}15`, color: config.color }}>
|
||||||
}
|
{getFieldTypeIcon(type)}
|
||||||
},
|
<span style={{ marginLeft: '4px' }}>{config.text}</span>
|
||||||
{
|
</Tag>
|
||||||
title: '必填',
|
);
|
||||||
dataIndex: 'required',
|
},
|
||||||
key: 'required',
|
},
|
||||||
width: 80,
|
{
|
||||||
render: (required) => (
|
title: '必填',
|
||||||
<span style={{
|
dataIndex: 'required',
|
||||||
color: required ? designTokens.colors.success.main : designTokens.colors.text.tertiary,
|
key: 'required',
|
||||||
...tableCellStyle
|
width: 80,
|
||||||
}}>
|
render: required => (
|
||||||
{required ? '是' : '否'}
|
<span
|
||||||
</span>
|
style={{
|
||||||
)
|
color: required
|
||||||
},
|
? designTokens.colors.success.main
|
||||||
{
|
: designTokens.colors.text.tertiary,
|
||||||
title: '可见',
|
...tableCellStyle,
|
||||||
dataIndex: 'visible',
|
}}
|
||||||
key: 'visible',
|
>
|
||||||
width: 80,
|
{required ? '是' : '否'}
|
||||||
render: (visible) => (
|
</span>
|
||||||
<span style={{
|
),
|
||||||
color: visible ? designTokens.colors.primary.main : designTokens.colors.text.tertiary,
|
},
|
||||||
...tableCellStyle
|
{
|
||||||
}}>
|
title: '可见',
|
||||||
{visible ? '是' : '否'}
|
dataIndex: 'visible',
|
||||||
</span>
|
key: 'visible',
|
||||||
)
|
width: 80,
|
||||||
},
|
render: visible => (
|
||||||
{
|
<span
|
||||||
title: '顺序',
|
style={{
|
||||||
dataIndex: 'order',
|
color: visible ? designTokens.colors.primary.main : designTokens.colors.text.tertiary,
|
||||||
key: 'order',
|
...tableCellStyle,
|
||||||
width: 80,
|
}}
|
||||||
render: (order) => <span style={orderBadgeStyle}>{order}</span>
|
>
|
||||||
},
|
{visible ? '是' : '否'}
|
||||||
{
|
</span>
|
||||||
title: '操作',
|
),
|
||||||
key: 'action',
|
},
|
||||||
width: 160,
|
{
|
||||||
fixed: 'right',
|
title: '顺序',
|
||||||
render: (_, record) => (
|
dataIndex: 'order',
|
||||||
<Space size="small">
|
key: 'order',
|
||||||
<Button type="text" icon={<EditOutlined />} onClick={() => showModal(record)} style={editButtonStyle}>
|
width: 80,
|
||||||
编辑
|
render: order => <span style={orderBadgeStyle}>{order}</span>,
|
||||||
</Button>
|
},
|
||||||
{record.isSystem ? (
|
{
|
||||||
<Tooltip title="系统字段不可删除">
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 160,
|
||||||
|
fixed: 'right',
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space size="small">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => showModal(record)}
|
||||||
|
style={editButtonStyle}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
{record.isSystem ? (
|
||||||
|
<Tooltip title="系统字段不可删除">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
disabled
|
||||||
|
style={{ ...deleteButtonStyle, opacity: 0.3, cursor: 'not-allowed' }}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
danger
|
danger
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
disabled
|
onClick={() => handleDelete(record.fieldId)}
|
||||||
style={{ ...deleteButtonStyle, opacity: 0.3, cursor: 'not-allowed' }}
|
style={deleteButtonStyle}
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
)}
|
||||||
) : (
|
</Space>
|
||||||
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.fieldId)} style={deleteButtonStyle}>
|
),
|
||||||
删除
|
},
|
||||||
</Button>
|
],
|
||||||
)}
|
[]
|
||||||
</Space>
|
);
|
||||||
),
|
|
||||||
},
|
|
||||||
], []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={pageContainerStyle}>
|
<div style={pageContainerStyle}>
|
||||||
@@ -431,7 +480,12 @@ function DeviceFieldManagement() {
|
|||||||
<AppstoreOutlined style={titleIconStyle} />
|
<AppstoreOutlined style={titleIconStyle} />
|
||||||
设备字段管理
|
设备字段管理
|
||||||
</div>
|
</div>
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => showModal()} style={primaryActionStyle}>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => showModal()}
|
||||||
|
style={primaryActionStyle}
|
||||||
|
>
|
||||||
添加字段
|
添加字段
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -443,11 +497,11 @@ function DeviceFieldManagement() {
|
|||||||
rowKey="fieldId"
|
rowKey="fieldId"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onChange={(newPagination) => {
|
onChange={newPagination => {
|
||||||
setPagination({
|
setPagination({
|
||||||
...pagination,
|
...pagination,
|
||||||
current: newPagination.current,
|
current: newPagination.current,
|
||||||
pageSize: newPagination.pageSize
|
pageSize: newPagination.pageSize,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
scroll={{ x: 900 }}
|
scroll={{ x: 900 }}
|
||||||
@@ -488,7 +542,9 @@ function DeviceFieldManagement() {
|
|||||||
>
|
>
|
||||||
<Select placeholder="请选择字段类型">
|
<Select placeholder="请选择字段类型">
|
||||||
{FIELD_TYPE_OPTIONS.map(opt => (
|
{FIELD_TYPE_OPTIONS.map(opt => (
|
||||||
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
|
<Option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -526,13 +582,19 @@ function DeviceFieldManagement() {
|
|||||||
label={<span style={formLabelStyle}>选项配置(JSON格式)</span>}
|
label={<span style={formLabelStyle}>选项配置(JSON格式)</span>}
|
||||||
tooltip="格式示例:[{value: 'option1', label: '选项1'}],仅下拉选择类型需要配置"
|
tooltip="格式示例:[{value: 'option1', label: '选项1'}],仅下拉选择类型需要配置"
|
||||||
>
|
>
|
||||||
<Input.TextArea rows={3} placeholder="请输入JSON格式的选项配置,使用单引号" style={textAreaStyle} />
|
<Input.TextArea
|
||||||
|
rows={3}
|
||||||
|
placeholder="请输入JSON格式的选项配置,使用单引号"
|
||||||
|
style={textAreaStyle}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item style={formActionsStyle}>
|
<Form.Item style={formActionsStyle}>
|
||||||
<Space>
|
<Space>
|
||||||
<Button onClick={handleCancel}>取消</Button>
|
<Button onClick={handleCancel}>取消</Button>
|
||||||
<Button type="primary" htmlType="submit">确定</Button>
|
<Button type="primary" htmlType="submit">
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
+1083
-475
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ import {
|
|||||||
MailOutlined,
|
MailOutlined,
|
||||||
PhoneOutlined,
|
PhoneOutlined,
|
||||||
SafetyCertificateOutlined,
|
SafetyCertificateOutlined,
|
||||||
RobotOutlined
|
RobotOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
@@ -40,7 +40,7 @@ const Login = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFinishLogin = async (values) => {
|
const onFinishLogin = async values => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await login(values.username, values.password);
|
const result = await login(values.username, values.password);
|
||||||
@@ -61,7 +61,7 @@ const Login = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFinishUnlock = async (values) => {
|
const onFinishUnlock = async values => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await authAPI.unlock(values);
|
const response = await authAPI.unlock(values);
|
||||||
@@ -78,7 +78,7 @@ const Login = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFinishRegister = async (values) => {
|
const onFinishRegister = async values => {
|
||||||
if (values.password !== values.confirmPassword) {
|
if (values.password !== values.confirmPassword) {
|
||||||
message.error('两次输入的密码不一致');
|
message.error('两次输入的密码不一致');
|
||||||
return;
|
return;
|
||||||
@@ -91,7 +91,7 @@ const Login = () => {
|
|||||||
password: values.password,
|
password: values.password,
|
||||||
email: values.email,
|
email: values.email,
|
||||||
phone: values.phone,
|
phone: values.phone,
|
||||||
realName: values.realName
|
realName: values.realName,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -123,14 +123,14 @@ const Login = () => {
|
|||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #6B8DD6 100%)',
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #6B8DD6 100%)',
|
||||||
padding: '24px',
|
padding: '24px',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden'
|
overflow: 'hidden',
|
||||||
};
|
};
|
||||||
|
|
||||||
const backgroundDecorationStyle = {
|
const backgroundDecorationStyle = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
filter: 'blur(80px)',
|
filter: 'blur(80px)',
|
||||||
opacity: '0.3'
|
opacity: '0.3',
|
||||||
};
|
};
|
||||||
|
|
||||||
const cardStyle = {
|
const cardStyle = {
|
||||||
@@ -140,13 +140,13 @@ const Login = () => {
|
|||||||
boxShadow: '0 20px 60px rgba(0,0,0,0.25), 0 8px 20px rgba(0,0,0,0.15)',
|
boxShadow: '0 20px 60px rgba(0,0,0,0.25), 0 8px 20px rgba(0,0,0,0.15)',
|
||||||
background: 'rgba(255, 255, 255, 0.95)',
|
background: 'rgba(255, 255, 255, 0.95)',
|
||||||
backdropFilter: 'blur(20px)',
|
backdropFilter: 'blur(20px)',
|
||||||
border: '1px solid rgba(255, 255, 255, 0.3)'
|
border: '1px solid rgba(255, 255, 255, 0.3)',
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerStyle = {
|
const headerStyle = {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
marginBottom: '32px',
|
marginBottom: '32px',
|
||||||
paddingTop: '8px'
|
paddingTop: '8px',
|
||||||
};
|
};
|
||||||
|
|
||||||
const iconContainerStyle = {
|
const iconContainerStyle = {
|
||||||
@@ -158,7 +158,7 @@ const Login = () => {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
margin: '0 auto 20px',
|
margin: '0 auto 20px',
|
||||||
boxShadow: '0 8px 24px rgba(102, 126, 234, 0.4)'
|
boxShadow: '0 8px 24px rgba(102, 126, 234, 0.4)',
|
||||||
};
|
};
|
||||||
|
|
||||||
const titleStyle = {
|
const titleStyle = {
|
||||||
@@ -167,22 +167,22 @@ const Login = () => {
|
|||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
WebkitBackgroundClip: 'text',
|
WebkitBackgroundClip: 'text',
|
||||||
WebkitTextFillColor: 'transparent',
|
WebkitTextFillColor: 'transparent',
|
||||||
marginBottom: '8px'
|
marginBottom: '8px',
|
||||||
};
|
};
|
||||||
|
|
||||||
const subtitleStyle = {
|
const subtitleStyle = {
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
color: '#8c8c8c'
|
color: '#8c8c8c',
|
||||||
};
|
};
|
||||||
|
|
||||||
const formStyle = {
|
const formStyle = {
|
||||||
marginTop: '24px'
|
marginTop: '24px',
|
||||||
};
|
};
|
||||||
|
|
||||||
const inputStyle = {
|
const inputStyle = {
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
height: '48px',
|
height: '48px',
|
||||||
border: '1px solid #e8e8e8'
|
border: '1px solid #e8e8e8',
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitButtonStyle = {
|
const submitButtonStyle = {
|
||||||
@@ -194,13 +194,13 @@ const Login = () => {
|
|||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
boxShadow: '0 4px 15px rgba(102, 126, 234, 0.4)',
|
boxShadow: '0 4px 15px rgba(102, 126, 234, 0.4)',
|
||||||
transition: 'all 0.3s ease'
|
transition: 'all 0.3s ease',
|
||||||
};
|
};
|
||||||
|
|
||||||
const footerStyle = {
|
const footerStyle = {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
marginTop: '24px',
|
marginTop: '24px',
|
||||||
paddingBottom: '16px'
|
paddingBottom: '16px',
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleButtonStyle = {
|
const toggleButtonStyle = {
|
||||||
@@ -208,32 +208,36 @@ const Login = () => {
|
|||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
padding: '4px 8px',
|
padding: '4px 8px',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
transition: 'all 0.3s ease'
|
transition: 'all 0.3s ease',
|
||||||
};
|
};
|
||||||
|
|
||||||
const inputPrefixStyle = {
|
const inputPrefixStyle = {
|
||||||
color: '#667eea',
|
color: '#667eea',
|
||||||
fontSize: '18px'
|
fontSize: '18px',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={containerStyle}>
|
<div style={containerStyle}>
|
||||||
<div style={{
|
<div
|
||||||
...backgroundDecorationStyle,
|
style={{
|
||||||
width: '400px',
|
...backgroundDecorationStyle,
|
||||||
height: '400px',
|
width: '400px',
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
height: '400px',
|
||||||
top: '-100px',
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
right: '-100px'
|
top: '-100px',
|
||||||
}} />
|
right: '-100px',
|
||||||
<div style={{
|
}}
|
||||||
...backgroundDecorationStyle,
|
/>
|
||||||
width: '300px',
|
<div
|
||||||
height: '300px',
|
style={{
|
||||||
background: 'linear-gradient(135deg, #764ba2 0%, #6B8DD6 100%)',
|
...backgroundDecorationStyle,
|
||||||
bottom: '-50px',
|
width: '300px',
|
||||||
left: '-50px'
|
height: '300px',
|
||||||
}} />
|
background: 'linear-gradient(135deg, #764ba2 0%, #6B8DD6 100%)',
|
||||||
|
bottom: '-50px',
|
||||||
|
left: '-50px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<Card style={cardStyle}>
|
<Card style={cardStyle}>
|
||||||
<div style={headerStyle}>
|
<div style={headerStyle}>
|
||||||
@@ -244,8 +248,11 @@ const Login = () => {
|
|||||||
{isFirstUser ? '创建管理员账户' : unlockMode ? '账户解锁' : 'IDC设备管理系统'}
|
{isFirstUser ? '创建管理员账户' : unlockMode ? '账户解锁' : 'IDC设备管理系统'}
|
||||||
</Title>
|
</Title>
|
||||||
<Text style={subtitleStyle}>
|
<Text style={subtitleStyle}>
|
||||||
{isFirstUser ? '首次使用,请创建系统管理员账户' :
|
{isFirstUser
|
||||||
unlockMode ? '输入账户信息以解锁账户' : '安全登录您的账户'}
|
? '首次使用,请创建系统管理员账户'
|
||||||
|
: unlockMode
|
||||||
|
? '输入账户信息以解锁账户'
|
||||||
|
: '安全登录您的账户'}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -272,7 +279,7 @@ const Login = () => {
|
|||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: '请输入用户名' },
|
{ required: true, message: '请输入用户名' },
|
||||||
{ min: 3, max: 20, message: '用户名长度必须在3-20个字符之间' },
|
{ min: 3, max: 20, message: '用户名长度必须在3-20个字符之间' },
|
||||||
{ pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线' }
|
{ pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线' },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
@@ -282,10 +289,7 @@ const Login = () => {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item name="realName" rules={[{ required: true, message: '请输入真实姓名' }]}>
|
||||||
name="realName"
|
|
||||||
rules={[{ required: true, message: '请输入真实姓名' }]}
|
|
||||||
>
|
|
||||||
<Input
|
<Input
|
||||||
prefix={<SafetyCertificateOutlined style={inputPrefixStyle} />}
|
prefix={<SafetyCertificateOutlined style={inputPrefixStyle} />}
|
||||||
placeholder="真实姓名"
|
placeholder="真实姓名"
|
||||||
@@ -297,7 +301,7 @@ const Login = () => {
|
|||||||
name="email"
|
name="email"
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: '请输入邮箱' },
|
{ required: true, message: '请输入邮箱' },
|
||||||
{ type: 'email', message: '请输入有效的邮箱地址' }
|
{ type: 'email', message: '请输入有效的邮箱地址' },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
@@ -307,9 +311,7 @@ const Login = () => {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item name="phone">
|
||||||
name="phone"
|
|
||||||
>
|
|
||||||
<Input
|
<Input
|
||||||
prefix={<PhoneOutlined style={inputPrefixStyle} />}
|
prefix={<PhoneOutlined style={inputPrefixStyle} />}
|
||||||
placeholder="手机号(可选)"
|
placeholder="手机号(可选)"
|
||||||
@@ -321,7 +323,7 @@ const Login = () => {
|
|||||||
name="password"
|
name="password"
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: '请输入密码' },
|
{ required: true, message: '请输入密码' },
|
||||||
{ min: 6, message: '密码长度不能少于6个字符' }
|
{ min: 6, message: '密码长度不能少于6个字符' },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input.Password
|
<Input.Password
|
||||||
@@ -363,10 +365,7 @@ const Login = () => {
|
|||||||
style={{ marginBottom: '24px', borderRadius: '8px' }}
|
style={{ marginBottom: '24px', borderRadius: '8px' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}>
|
||||||
name="username"
|
|
||||||
rules={[{ required: true, message: '请输入用户名' }]}
|
|
||||||
>
|
|
||||||
<Input
|
<Input
|
||||||
prefix={<UserOutlined style={inputPrefixStyle} />}
|
prefix={<UserOutlined style={inputPrefixStyle} />}
|
||||||
placeholder="用户名"
|
placeholder="用户名"
|
||||||
@@ -374,10 +373,7 @@ const Login = () => {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}>
|
||||||
name="password"
|
|
||||||
rules={[{ required: true, message: '请输入密码' }]}
|
|
||||||
>
|
|
||||||
<Input.Password
|
<Input.Password
|
||||||
prefix={<LockOutlined style={inputPrefixStyle} />}
|
prefix={<LockOutlined style={inputPrefixStyle} />}
|
||||||
placeholder="密码"
|
placeholder="密码"
|
||||||
@@ -387,10 +383,7 @@ const Login = () => {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Form.Item
|
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}>
|
||||||
name="username"
|
|
||||||
rules={[{ required: true, message: '请输入用户名' }]}
|
|
||||||
>
|
|
||||||
<Input
|
<Input
|
||||||
prefix={<UserOutlined style={inputPrefixStyle} />}
|
prefix={<UserOutlined style={inputPrefixStyle} />}
|
||||||
placeholder="用户名"
|
placeholder="用户名"
|
||||||
@@ -398,10 +391,7 @@ const Login = () => {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}>
|
||||||
name="password"
|
|
||||||
rules={[{ required: true, message: '请输入密码' }]}
|
|
||||||
>
|
|
||||||
<Input.Password
|
<Input.Password
|
||||||
prefix={<LockOutlined style={inputPrefixStyle} />}
|
prefix={<LockOutlined style={inputPrefixStyle} />}
|
||||||
placeholder="密码"
|
placeholder="密码"
|
||||||
@@ -412,12 +402,7 @@ const Login = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Form.Item style={{ marginBottom: '16px', marginTop: '24px' }}>
|
<Form.Item style={{ marginBottom: '16px', marginTop: '24px' }}>
|
||||||
<Button
|
<Button type="primary" htmlType="submit" loading={loading} style={submitButtonStyle}>
|
||||||
type="primary"
|
|
||||||
htmlType="submit"
|
|
||||||
loading={loading}
|
|
||||||
style={submitButtonStyle}
|
|
||||||
>
|
|
||||||
{registerMode ? '立即注册' : unlockMode ? '解 锁' : '登 录'}
|
{registerMode ? '立即注册' : unlockMode ? '解 锁' : '登 录'}
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -435,10 +420,10 @@ const Login = () => {
|
|||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
style={toggleButtonStyle}
|
style={toggleButtonStyle}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={e => {
|
||||||
e.target.style.background = 'rgba(102, 126, 234, 0.1)';
|
e.target.style.background = 'rgba(102, 126, 234, 0.1)';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={e => {
|
||||||
e.target.style.background = 'transparent';
|
e.target.style.background = 'transparent';
|
||||||
}}
|
}}
|
||||||
onClick={() => setUnlockMode(false)}
|
onClick={() => setUnlockMode(false)}
|
||||||
@@ -452,10 +437,10 @@ const Login = () => {
|
|||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
style={toggleButtonStyle}
|
style={toggleButtonStyle}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={e => {
|
||||||
e.target.style.background = 'rgba(102, 126, 234, 0.1)';
|
e.target.style.background = 'rgba(102, 126, 234, 0.1)';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={e => {
|
||||||
e.target.style.background = 'transparent';
|
e.target.style.background = 'transparent';
|
||||||
}}
|
}}
|
||||||
onClick={() => setRegisterMode(!registerMode)}
|
onClick={() => setRegisterMode(!registerMode)}
|
||||||
@@ -466,10 +451,10 @@ const Login = () => {
|
|||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
style={toggleButtonStyle}
|
style={toggleButtonStyle}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={e => {
|
||||||
e.target.style.background = 'rgba(102, 126, 234, 0.1)';
|
e.target.style.background = 'rgba(102, 126, 234, 0.1)';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={e => {
|
||||||
e.target.style.background = 'transparent';
|
e.target.style.background = 'transparent';
|
||||||
}}
|
}}
|
||||||
onClick={() => setUnlockMode(true)}
|
onClick={() => setUnlockMode(true)}
|
||||||
|
|||||||
@@ -1,6 +1,46 @@
|
|||||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||||
import { Table, Button, Modal, Form, Input, Select, message, Card, Space, Popconfirm, Tag, Tooltip, InputNumber, Collapse, Empty, Spin, Upload, Progress, Checkbox, Tabs, Badge, List, Typography } from 'antd';
|
import {
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ReloadOutlined, ExportOutlined, ImportOutlined, DownloadOutlined, UploadOutlined as UploadIcon, AppstoreOutlined, UnorderedListOutlined, FilterOutlined, EyeOutlined, CompressOutlined, CloudServerOutlined } from '@ant-design/icons';
|
Table,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
message,
|
||||||
|
Card,
|
||||||
|
Space,
|
||||||
|
Popconfirm,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
InputNumber,
|
||||||
|
Collapse,
|
||||||
|
Empty,
|
||||||
|
Spin,
|
||||||
|
Upload,
|
||||||
|
Progress,
|
||||||
|
Checkbox,
|
||||||
|
Tabs,
|
||||||
|
Badge,
|
||||||
|
List,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
SearchOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
ExportOutlined,
|
||||||
|
ImportOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
|
UploadOutlined as UploadIcon,
|
||||||
|
AppstoreOutlined,
|
||||||
|
UnorderedListOutlined,
|
||||||
|
FilterOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
CompressOutlined,
|
||||||
|
CloudServerOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import Papa from 'papaparse';
|
import Papa from 'papaparse';
|
||||||
@@ -19,35 +59,35 @@ const designTokens = {
|
|||||||
main: '#667eea',
|
main: '#667eea',
|
||||||
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
light: '#8b9ff0',
|
light: '#8b9ff0',
|
||||||
dark: '#4f5db8'
|
dark: '#4f5db8',
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
main: '#10b981',
|
main: '#10b981',
|
||||||
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||||
light: '#34d399',
|
light: '#34d399',
|
||||||
dark: '#047857'
|
dark: '#047857',
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
main: '#f59e0b',
|
main: '#f59e0b',
|
||||||
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||||||
light: '#fbbf24',
|
light: '#fbbf24',
|
||||||
dark: '#b45309'
|
dark: '#b45309',
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
main: '#ef4444',
|
main: '#ef4444',
|
||||||
gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
|
gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
|
||||||
light: '#f87171',
|
light: '#f87171',
|
||||||
dark: '#b91c1c'
|
dark: '#b91c1c',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
small: '6px',
|
small: '6px',
|
||||||
medium: '10px',
|
medium: '10px',
|
||||||
large: '16px'
|
large: '16px',
|
||||||
},
|
},
|
||||||
shadows: {
|
shadows: {
|
||||||
medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)'
|
medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function PortManagement() {
|
function PortManagement() {
|
||||||
@@ -60,7 +100,7 @@ function PortManagement() {
|
|||||||
deviceId: '',
|
deviceId: '',
|
||||||
status: 'all',
|
status: 'all',
|
||||||
portType: 'all',
|
portType: 'all',
|
||||||
portSpeed: 'all'
|
portSpeed: 'all',
|
||||||
});
|
});
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const [editingPort, setEditingPort] = useState(null);
|
const [editingPort, setEditingPort] = useState(null);
|
||||||
@@ -81,7 +121,7 @@ function PortManagement() {
|
|||||||
const [panelFilters, setPanelFilters] = useState({
|
const [panelFilters, setPanelFilters] = useState({
|
||||||
deviceType: 'all',
|
deviceType: 'all',
|
||||||
searchText: '',
|
searchText: '',
|
||||||
showOnlyOccupied: false
|
showOnlyOccupied: false,
|
||||||
});
|
});
|
||||||
const [visibleDeviceCount, setVisibleDeviceCount] = useState(10);
|
const [visibleDeviceCount, setVisibleDeviceCount] = useState(10);
|
||||||
const [expandedDevices, setExpandedDevices] = useState({});
|
const [expandedDevices, setExpandedDevices] = useState({});
|
||||||
@@ -96,7 +136,7 @@ function PortManagement() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const params = {
|
const params = {
|
||||||
pageSize: 1000 // 获取所有端口,不分页
|
pageSize: 1000, // 获取所有端口,不分页
|
||||||
};
|
};
|
||||||
|
|
||||||
if (filters.deviceId) params.deviceId = filters.deviceId;
|
if (filters.deviceId) params.deviceId = filters.deviceId;
|
||||||
@@ -146,7 +186,7 @@ function PortManagement() {
|
|||||||
if (!grouped[deviceId]) {
|
if (!grouped[deviceId]) {
|
||||||
grouped[deviceId] = {
|
grouped[deviceId] = {
|
||||||
device: devices.find(d => d.deviceId === deviceId),
|
device: devices.find(d => d.deviceId === deviceId),
|
||||||
ports: []
|
ports: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
grouped[deviceId].ports.push(port);
|
grouped[deviceId].ports.push(port);
|
||||||
@@ -155,7 +195,7 @@ function PortManagement() {
|
|||||||
// 对每个设备的端口按名称升序排序
|
// 对每个设备的端口按名称升序排序
|
||||||
Object.keys(grouped).forEach(deviceId => {
|
Object.keys(grouped).forEach(deviceId => {
|
||||||
grouped[deviceId].ports.sort((a, b) => {
|
grouped[deviceId].ports.sort((a, b) => {
|
||||||
const extractNumbers = (str) => {
|
const extractNumbers = str => {
|
||||||
const matches = str.match(/\d+/g);
|
const matches = str.match(/\d+/g);
|
||||||
return matches ? matches.map(Number) : [];
|
return matches ? matches.map(Number) : [];
|
||||||
};
|
};
|
||||||
@@ -182,7 +222,7 @@ function PortManagement() {
|
|||||||
deviceId: '',
|
deviceId: '',
|
||||||
status: 'all',
|
status: 'all',
|
||||||
portType: 'all',
|
portType: 'all',
|
||||||
portSpeed: 'all'
|
portSpeed: 'all',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -192,24 +232,24 @@ function PortManagement() {
|
|||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddPortForDevice = (device) => {
|
const handleAddPortForDevice = device => {
|
||||||
setEditingPort(null);
|
setEditingPort(null);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
// 自动选中当前设备
|
// 自动选中当前设备
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
deviceId: device.deviceId
|
deviceId: device.deviceId,
|
||||||
});
|
});
|
||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 打开网卡管理模态框
|
// 打开网卡管理模态框
|
||||||
const handleManageNetworkCards = (device) => {
|
const handleManageNetworkCards = device => {
|
||||||
setSelectedDeviceForNic(device);
|
setSelectedDeviceForNic(device);
|
||||||
setNetworkCardModalVisible(true);
|
setNetworkCardModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 打开添加网卡模态框
|
// 打开添加网卡模态框
|
||||||
const handleAddNetworkCard = (device) => {
|
const handleAddNetworkCard = device => {
|
||||||
setSelectedDeviceForNic(device);
|
setSelectedDeviceForNic(device);
|
||||||
setPortCreateModalVisible(true);
|
setPortCreateModalVisible(true);
|
||||||
};
|
};
|
||||||
@@ -227,7 +267,7 @@ function PortManagement() {
|
|||||||
fetchPorts();
|
fetchPorts();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (port) => {
|
const handleEdit = port => {
|
||||||
setEditingPort(port);
|
setEditingPort(port);
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
portId: port.portId,
|
portId: port.portId,
|
||||||
@@ -237,12 +277,12 @@ function PortManagement() {
|
|||||||
portSpeed: port.portSpeed,
|
portSpeed: port.portSpeed,
|
||||||
status: port.status,
|
status: port.status,
|
||||||
vlanId: port.vlanId,
|
vlanId: port.vlanId,
|
||||||
description: port.description
|
description: port.description,
|
||||||
});
|
});
|
||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (portId) => {
|
const handleDelete = async portId => {
|
||||||
try {
|
try {
|
||||||
await axios.delete(`/api/device-ports/${portId}`);
|
await axios.delete(`/api/device-ports/${portId}`);
|
||||||
message.success('删除成功');
|
message.success('删除成功');
|
||||||
@@ -254,14 +294,15 @@ function PortManagement() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 解析端口名称范围,例如 "1/0/1-1/0/48" -> ["1/0/1", "1/0/2", ..., "1/0/48"]
|
// 解析端口名称范围,例如 "1/0/1-1/0/48" -> ["1/0/1", "1/0/2", ..., "1/0/48"]
|
||||||
const parsePortRange = (portName) => {
|
const parsePortRange = portName => {
|
||||||
const rangeMatch = portName.match(/^(.*?)\/(\d+)-\1\/(\d+)$/);
|
const rangeMatch = portName.match(/^(.*?)\/(\d+)-\1\/(\d+)$/);
|
||||||
if (rangeMatch) {
|
if (rangeMatch) {
|
||||||
const prefix = rangeMatch[1];
|
const prefix = rangeMatch[1];
|
||||||
const start = parseInt(rangeMatch[2]);
|
const start = parseInt(rangeMatch[2]);
|
||||||
const end = parseInt(rangeMatch[3]);
|
const end = parseInt(rangeMatch[3]);
|
||||||
|
|
||||||
if (start <= end && end - start < 100) { // 限制最多100个端口
|
if (start <= end && end - start < 100) {
|
||||||
|
// 限制最多100个端口
|
||||||
return Array.from({ length: end - start + 1 }, (_, i) => `${prefix}/${start + i}`);
|
return Array.from({ length: end - start + 1 }, (_, i) => `${prefix}/${start + i}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,7 +330,7 @@ function PortManagement() {
|
|||||||
portSpeed: values.portSpeed,
|
portSpeed: values.portSpeed,
|
||||||
status: values.status,
|
status: values.status,
|
||||||
vlanId: values.vlanId,
|
vlanId: values.vlanId,
|
||||||
description: values.description
|
description: values.description,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const response = await axios.post('/api/device-ports/batch', { ports: portsData });
|
const response = await axios.post('/api/device-ports/batch', { ports: portsData });
|
||||||
@@ -322,12 +363,12 @@ function PortManagement() {
|
|||||||
setImportProgress({ current: 0, total: 0 });
|
setImportProgress({ current: 0, total: 0 });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileUpload = (info) => {
|
const handleFileUpload = info => {
|
||||||
const { file } = info;
|
const { file } = info;
|
||||||
setImportFileList([file]);
|
setImportFileList([file]);
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = async (e) => {
|
reader.onload = async e => {
|
||||||
try {
|
try {
|
||||||
const data = e.target.result;
|
const data = e.target.result;
|
||||||
let parsedData = [];
|
let parsedData = [];
|
||||||
@@ -341,9 +382,9 @@ function PortManagement() {
|
|||||||
Papa.parse(data, {
|
Papa.parse(data, {
|
||||||
header: true,
|
header: true,
|
||||||
skipEmptyLines: true,
|
skipEmptyLines: true,
|
||||||
complete: (results) => {
|
complete: results => {
|
||||||
parsedData = results.data;
|
parsedData = results.data;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
message.error('不支持的文件格式,请上传 .xlsx 或 .csv 文件');
|
message.error('不支持的文件格式,请上传 .xlsx 或 .csv 文件');
|
||||||
@@ -362,7 +403,7 @@ function PortManagement() {
|
|||||||
reader.readAsBinaryString(file);
|
reader.readAsBinaryString(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateImportData = async (data) => {
|
const validateImportData = async data => {
|
||||||
const validatedData = [];
|
const validatedData = [];
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
@@ -430,9 +471,9 @@ function PortManagement() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
'空闲': 'free',
|
空闲: 'free',
|
||||||
'占用': 'occupied',
|
占用: 'occupied',
|
||||||
'故障': 'fault'
|
故障: 'fault',
|
||||||
};
|
};
|
||||||
|
|
||||||
const portsData = importPreview.map((row, index) => ({
|
const portsData = importPreview.map((row, index) => ({
|
||||||
@@ -443,7 +484,7 @@ function PortManagement() {
|
|||||||
portSpeed: row['端口速率'],
|
portSpeed: row['端口速率'],
|
||||||
status: statusMap[row['状态']] || 'free',
|
status: statusMap[row['状态']] || 'free',
|
||||||
vlanId: row['VLAN ID'],
|
vlanId: row['VLAN ID'],
|
||||||
description: row['描述']
|
description: row['描述'],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const response = await axios.post('/api/device-ports/batch', { ports: portsData });
|
const response = await axios.post('/api/device-ports/batch', { ports: portsData });
|
||||||
@@ -473,14 +514,14 @@ function PortManagement() {
|
|||||||
const handleDownloadTemplate = () => {
|
const handleDownloadTemplate = () => {
|
||||||
const templateData = [
|
const templateData = [
|
||||||
{
|
{
|
||||||
'设备ID': 'DEV001',
|
设备ID: 'DEV001',
|
||||||
'端口名称': 'eth0/1',
|
端口名称: 'eth0/1',
|
||||||
'端口类型': 'RJ45',
|
端口类型: 'RJ45',
|
||||||
'端口速率': '1G',
|
端口速率: '1G',
|
||||||
'状态': '空闲',
|
状态: '空闲',
|
||||||
'VLAN ID': '100',
|
'VLAN ID': '100',
|
||||||
'描述': '示例端口'
|
描述: '示例端口',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const worksheet = XLSX.utils.json_to_sheet(templateData);
|
const worksheet = XLSX.utils.json_to_sheet(templateData);
|
||||||
@@ -489,27 +530,27 @@ function PortManagement() {
|
|||||||
XLSX.writeFile(workbook, '端口导入模板.xlsx');
|
XLSX.writeFile(workbook, '端口导入模板.xlsx');
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusTag = (status) => {
|
const getStatusTag = status => {
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
'free': { color: 'success', text: '空闲' },
|
free: { color: 'success', text: '空闲' },
|
||||||
'occupied': { color: 'processing', text: '占用' },
|
occupied: { color: 'processing', text: '占用' },
|
||||||
'fault': { color: 'error', text: '故障' },
|
fault: { color: 'error', text: '故障' },
|
||||||
'空闲': { color: 'success', text: '空闲' },
|
空闲: { color: 'success', text: '空闲' },
|
||||||
'占用': { color: 'processing', text: '占用' },
|
占用: { color: 'processing', text: '占用' },
|
||||||
'故障': { color: 'error', text: '故障' }
|
故障: { color: 'error', text: '故障' },
|
||||||
};
|
};
|
||||||
const config = statusMap[status] || { color: 'default', text: status };
|
const config = statusMap[status] || { color: 'default', text: status };
|
||||||
return <Tag color={config.color}>{config.text}</Tag>;
|
return <Tag color={config.color}>{config.text}</Tag>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPortTypeTag = (type) => {
|
const getPortTypeTag = type => {
|
||||||
const typeMap = {
|
const typeMap = {
|
||||||
'RJ45': { color: 'blue', text: 'RJ45' },
|
RJ45: { color: 'blue', text: 'RJ45' },
|
||||||
'SFP': { color: 'green', text: 'SFP' },
|
SFP: { color: 'green', text: 'SFP' },
|
||||||
'SFP+': { color: 'cyan', text: 'SFP+' },
|
'SFP+': { color: 'cyan', text: 'SFP+' },
|
||||||
'SFP28': { color: 'purple', text: 'SFP28' },
|
SFP28: { color: 'purple', text: 'SFP28' },
|
||||||
'QSFP': { color: 'orange', text: 'QSFP' },
|
QSFP: { color: 'orange', text: 'QSFP' },
|
||||||
'QSFP28': { color: 'red', text: 'QSFP28' }
|
QSFP28: { color: 'red', text: 'QSFP28' },
|
||||||
};
|
};
|
||||||
const config = typeMap[type] || { color: 'default', text: type };
|
const config = typeMap[type] || { color: 'default', text: type };
|
||||||
return <Tag color={config.color}>{config.text}</Tag>;
|
return <Tag color={config.color}>{config.text}</Tag>;
|
||||||
@@ -520,45 +561,45 @@ function PortManagement() {
|
|||||||
title: '端口名称',
|
title: '端口名称',
|
||||||
dataIndex: 'portName',
|
dataIndex: 'portName',
|
||||||
key: 'portName',
|
key: 'portName',
|
||||||
width: 120
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '端口类型',
|
title: '端口类型',
|
||||||
dataIndex: 'portType',
|
dataIndex: 'portType',
|
||||||
key: 'portType',
|
key: 'portType',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (type) => getPortTypeTag(type)
|
render: type => getPortTypeTag(type),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '端口速率',
|
title: '端口速率',
|
||||||
dataIndex: 'portSpeed',
|
dataIndex: 'portSpeed',
|
||||||
key: 'portSpeed',
|
key: 'portSpeed',
|
||||||
width: 100
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
key: 'status',
|
key: 'status',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (status) => getStatusTag(status)
|
render: status => getStatusTag(status),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'VLAN ID',
|
title: 'VLAN ID',
|
||||||
dataIndex: 'vlanId',
|
dataIndex: 'vlanId',
|
||||||
key: 'vlanId',
|
key: 'vlanId',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (vlanId) => vlanId || '-'
|
render: vlanId => vlanId || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '描述',
|
title: '描述',
|
||||||
dataIndex: 'description',
|
dataIndex: 'description',
|
||||||
key: 'description',
|
key: 'description',
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
render: (text) => (
|
render: text => (
|
||||||
<Tooltip title={text}>
|
<Tooltip title={text}>
|
||||||
<span>{text || '-'}</span>
|
<span>{text || '-'}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
@@ -581,18 +622,13 @@ function PortManagement() {
|
|||||||
okText="确定"
|
okText="确定"
|
||||||
cancelText="取消"
|
cancelText="取消"
|
||||||
>
|
>
|
||||||
<Button
|
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
danger
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
>
|
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Space>
|
</Space>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -601,7 +637,7 @@ function PortManagement() {
|
|||||||
style={{
|
style={{
|
||||||
borderRadius: designTokens.borderRadius.large,
|
borderRadius: designTokens.borderRadius.large,
|
||||||
boxShadow: designTokens.shadows.medium,
|
boxShadow: designTokens.shadows.medium,
|
||||||
marginBottom: 16
|
marginBottom: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
@@ -610,7 +646,7 @@ function PortManagement() {
|
|||||||
placeholder="选择设备"
|
placeholder="选择设备"
|
||||||
style={{ width: 200 }}
|
style={{ width: 200 }}
|
||||||
value={filters.deviceId || undefined}
|
value={filters.deviceId || undefined}
|
||||||
onChange={(value) => setFilters(prev => ({ ...prev, deviceId: value }))}
|
onChange={value => setFilters(prev => ({ ...prev, deviceId: value }))}
|
||||||
allowClear
|
allowClear
|
||||||
showSearch
|
showSearch
|
||||||
filterOption={(input, option) => {
|
filterOption={(input, option) => {
|
||||||
@@ -631,7 +667,7 @@ function PortManagement() {
|
|||||||
placeholder="端口类型"
|
placeholder="端口类型"
|
||||||
style={{ width: 120 }}
|
style={{ width: 120 }}
|
||||||
value={filters.portType}
|
value={filters.portType}
|
||||||
onChange={(value) => setFilters(prev => ({ ...prev, portType: value }))}
|
onChange={value => setFilters(prev => ({ ...prev, portType: value }))}
|
||||||
>
|
>
|
||||||
<Option value="all">全部</Option>
|
<Option value="all">全部</Option>
|
||||||
<Option value="RJ45">RJ45</Option>
|
<Option value="RJ45">RJ45</Option>
|
||||||
@@ -646,7 +682,7 @@ function PortManagement() {
|
|||||||
placeholder="端口速率"
|
placeholder="端口速率"
|
||||||
style={{ width: 120 }}
|
style={{ width: 120 }}
|
||||||
value={filters.portSpeed}
|
value={filters.portSpeed}
|
||||||
onChange={(value) => setFilters(prev => ({ ...prev, portSpeed: value }))}
|
onChange={value => setFilters(prev => ({ ...prev, portSpeed: value }))}
|
||||||
>
|
>
|
||||||
<Option value="all">全部</Option>
|
<Option value="all">全部</Option>
|
||||||
<Option value="100M">100M</Option>
|
<Option value="100M">100M</Option>
|
||||||
@@ -661,7 +697,7 @@ function PortManagement() {
|
|||||||
placeholder="状态"
|
placeholder="状态"
|
||||||
style={{ width: 120 }}
|
style={{ width: 120 }}
|
||||||
value={filters.status}
|
value={filters.status}
|
||||||
onChange={(value) => setFilters(prev => ({ ...prev, status: value }))}
|
onChange={value => setFilters(prev => ({ ...prev, status: value }))}
|
||||||
>
|
>
|
||||||
<Option value="all">全部</Option>
|
<Option value="all">全部</Option>
|
||||||
<Option value="free">空闲</Option>
|
<Option value="free">空闲</Option>
|
||||||
@@ -684,7 +720,14 @@ function PortManagement() {
|
|||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: 16,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -704,9 +747,7 @@ function PortManagement() {
|
|||||||
批量导入
|
批量导入
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button icon={<ExportOutlined />}>
|
<Button icon={<ExportOutlined />}>导出</Button>
|
||||||
导出
|
|
||||||
</Button>
|
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<Space>
|
<Space>
|
||||||
@@ -740,13 +781,15 @@ function PortManagement() {
|
|||||||
) : viewMode === 'panel' ? (
|
) : viewMode === 'panel' ? (
|
||||||
// 面板视图 - 使用虚拟滚动优化
|
// 面板视图 - 使用虚拟滚动优化
|
||||||
<VirtualDeviceList
|
<VirtualDeviceList
|
||||||
devices={Object.values(groupedPorts).map(g => g.device).filter(Boolean)}
|
devices={Object.values(groupedPorts)
|
||||||
|
.map(g => g.device)
|
||||||
|
.filter(Boolean)}
|
||||||
groupedPorts={groupedPorts}
|
groupedPorts={groupedPorts}
|
||||||
cables={cables}
|
cables={cables}
|
||||||
allDevices={devices}
|
allDevices={devices}
|
||||||
onPortClick={(port) => handleEdit(port)}
|
onPortClick={port => handleEdit(port)}
|
||||||
onAddPort={(device) => handleAddPortForDevice(device)}
|
onAddPort={device => handleAddPortForDevice(device)}
|
||||||
onManageNetworkCards={(device) => handleManageNetworkCards(device)}
|
onManageNetworkCards={device => handleManageNetworkCards(device)}
|
||||||
initialVisibleCount={5}
|
initialVisibleCount={5}
|
||||||
loadMoreCount={5}
|
loadMoreCount={5}
|
||||||
/>
|
/>
|
||||||
@@ -767,22 +810,35 @@ function PortManagement() {
|
|||||||
<Panel
|
<Panel
|
||||||
key={deviceId}
|
key={deviceId}
|
||||||
header={
|
header={
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
<div style={{
|
<div
|
||||||
width: '40px',
|
style={{
|
||||||
height: '40px',
|
width: '40px',
|
||||||
borderRadius: designTokens.borderRadius.medium,
|
height: '40px',
|
||||||
background: designTokens.colors.primary.gradient,
|
borderRadius: designTokens.borderRadius.medium,
|
||||||
display: 'flex',
|
background: designTokens.colors.primary.gradient,
|
||||||
alignItems: 'center',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
color: '#fff',
|
justifyContent: 'center',
|
||||||
fontSize: '18px'
|
color: '#fff',
|
||||||
}}>
|
fontSize: '18px',
|
||||||
{device?.type?.toLowerCase()?.includes('server') ? '🖥️' :
|
}}
|
||||||
device?.type?.toLowerCase()?.includes('switch') ? '🔀' :
|
>
|
||||||
device?.type?.toLowerCase()?.includes('router') ? '🌐' : '📦'}
|
{device?.type?.toLowerCase()?.includes('server')
|
||||||
|
? '🖥️'
|
||||||
|
: device?.type?.toLowerCase()?.includes('switch')
|
||||||
|
? '🔀'
|
||||||
|
: device?.type?.toLowerCase()?.includes('router')
|
||||||
|
? '🌐'
|
||||||
|
: '📦'}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontWeight: 600, fontSize: '16px', color: '#1e293b' }}>
|
<div style={{ fontWeight: 600, fontSize: '16px', color: '#1e293b' }}>
|
||||||
@@ -804,11 +860,14 @@ function PortManagement() {
|
|||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<CloudServerOutlined />}
|
icon={<CloudServerOutlined />}
|
||||||
onClick={(e) => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleManageNetworkCards(device);
|
handleManageNetworkCards(device);
|
||||||
}}
|
}}
|
||||||
style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
|
style={{
|
||||||
|
background: designTokens.colors.primary.gradient,
|
||||||
|
border: 'none',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
网卡管理
|
网卡管理
|
||||||
</Button>
|
</Button>
|
||||||
@@ -824,8 +883,8 @@ function PortManagement() {
|
|||||||
pagination={{
|
pagination={{
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
showTotal: (total) => `共 ${total} 个端口`,
|
showTotal: total => `共 ${total} 个端口`,
|
||||||
pageSizeOptions: ['10', '20', '50', '100']
|
pageSizeOptions: ['10', '20', '50', '100'],
|
||||||
}}
|
}}
|
||||||
size="small"
|
size="small"
|
||||||
scroll={{ x: 1000 }}
|
scroll={{ x: 1000 }}
|
||||||
@@ -877,7 +936,7 @@ function PortManagement() {
|
|||||||
name="portName"
|
name="portName"
|
||||||
label="端口名称"
|
label="端口名称"
|
||||||
rules={[{ required: true, message: '请输入端口名称' }]}
|
rules={[{ required: true, message: '请输入端口名称' }]}
|
||||||
extra={!editingPort && "支持批量添加,例如: 1/0/1-1/0/48 将创建 48 个端口"}
|
extra={!editingPort && '支持批量添加,例如: 1/0/1-1/0/48 将创建 48 个端口'}
|
||||||
>
|
>
|
||||||
<Input placeholder="例如: eth0/1 或 1/0/1-1/0/48" />
|
<Input placeholder="例如: eth0/1 或 1/0/1-1/0/48" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -927,17 +986,11 @@ function PortManagement() {
|
|||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item name="vlanId" label="VLAN ID">
|
||||||
name="vlanId"
|
|
||||||
label="VLAN ID"
|
|
||||||
>
|
|
||||||
<InputNumber placeholder="请输入VLAN ID" min={1} max={4094} />
|
<InputNumber placeholder="请输入VLAN ID" min={1} max={4094} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item name="description" label="描述">
|
||||||
name="description"
|
|
||||||
label="描述"
|
|
||||||
>
|
|
||||||
<Input.TextArea rows={3} placeholder="请输入描述" />
|
<Input.TextArea rows={3} placeholder="请输入描述" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -956,11 +1009,7 @@ function PortManagement() {
|
|||||||
<Button key="cancel" onClick={() => setImportModalVisible(false)}>
|
<Button key="cancel" onClick={() => setImportModalVisible(false)}>
|
||||||
取消
|
取消
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button
|
<Button key="download" icon={<DownloadOutlined />} onClick={handleDownloadTemplate}>
|
||||||
key="download"
|
|
||||||
icon={<DownloadOutlined />}
|
|
||||||
onClick={handleDownloadTemplate}
|
|
||||||
>
|
|
||||||
下载模板
|
下载模板
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button
|
<Button
|
||||||
@@ -973,7 +1022,7 @@ function PortManagement() {
|
|||||||
style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
|
style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
|
||||||
>
|
>
|
||||||
开始导入
|
开始导入
|
||||||
</Button>
|
</Button>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
@@ -995,10 +1044,10 @@ function PortManagement() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '12px', marginBottom: 16 }}>
|
<div style={{ display: 'flex', gap: '12px', marginBottom: 16 }}>
|
||||||
<Checkbox checked={skipExisting} onChange={(e) => setSkipExisting(e.target.checked)}>
|
<Checkbox checked={skipExisting} onChange={e => setSkipExisting(e.target.checked)}>
|
||||||
跳过已存在的端口
|
跳过已存在的端口
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Checkbox checked={updateExisting} onChange={(e) => setUpdateExisting(e.target.checked)}>
|
<Checkbox checked={updateExisting} onChange={e => setUpdateExisting(e.target.checked)}>
|
||||||
更新已存在的端口
|
更新已存在的端口
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
@@ -1006,13 +1055,16 @@ function PortManagement() {
|
|||||||
{importPreview.length > 0 && (
|
{importPreview.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Text strong>数据预览(前10条)</Text>
|
<Text strong>数据预览(前10条)</Text>
|
||||||
<Button
|
<Button size="small" icon={<DownloadOutlined />} onClick={handleDownloadTemplate}>
|
||||||
size="small"
|
|
||||||
icon={<DownloadOutlined />}
|
|
||||||
onClick={handleDownloadTemplate}
|
|
||||||
>
|
|
||||||
下载模板
|
下载模板
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1022,52 +1074,52 @@ function PortManagement() {
|
|||||||
title: '设备ID',
|
title: '设备ID',
|
||||||
dataIndex: '设备ID',
|
dataIndex: '设备ID',
|
||||||
key: 'deviceId',
|
key: 'deviceId',
|
||||||
width: 150
|
width: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '端口名称',
|
title: '端口名称',
|
||||||
dataIndex: '端口名称',
|
dataIndex: '端口名称',
|
||||||
key: 'portName',
|
key: 'portName',
|
||||||
width: 120
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '端口类型',
|
title: '端口类型',
|
||||||
dataIndex: '端口类型',
|
dataIndex: '端口类型',
|
||||||
key: 'portType',
|
key: 'portType',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (type) => getPortTypeTag(type)
|
render: type => getPortTypeTag(type),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '端口速率',
|
title: '端口速率',
|
||||||
dataIndex: '端口速率',
|
dataIndex: '端口速率',
|
||||||
key: 'portSpeed',
|
key: 'portSpeed',
|
||||||
width: 100
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: '状态',
|
dataIndex: '状态',
|
||||||
key: 'status',
|
key: 'status',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (status) => getStatusTag(status)
|
render: status => getStatusTag(status),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'VLAN ID',
|
title: 'VLAN ID',
|
||||||
dataIndex: 'VLAN ID',
|
dataIndex: 'VLAN ID',
|
||||||
key: 'vlanId',
|
key: 'vlanId',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (vlanId) => vlanId || '-'
|
render: vlanId => vlanId || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '描述',
|
title: '描述',
|
||||||
dataIndex: '描述',
|
dataIndex: '描述',
|
||||||
key: 'description',
|
key: 'description',
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
render: (text) => (
|
render: text => (
|
||||||
<Tooltip title={text}>
|
<Tooltip title={text}>
|
||||||
<span>{text || '-'}</span>
|
<span>{text || '-'}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
dataSource={importPreview.slice(0, 10)}
|
dataSource={importPreview.slice(0, 10)}
|
||||||
rowKey={(record, index) => index}
|
rowKey={(record, index) => index}
|
||||||
@@ -1094,7 +1146,7 @@ function PortManagement() {
|
|||||||
status="active"
|
status="active"
|
||||||
strokeColor={{
|
strokeColor={{
|
||||||
'0%': designTokens.colors.primary.main,
|
'0%': designTokens.colors.primary.main,
|
||||||
'100%': designTokens.colors.success.main
|
'100%': designTokens.colors.success.main,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div style={{ marginTop: 8 }}>
|
<div style={{ marginTop: 8 }}>
|
||||||
@@ -1103,7 +1155,8 @@ function PortManagement() {
|
|||||||
</Text>
|
</Text>
|
||||||
{importProgress.current > 0 && (
|
{importProgress.current > 0 && (
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
预计剩余时间:{Math.ceil((importProgress.total - importProgress.current) / 5)} 秒
|
预计剩余时间:{Math.ceil((importProgress.total - importProgress.current) / 5)}{' '}
|
||||||
|
秒
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,48 @@
|
|||||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Table, Button, Modal, Form, Input, Select, message, Card, Space,
|
Table,
|
||||||
InputNumber, Progress, Drawer, Tag, Tooltip, Dropdown, Badge,
|
Button,
|
||||||
Row, Col, Statistic, Typography, Empty, Spin, Alert, Checkbox
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
message,
|
||||||
|
Card,
|
||||||
|
Space,
|
||||||
|
InputNumber,
|
||||||
|
Progress,
|
||||||
|
Drawer,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
Dropdown,
|
||||||
|
Badge,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Statistic,
|
||||||
|
Typography,
|
||||||
|
Empty,
|
||||||
|
Spin,
|
||||||
|
Alert,
|
||||||
|
Checkbox,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined,
|
PlusOutlined,
|
||||||
SearchOutlined, FilterOutlined, MoreOutlined, EyeOutlined,
|
EditOutlined,
|
||||||
CloudOutlined, EnvironmentOutlined, DashboardOutlined,
|
DeleteOutlined,
|
||||||
ExpandOutlined, CompressOutlined, CheckCircleOutlined,
|
ReloadOutlined,
|
||||||
WarningOutlined, SyncOutlined, DeleteFilled
|
SearchOutlined,
|
||||||
|
FilterOutlined,
|
||||||
|
MoreOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
CloudOutlined,
|
||||||
|
EnvironmentOutlined,
|
||||||
|
DashboardOutlined,
|
||||||
|
ExpandOutlined,
|
||||||
|
CompressOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
WarningOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
DeleteFilled,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
@@ -21,45 +54,45 @@ const designTokens = {
|
|||||||
primary: {
|
primary: {
|
||||||
main: '#1890ff',
|
main: '#1890ff',
|
||||||
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
bgGradient: 'linear-gradient(135deg, #667eea15 0%, #764ba208 100%)'
|
bgGradient: 'linear-gradient(135deg, #667eea15 0%, #764ba208 100%)',
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
main: '#52c41a',
|
main: '#52c41a',
|
||||||
gradient: 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)'
|
gradient: 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)',
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
main: '#faad14',
|
main: '#faad14',
|
||||||
gradient: 'linear-gradient(135deg, #faad14 0%, #d48806 100%)'
|
gradient: 'linear-gradient(135deg, #faad14 0%, #d48806 100%)',
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
main: '#ff4d4f',
|
main: '#ff4d4f',
|
||||||
gradient: 'linear-gradient(135deg, #ff4d4f 0%, #cf1322 100%)'
|
gradient: 'linear-gradient(135deg, #ff4d4f 0%, #cf1322 100%)',
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
primary: '#262626',
|
primary: '#262626',
|
||||||
secondary: '#8c8c8c',
|
secondary: '#8c8c8c',
|
||||||
tertiary: '#bfbfbf'
|
tertiary: '#bfbfbf',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
shadows: {
|
shadows: {
|
||||||
small: '0 2px 8px rgba(0, 0, 0, 0.06)',
|
small: '0 2px 8px rgba(0, 0, 0, 0.06)',
|
||||||
medium: '0 4px 16px rgba(0, 0, 0, 0.08)',
|
medium: '0 4px 16px rgba(0, 0, 0, 0.08)',
|
||||||
large: '0 8px 24px rgba(0, 0, 0, 0.12)'
|
large: '0 8px 24px rgba(0, 0, 0, 0.12)',
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
small: '8px',
|
small: '8px',
|
||||||
medium: '12px',
|
medium: '12px',
|
||||||
large: '16px'
|
large: '16px',
|
||||||
},
|
},
|
||||||
transitions: {
|
transitions: {
|
||||||
normal: '0.3s cubic-bezier(0.4, 0, 0.2, 1)'
|
normal: '0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const containerStyle = {
|
const containerStyle = {
|
||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
background: 'linear-gradient(180deg, #f5f7fa 0%, #e8ecf1 100%)',
|
background: 'linear-gradient(180deg, #f5f7fa 0%, #e8ecf1 100%)',
|
||||||
padding: '24px'
|
padding: '24px',
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerStyle = {
|
const headerStyle = {
|
||||||
@@ -68,15 +101,15 @@ const headerStyle = {
|
|||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
borderRadius: '20px',
|
borderRadius: '20px',
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: '0 8px 32px rgba(102, 126, 234, 0.3)'
|
boxShadow: '0 8px 32px rgba(102, 126, 234, 0.3)',
|
||||||
};
|
};
|
||||||
|
|
||||||
const statCardStyle = (color) => ({
|
const statCardStyle = color => ({
|
||||||
background: 'rgba(255, 255, 255, 0.15)',
|
background: 'rgba(255, 255, 255, 0.15)',
|
||||||
borderRadius: designTokens.borderRadius.medium,
|
borderRadius: designTokens.borderRadius.medium,
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||||
backdropFilter: 'blur(10px)'
|
backdropFilter: 'blur(10px)',
|
||||||
});
|
});
|
||||||
|
|
||||||
const cardStyle = {
|
const cardStyle = {
|
||||||
@@ -84,13 +117,13 @@ const cardStyle = {
|
|||||||
border: 'none',
|
border: 'none',
|
||||||
boxShadow: designTokens.shadows.medium,
|
boxShadow: designTokens.shadows.medium,
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
overflow: 'hidden'
|
overflow: 'hidden',
|
||||||
};
|
};
|
||||||
|
|
||||||
const cardHeadStyle = {
|
const cardHeadStyle = {
|
||||||
borderBottom: '1px solid #f0f0f0',
|
borderBottom: '1px solid #f0f0f0',
|
||||||
padding: '16px 24px',
|
padding: '16px 24px',
|
||||||
background: 'linear-gradient(135deg, #f8f9ff 0%, #ffffff 100%)'
|
background: 'linear-gradient(135deg, #f8f9ff 0%, #ffffff 100%)',
|
||||||
};
|
};
|
||||||
|
|
||||||
const primaryButtonStyle = {
|
const primaryButtonStyle = {
|
||||||
@@ -99,25 +132,25 @@ const primaryButtonStyle = {
|
|||||||
background: designTokens.colors.primary.gradient,
|
background: designTokens.colors.primary.gradient,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
boxShadow: '0 4px 16px rgba(102, 126, 234, 0.35)',
|
boxShadow: '0 4px 16px rgba(102, 126, 234, 0.35)',
|
||||||
fontWeight: '500'
|
fontWeight: '500',
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionButtonStyle = {
|
const actionButtonStyle = {
|
||||||
height: '36px',
|
height: '36px',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
border: '1px solid #e8e8e8'
|
border: '1px solid #e8e8e8',
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchInputStyle = {
|
const searchInputStyle = {
|
||||||
borderRadius: '10px',
|
borderRadius: '10px',
|
||||||
height: '42px',
|
height: '42px',
|
||||||
border: '1px solid #e8e8e8'
|
border: '1px solid #e8e8e8',
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
active: { text: '在用', color: 'success', icon: <CheckCircleOutlined /> },
|
active: { text: '在用', color: 'success', icon: <CheckCircleOutlined /> },
|
||||||
maintenance: { text: '维护中', color: 'warning', icon: <SyncOutlined spin /> },
|
maintenance: { text: '维护中', color: 'warning', icon: <SyncOutlined spin /> },
|
||||||
inactive: { text: '停用', color: 'default', icon: <WarningOutlined /> }
|
inactive: { text: '停用', color: 'default', icon: <WarningOutlined /> },
|
||||||
};
|
};
|
||||||
|
|
||||||
const CapacityProgress = ({ used, capacity, color }) => {
|
const CapacityProgress = ({ used, capacity, color }) => {
|
||||||
@@ -130,9 +163,7 @@ const CapacityProgress = ({ used, capacity, color }) => {
|
|||||||
<Text style={{ fontSize: '13px', color: designTokens.colors.text.secondary }}>
|
<Text style={{ fontSize: '13px', color: designTokens.colors.text.secondary }}>
|
||||||
{used} / {capacity}
|
{used} / {capacity}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ fontSize: '13px', fontWeight: '600', color }}>
|
<Text style={{ fontSize: '13px', fontWeight: '600', color }}>{percentage.toFixed(1)}%</Text>
|
||||||
{percentage.toFixed(1)}%
|
|
||||||
</Text>
|
|
||||||
</div>
|
</div>
|
||||||
<Progress
|
<Progress
|
||||||
percent={percentage}
|
percent={percentage}
|
||||||
@@ -159,13 +190,20 @@ const RoomCard = ({ room, onEdit, onDelete, onView, selected, onSelect }) => {
|
|||||||
border: selected ? `2px solid ${designTokens.colors.primary.main}` : '1px solid #f0f0f0',
|
border: selected ? `2px solid ${designTokens.colors.primary.main}` : '1px solid #f0f0f0',
|
||||||
boxShadow: designTokens.shadows.small,
|
boxShadow: designTokens.shadows.small,
|
||||||
transition: `all ${designTokens.transitions.normal}`,
|
transition: `all ${designTokens.transitions.normal}`,
|
||||||
cursor: 'pointer'
|
cursor: 'pointer',
|
||||||
}}
|
}}
|
||||||
onClick={() => onSelect(room.roomId)}
|
onClick={() => onSelect(room.roomId)}
|
||||||
onDoubleClick={() => onView(room)}
|
onDoubleClick={() => onView(room)}
|
||||||
styles={{ body: { padding: '20px' } }}
|
styles={{ body: { padding: '20px' } }}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '16px' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
marginBottom: '16px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
|
||||||
<CloudOutlined style={{ fontSize: '20px', color: designTokens.colors.primary.main }} />
|
<CloudOutlined style={{ fontSize: '20px', color: designTokens.colors.primary.main }} />
|
||||||
@@ -173,7 +211,9 @@ const RoomCard = ({ room, onEdit, onDelete, onView, selected, onSelect }) => {
|
|||||||
{room.name}
|
{room.name}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<Text type="secondary" style={{ fontSize: '13px' }}>{room.roomId}</Text>
|
<Text type="secondary" style={{ fontSize: '13px' }}>
|
||||||
|
{room.roomId}
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<Tag color={statusInfo.color} icon={statusInfo.icon} style={{ borderRadius: '20px' }}>
|
<Tag color={statusInfo.color} icon={statusInfo.icon} style={{ borderRadius: '20px' }}>
|
||||||
{statusInfo.text}
|
{statusInfo.text}
|
||||||
@@ -191,21 +231,43 @@ const RoomCard = ({ room, onEdit, onDelete, onView, selected, onSelect }) => {
|
|||||||
<CapacityProgress
|
<CapacityProgress
|
||||||
used={rackCount}
|
used={rackCount}
|
||||||
capacity={room.capacity}
|
capacity={room.capacity}
|
||||||
color={capacityUsage >= 90 ? designTokens.colors.error.main : capacityUsage >= 70 ? designTokens.colors.warning.main : designTokens.colors.success.main}
|
color={
|
||||||
|
capacityUsage >= 90
|
||||||
|
? designTokens.colors.error.main
|
||||||
|
: capacityUsage >= 70
|
||||||
|
? designTokens.colors.warning.main
|
||||||
|
: designTokens.colors.success.main
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<div style={{ display: 'flex', gap: '16px' }}>
|
<div style={{ display: 'flex', gap: '16px' }}>
|
||||||
<div>
|
<div>
|
||||||
<Text type="secondary" style={{ fontSize: '12px' }}>面积</Text>
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||||
<div style={{ fontSize: '14px', fontWeight: '600', color: designTokens.colors.text.primary }}>
|
面积
|
||||||
|
</Text>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: designTokens.colors.text.primary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{room.area} ㎡
|
{room.area} ㎡
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Text type="secondary" style={{ fontSize: '12px' }}>机柜</Text>
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||||
<div style={{ fontSize: '14px', fontWeight: '600', color: designTokens.colors.text.primary }}>
|
机柜
|
||||||
|
</Text>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: designTokens.colors.text.primary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{rackCount}
|
{rackCount}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -215,7 +277,10 @@ const RoomCard = ({ room, onEdit, onDelete, onView, selected, onSelect }) => {
|
|||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<EyeOutlined />}
|
icon={<EyeOutlined />}
|
||||||
onClick={(e) => { e.stopPropagation(); onView(room); }}
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onView(room);
|
||||||
|
}}
|
||||||
style={{ color: designTokens.colors.text.secondary }}
|
style={{ color: designTokens.colors.text.secondary }}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -223,7 +288,10 @@ const RoomCard = ({ room, onEdit, onDelete, onView, selected, onSelect }) => {
|
|||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
onClick={(e) => { e.stopPropagation(); onEdit(room); }}
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onEdit(room);
|
||||||
|
}}
|
||||||
style={{ color: designTokens.colors.primary.main }}
|
style={{ color: designTokens.colors.primary.main }}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -232,7 +300,10 @@ const RoomCard = ({ room, onEdit, onDelete, onView, selected, onSelect }) => {
|
|||||||
type="text"
|
type="text"
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
danger
|
danger
|
||||||
onClick={(e) => { e.stopPropagation(); onDelete(room.roomId); }}
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDelete(room.roomId);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -286,7 +357,7 @@ function RoomManagement() {
|
|||||||
setEditingRoom(null);
|
setEditingRoom(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (values) => {
|
const handleSubmit = async values => {
|
||||||
try {
|
try {
|
||||||
if (editingRoom) {
|
if (editingRoom) {
|
||||||
await axios.put(`/api/rooms/${editingRoom.roomId}`, values);
|
await axios.put(`/api/rooms/${editingRoom.roomId}`, values);
|
||||||
@@ -304,7 +375,7 @@ function RoomManagement() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (roomId) => {
|
const handleDelete = async roomId => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '确认删除',
|
title: '确认删除',
|
||||||
content: '确定要删除这个机房吗?删除后无法恢复。',
|
content: '确定要删除这个机房吗?删除后无法恢复。',
|
||||||
@@ -320,7 +391,7 @@ function RoomManagement() {
|
|||||||
message.error('机房删除失败');
|
message.error('机房删除失败');
|
||||||
console.error('机房删除失败:', error);
|
console.error('机房删除失败:', error);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -346,18 +417,19 @@ function RoomManagement() {
|
|||||||
message.error('批量删除失败');
|
message.error('批量删除失败');
|
||||||
console.error('批量删除失败:', error);
|
console.error('批量删除失败:', error);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleView = (room) => {
|
const handleView = room => {
|
||||||
setViewingRoom(room);
|
setViewingRoom(room);
|
||||||
setDrawerVisible(true);
|
setDrawerVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredRooms = useMemo(() => {
|
const filteredRooms = useMemo(() => {
|
||||||
return rooms.filter(room => {
|
return rooms.filter(room => {
|
||||||
const matchKeyword = !searchKeyword ||
|
const matchKeyword =
|
||||||
|
!searchKeyword ||
|
||||||
room.name?.toLowerCase().includes(searchKeyword.toLowerCase()) ||
|
room.name?.toLowerCase().includes(searchKeyword.toLowerCase()) ||
|
||||||
room.roomId?.toLowerCase().includes(searchKeyword.toLowerCase()) ||
|
room.roomId?.toLowerCase().includes(searchKeyword.toLowerCase()) ||
|
||||||
room.location?.toLowerCase().includes(searchKeyword.toLowerCase());
|
room.location?.toLowerCase().includes(searchKeyword.toLowerCase());
|
||||||
@@ -368,14 +440,17 @@ function RoomManagement() {
|
|||||||
});
|
});
|
||||||
}, [rooms, searchKeyword, statusFilter]);
|
}, [rooms, searchKeyword, statusFilter]);
|
||||||
|
|
||||||
const stats = useMemo(() => ({
|
const stats = useMemo(
|
||||||
total: rooms.length,
|
() => ({
|
||||||
active: rooms.filter(r => r.status === 'active').length,
|
total: rooms.length,
|
||||||
maintenance: rooms.filter(r => r.status === 'maintenance').length,
|
active: rooms.filter(r => r.status === 'active').length,
|
||||||
inactive: rooms.filter(r => r.status === 'inactive').length,
|
maintenance: rooms.filter(r => r.status === 'maintenance').length,
|
||||||
totalRacks: rooms.reduce((sum, r) => sum + (r.Racks?.length || 0), 0),
|
inactive: rooms.filter(r => r.status === 'inactive').length,
|
||||||
totalCapacity: rooms.reduce((sum, r) => sum + (r.capacity || 0), 0)
|
totalRacks: rooms.reduce((sum, r) => sum + (r.Racks?.length || 0), 0),
|
||||||
}), [rooms]);
|
totalCapacity: rooms.reduce((sum, r) => sum + (r.capacity || 0), 0),
|
||||||
|
}),
|
||||||
|
[rooms]
|
||||||
|
);
|
||||||
|
|
||||||
const tableColumns = [
|
const tableColumns = [
|
||||||
{
|
{
|
||||||
@@ -383,15 +458,17 @@ function RoomManagement() {
|
|||||||
key: 'roomInfo',
|
key: 'roomInfo',
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
<div style={{
|
<div
|
||||||
width: '44px',
|
style={{
|
||||||
height: '44px',
|
width: '44px',
|
||||||
borderRadius: '10px',
|
height: '44px',
|
||||||
background: designTokens.colors.primary.bgGradient,
|
borderRadius: '10px',
|
||||||
display: 'flex',
|
background: designTokens.colors.primary.bgGradient,
|
||||||
alignItems: 'center',
|
display: 'flex',
|
||||||
justifyContent: 'center'
|
alignItems: 'center',
|
||||||
}}>
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<CloudOutlined style={{ fontSize: '22px', color: designTokens.colors.primary.main }} />
|
<CloudOutlined style={{ fontSize: '22px', color: designTokens.colors.primary.main }} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -403,18 +480,18 @@ function RoomManagement() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '位置',
|
title: '位置',
|
||||||
dataIndex: 'location',
|
dataIndex: 'location',
|
||||||
key: 'location',
|
key: 'location',
|
||||||
render: (location) => (
|
render: location => (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||||
<EnvironmentOutlined style={{ color: designTokens.colors.text.tertiary }} />
|
<EnvironmentOutlined style={{ color: designTokens.colors.text.tertiary }} />
|
||||||
<span>{location}</span>
|
<span>{location}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '面积/容量',
|
title: '面积/容量',
|
||||||
@@ -430,13 +507,13 @@ function RoomManagement() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
key: 'status',
|
key: 'status',
|
||||||
render: (status) => {
|
render: status => {
|
||||||
const config = statusConfig[status];
|
const config = statusConfig[status];
|
||||||
return (
|
return (
|
||||||
<Tag color={config.color} icon={config.icon} style={{ borderRadius: '20px' }}>
|
<Tag color={config.color} icon={config.icon} style={{ borderRadius: '20px' }}>
|
||||||
@@ -447,9 +524,9 @@ function RoomManagement() {
|
|||||||
filters: [
|
filters: [
|
||||||
{ text: '在用', value: 'active' },
|
{ text: '在用', value: 'active' },
|
||||||
{ text: '维护中', value: 'maintenance' },
|
{ text: '维护中', value: 'maintenance' },
|
||||||
{ text: '停用', value: 'inactive' }
|
{ text: '停用', value: 'inactive' },
|
||||||
],
|
],
|
||||||
onFilter: (value, record) => record.status === value
|
onFilter: (value, record) => record.status === value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '机柜数',
|
title: '机柜数',
|
||||||
@@ -460,14 +537,14 @@ function RoomManagement() {
|
|||||||
<span>{record.Racks?.length || 0}</span>
|
<span>{record.Racks?.length || 0}</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
sorter: (a, b) => (a.Racks?.length || 0) - (b.Racks?.length || 0)
|
sorter: (a, b) => (a.Racks?.length || 0) - (b.Racks?.length || 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '创建时间',
|
title: '创建时间',
|
||||||
dataIndex: 'createdAt',
|
dataIndex: 'createdAt',
|
||||||
key: 'createdAt',
|
key: 'createdAt',
|
||||||
render: (date) => date ? new Date(date).toLocaleString() : '-',
|
render: date => (date ? new Date(date).toLocaleString() : '-'),
|
||||||
sorter: (a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0)
|
sorter: (a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
@@ -501,29 +578,44 @@ function RoomManagement() {
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rowSelection = {
|
const rowSelection = {
|
||||||
selectedRowKeys: selectedRoomIds,
|
selectedRowKeys: selectedRoomIds,
|
||||||
onChange: (selectedRowKeys) => {
|
onChange: selectedRowKeys => {
|
||||||
setSelectedRoomIds(selectedRowKeys);
|
setSelectedRoomIds(selectedRowKeys);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={containerStyle}>
|
<div style={containerStyle}>
|
||||||
<div style={headerStyle}>
|
<div style={headerStyle}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '16px' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: '16px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<h1 style={{ fontSize: '24px', fontWeight: '700', margin: '0 0 4px 0', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
<h1
|
||||||
|
style={{
|
||||||
|
fontSize: '24px',
|
||||||
|
fontWeight: '700',
|
||||||
|
margin: '0 0 4px 0',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<CloudOutlined />
|
<CloudOutlined />
|
||||||
机房管理
|
机房管理
|
||||||
</h1>
|
</h1>
|
||||||
<p style={{ margin: 0, opacity: 0.9, fontSize: '14px' }}>
|
<p style={{ margin: 0, opacity: 0.9, fontSize: '14px' }}>管理和监控所有机房设施</p>
|
||||||
管理和监控所有机房设施
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: '12px' }}>
|
<div style={{ display: 'flex', gap: '12px' }}>
|
||||||
<div style={statCardStyle()}>
|
<div style={statCardStyle()}>
|
||||||
@@ -532,7 +624,9 @@ function RoomManagement() {
|
|||||||
</div>
|
</div>
|
||||||
<div style={statCardStyle()}>
|
<div style={statCardStyle()}>
|
||||||
<Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: '12px' }}>在用机房</Text>
|
<Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: '12px' }}>在用机房</Text>
|
||||||
<div style={{ fontSize: '24px', fontWeight: '700', color: '#52c41a' }}>{stats.active}</div>
|
<div style={{ fontSize: '24px', fontWeight: '700', color: '#52c41a' }}>
|
||||||
|
{stats.active}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={statCardStyle()}>
|
<div style={statCardStyle()}>
|
||||||
<Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: '12px' }}>总机柜</Text>
|
<Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: '12px' }}>总机柜</Text>
|
||||||
@@ -543,13 +637,22 @@ function RoomManagement() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card style={cardStyle} styles={{ header: cardHeadStyle, body: { padding: '20px 24px' } }}>
|
<Card style={cardStyle} styles={{ header: cardHeadStyle, body: { padding: '20px 24px' } }}>
|
||||||
<div style={{ marginBottom: '20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '16px' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: '20px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: '16px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div style={{ display: 'flex', gap: '12px', flex: 1, maxWidth: '600px' }}>
|
<div style={{ display: 'flex', gap: '12px', flex: 1, maxWidth: '600px' }}>
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索机房名称、ID、位置..."
|
placeholder="搜索机房名称、ID、位置..."
|
||||||
prefix={<SearchOutlined style={{ color: '#bfbfbf' }} />}
|
prefix={<SearchOutlined style={{ color: '#bfbfbf' }} />}
|
||||||
value={searchKeyword}
|
value={searchKeyword}
|
||||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
onChange={e => setSearchKeyword(e.target.value)}
|
||||||
style={searchInputStyle}
|
style={searchInputStyle}
|
||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
@@ -625,7 +728,7 @@ function RoomManagement() {
|
|||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
showQuickJumper: true,
|
showQuickJumper: true,
|
||||||
showTotal: (total) => `共 ${total} 条记录`
|
showTotal: total => `共 ${total} 条记录`,
|
||||||
}}
|
}}
|
||||||
scroll={{ x: 1000 }}
|
scroll={{ x: 1000 }}
|
||||||
rowClassName={() => 'table-row'}
|
rowClassName={() => 'table-row'}
|
||||||
@@ -641,7 +744,7 @@ function RoomManagement() {
|
|||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onView={handleView}
|
onView={handleView}
|
||||||
selected={selectedRoomIds.includes(room.roomId)}
|
selected={selectedRoomIds.includes(room.roomId)}
|
||||||
onSelect={(id) => {
|
onSelect={id => {
|
||||||
if (selectedRoomIds.includes(id)) {
|
if (selectedRoomIds.includes(id)) {
|
||||||
setSelectedRoomIds(selectedRoomIds.filter(rid => rid !== id));
|
setSelectedRoomIds(selectedRoomIds.filter(rid => rid !== id));
|
||||||
} else {
|
} else {
|
||||||
@@ -663,12 +766,14 @@ function RoomManagement() {
|
|||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||||
<div style={{
|
<div
|
||||||
width: '4px',
|
style={{
|
||||||
height: '20px',
|
width: '4px',
|
||||||
background: designTokens.colors.primary.gradient,
|
height: '20px',
|
||||||
borderRadius: '2px'
|
background: designTokens.colors.primary.gradient,
|
||||||
}} />
|
borderRadius: '2px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{editingRoom ? '编辑机房' : '添加机房'}
|
{editingRoom ? '编辑机房' : '添加机房'}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -679,7 +784,7 @@ function RoomManagement() {
|
|||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
styles={{
|
styles={{
|
||||||
body: { padding: '24px' },
|
body: { padding: '24px' },
|
||||||
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }
|
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' },
|
||||||
}}
|
}}
|
||||||
style={{ borderRadius: '16px' }}
|
style={{ borderRadius: '16px' }}
|
||||||
>
|
>
|
||||||
@@ -710,7 +815,11 @@ function RoomManagement() {
|
|||||||
label="位置"
|
label="位置"
|
||||||
rules={[{ required: true, message: '请输入机房位置' }]}
|
rules={[{ required: true, message: '请输入机房位置' }]}
|
||||||
>
|
>
|
||||||
<Input prefix={<EnvironmentOutlined />} placeholder="请输入机房位置" style={{ borderRadius: '8px' }} />
|
<Input
|
||||||
|
prefix={<EnvironmentOutlined />}
|
||||||
|
placeholder="请输入机房位置"
|
||||||
|
style={{ borderRadius: '8px' }}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
@@ -720,7 +829,12 @@ function RoomManagement() {
|
|||||||
label="面积(㎡)"
|
label="面积(㎡)"
|
||||||
rules={[{ required: true, message: '请输入机房面积' }]}
|
rules={[{ required: true, message: '请输入机房面积' }]}
|
||||||
>
|
>
|
||||||
<InputNumber placeholder="请输入机房面积" min={0} step={0.1} style={{ width: '100%', borderRadius: '8px' }} />
|
<InputNumber
|
||||||
|
placeholder="请输入机房面积"
|
||||||
|
min={0}
|
||||||
|
step={0.1}
|
||||||
|
style={{ width: '100%', borderRadius: '8px' }}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
@@ -729,16 +843,16 @@ function RoomManagement() {
|
|||||||
label="容量(机柜数)"
|
label="容量(机柜数)"
|
||||||
rules={[{ required: true, message: '请输入机柜容量' }]}
|
rules={[{ required: true, message: '请输入机柜容量' }]}
|
||||||
>
|
>
|
||||||
<InputNumber placeholder="请输入机柜容量" min={0} style={{ width: '100%', borderRadius: '8px' }} />
|
<InputNumber
|
||||||
|
placeholder="请输入机柜容量"
|
||||||
|
min={0}
|
||||||
|
style={{ width: '100%', borderRadius: '8px' }}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item name="status" label="状态" rules={[{ required: true, message: '请选择状态' }]}>
|
||||||
name="status"
|
|
||||||
label="状态"
|
|
||||||
rules={[{ required: true, message: '请选择状态' }]}
|
|
||||||
>
|
|
||||||
<Select placeholder="请选择状态" style={{ borderRadius: '8px' }}>
|
<Select placeholder="请选择状态" style={{ borderRadius: '8px' }}>
|
||||||
<Option value="active">在用</Option>
|
<Option value="active">在用</Option>
|
||||||
<Option value="maintenance">维护中</Option>
|
<Option value="maintenance">维护中</Option>
|
||||||
@@ -750,7 +864,9 @@ function RoomManagement() {
|
|||||||
<Input.TextArea placeholder="请输入机房描述" rows={3} style={{ borderRadius: '8px' }} />
|
<Input.TextArea placeholder="请输入机房描述" rows={3} style={{ borderRadius: '8px' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px', marginTop: '24px' }}>
|
<div
|
||||||
|
style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px', marginTop: '24px' }}
|
||||||
|
>
|
||||||
<Button onClick={handleCancel} style={{ borderRadius: '8px', height: '42px' }}>
|
<Button onClick={handleCancel} style={{ borderRadius: '8px', height: '42px' }}>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
@@ -773,21 +889,33 @@ function RoomManagement() {
|
|||||||
width={480}
|
width={480}
|
||||||
styles={{
|
styles={{
|
||||||
header: { borderBottom: '1px solid #f0f0f0' },
|
header: { borderBottom: '1px solid #f0f0f0' },
|
||||||
body: { padding: '24px' }
|
body: { padding: '24px' },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{viewingRoom && (
|
{viewingRoom && (
|
||||||
<div>
|
<div>
|
||||||
<div style={{
|
<div
|
||||||
padding: '20px',
|
style={{
|
||||||
background: designTokens.colors.primary.bgGradient,
|
padding: '20px',
|
||||||
borderRadius: designTokens.borderRadius.medium,
|
background: designTokens.colors.primary.bgGradient,
|
||||||
marginBottom: '20px'
|
borderRadius: designTokens.borderRadius.medium,
|
||||||
}}>
|
marginBottom: '20px',
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '16px' }}>
|
}}
|
||||||
<CloudOutlined style={{ fontSize: '32px', color: designTokens.colors.primary.main }} />
|
>
|
||||||
|
<div
|
||||||
|
style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '16px' }}
|
||||||
|
>
|
||||||
|
<CloudOutlined
|
||||||
|
style={{ fontSize: '32px', color: designTokens.colors.primary.main }}
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: '18px', fontWeight: '600', color: designTokens.colors.text.primary }}>
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: designTokens.colors.text.primary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{viewingRoom.name}
|
{viewingRoom.name}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '13px', color: designTokens.colors.text.secondary }}>
|
<div style={{ fontSize: '13px', color: designTokens.colors.text.secondary }}>
|
||||||
@@ -801,7 +929,9 @@ function RoomManagement() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: '20px' }}>
|
<div style={{ marginBottom: '20px' }}>
|
||||||
<Text type="secondary" style={{ fontSize: '13px' }}>位置</Text>
|
<Text type="secondary" style={{ fontSize: '13px' }}>
|
||||||
|
位置
|
||||||
|
</Text>
|
||||||
<div style={{ fontSize: '15px', fontWeight: '500', marginTop: '4px' }}>
|
<div style={{ fontSize: '15px', fontWeight: '500', marginTop: '4px' }}>
|
||||||
<EnvironmentOutlined style={{ marginRight: '8px' }} />
|
<EnvironmentOutlined style={{ marginRight: '8px' }} />
|
||||||
{viewingRoom.location}
|
{viewingRoom.location}
|
||||||
@@ -811,16 +941,32 @@ function RoomManagement() {
|
|||||||
<Row gutter={16} style={{ marginBottom: '20px' }}>
|
<Row gutter={16} style={{ marginBottom: '20px' }}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<div style={{ padding: '16px', background: '#fafafa', borderRadius: '10px' }}>
|
<div style={{ padding: '16px', background: '#fafafa', borderRadius: '10px' }}>
|
||||||
<Text type="secondary" style={{ fontSize: '12px' }}>面积</Text>
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||||
<div style={{ fontSize: '20px', fontWeight: '600', color: designTokens.colors.text.primary }}>
|
面积
|
||||||
|
</Text>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '20px',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: designTokens.colors.text.primary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{viewingRoom.area} ㎡
|
{viewingRoom.area} ㎡
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<div style={{ padding: '16px', background: '#fafafa', borderRadius: '10px' }}>
|
<div style={{ padding: '16px', background: '#fafafa', borderRadius: '10px' }}>
|
||||||
<Text type="secondary" style={{ fontSize: '12px' }}>容量</Text>
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||||
<div style={{ fontSize: '20px', fontWeight: '600', color: designTokens.colors.text.primary }}>
|
容量
|
||||||
|
</Text>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '20px',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: designTokens.colors.text.primary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{viewingRoom.capacity} 机柜
|
{viewingRoom.capacity} 机柜
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -828,7 +974,9 @@ function RoomManagement() {
|
|||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<div style={{ marginBottom: '20px' }}>
|
<div style={{ marginBottom: '20px' }}>
|
||||||
<Text type="secondary" style={{ fontSize: '13px' }}>机柜使用情况</Text>
|
<Text type="secondary" style={{ fontSize: '13px' }}>
|
||||||
|
机柜使用情况
|
||||||
|
</Text>
|
||||||
<div style={{ marginTop: '12px' }}>
|
<div style={{ marginTop: '12px' }}>
|
||||||
<CapacityProgress
|
<CapacityProgress
|
||||||
used={viewingRoom.Racks?.length || 0}
|
used={viewingRoom.Racks?.length || 0}
|
||||||
@@ -840,8 +988,17 @@ function RoomManagement() {
|
|||||||
|
|
||||||
{viewingRoom.description && (
|
{viewingRoom.description && (
|
||||||
<div style={{ marginBottom: '20px' }}>
|
<div style={{ marginBottom: '20px' }}>
|
||||||
<Text type="secondary" style={{ fontSize: '13px' }}>描述</Text>
|
<Text type="secondary" style={{ fontSize: '13px' }}>
|
||||||
<div style={{ marginTop: '8px', padding: '12px', background: '#fafafa', borderRadius: '8px' }}>
|
描述
|
||||||
|
</Text>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: '8px',
|
||||||
|
padding: '12px',
|
||||||
|
background: '#fafafa',
|
||||||
|
borderRadius: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{viewingRoom.description}
|
{viewingRoom.description}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,28 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Tabs, Form, Input, Switch, Select, Button, Card, Space, message, Modal, Tag, Divider, Descriptions, Alert } from 'antd';
|
import {
|
||||||
import { SettingOutlined, GlobalOutlined, BgColorsOutlined, InfoCircleOutlined, CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
|
Tabs,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Switch,
|
||||||
|
Select,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Space,
|
||||||
|
message,
|
||||||
|
Modal,
|
||||||
|
Tag,
|
||||||
|
Divider,
|
||||||
|
Descriptions,
|
||||||
|
Alert,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
SettingOutlined,
|
||||||
|
GlobalOutlined,
|
||||||
|
BgColorsOutlined,
|
||||||
|
InfoCircleOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useConfig } from '../context/ConfigContext';
|
import { useConfig } from '../context/ConfigContext';
|
||||||
|
|
||||||
@@ -48,7 +70,7 @@ const SystemSettings = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveSettings = async (values) => {
|
const handleSaveSettings = async values => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const updates = {};
|
const updates = {};
|
||||||
@@ -76,7 +98,10 @@ const SystemSettings = () => {
|
|||||||
title: '前端端口已修改(生产环境)',
|
title: '前端端口已修改(生产环境)',
|
||||||
content: (
|
content: (
|
||||||
<div>
|
<div>
|
||||||
<p>前端端口已从 <strong>{settings.frontend_port?.value}</strong> 更改为 <strong>{updates.frontend_port}</strong></p>
|
<p>
|
||||||
|
前端端口已从 <strong>{settings.frontend_port?.value}</strong> 更改为{' '}
|
||||||
|
<strong>{updates.frontend_port}</strong>
|
||||||
|
</p>
|
||||||
<Alert
|
<Alert
|
||||||
message="请手动更新服务器配置"
|
message="请手动更新服务器配置"
|
||||||
description={
|
description={
|
||||||
@@ -93,7 +118,7 @@ const SystemSettings = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
okText: '知道了'
|
okText: '知道了',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 开发环境:显示确认对话框并自动重启
|
// 开发环境:显示确认对话框并自动重启
|
||||||
@@ -102,7 +127,10 @@ const SystemSettings = () => {
|
|||||||
icon: <ExclamationCircleOutlined />,
|
icon: <ExclamationCircleOutlined />,
|
||||||
content: (
|
content: (
|
||||||
<div>
|
<div>
|
||||||
<p>前端端口已从 <strong>{settings.frontend_port?.value}</strong> 更改为 <strong>{updates.frontend_port}</strong></p>
|
<p>
|
||||||
|
前端端口已从 <strong>{settings.frontend_port?.value}</strong> 更改为{' '}
|
||||||
|
<strong>{updates.frontend_port}</strong>
|
||||||
|
</p>
|
||||||
<p>是否立即重启前端服务以应用新端口?</p>
|
<p>是否立即重启前端服务以应用新端口?</p>
|
||||||
<Alert
|
<Alert
|
||||||
message="注意"
|
message="注意"
|
||||||
@@ -125,9 +153,13 @@ const SystemSettings = () => {
|
|||||||
title: '正在重启前端服务',
|
title: '正在重启前端服务',
|
||||||
content: (
|
content: (
|
||||||
<div>
|
<div>
|
||||||
<p>前端服务正在重启,新端口:<strong>{newPort}</strong></p>
|
<p>
|
||||||
|
前端服务正在重启,新端口:<strong>{newPort}</strong>
|
||||||
|
</p>
|
||||||
<p>页面将在3秒后自动跳转到新地址...</p>
|
<p>页面将在3秒后自动跳转到新地址...</p>
|
||||||
<p>如果跳转失败,请手动访问:<a href={newUrl}>{newUrl}</a></p>
|
<p>
|
||||||
|
如果跳转失败,请手动访问:<a href={newUrl}>{newUrl}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
okText: '立即跳转',
|
okText: '立即跳转',
|
||||||
@@ -135,14 +167,18 @@ const SystemSettings = () => {
|
|||||||
maskClosable: false,
|
maskClosable: false,
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
window.location.href = newUrl;
|
window.location.href = newUrl;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 延迟调用重启API,让用户看到提示
|
// 延迟调用重启API,让用户看到提示
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
// 调用重启API(这个请求可能会因为服务重启而失败)
|
// 调用重启API(这个请求可能会因为服务重启而失败)
|
||||||
await axios.post('/api/system-settings/frontend/restart', {}, { timeout: 5000 });
|
await axios.post(
|
||||||
|
'/api/system-settings/frontend/restart',
|
||||||
|
{},
|
||||||
|
{ timeout: 5000 }
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 忽略错误,因为服务重启会导致连接中断
|
// 忽略错误,因为服务重启会导致连接中断
|
||||||
console.log('重启请求已发送,服务正在重启...');
|
console.log('重启请求已发送,服务正在重启...');
|
||||||
@@ -153,7 +189,7 @@ const SystemSettings = () => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = newUrl;
|
window.location.href = newUrl;
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (syncError) {
|
} catch (syncError) {
|
||||||
@@ -174,7 +210,7 @@ const SystemSettings = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResetSetting = (key) => {
|
const handleResetSetting = key => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '确认重置',
|
title: '确认重置',
|
||||||
icon: <ExclamationCircleOutlined />,
|
icon: <ExclamationCircleOutlined />,
|
||||||
@@ -189,18 +225,14 @@ const SystemSettings = () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('重置失败');
|
message.error('重置失败');
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderFormItem = (key, data) => {
|
const renderFormItem = (key, data) => {
|
||||||
if (!data.isEditable) {
|
if (!data.isEditable) {
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item key={key} label={data.description || key} name={key}>
|
||||||
key={key}
|
|
||||||
label={data.description || key}
|
|
||||||
name={key}
|
|
||||||
>
|
|
||||||
<Input disabled suffix={<LockOutlined />} />
|
<Input disabled suffix={<LockOutlined />} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
@@ -209,12 +241,7 @@ const SystemSettings = () => {
|
|||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item key={key} label={data.description || key} name={key} valuePropName="checked">
|
||||||
key={key}
|
|
||||||
label={data.description || key}
|
|
||||||
name={key}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
@@ -227,56 +254,63 @@ const SystemSettings = () => {
|
|||||||
name={key}
|
name={key}
|
||||||
rules={[
|
rules={[
|
||||||
{ required: false, message: `请输入${data.description || key}` },
|
{ required: false, message: `请输入${data.description || key}` },
|
||||||
...(isPortField ? [
|
...(isPortField
|
||||||
{ type: 'number', min: 1, max: 65535, message: '端口号必须在 1-65535 之间', transform: value => Number(value) }
|
? [
|
||||||
] : [])
|
{
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
message: '端口号必须在 1-65535 之间',
|
||||||
|
transform: value => Number(value),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input type="number" style={{ width: '100%' }} min={isPortField ? 1 : undefined} max={isPortField ? 65535 : undefined} />
|
<Input
|
||||||
|
type="number"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
min={isPortField ? 1 : undefined}
|
||||||
|
max={isPortField ? 65535 : undefined}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
case 'select':
|
case 'select':
|
||||||
const options = getSelectOptions(key);
|
const options = getSelectOptions(key);
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item key={key} label={data.description || key} name={key}>
|
||||||
key={key}
|
|
||||||
label={data.description || key}
|
|
||||||
name={key}
|
|
||||||
>
|
|
||||||
<Select>
|
<Select>
|
||||||
{options.map(opt => (
|
{options.map(opt => (
|
||||||
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
|
<Option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item key={key} label={data.description || key} name={key}>
|
||||||
key={key}
|
|
||||||
label={data.description || key}
|
|
||||||
name={key}
|
|
||||||
>
|
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSelectOptions = (key) => {
|
const getSelectOptions = key => {
|
||||||
const optionsMap = {
|
const optionsMap = {
|
||||||
timezone: [
|
timezone: [
|
||||||
{ value: 'Asia/Shanghai', label: '亚洲/上海 (UTC+8)' },
|
{ value: 'Asia/Shanghai', label: '亚洲/上海 (UTC+8)' },
|
||||||
{ value: 'Asia/Beijing', label: '亚洲/北京 (UTC+8)' },
|
{ value: 'Asia/Beijing', label: '亚洲/北京 (UTC+8)' },
|
||||||
{ value: 'America/New_York', label: '美洲/纽约 (UTC-5)' },
|
{ value: 'America/New_York', label: '美洲/纽约 (UTC-5)' },
|
||||||
{ value: 'Europe/London', label: '欧洲/伦敦 (UTC+0)' },
|
{ value: 'Europe/London', label: '欧洲/伦敦 (UTC+0)' },
|
||||||
{ value: 'UTC', label: 'UTC (UTC+0)' }
|
{ value: 'UTC', label: 'UTC (UTC+0)' },
|
||||||
],
|
],
|
||||||
date_format: [
|
date_format: [
|
||||||
{ value: 'YYYY-MM-DD', label: '2024-01-01' },
|
{ value: 'YYYY-MM-DD', label: '2024-01-01' },
|
||||||
{ value: 'YYYY/MM/DD', label: '2024/01/01' },
|
{ value: 'YYYY/MM/DD', label: '2024/01/01' },
|
||||||
{ value: 'DD/MM/YYYY', label: '01/01/2024' },
|
{ value: 'DD/MM/YYYY', label: '01/01/2024' },
|
||||||
{ value: 'MM/DD/YYYY', label: '01/01/2024' }
|
{ value: 'MM/DD/YYYY', label: '01/01/2024' },
|
||||||
],
|
],
|
||||||
primary_color: [
|
primary_color: [
|
||||||
{ value: '#667eea', label: '蓝色 (#667eea)' },
|
{ value: '#667eea', label: '蓝色 (#667eea)' },
|
||||||
@@ -288,7 +322,7 @@ const SystemSettings = () => {
|
|||||||
{ value: '#fee140', label: '黄色 (#fee140)' },
|
{ value: '#fee140', label: '黄色 (#fee140)' },
|
||||||
{ value: '#00b4db', label: '青色 (#00b4db)' },
|
{ value: '#00b4db', label: '青色 (#00b4db)' },
|
||||||
{ value: '#0083b0', label: '深蓝色 (#0083b0)' },
|
{ value: '#0083b0', label: '深蓝色 (#0083b0)' },
|
||||||
{ value: '#fcb045', label: '橙色 (#fcb045)' }
|
{ value: '#fcb045', label: '橙色 (#fcb045)' },
|
||||||
],
|
],
|
||||||
secondary_color: [
|
secondary_color: [
|
||||||
{ value: '#764ba2', label: '紫色 (#764ba2)' },
|
{ value: '#764ba2', label: '紫色 (#764ba2)' },
|
||||||
@@ -300,36 +334,44 @@ const SystemSettings = () => {
|
|||||||
{ value: '#00b4db', label: '青色 (#00b4db)' },
|
{ value: '#00b4db', label: '青色 (#00b4db)' },
|
||||||
{ value: '#0083b0', label: '深蓝色 (#0083b0)' },
|
{ value: '#0083b0', label: '深蓝色 (#0083b0)' },
|
||||||
{ value: '#fcb045', label: '橙色 (#fcb045)' },
|
{ value: '#fcb045', label: '橙色 (#fcb045)' },
|
||||||
{ value: '#f093fb', label: '粉色 (#f093fb)' }
|
{ value: '#f093fb', label: '粉色 (#f093fb)' },
|
||||||
],
|
],
|
||||||
table_row_height: [
|
table_row_height: [
|
||||||
{ value: 'small', label: '紧凑 (Small)' },
|
{ value: 'small', label: '紧凑 (Small)' },
|
||||||
{ value: 'default', label: '默认 (Default)' },
|
{ value: 'default', label: '默认 (Default)' },
|
||||||
{ value: 'middle', label: '中等 (Middle)' },
|
{ value: 'middle', label: '中等 (Middle)' },
|
||||||
{ value: 'large', label: '宽松 (Large)' }
|
{ value: 'large', label: '宽松 (Large)' },
|
||||||
],
|
],
|
||||||
dark_mode: [
|
dark_mode: [
|
||||||
{ value: 'false', label: '关闭' },
|
{ value: 'false', label: '关闭' },
|
||||||
{ value: 'true', label: '开启' }
|
{ value: 'true', label: '开启' },
|
||||||
],
|
],
|
||||||
compact_mode: [
|
compact_mode: [
|
||||||
{ value: 'false', label: '关闭' },
|
{ value: 'false', label: '关闭' },
|
||||||
{ value: 'true', label: '开启' }
|
{ value: 'true', label: '开启' },
|
||||||
],
|
],
|
||||||
animation_enabled: [
|
animation_enabled: [
|
||||||
{ value: 'false', label: '关闭' },
|
{ value: 'false', label: '关闭' },
|
||||||
{ value: 'true', label: '开启' }
|
{ value: 'true', label: '开启' },
|
||||||
],
|
],
|
||||||
sidebar_collapsed: [
|
sidebar_collapsed: [
|
||||||
{ value: 'false', label: '展开' },
|
{ value: 'false', label: '展开' },
|
||||||
{ value: 'true', label: '折叠' }
|
{ value: 'true', label: '折叠' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
return optionsMap[key] || [];
|
return optionsMap[key] || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderGeneralSettings = () => {
|
const renderGeneralSettings = () => {
|
||||||
const generalKeys = ['site_name', 'site_logo', 'timezone', 'date_format', 'session_timeout', 'max_login_attempts', 'maintenance_mode'];
|
const generalKeys = [
|
||||||
|
'site_name',
|
||||||
|
'site_logo',
|
||||||
|
'timezone',
|
||||||
|
'date_format',
|
||||||
|
'session_timeout',
|
||||||
|
'max_login_attempts',
|
||||||
|
'maintenance_mode',
|
||||||
|
];
|
||||||
return (
|
return (
|
||||||
<Card title="全局配置" bordered={false}>
|
<Card title="全局配置" bordered={false}>
|
||||||
<Form form={form} layout="vertical" onFinish={handleSaveSettings}>
|
<Form form={form} layout="vertical" onFinish={handleSaveSettings}>
|
||||||
@@ -343,7 +385,9 @@ const SystemSettings = () => {
|
|||||||
})}
|
})}
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="primary" htmlType="submit" loading={saving}>保存设置</Button>
|
<Button type="primary" htmlType="submit" loading={saving}>
|
||||||
|
保存设置
|
||||||
|
</Button>
|
||||||
<Button onClick={() => fetchSettings()}>重置表单</Button>
|
<Button onClick={() => fetchSettings()}>重置表单</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -353,7 +397,14 @@ const SystemSettings = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderAppearanceSettings = () => {
|
const renderAppearanceSettings = () => {
|
||||||
const appearanceKeys = ['primary_color', 'secondary_color', 'compact_mode', 'sidebar_collapsed', 'table_row_height', 'animation_enabled'];
|
const appearanceKeys = [
|
||||||
|
'primary_color',
|
||||||
|
'secondary_color',
|
||||||
|
'compact_mode',
|
||||||
|
'sidebar_collapsed',
|
||||||
|
'table_row_height',
|
||||||
|
'animation_enabled',
|
||||||
|
];
|
||||||
return (
|
return (
|
||||||
<Card title="外观设置" bordered={false}>
|
<Card title="外观设置" bordered={false}>
|
||||||
<Form form={form} layout="vertical" onFinish={handleSaveSettings}>
|
<Form form={form} layout="vertical" onFinish={handleSaveSettings}>
|
||||||
@@ -374,7 +425,9 @@ const SystemSettings = () => {
|
|||||||
})}
|
})}
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="primary" htmlType="submit" loading={saving}>保存设置</Button>
|
<Button type="primary" htmlType="submit" loading={saving}>
|
||||||
|
保存设置
|
||||||
|
</Button>
|
||||||
<Button onClick={() => fetchSettings()}>重置表单</Button>
|
<Button onClick={() => fetchSettings()}>重置表单</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -386,14 +439,25 @@ const SystemSettings = () => {
|
|||||||
// 数据备份功能已移除
|
// 数据备份功能已移除
|
||||||
|
|
||||||
const renderAboutPage = () => {
|
const renderAboutPage = () => {
|
||||||
const aboutKeys = ['app_version', 'company_name', 'contact_email', 'contact_phone', 'company_address', 'system_description', 'privacy_policy', 'terms_of_service'];
|
const aboutKeys = [
|
||||||
|
'app_version',
|
||||||
|
'company_name',
|
||||||
|
'contact_email',
|
||||||
|
'contact_phone',
|
||||||
|
'company_address',
|
||||||
|
'system_description',
|
||||||
|
'privacy_policy',
|
||||||
|
'terms_of_service',
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Card title="关于系统" bordered={false} style={{ marginBottom: 16 }}>
|
<Card title="关于系统" bordered={false} style={{ marginBottom: 16 }}>
|
||||||
<Descriptions column={{ xs: 1, sm: 2, md: 3 }} bordered>
|
<Descriptions column={{ xs: 1, sm: 2, md: 3 }} bordered>
|
||||||
<Descriptions.Item label="系统名称">机柜管理系统</Descriptions.Item>
|
<Descriptions.Item label="系统名称">机柜管理系统</Descriptions.Item>
|
||||||
<Descriptions.Item label="版本号">{settings.app_version?.value || '1.0.0'}</Descriptions.Item>
|
<Descriptions.Item label="版本号">
|
||||||
|
{settings.app_version?.value || '1.0.0'}
|
||||||
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="系统状态">
|
<Descriptions.Item label="系统状态">
|
||||||
<Tag color="success">运行正常</Tag>
|
<Tag color="success">运行正常</Tag>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
@@ -405,7 +469,14 @@ const SystemSettings = () => {
|
|||||||
{aboutKeys.slice(1).map(key => settings[key] && renderFormItem(key, settings[key]))}
|
{aboutKeys.slice(1).map(key => settings[key] && renderFormItem(key, settings[key]))}
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="primary" htmlType="submit" loading={saving} onClick={() => form.submit()}>保存信息</Button>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
loading={saving}
|
||||||
|
onClick={() => form.submit()}
|
||||||
|
>
|
||||||
|
保存信息
|
||||||
|
</Button>
|
||||||
<Button onClick={() => fetchSettings()}>重置</Button>
|
<Button onClick={() => fetchSettings()}>重置</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -415,24 +486,42 @@ const SystemSettings = () => {
|
|||||||
{systemInfo && (
|
{systemInfo && (
|
||||||
<Card title="系统统计信息" bordered={false}>
|
<Card title="系统统计信息" bordered={false}>
|
||||||
<Descriptions column={{ xs: 1, sm: 2, md: 4 }} bordered size="small">
|
<Descriptions column={{ xs: 1, sm: 2, md: 4 }} bordered size="small">
|
||||||
<Descriptions.Item label="设备总数">{systemInfo.statistics?.devices || 0}</Descriptions.Item>
|
<Descriptions.Item label="设备总数">
|
||||||
<Descriptions.Item label="机柜总数">{systemInfo.statistics?.racks || 0}</Descriptions.Item>
|
{systemInfo.statistics?.devices || 0}
|
||||||
<Descriptions.Item label="机房总数">{systemInfo.statistics?.rooms || 0}</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="用户总数">{systemInfo.statistics?.users || 0}</Descriptions.Item>
|
<Descriptions.Item label="机柜总数">
|
||||||
|
{systemInfo.statistics?.racks || 0}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="机房总数">
|
||||||
|
{systemInfo.statistics?.rooms || 0}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="用户总数">
|
||||||
|
{systemInfo.statistics?.users || 0}
|
||||||
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Descriptions column={{ xs: 1, sm: 2 }} bordered size="small">
|
<Descriptions column={{ xs: 1, sm: 2 }} bordered size="small">
|
||||||
<Descriptions.Item label="Node.js 版本">{systemInfo.system?.nodeVersion}</Descriptions.Item>
|
<Descriptions.Item label="Node.js 版本">
|
||||||
<Descriptions.Item label="运行平台">{systemInfo.system?.platform} ({systemInfo.system?.arch})</Descriptions.Item>
|
{systemInfo.system?.nodeVersion}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="运行平台">
|
||||||
|
{systemInfo.system?.platform} ({systemInfo.system?.arch})
|
||||||
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="进程 ID">{systemInfo.system?.pid}</Descriptions.Item>
|
<Descriptions.Item label="进程 ID">{systemInfo.system?.pid}</Descriptions.Item>
|
||||||
<Descriptions.Item label="运行时间">
|
<Descriptions.Item label="运行时间">
|
||||||
{systemInfo.system?.uptime ? `${Math.floor(systemInfo.system.uptime / 3600)}小时${Math.floor((systemInfo.system.uptime % 3600) / 60)}分钟` : '-'}
|
{systemInfo.system?.uptime
|
||||||
|
? `${Math.floor(systemInfo.system.uptime / 3600)}小时${Math.floor((systemInfo.system.uptime % 3600) / 60)}分钟`
|
||||||
|
: '-'}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="内存使用">
|
<Descriptions.Item label="内存使用">
|
||||||
{systemInfo.system?.memoryUsage ? `${(systemInfo.system.memoryUsage.heapUsed / 1024 / 1024).toFixed(2)} MB` : '-'}
|
{systemInfo.system?.memoryUsage
|
||||||
|
? `${(systemInfo.system.memoryUsage.heapUsed / 1024 / 1024).toFixed(2)} MB`
|
||||||
|
: '-'}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="系统时间">
|
<Descriptions.Item label="系统时间">
|
||||||
{systemInfo.timestamp ? new Date(systemInfo.timestamp).toLocaleString('zh-CN') : '-'}
|
{systemInfo.timestamp
|
||||||
|
? new Date(systemInfo.timestamp).toLocaleString('zh-CN')
|
||||||
|
: '-'}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -445,19 +534,31 @@ const SystemSettings = () => {
|
|||||||
<div style={{ padding: 24 }}>
|
<div style={{ padding: 24 }}>
|
||||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||||
<TabPane
|
<TabPane
|
||||||
tab={<span><GlobalOutlined /> 全局配置</span>}
|
tab={
|
||||||
|
<span>
|
||||||
|
<GlobalOutlined /> 全局配置
|
||||||
|
</span>
|
||||||
|
}
|
||||||
key="general"
|
key="general"
|
||||||
>
|
>
|
||||||
{renderGeneralSettings()}
|
{renderGeneralSettings()}
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane
|
<TabPane
|
||||||
tab={<span><BgColorsOutlined /> 外观设置</span>}
|
tab={
|
||||||
|
<span>
|
||||||
|
<BgColorsOutlined /> 外观设置
|
||||||
|
</span>
|
||||||
|
}
|
||||||
key="appearance"
|
key="appearance"
|
||||||
>
|
>
|
||||||
{renderAppearanceSettings()}
|
{renderAppearanceSettings()}
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane
|
<TabPane
|
||||||
tab={<span><InfoCircleOutlined /> 关于</span>}
|
tab={
|
||||||
|
<span>
|
||||||
|
<InfoCircleOutlined /> 关于
|
||||||
|
</span>
|
||||||
|
}
|
||||||
key="about"
|
key="about"
|
||||||
>
|
>
|
||||||
{renderAboutPage()}
|
{renderAboutPage()}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function TicketCategoryManagement() {
|
|||||||
priority: category.priority,
|
priority: category.priority,
|
||||||
defaultPriority: category.defaultPriority,
|
defaultPriority: category.defaultPriority,
|
||||||
expectedDuration: category.expectedDuration,
|
expectedDuration: category.expectedDuration,
|
||||||
isActive: category.isActive
|
isActive: category.isActive,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
@@ -62,7 +62,7 @@ function TicketCategoryManagement() {
|
|||||||
setEditingCategory(null);
|
setEditingCategory(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (values) => {
|
const handleSubmit = async values => {
|
||||||
try {
|
try {
|
||||||
if (editingCategory) {
|
if (editingCategory) {
|
||||||
await axios.put(`/api/ticket-categories/${editingCategory.categoryId}`, values);
|
await axios.put(`/api/ticket-categories/${editingCategory.categoryId}`, values);
|
||||||
@@ -81,7 +81,7 @@ function TicketCategoryManagement() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (categoryId) => {
|
const handleDelete = async categoryId => {
|
||||||
try {
|
try {
|
||||||
await axios.delete(`/api/ticket-categories/${categoryId}`);
|
await axios.delete(`/api/ticket-categories/${categoryId}`);
|
||||||
message.success('分类删除成功');
|
message.success('分类删除成功');
|
||||||
@@ -97,49 +97,47 @@ function TicketCategoryManagement() {
|
|||||||
title: '分类ID',
|
title: '分类ID',
|
||||||
dataIndex: 'categoryId',
|
dataIndex: 'categoryId',
|
||||||
key: 'categoryId',
|
key: 'categoryId',
|
||||||
width: 150
|
width: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '分类名称',
|
title: '分类名称',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
width: 180
|
width: 180,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '分类说明',
|
title: '分类说明',
|
||||||
dataIndex: 'description',
|
dataIndex: 'description',
|
||||||
key: 'description',
|
key: 'description',
|
||||||
width: 300,
|
width: 300,
|
||||||
ellipsis: true
|
ellipsis: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '优先级',
|
title: '优先级',
|
||||||
dataIndex: 'priority',
|
dataIndex: 'priority',
|
||||||
key: 'priority',
|
key: 'priority',
|
||||||
width: 80
|
width: 80,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '默认优先级',
|
title: '默认优先级',
|
||||||
dataIndex: 'defaultPriority',
|
dataIndex: 'defaultPriority',
|
||||||
key: 'defaultPriority',
|
key: 'defaultPriority',
|
||||||
width: 100
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '预计时长(分钟)',
|
title: '预计时长(分钟)',
|
||||||
dataIndex: 'expectedDuration',
|
dataIndex: 'expectedDuration',
|
||||||
key: 'expectedDuration',
|
key: 'expectedDuration',
|
||||||
width: 120
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '启用状态',
|
title: '启用状态',
|
||||||
dataIndex: 'isActive',
|
dataIndex: 'isActive',
|
||||||
key: 'isActive',
|
key: 'isActive',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (text) => (
|
render: text => (
|
||||||
<span style={{ color: text ? 'green' : 'red' }}>
|
<span style={{ color: text ? 'green' : 'red' }}>{text ? '启用' : '禁用'}</span>
|
||||||
{text ? '启用' : '禁用'}
|
),
|
||||||
</span>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
@@ -147,11 +145,7 @@ function TicketCategoryManagement() {
|
|||||||
width: 150,
|
width: 150,
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Space size="small">
|
<Space size="small">
|
||||||
<Button
|
<Button type="link" icon={<EditOutlined />} onClick={() => showModal(record)}>
|
||||||
type="link"
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
onClick={() => showModal(record)}
|
|
||||||
>
|
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
@@ -166,22 +160,25 @@ function TicketCategoryManagement() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Space>
|
</Space>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 24 }}>
|
<div style={{ padding: 24 }}>
|
||||||
<Card title="故障分类管理" extra={
|
<Card
|
||||||
<Space>
|
title="故障分类管理"
|
||||||
<Button icon={<ReloadOutlined />} onClick={initCategories}>
|
extra={
|
||||||
初始化分类
|
<Space>
|
||||||
</Button>
|
<Button icon={<ReloadOutlined />} onClick={initCategories}>
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => showModal()}>
|
初始化分类
|
||||||
添加分类
|
</Button>
|
||||||
</Button>
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => showModal()}>
|
||||||
</Space>
|
添加分类
|
||||||
}>
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={categories}
|
dataSource={categories}
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Table, Button, Modal, Form, Input, Select, message, Card, Space, InputNumber, Switch } from 'antd';
|
import {
|
||||||
|
Table,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
message,
|
||||||
|
Card,
|
||||||
|
Space,
|
||||||
|
InputNumber,
|
||||||
|
Switch,
|
||||||
|
} from 'antd';
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
@@ -34,7 +46,7 @@ function TicketFieldManagement() {
|
|||||||
if (field) {
|
if (field) {
|
||||||
const fieldData = {
|
const fieldData = {
|
||||||
...field,
|
...field,
|
||||||
options: field.options ? JSON.stringify(field.options, null, 2) : ''
|
options: field.options ? JSON.stringify(field.options, null, 2) : '',
|
||||||
};
|
};
|
||||||
form.setFieldsValue(fieldData);
|
form.setFieldsValue(fieldData);
|
||||||
} else {
|
} else {
|
||||||
@@ -48,11 +60,11 @@ function TicketFieldManagement() {
|
|||||||
setEditingField(null);
|
setEditingField(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (values) => {
|
const handleSubmit = async values => {
|
||||||
try {
|
try {
|
||||||
const fieldData = {
|
const fieldData = {
|
||||||
...values,
|
...values,
|
||||||
options: values.options ? JSON.parse(values.options || '[]') : null
|
options: values.options ? JSON.parse(values.options || '[]') : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (editingField) {
|
if (editingField) {
|
||||||
@@ -72,7 +84,7 @@ function TicketFieldManagement() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (fieldId) => {
|
const handleDelete = async fieldId => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '确认删除',
|
title: '确认删除',
|
||||||
content: '确定要删除这个字段吗?',
|
content: '确定要删除这个字段吗?',
|
||||||
@@ -88,7 +100,7 @@ function TicketFieldManagement() {
|
|||||||
message.error('字段删除失败');
|
message.error('字段删除失败');
|
||||||
console.error('字段删除失败:', error);
|
console.error('字段删除失败:', error);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -107,7 +119,7 @@ function TicketFieldManagement() {
|
|||||||
title: '字段类型',
|
title: '字段类型',
|
||||||
dataIndex: 'fieldType',
|
dataIndex: 'fieldType',
|
||||||
key: 'fieldType',
|
key: 'fieldType',
|
||||||
render: (type) => {
|
render: type => {
|
||||||
const typeMap = {
|
const typeMap = {
|
||||||
string: '文本',
|
string: '文本',
|
||||||
number: '数字',
|
number: '数字',
|
||||||
@@ -116,26 +128,22 @@ function TicketFieldManagement() {
|
|||||||
date: '日期',
|
date: '日期',
|
||||||
datetime: '日期时间',
|
datetime: '日期时间',
|
||||||
textarea: '多行文本',
|
textarea: '多行文本',
|
||||||
device: '设备选择'
|
device: '设备选择',
|
||||||
};
|
};
|
||||||
return typeMap[type] || type;
|
return typeMap[type] || type;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '必填',
|
title: '必填',
|
||||||
dataIndex: 'required',
|
dataIndex: 'required',
|
||||||
key: 'required',
|
key: 'required',
|
||||||
render: (required) => (
|
render: required => <Switch checked={required} disabled />,
|
||||||
<Switch checked={required} disabled />
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '可见',
|
title: '可见',
|
||||||
dataIndex: 'visible',
|
dataIndex: 'visible',
|
||||||
key: 'visible',
|
key: 'visible',
|
||||||
render: (visible) => (
|
render: visible => <Switch checked={visible} disabled />,
|
||||||
<Switch checked={visible} disabled />
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '顺序',
|
title: '顺序',
|
||||||
@@ -147,10 +155,20 @@ function TicketFieldManagement() {
|
|||||||
key: 'action',
|
key: 'action',
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Space size="middle">
|
<Space size="middle">
|
||||||
<Button type="primary" icon={<EditOutlined />} onClick={() => showModal(record)} size="small">
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => showModal(record)}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
<Button danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.fieldId)} size="small">
|
<Button
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() => handleDelete(record.fieldId)}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -160,11 +178,14 @@ function TicketFieldManagement() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Card title="工单字段管理" extra={
|
<Card
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => showModal()}>
|
title="工单字段管理"
|
||||||
添加字段
|
extra={
|
||||||
</Button>
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => showModal()}>
|
||||||
}>
|
添加字段
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={fields}
|
dataSource={fields}
|
||||||
@@ -181,11 +202,7 @@ function TicketFieldManagement() {
|
|||||||
footer={null}
|
footer={null}
|
||||||
width={600}
|
width={600}
|
||||||
>
|
>
|
||||||
<Form
|
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||||
form={form}
|
|
||||||
layout="vertical"
|
|
||||||
onFinish={handleSubmit}
|
|
||||||
>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="fieldName"
|
name="fieldName"
|
||||||
label="字段名称"
|
label="字段名称"
|
||||||
@@ -219,17 +236,11 @@ function TicketFieldManagement() {
|
|||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item name="required" label="必填">
|
||||||
name="required"
|
|
||||||
label="必填"
|
|
||||||
>
|
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item name="visible" label="可见">
|
||||||
name="visible"
|
|
||||||
label="可见"
|
|
||||||
>
|
|
||||||
<Switch defaultChecked />
|
<Switch defaultChecked />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@@ -246,10 +257,7 @@ function TicketFieldManagement() {
|
|||||||
label="选项配置(仅下拉选择类型,JSON格式)"
|
label="选项配置(仅下拉选择类型,JSON格式)"
|
||||||
tooltip="格式示例:[{value: 'option1', label: '选项1'}]"
|
tooltip="格式示例:[{value: 'option1', label: '选项1'}]"
|
||||||
>
|
>
|
||||||
<Input.TextArea
|
<Input.TextArea rows={3} placeholder='[{"value": "option1", "label": "选项1"}]' />
|
||||||
rows={3}
|
|
||||||
placeholder='[{"value": "option1", "label": "选项1"}]'
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item style={{ textAlign: 'right' }}>
|
<Form.Item style={{ textAlign: 'right' }}>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,60 +1,65 @@
|
|||||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { Card, Row, Col, Statistic, Table, DatePicker, Select, Space, Tag, message } from 'antd';
|
import { Card, Row, Col, Statistic, Table, DatePicker, Select, Space, Tag, message } from 'antd';
|
||||||
import { BarChartOutlined, PieChartOutlined, RiseOutlined, FallOutlined, ClockCircleOutlined, CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
|
import {
|
||||||
|
BarChartOutlined,
|
||||||
|
PieChartOutlined,
|
||||||
|
RiseOutlined,
|
||||||
|
FallOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
const { RangePicker } = DatePicker;
|
const { RangePicker } = DatePicker;
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
const getStatusColor = (status) => {
|
const getStatusColor = status => {
|
||||||
const colors = {
|
const colors = {
|
||||||
pending: 'orange',
|
pending: 'orange',
|
||||||
assigned: 'blue',
|
assigned: 'blue',
|
||||||
in_progress: 'processing',
|
in_progress: 'processing',
|
||||||
completed: 'green',
|
completed: 'green',
|
||||||
closed: 'default'
|
closed: 'default',
|
||||||
};
|
};
|
||||||
return colors[status] || 'default';
|
return colors[status] || 'default';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusText = (status) => {
|
const getStatusText = status => {
|
||||||
const texts = {
|
const texts = {
|
||||||
pending: '待处理',
|
pending: '待处理',
|
||||||
assigned: '已分配',
|
assigned: '已分配',
|
||||||
in_progress: '处理中',
|
in_progress: '处理中',
|
||||||
completed: '已完成',
|
completed: '已完成',
|
||||||
closed: '已关闭'
|
closed: '已关闭',
|
||||||
};
|
};
|
||||||
return texts[status] || status;
|
return texts[status] || status;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPriorityColor = (priority) => {
|
const getPriorityColor = priority => {
|
||||||
const colors = {
|
const colors = {
|
||||||
low: 'green',
|
low: 'green',
|
||||||
medium: 'orange',
|
medium: 'orange',
|
||||||
high: 'red',
|
high: 'red',
|
||||||
urgent: 'magenta'
|
urgent: 'magenta',
|
||||||
};
|
};
|
||||||
return colors[priority] || 'default';
|
return colors[priority] || 'default';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPriorityText = (priority) => {
|
const getPriorityText = priority => {
|
||||||
const texts = {
|
const texts = {
|
||||||
low: '低',
|
low: '低',
|
||||||
medium: '中',
|
medium: '中',
|
||||||
high: '高',
|
high: '高',
|
||||||
urgent: '紧急'
|
urgent: '紧急',
|
||||||
};
|
};
|
||||||
return texts[priority] || priority;
|
return texts[priority] || priority;
|
||||||
};
|
};
|
||||||
|
|
||||||
function TicketStatistics() {
|
function TicketStatistics() {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [dateRange, setDateRange] = useState([
|
const [dateRange, setDateRange] = useState([dayjs().subtract(30, 'days'), dayjs()]);
|
||||||
dayjs().subtract(30, 'days'),
|
|
||||||
dayjs()
|
|
||||||
]);
|
|
||||||
const [statistics, setStatistics] = useState({
|
const [statistics, setStatistics] = useState({
|
||||||
total: 0,
|
total: 0,
|
||||||
pending: 0,
|
pending: 0,
|
||||||
@@ -66,7 +71,7 @@ function TicketStatistics() {
|
|||||||
byPriority: [],
|
byPriority: [],
|
||||||
byStatus: [],
|
byStatus: [],
|
||||||
byDevice: [],
|
byDevice: [],
|
||||||
trend: []
|
trend: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchStatistics = useCallback(async () => {
|
const fetchStatistics = useCallback(async () => {
|
||||||
@@ -74,7 +79,7 @@ function TicketStatistics() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const params = {
|
const params = {
|
||||||
startDate: dateRange[0].format('YYYY-MM-DD'),
|
startDate: dateRange[0].format('YYYY-MM-DD'),
|
||||||
endDate: dateRange[1].format('YYYY-MM-DD')
|
endDate: dateRange[1].format('YYYY-MM-DD'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await axios.get('/api/tickets/statistics', { params });
|
const response = await axios.get('/api/tickets/statistics', { params });
|
||||||
@@ -91,192 +96,198 @@ function TicketStatistics() {
|
|||||||
fetchStatistics();
|
fetchStatistics();
|
||||||
}, [fetchStatistics]);
|
}, [fetchStatistics]);
|
||||||
|
|
||||||
const handleDateChange = useCallback((dates) => {
|
const handleDateChange = useCallback(dates => {
|
||||||
if (dates) {
|
if (dates) {
|
||||||
setDateRange(dates);
|
setDateRange(dates);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getStatusColor = (status) => {
|
const getStatusColor = status => {
|
||||||
const colors = {
|
const colors = {
|
||||||
pending: 'orange',
|
pending: 'orange',
|
||||||
assigned: 'blue',
|
assigned: 'blue',
|
||||||
in_progress: 'processing',
|
in_progress: 'processing',
|
||||||
completed: 'green',
|
completed: 'green',
|
||||||
closed: 'default'
|
closed: 'default',
|
||||||
};
|
};
|
||||||
return colors[status] || 'default';
|
return colors[status] || 'default';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusText = (status) => {
|
const getStatusText = status => {
|
||||||
const texts = {
|
const texts = {
|
||||||
pending: '待处理',
|
pending: '待处理',
|
||||||
assigned: '已分配',
|
assigned: '已分配',
|
||||||
in_progress: '处理中',
|
in_progress: '处理中',
|
||||||
completed: '已完成',
|
completed: '已完成',
|
||||||
closed: '已关闭'
|
closed: '已关闭',
|
||||||
};
|
};
|
||||||
return texts[status] || status;
|
return texts[status] || status;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPriorityColor = (priority) => {
|
const getPriorityColor = priority => {
|
||||||
const colors = {
|
const colors = {
|
||||||
low: 'green',
|
low: 'green',
|
||||||
medium: 'orange',
|
medium: 'orange',
|
||||||
high: 'red',
|
high: 'red',
|
||||||
urgent: 'magenta'
|
urgent: 'magenta',
|
||||||
};
|
};
|
||||||
return colors[priority] || 'default';
|
return colors[priority] || 'default';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPriorityText = (priority) => {
|
const getPriorityText = priority => {
|
||||||
const texts = {
|
const texts = {
|
||||||
low: '低',
|
low: '低',
|
||||||
medium: '中',
|
medium: '中',
|
||||||
high: '高',
|
high: '高',
|
||||||
urgent: '紧急'
|
urgent: '紧急',
|
||||||
};
|
};
|
||||||
return texts[priority] || priority;
|
return texts[priority] || priority;
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusColumns = useMemo(() => [
|
const statusColumns = useMemo(
|
||||||
{
|
() => [
|
||||||
title: '状态',
|
{
|
||||||
dataIndex: 'status',
|
title: '状态',
|
||||||
key: 'status',
|
dataIndex: 'status',
|
||||||
width: 120,
|
key: 'status',
|
||||||
render: (status) => (
|
width: 120,
|
||||||
<Tag color={getStatusColor(status)}>
|
render: status => <Tag color={getStatusColor(status)}>{getStatusText(status)}</Tag>,
|
||||||
{getStatusText(status)}
|
},
|
||||||
</Tag>
|
{
|
||||||
)
|
title: '工单数量',
|
||||||
},
|
dataIndex: 'count',
|
||||||
{
|
key: 'count',
|
||||||
title: '工单数量',
|
width: 120,
|
||||||
dataIndex: 'count',
|
render: count => <Statistic value={count} valueStyle={{ fontSize: 16 }} />,
|
||||||
key: 'count',
|
},
|
||||||
width: 120,
|
{
|
||||||
render: (count) => <Statistic value={count} valueStyle={{ fontSize: 16 }} />
|
title: '占比',
|
||||||
},
|
dataIndex: 'percentage',
|
||||||
{
|
key: 'percentage',
|
||||||
title: '占比',
|
width: 120,
|
||||||
dataIndex: 'percentage',
|
render: pct => (
|
||||||
key: 'percentage',
|
<span style={{ color: pct > 30 ? '#ff4d4f' : '#52c41a' }}>
|
||||||
width: 120,
|
{pct !== undefined && pct !== null ? `${pct.toFixed(1)}%` : '-'}
|
||||||
render: (pct) => (
|
</span>
|
||||||
<span style={{ color: pct > 30 ? '#ff4d4f' : '#52c41a' }}>
|
),
|
||||||
{pct !== undefined && pct !== null ? `${pct.toFixed(1)}%` : '-'}
|
},
|
||||||
</span>
|
],
|
||||||
)
|
[]
|
||||||
}
|
);
|
||||||
], []);
|
|
||||||
|
|
||||||
const categoryColumns = useMemo(() => [
|
const categoryColumns = useMemo(
|
||||||
{
|
() => [
|
||||||
title: '故障分类',
|
{
|
||||||
dataIndex: 'category',
|
title: '故障分类',
|
||||||
key: 'category',
|
dataIndex: 'category',
|
||||||
width: 150
|
key: 'category',
|
||||||
},
|
width: 150,
|
||||||
{
|
},
|
||||||
title: '工单数量',
|
{
|
||||||
dataIndex: 'count',
|
title: '工单数量',
|
||||||
key: 'count',
|
dataIndex: 'count',
|
||||||
width: 120,
|
key: 'count',
|
||||||
render: (count) => <Statistic value={count} valueStyle={{ fontSize: 16 }} />
|
width: 120,
|
||||||
},
|
render: count => <Statistic value={count} valueStyle={{ fontSize: 16 }} />,
|
||||||
{
|
},
|
||||||
title: '占比',
|
{
|
||||||
dataIndex: 'percentage',
|
title: '占比',
|
||||||
key: 'percentage',
|
dataIndex: 'percentage',
|
||||||
width: 100,
|
key: 'percentage',
|
||||||
render: (pct) => `${pct !== undefined && pct !== null ? pct.toFixed(1) : 0}%`
|
width: 100,
|
||||||
},
|
render: pct => `${pct !== undefined && pct !== null ? pct.toFixed(1) : 0}%`,
|
||||||
{
|
},
|
||||||
title: '已完成',
|
{
|
||||||
dataIndex: 'completed',
|
title: '已完成',
|
||||||
key: 'completed',
|
dataIndex: 'completed',
|
||||||
width: 100,
|
key: 'completed',
|
||||||
render: (count) => <Tag color="green">{count}</Tag>
|
width: 100,
|
||||||
},
|
render: count => <Tag color="green">{count}</Tag>,
|
||||||
{
|
},
|
||||||
title: '平均处理时间(小时)',
|
{
|
||||||
dataIndex: 'avgTime',
|
title: '平均处理时间(小时)',
|
||||||
key: 'avgTime',
|
dataIndex: 'avgTime',
|
||||||
width: 150,
|
key: 'avgTime',
|
||||||
render: (time) => time !== undefined && time !== null ? time.toFixed(1) : '-'
|
width: 150,
|
||||||
}
|
render: time => (time !== undefined && time !== null ? time.toFixed(1) : '-'),
|
||||||
], []);
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const priorityColumns = useMemo(() => [
|
const priorityColumns = useMemo(
|
||||||
{
|
() => [
|
||||||
title: '优先级',
|
{
|
||||||
dataIndex: 'priority',
|
title: '优先级',
|
||||||
key: 'priority',
|
dataIndex: 'priority',
|
||||||
width: 100,
|
key: 'priority',
|
||||||
render: (priority) => (
|
width: 100,
|
||||||
<Tag color={getPriorityColor(priority)}>
|
render: priority => (
|
||||||
{getPriorityText(priority)}
|
<Tag color={getPriorityColor(priority)}>{getPriorityText(priority)}</Tag>
|
||||||
</Tag>
|
),
|
||||||
)
|
},
|
||||||
},
|
{
|
||||||
{
|
title: '工单数量',
|
||||||
title: '工单数量',
|
dataIndex: 'count',
|
||||||
dataIndex: 'count',
|
key: 'count',
|
||||||
key: 'count',
|
width: 120,
|
||||||
width: 120,
|
render: count => <Statistic value={count} valueStyle={{ fontSize: 16 }} />,
|
||||||
render: (count) => <Statistic value={count} valueStyle={{ fontSize: 16 }} />
|
},
|
||||||
},
|
{
|
||||||
{
|
title: '已完成',
|
||||||
title: '已完成',
|
dataIndex: 'completed',
|
||||||
dataIndex: 'completed',
|
key: 'completed',
|
||||||
key: 'completed',
|
width: 100,
|
||||||
width: 100,
|
render: count => <Tag color="green">{count}</Tag>,
|
||||||
render: (count) => <Tag color="green">{count}</Tag>
|
},
|
||||||
},
|
{
|
||||||
{
|
title: '平均处理时间(小时)',
|
||||||
title: '平均处理时间(小时)',
|
dataIndex: 'avgTime',
|
||||||
dataIndex: 'avgTime',
|
key: 'avgTime',
|
||||||
key: 'avgTime',
|
width: 150,
|
||||||
width: 150,
|
render: time => (time !== undefined && time !== null ? time.toFixed(1) : '-'),
|
||||||
render: (time) => time !== undefined && time !== null ? time.toFixed(1) : '-'
|
},
|
||||||
}
|
],
|
||||||
], []);
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const deviceColumns = useMemo(() => [
|
const deviceColumns = useMemo(
|
||||||
{
|
() => [
|
||||||
title: '设备名称',
|
{
|
||||||
dataIndex: 'deviceName',
|
title: '设备名称',
|
||||||
key: 'deviceName',
|
dataIndex: 'deviceName',
|
||||||
width: 180
|
key: 'deviceName',
|
||||||
},
|
width: 180,
|
||||||
{
|
},
|
||||||
title: '故障次数',
|
{
|
||||||
dataIndex: 'count',
|
title: '故障次数',
|
||||||
key: 'count',
|
dataIndex: 'count',
|
||||||
width: 100,
|
key: 'count',
|
||||||
render: (count) => <Tag color="red">{count}</Tag>
|
width: 100,
|
||||||
},
|
render: count => <Tag color="red">{count}</Tag>,
|
||||||
{
|
},
|
||||||
title: '最后故障时间',
|
{
|
||||||
dataIndex: 'lastFaultTime',
|
title: '最后故障时间',
|
||||||
key: 'lastFaultTime',
|
dataIndex: 'lastFaultTime',
|
||||||
width: 160,
|
key: 'lastFaultTime',
|
||||||
render: (text) => text ? dayjs(text).format('YYYY-MM-DD HH:mm') : '-'
|
width: 160,
|
||||||
},
|
render: text => (text ? dayjs(text).format('YYYY-MM-DD HH:mm') : '-'),
|
||||||
{
|
},
|
||||||
title: '设备类型',
|
{
|
||||||
dataIndex: 'deviceType',
|
title: '设备类型',
|
||||||
key: 'deviceType',
|
dataIndex: 'deviceType',
|
||||||
width: 100
|
key: 'deviceType',
|
||||||
}
|
width: 100,
|
||||||
], []);
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const simpleBarData = [
|
const simpleBarData = [
|
||||||
{ name: '待处理', value: statistics.pending },
|
{ name: '待处理', value: statistics.pending },
|
||||||
{ name: '处理中', value: statistics.inProgress },
|
{ name: '处理中', value: statistics.inProgress },
|
||||||
{ name: '已完成', value: statistics.completed },
|
{ name: '已完成', value: statistics.completed },
|
||||||
{ name: '已关闭', value: statistics.closed }
|
{ name: '已关闭', value: statistics.closed },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -285,11 +296,7 @@ function TicketStatistics() {
|
|||||||
title="工单统计报表"
|
title="工单统计报表"
|
||||||
extra={
|
extra={
|
||||||
<Space>
|
<Space>
|
||||||
<RangePicker
|
<RangePicker value={dateRange} onChange={handleDateChange} allowClear={false} />
|
||||||
value={dateRange}
|
|
||||||
onChange={handleDateChange}
|
|
||||||
allowClear={false}
|
|
||||||
/>
|
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -362,7 +369,11 @@ function TicketStatistics() {
|
|||||||
<Card bordered={false} style={{ background: '#fff1f0' }}>
|
<Card bordered={false} style={{ background: '#fff1f0' }}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="完成率"
|
title="完成率"
|
||||||
value={statistics.total > 0 ? ((statistics.completed / statistics.total) * 100).toFixed(1) : 0}
|
value={
|
||||||
|
statistics.total > 0
|
||||||
|
? ((statistics.completed / statistics.total) * 100).toFixed(1)
|
||||||
|
: 0
|
||||||
|
}
|
||||||
suffix="%"
|
suffix="%"
|
||||||
prefix={<PieChartOutlined style={{ color: '#ff4d4f' }} />}
|
prefix={<PieChartOutlined style={{ color: '#ff4d4f' }} />}
|
||||||
valueStyle={{ color: '#ff4d4f' }}
|
valueStyle={{ color: '#ff4d4f' }}
|
||||||
@@ -373,7 +384,11 @@ function TicketStatistics() {
|
|||||||
<Card bordered={false} style={{ background: '#f6ffed' }}>
|
<Card bordered={false} style={{ background: '#f6ffed' }}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="处理中占比"
|
title="处理中占比"
|
||||||
value={statistics.total > 0 ? ((statistics.inProgress / statistics.total) * 100).toFixed(1) : 0}
|
value={
|
||||||
|
statistics.total > 0
|
||||||
|
? ((statistics.inProgress / statistics.total) * 100).toFixed(1)
|
||||||
|
: 0
|
||||||
|
}
|
||||||
suffix="%"
|
suffix="%"
|
||||||
prefix={<RiseOutlined style={{ color: '#52c41a' }} />}
|
prefix={<RiseOutlined style={{ color: '#52c41a' }} />}
|
||||||
valueStyle={{ color: '#52c41a' }}
|
valueStyle={{ color: '#52c41a' }}
|
||||||
@@ -446,43 +461,43 @@ function TicketStatistics() {
|
|||||||
dataIndex: 'date',
|
dataIndex: 'date',
|
||||||
key: 'date',
|
key: 'date',
|
||||||
width: 120,
|
width: 120,
|
||||||
render: (text) => text ? dayjs(text).format('YYYY-MM-DD') : '-'
|
render: text => (text ? dayjs(text).format('YYYY-MM-DD') : '-'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '新建工单',
|
title: '新建工单',
|
||||||
dataIndex: 'created',
|
dataIndex: 'created',
|
||||||
key: 'created',
|
key: 'created',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (count) => <Tag color="blue">{count}</Tag>
|
render: count => <Tag color="blue">{count}</Tag>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '已完成',
|
title: '已完成',
|
||||||
dataIndex: 'completed',
|
dataIndex: 'completed',
|
||||||
key: 'completed',
|
key: 'completed',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (count) => <Tag color="green">{count}</Tag>
|
render: count => <Tag color="green">{count}</Tag>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '关闭工单',
|
title: '关闭工单',
|
||||||
dataIndex: 'closed',
|
dataIndex: 'closed',
|
||||||
key: 'closed',
|
key: 'closed',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (count) => <Tag color="default">{count}</Tag>
|
render: count => <Tag color="default">{count}</Tag>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '当日处理中',
|
title: '当日处理中',
|
||||||
dataIndex: 'inProgress',
|
dataIndex: 'inProgress',
|
||||||
key: 'inProgress',
|
key: 'inProgress',
|
||||||
width: 120,
|
width: 120,
|
||||||
render: (count) => <Tag color="processing">{count}</Tag>
|
render: count => <Tag color="processing">{count}</Tag>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '新增待处理',
|
title: '新增待处理',
|
||||||
dataIndex: 'pending',
|
dataIndex: 'pending',
|
||||||
key: 'pending',
|
key: 'pending',
|
||||||
width: 120,
|
width: 120,
|
||||||
render: (count) => <Tag color="orange">{count}</Tag>
|
render: count => <Tag color="orange">{count}</Tag>,
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
dataSource={statistics.trend}
|
dataSource={statistics.trend}
|
||||||
rowKey="date"
|
rowKey="date"
|
||||||
|
|||||||
@@ -1,6 +1,31 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||||
import { Card, Table, Button, Space, Modal, Form, Input, Select, message, Tag, Popconfirm, Avatar, Tooltip, Badge } from 'antd';
|
import {
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined, UserOutlined, ReloadOutlined, LockOutlined, CameraOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
Card,
|
||||||
|
Table,
|
||||||
|
Button,
|
||||||
|
Space,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
message,
|
||||||
|
Tag,
|
||||||
|
Popconfirm,
|
||||||
|
Avatar,
|
||||||
|
Tooltip,
|
||||||
|
Badge,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
LockOutlined,
|
||||||
|
CameraOutlined,
|
||||||
|
CheckOutlined,
|
||||||
|
CloseOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import { userAPI, roleAPI } from '../api';
|
import { userAPI, roleAPI } from '../api';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
@@ -32,7 +57,7 @@ const UserManagement = () => {
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
page: pagination.current,
|
page: pagination.current,
|
||||||
pageSize: pagination.pageSize
|
pageSize: pagination.pageSize,
|
||||||
};
|
};
|
||||||
if (activeTab !== 'all') {
|
if (activeTab !== 'all') {
|
||||||
params.status = activeTab;
|
params.status = activeTab;
|
||||||
@@ -69,7 +94,7 @@ const UserManagement = () => {
|
|||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleEdit = useCallback((user) => {
|
const handleEdit = useCallback(user => {
|
||||||
setEditingUser(user);
|
setEditingUser(user);
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
username: user.username,
|
username: user.username,
|
||||||
@@ -77,55 +102,58 @@ const UserManagement = () => {
|
|||||||
phone: user.phone,
|
phone: user.phone,
|
||||||
realName: user.realName,
|
realName: user.realName,
|
||||||
status: user.status,
|
status: user.status,
|
||||||
roleIds: user.roles?.map(r => r.roleId) || []
|
roleIds: user.roles?.map(r => r.roleId) || [],
|
||||||
});
|
});
|
||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleResetPassword = useCallback((user) => {
|
const handleResetPassword = useCallback(user => {
|
||||||
setPasswordUser(user);
|
setPasswordUser(user);
|
||||||
passwordForm.resetFields();
|
passwordForm.resetFields();
|
||||||
setPasswordModalVisible(true);
|
setPasswordModalVisible(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleAvatarClick = useCallback((user) => {
|
const handleAvatarClick = useCallback(user => {
|
||||||
setAvatarUser(user);
|
setAvatarUser(user);
|
||||||
setAvatarModalVisible(true);
|
setAvatarModalVisible(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleAvatarUpload = useCallback(async (e) => {
|
const handleAvatarUpload = useCallback(
|
||||||
const file = e.target.files[0];
|
async e => {
|
||||||
if (!file) return;
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
if (!file.type.match(/image\/(jpeg|png|gif|webp)/)) {
|
if (!file.type.match(/image\/(jpeg|png|gif|webp)/)) {
|
||||||
message.error('只支持 JPG、PNG、GIF 和 WebP 格式的图片');
|
message.error('只支持 JPG、PNG、GIF 和 WebP 格式的图片');
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (file.size > 5 * 1024 * 1024) {
|
|
||||||
message.error('图片大小不能超过 5MB');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setUploadLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await userAPI.uploadAvatar(avatarUser.userId, file);
|
|
||||||
if (response.success) {
|
|
||||||
message.success('头像上传成功');
|
|
||||||
fetchUsers();
|
|
||||||
setAvatarModalVisible(false);
|
|
||||||
} else {
|
|
||||||
message.error(response.message || '上传失败');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
message.error('上传失败');
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
} finally {
|
message.error('图片大小不能超过 5MB');
|
||||||
setUploadLoading(false);
|
return;
|
||||||
if (fileInputRef.current) {
|
|
||||||
fileInputRef.current.value = '';
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, [avatarUser, fetchUsers]);
|
setUploadLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await userAPI.uploadAvatar(avatarUser.userId, file);
|
||||||
|
if (response.success) {
|
||||||
|
message.success('头像上传成功');
|
||||||
|
fetchUsers();
|
||||||
|
setAvatarModalVisible(false);
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '上传失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('上传失败');
|
||||||
|
} finally {
|
||||||
|
setUploadLoading(false);
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[avatarUser, fetchUsers]
|
||||||
|
);
|
||||||
|
|
||||||
const handleAvatarDelete = useCallback(async () => {
|
const handleAvatarDelete = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -142,283 +170,302 @@ const UserManagement = () => {
|
|||||||
}
|
}
|
||||||
}, [avatarUser, fetchUsers]);
|
}, [avatarUser, fetchUsers]);
|
||||||
|
|
||||||
const handleDelete = useCallback(async (userId) => {
|
const handleDelete = useCallback(
|
||||||
try {
|
async userId => {
|
||||||
const response = await userAPI.delete(userId);
|
try {
|
||||||
if (response.success) {
|
const response = await userAPI.delete(userId);
|
||||||
message.success('删除成功');
|
if (response.success) {
|
||||||
fetchUsers();
|
message.success('删除成功');
|
||||||
} else {
|
fetchUsers();
|
||||||
message.error(response.message || '删除失败');
|
} else {
|
||||||
|
message.error(response.message || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('删除失败');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
},
|
||||||
message.error('删除失败');
|
[fetchUsers]
|
||||||
}
|
);
|
||||||
}, [fetchUsers]);
|
|
||||||
|
|
||||||
const handleLockUnlock = useCallback(async (record) => {
|
const handleLockUnlock = useCallback(
|
||||||
try {
|
async record => {
|
||||||
const newStatus = record.status === 'locked' ? 'active' : 'locked';
|
try {
|
||||||
const response = await userAPI.update(record.userId, {
|
const newStatus = record.status === 'locked' ? 'active' : 'locked';
|
||||||
status: newStatus
|
const response = await userAPI.update(record.userId, {
|
||||||
});
|
status: newStatus,
|
||||||
if (response.success) {
|
});
|
||||||
message.success(record.status === 'locked' ? '解锁成功' : '锁定成功');
|
if (response.success) {
|
||||||
fetchUsers();
|
message.success(record.status === 'locked' ? '解锁成功' : '锁定成功');
|
||||||
} else {
|
fetchUsers();
|
||||||
message.error(response.message || '操作失败');
|
} else {
|
||||||
|
message.error(response.message || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('操作失败');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
},
|
||||||
message.error('操作失败');
|
[fetchUsers]
|
||||||
}
|
);
|
||||||
}, [fetchUsers]);
|
|
||||||
|
|
||||||
const handleSubmit = useCallback(async (values) => {
|
const handleSubmit = useCallback(
|
||||||
try {
|
async values => {
|
||||||
let response;
|
try {
|
||||||
if (editingUser) {
|
let response;
|
||||||
response = await userAPI.update(editingUser.userId, values);
|
if (editingUser) {
|
||||||
} else {
|
response = await userAPI.update(editingUser.userId, values);
|
||||||
response = await userAPI.create(values);
|
} else {
|
||||||
|
response = await userAPI.create(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
message.success(editingUser ? '更新成功' : '创建成功');
|
||||||
|
setModalVisible(false);
|
||||||
|
fetchUsers();
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('操作失败');
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
[editingUser, fetchUsers]
|
||||||
|
);
|
||||||
|
|
||||||
if (response.success) {
|
const handleResetPasswordSubmit = useCallback(
|
||||||
message.success(editingUser ? '更新成功' : '创建成功');
|
async values => {
|
||||||
setModalVisible(false);
|
try {
|
||||||
fetchUsers();
|
const response = await userAPI.resetPassword(passwordUser.userId, values);
|
||||||
} else {
|
if (response.success) {
|
||||||
message.error(response.message || '操作失败');
|
message.success('密码重置成功');
|
||||||
|
setPasswordModalVisible(false);
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '重置失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('重置失败');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
},
|
||||||
message.error('操作失败');
|
[passwordUser]
|
||||||
}
|
);
|
||||||
}, [editingUser, fetchUsers]);
|
|
||||||
|
|
||||||
const handleResetPasswordSubmit = useCallback(async (values) => {
|
const getStatusColor = status => {
|
||||||
try {
|
|
||||||
const response = await userAPI.resetPassword(passwordUser.userId, values);
|
|
||||||
if (response.success) {
|
|
||||||
message.success('密码重置成功');
|
|
||||||
setPasswordModalVisible(false);
|
|
||||||
} else {
|
|
||||||
message.error(response.message || '重置失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
message.error('重置失败');
|
|
||||||
}
|
|
||||||
}, [passwordUser]);
|
|
||||||
|
|
||||||
const getStatusColor = (status) => {
|
|
||||||
const colors = {
|
const colors = {
|
||||||
active: 'green',
|
active: 'green',
|
||||||
inactive: 'red',
|
inactive: 'red',
|
||||||
locked: 'orange',
|
locked: 'orange',
|
||||||
pending: 'blue'
|
pending: 'blue',
|
||||||
};
|
};
|
||||||
return colors[status] || 'default';
|
return colors[status] || 'default';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusText = (status) => {
|
const getStatusText = status => {
|
||||||
const texts = {
|
const texts = {
|
||||||
active: '正常',
|
active: '正常',
|
||||||
inactive: '禁用',
|
inactive: '禁用',
|
||||||
locked: '锁定',
|
locked: '锁定',
|
||||||
pending: '待审核'
|
pending: '待审核',
|
||||||
};
|
};
|
||||||
return texts[status] || status;
|
return texts[status] || status;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApprove = useCallback(async (userId) => {
|
const handleApprove = useCallback(
|
||||||
try {
|
async userId => {
|
||||||
const response = await userAPI.approve(userId);
|
try {
|
||||||
if (response.success) {
|
const response = await userAPI.approve(userId);
|
||||||
message.success('审核通过');
|
if (response.success) {
|
||||||
fetchUsers();
|
message.success('审核通过');
|
||||||
} else {
|
fetchUsers();
|
||||||
message.error(response.message || '审核失败');
|
} else {
|
||||||
|
message.error(response.message || '审核失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('审核失败');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
},
|
||||||
message.error('审核失败');
|
[fetchUsers]
|
||||||
}
|
);
|
||||||
}, [fetchUsers]);
|
|
||||||
|
|
||||||
const handleReject = useCallback(async (userId) => {
|
const handleReject = useCallback(
|
||||||
try {
|
async userId => {
|
||||||
const response = await userAPI.reject(userId);
|
try {
|
||||||
if (response.success) {
|
const response = await userAPI.reject(userId);
|
||||||
message.success('已拒绝该用户的注册申请');
|
if (response.success) {
|
||||||
fetchUsers();
|
message.success('已拒绝该用户的注册申请');
|
||||||
} else {
|
fetchUsers();
|
||||||
message.error(response.message || '操作失败');
|
} else {
|
||||||
|
message.error(response.message || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('操作失败');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
},
|
||||||
message.error('操作失败');
|
[fetchUsers]
|
||||||
}
|
);
|
||||||
}, [fetchUsers]);
|
|
||||||
|
|
||||||
const getAvatarUrl = (user) => {
|
const getAvatarUrl = user => {
|
||||||
if (!user?.avatar) return null;
|
if (!user?.avatar) return null;
|
||||||
return user.avatar;
|
return user.avatar;
|
||||||
};
|
};
|
||||||
|
|
||||||
const tableColumns = useMemo(() => [
|
const tableColumns = useMemo(
|
||||||
{
|
() => [
|
||||||
title: '头像',
|
{
|
||||||
key: 'avatar',
|
title: '头像',
|
||||||
width: 80,
|
key: 'avatar',
|
||||||
render: (_, record) => (
|
width: 80,
|
||||||
<Badge dot={!!record.avatar} color="green" offset={[-5, 35]}>
|
render: (_, record) => (
|
||||||
<Avatar
|
<Badge dot={!!record.avatar} color="green" offset={[-5, 35]}>
|
||||||
size={48}
|
<Avatar
|
||||||
icon={!record.avatar && <UserOutlined />}
|
size={48}
|
||||||
src={getAvatarUrl(record)}
|
icon={!record.avatar && <UserOutlined />}
|
||||||
style={{
|
src={getAvatarUrl(record)}
|
||||||
backgroundColor: record.avatar ? 'transparent' : '#1890ff',
|
style={{
|
||||||
cursor: 'pointer'
|
backgroundColor: record.avatar ? 'transparent' : '#1890ff',
|
||||||
}}
|
cursor: 'pointer',
|
||||||
onClick={() => handleAvatarClick(record)}
|
}}
|
||||||
/>
|
onClick={() => handleAvatarClick(record)}
|
||||||
</Badge>
|
/>
|
||||||
)
|
</Badge>
|
||||||
},
|
),
|
||||||
{
|
},
|
||||||
title: '用户名',
|
{
|
||||||
key: 'username',
|
title: '用户名',
|
||||||
width: 150,
|
key: 'username',
|
||||||
render: (_, record) => (
|
width: 150,
|
||||||
<div>
|
render: (_, record) => (
|
||||||
<div style={{ fontWeight: 500 }}>{record.realName || record.username}</div>
|
<div>
|
||||||
<div style={{ fontSize: '12px', color: '#999' }}>@{record.username}</div>
|
<div style={{ fontWeight: 500 }}>{record.realName || record.username}</div>
|
||||||
</div>
|
<div style={{ fontSize: '12px', color: '#999' }}>@{record.username}</div>
|
||||||
)
|
</div>
|
||||||
},
|
),
|
||||||
{
|
},
|
||||||
title: '邮箱',
|
{
|
||||||
dataIndex: 'email',
|
title: '邮箱',
|
||||||
key: 'email',
|
dataIndex: 'email',
|
||||||
width: 200,
|
key: 'email',
|
||||||
render: (email) => email || '-'
|
width: 200,
|
||||||
},
|
render: email => email || '-',
|
||||||
{
|
},
|
||||||
title: '手机号',
|
{
|
||||||
dataIndex: 'phone',
|
title: '手机号',
|
||||||
key: 'phone',
|
dataIndex: 'phone',
|
||||||
width: 130,
|
key: 'phone',
|
||||||
render: (phone) => phone || '-'
|
width: 130,
|
||||||
},
|
render: phone => phone || '-',
|
||||||
{
|
},
|
||||||
title: '角色',
|
{
|
||||||
key: 'roles',
|
title: '角色',
|
||||||
render: (_, record) => (
|
key: 'roles',
|
||||||
<Space wrap>
|
render: (_, record) => (
|
||||||
{record.roles?.map(role => (
|
<Space wrap>
|
||||||
<Tag key={role.roleId} color={role.roleCode === 'admin' ? 'blue' : 'green'}>
|
{record.roles?.map(role => (
|
||||||
{role.roleName}
|
<Tag key={role.roleId} color={role.roleCode === 'admin' ? 'blue' : 'green'}>
|
||||||
</Tag>
|
{role.roleName}
|
||||||
)) || '-'}
|
</Tag>
|
||||||
</Space>
|
)) || '-'}
|
||||||
)
|
</Space>
|
||||||
},
|
),
|
||||||
{
|
},
|
||||||
title: '状态',
|
{
|
||||||
dataIndex: 'status',
|
title: '状态',
|
||||||
key: 'status',
|
dataIndex: 'status',
|
||||||
render: (status) => (
|
key: 'status',
|
||||||
<Tag color={getStatusColor(status)}>{getStatusText(status)}</Tag>
|
render: status => <Tag color={getStatusColor(status)}>{getStatusText(status)}</Tag>,
|
||||||
)
|
},
|
||||||
},
|
{
|
||||||
{
|
title: '最后登录',
|
||||||
title: '最后登录',
|
key: 'lastLogin',
|
||||||
key: 'lastLogin',
|
render: (_, record) => (
|
||||||
render: (_, record) => (
|
<div style={{ fontSize: '12px' }}>
|
||||||
<div style={{ fontSize: '12px' }}>
|
<div>
|
||||||
<div>{record.lastLoginTime ? new Date(record.lastLoginTime).toLocaleString() : '从未登录'}</div>
|
{record.lastLoginTime ? new Date(record.lastLoginTime).toLocaleString() : '从未登录'}
|
||||||
<div style={{ color: '#999' }}>{record.lastLoginIp || '-'}</div>
|
</div>
|
||||||
</div>
|
<div style={{ color: '#999' }}>{record.lastLoginIp || '-'}</div>
|
||||||
)
|
</div>
|
||||||
},
|
),
|
||||||
{
|
},
|
||||||
title: '操作',
|
{
|
||||||
key: 'action',
|
title: '操作',
|
||||||
render: (_, record) => (
|
key: 'action',
|
||||||
<Space size="small">
|
render: (_, record) => (
|
||||||
{record.status === 'pending' ? (
|
<Space size="small">
|
||||||
<>
|
{record.status === 'pending' ? (
|
||||||
<Popconfirm
|
<>
|
||||||
title="确定要通过该用户的注册申请吗?"
|
<Popconfirm
|
||||||
onConfirm={() => handleApprove(record.userId)}
|
title="确定要通过该用户的注册申请吗?"
|
||||||
okText="通过"
|
onConfirm={() => handleApprove(record.userId)}
|
||||||
cancelText="取消"
|
okText="通过"
|
||||||
>
|
cancelText="取消"
|
||||||
<Tooltip title="通过">
|
>
|
||||||
|
<Tooltip title="通过">
|
||||||
|
<Button type="text" icon={<CheckOutlined />} style={{ color: '#52c41a' }} />
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>
|
||||||
|
<Popconfirm
|
||||||
|
title="确定要拒绝该用户的注册申请吗?"
|
||||||
|
onConfirm={() => handleReject(record.userId)}
|
||||||
|
okText="拒绝"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Tooltip title="拒绝">
|
||||||
|
<Button type="text" icon={<CloseOutlined />} style={{ color: '#ff4d4f' }} />
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Tooltip title="编辑">
|
||||||
|
<Button type="text" icon={<EditOutlined />} onClick={() => handleEdit(record)} />
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="重置密码">
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<CheckOutlined />}
|
icon={<LockOutlined />}
|
||||||
style={{ color: '#52c41a' }}
|
onClick={() => handleResetPassword(record)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Popconfirm>
|
<Popconfirm
|
||||||
<Popconfirm
|
title={
|
||||||
title="确定要拒绝该用户的注册申请吗?"
|
record.status === 'locked' ? '确定要解锁此用户吗?' : '确定要锁定此用户吗?'
|
||||||
onConfirm={() => handleReject(record.userId)}
|
}
|
||||||
okText="拒绝"
|
onConfirm={() => handleLockUnlock(record)}
|
||||||
cancelText="取消"
|
okText="确定"
|
||||||
>
|
cancelText="取消"
|
||||||
<Tooltip title="拒绝">
|
>
|
||||||
<Button
|
<Tooltip title={record.status === 'locked' ? '解锁' : '锁定'}>
|
||||||
type="text"
|
<Button
|
||||||
icon={<CloseOutlined />}
|
type="text"
|
||||||
style={{ color: '#ff4d4f' }}
|
icon={record.status === 'locked' ? <ReloadOutlined /> : <LockOutlined />}
|
||||||
/>
|
style={{ color: record.status === 'locked' ? '#52c41a' : '#faad14' }}
|
||||||
</Tooltip>
|
/>
|
||||||
</Popconfirm>
|
</Tooltip>
|
||||||
</>
|
</Popconfirm>
|
||||||
) : (
|
<Popconfirm
|
||||||
<>
|
title="确定要删除此用户吗?"
|
||||||
<Tooltip title="编辑">
|
onConfirm={() => handleDelete(record.userId)}
|
||||||
<Button
|
okText="确定"
|
||||||
type="text"
|
cancelText="取消"
|
||||||
icon={<EditOutlined />}
|
>
|
||||||
onClick={() => handleEdit(record)}
|
<Tooltip title="删除">
|
||||||
/>
|
<Button type="text" danger icon={<DeleteOutlined />} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="重置密码">
|
</Popconfirm>
|
||||||
<Button
|
</>
|
||||||
type="text"
|
)}
|
||||||
icon={<LockOutlined />}
|
</Space>
|
||||||
onClick={() => handleResetPassword(record)}
|
),
|
||||||
/>
|
},
|
||||||
</Tooltip>
|
],
|
||||||
<Popconfirm
|
[
|
||||||
title={record.status === 'locked' ? '确定要解锁此用户吗?' : '确定要锁定此用户吗?'}
|
handleAvatarClick,
|
||||||
onConfirm={() => handleLockUnlock(record)}
|
handleEdit,
|
||||||
okText="确定"
|
handleResetPassword,
|
||||||
cancelText="取消"
|
handleDelete,
|
||||||
>
|
handleLockUnlock,
|
||||||
<Tooltip title={record.status === 'locked' ? '解锁' : '锁定'}>
|
handleApprove,
|
||||||
<Button
|
handleReject,
|
||||||
type="text"
|
]
|
||||||
icon={record.status === 'locked' ? <ReloadOutlined /> : <LockOutlined />}
|
);
|
||||||
style={{ color: record.status === 'locked' ? '#52c41a' : '#faad14' }}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Popconfirm>
|
|
||||||
<Popconfirm
|
|
||||||
title="确定要删除此用户吗?"
|
|
||||||
onConfirm={() => handleDelete(record.userId)}
|
|
||||||
okText="确定"
|
|
||||||
cancelText="取消"
|
|
||||||
>
|
|
||||||
<Tooltip title="删除">
|
|
||||||
<Button type="text" danger icon={<DeleteOutlined />} />
|
|
||||||
</Tooltip>
|
|
||||||
</Popconfirm>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
], [handleAvatarClick, handleEdit, handleResetPassword, handleDelete, handleLockUnlock, handleApprove, handleReject]);
|
|
||||||
|
|
||||||
const pageHeaderStyle = {
|
const pageHeaderStyle = {
|
||||||
marginBottom: '24px',
|
marginBottom: '24px',
|
||||||
@@ -426,7 +473,7 @@ const UserManagement = () => {
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
gap: '16px'
|
gap: '16px',
|
||||||
};
|
};
|
||||||
|
|
||||||
const titleStyle = {
|
const titleStyle = {
|
||||||
@@ -436,20 +483,20 @@ const UserManagement = () => {
|
|||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
WebkitBackgroundClip: 'text',
|
WebkitBackgroundClip: 'text',
|
||||||
WebkitTextFillColor: 'transparent',
|
WebkitTextFillColor: 'transparent',
|
||||||
backgroundClip: 'text'
|
backgroundClip: 'text',
|
||||||
};
|
};
|
||||||
|
|
||||||
const cardStyle = {
|
const cardStyle = {
|
||||||
borderRadius: '16px',
|
borderRadius: '16px',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)',
|
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)',
|
||||||
overflow: 'hidden'
|
overflow: 'hidden',
|
||||||
};
|
};
|
||||||
|
|
||||||
const cardHeadStyle = {
|
const cardHeadStyle = {
|
||||||
borderBottom: '1px solid #f0f0f0',
|
borderBottom: '1px solid #f0f0f0',
|
||||||
padding: '16px 24px',
|
padding: '16px 24px',
|
||||||
background: 'linear-gradient(135deg, #f8f9ff 0%, #ffffff 100%)'
|
background: 'linear-gradient(135deg, #f8f9ff 0%, #ffffff 100%)',
|
||||||
};
|
};
|
||||||
|
|
||||||
const primaryButtonStyle = {
|
const primaryButtonStyle = {
|
||||||
@@ -459,21 +506,21 @@ const UserManagement = () => {
|
|||||||
border: 'none',
|
border: 'none',
|
||||||
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.35)',
|
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.35)',
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
transition: 'all 0.3s ease'
|
transition: 'all 0.3s ease',
|
||||||
};
|
};
|
||||||
|
|
||||||
const secondaryButtonStyle = {
|
const secondaryButtonStyle = {
|
||||||
height: '40px',
|
height: '40px',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
border: '1px solid #e8e8e8',
|
border: '1px solid #e8e8e8',
|
||||||
transition: 'all 0.3s ease'
|
transition: 'all 0.3s ease',
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionButtonStyle = {
|
const actionButtonStyle = {
|
||||||
height: '32px',
|
height: '32px',
|
||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
border: '1px solid #e8e8e8',
|
border: '1px solid #e8e8e8',
|
||||||
transition: 'all 0.3s ease'
|
transition: 'all 0.3s ease',
|
||||||
};
|
};
|
||||||
|
|
||||||
const modalHeaderStyle = {
|
const modalHeaderStyle = {
|
||||||
@@ -481,25 +528,25 @@ const UserManagement = () => {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
fontSize: '18px',
|
fontSize: '18px',
|
||||||
fontWeight: '600'
|
fontWeight: '600',
|
||||||
};
|
};
|
||||||
|
|
||||||
const modalHeaderAccent = {
|
const modalHeaderAccent = {
|
||||||
width: '4px',
|
width: '4px',
|
||||||
height: '20px',
|
height: '20px',
|
||||||
background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)',
|
background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)',
|
||||||
borderRadius: '2px'
|
borderRadius: '2px',
|
||||||
};
|
};
|
||||||
|
|
||||||
const avatarModalStyle = {
|
const avatarModalStyle = {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
padding: '20px 0'
|
padding: '20px 0',
|
||||||
};
|
};
|
||||||
|
|
||||||
const avatarWrapperStyle = {
|
const avatarWrapperStyle = {
|
||||||
marginBottom: '24px',
|
marginBottom: '24px',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
display: 'inline-block'
|
display: 'inline-block',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -507,11 +554,7 @@ const UserManagement = () => {
|
|||||||
<div style={pageHeaderStyle}>
|
<div style={pageHeaderStyle}>
|
||||||
<h1 style={titleStyle}>用户管理</h1>
|
<h1 style={titleStyle}>用户管理</h1>
|
||||||
<Space size="middle">
|
<Space size="middle">
|
||||||
<Button
|
<Button icon={<ReloadOutlined />} onClick={fetchUsers} style={secondaryButtonStyle}>
|
||||||
icon={<ReloadOutlined />}
|
|
||||||
onClick={fetchUsers}
|
|
||||||
style={secondaryButtonStyle}
|
|
||||||
>
|
|
||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -533,10 +576,10 @@ const UserManagement = () => {
|
|||||||
{ key: 'pending', tab: '待审核' },
|
{ key: 'pending', tab: '待审核' },
|
||||||
{ key: 'active', tab: '正常' },
|
{ key: 'active', tab: '正常' },
|
||||||
{ key: 'locked', tab: '锁定' },
|
{ key: 'locked', tab: '锁定' },
|
||||||
{ key: 'inactive', tab: '禁用' }
|
{ key: 'inactive', tab: '禁用' },
|
||||||
]}
|
]}
|
||||||
activeTabKey={activeTab}
|
activeTabKey={activeTab}
|
||||||
onTabChange={(key) => {
|
onTabChange={key => {
|
||||||
setActiveTab(key);
|
setActiveTab(key);
|
||||||
setPagination(prev => ({ ...prev, current: 1 }));
|
setPagination(prev => ({ ...prev, current: 1 }));
|
||||||
}}
|
}}
|
||||||
@@ -550,9 +593,9 @@ const UserManagement = () => {
|
|||||||
...pagination,
|
...pagination,
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
showQuickJumper: true,
|
showQuickJumper: true,
|
||||||
showTotal: (total) => `共 ${total} 条记录`
|
showTotal: total => `共 ${total} 条记录`,
|
||||||
}}
|
}}
|
||||||
onChange={(newPagination) => {
|
onChange={newPagination => {
|
||||||
setPagination(prev => ({ ...prev, ...newPagination }));
|
setPagination(prev => ({ ...prev, ...newPagination }));
|
||||||
}}
|
}}
|
||||||
rowClassName={() => 'table-row'}
|
rowClassName={() => 'table-row'}
|
||||||
@@ -573,22 +616,17 @@ const UserManagement = () => {
|
|||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
styles={{
|
styles={{
|
||||||
body: { padding: '24px' },
|
body: { padding: '24px' },
|
||||||
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }
|
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' },
|
||||||
}}
|
}}
|
||||||
style={{ borderRadius: '16px', overflow: 'hidden' }}
|
style={{ borderRadius: '16px', overflow: 'hidden' }}
|
||||||
>
|
>
|
||||||
<Form
|
<Form form={form} layout="vertical" onFinish={handleSubmit} style={{ marginTop: '20px' }}>
|
||||||
form={form}
|
|
||||||
layout="vertical"
|
|
||||||
onFinish={handleSubmit}
|
|
||||||
style={{ marginTop: '20px' }}
|
|
||||||
>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="username"
|
name="username"
|
||||||
label="用户名"
|
label="用户名"
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: '请输入用户名' },
|
{ required: true, message: '请输入用户名' },
|
||||||
{ min: 3, max: 20, message: '用户名长度必须在3-20个字符之间' }
|
{ min: 3, max: 20, message: '用户名长度必须在3-20个字符之间' },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder="请输入用户名" style={{ borderRadius: '8px' }} />
|
<Input placeholder="请输入用户名" style={{ borderRadius: '8px' }} />
|
||||||
@@ -607,7 +645,7 @@ const UserManagement = () => {
|
|||||||
label="邮箱"
|
label="邮箱"
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: '请输入邮箱' },
|
{ required: true, message: '请输入邮箱' },
|
||||||
{ type: 'email', message: '请输入有效的邮箱地址' }
|
{ type: 'email', message: '请输入有效的邮箱地址' },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder="请输入邮箱" style={{ borderRadius: '8px' }} />
|
<Input placeholder="请输入邮箱" style={{ borderRadius: '8px' }} />
|
||||||
@@ -637,7 +675,7 @@ const UserManagement = () => {
|
|||||||
label="初始密码"
|
label="初始密码"
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: '请输入初始密码' },
|
{ required: true, message: '请输入初始密码' },
|
||||||
{ min: 6, message: '密码长度不能少于6个字符' }
|
{ min: 6, message: '密码长度不能少于6个字符' },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input.Password placeholder="请输入初始密码" style={{ borderRadius: '8px' }} />
|
<Input.Password placeholder="请输入初始密码" style={{ borderRadius: '8px' }} />
|
||||||
@@ -656,9 +694,7 @@ const UserManagement = () => {
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
name="newPassword"
|
name="newPassword"
|
||||||
label="新密码"
|
label="新密码"
|
||||||
rules={[
|
rules={[{ min: 6, message: '密码长度不能少于6个字符' }]}
|
||||||
{ min: 6, message: '密码长度不能少于6个字符' }
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<Input.Password placeholder="留空则不修改密码" style={{ borderRadius: '8px' }} />
|
<Input.Password placeholder="留空则不修改密码" style={{ borderRadius: '8px' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -680,7 +716,7 @@ const UserManagement = () => {
|
|||||||
...primaryButtonStyle,
|
...primaryButtonStyle,
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
paddingLeft: '24px',
|
paddingLeft: '24px',
|
||||||
paddingRight: '24px'
|
paddingRight: '24px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{editingUser ? '更新' : '创建'}
|
{editingUser ? '更新' : '创建'}
|
||||||
@@ -704,7 +740,7 @@ const UserManagement = () => {
|
|||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
styles={{
|
styles={{
|
||||||
body: { padding: '24px' },
|
body: { padding: '24px' },
|
||||||
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }
|
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' },
|
||||||
}}
|
}}
|
||||||
style={{ borderRadius: '16px', overflow: 'hidden' }}
|
style={{ borderRadius: '16px', overflow: 'hidden' }}
|
||||||
>
|
>
|
||||||
@@ -719,7 +755,7 @@ const UserManagement = () => {
|
|||||||
label="新密码"
|
label="新密码"
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: '请输入新密码' },
|
{ required: true, message: '请输入新密码' },
|
||||||
{ min: 6, message: '密码长度不能少于6个字符' }
|
{ min: 6, message: '密码长度不能少于6个字符' },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input.Password placeholder="请输入新密码" style={{ borderRadius: '8px' }} />
|
<Input.Password placeholder="请输入新密码" style={{ borderRadius: '8px' }} />
|
||||||
@@ -741,7 +777,7 @@ const UserManagement = () => {
|
|||||||
...primaryButtonStyle,
|
...primaryButtonStyle,
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
paddingLeft: '24px',
|
paddingLeft: '24px',
|
||||||
paddingRight: '24px'
|
paddingRight: '24px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
重置
|
重置
|
||||||
@@ -765,7 +801,7 @@ const UserManagement = () => {
|
|||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
styles={{
|
styles={{
|
||||||
body: { padding: '24px' },
|
body: { padding: '24px' },
|
||||||
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }
|
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' },
|
||||||
}}
|
}}
|
||||||
style={{ borderRadius: '16px', overflow: 'hidden' }}
|
style={{ borderRadius: '16px', overflow: 'hidden' }}
|
||||||
>
|
>
|
||||||
@@ -779,7 +815,7 @@ const UserManagement = () => {
|
|||||||
style={{
|
style={{
|
||||||
backgroundColor: avatarUser?.avatar ? 'transparent' : '#1890ff',
|
backgroundColor: avatarUser?.avatar ? 'transparent' : '#1890ff',
|
||||||
border: '1px solid #f0f0f0',
|
border: '1px solid #f0f0f0',
|
||||||
cursor: 'pointer'
|
cursor: 'pointer',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -802,7 +838,7 @@ const UserManagement = () => {
|
|||||||
block
|
block
|
||||||
style={{
|
style={{
|
||||||
...primaryButtonStyle,
|
...primaryButtonStyle,
|
||||||
height: '44px'
|
height: '44px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{avatarUser?.avatar ? '更换头像' : '上传头像'}
|
{avatarUser?.avatar ? '更换头像' : '上传头像'}
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ const { colors, shadows, borderRadius, transitions, spacing } = designTokens;
|
|||||||
export const pageContainerStyle = {
|
export const pageContainerStyle = {
|
||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
background: colors.background.secondary,
|
background: colors.background.secondary,
|
||||||
padding: spacing.lg
|
padding: spacing.lg,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 头部样式
|
// 头部样式
|
||||||
export const headerStyle = {
|
export const headerStyle = {
|
||||||
marginBottom: spacing.lg
|
marginBottom: spacing.lg,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 标题行样式
|
// 标题行样式
|
||||||
@@ -26,14 +26,14 @@ export const titleRowStyle = {
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
marginBottom: spacing.lg,
|
marginBottom: spacing.lg,
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
gap: spacing.md
|
gap: spacing.md,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 标题区域样式
|
// 标题区域样式
|
||||||
export const titleSectionStyle = {
|
export const titleSectionStyle = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: spacing.md
|
gap: spacing.md,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 标题图标样式
|
// 标题图标样式
|
||||||
@@ -45,14 +45,14 @@ export const titleIconStyle = {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
boxShadow: shadows.medium
|
boxShadow: shadows.medium,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 标题文本样式
|
// 标题文本样式
|
||||||
export const titleTextStyle = {
|
export const titleTextStyle = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: '2px'
|
gap: '2px',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 页面标题样式
|
// 页面标题样式
|
||||||
@@ -61,14 +61,14 @@ export const pageTitleStyle = {
|
|||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
margin: 0,
|
margin: 0,
|
||||||
color: colors.text.primary,
|
color: colors.text.primary,
|
||||||
lineHeight: 1.2
|
lineHeight: 1.2,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 页面副标题样式
|
// 页面副标题样式
|
||||||
export const pageSubtitleStyle = {
|
export const pageSubtitleStyle = {
|
||||||
fontSize: '13px',
|
fontSize: '13px',
|
||||||
color: colors.text.secondary,
|
color: colors.text.secondary,
|
||||||
margin: 0
|
margin: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 操作按钮基础样式
|
// 操作按钮基础样式
|
||||||
@@ -79,7 +79,7 @@ export const actionButtonStyle = {
|
|||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '6px'
|
gap: '6px',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 主要操作按钮样式
|
// 主要操作按钮样式
|
||||||
@@ -88,7 +88,7 @@ export const primaryActionStyle = {
|
|||||||
background: colors.primary.gradient,
|
background: colors.primary.gradient,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
color: '#ffffff !important',
|
color: '#ffffff !important',
|
||||||
boxShadow: shadows.small
|
boxShadow: shadows.small,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 次要操作按钮样式
|
// 次要操作按钮样式
|
||||||
@@ -96,7 +96,7 @@ export const secondaryActionStyle = {
|
|||||||
...actionButtonStyle,
|
...actionButtonStyle,
|
||||||
background: colors.background.primary,
|
background: colors.background.primary,
|
||||||
border: `1px solid ${colors.border.light}`,
|
border: `1px solid ${colors.border.light}`,
|
||||||
color: colors.text.primary
|
color: colors.text.primary,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 危险操作按钮样式
|
// 危险操作按钮样式
|
||||||
@@ -104,7 +104,7 @@ export const dangerActionStyle = {
|
|||||||
...actionButtonStyle,
|
...actionButtonStyle,
|
||||||
background: colors.error.main,
|
background: colors.error.main,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
color: '#ffffff'
|
color: '#ffffff',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 主要按钮样式(大)
|
// 主要按钮样式(大)
|
||||||
@@ -118,7 +118,7 @@ export const primaryButtonStyle = {
|
|||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center'
|
justifyContent: 'center',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 统计卡片行样式
|
// 统计卡片行样式
|
||||||
@@ -126,7 +126,7 @@ export const statsRowStyle = {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: spacing.md,
|
gap: spacing.md,
|
||||||
marginBottom: spacing.lg,
|
marginBottom: spacing.lg,
|
||||||
flexWrap: 'wrap'
|
flexWrap: 'wrap',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 统计卡片基础样式
|
// 统计卡片基础样式
|
||||||
@@ -139,7 +139,7 @@ export const statCardStyle = {
|
|||||||
borderRadius: borderRadius.medium,
|
borderRadius: borderRadius.medium,
|
||||||
border: `1px solid ${colors.border.light}`,
|
border: `1px solid ${colors.border.light}`,
|
||||||
boxShadow: shadows.small,
|
boxShadow: shadows.small,
|
||||||
transition: `all ${transitions.fast}`
|
transition: `all ${transitions.fast}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 统计数值样式
|
// 统计数值样式
|
||||||
@@ -147,35 +147,35 @@ export const statValueStyle = {
|
|||||||
fontSize: '24px',
|
fontSize: '24px',
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: colors.text.primary,
|
color: colors.text.primary,
|
||||||
lineHeight: 1.2
|
lineHeight: 1.2,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 统计标签样式
|
// 统计标签样式
|
||||||
export const statLabelStyle = {
|
export const statLabelStyle = {
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
color: colors.text.secondary,
|
color: colors.text.secondary,
|
||||||
marginTop: '4px'
|
marginTop: '4px',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 运行中状态统计卡片样式
|
// 运行中状态统计卡片样式
|
||||||
export const statCardRunningStyle = {
|
export const statCardRunningStyle = {
|
||||||
...statCardStyle,
|
...statCardStyle,
|
||||||
borderLeft: `3px solid ${colors.success.main}`,
|
borderLeft: `3px solid ${colors.success.main}`,
|
||||||
background: `${colors.success.main}08`
|
background: `${colors.success.main}08`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 维护中状态统计卡片样式
|
// 维护中状态统计卡片样式
|
||||||
export const statCardMaintenanceStyle = {
|
export const statCardMaintenanceStyle = {
|
||||||
...statCardStyle,
|
...statCardStyle,
|
||||||
borderLeft: `3px solid ${colors.warning.main}`,
|
borderLeft: `3px solid ${colors.warning.main}`,
|
||||||
background: `${colors.warning.main}08`
|
background: `${colors.warning.main}08`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 故障状态统计卡片样式
|
// 故障状态统计卡片样式
|
||||||
export const statCardFaultStyle = {
|
export const statCardFaultStyle = {
|
||||||
...statCardStyle,
|
...statCardStyle,
|
||||||
borderLeft: `3px solid ${colors.error.main}`,
|
borderLeft: `3px solid ${colors.error.main}`,
|
||||||
background: `${colors.error.main}08`
|
background: `${colors.error.main}08`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 卡片基础样式
|
// 卡片基础样式
|
||||||
@@ -184,7 +184,7 @@ export const cardStyle = {
|
|||||||
border: 'none',
|
border: 'none',
|
||||||
boxShadow: shadows.medium,
|
boxShadow: shadows.medium,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
background: colors.background.primary
|
background: colors.background.primary,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 筛选卡片样式
|
// 筛选卡片样式
|
||||||
@@ -193,7 +193,7 @@ export const filterCardStyle = {
|
|||||||
border: 'none',
|
border: 'none',
|
||||||
boxShadow: shadows.small,
|
boxShadow: shadows.small,
|
||||||
background: colors.background.primary,
|
background: colors.background.primary,
|
||||||
marginBottom: spacing.lg
|
marginBottom: spacing.lg,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 模态框头部样式
|
// 模态框头部样式
|
||||||
@@ -202,7 +202,7 @@ export const modalHeaderStyle = {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: spacing.sm,
|
gap: spacing.sm,
|
||||||
fontSize: '18px',
|
fontSize: '18px',
|
||||||
fontWeight: '600'
|
fontWeight: '600',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 表格样式常量
|
// 表格样式常量
|
||||||
@@ -210,7 +210,7 @@ export const tableStyles = {
|
|||||||
// 表格容器样式
|
// 表格容器样式
|
||||||
wrapper: {
|
wrapper: {
|
||||||
borderRadius: borderRadius.medium,
|
borderRadius: borderRadius.medium,
|
||||||
overflow: 'hidden'
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 空状态样式
|
// 空状态样式
|
||||||
@@ -218,15 +218,15 @@ export const tableStyles = {
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
padding: '60px 20px',
|
padding: '60px 20px',
|
||||||
color: colors.text.secondary,
|
color: colors.text.secondary,
|
||||||
fontSize: '15px'
|
fontSize: '15px',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 空状态图标样式
|
// 空状态图标样式
|
||||||
emptyIcon: {
|
emptyIcon: {
|
||||||
fontSize: '48px',
|
fontSize: '48px',
|
||||||
marginBottom: '16px',
|
marginBottom: '16px',
|
||||||
color: colors.border.light
|
color: colors.border.light,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 搜索输入框样式
|
// 搜索输入框样式
|
||||||
@@ -234,24 +234,24 @@ export const searchInputStyle = {
|
|||||||
width: '280px',
|
width: '280px',
|
||||||
borderRadius: borderRadius.medium,
|
borderRadius: borderRadius.medium,
|
||||||
border: `1px solid ${colors.border.light}`,
|
border: `1px solid ${colors.border.light}`,
|
||||||
transition: `all ${transitions.fast}`
|
transition: `all ${transitions.fast}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 选择器样式
|
// 选择器样式
|
||||||
export const selectStyle = {
|
export const selectStyle = {
|
||||||
borderRadius: borderRadius.medium
|
borderRadius: borderRadius.medium,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 下拉菜单样式
|
// 下拉菜单样式
|
||||||
export const dropdownStyle = {
|
export const dropdownStyle = {
|
||||||
borderRadius: borderRadius.medium
|
borderRadius: borderRadius.medium,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 刷新按钮样式
|
// 刷新按钮样式
|
||||||
export const refreshButtonStyle = {
|
export const refreshButtonStyle = {
|
||||||
borderRadius: borderRadius.medium,
|
borderRadius: borderRadius.medium,
|
||||||
border: `1px solid ${colors.border.light}`,
|
border: `1px solid ${colors.border.light}`,
|
||||||
height: '36px'
|
height: '36px',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 搜索按钮样式
|
// 搜索按钮样式
|
||||||
@@ -260,14 +260,14 @@ export const searchButtonStyle = {
|
|||||||
borderRadius: borderRadius.medium,
|
borderRadius: borderRadius.medium,
|
||||||
background: colors.primary.gradient,
|
background: colors.primary.gradient,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
boxShadow: shadows.small
|
boxShadow: shadows.small,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 重置按钮样式
|
// 重置按钮样式
|
||||||
export const resetButtonStyle = {
|
export const resetButtonStyle = {
|
||||||
height: '36px',
|
height: '36px',
|
||||||
borderRadius: borderRadius.medium,
|
borderRadius: borderRadius.medium,
|
||||||
border: `1px solid ${colors.border.light}`
|
border: `1px solid ${colors.border.light}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 导入模态框样式
|
// 导入模态框样式
|
||||||
@@ -278,14 +278,14 @@ export const importModalStyles = {
|
|||||||
padding: '16px',
|
padding: '16px',
|
||||||
background: 'linear-gradient(180deg, #fafafa 0%, #ffffff 100%)',
|
background: 'linear-gradient(180deg, #fafafa 0%, #ffffff 100%)',
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
border: '1px solid #f0f0f0'
|
border: '1px solid #f0f0f0',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 标题样式
|
// 标题样式
|
||||||
title: {
|
title: {
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
marginBottom: '8px',
|
marginBottom: '8px',
|
||||||
color: '#333'
|
color: '#333',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 列表样式
|
// 列表样式
|
||||||
@@ -294,14 +294,14 @@ export const importModalStyles = {
|
|||||||
marginBottom: '10px',
|
marginBottom: '10px',
|
||||||
color: '#666',
|
color: '#666',
|
||||||
fontSize: '13px',
|
fontSize: '13px',
|
||||||
marginTop: '12px'
|
marginTop: '12px',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 进度容器样式
|
// 进度容器样式
|
||||||
progressContainer: {
|
progressContainer: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: '16px'
|
marginBottom: '16px',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 进度图标样式
|
// 进度图标样式
|
||||||
@@ -315,7 +315,7 @@ export const importModalStyles = {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginRight: '16px',
|
marginRight: '16px',
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
fontSize: '20px'
|
fontSize: '20px',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 进度信息样式
|
// 进度信息样式
|
||||||
@@ -324,37 +324,40 @@ export const importModalStyles = {
|
|||||||
margin: '0 0 4px 0',
|
margin: '0 0 4px 0',
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#333',
|
color: '#333',
|
||||||
fontSize: '16px'
|
fontSize: '16px',
|
||||||
},
|
},
|
||||||
phase: {
|
phase: {
|
||||||
margin: 0,
|
margin: 0,
|
||||||
color: colors.primary.main,
|
color: colors.primary.main,
|
||||||
fontSize: '14px'
|
fontSize: '14px',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// 结果卡片样式
|
// 结果卡片样式
|
||||||
resultCard: (type) => ({
|
resultCard: type => ({
|
||||||
padding: '12px',
|
padding: '12px',
|
||||||
background: type === 'total' ? colors.primary.gradient :
|
background:
|
||||||
type === 'success' ? 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)' :
|
type === 'total'
|
||||||
'linear-gradient(135deg, #ff4d4f 0%, #cf1322 100%)',
|
? colors.primary.gradient
|
||||||
|
: type === 'success'
|
||||||
|
? 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)'
|
||||||
|
: 'linear-gradient(135deg, #ff4d4f 0%, #cf1322 100%)',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
textAlign: 'center'
|
textAlign: 'center',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 结果数值样式
|
// 结果数值样式
|
||||||
resultValue: {
|
resultValue: {
|
||||||
fontSize: '24px',
|
fontSize: '24px',
|
||||||
fontWeight: '700'
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 结果标签样式
|
// 结果标签样式
|
||||||
resultLabel: {
|
resultLabel: {
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
opacity: 0.9
|
opacity: 0.9,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 详情模态框样式
|
// 详情模态框样式
|
||||||
@@ -363,17 +366,17 @@ export const detailModalStyles = {
|
|||||||
infoItem: {
|
infoItem: {
|
||||||
label: {
|
label: {
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
color: '#666'
|
color: '#666',
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
marginLeft: 8,
|
marginLeft: 8,
|
||||||
color: '#333'
|
color: '#333',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// 描述区域样式
|
// 描述区域样式
|
||||||
description: {
|
description: {
|
||||||
marginTop: '16px'
|
marginTop: '16px',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 描述内容样式
|
// 描述内容样式
|
||||||
@@ -382,8 +385,8 @@ export const detailModalStyles = {
|
|||||||
padding: '12px',
|
padding: '12px',
|
||||||
backgroundColor: '#fafafa',
|
backgroundColor: '#fafafa',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
color: '#333'
|
color: '#333',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 导出模态框样式
|
// 导出模态框样式
|
||||||
@@ -394,17 +397,17 @@ export const exportModalStyles = {
|
|||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
border: '1px solid #f0f0f0',
|
border: '1px solid #f0f0f0',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
padding: '12px'
|
padding: '12px',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 字段项样式
|
// 字段项样式
|
||||||
fieldItem: {
|
fieldItem: {
|
||||||
marginBottom: '8px'
|
marginBottom: '8px',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// CSS-in-JS 样式字符串生成函数
|
// CSS-in-JS 样式字符串生成函数
|
||||||
export const generateGlobalStyles = (tokens) => `
|
export const generateGlobalStyles = tokens => `
|
||||||
.device-modal .ant-modal-close {
|
.device-modal .ant-modal-close {
|
||||||
top: 16px;
|
top: 16px;
|
||||||
right: 24px;
|
right: 24px;
|
||||||
|
|||||||
Reference in New Issue
Block a user