perf(frontend): 优化前端性能与代码结构

refactor(components): 使用React.memo和useCallback优化组件渲染
feat(cache): 添加API缓存管理功能
perf(build): 配置Vite构建优化选项
style: 提取工具函数减少重复代码
chore: 添加懒加载和Suspense支持
This commit is contained in:
zhang1106
2025-12-26 11:03:47 +08:00
parent 8d1cd417fa
commit f2327ea3f1
11 changed files with 2147 additions and 895 deletions
+72 -69
View File
@@ -1,27 +1,28 @@
import React, { useState } from 'react';
import React, { useState, Suspense, lazy } from 'react';
import { Layout, Menu, theme, Button, Dropdown, Avatar, message, Space, Divider } from 'antd';
import { BarChartOutlined, DatabaseOutlined, CloudServerOutlined, MenuUnfoldOutlined, MenuFoldOutlined, EyeOutlined, BuildOutlined, HomeOutlined, ShoppingCartOutlined, InboxOutlined, ImportOutlined, FileTextOutlined, UserOutlined, LogoutOutlined, UserOutlined as UserIcon, HistoryOutlined, AuditOutlined, ToolOutlined, ScheduleOutlined } from '@ant-design/icons';
import { BarChartOutlined, DatabaseOutlined, CloudServerOutlined, MenuUnfoldOutlined, MenuFoldOutlined, EyeOutlined, BuildOutlined, HomeOutlined, ShoppingCartOutlined, InboxOutlined, ImportOutlined, FileTextOutlined, UserOutlined, LogoutOutlined, HistoryOutlined, AuditOutlined, ToolOutlined, ScheduleOutlined } from '@ant-design/icons';
import { BrowserRouter as Router, Routes, Route, Link, Navigate, useLocation, useNavigate } from 'react-router-dom';
import { useAuth } from './context/AuthContext';
import Dashboard from './pages/Dashboard';
import DeviceManagement from './pages/DeviceManagement';
import RackManagement from './pages/RackManagement';
import RoomManagement from './pages/RoomManagement';
import DeviceFieldManagement from './pages/DeviceFieldManagement';
import RackVisualization from './pages/RackVisualization';
import ConsumableManagement from './pages/ConsumableManagement';
import ConsumableStatistics from './pages/ConsumableStatistics';
import ConsumableLogs from './pages/ConsumableLogs';
import CategoryManagement from './pages/CategoryManagement';
import UserManagement from './pages/UserManagement';
import LoginHistory from './pages/LoginHistory';
import OperationLogs from './pages/OperationLogs';
import Login from './pages/Login';
import TicketManagement from './pages/TicketManagement';
import TicketCategoryManagement from './pages/TicketCategoryManagement';
import TicketStatistics from './pages/TicketStatistics';
import { Spin } from 'antd';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const DeviceManagement = lazy(() => import('./pages/DeviceManagement'));
const RackManagement = lazy(() => import('./pages/RackManagement'));
const RoomManagement = lazy(() => import('./pages/RoomManagement'));
const DeviceFieldManagement = lazy(() => import('./pages/DeviceFieldManagement'));
const RackVisualization = lazy(() => import('./pages/RackVisualization'));
const ConsumableManagement = lazy(() => import('./pages/ConsumableManagement'));
const ConsumableStatistics = lazy(() => import('./pages/ConsumableStatistics'));
const ConsumableLogs = lazy(() => import('./pages/ConsumableLogs'));
const CategoryManagement = lazy(() => import('./pages/CategoryManagement'));
const UserManagement = lazy(() => import('./pages/UserManagement'));
const LoginHistory = lazy(() => import('./pages/LoginHistory'));
const OperationLogs = lazy(() => import('./pages/OperationLogs'));
const Login = lazy(() => import('./pages/Login'));
const TicketManagement = lazy(() => import('./pages/TicketManagement'));
const TicketCategoryManagement = lazy(() => import('./pages/TicketCategoryManagement'));
const TicketStatistics = lazy(() => import('./pages/TicketStatistics'));
const { Header, Content, Sider } = Layout;
const PrivateRoute = ({ children }) => {
@@ -46,7 +47,25 @@ const PrivateRoute = ({ children }) => {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
return (
<Suspense
fallback={
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
background: '#f5f5f5'
}}>
<Spin size="large" tip="正在加载页面..." />
</div>
}
>
<AppLayout>
{children}
</AppLayout>
</Suspense>
);
};
const AppLayout = ({ children }) => {
@@ -285,14 +304,28 @@ function App() {
return (
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/login" element={
<Suspense
fallback={
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
background: '#f5f5f5'
}}>
<Spin size="large" tip="正在加载登录页面..." />
</div>
}
>
<Login />
</Suspense>
} />
<Route
path="/"
element={
<PrivateRoute>
<AppLayout>
<Dashboard />
</AppLayout>
<Dashboard />
</PrivateRoute>
}
/>
@@ -300,9 +333,7 @@ function App() {
path="/devices"
element={
<PrivateRoute>
<AppLayout>
<DeviceManagement />
</AppLayout>
<DeviceManagement />
</PrivateRoute>
}
/>
@@ -310,9 +341,7 @@ function App() {
path="/racks"
element={
<PrivateRoute>
<AppLayout>
<RackManagement />
</AppLayout>
<RackManagement />
</PrivateRoute>
}
/>
@@ -320,9 +349,7 @@ function App() {
path="/rooms"
element={
<PrivateRoute>
<AppLayout>
<RoomManagement />
</AppLayout>
<RoomManagement />
</PrivateRoute>
}
/>
@@ -330,9 +357,7 @@ function App() {
path="/fields"
element={
<PrivateRoute>
<AppLayout>
<DeviceFieldManagement />
</AppLayout>
<DeviceFieldManagement />
</PrivateRoute>
}
/>
@@ -340,9 +365,7 @@ function App() {
path="/visualization"
element={
<PrivateRoute>
<AppLayout>
<RackVisualization />
</AppLayout>
<RackVisualization />
</PrivateRoute>
}
/>
@@ -350,9 +373,7 @@ function App() {
path="/consumables"
element={
<PrivateRoute>
<AppLayout>
<ConsumableManagement />
</AppLayout>
<ConsumableManagement />
</PrivateRoute>
}
/>
@@ -360,9 +381,7 @@ function App() {
path="/consumables-categories"
element={
<PrivateRoute>
<AppLayout>
<CategoryManagement />
</AppLayout>
<CategoryManagement />
</PrivateRoute>
}
/>
@@ -370,9 +389,7 @@ function App() {
path="/consumables-stats"
element={
<PrivateRoute>
<AppLayout>
<ConsumableStatistics />
</AppLayout>
<ConsumableStatistics />
</PrivateRoute>
}
/>
@@ -380,9 +397,7 @@ function App() {
path="/consumables-logs"
element={
<PrivateRoute>
<AppLayout>
<ConsumableLogs />
</AppLayout>
<ConsumableLogs />
</PrivateRoute>
}
/>
@@ -390,9 +405,7 @@ function App() {
path="/users"
element={
<PrivateRoute>
<AppLayout>
<UserManagement />
</AppLayout>
<UserManagement />
</PrivateRoute>
}
/>
@@ -400,9 +413,7 @@ function App() {
path="/login-history"
element={
<PrivateRoute>
<AppLayout>
<LoginHistory />
</AppLayout>
<LoginHistory />
</PrivateRoute>
}
/>
@@ -410,9 +421,7 @@ function App() {
path="/operation-logs"
element={
<PrivateRoute>
<AppLayout>
<OperationLogs />
</AppLayout>
<OperationLogs />
</PrivateRoute>
}
/>
@@ -420,9 +429,7 @@ function App() {
path="/tickets"
element={
<PrivateRoute>
<AppLayout>
<TicketManagement />
</AppLayout>
<TicketManagement />
</PrivateRoute>
}
/>
@@ -430,9 +437,7 @@ function App() {
path="/ticket-categories"
element={
<PrivateRoute>
<AppLayout>
<TicketCategoryManagement />
</AppLayout>
<TicketCategoryManagement />
</PrivateRoute>
}
/>
@@ -440,9 +445,7 @@ function App() {
path="/ticket-statistics"
element={
<PrivateRoute>
<AppLayout>
<TicketStatistics />
</AppLayout>
<TicketStatistics />
</PrivateRoute>
}
/>
+286
View File
@@ -0,0 +1,286 @@
const cacheManager = (() => {
const cache = new Map();
const cacheTimestamps = new Map();
const defaultTTL = 5 * 60 * 1000;
const config = new Map();
const generateKey = (method, url, params) => {
const paramsStr = params ? JSON.stringify(params, Object.keys(params).sort()) : '';
return `${method}:${url}:${paramsStr}`;
};
const isExpired = (key) => {
const timestamp = cacheTimestamps.get(key);
if (!timestamp) return true;
const ttl = config.get(key)?.ttl || defaultTTL;
return Date.now() - timestamp > ttl;
};
const get = (method, url, params) => {
const key = generateKey(method, url, params);
if (isExpired(key)) {
cache.delete(key);
cacheTimestamps.delete(key);
return null;
}
return cache.get(key);
};
const set = (method, url, params, data, ttl) => {
const key = generateKey(method, url, params);
cache.set(key, data);
cacheTimestamps.set(key, Date.now());
config.set(key, { ttl });
return key;
};
const invalidate = (url) => {
const keysToDelete = [];
cache.forEach((_, key) => {
if (key.includes(url)) {
keysToDelete.push(key);
}
});
keysToDelete.forEach(key => {
cache.delete(key);
cacheTimestamps.delete(key);
config.delete(key);
});
return keysToDelete.length;
};
const invalidatePattern = (pattern) => {
const regex = new RegExp(pattern);
const keysToDelete = [];
cache.forEach((_, key) => {
if (regex.test(key)) {
keysToDelete.push(key);
}
});
keysToDelete.forEach(key => {
cache.delete(key);
cacheTimestamps.delete(key);
config.delete(key);
});
return keysToDelete.length;
};
const clear = () => {
cache.clear();
cacheTimestamps.clear();
config.clear();
};
const setTTL = (url, ttl) => {
config.set(url, { ttl });
};
const getStats = () => {
return {
size: cache.size,
keys: Array.from(cache.keys())
};
};
return {
get,
set,
invalidate,
invalidatePattern,
clear,
setTTL,
getStats,
defaultTTL
};
})();
const cacheInterceptor = (api) => {
const requestCache = new Set();
const pendingRequests = new Map();
api.interceptors.request.use(
(config) => {
if (config.method?.toLowerCase() === 'get') {
const cacheKey = cacheManager.generateKey(
config.method,
config.url,
config.params
);
if (requestCache.has(cacheKey)) {
config.adapter = () => {
const cachedData = cacheManager.get(
config.method,
config.url,
config.params
);
if (cachedData) {
return Promise.resolve({
data: cachedData,
status: 200,
statusText: 'OK',
headers: {},
config
});
}
requestCache.delete(cacheKey);
return api.request(config);
};
}
}
return config;
},
(error) => Promise.reject(error)
);
api.interceptors.response.use(
(response) => {
if (response.config.method?.toLowerCase() === 'get') {
const cacheKey = cacheManager.generateKey(
response.config.method,
response.config.url,
response.config.params
);
cacheManager.set(
response.config.method,
response.config.url,
response.config.params,
response.data
);
requestCache.add(cacheKey);
}
return response;
},
(error) => {
if (error.config) {
const cacheKey = cacheManager.generateKey(
error.config.method,
error.config.url,
error.config.params
);
requestCache.delete(cacheKey);
}
return Promise.reject(error);
}
);
};
export const cachedAPI = {
get: async (url, params = {}, ttl) => {
const method = 'get';
const cached = cacheManager.get(method, url, params);
if (cached) {
return cached;
}
return api.get(url, { params }).then(data => {
cacheManager.set(method, url, params, data, ttl);
return data;
});
},
post: (url, data) => api.post(url, data).then(data => {
cacheManager.invalidate(url);
return data;
}),
put: (url, data) => api.put(url, data).then(data => {
cacheManager.invalidate(url);
return data;
}),
delete: (url) => api.delete(url).then(data => {
cacheManager.invalidate(url);
return data;
}),
invalidate: (url) => cacheManager.invalidate(url),
invalidatePattern: (pattern) => cacheManager.invalidatePattern(pattern),
clearCache: () => cacheManager.clear(),
setCacheTTL: (url, ttl) => cacheManager.setTTL(url, ttl),
getCacheStats: () => cacheManager.getStats()
};
export const deviceAPI = {
list: (params) => cachedAPI.get('/devices', params),
get: (deviceId) => cachedAPI.get(`/devices/${deviceId}`),
create: (data) => cachedAPI.post('/devices', data),
update: (deviceId, data) => cachedAPI.put(`/api/devices/${deviceId}`, data),
delete: (deviceId) => cachedAPI.delete(`/api/devices/${deviceId}`),
batchOffline: (data) => cachedAPI.post('/devices/batch-offline', data),
batchDelete: (data) => cachedAPI.delete('/devices/batch-delete', { data }),
export: (params) => api.get('/devices/export', { params, responseType: 'blob' }),
import: (formData) => api.post('/devices/import', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
};
export const rackAPI = {
list: (params) => cachedAPI.get('/racks', params),
get: (rackId) => cachedAPI.get(`/racks/${rackId}`),
create: (data) => cachedAPI.post('/racks', data),
update: (rackId, data) => cachedAPI.put(`/racks/${rackId}`, data),
delete: (rackId) => cachedAPI.delete(`/racks/${rackId}`),
import: (formData) => api.post('/racks/import', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
};
export const roomAPI = {
list: (params) => cachedAPI.get('/rooms', params),
get: (roomId) => cachedAPI.get(`/rooms/${roomId}`),
create: (data) => cachedAPI.post('/rooms', data),
update: (roomId, data) => cachedAPI.put(`/rooms/${roomId}`, data),
delete: (roomId) => cachedAPI.delete(`/rooms/${roomId}`)
};
export const deviceFieldAPI = {
list: () => cachedAPI.get('/deviceFields'),
get: (fieldId) => cachedAPI.get(`/deviceFields/${fieldId}`),
create: (data) => cachedAPI.post('/deviceFields', data),
update: (fieldId, data) => cachedAPI.put(`/deviceFields/${fieldId}`, data),
delete: (fieldId) => cachedAPI.delete(`/deviceFields/${fieldId}`),
updateConfig: (data) => cachedAPI.post('/deviceFields/config', data)
};
export const consumableAPI = {
list: (params) => cachedAPI.get('/consumables', params),
get: (consumableId) => cachedAPI.get(`/consumables/${consumableId}`),
create: (data) => cachedAPI.post('/consumables', data),
update: (consumableId, data) => cachedAPI.put(`/consumables/${consumableId}`, data),
delete: (consumableId) => cachedAPI.delete(`/consumables/${consumableId}`),
import: (data) => cachedAPI.post('/consumables/import', data),
quickInOut: (data) => cachedAPI.post('/consumables/quick-inout', data),
getStatistics: () => cachedAPI.get('/consumables/statistics/summary'),
getLowStock: () => cachedAPI.get('/consumables/low-stock')
};
export const consumableCategoryAPI = {
list: (params) => cachedAPI.get('/consumable-categories', params),
getList: (params) => cachedAPI.get('/consumable-categories/list', params),
create: (data) => cachedAPI.post('/consumable-categories', data),
update: (id, data) => cachedAPI.put(`/consumable-categories/${id}`, data),
delete: (id) => cachedAPI.delete(`/consumable-categories/${id}`)
};
export const consumableLogAPI = {
list: (params) => cachedAPI.get('/consumables/logs', params),
create: (data) => cachedAPI.post('/consumables/logs', data),
export: (params) => api.get('/consumables/logs/export', { params, responseType: 'blob' }),
import: (data) => cachedAPI.post('/consumables/logs/import', data)
};
export const ticketCategoryAPI = {
list: (params) => cachedAPI.get('/ticket-categories', params),
create: (data) => cachedAPI.post('/ticket-categories', data),
update: (code, data) => cachedAPI.put(`/ticket-categories/${code}`, data),
delete: (code) => cachedAPI.delete(`/ticket-categories/${code}`),
getTree: () => cachedAPI.get('/ticket-categories/tree'),
init: () => cachedAPI.post('/ticket-categories/init')
};
export { cacheManager };
export default { cachedAPI, cacheManager };
+25 -25
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } 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 } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ExportOutlined, ImportOutlined, UploadOutlined, FileExcelOutlined, InboxOutlined } from '@ant-design/icons';
import axios from 'axios';
@@ -31,7 +31,7 @@ function ConsumableManagement() {
const [stockType, setStockType] = useState('in');
const [stockForm] = Form.useForm();
const fetchConsumables = async (page = 1, pageSize = 10) => {
const fetchConsumables = useCallback(async (page = 1, pageSize = 10) => {
try {
setLoading(true);
const response = await axios.get('/api/consumables', {
@@ -45,23 +45,23 @@ function ConsumableManagement() {
} finally {
setLoading(false);
}
};
}, [keyword, category, status]);
const fetchCategories = async () => {
const fetchCategories = useCallback(async () => {
try {
const response = await axios.get('/api/consumable-categories/list');
setCategories(response.data);
} catch (error) {
console.error('获取分类列表失败:', error);
}
};
}, []);
useEffect(() => {
fetchConsumables();
fetchCategories();
}, [keyword, category, status]);
}, [fetchConsumables, fetchCategories]);
const showModal = (consumable = null) => {
const showModal = useCallback((consumable = null) => {
setEditingConsumable(consumable);
if (consumable) {
form.setFieldsValue(consumable);
@@ -69,14 +69,14 @@ function ConsumableManagement() {
form.resetFields();
}
setModalVisible(true);
};
}, [form]);
const handleCancel = () => {
const handleCancel = useCallback(() => {
setModalVisible(false);
setEditingConsumable(null);
};
}, []);
const handleSubmit = async (values) => {
const handleSubmit = useCallback(async (values) => {
try {
if (editingConsumable) {
await axios.put(`/api/consumables/${editingConsumable.consumableId}`, values);
@@ -95,9 +95,9 @@ function ConsumableManagement() {
message.error(editingConsumable ? '耗材更新失败' : '耗材创建失败');
console.error('提交失败:', error);
}
};
}, [editingConsumable, fetchConsumables]);
const handleDelete = async (consumableId) => {
const handleDelete = useCallback(async (consumableId) => {
try {
await axios.delete(`/api/consumables/${consumableId}`);
message.success('删除成功');
@@ -106,11 +106,11 @@ function ConsumableManagement() {
message.error('删除失败');
console.error('删除失败:', error);
}
};
}, [fetchConsumables]);
const handleSearch = (value) => {
const handleSearch = useCallback((value) => {
setKeyword(value);
};
}, []);
const exportToCSV = (data, filename) => {
const headers = ['耗材ID', '名称', '分类', '单位', '当前库存', '最小库存', '最大库存', '单价', '供应商', '存放位置', '状态'];
@@ -267,7 +267,7 @@ function ConsumableManagement() {
window.URL.revokeObjectURL(url);
};
const showStockModal = (record, type) => {
const showStockModal = useCallback((record, type) => {
setStockRecord(record);
setStockType(type);
stockForm.setFieldsValue({
@@ -278,14 +278,14 @@ function ConsumableManagement() {
notes: ''
});
setStockModalVisible(true);
};
}, [stockForm]);
const handleStockCancel = () => {
const handleStockCancel = useCallback(() => {
setStockModalVisible(false);
setStockRecord(null);
};
}, []);
const handleStockSubmit = async (values) => {
const handleStockSubmit = useCallback(async (values) => {
try {
const response = await axios.post('/api/consumables/quick-inout', {
consumableId: stockRecord.consumableId,
@@ -302,9 +302,9 @@ function ConsumableManagement() {
message.error(error.response?.data?.error || `${stockType === 'in' ? '入库' : '出库'}操作失败`);
console.error('操作失败:', error);
}
};
}, [stockRecord, stockType, fetchConsumables]);
const columns = [
const columns = useMemo(() => [
{
title: '耗材ID',
dataIndex: 'consumableId',
@@ -402,7 +402,7 @@ function ConsumableManagement() {
</Space>
)
}
];
], [showModal, showStockModal, handleDelete]);
const previewColumns = [
{ title: '名称', dataIndex: '名称', key: 'name', width: 120 },
@@ -608,4 +608,4 @@ function ConsumableManagement() {
);
}
export default ConsumableManagement;
export default React.memo(ConsumableManagement);
+335 -424
View File
@@ -1,12 +1,11 @@
import React, { useState, useEffect } from 'react';
import { Card, Row, Col, Statistic, Spin, message, Button, Tag } from 'antd';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Card, Row, Col, Statistic, message, Button, Tag } from 'antd';
import {
DatabaseOutlined,
CloudServerOutlined,
WarningOutlined,
PoweroffOutlined,
HomeOutlined,
MonitorOutlined,
SettingOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
@@ -15,197 +14,351 @@ import {
} from '@ant-design/icons';
import axios from 'axios';
const theme = {
primary: '#1890ff',
primaryDark: '#096dd9',
secondary: '#722ed1',
success: '#52c41a',
warning: '#faad14',
error: '#ff4d4f',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
cardBg: 'rgba(255, 255, 255, 0.95)',
textPrimary: '#262626',
textSecondary: '#8c8c8c'
};
const containerStyle = {
minHeight: '100vh',
background: 'linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%)',
padding: '24px'
};
const headerStyle = {
textAlign: 'center',
marginBottom: '32px'
};
const titleStyle = {
fontSize: '2.5rem',
fontWeight: '700',
background: theme.background,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
margin: '0 0 8px 0'
};
const subtitleStyle = {
fontSize: '1.1rem',
color: theme.textSecondary,
margin: '0'
};
const statCardStyle = {
borderRadius: '16px',
border: 'none',
boxShadow: '0 4px 16px rgba(24, 144, 255, 0.1)',
background: theme.cardBg,
backdropFilter: 'blur(10px)',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative',
overflow: 'hidden',
cursor: 'pointer'
};
const systemOverviewStyle = {
marginTop: '32px'
};
const overviewCardStyle = {
borderRadius: '20px',
border: 'none',
boxShadow: '0 8px 32px rgba(24, 144, 255, 0.15)',
background: theme.cardBg,
backdropFilter: 'blur(10px)'
};
const welcomeSectionStyle = {
background: theme.background,
borderRadius: '12px',
padding: '20px',
textAlign: 'center',
color: 'white',
marginBottom: '24px'
};
const navigationGridStyle = {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))',
gap: '16px',
marginBottom: '24px'
};
const navButtonStyle = {
height: 'auto',
padding: '20px 16px',
borderRadius: '12px',
border: '2px solid rgba(24, 144, 255, 0.1)',
background: 'rgba(24, 144, 255, 0.02)',
transition: 'all 0.3s ease',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '8px'
};
const navIconStyle = {
fontSize: '2rem',
color: theme.primary
};
const navTextStyle = {
fontSize: '0.95rem',
fontWeight: '600',
color: theme.textPrimary
};
const systemInfoStyle = {
background: 'linear-gradient(135deg, #e8f4fd 0%, #f0f9ff 100%)',
borderRadius: '12px',
padding: '16px',
border: '1px solid rgba(24, 144, 255, 0.1)'
};
const navButtonsData = [
{ key: 'devices', icon: CloudServerOutlined, text: '设备管理', path: '/devices' },
{ key: 'racks', icon: DatabaseOutlined, text: '资源规划', path: '/racks' },
{ key: 'faults', icon: WarningOutlined, text: '故障监控', path: '/faults' },
{ key: 'settings', icon: SettingOutlined, text: '系统配置', path: '/settings' }
];
const createTrendStyle = (trend) => ({
display: 'flex',
alignItems: 'center',
fontSize: '0.875rem',
fontWeight: '500',
color: trend > 0 ? theme.success : theme.error,
marginTop: '8px'
});
const createStatCardBg = (color) => ({
position: 'absolute',
top: '0',
right: '0',
width: '60px',
height: '60px',
background: `linear-gradient(135deg, ${color} 0%, ${color}99 100%)`,
borderRadius: '50%',
opacity: '0.1'
});
function Dashboard() {
const [stats, setStats] = useState({
totalDevices: 0,
totalRacks: 0,
totalRooms: 0,
faultDevices: 0,
deviceGrowth: 2.5, // 示例增长数据
faultTrend: -12.3 // 示例趋势数据
});
totalDevices: 0,
totalRacks: 0,
totalRooms: 0,
faultDevices: 0,
deviceGrowth: 2.5,
faultTrend: -12.3
});
const [loading, setLoading] = useState(true);
useEffect(() => {
// 获取统计数据
const fetchStats = async () => {
try {
setLoading(true);
// 获取所有设备
const devicesRes = await axios.get('/api/devices');
// 设备API返回的是包含total和devices数组的对象
const totalDevices = devicesRes.data.total;
// 获取所有设备以统计故障设备数量
const allDevicesRes = await axios.get('/api/devices', { params: { pageSize: totalDevices } });
const allDevices = allDevicesRes.data.devices || allDevicesRes.data;
const faultDevices = allDevices.filter(device => device.status === 'fault').length;
// 获取所有机柜
const racksRes = await axios.get('/api/racks');
// 机柜API返回的是包含total和racks数组的对象
const totalRacks = racksRes.data.total;
const racks = racksRes.data.racks || [];
// 获取所有机房
const roomsRes = await axios.get('/api/rooms');
const rooms = roomsRes.data;
const totalRooms = rooms.length;
setStats({
totalDevices,
totalRacks,
totalRooms,
faultDevices,
// 保留或更新趋势数据
deviceGrowth: stats.deviceGrowth || 0,
faultTrend: stats.faultTrend || 0
});
} catch (error) {
message.error('获取统计数据失败');
console.error('获取统计数据失败:', error);
} finally {
setLoading(false);
const fetchStats = useCallback(async () => {
try {
setLoading(true);
const [devicesRes, racksRes, roomsRes] = await Promise.all([
axios.get('/api/devices', { params: { pageSize: 1 } }),
axios.get('/api/racks', { params: { pageSize: 1 } }),
axios.get('/api/rooms')
]);
const totalDevices = devicesRes.data.total || 0;
const totalRacks = racksRes.data.total || 0;
const rooms = roomsRes.data || [];
const totalRooms = rooms.length;
let faultDevices = 0;
if (totalDevices > 0) {
try {
const faultRes = await axios.get('/api/devices/count', {
params: { status: 'fault' }
});
faultDevices = faultRes.data.count || 0;
} catch {
faultDevices = 0;
}
}
};
fetchStats();
setStats({
totalDevices,
totalRacks,
totalRooms,
faultDevices,
deviceGrowth: 2.5,
faultTrend: -12.3
});
} catch (error) {
message.error('获取统计数据失败');
console.error('获取统计数据失败:', error);
} finally {
setLoading(false);
}
}, []);
// 科技感配色主题
const theme = {
primary: '#1890ff', // 科技蓝
primaryDark: '#096dd9', // 深蓝
secondary: '#722ed1', // 紫色
success: '#52c41a', // 绿色
warning: '#faad14', // 橙色
error: '#ff4d4f', // 红色
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
cardBg: 'rgba(255, 255, 255, 0.95)',
textPrimary: '#262626',
textSecondary: '#8c8c8c'
};
useEffect(() => {
fetchStats();
}, [fetchStats]);
const containerStyle = {
minHeight: '100vh',
background: 'linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%)',
padding: '24px'
};
const handleNavHover = useCallback((e, isEnter) => {
if (isEnter) {
e.currentTarget.style.borderColor = theme.primary;
e.currentTarget.style.background = 'rgba(24, 144, 255, 0.1)';
e.currentTarget.style.transform = 'translateY(-2px)';
} else {
e.currentTarget.style.borderColor = 'rgba(24, 144, 255, 0.1)';
e.currentTarget.style.background = 'rgba(24, 144, 255, 0.02)';
e.currentTarget.style.transform = 'translateY(0)';
}
}, []);
const headerStyle = {
textAlign: 'center',
marginBottom: '32px'
};
const handleRefresh = useCallback(() => {
fetchStats();
}, [fetchStats]);
const titleStyle = {
fontSize: '2.5rem',
fontWeight: '700',
background: theme.background,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
margin: '0 0 8px 0'
};
const statCards = useMemo(() => [
{
key: 'devices',
xs: 24, sm: 12, lg: 6,
icon: CloudServerOutlined,
color: '#1890ff',
statKey: 'totalDevices',
title: '总设备数',
trend: stats.deviceGrowth,
tagColor: 'blue'
},
{
key: 'racks',
xs: 24, sm: 12, lg: 6,
icon: DatabaseOutlined,
color: '#722ed1',
statKey: 'totalRacks',
title: '总机柜数',
trend: 0,
tagColor: 'green',
customStatus: true
},
{
key: 'rooms',
xs: 24, sm: 12, lg: 6,
icon: HomeOutlined,
color: '#52c41a',
statKey: 'totalRooms',
title: '总机房数',
trend: 0,
tagColor: 'green',
customStatus: true
},
{
key: 'faults',
xs: 24, sm: 12, lg: 6,
icon: WarningOutlined,
color: '#ff4d4f',
statKey: 'faultDevices',
title: '故障设备',
trend: stats.faultTrend,
tagColor: 'red'
}
], [stats.deviceGrowth, stats.faultTrend]);
const subtitleStyle = {
fontSize: '1.1rem',
color: theme.textSecondary,
margin: '0'
};
const renderStatCard = useCallback((config) => {
const { icon: Icon, color, statKey, title, trend, tagColor, customStatus, xs, sm, lg } = config;
const colProps = { xs, sm, lg };
return (
<Col key={statKey} {...colProps}>
<Card style={statCardStyle}>
<div style={{ position: 'relative' }}>
<div style={createStatCardBg(color)} />
<Statistic
title={
<span style={{ fontSize: '0.95rem', fontWeight: '600', color: theme.textSecondary }}>
{title}
</span>
}
value={stats[statKey]}
prefix={<Icon style={{ color, fontSize: '1.2rem' }} />}
valueStyle={{
fontSize: '2rem',
fontWeight: '700',
color: theme.textPrimary,
marginBottom: '8px'
}}
loading={loading}
/>
{customStatus ? (
statKey === 'totalRacks' ? (
<div style={{ display: 'flex', alignItems: 'center', fontSize: '0.875rem', color: theme.success }}>
<PoweroffOutlined style={{ marginRight: '4px' }} />
<span>正常运行</span>
</div>
) : (
<div style={{ display: 'flex', alignItems: 'center', fontSize: '0.875rem', color: theme.success }}>
<span style={{ width: '8px', height: '8px', background: theme.success, borderRadius: '50%', marginRight: '8px' }} />
<span>全部在线</span>
</div>
)
) : (
<div style={createTrendStyle(trend)}>
{trend > 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
<span style={{ marginLeft: '4px' }}>{Math.abs(trend)}%</span>
<Tag color={tagColor} style={{ marginLeft: '8px', fontSize: '0.75rem' }}>本月</Tag>
</div>
)}
</div>
</Card>
</Col>
);
}, [stats, loading]);
const statCardStyle = {
borderRadius: '16px',
border: 'none',
boxShadow: '0 4px 16px rgba(24, 144, 255, 0.1)',
background: theme.cardBg,
backdropFilter: 'blur(10px)',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative',
overflow: 'hidden',
cursor: 'pointer'
};
const navButtons = useMemo(() => navButtonsData.map(({ key, icon: Icon, text }) => (
<Button
key={key}
type="text"
style={navButtonStyle}
onMouseEnter={(e) => handleNavHover(e, true)}
onMouseLeave={(e) => handleNavHover(e, false)}
>
<Icon style={navIconStyle} />
<span style={navTextStyle}>{text}</span>
</Button>
)), [handleNavHover]);
const statCardHoverStyle = {
transform: 'translateY(-4px)',
boxShadow: '0 8px 32px rgba(24, 144, 255, 0.2)'
};
const trendStyle = (trend) => ({
display: 'flex',
alignItems: 'center',
fontSize: '0.875rem',
fontWeight: '500',
color: trend > 0 ? theme.success : theme.error,
marginTop: '8px'
});
const systemOverviewStyle = {
marginTop: '32px'
};
const overviewCardStyle = {
borderRadius: '20px',
border: 'none',
boxShadow: '0 8px 32px rgba(24, 144, 255, 0.15)',
background: theme.cardBg,
backdropFilter: 'blur(10px)'
};
const welcomeSectionStyle = {
background: theme.background,
borderRadius: '12px',
padding: '20px',
textAlign: 'center',
color: 'white',
marginBottom: '24px'
};
const navigationGridStyle = {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))',
gap: '16px',
marginBottom: '24px'
};
const navButtonStyle = {
height: 'auto',
padding: '20px 16px',
borderRadius: '12px',
border: '2px solid rgba(24, 144, 255, 0.1)',
background: 'rgba(24, 144, 255, 0.02)',
transition: 'all 0.3s ease',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '8px'
};
const navButtonHoverStyle = {
borderColor: theme.primary,
background: 'rgba(24, 144, 255, 0.1)',
transform: 'translateY(-2px)'
};
const navIconStyle = {
fontSize: '2rem',
color: theme.primary
};
const navTextStyle = {
fontSize: '0.95rem',
fontWeight: '600',
color: theme.textPrimary
};
const systemInfoStyle = {
background: 'linear-gradient(135deg, #e8f4fd 0%, #f0f9ff 100%)',
borderRadius: '12px',
padding: '16px',
border: '1px solid rgba(24, 144, 255, 0.1)'
};
const systemInfo = useMemo(() => (
<div style={systemInfoStyle}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<p style={{ margin: '0', fontSize: '0.95rem', color: theme.textPrimary, fontWeight: '500' }}>
<strong>系统版本</strong> v1.0.0
</p>
<p style={{ margin: '4px 0 0 0', fontSize: '0.85rem', color: theme.textSecondary }}>
<strong>最后更新</strong>{new Date().toLocaleDateString()}
</p>
</div>
<Button
type="primary"
icon={<ReloadOutlined />}
size="small"
onClick={handleRefresh}
style={{ background: theme.primary, borderColor: theme.primary }}
>
刷新数据
</Button>
</div>
</div>
), [handleRefresh]);
return (
<div style={containerStyle}>
{/* 页面头部 */}
<div style={headerStyle}>
<h1 style={titleStyle}>
<DashboardOutlined style={{ marginRight: '12px' }} />
@@ -214,164 +367,13 @@ function Dashboard() {
<p style={subtitleStyle}>实时监控 智能管理 高效运维</p>
</div>
{/* 数据概览卡片 */}
<Row gutter={[24, 24]} style={{ marginBottom: '32px' }}>
<Col xs={24} sm={12} lg={6}>
<Card style={statCardStyle}>
<div style={{ position: 'relative' }}>
<div style={{
position: 'absolute',
top: '0',
right: '0',
width: '60px',
height: '60px',
background: 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)',
borderRadius: '50%',
opacity: '0.1'
}} />
<Statistic
title={
<span style={{ fontSize: '0.95rem', fontWeight: '600', color: theme.textSecondary }}>
总设备数
</span>
}
value={stats.totalDevices}
prefix={<CloudServerOutlined style={{ color: theme.primary, fontSize: '1.2rem' }} />}
valueStyle={{
fontSize: '2rem',
fontWeight: '700',
color: theme.textPrimary,
marginBottom: '8px'
}}
loading={loading}
/>
<div style={trendStyle(stats.deviceGrowth)}>
{stats.deviceGrowth > 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
<span style={{ marginLeft: '4px' }}>{Math.abs(stats.deviceGrowth)}%</span>
<Tag color="blue" style={{ marginLeft: '8px', fontSize: '0.75rem' }}>本月</Tag>
</div>
</div>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card style={statCardStyle}>
<div style={{ position: 'relative' }}>
<div style={{
position: 'absolute',
top: '0',
right: '0',
width: '60px',
height: '60px',
background: 'linear-gradient(135deg, #722ed1 0%, #531dab 100%)',
borderRadius: '50%',
opacity: '0.1'
}} />
<Statistic
title={
<span style={{ fontSize: '0.95rem', fontWeight: '600', color: theme.textSecondary }}>
总机柜数
</span>
}
value={stats.totalRacks}
prefix={<DatabaseOutlined style={{ color: theme.secondary, fontSize: '1.2rem' }} />}
valueStyle={{
fontSize: '2rem',
fontWeight: '700',
color: theme.textPrimary,
marginBottom: '8px'
}}
loading={loading}
/>
<div style={{ display: 'flex', alignItems: 'center', fontSize: '0.875rem', color: theme.success }}>
<PoweroffOutlined style={{ marginRight: '4px' }} />
<span>正常运行</span>
</div>
</div>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card style={statCardStyle}>
<div style={{ position: 'relative' }}>
<div style={{
position: 'absolute',
top: '0',
right: '0',
width: '60px',
height: '60px',
background: 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)',
borderRadius: '50%',
opacity: '0.1'
}} />
<Statistic
title={
<span style={{ fontSize: '0.95rem', fontWeight: '600', color: theme.textSecondary }}>
总机房数
</span>
}
value={stats.totalRooms}
prefix={<HomeOutlined style={{ color: theme.success, fontSize: '1.2rem' }} />}
valueStyle={{
fontSize: '2rem',
fontWeight: '700',
color: theme.textPrimary,
marginBottom: '8px'
}}
loading={loading}
/>
<div style={{ display: 'flex', alignItems: 'center', fontSize: '0.875rem', color: theme.success }}>
<span style={{ width: '8px', height: '8px', background: theme.success, borderRadius: '50%', marginRight: '8px' }} />
<span>全部在线</span>
</div>
</div>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card style={{ ...statCardStyle, borderLeft: `4px solid ${theme.error}` }}>
<div style={{ position: 'relative' }}>
<div style={{
position: 'absolute',
top: '0',
right: '0',
width: '60px',
height: '60px',
background: 'linear-gradient(135deg, #ff4d4f 0%, #d73027 100%)',
borderRadius: '50%',
opacity: '0.1'
}} />
<Statistic
title={
<span style={{ fontSize: '0.95rem', fontWeight: '600', color: theme.textSecondary }}>
故障设备
</span>
}
value={stats.faultDevices}
prefix={<WarningOutlined style={{ color: theme.error, fontSize: '1.2rem' }} />}
valueStyle={{
fontSize: '2rem',
fontWeight: '700',
color: theme.error,
marginBottom: '8px'
}}
loading={loading}
/>
<div style={trendStyle(stats.faultTrend)}>
{stats.faultTrend < 0 ? <ArrowDownOutlined /> : <ArrowUpOutlined />}
<span style={{ marginLeft: '4px' }}>{Math.abs(stats.faultTrend)}%</span>
<Tag color="red" style={{ marginLeft: '8px', fontSize: '0.75rem' }}>本周</Tag>
</div>
</div>
</Card>
</Col>
{statCards.map(renderStatCard)}
</Row>
{/* 系统概览 */}
<div style={systemOverviewStyle}>
<Card style={overviewCardStyle}>
<div style={{ padding: '24px' }}>
{/* 欢迎区域 */}
<div style={welcomeSectionStyle}>
<h2 style={{
fontSize: '1.5rem',
@@ -391,102 +393,11 @@ function Dashboard() {
</p>
</div>
{/* 导航功能网格 */}
<div style={navigationGridStyle}>
<Button
type="text"
style={navButtonStyle}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = theme.primary;
e.currentTarget.style.background = 'rgba(24, 144, 255, 0.1)';
e.currentTarget.style.transform = 'translateY(-2px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'rgba(24, 144, 255, 0.1)';
e.currentTarget.style.background = 'rgba(24, 144, 255, 0.02)';
e.currentTarget.style.transform = 'translateY(0)';
}}
>
<CloudServerOutlined style={navIconStyle} />
<span style={navTextStyle}>设备管理</span>
</Button>
<Button
type="text"
style={navButtonStyle}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = theme.primary;
e.currentTarget.style.background = 'rgba(24, 144, 255, 0.1)';
e.currentTarget.style.transform = 'translateY(-2px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'rgba(24, 144, 255, 0.1)';
e.currentTarget.style.background = 'rgba(24, 144, 255, 0.02)';
e.currentTarget.style.transform = 'translateY(0)';
}}
>
<DatabaseOutlined style={navIconStyle} />
<span style={navTextStyle}>资源规划</span>
</Button>
<Button
type="text"
style={navButtonStyle}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = theme.primary;
e.currentTarget.style.background = 'rgba(24, 144, 255, 0.1)';
e.currentTarget.style.transform = 'translateY(-2px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'rgba(24, 144, 255, 0.1)';
e.currentTarget.style.background = 'rgba(24, 144, 255, 0.02)';
e.currentTarget.style.transform = 'translateY(0)';
}}
>
<WarningOutlined style={navIconStyle} />
<span style={navTextStyle}>故障监控</span>
</Button>
<Button
type="text"
style={navButtonStyle}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = theme.primary;
e.currentTarget.style.background = 'rgba(24, 144, 255, 0.1)';
e.currentTarget.style.transform = 'translateY(-2px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'rgba(24, 144, 255, 0.1)';
e.currentTarget.style.background = 'rgba(24, 144, 255, 0.02)';
e.currentTarget.style.transform = 'translateY(0)';
}}
>
<SettingOutlined style={navIconStyle} />
<span style={navTextStyle}>系统配置</span>
</Button>
{navButtons}
</div>
{/* 系统信息 */}
<div style={systemInfoStyle}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<p style={{ margin: '0', fontSize: '0.95rem', color: theme.textPrimary, fontWeight: '500' }}>
<strong>系统版本</strong> v1.0.0
</p>
<p style={{ margin: '4px 0 0 0', fontSize: '0.85rem', color: theme.textSecondary }}>
最后更新{new Date().toLocaleDateString()}
</p>
</div>
<Button
type="primary"
icon={<ReloadOutlined />}
size="small"
style={{ background: theme.primary, borderColor: theme.primary }}
>
刷新数据
</Button>
</div>
</div>
{systemInfo}
</div>
</Card>
</div>
@@ -494,4 +405,4 @@ function Dashboard() {
);
}
export default Dashboard;
export default React.memo(Dashboard);
+77 -4
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { Table, Button, Modal, Form, Input, Select, DatePicker, message, Card, Space, InputNumber, Switch, Upload, Progress, Checkbox } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, UploadOutlined, DownloadOutlined, SettingOutlined, UndoOutlined, CloudServerOutlined, SwapOutlined, SafetyOutlined, DatabaseOutlined, AppstoreOutlined } from '@ant-design/icons';
import axios from 'axios';
@@ -7,6 +7,79 @@ import dayjs from 'dayjs';
const { Option } = Select;
const { RangePicker } = DatePicker;
// 工具函数提取到组件外部,避免每次渲染重复创建
const getStatusConfig = (status) => {
const statusMap = {
running: { text: '运行中', color: 'green' },
maintenance: { text: '维护中', color: 'orange' },
offline: { text: '离线', color: 'gray' },
fault: { text: '故障', color: 'red' }
};
return statusMap[status] || { text: status, color: 'black' };
};
const getTypeLabel = (type) => {
const typeMap = {
server: '服务器',
switch: '交换机',
router: '路由器',
storage: '存储设备',
other: '其他设备'
};
return typeMap[type] || type;
};
const getDeviceTypeIcon = (type) => {
const iconMap = {
server: <CloudServerOutlined style={{ color: '#1890ff' }} />,
switch: <SwapOutlined style={{ color: '#52c41a' }} />,
router: <SafetyOutlined style={{ color: '#faad14' }} />,
storage: <DatabaseOutlined style={{ color: '#722ed1' }} />,
other: <AppstoreOutlined style={{ color: '#8c8c8c' }} />
};
return iconMap[type] || <AppstoreOutlined style={{ color: '#8c8c8c' }} />;
};
// 格式化日期
const formatDate = (date, fieldName) => {
if (!date) return '';
const dateObj = new Date(date);
const formattedDate = dateObj.toLocaleDateString('zh-CN');
if (fieldName === 'warrantyExpiry') {
const today = new Date();
today.setHours(0, 0, 0, 0);
dateObj.setHours(0, 0, 0, 0);
if (dateObj < today) {
return <span style={{ color: '#d93025', fontWeight: 'bold' }}>{formattedDate}</span>;
}
}
return formattedDate;
};
// 默认设备字段配置
const defaultDeviceFields = [
{ fieldName: 'deviceId', displayName: '设备ID', fieldType: 'string', required: true, order: 1, visible: true },
{ fieldName: 'name', displayName: '设备名称', fieldType: 'string', required: true, order: 2, visible: true },
{ fieldName: 'type', displayName: '设备类型', fieldType: 'select', required: true, order: 3, visible: true,
options: [{ value: 'server', label: '服务器' }, { value: 'switch', label: '交换机' }, { value: 'router', label: '路由器' }, { value: 'storage', label: '存储设备' }, { value: 'other', label: '其他设备' }] },
{ fieldName: 'model', displayName: '型号', fieldType: 'string', required: true, order: 4, visible: true },
{ fieldName: 'serialNumber', displayName: '序列号', fieldType: 'string', required: true, order: 5, visible: true },
{ fieldName: 'rackId', displayName: '所在机柜', fieldType: 'select', required: true, order: 6, visible: true },
{ fieldName: 'position', displayName: '位置(U)', fieldType: 'number', required: true, order: 7, visible: true },
{ fieldName: 'height', displayName: '高度(U)', fieldType: 'number', required: true, order: 8, visible: true },
{ fieldName: 'powerConsumption', displayName: '功率(W)', fieldType: 'number', required: true, order: 9, visible: true },
{ fieldName: 'status', displayName: '状态', fieldType: 'select', required: true, order: 10, visible: true,
options: [{ value: 'running', label: '运行中' }, { value: 'maintenance', label: '维护中' }, { value: 'offline', label: '离线' }, { value: 'fault', label: '故障' }] },
{ fieldName: 'purchaseDate', displayName: '购买日期', fieldType: 'date', required: true, order: 11, visible: true },
{ fieldName: 'warrantyExpiry', displayName: '保修到期', fieldType: 'date', required: true, order: 12, visible: true },
{ fieldName: 'ipAddress', displayName: 'IP地址', fieldType: 'string', required: false, order: 13, visible: true },
{ fieldName: 'description', displayName: '描述', fieldType: 'textarea', required: false, order: 14, visible: true }
];
// 可调整列宽的表头组件
const ResizeableTitle = (props) => {
const { onResize, width, ...restProps } = props;
@@ -18,13 +91,12 @@ const ResizeableTitle = (props) => {
const handleMouseDown = (e) => {
if (!onResize) return;
const startX = e.pageX;
const startWidth = width;
const handleMouseMove = (moveEvent) => {
const diff = moveEvent.pageX - startX;
const newWidth = Math.max(50, startWidth + diff); // 最小宽度50px
const newWidth = Math.max(50, startWidth + diff);
onResize(newWidth);
};
@@ -855,7 +927,8 @@ function DeviceManagement() {
loading={loading}
pagination={pagination}
onChange={handleTableChange}
scroll={{ x: 'max-content' }}
scroll={{ y: 600, x: 'max-content' }}
virtual
rowSelection={{
selectedRowKeys: selectedDevices,
onChange: setSelectedDevices,
+29 -34
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Table, Button, Modal, Form, Input, Select, message, Card, Space, InputNumber, Upload, Progress } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, UploadOutlined, DownloadOutlined } from '@ant-design/icons';
import axios from 'axios';
@@ -6,6 +6,13 @@ import * as XLSX from 'xlsx';
const { Option } = Select;
// 状态映射函数
const statusMap = {
active: { text: '在用', color: 'green' },
maintenance: { text: '维护中', color: 'orange' },
inactive: { text: '停用', color: 'gray' }
};
function RackManagement() {
const [racks, setRacks] = useState([]);
const [rooms, setRooms] = useState([]);
@@ -28,10 +35,7 @@ function RackManagement() {
const [isImporting, setIsImporting] = useState(false);
const [importResult, setImportResult] = useState(null);
// 获取所有机柜
const fetchRacks = async (page = 1, pageSize = 10) => {
const fetchRacks = useCallback(async (page = 1, pageSize = 10) => {
try {
setLoading(true);
const response = await axios.get('/api/racks', {
@@ -51,10 +55,9 @@ function RackManagement() {
} finally {
setLoading(false);
}
};
}, []);
// 获取所有机房
const fetchRooms = async () => {
const fetchRooms = useCallback(async () => {
try {
const response = await axios.get('/api/rooms');
setRooms(response.data);
@@ -62,20 +65,19 @@ function RackManagement() {
message.error('获取机房列表失败');
console.error('获取机房列表失败:', error);
}
};
}, []);
// 处理表格分页变化
const handleTableChange = (pagination) => {
const handleTableChange = useCallback((pagination) => {
fetchRacks(pagination.current, pagination.pageSize);
};
}, [fetchRacks]);
useEffect(() => {
fetchRacks(pagination.current, pagination.pageSize);
fetchRooms();
}, []);
}, [fetchRacks, fetchRooms]);
// 打开模态框
const showModal = (rack = null) => {
const showModal = useCallback((rack = null) => {
setEditingRack(rack);
if (rack) {
form.setFieldsValue(rack);
@@ -83,16 +85,16 @@ function RackManagement() {
form.resetFields();
}
setModalVisible(true);
};
}, []);
// 关闭模态框
const handleCancel = () => {
const handleCancel = useCallback(() => {
setModalVisible(false);
setEditingRack(null);
};
}, []);
// 提交表单
const handleSubmit = async (values) => {
const handleSubmit = useCallback(async (values) => {
try {
if (editingRack) {
// 更新机柜
@@ -111,10 +113,10 @@ function RackManagement() {
message.error(editingRack ? '机柜更新失败' : '机柜创建失败');
console.error(editingRack ? '机柜更新失败:' : '机柜创建失败:', error);
}
};
}, [editingRack, fetchRacks]);
// 删除机柜
const handleDelete = async (rackId) => {
const handleDelete = useCallback(async (rackId) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除这个机柜吗?',
@@ -132,17 +134,17 @@ function RackManagement() {
}
}
});
};
}, [fetchRacks]);
// 下载导入模板
const handleDownloadTemplate = () => {
const handleDownloadTemplate = useCallback(() => {
// 调用后端API下载模板
window.open('/api/racks/import-template', '_blank');
message.success('模板下载成功');
};
}, []);
// 导入机柜数据
const handleImport = async (file) => {
const handleImport = useCallback(async (file) => {
try {
setIsImporting(true);
setImportProgress(0);
@@ -213,17 +215,10 @@ function RackManagement() {
// 阻止自动上传
return false;
}
};
// 状态标签映射
const statusMap = {
active: { text: '在用', color: 'green' },
maintenance: { text: '维护中', color: 'orange' },
inactive: { text: '停用', color: 'gray' }
};
}, []);
// 表格列配置
const columns = [
const columns = useMemo(() => [
{
title: '机柜ID',
dataIndex: 'rackId',
@@ -493,4 +488,4 @@ function RackManagement() {
);
}
export default RackManagement;
export default React.memo(RackManagement);
+237 -223
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { Card, Select, Button, Space, message, Tooltip, Modal, Form, Switch, Checkbox } from 'antd';
import {
ReloadOutlined,
@@ -18,101 +18,226 @@ import axios from 'axios';
const { Option } = Select;
// 添加动画样式
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-50%) translateX(20px) scale(0.95);
// 工具函数提取到组件外部,避免每次渲染重复创建
const getDeviceIcon = (deviceType) => {
try {
if (!deviceType) return <CloudServerOutlined style={{ color: '#ffffff' }} />;
const type = deviceType.toLowerCase();
if (type.includes('server') || type.includes('服务器')) return <CloudServerOutlined style={{ color: '#ffffff' }} />;
if (type.includes('switch') || type.includes('交换机')) return <SwitcherOutlined style={{ color: '#ffffff' }} />;
if (type.includes('storage') || type.includes('存储')) return <DatabaseOutlined style={{ color: '#ffffff' }} />;
if (type.includes('router') || type.includes('路由器')) return <CloudOutlined style={{ color: '#ffffff' }} />;
if (type.includes('laptop') || type.includes('笔记本')) return <LaptopOutlined style={{ color: '#ffffff' }} />;
if (type.includes('mobile') || type.includes('手机')) return <MobileOutlined style={{ color: '#ffffff' }} />;
if (type.includes('printer') || type.includes('打印机')) return <PrinterOutlined style={{ color: '#ffffff' }} />;
return <CloudServerOutlined style={{ color: '#ffffff' }} />;
} catch (error) {
console.error('设备图标渲染错误:', error);
return <CloudServerOutlined style={{ color: '#ffffff' }} />;
}
};
const getDeviceColor = (deviceType) => {
if (!deviceType) return '#1890ff';
const type = deviceType.toLowerCase();
if (type.includes('server') || type.includes('服务器')) return '#1890ff';
if (type.includes('switch') || type.includes('交换机')) return '#52c41a';
if (type.includes('storage') || type.includes('存储')) return '#faad14';
if (type.includes('router') || type.includes('路由器')) return '#f5222d';
if (type.includes('laptop') || type.includes('笔记本')) return '#722ed1';
if (type.includes('mobile') || type.includes('手机')) return '#eb2f96';
if (type.includes('printer') || type.includes('打印机')) return '#13c2c2';
return '#1890ff';
};
const getDeviceStatusColor = (status) => {
const statusColorMap = {
'normal': '#10b981',
'warning': '#f59e0b',
'error': '#ef4444',
'offline': '#6b7280',
'maintenance': '#3b82f6',
undefined: '#3b82f6',
null: '#3b82f6'
};
return statusColorMap[status] || '#3b82f6';
};
const getDeviceTypeTheme = (type) => {
const themeMap = {
'server': {
borderColor: '#38bdf8',
accentColor: '#0ea5e9',
glowColor: 'rgba(56, 189, 248, 0.3)',
iconColor: '#38bdf8',
label: '服务器'
},
'switch': {
borderColor: '#22c55e',
accentColor: '#16a34a',
glowColor: 'rgba(34, 197, 94, 0.3)',
iconColor: '#22c55e',
label: '交换机'
},
'router': {
borderColor: '#f59e0b',
accentColor: '#d97706',
glowColor: 'rgba(245, 158, 11, 0.3)',
iconColor: '#f59e0b',
label: '路由器'
},
'storage': {
borderColor: '#8b5cf6',
accentColor: '#7c3aed',
glowColor: 'rgba(139, 92, 246, 0.3)',
iconColor: '#8b5cf6',
label: '存储'
},
'firewall': {
borderColor: '#ef4444',
accentColor: '#dc2626',
glowColor: 'rgba(239, 68, 68, 0.3)',
iconColor: '#ef4444',
label: '防火墙'
},
'ups': {
borderColor: '#14b8a6',
accentColor: '#0d9488',
glowColor: 'rgba(20, 184, 166, 0.3)',
iconColor: '#14b8a6',
label: 'UPS'
},
'pdus': {
borderColor: '#64748b',
accentColor: '#475569',
glowColor: 'rgba(100, 116, 139, 0.3)',
iconColor: '#64748b',
label: 'PDU'
},
'other': {
borderColor: '#94a3b8',
accentColor: '#64748b',
glowColor: 'rgba(148, 163, 184, 0.3)',
iconColor: '#94a3b8',
label: '其他设备'
}
to {
opacity: 1;
transform: translateY(-50%) translateX(0) scale(1);
};
const normalizedType = type?.toLowerCase();
if (normalizedType?.includes('server') || normalizedType?.includes('服务器')) return themeMap.server;
if (normalizedType?.includes('switch') || normalizedType?.includes('交换机')) return themeMap.switch;
if (normalizedType?.includes('router') || normalizedType?.includes('路由器')) return themeMap.router;
if (normalizedType?.includes('storage') || normalizedType?.includes('存储')) return themeMap.storage;
if (normalizedType?.includes('firewall') || normalizedType?.includes('防火墙')) return themeMap.firewall;
if (normalizedType?.includes('ups') || normalizedType?.includes('不间断电源')) return themeMap.ups;
if (normalizedType?.includes('pdu') || normalizedType?.includes('电源分配')) return themeMap.pdus;
return themeMap.other;
};
// 初始化动画样式
const initAnimationStyles = () => {
const existingStyle = document.getElementById('rack-visualization-styles');
if (existingStyle) {
return existingStyle;
}
const style = document.createElement('style');
style.id = 'rack-visualization-styles';
style.textContent = `
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-50%) translateX(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(-50%) translateX(0) scale(1);
}
}
}
/* LED指示灯闪烁动画 */
@keyframes ledBlink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0.3; }
}
/* Tooltip淡入动画 */
@keyframes tooltipFadeIn {
from {
opacity: 0;
transform: translateY(-50%) translateX(-10px) scale(0.95);
@keyframes ledBlink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0.3; }
}
to {
opacity: 1;
transform: translateY(-50%) translateX(0) scale(1);
@keyframes tooltipFadeIn {
from {
opacity: 0;
transform: translateY(-50%) translateX(-10px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(-50%) translateX(0) scale(1);
}
}
}
/* 金属拉丝纹理 */
.metal-texture {
background-image:
linear-gradient(90deg,
transparent 0%,
rgba(255,255,255,0.03) 50%,
transparent 100%),
repeating-linear-gradient(0deg,
transparent 0px,
rgba(255,255,255,0.02) 1px,
transparent 2px,
transparent 3px);
background-size: 100% 100%, 4px 4px;
}
/* 散热格栅效果 */
.ventilation-grille {
background-image: repeating-linear-gradient(
0deg,
#334155 0px,
#334155 1px,
transparent 1px,
transparent 2px
);
}
/* 悬停提亮效果 */
.device-hover {
background: linear-gradient(145deg, #1e293b, #0f172a) !important;
box-shadow: 0 6px 16px rgba(56, 189, 248, 0.3), 0 0 12px rgba(56, 189, 248, 0.2) !important;
border-color: #38bdf8 !important;
}
/* Tooltip样式 */
.device-tooltip {
background: rgba(0, 0, 0, 0.9);
color: #5eead4;
padding: 8px 12px;
border-radius: 4px;
font-family: 'Roboto Mono', monospace;
font-size: 11px;
border: 1px solid rgba(94, 234, 212, 0.3);
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
white-space: nowrap;
}
/* 设备数量badge */
.device-count-badge {
background: linear-gradient(135deg, rgba(56, 189, 248, 0.2), rgba(14, 165, 233, 0.2));
color: #38bdf8;
padding: 6px 14px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
box-shadow: 0 4px 15px rgba(56, 189, 248, 0.2);
border: 1px solid rgba(56, 189, 248, 0.3);
backdrop-filter: blur(10px);
transition: all 0.3s ease;
white-space: nowrap;
font-family: 'JetBrains Mono', 'Roboto Mono', monospace;
}
`;
document.head.appendChild(style);
.metal-texture {
background-image:
linear-gradient(90deg,
transparent 0%,
rgba(255,255,255,0.03) 50%,
transparent 100%),
repeating-linear-gradient(0deg,
transparent 0px,
rgba(255,255,255,0.02) 1px,
transparent 2px,
transparent 3px);
background-size: 100% 100%, 4px 4px;
}
.ventilation-grille {
background-image: repeating-linear-gradient(
0deg,
#334155 0px,
#334155 1px,
transparent 1px,
transparent 2px
);
}
.device-hover {
background: linear-gradient(145deg, #1e293b, #0f172a) !important;
box-shadow: 0 6px 16px rgba(56, 189, 248, 0.3), 0 0 12px rgba(56, 189, 248, 0.2) !important;
border-color: #38bdf8 !important;
}
.device-tooltip {
background: rgba(0, 0, 0, 0.9);
color: #5eead4;
padding: 8px 12px;
border-radius: 4px;
font-family: 'Roboto Mono', monospace;
font-size: 11px;
border: 1px solid rgba(94, 234, 212, 0.3);
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
white-space: nowrap;
}
.device-count-badge {
background: linear-gradient(135deg, rgba(56, 189, 248, 0.2), rgba(14, 165, 233, 0.2));
color: #38bdf8;
padding: 6px 14px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
box-shadow: 0 4px 15px rgba(56, 189, 248, 0.2);
border: 1px solid rgba(56, 189, 248, 0.3);
backdrop-filter: blur(10px);
transition: all 0.3s ease;
white-space: nowrap;
font-family: 'JetBrains Mono', 'Roboto Mono', monospace;
}
`;
document.head.appendChild(style);
return style;
};
// 错误边界组件
class ErrorBoundary extends React.Component {
constructor(props) {
@@ -191,7 +316,7 @@ function RackVisualization() {
const [tooltipFields, setTooltipFields] = useState({});
// 默认设备字段配置
const defaultTooltipFields = {
const defaultTooltipFields = useMemo(() => ({
name: { label: '设备名称', enabled: true, field: 'name', fieldType: 'string' },
deviceId: { label: '设备ID', enabled: true, field: 'deviceId', fieldType: 'string' },
type: { label: '设备类型', enabled: true, field: 'type', fieldType: 'string' },
@@ -202,10 +327,15 @@ function RackVisualization() {
height: { label: '高度', enabled: true, field: 'height', fieldType: 'number' },
ipAddress: { label: 'IP地址', enabled: true, field: 'ipAddress', fieldType: 'string' },
power: { label: '功率', enabled: true, field: 'power', fieldType: 'number' }
};
}), []);
// 获取设备字段配置
const fetchTooltipDeviceFields = async () => {
// 初始化样式
useEffect(() => {
initAnimationStyles();
}, []);
// 获取设备字段配置 - 使用 useCallback 避免重复创建
const fetchTooltipDeviceFields = useCallback(async () => {
try {
setLoadingTooltipFields(true);
console.log('开始获取设备字段配置...');
@@ -242,10 +372,10 @@ function RackVisualization() {
} finally {
setLoadingTooltipFields(false);
}
};
}, [defaultTooltipFields]);
// 保存tooltip字段配置
const saveTooltipConfig = async () => {
const saveTooltipConfig = useCallback(async () => {
try {
setSavingTooltipConfig(true);
@@ -268,10 +398,10 @@ function RackVisualization() {
} finally {
setSavingTooltipConfig(false);
}
};
}, [tooltipFields, fetchTooltipDeviceFields]);
// 获取所有机柜
const fetchRacks = async () => {
// 获取所有机柜 - 使用 useCallback 避免重复创建
const fetchRacks = useCallback(async () => {
try {
setLoading(true);
setError(null);
@@ -317,10 +447,10 @@ function RackVisualization() {
} finally {
setLoading(false);
}
};
}, []);
// 获取机柜内的设备
const fetchDevices = async (rackId) => {
// 获取机柜内的设备 - 使用 useCallback 避免重复创建
const fetchDevices = useCallback(async (rackId) => {
try {
setLoadingDevices(true);
console.log(`=== 开始获取机柜 ${rackId} 的设备数据 ===`);
@@ -462,13 +592,11 @@ function RackVisualization() {
} finally {
setLoadingDevices(false);
}
};
}, []);
useEffect(() => {
fetchRacks();
loadBackgroundSettings();
fetchTooltipDeviceFields();
}, []);
}, [fetchRacks]);
// 打开字段配置模态框时获取数据
const handleOpenTooltipConfig = () => {
@@ -478,120 +606,6 @@ function RackVisualization() {
setShowTooltipConfig(true);
};
// 根据设备类型获取图标
const getDeviceIcon = (deviceType) => {
try {
if (!deviceType) return <CloudServerOutlined style={{ color: '#ffffff' }} />;
const type = deviceType.toLowerCase();
if (type.includes('server') || type.includes('服务器')) return <CloudServerOutlined style={{ color: '#ffffff' }} />;
if (type.includes('switch') || type.includes('交换机')) return <SwitcherOutlined style={{ color: '#ffffff' }} />;
if (type.includes('storage') || type.includes('存储')) return <DatabaseOutlined style={{ color: '#ffffff' }} />;
if (type.includes('router') || type.includes('路由器')) return <CloudOutlined style={{ color: '#ffffff' }} />;
if (type.includes('laptop') || type.includes('笔记本')) return <LaptopOutlined style={{ color: '#ffffff' }} />;
if (type.includes('mobile') || type.includes('手机')) return <MobileOutlined style={{ color: '#ffffff' }} />;
if (type.includes('printer') || type.includes('打印机')) return <PrinterOutlined style={{ color: '#ffffff' }} />;
return <CloudServerOutlined style={{ color: '#ffffff' }} />;
} catch (error) {
console.error('设备图标渲染错误:', error);
return <CloudServerOutlined style={{ color: '#ffffff' }} />;
}
};
// 根据设备类型获取背景色
const getDeviceColor = (deviceType) => {
if (!deviceType) return '#1890ff';
const type = deviceType.toLowerCase();
if (type.includes('server') || type.includes('服务器')) return '#1890ff'; // 蓝色
if (type.includes('switch') || type.includes('交换机')) return '#52c41a'; // 绿色
if (type.includes('storage') || type.includes('存储')) return '#faad14'; // 黄色
if (type.includes('router') || type.includes('路由器')) return '#f5222d'; // 红色
if (type.includes('laptop') || type.includes('笔记本')) return '#722ed1'; // 紫色
if (type.includes('mobile') || type.includes('手机')) return '#eb2f96'; // 粉色
if (type.includes('printer') || type.includes('打印机')) return '#13c2c2'; // 青色
return '#1890ff'; // 默认蓝色
};
// 获取设备状态颜色
const getDeviceStatusColor = (status) => {
const statusColorMap = {
'normal': '#10b981', // 正常 - 绿色常亮
'warning': '#f59e0b', // 预警 - 黄色常亮
'error': '#ef4444', // 告警 - 红色慢闪
'offline': '#6b7280', // 离线 - 灰色
'maintenance': '#3b82f6', // 维护 - 蓝色常亮
undefined: '#3b82f6', // 默认普通设备 - 蓝色常亮
null: '#3b82f6'
};
return statusColorMap[status] || '#3b82f6';
};
// 获取设备类型对应的颜色主题
const getDeviceTypeTheme = (type) => {
const themeMap = {
'server': {
borderColor: '#38bdf8',
accentColor: '#0ea5e9',
glowColor: 'rgba(56, 189, 248, 0.3)',
iconColor: '#38bdf8',
label: '服务器'
},
'switch': {
borderColor: '#22c55e',
accentColor: '#16a34a',
glowColor: 'rgba(34, 197, 94, 0.3)',
iconColor: '#22c55e',
label: '交换机'
},
'router': {
borderColor: '#f59e0b',
accentColor: '#d97706',
glowColor: 'rgba(245, 158, 11, 0.3)',
iconColor: '#f59e0b',
label: '路由器'
},
'storage': {
borderColor: '#8b5cf6',
accentColor: '#7c3aed',
glowColor: 'rgba(139, 92, 246, 0.3)',
iconColor: '#8b5cf6',
label: '存储'
},
'firewall': {
borderColor: '#ef4444',
accentColor: '#dc2626',
glowColor: 'rgba(239, 68, 68, 0.3)',
iconColor: '#ef4444',
label: '防火墙'
},
'ups': {
borderColor: '#14b8a6',
accentColor: '#0d9488',
glowColor: 'rgba(20, 184, 166, 0.3)',
iconColor: '#14b8a6',
label: 'UPS'
},
'pdus': {
borderColor: '#64748b',
accentColor: '#475569',
glowColor: 'rgba(100, 116, 139, 0.3)',
iconColor: '#64748b',
label: 'PDU'
},
'other': {
borderColor: '#94a3b8',
accentColor: '#64748b',
glowColor: 'rgba(148, 163, 184, 0.3)',
iconColor: '#94a3b8',
label: '其他'
}
};
return themeMap[type?.toLowerCase()] || themeMap['other'];
};
// 生成模拟监控数据
const generateMonitoringData = (device) => {
const baseTemp = device.type?.includes('服务器') ? 65 :
@@ -1987,4 +2001,4 @@ function RackVisualization() {
);
}
export default RackVisualization;
export default React.memo(RackVisualization);
+73 -73
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Table, Button, Modal, Form, Input, Select, DatePicker, message, Card, Space, Tag, Dropdown, Menu, Tabs, Timeline, Descriptions } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, EyeOutlined, MoreOutlined, UserOutlined, ToolOutlined, CheckCircleOutlined, SyncOutlined, ClockCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
import axios from 'axios';
@@ -9,6 +9,46 @@ const { RangePicker } = DatePicker;
const { TextArea } = Input;
const { TabPane } = Tabs;
const getStatusColor = (status) => {
const colors = {
pending: 'orange',
in_progress: 'processing',
completed: 'green',
closed: 'default'
};
return colors[status] || 'default';
};
const getStatusText = (status) => {
const texts = {
pending: '待处理',
in_progress: '处理中',
completed: '已完成',
closed: '已关闭'
};
return texts[status] || status;
};
const getPriorityColor = (priority) => {
const colors = {
low: 'green',
medium: 'orange',
high: 'red',
urgent: 'magenta'
};
return colors[priority] || 'default';
};
const getPriorityText = (priority) => {
const texts = {
low: '低',
medium: '中',
high: '高',
urgent: '紧急'
};
return texts[priority] || priority;
};
function TicketManagement() {
const [tickets, setTickets] = useState([]);
const [devices, setDevices] = useState([]);
@@ -35,7 +75,7 @@ function TicketManagement() {
const [searchFilters, setSearchFilters] = useState({});
const fetchTickets = async (page = 1, pageSize = 10, filters = {}) => {
const fetchTickets = useCallback(async (page = 1, pageSize = 10, filters = {}) => {
try {
setLoading(true);
const params = {
@@ -56,27 +96,27 @@ function TicketManagement() {
} finally {
setLoading(false);
}
};
}, [searchFilters]);
const fetchDevices = async () => {
const fetchDevices = useCallback(async () => {
try {
const response = await axios.get('/api/devices', { params: { pageSize: 1000 } });
setDevices(response.data.devices || []);
} catch (error) {
console.error('获取设备列表失败:', error);
}
};
}, []);
const fetchCategories = async () => {
const fetchCategories = useCallback(async () => {
try {
const response = await axios.get('/api/ticket-categories');
setCategories(response.data || []);
} catch (error) {
console.error('获取分类列表失败:', error);
}
};
}, []);
const fetchTicketDetail = async (ticketId) => {
const fetchTicketDetail = useCallback(async (ticketId) => {
try {
const [ticketRes, operationsRes] = await Promise.all([
axios.get(`/api/tickets/${ticketId}`),
@@ -90,15 +130,15 @@ function TicketManagement() {
message.error('获取工单详情失败');
console.error('获取工单详情失败:', error);
}
};
}, []);
useEffect(() => {
fetchTickets();
fetchDevices();
fetchCategories();
}, []);
}, [fetchTickets, fetchDevices, fetchCategories]);
const showModal = (ticket = null) => {
const showModal = useCallback((ticket = null) => {
setEditingTicket(ticket);
if (ticket) {
const ticketData = { ...ticket };
@@ -113,14 +153,14 @@ function TicketManagement() {
form.resetFields();
}
setModalVisible(true);
};
}, []);
const handleCancel = () => {
const handleCancel = useCallback(() => {
setModalVisible(false);
setEditingTicket(null);
};
}, []);
const handleSubmit = async (values) => {
const handleSubmit = useCallback(async (values) => {
try {
const ticketData = {
...values,
@@ -146,9 +186,9 @@ function TicketManagement() {
message.error(editingTicket ? '工单更新失败' : '工单创建失败');
console.error(error);
}
};
}, [editingTicket, fetchTickets]);
const handleDelete = async (ticketId) => {
const handleDelete = useCallback(async (ticketId) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除这个工单吗?',
@@ -166,15 +206,15 @@ function TicketManagement() {
}
}
});
};
}, [fetchTickets]);
const handleProcess = (ticket) => {
const handleProcess = useCallback((ticket) => {
setSelectedTicket(ticket);
processForm.resetFields();
setProcessingModalVisible(true);
};
}, []);
const handleProcessSubmit = async (values) => {
const handleProcessSubmit = useCallback(async (values) => {
try {
await axios.put(`/api/tickets/${selectedTicket.ticketId}/process`, {
...values,
@@ -188,9 +228,9 @@ function TicketManagement() {
message.error('处理失败');
console.error(error);
}
};
}, [selectedTicket, fetchTickets]);
const handleStatusChange = async (ticketId, newStatus) => {
const handleStatusChange = useCallback(async (ticketId, newStatus) => {
try {
await axios.put(`/api/tickets/${ticketId}/status`, {
status: newStatus,
@@ -203,65 +243,25 @@ function TicketManagement() {
message.error('状态更新失败');
console.error(error);
}
};
}, [fetchTickets]);
const handleSearch = (values) => {
const handleSearch = useCallback((values) => {
setSearchFilters(values);
fetchTickets(1, pagination.pageSize, values);
};
}, [fetchTickets, pagination.pageSize]);
const handleReset = () => {
const handleReset = useCallback(() => {
searchForm.resetFields();
setSearchFilters({});
fetchTickets(1, pagination.pageSize, {});
};
}, [fetchTickets, pagination.pageSize]);
const handleTableChange = (paginationInfo) => {
const handleTableChange = useCallback((paginationInfo) => {
setPagination(paginationInfo);
fetchTickets(paginationInfo.current, paginationInfo.pageSize, searchFilters);
};
}, [fetchTickets, searchFilters]);
const getStatusColor = (status) => {
const colors = {
pending: 'orange',
in_progress: 'processing',
completed: 'green',
closed: 'default'
};
return colors[status] || 'default';
};
const getStatusText = (status) => {
const texts = {
pending: '待处理',
in_progress: '处理中',
completed: '已完成',
closed: '已关闭'
};
return texts[status] || status;
};
const getPriorityColor = (priority) => {
const colors = {
low: 'green',
medium: 'orange',
high: 'red',
urgent: 'magenta'
};
return colors[priority] || 'default';
};
const getPriorityText = (priority) => {
const texts = {
low: '低',
medium: '中',
high: '高',
urgent: '紧急'
};
return texts[priority] || priority;
};
const columns = [
const columns = useMemo(() => [
{
title: '工单编号',
dataIndex: 'ticketId',
@@ -404,7 +404,7 @@ function TicketManagement() {
</Space>
)
}
];
], [fetchTicketDetail, handleStatusChange, handleDelete]);
return (
<div style={{ padding: 24 }}>
@@ -651,4 +651,4 @@ function TicketManagement() {
);
}
export default TicketManagement;
export default React.memo(TicketManagement);
+84 -42
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
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 axios from 'axios';
@@ -7,6 +7,48 @@ import dayjs from 'dayjs';
const { RangePicker } = DatePicker;
const { Option } = Select;
const getStatusColor = (status) => {
const colors = {
pending: 'orange',
assigned: 'blue',
in_progress: 'processing',
completed: 'green',
closed: 'default'
};
return colors[status] || 'default';
};
const getStatusText = (status) => {
const texts = {
pending: '待处理',
assigned: '已分配',
in_progress: '处理中',
completed: '已完成',
closed: '已关闭'
};
return texts[status] || status;
};
const getPriorityColor = (priority) => {
const colors = {
low: 'green',
medium: 'orange',
high: 'red',
urgent: 'magenta'
};
return colors[priority] || 'default';
};
const getPriorityText = (priority) => {
const texts = {
low: '低',
medium: '中',
high: '高',
urgent: '紧急'
};
return texts[priority] || priority;
};
function TicketStatistics() {
const [loading, setLoading] = useState(true);
const [dateRange, setDateRange] = useState([
@@ -27,7 +69,7 @@ function TicketStatistics() {
trend: []
});
const fetchStatistics = async () => {
const fetchStatistics = useCallback(async () => {
try {
setLoading(true);
const params = {
@@ -43,17 +85,17 @@ function TicketStatistics() {
} finally {
setLoading(false);
}
};
}, [dateRange]);
useEffect(() => {
fetchStatistics();
}, [dateRange]);
}, [fetchStatistics]);
const handleDateChange = (dates) => {
const handleDateChange = useCallback((dates) => {
if (dates) {
setDateRange(dates);
}
};
}, []);
const getStatusColor = (status) => {
const colors = {
@@ -97,7 +139,7 @@ function TicketStatistics() {
return texts[priority] || priority;
};
const statusColumns = [
const statusColumns = useMemo(() => [
{
title: '状态',
dataIndex: 'status',
@@ -127,9 +169,9 @@ function TicketStatistics() {
</span>
)
}
];
], []);
const categoryColumns = [
const categoryColumns = useMemo(() => [
{
title: '故障分类',
dataIndex: 'category',
@@ -164,38 +206,9 @@ function TicketStatistics() {
width: 150,
render: (time) => time !== undefined && time !== null ? time.toFixed(1) : '-'
}
];
], []);
const deviceColumns = [
{
title: '设备名称',
dataIndex: 'deviceName',
key: 'deviceName',
width: 180
},
{
title: '故障次数',
dataIndex: 'count',
key: 'count',
width: 100,
render: (count) => <Tag color="red">{count}</Tag>
},
{
title: '最后故障时间',
dataIndex: 'lastFaultTime',
key: 'lastFaultTime',
width: 160,
render: (text) => text ? dayjs(text).format('YYYY-MM-DD HH:mm') : '-'
},
{
title: '设备类型',
dataIndex: 'deviceType',
key: 'deviceType',
width: 100
}
];
const priorityColumns = [
const priorityColumns = useMemo(() => [
{
title: '优先级',
dataIndex: 'priority',
@@ -228,7 +241,36 @@ function TicketStatistics() {
width: 150,
render: (time) => time !== undefined && time !== null ? time.toFixed(1) : '-'
}
];
], []);
const deviceColumns = useMemo(() => [
{
title: '设备名称',
dataIndex: 'deviceName',
key: 'deviceName',
width: 180
},
{
title: '故障次数',
dataIndex: 'count',
key: 'count',
width: 100,
render: (count) => <Tag color="red">{count}</Tag>
},
{
title: '最后故障时间',
dataIndex: 'lastFaultTime',
key: 'lastFaultTime',
width: 160,
render: (text) => text ? dayjs(text).format('YYYY-MM-DD HH:mm') : '-'
},
{
title: '设备类型',
dataIndex: 'deviceType',
key: 'deviceType',
width: 100
}
], []);
const simpleBarData = [
{ name: '待处理', value: statistics.pending },
@@ -454,4 +496,4 @@ function TicketStatistics() {
);
}
export default TicketStatistics;
export default React.memo(TicketStatistics);
+25 -1
View File
@@ -19,6 +19,30 @@ export default defineConfig({
},
build: {
outDir: 'dist',
sourcemap: true
sourcemap: false,
rollupOptions: {
output: {
manualChunks: {
'antd': ['antd', '@ant-design/icons'],
'vendor': ['react', 'react-dom', 'react-router-dom'],
'charts': ['axios', 'dayjs'],
'utils': ['three', 'xlsx']
},
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: '[ext]/[name]-[hash].[ext]',
compact: true
}
},
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
},
optimizeDeps: {
include: ['antd', '@ant-design/icons', 'axios', 'react-router-dom']
}
});