Files
d8d-vite-starter/src/client/admin/pages/pages_alert_handle.tsx
D8D Developer b9a3c991d0 update
2025-06-27 01:56:30 +00:00

336 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
};