pages_alert_handle.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. import React, { useState, useEffect } from 'react';
  2. import {
  3. useNavigate,
  4. useLocation,
  5. useParams
  6. } from 'react-router';
  7. import {
  8. Button, Space,
  9. Form, Input, Select, message,
  10. Card, Spin, Typography,
  11. Switch, Divider, Descriptions,
  12. Tag, List,
  13. } from 'antd';
  14. import {
  15. FileImageOutlined,
  16. FilePdfOutlined,
  17. FileOutlined,
  18. } from '@ant-design/icons';
  19. import dayjs from 'dayjs';
  20. import 'dayjs/locale/zh-cn';
  21. // 从share/types.ts导入所有类型,包括MapMode
  22. import type {
  23. AlertHandleLog, DeviceAlert, Attachment,
  24. } from '../share/monitorTypes.ts';
  25. import {
  26. AlertLevel, AlertStatus,
  27. HandleType, ProblemType,
  28. HandleTypeNameMap, ProblemTypeNameMap,
  29. } from '../share/monitorTypes.ts';
  30. import { getEnumOptions } from './utils.ts';
  31. import { AlertAPI, AlertHandleAPI } from './api/index.ts';
  32. import { Uploader } from "./components_uploader.tsx";
  33. const { Text } = Typography;
  34. // 告警处理页面
  35. export const AlertHandlePage = () => {
  36. const { id } = useParams<{ id: string }>();
  37. const [loading, setLoading] = useState(false);
  38. const [submitting, setSubmitting] = useState(false);
  39. const [alert, setAlert] = useState<DeviceAlert | null>(null);
  40. const [form] = Form.useForm();
  41. const navigate = useNavigate();
  42. const [uploadedFiles, setUploadedFiles] = useState<Attachment[]>([]);
  43. const location = useLocation();
  44. const searchParams = new URLSearchParams(location.search);
  45. const mode = searchParams.get('mode') || 'view'; // 默认为查看模式
  46. // 判断是否可编辑
  47. const isEditable = mode === 'edit' && alert &&
  48. (alert.status === AlertStatus.PENDING || alert.status === AlertStatus.HANDLING);
  49. useEffect(() => {
  50. if (id) {
  51. fetchAlertData(parseInt(id));
  52. }
  53. }, [id]);
  54. const fetchAlertData = async (alertId: number) => {
  55. setLoading(true);
  56. try {
  57. const response = await AlertAPI.getAlert(alertId);
  58. if (response) {
  59. setAlert(response);
  60. }
  61. } catch (error) {
  62. console.error('获取告警数据失败:', error);
  63. message.error('获取告警数据失败');
  64. } finally {
  65. setLoading(false);
  66. }
  67. };
  68. const handleSubmit = async (values: any) => {
  69. if (!id) return;
  70. setSubmitting(true);
  71. try {
  72. const alertHandleLog: Partial<AlertHandleLog> = {
  73. alert_id: parseInt(id),
  74. handle_type: values.handle_type,
  75. problem_type: values.problem_type,
  76. handle_result: values.handle_result,
  77. notify_disabled: values.notify_disabled ? 1 : 0,
  78. attachments: uploadedFiles
  79. };
  80. const response = await AlertHandleAPI.createAlertHandle(alertHandleLog);
  81. if (response.data) {
  82. message.success('告警处理成功');
  83. navigate('/admin/alert-records');
  84. }
  85. } catch (error) {
  86. console.error('告警处理失败:', error);
  87. message.error('告警处理失败');
  88. } finally {
  89. setSubmitting(false);
  90. }
  91. };
  92. // 文件上传成功回调
  93. const handleFileUploadSuccess = (fileUrl: string, fileInfo: any) => {
  94. // 添加上传成功的文件到列表
  95. const newFile: Attachment = {
  96. id: fileInfo.id || String(Date.now()),
  97. name: fileInfo.file_name,
  98. url: fileUrl,
  99. type: fileInfo.file_type,
  100. size: fileInfo.file_size,
  101. upload_time: new Date().toISOString()
  102. };
  103. setUploadedFiles(prev => [...prev, newFile]);
  104. };
  105. // 删除已上传文件
  106. const handleFileDelete = (fileId: string) => {
  107. setUploadedFiles(prev => prev.filter(file => file.id !== fileId));
  108. };
  109. const handleTypeOptions = getEnumOptions(HandleType, HandleTypeNameMap);
  110. const problemTypeOptions = getEnumOptions(ProblemType, ProblemTypeNameMap);
  111. const getAlertLevelTag = (level?: AlertLevel) => {
  112. if (level === undefined) return <Tag>未知</Tag>;
  113. switch (level) {
  114. case AlertLevel.MINOR:
  115. return <Tag color="blue">次要</Tag>;
  116. case AlertLevel.NORMAL:
  117. return <Tag color="green">一般</Tag>;
  118. case AlertLevel.IMPORTANT:
  119. return <Tag color="orange">重要</Tag>;
  120. case AlertLevel.URGENT:
  121. return <Tag color="red">紧急</Tag>;
  122. default:
  123. return <Tag>未知</Tag>;
  124. }
  125. };
  126. const getAlertStatusTag = (status?: AlertStatus) => {
  127. if (status === undefined) return <Tag>未知</Tag>;
  128. switch (status) {
  129. case AlertStatus.PENDING:
  130. return <Tag color="red">待处理</Tag>;
  131. case AlertStatus.HANDLING:
  132. return <Tag color="orange">处理中</Tag>;
  133. case AlertStatus.RESOLVED:
  134. return <Tag color="green">已解决</Tag>;
  135. case AlertStatus.IGNORED:
  136. return <Tag color="default">已忽略</Tag>;
  137. default:
  138. return <Tag>未知</Tag>;
  139. }
  140. };
  141. if (loading) {
  142. return (
  143. <div style={{ textAlign: 'center', padding: '50px' }}>
  144. <Spin size="large" />
  145. </div>
  146. );
  147. }
  148. if (!alert) {
  149. return (
  150. <div style={{ textAlign: 'center', padding: '50px' }}>
  151. <Text>未找到告警数据</Text>
  152. </div>
  153. );
  154. }
  155. return (
  156. <div>
  157. <Card title={isEditable ? "告警处理" : "告警查看"} style={{ marginBottom: 16 }}>
  158. <Descriptions bordered column={2} style={{ marginBottom: 16 }}>
  159. <Descriptions.Item label="告警ID">{alert.id}</Descriptions.Item>
  160. <Descriptions.Item label="设备名称">{alert.device_name}</Descriptions.Item>
  161. <Descriptions.Item label="告警等级">{getAlertLevelTag(alert.alert_level)}</Descriptions.Item>
  162. <Descriptions.Item label="状态">{getAlertStatusTag(alert.status)}</Descriptions.Item>
  163. <Descriptions.Item label="监控指标">{alert.metric_type}</Descriptions.Item>
  164. <Descriptions.Item label="触发值">{alert.metric_value}</Descriptions.Item>
  165. <Descriptions.Item label="告警消息">{alert.alert_message}</Descriptions.Item>
  166. <Descriptions.Item label="告警时间">{dayjs(alert.created_at).format('YYYY-MM-DD HH:mm:ss')}</Descriptions.Item>
  167. </Descriptions>
  168. {/* 只有可编辑模式或者已经有处理记录的情况下才显示表单 */}
  169. {isEditable && (
  170. <>
  171. <Divider />
  172. <Form
  173. form={form}
  174. layout="vertical"
  175. onFinish={handleSubmit}
  176. initialValues={{
  177. handle_type: HandleType.CONFIRM,
  178. notify_disabled: false,
  179. }}
  180. >
  181. <Form.Item
  182. name="handle_type"
  183. label="处理类型"
  184. rules={[{ required: true, message: '请选择处理类型' }]}
  185. >
  186. <Select options={handleTypeOptions} />
  187. </Form.Item>
  188. <Form.Item
  189. name="problem_type"
  190. label="问题类型"
  191. rules={[{ required: true, message: '请选择问题类型' }]}
  192. >
  193. <Select options={problemTypeOptions} />
  194. </Form.Item>
  195. <Form.Item
  196. name="handle_result"
  197. label="处理结果"
  198. rules={[{ required: true, message: '请输入处理结果' }]}
  199. >
  200. <Input.TextArea rows={4} />
  201. </Form.Item>
  202. <Form.Item
  203. label="附件"
  204. >
  205. <div className="upload-attachments">
  206. {/* 使用MinIOUploader代替原始Upload组件 */}
  207. <Uploader
  208. onSuccess={handleFileUploadSuccess}
  209. onError={(error) => message.error(`上传失败: ${error.message}`)}
  210. onProgress={(percent) => console.log(`上传进度: ${percent}%`)}
  211. prefix="alerts/"
  212. maxSize={20 * 1024 * 1024}
  213. allowedTypes={['image/jpeg', 'image/png', 'application/pdf', 'text/plain', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']}
  214. />
  215. {/* 已上传文件列表 */}
  216. {uploadedFiles.length > 0 && (
  217. <div style={{ marginTop: 16 }}>
  218. <h4>已上传文件:</h4>
  219. <List
  220. size="small"
  221. bordered
  222. dataSource={uploadedFiles}
  223. renderItem={file => (
  224. <List.Item
  225. actions={[
  226. <Button
  227. key="delete"
  228. type="link"
  229. danger
  230. onClick={() => handleFileDelete(file.id)}
  231. >
  232. 删除
  233. </Button>
  234. ]}
  235. >
  236. <Space>
  237. {file.type.includes('image') ? <FileImageOutlined /> :
  238. file.type.includes('pdf') ? <FilePdfOutlined /> :
  239. <FileOutlined />}
  240. <a href={file.url} target="_blank" rel="noopener noreferrer">
  241. {file.name}
  242. </a>
  243. <Text type="secondary">
  244. ({file.size < 1024 * 1024
  245. ? `${(file.size / 1024).toFixed(2)} KB`
  246. : `${(file.size / 1024 / 1024).toFixed(2)} MB`})
  247. </Text>
  248. </Space>
  249. </List.Item>
  250. )}
  251. />
  252. </div>
  253. )}
  254. </div>
  255. </Form.Item>
  256. <Form.Item
  257. name="notify_disabled"
  258. valuePropName="checked"
  259. >
  260. <Switch checkedChildren="禁用通知" unCheckedChildren="启用通知" />
  261. </Form.Item>
  262. <Form.Item>
  263. <Button type="primary" htmlType="submit" loading={submitting}>
  264. 提交
  265. </Button>
  266. <Button style={{ marginLeft: 8 }} onClick={() => navigate('/admin/alert-records')}>
  267. 返回
  268. </Button>
  269. </Form.Item>
  270. </Form>
  271. </>
  272. )}
  273. {/* 不可编辑模式时只显示返回按钮 */}
  274. {!isEditable && (
  275. <Form.Item style={{ marginTop: 16 }}>
  276. <Button onClick={() => navigate('/admin/alert-records')}>
  277. 返回
  278. </Button>
  279. </Form.Item>
  280. )}
  281. </Card>
  282. </div>
  283. );
  284. };