336 lines
12 KiB
TypeScript
336 lines
12 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import {
|
||
useNavigate,
|
||
useLocation,
|
||
useParams
|
||
} from 'react-router';
|
||
import {
|
||
Button, Space,
|
||
Form, Input, Select, message,
|
||
Card, Spin, Typography,
|
||
Switch, Divider, Descriptions,
|
||
Tag, List,
|
||
} from 'antd';
|
||
import {
|
||
FileImageOutlined,
|
||
FilePdfOutlined,
|
||
FileOutlined,
|
||
} from '@ant-design/icons';
|
||
import dayjs from 'dayjs';
|
||
import 'dayjs/locale/zh-cn';
|
||
// 从share/types.ts导入所有类型,包括MapMode
|
||
import type {
|
||
Attachment,
|
||
} from '@/share/monitorTypes';
|
||
|
||
import {
|
||
AlertLevel, AlertStatus,
|
||
HandleType, ProblemType,
|
||
HandleTypeNameMap, ProblemTypeNameMap,
|
||
} from '@/share/monitorTypes';
|
||
|
||
import { getEnumOptions } from '../utils';
|
||
|
||
import { alertsClient, alertsHandleLogsClient } from '@/client/api';
|
||
import type { InferRequestType, InferResponseType } from 'hono/client';
|
||
import { Uploader } from "../components/components_uploader";
|
||
|
||
type AlertResponse = InferResponseType<typeof alertsClient.$get, 200>;
|
||
type AlertHandleLogRequest = InferRequestType<typeof alertsHandleLogsClient.$post>['json'];
|
||
type DeviceAlert = InferResponseType<typeof alertsClient.$get, 200>['data'][0];
|
||
type AlertHandleLog = InferResponseType<typeof alertsHandleLogsClient.$post, 200>;
|
||
|
||
|
||
|
||
const { Text } = Typography;
|
||
|
||
// 告警处理页面
|
||
export const AlertHandlePage = () => {
|
||
const { id } = useParams<{ id: string }>();
|
||
const [loading, setLoading] = useState(false);
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [alert, setAlert] = useState<DeviceAlert | null>(null);
|
||
const [form] = Form.useForm();
|
||
const navigate = useNavigate();
|
||
const [uploadedFiles, setUploadedFiles] = useState<Attachment[]>([]);
|
||
const location = useLocation();
|
||
const searchParams = new URLSearchParams(location.search);
|
||
const mode = searchParams.get('mode') || 'view'; // 默认为查看模式
|
||
|
||
// 判断是否可编辑
|
||
const isEditable = mode === 'edit' && alert &&
|
||
(alert.status === AlertStatus.PENDING || alert.status === AlertStatus.HANDLING);
|
||
|
||
useEffect(() => {
|
||
if (id) {
|
||
fetchAlertData(parseInt(id));
|
||
}
|
||
}, [id]);
|
||
|
||
const fetchAlertData = async (alertId: number) => {
|
||
setLoading(true);
|
||
try {
|
||
const res = await alertsClient[':id'].$get({
|
||
param: { id: alertId }
|
||
});
|
||
|
||
if (res.status === 200) {
|
||
const data = await res.json();
|
||
setAlert(data);
|
||
} else {
|
||
throw new Error(res.statusText);
|
||
}
|
||
} catch (error) {
|
||
console.error('获取告警数据失败:', error);
|
||
message.error('获取告警数据失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleSubmit = async (values: any) => {
|
||
if (!id) return;
|
||
|
||
setSubmitting(true);
|
||
try {
|
||
const alertHandleLog: AlertHandleLogRequest = {
|
||
alertId: parseInt(id),
|
||
handleType: values.handleType,
|
||
problemType: values.problemType,
|
||
handleResult: values.handleResult,
|
||
notifyDisabled: values.notifyDisabled ? 1 : 0,
|
||
attachments: uploadedFiles
|
||
};
|
||
|
||
const res = await alertsHandleLogsClient.$post({
|
||
json: alertHandleLog
|
||
});
|
||
|
||
if (res.status === 200) {
|
||
message.success('告警处理成功');
|
||
navigate('/admin/alert-records');
|
||
} else {
|
||
const error = await res.json();
|
||
throw new Error(error.message || '处理失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('告警处理失败:', error);
|
||
message.error('告警处理失败');
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
// 文件上传成功回调
|
||
const handleFileUploadSuccess = (fileUrl: string, fileInfo: any) => {
|
||
// 添加上传成功的文件到列表
|
||
const newFile: Attachment = {
|
||
id: fileInfo.id || String(Date.now()),
|
||
name: fileInfo.file_name,
|
||
url: fileUrl,
|
||
type: fileInfo.file_type,
|
||
size: fileInfo.file_size,
|
||
upload_time: new Date().toISOString()
|
||
};
|
||
|
||
setUploadedFiles(prev => [...prev, newFile]);
|
||
};
|
||
|
||
// 删除已上传文件
|
||
const handleFileDelete = (fileId: string) => {
|
||
setUploadedFiles(prev => prev.filter(file => file.id !== fileId));
|
||
};
|
||
|
||
const handleTypeOptions = getEnumOptions(HandleType, HandleTypeNameMap);
|
||
|
||
const problemTypeOptions = getEnumOptions(ProblemType, ProblemTypeNameMap);
|
||
|
||
const getAlertLevelTag = (level: AlertLevel | null) => {
|
||
if (level === null) return <Tag>未知</Tag>;
|
||
|
||
switch (level) {
|
||
case AlertLevel.MINOR:
|
||
return <Tag color="blue">次要</Tag>;
|
||
case AlertLevel.NORMAL:
|
||
return <Tag color="green">一般</Tag>;
|
||
case AlertLevel.IMPORTANT:
|
||
return <Tag color="orange">重要</Tag>;
|
||
case AlertLevel.URGENT:
|
||
return <Tag color="red">紧急</Tag>;
|
||
default:
|
||
return <Tag>未知</Tag>;
|
||
}
|
||
};
|
||
|
||
const getAlertStatusTag = (status?: AlertStatus) => {
|
||
if (status === undefined) return <Tag>未知</Tag>;
|
||
|
||
switch (status) {
|
||
case AlertStatus.PENDING:
|
||
return <Tag color="red">待处理</Tag>;
|
||
case AlertStatus.HANDLING:
|
||
return <Tag color="orange">处理中</Tag>;
|
||
case AlertStatus.RESOLVED:
|
||
return <Tag color="green">已解决</Tag>;
|
||
case AlertStatus.IGNORED:
|
||
return <Tag color="default">已忽略</Tag>;
|
||
default:
|
||
return <Tag>未知</Tag>;
|
||
}
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div style={{ textAlign: 'center', padding: '50px' }}>
|
||
<Spin size="large" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!alert) {
|
||
return (
|
||
<div style={{ textAlign: 'center', padding: '50px' }}>
|
||
<Text>未找到告警数据</Text>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<Card title={isEditable ? "告警处理" : "告警查看"} style={{ marginBottom: 16 }}>
|
||
<Descriptions bordered column={2} style={{ marginBottom: 16 }}>
|
||
<Descriptions.Item label="告警ID">{alert.id}</Descriptions.Item>
|
||
<Descriptions.Item label="设备名称">{alert.deviceName}</Descriptions.Item>
|
||
<Descriptions.Item label="告警等级">{getAlertLevelTag(alert.alertLevel)}</Descriptions.Item>
|
||
<Descriptions.Item label="状态">{getAlertStatusTag(alert.status)}</Descriptions.Item>
|
||
<Descriptions.Item label="监控指标">{alert.metricType}</Descriptions.Item>
|
||
<Descriptions.Item label="触发值">{alert.metricValue}</Descriptions.Item>
|
||
<Descriptions.Item label="告警消息">{alert.alertMessage}</Descriptions.Item>
|
||
<Descriptions.Item label="告警时间">{dayjs(alert.createdAt).format('YYYY-MM-DD HH:mm:ss')}</Descriptions.Item>
|
||
</Descriptions>
|
||
|
||
{/* 只有可编辑模式或者已经有处理记录的情况下才显示表单 */}
|
||
{isEditable && (
|
||
<>
|
||
<Divider />
|
||
|
||
<Form
|
||
form={form}
|
||
layout="vertical"
|
||
onFinish={handleSubmit}
|
||
initialValues={{
|
||
handle_type: HandleType.CONFIRM,
|
||
notify_disabled: false,
|
||
}}
|
||
>
|
||
<Form.Item
|
||
name="handleType"
|
||
label="处理类型"
|
||
rules={[{ required: true, message: '请选择处理类型' }]}
|
||
>
|
||
<Select options={handleTypeOptions} />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="problemType"
|
||
label="问题类型"
|
||
rules={[{ required: true, message: '请选择问题类型' }]}
|
||
>
|
||
<Select options={problemTypeOptions} />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="handleResult"
|
||
label="处理结果"
|
||
rules={[{ required: true, message: '请输入处理结果' }]}
|
||
>
|
||
<Input.TextArea rows={4} />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
label="附件"
|
||
>
|
||
<div className="upload-attachments">
|
||
{/* 使用MinIOUploader代替原始Upload组件 */}
|
||
<Uploader
|
||
onSuccess={handleFileUploadSuccess}
|
||
onError={(error) => message.error(`上传失败: ${error.message}`)}
|
||
onProgress={(percent) => console.log(`上传进度: ${percent}%`)}
|
||
prefix="alerts/"
|
||
maxSize={20 * 1024 * 1024}
|
||
allowedTypes={['image/jpeg', 'image/png', 'application/pdf', 'text/plain', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']}
|
||
/>
|
||
|
||
{/* 已上传文件列表 */}
|
||
{uploadedFiles.length > 0 && (
|
||
<div style={{ marginTop: 16 }}>
|
||
<h4>已上传文件:</h4>
|
||
<List
|
||
size="small"
|
||
bordered
|
||
dataSource={uploadedFiles}
|
||
renderItem={file => (
|
||
<List.Item
|
||
actions={[
|
||
<Button
|
||
key="delete"
|
||
type="link"
|
||
danger
|
||
onClick={() => handleFileDelete(file.id)}
|
||
>
|
||
删除
|
||
</Button>
|
||
]}
|
||
>
|
||
<Space>
|
||
{file.type.includes('image') ? <FileImageOutlined /> :
|
||
file.type.includes('pdf') ? <FilePdfOutlined /> :
|
||
<FileOutlined />}
|
||
<a href={file.url} target="_blank" rel="noopener noreferrer">
|
||
{file.name}
|
||
</a>
|
||
<Text type="secondary">
|
||
({file.size < 1024 * 1024
|
||
? `${(file.size / 1024).toFixed(2)} KB`
|
||
: `${(file.size / 1024 / 1024).toFixed(2)} MB`})
|
||
</Text>
|
||
</Space>
|
||
</List.Item>
|
||
)}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="notifyDisabled"
|
||
valuePropName="checked"
|
||
>
|
||
<Switch checkedChildren="禁用通知" unCheckedChildren="启用通知" />
|
||
</Form.Item>
|
||
|
||
<Form.Item>
|
||
<Button type="primary" htmlType="submit" loading={submitting}>
|
||
提交
|
||
</Button>
|
||
<Button style={{ marginLeft: 8 }} onClick={() => navigate('/admin/alert-records')}>
|
||
返回
|
||
</Button>
|
||
</Form.Item>
|
||
</Form>
|
||
</>
|
||
)}
|
||
|
||
{/* 不可编辑模式时只显示返回按钮 */}
|
||
{!isEditable && (
|
||
<Form.Item style={{ marginTop: 16 }}>
|
||
<Button onClick={() => navigate('/admin/alert-records')}>
|
||
返回
|
||
</Button>
|
||
</Form.Item>
|
||
)}
|
||
</Card>
|
||
</div>
|
||
);
|
||
}; |