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
@@ -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;