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
+20 -11
View File
@@ -304,17 +304,26 @@ function CableManagement() {
};
const handleDelete = async cableId => {
try {
await axios.delete(`/api/cables/${cableId}`);
message.success({
content: '删除成功',
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
});
fetchCables();
} catch (error) {
message.error('删除失败');
console.error('删除失败:', error);
}
Modal.confirm({
title: '确认删除',
content: '确定要删除这条接线吗?此操作不可恢复!',
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
await axios.delete(`/api/cables/${cableId}`);
message.success({
content: '删除成功',
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
});
fetchCables();
} catch (error) {
message.error('删除失败');
console.error('删除失败:', error);
}
},
});
};
const handleSubmit = async () => {
+17 -8
View File
@@ -102,14 +102,23 @@ function CategoryManagement() {
};
const handleDelete = async id => {
try {
await axios.delete(`/api/consumable-categories/${id}`);
message.success('删除成功');
fetchCategories();
} catch (error) {
message.error(error.response?.data?.error || '删除失败');
console.error('删除失败:', error);
}
Modal.confirm({
title: '确认删除',
content: '确定要删除这个耗材分类吗?此操作不可恢复!',
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
await axios.delete(`/api/consumable-categories/${id}`);
message.success('删除成功');
fetchCategories();
} catch (error) {
message.error(error.response?.data?.error || '删除失败');
console.error('删除失败:', error);
}
},
});
};
const columns = [
+20 -11
View File
@@ -243,17 +243,26 @@ function ConsumableManagement() {
const handleDelete = useCallback(
async consumableId => {
try {
await axios.delete(`/api/consumables/${consumableId}`);
message.success({
content: '删除成功',
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
});
fetchConsumables();
} catch (error) {
message.error('删除失败');
console.error('删除失败:', error);
}
Modal.confirm({
title: '确认删除',
content: '确定要删除这个耗材吗?此操作不可恢复!',
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
await axios.delete(`/api/consumables/${consumableId}`);
message.success({
content: '删除成功',
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
});
fetchConsumables();
} catch (error) {
message.error('删除失败');
console.error('删除失败:', error);
}
},
});
},
[fetchConsumables]
);
+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
+942
View File
@@ -0,0 +1,942 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Table,
Button,
Modal,
Form,
Input,
Select,
message,
Card,
Space,
Tag,
Tooltip,
Typography,
Pagination,
Popconfirm,
Row,
Col,
Badge,
Avatar,
Statistic,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
SearchOutlined,
ReloadOutlined,
InboxOutlined,
ClockCircleOutlined,
UploadOutlined,
} from '@ant-design/icons';
import axios from 'axios';
const { Title, Text, Paragraph } = Typography;
const { Option } = Select;
const { TextArea } = Input;
const IdleDeviceManagement = () => {
const [idleDevices, setIdleDevices] = useState([]);
const [loading, setLoading] = useState(false);
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
const [searchKeyword, setSearchKeyword] = useState('');
const [sourceTypeFilter, setSourceTypeFilter] = useState('all');
const [isModalVisible, setIsModalVisible] = useState(false);
const [isShelveModalVisible, setIsShelveModalVisible] = useState(false);
const [editingDevice, setEditingDevice] = useState(null);
const [shelvingDevice, setShelvingDevice] = useState(null);
const [form] = Form.useForm();
const [shelveForm] = Form.useForm();
const [racks, setRacks] = useState([]);
const [rooms, setRooms] = useState([]);
const [selectedRoomId, setSelectedRoomId] = useState(null);
const [selectedShelveRoomId, setSelectedShelveRoomId] = useState(null);
const fetchIdleDevices = useCallback(async () => {
setLoading(true);
try {
const params = {
page: pagination.current,
pageSize: pagination.pageSize,
keyword: searchKeyword,
sourceType: sourceTypeFilter,
};
const response = await axios.get('/api/idle-devices', { params });
setIdleDevices(response.data.idleDevices || []);
setPagination((prev) => ({
...prev,
total: response.data.total || 0,
}));
} catch (error) {
message.error('获取空闲设备列表失败');
} finally {
setLoading(false);
}
}, [pagination.current, pagination.pageSize, searchKeyword, sourceTypeFilter]);
const fetchRacks = async () => {
try {
const response = await axios.get('/api/racks', { params: { pageSize: 100 } });
setRacks(response.data.racks || []);
} catch (error) {
console.error('获取机柜列表失败', error);
}
};
const fetchRooms = async () => {
try {
const response = await axios.get('/api/rooms', { params: { pageSize: 100 } });
setRooms(response.data.rooms || []);
} catch (error) {
console.error('获取机房列表失败', error);
}
};
useEffect(() => {
fetchIdleDevices();
}, [fetchIdleDevices]);
useEffect(() => {
fetchRacks();
fetchRooms();
}, []);
const handleAdd = () => {
setEditingDevice(null);
setSelectedRoomId(null);
form.resetFields();
setIsModalVisible(true);
};
const handleEdit = (record) => {
setEditingDevice(record);
let roomId = null;
if (record.rackId && record.Rack) {
roomId = record.Rack.roomId;
setSelectedRoomId(roomId);
}
form.setFieldsValue({
name: record.name,
type: record.type,
model: record.model,
serialNumber: record.serialNumber,
powerConsumption: record.powerConsumption,
idleReason: record.idleReason,
warehouseId: record.warehouseId,
roomId: roomId,
rackId: record.rackId,
position: record.position,
description: record.description,
});
setIsModalVisible(true);
};
const handleDelete = async (deviceId) => {
try {
await axios.delete(`/api/idle-devices/${deviceId}`);
message.success('删除成功');
fetchIdleDevices();
} catch (error) {
message.error(error.response?.data?.error || '删除失败');
}
};
const handleShelve = (record) => {
setShelvingDevice(record);
let roomId = null;
if (record.rackId) {
const rack = racks.find(r => r.rackId === record.rackId);
if (rack) {
roomId = rack.roomId;
}
}
setSelectedShelveRoomId(roomId);
shelveForm.setFieldsValue({
name: record.name,
type: record.type,
model: record.model,
serialNumber: record.serialNumber,
height: record.height || 1,
powerConsumption: record.powerConsumption,
roomId: roomId,
rackId: record.rackId,
position: record.position,
description: record.description,
});
setIsShelveModalVisible(true);
};
const handleShelveSubmit = async () => {
try {
const values = await shelveForm.validateFields();
const submitData = {
name: values.name,
type: values.type,
model: values.model,
serialNumber: values.serialNumber,
height: values.height || 1,
powerConsumption: values.powerConsumption || 0,
rackId: values.rackId,
position: values.position,
description: values.description || '',
};
await axios.put(`/api/idle-devices/${shelvingDevice.deviceId}/shelve`, submitData);
message.success('设备上架成功');
setIsShelveModalVisible(false);
fetchIdleDevices();
} catch (error) {
message.error(error.response?.data?.error || '上架失败');
}
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
const submitData = { ...values };
if (submitData.warehouseId) {
submitData.rackId = null;
submitData.position = null;
submitData.sourceType = 'warehouse';
} else if (submitData.rackId) {
submitData.warehouseId = null;
submitData.sourceType = 'rack';
}
if (editingDevice) {
await axios.put(`/api/idle-devices/${editingDevice.deviceId}`, submitData);
message.success('更新成功');
} else {
const response = await axios.post('/api/idle-devices', submitData);
message.success(`添加成功,设备ID${response.data.deviceId}`);
}
setIsModalVisible(false);
fetchIdleDevices();
} catch (error) {
message.error(error.response?.data?.error || '操作失败');
}
};
const getIdleDays = (idleDate) => {
if (!idleDate) return 0;
const diff = new Date() - new Date(idleDate);
return Math.floor(diff / (1000 * 60 * 60 * 24));
};
const columns = [
{
title: '序号',
key: 'index',
width: 60,
align: 'center',
render: (_, __, index) => (
<Badge count={index + 1 + (pagination.current - 1) * pagination.pageSize} style={{ backgroundColor: '#f59e0b' }} />
),
},
{
title: '设备信息',
key: 'deviceInfo',
width: 220,
render: (_, record) => (
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Avatar
style={{ backgroundColor: record.type === 'server' ? '#3b82f6' : record.type === 'switch' ? '#8b5cf6' : '#64748b' }}
icon={<InboxOutlined />}
/>
<div>
<Text strong style={{ fontSize: '14px', display: 'block' }}>{record.name || '-'}</Text>
<Space size={4}>
<Tag color={record.type === 'server' ? 'blue' : record.type === 'switch' ? 'purple' : 'default'} style={{ marginRight: 0 }}>
{record.type === 'server' ? '服务器' : record.type === 'switch' ? '交换机' : '其他'}
</Tag>
<Text type="secondary" style={{ fontSize: '12px' }}>{record.model || '-'}</Text>
</Space>
</div>
</div>
),
},
{
title: '设备ID',
dataIndex: 'deviceId',
key: 'deviceId',
width: 100,
render: (text) => (
<Text code style={{ fontSize: '12px', padding: '2px 6px' }}>{text}</Text>
),
},
{
title: '位置',
key: 'location',
width: 160,
render: (_, record) => {
if (record.sourceType === 'warehouse' && record.warehouseId) {
return (
<Space>
<InboxOutlined style={{ color: '#64748b' }} />
<Text type="secondary">{record.warehouseId}</Text>
</Space>
);
}
if (record.sourceType === 'rack' && record.Rack) {
const location = [record.Rack.Room?.name, record.Rack.name, record.position ? `U${record.position}` : null].filter(Boolean).join(' / ');
return (
<Space>
<InboxOutlined style={{ color: '#3b82f6' }} />
<Text type="secondary">{location || '-'}</Text>
</Space>
);
}
return <Text type="secondary">-</Text>;
},
},
{
title: '空闲天数',
key: 'idleDays',
width: 100,
align: 'center',
render: (_, record) => {
const days = getIdleDays(record.idleDate);
const color = days > 30 ? '#ef4444' : days > 7 ? '#f59e0b' : '#22c55e';
return (
<div style={{ textAlign: 'center' }}>
<Text style={{ color, fontWeight: 600, fontSize: '16px' }}>{days}</Text>
<br />
<Text type="secondary" style={{ fontSize: '11px' }}></Text>
</div>
);
},
},
{
title: '空闲原因',
dataIndex: 'idleReason',
key: 'idleReason',
width: 140,
ellipsis: true,
render: (text) => (
<Tooltip title={text || '-'}>
<Text type="secondary" ellipsis>{text || '-'}</Text>
</Tooltip>
),
},
{
title: '来源',
dataIndex: 'sourceType',
key: 'sourceType',
width: 80,
align: 'center',
render: (type) => (
<Tag color={type === 'warehouse' ? 'green' : 'blue'}>
{type === 'warehouse' ? '库房' : '机架'}
</Tag>
),
},
{
title: '操作',
key: 'action',
width: 140,
fixed: 'right',
align: 'center',
render: (_, record) => (
<Space size="small">
<Tooltip title="上架">
<Button
type="text"
icon={<UploadOutlined style={{ color: '#22c55e' }} />}
onClick={() => handleShelve(record)}
style={{ borderRadius: '6px' }}
/>
</Tooltip>
<Tooltip title="编辑">
<Button type="text" icon={<EditOutlined />} onClick={() => handleEdit(record)} style={{ borderRadius: '6px', color: '#3b82f6' }} />
</Tooltip>
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.deviceId)} okText="确认" cancelText="取消" okButtonProps={{ danger: true }}>
<Tooltip title="删除">
<Button type="text" danger icon={<DeleteOutlined />} style={{ borderRadius: '6px' }} />
</Tooltip>
</Popconfirm>
</Space>
),
},
];
const statCards = [
{
title: '空闲设备总数',
value: pagination.total,
icon: <InboxOutlined />,
color: '#f59e0b',
bg: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
},
{
title: '本周新增',
value: idleDevices.filter(d => {
if (!d.idleDate) return false;
const diff = new Date() - new Date(d.idleDate);
return diff < 7 * 24 * 60 * 60 * 1000;
}).length,
icon: <PlusOutlined />,
color: '#22c55e',
bg: 'linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%)',
},
{
title: '长期空闲(>30天)',
value: idleDevices.filter(d => getIdleDays(d.idleDate) > 30).length,
icon: <ClockCircleOutlined />,
color: '#ef4444',
bg: 'linear-gradient(135deg, #fee2e2 0%, #fecaca 100%)',
},
];
return (
<div style={{ minHeight: '100vh', background: '#f8fafc', padding: '24px' }}>
<div style={{ marginBottom: '24px' }}>
<Title level={4} style={{ marginBottom: '4px', color: '#1e293b' }}>空闲设备管理</Title>
<Text type="secondary">管理已下线或空闲的设备支持恢复领用到设备管理</Text>
</div>
<Row gutter={[16, 16]} style={{ marginBottom: '24px' }}>
{statCards.map((stat, index) => (
<Col key={index} xs={12} sm={8} md={8}>
<Card
style={{
borderRadius: '16px',
border: 'none',
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
background: stat.bg,
}}
bodyStyle={{ padding: '20px' }}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<Text type="secondary" style={{ fontSize: '13px' }}>{stat.title}</Text>
<div style={{ fontSize: '28px', fontWeight: 700, color: stat.color, lineHeight: 1.2, marginTop: '4px' }}>
{stat.value}
</div>
</div>
<div style={{
width: '48px',
height: '48px',
borderRadius: '12px',
background: 'rgba(255,255,255,0.7)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '20px',
color: stat.color
}}>
{stat.icon}
</div>
</div>
</Card>
</Col>
))}
</Row>
<Card
style={{
borderRadius: '16px',
border: 'none',
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
}}
bodyStyle={{ padding: 0 }}
>
<div style={{
padding: '20px 24px',
borderBottom: '1px solid #f1f5f9',
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
borderRadius: '16px 16px 0 0',
}}>
<Row gutter={16} align="middle">
<Col flex="auto">
<Space size="middle" wrap>
<Input
placeholder="搜索设备ID/名称/序列号"
prefix={<SearchOutlined style={{ color: '#94a3b8' }} />}
style={{ borderRadius: '10px', width: '260px', height: '40px' }}
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
allowClear
/>
<Select
value={sourceTypeFilter}
onChange={setSourceTypeFilter}
style={{ width: 120, height: 40 }}
>
<Option value="all">全部来源</Option>
<Option value="rack">机架</Option>
<Option value="warehouse">库房</Option>
</Select>
<Button icon={<ReloadOutlined />} onClick={fetchIdleDevices} style={{ height: 40, borderRadius: '10px' }}>
刷新
</Button>
</Space>
</Col>
<Col>
<Space size="middle">
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAdd}
style={{
height: 40,
borderRadius: '10px',
background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
border: 'none',
boxShadow: '0 4px 12px rgba(245, 158, 11, 0.3)',
}}
>
添加空闲设备
</Button>
</Space>
</Col>
</Row>
</div>
<Table
columns={columns}
dataSource={idleDevices}
rowKey="deviceId"
loading={loading}
pagination={false}
scroll={{ x: 1100 }}
rowClassName={(record, index) => index % 2 === 0 ? 'table-row-even' : 'table-row-odd'}
style={{ borderRadius: '0 0 16px 16px' }}
/>
{pagination.total > 0 && (
<div style={{ padding: '16px 24px', borderTop: '1px solid #f1f5f9', background: '#fafafa' }}>
<Row justify="space-between" align="middle">
<Col>
<Text type="secondary">
<Text strong>{pagination.total}</Text> 条记录
</Text>
</Col>
<Col>
<Pagination
current={pagination.current}
pageSize={pagination.pageSize}
total={pagination.total}
onChange={(page, pageSize) =>
setPagination((prev) => ({ ...prev, current: page, pageSize }))
}
showSizeChanger
showQuickJumper
showTotal={(total) => `${total}`}
size="small"
/>
</Col>
</Row>
</div>
)}
</Card>
<Modal
title={
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{
width: '8px',
height: '8px',
borderRadius: '50%',
background: editingDevice ? '#3b82f6' : '#f59e0b'
}} />
{editingDevice ? '编辑空闲设备' : '添加空闲设备'}
</div>
}
open={isModalVisible}
onOk={handleSubmit}
onCancel={() => setIsModalVisible(false)}
okText="确定"
cancelText="取消"
width={680}
destroyOnClose
bodyStyle={{ padding: '24px' }}
style={{ top: 100 }}
>
<Form form={form} layout="vertical" size="middle">
<div style={{
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
borderRadius: '12px',
padding: '20px',
marginBottom: '16px',
border: '1px solid #e2e8f0'
}}>
<div style={{
fontSize: '14px',
fontWeight: 600,
color: '#334155',
marginBottom: '16px',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<div style={{ width: '4px', height: '16px', background: '#3b82f6', borderRadius: '2px' }} />
设备基本信息
</div>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="name" label="设备名称" rules={[{ required: true, message: '请输入设备名称' }]}>
<Input placeholder="请输入设备名称" style={{ borderRadius: '8px' }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="type" label="设备类型">
<Select placeholder="请选择设备类型" allowClear style={{ borderRadius: '8px' }}>
<Option value="server">服务器</Option>
<Option value="switch">交换机</Option>
<Option value="router">路由器</Option>
<Option value="storage">存储设备</Option>
<Option value="other">其他</Option>
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="model" label="设备型号">
<Input placeholder="请输入设备型号" style={{ borderRadius: '8px' }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="serialNumber" label="序列号">
<Input placeholder="请输入序列号" style={{ borderRadius: '8px' }} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="powerConsumption" label="功耗(W)">
<Input type="number" placeholder="请输入功耗" min={0} style={{ borderRadius: '8px' }} />
</Form.Item>
</Col>
{editingDevice && (
<Col span={12}>
<Form.Item label="设备ID">
<Input value={editingDevice.deviceId} disabled style={{ borderRadius: '8px' }} />
</Form.Item>
</Col>
)}
</Row>
</div>
<div style={{
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
borderRadius: '12px',
padding: '20px',
marginBottom: '16px',
border: '1px solid #e2e8f0'
}}>
<div style={{
fontSize: '14px',
fontWeight: 600,
color: '#334155',
marginBottom: '16px',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<div style={{ width: '4px', height: '16px', background: '#22c55e', borderRadius: '2px' }} />
位置信息
</div>
<Row gutter={16}>
<Col span={8}>
<Form.Item name="roomId" label="机房">
<Select
placeholder="请选择机房"
allowClear
onChange={(value) => {
setSelectedRoomId(value);
form.setFieldsValue({ rackId: null, position: null });
if (value) {
form.setFieldsValue({ warehouseId: null });
}
}}
style={{ borderRadius: '8px' }}
>
{rooms.map((room) => (
<Option key={room.roomId} value={room.roomId}>
{room.name}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="rackId" label="机柜">
<Select
placeholder={selectedRoomId ? "请选择机柜" : "请先选择机房"}
allowClear
disabled={!selectedRoomId}
style={{ borderRadius: '8px' }}
>
{racks
.filter((rack) => rack.roomId === selectedRoomId)
.map((rack) => (
<Option key={rack.rackId} value={rack.rackId}>
{rack.name}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="position" label="U位">
<Input type="number" placeholder="请输入U位" min={1} disabled={!selectedRoomId} style={{ borderRadius: '8px' }} />
</Form.Item>
</Col>
</Row>
<div style={{ textAlign: 'center', color: '#94a3b8', fontSize: '12px', margin: '8px 0' }}>
</div>
<Row gutter={16}>
<Col span={24}>
<Form.Item name="warehouseId" label="库房位置">
<Input
placeholder="手动输入库房位置"
allowClear
onChange={() => {
if (form.getFieldValue('warehouseId')) {
form.setFieldsValue({ roomId: null, rackId: null, position: null });
setSelectedRoomId(null);
}
}}
style={{ borderRadius: '8px' }}
prefix={<InboxOutlined style={{ color: '#94a3b8' }} />}
/>
</Form.Item>
</Col>
</Row>
</div>
<div style={{
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
borderRadius: '12px',
padding: '20px',
border: '1px solid #e2e8f0'
}}>
<div style={{
fontSize: '14px',
fontWeight: 600,
color: '#334155',
marginBottom: '16px',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<div style={{ width: '4px', height: '16px', background: '#64748b', borderRadius: '2px' }} />
附加信息
</div>
<Row gutter={16}>
<Col span={24}>
<Form.Item name="idleReason" label="空闲原因">
<Input placeholder="请输入空闲原因,如:设备下线、备件库存等" style={{ borderRadius: '8px' }} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={24}>
<Form.Item name="description" label="备注">
<TextArea rows={2} placeholder="请输入备注信息" style={{ borderRadius: '8px' }} />
</Form.Item>
</Col>
</Row>
</div>
</Form>
</Modal>
<Modal
title={
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ width: '8px', height: '8px', borderRadius: '50%', background: '#22c55e' }} />
设备上架
</div>
}
open={isShelveModalVisible}
onOk={handleShelveSubmit}
onCancel={() => setIsShelveModalVisible(false)}
okText="确认上架"
cancelText="取消"
width={680}
destroyOnClose
bodyStyle={{ padding: '24px' }}
>
<Form form={shelveForm} layout="vertical" size="middle">
<div style={{
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
borderRadius: '12px',
padding: '20px',
marginBottom: '16px',
border: '1px solid #e2e8f0'
}}>
<div style={{
fontSize: '14px',
fontWeight: 600,
color: '#334155',
marginBottom: '16px',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<div style={{ width: '4px', height: '16px', background: '#3b82f6', borderRadius: '2px' }} />
设备基本信息
</div>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="name" label="设备名称" rules={[{ required: true, message: '请输入设备名称' }]}>
<Input placeholder="请输入设备名称" style={{ borderRadius: '8px' }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="type" label="设备类型" rules={[{ required: true, message: '请选择设备类型' }]}>
<Select placeholder="请选择设备类型" style={{ borderRadius: '8px' }}>
<Option value="server">服务器</Option>
<Option value="switch">交换机</Option>
<Option value="router">路由器</Option>
<Option value="storage">存储设备</Option>
<Option value="other">其他设备</Option>
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="model" label="设备型号">
<Input placeholder="请输入设备型号" style={{ borderRadius: '8px' }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="serialNumber" label="序列号" rules={[{ required: true, message: '请输入序列号' }]}>
<Input placeholder="请输入序列号" style={{ borderRadius: '8px' }} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="height" label="高度(U)">
<Input type="number" placeholder="请输入高度" min={1} style={{ borderRadius: '8px' }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="powerConsumption" label="功率(W)">
<Input type="number" placeholder="请输入功率" min={0} style={{ borderRadius: '8px' }} />
</Form.Item>
</Col>
</Row>
</div>
<div style={{
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
borderRadius: '12px',
padding: '20px',
marginBottom: '16px',
border: '1px solid #e2e8f0'
}}>
<div style={{
fontSize: '14px',
fontWeight: 600,
color: '#334155',
marginBottom: '16px',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<div style={{ width: '4px', height: '16px', background: '#22c55e', borderRadius: '2px' }} />
上架位置
</div>
<Row gutter={16}>
<Col span={8}>
<Form.Item name="roomId" label="机房">
<Select
placeholder="请选择机房"
onChange={(value) => {
setSelectedShelveRoomId(value);
shelveForm.setFieldsValue({ rackId: null });
}}
style={{ borderRadius: '8px' }}
>
{rooms.map((room) => (
<Option key={room.roomId} value={room.roomId}>
{room.name}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="rackId" label="机柜">
<Select
placeholder={selectedShelveRoomId ? "请选择机柜" : "请先选择机房"}
disabled={!selectedShelveRoomId}
style={{ borderRadius: '8px' }}
>
{racks
.filter((rack) => rack.roomId === selectedShelveRoomId)
.map((rack) => (
<Option key={rack.rackId} value={rack.rackId}>
{rack.name}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="position" label="U位">
<Input type="number" placeholder="请输入U位" min={1} style={{ borderRadius: '8px' }} />
</Form.Item>
</Col>
</Row>
</div>
<div style={{
background: 'linear-gradient(135deg, #fefefe 0%, #f8fafc 100%)',
borderRadius: '12px',
padding: '20px',
border: '1px solid #e2e8f0'
}}>
<div style={{
fontSize: '14px',
fontWeight: 600,
color: '#334155',
marginBottom: '16px',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<div style={{ width: '4px', height: '16px', background: '#64748b', borderRadius: '2px' }} />
备注信息
</div>
<Row gutter={16}>
<Col span={24}>
<Form.Item name="description" label="备注">
<TextArea rows={2} placeholder="请输入备注信息" style={{ borderRadius: '8px' }} />
</Form.Item>
</Col>
</Row>
</div>
</Form>
</Modal>
<style>{`
.ant-table-thead > tr > th {
background: #f8fafc !important;
font-weight: 600 !important;
color: #334155 !important;
border-bottom: 2px solid #e2e8f0 !important;
}
.ant-table-tbody > tr > td {
border-bottom: 1px solid #f1f5f9 !important;
padding: 16px 12px !important;
}
.ant-table-tbody > tr:hover > td {
background: #fafafa !important;
}
.ant-badge-count {
box-shadow: none !important;
}
.ant-btn-primary:hover {
opacity: 0.9 !important;
}
`}</style>
</div>
);
};
export default IdleDeviceManagement;
+17 -8
View File
@@ -183,14 +183,23 @@ const InventoryManagement = () => {
};
const handleDelete = async (planId) => {
try {
await api.delete(`/inventory/plans/${planId}`);
message.success('删除成功');
fetchPlans();
fetchStats();
} catch (error) {
message.error('删除失败');
}
Modal.confirm({
title: '确认删除',
content: '确定要删除这个盘点计划吗?此操作不可恢复!',
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
await api.delete(`/inventory/plans/${planId}`);
message.success('删除成功');
fetchPlans();
fetchStats();
} catch (error) {
message.error('删除失败');
}
},
});
};
const handleSubmit = async (values) => {
+482
View File
@@ -0,0 +1,482 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Table,
Card,
Space,
Select,
DatePicker,
Input,
Tag,
Button,
message,
Modal,
Row,
Col,
Descriptions,
Typography,
Drawer,
Statistic,
} from 'antd';
import {
HistoryOutlined,
SearchOutlined,
EyeOutlined,
FilterOutlined,
ClearOutlined,
} from '@ant-design/icons';
import api from '../api';
import CloseButton from '../components/CloseButton';
import dayjs from 'dayjs';
import { selectStyles, filterInputStyles, inputPlaceholders } from '../styles/deviceManagementStyles';
const { RangePicker } = DatePicker;
const { Option } = Select;
const { Text } = Typography;
const MODULE_OPTIONS = [
{ value: 'device', label: '设备管理' },
{ value: 'user', label: '用户管理' },
{ value: 'role', label: '角色管理' },
];
const OPERATION_TYPE_OPTIONS = [
{ value: 'create', label: '创建' },
{ value: 'update', label: '更新' },
{ value: 'delete', label: '删除' },
{ value: 'batch_delete', label: '批量删除' },
{ value: 'status_change', label: '状态变更' },
{ value: 'move', label: '移动' },
{ value: 'permission_change', label: '权限变更' },
];
const RESULT_OPTIONS = [
{ value: 'success', label: '成功' },
{ value: 'failed', label: '失败' },
];
function OperationLogs() {
const [logs, setLogs] = useState([]);
const [loading, setLoading] = useState(true);
const [pagination, setPagination] = useState({ current: 1, pageSize: 20, total: 0 });
const [filters, setFilters] = useState({
module: null,
operationType: null,
keyword: '',
dateRange: null,
result: null,
});
const [detailVisible, setDetailVisible] = useState(false);
const [currentLog, setCurrentLog] = useState(null);
const [detailLoading, setDetailLoading] = useState(false);
const fetchLogs = useCallback(async (page = 1, pageSize = 20, currentFilters = filters) => {
try {
setLoading(true);
const params = { page, pageSize };
if (currentFilters.module) {
params.module = currentFilters.module;
}
if (currentFilters.operationType) {
params.operationType = currentFilters.operationType;
}
if (currentFilters.keyword) {
params.keyword = currentFilters.keyword;
}
if (currentFilters.result) {
params.result = currentFilters.result;
}
if (currentFilters.dateRange && currentFilters.dateRange.length === 2) {
params.startDate = currentFilters.dateRange[0].format('YYYY-MM-DD');
params.endDate = currentFilters.dateRange[1].format('YYYY-MM-DD');
}
const response = await api.get('/operation-logs', { params });
if (response.success) {
setLogs(response.data.logs);
setPagination(prev => ({
...prev,
current: page,
pageSize,
total: response.data.total
}));
}
} catch (error) {
message.error('获取操作日志失败');
console.error('获取操作日志失败:', error);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchLogs(1, pagination.pageSize, filters);
}, []);
const handleTableChange = (newPagination, tableFilters) => {
fetchLogs(newPagination.current, newPagination.pageSize, filters);
};
const handleFilterChange = (key, value) => {
const newFilters = { ...filters, [key]: value };
setFilters(newFilters);
fetchLogs(1, pagination.pageSize, newFilters);
};
const handleClearFilters = () => {
const clearedFilters = {
module: null,
operationType: null,
keyword: '',
dateRange: null,
result: null,
};
setFilters(clearedFilters);
fetchLogs(1, pagination.pageSize, clearedFilters);
};
const handleViewDetail = async (record) => {
setDetailLoading(true);
setDetailVisible(true);
try {
const response = await api.get(`/operation-logs/${record.recordId}`);
if (response.success) {
setCurrentLog(response.data);
}
} catch (error) {
message.error('获取日志详情失败');
console.error('获取日志详情失败:', error);
} finally {
setDetailLoading(false);
}
};
const getModuleTag = (module) => {
const colors = {
device: 'blue',
user: 'green',
role: 'purple',
consumable: 'orange',
rack: 'cyan',
room: 'magenta',
ticket: 'red',
backup: 'gold',
};
const names = {
device: '设备',
user: '用户',
role: '角色',
consumable: '耗材',
rack: '机柜',
room: '机房',
ticket: '工单',
backup: '备份',
};
return <Tag color={colors[module] || 'default'}>{names[module] || module}</Tag>;
};
const getOperationTag = (type) => {
const colors = {
create: 'green',
update: 'blue',
delete: 'red',
batch_delete: 'red',
batch_update: 'orange',
status_change: 'cyan',
move: 'purple',
permission_change: 'gold',
import: 'lime',
export: 'lime',
};
const names = {
create: '创建',
update: '更新',
delete: '删除',
batch_delete: '批量删除',
batch_update: '批量更新',
status_change: '状态变更',
move: '移动',
permission_change: '权限变更',
import: '导入',
export: '导出',
};
return <Tag color={colors[type] || 'default'}>{names[type] || type}</Tag>;
};
const getResultTag = (result) => {
return result === 'success'
? <Tag color="success">成功</Tag>
: <Tag color="error">失败</Tag>;
};
const columns = [
{
title: '时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 180,
sorter: (a, b) => new Date(a.createdAt) - new Date(b.createdAt),
render: date => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
},
{
title: '模块',
dataIndex: 'module',
key: 'module',
width: 100,
render: module => getModuleTag(module),
},
{
title: '操作类型',
dataIndex: 'operationType',
key: 'operationType',
width: 120,
render: type => getOperationTag(type),
},
{
title: '操作描述',
dataIndex: 'operationDescription',
key: 'operationDescription',
ellipsis: true,
},
{
title: '操作对象',
dataIndex: 'targetName',
key: 'targetName',
width: 150,
ellipsis: true,
render: (text, record) => text || record.targetId,
},
{
title: '操作人',
dataIndex: 'operatorName',
key: 'operatorName',
width: 120,
},
{
title: '结果',
dataIndex: 'result',
key: 'result',
width: 80,
render: result => getResultTag(result),
},
{
title: 'IP地址',
dataIndex: 'ipAddress',
key: 'ipAddress',
width: 140,
render: ip => ip || '-',
},
{
title: '操作',
key: 'action',
width: 80,
fixed: 'right',
render: (_, record) => (
<Button
type="link"
icon={<EyeOutlined />}
onClick={() => handleViewDetail(record)}
>
详情
</Button>
),
},
];
const hasFilters = filters.module || filters.operationType || filters.keyword || filters.dateRange || filters.result;
return (
<div style={{ padding: '24px' }}>
<Card
title={
<Space>
<HistoryOutlined />
<span>操作日志审计</span>
</Space>
}
extra={
<Space>
<Text type="secondary"> {pagination.total} 条记录</Text>
</Space>
}
>
<Card size="small" style={{ marginBottom: 16 }}>
<Row gutter={16} align="middle">
<Col flex="200px">
<Select
placeholder="选择模块"
allowClear
style={{ width: '100%', ...selectStyles }}
value={filters.module}
onChange={value => handleFilterChange('module', value)}
>
{MODULE_OPTIONS.map(opt => (
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
))}
</Select>
</Col>
<Col flex="200px">
<Select
placeholder="选择操作类型"
allowClear
style={{ width: '100%', ...selectStyles }}
value={filters.operationType}
onChange={value => handleFilterChange('operationType', value)}
>
{OPERATION_TYPE_OPTIONS.map(opt => (
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
))}
</Select>
</Col>
<Col flex="200px">
<Select
placeholder="选择结果"
allowClear
style={{ width: '100%', ...selectStyles }}
value={filters.result}
onChange={value => handleFilterChange('result', value)}
>
{RESULT_OPTIONS.map(opt => (
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
))}
</Select>
</Col>
<Col flex="auto">
<Input
placeholder={inputPlaceholders.keyword || '搜索操作描述/对象/操作人'}
prefix={<SearchOutlined />}
style={{ ...filterInputStyles }}
value={filters.keyword}
onChange={e => handleFilterChange('keyword', e.target.value)}
allowClear
/>
</Col>
<Col flex="280px">
<RangePicker
style={{ width: '100%' }}
value={filters.dateRange}
onChange={dates => handleFilterChange('dateRange', dates)}
placeholder={['开始日期', '结束日期']}
/>
</Col>
<Col>
<Space>
<Button
icon={<FilterOutlined />}
onClick={() => fetchLogs(1, pagination.pageSize, filters)}
>
筛选
</Button>
{hasFilters && (
<Button
icon={<ClearOutlined />}
onClick={handleClearFilters}
>
清除
</Button>
)}
</Space>
</Col>
</Row>
</Card>
<Table
columns={columns}
dataSource={logs}
loading={loading}
rowKey="recordId"
pagination={{
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
pageSizeOptions: ['10', '20', '50', '100'],
}}
onChange={handleTableChange}
scroll={{ x: 1200 }}
/>
</Card>
<Drawer
title="操作日志详情"
placement="right"
width={600}
onClose={() => {
setDetailVisible(false);
setCurrentLog(null);
}}
open={detailVisible}
extra={
currentLog && (
<Space>
{getModuleTag(currentLog.module)}
{getOperationTag(currentLog.operationType)}
{getResultTag(currentLog.result)}
</Space>
)
}
>
{detailLoading ? (
<div style={{ textAlign: 'center', padding: 40 }}>加载中...</div>
) : currentLog ? (
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="日志ID">{currentLog.recordId}</Descriptions.Item>
<Descriptions.Item label="操作时间">
{dayjs(currentLog.createdAt).format('YYYY-MM-DD HH:mm:ss')}
</Descriptions.Item>
<Descriptions.Item label="模块">{currentLog.module}</Descriptions.Item>
<Descriptions.Item label="操作类型">{currentLog.operationType}</Descriptions.Item>
<Descriptions.Item label="操作描述">{currentLog.operationDescription}</Descriptions.Item>
<Descriptions.Item label="目标ID">{currentLog.targetId || '-'}</Descriptions.Item>
<Descriptions.Item label="目标名称">{currentLog.targetName || '-'}</Descriptions.Item>
<Descriptions.Item label="操作人ID">{currentLog.operatorId}</Descriptions.Item>
<Descriptions.Item label="操作人">{currentLog.operatorName}</Descriptions.Item>
{currentLog.operatorRole && (
<Descriptions.Item label="操作人角色">{currentLog.operatorRole}</Descriptions.Item>
)}
<Descriptions.Item label="IP地址">{currentLog.ipAddress || '-'}</Descriptions.Item>
<Descriptions.Item label="用户代理">{currentLog.userAgent || '-'}</Descriptions.Item>
<Descriptions.Item label="结果">
{currentLog.result === 'success' ? '成功' : '失败'}
</Descriptions.Item>
</Descriptions>
) : null}
{currentLog && (currentLog.beforeState || currentLog.afterState) && (
<>
<h4 style={{ marginTop: 16 }}>状态变更</h4>
{currentLog.beforeState && (
<>
<Text strong>变更前</Text>
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4, overflow: 'auto', maxHeight: 200 }}>
{JSON.stringify(currentLog.beforeState, null, 2)}
</pre>
</>
)}
{currentLog.afterState && (
<>
<Text strong>变更后</Text>
<pre style={{ background: '#f0f0f0', padding: 12, borderRadius: 4, overflow: 'auto', maxHeight: 200 }}>
{JSON.stringify(currentLog.afterState, null, 2)}
</pre>
</>
)}
</>
)}
{currentLog && currentLog.metadata && Object.keys(currentLog.metadata).length > 0 && (
<>
<h4 style={{ marginTop: 16 }}>扩展信息</h4>
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4, overflow: 'auto', maxHeight: 200 }}>
{JSON.stringify(currentLog.metadata, null, 2)}
</pre>
</>
)}
</Drawer>
</div>
);
}
export default OperationLogs;
+17 -8
View File
@@ -284,14 +284,23 @@ const PendingDeviceManagement = () => {
};
const handleDelete = async (pendingId) => {
try {
await api.delete(`/inventory/pending-devices/${pendingId}`);
message.success('删除成功');
fetchPendingDevices();
fetchStats();
} catch (error) {
message.error(error.response?.data?.error || '删除失败');
}
Modal.confirm({
title: '确认删除',
content: '确定要删除这个待同步设备吗?此操作不可恢复!',
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
await api.delete(`/inventory/pending-devices/${pendingId}`);
message.success('删除成功');
fetchPendingDevices();
fetchStats();
} catch (error) {
message.error(error.response?.data?.error || '删除失败');
}
},
});
};
const getStatusTag = (status) => {
+20 -11
View File
@@ -298,17 +298,26 @@ function PortManagement() {
};
const handleDelete = async portId => {
try {
await api.delete(`/device-ports/${portId}`);
message.success({
content: '删除成功',
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
});
fetchPorts();
} catch (error) {
message.error('删除失败');
console.error('删除失败:', error);
}
Modal.confirm({
title: '确认删除',
content: '确定要删除这个端口吗?此操作不可恢复!',
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
await api.delete(`/device-ports/${portId}`);
message.success({
content: '删除成功',
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
});
fetchPorts();
} catch (error) {
message.error('删除失败');
console.error('删除失败:', error);
}
},
});
};
const parsePortRange = portName => {
+1 -1
View File
@@ -291,7 +291,7 @@ function RoomManagement() {
try {
setLoading(true);
const response = await axios.get('/api/rooms');
setRooms(response.data);
setRooms(response.data.rooms || []);
} catch (error) {
message.error('获取机房列表失败');
console.error('获取机房列表失败:', error);
@@ -83,14 +83,23 @@ function TicketCategoryManagement() {
};
const handleDelete = async categoryId => {
try {
await axios.delete(`/api/ticket-categories/${categoryId}`);
message.success('分类删除成功');
fetchCategories();
} catch (error) {
message.error('分类删除失败');
console.error(error);
}
Modal.confirm({
title: '确认删除',
content: '确定要删除这个故障分类吗?此操作不可恢复!',
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
await axios.delete(`/api/ticket-categories/${categoryId}`);
message.success('分类删除成功');
fetchCategories();
} catch (error) {
message.error('分类删除失败');
console.error(error);
}
},
});
};
const columns = [
+20 -11
View File
@@ -173,17 +173,26 @@ const UserManagement = () => {
const handleDelete = useCallback(
async userId => {
try {
const response = await userAPI.delete(userId);
if (response.success) {
message.success('删除成功');
fetchUsers();
} else {
message.error(response.message || '删除失败');
}
} catch (error) {
message.error('删除失败');
}
Modal.confirm({
title: '确认删除',
content: '确定要删除这个用户吗?此操作不可恢复!',
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
const response = await userAPI.delete(userId);
if (response.success) {
message.success('删除成功');
fetchUsers();
} else {
message.error(response.message || '删除失败');
}
} catch (error) {
message.error('删除失败');
}
},
});
},
[fetchUsers]
);