feat: 添加数据库索引优化查询性能

refactor(前端): 使用useMemo和useCallback优化性能
perf(后端): 优化统计查询性能
style: 统一前端样式定义
build: 添加创建索引脚本
This commit is contained in:
zhang1106
2026-01-20 14:31:20 +08:00
parent 88e000a41a
commit a2f0032bad
25 changed files with 803 additions and 701 deletions
+62 -203
View File
@@ -27,6 +27,36 @@ const SystemSettings = lazy(() => import('./pages/SystemSettings'));
const { Header, Content, Sider } = Layout;
const PageLoading = () => (
<div style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
background: '#f5f5f5',
gap: '16px'
}}>
<Spin size="large" />
<span style={{ color: '#8c8c8c', fontSize: '14px' }}>正在加载页面...</span>
</div>
);
const AuthLoading = () => (
<div style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
background: '#f5f5f5',
gap: '16px'
}}>
<Spin size="large" />
<span style={{ color: '#8c8c8c', fontSize: '14px' }}>正在加载认证状态...</span>
</div>
);
const designTokens = {
colors: {
primary: {
@@ -81,50 +111,22 @@ const PrivateRoute = ({ children }) => {
const location = useLocation();
if (!initialized) {
return (
<div style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
background: '#f5f5f5',
gap: '16px'
}}>
<Spin size="large" />
<span style={{ color: '#8c8c8c', fontSize: '14px' }}>正在加载认证状态...</span>
</div>
);
return <AuthLoading />;
}
if (!token) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return (
<Suspense
fallback={
<div style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
background: '#f5f5f5',
gap: '16px'
}}>
<Spin size="large" />
<span style={{ color: '#8c8c8c', fontSize: '14px' }}>正在加载页面...</span>
</div>
}
>
<AppLayout>
{children}
</AppLayout>
</Suspense>
);
return <AppLayout>{children}</AppLayout>;
};
const ProtectedRoute = ({ component: Component }) => (
<PrivateRoute>
<Component />
</PrivateRoute>
);
const AppLayout = ({ children }) => {
const [collapsed, setCollapsed] = useState(false);
const [activeKey, setActiveKey] = useState('dashboard');
@@ -464,173 +466,30 @@ const AppLayout = ({ children }) => {
function App() {
return (
<Router>
<Routes>
<Route path="/login" element={
<Suspense
fallback={
<div style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
background: '#f5f5f5',
gap: '16px'
}}>
<Spin size="large" />
<span style={{ color: '#8c8c8c', fontSize: '14px' }}>正在加载登录页面...</span>
</div>
}
>
<Login />
</Suspense>
} />
<Route
path="/"
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>
<Route
path="/devices"
element={
<PrivateRoute>
<DeviceManagement />
</PrivateRoute>
}
/>
<Route
path="/racks"
element={
<PrivateRoute>
<RackManagement />
</PrivateRoute>
}
/>
<Route
path="/rooms"
element={
<PrivateRoute>
<RoomManagement />
</PrivateRoute>
}
/>
<Route
path="/fields"
element={
<PrivateRoute>
<DeviceFieldManagement />
</PrivateRoute>
}
/>
<Route
path="/visualization"
element={
<PrivateRoute>
<RackVisualization />
</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="/login-history"
element={
<PrivateRoute>
<LoginHistory />
</PrivateRoute>
}
/>
<Route
path="/operation-logs"
element={
<PrivateRoute>
<OperationLogs />
</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="*" element={<Navigate to="/" replace />} />
</Routes>
<Suspense fallback={<PageLoading />}>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/" element={<PrivateRoute><Dashboard /></PrivateRoute>} />
<Route path="/devices" element={<PrivateRoute><DeviceManagement /></PrivateRoute>} />
<Route path="/racks" element={<PrivateRoute><RackManagement /></PrivateRoute>} />
<Route path="/rooms" element={<PrivateRoute><RoomManagement /></PrivateRoute>} />
<Route path="/fields" element={<PrivateRoute><DeviceFieldManagement /></PrivateRoute>} />
<Route path="/visualization" element={<PrivateRoute><RackVisualization /></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="/login-history" element={<PrivateRoute><LoginHistory /></PrivateRoute>} />
<Route path="/operation-logs" element={<PrivateRoute><OperationLogs /></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="*" element={<Navigate to="/" replace />} />
</Routes>
</Suspense>
</Router>
);
}
+119 -63
View File
@@ -134,7 +134,65 @@ const subtitleStyle = {
margin: '0'
};
const statCardStyle = (color) => ({
const progressCardStyle = {
borderRadius: designTokens.borderRadius.large,
border: 'none',
boxShadow: designTokens.shadows.medium,
background: '#fff',
height: '100%',
animation: 'fadeInUp 0.6s ease-out 0.3s backwards'
};
const chartContainerStyle = {
padding: '20px',
borderRadius: designTokens.borderRadius.medium,
background: 'linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%)',
border: '1px solid #f0f0f0'
};
const PIE_CHART_COLORS = {
success: designTokens.colors.success.main,
warning: designTokens.colors.warning.main,
error: designTokens.colors.error.main,
primary: designTokens.colors.primary.main
};
const pieChartStyle = {
width: '180px',
height: '180px',
borderRadius: '50%',
background: `conic-gradient(
${PIE_CHART_COLORS.success} 0deg 216deg,
${PIE_CHART_COLORS.warning} 216deg 288deg,
${PIE_CHART_COLORS.error} 288deg 324deg,
${PIE_CHART_COLORS.primary} 324deg 360deg
)`,
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
};
const pieChartInner = {
width: '120px',
height: '120px',
borderRadius: '50%',
background: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column'
};
const trendItemStyle = {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 0',
borderBottom: '1px solid #f0f0f0'
};
const STAT_CARD_BASE_STYLE = {
borderRadius: designTokens.borderRadius.large,
border: 'none',
boxShadow: designTokens.shadows.medium,
@@ -145,9 +203,9 @@ const statCardStyle = (color) => ({
cursor: 'pointer',
height: '100%',
animation: 'fadeInUp 0.6s ease-out backwards'
});
};
const statIconContainer = (color) => ({
const STAT_ICON_CONTAINER_BASE = {
position: 'absolute',
top: '20px',
right: '20px',
@@ -157,19 +215,68 @@ const statIconContainer = (color) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: `linear-gradient(135deg, ${color}20 0%, ${color}10 100%)`,
fontSize: '32px',
transition: `all ${designTokens.transitions.normal}`
});
};
const topBorderStyle = (color) => ({
const TOP_BORDER_BASE = {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '4px',
background: `linear-gradient(90deg, ${color}, ${color}80)`,
borderRadius: `${designTokens.borderRadius.large} ${designTokens.borderRadius.large} 0 0`
};
const NAV_BUTTON_BASE = {
height: 'auto',
padding: '24px 20px',
borderRadius: designTokens.borderRadius.medium,
border: '2px solid #f0f0f0',
background: '#fff',
transition: `all ${designTokens.transitions.normal}`,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '12px',
cursor: 'pointer',
boxShadow: designTokens.shadows.small
};
const NAV_ICON_CONTAINER_BASE = {
width: '60px',
height: '60px',
borderRadius: designTokens.borderRadius.medium,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '28px',
transition: `all ${designTokens.transitions.normal}`
};
const createStatCardStyle = (color) => ({
...STAT_CARD_BASE_STYLE,
borderTop: `4px solid ${color}`
});
const createStatIconContainer = (color) => ({
...STAT_ICON_CONTAINER_BASE,
background: `linear-gradient(135deg, ${color}20 0%, ${color}10 100%)`
});
const createTopBorderStyle = (color) => ({
...TOP_BORDER_BASE,
background: `linear-gradient(90deg, ${color}, ${color}80)`
});
const createNavButtonStyle = (color) => ({
...NAV_BUTTON_BASE,
borderColor: color
});
const createNavIconContainer = (color) => ({
...NAV_ICON_CONTAINER_BASE,
background: `linear-gradient(135deg, ${color}20 0%, ${color}10 100%)`
});
const overviewCardStyle = {
@@ -245,57 +352,6 @@ const quickStatItemStyle = {
boxShadow: designTokens.shadows.small
};
const progressCardStyle = {
borderRadius: designTokens.borderRadius.large,
border: 'none',
boxShadow: designTokens.shadows.medium,
background: '#fff',
height: '100%',
animation: 'fadeInUp 0.6s ease-out 0.3s backwards'
};
const chartContainerStyle = {
padding: '20px',
borderRadius: designTokens.borderRadius.medium,
background: 'linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%)',
border: '1px solid #f0f0f0'
};
const pieChartStyle = {
width: '180px',
height: '180px',
borderRadius: '50%',
background: `conic-gradient(
${designTokens.colors.success.main} 0deg 216deg,
${designTokens.colors.warning.main} 216deg 288deg,
${designTokens.colors.error.main} 288deg 324deg,
${designTokens.colors.primary.main} 324deg 360deg
)`,
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
};
const pieChartInner = {
width: '120px',
height: '120px',
borderRadius: '50%',
background: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column'
};
const trendItemStyle = {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 0',
borderBottom: '1px solid #f0f0f0'
};
const AnimatedCounter = ({ value, duration = 1500 }) => {
const [displayValue, setDisplayValue] = useState(0);
const animationRef = useRef(null);
@@ -680,7 +736,7 @@ function Dashboard() {
const { icon: Icon, color, statKey, title, trend, tagColor, customStatus, xs, sm, lg, xl, delay } = config;
const colProps = { xs, sm, lg, xl };
const cardStyle = {
...statCardStyle(color),
...createStatCardStyle(color),
...(hoveredCard === statKey ? { transform: 'translateY(-6px)', boxShadow: designTokens.shadows.hover } : {}),
animationDelay: `${delay * 0.1}s`
};
@@ -693,8 +749,8 @@ function Dashboard() {
onMouseLeave={() => setHoveredCard(null)}
>
<div style={{ position: 'relative' }}>
<div style={topBorderStyle(color)} />
<div style={statIconContainer(color)}>
<div style={createTopBorderStyle(color)} />
<div style={createStatIconContainer(color)}>
<Icon style={{ color }} />
</div>
<div style={{
@@ -770,7 +826,7 @@ function Dashboard() {
<div
key={key}
style={{
...navButtonStyle(color),
...createNavButtonStyle(color),
...(hoveredCard === `nav-${key}` ? {
transform: 'translateY(-4px)',
boxShadow: designTokens.shadows.large,
@@ -781,7 +837,7 @@ function Dashboard() {
onMouseEnter={(e) => handleNavHover(e, true, `nav-${key}`)}
onMouseLeave={(e) => handleNavHover(e, false, `nav-${key}`)}
>
<div style={navIconContainer(color)}>
<div style={createNavIconContainer(color)}>
<Icon style={{ color, fontSize: '28px' }} />
</div>
<span style={navTextStyle}>{text}</span>
+118 -104
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import { Table, Button, Modal, Form, Input, Select, message, Card, Space, InputNumber, Switch, Tag, Statistic } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, AppstoreOutlined, FontSizeOutlined, NumberOutlined, CheckCircleOutlined, CalendarOutlined, FileTextOutlined } from '@ant-design/icons';
import axios from 'axios';
@@ -126,6 +126,92 @@ const tableStyle = {
background: designTokens.colors.background.primary
};
const titleIconStyle = {
color: designTokens.colors.primary.main
};
const modalTitleStyle = {
fontWeight: '600'
};
const formLabelStyle = {
fontWeight: '500'
};
const tableCellStyle = {
fontWeight: '500',
color: designTokens.colors.text.primary
};
const typeTagStyle = {
border: 'none',
borderRadius: designTokens.borderRadius.small,
fontWeight: '500'
};
const orderBadgeStyle = {
background: designTokens.colors.background.tertiary,
padding: '2px 8px',
borderRadius: designTokens.borderRadius.small,
fontSize: '12px',
fontWeight: '500'
};
const editButtonStyle = {
color: designTokens.colors.primary.main,
height: '28px',
padding: '0 8px'
};
const deleteButtonStyle = {
height: '28px',
padding: '0 8px'
};
const formRowStyle = {
display: 'flex',
gap: designTokens.spacing.md
};
const formItemFlexStyle = {
flex: 1
};
const textAreaStyle = {
fontFamily: 'monospace'
};
const modalBodyStyle = {
padding: designTokens.spacing.lg
};
const formActionsStyle = {
marginBottom: 0,
textAlign: 'right'
};
const modalStyle = {
borderRadius: designTokens.borderRadius.large
};
const FIELD_TYPE_MAP = {
string: { text: '文本', color: designTokens.colors.fieldType.string },
number: { text: '数字', color: designTokens.colors.fieldType.number },
boolean: { text: '布尔值', color: designTokens.colors.fieldType.boolean },
select: { text: '下拉选择', color: designTokens.colors.fieldType.select },
date: { text: '日期', color: designTokens.colors.fieldType.date },
textarea: { text: '多行文本', color: designTokens.colors.fieldType.textarea }
};
const FIELD_TYPE_OPTIONS = [
{ value: 'string', label: '文本' },
{ value: 'number', label: '数字' },
{ value: 'boolean', label: '布尔值' },
{ value: 'select', label: '下拉选择' },
{ value: 'date', label: '日期' },
{ value: 'textarea', label: '多行文本' }
];
function DeviceFieldManagement() {
const [fields, setFields] = useState([]);
const [loading, setLoading] = useState(true);
@@ -225,17 +311,13 @@ function DeviceFieldManagement() {
return iconMap[type] || <FontSizeOutlined />;
};
const columns = [
const columns = useMemo(() => [
{
title: '字段名称',
dataIndex: 'fieldName',
key: 'fieldName',
width: 150,
render: (text) => (
<span style={{ fontWeight: '500', color: designTokens.colors.text.primary }}>
{text}
</span>
)
render: (text) => <span style={tableCellStyle}>{text}</span>
},
{
title: '显示名称',
@@ -249,25 +331,9 @@ function DeviceFieldManagement() {
key: 'fieldType',
width: 110,
render: (type) => {
const typeMap = {
string: { text: '文本', color: designTokens.colors.fieldType.string },
number: { text: '数字', color: designTokens.colors.fieldType.number },
boolean: { text: '布尔值', color: designTokens.colors.fieldType.boolean },
select: { text: '下拉选择', color: designTokens.colors.fieldType.select },
date: { text: '日期', color: designTokens.colors.fieldType.date },
textarea: { text: '多行文本', color: designTokens.colors.fieldType.textarea }
};
const config = typeMap[type] || { text: type, color: designTokens.colors.text.tertiary };
const config = FIELD_TYPE_MAP[type] || { text: type, color: designTokens.colors.text.tertiary };
return (
<Tag
style={{
border: 'none',
borderRadius: designTokens.borderRadius.small,
background: `${config.color}15`,
color: config.color,
fontWeight: '500'
}}
>
<Tag style={{ ...typeTagStyle, background: `${config.color}15`, color: config.color }}>
{getFieldTypeIcon(type)}
<span style={{ marginLeft: '4px' }}>{config.text}</span>
</Tag>
@@ -282,7 +348,7 @@ function DeviceFieldManagement() {
render: (required) => (
<span style={{
color: required ? designTokens.colors.success.main : designTokens.colors.text.tertiary,
fontWeight: '500'
...tableCellStyle
}}>
{required ? '是' : '否'}
</span>
@@ -296,7 +362,7 @@ function DeviceFieldManagement() {
render: (visible) => (
<span style={{
color: visible ? designTokens.colors.primary.main : designTokens.colors.text.tertiary,
fontWeight: '500'
...tableCellStyle
}}>
{visible ? '是' : '否'}
</span>
@@ -307,17 +373,7 @@ function DeviceFieldManagement() {
dataIndex: 'order',
key: 'order',
width: 80,
render: (order) => (
<span style={{
background: designTokens.colors.background.tertiary,
padding: `2px ${designTokens.spacing.sm}`,
borderRadius: designTokens.borderRadius.small,
fontSize: '12px',
fontWeight: '500'
}}>
{order}
</span>
)
render: (order) => <span style={orderBadgeStyle}>{order}</span>
},
{
title: '操作',
@@ -326,48 +382,25 @@ function DeviceFieldManagement() {
fixed: 'right',
render: (_, record) => (
<Space size="small">
<Button
type="text"
icon={<EditOutlined />}
onClick={() => showModal(record)}
style={{
color: designTokens.colors.primary.main,
height: '28px',
padding: '0 8px'
}}
>
<Button type="text" icon={<EditOutlined />} onClick={() => showModal(record)} style={editButtonStyle}>
编辑
</Button>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(record.fieldId)}
style={{
height: '28px',
padding: '0 8px'
}}
>
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.fieldId)} style={deleteButtonStyle}>
删除
</Button>
</Space>
),
},
];
], []);
return (
<div style={pageContainerStyle}>
<div style={titleRowStyle}>
<div style={titleStyle}>
<AppstoreOutlined style={{ color: designTokens.colors.primary.main }} />
<AppstoreOutlined style={titleIconStyle} />
设备字段管理
</div>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => showModal()}
style={primaryActionStyle}
>
<Button type="primary" icon={<PlusOutlined />} onClick={() => showModal()} style={primaryActionStyle}>
添加字段
</Button>
</div>
@@ -390,30 +423,18 @@ function DeviceFieldManagement() {
</div>
<Modal
title={
<span style={{ fontWeight: '600' }}>
{editingField ? '编辑字段' : '添加字段'}
</span>
}
title={<span style={modalTitleStyle}>{editingField ? '编辑字段' : '添加字段'}</span>}
open={modalVisible}
onCancel={handleCancel}
footer={null}
width={600}
styles={{
body: { padding: designTokens.spacing.lg }
}}
style={{
borderRadius: designTokens.borderRadius.large
}}
styles={{ body: modalBodyStyle }}
style={modalStyle}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Form.Item
name="fieldName"
label={<span style={{ fontWeight: '500' }}>字段名称</span>}
label={<span style={formLabelStyle}>字段名称</span>}
rules={[{ required: true, message: '请输入字段名称' }]}
>
<Input placeholder="请输入字段名称(英文,如:deviceId)" />
@@ -421,7 +442,7 @@ function DeviceFieldManagement() {
<Form.Item
name="displayName"
label={<span style={{ fontWeight: '500' }}>显示名称</span>}
label={<span style={formLabelStyle}>显示名称</span>}
rules={[{ required: true, message: '请输入显示名称' }]}
>
<Input placeholder="请输入显示名称(中文,如:设备ID)" />
@@ -429,34 +450,31 @@ function DeviceFieldManagement() {
<Form.Item
name="fieldType"
label={<span style={{ fontWeight: '500' }}>字段类型</span>}
label={<span style={formLabelStyle}>字段类型</span>}
rules={[{ required: true, message: '请选择字段类型' }]}
>
<Select placeholder="请选择字段类型">
<Option value="string">文本</Option>
<Option value="number">数字</Option>
<Option value="boolean">布尔值</Option>
<Option value="select">下拉选择</Option>
<Option value="date">日期</Option>
<Option value="textarea">多行文本</Option>
{FIELD_TYPE_OPTIONS.map(opt => (
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
))}
</Select>
</Form.Item>
<div style={{ display: 'flex', gap: designTokens.spacing.md }}>
<div style={formRowStyle}>
<Form.Item
name="required"
label={<span style={{ fontWeight: '500' }}>必填</span>}
label={<span style={formLabelStyle}>必填</span>}
valuePropName="checked"
style={{ flex: 1 }}
style={formItemFlexStyle}
>
<Switch />
</Form.Item>
<Form.Item
name="visible"
label={<span style={{ fontWeight: '500' }}>可见</span>}
label={<span style={formLabelStyle}>可见</span>}
valuePropName="checked"
style={{ flex: 1 }}
style={formItemFlexStyle}
>
<Switch defaultChecked />
</Form.Item>
@@ -464,7 +482,7 @@ function DeviceFieldManagement() {
<Form.Item
name="order"
label={<span style={{ fontWeight: '500' }}>显示顺序</span>}
label={<span style={formLabelStyle}>显示顺序</span>}
rules={[{ required: true, message: '请输入显示顺序' }]}
>
<InputNumber placeholder="请输入显示顺序" min={0} style={{ width: '100%' }} />
@@ -472,17 +490,13 @@ function DeviceFieldManagement() {
<Form.Item
name="options"
label={<span style={{ fontWeight: '500' }}>选项配置JSON格式</span>}
label={<span style={formLabelStyle}>选项配置JSON格式</span>}
tooltip="格式示例:[{value: 'option1', label: '选项1'}],仅下拉选择类型需要配置"
>
<Input.TextArea
rows={3}
placeholder="请输入JSON格式的选项配置,使用单引号"
style={{ fontFamily: 'monospace' }}
/>
<Input.TextArea rows={3} placeholder="请输入JSON格式的选项配置,使用单引号" style={textAreaStyle} />
</Form.Item>
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
<Form.Item style={formActionsStyle}>
<Space>
<Button onClick={handleCancel}>取消</Button>
<Button type="primary" htmlType="submit">确定</Button>
+25 -148
View File
@@ -312,174 +312,51 @@ function DeviceManagement() {
// 列宽状态
const [columnWidths, setColumnWidths] = useState({});
// 缓存用于搜索的数据(避免重复处理)
const devicesCacheRef = useRef({
timestamp: 0,
data: null,
TTL: 5 * 60 * 1000 // 缓存5分钟
});
// 防抖搜索关键词
const debouncedKeyword = useDebounce(keyword, 300);
// 预计算所有设备的搜索索引(提升搜索性能
const searchIndexRef = useRef(new Map());
// 使用 useMemo 缓存筛选后的设备数据(现在直接使用 allDevices,因为后端已经处理了筛选
const filteredDevicesMemo = useMemo(() => {
return allDevices;
}, [allDevices]);
// 构建设备搜索索引
const buildSearchIndex = useCallback((devices) => {
const index = new Map();
devices.forEach((device, idx) => {
const searchableValues = [];
// 收集所有基本类型字段值
Object.entries(device).forEach(([key, value]) => {
if (value === null || value === undefined) return;
if (typeof value === 'object') {
// 收集嵌套对象值
if (device.Rack?.name) searchableValues.push(String(device.Rack.name).toLowerCase());
if (device.Rack?.Room?.name) searchableValues.push(String(device.Rack.Room.name).toLowerCase());
// 收集自定义字段值
if (device.customFields && typeof device.customFields === 'object') {
Object.values(device.customFields).forEach(cfValue => {
if (cfValue !== null && cfValue !== undefined && typeof cfValue !== 'object') {
searchableValues.push(String(cfValue).toLowerCase());
}
});
}
} else {
searchableValues.push(String(value).toLowerCase());
}
});
index.set(idx, searchableValues);
});
return index;
}, []);
// 优化的全字段搜索函数
const searchDevices = useCallback((devices, keyword) => {
if (!keyword || !keyword.trim()) {
return devices;
}
const searchTerm = keyword.toLowerCase().trim();
return devices.filter((device, idx) => {
// 使用预计算的搜索索引
let searchableValues = searchIndexRef.current.get(idx);
if (!searchableValues) {
// 如果没有预计算索引,当场计算并缓存
searchableValues = [];
Object.entries(device).forEach(([key, value]) => {
if (value === null || value === undefined) return;
if (typeof value === 'object') {
if (device.Rack?.name) searchableValues.push(String(device.Rack.name).toLowerCase());
if (device.Rack?.Room?.name) searchableValues.push(String(device.Rack.Room.name).toLowerCase());
if (device.customFields && typeof device.customFields === 'object') {
Object.values(device.customFields).forEach(cfValue => {
if (cfValue !== null && cfValue !== undefined && typeof cfValue !== 'object') {
searchableValues.push(String(cfValue).toLowerCase());
}
});
}
} else {
searchableValues.push(String(value).toLowerCase());
}
});
searchIndexRef.current.set(idx, searchableValues);
}
return searchableValues.some(value => value.includes(searchTerm));
});
}, []);
// 获取所有设备数据(不分页,用于本地搜索)- 使用缓存
const fetchAllDevices = useCallback(async (forceRefresh = false) => {
const now = Date.now();
const cache = devicesCacheRef.current;
// 检查缓存是否有效
if (!forceRefresh && cache.data && (now - cache.timestamp) < cache.TTL) {
return cache.data;
}
// 获取所有设备(支持搜索、筛选和分页)
const fetchDevices = useCallback(async (page = 1, pageSize = 10, forceRefresh = false) => {
try {
const response = await axios.get('/api/devices', {
params: { page: 1, pageSize: 99999 }
});
const { devices } = response.data;
setLoading(true);
// 将customFields中的字段值映射为设备对象的直接属性
// 使用后端分页加载
const params = {
page,
pageSize,
keyword: debouncedKeyword || undefined,
status: status !== 'all' ? status : undefined,
type: type !== 'all' ? type : undefined
};
const response = await axios.get('/api/devices', { params });
const { devices, total } = response.data;
// 处理设备数据,展开自定义字段
const processedDevices = devices.map(device => {
const deviceWithFields = { ...device };
// 如果有自定义字段,将其展开为设备对象的直接属性
if (device.customFields && typeof device.customFields === 'object') {
Object.entries(device.customFields).forEach(([fieldName, value]) => {
deviceWithFields[fieldName] = value;
});
}
return deviceWithFields;
});
// 更新缓存
cache.data = processedDevices;
cache.timestamp = now;
// 预计算搜索索引
searchIndexRef.current = buildSearchIndex(processedDevices);
return processedDevices;
} catch (error) {
console.error('获取所有设备数据失败:', error);
return cache.data || [];
}
}, [buildSearchIndex]);
// 使用 useMemo 缓存筛选后的设备数据
const filteredDevicesMemo = useMemo(() => {
if (!allDevices.length) return [];
let result = allDevices;
// 状态筛选
if (status && status !== 'all') {
result = result.filter(device => device.status === status);
}
// 类型筛选
if (type && type !== 'all') {
result = result.filter(device => device.type === type);
}
// 关键词搜索(使用防抖后的关键词)
if (debouncedKeyword && debouncedKeyword.trim()) {
result = searchDevices(result, debouncedKeyword);
}
return result;
}, [allDevices, status, type, debouncedKeyword, searchDevices]);
// 获取所有设备(支持搜索、筛选和分页)- 使用缓存和useCallback
const fetchDevices = useCallback(async (page = 1, pageSize = 10, forceRefresh = false) => {
try {
setLoading(true);
// 先获取所有设备数据(使用缓存,批量删除后强制刷新)
const allData = await fetchAllDevices(forceRefresh);
setAllDevices(allData);
// 更新分页信息(筛选后的数据会通过useMemo自动更新)
setPagination(prev => ({ ...prev, current: page, pageSize, total: filteredDevicesMemo.length }));
setAllDevices(processedDevices);
setPagination(prev => ({ ...prev, current: page, pageSize, total }));
} catch (error) {
message.error('获取设备列表失败');
console.error('获取设备列表失败:', error);
} finally {
setLoading(false);
}
}, [fetchAllDevices, filteredDevicesMemo.length]);
}, [debouncedKeyword, status, type]);
// 获取设备字段配置
const fetchDeviceFields = async () => {
@@ -533,10 +410,10 @@ function DeviceManagement() {
};
useEffect(() => {
fetchDevices();
fetchDevices(1, pagination.pageSize);
fetchRacks();
fetchDeviceFields();
}, []);
}, [fetchDevices]);
// 同步当前页设备数据
useEffect(() => {
+12 -12
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Card, Table, Tag, Space, Button, DatePicker, Select, message, Popconfirm, Typography, Descriptions } from 'antd';
import { ReloadOutlined, DeleteOutlined, EyeOutlined, SafetyCertificateOutlined } from '@ant-design/icons';
import { loginHistoryAPI } from '../api';
@@ -17,7 +17,7 @@ const LoginHistory = () => {
fetchHistories();
}, [pagination.current, filters]);
const fetchHistories = async () => {
const fetchHistories = useCallback(async () => {
setLoading(true);
try {
const params = {
@@ -35,14 +35,14 @@ const LoginHistory = () => {
} finally {
setLoading(false);
}
};
}, [pagination.current, pagination.pageSize, filters]);
const handleFilterChange = (key, value) => {
const handleFilterChange = useCallback((key, value) => {
setFilters(prev => ({ ...prev, [key]: value }));
setPagination(prev => ({ ...prev, current: 1 }));
};
}, []);
const handleDateChange = (dates) => {
const handleDateChange = useCallback((dates) => {
if (dates) {
setFilters(prev => ({
...prev,
@@ -53,9 +53,9 @@ const LoginHistory = () => {
setFilters(prev => ({ ...prev, startDate: undefined, endDate: undefined }));
}
setPagination(prev => ({ ...prev, current: 1 }));
};
}, []);
const handleClear = async () => {
const handleClear = useCallback(async () => {
try {
const response = await loginHistoryAPI.clear({ days: 30 });
if (response.success) {
@@ -65,9 +65,9 @@ const LoginHistory = () => {
} catch (error) {
message.error('清理失败');
}
};
}, [fetchHistories]);
const columns = [
const tableColumns = useMemo(() => [
{
title: '用户名',
dataIndex: 'username',
@@ -128,7 +128,7 @@ const LoginHistory = () => {
return browser;
}
}
];
], []);
const pageHeaderStyle = {
marginBottom: '24px',
@@ -172,7 +172,7 @@ const LoginHistory = () => {
<Card>
<Table
columns={columns}
columns={tableColumns}
dataSource={histories}
rowKey="id"
loading={loading}
+6 -6
View File
@@ -76,13 +76,13 @@ const headerStyle = {
boxShadow: '0 8px 32px rgba(102, 126, 234, 0.3)'
};
const statCardStyle = () => ({
const statCardStyle = {
background: 'rgba(255, 255, 255, 0.15)',
borderRadius: designTokens.borderRadius.medium,
padding: '16px',
border: '1px solid rgba(255, 255, 255, 0.2)',
backdropFilter: 'blur(10px)'
});
};
const cardStyle = {
borderRadius: designTokens.borderRadius.large,
@@ -654,19 +654,19 @@ function RackManagement() {
</p>
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<div style={statCardStyle()}>
<div style={statCardStyle}>
<Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: '12px' }}>总机柜</Text>
<div style={{ fontSize: '24px', fontWeight: '700' }}>{stats.total}</div>
</div>
<div style={statCardStyle()}>
<div style={statCardStyle}>
<Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: '12px' }}>在用机柜</Text>
<div style={{ fontSize: '24px', fontWeight: '700', color: '#52c41a' }}>{stats.active}</div>
</div>
<div style={statCardStyle()}>
<div style={statCardStyle}>
<Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: '12px' }}>设备总数</Text>
<div style={{ fontSize: '24px', fontWeight: '700' }}>{stats.totalDevices}</div>
</div>
<div style={statCardStyle()}>
<div style={statCardStyle}>
<Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: '12px' }}>总功率</Text>
<div style={{ fontSize: '24px', fontWeight: '700' }}>{(stats.totalPower / 1000).toFixed(1)}kW</div>
</div>
+3 -3
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } 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, Checkbox, Popover, InputNumber, Switch } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, EyeOutlined, MoreOutlined, UserOutlined, ToolOutlined, CheckCircleOutlined, SyncOutlined, ClockCircleOutlined, CloseCircleOutlined, SettingOutlined } from '@ant-design/icons';
import axios from 'axios';
@@ -233,7 +233,7 @@ function TicketManagement() {
);
}, [devices]);
const getTableColumns = useCallback(() => {
const tableColumns = useMemo(() => {
const baseColumns = [
{ title: '工单编号', dataIndex: 'ticketId', key: 'ticketId', width: 150, fixed: 'left' },
{ title: '标题', dataIndex: 'title', key: 'title', width: 200, ellipsis: true },
@@ -609,7 +609,7 @@ function TicketManagement() {
</Form>
<Table
columns={getTableColumns()}
columns={tableColumns}
dataSource={tickets}
rowKey="ticketId"
pagination={pagination}
+26 -26
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } 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 { PlusOutlined, EditOutlined, DeleteOutlined, UserOutlined, ReloadOutlined, LockOutlined, CameraOutlined } from '@ant-design/icons';
import { userAPI, roleAPI } from '../api';
@@ -26,7 +26,7 @@ const UserManagement = () => {
fetchRoles();
}, [pagination.current]);
const fetchUsers = async () => {
const fetchUsers = useCallback(async () => {
setLoading(true);
try {
const response = await userAPI.list({
@@ -42,9 +42,9 @@ const UserManagement = () => {
} finally {
setLoading(false);
}
};
}, [pagination.current, pagination.pageSize]);
const fetchRoles = async () => {
const fetchRoles = useCallback(async () => {
try {
const response = await roleAPI.all();
if (response.success) {
@@ -56,15 +56,15 @@ const UserManagement = () => {
console.error('获取角色列表失败:', error);
message.error('获取角色列表失败,请检查网络连接');
}
};
}, []);
const handleAdd = () => {
const handleAdd = useCallback(() => {
setEditingUser(null);
form.resetFields();
setModalVisible(true);
};
}, []);
const handleEdit = (user) => {
const handleEdit = useCallback((user) => {
setEditingUser(user);
form.setFieldsValue({
username: user.username,
@@ -75,20 +75,20 @@ const UserManagement = () => {
roleIds: user.roles?.map(r => r.roleId) || []
});
setModalVisible(true);
};
}, []);
const handleResetPassword = (user) => {
const handleResetPassword = useCallback((user) => {
setPasswordUser(user);
passwordForm.resetFields();
setPasswordModalVisible(true);
};
}, []);
const handleAvatarClick = (user) => {
const handleAvatarClick = useCallback((user) => {
setAvatarUser(user);
setAvatarModalVisible(true);
};
}, []);
const handleAvatarUpload = async (e) => {
const handleAvatarUpload = useCallback(async (e) => {
const file = e.target.files[0];
if (!file) return;
@@ -120,9 +120,9 @@ const UserManagement = () => {
fileInputRef.current.value = '';
}
}
};
}, [avatarUser, fetchUsers]);
const handleAvatarDelete = async () => {
const handleAvatarDelete = useCallback(async () => {
try {
const response = await userAPI.deleteAvatar(avatarUser.userId);
if (response.success) {
@@ -135,9 +135,9 @@ const UserManagement = () => {
} catch (error) {
message.error('删除失败');
}
};
}, [avatarUser, fetchUsers]);
const handleDelete = async (userId) => {
const handleDelete = useCallback(async (userId) => {
try {
const response = await userAPI.delete(userId);
if (response.success) {
@@ -149,9 +149,9 @@ const UserManagement = () => {
} catch (error) {
message.error('删除失败');
}
};
}, [fetchUsers]);
const handleSubmit = async (values) => {
const handleSubmit = useCallback(async (values) => {
try {
let response;
if (editingUser) {
@@ -170,9 +170,9 @@ const UserManagement = () => {
} catch (error) {
message.error('操作失败');
}
};
}, [editingUser, fetchUsers]);
const handleResetPasswordSubmit = async (values) => {
const handleResetPasswordSubmit = useCallback(async (values) => {
try {
const response = await userAPI.resetPassword(passwordUser.userId, values);
if (response.success) {
@@ -184,7 +184,7 @@ const UserManagement = () => {
} catch (error) {
message.error('重置失败');
}
};
}, [passwordUser]);
const getStatusColor = (status) => {
const colors = {
@@ -209,7 +209,7 @@ const UserManagement = () => {
return user.avatar;
};
const columns = [
const tableColumns = useMemo(() => [
{
title: '头像',
key: 'avatar',
@@ -317,7 +317,7 @@ const UserManagement = () => {
</Space>
)
}
];
], [handleAvatarClick, handleEdit, handleResetPassword, handleDelete]);
const pageHeaderStyle = {
marginBottom: '24px',
@@ -426,7 +426,7 @@ const UserManagement = () => {
<Card style={cardStyle} styles={{ header: cardHeadStyle, body: { padding: '20px 24px' } }}>
<Table
columns={columns}
columns={tableColumns}
dataSource={users}
rowKey="userId"
loading={loading}