feat: 添加请求参数验证中间件和设计令牌Hook

feat(validation): 为机房、机柜和设备路由添加Joi验证中间件

feat(hooks): 创建useDesignTokens Hook集中管理主题配置

feat(models): 为耗材模型添加乐观锁version字段和updatedAt索引

feat(3d): 增强3D场景和设备模型视觉效果与交互

refactor: 移除数据备份相关功能并优化代码结构

fix: 修复前端安全日志和密码加密工具

chore: 更新依赖并添加axios和joi库

docs: 更新注释和文档说明

style: 改进代码格式和命名一致性
This commit is contained in:
zhang1106
2026-01-29 16:53:29 +08:00
parent 6d61d6e54c
commit 9e6a3da888
23 changed files with 1518 additions and 1858 deletions
+21 -7
View File
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { Layout, Select, Card, Spin, message, Typography, Descriptions, Tag, Button, Space, Empty, Modal, Form, Input, InputNumber, DatePicker, Checkbox, Switch } from 'antd';
import { CloudServerOutlined, ReloadOutlined, ArrowLeftOutlined, InfoCircleOutlined, UpOutlined, DownOutlined, EditOutlined, SettingOutlined, FullscreenOutlined } from '@ant-design/icons';
import { CloudServerOutlined, ReloadOutlined, ArrowLeftOutlined, InfoCircleOutlined, UpOutlined, DownOutlined, EditOutlined, SettingOutlined, FullscreenOutlined, EyeOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import dayjs from 'dayjs';
@@ -17,7 +17,10 @@ const { Option } = Select;
const Rack3DVisualization = () => {
const navigate = useNavigate();
// Scene 组件的 ref,用于调用重置视角方法
const sceneRef = useRef(null);
// 使用 Scene3DContext 管理3D场景状态
const {
devices,
@@ -465,16 +468,26 @@ const Rack3DVisualization = () => {
<Option key={rack.rackId} value={rack.rackId}>{rack.name}</Option>
))}
</Select>
<Button
<Button
type="primary"
ghost
icon={<ReloadOutlined />}
icon={<ReloadOutlined />}
onClick={() => { fetchRacks(); if(selectedRack) fetchDevices(selectedRack.rackId); }}
style={{ borderRadius: '6px', borderColor: 'rgba(255,255,255,0.3)', color: 'rgba(255,255,255,0.9)' }}
className="hover-bright"
>
刷新
</Button>
<Button
type="primary"
ghost
icon={<EyeOutlined />}
onClick={() => { if(sceneRef.current) sceneRef.current.resetView(); }}
style={{ borderRadius: '6px', borderColor: 'rgba(255,255,255,0.3)', color: 'rgba(255,255,255,0.9)' }}
className="hover-bright"
>
重置视角
</Button>
<div style={{
display: 'flex',
alignItems: 'center',
@@ -546,9 +559,10 @@ const Rack3DVisualization = () => {
</div>
) : selectedRack ? (
<>
<Scene
rack={selectedRack}
devices={devices}
<Scene
ref={sceneRef}
rack={selectedRack}
devices={devices}
selectedDeviceId={selectedDevice?.deviceId || selectedDevice?.id}
onDeviceClick={handleDeviceClick}
onDeviceLeave={handleDeviceLeave}
+3 -167
View File
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Tabs, Form, Input, Switch, Select, Button, Card, Space, message, Modal, Table, Tag, Progress, Divider, Descriptions, Alert } from 'antd';
import { SettingOutlined, GlobalOutlined, BgColorsOutlined, DatabaseOutlined, InfoCircleOutlined, CloudUploadOutlined, DeleteOutlined, ReloadOutlined, DownloadOutlined, SyncOutlined, CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
import { Tabs, Form, Input, Switch, Select, Button, Card, Space, message, Modal, Tag, Divider, Descriptions, Alert } from 'antd';
import { SettingOutlined, GlobalOutlined, BgColorsOutlined, InfoCircleOutlined, CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
import axios from 'axios';
import { useConfig } from '../context/ConfigContext';
@@ -12,15 +12,12 @@ const SystemSettings = () => {
const [saving, setSaving] = useState(false);
const [settings, setSettings] = useState({});
const [activeTab, setActiveTab] = useState('general');
const [backupList, setBackupList] = useState([]);
const [backupLoading, setBackupLoading] = useState(false);
const [systemInfo, setSystemInfo] = useState(null);
const [form] = Form.useForm();
const { reloadConfig } = useConfig();
useEffect(() => {
fetchSettings();
fetchBackupList();
fetchSystemInfo();
}, []);
@@ -42,18 +39,6 @@ const SystemSettings = () => {
}
};
const fetchBackupList = async () => {
setBackupLoading(true);
try {
const response = await axios.get('/api/system-settings/backup/list');
setBackupList(response.data.backups || []);
} catch (error) {
console.error('获取备份列表失败');
} finally {
setBackupLoading(false);
}
};
const fetchSystemInfo = async () => {
try {
const response = await axios.get('/api/system-settings/system/info');
@@ -85,66 +70,6 @@ const SystemSettings = () => {
}
};
const handleCreateBackup = async () => {
Modal.confirm({
title: '确认创建备份',
icon: <ExclamationCircleOutlined />,
content: '确定要创建系统备份吗?这将导出所有设备、机柜、机房和耗材数据。',
onOk: async () => {
try {
message.loading('正在创建备份...', 0);
const response = await axios.post('/api/system-settings/backup');
message.destroy();
message.success('备份创建成功');
fetchBackupList();
} catch (error) {
message.destroy();
message.error('备份创建失败');
}
}
});
};
const handleRestoreBackup = (filename) => {
Modal.confirm({
title: '确认恢复备份',
icon: <ExclamationCircleOutlined />,
content: `确定要恢复备份 "${filename}" 吗?当前数据将被覆盖,且此操作不可撤销。`,
onOk: async () => {
try {
message.loading('正在恢复备份...', 0);
await axios.post('/api/system-settings/backup/restore', { filename });
message.destroy();
message.success('恢复成功,请刷新页面查看最新数据');
} catch (error) {
message.destroy();
message.error('恢复备份失败');
}
}
});
};
const handleDeleteBackup = (filename) => {
Modal.confirm({
title: '确认删除备份',
icon: <ExclamationCircleOutlined />,
content: `确定要删除备份 "${filename}" 吗?`,
onOk: async () => {
try {
await axios.delete(`/api/system-settings/backup/${filename}`);
message.success('删除成功');
fetchBackupList();
} catch (error) {
message.error('删除失败');
}
}
});
};
const handleDownloadBackup = (filename) => {
window.open(`/api/system-settings/backup/download/${filename}`, '_blank');
};
const handleResetSetting = (key) => {
Modal.confirm({
title: '确认重置',
@@ -289,10 +214,6 @@ const SystemSettings = () => {
{ value: 'false', label: '展开' },
{ value: 'true', label: '折叠' }
],
auto_backup_enabled: [
{ value: 'false', label: '关闭' },
{ value: 'true', label: '开启' }
]
};
return optionsMap[key] || [];
};
@@ -352,86 +273,7 @@ const SystemSettings = () => {
);
};
const renderBackupSettings = () => {
const backupColumns = [
{
title: '文件名',
dataIndex: 'filename',
key: 'filename',
render: (text) => <code>{text}</code>
},
{
title: '大小',
dataIndex: 'size',
key: 'size',
render: (size) => {
const kb = size / 1024;
return kb < 1024 ? `${kb.toFixed(2)} KB` : `${(kb / 1024).toFixed(2)} MB`;
}
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
render: (date) => new Date(date).toLocaleString('zh-CN')
},
{
title: '操作',
key: 'action',
render: (_, record) => (
<Space size="small">
<Button size="small" icon={<ReloadOutlined />} onClick={() => handleRestoreBackup(record.filename)}>恢复</Button>
<Button size="small" icon={<DownloadOutlined />} onClick={() => handleDownloadBackup(record.filename)}>下载</Button>
<Button size="small" danger icon={<DeleteOutlined />} onClick={() => handleDeleteBackup(record.filename)}>删除</Button>
</Space>
)
}
];
// 备份设置键列表
const backupKeys = ['auto_backup_enabled', 'backup_interval', 'backup_retention', 'backup_path', 'last_backup_time', 'backup_count'];
return (
<div>
<Card title="自动备份设置" bordered={false} style={{ marginBottom: 16 }}>
<Form form={form} layout="vertical" onFinish={handleSaveSettings}>
{backupKeys.map(key => {
if (settings[key]) {
return renderFormItem(key, settings[key]);
}
return null;
})}
<Form.Item>
<Space>
<Button type="primary" htmlType="submit" loading={saving}>保存设置</Button>
</Space>
</Form.Item>
</Form>
</Card>
<Card title="手动备份管理" bordered={false}>
<Alert
message="数据安全提示"
description="建议定期创建备份,并将备份文件保存到安全的位置。恢复备份前请确保已创建当前数据的备份。"
type="warning"
showIcon
style={{ marginBottom: 16 }}
/>
<Space style={{ marginBottom: 16 }}>
<Button type="primary" icon={<CloudUploadOutlined />} onClick={handleCreateBackup}>立即备份</Button>
<Button icon={<SyncOutlined />} onClick={fetchBackupList}>刷新列表</Button>
</Space>
<Table
dataSource={backupList}
columns={backupColumns}
rowKey="filename"
loading={backupLoading}
pagination={{ pageSize: 5 }}
/>
</Card>
</div>
);
};
// 数据备份功能已移除
const renderAboutPage = () => {
const aboutKeys = ['app_version', 'company_name', 'contact_email', 'contact_phone', 'company_address', 'system_description', 'privacy_policy', 'terms_of_service'];
@@ -504,12 +346,6 @@ const SystemSettings = () => {
>
{renderAppearanceSettings()}
</TabPane>
<TabPane
tab={<span><DatabaseOutlined /> 数据备份</span>}
key="backup"
>
{renderBackupSettings()}
</TabPane>
<TabPane
tab={<span><InfoCircleOutlined /> 关于</span>}
key="about"
+16 -3
View File
@@ -4,6 +4,17 @@ import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, EyeOutlined
import axios from 'axios';
import dayjs from 'dayjs';
// 安全地从 localStorage 获取用户信息
const getUserFromStorage = () => {
try {
const userStr = localStorage.getItem('user');
return userStr ? JSON.parse(userStr) : {};
} catch (e) {
console.error('解析用户信息失败:', e);
return {};
}
};
const { Option } = Select;
const { RangePicker } = DatePicker;
const { TextArea } = Input;
@@ -412,7 +423,7 @@ function TicketManagement() {
await axios.put(`/api/tickets/${editingTicket.ticketId}`, ticketData);
message.success('工单更新成功');
} else {
const user = JSON.parse(localStorage.getItem('user') || '{}');
const user = getUserFromStorage();
ticketData.reporterId = user.userId || localStorage.getItem('userId') || 'USER001';
ticketData.reporterName = user.username || '系统用户';
await axios.post('/api/tickets', ticketData);
@@ -457,10 +468,11 @@ function TicketManagement() {
const handleProcessSubmit = useCallback(async (values) => {
try {
const user = getUserFromStorage();
await axios.put(`/api/tickets/${selectedTicket.ticketId}/process`, {
...values,
operatorId: localStorage.getItem('userId'),
operatorName: JSON.parse(localStorage.getItem('user') || '{}').username
operatorName: user.username
});
message.success('工单处理完成');
setProcessingModalVisible(false);
@@ -473,10 +485,11 @@ function TicketManagement() {
const handleStatusChange = useCallback(async (ticketId, newStatus) => {
try {
const user = getUserFromStorage();
await axios.put(`/api/tickets/${ticketId}/status`, {
status: newStatus,
operatorId: localStorage.getItem('userId'),
operatorName: JSON.parse(localStorage.getItem('user') || '{}').username
operatorName: user.username
});
message.success('状态更新成功');
fetchTickets();