feat: 添加操作日志、危险操作确认和业务关联功能
1. 新增操作日志记录功能,记录关键操作 2. 实现危险操作确认对话框,防止误删 3. 添加业务和库房管理模块 4. 支持设备标记为空闲状态 5. 完善API文档和健康检查 6. 优化前端删除操作的确认流程 7. 添加Swagger API文档支持 8. 实现设备与业务的关联功能 9. 改进设备模型,添加空闲相关字段 10. 优化用户、角色管理操作日志
This commit is contained in:
+17
-3
@@ -80,7 +80,9 @@ const PendingDeviceManagement = lazy(() => import('./pages/PendingDeviceManageme
|
||||
const BackupManagement = lazy(() => import('./pages/BackupManagement'));
|
||||
const AutoBackupSettings = lazy(() => import('./pages/AutoBackupSettings'));
|
||||
const RemoteBackupSettings = lazy(() => import('./pages/RemoteBackupSettings'));
|
||||
const OperationLogs = lazy(() => import('./pages/OperationLogs'));
|
||||
const ErrorBoundaryTest = lazy(() => import('./pages/ErrorBoundaryTest'));
|
||||
const IdleDeviceManagement = lazy(() => import('./pages/IdleDeviceManagement'));
|
||||
|
||||
const { Header, Content, Sider } = Layout;
|
||||
|
||||
@@ -191,11 +193,11 @@ const AppLayout = ({ children }) => {
|
||||
if (path === '/') return 'dashboard';
|
||||
if (path.startsWith('/visualization-3d')) return 'visualization-3d';
|
||||
if (path.startsWith('/rooms') || path.startsWith('/racks')) return 'room-management';
|
||||
if (
|
||||
path.startsWith('/devices') ||
|
||||
if (path.startsWith('/devices') ||
|
||||
path.startsWith('/fields') ||
|
||||
path.startsWith('/cables') ||
|
||||
path.startsWith('/ports')
|
||||
path.startsWith('/ports') ||
|
||||
path.startsWith('/idle-devices')
|
||||
)
|
||||
return 'asset-management';
|
||||
if (path.startsWith('/consumables')) return 'consumables-management';
|
||||
@@ -250,6 +252,11 @@ const AppLayout = ({ children }) => {
|
||||
icon: <CloudServerOutlined style={{ fontSize: '16px' }} />,
|
||||
label: <Link to="/devices">设备管理</Link>,
|
||||
},
|
||||
{
|
||||
key: 'idle-devices',
|
||||
icon: <CloudServerOutlined style={{ fontSize: '16px' }} />,
|
||||
label: <Link to="/idle-devices">空闲设备</Link>,
|
||||
},
|
||||
{
|
||||
key: 'fields',
|
||||
icon: <DatabaseOutlined style={{ fontSize: '16px' }} />,
|
||||
@@ -358,6 +365,11 @@ const AppLayout = ({ children }) => {
|
||||
icon: <DatabaseOutlined style={{ fontSize: '16px' }} />,
|
||||
label: <Link to="/backup">数据备份</Link>,
|
||||
},
|
||||
{
|
||||
key: 'operation-logs',
|
||||
icon: <AuditOutlined style={{ fontSize: '16px' }} />,
|
||||
label: <Link to="/operation-logs">操作日志</Link>,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -597,7 +609,9 @@ const routeConfig = [
|
||||
{ path: '/backup', component: BackupManagement },
|
||||
{ path: '/auto-backup-settings', component: AutoBackupSettings },
|
||||
{ path: '/remote-backup-settings', component: RemoteBackupSettings },
|
||||
{ path: '/operation-logs', component: OperationLogs },
|
||||
{ path: '/error-boundary-test', component: ErrorBoundaryTest },
|
||||
{ path: '/idle-devices', component: IdleDeviceManagement },
|
||||
];
|
||||
|
||||
const ThemeConfig = () => {
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Modal, Input, Alert, Typography, Space, Divider, Spin } from 'antd';
|
||||
import { WarningOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
RISK_LEVEL,
|
||||
RISK_CONFIG,
|
||||
OPERATION_TYPES,
|
||||
OPERATION_LABELS,
|
||||
ENTITY_LABELS,
|
||||
getRiskLevel,
|
||||
} from '../config/dangerousOperationConfig';
|
||||
|
||||
const { Text, Paragraph, Title } = Typography;
|
||||
|
||||
export const DangerConfirmModal = ({
|
||||
open,
|
||||
operationType = OPERATION_TYPES.DELETE_SINGLE,
|
||||
entityType = 'item',
|
||||
items = [],
|
||||
itemCount = 1,
|
||||
title = '',
|
||||
description = '',
|
||||
impactDetails = {},
|
||||
onConfirm,
|
||||
onCancel,
|
||||
okText = '确认',
|
||||
cancelText = '取消',
|
||||
}) => {
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
const riskLevel = getRiskLevel(operationType, itemCount, {
|
||||
isSystemLevel: impactDetails.isSystemLevel,
|
||||
hasRelatedData: impactDetails.relatedDataCount > 0,
|
||||
});
|
||||
|
||||
const config = RISK_CONFIG[riskLevel];
|
||||
const entityLabel = ENTITY_LABELS[entityType] || entityType;
|
||||
const operationLabel = OPERATION_LABELS[operationType] || operationType;
|
||||
|
||||
const isKeywordRequired = riskLevel === RISK_LEVEL.EXTREME && config.requireKeyword;
|
||||
const isKeywordValid = !isKeywordRequired || keyword.toUpperCase() === config.keyword;
|
||||
|
||||
useEffect(() => {
|
||||
if (open && isKeywordRequired) {
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 100);
|
||||
}
|
||||
}, [open, isKeywordRequired]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setKeyword('');
|
||||
setConfirmLoading(false);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleOk = async () => {
|
||||
if (!isKeywordValid) return;
|
||||
|
||||
setConfirmLoading(true);
|
||||
try {
|
||||
await onConfirm?.();
|
||||
} finally {
|
||||
setConfirmLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderImpactDetails = () => {
|
||||
if (!impactDetails || Object.keys(impactDetails).length === 0) return null;
|
||||
|
||||
const items = [];
|
||||
|
||||
if (impactDetails.relatedDevices !== undefined) {
|
||||
items.push({ label: '关联设备', value: `${impactDetails.relatedDevices} 个` });
|
||||
}
|
||||
if (impactDetails.relatedCables !== undefined) {
|
||||
items.push({ label: '关联接线', value: `${impactDetails.relatedCables} 条` });
|
||||
}
|
||||
if (impactDetails.relatedPorts !== undefined) {
|
||||
items.push({ label: '关联端口', value: `${impactDetails.relatedPorts} 个` });
|
||||
}
|
||||
if (impactDetails.relatedNetworkCards !== undefined) {
|
||||
items.push({ label: '关联网卡', value: `${impactDetails.relatedNetworkCards} 张` });
|
||||
}
|
||||
if (impactDetails.relatedTickets !== undefined) {
|
||||
items.push({ label: '关联工单', value: `${impactDetails.relatedTickets} 个` });
|
||||
}
|
||||
if (impactDetails.relatedRacks !== undefined) {
|
||||
items.push({ label: '关联机柜', value: `${impactDetails.relatedRacks} 个` });
|
||||
}
|
||||
if (impactDetails.relatedRooms !== undefined) {
|
||||
items.push({ label: '关联机房', value: `${impactDetails.relatedRooms} 个` });
|
||||
}
|
||||
if (impactDetails.relatedDataCount > 0) {
|
||||
items.push({ label: '其他关联数据', value: `${impactDetails.relatedDataCount} 项` });
|
||||
}
|
||||
if (impactDetails.totalAffected !== undefined) {
|
||||
items.push({ label: '总影响数量', value: `${impactDetails.totalAffected} 项` });
|
||||
}
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div style={styles.impactSection}>
|
||||
<Text strong style={{ display: 'block', marginBottom: 8, color: config.color }}>
|
||||
⚠️ 影响范围
|
||||
</Text>
|
||||
<div style={styles.impactList}>
|
||||
{items.map((item, index) => (
|
||||
<div key={index} style={styles.impactItem}>
|
||||
<Text type="secondary">{item.label}:</Text>
|
||||
<Text strong>{item.value}</Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<div style={styles.contentContainer}>
|
||||
<Alert
|
||||
message={
|
||||
<Space>
|
||||
<WarningOutlined style={{ color: config.color }} />
|
||||
<Text strong style={{ color: config.color, fontSize: 16 }}>
|
||||
{config.title}
|
||||
</Text>
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<Paragraph style={{ marginBottom: 0, marginTop: 8 }}>
|
||||
{description || `确定要${operationLabel} ${itemCount > 1 ? `${itemCount} 个` : '该'}${entityLabel}吗?`}
|
||||
</Paragraph>
|
||||
}
|
||||
type={riskLevel === RISK_LEVEL.EXTREME ? 'error' : riskLevel === RISK_LEVEL.HIGH ? 'warning' : 'info'}
|
||||
style={{
|
||||
backgroundColor: config.bgColor,
|
||||
borderColor: config.borderColor,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Divider style={{ margin: '16px 0' }} />
|
||||
|
||||
{riskLevel !== RISK_LEVEL.LOW && (
|
||||
<div style={styles.warningBox}>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
<CloseCircleOutlined style={{ color: '#ff4d4f', marginRight: 6 }} />
|
||||
此操作不可逆,一旦删除将无法恢复
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{config.showImpactDetails && renderImpactDetails()}
|
||||
|
||||
{isKeywordRequired && (
|
||||
<div style={styles.keywordSection}>
|
||||
<Divider style={{ margin: '16px 0' }} />
|
||||
<Alert
|
||||
message={
|
||||
<Text>
|
||||
为确认此{operationLabel}操作,请输入确认关键词:
|
||||
<Text code strong style={{ marginLeft: 8, fontSize: 14 }}>
|
||||
{config.keyword}
|
||||
</Text>
|
||||
</Text>
|
||||
}
|
||||
type="error"
|
||||
style={{ marginBottom: 12 }}
|
||||
showIcon
|
||||
/>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
placeholder={`请输入 ${config.keyword}`}
|
||||
status={keyword && !isKeywordValid ? 'error' : undefined}
|
||||
onPressEnter={handleOk}
|
||||
style={{ fontSize: 16, textAlign: 'center', letterSpacing: 2 }}
|
||||
/>
|
||||
{keyword && !isKeywordValid && (
|
||||
<Text type="danger" style={{ display: 'block', marginTop: 4, fontSize: 12 }}>
|
||||
关键词不正确,请重新输入
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{itemCount > 1 && itemCount <= 5 && items.length > 0 && (
|
||||
<div style={styles.itemPreview}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
即将删除:
|
||||
</Text>
|
||||
<div style={styles.itemList}>
|
||||
{items.slice(0, 5).map((item, index) => (
|
||||
<Text key={index} style={styles.itemTag}>
|
||||
{typeof item === 'string' ? item : item.name || item.label || item}
|
||||
</Text>
|
||||
))}
|
||||
{itemCount > 5 && (
|
||||
<Text type="secondary">...还有 {itemCount - 5} 项</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
<WarningOutlined style={{ color: config.color }} />
|
||||
<span style={{ color: config.color }}>
|
||||
{title || `${operationLabel}确认`}
|
||||
</span>
|
||||
</Space>
|
||||
}
|
||||
open={open}
|
||||
onOk={handleOk}
|
||||
onCancel={onCancel}
|
||||
okText={okText}
|
||||
cancelText={cancelText}
|
||||
okButtonProps={{
|
||||
danger: true,
|
||||
disabled: !isKeywordValid,
|
||||
loading: confirmLoading,
|
||||
}}
|
||||
cancelButtonProps={{
|
||||
disabled: confirmLoading,
|
||||
}}
|
||||
width={520}
|
||||
centered
|
||||
maskClosable={!confirmLoading}
|
||||
closable={!confirmLoading}
|
||||
>
|
||||
<Spin spinning={confirmLoading} tip="正在执行操作...">
|
||||
{renderContent()}
|
||||
</Spin>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
contentContainer: {
|
||||
padding: '8px 0',
|
||||
},
|
||||
impactSection: {
|
||||
backgroundColor: '#fafafa',
|
||||
padding: '12px 16px',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #f0f0f0',
|
||||
},
|
||||
impactList: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||
gap: 8,
|
||||
},
|
||||
impactItem: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
warningBox: {
|
||||
backgroundColor: '#fff2f0',
|
||||
padding: '10px 14px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #ffccc7',
|
||||
},
|
||||
keywordSection: {
|
||||
marginTop: 8,
|
||||
},
|
||||
itemPreview: {
|
||||
marginTop: 16,
|
||||
padding: 12,
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: 6,
|
||||
},
|
||||
itemList: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
marginTop: 8,
|
||||
},
|
||||
itemTag: {
|
||||
backgroundColor: '#fff',
|
||||
padding: '2px 8px',
|
||||
borderRadius: 4,
|
||||
border: '1px solid #d9d9d9',
|
||||
fontSize: 12,
|
||||
},
|
||||
};
|
||||
|
||||
export default DangerConfirmModal;
|
||||
@@ -0,0 +1,118 @@
|
||||
export const RISK_LEVEL = {
|
||||
EXTREME: 'EXTREME',
|
||||
HIGH: 'HIGH',
|
||||
MEDIUM: 'MEDIUM',
|
||||
LOW: 'LOW',
|
||||
};
|
||||
|
||||
export const RISK_CONFIG = {
|
||||
[RISK_LEVEL.EXTREME]: {
|
||||
title: '⚠️ 极高危险操作',
|
||||
color: '#ff4d4f',
|
||||
bgColor: '#fff2f0',
|
||||
borderColor: '#ffccc7',
|
||||
requireKeyword: true,
|
||||
keyword: 'CONFIRM',
|
||||
showImpactDetails: true,
|
||||
icon: '🔥',
|
||||
},
|
||||
[RISK_LEVEL.HIGH]: {
|
||||
title: '⚠️ 高风险操作',
|
||||
color: '#fa8c16',
|
||||
bgColor: '#fff7e6',
|
||||
borderColor: '#ffd591',
|
||||
requireKeyword: false,
|
||||
showImpactDetails: true,
|
||||
icon: '⚡',
|
||||
},
|
||||
[RISK_LEVEL.MEDIUM]: {
|
||||
title: '⚡ 操作确认',
|
||||
color: '#1890ff',
|
||||
bgColor: '#e6f7ff',
|
||||
borderColor: '#91d5ff',
|
||||
requireKeyword: false,
|
||||
showImpactDetails: false,
|
||||
icon: '💡',
|
||||
},
|
||||
[RISK_LEVEL.LOW]: {
|
||||
title: '确认操作',
|
||||
color: '#52c41a',
|
||||
bgColor: '#f6ffed',
|
||||
borderColor: '#b7eb8f',
|
||||
requireKeyword: false,
|
||||
showImpactDetails: false,
|
||||
icon: '✓',
|
||||
},
|
||||
};
|
||||
|
||||
export const OPERATION_TYPES = {
|
||||
DELETE_SINGLE: 'DELETE_SINGLE',
|
||||
DELETE_BATCH: 'DELETE_BATCH',
|
||||
DELETE_ALL: 'DELETE_ALL',
|
||||
UPDATE_BATCH: 'UPDATE_BATCH',
|
||||
RESTORE: 'RESTORE',
|
||||
PURGE: 'PURGE',
|
||||
};
|
||||
|
||||
export const getRiskLevel = (operationType, itemCount = 1, options = {}) => {
|
||||
const { hasRelatedData = false, isSystemLevel = false } = options;
|
||||
|
||||
if (operationType === OPERATION_TYPES.DELETE_ALL || isSystemLevel) {
|
||||
return RISK_LEVEL.EXTREME;
|
||||
}
|
||||
|
||||
if (operationType === OPERATION_TYPES.DELETE_BATCH) {
|
||||
if (itemCount > 10) {
|
||||
return RISK_LEVEL.EXTREME;
|
||||
}
|
||||
return itemCount > 3 ? RISK_LEVEL.HIGH : RISK_LEVEL.MEDIUM;
|
||||
}
|
||||
|
||||
if (hasRelatedData) {
|
||||
return RISK_LEVEL.MEDIUM;
|
||||
}
|
||||
|
||||
return RISK_LEVEL.LOW;
|
||||
};
|
||||
|
||||
export const OPERATION_LABELS = {
|
||||
[OPERATION_TYPES.DELETE_SINGLE]: '删除',
|
||||
[OPERATION_TYPES.DELETE_BATCH]: '批量删除',
|
||||
[OPERATION_TYPES.DELETE_ALL]: '删除所有',
|
||||
[OPERATION_TYPES.UPDATE_BATCH]: '批量更新',
|
||||
[OPERATION_TYPES.RESTORE]: '恢复',
|
||||
[OPERATION_TYPES.PURGE]: '清除',
|
||||
};
|
||||
|
||||
export const ENTITY_LABELS = {
|
||||
device: '设备',
|
||||
devices: '设备',
|
||||
rack: '机柜',
|
||||
racks: '机柜',
|
||||
room: '机房',
|
||||
rooms: '机房',
|
||||
cable: '接线',
|
||||
cables: '接线',
|
||||
port: '端口',
|
||||
ports: '端口',
|
||||
networkCard: '网卡',
|
||||
networkCards: '网卡',
|
||||
user: '用户',
|
||||
users: '用户',
|
||||
role: '角色',
|
||||
roles: '角色',
|
||||
consumable: '耗材',
|
||||
consumables: '耗材',
|
||||
ticket: '工单',
|
||||
tickets: '工单',
|
||||
category: '分类',
|
||||
categories: '分类',
|
||||
idleDevice: '空闲设备',
|
||||
idleDevices: '空闲设备',
|
||||
pendingDevice: '待同步设备',
|
||||
pendingDevices: '待同步设备',
|
||||
inventoryPlan: '盘点计划',
|
||||
inventoryPlans: '盘点计划',
|
||||
backup: '备份',
|
||||
backups: '备份',
|
||||
};
|
||||
@@ -0,0 +1,220 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Input, Alert, Typography, Descriptions, Divider } from 'antd';
|
||||
import { WarningOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
RISK_LEVEL,
|
||||
RISK_CONFIG,
|
||||
OPERATION_TYPES,
|
||||
OPERATION_LABELS,
|
||||
ENTITY_LABELS,
|
||||
getRiskLevel,
|
||||
} from '../config/dangerousOperationConfig';
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
|
||||
export const useDangerousOperation = () => {
|
||||
const confirm = async ({
|
||||
operationType = OPERATION_TYPES.DELETE_SINGLE,
|
||||
entityType = 'item',
|
||||
items = [],
|
||||
itemCount = 1,
|
||||
title = '',
|
||||
description = '',
|
||||
impactDetails = {},
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}) => {
|
||||
const riskLevel = getRiskLevel(operationType, itemCount, {
|
||||
isSystemLevel: impactDetails.isSystemLevel,
|
||||
hasRelatedData: impactDetails.relatedDataCount > 0,
|
||||
});
|
||||
|
||||
const config = RISK_CONFIG[riskLevel];
|
||||
const entityLabel = ENTITY_LABELS[entityType] || entityType;
|
||||
const operationLabel = OPERATION_LABELS[operationType] || operationType;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const modalKey = `dangerous-${Date.now()}`;
|
||||
|
||||
const handleOk = () => {
|
||||
Modal.confirm({
|
||||
title: '确认执行此操作?',
|
||||
content: '此操作不可逆,请再次确认。',
|
||||
okText: '确认执行',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
if (onConfirm) {
|
||||
await onConfirm();
|
||||
}
|
||||
resolve(true);
|
||||
} catch (error) {
|
||||
resolve(false);
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
if (onCancel) onCancel();
|
||||
resolve(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
const impactText = renderImpactDetails(impactDetails, entityLabel);
|
||||
|
||||
return (
|
||||
<div style={styles.contentContainer}>
|
||||
<Alert
|
||||
message={
|
||||
<Text strong style={{ color: config.color }}>
|
||||
{config.icon} {config.title}
|
||||
</Text>
|
||||
}
|
||||
description={
|
||||
<Paragraph style={{ marginBottom: 0 }}>
|
||||
{description || `您即将${operationLabel} ${itemCount > 1 ? `${itemCount} 个` : '1 个'}${entityLabel}。`}
|
||||
<br />
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
此操作不可逆,一旦删除将无法恢复。
|
||||
</Text>
|
||||
</Paragraph>
|
||||
}
|
||||
type={riskLevel === RISK_LEVEL.EXTREME ? 'error' : riskLevel === RISK_LEVEL.HIGH ? 'warning' : 'info'}
|
||||
style={{
|
||||
backgroundColor: config.bgColor,
|
||||
borderColor: config.borderColor,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
/>
|
||||
|
||||
{config.showImpactDetails && impactText && (
|
||||
<>
|
||||
<div style={styles.impactSection}>
|
||||
<Text strong style={{ display: 'block', marginBottom: 8 }}>
|
||||
<InfoCircleOutlined /> 影响范围:
|
||||
</Text>
|
||||
{impactText}
|
||||
</div>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{riskLevel === RISK_LEVEL.EXTREME && config.requireKeyword && (
|
||||
<div style={styles.keywordSection}>
|
||||
<Alert
|
||||
message={
|
||||
<Text>
|
||||
为确认此操作,请输入 <Text code strong>CONFIRM</Text>:
|
||||
</Text>
|
||||
}
|
||||
type="error"
|
||||
style={{ marginBottom: 8 }}
|
||||
/>
|
||||
<Input
|
||||
id={`keyword-input-${modalKey}`}
|
||||
placeholder="请输入 CONFIRM"
|
||||
onChange={(e) => {
|
||||
const inputValue = e.target.value;
|
||||
const okButton = document.querySelector('.ant-modal-confirm .ant-btn-primary');
|
||||
if (okButton) {
|
||||
okButton.disabled = inputValue !== config.keyword;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderImpactDetails = (details, entityLabel) => {
|
||||
if (!details || Object.keys(details).length === 0) return null;
|
||||
|
||||
const items = [];
|
||||
|
||||
if (details.relatedDevices) {
|
||||
items.push(`关联设备:${details.relatedDevices} 个`);
|
||||
}
|
||||
if (details.relatedCables) {
|
||||
items.push(`关联接线:${details.relatedCables} 条`);
|
||||
}
|
||||
if (details.relatedPorts) {
|
||||
items.push(`关联端口:${details.relatedPorts} 个`);
|
||||
}
|
||||
if (details.relatedNetworkCards) {
|
||||
items.push(`关联网卡:${details.relatedNetworkCards} 张`);
|
||||
}
|
||||
if (details.relatedTickets) {
|
||||
items.push(`关联工单:${details.relatedTickets} 个`);
|
||||
}
|
||||
if (details.relatedDataCount > 0) {
|
||||
items.push(`其他关联数据:${details.relatedDataCount} 项`);
|
||||
}
|
||||
|
||||
return items.length > 0 ? (
|
||||
<ul style={{ margin: 0, paddingLeft: 20, color: '#666' }}>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null;
|
||||
};
|
||||
|
||||
Modal.confirm({
|
||||
title: (
|
||||
<span style={{ color: config.color }}>
|
||||
{config.icon} {title || `${operationLabel}确认`}
|
||||
</span>
|
||||
),
|
||||
icon: <WarningOutlined style={{ color: config.color }} />,
|
||||
content: renderContent(),
|
||||
okText: '确认',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
okButtonProps: {
|
||||
disabled: riskLevel === RISK_LEVEL.EXTREME,
|
||||
},
|
||||
width: 520,
|
||||
onOk: handleOk,
|
||||
onCancel: () => {
|
||||
if (onCancel) onCancel();
|
||||
resolve(false);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const logOperation = async ({
|
||||
operationType,
|
||||
targetType,
|
||||
targetId,
|
||||
targetName,
|
||||
metadata = {},
|
||||
success = true,
|
||||
}) => {
|
||||
try {
|
||||
await axios.post('/api/operation-logs/dangerous', {
|
||||
operationType,
|
||||
targetType,
|
||||
targetId,
|
||||
targetName,
|
||||
metadata,
|
||||
success,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to log dangerous operation:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return { confirm, logOperation };
|
||||
};
|
||||
|
||||
export const confirmDangerousOperation = async (options) => {
|
||||
const hook = useDangerousOperation();
|
||||
return hook.confirm(options);
|
||||
};
|
||||
|
||||
export default useDangerousOperation;
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user