feat: 修改了bug

This commit is contained in:
zhang1106
2026-03-06 13:57:37 +08:00
parent 0bb51ec6ac
commit 0cd0107008
12 changed files with 710 additions and 331 deletions
+2 -2
View File
@@ -133,8 +133,8 @@ const ProtectedRoute = ({ component: Component }) => (
// 默认空闲超时配置
const DEFAULT_IDLE_CONFIG = {
timeout: 10 * 60 * 1000, // 10分钟
warningTime: 30 * 1000, // 30秒
timeout: 30 * 60 * 1000, // 30分钟
warningTime: 60 * 1000, // 60秒
};
const AppLayout = ({ children }) => {
+2 -2
View File
@@ -202,7 +202,7 @@ const RackModel = ({
return (
<LODManager
key={device.id}
key={device.deviceId || device.id}
device={device}
uHeight={uHeight}
rackDepth={depth}
@@ -216,7 +216,7 @@ const RackModel = ({
uHeight={uHeight}
rackDepth={depth}
position={[0, 0, 0]}
isSelected={selectedDeviceId === device.id}
isSelected={selectedDeviceId === (device.deviceId || device.id)}
onClick={onDeviceClick}
onPointerOver={onDeviceHover}
onPointerOut={onDeviceLeave}
+328 -115
View File
@@ -229,6 +229,8 @@ function DeviceManagement() {
const [devices, setDevices] = useState([]);
const [allDevices, setAllDevices] = useState([]);
const [racks, setRacks] = useState([]);
const [rooms, setRooms] = useState([]);
const [selectedRoomId, setSelectedRoomId] = useState(null);
const [loading, setLoading] = useState(true);
const [searching, setSearching] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
@@ -361,9 +363,9 @@ function DeviceManagement() {
fieldName: 'deviceId',
displayName: '设备ID',
fieldType: 'string',
required: true,
required: false,
order: 1,
visible: true,
visible: false,
},
{
fieldName: 'name',
@@ -392,7 +394,7 @@ function DeviceManagement() {
fieldName: 'model',
displayName: '型号',
fieldType: 'string',
required: true,
required: false,
order: 4,
visible: true,
},
@@ -454,7 +456,7 @@ function DeviceManagement() {
fieldName: 'purchaseDate',
displayName: '购买日期',
fieldType: 'date',
required: true,
required: false,
order: 11,
visible: true,
},
@@ -462,7 +464,7 @@ function DeviceManagement() {
fieldName: 'warrantyExpiry',
displayName: '保修到期',
fieldType: 'date',
required: true,
required: false,
order: 12,
visible: true,
},
@@ -487,8 +489,9 @@ function DeviceManagement() {
// 获取所有机柜
const fetchRacks = async () => {
try {
const response = await axios.get('/api/racks');
// 现在API返回的格式是 { racks: [], total: number }
const response = await axios.get('/api/racks', {
params: { pageSize: 1000 }
});
setRacks(response.data.racks || []);
} catch (error) {
message.error('获取机柜列表失败');
@@ -496,9 +499,21 @@ function DeviceManagement() {
}
};
// 获取所有机房
const fetchRooms = async () => {
try {
const response = await axios.get('/api/rooms');
setRooms(response.data || []);
} catch (error) {
message.error('获取机房列表失败');
console.error('获取机房列表失败:', error);
}
};
useEffect(() => {
fetchDevices(1, pagination.pageSize);
fetchRacks();
fetchRooms();
fetchDeviceFields();
}, [fetchDevices]);
@@ -576,8 +591,17 @@ function DeviceManagement() {
}
form.setFieldsValue(cleanDeviceData);
// 编辑设备时,根据 rackId 找到对应的机房并设置 selectedRoomId
if (device.rackId) {
const rack = racks.find(r => r.rackId === device.rackId);
if (rack) {
setSelectedRoomId(rack.roomId);
}
}
} else {
form.resetFields();
setSelectedRoomId(null);
}
setModalVisible(true);
};
@@ -586,12 +610,12 @@ function DeviceManagement() {
const handleCancel = () => {
setModalVisible(false);
setEditingDevice(null);
setSelectedRoomId(null);
};
// 提交表单
const handleSubmit = async values => {
try {
// 定义设备模型的固定字段
const fixedFields = [
'deviceId',
'name',
@@ -607,25 +631,25 @@ function DeviceManagement() {
'warrantyExpiry',
'ipAddress',
'description',
'roomId',
];
// 构建最终的设备数据,包含固定字段和自定义字段
const deviceData = {
...values,
purchaseDate: values.purchaseDate ? values.purchaseDate.format('YYYY-MM-DD') : null,
warrantyExpiry: values.warrantyExpiry ? values.warrantyExpiry.format('YYYY-MM-DD') : null,
customFields: {}, // 用于存储自定义字段
customFields: {},
};
// 分离固定字段和自定义字段
Object.keys(deviceData).forEach(key => {
if (!fixedFields.includes(key) && key !== 'customFields') {
// 将非固定字段移动到customFields对象中
deviceData.customFields[key] = deviceData[key];
delete deviceData[key];
}
});
delete deviceData.roomId;
if (editingDevice) {
// 更新设备
await axios.put(`/api/devices/${editingDevice.deviceId}`, deviceData);
@@ -640,7 +664,8 @@ function DeviceManagement() {
fetchDevices();
setEditingDevice(null);
} catch (error) {
message.error(editingDevice ? '设备更新失败' : '设备创建失败');
const errorMsg = error.response?.data?.error || error.message || '未知错误';
message.error(editingDevice ? `设备更新失败: ${errorMsg}` : `设备创建失败: ${errorMsg}`);
console.error(editingDevice ? '设备更新失败:' : '设备创建失败:', error);
}
};
@@ -1242,17 +1267,17 @@ function DeviceManagement() {
// 保存字段配置
const handleSaveFieldConfig = async values => {
try {
// 更新设备字段配置的可见性
const updatedFields = deviceFields.map(field => ({
...field,
visible: values[field.fieldName],
fieldId: field.fieldId,
fieldName: field.fieldName,
displayName: field.displayName,
visible: values[`visible_${field.fieldName}`] ?? field.visible,
required: values[`required_${field.fieldName}`] ?? field.required,
}));
// 保存到后端
await axios.post('/api/deviceFields/config', updatedFields);
const response = await axios.post('/api/deviceFields/config', updatedFields);
// 更新本地状态
setDeviceFields(updatedFields);
setDeviceFields(response.data);
message.success('字段配置保存成功');
setFieldConfigModalVisible(false);
} catch (error) {
@@ -1616,10 +1641,18 @@ function DeviceManagement() {
className="device-modal"
>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
{deviceFields
.filter(field => field.fieldName !== 'deviceId')
.map(field => {
{(() => {
const filteredFields = deviceFields.filter(
field => field.fieldName !== 'deviceId' && field.fieldName !== 'rackId'
);
const formItems = [];
filteredFields.forEach((field, index) => {
let control = null;
const inputStyle = {
borderRadius: '8px',
transition: 'all 0.3s ease',
};
switch (field.fieldType) {
case 'text':
@@ -1627,7 +1660,8 @@ function DeviceManagement() {
control = (
<Input
placeholder={`请输入${field.displayName}`}
style={{ borderRadius: '8px' }}
style={inputStyle}
className="form-input-enhanced"
/>
);
break;
@@ -1636,7 +1670,8 @@ function DeviceManagement() {
<InputNumber
placeholder={`请输入${field.displayName}`}
min={0}
style={{ width: '100%', borderRadius: '8px' }}
style={{ width: '100%', ...inputStyle }}
className="form-input-enhanced"
/>
);
break;
@@ -1646,8 +1681,9 @@ function DeviceManagement() {
case 'date':
control = (
<DatePicker
style={{ width: '100%', borderRadius: '8px' }}
style={{ width: '100%', ...inputStyle }}
placeholder={`请选择${field.displayName}`}
className="form-input-enhanced"
/>
);
break;
@@ -1656,90 +1692,250 @@ function DeviceManagement() {
<Input.TextArea
placeholder={`请输入${field.displayName}`}
rows={3}
style={{ borderRadius: '8px' }}
style={inputStyle}
className="form-input-enhanced"
/>
);
break;
case 'select':
if (field.fieldName === 'rackId') {
control = (
<Select
placeholder={`请选择${field.displayName}`}
style={{ borderRadius: '8px' }}
>
{racks.map(rack => (
<Option key={rack.rackId} value={rack.rackId}>
{rack.name} ({rack.rackId})
control = (
<Select
placeholder={`请选择${field.displayName}`}
style={inputStyle}
className="form-input-enhanced"
>
{field.options &&
field.options.map(option => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
))}
</Select>
);
} else {
control = (
<Select
placeholder={`请选择${field.displayName}`}
style={{ borderRadius: '8px' }}
>
{field.options &&
field.options.map(option => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
))}
</Select>
);
}
</Select>
);
break;
default:
control = (
<Input
placeholder={`请输入${field.displayName}`}
style={{ borderRadius: '8px' }}
style={inputStyle}
className="form-input-enhanced"
/>
);
}
return (
<Form.Item
key={field.fieldName}
name={field.fieldName}
label={field.displayName}
rules={
field.required && field.fieldName !== 'deviceId'
? [{ required: true, message: `请输入${field.displayName}` }]
: []
}
>
{control}
</Form.Item>
);
})}
// 机房和机柜联动选择区域特殊处理
if (field.fieldName === 'serialNumber') {
formItems.push(
<React.Fragment key={field.fieldName}>
<Col span={12} key={`${field.fieldName}-col`}>
<Form.Item
name={field.fieldName}
label={
<span>
{field.displayName}
{field.required && (
<span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>
)}
</span>
}
rules={
field.required
? [{ required: true, message: `请输入${field.displayName}` }]
: []
}
>
{control}
</Form.Item>
</Col>
{/* 机房机柜联动选择区域 - 特殊突出显示 */}
<Col span={24} key="room-rack-section">
<div
style={{
background: 'linear-gradient(135deg, #f0f5ff 0%, #e6f7ff 100%)',
borderRadius: '12px',
padding: '20px',
marginBottom: '16px',
border: '2px solid #d6e4ff',
boxShadow: '0 2px 8px rgba(24, 144, 255, 0.1)',
}}
>
<div
style={{
fontSize: '14px',
fontWeight: '600',
color: '#1890ff',
marginBottom: '16px',
display: 'flex',
alignItems: 'center',
}}
>
<DatabaseOutlined style={{ marginRight: '8px' }} />
设备位置选择
</div>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="roomId"
label={
<span>
机房
<span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>
</span>
}
rules={[{ required: true, message: '请选择机房' }]}
style={{ marginBottom: '0' }}
>
<Select
placeholder="请选择机房"
style={{ borderRadius: '8px' }}
showSearch
optionFilterProp="children"
onChange={(value) => {
setSelectedRoomId(value);
form.setFieldValue('rackId', undefined);
}}
>
{rooms.map(room => (
<Option key={room.roomId} value={room.roomId}>
{room.name}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="rackId"
label={
<span>
机柜
<span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>
</span>
}
rules={[{ required: true, message: '请选择机柜' }]}
style={{ marginBottom: '0' }}
>
<Select
placeholder={selectedRoomId ? '请选择机柜' : '请先选择机房'}
style={{ borderRadius: '8px' }}
disabled={!selectedRoomId}
showSearch
optionFilterProp="children"
>
{(selectedRoomId
? racks.filter(rack => rack.roomId === selectedRoomId)
: []
).map(rack => (
<Option key={rack.rackId} value={rack.rackId}>
{rack.name} ({rack.rackId})
</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
</div>
</Col>
</React.Fragment>
);
} else if (field.fieldType === 'textarea') {
// textarea 占整行
formItems.push(
<Col span={24} key={field.fieldName}>
<Form.Item
name={field.fieldName}
label={
<span>
{field.displayName}
{field.required && (
<span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>
)}
</span>
}
rules={
field.required
? [{ required: true, message: `请输入${field.displayName}` }]
: []
}
>
{control}
</Form.Item>
</Col>
);
} else {
// 其他字段两列布局
formItems.push(
<Col span={12} key={field.fieldName}>
<Form.Item
name={field.fieldName}
label={
<span>
{field.displayName}
{field.required && (
<span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>
)}
</span>
}
rules={
field.required
? [{ required: true, message: `请输入${field.displayName}` }]
: []
}
>
{control}
</Form.Item>
</Col>
);
}
});
<Form.Item style={{ textAlign: 'right', marginTop: '24px' }}>
<Space>
<Button onClick={handleCancel} style={secondaryActionStyle}>
取消
</Button>
<Button
type="primary"
htmlType="submit"
style={{
height: '40px',
borderRadius: designTokens.borderRadius.small,
background: designTokens.colors.primary.gradient,
border: 'none',
color: '#ffffff',
boxShadow: designTokens.shadows.small,
fontWeight: '500',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
确定
</Button>
</Space>
</Form.Item>
return <Row gutter={16}>{formItems}</Row>;
})()}
{/* 底部按钮区域 */}
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
gap: '12px',
marginTop: '32px',
paddingTop: '24px',
borderTop: '1px solid #f0f0f0',
}}
>
<Button
onClick={handleCancel}
style={{
height: '40px',
borderRadius: '8px',
padding: '0 24px',
fontWeight: '500',
transition: 'all 0.3s ease',
}}
>
取消
</Button>
<Button
type="primary"
htmlType="submit"
style={{
height: '40px',
borderRadius: '8px',
background: designTokens.colors.primary.gradient,
border: 'none',
color: '#ffffff',
boxShadow: designTokens.shadows.small,
fontWeight: '500',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
padding: '0 32px',
transition: 'all 0.3s ease',
}}
>
确定
</Button>
</div>
</Form>
</Modal>
@@ -1762,29 +1958,46 @@ function DeviceManagement() {
<Form
layout="vertical"
onFinish={handleSaveFieldConfig}
initialValues={deviceFields.reduce(
(acc, field) => ({
...acc,
[field.fieldName]: field.visible,
}),
{}
)}
initialValues={{
...deviceFields.reduce(
(acc, field) => ({
...acc,
[`visible_${field.fieldName}`]: field.visible,
[`required_${field.fieldName}`]: field.required,
}),
{}
),
}}
>
<div style={{ maxHeight: 400, overflowY: 'auto' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '12px' }}>
{deviceFields
.filter(field => field.fieldName !== 'deviceId')
.map(field => (
<Form.Item
key={field.fieldName}
name={field.fieldName}
valuePropName="checked"
noStyle
>
<Checkbox style={{ marginBottom: '8px' }}>{field.displayName}</Checkbox>
</Form.Item>
))}
</div>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid #f0f0f0' }}>
<th style={{ padding: '8px', textAlign: 'left', width: '40%' }}>字段名称</th>
<th style={{ padding: '8px', textAlign: 'center', width: '30%' }}>可见</th>
<th style={{ padding: '8px', textAlign: 'center', width: '30%' }}>必填</th>
</tr>
</thead>
<tbody>
{deviceFields
.filter(field => field.fieldName !== 'deviceId')
.map(field => (
<tr key={field.fieldName} style={{ borderBottom: '1px solid #f0f0f0' }}>
<td style={{ padding: '8px' }}>{field.displayName}</td>
<td style={{ padding: '8px', textAlign: 'center' }}>
<Form.Item name={`visible_${field.fieldName}`} valuePropName="checked" noStyle>
<Switch size="small" />
</Form.Item>
</td>
<td style={{ padding: '8px', textAlign: 'center' }}>
<Form.Item name={`required_${field.fieldName}`} valuePropName="checked" noStyle>
<Switch size="small" />
</Form.Item>
</td>
</tr>
))}
</tbody>
</table>
</div>
<Form.Item style={{ textAlign: 'right', marginTop: '20px' }}>
+1 -1
View File
@@ -241,7 +241,7 @@ const Rack3DVisualization = () => {
if (!rackId) return;
try {
setLoadingDevices(true);
const response = await axios.get(`/api/devices?rackId=${rackId}`);
const response = await axios.get(`/api/devices?rackId=${rackId}&pageSize=100`);
let devicesData = [];
if (Array.isArray(response.data)) {
@@ -954,6 +954,37 @@ export const generateGlobalStyles = tokens => `
background: inherit !important;
}
/* 表单输入框增强样式 */
.form-input-enhanced:hover {
border-color: ${tokens.colors.primary.main} !important;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1) !important;
}
.form-input-enhanced:focus,
.form-input-enhanced.ant-input-focused,
.form-input-enhanced.ant-select-focused .ant-select-selector,
.form-input-enhanced.ant-input-number-focused {
border-color: ${tokens.colors.primary.main} !important;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2) !important;
}
/* 表单项标签增强 */
.device-modal .ant-form-item-label > label {
font-weight: 500;
color: ${tokens.colors.text.primary};
font-size: 14px;
}
/* 表单项间距优化 */
.device-modal .ant-form-item {
margin-bottom: 20px;
}
/* 必填项标识动画 */
.device-modal .ant-form-item-label > label::after {
content: '';
}
@media screen and (max-width: 768px) {
.device-table-wrapper .ant-table-tbody > tr > td {
max-width: 150px !important;