valentichu 1 рік тому
батько
коміт
7bd468efb5

+ 0 - 1
.env.development

@@ -1 +0,0 @@
-REACT_APP_PUBLIC_PATH=http://192.168.1.109:32080

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
lib/index.js


+ 10 - 14
src/App.jsx

@@ -3,23 +3,19 @@ import Alarm from "./entry";
 
 function Home(props) {
   const compRef = useRef(null);
+  const [open, setOpen] = useState(true);
 
   return (
     <>
-      <Alarm.EasyConfig
-        ref={compRef}
-        editId={-1}
-        onConfirm={() => {
-          console.log(1);
-        }}
-      ></Alarm.EasyConfig>
-      <button
-        onClick={() => {
-          compRef.current.ok();
-        }}
-      >
-        测试
-      </button>
+      {/* <Alarm.DetailModal
+        open={open}
+        onCancel={() => setOpen(false)}
+        options={{ ruleId: 18 }}
+        // options={{
+        //   pointId: "ABC123_AAA_chr1_AEff_MIN_H",
+        // }}
+      ></Alarm.DetailModal> */}
+      <Alarm.EasyConfig id={-1}></Alarm.EasyConfig>
     </>
   );
 }

+ 59 - 6
src/api/alarm.js

@@ -1,7 +1,7 @@
 import axios from "axios";
 const config = {
   baseURL: "http://192.168.1.109:32080",
-  // baseURL: '/api/aragron',
+  // baseURL: '/api',
   noNeedInterceptor: false,
   headers: {
     Authorization: localStorage.getItem("token"),
@@ -9,22 +9,70 @@ const config = {
 };
 
 export async function addRule(data) {
-  const res = await axios.post("/api/aragron/unialarm/add_rule", data, config);
+  const res = await axios.post("/api/unialarm/add_rule", data, config);
   return res.statusText ? res.data : res;
 }
 
 export async function editRule(data) {
-  const res = await axios.post("/api/aragron/unialarm/edit_rule", data, config);
+  const res = await axios.post("/api/unialarm/edit_rule", data, config);
   return res.statusText ? res.data : res;
 }
 
 export async function searchPoint(data) {
+  const res = await axios.post("/api/unialarm/search_point", data, config);
+  return res.statusText ? res.data : res;
+}
+
+export async function getAlarmHistory(page, rule_id, size = 10) {
+  const res = await axios.post(
+    "/api/unialarm/get_alarm_history",
+    {
+      rule_id,
+      page_num: page,
+      page_size: size,
+      types: [1],
+    },
+    config
+  );
+  return res.statusText ? res.data : res;
+}
+
+export async function getRuleIdByPointId(point_id) {
+  const res = await axios.post(
+    "/api/unialarm/get_rule_with_point",
+    {
+      point_id,
+    },
+    config
+  );
+  return res.statusText ? res.data : res;
+}
+export async function detailAlarm(id) {
+  const res = await axios.post(
+    "/api/unialarm//get_rule_with_id",
+    {
+      id,
+    },
+    config
+  );
+  return res.statusText ? res.data : res;
+}
+
+export async function confirmAlarm(data) {
+  const res = await axios.post("/api/unialarm/confirm_alarm", data, config);
+  return res.statusText ? res.data : res;
+}
+
+export async function getAlarmPointData(rule_id, begin, end) {
   const res = await axios.post(
-    "/api/aragron/unialarm/search_point",
-    data,
+    "/api/unialarm/get_alarm_point_data",
+    {
+      rule_id,
+      begin,
+      end,
+    },
     config
   );
-  console.log(res)
   return res.statusText ? res.data : res;
 }
 
@@ -32,4 +80,9 @@ export default {
   editRule,
   addRule,
   searchPoint,
+  getAlarmHistory,
+  getRuleIdByPointId,
+  detailAlarm,
+  confirmAlarm,
+  getAlarmPointData,
 };

+ 28 - 2
src/entry.jsx

@@ -9,6 +9,7 @@ import styles from "./entry.module.scss";
 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 { getAntdToken } from "./utils/theme";
 import ThemeWrapper from "./utils/theme/ThemeWrapper";
 import { ConfigProvider, App } from "antd";
@@ -76,8 +77,6 @@ const ComplexConfig = forwardRef((props, ref) => {
     };
   });
 
-  console.log(compRef);
-
   return (
     <ThemeWrapper setThemeName={setThemeName}>
       {themeName && (
@@ -100,7 +99,34 @@ const ComplexConfig = forwardRef((props, ref) => {
   );
 });
 
+const DetailModal = (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}>
+            <Detail {...props} />
+          </App>
+        </ConfigProvider>
+      )}
+    </ThemeWrapper>
+  );
+};
+
 export default {
   EasyConfig,
   ComplexConfig,
+  DetailModal,
 };

+ 6 - 16
src/pages/Alarm/components/complex.jsx

@@ -65,28 +65,19 @@ const Complex = (props, ref) => {
   useEffect(() => {
     async function run() {
       if (alarmId && alarmId != -1) {
+        const { state, data } = await API2.detailAlarm(Number(alarmId));
         form.setFieldsValue({
-          name: alarmId.name,
-          alarm_level: alarmId.alarm_level,
-          group_id: alarmId.group_id,
+          name: data.name,
+          alarm_level: data.alarm_level,
+          group_id: data.group_id,
         });
-        console.log(alarmId.formula);
-        formuRef.current.backEndInput(alarmId.formula);
+        formuRef.current.backEndInput(data.formula);
+        setData(data);
       }
     }
     run();
   }, [alarmId]);
 
-  useEffect(() => {
-    if (data) {
-      form.setFieldsValue({
-        name: data.name,
-        alarm_level: data.alarm_level,
-        group_id: data.group_id,
-      });
-    }
-  }, [data]);
-
   const onModalOk = async () => {
     if (alarmId == -1) {
       const value = await form.validateFields();
@@ -110,7 +101,6 @@ const Complex = (props, ref) => {
       const value = await form.validateFields();
 
       const obj = {
-        ...alarmId,
         ...data,
         ...value,
         formula: formuRef.current.getFinalStr(),

+ 100 - 0
src/pages/Alarm/components/components/AlarmConfirm/index.jsx

@@ -0,0 +1,100 @@
+import React, { useState } from "react";
+import { Modal, Form, Input, message } from "antd";
+import { confirmAlarm } from "../../../../../api/alarm";
+import styles from "./index.module.less";
+import _ from "lodash";
+const { TextArea } = Input;
+
+function TranslateText(arr) {
+  const dtLanguage = localStorage.getItem("dtLanguage");
+  if (!Array.isArray(arr)) {
+    return arr;
+  }
+
+  if (arr.length < 2 && arr.length > 0) {
+    return arr?.[0];
+  }
+
+  if (dtLanguage === "en") {
+    return arr?.[1];
+  }
+
+  return arr?.[0];
+}
+
+const Index = (props) => {
+  const [options, setOptions] = useState([]);
+  const [form] = Form.useForm();
+  const [loading, setLoading] = useState(false);
+  const [remark, setRemark] = useState(props.currentAlarm?.remark ?? "");
+
+  const onModalOk = async () => {
+    const value = await form.validateFields();
+    // setLoading(true) //拦截器拦截导致loading不false
+    const res = await confirmAlarm({
+      id: props.currentAlarm?.id,
+      remark: value?.remark ?? "",
+    });
+
+    if (res.state === 0) {
+      message.success("确认成功");
+      props.onConfirm();
+      props.onCancel();
+    } else {
+      message.error(res.state_info);
+      if (res.state === 100) {
+        props.onConfirm();
+        props.onCancel();
+      }
+    }
+  };
+
+  const onModalCancel = () => {
+    props.onCancel();
+    form.resetFields();
+  };
+  return (
+    <Modal
+      width={500}
+      title={TranslateText(["告警确认", "Alarm Confirm"])}
+      open={props.open}
+      confirmLoading={loading}
+      onOk={onModalOk}
+      onCancel={onModalCancel}
+      destroyOnClose
+    >
+      <Form
+        name="basic"
+        form={form}
+        colon={false}
+        labelCol={{
+          span: 4,
+        }}
+        wrapperCol={{
+          span: 16,
+        }}
+        preserve={false}
+      >
+        <Form.Item
+          label={TranslateText(["备注", "Remarks"])}
+          name="remark"
+          rules={[
+            {
+              required: false,
+              message: TranslateText(["请输入", "Please input"]),
+            },
+          ]}
+        >
+          <TextArea
+            autoSize={{
+              minRows: 3,
+              maxRows: 4,
+            }}
+          />
+        </Form.Item>
+      </Form>
+    </Modal>
+  );
+};
+
+export default Index;

+ 53 - 0
src/pages/Alarm/components/components/AlarmConfirm/index.module.less

@@ -0,0 +1,53 @@
+.table {
+  :global {
+    .ant-table-body,
+    .ant-table-content,
+    .scroll_bar_restyle {
+      scrollbar-color: auto;
+      scrollbar-width: auto;
+    }
+
+    .ant-table-body::-webkit-scrollbar,
+    .ant-table-content::-webkit-scrollbar,
+    .scroll_bar_restyle::-webkit-scrollbar {
+      height: 5px;
+      width: 5px;
+    }
+
+    .ant-table-body::-webkit-scrollbar-thumb,
+    .ant-table-content::-webkit-scrollbar-thumb,
+    .scroll_bar_restyle::-webkit-scrollbar-thumb {
+      background-color: var(--dt-line-color3) !important;
+      border-radius: 3px;
+    }
+
+    .ant-table-body::-webkit-scrollbar-thumb:hover,
+    .ant-table-content::-webkit-scrollbar-thumb:hover,
+    .scroll_bar_restyle::-webkit-scrollbar-thumb:hover {
+      background-color: #a8a8a8;
+    }
+
+    .ant-table-body::-webkit-scrollbar-thumb:active,
+    .ant-table-content::-webkit-scrollbar-thumb:active,
+    .scroll_bar_restyle::-webkit-scrollbar-thumb:active {
+      background-color: #787878;
+    }
+
+    .ant-table-body::-webkit-scrollbar-track,
+    .ant-table-content::-webkit-scrollbar-track,
+    .scroll_bar_restyle::-webkit-scrollbar-track {
+      background-color: initial;
+    }
+  }
+}
+
+.page {
+  margin-top: 16px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.range {
+  display: flex;
+  justify-content: flex-end;
+}

+ 361 - 0
src/pages/Alarm/components/components/AlarmDetail/index.jsx

@@ -0,0 +1,361 @@
+import React, {
+  useEffect,
+  useState,
+  forwardRef,
+  useImperativeHandle,
+} from "react";
+import API from "../../../../../api/alarm";
+import { Table } from "antd";
+import styles from "./index.module.less";
+import { useMemoizedFn } from "ahooks";
+import { CloseOutlined } from "@ant-design/icons";
+import _ from "lodash";
+import dayjs from "dayjs";
+import ReactECharts from "echarts-for-react";
+import * as echarts from "echarts";
+import { getVariable } from "../../../../../utils/theme";
+import { Descriptions, DatePicker } from "antd";
+const { RangePicker } = DatePicker;
+
+const alarmTypeMap = {
+  1: "过程告警",
+  2: "指令告警",
+  3: "系统告警",
+};
+
+function TranslateText(arr) {
+  const dtLanguage = localStorage.getItem("dtLanguage");
+  if (!Array.isArray(arr)) {
+    return arr;
+  }
+
+  if (arr.length < 2 && arr.length > 0) {
+    return arr?.[0];
+  }
+
+  if (dtLanguage === "en") {
+    return arr?.[1];
+  }
+
+  return arr?.[0];
+}
+
+const alarmStatusMap = {
+  0: TranslateText(["进行中", "In progress"]),
+  1: TranslateText(["进行中", "In progress"]),
+  2: TranslateText(["已结束", "Ended"]),
+};
+
+const timeTranslateMap = {
+  s: TranslateText(["秒", "s"]),
+  m: TranslateText(["分钟", "min"]),
+  h: TranslateText(["小时", "h"]),
+  d: TranslateText(["天", "d"]),
+};
+
+const levelMap = {
+  1: TranslateText(["低", "Low"]),
+  2: TranslateText(["中", "Medium"]),
+  3: TranslateText(["高", "High"]),
+};
+
+const getDur = (text) => {
+  if (text < 60) {
+    return `${text}${timeTranslateMap["s"]}`;
+  } else if (text >= 60 && text < 3600) {
+    return `${parseInt(text / 60)}${timeTranslateMap["m"]}${
+      text % 60 === 0 ? "" : `${parseInt(text % 60)}${timeTranslateMap["s"]}`
+    }`;
+  } else if (text >= 3600 && text < 3600 * 24) {
+    return `${parseInt(text / 3600)}${timeTranslateMap["h"]}${
+      (text / 60) % 60 === 0
+        ? ""
+        : `${parseInt((text / 60) % 60)}${timeTranslateMap["m"]}`
+    }`;
+  } else if (text >= 3600 * 24) {
+    let hour = parseInt((parseInt(text / 36) / 100) % 24);
+    return `${parseInt(text / (3600 * 24))}${timeTranslateMap["d"]}${
+      text % 36 === 0 ? "" : hour > 0 ? `${hour}${timeTranslateMap["h"]}` : ""
+    }`;
+  }
+  // return `${Math.ceil(text / 36) / 100}小时`
+};
+
+const colors = [
+  "#2A6FF6",
+  "#14C9C9",
+  "#2DA641",
+  "#FAD20C",
+  "#F18F49",
+  "#D40000",
+  "#D42A8D",
+  "#7031FF",
+];
+
+const initialOption = {
+  tooltip: {
+    trigger: "axis",
+  },
+  legend: {
+    show: true,
+    top: 0,
+    left: "center",
+    icon: "circle",
+    itemWidth: 8,
+    textStyle: {
+      color: getVariable("--dt-text-color3"),
+    },
+  },
+  grid: {
+    top: 32,
+    left: "0%",
+    right: 64,
+    bottom: "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"),
+    getVariable("--dt-warning-color1"),
+    getVariable("--dt-warning-color1"),
+    getVariable("--dt-error-color1"),
+  ],
+  series: [
+    {
+      name: "值",
+      type: "line",
+      data: [],
+      symbol: "none",
+      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 Index = (props) => {
+  const [chartData, setChartData] = useState({ ...initialOption });
+  const [range, setRange] = useState([
+    dayjs(props.data?.data?.created_time).subtract(3, "day"),
+    dayjs(props.data?.data?.created_time).add(3, "day"),
+  ]);
+  const fetchData = useMemoizedFn(async () => {
+    const res = await API.getAlarmPointData(
+      props.data?.data?.rule_id,
+      range[0].unix(),
+      range[1].unix()
+    );
+    const newData = _.cloneDeep(chartData);
+    newData.series[0].data = res?.data?.point?.map((item) => item[1]);
+    newData.xAxis.data = res?.data?.point?.map((item) =>
+      dayjs.unix(item[0]).format("YYYY-MM-DD HH:mm:ss")
+    );
+    if (res?.data?.hh) {
+      newData.series.push({
+        name: "上上限",
+        type: "line",
+        data: res?.data?.hh?.map((item) => item[1]),
+        symbol: "none",
+        lineStyle: {
+          width: 1,
+          type: "dashed",
+        },
+        endLabel: {
+          show: true,
+          formatter: "{a}",
+          color: "inherit",
+        },
+      });
+    }
+    if (res?.data?.h) {
+      newData.series.push({
+        name: "上限",
+        type: "line",
+        data: res?.data?.h?.map((item) => item[1]),
+        symbol: "none",
+        lineStyle: {
+          width: 1,
+          type: "dashed",
+        },
+        endLabel: {
+          show: true,
+          formatter: "{a}",
+          color: "inherit",
+        },
+      });
+    }
+    if (res?.data?.l) {
+      newData.series.push({
+        name: "下限",
+        type: "line",
+        data: res?.data?.l?.map((item) => item[1]),
+        symbol: "none",
+        lineStyle: {
+          width: 1,
+          type: "dashed",
+        },
+        endLabel: {
+          show: true,
+          formatter: "{a}",
+          color: "inherit",
+        },
+      });
+    }
+    if (res?.data?.ll) {
+      newData.series.push({
+        name: "下下限",
+        type: "line",
+        data: res?.data?.ll?.map((item) => item[1]),
+        symbol: "none",
+        lineStyle: {
+          width: 1,
+          type: "dashed",
+        },
+        endLabel: {
+          show: true,
+          formatter: "{a}",
+          color: "inherit",
+        },
+      });
+    }
+    if (res?.data?.threshold) {
+      newData.series.push({
+        name: "告警值",
+        type: "line",
+        data: res?.data?.threshold?.map((item) => item[1]),
+        symbol: "none",
+        lineStyle: {
+          width: 1,
+          type: "dashed",
+        },
+        endLabel: {
+          show: true,
+          formatter: "{a}",
+          color: "inherit",
+        },
+      });
+    }
+    setChartData(newData);
+  });
+  useEffect(() => {
+    fetchData();
+  }, [range, props.data?.data?.rule_id]);
+  return (
+    <>
+      <Descriptions>
+        <Descriptions.Item
+          span={props.data?.data?.point_id ? 2 : 3}
+          label="告警名称"
+        >
+          {props.data?.data?.name}
+        </Descriptions.Item>
+        {props.data?.data?.point_id && (
+          <Descriptions.Item label="点位编号">
+            {props.data?.data?.point_id}
+          </Descriptions.Item>
+        )}
+        <Descriptions.Item label="告警时间">
+          {props.data?.data?.created_time}
+        </Descriptions.Item>
+        <Descriptions.Item label="恢复时间">
+          {props.data?.data?.recovery_time || "-"}
+        </Descriptions.Item>
+        <Descriptions.Item label="持续时间">
+          {getDur(props?.data?.data?.duration)}
+        </Descriptions.Item>
+        <Descriptions.Item label="告警类型">
+          {alarmTypeMap[props.data?.data?.type]}
+        </Descriptions.Item>
+        <Descriptions.Item label="告警级别">
+          {levelMap[props.data?.data?.alarm_level]}
+        </Descriptions.Item>
+        <Descriptions.Item label="告警状态">
+          {alarmStatusMap[props.data?.data?.status]}
+        </Descriptions.Item>
+        <Descriptions.Item label="确认时间">
+          {props.data?.data?.confirmed_time}
+        </Descriptions.Item>
+        <Descriptions.Item label="确认人">
+          {props.data?.data?.confirmed_oper_name}
+        </Descriptions.Item>
+        <Descriptions.Item label="备注">
+          {props.data?.data?.remark}
+        </Descriptions.Item>
+      </Descriptions>
+      <div className={styles.range}>
+        <RangePicker
+          value={range}
+          onChange={setRange}
+          style={{ width: 440, marginTop: 50 }}
+        />
+      </div>
+
+      <ReactECharts
+        style={{ height: 250 }}
+        option={chartData}
+        notMerge={true}
+      />
+    </>
+  );
+};
+
+export default Index;

+ 53 - 0
src/pages/Alarm/components/components/AlarmDetail/index.module.less

@@ -0,0 +1,53 @@
+.table {
+  :global {
+    .ant-table-body,
+    .ant-table-content,
+    .scroll_bar_restyle {
+      scrollbar-color: auto;
+      scrollbar-width: auto;
+    }
+
+    .ant-table-body::-webkit-scrollbar,
+    .ant-table-content::-webkit-scrollbar,
+    .scroll_bar_restyle::-webkit-scrollbar {
+      height: 5px;
+      width: 5px;
+    }
+
+    .ant-table-body::-webkit-scrollbar-thumb,
+    .ant-table-content::-webkit-scrollbar-thumb,
+    .scroll_bar_restyle::-webkit-scrollbar-thumb {
+      background-color: var(--dt-line-color3) !important;
+      border-radius: 3px;
+    }
+
+    .ant-table-body::-webkit-scrollbar-thumb:hover,
+    .ant-table-content::-webkit-scrollbar-thumb:hover,
+    .scroll_bar_restyle::-webkit-scrollbar-thumb:hover {
+      background-color: #a8a8a8;
+    }
+
+    .ant-table-body::-webkit-scrollbar-thumb:active,
+    .ant-table-content::-webkit-scrollbar-thumb:active,
+    .scroll_bar_restyle::-webkit-scrollbar-thumb:active {
+      background-color: #787878;
+    }
+
+    .ant-table-body::-webkit-scrollbar-track,
+    .ant-table-content::-webkit-scrollbar-track,
+    .scroll_bar_restyle::-webkit-scrollbar-track {
+      background-color: initial;
+    }
+  }
+}
+
+.page {
+  margin-top: 16px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.range {
+  display: flex;
+  justify-content: flex-end;
+}

+ 306 - 0
src/pages/Alarm/components/components/AlarmHistory/index.jsx

@@ -0,0 +1,306 @@
+import React, {
+  useEffect,
+  useState,
+  forwardRef,
+  useImperativeHandle,
+} from "react";
+import { Tooltip, Space, Pagination, Button } from "antd";
+import { Table } from "antd";
+import styles from "./index.module.less";
+import { useMemoizedFn } from "ahooks";
+import { CloseOutlined } from "@ant-design/icons";
+import _ from "lodash";
+import dayjs from "dayjs";
+import API from "../../../../../api/alarm";
+import AlarmConfirm from "../AlarmConfirm";
+
+function TranslateText(arr) {
+  const dtLanguage = localStorage.getItem("dtLanguage");
+  if (!Array.isArray(arr)) {
+    return arr;
+  }
+
+  if (arr.length < 2 && arr.length > 0) {
+    return arr?.[0];
+  }
+
+  if (dtLanguage === "en") {
+    return arr?.[1];
+  }
+
+  return arr?.[0];
+}
+
+const timeTranslateMap = {
+  s: TranslateText(["秒", "s"]),
+  m: TranslateText(["分钟", "min"]),
+  h: TranslateText(["小时", "h"]),
+  d: TranslateText(["天", "d"]),
+};
+
+const statusEnum = {
+  0: "执行中",
+  1: "执行成功",
+  2: "执行失败",
+  3: "已取消",
+  4: "取消中",
+};
+
+const statusEnumColor = {
+  0: "blue",
+  1: "green",
+  2: "red",
+  3: "gray",
+  4: "gray",
+};
+
+const alarmTypeMap = {
+  1: "过程告警",
+  2: "指令告警",
+  3: "系统告警",
+};
+
+const levelMap = {
+  1: TranslateText(["低", "Low"]),
+  2: TranslateText(["中", "Medium"]),
+  3: TranslateText(["高", "High"]),
+};
+
+const levelColor = {
+  1: styles.low,
+  2: styles.medium,
+  3: styles.high,
+};
+
+const statusColor = {
+  0: styles.high,
+  1: styles.high,
+  2: styles.low,
+};
+
+const alarmStatusMap = {
+  0: TranslateText(["进行中", "In progress"]),
+  1: TranslateText(["进行中", "In progress"]),
+  2: TranslateText(["已结束", "Ended"]),
+};
+
+const showTotal = (total) => `共${total}条`;
+
+const Index = (props) => {
+  const [tags, setTags] = useState([{ name: "详情1", id: "1" }]);
+  const [current, setCurrent] = useState("history");
+  const [loading, setLoading] = useState(false);
+  const [data, setData] = useState([]);
+  const [total, setTotal] = useState(1);
+  const [page, setPage] = useState(1);
+  const [currentAlarm, setCurrentAlarm] = useState({});
+  const [showConfirm, setShowConfirm] = useState(false);
+
+  const fetchData = useMemoizedFn(async () => {
+    if (props.ruleId === -1) return;
+    const res = await API.getAlarmHistory(page, props.ruleId);
+    setData(res?.data?.history);
+    setTotal(res?.data?.total);
+  });
+
+  const onConfirm = (record) => {
+    setCurrentAlarm(record);
+    setShowConfirm(true);
+  };
+  useEffect(() => {
+    fetchData();
+  }, [page, props.ruleId, fetchData]);
+  const columns = [
+    // {
+    //   title: '序号',
+    //   width: '58px',
+    //   render: (text, record, index) =>
+    //     `${(paginationProps.current - 1) * (paginationProps.pageSize) + (index + 1)}`
+    // },
+    {
+      title: TranslateText(["告警时间", "Alarm time"]),
+      width: 180,
+      dataIndex: "created_time",
+      render: (text, record) =>
+        text ? dayjs(text).format("YYYY-MM-DD HH:mm:ss") : "暂无",
+    },
+    {
+      title: TranslateText(["告警名称", "Alarm name"]),
+      dataIndex: "name",
+      ellipsis: true,
+      width: 160,
+      render: (text, record) => {
+        return <Tooltip title={text}>{text}</Tooltip>;
+      },
+    },
+    {
+      title: () => {
+        return (
+          <Tooltip title={TranslateText(["告警级别", "Level"])}>
+            <span>{TranslateText(["告警级别", "Level"])}</span>
+          </Tooltip>
+        );
+      },
+      onHeaderCell: () => ({
+        style: {
+          whiteSpace: "nowrap",
+          overflow: "hidden",
+          textOverflow: "ellipsis",
+        },
+      }),
+      dataIndex: "alarm_level",
+      width: 100,
+      render: (text, record) => {
+        return (
+          <div className={styles.state}>
+            <div className={levelColor[text]} />
+            <div>{levelMap[text]}</div>
+          </div>
+        );
+      },
+    },
+    {
+      title: TranslateText(["恢复时间", "Recovery time"]),
+      dataIndex: "recovery_time",
+      width: 180,
+      render: (text, record) =>
+        text ? dayjs(text).format("YYYY-MM-DD HH:mm:ss") : "-",
+    },
+    {
+      title: TranslateText(["持续时间", "duration"]),
+      dataIndex: "duration",
+      width: 180,
+      render: (text, record) => {
+        if (text < 60) {
+          return `${text}${timeTranslateMap["s"]}`;
+        } else if (text >= 60 && text < 3600) {
+          return `${parseInt(text / 60)}${timeTranslateMap["m"]}${
+            text % 60 === 0
+              ? ""
+              : `${parseInt(text % 60)}${timeTranslateMap["s"]}`
+          }`;
+        } else if (text >= 3600 && text < 3600 * 24) {
+          return `${parseInt(text / 3600)}${timeTranslateMap["h"]}${
+            (text / 60) % 60 === 0
+              ? ""
+              : `${parseInt((text / 60) % 60)}${timeTranslateMap["m"]}`
+          }`;
+        } else if (text >= 3600 * 24) {
+          let hour = parseInt((parseInt(text / 36) / 100) % 24);
+          return `${parseInt(text / (3600 * 24))}${timeTranslateMap["d"]}${
+            text % 36 === 0
+              ? ""
+              : hour > 0
+              ? `${hour}${timeTranslateMap["h"]}`
+              : ""
+          }`;
+        }
+        // return `${Math.ceil(text / 36) / 100}小时`
+      },
+    },
+    {
+      title: TranslateText(["告警状态", "Status"]),
+      dataIndex: "status",
+      width: 100,
+      render: (text, record) => {
+        return (
+          <div className={styles.state}>
+            <div className={statusColor[text]} />
+            <div>{alarmStatusMap[text]}</div>
+          </div>
+        );
+      },
+    },
+    {
+      title: () => {
+        return (
+          <Tooltip title={TranslateText(["确认人", "Confirm person"])}>
+            <span>{TranslateText(["确认人", "Confirm person"])}</span>
+          </Tooltip>
+        );
+      },
+      onHeaderCell: () => ({
+        style: {
+          whiteSpace: "nowrap",
+          overflow: "hidden",
+          textOverflow: "ellipsis",
+        },
+      }),
+      dataIndex: "confirmed_oper_name",
+      width: 100,
+    },
+    {
+      title: TranslateText(["确认时间", "Confirm time"]),
+      dataIndex: "confirmed_time",
+      width: 180,
+      render: (text, record) =>
+        text ? dayjs(text).format("YYYY-MM-DD HH:mm:ss") : "-",
+    },
+    {
+      title: TranslateText(["备注", "Remarks"]),
+      dataIndex: "remark",
+      width: 120,
+      ellipsis: "true",
+      render: (text, record) => {
+        return <Tooltip title={text}>{text}</Tooltip>;
+      },
+    },
+    {
+      title: TranslateText(["操作", "Operation"]),
+      dataIndex: "action",
+      width: 120,
+      fixed: "right",
+      render: (_, record) => (
+        <div style={{ display: "flex" }}>
+          <Button
+            onClick={() => props.onSelect(record)}
+            style={{ width: 0 }}
+            type="link"
+          >
+            详情
+          </Button>
+          <Button
+            onClick={() => onConfirm(record)}
+            style={{ width: 0 }}
+            type="link"
+          >
+            确认
+          </Button>
+        </div>
+      ),
+    },
+  ];
+  return (
+    <>
+      <Table
+        className={styles.table}
+        loading={loading}
+        columns={columns}
+        dataSource={data}
+        rowKey="id"
+        pagination={false}
+        size="middle"
+        scroll={{
+          x: 1160,
+          y: 432,
+        }}
+      />
+      <div className={styles.page}>
+        <Pagination
+          onChange={setPage}
+          total={total}
+          showTotal={showTotal}
+          showSizeChanger={false}
+        />
+      </div>
+      <AlarmConfirm
+        currentAlarm={currentAlarm}
+        open={showConfirm}
+        onCancel={() => setShowConfirm(false)}
+        onConfirm={fetchData}
+      />
+    </>
+  );
+};
+
+export default Index;

+ 77 - 0
src/pages/Alarm/components/components/AlarmHistory/index.module.less

@@ -0,0 +1,77 @@
+.table {
+  :global {
+    .ant-table-body,
+    .ant-table-content,
+    .scroll_bar_restyle {
+      scrollbar-color: auto;
+      scrollbar-width: auto;
+    }
+
+    .ant-table-body::-webkit-scrollbar,
+    .ant-table-content::-webkit-scrollbar,
+    .scroll_bar_restyle::-webkit-scrollbar {
+      height: 5px;
+      width: 5px;
+    }
+
+    .ant-table-body::-webkit-scrollbar-thumb,
+    .ant-table-content::-webkit-scrollbar-thumb,
+    .scroll_bar_restyle::-webkit-scrollbar-thumb {
+      background-color: var(--dt-line-color3) !important;
+      border-radius: 3px;
+    }
+
+    .ant-table-body::-webkit-scrollbar-thumb:hover,
+    .ant-table-content::-webkit-scrollbar-thumb:hover,
+    .scroll_bar_restyle::-webkit-scrollbar-thumb:hover {
+      background-color: #a8a8a8;
+    }
+
+    .ant-table-body::-webkit-scrollbar-thumb:active,
+    .ant-table-content::-webkit-scrollbar-thumb:active,
+    .scroll_bar_restyle::-webkit-scrollbar-thumb:active {
+      background-color: #787878;
+    }
+
+    .ant-table-body::-webkit-scrollbar-track,
+    .ant-table-content::-webkit-scrollbar-track,
+    .scroll_bar_restyle::-webkit-scrollbar-track {
+      background-color: initial;
+    }
+  }
+}
+
+.page {
+  margin-top: 16px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.state {
+  display: flex;
+  align-items: center;
+}
+
+.high {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background-color: var(--dt-error-color1);
+  margin-right: 6px;
+}
+
+.medium {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background-color: var(--dt-warning-color1);
+  margin-right: 6px;
+}
+
+.low {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background-color: var(--dt-text-color4);
+  margin-right: 6px;
+}

+ 106 - 0
src/pages/Alarm/components/detail.jsx

@@ -0,0 +1,106 @@
+import React, {
+  useEffect,
+  useState,
+  forwardRef,
+  useImperativeHandle,
+} from "react";
+import { Form, Input, Select, message, Switch, Space, Col } from "antd";
+import { Modal, Button, Radio } from "antd";
+import styles from "./detail.module.less";
+import API from "../../../api/alarm";
+import { useMemoizedFn } from "ahooks";
+import { CloseOutlined } from "@ant-design/icons";
+import _ from "lodash";
+import AlarmHistory from "./components/AlarmHistory";
+import AlarmDetail from "./components/AlarmDetail";
+
+const Detail = (props) => {
+  const { options } = props;
+  const [tags, setTags] = useState([]);
+  const [current, setCurrent] = useState("history");
+  const [ruleId, setRuleId] = useState(-1);
+  const getRuleIdByPointId = useMemoizedFn(async (pointId) => {
+    const res = await API.getRuleIdByPointId(pointId);
+    setRuleId(res?.data?.[0]?.id);
+    setCurrent("history");
+    setTags([]);
+  });
+  useEffect(() => {
+    if (options.ruleId) {
+      setRuleId(options.ruleId);
+      setCurrent("history");
+      setTags([]);
+    } else if (options.pointId) {
+      getRuleIdByPointId(options.pointId);
+    } else if (options.detail) {
+      setRuleId(options.detail.rule_id);
+      setTags([
+        {
+          id: options.detail.id,
+          data: options.detail,
+        },
+      ]);
+      setCurrent(options.detail.id);
+    }
+  }, [options]);
+  const currentData = tags.find((item) => item.id === current) ?? {};
+  const onRemove = (index) => {
+    const newTags = _.cloneDeep(tags);
+    newTags.splice(index, 1);
+    setTags(newTags);
+  };
+  const onSelect = (record) => {
+    if (!tags.some((item) => item.id === record.id)) {
+      const newTags = _.cloneDeep(tags);
+      newTags.push({
+        id: record.id,
+        data: record,
+      });
+      setTags(newTags);
+    }
+    setCurrent(record.id);
+  };
+  return (
+    <Modal
+      title="告警记录"
+      open={props.open}
+      width={1200}
+      onCancel={() => {
+        props.onCancel();
+      }}
+      zIndex="1001"
+      footer={null}
+    >
+      <div className={styles.tags}>
+        <Radio.Group
+          value={current}
+          onChange={(e) => setCurrent(e.target.value)}
+          size="large"
+        >
+          <Radio.Button value="history">告警记录</Radio.Button>
+          {tags.map((item, index) => (
+            <Radio.Button value={item.id}>
+              <div className={styles.tagContainer}>
+                <div className={styles.label}>详情{index + 1}</div>
+                <CloseOutlined
+                  onClick={() => {
+                    if (current === item.id) {
+                      setCurrent("history");
+                    }
+                    onRemove(index);
+                  }}
+                />
+              </div>
+            </Radio.Button>
+          ))}
+        </Radio.Group>
+      </div>
+      {current === "history" && (
+        <AlarmHistory onSelect={onSelect} ruleId={ruleId}></AlarmHistory>
+      )}
+      {current !== "history" && <AlarmDetail data={currentData}></AlarmDetail>}
+    </Modal>
+  );
+};
+
+export default Detail;

+ 25 - 0
src/pages/Alarm/components/detail.module.less

@@ -0,0 +1,25 @@
+.label {
+  width: 60px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  display: inline-block;
+}
+
+.tagContainer {
+  display: flex;
+  align-items: center;
+}
+.tags {
+  :global {
+    .ant-radio-button-wrapper {
+      border-color: var(--dt-line-color2) !important;
+    }
+    .ant-radio-button-wrapper-checked:not(
+        .ant-radio-button-wrapper-disabled
+      )::before {
+      background-color: var(--dt-line-color2) !important;
+    }
+  }
+  margin-bottom: 16px;
+}

+ 14 - 23
src/pages/Alarm/components/easy.jsx

@@ -253,12 +253,12 @@ const Easy = (props, ref) => {
 
   useEffect(() => {
     async function run() {
-      if (alarmId && alarmId != -1) {
-        // const { state, data } = await API.detailAlarm(Number(alarmId))
-        setData(alarmId);
-        setDeferUnit(alarmId?.defer_unit);
-        setStatus(alarmId.defer_seconds > 0 ? 1 : 2);
-        setDateTime(alarmId?.defer_seconds);
+      if (alarmId && alarmId !== -1) {
+        const { state, data } = await API2.detailAlarm(Number(alarmId));
+        setData(data);
+        setDeferUnit(data?.defer_unit);
+        setStatus(data.defer_seconds > 0 ? 1 : 2);
+        setDateTime(data?.defer_seconds);
       }
     }
     run();
@@ -317,7 +317,9 @@ const Easy = (props, ref) => {
     } else {
       form.setFieldsValue({
         alarm_level: 1,
+        sub_type: 13,
       });
+      setSubType(13);
     }
   }, [data]);
 
@@ -407,22 +409,11 @@ const Easy = (props, ref) => {
         span: 17,
       }}
     >
-      <Form.Item label={TranslateText(["告警模式", "Mode"])} name={"sub_type"}>
-        <Select
-          style={{ width: 240 }}
-          disabled={alarmId !== -1}
-          options={[
-            {
-              label: "离散报警",
-              value: 13,
-            },
-            {
-              label: "限值报警",
-              value: 14,
-            },
-          ]}
-          placeholder={TranslateText(["请选择告警模式", "Please choose"])}
-        />
+      <Form.Item label={TranslateText(["告警类型", "Mode"])} name={"sub_type"}>
+        <Radio.Group disabled={alarmId !== -1}>
+          <Radio value={13}>离散报警</Radio>
+          <Radio value={14}>限值报警</Radio>
+        </Radio.Group>
       </Form.Item>
       <Form.Item
         label={TranslateText(["点位名称", "Point name"])}
@@ -717,7 +708,7 @@ const Easy = (props, ref) => {
         <Form.Item
           label={
             <Button
-            type="link"
+              type="link"
               onClick={() => {
                 setShowMore(true);
               }}

Деякі файли не було показано, через те що забагато файлів було змінено