|
|
@@ -0,0 +1,465 @@
|
|
|
+import React, { useEffect, useState } from "react";
|
|
|
+import { Descriptions, Modal, DatePicker } from "antd";
|
|
|
+import styles from "./topoDetail.module.less";
|
|
|
+import { getVariable } from "../../../utils/theme";
|
|
|
+import ReactECharts from "echarts-for-react";
|
|
|
+import * as echarts from "echarts";
|
|
|
+import dayjs from "dayjs";
|
|
|
+import _ from "lodash";
|
|
|
+import API from "../../../api/alarm";
|
|
|
+
|
|
|
+const { RangePicker } = DatePicker;
|
|
|
+
|
|
|
+// 告警状态图标 - 告警中
|
|
|
+const Alert = (props) => (
|
|
|
+ <svg
|
|
|
+ width="32"
|
|
|
+ height="32"
|
|
|
+ viewBox="0 0 32 32"
|
|
|
+ fill="none"
|
|
|
+ xmlns="http://www.w3.org/2000/svg"
|
|
|
+ {...props}
|
|
|
+ >
|
|
|
+ <path
|
|
|
+ d="M1.20508 15.7949C1.20508 7.62389 7.82897 1 16 1C24.1709 1 30.7948 7.62389 30.7948 15.7949C30.7948 23.9659 24.1709 30.5897 16 30.5897C7.82897 30.5897 1.20508 23.9659 1.20508 15.7949Z"
|
|
|
+ fill="#F76965"
|
|
|
+ fillOpacity="0.2"
|
|
|
+ stroke="#F76965"
|
|
|
+ strokeWidth="2"
|
|
|
+ strokeLinecap="round"
|
|
|
+ strokeLinejoin="round"
|
|
|
+ />
|
|
|
+ <rect
|
|
|
+ x="14.5602"
|
|
|
+ y="8.61694"
|
|
|
+ width="2.86575"
|
|
|
+ height="8.59828"
|
|
|
+ rx="1.43288"
|
|
|
+ fill="#F76965"
|
|
|
+ />
|
|
|
+ <circle cx="16.0077" cy="21.5274" r="1.44753" fill="#F76965" />
|
|
|
+ </svg>
|
|
|
+);
|
|
|
+
|
|
|
+// 告警状态图标 - 已结束
|
|
|
+const Check = (props) => (
|
|
|
+ <svg
|
|
|
+ width="32"
|
|
|
+ height="32"
|
|
|
+ viewBox="0 0 32 32"
|
|
|
+ fill="none"
|
|
|
+ xmlns="http://www.w3.org/2000/svg"
|
|
|
+ {...props}
|
|
|
+ >
|
|
|
+ <path
|
|
|
+ d="M2.33325 16C2.33325 8.4521 8.45203 2.33333 15.9999 2.33333C23.5478 2.33333 29.6666 8.4521 29.6666 16C29.6666 23.5479 23.5478 29.6667 15.9999 29.6667C8.45203 29.6667 2.33325 23.5479 2.33325 16Z"
|
|
|
+ fill="#508DF8"
|
|
|
+ fillOpacity="0.2"
|
|
|
+ stroke="#508DF8"
|
|
|
+ strokeWidth="2"
|
|
|
+ strokeLinecap="round"
|
|
|
+ strokeLinejoin="round"
|
|
|
+ />
|
|
|
+ <rect x="11" y="11" width="10" height="10" fill="#508DF8" />
|
|
|
+ </svg>
|
|
|
+);
|
|
|
+
|
|
|
+// 常量定义
|
|
|
+const alarmTypeMap = {
|
|
|
+ 0: "系统告警",
|
|
|
+ 1: "过程告警",
|
|
|
+ 2: "指令告警",
|
|
|
+ 3: "系统告警",
|
|
|
+};
|
|
|
+
|
|
|
+const alarmStatusMap = {
|
|
|
+ 0: "进行中",
|
|
|
+ 1: "已结束",
|
|
|
+ 2: "已结束",
|
|
|
+};
|
|
|
+
|
|
|
+const levelMap = {
|
|
|
+ 1: "低",
|
|
|
+ 2: "中",
|
|
|
+ 3: "高",
|
|
|
+};
|
|
|
+
|
|
|
+// 图表初始配置
|
|
|
+const initialOption = {
|
|
|
+ tooltip: {
|
|
|
+ trigger: "axis",
|
|
|
+ className: styles.tooltip,
|
|
|
+ textStyle: {
|
|
|
+ color: getVariable("--dt-text-color1"),
|
|
|
+ },
|
|
|
+ formatter: (params) => {
|
|
|
+ const axisValue = params?.[0]?.axisValue ?? "--";
|
|
|
+ return (
|
|
|
+ axisValue +
|
|
|
+ "<br/>" +
|
|
|
+ params
|
|
|
+ .map((item) => {
|
|
|
+ return (
|
|
|
+ `<span style="display:inline-block;margin-right:4px;margin-bottom:5px;width:10px;height:2px;background:${item.color};"></span>` +
|
|
|
+ item.seriesName +
|
|
|
+ " " +
|
|
|
+ item.value
|
|
|
+ );
|
|
|
+ })
|
|
|
+ .join("<br/>")
|
|
|
+ );
|
|
|
+ },
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ show: true,
|
|
|
+ top: 34,
|
|
|
+ left: 28,
|
|
|
+ itemWidth: 8,
|
|
|
+ textStyle: {
|
|
|
+ color: getVariable("--dt-text-color3"),
|
|
|
+ },
|
|
|
+ icon: "path://M0,0L9,0L9,1L0,1",
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ top: 66,
|
|
|
+ bottom: 30,
|
|
|
+ left: 0,
|
|
|
+ right: 0,
|
|
|
+ containLabel: true,
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: "category",
|
|
|
+ axisTick: {
|
|
|
+ show: true,
|
|
|
+ },
|
|
|
+ axisLabel: {
|
|
|
+ color: getVariable("--dt-text-color3"),
|
|
|
+ fontSize: 12,
|
|
|
+ fontWeight: 400,
|
|
|
+ },
|
|
|
+ axisLine: {
|
|
|
+ show: false,
|
|
|
+ lineStyle: {
|
|
|
+ color: getVariable("--dt-line-color2"),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ splitLine: {
|
|
|
+ show: false,
|
|
|
+ },
|
|
|
+ data: [],
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: "value",
|
|
|
+ name: "%",
|
|
|
+ nameTextStyle: {
|
|
|
+ color: getVariable("--dt-text-color3"),
|
|
|
+ fontSize: 12,
|
|
|
+ fontWeight: 400,
|
|
|
+ },
|
|
|
+ axisLabel: {
|
|
|
+ color: getVariable("--dt-text-color3"),
|
|
|
+ fontSize: 12,
|
|
|
+ fontWeight: 400,
|
|
|
+ },
|
|
|
+ axisTick: {
|
|
|
+ show: false,
|
|
|
+ },
|
|
|
+ axisLine: {
|
|
|
+ show: false,
|
|
|
+ lineStyle: {
|
|
|
+ color: getVariable("--dt-line-color2"),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ splitLine: {
|
|
|
+ lineStyle: {
|
|
|
+ type: "dashed",
|
|
|
+ color: getVariable("--dt-line-color2"),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ color: [getVariable("--dt-primary-color1"), getVariable("--dt-error-color1")],
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: "值",
|
|
|
+ type: "line",
|
|
|
+ data: [],
|
|
|
+ itemStyle: {
|
|
|
+ normal: {
|
|
|
+ borderColor: getVariable("--dt-error-color1"),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ areaStyle: {
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
+ {
|
|
|
+ offset: 0,
|
|
|
+ color: "rgba(42, 111, 246, 0.2)",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ offset: 1,
|
|
|
+ color: "rgba(42, 111, 246, 0)",
|
|
|
+ },
|
|
|
+ ]),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ],
|
|
|
+};
|
|
|
+
|
|
|
+const TopoDetail = (props) => {
|
|
|
+ const { id, alarmTime } = props;
|
|
|
+ const [alarmData, setAlarmData] = useState(null);
|
|
|
+ const [chartData, setChartData] = useState(null);
|
|
|
+ const [loading, setLoading] = useState(false);
|
|
|
+
|
|
|
+ // 设置默认时间范围为告警时间前后72小时
|
|
|
+ const [range, setRange] = useState([
|
|
|
+ dayjs(alarmTime).subtract(72, "hour"),
|
|
|
+ dayjs(alarmTime).add(72, "hour"),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 获取告警详情数据
|
|
|
+ useEffect(() => {
|
|
|
+ if (id) {
|
|
|
+ fetchAlarmDetail(id, range[0], range[1]);
|
|
|
+ }
|
|
|
+ }, [id]);
|
|
|
+
|
|
|
+ // 获取告警详情
|
|
|
+ const fetchAlarmDetail = async (alarmId, startTime, endTime) => {
|
|
|
+ try {
|
|
|
+ setLoading(true);
|
|
|
+
|
|
|
+ // 转换时间为Unix时间戳
|
|
|
+ const begin = dayjs(startTime).unix();
|
|
|
+ const end = dayjs(endTime).unix();
|
|
|
+
|
|
|
+ const response = await API.getTopoAlarmDetail(alarmId, begin, end);
|
|
|
+
|
|
|
+ if (response && response.state === 0 && response.data) {
|
|
|
+ const apiData = response.data;
|
|
|
+
|
|
|
+ // 设置告警基本数据
|
|
|
+ setAlarmData({
|
|
|
+ id: alarmId,
|
|
|
+ name: apiData.name,
|
|
|
+ point_id: apiData.node_code,
|
|
|
+ type: apiData.alarm_type,
|
|
|
+ alarm_level: apiData.alarm_level,
|
|
|
+ created_time: apiData.created_time,
|
|
|
+ confirmed_time: apiData.confirmed_time,
|
|
|
+ confirmed_oper_name: apiData.confirmed_oper_name || "-",
|
|
|
+ status: apiData.status,
|
|
|
+ op_status: apiData.confirmed_time ? 1 : 0,
|
|
|
+ remark: apiData.remark || "-",
|
|
|
+ });
|
|
|
+
|
|
|
+ const data = processChartData(apiData.history, apiData.threshold);
|
|
|
+
|
|
|
+ // 设置图表数据
|
|
|
+ setChartData(data);
|
|
|
+ } else {
|
|
|
+ console.error("获取告警详情失败:", response);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("获取告警详情失败:", error);
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 处理图表数据
|
|
|
+ const processChartData = (historyData, threshold) => {
|
|
|
+ if (!historyData || historyData.length === 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const newData = _.cloneDeep(initialOption);
|
|
|
+
|
|
|
+ // 处理历史数据
|
|
|
+ const xAxisData = historyData.map((point) =>
|
|
|
+ dayjs.unix(point.ts).format("YYYY-MM-DD")
|
|
|
+ );
|
|
|
+
|
|
|
+ const seriesData = historyData.map((point) => {
|
|
|
+ // 转换为百分比显示
|
|
|
+ return (point.value * 100).toFixed(2);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 阈值也转换为百分比
|
|
|
+ const thresholdValue = (threshold * 100).toFixed(2);
|
|
|
+
|
|
|
+ // 配置数据点样式
|
|
|
+ newData.series[0] = {
|
|
|
+ name: "值",
|
|
|
+ type: "line",
|
|
|
+ data: seriesData,
|
|
|
+ symbol: "emptyCircle", // 使用空心圆圈
|
|
|
+ symbolSize: (value, params) => {
|
|
|
+ // 只有超过阈值的点才显示圆圈
|
|
|
+ return value > thresholdValue ? 8 : 0;
|
|
|
+ },
|
|
|
+ itemStyle: {
|
|
|
+ color: ({ data }) => {
|
|
|
+ return data > thresholdValue
|
|
|
+ ? getVariable("--dt-error-color1")
|
|
|
+ : getVariable("--dt-primary-color1");
|
|
|
+ },
|
|
|
+ },
|
|
|
+ lineStyle: {
|
|
|
+ color: getVariable("--dt-primary-color1"),
|
|
|
+ },
|
|
|
+ areaStyle: {
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
+ {
|
|
|
+ offset: 0,
|
|
|
+ color: "rgba(42, 111, 246, 0.2)",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ offset: 1,
|
|
|
+ color: "rgba(42, 111, 246, 0)",
|
|
|
+ },
|
|
|
+ ]),
|
|
|
+ },
|
|
|
+ // 添加阈值线 - 使用markLine
|
|
|
+ markLine: {
|
|
|
+ silent: true,
|
|
|
+ symbol: "none", // 不显示起始点和结束点
|
|
|
+ lineStyle: {
|
|
|
+ color: getVariable("--dt-error-color1"),
|
|
|
+ type: "dashed",
|
|
|
+ width: 1,
|
|
|
+ },
|
|
|
+ data: [
|
|
|
+ {
|
|
|
+ yAxis: thresholdValue,
|
|
|
+ name: "阈值",
|
|
|
+ label: {
|
|
|
+ show: false, // 不显示标签
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ // 添加阈值线作为单独的系列,以便在legend和tooltip中显示
|
|
|
+ newData.series.push({
|
|
|
+ name: "阈值",
|
|
|
+ type: "line",
|
|
|
+ data: Array(xAxisData.length).fill(thresholdValue),
|
|
|
+ symbol: "none",
|
|
|
+ lineStyle: {
|
|
|
+ width: 0, // 设置为0,使线不可见(已由markLine显示)
|
|
|
+ type: "dashed",
|
|
|
+ color: getVariable("--dt-error-color1"),
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ newData.xAxis.data = xAxisData;
|
|
|
+
|
|
|
+ return newData;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 处理日期范围变化
|
|
|
+ const handleRangeChange = (dates) => {
|
|
|
+ if (dates && dates.length === 2) {
|
|
|
+ setRange(dates);
|
|
|
+
|
|
|
+ // 当日期范围变化时,重新获取图表数据
|
|
|
+ if (alarmData && alarmData.id) {
|
|
|
+ fetchAlarmDetail(alarmData.id, dates[0], dates[1]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Modal
|
|
|
+ title="告警记录"
|
|
|
+ centered
|
|
|
+ open={props.open}
|
|
|
+ width="90vw"
|
|
|
+ onCancel={props.onCancel}
|
|
|
+ zIndex="1001"
|
|
|
+ footer={null}
|
|
|
+ destroyOnClose
|
|
|
+ bodyStyle={{
|
|
|
+ height: "calc(90vh - 80px)",
|
|
|
+ display: "flex",
|
|
|
+ flexDirection: "column",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {props.open && alarmData && (
|
|
|
+ <>
|
|
|
+ <div className={styles.desc}>
|
|
|
+ <Descriptions
|
|
|
+ column={3}
|
|
|
+ style={{
|
|
|
+ margin: "0px 0px",
|
|
|
+ width: "calc(100% - 287px)",
|
|
|
+ }}
|
|
|
+ labelStyle={{ width: 72 }}
|
|
|
+ >
|
|
|
+ <Descriptions.Item span={3} label="告警名称">
|
|
|
+ {alarmData.name}
|
|
|
+ </Descriptions.Item>
|
|
|
+ {alarmData.point_id && (
|
|
|
+ <Descriptions.Item label="节点编号">
|
|
|
+ {alarmData.point_id}
|
|
|
+ </Descriptions.Item>
|
|
|
+ )}
|
|
|
+ <Descriptions.Item label="告警类型">
|
|
|
+ {alarmTypeMap[alarmData.type]}
|
|
|
+ </Descriptions.Item>
|
|
|
+ <Descriptions.Item label="告警级别">
|
|
|
+ {levelMap[alarmData.alarm_level]}
|
|
|
+ </Descriptions.Item>
|
|
|
+ <Descriptions.Item label="告警时间">
|
|
|
+ {alarmData.created_time}
|
|
|
+ </Descriptions.Item>
|
|
|
+ <Descriptions.Item label="确认时间">
|
|
|
+ {alarmData.confirmed_time || "-"}
|
|
|
+ </Descriptions.Item>
|
|
|
+ <Descriptions.Item label="确认人">
|
|
|
+ {alarmData.confirmed_oper_name}
|
|
|
+ </Descriptions.Item>
|
|
|
+ <Descriptions.Item label="备注信息">
|
|
|
+ {alarmData.remark}
|
|
|
+ </Descriptions.Item>
|
|
|
+ </Descriptions>
|
|
|
+ <div className={styles.divide}></div>
|
|
|
+ <div className={styles.oper}>
|
|
|
+ {alarmData.status === 0 ? <Alert /> : <Check />}
|
|
|
+ <div className={styles.status}>
|
|
|
+ {alarmStatusMap[alarmData.status]}
|
|
|
+ </div>
|
|
|
+ <div className={styles.opStatus}>
|
|
|
+ {alarmData.op_status === 0 ? "未确认" : "已确认"}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div style={{ position: "relative", flex: 1, height: 0 }}>
|
|
|
+ <div className={styles.range}>
|
|
|
+ <RangePicker
|
|
|
+ showTime
|
|
|
+ format="YYYY-MM-DD HH:mm"
|
|
|
+ value={range}
|
|
|
+ onChange={handleRangeChange}
|
|
|
+ style={{ width: 360, zIndex: 99999 }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {chartData && (
|
|
|
+ <ReactECharts
|
|
|
+ style={{ height: "100%" }}
|
|
|
+ option={chartData}
|
|
|
+ notMerge={true}
|
|
|
+ loading={loading}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </Modal>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default TopoDetail;
|