feat: 添加操作日志、危险操作确认和业务关联功能

1. 新增操作日志记录功能,记录关键操作
2. 实现危险操作确认对话框,防止误删
3. 添加业务和库房管理模块
4. 支持设备标记为空闲状态
5. 完善API文档和健康检查
6. 优化前端删除操作的确认流程
7. 添加Swagger API文档支持
8. 实现设备与业务的关联功能
9. 改进设备模型,添加空闲相关字段
10. 优化用户、角色管理操作日志
This commit is contained in:
zhang1106
2026-03-20 17:15:14 +08:00
parent c392df9ce3
commit 73cbe4ac1b
52 changed files with 11149 additions and 1756 deletions
+91 -17
View File
@@ -89,6 +89,7 @@ function DeviceManagement() {
const [type, setType] = useState('all');
const [roomId, setRoomId] = useState('all');
const [rackId, setRackId] = useState('all');
const [isIdle, setIsIdle] = useState('');
const [searchForm] = Form.useForm();
const [pagination, setPagination] = useState({
@@ -137,6 +138,7 @@ function DeviceManagement() {
type: type !== 'all' ? type : undefined,
roomId: roomId !== 'all' ? roomId : undefined,
rackId: rackId !== 'all' ? rackId : undefined,
isIdle: isIdle || undefined,
};
const response = await axios.get('/api/devices', { params });
@@ -160,8 +162,22 @@ function DeviceManagement() {
try {
setLoadingFields(true);
const response = await axios.get('/api/deviceFields');
const sortedFields = response.data.sort((a, b) => a.order - b.order);
setDeviceFields(sortedFields);
let fields = response.data.sort((a, b) => a.order - b.order);
// 补充缺失的 options(状态和设备类型)
fields = fields.map(field => {
if (field.fieldName === 'type' && !field.options) {
const defaultTypeField = DEFAULT_DEVICE_FIELDS_LOCAL.find(f => f.fieldName === 'type');
return { ...field, options: defaultTypeField?.options || [] };
}
if (field.fieldName === 'status' && !field.options) {
const defaultStatusField = DEFAULT_DEVICE_FIELDS_LOCAL.find(f => f.fieldName === 'status');
return { ...field, options: defaultStatusField?.options || [] };
}
return field;
});
setDeviceFields(fields);
} catch (error) {
message.error('获取字段配置失败');
console.error('获取字段配置失败:', error);
@@ -186,7 +202,7 @@ function DeviceManagement() {
const fetchRooms = async () => {
try {
const response = await axios.get('/api/rooms');
setRooms(response.data || []);
setRooms(response.data.rooms || []);
} catch (error) {
message.error('获取机房列表失败');
console.error('获取机房列表失败:', error);
@@ -277,6 +293,7 @@ function DeviceManagement() {
setType(values.type || 'all');
setRoomId(values.roomId || 'all');
setRackId(values.rackId || 'all');
setIsIdle(values.isIdle || '');
setPagination((prev) => ({ ...prev, current: 1 }));
@@ -291,6 +308,7 @@ function DeviceManagement() {
setType('all');
setRoomId('all');
setRackId('all');
setIsIdle('');
searchForm.resetFields();
setTimeout(() => setSearching(false), 300);
@@ -358,6 +376,34 @@ function DeviceManagement() {
});
};
const handleBatchToIdle = async () => {
if (selectedDevices.length === 0) {
message.warning('请先选择要标记为空闲的设备');
return;
}
Modal.confirm({
title: '确认标记为空闲',
content: `确定要将选中的 ${selectedDevices.length} 个设备标记为空闲设备吗?`,
okText: '确认',
cancelText: '取消',
onOk: async () => {
try {
const response = await axios.post('/api/idle-devices/batch-from-devices', {
deviceIds: selectedDevices,
idleReason: '从设备管理批量转入',
});
message.success(response.data.message || '设备已标记为空闲');
setSelectedDevices([]);
setSelectAll(false);
fetchDevices(1, 10, true);
} catch (error) {
message.error(error.response?.data?.error || '标记为空闲失败');
console.error('标记为空闲失败:', error);
}
},
});
};
const handleDelete = async (deviceId) => {
Modal.confirm({
title: '确认删除',
@@ -628,21 +674,27 @@ function DeviceManagement() {
width: columnWidths[field.fieldName] || 100,
onHeaderCell: handleHeaderCellResize(field.fieldName),
render: (status) => {
if (Array.isArray(status)) {
return (
<Space>
{status.map((s) => (
<span key={s} style={{ color: STATUS_MAP[s]?.color || 'black' }}>
{STATUS_MAP[s]?.text || s}
</span>
))}
</Space>
);
}
const config = STATUS_MAP[status] || { text: status, color: 'default' };
const statusStyles = {
running: { bg: '#f6ffed', border: '#52c41a', text: '#389e0d' },
maintenance: { bg: '#fffbE6', border: '#faad14', text: '#d48806' },
offline: { bg: '#f5f5f5', border: '#8c8c8c', text: '#595959' },
fault: { bg: '#fff2f0', border: '#ff4d4f', text: '#cf1322' },
};
const style = statusStyles[status] || { bg: '#fafafa', border: '#d9d9d9', text: '#595959' };
return (
<span style={{ color: STATUS_MAP[status]?.color || 'black' }}>
{STATUS_MAP[status]?.text || status}
</span>
<Tag
style={{
backgroundColor: style.bg,
borderColor: style.border,
color: style.text,
borderRadius: '4px',
fontWeight: 500,
boxShadow: `0 1px 2px ${style.border}30`,
}}
>
{config.text}
</Tag>
);
},
});
@@ -830,6 +882,18 @@ function DeviceManagement() {
>
状态变更 ({selectedDevices.length})
</Button>
<Button
style={{
...secondaryActionStyle,
color: '#f59e0b',
borderColor: '#f59e0b',
}}
icon={<CloudServerOutlined />}
disabled={selectedDevices.length === 0}
onClick={handleBatchToIdle}
>
标记为空闲 ({selectedDevices.length})
</Button>
<Button
style={{
...secondaryActionStyle,
@@ -964,6 +1028,16 @@ function DeviceManagement() {
</Select>
</Form.Item>
<Form.Item name="isIdle" style={{ margin: 0 }}>
<Select
style={{ width: 140, borderRadius: designTokens.borderRadius.medium }}
>
<Option value="">所有设备</Option>
<Option value="false">在用设备</Option>
<Option value="true">空闲设备</Option>
</Select>
</Form.Item>
<Form.Item style={{ margin: 0 }}>
<Space>
<Button