Просмотр исходного кода

在App.jsx中注释掉DetailModal组件,并添加TopoDetailModal组件;在entry.jsx中引入TopoDetail组件并定义TopoDetailModal组件。

valentichu 7 месяцев назад
Родитель
Сommit
ed1e8f89f7

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
lib/index.js


+ 8 - 2
src/App.jsx

@@ -10,7 +10,7 @@ function Home(props) {
       {/* <div style={{ padding: "0px 30px" }}>
         <Alarm.ComplexConfig ref={compRef} editId={-1}></Alarm.ComplexConfig>
       </div> */}
-      <Alarm.DetailModal
+      {/* <Alarm.DetailModal
         open={open}
         onCancel={() => setOpen(false)}
         options={{
@@ -44,7 +44,7 @@ function Home(props) {
         // // options={{
         // //   pointId: "ABC123_AAA_chr1_AEff_MIN_H",
         // // }}
-      ></Alarm.DetailModal>
+      ></Alarm.DetailModal> */}
       {/* <Alarm.ComplexConfig
         ref={compRef}
         editId={-1}
@@ -59,6 +59,12 @@ function Home(props) {
           editId={-1}
         ></Alarm.EasyConfig>
       </div> */}
+      <Alarm.TopoDetailModal
+        id={689504}
+        alarmTime="2025-06-12 00:00:00"
+        open={true}
+        onCancel={() => {}}
+      />
     </>
   );
 }

+ 14 - 0
src/api/alarm.js

@@ -142,6 +142,19 @@ export async function getGroupList() {
   return res.statusText ? res.data : res;
 }
 
+export async function getTopoAlarmDetail(alarm_id, begin, end) {
+  const res = await axios.post(
+    "/api/configapi/topo/alarm_detail",
+    {
+      alarm_id,
+      begin,
+      end,
+    },
+    getConfig()
+  );
+  return res.statusText ? res.data : res;
+}
+
 export default {
   editRule,
   addRule,
@@ -154,4 +167,5 @@ export default {
   getRuleList,
   getGroupList,
   download,
+  getTopoAlarmDetail,
 };

+ 36 - 1
src/entry.jsx

@@ -10,6 +10,7 @@ import "./style/global.less";
 import Easy from "./pages/Alarm/components/easy";
 import Complex from "./pages/Alarm/components/complex";
 import Detail from "./pages/Alarm/components/detail";
+import TopoDetail from "./pages/Alarm/components/topoDetail";
 import { getAntdToken } from "./utils/theme";
 import ThemeWrapper from "./utils/theme/ThemeWrapper";
 import { ConfigProvider, App } from "antd";
@@ -63,7 +64,14 @@ const EasyConfig = forwardRef((props, ref) => {
 });
 
 const ComplexConfig = forwardRef((props, ref) => {
-  const { editId = -1, onConfirm, groupList, showList, style, rightStyle } = props;
+  const {
+    editId = -1,
+    onConfirm,
+    groupList,
+    showList,
+    style,
+    rightStyle,
+  } = props;
   const compRef = useRef(null);
 
   const [themeName, setThemeName] = useState();
@@ -131,8 +139,35 @@ const DetailModal = (props) => {
   );
 };
 
+const TopoDetailModal = (props) => {
+  const [themeName, setThemeName] = useState();
+
+  const antdToken = useMemo(() => {
+    if (themeName) {
+      return getAntdToken(themeName);
+    }
+  }, [themeName]);
+
+  return (
+    <ThemeWrapper setThemeName={setThemeName}>
+      {themeName && (
+        <ConfigProvider
+          locale={zhCN}
+          theme={antdToken}
+          autoInsertSpaceInButton={false}
+        >
+          <App className={styles.App}>
+            <TopoDetail {...props} />
+          </App>
+        </ConfigProvider>
+      )}
+    </ThemeWrapper>
+  );
+};
+
 export default {
   EasyConfig,
   ComplexConfig,
   DetailModal,
+  TopoDetailModal,
 };

+ 465 - 0
src/pages/Alarm/components/topoDetail.jsx

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

+ 54 - 0
src/pages/Alarm/components/topoDetail.module.less

@@ -0,0 +1,54 @@
+.desc {
+  display: flex;
+  align-items: center;
+  :global {
+    .ant-descriptions .ant-descriptions-row > td {
+      padding-bottom: 10px;
+    }
+  }
+  padding-top: 40px;
+  padding-bottom: 40px;
+}
+
+.divide {
+  border-left: 1px solid var(--dt-line-color2);
+  height: 112px;
+}
+
+.oper {
+  width: 287px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  img {
+    width: 32px;
+    height: 32px;
+    margin-bottom: 4px;
+  }
+  .status {
+    color: var(--dt-text-color2);
+    font-size: 18px;
+    margin-bottom: 18px;
+    margin-top: 4px;
+  }
+  .opStatus {
+    color: var(--dt-text-color4);
+    font-size: 14px;
+    margin-top: 18px;
+  }
+}
+
+.range {
+  display: flex;
+  justify-content: flex-end;
+  position: absolute;
+  right: 0px;
+  top: 18px;
+}
+
+.tooltip {
+  background-color: var(--dt-fill-color3) !important;
+  border-width: 0 !important;
+  box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1) !important;
+} 

Некоторые файлы не были показаны из-за большого количества измененных файлов