Bladeren bron

fix: merge

wzyyy 3 jaren geleden
bovenliggende
commit
9a53ea6adf
35 gewijzigde bestanden met toevoegingen van 1241 en 339 verwijderingen
  1. BIN
      public/images/home/comprehensive.png
  2. BIN
      public/images/home/device.png
  3. BIN
      public/images/home/ops.png
  4. 12 14
      src/components/DashBoard/echarts.tsx
  5. 1 1
      src/components/DashBoard/topCard.tsx
  6. 1 1
      src/components/RightContent/index.tsx
  7. 12 1
      src/pages/Log/index.tsx
  8. 34 31
      src/pages/device/Instance/Detail/Diagnose/index.less
  9. 2 2
      src/pages/device/Instance/Detail/Diagnose/index.tsx
  10. 5 3
      src/pages/device/Instance/Detail/Running/Property/PropertyCard.tsx
  11. 9 1
      src/pages/device/Instance/Detail/index.tsx
  12. 6 0
      src/pages/device/Instance/index.tsx
  13. 9 2
      src/pages/device/Product/Detail/index.tsx
  14. 5 3
      src/pages/device/Product/index.tsx
  15. 163 0
      src/pages/home/components/DeviceChoose.tsx
  16. 8 5
      src/pages/home/components/Guide.tsx
  17. 23 31
      src/pages/home/components/Pie.tsx
  18. 119 0
      src/pages/home/components/ProductChoose.tsx
  19. 6 6
      src/pages/home/components/Statistics.tsx
  20. 40 62
      src/pages/home/components/Steps.tsx
  21. 1 1
      src/pages/home/components/Title.tsx
  22. 106 26
      src/pages/home/components/index.less
  23. 197 25
      src/pages/home/comprehensive/index.tsx
  24. 98 27
      src/pages/home/device/index.tsx
  25. 3 2
      src/pages/home/index.tsx
  26. 206 1
      src/pages/home/ops/index.tsx
  27. 11 1
      src/pages/media/DashBoard/index.tsx
  28. 95 71
      src/pages/media/Home/index.tsx
  29. 12 1
      src/pages/rule-engine/Instance/index.tsx
  30. 5 0
      src/pages/system/Platforms/Api/base.tsx
  31. 15 7
      src/pages/system/Platforms/Api/basePage.tsx
  32. 5 1
      src/pages/system/Platforms/Setting/index.tsx
  33. 14 12
      src/pages/system/Platforms/index.tsx
  34. 18 0
      src/pages/system/Platforms/service.ts
  35. 0 1
      src/pages/system/Role/index.tsx

BIN
public/images/home/comprehensive.png


BIN
public/images/home/device.png


BIN
public/images/home/ops.png


+ 12 - 14
src/components/DashBoard/echarts.tsx

@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useRef } from 'react';
+import { useEffect, useRef } from 'react';
 import * as echarts from 'echarts/core';
 import type { ECharts, EChartsOption } from 'echarts';
 import {
@@ -57,11 +57,13 @@ export default (props: EchartsProps) => {
   const chartsRef = useRef<any>(null);
 
   const initEcharts = (dom: HTMLDivElement) => {
-    chartsRef.current = echarts.init(dom);
-    if (props.options) {
-      chartsRef.current.setOption(props.options);
-    } else {
-      chartsRef.current.setOption(DefaultOptions);
+    if (!chartsRef.current) {
+      chartsRef.current = echarts.init(dom);
+      if (props.options) {
+        chartsRef.current.setOption(props.options);
+      } else {
+        chartsRef.current.setOption(DefaultOptions);
+      }
     }
   };
 
@@ -72,12 +74,6 @@ export default (props: EchartsProps) => {
     }
   };
 
-  const updateOptions = useCallback(() => {
-    if (chartsRef.current && props.options) {
-      chartsRef.current.setOption(props.options);
-    }
-  }, [props.options]);
-
   useEffect(() => {
     (window as Window).addEventListener('resize', updateSize);
 
@@ -87,8 +83,10 @@ export default (props: EchartsProps) => {
   }, []);
 
   useEffect(() => {
-    updateOptions();
-  }, [props.options, chartsRef.current]);
+    if (chartsRef.current && props.options) {
+      chartsRef.current.setOption(props.options);
+    }
+  }, [props.options]);
 
   return (
     <div

+ 1 - 1
src/components/DashBoard/topCard.tsx

@@ -17,7 +17,7 @@ interface FooterItem {
 
 interface CardItemProps {
   span: number;
-  title: string;
+  title: string | React.ReactNode;
   value: any;
   footer: false | FooterItem[];
   showValue?: boolean;

+ 1 - 1
src/components/RightContent/index.tsx

@@ -55,7 +55,7 @@ const GlobalHeaderRight: React.FC = () => {
       <span
         className={styles.action}
         onClick={() => {
-          window.open('https://doc.jetlinks.cn');
+          window.open('http://doc.jetlinks.cn');
         }}
       >
         <QuestionCircleOutlined />

+ 12 - 1
src/pages/Log/index.tsx

@@ -1,10 +1,21 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
 import Access from '@/pages/Log/Access';
 import System from '@/pages/Log/System';
+import useLocation from '@/hooks/route/useLocation';
 
 const Log = () => {
   const [tab, setTab] = useState<string>('access');
+
+  const location = useLocation();
+
+  useEffect(() => {
+    const { state } = location;
+    if (state?.key) {
+      setTab(state?.key);
+    }
+  }, [location]);
+
   const list = [
     {
       key: 'access',

+ 34 - 31
src/pages/device/Instance/Detail/Diagnose/index.less

@@ -1,37 +1,40 @@
-.header {
-  width: 100%;
-}
-.header-message {
-  width: 100%;
-  background: url('/images/diagnose/back.png') no-repeat;
-  background-size: 100% 100%;
-}
+.diagnose {
+  .header {
+    width: 100%;
+  }
 
-.container {
-  margin-top: 20px;
-}
+  .header-message {
+    width: 100%;
+    background: url('/images/diagnose/back.png') no-repeat;
+    background-size: 100% 100%;
+  }
 
-.item-box {
-  width: 100%;
-  padding: 10px;
-  background-repeat: no-repeat;
-  background-size: 100% 100%;
-  cursor: pointer;
-}
+  .container {
+    margin-top: 20px;
+  }
 
-.item-title {
-  font-weight: 700;
-  font-size: 14px;
-}
+  .item-box {
+    width: 100%;
+    padding: 10px;
+    background-repeat: no-repeat;
+    background-size: 100% 100%;
+    cursor: pointer;
+  }
 
-.item-context {
-  height: 40px;
-  font-weight: 700;
-  font-size: 24px;
-}
+  .item-title {
+    font-weight: 700;
+    font-size: 14px;
+  }
+
+  .item-context {
+    height: 40px;
+    font-weight: 700;
+    font-size: 24px;
+  }
 
-.item-message {
-  color: rgba(0, 0, 0, 0.85);
-  font-weight: 400;
-  font-size: 14px;
+  .item-message {
+    color: rgba(0, 0, 0, 0.85);
+    font-weight: 400;
+    font-size: 14px;
+  }
 }

+ 2 - 2
src/pages/device/Instance/Detail/Diagnose/index.tsx

@@ -131,9 +131,9 @@ const Diagnose = () => {
     };
   }, []);
   return (
-    <Card>
+    <Card className="diagnose">
       <div className={current === 'message' ? 'header-message' : 'header'}>
-        <Row gutter={24} style={{ padding: 10 }}>
+        <Row gutter={24} style={{ padding: 10, width: '100%' }}>
           {list.map((item: ListProps) => (
             <Col
               span={8}

+ 5 - 3
src/pages/device/Instance/Detail/Running/Property/PropertyCard.tsx

@@ -70,9 +70,11 @@ const Property = (props: Props) => {
                 />
               </Tooltip>
             )}
-          <Tooltip placement="top" title="获取最新属性值">
-            <SyncOutlined onClick={refreshProperty} />
-          </Tooltip>
+          {data.expands?.type.includes('read') && (
+            <Tooltip placement="top" title="获取最新属性值">
+              <SyncOutlined onClick={refreshProperty} />
+            </Tooltip>
+          )}
           <Tooltip placement="top" title="详情">
             <UnorderedListOutlined
               onClick={() => {

+ 9 - 1
src/pages/device/Instance/Detail/index.tsx

@@ -26,6 +26,7 @@ import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
 import { PermissionButton } from '@/components';
 import { QuestionCircleOutlined } from '@ant-design/icons';
 import Service from '@/pages/device/Instance/service';
+import useLocation from '@/hooks/route/useLocation';
 
 export const deviceStatus = new Map();
 deviceStatus.set('online', <Badge status="success" text={'在线'} />);
@@ -38,6 +39,7 @@ const InstanceDetail = observer(() => {
   const params = useParams<{ id: string }>();
   const service = new Service('device-instance');
   const { permission } = PermissionButton.usePermission('device/Instance');
+  const location = useLocation();
 
   // const resetMetadata = async () => {
   //   const resp = await service.deleteMetadata(params.id);
@@ -155,7 +157,6 @@ const InstanceDetail = observer(() => {
 
   const getDetail = (id: string) => {
     service.detail(id).then((response) => {
-      console.log(response.result);
       InstanceModel.detail = response?.result;
       const datalist = [...baseList];
       if (response.result.protocol === 'modbus-tcp') {
@@ -244,6 +245,13 @@ const InstanceDetail = observer(() => {
     return () => subscription.unsubscribe();
   }, []);
 
+  useEffect(() => {
+    const { state } = location;
+    if (state && state?.tab) {
+      setTab(state?.tab);
+    }
+  }, [location]);
+
   return (
     <PageContainer
       className={'page-title-show'}

+ 6 - 0
src/pages/device/Instance/index.tsx

@@ -90,6 +90,12 @@ const Instance = () => {
           type: 'or',
         },
       ]);
+      if (location.state && location.state?.save) {
+        setVisible(true);
+        setCurrent({});
+      } else if (location.state && location.state?.import) {
+        setImportVisible(true);
+      }
     }
   }, [location]);
 

+ 9 - 2
src/pages/device/Product/Detail/index.tsx

@@ -1,11 +1,11 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import { useIntl, useLocation, useParams } from 'umi';
+import { useIntl, useParams } from 'umi';
 import { Badge, Card, Descriptions, message, Popconfirm, Space, Spin, Switch, Tooltip } from 'antd';
 import BaseInfo from '@/pages/device/Product/Detail/BaseInfo';
 import { observer } from '@formily/react';
 import { productModel, service } from '@/pages/device/Product';
 import { useCallback, useEffect, useState } from 'react';
-import { useHistory } from '@/hooks';
+import { useHistory, useLocation } from '@/hooks';
 import Metadata from '@/pages/device/components/Metadata';
 import Access from '@/pages/device/Product/Detail/Access';
 import type { DeviceMetadata } from '@/pages/device/Product/typings';
@@ -217,6 +217,13 @@ const ProductDetail = observer(() => {
     return () => subscription.unsubscribe();
   }, []);
 
+  useEffect(() => {
+    const { state } = location;
+    if (state && state?.tab) {
+      setMode(state?.tab);
+    }
+  }, [location]);
+
   return (
     <PageContainer
       className={'page-title-show'}

+ 5 - 3
src/pages/device/Product/index.tsx

@@ -12,7 +12,7 @@ import {
 import Service from '@/pages/device/Product/service';
 import { observer } from '@formily/react';
 import { model } from '@formily/reactive';
-import { useHistory, useLocation } from 'umi';
+import { useHistory } from 'umi';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import { useEffect, useRef, useState } from 'react';
@@ -25,6 +25,7 @@ import { downloadObject } from '@/utils/util';
 import { service as categoryService } from '@/pages/device/Category';
 import { service as deptService } from '@/pages/system/Department';
 import { omit } from 'lodash';
+import useLocation from '@/hooks/route/useLocation';
 
 export const service = new Service('device-product');
 export const statusMap = {
@@ -81,11 +82,12 @@ const Product = observer(() => {
   const location = useLocation();
 
   useEffect(() => {
-    if ((location as any).query?.save === 'true') {
+    const { state } = location;
+    if (state && state.save) {
       setCurrent(undefined);
       setVisible(true);
     }
-  }, []);
+  }, [location]);
 
   const deleteItem = async (id: string) => {
     const response: any = await service.remove(id);

+ 163 - 0
src/pages/home/components/DeviceChoose.tsx

@@ -0,0 +1,163 @@
+import { message, Modal } from 'antd';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
+import { service } from '@/pages/device/Instance';
+import SearchComponent from '@/components/SearchComponent';
+import type { DeviceItem } from '@/pages/media/Home/typings';
+import { useEffect, useRef, useState } from 'react';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { BadgeStatus, PermissionButton } from '@/components';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import useHistory from '@/hooks/route/useHistory';
+import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+
+interface DeviceModalProps {
+  visible: boolean;
+  url?: string;
+  onCancel: () => void;
+}
+
+export default (props: DeviceModalProps) => {
+  const intl = useIntl();
+  const history = useHistory();
+  const permission = PermissionButton.usePermission('device/Instance').permission;
+
+  const actionRef = useRef<ActionType>();
+  const [searchParam, setSearchParam] = useState({});
+  const [deviceItem, setDeviceItem] = useState<any>({});
+
+  useEffect(() => {
+    if (!props.visible) {
+      setDeviceItem({});
+      setSearchParam({});
+    }
+  }, [props.visible]);
+
+  const cancel = () => {
+    if (props.onCancel) {
+      props.onCancel();
+    }
+  };
+
+  const columns: ProColumns<DeviceItem>[] = [
+    {
+      dataIndex: 'id',
+      title: '设备ID',
+      width: 220,
+    },
+    {
+      dataIndex: 'name',
+      title: '设备名称',
+    },
+    {
+      dataIndex: 'productName',
+      title: '产品名称',
+    },
+    {
+      dataIndex: 'modifyTime',
+      title: '注册时间',
+      valueType: 'dateTime',
+      width: 200,
+    },
+    {
+      dataIndex: 'state',
+      title: intl.formatMessage({
+        id: 'pages.searchTable.titleStatus',
+        defaultMessage: '状态',
+      }),
+      render: (_, record) => (
+        <BadgeStatus
+          status={record.state.value}
+          statusNames={{
+            online: StatusColorEnum.success,
+            offline: StatusColorEnum.error,
+            notActive: StatusColorEnum.processing,
+          }}
+          text={record.state.text}
+        />
+      ),
+      valueType: 'select',
+      valueEnum: {
+        offline: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.offLine',
+            defaultMessage: '离线',
+          }),
+          status: 'offline',
+        },
+        online: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.onLine',
+            defaultMessage: '在线',
+          }),
+          status: 'online',
+        },
+      },
+      filterMultiple: false,
+    },
+  ];
+
+  return (
+    <Modal
+      title={'选择设备'}
+      onCancel={cancel}
+      onOk={() => {
+        if (deviceItem?.id) {
+          if (!!permission.update) {
+            history.push(
+              `${getMenuPathByParams(MENUS_CODE['device/Instance/Detail'], deviceItem.id)}`,
+              {
+                tab: 'diagnose',
+              },
+            );
+          } else {
+            message.warning('暂无权限,请联系管理员');
+            cancel();
+          }
+        } else {
+          message.warning('请选择设备');
+        }
+      }}
+      destroyOnClose={true}
+      maskClosable={false}
+      visible={props.visible}
+      width={1000}
+    >
+      <SearchComponent<DeviceItem>
+        field={columns}
+        enableSave={false}
+        model="simple"
+        onSearch={async (data) => {
+          setSearchParam(data);
+        }}
+        target="choose-device"
+      />
+      <ProTable<DeviceItem>
+        actionRef={actionRef}
+        columns={columns}
+        rowKey={'id'}
+        search={false}
+        request={(params) =>
+          service.query({
+            ...params,
+            sorts: [
+              {
+                name: 'createTime',
+                order: 'desc',
+              },
+            ],
+          })
+        }
+        rowSelection={{
+          type: 'radio',
+          selectedRowKeys: deviceItem.id ? [deviceItem.id] : undefined,
+          onSelect: (record) => {
+            setDeviceItem(record);
+          },
+        }}
+        tableAlertOptionRender={() => false}
+        params={searchParam}
+      />
+    </Modal>
+  );
+};

+ 8 - 5
src/pages/home/components/Guide.tsx

@@ -20,7 +20,7 @@ interface GuideItemProps {
   name: string;
   english: string;
   url: string;
-  param: string;
+  param?: Record<string, any>;
   index?: number;
   auth: boolean;
 }
@@ -31,14 +31,14 @@ const GuideItem = (props: GuideItemProps) => {
 
   const jumpPage = () => {
     if (path && props.auth) {
-      history.push(`${path}${props.param}`);
+      history.push(`${path}`, props.param);
     } else {
       message.warning('暂无权限,请联系管理员');
     }
   };
 
   return (
-    <div className={'home-guide-item arrow'} onClick={jumpPage}>
+    <div className={'home-guide-item step-bar arrow-2'} onClick={jumpPage}>
       <div className={'item-english'}>{props.english}</div>
       <div className={'item-title'}>{props.name}</div>
       <div className={`item-index`}>
@@ -52,9 +52,12 @@ const Guide = (props: GuideProps) => {
   return (
     <div className={'home-guide'}>
       <Title title={props.title} />
-      <div className={'home-guide-items'}>
+      <div
+        className={'home-guide-items'}
+        style={{ gridTemplateColumns: `repeat(${props.data ? props.data.length : 1}, 1fr)` }}
+      >
         {props.data.map((item, index) => (
-          <GuideItem {...item} index={index + 1} />
+          <GuideItem {...item} index={index + 1} key={item.key} />
         ))}
       </div>
     </div>

+ 23 - 31
src/pages/home/components/Pie.tsx

@@ -1,44 +1,36 @@
-import * as echarts from 'echarts';
-import { useEffect, useRef } from 'react';
+import Echarts from '@/components/DashBoard/echarts';
+import { useEffect, useState } from 'react';
+import type { EChartsOption } from 'echarts';
 
 interface Props {
   value: number;
 }
 
 const Pie = (props: Props) => {
-  const myChart: any = useRef(null);
+  const [options, setOptions] = useState<EChartsOption>({});
 
   useEffect(() => {
-    const dom = document.getElementById('charts');
-    if (dom) {
-      const option = {
-        series: [
-          {
-            type: 'pie',
-            radius: [20, 40],
-            top: 0,
-            height: 70,
-            left: 'center',
-            width: 70,
-            itemStyle: {
-              borderColor: '#fff',
-              borderWidth: 1,
-            },
-            label: {
-              show: false,
-            },
-            labelLine: {
-              show: false,
-            },
-            data: [props.value, 100 - props.value],
+    setOptions({
+      color: ['#2F54EB', '#979AFF'],
+      series: [
+        {
+          type: 'pie',
+          radius: ['100%', '50%'],
+          center: ['50%', '50%'],
+          label: {
+            show: false,
           },
-        ],
-      };
-      myChart.current = myChart.current || echarts.init(dom);
-      myChart.current.setOption(option);
-    }
+          data: [props.value, 100 - props.value],
+        },
+      ],
+    });
   }, [props.value]);
-  return <div id="charts" style={{ width: '100%', height: 80 }}></div>;
+
+  return (
+    <div style={{ width: '100%', height: 80 }}>
+      <Echarts options={options} />
+    </div>
+  );
 };
 
 export default Pie;

+ 119 - 0
src/pages/home/components/ProductChoose.tsx

@@ -0,0 +1,119 @@
+import { FormItem, FormLayout, Select } from '@formily/antd';
+import { createForm } from '@formily/core';
+import { createSchemaField, FormProvider } from '@formily/react';
+import { Button, message, Modal } from 'antd';
+import 'antd/lib/tree-select/style/index.less';
+import { useEffect, useState } from 'react';
+import { service } from '@/pages/device/Instance';
+import encodeQuery from '@/utils/encodeQuery';
+import { PermissionButton } from '@/components';
+import useHistory from '@/hooks/route/useHistory';
+import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+
+interface Props {
+  visible: boolean;
+  close: () => void;
+}
+
+const ProductChoose = (props: Props) => {
+  const productPermission = PermissionButton.usePermission('device/Product').permission;
+  const { visible, close } = props;
+  const [productList, setProductList] = useState<any[]>([]);
+
+  const SchemaField = createSchemaField({
+    components: {
+      Select,
+      FormItem,
+      FormLayout,
+    },
+  });
+
+  useEffect(() => {
+    service.getProductList(encodeQuery({ paging: false, terms: { state: 1 } })).then((resp) => {
+      if (resp.status === 200) {
+        const list = resp.result.map((item: { name: any; id: any }) => ({
+          label: item.name,
+          value: item.id,
+        }));
+        setProductList(list);
+      }
+    });
+  }, []);
+
+  const form = createForm({});
+
+  const schema = {
+    type: 'object',
+    properties: {
+      layout: {
+        type: 'void',
+        'x-component': 'FormLayout',
+        'x-component-props': {
+          layout: 'vertical',
+        },
+        properties: {
+          product: {
+            type: 'string',
+            title: '产品',
+            required: true,
+            'x-decorator': 'FormItem',
+            'x-component': 'Select',
+            enum: [...productList],
+            'x-component-props': {
+              showSearch: true,
+              showArrow: true,
+              filterOption: (input: string, option: any) =>
+                option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+            },
+          },
+        },
+      },
+    },
+  };
+
+  const history = useHistory();
+
+  return (
+    <Modal
+      maskClosable={false}
+      visible={visible}
+      onCancel={() => close()}
+      width="35vw"
+      title="选择产品"
+      onOk={() => close()}
+      footer={[
+        <Button key="cancel" onClick={() => close()}>
+          取消
+        </Button>,
+        <Button
+          key="ok"
+          type="primary"
+          onClick={async () => {
+            const data: any = await form.submit();
+            const path = getMenuPathByParams(`device/Product/Detail`);
+            if (path && !!productPermission.update) {
+              history.push(
+                `${getMenuPathByParams(MENUS_CODE['device/Product/Detail'], data.product)}`,
+                {
+                  tab: 'access',
+                },
+              );
+            } else {
+              message.warning('暂无权限,请联系管理员');
+              close();
+            }
+          }}
+        >
+          确认
+        </Button>,
+      ]}
+    >
+      <div style={{ marginTop: '20px' }}>
+        <FormProvider form={form}>
+          <SchemaField schema={schema} />
+        </FormProvider>
+      </div>
+    </Modal>
+  );
+};
+export default ProductChoose;

+ 6 - 6
src/pages/home/components/Statistics.tsx

@@ -25,13 +25,13 @@ const Statistics = (props: StatisticsProps) => {
           <div className={'home-guide-item'} key={item.name}>
             <div className={'item-english'}>{item.name}</div>
             <div className={'item-title'}>{item.value}</div>
-            <div className={`item-index`}>
-              {typeof item.children === 'string' ? (
+            {typeof item.children === 'string' ? (
+              <div className={`item-index`}>
                 <img src={item.children || defaultImage} />
-              ) : (
-                item.children
-              )}
-            </div>
+              </div>
+            ) : (
+              <div className={'item-index-echarts'}>{item.children}</div>
+            )}
           </div>
         ))}
       </div>

+ 40 - 62
src/pages/home/components/Steps.tsx

@@ -1,67 +1,45 @@
-import { RightOutlined } from '@ant-design/icons';
-import { Card, Col, Row } from 'antd';
+import './index.less';
+import Title from '@/pages/home/components/Title';
+import React from 'react';
 
-const Steps = () => {
+interface StepItemProps {
+  title: string | React.ReactNode;
+  content: string | React.ReactNode;
+  onClick: () => void;
+  url?: string;
+}
+
+interface StepsProps {
+  title: string | React.ReactNode;
+  data: StepItemProps[];
+}
+
+const ItemDefaultImg = require('/public/images/home/bottom-1.png');
+const StepsItem = (props: StepItemProps) => {
+  return (
+    <div className={'step-item step-bar arrow-1'} onClick={props.onClick}>
+      <div className={'step-item-title'}>
+        <div className={'step-item-img'}>
+          <img src={props.url || ItemDefaultImg} />
+        </div>
+        <span>{props.title}</span>
+      </div>
+      <div className={'step-item-content'}>{props.content}</div>
+    </div>
+  );
+};
+
+const Steps = (props: StepsProps) => {
   return (
-    <Card title={'设备接入推荐步骤'}>
-      <Row gutter={24}>
-        <Col span={4}>
-          <Card
-            bordered
-            title="创建产品"
-            onClick={() => {
-              // pageJump(!!devicePermission.add, 'device/Instance')
-            }}
-          >
-            产品是设备的集合,通常指一组具有相同功能的设备。物联设备必须通过产品进行接入方式配置。
-          </Card>
-        </Col>
-        <Col span={1}>
-          <RightOutlined />
-        </Col>
-        <Col span={4}>
-          <Card bordered title="配置产品接入方式" onClick={() => {}}>
-            通过产品对同一类型的所有设备进行统一的接入方式配置。请参照设备铭牌说明选择匹配的接入方式。
-          </Card>
-        </Col>
-        <Col span={1}>
-          <RightOutlined />
-        </Col>
-        <Col span={4}>
-          <Card
-            bordered
-            title="添加测试设备"
-            onClick={() => {
-              // pageJump(!!devicePermission.add, 'device/Instance')
-            }}
-          >
-            添加单个设备,用于验证产品模型是否配置正确。
-          </Card>
-        </Col>
-        <Col span={1}>
-          <RightOutlined />
-        </Col>
-        <Col span={4}>
-          <Card bordered title="功能调试" onClick={() => {}}>
-            对添加的测试设备进行功能调试,验证能否连接到平台,设备功能是否配置正确。
-          </Card>
-        </Col>
-        <Col span={1}>
-          <RightOutlined />
-        </Col>
-        <Col span={4}>
-          <Card
-            bordered
-            title="批量添加设备"
-            onClick={() => {
-              // pageJump(!!devicePermission.add, 'device/Instance')
-            }}
-          >
-            批量添加同一产品下的设备
-          </Card>
-        </Col>
-      </Row>
-    </Card>
+    <div className={'home-step'}>
+      <Title title={props.title} />
+      <div
+        className={'home-step-items'}
+        style={{ gridTemplateColumns: `repeat(${props.data ? props.data.length : 1}, 1fr)` }}
+      >
+        {props.data && props.data.map((item) => <StepsItem {...item} />)}
+      </div>
+    </div>
   );
 };
 

+ 1 - 1
src/pages/home/components/Title.tsx

@@ -2,7 +2,7 @@ import classNames from 'classnames';
 import React from 'react';
 
 interface TitleProps {
-  title: string;
+  title: string | React.ReactNode;
   english?: string;
   className?: string;
   extra?: React.ReactNode | string;

+ 106 - 26
src/pages/home/components/index.less

@@ -3,40 +3,30 @@
 @bodyPadding: 24px 16px;
 @margin: 24px;
 
+.home-base {
+  position: relative;
+  padding: @bodyPadding;
+  background-color: #fff;
+}
+
 .home-guide {
   margin-bottom: @margin;
   padding: @bodyPadding;
   background-color: #fff;
 
   .home-guide-items {
-    display: flex;
-    gap: 56px;
+    display: grid;
+    grid-column-gap: 56px;
   }
 }
 
 .home-guide-item {
   position: relative;
-  flex-grow: 1;
   padding: 16px;
   background: linear-gradient(135.62deg, #f6f7fd 22.27%, rgba(255, 255, 255, 0.86) 91.82%);
   border-radius: 2px;
   box-shadow: 0 4px 18px #efefef;
 
-  &.arrow {
-    &:not(:last-child) {
-      &::after {
-        position: absolute;
-        top: 50%;
-        right: -60px;
-        width: 60px;
-        height: 40px;
-        background: url('/images/home/arrow-2.png') no-repeat center;
-        transform: translateY(-50%);
-        content: ' ';
-      }
-    }
-  }
-
   .item-english {
     color: #4f4f4f;
   }
@@ -53,6 +43,14 @@
     right: 10%;
     bottom: 0;
   }
+
+  .item-index-echarts {
+    .item-index;
+
+    right: 12px;
+    bottom: 5%;
+    width: 50%;
+  }
 }
 
 .home-title {
@@ -86,29 +84,111 @@
 }
 
 .home-body {
-  position: relative;
-  height: 500px;
+  .home-base;
+
   margin-bottom: @margin;
-  padding: @bodyPadding;
+  padding-bottom: 26.5%;
   overflow: hidden;
-  background-color: #fff;
 
   .home-body-img {
     position: absolute;
     top: 0;
     left: 0;
     z-index: 1;
+    width: 100%;
     height: 100%;
+
+    > img {
+      width: 100%;
+      height: 100%;
+    }
   }
 }
 
 .home-statistics {
-  position: relative;
-  padding: @bodyPadding;
-  background-color: #fff;
+  .home-base;
 
   .home-statistics-body {
-    display: flex;
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
     gap: 24px;
   }
 }
+
+.step-item-after {
+  position: absolute;
+  top: 50%;
+  right: -60px;
+  width: 60px;
+  height: 40px;
+  transform: translateY(-50%);
+  content: ' ';
+}
+
+.home-step {
+  .home-base;
+
+  .home-step-items {
+    display: grid;
+    grid-column-gap: 66px;
+
+    .step-item {
+      display: flex;
+      flex-direction: column;
+
+      .step-item-title {
+        position: relative;
+        padding: 16px 24px;
+        color: #333;
+        font-weight: bold;
+        font-size: 20px;
+        background-color: #f8f9fd;
+
+        .step-item-img {
+          position: absolute;
+          top: 0;
+          right: 0;
+          z-index: 1;
+        }
+
+        > span {
+          position: relative;
+          z-index: 2;
+        }
+      }
+
+      .step-item-content {
+        flex-grow: 1;
+        height: auto;
+        padding: 24px;
+        border-right: 1px solid #e5edf4;
+        border-bottom: 1px solid #e5edf4;
+        border-left: 1px solid #e5edf4;
+      }
+    }
+  }
+}
+
+.step-bar {
+  position: relative;
+
+  &.arrow-1 {
+    &:not(:last-child) {
+      &::after {
+        .step-item-after;
+
+        background: url('/images/home/arrow-1.png') no-repeat center;
+      }
+    }
+  }
+
+  &.arrow-2 {
+    &:not(:last-child) {
+      &::after {
+        .step-item-after;
+
+        background: url('/images/home/arrow-2.png') no-repeat center;
+      }
+    }
+  }
+}

+ 197 - 25
src/pages/home/comprehensive/index.tsx

@@ -1,7 +1,7 @@
 import { PermissionButton } from '@/components';
 import useHistory from '@/hooks/route/useHistory';
 import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
-import { Col, message, Row } from 'antd';
+import { Col, message, Row, Tooltip } from 'antd';
 import Body from '../components/Body';
 import Guide from '../components/Guide';
 import Statistics from '../components/Statistics';
@@ -11,17 +11,25 @@ import { useEffect, useState } from 'react';
 import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
 import { map } from 'rxjs';
 import Pie from '../components/Pie';
+import { QuestionCircleOutlined } from '@ant-design/icons';
+import ProductChoose from '../components/ProductChoose';
+import DeviceChoose from '../components/DeviceChoose';
 
 const Comprehensive = () => {
   const [subscribeTopic] = useSendWebsocketMessage();
   const productPermission = PermissionButton.usePermission('device/Product').permission;
   const devicePermission = PermissionButton.usePermission('device/Instance').permission;
   const rulePermission = PermissionButton.usePermission('rule-engine/Instance').permission;
+  const accessPermission = getMenuPathByCode(MENUS_CODE['link/AccessConfig']);
+  const logPermission = getMenuPathByCode(MENUS_CODE['Log']);
+  const linkPermission = getMenuPathByCode(MENUS_CODE['link/DashBoard']);
 
   const [productCount, setProductCount] = useState<number>(0);
   const [deviceCount, setDeviceCount] = useState<number>(0);
   const [cpuValue, setCpuValue] = useState<number>(0);
   const [jvmValue, setJvmValue] = useState<number>(0);
+  const [productVisible, setProductVisible] = useState<boolean>(false);
+  const [deviceVisible, setDeviceVisible] = useState<boolean>(false);
 
   const getProductCount = async () => {
     const resp = await service.productCount({});
@@ -37,7 +45,6 @@ const Comprehensive = () => {
     }
   };
 
-  // websocket
   useEffect(() => {
     getProductCount();
     getDeviceCount();
@@ -80,7 +87,6 @@ const Comprehensive = () => {
 
   const history = useHistory();
   // // 跳转
-
   const guideList = [
     {
       key: 'product',
@@ -88,7 +94,9 @@ const Comprehensive = () => {
       english: 'CREATE PRODUCT',
       auth: !!productPermission.add,
       url: 'device/Product',
-      param: '?save=true',
+      param: {
+        save: true,
+      },
     },
     {
       key: 'device',
@@ -96,7 +104,9 @@ const Comprehensive = () => {
       english: 'CREATE DEVICE',
       auth: !!devicePermission.add,
       url: 'device/Instance',
-      param: '?save=true',
+      param: {
+        save: true,
+      },
     },
     {
       key: 'rule-engine',
@@ -104,34 +114,39 @@ const Comprehensive = () => {
       english: 'RULE ENGINE',
       auth: !!rulePermission.add,
       url: 'rule-engine/Instance',
-      param: '?save=true',
+      param: {
+        save: true,
+      },
     },
   ];
 
   const guideOpsList = [
     {
-      key: 'product',
+      key: 'access',
       name: '设备接入配置',
-      english: 'CREATE PRODUCT',
-      auth: !!productPermission.add,
-      url: 'device/Product',
-      param: '?save=true',
+      english: 'DEVICE ACCESS CONFIGURATION',
+      auth: !!accessPermission,
+      url: 'link/AccessConfig',
     },
     {
-      key: 'device',
+      key: 'logger',
       name: '日志排查',
-      english: 'CREATE DEVICE',
-      auth: !!devicePermission.add,
-      url: 'device/Instance',
-      param: '?save=true',
+      english: 'LOG SCREEN',
+      auth: !!logPermission,
+      url: 'Log',
+      param: {
+        key: 'system',
+      },
     },
     {
-      key: 'rule-engine',
+      key: 'realtime',
       name: '实时监控',
-      english: 'RULE ENGINE',
-      auth: !!rulePermission.add,
-      url: 'rule-engine/Instance',
-      param: '?save=true',
+      english: 'REAL-TIME MONITORING',
+      auth: !!linkPermission,
+      url: 'link/DashBoard',
+      param: {
+        save: true,
+      },
     },
   ];
 
@@ -195,7 +210,7 @@ const Comprehensive = () => {
             <div style={{ fontSize: 14, fontWeight: 400 }}>
               <a
                 onClick={() => {
-                  const url = getMenuPathByCode(MENUS_CODE['device/DashBoard']);
+                  const url = getMenuPathByCode(MENUS_CODE['link/DashBoard']);
                   if (!!url) {
                     history.push(`${url}`);
                   } else {
@@ -213,11 +228,168 @@ const Comprehensive = () => {
         <Body title={'平台架构图'} english={'PLATFORM ARCHITECTURE DIAGRAM'} />
       </Col>
       <Col span={24}>
-        <Steps />
+        <Steps
+          title={
+            <span>
+              设备接入推荐步骤
+              <Tooltip title={'不同的设备因为通信协议的不用,存在接入步骤的差异'}>
+                <QuestionCircleOutlined style={{ paddingLeft: 12 }} />
+              </Tooltip>
+            </span>
+          }
+          data={[
+            {
+              title: '创建产品',
+              content:
+                '产品是设备的集合,通常指一组具有相同功能的设备。物联设备必须通过产品进行接入方式配置。',
+              onClick: () => {
+                const path = getMenuPathByCode('device/Product');
+                if (path && !!productPermission.add) {
+                  history.push(`${path}`, {
+                    save: true,
+                  });
+                } else {
+                  message.warning('暂无权限,请联系管理员');
+                }
+              },
+            },
+            {
+              title: '配置产品接入方式',
+              content:
+                '通过产品对同一类型的所有设备进行统一的接入方式配置。请参照设备铭牌说明选择匹配的接入方式。',
+              onClick: () => {
+                setProductVisible(true);
+              },
+            },
+            {
+              title: '添加测试设备',
+              content: '添加单个设备,用于验证产品模型是否配置正确。',
+              onClick: () => {
+                const path = getMenuPathByCode('device/Instance');
+                if (path && !!devicePermission.add) {
+                  history.push(`${path}`, {
+                    save: true,
+                  });
+                } else {
+                  message.warning('暂无权限,请联系管理员');
+                }
+              },
+            },
+            {
+              title: '功能调试',
+              content: '对添加的测试设备进行功能调试,验证能否连接到平台,设备功能是否配置正确。',
+              onClick: () => {
+                setDeviceVisible(true);
+              },
+            },
+            {
+              title: '批量添加设备',
+              content: '批量添加同一产品下的设备',
+              onClick: () => {
+                const path = getMenuPathByCode('device/Instance');
+                if (path && !!devicePermission.import) {
+                  history.push(`${path}`, {
+                    import: true,
+                  });
+                } else {
+                  message.warning('暂无权限,请联系管理员');
+                }
+              },
+            },
+          ]}
+        />
       </Col>
-      <Col span={24}>
-        <Steps />
+      <Col span={24} style={{ marginTop: 24 }}>
+        <Steps
+          title={
+            <span>
+              运维管理推荐步骤
+              <Tooltip title="请根据业务需要对下述步骤进行选择性操作。">
+                <QuestionCircleOutlined style={{ paddingLeft: 12 }} />
+              </Tooltip>
+            </span>
+          }
+          data={[
+            {
+              title: '协议管理',
+              content: '根据业务需求自定义开发对应的产品(设备模型)接入协议,并上传到平台。',
+              url: require('/public/images/home/bottom-1.png'),
+              onClick: () => {
+                const url = getMenuPathByCode(MENUS_CODE['link/Protocol']);
+                if (!!url) {
+                  history.push(url);
+                } else {
+                  message.warning('暂无权限,请联系管理员');
+                }
+              },
+            },
+            {
+              title: '证书管理',
+              content: '统一维护平台内的证书,用于数据通信加密。',
+              url: require('/public/images/home/bottom-6.png'),
+              onClick: () => {
+                const url = getMenuPathByCode(MENUS_CODE['link/Certificate']);
+                if (!!url) {
+                  history.push(url);
+                } else {
+                  message.warning('暂无权限,请联系管理员');
+                }
+              },
+            },
+            {
+              title: '网络组件',
+              content: '根据不同的传输类型配置平台底层网络组件相关参数。',
+              url: require('/public/images/home/bottom-3.png'),
+              onClick: () => {
+                const url = getMenuPathByCode(MENUS_CODE['link/Type']);
+                if (!!url) {
+                  history.push(url);
+                } else {
+                  message.warning('暂无权限,请联系管理员');
+                }
+              },
+            },
+            {
+              title: '设备接入网关',
+              content: '根据不同的传输类型,关联消息协议,配置设备接入网关相关参数。',
+              url: require('/public/images/home/bottom-4.png'),
+              onClick: () => {
+                const url = getMenuPathByCode(MENUS_CODE['link/Gateway']);
+                if (!!url) {
+                  history.push(url);
+                } else {
+                  message.warning('暂无权限,请联系管理员');
+                }
+              },
+            },
+            {
+              title: '日志管理',
+              content: '监控系统日志,及时处理系统异常。',
+              url: require('/public/images/home/bottom-5.png'),
+              onClick: () => {
+                const url = getMenuPathByCode(MENUS_CODE['Log']);
+                if (!!url) {
+                  history.push(url);
+                } else {
+                  message.warning('暂无权限,请联系管理员');
+                }
+              },
+            },
+          ]}
+        />
       </Col>
+      <ProductChoose
+        visible={productVisible}
+        close={() => {
+          setProductVisible(false);
+        }}
+      />
+      <DeviceChoose
+        visible={deviceVisible}
+        onCancel={() => {
+          setDeviceVisible(false);
+        }}
+      />
     </Row>
   );
 };

+ 98 - 27
src/pages/home/device/index.tsx

@@ -1,4 +1,4 @@
-import { Col, message, Row } from 'antd';
+import { Col, message, Row, Tooltip } from 'antd';
 import { PermissionButton } from '@/components';
 import { Body, Guide } from '../components';
 import Statistics from '../components/Statistics';
@@ -7,6 +7,9 @@ import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
 import { useHistory } from '@/hooks';
 import { service } from '..';
 import { useEffect, useState } from 'react';
+import { QuestionCircleOutlined } from '@ant-design/icons';
+import ProductChoose from '../components/ProductChoose';
+import DeviceChoose from '../components/DeviceChoose';
 
 const Device = () => {
   const productPermission = PermissionButton.usePermission('device/Product').permission;
@@ -16,6 +19,9 @@ const Device = () => {
   const [productCount, setProductCount] = useState<number>(0);
   const [deviceCount, setDeviceCount] = useState<number>(0);
 
+  const [productVisible, setProductVisible] = useState<boolean>(false);
+  const [deviceVisible, setDeviceVisible] = useState<boolean>(false);
+
   const getProductCount = async () => {
     const resp = await service.productCount({});
     if (resp.status === 200) {
@@ -45,7 +51,9 @@ const Device = () => {
       english: 'CREATE PRODUCT',
       auth: !!productPermission.add,
       url: 'device/Product',
-      param: '?save=true',
+      param: {
+        save: true,
+      },
     },
     {
       key: 'device',
@@ -53,7 +61,9 @@ const Device = () => {
       english: 'CREATE DEVICE',
       auth: !!devicePermission.add,
       url: 'device/Instance',
-      param: '?save=true',
+      param: {
+        save: true,
+      },
     },
     {
       key: 'rule-engine',
@@ -61,32 +71,12 @@ const Device = () => {
       english: 'RULE ENGINE',
       auth: !!rulePermission.add,
       url: 'rule-engine/Instance',
-      param: '?save=true',
+      param: {
+        save: true,
+      },
     },
   ];
 
-  // const statisticsList = [{
-  //   key: 'product',
-  //   name: '1、创建产品',
-  //   auth: !!productPermission.add,
-  //   url: 'device/Product',
-  //   param: "?save=true"
-  // }, {
-  //   key: 'device',
-  //   name: '2、创建设备',
-  //   auth: !!devicePermission.add,
-  //   url: 'device/Instance',
-  //   param: "?save=true"
-  // },
-  // {
-  //   key: 'rule-engine',
-  //   name: '3、规则引擎',
-  //   auth: !!rulePermission.add,
-  //   url: 'rule-engine/Instance',
-  //   param: "?save=true"
-  // }
-  // ];
-
   return (
     <Row gutter={24}>
       <Col span={14}>
@@ -135,8 +125,89 @@ const Device = () => {
         <Body title={'平台架构图'} english={'PLATFORM ARCHITECTURE DIAGRAM'} />
       </Col>
       <Col span={24}>
-        <Steps />
+        <Steps
+          title={
+            <span>
+              设备接入推荐步骤
+              <Tooltip title={'不同的设备因为通信协议的不用,存在接入步骤的差异'}>
+                <QuestionCircleOutlined style={{ paddingLeft: 12 }} />
+              </Tooltip>
+            </span>
+          }
+          data={[
+            {
+              title: '创建产品',
+              content:
+                '产品是设备的集合,通常指一组具有相同功能的设备。物联设备必须通过产品进行接入方式配置。',
+              onClick: () => {
+                const path = getMenuPathByCode('device/Product');
+                if (path && !!productPermission.add) {
+                  history.push(`${path}`, {
+                    save: true,
+                  });
+                } else {
+                  message.warning('暂无权限,请联系管理员');
+                }
+              },
+            },
+            {
+              title: '配置产品接入方式',
+              content:
+                '通过产品对同一类型的所有设备进行统一的接入方式配置。请参照设备铭牌说明选择匹配的接入方式。',
+              onClick: () => {
+                setProductVisible(true);
+              },
+            },
+            {
+              title: '添加测试设备',
+              content: '添加单个设备,用于验证产品模型是否配置正确。',
+              onClick: () => {
+                const path = getMenuPathByCode('device/Instance');
+                if (path && !!devicePermission.add) {
+                  history.push(`${path}`, {
+                    save: true,
+                  });
+                } else {
+                  message.warning('暂无权限,请联系管理员');
+                }
+              },
+            },
+            {
+              title: '功能调试',
+              content: '对添加的测试设备进行功能调试,验证能否连接到平台,设备功能是否配置正确。',
+              onClick: () => {
+                setDeviceVisible(true);
+              },
+            },
+            {
+              title: '批量添加设备',
+              content: '批量添加同一产品下的设备',
+              onClick: () => {
+                const path = getMenuPathByCode('device/Instance');
+                if (path && !!devicePermission.import) {
+                  history.push(`${path}`, {
+                    import: true,
+                  });
+                } else {
+                  message.warning('暂无权限,请联系管理员');
+                }
+              },
+            },
+          ]}
+        />
       </Col>
+      <ProductChoose
+        visible={productVisible}
+        close={() => {
+          setProductVisible(false);
+        }}
+      />
+      <DeviceChoose
+        visible={deviceVisible}
+        onCancel={() => {
+          setDeviceVisible(false);
+        }}
+      />
     </Row>
   );
 };

+ 3 - 2
src/pages/home/index.tsx

@@ -22,10 +22,11 @@ const Home = () => {
     service.queryView().then((resp) => {
       if (resp.status === 200) {
         if (resp.result.length == 0) {
-          setCurrent('init');
+          setCurrent('ops');
         } else {
-          setCurrent(resp.result[0]?.content);
+          // setCurrent(resp.result[0]?.content);
         }
+        setCurrent('ops');
       }
     });
   }, []);

+ 206 - 1
src/pages/home/ops/index.tsx

@@ -1,4 +1,209 @@
+import { Col, message, Row, Tooltip } from 'antd';
+import Guide from '../components/Guide';
+import { PermissionButton } from '@/components';
+import Statistics from '../components/Statistics';
+import Pie from '@/pages/home/components/Pie';
+import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
+import { useEffect, useState } from 'react';
+import { map } from 'rxjs';
+import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
+import useHistory from '@/hooks/route/useHistory';
+import Body from '@/pages/home/components/Body';
+import Steps from '@/pages/home/components/Steps';
+import { QuestionCircleOutlined } from '@ant-design/icons';
+
 const Ops = () => {
-  return <div>运维管理视图</div>;
+  const [subscribeTopic] = useSendWebsocketMessage();
+  const history = useHistory();
+
+  const productPermission = PermissionButton.usePermission('device/Product').permission;
+  const devicePermission = PermissionButton.usePermission('device/Instance').permission;
+  const rulePermission = PermissionButton.usePermission('rule-engine/Instance').permission;
+
+  const [cpuValue, setCpuValue] = useState<number>(0);
+  const [jvmValue, setJvmValue] = useState<number>(0);
+
+  useEffect(() => {
+    const cpuRealTime = subscribeTopic!(
+      `operations-statistics-system-info-cpu-realTime`,
+      `/dashboard/systemMonitor/stats/info/realTime`,
+      {
+        type: 'cpu',
+        interval: '2s',
+        agg: 'avg',
+      },
+    )
+      ?.pipe(map((res) => res.payload))
+      .subscribe((payload: any) => {
+        setCpuValue(payload.value?.systemUsage || 0);
+      });
+
+    const jvmRealTime = subscribeTopic!(
+      `operations-statistics-system-info-memory-realTime`,
+      `/dashboard/systemMonitor/stats/info/realTime`,
+      {
+        type: 'memory',
+        interval: '2s',
+        agg: 'avg',
+      },
+    )
+      ?.pipe(map((res) => res.payload))
+      .subscribe((payload: any) => {
+        setJvmValue(payload.value?.jvmHeapUsage || 0);
+      });
+
+    return () => {
+      cpuRealTime?.unsubscribe();
+      jvmRealTime?.unsubscribe();
+    };
+  }, []);
+  const guideOpsList: any[] = [
+    {
+      key: 'product',
+      name: '设备接入配置',
+      english: 'CREATE PRODUCT',
+      auth: !!productPermission.add,
+      url: 'device/Product',
+      param: '?save=true',
+    },
+    {
+      key: 'device',
+      name: '日志排查',
+      english: 'CREATE DEVICE',
+      auth: !!devicePermission.add,
+      url: 'device/Instance',
+      param: '?save=true',
+    },
+    {
+      key: 'rule-engine',
+      name: '实时监控',
+      english: 'RULE ENGINE',
+      auth: !!rulePermission.add,
+      url: 'rule-engine/Instance',
+      param: '?save=true',
+    },
+  ];
+  return (
+    <Row gutter={24}>
+      <Col span={14}>
+        <Guide title="运维引导" data={guideOpsList} />
+      </Col>
+      <Col span={10}>
+        <Statistics
+          data={[
+            {
+              name: 'CPU使用率',
+              value: String(cpuValue) + '%',
+              children: <Pie value={cpuValue} />,
+            },
+            {
+              name: 'JVM内存',
+              value: String(jvmValue) + '%',
+              children: <Pie value={jvmValue} />,
+            },
+          ]}
+          title="基础统计"
+          extra={
+            <div style={{ fontSize: 14, fontWeight: 400 }}>
+              <a
+                onClick={() => {
+                  const url = getMenuPathByCode(MENUS_CODE['device/DashBoard']);
+                  if (!!url) {
+                    history.push(`${url}`);
+                  } else {
+                    message.warning('暂无权限,请联系管理员');
+                  }
+                }}
+              >
+                详情
+              </a>
+            </div>
+          }
+        />
+      </Col>
+      <Col span={24}>
+        <Body title={'平台架构图'} english={'PLATFORM ARCHITECTURE DIAGRAM'} />
+      </Col>
+      <Col span={24}>
+        <Steps
+          title={
+            <span>
+              运维管理推荐步骤
+              <Tooltip title="请根据业务需要对下述步骤进行选择性操作。">
+                <QuestionCircleOutlined style={{ paddingLeft: 12 }} />
+              </Tooltip>
+            </span>
+          }
+          data={[
+            {
+              title: '协议管理',
+              content: '根据业务需求自定义开发对应的产品(设备模型)接入协议,并上传到平台。',
+              url: require('/public/images/home/bottom-1.png'),
+              onClick: () => {
+                const url = getMenuPathByCode(MENUS_CODE['link/Protocol']);
+                if (!!url) {
+                  history.push(url);
+                } else {
+                  message.warning('暂无权限,请联系管理员');
+                }
+              },
+            },
+            {
+              title: '证书管理',
+              content: '统一维护平台内的证书,用于数据通信加密。',
+              url: require('/public/images/home/bottom-6.png'),
+              onClick: () => {
+                const url = getMenuPathByCode(MENUS_CODE['link/Certificate']);
+                if (!!url) {
+                  history.push(url);
+                } else {
+                  message.warning('暂无权限,请联系管理员');
+                }
+              },
+            },
+            {
+              title: '网络组件',
+              content: '根据不同的传输类型配置平台底层网络组件相关参数。',
+              url: require('/public/images/home/bottom-3.png'),
+              onClick: () => {
+                const url = getMenuPathByCode(MENUS_CODE['link/Type']);
+                if (!!url) {
+                  history.push(url);
+                } else {
+                  message.warning('暂无权限,请联系管理员');
+                }
+              },
+            },
+            {
+              title: '设备接入网关',
+              content: '根据不同的传输类型,关联消息协议,配置设备接入网关相关参数。',
+              url: require('/public/images/home/bottom-4.png'),
+              onClick: () => {
+                const url = getMenuPathByCode(MENUS_CODE['link/Gateway']);
+                if (!!url) {
+                  history.push(url);
+                } else {
+                  message.warning('暂无权限,请联系管理员');
+                }
+              },
+            },
+            {
+              title: '日志管理',
+              content: '监控系统日志,及时处理系统异常。',
+              url: require('/public/images/home/bottom-5.png'),
+              onClick: () => {
+                const url = getMenuPathByCode(MENUS_CODE['Log']);
+                if (!!url) {
+                  history.push(url);
+                } else {
+                  message.warning('暂无权限,请联系管理员');
+                }
+              },
+            },
+          ]}
+        />
+      </Col>
+    </Row>
+  );
 };
 export default Ops;

+ 11 - 1
src/pages/media/DashBoard/index.tsx

@@ -7,6 +7,8 @@ import './index.less';
 import encodeQuery from '@/utils/encodeQuery';
 import type { EChartsOption } from 'echarts';
 import moment from 'moment';
+import { Tooltip } from 'antd';
+import { QuestionCircleOutlined } from '@ant-design/icons';
 
 const service = new Service('media');
 
@@ -200,7 +202,14 @@ export default () => {
             <img src={require('/public/images/media/dashboard-3.png')} />
           </DashBoardTopCard.Item>
           <DashBoardTopCard.Item
-            title={'播放中数量'}
+            title={
+              <span>
+                播放中数量
+                <Tooltip title={'当前正在播放的通道数量之和'}>
+                  <QuestionCircleOutlined style={{ marginLeft: 12 }} />
+                </Tooltip>
+              </span>
+            }
             value={playObject ? playObject.playerTotal : 0}
             footer={[
               {
@@ -218,6 +227,7 @@ export default () => {
           title={'播放数量(人次)'}
           options={options}
           height={500}
+          defaultTime={'week'}
           onParamsChange={getEcharts}
         />
       </div>

+ 95 - 71
src/pages/media/Home/index.tsx

@@ -1,5 +1,5 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import { Card, Col, message, Row, Tooltip, Typography } from 'antd';
+import { Col, message, Row, Tooltip } from 'antd';
 import { PermissionButton } from '@/components';
 import { getMenuPathByCode } from '@/utils/menu';
 import useHistory from '@/hooks/route/useHistory';
@@ -9,6 +9,8 @@ import Service from './service';
 import { useState } from 'react';
 import DeviceModal from './deviceModal';
 import './index.less';
+import { Body, Guide, Statistics } from '@/pages/home/components';
+import Steps from '@/pages/home/components/Steps';
 
 const permissionTip = '暂无权限,请联系管理员';
 
@@ -19,7 +21,6 @@ export default () => {
   const deviceUrl = getMenuPathByCode('media/Device');
   const channelUrl = getMenuPathByCode('media/Device/Channel');
   const splitScreenUrl = getMenuPathByCode('media/SplitScreen');
-  const cascadeUrl = getMenuPathByCode('media/Cascade');
 
   const [visible, setVisible] = useState(false);
 
@@ -54,14 +55,6 @@ export default () => {
     }
   };
 
-  const jumpCascade = () => {
-    if (cascadeUrl) {
-      history.push(cascadeUrl);
-    } else {
-      message.warning(permissionTip);
-    }
-  };
-
   const jumpChannel = () => {
     if (channelUrl) {
       setVisible(true);
@@ -70,6 +63,31 @@ export default () => {
     }
   };
 
+  const guideList = [
+    {
+      key: 'EQUIPMENT',
+      name: '添加视频设备',
+      english: 'ADD VIDEO EQUIPMENT',
+      auth: !!devicePermission.add,
+      url: deviceUrl,
+      param: { save: true },
+    },
+    {
+      key: 'SCREEN',
+      name: '分屏展示',
+      english: 'SPLIT SCREEN DISPLAY',
+      auth: !!splitScreenUrl,
+      url: splitScreenUrl,
+    },
+    {
+      key: 'CASCADE',
+      name: '国标级联',
+      english: 'GB CASCADE',
+      auth: !!channelUrl,
+      url: channelUrl,
+    },
+  ];
+
   return (
     <PageContainer>
       <DeviceModal
@@ -79,72 +97,78 @@ export default () => {
           setVisible(false);
         }}
       />
-      <Card className={'media-home'}>
-        <Row gutter={[12, 12]}>
-          <Col span={14}>
-            <div className={'media-home-top'}>
-              <Typography.Title level={5}>视频中心引导</Typography.Title>
-              <div className={'media-guide'}>
-                <div onClick={addDevice}>添加视频设备</div>
-                <div onClick={jumpSplitScreen}>分屏展示</div>
-                <div onClick={jumpCascade}>国标级联</div>
-              </div>
-            </div>
-          </Col>
-          <Col span={10}>
-            <div className={'media-home-top'}>
-              <Typography.Title level={5}>
-                基础统计
-                <PermissionButton
-                  isPermission={!!dashBoardUrl}
+      <Row gutter={24}>
+        <Col span={14}>
+          <Guide title={'视频中心引导'} data={guideList} />
+        </Col>
+        <Col span={10}>
+          <Statistics
+            title={'基础统计'}
+            data={[
+              {
+                name: '设备数量',
+                value: deviceTotal,
+                children: require('/public/images/home/top-1.png'),
+              },
+              {
+                name: '通道数量',
+                value: channelTotal || 0,
+                children: require('/public/images/home/top-2.png'),
+              },
+            ]}
+            extra={
+              <div style={{ fontSize: 14, fontWeight: 400 }}>
+                <a
                   onClick={() => {
-                    history.push(dashBoardUrl);
+                    if (!!dashBoardUrl) {
+                      history.push(`${dashBoardUrl}`);
+                    } else {
+                      message.warning('暂无权限,请联系管理员');
+                    }
                   }}
-                  type={'link'}
                 >
                   详情
-                </PermissionButton>
-              </Typography.Title>
-              <div className={'media-statistics'}>
-                <div>
-                  设备数量
-                  {deviceTotal}
-                </div>
-                <div>
-                  通道数量
-                  {channelTotal}
-                </div>
-              </div>
-            </div>
-          </Col>
-          <Col span={24}>
-            <Typography.Title level={5}>平台架构图</Typography.Title>
-            <div className={'media-home-content'}></div>
-          </Col>
-          <Col span={24}>
-            <Typography.Title level={5}>
-              <span style={{ paddingRight: 12 }}>视频设备管理推荐步骤</span>
-              <Tooltip title={'请根据业务需要对下述步骤进行选择性操作'}>
-                <QuestionCircleOutlined />
-              </Tooltip>
-            </Typography.Title>
-            <div className={'media-home-steps'}>
-              <div onClick={addDevice}>
-                添加视频设备
-                <div>根据视频设备的传输协议,在已创建的产品下添加对应的设备</div>
-              </div>
-              <div onClick={jumpChannel}>
-                查看通道
-                <div>查看设备下的通道数据,可以进行直播、录制等操作</div>
-              </div>
-              <div onClick={jumpSplitScreen}>
-                分屏展示
-                <div>对多个通道的视频流数据进行分屏展示</div>
+                </a>
               </div>
-            </div>
-          </Col>
-        </Row>
-      </Card>
+            }
+          />
+        </Col>
+        <Col span={24}>
+          <Body title={'平台架构图'} english={'PLATFORM ARCHITECTURE DIAGRAM'} />
+        </Col>
+        <Col span={24}>
+          <Steps
+            title={
+              <span>
+                设备接入推荐步骤
+                <Tooltip title={'不同的设备因为通信协议的不用,存在接入步骤的差异'}>
+                  <QuestionCircleOutlined style={{ paddingLeft: 12 }} />
+                </Tooltip>
+              </span>
+            }
+            data={[
+              {
+                title: '添加视频设备',
+                content: '根据视频设备的传输协议,在已创建的产品下添加对应的设备。',
+                onClick: addDevice,
+                url: require('/public/images/home/bottom-6.png'),
+              },
+              {
+                title: '查看通道',
+                content: '查看设备下的通道数据,可以进行直播、录制等操作。',
+                onClick: jumpChannel,
+                url: require('/public/images/home/bottom-7.png'),
+              },
+              {
+                title: '分屏展示',
+                content: '对多个通道的视频流数据进行分屏展示。',
+                onClick: jumpSplitScreen,
+                url: require('/public/images/home/bottom-8.png'),
+              },
+            ]}
+          />
+        </Col>
+      </Row>
     </PageContainer>
   );
 };

+ 12 - 1
src/pages/rule-engine/Instance/index.tsx

@@ -1,7 +1,7 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import Service from '@/pages/rule-engine/Instance/serivce';
 import type { InstanceItem } from '@/pages/rule-engine/Instance/typings';
-import { useRef, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import {
   DeleteOutlined,
@@ -19,6 +19,7 @@ import RuleInstanceCard from '@/components/ProTableCard/CardItems/ruleInstance';
 import Save from '@/pages/rule-engine/Instance/Save';
 import SystemConst from '@/utils/const';
 import { StatusColorEnum } from '@/components/BadgeStatus';
+import useLocation from '@/hooks/route/useLocation';
 
 export const service = new Service('rule-engine/instance');
 
@@ -30,6 +31,16 @@ const Instance = () => {
   const [searchParams, setSearchParams] = useState<any>({});
   const { permission } = PermissionButton.usePermission('rule-engine/Instance');
 
+  const location = useLocation();
+
+  useEffect(() => {
+    const { state } = location;
+    if (state && state.save) {
+      setCurrent({});
+      setVisible(true);
+    }
+  }, [location]);
+
   const tools = (record: InstanceItem) => [
     <PermissionButton
       isPermission={permission.update}

+ 5 - 0
src/pages/system/Platforms/Api/base.tsx

@@ -68,6 +68,11 @@ export default observer((props: ApiPageProps) => {
     const code = param.get('code');
 
     if (props.isOpenGranted === false) {
+      service.apiOperations().then((resp: any) => {
+        if (resp.status === 200) {
+          setGrantKeys(resp.result);
+        }
+      });
     } else {
       service.getApiGranted(code!).then((resp: any) => {
         if (resp.status === 200) {

+ 15 - 7
src/pages/system/Platforms/Api/basePage.tsx

@@ -7,7 +7,7 @@ import { ApiModel } from '@/pages/system/Platforms/Api/base';
 interface TableProps {
   data: any;
   operations: string[];
-  // 是否只暂时已授权的接口
+  // 是否只展示已授权的接口
   isShowGranted?: boolean;
   //
   isOpenGranted?: boolean;
@@ -99,13 +99,21 @@ export default (props: TableProps) => {
     grantCache.current = addGrant;
 
     setLoading(true);
-    const resp = await service.addApiGrant(code!, { operations: addOperations });
-    const resp2 = await service.removeApiGrant(code!, { operations: removeOperations });
-    setLoading(false);
-    if (resp.status === 200 || resp2.status === 200) {
-      message.success('操作成功');
+    if (props.isOpenGranted === false) {
+      const resp = await service.apiOperationsAdd(addGrant);
+      const resp2 = removeGrant.length ? await service.apiOperationsRemove(removeGrant) : {};
+      if (resp.status === 200 || resp2.status === 200) {
+        message.success('操作成功');
+      }
+    } else {
+      const resp = await service.addApiGrant(code!, { operations: addOperations });
+      const resp2 = await service.removeApiGrant(code!, { operations: removeOperations });
+      if (resp.status === 200 || resp2.status === 200) {
+        message.success('操作成功');
+      }
     }
-  }, [selectKeys, location, dataSource]);
+    setLoading(false);
+  }, [selectKeys, location, dataSource, props.isOpenGranted]);
 
   return (
     <div className={'platforms-api-table'}>

+ 5 - 1
src/pages/system/Platforms/Setting/index.tsx

@@ -1,10 +1,14 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import ApiPage from '../Api/base';
+import { ExclamationCircleOutlined } from '@ant-design/icons';
 
 export default () => {
   return (
     <PageContainer>
-      <div>配置系统支持API赋权的范围</div>
+      <div style={{ padding: '24px 24px 0 24px', background: '#fff' }}>
+        <ExclamationCircleOutlined style={{ marginRight: 12, fontSize: 16 }} />
+        配置系统支持API赋权的范围
+      </div>
       <ApiPage showDebugger={true} isOpenGranted={false} />
     </PageContainer>
   );

+ 14 - 12
src/pages/system/Platforms/index.tsx

@@ -306,18 +306,20 @@ export default () => {
           </div>,
         ]}
       />
-      <SaveModal
-        visible={saveVisible}
-        data={editData}
-        type={saveType}
-        onCancel={() => {
-          setSaveVisible(false);
-          setEditData(undefined);
-        }}
-        onReload={() => {
-          actionRef.current?.reload();
-        }}
-      />
+      {saveVisible && (
+        <SaveModal
+          visible={saveVisible}
+          data={editData}
+          type={saveType}
+          onCancel={() => {
+            setSaveVisible(false);
+            setEditData(undefined);
+          }}
+          onReload={() => {
+            actionRef.current?.reload();
+          }}
+        />
+      )}
       <PasswordModal
         visible={passwordVisible}
         onCancel={() => {

+ 18 - 0
src/pages/system/Platforms/service.ts

@@ -68,6 +68,24 @@ class Service extends BaseService<platformsType> {
     request(`/${SystemConst.API_BASE}/api-client/operations`, { method: 'GET' });
 
   /**
+   * 新增可授权的接口ID
+   */
+  apiOperationsAdd = (data?: any) =>
+    request(`/${SystemConst.API_BASE}/api-client/operations/_batch`, {
+      method: 'PATCH',
+      data: data || [],
+    });
+
+  /**
+   * 删除可授权的接口ID
+   */
+  apiOperationsRemove = (data?: any) =>
+    request(`/${SystemConst.API_BASE}/api-client/operations/_batch`, {
+      method: 'DELETE',
+      data: data || [],
+    });
+
+  /**
    * 获取可授权的接口ID
    */
   getSdk = () => request(`${this.uri}/sdk`, { method: 'GET' });

+ 0 - 1
src/pages/system/Role/index.tsx

@@ -197,7 +197,6 @@ const Role: React.FC = observer(() => {
       CurdModel.add();
     }
     const subscription = Store.subscribe(SystemConst.BASE_UPDATE_DATA, (data) => {
-      debugger;
       console.log('订阅数据');
       if ((window as any).onTabSaveSuccess) {
         (window as any).onTabSaveSuccess(data);