wzyyy 3 lat temu
rodzic
commit
b0cf6cd3b4
67 zmienionych plików z 2789 dodań i 1398 usunięć
  1. 2 2
      config/proxy.ts
  2. 11 0
      config/routes.ts
  3. 2 2
      docker/nginx.conf
  4. 1 0
      package.json
  5. BIN
      public/images/firmware/error.png
  6. BIN
      public/images/firmware/finish.png
  7. BIN
      public/images/firmware/loading.png
  8. BIN
      public/images/firmware/waiting.png
  9. BIN
      public/images/init-home/background.png
  10. 6 6
      src/app.tsx
  11. 3 1
      src/components/BindParentDevice/index.tsx
  12. 129 0
      src/components/FIndicators/index.tsx
  13. 3 0
      src/components/FUpload/index.less
  14. 52 0
      src/components/FUpload/index.tsx
  15. 8 2
      src/components/Upload/Image/index.tsx
  16. 4 2
      src/hooks/websocket/useWebSocket.ts
  17. 1 0
      src/pages/Northbound/AliCloud/index.tsx
  18. 1 0
      src/pages/Northbound/DuerOS/index.tsx
  19. 1 0
      src/pages/cloud/DuerOS/index.tsx
  20. 0 66
      src/pages/device/Firmware/Detail/History/index.tsx
  21. 0 114
      src/pages/device/Firmware/Detail/Task/Detail/index.tsx
  22. 0 92
      src/pages/device/Firmware/Detail/Task/Release/index.tsx
  23. 0 86
      src/pages/device/Firmware/Detail/Task/Save/index.tsx
  24. 0 136
      src/pages/device/Firmware/Detail/Task/index.tsx
  25. 0 68
      src/pages/device/Firmware/Detail/index.tsx
  26. 249 18
      src/pages/device/Firmware/Save/index.tsx
  27. 25 0
      src/pages/device/Firmware/Task/Detail/index.less
  28. 170 0
      src/pages/device/Firmware/Task/Detail/index.tsx
  29. 279 0
      src/pages/device/Firmware/Task/Save/index.tsx
  30. 185 0
      src/pages/device/Firmware/Task/index.tsx
  31. 102 80
      src/pages/device/Firmware/index.tsx
  32. 92 35
      src/pages/device/Instance/Detail/Diagnose/Status/index.tsx
  33. 23 9
      src/pages/device/Instance/index.tsx
  34. 1 0
      src/pages/device/Product/index.tsx
  35. 143 102
      src/pages/device/components/Metadata/Base/Edit/index.tsx
  36. 42 0
      src/pages/init-home/index.less
  37. 46 0
      src/pages/init-home/index.tsx
  38. 49 46
      src/pages/link/Channel/channelCard.tsx
  39. 65 65
      src/pages/link/Channel/index.less
  40. 1 0
      src/pages/link/Protocol/index.tsx
  41. 1 0
      src/pages/link/Type/index.tsx
  42. 1 0
      src/pages/media/Cascade/index.tsx
  43. 1 0
      src/pages/media/Device/index.tsx
  44. 1 0
      src/pages/notice/Config/index.tsx
  45. 1 0
      src/pages/notice/Template/index.tsx
  46. 118 0
      src/pages/rule-engine/Alarm/Config/Save/input.tsx
  47. 155 0
      src/pages/rule-engine/Alarm/Config/Save/output.tsx
  48. 77 175
      src/pages/rule-engine/Alarm/Config/index.tsx
  49. 1 0
      src/pages/rule-engine/Alarm/Configuration/index.tsx
  50. 1 0
      src/pages/rule-engine/Instance/index.tsx
  51. 1 0
      src/pages/rule-engine/Scene/index.tsx
  52. 141 141
      src/pages/system/Basis/index.tsx
  53. 84 86
      src/pages/system/Basis/init.tsx
  54. 1 0
      src/pages/system/Department/Assets/deivce/bind.tsx
  55. 1 0
      src/pages/system/Department/Assets/deivce/index.tsx
  56. 1 0
      src/pages/system/Department/Assets/product/bind.tsx
  57. 1 0
      src/pages/system/Department/Assets/product/index.tsx
  58. 125 0
      src/pages/system/Menu/Setting/baseMenu.ts
  59. 24 0
      src/pages/system/Menu/Setting/dragItem.tsx
  60. 67 0
      src/pages/system/Menu/Setting/index.less
  61. 168 0
      src/pages/system/Menu/Setting/index.tsx
  62. 58 0
      src/pages/system/Menu/Setting/tree.tsx
  63. 13 51
      src/pages/system/Menu/index.tsx
  64. 1 1
      src/utils/const.ts
  65. 6 1
      src/utils/menu/index.ts
  66. 5 9
      src/utils/menu/router.ts
  67. 39 2
      yarn.lock

+ 2 - 2
config/proxy.ts

@@ -8,7 +8,7 @@
  */
 export default {
   dev: {
-    '/jetlinks': {
+    '/api': {
       // target: 'http://192.168.32.8:8844/',
       // ws: 'ws://192.168.32.8:8844/',
       // 开发环境
@@ -22,7 +22,7 @@ export default {
       // ws: 'ws://demo.jetlinks.cn/jetlinks',
       // target: 'http://demo.jetlinks.cn/jetlinks',
       changeOrigin: true,
-      pathRewrite: { '^/jetlinks': '' },
+      pathRewrite: { '^/api': '' },
     },
   },
   test: {

+ 11 - 0
config/routes.ts

@@ -31,6 +31,17 @@
       },
     ],
   },
+  {
+    path: '/init-home',
+    layout: false,
+    routes: [
+      {
+        name: '初始化',
+        path: '/init-home',
+        component: './init-home',
+      },
+    ],
+  },
   // {
   //   path: '/analysis',
   //   name: 'analysis',

+ 2 - 2
docker/nginx.conf

@@ -15,8 +15,8 @@ server {
         index  index.html;
     }
 
-    location ^~/jetlinks/ {
-        if ($request_uri ~* ^/jetlinks/(.*)$) {
+    location ^~/api/ {
+        if ($request_uri ~* ^/api/(.*)$) {
             proxy_pass http://host.docker.internal:8840/$1;
         }
         #proxy_pass http://host.docker.internal:8840/;

+ 1 - 0
package.json

@@ -94,6 +94,7 @@
     "omit.js": "^2.0.2",
     "react": "^17.0.0",
     "react-amap": "^1.2.8",
+    "react-beautiful-dnd": "^13.1.0",
     "react-custom-scrollbars": "^4.2.1",
     "react-dev-inspector": "^1.1.1",
     "react-dom": "^17.0.0",

BIN
public/images/firmware/error.png


BIN
public/images/firmware/finish.png


BIN
public/images/firmware/loading.png


BIN
public/images/firmware/waiting.png


BIN
public/images/init-home/background.png


+ 6 - 6
src/app.tsx

@@ -241,14 +241,14 @@ const MenuItemIcon = (
 // ProLayout 支持的api https://procomponents.ant.design/components/layout
 export const layout: RunTimeLayoutConfig = ({ initialState }) => {
   // console.log({ ...initialState });
-  let ico:any = document.querySelector('link[rel="icon"]')
-  if(ico!==null){
-    Service.settingDetail('basis').then(res=>{
-      if(res.status===200){
+  const ico: any = document.querySelector('link[rel="icon"]');
+  if (ico !== null) {
+    Service.settingDetail('basis').then((res) => {
+      if (res.status === 200) {
         // console.log(res.result.ico)
-        ico.href=res.result.ico
+        ico.href = res.result.ico;
       }
-    })
+    });
   }
   return {
     navTheme: 'light',

+ 3 - 1
src/components/BindParentDevice/index.tsx

@@ -48,7 +48,9 @@ const BindParentDevice = (props: Props) => {
       ellipsis: true,
       width: '200px',
       valueType: 'dateTime',
-      render: (text: any) => (!!text ? moment(text).format('YYYY-MM-DD HH:mm:ss') : '/'),
+      renderText: (text: any) => {
+        return !!text ? moment(text).format('YYYY-MM-DD HH:mm:ss') : '';
+      },
       sorter: true,
     },
     {

+ 129 - 0
src/components/FIndicators/index.tsx

@@ -0,0 +1,129 @@
+import { Checkbox, InputNumber, Space, DatePicker, Input } from 'antd';
+import moment from 'moment';
+
+interface Props {
+  value: any;
+  type: any;
+  onChange: (value: any) => void;
+}
+
+const FIndicators = (props: Props) => {
+  const { value, onChange, type } = props;
+  const DatePicker1: any = DatePicker;
+
+  const renderComponent = () => {
+    if (['int', 'long', 'double', 'float'].includes(type)) {
+      return (
+        <>
+          <InputNumber
+            value={value?.value ? value?.value[0] : ''}
+            onChange={(val) => {
+              onChange({
+                ...value,
+                value: [
+                  value?.range && val < value?.value[1] ? val : value?.value[0],
+                  value?.value[1],
+                ],
+              });
+            }}
+          />
+          {value.range && (
+            <>
+              ~
+              <InputNumber
+                value={value?.value ? value?.value[1] : ''}
+                onChange={(val) => {
+                  onChange({
+                    ...value,
+                    value: [value?.value && value?.value[0], val],
+                  });
+                }}
+              />
+            </>
+          )}
+        </>
+      );
+    } else if (type === 'date') {
+      if (value.range) {
+        return (
+          <DatePicker1.RangePicker
+            allowClear={false}
+            showTime
+            value={
+              value?.value && [
+                !!value?.value[0] && moment(value.value[0], 'YYYY-MM-DD HH:mm:ss'),
+                !!value?.value[1] && moment(value.value[1], 'YYYY-MM-DD HH:mm:ss'),
+              ]
+            }
+            onChange={(_: any, date: string[]) => {
+              onChange({
+                ...value,
+                value: [...date],
+              });
+            }}
+          />
+        );
+      } else {
+        return (
+          <DatePicker1
+            showTime
+            allowClear={false}
+            value={value?.value ? moment(value.value[0], 'YYYY-MM-DD HH:mm:ss') : ''}
+            onChange={(_: any, date: string) => {
+              onChange({
+                ...value,
+                value: [date],
+              });
+            }}
+          />
+        );
+      }
+    } else {
+      return (
+        <>
+          <Input
+            value={value?.value ? value?.value[0] : ''}
+            onChange={(val) => {
+              onChange({
+                ...value,
+                value: [val, value?.value && value?.value[1]],
+              });
+            }}
+          />
+          {value.range && (
+            <>
+              ~
+              <Input
+                value={value?.value ? value?.value[1] : ''}
+                onChange={(val) => {
+                  onChange({
+                    ...value,
+                    value: [value?.value && value?.value[0], val],
+                  });
+                }}
+              />
+            </>
+          )}
+        </>
+      );
+    }
+  };
+  return (
+    <Space align="baseline">
+      {renderComponent()}
+      <Checkbox
+        style={{ minWidth: 60 }}
+        checked={value?.range}
+        onChange={(e) => {
+          onChange({
+            ...value,
+            range: e.target.checked,
+          });
+        }}
+      >
+        范围
+      </Checkbox>
+    </Space>
+  );
+};
+export default FIndicators;

+ 3 - 0
src/components/FUpload/index.less

@@ -0,0 +1,3 @@
+.ant-upload.ant-upload-select {
+  display: block;
+}

+ 52 - 0
src/components/FUpload/index.tsx

@@ -0,0 +1,52 @@
+import { UploadOutlined } from '@ant-design/icons';
+import SystemConst from '@/utils/const';
+import Token from '@/utils/token';
+import { useState } from 'react';
+import { connect } from '@formily/react';
+import { Input, Upload } from 'antd';
+import type { UploadChangeParam } from 'antd/lib/upload/interface';
+import './index.less';
+
+interface Props {
+  value: any;
+  onChange: (value: any) => void;
+  placeholder: string;
+  beforeUpload: any;
+}
+
+const FUpload = connect((props: Props) => {
+  const [url, setUrl] = useState<any>(props?.value?.url);
+
+  const handleChange = (info: UploadChangeParam) => {
+    if (info.file.status === 'done') {
+      const result = info.file.response?.result;
+      const f = {
+        ...result,
+        url: `${location.protocol}://${SystemConst.API_BASE}/file/${result?.id}?accessKey=${result?.others?.accessKey}`,
+      };
+      setUrl(f.url);
+      props.onChange(f);
+    }
+  };
+
+  return (
+    <Upload
+      beforeUpload={props.beforeUpload}
+      action={`/${SystemConst.API_BASE}/file/upload`}
+      headers={{
+        'X-Access-Token': Token.get(),
+      }}
+      multiple={false}
+      onChange={handleChange}
+      progress={{}}
+    >
+      <Input
+        placeholder={props.placeholder}
+        value={url || ''}
+        readOnly
+        addonAfter={<UploadOutlined />}
+      />
+    </Upload>
+  );
+});
+export default FUpload;

+ 8 - 2
src/components/Upload/Image/index.tsx

@@ -20,7 +20,7 @@ interface UploadImageProps {
    */
   size?: number;
   style?: React.CSSProperties;
-  backgroundSize?:string;
+  backgroundSize?: string;
 }
 
 export default ({ onChange, value, ...extraProps }: UploadImageProps) => {
@@ -77,7 +77,13 @@ export default ({ onChange, value, ...extraProps }: UploadImageProps) => {
             {values ? (
               <>
                 {/*<img width={120} height={120} src={values} />*/}
-                <div className={'upload-image'} style={{ backgroundImage: `url(${values})`,backgroundSize:extraProps.backgroundSize }} />
+                <div
+                  className={'upload-image'}
+                  style={{
+                    backgroundImage: `url(${values})`,
+                    backgroundSize: extraProps.backgroundSize,
+                  }}
+                />
                 <div className={'upload-image-mask'}>点击修改</div>
               </>
             ) : (

+ 4 - 2
src/hooks/websocket/useWebSocket.ts

@@ -144,11 +144,13 @@ export default function useWebSocket(socketUrl: string, options: Options = {}):
     setReadyState(ws?.readyState);
     if (readyState === ReadyState.Open) {
       ws.send(message);
-    } else {
+    } else if (ws) {
       connectWs();
       // todo 考虑重写
       setTimeout(() => {
-        ws.send(message);
+        if (readyState === ReadyState.Open) {
+          ws.send(message);
+        }
       }, 3000);
       // throw new Error('WebSocket disconnected');
     }

+ 1 - 0
src/pages/Northbound/AliCloud/index.tsx

@@ -206,6 +206,7 @@ const AliCloud = () => {
         search={false}
         scroll={{ x: 1366 }}
         columns={columns}
+        columnEmptyText={''}
         actionRef={actionRef}
         params={searchParams}
         options={{ fullScreen: true }}

+ 1 - 0
src/pages/Northbound/DuerOS/index.tsx

@@ -241,6 +241,7 @@ export default () => {
         columns={columns}
         actionRef={actionRef}
         params={searchParams}
+        columnEmptyText={''}
         scroll={{ x: 1366 }}
         options={{ fullScreen: true }}
         request={(params) =>

+ 1 - 0
src/pages/cloud/DuerOS/index.tsx

@@ -102,6 +102,7 @@ const DuerOS = () => {
         search={false}
         params={param}
         columns={columns}
+        columnEmptyText={''}
         request={(params) =>
           service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
         }

+ 0 - 66
src/pages/device/Firmware/Detail/History/index.tsx

@@ -1,66 +0,0 @@
-import type { ProColumns } from '@jetlinks/pro-table';
-import ProTable from '@jetlinks/pro-table';
-import { service, state } from '@/pages/device/Firmware';
-import type { HistoryItem } from '@/pages/device/Firmware/typings';
-import { useParams } from 'umi';
-import { useEffect, useState } from 'react';
-
-const History = () => {
-  const param = useParams<{ id: string }>();
-
-  const [defaultParams, setParams] = useState<Record<string, unknown>>();
-  useEffect(() => {
-    if (state.historyParams) {
-      setParams({ ...state.historyParams });
-    }
-    return () => {
-      state.historyParams = undefined;
-      state.taskItem = undefined;
-    };
-  }, []);
-  const columns: ProColumns<HistoryItem>[] = [
-    {
-      dataIndex: 'index',
-      valueType: 'indexBorder',
-      width: 48,
-    },
-    {
-      title: '设备名称',
-      dataIndex: 'deviceName',
-    },
-    {
-      title: '任务名称',
-      dataIndex: 'taskName',
-    },
-    {
-      title: '版本',
-      dataIndex: 'version',
-    },
-    {
-      title: '状态',
-      dataIndex: 'state',
-      renderText: (text) => text.text,
-    },
-    {
-      title: '进度(%)',
-      dataIndex: 'progress',
-    },
-    {
-      title: '创建时间',
-      dataIndex: 'createTime',
-      valueType: 'dateTime',
-    },
-  ];
-  return (
-    <ProTable
-      columns={columns}
-      defaultParams={{
-        ...defaultParams,
-        firmwareId: param.id,
-      }}
-      request={(params) => service.history(params)}
-      rowKey="id"
-    />
-  );
-};
-export default History;

+ 0 - 114
src/pages/device/Firmware/Detail/Task/Detail/index.tsx

@@ -1,114 +0,0 @@
-import { Badge, Col, Modal, Row, Statistic } from 'antd';
-import { useEffect, useState } from 'react';
-import { service, state } from '@/pages/device/Firmware';
-import encodeQuery from '@/utils/encodeQuery';
-
-interface Props {
-  visible: boolean;
-  close: () => void;
-}
-
-type TaskState = 'waiting' | 'processing' | 'success' | 'failed';
-const map = {
-  waiting: {
-    status: 'warning',
-    text: '等待升级',
-  },
-  processing: {
-    status: 'processing',
-    text: '升级中',
-  },
-  success: {
-    status: 'success',
-    text: '完成',
-  },
-  failed: {
-    status: 'error',
-    text: '失败',
-  },
-};
-const Detail = (props: Props) => {
-  const [count, setCount] = useState<{
-    waiting: number;
-    processing: number;
-    success: number;
-    failed: number;
-  }>({
-    waiting: 0,
-    processing: 0,
-    success: 0,
-    failed: 0,
-  });
-
-  const getStateCount = (status: TaskState) =>
-    service
-      .historyCount(
-        encodeQuery({
-          terms: {
-            taskId: state.taskItem?.id,
-            state: status,
-          },
-        }),
-      )
-      .then((resp) => {
-        count[`${status}`] = resp.result;
-        setCount({ ...count });
-      });
-
-  useEffect(() => {
-    (['waiting', 'processing', 'success', 'failed'] as TaskState[]).forEach((s) => {
-      if (state.taskItem?.id) {
-        getStateCount(s);
-      }
-    });
-  }, [state.taskItem]);
-
-  return (
-    <Modal
-      maskClosable={false}
-      width="30vw"
-      visible={props.visible}
-      onCancel={() => props.close()}
-      title="任务详情"
-    >
-      <Row gutter={16}>
-        {Object.keys(count)
-          .reduce((previousValue: any[], currentValue) => {
-            previousValue.push({
-              key: currentValue,
-              value: count[currentValue],
-              ...map[currentValue],
-            });
-            return previousValue;
-          }, [])
-          .map((item) => (
-            <Col span={6} key={item.key}>
-              <Statistic
-                title={
-                  <Badge
-                    status={item.status}
-                    text={
-                      <a
-                        onClick={() => {
-                          state.taskDetail = false;
-                          state.tab = 'history';
-                          state.historyParams = {
-                            taskId: state.taskItem?.id,
-                            state: item.key,
-                          };
-                        }}
-                      >
-                        {item.text}
-                      </a>
-                    }
-                  />
-                }
-                value={item.value}
-              />
-            </Col>
-          ))}
-      </Row>
-    </Modal>
-  );
-};
-export default Detail;

+ 0 - 92
src/pages/device/Firmware/Detail/Task/Release/index.tsx

@@ -1,92 +0,0 @@
-import { Modal } from 'antd';
-import { createForm } from '@formily/core';
-import { createSchemaField } from '@formily/react';
-import { Form, FormItem, Select } from '@formily/antd';
-import type { ISchema } from '@formily/json-schema';
-import FSelectDevices from '@/components/FSelectDevices';
-import { service, state } from '@/pages/device/Firmware';
-import type { DeviceInstance } from '@/pages/device/Instance/typings';
-import { onlyMessage } from '@/utils/util';
-
-interface Props {
-  close: () => void;
-  visible: boolean;
-}
-
-const Release = (props: Props) => {
-  const form = createForm({
-    validateFirst: true,
-  });
-
-  const SchemaField = createSchemaField({
-    components: {
-      FormItem,
-      Select,
-      FSelectDevices,
-    },
-  });
-
-  const save = async () => {
-    const values: { releaseType: 'all' | 'part'; part: DeviceInstance[] } = await form.submit();
-    if (!(values.part?.length && values.part?.length <= 0)) {
-      values.releaseType = 'all';
-    }
-    const resp = await service.deploy(
-      state.taskItem!.id,
-      values?.releaseType,
-      values?.part?.map((i) => i.id),
-    );
-    if (resp.status === 200) {
-      onlyMessage('操作成功');
-    } else {
-      onlyMessage('操作失败', 'error');
-    }
-    props.close();
-  };
-
-  const schema: ISchema = {
-    type: 'object',
-    properties: {
-      releaseType: {
-        title: '发布方式',
-        'x-component': 'Select',
-        'x-decorator': 'FormItem',
-        default: 'all',
-        enum: [
-          { label: '所有设备', value: 'all' },
-          { label: '选择设备', value: 'part' },
-        ],
-      },
-      part: {
-        title: '选择设备',
-        'x-decorator': 'FormItem',
-        'x-component': 'FSelectDevices',
-        'x-visible': false,
-        'x-reactions': {
-          dependencies: ['.releaseType'],
-          fulfill: {
-            state: {
-              visible: '{{$deps[0]==="part"}}',
-            },
-          },
-        },
-      },
-    },
-  };
-  return (
-    <Modal
-      maskClosable={false}
-      title="发布任务"
-      onOk={save}
-      visible={props.visible}
-      onCancel={() => {
-        props.close();
-      }}
-    >
-      <Form form={form}>
-        <SchemaField schema={schema} />
-      </Form>
-    </Modal>
-  );
-};
-export default Release;

+ 0 - 86
src/pages/device/Firmware/Detail/Task/Save/index.tsx

@@ -1,86 +0,0 @@
-import { Modal } from 'antd';
-import { service, state } from '../../..';
-import { createForm } from '@formily/core';
-import { createSchemaField } from '@formily/react';
-import { Form, FormItem, Input, NumberPicker, Select } from '@formily/antd';
-import type { ISchema } from '@formily/json-schema';
-import { onlyMessage } from '@/utils/util';
-
-interface Props {
-  visible: boolean;
-  close: () => void;
-}
-
-const Save = (props: Props) => {
-  const form = createForm({
-    validateFirst: true,
-  });
-
-  const SchemaField = createSchemaField({
-    components: {
-      FormItem,
-      Input,
-      Select,
-      NumberPicker,
-    },
-  });
-
-  const schema: ISchema = {
-    type: 'object',
-    properties: {
-      name: {
-        title: '名称',
-        'x-component': 'Input',
-        'x-decorator': 'FormItem',
-      },
-      timeoutSeconds: {
-        title: '超时时间',
-        'x-component': 'NumberPicker',
-        'x-decorator': 'FormItem',
-      },
-      mode: {
-        title: '推送方式',
-        'x-component': 'Select',
-        'x-decorator': 'FormItem',
-        enum: [
-          { label: '平台推送', value: 'push' },
-          { label: '设备拉取', value: 'pull' },
-        ],
-      },
-      description: {
-        title: '名称',
-        'x-component': 'Input.TextArea',
-        'x-decorator': 'FormItem',
-      },
-    },
-  };
-
-  const save = async () => {
-    const values: Record<string, unknown> = await form.submit();
-    // 判断current 没有数据应该回退上一页
-    values.productId = state.current?.productId;
-    values.firmwareId = state.current?.id;
-    const resp = await service.saveTask(values);
-    if (resp.status === 200) {
-      onlyMessage('操作成功');
-    } else {
-      onlyMessage('操作失败', 'error');
-    }
-    props.close();
-  };
-  return (
-    <Modal
-      maskClosable={false}
-      onOk={save}
-      width="40vw"
-      visible={props.visible}
-      onCancel={() => props.close()}
-      title="新建任务"
-    >
-      <Form form={form} labelCol={5} wrapperCol={16}>
-        <SchemaField schema={schema} />
-      </Form>
-    </Modal>
-  );
-};
-export default Save;

+ 0 - 136
src/pages/device/Firmware/Detail/Task/index.tsx

@@ -1,136 +0,0 @@
-import type { ProColumns } from '@jetlinks/pro-table';
-import ProTable from '@jetlinks/pro-table';
-import type { TaskItem } from '@/pages/device/Firmware/typings';
-import { service, state } from '@/pages/device/Firmware';
-import { Button, Tooltip } from 'antd';
-import {
-  CloudDownloadOutlined,
-  DeleteOutlined,
-  EyeOutlined,
-  PieChartOutlined,
-  PlusOutlined,
-} from '@ant-design/icons';
-import { useIntl, useParams } from 'umi';
-import Save from '@/pages/device/Firmware/Detail/Task/Save';
-import { observer } from '@formily/react';
-import Release from '@/pages/device/Firmware/Detail/Task/Release';
-import Detail from '@/pages/device/Firmware/Detail/Task/Detail';
-
-const Task = observer(() => {
-  const intl = useIntl();
-  const param = useParams<{ id: string }>();
-  const columns: ProColumns<TaskItem>[] = [
-    {
-      dataIndex: 'index',
-      valueType: 'indexBorder',
-      width: 48,
-    },
-    {
-      dataIndex: 'id',
-      title: 'id',
-      width: 200,
-    },
-    {
-      dataIndex: 'name',
-      title: '任务名称',
-    },
-    {
-      dataIndex: 'mode',
-      title: '升级方式',
-      renderText: (text) => text.text,
-    },
-    {
-      dataIndex: 'timeoutSeconds',
-      title: '超时时间(秒)',
-    },
-    {
-      dataIndex: 'createTime',
-      valueType: 'dateTime',
-      title: '创建时间',
-    },
-    {
-      title: '操作',
-      valueType: 'option',
-      align: 'center',
-      render: (text, record) => [
-        <a
-          key="cat"
-          onClick={() => {
-            state.task = true;
-          }}
-        >
-          <Tooltip title="查看">
-            <EyeOutlined />
-          </Tooltip>
-        </a>,
-        <a
-          key="task"
-          onClick={() => {
-            state.release = true;
-            state.taskItem = record;
-          }}
-        >
-          <Tooltip title="下发任务">
-            <CloudDownloadOutlined />
-          </Tooltip>
-        </a>,
-        <a
-          key="detail"
-          onClick={() => {
-            state.taskDetail = true;
-            state.taskItem = record;
-          }}
-        >
-          <Tooltip title="任务详情">
-            <PieChartOutlined />
-          </Tooltip>
-        </a>,
-        <a key="remove">
-          <Tooltip title="删除">
-            <DeleteOutlined />
-          </Tooltip>
-        </a>,
-      ],
-    },
-  ];
-  return (
-    <>
-      <ProTable
-        columns={columns}
-        rowKey="id"
-        defaultParams={{
-          firmwareId: param.id,
-        }}
-        toolBarRender={() => [
-          <Button onClick={() => {}} key="button" icon={<PlusOutlined />} type="primary">
-            {intl.formatMessage({
-              id: 'pages.data.option.add',
-              defaultMessage: '新增',
-            })}
-          </Button>,
-        ]}
-        request={async (params) => service.task(params)}
-      />
-      <Save
-        close={() => {
-          state.task = false;
-        }}
-        visible={state.task}
-      />
-      <Release
-        close={() => {
-          state.release = false;
-        }}
-        visible={state.release}
-      />
-      <Detail
-        visible={state.taskDetail}
-        close={() => {
-          state.taskDetail = false;
-          state.taskItem = undefined;
-        }}
-      />
-    </>
-  );
-});
-export default Task;

+ 0 - 68
src/pages/device/Firmware/Detail/index.tsx

@@ -1,68 +0,0 @@
-import { PageContainer } from '@ant-design/pro-layout';
-import { history, useParams } from 'umi';
-import { Descriptions } from 'antd';
-import { service, state } from '@/pages/device/Firmware';
-import History from './History';
-import { useEffect, useState } from 'react';
-import type { FirmwareItem } from '@/pages/device/Firmware/typings';
-import Task from '@/pages/device/Firmware/Detail/Task';
-import { observer } from '@formily/react';
-
-const Detail = observer(() => {
-  const [data, setData] = useState<FirmwareItem | undefined>(state.current);
-  const param = useParams<{ id: string }>();
-  useEffect(() => {
-    if (!state.current) {
-      service.detail(param.id).then((resp) => {
-        if (resp.status === 200) {
-          setData(resp.result);
-        }
-      });
-    }
-  }, [param.id]);
-
-  const list = [
-    {
-      key: 'task',
-      tab: '升级任务',
-      component: <Task />,
-    },
-    {
-      key: 'history',
-      tab: '升级记录',
-      component: <History />,
-    },
-  ];
-  return (
-    <PageContainer
-      tabActiveKey={state.tab}
-      onBack={history.goBack}
-      onTabChange={(key) => {
-        state.tab = key as 'task' | 'history';
-      }}
-      content={
-        <>
-          <Descriptions size="small" column={3}>
-            {[
-              { key: 'ID', value: data?.id },
-              { key: '所属产品', value: data?.productName },
-              { key: '版本号', value: data?.version },
-              { key: '版本序号', value: data?.versionOrder },
-              { key: '签名方式', value: data?.signMethod },
-              { key: '签名', value: data?.sign },
-            ].map((item) => (
-              <Descriptions.Item key={item.key} label={item.key}>
-                {item.value}
-              </Descriptions.Item>
-            ))}
-          </Descriptions>
-        </>
-      }
-      title={<>固件: {state.current?.name}</>}
-      tabList={list}
-    >
-      {list.find((k) => k.key === state.tab)?.component}
-    </PageContainer>
-  );
-});
-export default Detail;

+ 249 - 18
src/pages/device/Firmware/Save/index.tsx

@@ -1,14 +1,14 @@
 import { Modal } from 'antd';
 import type { FirmwareItem } from '@/pages/device/Firmware/typings';
 import { createSchemaField } from '@formily/react';
-import { Form, FormGrid, FormItem, Input, Select } from '@formily/antd';
+import { Form, FormGrid, FormItem, Input, Select, ArrayTable } from '@formily/antd';
 import type { Field } from '@formily/core';
+import { onFieldValueChange, onFormInit } from '@formily/core';
 import { createForm } from '@formily/core';
 import type { ISchema } from '@formily/json-schema';
-import FUpload from '@/components/Upload';
+import FUpload from '@/components/FUpload';
 import { action } from '@formily/reactive';
 import { service } from '@/pages/device/Firmware';
-import type { Response } from '@/utils/typings';
 import { useRef } from 'react';
 import type { ProductItem } from '@/pages/device/Product/typings';
 import { onlyMessage } from '@/utils/util';
@@ -21,13 +21,30 @@ interface Props {
 
 const Save = (props: Props) => {
   const { data, close, visible } = props;
+  const fileInfo = useRef<any>({});
+  const signMethod = useRef<'md5' | 'sha256'>('md5');
 
   const form = createForm({
     validateFirst: true,
     initialValues: data,
+    effects: () => {
+      onFormInit(async (form1) => {
+        if (!data?.id) return;
+        form1.setInitialValues({ ...data, upload: { url: data?.url } });
+      });
+      onFieldValueChange('signMethod', (field) => {
+        const value = (field as Field).value;
+        signMethod.current = value;
+      });
+      onFieldValueChange('upload', (field) => {
+        const value = (field as Field).value;
+        fileInfo.current = value;
+      });
+    },
   });
 
   const products = useRef<ProductItem[]>([]);
+
   const useAsyncDataSource = (services: (arg0: Field) => Promise<any>) => (field: Field) => {
     field.loading = true;
     services(field).then(
@@ -46,18 +63,24 @@ const Save = (props: Props) => {
       Input,
       FUpload,
       Select,
+      ArrayTable,
     },
   });
 
   const save = async () => {
-    const values: FirmwareItem = await form.submit();
+    const values: any = await form.submit();
     const product = products.current?.find((item) => item.id === values.productId);
     values.productName = product?.name || '';
-    const resp = (await service.save(values)) as Response<FirmwareItem>;
+    const { upload, ...extra } = values;
+    const params = {
+      ...extra,
+      url: upload.url || data?.url,
+      size: upload.length || data?.size,
+    };
+    const resp = (await service.update(params)) as any;
     if (resp.status === 200) {
       onlyMessage('保存成功!');
-    } else {
-      onlyMessage('保存失败!', 'error');
+      close();
     }
   };
   const schema: ISchema = {
@@ -71,47 +94,255 @@ const Save = (props: Props) => {
           maxColumns: 2,
         },
         properties: {
-          productId: {
-            title: '产品',
-            'x-decorator': 'FormItem',
-            'x-component': 'Select',
-            'x-reactions': ['{{useAsyncDataSource(loadData)}}'],
-          },
           name: {
             title: '名称',
             'x-decorator': 'FormItem',
             'x-component': 'Input',
+            'x-component-props': {
+              placeholder: '请输入固件名称',
+            },
+            required: true,
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入固件名称',
+              },
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+            ],
+          },
+          productId: {
+            title: '所属产品',
+            'x-decorator': 'FormItem',
+            'x-component': 'Select',
+            'x-reactions': ['{{useAsyncDataSource(loadData)}}'],
+            'x-component-props': {
+              placeholder: '请选择所属产品',
+            },
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            required: true,
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择所属产品',
+              },
+            ],
           },
           version: {
             title: '版本号',
             'x-decorator': 'FormItem',
             'x-component': 'Input',
+            'x-component-props': {
+              placeholder: '请输入版本号',
+            },
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            required: true,
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入版本号',
+              },
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+            ],
           },
           versionOrder: {
             title: '版本序号',
             'x-decorator': 'FormItem',
             'x-component': 'Input',
+            'x-component-props': {
+              placeholder: '请输入版本序号',
+            },
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            required: true,
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入版本号',
+              },
+              {
+                maximum: 99999,
+                minimum: 1,
+              },
+            ],
           },
           signMethod: {
             title: '签名方式',
             'x-decorator': 'FormItem',
             'x-component': 'Select',
             enum: [
-              { label: 'MD5', value: 'MD5' },
-              { label: 'SHA256', value: 'SHA256' },
+              { label: 'MD5', value: 'md5' },
+              { label: 'SHA256', value: 'sha256' },
+            ],
+            'x-component-props': {
+              placeholder: '请选择签名方式',
+            },
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            required: true,
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择签名方式',
+              },
             ],
           },
           sign: {
             title: '签名',
             'x-decorator': 'FormItem',
             'x-component': 'Input',
+            'x-component-props': {
+              placeholder: '请输入签名',
+            },
+            'x-decorator-props': {
+              tooltip: '请输入本地文件进行签名加密后的值',
+              gridSpan: 1,
+            },
+            required: true,
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入签名',
+              },
+              // {
+              //   validator: (value: string) => {
+              //     return new Promise((resolve, reject) => {
+              //       if (value !== '' && signMethod.current && fileInfo.current[signMethod.current]) {
+              //         if (value !== fileInfo.current[signMethod.current]) {
+              //           return reject(new Error('签名不一致,请检查文件是否上传正确'));
+              //         }
+              //       }
+              //       return resolve('');
+              //     });
+              //   },
+              // },
+            ],
+            'x-reactions': [
+              {
+                dependencies: ['.upload', 'signMethod'],
+                fulfill: {
+                  state: {
+                    selfErrors:
+                      '{{$deps[0] && $deps[1] && $deps[0][$deps[1]] && $self.value && $self.value !== $deps[0][$deps[1]] ? "签名不一致,请检查文件是否上传正确" : ""}}',
+                  },
+                },
+              },
+            ],
           },
-          '{url,size}': {
+          upload: {
             title: '文件上传',
             'x-decorator': 'FormItem',
             'x-component': 'FUpload',
             'x-component-props': {
               type: 'file',
+              placeholder: '请上传文件',
+            },
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            required: true,
+            'x-validator': [
+              {
+                required: true,
+                message: '请上传文件',
+              },
+            ],
+          },
+          properties: {
+            type: 'array',
+            'x-decorator': 'FormItem',
+            'x-component': 'ArrayTable',
+            title: '其他配置',
+            'x-component-props': {
+              pagination: { pageSize: 10 },
+              scroll: { x: '100%' },
+            },
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            items: {
+              type: 'object',
+              properties: {
+                column1: {
+                  type: 'void',
+                  'x-component': 'ArrayTable.Column',
+                  'x-component-props': { title: 'KEY' },
+                  properties: {
+                    id: {
+                      type: 'string',
+                      'x-decorator': 'Editable',
+                      'x-component': 'Input',
+                    },
+                  },
+                },
+                column2: {
+                  type: 'void',
+                  'x-component': 'ArrayTable.Column',
+                  'x-component-props': { title: 'VALUE' },
+                  properties: {
+                    value: {
+                      type: 'string',
+                      'x-decorator': 'FormItem',
+                      'x-component': 'Input',
+                    },
+                  },
+                },
+                column3: {
+                  type: 'void',
+                  'x-component': 'ArrayTable.Column',
+                  'x-component-props': {
+                    title: '操作',
+                    dataIndex: 'operations',
+                  },
+                  properties: {
+                    item: {
+                      type: 'void',
+                      'x-component': 'FormItem',
+                      properties: {
+                        remove: {
+                          type: 'void',
+                          'x-component': 'ArrayTable.Remove',
+                        },
+                      },
+                    },
+                  },
+                },
+              },
+            },
+            properties: {
+              add: {
+                type: 'void',
+                'x-component': 'ArrayTable.Addition',
+                title: '添加条目',
+              },
+            },
+          },
+          description: {
+            title: '说明',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input.TextArea',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              rows: 3,
+              showCount: true,
+              maxLength: 200,
+              placeholder: '请输入说明',
             },
           },
         },
@@ -123,12 +354,12 @@ const Save = (props: Props) => {
     <Modal
       maskClosable={false}
       width="50vw"
-      title="新增固件版本"
+      title={data?.id ? '编辑' : '新增'}
       onCancel={() => close()}
       onOk={() => save()}
       visible={visible}
     >
-      <Form form={form} labelCol={5} wrapperCol={16}>
+      <Form form={form} labelCol={5} wrapperCol={16} layout="vertical">
         <SchemaField schema={schema} scope={{ useAsyncDataSource, loadData }} />
       </Form>
     </Modal>

+ 25 - 0
src/pages/device/Firmware/Task/Detail/index.less

@@ -0,0 +1,25 @@
+.firmwareDetailCard {
+  position: relative;
+  height: 100px;
+  padding-top: 15px;
+  padding-left: 15px;
+  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;
+
+  .firmwareDetailCardTitle {
+    color: rgba(0, 0, 0, 0.64);
+    font-size: 14px;
+  }
+
+  .firmwareDetailCardNum {
+    font-size: 36px;
+  }
+
+  .firmwareDetailCardImg {
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    width: 120px;
+  }
+}

+ 170 - 0
src/pages/device/Firmware/Task/Detail/index.tsx

@@ -0,0 +1,170 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { observer } from '@formily/react';
+import { Badge, Card, Col, Row } from 'antd';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
+import { Tooltip } from 'antd';
+import { useRef, useState } from 'react';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { EyeOutlined } from '@ant-design/icons';
+// import { useHistory } from 'umi';
+import type { FirmwareItem } from '@/pages/device/Firmware/typings';
+import useDomFullHeight from '@/hooks/document/useDomFullHeight';
+// import usePermissions from '@/hooks/permission';
+import SearchComponent from '@/components/SearchComponent';
+import { service } from '@/pages/device/Firmware';
+import styles from './index.less';
+import { model } from '@formily/reactive';
+
+const colorMap = new Map();
+colorMap.set('waiting', '#FF9000');
+colorMap.set('loading', '#4293FF');
+colorMap.set('finish', '#24B276');
+colorMap.set('error', '#F76F5D');
+
+const state = model<{
+  waiting: number;
+  loading: number;
+  finish: number;
+  error: number;
+}>({
+  waiting: 0,
+  loading: 2,
+  finish: 4,
+  error: 0,
+});
+
+const Detail = observer(() => {
+  const actionRef = useRef<ActionType>();
+  const intl = useIntl();
+  const { minHeight } = useDomFullHeight(`.firmware-task-detail`, 24);
+  // const { permission } = usePermissions('device/Firmware');
+  const [param, setParam] = useState({});
+
+  const arr = [
+    {
+      key: 'waiting',
+      name: '等待升级',
+      img: require('/public/images/firmware/waiting.png'),
+    },
+    {
+      key: 'loading',
+      name: '升级中',
+      img: require('/public/images/firmware/loading.png'),
+    },
+    {
+      key: 'finish',
+      name: '升级完成',
+      img: require('/public/images/firmware/finish.png'),
+    },
+    {
+      key: 'error',
+      name: '升级失败',
+      img: require('/public/images/firmware/error.png'),
+    },
+  ];
+
+  const columns: ProColumns<FirmwareItem>[] = [
+    {
+      title: '设备名称',
+      ellipsis: true,
+      dataIndex: 'name',
+    },
+    {
+      title: '所属产品',
+      ellipsis: true,
+      dataIndex: 'version',
+    },
+    {
+      title: '创建时间',
+      ellipsis: true,
+      dataIndex: 'signMethod',
+    },
+    {
+      title: '完成时间',
+      ellipsis: true,
+      dataIndex: 'signMethod',
+    },
+    {
+      title: '进度',
+      ellipsis: true,
+      dataIndex: 'signMethod',
+    },
+    {
+      title: '状态',
+      ellipsis: true,
+      dataIndex: 'signMethod',
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: () => [
+        <a onClick={() => {}} key="link">
+          <Tooltip
+            title={intl.formatMessage({
+              id: 'pages.data.option.detail',
+              defaultMessage: '查看',
+            })}
+            key={'detail'}
+          >
+            <EyeOutlined />
+          </Tooltip>
+        </a>,
+      ],
+    },
+  ];
+  return (
+    <PageContainer>
+      <Card style={{ marginBottom: 20 }}>
+        <Row gutter={24}>
+          {arr.map((item) => (
+            <Col span={6} key={item.key}>
+              <div className={styles.firmwareDetailCard}>
+                <div className={styles.firmwareDetailCardTitle}>
+                  <Badge color={colorMap.get(item.key)} />
+                  {item.name}
+                </div>
+                <div
+                  className={styles.firmwareDetailCardNum}
+                  style={{ color: colorMap.get(item.key) }}
+                >
+                  {state[item.key]}
+                </div>
+                <div className={styles.firmwareDetailCardImg}>
+                  <img style={{ width: '100%' }} src={item.img} />
+                </div>
+              </div>
+            </Col>
+          ))}
+        </Row>
+      </Card>
+      <SearchComponent<FirmwareItem>
+        field={columns}
+        target="firmware-task-detail"
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTable<FirmwareItem>
+        scroll={{ x: 1366 }}
+        tableClassName={'firmware-task-detail'}
+        tableStyle={{ minHeight }}
+        search={false}
+        params={param}
+        request={async (params) =>
+          service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        }
+        columns={columns}
+        actionRef={actionRef}
+      />
+    </PageContainer>
+  );
+});
+export default Detail;

+ 279 - 0
src/pages/device/Firmware/Task/Save/index.tsx

@@ -0,0 +1,279 @@
+import { Modal } from 'antd';
+import type { FirmwareItem } from '@/pages/device/Firmware/typings';
+import { createSchemaField } from '@formily/react';
+import {
+  Form,
+  FormGrid,
+  FormItem,
+  Input,
+  Select,
+  ArrayTable,
+  NumberPicker,
+  Radio,
+} from '@formily/antd';
+import { createForm, onFieldValueChange } from '@formily/core';
+import type { ISchema } from '@formily/json-schema';
+import FUpload from '@/components/Upload';
+import { service } from '@/pages/device/Firmware';
+import type { Response } from '@/utils/typings';
+import { useRef } from 'react';
+import type { ProductItem } from '@/pages/device/Product/typings';
+import { onlyMessage } from '@/utils/util';
+import FSelectDevices from '@/components/FSelectDevices';
+
+interface Props {
+  data?: FirmwareItem;
+  close: () => void;
+  visible: boolean;
+}
+
+const Save = (props: Props) => {
+  const { data, close, visible } = props;
+
+  const form = createForm({
+    validateFirst: true,
+    initialValues: data,
+    effects() {
+      onFieldValueChange('mode', async (field) => {
+        field
+          .query('timeoutSeconds1')
+          .take()
+          .setDecoratorProps({
+            gridSpan: field.value === 'push' ? 1 : 2,
+          });
+        field
+          .query('timeoutSeconds')
+          .take()
+          .setDecoratorProps({
+            gridSpan: field.value === 'push' ? 1 : 2,
+          });
+      });
+      onFieldValueChange('releaseType', async (field) => {
+        field.setDecoratorProps({
+          gridSpan: field.value === 'all' ? 2 : 1,
+        });
+      });
+    },
+  });
+
+  const products = useRef<ProductItem[]>([]);
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      FormGrid,
+      Input,
+      FUpload,
+      Select,
+      ArrayTable,
+      NumberPicker,
+      Radio,
+      FSelectDevices,
+    },
+  });
+
+  const save = async () => {
+    const values: FirmwareItem = await form.submit();
+    const product = products.current?.find((item) => item.id === values.productId);
+    values.productName = product?.name || '';
+    const resp = (await service.save(values)) as Response<FirmwareItem>;
+    if (resp.status === 200) {
+      onlyMessage('保存成功!');
+    } else {
+      onlyMessage('保存失败!', 'error');
+    }
+  };
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      grid: {
+        type: 'void',
+        'x-component': 'FormGrid',
+        'x-component-props': {
+          minColumns: 2,
+          maxColumns: 2,
+        },
+        properties: {
+          name: {
+            title: '任务名称',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-component-props': {
+              placeholder: '请输入任务名称',
+            },
+            required: true,
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入任务名称',
+              },
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+            ],
+          },
+          mode: {
+            title: '推送方式',
+            'x-component': 'Select',
+            'x-decorator': 'FormItem',
+            enum: [
+              { label: '平台推送', value: 'push' },
+              { label: '设备拉取', value: 'pull' },
+            ],
+            'x-component-props': {
+              placeholder: '请选择推送方式',
+            },
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            required: true,
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择推送方式',
+              },
+            ],
+          },
+          timeoutSeconds: {
+            title: '响应超时时间',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-component-props': {
+              placeholder: '请输入响应超时时间(秒)',
+            },
+            'x-visible': false,
+            required: true,
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入响应超时时间',
+              },
+              {
+                maximum: 99999,
+                minimum: 1,
+              },
+            ],
+            'x-reactions': {
+              dependencies: ['.mode'],
+              fulfill: {
+                state: {
+                  visible: '{{$deps[0]==="push"}}',
+                },
+              },
+            },
+          },
+          timeoutSeconds1: {
+            title: '升级超时时间',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-component-props': {
+              placeholder: '请输入升级超时时间(秒)',
+            },
+            'x-visible': false,
+            required: true,
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入升级超时时间',
+              },
+              {
+                maximum: 99999,
+                minimum: 1,
+              },
+            ],
+            'x-reactions': {
+              dependencies: ['.mode'],
+              fulfill: {
+                state: {
+                  visible: '{{!!$deps[0]}}',
+                },
+              },
+            },
+          },
+          releaseType: {
+            type: 'number',
+            title: '升级设备',
+            default: 'all',
+            'x-visible': false,
+            enum: [
+              { label: '所有设备', value: 'all' },
+              { label: '选择设备', value: 'part' },
+            ],
+            'x-decorator': 'FormItem',
+            'x-component': 'Radio.Group',
+            required: true,
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择升级设备',
+              },
+            ],
+            'x-reactions': {
+              dependencies: ['.mode'],
+              fulfill: {
+                state: {
+                  visible: '{{!!$deps[0]}}',
+                },
+              },
+            },
+          },
+          part: {
+            title: '选择设备',
+            'x-decorator': 'FormItem',
+            'x-component': 'FSelectDevices',
+            'x-visible': false,
+            required: true,
+            'x-reactions': {
+              dependencies: ['.releaseType'],
+              fulfill: {
+                state: {
+                  visible: '{{$deps[0]==="part"}}',
+                },
+              },
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择设备',
+              },
+            ],
+          },
+          description: {
+            title: '说明',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input.TextArea',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              rows: 3,
+              showCount: true,
+              maxLength: 200,
+              placeholder: '请输入说明',
+            },
+          },
+        },
+      },
+    },
+  };
+
+  return (
+    <Modal
+      maskClosable={false}
+      width="50vw"
+      title="新增任务"
+      onCancel={() => close()}
+      onOk={() => save()}
+      visible={visible}
+    >
+      <Form form={form} labelCol={5} wrapperCol={16} layout="vertical">
+        <SchemaField schema={schema} />
+      </Form>
+    </Modal>
+  );
+};
+export default Save;

+ 185 - 0
src/pages/device/Firmware/Task/index.tsx

@@ -0,0 +1,185 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
+import { Popconfirm, Tooltip } from 'antd';
+import { useRef, useState } from 'react';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { EditOutlined, EyeOutlined, MinusOutlined, PlusOutlined } from '@ant-design/icons';
+import { Link, useHistory } from 'umi';
+import { model } from '@formily/reactive';
+import { observer } from '@formily/react';
+import type { FirmwareItem } from '@/pages/device/Firmware/typings';
+import Save from './Save';
+import { onlyMessage } from '@/utils/util';
+import { PermissionButton } from '@/components';
+import useDomFullHeight from '@/hooks/document/useDomFullHeight';
+import usePermissions from '@/hooks/permission';
+import SearchComponent from '@/components/SearchComponent';
+import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+import { service } from '@/pages/device/Firmware';
+
+export const state = model<{
+  current?: FirmwareItem;
+  visible: boolean;
+}>({
+  visible: false,
+});
+const Task = observer(() => {
+  const actionRef = useRef<ActionType>();
+  const intl = useIntl();
+  const { minHeight } = useDomFullHeight(`.firmware-task`, 24);
+  const { permission } = usePermissions('device/Firmware');
+  const [param, setParam] = useState({});
+  const history = useHistory<Record<string, string>>();
+
+  const columns: ProColumns<FirmwareItem>[] = [
+    {
+      title: '任务名称',
+      ellipsis: true,
+      dataIndex: 'name',
+    },
+    {
+      title: '推送方式',
+      ellipsis: true,
+      dataIndex: 'version',
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.table.description',
+        defaultMessage: '说明',
+      }),
+      ellipsis: true,
+      align: 'center',
+      dataIndex: 'description',
+    },
+    {
+      title: '完成比例',
+      ellipsis: true,
+      dataIndex: 'signMethod',
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+
+      render: (text, record) => [
+        <Link
+          onClick={() => {
+            const url = getMenuPathByParams(MENUS_CODE['device/Firmware/Task/Detail'], '123');
+            history.push(url);
+          }}
+          to={`/device/firmware/detail/${record.id}`}
+          key="link"
+        >
+          <Tooltip
+            title={intl.formatMessage({
+              id: 'pages.data.option.detail',
+              defaultMessage: '查看',
+            })}
+            key={'detail'}
+          >
+            <EyeOutlined />
+          </Tooltip>
+        </Link>,
+        <a
+          key="editable"
+          onClick={() => {
+            state.visible = true;
+          }}
+        >
+          <Tooltip
+            title={intl.formatMessage({
+              id: 'pages.data.option.edit',
+              defaultMessage: '编辑',
+            })}
+          >
+            <EditOutlined />
+          </Tooltip>
+        </a>,
+        <a key="delete">
+          <Popconfirm
+            title={intl.formatMessage({
+              id: 'pages.data.option.remove.tips',
+              defaultMessage: '确认删除?',
+            })}
+            onConfirm={async () => {
+              await service.remove(record.id);
+              onlyMessage(
+                intl.formatMessage({
+                  id: 'pages.data.option.success',
+                  defaultMessage: '操作成功!',
+                }),
+              );
+              actionRef.current?.reload();
+            }}
+          >
+            <Tooltip
+              title={intl.formatMessage({
+                id: 'pages.data.option.remove',
+                defaultMessage: '删除',
+              })}
+            >
+              <MinusOutlined />
+            </Tooltip>
+          </Popconfirm>
+        </a>,
+      ],
+    },
+  ];
+
+  return (
+    <PageContainer>
+      <SearchComponent<FirmwareItem>
+        field={columns}
+        target="firmware-task"
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTable<FirmwareItem>
+        scroll={{ x: 1366 }}
+        tableClassName={'firmware-task'}
+        tableStyle={{ minHeight }}
+        search={false}
+        params={param}
+        headerTitle={
+          <div>
+            <PermissionButton
+              onClick={() => {
+                state.visible = true;
+              }}
+              isPermission={permission.add}
+              key="button"
+              icon={<PlusOutlined />}
+              type="primary"
+            >
+              {intl.formatMessage({
+                id: 'pages.data.option.add',
+                defaultMessage: '新增',
+              })}
+            </PermissionButton>
+          </div>
+        }
+        request={async (params) =>
+          service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        }
+        columns={columns}
+        actionRef={actionRef}
+      />
+      <Save
+        data={state.current}
+        visible={state.visible}
+        close={() => {
+          state.visible = false;
+        }}
+      />
+    </PageContainer>
+  );
+});
+export default Task;

+ 102 - 80
src/pages/device/Firmware/index.tsx

@@ -1,52 +1,46 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
-import { Button, Popconfirm, Tooltip } from 'antd';
+import { Popconfirm } from 'antd';
 import moment from 'moment';
-import { useRef } from 'react';
+import { useRef, useState } from 'react';
 import { useIntl } from '@@/plugin-locale/localeExports';
-import { EditOutlined, EyeOutlined, MinusOutlined, PlusOutlined } from '@ant-design/icons';
-import { Link } from 'umi';
+import { DeleteOutlined, EditOutlined, NodeExpandOutlined, PlusOutlined } from '@ant-design/icons';
+import { useHistory } from 'umi';
 import { model } from '@formily/reactive';
 import { observer } from '@formily/react';
-import type { FirmwareItem, TaskItem } from '@/pages/device/Firmware/typings';
+import type { FirmwareItem } from '@/pages/device/Firmware/typings';
 import Service from '@/pages/device/Firmware/service';
 import Save from '@/pages/device/Firmware/Save';
-import { onlyMessage } from '@/utils/util';
+import { PermissionButton } from '@/components';
+import useDomFullHeight from '@/hooks/document/useDomFullHeight';
+import usePermissions from '@/hooks/permission';
+import SearchComponent from '@/components/SearchComponent';
+import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
 
 export const service = new Service('firmware');
 
 export const state = model<{
   current?: FirmwareItem;
   visible: boolean;
-  task: boolean;
-  release: boolean;
-  taskItem?: TaskItem;
-  taskDetail: boolean;
-  tab: 'task' | 'history';
-  historyParams?: Record<string, unknown>;
 }>({
   visible: false,
-  task: false,
-  release: false,
-  taskDetail: false,
-  tab: 'task',
 });
 const Firmware = observer(() => {
   const actionRef = useRef<ActionType>();
   const intl = useIntl();
+  const { minHeight } = useDomFullHeight(`.firmware`, 24);
+  const { permission } = usePermissions('device/Firmware');
+  const [param, setParam] = useState({});
+  const history = useHistory<Record<string, string>>();
 
   const columns: ProColumns<FirmwareItem>[] = [
     {
-      dataIndex: 'index',
-      valueType: 'indexBorder',
-      width: 48,
-    },
-    {
       title: intl.formatMessage({
         id: 'pages.device.firmware.name',
         defaultMessage: '固件名称',
       }),
+      ellipsis: true,
       dataIndex: 'name',
     },
     {
@@ -54,6 +48,7 @@ const Firmware = observer(() => {
         id: 'pages.device.firmware.version',
         defaultMessage: '固件版本',
       }),
+      ellipsis: true,
       dataIndex: 'version',
     },
     {
@@ -61,6 +56,7 @@ const Firmware = observer(() => {
         id: 'pages.device.firmware.productName',
         defaultMessage: '所属产品',
       }),
+      ellipsis: true,
       dataIndex: 'productName',
     },
     {
@@ -68,6 +64,7 @@ const Firmware = observer(() => {
         id: 'pages.device.firmware.signMethod',
         defaultMessage: '签名方式',
       }),
+      ellipsis: true,
       dataIndex: 'signMethod',
     },
     {
@@ -78,9 +75,20 @@ const Firmware = observer(() => {
       dataIndex: 'createTime',
       width: '200px',
       align: 'center',
+      ellipsis: true,
+      valueType: 'dateTime',
       render: (text: any) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
-      sorter: true,
-      defaultSortOrder: 'descend',
+      // sorter: true,
+      // defaultSortOrder: 'descend',
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.table.description',
+        defaultMessage: '说明',
+      }),
+      ellipsis: true,
+      align: 'center',
+      dataIndex: 'description',
     },
     {
       title: intl.formatMessage({
@@ -90,87 +98,100 @@ const Firmware = observer(() => {
       valueType: 'option',
       align: 'center',
       width: 200,
-
       render: (text, record) => [
-        <Link
+        <PermissionButton
+          style={{ padding: 0 }}
+          type="link"
+          isPermission={permission.action}
+          key="upgrade"
           onClick={() => {
-            state.current = record;
+            const url = getMenuPathByParams(MENUS_CODE['device/Firmware/Task'], record?.id);
+            history.push(url);
+          }}
+          tooltip={{
+            title: '升级任务',
           }}
-          to={`/device/firmware/detail/${record.id}`}
-          key="link"
         >
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.data.option.detail',
-              defaultMessage: '查看',
-            })}
-            key={'detail'}
-          >
-            <EyeOutlined />
-          </Tooltip>
-        </Link>,
-        <a
+          <NodeExpandOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          style={{ padding: 0 }}
+          type="link"
+          isPermission={permission.update}
           key="editable"
           onClick={() => {
             state.visible = true;
+            state.current = record;
           }}
-        >
-          <Tooltip
-            title={intl.formatMessage({
+          tooltip={{
+            title: intl.formatMessage({
               id: 'pages.data.option.edit',
               defaultMessage: '编辑',
-            })}
-          >
-            <EditOutlined />
-          </Tooltip>
-        </a>,
-        <a key="delete">
+            }),
+          }}
+        >
+          <EditOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          type="link"
+          key="delete"
+          style={{ padding: 0 }}
+          isPermission={permission.delete}
+        >
           <Popconfirm
-            title={intl.formatMessage({
-              id: 'pages.data.option.remove.tips',
-              defaultMessage: '确认删除?',
-            })}
             onConfirm={async () => {
               await service.remove(record.id);
-              onlyMessage(
-                intl.formatMessage({
-                  id: 'pages.data.option.success',
-                  defaultMessage: '操作成功!',
-                }),
-              );
               actionRef.current?.reload();
             }}
+            title="确认删除?"
           >
-            <Tooltip
-              title={intl.formatMessage({
-                id: 'pages.data.option.remove',
-                defaultMessage: '删除',
-              })}
-            >
-              <MinusOutlined />
-            </Tooltip>
+            <DeleteOutlined />
           </Popconfirm>
-        </a>,
+        </PermissionButton>,
       ],
     },
   ];
 
   return (
     <PageContainer>
+      <SearchComponent<FirmwareItem>
+        field={columns}
+        target="firmware"
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
       <ProTable<FirmwareItem>
-        toolBarRender={() => [
-          <Button
-            onClick={() => {
-              state.visible = true;
-            }}
-            key="button"
-            icon={<PlusOutlined />}
-            type="primary"
-          >
-            新增
-          </Button>,
-        ]}
-        request={async (params) => service.query(params)}
+        scroll={{ x: 1366 }}
+        tableClassName={'firmware'}
+        tableStyle={{ minHeight }}
+        search={false}
+        params={param}
+        columnEmptyText={''}
+        headerTitle={
+          <div>
+            <PermissionButton
+              onClick={() => {
+                state.visible = true;
+                state.current = undefined;
+              }}
+              isPermission={permission.add}
+              key="button"
+              icon={<PlusOutlined />}
+              type="primary"
+            >
+              {intl.formatMessage({
+                id: 'pages.data.option.add',
+                defaultMessage: '新增',
+              })}
+            </PermissionButton>
+          </div>
+        }
+        request={async (params) =>
+          service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        }
         columns={columns}
         actionRef={actionRef}
       />
@@ -179,6 +200,7 @@ const Firmware = observer(() => {
         visible={state.visible}
         close={() => {
           state.visible = false;
+          actionRef.current?.reload();
         }}
       />
     </PageContainer>

+ 92 - 35
src/pages/device/Instance/Detail/Diagnose/Status/index.tsx

@@ -112,7 +112,6 @@ const Status = observer((props: Props) => {
             text: '正常',
             info: null,
           });
-          DiagnoseStatusModel.count++;
           resolve({});
         }, time);
       } else {
@@ -205,7 +204,6 @@ const Status = observer((props: Props) => {
                 if (item) {
                   DiagnoseStatusModel.list = modifyArrayList(DiagnoseStatusModel.list, item);
                 }
-                DiagnoseStatusModel.count++;
                 resolve({});
               }, time);
             } else {
@@ -235,7 +233,6 @@ const Status = observer((props: Props) => {
             text: '正常',
             info: null,
           });
-          DiagnoseStatusModel.count++;
           resolve({});
         }, time);
       } else {
@@ -371,7 +368,6 @@ const Status = observer((props: Props) => {
                   if (item) {
                     DiagnoseStatusModel.list = modifyArrayList(DiagnoseStatusModel.list, item);
                   }
-                  DiagnoseStatusModel.count++;
                   resolve({});
                 }, time);
               } else {
@@ -503,7 +499,6 @@ const Status = observer((props: Props) => {
           setTimeout(() => {
             if (item) {
               DiagnoseStatusModel.list = modifyArrayList(DiagnoseStatusModel.list, item);
-              DiagnoseStatusModel.count++;
             }
             resolve({});
           }, time);
@@ -527,7 +522,6 @@ const Status = observer((props: Props) => {
             text: '正常',
             info: null,
           });
-          DiagnoseStatusModel.count++;
           resolve({});
         }, time);
       } else {
@@ -562,7 +556,6 @@ const Status = observer((props: Props) => {
                 </div>
               ),
             });
-            DiagnoseStatusModel.count++;
             resolve({});
           }, time);
         } else {
@@ -651,7 +644,6 @@ const Status = observer((props: Props) => {
             setTimeout(() => {
               if (item) {
                 DiagnoseStatusModel.list = modifyArrayList(DiagnoseStatusModel.list, item);
-                DiagnoseStatusModel.count++;
               }
               resolve({});
             }, time);
@@ -679,7 +671,6 @@ const Status = observer((props: Props) => {
             text: '正常',
             info: null,
           });
-          DiagnoseStatusModel.count++;
           resolve({});
         }, time);
       } else {
@@ -744,7 +735,6 @@ const Status = observer((props: Props) => {
                 if (item) {
                   DiagnoseStatusModel.list = modifyArrayList(DiagnoseStatusModel.list, item);
                 }
-                DiagnoseStatusModel.count++;
                 resolve({});
               }, time);
             } else {
@@ -771,7 +761,6 @@ const Status = observer((props: Props) => {
             text: '正常',
             info: null,
           });
-          DiagnoseStatusModel.count++;
           resolve({});
         }, time);
       } else {
@@ -840,7 +829,6 @@ const Status = observer((props: Props) => {
           if (item) {
             DiagnoseStatusModel.list = modifyArrayList(DiagnoseStatusModel.list, item);
           }
-          DiagnoseStatusModel.count++;
           resolve({});
         }, time);
       }
@@ -884,7 +872,6 @@ const Status = observer((props: Props) => {
                   text: '正常',
                   info: null,
                 });
-                DiagnoseStatusModel.count++;
                 resolve({});
               }, time);
             } else if (
@@ -941,7 +928,6 @@ const Status = observer((props: Props) => {
                     </div>
                   ),
                 });
-                DiagnoseStatusModel.count++;
                 resolve({});
               }, time);
             } else {
@@ -1002,7 +988,6 @@ const Status = observer((props: Props) => {
                     </div>
                   ),
                 });
-                DiagnoseStatusModel.count++;
                 resolve({});
               }, time);
             }
@@ -1051,7 +1036,6 @@ const Status = observer((props: Props) => {
                   text: '正常',
                   info: null,
                 });
-                DiagnoseStatusModel.count++;
                 resolve({});
               }, time);
             } else if (
@@ -1108,7 +1092,6 @@ const Status = observer((props: Props) => {
                     </div>
                   ),
                 });
-                DiagnoseStatusModel.count++;
                 resolve({});
               }, time);
             } else {
@@ -1169,7 +1152,6 @@ const Status = observer((props: Props) => {
                     </div>
                   ),
                 });
-                DiagnoseStatusModel.count++;
                 resolve({});
               }, time);
             }
@@ -1235,7 +1217,6 @@ const Status = observer((props: Props) => {
   //               DiagnoseStatusModel.list.length,
   //             );
   //           }
-  //           DiagnoseStatusModel.count++;
   //           resolve({});
   //         }, time);
   //       }
@@ -1291,7 +1272,6 @@ const Status = observer((props: Props) => {
   //       //     if (item) {
   //       //       DiagnoseStatusModel.list = modifyArrayList(DiagnoseStatusModel.list, item, DiagnoseStatusModel.list.length);
   //       //     }
-  //       //     DiagnoseStatusModel.count++;
   //       //     resolve({});
   //       //   }, time);
   //       // }
@@ -1354,7 +1334,6 @@ const Status = observer((props: Props) => {
   //               DiagnoseStatusModel.list.length,
   //             );
   //           }
-  //           DiagnoseStatusModel.count++;
   //           resolve({});
   //         }, time);
   //       }
@@ -1488,7 +1467,6 @@ const Status = observer((props: Props) => {
               DiagnoseStatusModel.list.length,
             );
           }
-          DiagnoseStatusModel.count++;
           resolve({});
         }, time);
       } else {
@@ -1604,7 +1582,6 @@ const Status = observer((props: Props) => {
               DiagnoseStatusModel.list.length,
             );
           }
-          DiagnoseStatusModel.count++;
           resolve({});
         }, time);
       } else {
@@ -1777,6 +1754,7 @@ const Status = observer((props: Props) => {
 
   useEffect(() => {
     if (DiagnoseStatusModel.status === 'finish') {
+      DiagnoseStatusModel.count = 0;
       const list = _.uniq(_.map(DiagnoseStatusModel.list, 'status'));
       if (device.state?.value !== 'online') {
         DiagnoseStatusModel.state = 'error';
@@ -1786,6 +1764,9 @@ const Status = observer((props: Props) => {
       } else {
         DiagnoseStatusModel.state = 'success';
       }
+    } else if (DiagnoseStatusModel.status === 'loading') {
+      const arr = _.map(DiagnoseStatusModel.list, 'status').filter((i) => i !== 'loading');
+      DiagnoseStatusModel.count = arr.length;
     }
   }, [DiagnoseStatusModel.status, DiagnoseStatusModel.list]);
 
@@ -1802,7 +1783,6 @@ const Status = observer((props: Props) => {
       product: [],
       device: [],
     };
-    DiagnoseStatusModel.count = 0;
     DiagnoseStatusModel.status = 'loading';
     DiagnoseStatusModel.percent = 0;
     let arr: any[] = [];
@@ -2022,17 +2002,94 @@ const Status = observer((props: Props) => {
           onCancel={() => {
             setBindParentVisible(false);
           }}
-          onOk={(parentId: string) => {
-            DiagnoseStatusModel.list = modifyArrayList(DiagnoseStatusModel.list, {
-              key: 'parent-device',
-              name: '网关父设备',
-              desc: '诊断网关父设备状态是否正常,禁用或离线将导致连接失败',
-              status: 'success',
-              text: '正常',
-              info: null,
-            });
-            InstanceModel.detail.parentId = parentId;
-            setBindParentVisible(false);
+          onOk={async (parentId: string) => {
+            let item: ListProps | undefined = undefined;
+            const response = await service.detail(parentId);
+            if (response.status === 200) {
+              if (response?.result?.state?.value === 'notActive') {
+                item = {
+                  key: 'parent-device',
+                  name: '网关父设备',
+                  desc: '诊断网关父设备状态是否正常,禁用或离线将导致连接失败',
+                  status: 'error',
+                  text: '异常',
+                  info: (
+                    <div>
+                      <div className={styles.infoItem}>
+                        <Badge
+                          status="default"
+                          text={
+                            productPermission.action ? (
+                              <span>
+                                网关父设备已禁用,请先
+                                <Popconfirm
+                                  title="确认启用"
+                                  onConfirm={async () => {
+                                    const resp = await service.deployDevice(
+                                      response?.result?.id || '',
+                                    );
+                                    if (resp.status === 200) {
+                                      onlyMessage('操作成功!');
+                                      DiagnoseStatusModel.list = modifyArrayList(
+                                        DiagnoseStatusModel.list,
+                                        {
+                                          key: 'parent-device',
+                                          name: '网关父设备',
+                                          desc: '诊断网关父设备状态是否正常,禁用或离线将导致连接失败',
+                                          status: 'success',
+                                          text: '正常',
+                                          info: null,
+                                        },
+                                      );
+                                    }
+                                  }}
+                                >
+                                  <a>启用</a>
+                                </Popconfirm>
+                              </span>
+                            ) : (
+                              '暂无权限,请联系管理员处理'
+                            )
+                          }
+                        />
+                      </div>
+                    </div>
+                  ),
+                };
+              } else if (response?.state?.value === 'online') {
+                item = {
+                  key: 'parent-device',
+                  name: '网关父设备',
+                  desc: '诊断网关父设备状态是否正常,禁用或离线将导致连接失败',
+                  status: 'success',
+                  text: '正常',
+                  info: null,
+                };
+              } else {
+                item = {
+                  key: 'parent-device',
+                  name: '网关父设备',
+                  desc: '诊断网关父设备状态是否正常,禁用或离线将导致连接失败',
+                  status: 'error',
+                  text: '异常',
+                  info: (
+                    <div>
+                      <div className={styles.infoItem}>
+                        <Badge
+                          status="default"
+                          text={<span>网关父设备已离线,请先排查网关设备故障</span>}
+                        />
+                      </div>
+                    </div>
+                  ),
+                };
+              }
+              if (item) {
+                DiagnoseStatusModel.list = modifyArrayList(DiagnoseStatusModel.list, item);
+              }
+              InstanceModel.detail.parentId = parentId;
+              setBindParentVisible(false);
+            }
           }}
         />
       )}

+ 23 - 9
src/pages/device/Instance/index.tsx

@@ -558,6 +558,7 @@ const Instance = () => {
         actionRef={actionRef}
         params={searchParams}
         options={{ fullScreen: true }}
+        columnEmptyText={''}
         request={(params) =>
           service.query({
             ...params,
@@ -644,17 +645,30 @@ const Instance = () => {
                   }),
                   onConfirm: async () => {
                     if (record.state.value !== 'notActive') {
-                      await service.undeployDevice(record.id);
+                      await service.undeployDevice(record.id).then((res) => {
+                        if (res.status === 200) {
+                          onlyMessage(
+                            intl.formatMessage({
+                              id: 'pages.data.option.success',
+                              defaultMessage: '操作成功!',
+                            }),
+                          );
+                          actionRef.current?.reload();
+                        }
+                      });
                     } else {
-                      await service.deployDevice(record.id);
+                      await service.deployDevice(record.id).then((res) => {
+                        if (res.status === 200) {
+                          onlyMessage(
+                            intl.formatMessage({
+                              id: 'pages.data.option.success',
+                              defaultMessage: '操作成功!',
+                            }),
+                          );
+                          actionRef.current?.reload();
+                        }
+                      });
                     }
-                    onlyMessage(
-                      intl.formatMessage({
-                        id: 'pages.data.option.success',
-                        defaultMessage: '操作成功!',
-                      }),
-                    );
-                    actionRef.current?.reload();
                   },
                 }}
               >

+ 1 - 0
src/pages/device/Product/index.tsx

@@ -422,6 +422,7 @@ const Product = observer(() => {
       <ProTableCard<ProductItem>
         columns={columns}
         actionRef={actionRef}
+        columnEmptyText={''}
         options={{ fullScreen: true }}
         scroll={{ x: 1366 }}
         // request={async (params = {}) => {

+ 143 - 102
src/pages/device/components/Metadata/Base/Edit/index.tsx

@@ -50,6 +50,7 @@ import DB from '@/db';
 import _ from 'lodash';
 import { InstanceModel } from '@/pages/device/Instance';
 import FRuleEditor from '@/components/FRuleEditor';
+import FIndicators from '@/components/FIndicators';
 import { action } from '@formily/reactive';
 import { asyncUpdateMedata, updateMetadata } from '../../metadata';
 import { onlyMessage } from '@/utils/util';
@@ -160,6 +161,7 @@ const Edit = observer((props: Props) => {
       Checkbox,
       FormGrid,
       DatePicker,
+      FIndicators,
     },
     scope: {
       async asyncOtherConfig(field: Field) {
@@ -778,121 +780,160 @@ const Edit = observer((props: Props) => {
                         },
                       ],
                     },
-                    space: {
-                      type: 'void',
+                    '{value, range}': {
                       title: '指标值',
                       'x-decorator': 'FormItem',
-                      'x-component': 'FormGrid',
+                      'x-component': 'FIndicators',
                       'x-decorator-props': {
                         labelAlign: 'left',
                         layout: 'vertical',
                       },
+                      'x-reactions': {
+                        dependencies: ['valueType.type'],
+                        fulfill: {
+                          state: {
+                            componentProps: {
+                              type: '{{$deps[0]}}',
+                            },
+                          },
+                        },
+                      },
                       'x-validator': [
                         {
                           required: true,
                           message: '请输入指标值',
                         },
-                      ],
-                      'x-component-props': {
-                        maxColumns: 12,
-                        minColumns: 12,
-                      },
-                      properties: {
-                        'value[0]': {
-                          'x-decorator': 'FormItem',
-                          'x-component': 'Input',
-                          'x-decorator-props': {
-                            gridSpan: 5,
-                          },
-                          'x-validator': [
-                            {
-                              required: true,
-                              message: '请输入',
-                            },
-                          ],
-                          'x-reactions': {
-                            dependencies: ['..range', 'valueType.type'],
-                            fulfill: {
-                              state: {
-                                decoratorProps: {
-                                  gridSpan: '{{!!$deps[0]?5:$deps[1]==="boolean"?12:10}}',
-                                },
-                                componentType:
-                                  '{{["int","long","double","float"].includes($deps[1])?"NumberPicker":["date"].includes($deps[1])?"DatePicker":"Input"}}',
-                              },
-                            },
-                          },
-                          // 根据数据类型来渲染不同的组件
-                        },
-                        'value[1]': {
-                          title: '~',
-                          'x-decorator': 'FormItem',
-                          'x-component': 'Input',
-                          'x-decorator-props': {
-                            gridSpan: 5,
-                          },
-                          'x-validator': [
-                            {
-                              required: true,
-                              message: '请输入',
-                            },
-                          ],
-                          'x-reactions': [
-                            {
-                              dependencies: ['..range', 'valueType.type'],
-                              fulfill: {
-                                state: {
-                                  visible: '{{!!$deps[0]}}',
-                                  componentType:
-                                    '{{["int","long","double","float"].includes($deps[1])?"NumberPicker":["date"].includes($deps[1])?"DatePicker":"Input"}}',
-                                },
-                              },
-                            },
-                            {
-                              dependencies: ['valueType.type'],
-                              fulfill: {
-                                state: {
-                                  visible: '{{!$deps[0]==="boolean"}}',
-                                },
-                              },
-                            },
-                          ],
-                        },
-                        // 根据数据类型来渲染不同的组件
-                        range: {
-                          type: 'boolean',
-                          default: false,
-                          'x-decorator': 'FormItem',
-                          'x-component': 'Checkbox',
-                          'x-component-props': {
-                            children: '范围',
-                          },
-                          'x-decorator-props': {
-                            gridSpan: 2,
-                          },
-                          'x-reactions': {
-                            dependencies: ['valueType.type'],
-                            when: '{{$deps[0]==="boolean"}}',
-                            fulfill: {
-                              state: {
-                                visible: false,
-                                decoratorProps: {
-                                  gridSpan: 0,
-                                },
-                              },
-                            },
-                            otherwise: {
-                              state: {
-                                visible: true,
-                                decoratorProps: {
-                                  gridSpan: 2,
-                                },
-                              },
-                            },
+                        {
+                          validator: (value: any) => {
+                            if (value?.range) {
+                              if (!value?.value || !value?.value[0] || !value?.value[1]) {
+                                return Promise.reject(new Error('请输入指标值'));
+                              }
+                            } else {
+                              if (value?.value && !value?.value[0]) {
+                                return Promise.reject(new Error('请输入指标值'));
+                              }
+                            }
+                            return Promise.resolve();
                           },
                         },
-                      },
+                      ],
                     },
+                    // space: {
+                    //   type: 'void',
+                    //   title: '指标值',
+                    //   'x-decorator': 'FormItem',
+                    //   'x-component': 'FormGrid',
+                    //   'x-decorator-props': {
+                    //     labelAlign: 'left',
+                    //     layout: 'vertical',
+                    //   },
+                    //   'x-validator': [
+                    //     {
+                    //       required: true,
+                    //       message: '请输入指标值',
+                    //     },
+                    //   ],
+                    //   'x-component-props': {
+                    //     maxColumns: 12,
+                    //     minColumns: 12,
+                    //   },
+                    //   properties: {
+                    //     'value[0]': {
+                    //       'x-decorator': 'FormItem',
+                    //       'x-component': 'Input',
+                    //       'x-decorator-props': {
+                    //         gridSpan: 5,
+                    //       },
+                    //       'x-validator': [
+                    //         {
+                    //           required: true,
+                    //           message: '请输入',
+                    //         },
+                    //       ],
+                    //       'x-reactions': {
+                    //         dependencies: ['..range', 'valueType.type'],
+                    //         fulfill: {
+                    //           state: {
+                    //             decoratorProps: {
+                    //               gridSpan: '{{!!$deps[0]?5:$deps[1]==="boolean"?12:10}}',
+                    //             },
+                    //             componentType:
+                    //               '{{["int","long","double","float"].includes($deps[1])?"NumberPicker":["date"].includes($deps[1])?"DatePicker":"Input"}}',
+                    //           },
+                    //         },
+                    //       },
+                    //       // 根据数据类型来渲染不同的组件
+                    //     },
+                    //     'value[1]': {
+                    //       title: '~',
+                    //       'x-decorator': 'FormItem',
+                    //       'x-component': 'Input',
+                    //       'x-decorator-props': {
+                    //         gridSpan: 5,
+                    //       },
+                    //       'x-validator': [
+                    //         {
+                    //           required: true,
+                    //           message: '请输入',
+                    //         },
+                    //       ],
+                    //       'x-reactions': [
+                    //         {
+                    //           dependencies: ['..range', 'valueType.type'],
+                    //           fulfill: {
+                    //             state: {
+                    //               visible: '{{!!$deps[0]}}',
+                    //               componentType:
+                    //                 '{{["int","long","double","float"].includes($deps[1])?"NumberPicker":["date"].includes($deps[1])?"DatePicker":"Input"}}',
+                    //             },
+                    //           },
+                    //         },
+                    //         {
+                    //           dependencies: ['valueType.type'],
+                    //           fulfill: {
+                    //             state: {
+                    //               visible: '{{!$deps[0]==="boolean"}}',
+                    //             },
+                    //           },
+                    //         },
+                    //       ],
+                    //     },
+                    //     // 根据数据类型来渲染不同的组件
+                    //     range: {
+                    //       type: 'boolean',
+                    //       default: false,
+                    //       'x-decorator': 'FormItem',
+                    //       'x-component': 'Checkbox',
+                    //       'x-component-props': {
+                    //         children: '范围',
+                    //       },
+                    //       'x-decorator-props': {
+                    //         gridSpan: 2,
+                    //       },
+                    //       'x-reactions': {
+                    //         dependencies: ['valueType.type'],
+                    //         when: '{{$deps[0]==="boolean"}}',
+                    //         fulfill: {
+                    //           state: {
+                    //             visible: false,
+                    //             decoratorProps: {
+                    //               gridSpan: 0,
+                    //             },
+                    //           },
+                    //         },
+                    //         otherwise: {
+                    //           state: {
+                    //             visible: true,
+                    //             decoratorProps: {
+                    //               gridSpan: 2,
+                    //             },
+                    //           },
+                    //         },
+                    //       },
+                    //     },
+                    //   },
+                    // },
                   },
                 },
                 right: {
@@ -1084,7 +1125,7 @@ const Edit = observer((props: Props) => {
     if (params?.id) {
       const result1 = await DB.getDB().table(`${type}`).where('id').equals(params.id).toArray();
 
-      if (result1.length > 0) {
+      if (result1.length > 0 && MetadataModel.action === 'add') {
         message.error('标识已存在');
         setLoading(false);
         return;

+ 42 - 0
src/pages/init-home/index.less

@@ -0,0 +1,42 @@
+.init {
+  width: 100%;
+  height: 100vh;
+  padding: 32px 128px 64px 128px;
+  overflow: hidden;
+  background-image: url('/images/init-home/background.png');
+  background-repeat: no-repeat;
+  background-size: 100% 100%;
+
+  .box {
+    width: 100%;
+    height: 100%;
+    padding: 24px;
+    background: white;
+
+    .container {
+      display: flex;
+      justify-content: space-between;
+      width: 100%;
+      height: 100%;
+      overflow-y: auto;
+
+      .left {
+        width: 30px;
+        height: 100%;
+      }
+
+      .right {
+        width: calc(100% - 50px);
+        height: 1200px;
+
+        // .collapseTitle {
+
+        // }
+
+        // .collapseDesc {
+
+        // }
+      }
+    }
+  }
+}

+ 46 - 0
src/pages/init-home/index.tsx

@@ -0,0 +1,46 @@
+import { TitleComponent } from '@/components';
+import { Collapse, Steps } from 'antd';
+import styles from './index.less';
+
+const InitHome = () => {
+  const text = `
+        A dog is a type of domesticated animal.
+        Known for its loyalty and faithfulness,
+        it can be found as a welcome guest in many households across the world.
+    `;
+  return (
+    <div className={styles.init}>
+      <TitleComponent data={'系统初始化'} />
+      <div className={styles.box}>
+        <div className={styles.container}>
+          <div className={styles.left}>
+            <Steps direction="vertical" current={1} percent={60} style={{ height: '100%' }}>
+              <Steps.Step />
+              <Steps.Step />
+              <Steps.Step />
+              <Steps.Step />
+            </Steps>
+          </div>
+          <div className={styles.right}>
+            <Collapse defaultActiveKey={['1', '2', '3', '4']}>
+              <Collapse.Panel header={<div>基本信息</div>} key="1">
+                <p>{text}</p>
+              </Collapse.Panel>
+              <Collapse.Panel header="This is panel header 2" key="2">
+                <p>{text}</p>
+              </Collapse.Panel>
+              <Collapse.Panel header="This is panel header 3" key="3">
+                <p>{text}</p>
+              </Collapse.Panel>
+              <Collapse.Panel header="This is panel header 3" key="4">
+                <p>{text}</p>
+              </Collapse.Panel>
+            </Collapse>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default InitHome;

+ 49 - 46
src/pages/link/Channel/channelCard.tsx

@@ -1,52 +1,55 @@
-import { Badge } from "antd";
-import classNames from "classnames";
-import { useState } from "react";
-import './index.less'
+import { Badge } from 'antd';
+import classNames from 'classnames';
+import { useState } from 'react';
+import './index.less';
 interface Props {
-    actions: any;
-    onClick: Function;
-    active: boolean;
-    data: any;
-    defaultActiveKey?: any;
+  actions: any;
+  onClick: Function;
+  active: boolean;
+  data: any;
+  defaultActiveKey?: any;
 }
 
 const ChannelCard = (props: Props) => {
-    const [actions, setActions] = useState<boolean>(false)
-    const connectImg = require('/public/images/channel/connect.png')
-    const disconnectImg = require('/public/images/channel/disconnect.png')
-
-    return (
-        <div
-            className={
-                classNames("channel-card",
-                    {
-                        active: props.active,
-                        connect: props.data.status === 'connect',
-                        disconnect: props.data.status === 'disconnect'
-                    }
-                )}
-            onMouseEnter={() => { setActions(true) }}
-            onMouseLeave={() => { setActions(false) }}
-        >
-            <div
-                className="channel-card-top"
-                onClick={() => { props.onClick() }}
-            >
-                <div className="card-top-img"> <img src={props.data.status === 'connect' ? connectImg : disconnectImg} /></div>
-                <div className="card-top-name">这里是通道名称</div>
-                <div className="card-top-status">
-                    {props.data.status === 'connect' ?
-                        <Badge status="processing" color={'green'} text={'正常'} /> :
-                        <Badge status="processing" color={'red'} text={'禁用'} />}
-                </div>
-            </div>
-            {
-                actions && <div className="channel-card-actions">
-                    {props.actions}
-                </div>
-            }
+  const [actions, setActions] = useState<boolean>(false);
+  const connectImg = require('/public/images/channel/connect.png');
+  const disconnectImg = require('/public/images/channel/disconnect.png');
 
+  return (
+    <div
+      className={classNames('channel-card', {
+        active: props.active,
+        connect: props.data.status === 'connect',
+        disconnect: props.data.status === 'disconnect',
+      })}
+      onMouseEnter={() => {
+        setActions(true);
+      }}
+      onMouseLeave={() => {
+        setActions(false);
+      }}
+    >
+      <div
+        className="channel-card-top"
+        onClick={() => {
+          props.onClick();
+        }}
+      >
+        <div className="card-top-img">
+          {' '}
+          <img src={props.data.status === 'connect' ? connectImg : disconnectImg} />
         </div>
-    )
-}
-export default ChannelCard;
+        <div className="card-top-name">这里是通道名称</div>
+        <div className="card-top-status">
+          {props.data.status === 'connect' ? (
+            <Badge status="processing" color={'green'} text={'正常'} />
+          ) : (
+            <Badge status="processing" color={'red'} text={'禁用'} />
+          )}
+        </div>
+      </div>
+      {actions && <div className="channel-card-actions">{props.actions}</div>}
+    </div>
+  );
+};
+export default ChannelCard;

+ 65 - 65
src/pages/link/Channel/index.less

@@ -1,81 +1,81 @@
 .item {
-    display: flex;
+  display: flex;
 
-    .item-left {
-        margin-right: 20px;
-        padding-right: 10px;
-        border-right: 1px #EEE solid;
-        .item-left-list{
-            height: 628px;
-            overflow: overlay;
-        }
-        ::-webkit-scrollbar{
-            // display:none
-            width: 2px;
-            height: 2px;
-            scrollbar-arrow-color: #eee;
-        }
-        ::-webkit-scrollbar-thumb {
-            border-radius: 5px;
-            background: rgb(136, 136, 136);
-        }
+  .item-left {
+    margin-right: 20px;
+    padding-right: 10px;
+    border-right: 1px #eee solid;
+    .item-left-list {
+      height: 628px;
+      overflow: overlay;
+    }
+    ::-webkit-scrollbar {
+      // display:none
+      width: 2px;
+      height: 2px;
+      scrollbar-arrow-color: #eee;
+    }
+    ::-webkit-scrollbar-thumb {
+      background: rgb(136, 136, 136);
+      border-radius: 5px;
     }
+  }
 
-    .item-right {
-        .ant-card-body {
-            padding-top: 0;
-            padding-left: 0;
-        }
+  .item-right {
+    .ant-card-body {
+      padding-top: 0;
+      padding-left: 0;
     }
+  }
 }
 
 .channel-card {
-    height: 112px;
-    width: 220px;
-    border: 1px #E0E0E0 solid;
-    margin-top: 16px;
+  width: 220px;
+  height: 112px;
+  margin-top: 16px;
+  border: 1px #e0e0e0 solid;
 
-    &.active {
-        &.connect {
-            background-color: #24b27614;
-            border: 1px solid #24B276;
-            border-radius: 2px;
-        }
-
-        &.disconnect {
-            background-color: rgba(255, 144, 0, 0.08);
-            border: 1px solid #FF9000;
-            border-radius: 2px;
-        }
+  &.active {
+    &.connect {
+      background-color: #24b27614;
+      border: 1px solid #24b276;
+      border-radius: 2px;
     }
 
-    .channel-card-top {
-        .card-top-img {
-            text-align: end;
-        }
+    &.disconnect {
+      background-color: rgba(255, 144, 0, 0.08);
+      border: 1px solid #ff9000;
+      border-radius: 2px;
+    }
+  }
 
-        .card-top-name {
-            color: #323130;
-            font-size: 16px;
-            font-weight: 600;
-            margin-left: 16px;
-            line-height: 22px;
-            position: relative;
-            bottom: 8px;
-        }
+  .channel-card-top {
+    .card-top-img {
+      text-align: end;
+    }
 
-        .card-top-status {
-            margin-left: 16px;
-            margin-bottom: 8px
-        }
+    .card-top-name {
+      position: relative;
+      bottom: 8px;
+      margin-left: 16px;
+      color: #323130;
+      font-weight: 600;
+      font-size: 16px;
+      line-height: 22px;
     }
 
-    .channel-card-actions {
-        // height: 40px;
-        background-color: #F6F6F6;
-        display: flex;
-        justify-content: space-evenly;
-        align-items: center;
-        border-top: 1px #eee solid;
+    .card-top-status {
+      margin-bottom: 8px;
+      margin-left: 16px;
     }
-}
+  }
+
+  .channel-card-actions {
+    display: flex;
+    align-items: center;
+    justify-content: space-evenly;
+    // height: 40px;
+    background-color: #f6f6f6;
+    border-top: 1px #eee solid;
+  }
+}

+ 1 - 0
src/pages/link/Protocol/index.tsx

@@ -199,6 +199,7 @@ const Protocol = () => {
         actionRef={actionRef}
         scroll={{ x: 1366 }}
         params={searchParams}
+        columnEmptyText={''}
         options={{ fullScreen: true }}
         request={(params) =>
           service.query({

+ 1 - 0
src/pages/link/Type/index.tsx

@@ -254,6 +254,7 @@ const Network = () => {
         columns={columns}
         scroll={{ x: 1366 }}
         search={false}
+        columnEmptyText={''}
         headerTitle={
           <PermissionButton
             isPermission={networkPermission.add}

+ 1 - 0
src/pages/media/Cascade/index.tsx

@@ -326,6 +326,7 @@ const Cascade = () => {
         actionRef={actionRef}
         params={searchParams}
         scroll={{ x: 1366 }}
+        columnEmptyText={''}
         options={{ fullScreen: true }}
         request={async (params = {}) => {
           return await lastValueFrom(

+ 1 - 0
src/pages/media/Device/index.tsx

@@ -318,6 +318,7 @@ const Device = () => {
         actionRef={actionRef}
         options={{ fullScreen: true }}
         params={queryParam}
+        columnEmptyText={''}
         scroll={{ x: 1366 }}
         request={(params = {}) =>
           service.query({

+ 1 - 0
src/pages/notice/Config/index.tsx

@@ -263,6 +263,7 @@ const Config = observer(() => {
         actionRef={actionRef}
         search={false}
         params={param}
+        columnEmptyText={''}
         columns={columns}
         scroll={{ x: 1366 }}
         headerTitle={

+ 1 - 0
src/pages/notice/Template/index.tsx

@@ -229,6 +229,7 @@ const Template = observer(() => {
         search={false}
         params={param}
         columns={columns}
+        columnEmptyText={''}
         headerTitle={
           <Space>
             <PermissionButton

+ 118 - 0
src/pages/rule-engine/Alarm/Config/Save/input.tsx

@@ -0,0 +1,118 @@
+import { Modal } from 'antd';
+import type { FirmwareItem } from '@/pages/device/Firmware/typings';
+import { createSchemaField } from '@formily/react';
+import { Form, FormGrid, FormItem, Input, Switch } from '@formily/antd';
+import { createForm, onFormInit } from '@formily/core';
+import type { ISchema } from '@formily/json-schema';
+import { service } from '@/pages/rule-engine/Alarm/Config';
+import { onlyMessage } from '@/utils/util';
+import type { IOConfigItem } from '../typing';
+
+interface Props {
+  data?: FirmwareItem;
+  close: () => void;
+}
+
+const InputSave = (props: Props) => {
+  const { data, close } = props;
+
+  const form = createForm({
+    validateFirst: true,
+    initialValues: data,
+    effects() {
+      onFormInit(async (f) => {
+        const resp = await service.getDataExchange('consume');
+        if (resp.status === 200) {
+          f.setInitialValues(resp.result?.config.config);
+          f.setValuesIn('id', resp.result?.id);
+          f.setValuesIn('state', resp.result?.state?.value === 'enabled' ? true : false);
+        }
+      });
+    },
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      FormGrid,
+      Input,
+      Switch,
+    },
+  });
+
+  const save = async () => {
+    form.validate();
+    const inputConfig: IOConfigItem = await form.submit();
+    const res = await service.saveOutputData({
+      config: {
+        sourceType: 'kafka',
+        config: inputConfig,
+      },
+      id: inputConfig.id,
+      sourceType: 'kafka',
+      exchangeType: 'consume',
+    });
+
+    if (res.status === 200) {
+      onlyMessage('操作成功');
+    }
+  };
+
+  const inputSchema: ISchema = {
+    type: 'object',
+    properties: {
+      id: {
+        'x-component': 'Input',
+        'x-hidden': true,
+      },
+      address: {
+        title: 'kafka地址',
+        type: 'string',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-component-props': {
+          placeholder: '请输入kafka地址',
+        },
+      },
+      topic: {
+        title: 'topic',
+        type: 'string',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-component-props': {
+          placeholder: '请输入topic',
+        },
+      },
+      state: {
+        title: '状态',
+        type: 'string',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Switch',
+        default: false,
+        'x-component-props': {
+          checkedChildren: '禁用',
+          unCheckedChildren: '启用',
+        },
+      },
+    },
+  };
+
+  return (
+    <Modal
+      maskClosable={false}
+      width="45vw"
+      title="编辑"
+      onCancel={() => close()}
+      onOk={() => save()}
+      visible
+    >
+      <Form form={form} labelCol={5} wrapperCol={16} layout="vertical">
+        <SchemaField schema={inputSchema} />
+      </Form>
+    </Modal>
+  );
+};
+export default InputSave;

+ 155 - 0
src/pages/rule-engine/Alarm/Config/Save/output.tsx

@@ -0,0 +1,155 @@
+import { Modal } from 'antd';
+import type { FirmwareItem } from '@/pages/device/Firmware/typings';
+import { createSchemaField } from '@formily/react';
+import { Form, FormGrid, FormItem, Input, Switch } from '@formily/antd';
+import { createForm, onFormInit } from '@formily/core';
+import type { ISchema } from '@formily/json-schema';
+import { service } from '@/pages/rule-engine/Alarm/Config';
+import { onlyMessage } from '@/utils/util';
+import type { IOConfigItem } from '../typing';
+
+interface Props {
+  data?: FirmwareItem;
+  close: () => void;
+}
+
+const OutputSave = (props: Props) => {
+  const { data, close } = props;
+
+  const form = createForm({
+    validateFirst: true,
+    initialValues: data,
+    effects() {
+      onFormInit(async (f) => {
+        const resp = await service.getDataExchange('producer');
+        if (resp.status === 200) {
+          f.setInitialValues(resp.result?.config.config);
+          f.setValuesIn('id', resp.result?.id);
+          f.setValuesIn('state', resp.result?.state?.value === 'enabled' ? true : false);
+        }
+      });
+    },
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      FormGrid,
+      Input,
+      Switch,
+    },
+  });
+
+  const save = async () => {
+    form.validate();
+    const inputConfig: IOConfigItem = await form.submit();
+    const res = await service.saveOutputData({
+      config: {
+        sourceType: 'kafka',
+        config: inputConfig,
+      },
+      id: inputConfig.id,
+      sourceType: 'kafka',
+      exchangeType: 'consume',
+    });
+
+    if (res.status === 200) {
+      onlyMessage('操作成功');
+    }
+  };
+
+  const outputSchema: ISchema = {
+    type: 'object',
+    properties: {
+      id: {
+        'x-component': 'Input',
+        'x-hidden': true,
+      },
+      address: {
+        title: 'kafka地址',
+        type: 'string',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-component-props': {
+          placeholder: '请输入kafka地址',
+        },
+      },
+      topic: {
+        title: 'topic',
+        type: 'string',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-component-props': {
+          placeholder: '请输入topic',
+        },
+      },
+      state: {
+        title: '状态',
+        type: 'string',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Switch',
+        default: false,
+        'x-component-props': {
+          checkedChildren: '禁用',
+          unCheckedChildren: '启用',
+        },
+      },
+      // layout2: {
+      //   type: 'void',
+      //   'x-decorator': 'FormGrid',
+      //   'x-decorator-props': {
+      //     maxColumns: 2,
+      //     minColumns: 2,
+      //     columnGap: 24,
+      //   },
+      //   properties: {
+      //     username: {
+      //       title: '用户名',
+      //       type: 'string',
+      //       // required: true,
+      //       'x-decorator': 'FormItem',
+      //       'x-component': 'Input',
+      //       'x-component-props': {
+      //         placeholder: '请输入用户名',
+      //       },
+      //       'x-decorator-props': {
+      //         gridSpan: 1,
+      //       },
+      //     },
+      //     password: {
+      //       title: '密码',
+      //       type: 'string',
+      //       // required: true,
+      //       'x-decorator': 'FormItem',
+      //       'x-component': 'Input',
+      //       'x-decorator-props': {
+      //         gridSpan: 1,
+      //       },
+      //       'x-component-props': {
+      //         placeholder: '请输入密码',
+      //       },
+      //     },
+      //   },
+      // },
+    },
+  };
+
+  return (
+    <Modal
+      maskClosable={false}
+      width="50vw"
+      title="编辑"
+      onCancel={() => close()}
+      onOk={() => save()}
+      visible
+    >
+      <Form form={form} labelCol={5} wrapperCol={16} layout="vertical">
+        <SchemaField schema={outputSchema} />
+      </Form>
+    </Modal>
+  );
+};
+export default OutputSave;

+ 77 - 175
src/pages/rule-engine/Alarm/Config/index.tsx

@@ -1,23 +1,29 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import { Button, Card, Col, Divider, Image, Row, Table, Tooltip } from 'antd';
+import { Badge, Button, Card, Col, Descriptions, Divider, Image, Row, Table, Tooltip } from 'antd';
 import TitleComponent from '@/components/TitleComponent';
 import { createSchemaField } from '@formily/react';
 import { ArrayItems, Form, FormButtonGroup, FormGrid, FormItem, Input } from '@formily/antd';
 import type { ISchema } from '@formily/json-schema';
-import { useMemo, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
 import { createForm, onFormInit } from '@formily/core';
 import FLevelInput from '@/components/FLevelInput';
-import type { IOConfigItem } from '@/pages/rule-engine/Alarm/Config/typing';
 import Service from '@/pages/rule-engine/Alarm/Config/service';
 import styles from './index.less';
 import ReactMarkdown from 'react-markdown';
-import { QuestionCircleOutlined } from '@ant-design/icons';
+import { EditOutlined, QuestionCircleOutlined } from '@ant-design/icons';
 import { onlyMessage } from '@/utils/util';
+import OutputSave from './Save/output';
+import InputSave from './Save/input';
 
 export const service = new Service('alarm/config');
 const ioImg = require('/public/images/alarm/io.png');
 const Config = () => {
   const [tab, setTab] = useState<'io' | 'config' | string>('config');
+  const [inputVisible, setInputVisible] = useState<boolean>(false);
+  const [outputVisible, setOutputVisible] = useState<boolean>(false);
+  const [input, setInput] = useState<any>({});
+  const [output, setOutput] = useState<any>({});
+
   const outputData = [
     {
       key: 'alarmName',
@@ -183,39 +189,6 @@ const Config = () => {
     [],
   );
 
-  const inputForm = useMemo(
-    () =>
-      createForm({
-        validateFirst: true,
-        effects() {
-          onFormInit(async (f) => {
-            const resp = await service.getDataExchange('consume');
-            if (resp.status === 200) {
-              f.setInitialValues(resp.result?.config.config);
-              f.setValuesIn('id', resp.result?.id);
-            }
-          });
-        },
-      }),
-    [],
-  );
-  const outputForm = useMemo(
-    () =>
-      createForm({
-        validateFirst: true,
-        effects() {
-          onFormInit(async (f) => {
-            const resp = await service.getDataExchange('producer');
-            if (resp.status === 200) {
-              f.setInitialValues(resp.result?.config.config);
-              f.setValuesIn('id', resp.result?.id);
-            }
-          });
-        },
-      }),
-    [],
-  );
-
   const levelSchema: ISchema = {
     type: 'object',
     properties: {
@@ -266,131 +239,6 @@ const Config = () => {
     },
   };
 
-  const outputSchema: ISchema = {
-    type: 'object',
-    properties: {
-      id: {
-        'x-component': 'Input',
-        'x-hidden': true,
-      },
-      address: {
-        title: 'kafka地址',
-        type: 'string',
-        required: true,
-        'x-decorator': 'FormItem',
-        'x-component': 'Input',
-        'x-component-props': {
-          placeholder: '请输入kafka地址',
-        },
-      },
-      topic: {
-        title: 'topic',
-        type: 'string',
-        required: true,
-        'x-decorator': 'FormItem',
-        'x-component': 'Input',
-        'x-component-props': {
-          placeholder: '请输入topic',
-        },
-      },
-      // layout2: {
-      //   type: 'void',
-      //   'x-decorator': 'FormGrid',
-      //   'x-decorator-props': {
-      //     maxColumns: 2,
-      //     minColumns: 2,
-      //     columnGap: 24,
-      //   },
-      //   properties: {
-      //     username: {
-      //       title: '用户名',
-      //       type: 'string',
-      //       // required: true,
-      //       'x-decorator': 'FormItem',
-      //       'x-component': 'Input',
-      //       'x-component-props': {
-      //         placeholder: '请输入用户名',
-      //       },
-      //       'x-decorator-props': {
-      //         gridSpan: 1,
-      //       },
-      //     },
-      //     password: {
-      //       title: '密码',
-      //       type: 'string',
-      //       // required: true,
-      //       'x-decorator': 'FormItem',
-      //       'x-component': 'Input',
-      //       'x-decorator-props': {
-      //         gridSpan: 1,
-      //       },
-      //       'x-component-props': {
-      //         placeholder: '请输入密码',
-      //       },
-      //     },
-      //   },
-      // },
-    },
-  };
-
-  const inputSchema: ISchema = {
-    type: 'object',
-    properties: {
-      id: {
-        'x-component': 'Input',
-        'x-hidden': true,
-      },
-      address: {
-        title: 'kafka地址',
-        type: 'string',
-        required: true,
-        'x-decorator': 'FormItem',
-        'x-component': 'Input',
-        'x-component-props': {
-          placeholder: '请输入kafka地址',
-        },
-      },
-      topic: {
-        title: 'topic',
-        type: 'string',
-        required: true,
-        'x-decorator': 'FormItem',
-        'x-component': 'Input',
-        'x-component-props': {
-          placeholder: '请输入topic',
-        },
-      },
-    },
-  };
-
-  const handleSaveIO = async () => {
-    outputForm.validate();
-    inputForm.validate();
-    const inputConfig: IOConfigItem = await inputForm.submit();
-    const outputConfig: IOConfigItem = await outputForm.submit();
-    const inputResp = await service.saveOutputData({
-      config: {
-        config: outputConfig,
-      },
-      id: outputConfig.id,
-      sourceType: 'kafka',
-      exchangeType: 'producer',
-    });
-    const outputResp = await service.saveOutputData({
-      config: {
-        sourceType: 'kafka',
-        config: inputConfig,
-      },
-      id: inputConfig.id,
-      sourceType: 'kafka',
-      exchangeType: 'consume',
-    });
-
-    if (inputResp.status === 200 && outputResp.status === 200) {
-      onlyMessage('操作成功');
-    }
-  };
-
   const handleSaveLevel = async () => {
     const values: { level: string[] } = await levelForm.submit();
     const _level = values?.level.map((l: string, i: number) => ({ level: i + 1, title: l }));
@@ -400,6 +248,19 @@ const Config = () => {
     }
   };
 
+  useEffect(() => {
+    service.getDataExchange('consume').then((resp) => {
+      if (resp.status === 200) {
+        setInput(resp.result);
+      }
+    });
+    service.getDataExchange('producer').then((resp) => {
+      if (resp.status === 200) {
+        setOutput(resp.result);
+      }
+    });
+  }, []);
+
   const outputText = `
   ~~~json
   {
@@ -469,12 +330,29 @@ const Config = () => {
                   <Tooltip title={'将告警数据输出到其他第三方系统'}>
                     <QuestionCircleOutlined style={{ marginLeft: 5 }} />
                   </Tooltip>
+                  <a
+                    style={{ marginLeft: 10 }}
+                    onClick={() => {
+                      setOutputVisible(true);
+                    }}
+                  >
+                    <EditOutlined />
+                  </a>
                 </span>
               }
             />
-            <Form form={outputForm} layout="vertical">
-              <SchemaField schema={outputSchema} />
-            </Form>
+            <Descriptions bordered column={2}>
+              <Descriptions.Item label="kafka地址">
+                {output?.config?.config?.kafka || ''}
+              </Descriptions.Item>
+              <Descriptions.Item label="topic">
+                {output?.config?.config?.topic || ''}
+              </Descriptions.Item>
+              <Descriptions.Item label="状态" span={2}>
+                <Badge status={output?.state?.value === 'enabled' ? 'success' : 'error'} />
+                {output?.state?.text || ''}
+              </Descriptions.Item>
+            </Descriptions>
             <Divider />
             <TitleComponent
               data={
@@ -483,19 +361,29 @@ const Config = () => {
                   <Tooltip title={'接收第三方系统处理的告警结果'}>
                     <QuestionCircleOutlined style={{ marginLeft: 5 }} />
                   </Tooltip>
+                  <a
+                    style={{ marginLeft: 10 }}
+                    onClick={() => {
+                      setInputVisible(true);
+                    }}
+                  >
+                    <EditOutlined />
+                  </a>
                 </span>
               }
             />
-            <Form form={inputForm} layout="vertical">
-              <SchemaField schema={inputSchema} />
-              <FormButtonGroup.Sticky>
-                <FormButtonGroup.FormItem>
-                  <Button type="primary" onClick={handleSaveIO}>
-                    保存
-                  </Button>
-                </FormButtonGroup.FormItem>
-              </FormButtonGroup.Sticky>
-            </Form>
+            <Descriptions bordered column={2}>
+              <Descriptions.Item label="kafka地址">
+                {input?.config?.config?.kafka || ''}
+              </Descriptions.Item>
+              <Descriptions.Item label="topic">
+                {input?.config?.config?.topic || ''}
+              </Descriptions.Item>
+              <Descriptions.Item label="状态" span={2}>
+                <Badge status={input?.state?.value === 'enabled' ? 'success' : 'error'} />
+                {input?.state?.text || ''}
+              </Descriptions.Item>
+            </Descriptions>
           </Card>
         </div>
       </Col>
@@ -541,6 +429,20 @@ const Config = () => {
   return (
     <PageContainer onTabChange={setTab} tabActiveKey={tab} tabList={list}>
       {list.find((k) => k.key === tab)?.component}
+      {inputVisible && (
+        <InputSave
+          close={() => {
+            setInputVisible(false);
+          }}
+        />
+      )}
+      {outputVisible && (
+        <OutputSave
+          close={() => {
+            setOutputVisible(false);
+          }}
+        />
+      )}
     </PageContainer>
   );
 };

+ 1 - 0
src/pages/rule-engine/Alarm/Configuration/index.tsx

@@ -276,6 +276,7 @@ const Configuration = () => {
         scroll={{ x: 1366 }}
         params={param}
         columns={columns}
+        columnEmptyText={''}
         request={(params) =>
           service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
         }

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

@@ -278,6 +278,7 @@ const Instance = () => {
         actionRef={actionRef}
         params={searchParams}
         scroll={{ x: 1366 }}
+        columnEmptyText={''}
         options={{ fullScreen: true }}
         request={(params) =>
           service.query({

+ 1 - 0
src/pages/rule-engine/Scene/index.tsx

@@ -263,6 +263,7 @@ const Scene = () => {
         actionRef={actionRef}
         scroll={{ x: 1366 }}
         params={searchParams}
+        columnEmptyText={''}
         options={{ fullScreen: true }}
         request={(params) =>
           service.query({

+ 141 - 141
src/pages/system/Basis/index.tsx

@@ -1,5 +1,5 @@
 import { UploadImage } from '@/components';
-import { Card, Col, Form, Input, Row, Select, } from 'antd';
+import { Card, Col, Form, Input, Row, Select } from 'antd';
 import Service from './service';
 import { useModel } from '@@/plugin-model/useModel';
 import usePermissions from '@/hooks/permission';
@@ -11,149 +11,149 @@ import { useDomFullHeight } from '@/hooks';
 import { PageContainer } from '@ant-design/pro-layout';
 
 const Basis = () => {
-    const { initialState, setInitialState } = useModel('@@initialState');
-    const { permission: userPermission } = usePermissions('system/Basis');
-    const [form] = Form.useForm();
-    const service = new Service();
-    const { minHeight } = useDomFullHeight(`.basis`);
+  const { initialState, setInitialState } = useModel('@@initialState');
+  const { permission: userPermission } = usePermissions('system/Basis');
+  const [form] = Form.useForm();
+  const service = new Service();
+  const { minHeight } = useDomFullHeight(`.basis`);
 
-
-    const detail = async (data: any) => {
-        const res = await service.detail(data);
-        if (res.status === 200) {
-            const basis = res.result?.filter((item: any) => item.scope === 'basis');
-            const api = res?.result.filter((item: any) => item.scope === 'api');
-            localStorage.setItem(SystemConst.AMAP_KEY, api[0].properties.api);
-            form.setFieldsValue({
-                ...basis[0].properties,
-                apikey: api[0].properties.api,
-            });
-            setInitialState({
-                ...initialState,
-                settings: {
-                    ...basis[0].properties,
-                },
-            });
-        }
-    };
-    const save = async () => {
-        const formData = await form.validateFields();
-        console.log(formData)
-        if (formData) {
-            const item = [
-                {
-                    scope: 'basis',
-                    properties: {
-                        ...formData,
-                        apikey: ''
-                    },
-                },
-                {
-                    scope: 'api',
-                    properties: {
-                        api: formData.apikey,
-                    },
-                },
-            ];
-            const res = await service.save(item);
-            if (res.status === 200) {
-                onlyMessage('保存成功');
-                detail(['basis', 'api']);
-            }
-        }
-    };
-
-    useEffect(() => {
+  const detail = async (data: any) => {
+    const res = await service.detail(data);
+    if (res.status === 200) {
+      const basis = res.result?.filter((item: any) => item.scope === 'basis');
+      const api = res?.result.filter((item: any) => item.scope === 'api');
+      localStorage.setItem(SystemConst.AMAP_KEY, api[0].properties.api);
+      form.setFieldsValue({
+        ...basis[0].properties,
+        apikey: api[0].properties.api,
+      });
+      setInitialState({
+        ...initialState,
+        settings: {
+          ...basis[0].properties,
+        },
+      });
+    }
+  };
+  const save = async () => {
+    const formData = await form.validateFields();
+    console.log(formData);
+    if (formData) {
+      const item = [
+        {
+          scope: 'basis',
+          properties: {
+            ...formData,
+            apikey: '',
+          },
+        },
+        {
+          scope: 'api',
+          properties: {
+            api: formData.apikey,
+          },
+        },
+      ];
+      const res = await service.save(item);
+      if (res.status === 200) {
+        onlyMessage('保存成功');
         detail(['basis', 'api']);
-    }, []);
-    return (
-        <PageContainer>
-            <Card className="basis" style={{ minHeight }}>
-                <Form layout="vertical" form={form}>
-                    <Row gutter={[24, 24]}>
-                        <Col span={10}>
-                            <Form.Item
-                                label="系统名称"
-                                name="title"
-                            >
-                                <Input />
-                            </Form.Item>
-                            <Form.Item
-                                label="主题色"
-                                name="headerTheme"
-                                initialValue="light"
-                                rules={[{ required: true, message: '请选择主题色' }]}
-                            >
-                                <Select>
-                                    <Select.Option value="light">白色</Select.Option>
-                                    <Select.Option value="dark">黑色</Select.Option>
-                                </Select>
-                            </Form.Item>
-                            <Form.Item
-                                label="高德API Key"
-                                name="apikey"
-                                tooltip="配置后平台可调用高德地图GIS服务"
-                            >
-                                <Input />
-                            </Form.Item>
-                            <Row gutter={[24, 24]}>
-                                <Col>
-                                    <Form.Item name={'logo'} label="系统logo" extra={
-                                            <>
-                                            <div>推荐尺寸200*200</div>
-                                            <div>支持jpg,png</div>
-                                            </>
-                                        }>
-                                        <UploadImage />
-                                    </Form.Item>
-                                </Col>
-                                <Col>
-                                    <Form.Item
-                                        name={'ico'}
-                                        label="浏览器页签"
-                                        tooltip='浏览器tab页中显示的图片元素'
-                                        extra={
-                                            <>
-                                            <div>推荐尺寸64*64</div>
-                                            <div>支持ico格式</div>
-                                            </>
-                                        }
-                                    >
-                                        <UploadImage size={1} types={['image/x-icon']} backgroundSize={'inherit'} />
-                                    </Form.Item>
-                                </Col>
-                            </Row>
-                        </Col>
-                        <Col span={14}>
-                            <Form.Item
-                                name={'backgroud'}
-                                label="登录背景图"
-                                extra={
-                                    <>
-                                    <div>支持4M以内的图片:支持jpg、png</div>
-                                    <div>建议尺寸1400x1080</div>
-                                    </>
-                                }
-                                rules={[{ required: true, message: '请上传背景图' }]}>
-                                <UploadImage size={4} style={{ width: 570, height: 415 }} />
-                            </Form.Item>
-                        </Col>
-                    </Row>
-                </Form>
-                <PermissionButton
-                    type="primary"
-                    key="basis"
-                    onClick={() => {
-                        save();
-                    }}
-                    isPermission={userPermission.update}
-                >
-                    保存
-                </PermissionButton>
-            </Card>
-        </PageContainer>
+      }
+    }
+  };
 
-    );
+  useEffect(() => {
+    detail(['basis', 'api']);
+  }, []);
+  return (
+    <PageContainer>
+      <Card className="basis" style={{ minHeight }}>
+        <Form layout="vertical" form={form}>
+          <Row gutter={[24, 24]}>
+            <Col span={10}>
+              <Form.Item label="系统名称" name="title">
+                <Input />
+              </Form.Item>
+              <Form.Item
+                label="主题色"
+                name="headerTheme"
+                initialValue="light"
+                rules={[{ required: true, message: '请选择主题色' }]}
+              >
+                <Select>
+                  <Select.Option value="light">白色</Select.Option>
+                  <Select.Option value="dark">黑色</Select.Option>
+                </Select>
+              </Form.Item>
+              <Form.Item
+                label="高德API Key"
+                name="apikey"
+                tooltip="配置后平台可调用高德地图GIS服务"
+              >
+                <Input />
+              </Form.Item>
+              <Row gutter={[24, 24]}>
+                <Col>
+                  <Form.Item
+                    name={'logo'}
+                    label="系统logo"
+                    extra={
+                      <>
+                        <div>推荐尺寸200*200</div>
+                        <div>支持jpg,png</div>
+                      </>
+                    }
+                  >
+                    <UploadImage />
+                  </Form.Item>
+                </Col>
+                <Col>
+                  <Form.Item
+                    name={'ico'}
+                    label="浏览器页签"
+                    tooltip="浏览器tab页中显示的图片元素"
+                    extra={
+                      <>
+                        <div>推荐尺寸64*64</div>
+                        <div>支持ico格式</div>
+                      </>
+                    }
+                  >
+                    <UploadImage size={1} types={['image/x-icon']} backgroundSize={'inherit'} />
+                  </Form.Item>
+                </Col>
+              </Row>
+            </Col>
+            <Col span={14}>
+              <Form.Item
+                name={'backgroud'}
+                label="登录背景图"
+                extra={
+                  <>
+                    <div>支持4M以内的图片:支持jpg、png</div>
+                    <div>建议尺寸1400x1080</div>
+                  </>
+                }
+                rules={[{ required: true, message: '请上传背景图' }]}
+              >
+                <UploadImage size={4} style={{ width: 570, height: 415 }} />
+              </Form.Item>
+            </Col>
+          </Row>
+        </Form>
+        <PermissionButton
+          type="primary"
+          key="basis"
+          onClick={() => {
+            save();
+          }}
+          isPermission={userPermission.update}
+        >
+          保存
+        </PermissionButton>
+      </Card>
+    </PageContainer>
+  );
 };
 
 export default Basis;

+ 84 - 86
src/pages/system/Basis/init.tsx

@@ -1,92 +1,90 @@
 import { UploadImage } from '@/components';
-import { Card, Col, Form, Input, Row, Select, } from 'antd';
-import { useEffect} from 'react';
+import { Card, Col, Form, Input, Row, Select } from 'antd';
+import { useEffect } from 'react';
 
-interface Props{
-    getData:Function
+interface Props {
+  getData: Function;
 }
 
-const Init = (props:Props) => {
-    const [form] = Form.useForm();
+const Init = (props: Props) => {
+  const [form] = Form.useForm();
 
-    useEffect(()=>{
-        props.getData(form)
-    },[])
+  useEffect(() => {
+    props.getData(form);
+  }, []);
 
-    return (
-        <Card>
-            <Form layout="vertical" form={form} >
-                <Row gutter={[24, 24]}>
-                    <Col span={10}>
-                        <Form.Item
-                            label="系统名称"
-                            name="title"
-                        >
-                            <Input />
-                        </Form.Item>
-                        <Form.Item
-                            label="主题色"
-                            name="headerTheme"
-                            initialValue="light"
-                            rules={[{ required: true, message: '请选择主题色' }]}
-                        >
-                            <Select>
-                                <Select.Option value="light">白色</Select.Option>
-                                <Select.Option value="dark">黑色</Select.Option>
-                            </Select>
-                        </Form.Item>
-                        <Form.Item
-                            label="高德API Key"
-                            name="apikey"
-                            tooltip="配置后平台可调用高德地图GIS服务"
-                        >
-                            <Input />
-                        </Form.Item>
-                        <Row gutter={[24, 24]}>
-                            <Col>
-                                <Form.Item name={'logo'} label="系统logo" extra={
-                                    <>
-                                        <div>推荐尺寸200*200</div>
-                                        <div>支持jpg,png</div>
-                                    </>
-                                }>
-                                    <UploadImage />
-                                </Form.Item>
-                            </Col>
-                            <Col>
-                                <Form.Item
-                                    name={'ico'}
-                                    label="浏览器页签"
-                                    tooltip='浏览器tab页中显示的图片元素'
-                                    extra={
-                                        <>
-                                            <div>推荐尺寸64*64</div>
-                                            <div>支持ico格式</div>
-                                        </>
-                                    }
-                                >
-                                    <UploadImage size={1} types={['image/x-icon']} backgroundSize={'inherit'} />
-                                </Form.Item>
-                            </Col>
-                        </Row>
-                    </Col>
-                    <Col span={14}>
-                        <Form.Item
-                            name={'backgroud'}
-                            label="登录背景图"
-                            extra={
-                                <>
-                                    <div>支持4M以内的图片:支持jpg、png</div>
-                                    <div>建议尺寸1400x1080</div>
-                                </>
-                            }
-                            rules={[{ required: true, message: '请上传背景图' }]}>
-                            <UploadImage size={4} style={{ width: 570, height: 415 }} />
-                        </Form.Item>
-                    </Col>
-                </Row>
-            </Form>
-        </Card>
-    )
-}
-export default Init
+  return (
+    <Card>
+      <Form layout="vertical" form={form}>
+        <Row gutter={[24, 24]}>
+          <Col span={10}>
+            <Form.Item label="系统名称" name="title">
+              <Input />
+            </Form.Item>
+            <Form.Item
+              label="主题色"
+              name="headerTheme"
+              initialValue="light"
+              rules={[{ required: true, message: '请选择主题色' }]}
+            >
+              <Select>
+                <Select.Option value="light">白色</Select.Option>
+                <Select.Option value="dark">黑色</Select.Option>
+              </Select>
+            </Form.Item>
+            <Form.Item label="高德API Key" name="apikey" tooltip="配置后平台可调用高德地图GIS服务">
+              <Input />
+            </Form.Item>
+            <Row gutter={[24, 24]}>
+              <Col>
+                <Form.Item
+                  name={'logo'}
+                  label="系统logo"
+                  extra={
+                    <>
+                      <div>推荐尺寸200*200</div>
+                      <div>支持jpg,png</div>
+                    </>
+                  }
+                >
+                  <UploadImage />
+                </Form.Item>
+              </Col>
+              <Col>
+                <Form.Item
+                  name={'ico'}
+                  label="浏览器页签"
+                  tooltip="浏览器tab页中显示的图片元素"
+                  extra={
+                    <>
+                      <div>推荐尺寸64*64</div>
+                      <div>支持ico格式</div>
+                    </>
+                  }
+                >
+                  <UploadImage size={1} types={['image/x-icon']} backgroundSize={'inherit'} />
+                </Form.Item>
+              </Col>
+            </Row>
+          </Col>
+          <Col span={14}>
+            <Form.Item
+              name={'backgroud'}
+              label="登录背景图"
+              extra={
+                <>
+                  <div>支持4M以内的图片:支持jpg、png</div>
+                  <div>建议尺寸1400x1080</div>
+                </>
+              }
+              rules={[{ required: true, message: '请上传背景图' }]}
+            >
+              <UploadImage size={4} style={{ width: 570, height: 415 }} />
+            </Form.Item>
+          </Col>
+        </Row>
+      </Form>
+    </Card>
+  );
+};
+export default Init;

+ 1 - 0
src/pages/system/Department/Assets/deivce/bind.tsx

@@ -213,6 +213,7 @@ const Bind = observer((props: Props) => {
           rowKey="id"
           search={false}
           gridColumn={2}
+          columnEmptyText={''}
           cardRender={(record) => (
             <ExtraDeviceCard showBindBtn={false} showTool={false} {...record} cardType={'bind'} />
           )}

+ 1 - 0
src/pages/system/Department/Assets/deivce/index.tsx

@@ -328,6 +328,7 @@ export default observer((props: { parentId: string }) => {
         rowKey="id"
         search={false}
         params={searchParam}
+        columnEmptyText={''}
         gridColumn={2}
         height={'none'}
         scroll={{ x: 1366 }}

+ 1 - 0
src/pages/system/Department/Assets/product/bind.tsx

@@ -133,6 +133,7 @@ const Bind = observer((props: Props) => {
             rowKey="id"
             search={false}
             gridColumn={2}
+            columnEmptyText={''}
             rowSelection={{
               selectedRowKeys: Models.bindKeys,
               onChange: (selectedRowKeys, selectedRows) => {

+ 1 - 0
src/pages/system/Department/Assets/product/index.tsx

@@ -277,6 +277,7 @@ export default observer((props: { parentId: string }) => {
         search={false}
         gridColumn={2}
         params={searchParam}
+        columnEmptyText={''}
         height={'none'}
         request={async (params) => {
           params.sorts = [{ name: 'createTime', order: 'desc' }];

+ 125 - 0
src/pages/system/Menu/Setting/baseMenu.ts

@@ -0,0 +1,125 @@
+export default [
+  // 物联网
+  {
+    code: 'iot',
+    name: '物联网',
+    id: '1',
+    children: [
+      { code: 'home', name: '首页', parentId: '1', id: '1-1' },
+      { code: 'notice', name: '通知管理', parentId: '1', id: '1-2' },
+
+      {
+        code: 'device',
+        name: '设备管理',
+        parentId: '1',
+        id: '1-3',
+        children: [
+          { code: 'device/DashBoard', name: '仪表盘', parentId: '1-3', id: '1-3-1' },
+          { code: 'device/Product', name: '产品', parentId: '1-3', id: '1-3-2' },
+          { code: 'device/Instance', name: '设备', parentId: '1-3', id: '1-3-3' },
+          { code: 'device/Category', name: '产品分类', parentId: '1-3', id: '1-3-4' },
+        ],
+      },
+
+      {
+        code: 'link',
+        name: '运维管理',
+        parentId: '1',
+        id: '1-4',
+        children: [
+          { code: 'link/DashBoard', name: '仪表盘', parentId: '1-4', id: '1-4-1' },
+          { code: 'link/AccessConfig', name: '设备接入网关', parentId: '1-4', id: '1-4-2' },
+          { code: 'link/Protocol', name: '协议管理', parentId: '1-4', id: '1-4-3' },
+          { code: 'Log', name: '日志管理', parentId: '1-4', id: '1-4-4' },
+          { code: 'link/Type', name: '网络组件', parentId: '1-4', id: '1-4-5' },
+          { code: 'link/Certificate', name: '证书管理', parentId: '1-4', id: '1-4-6' },
+          { code: 'media/Stream', name: '流媒体服务', parentId: '1-4', id: '1-4-7' },
+          {
+            code: 'link/Channel',
+            name: '通道配置',
+            parentId: '1-4',
+            id: '1-4-8',
+            children: [
+              { code: 'link/Channel/Opcua', name: 'OPC UA', parentId: '1-4-8', id: '1-4-8-1' },
+              { code: 'link/Channel/Modbus', name: 'Modbus', parentId: '1-4-8', id: '1-4-8-2' },
+            ],
+          },
+        ],
+      },
+
+      {
+        code: 'notice',
+        name: '通知管理',
+        parentId: '1',
+        id: '1-5',
+        children: [
+          { code: 'rule-engine/Alarm', name: '告警中心', parentId: '1-5', id: '1-5-1' },
+          { code: 'rule-engine/DashBoard', name: '仪表盘', parentId: '1-5', id: '1-5-2' },
+          {
+            code: 'rule-engine/Alarm/Configuration',
+            name: '告警配置',
+            parentId: '1-5',
+            id: '1-5-3',
+          },
+          { code: 'rule-engine/Alarm/Config', name: '基础配置', parentId: '1-5', id: '1-5-4' },
+          { code: 'rule-engine/Alarm/Log', name: '告警记录', parentId: '1-5', id: '1-5-5' },
+        ],
+      },
+
+      {
+        code: 'Northbound',
+        name: '北向输出',
+        parentId: '1',
+        id: '1-6',
+        children: [
+          { code: 'Northbound/DuerOS', name: 'DuerOS', parentId: '1-6', id: '1-6-1' },
+          { code: 'Northbound/AliCloud', name: '阿里云', parentId: '1-6', id: '1-6-2' },
+        ],
+      },
+
+      {
+        code: 'rule-engine',
+        name: '规则引擎',
+        parentId: '1',
+        id: '1-7',
+        children: [
+          { code: 'rule-engine/Instance', name: '规则编排', parentId: '1-7', id: '1-7-1' },
+          { code: 'rule-engine/Scene', name: '场景联动', parentId: '1-7', id: '1-7-2' },
+        ],
+      },
+    ],
+  },
+
+  // 视频中心
+  {
+    code: 'media',
+    name: '视频中心',
+    id: '2',
+    children: [
+      { code: 'media/Home', name: '首页', parentId: '2', id: '2-1' },
+      { code: 'media/DashBoard', name: '仪表盘', parentId: '2', id: '2-2' },
+      { code: 'media/Device', name: '视频设备', parentId: '2', id: '2-3' },
+      { code: 'media/SplitScreen', name: '分屏展示', parentId: '2', id: '2-4' },
+      { code: 'media/Cascade', name: '国标级联', parentId: '2', id: '2-5' },
+    ],
+  },
+
+  // 系统管理
+  {
+    code: 'system',
+    name: '系统管理',
+    id: '3',
+    children: [
+      { code: 'system/Basis', name: '基础配置', parentId: '3', id: '3-1' },
+      { code: 'system/User', name: '用户管理', parentId: '3', id: '3-2' },
+      { code: 'system/Department', name: '部门管理', parentId: '3', id: '3-3' },
+      { code: 'system/Role', name: '角色管理', parentId: '3', id: '3-4' },
+      { code: 'system/Menu', name: '菜单管理', parentId: '3', id: '3-5' },
+      { code: 'system/Permission', name: '权限管理', parentId: '3', id: '3-6' },
+      { code: 'system/Platforms', name: '第三方平台', parentId: '3', id: '3-7' },
+      { code: 'system/Relationship', name: '关系配置', parentId: '3', id: '3-8' },
+      { code: 'system/DataSource', name: '数据源管理', parentId: '3', id: '3-9' },
+      { code: 'system/Platforms/Setting', name: 'API配置', parentId: '3', id: '3-10' },
+    ],
+  },
+];

+ 24 - 0
src/pages/system/Menu/Setting/dragItem.tsx

@@ -0,0 +1,24 @@
+import { Draggable } from 'react-beautiful-dnd';
+
+interface DragItemProps {
+  data: any;
+  type: string;
+}
+
+const DragItem = (props: DragItemProps) => {
+  return (
+    <Draggable
+      draggableId={props.type + '&' + props.data.id}
+      index={props.type + '&' + props.data.id}
+      isCombineEnabled={true}
+    >
+      {(provided) => (
+        <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
+          {props.data.name}
+        </div>
+      )}
+    </Draggable>
+  );
+};
+
+export default DragItem;

+ 67 - 0
src/pages/system/Menu/Setting/index.less

@@ -0,0 +1,67 @@
+@bgColor: #f6f6f6;
+
+.menu-setting-warp {
+  display: flex;
+  flex-direction: column;
+  padding: 24px;
+  background-color: #fff;
+
+  .menu-setting-tip {
+    margin-bottom: 24px;
+    padding: 10px 24px;
+    color: rgba(0, 0, 0, 0.55);
+    background-color: @bgColor;
+  }
+
+  .menu-tree-content {
+    display: flex;
+    flex-grow: 1;
+    gap: 16px;
+    justify-content: space-between;
+    height: 0;
+
+    .menu-tree {
+      display: flex;
+      flex: 1 auto;
+      flex-direction: column;
+      justify-content: space-between;
+
+      .menu-tree-title {
+        display: flex;
+        justify-content: space-between;
+        margin-bottom: 16px;
+        padding: 14px 16px;
+        font-weight: 400;
+        font-size: 16px;
+        background-color: #f3f4f4;
+
+        > span {
+          margin-left: 16px;
+        }
+      }
+
+      .tree-content {
+        display: flex;
+        flex-direction: column;
+        flex-grow: 1;
+        padding: 12px;
+        border: 1px solid #e0e0e0;
+        border-radius: 4px;
+
+        .tree-body {
+          flex: 1 auto;
+          height: 0;
+          min-height: 300px;
+          margin-top: 16px;
+          overflow-y: auto;
+        }
+      }
+    }
+
+    .menu-tree-drag-btn {
+      padding: 8px;
+      border: 1px solid #e0e0e0;
+      border-radius: 2px;
+    }
+  }
+}

+ 168 - 0
src/pages/system/Menu/Setting/index.tsx

@@ -0,0 +1,168 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { useDomFullHeight } from '@/hooks';
+import {
+  ExclamationCircleOutlined,
+  QuestionCircleOutlined,
+  RightOutlined,
+} from '@ant-design/icons';
+import Tree from './tree';
+import './index.less';
+import { Button, Tooltip } from 'antd';
+import BaseTreeData from './baseMenu';
+import { useCallback, useEffect, useState } from 'react';
+import { DragDropContext } from 'react-beautiful-dnd';
+
+export default () => {
+  const { minHeight } = useDomFullHeight(`.menu-setting-warp`);
+  const [menuData, setMenuData] = useState<any[]>([]);
+  const [baseMenu, setBaseMenu] = useState<any[]>([]);
+
+  // const removeItem = (data: any[], id: string): any[] => {
+  //   return data.filter(item => {
+  //     if (item.id === id) {
+  //       return false
+  //     }
+  //
+  //     if (item.children) {
+  //       item.children = removeItem(item.children, id)
+  //     }
+  //     return true
+  //   })
+  // }
+
+  const findItem = (data: any[], id: string) => {
+    let object = null;
+    data.some((item) => {
+      if (item.id === id) {
+        object = item;
+        return true;
+      }
+
+      if (item.children) {
+        object = findItem(item.children, id);
+        return !!object;
+      }
+
+      return false;
+    });
+    return object;
+  };
+
+  const finedIndex = (data: any[], id: string): { index: number; menus: any[] } => {
+    let object = {
+      index: data.length,
+      menus: data,
+    };
+    data.some((item, index) => {
+      if (item.id === id) {
+        object = {
+          index,
+          menus: data,
+        };
+        return true;
+      }
+
+      if (item.children) {
+        object = finedIndex(item.children, id);
+        return !!object;
+      }
+
+      return false;
+    });
+
+    return object;
+  };
+
+  const onDragEnd = useCallback(
+    (result: any) => {
+      console.log(result);
+      if (result.source.droppableId.includes('source')) {
+        if (result.combine && result.combine.droppableId.includes('menu')) {
+          const sourceIndex = result.source.index.replace(/(source|menu)&/, '');
+          const draggableIdIndex = result.combine?.draggableId.replace(/(source|menu)&/, '');
+          const sourceItem = findItem(baseMenu, sourceIndex);
+          const newMenus = [...menuData];
+          const { index, menus } = finedIndex(newMenus, draggableIdIndex);
+          console.log(index, menus);
+          menus.splice(index + 1, 0, sourceItem);
+          console.log(newMenus);
+          setMenuData([...newMenus]);
+        } else if (result.destination && result.destination.includes('menu')) {
+          const sourceIndex = result.source.index.replace(/(source|menu)&/, '');
+          const sourceItem = findItem(baseMenu, sourceIndex);
+          const newMenus = [...menuData];
+          if (sourceItem) {
+            if (newMenus.length) {
+              const destinationIndex = result.destination?.index.replace(/(source|menu)&/, '');
+              // 获取右侧menu的位置
+              const { index, menus } = finedIndex(newMenus, destinationIndex);
+              console.log(index, menus);
+              menus.splice(index + 1, 0, sourceItem);
+            } else {
+              newMenus.push(sourceItem);
+            }
+            console.log(newMenus);
+            setMenuData([...newMenus]);
+          }
+        }
+      }
+    },
+    [menuData, baseMenu],
+  );
+
+  useEffect(() => {
+    setBaseMenu(BaseTreeData);
+  }, []);
+
+  return (
+    <PageContainer>
+      <div className={'menu-setting-warp'} style={{ minHeight }}>
+        <div className={'menu-setting-tip'}>
+          <ExclamationCircleOutlined />
+          基于系统源代码中的菜单数据,配置系统菜单。
+        </div>
+        <div className={'menu-tree-content'}>
+          <DragDropContext onDragEnd={onDragEnd}>
+            <div className={'menu-tree left-tree'}>
+              <div className={'menu-tree-title'}>
+                <div>
+                  源菜单
+                  <Tooltip title={'根据系统代码自动读取的菜单数据'}>
+                    <QuestionCircleOutlined />
+                  </Tooltip>
+                </div>
+                <Button type={'primary'} ghost>
+                  一键拷贝
+                </Button>
+              </div>
+              <Tree treeData={baseMenu} droppableId={'source'} />
+            </div>
+            <div style={{ display: 'flex', alignItems: 'center' }}>
+              <div className={'menu-tree-drag-btn'}>
+                请拖动至右侧
+                <RightOutlined />
+              </div>
+            </div>
+
+            <div className={'menu-tree right-tree'}>
+              <div className={'menu-tree-title'}>
+                <div>
+                  系统菜单
+                  <Tooltip title={'菜单管理页面配置的菜单数据'}>
+                    <QuestionCircleOutlined />
+                  </Tooltip>
+                </div>
+              </div>
+              <Tree treeData={menuData} droppableId={'menu'} />
+            </div>
+          </DragDropContext>
+        </div>
+        <div>
+          <Button type={'primary'} style={{ marginTop: 24 }}>
+            保存
+          </Button>
+        </div>
+      </div>
+    </PageContainer>
+  );
+};

+ 58 - 0
src/pages/system/Menu/Setting/tree.tsx

@@ -0,0 +1,58 @@
+import { Input, Tree } from 'antd';
+import { SearchOutlined } from '@ant-design/icons';
+import DragItem from '@/pages/system/Menu/Setting/dragItem';
+import { Droppable } from 'react-beautiful-dnd';
+
+interface TreeBodyProps {
+  treeData: any[];
+  droppableId: string;
+}
+
+const { TreeNode } = Tree;
+
+export default (props: TreeBodyProps) => {
+  const createTreeNode = (data: any[], type: string): React.ReactNode => {
+    return data.map((item: any) => {
+      if (item.children) {
+        return (
+          <TreeNode title={<DragItem data={item} type={type} />}>
+            {createTreeNode(item.children, type)}
+          </TreeNode>
+        );
+      }
+      return <TreeNode title={<DragItem data={item} type={type} />}></TreeNode>;
+    });
+  };
+
+  return (
+    <div className={'tree-content'}>
+      <div style={{ width: '75%' }}>
+        <Input
+          prefix={<SearchOutlined style={{ color: '#B3B3B3' }} />}
+          placeholder={'请输入菜单名称'}
+        />
+      </div>
+      <div className={'tree-body'}>
+        <Droppable
+          droppableId={props.droppableId}
+          direction="horizontal"
+          type="COLUMN"
+          isCombineEnabled={true}
+        >
+          {(provided) => (
+            <div
+              className="columns"
+              {...provided.droppableProps}
+              ref={provided.innerRef}
+              style={{ height: '100%' }}
+            >
+              <Tree draggable={props.droppableId === 'menu'}>
+                {createTreeNode(props.treeData, props.droppableId)}
+              </Tree>
+            </div>
+          )}
+        </Droppable>
+      </div>
+    </div>
+  );
+};

+ 13 - 51
src/pages/system/Menu/index.tsx

@@ -18,7 +18,7 @@ import SearchComponent from '@/components/SearchComponent';
 import Service from './service';
 import type { MenuItem } from './typing';
 import moment from 'moment';
-import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+import { getMenuPathByCode, getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
 import { PermissionButton } from '@/components';
 import { useDomFullHeight } from '@/hooks';
 import { onlyMessage } from '@/utils/util';
@@ -272,7 +272,7 @@ export default observer(() => {
             status: response.status,
           };
         }}
-        headerTitle={
+        headerTitle={[
           <PermissionButton
             isPermission={permission.add}
             onClick={() => {
@@ -286,56 +286,18 @@ export default observer(() => {
               id: 'pages.data.option.add',
               defaultMessage: '新增',
             })}
-          </PermissionButton>
-        }
+          </PermissionButton>,
+          <Button
+            style={{ marginLeft: 12 }}
+            onClick={() => {
+              console.log(getMenuPathByCode('system/Menu/Setting'));
+              history.push(getMenuPathByCode('system/Menu/Setting'));
+            }}
+          >
+            菜单配置
+          </Button>,
+        ]}
       />
-      {/*<Modal*/}
-      {/*  title={intl.formatMessage({*/}
-      {/*    id: State.current.parentId*/}
-      {/*      ? 'pages.system.menu.option.addChildren'*/}
-      {/*      : 'pages.data.option.add',*/}
-      {/*    defaultMessage: '新增',*/}
-      {/*  })}*/}
-      {/*  visible={State.visible}*/}
-      {/*  width={660}*/}
-      {/*  onOk={saveData}*/}
-      {/*  onCancel={modalCancel}*/}
-      {/*>*/}
-      {/*  <Form form={form} labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>*/}
-      {/*    <Form.Item*/}
-      {/*      name="code"*/}
-      {/*      label={intl.formatMessage({*/}
-      {/*        id: 'page.system.menu.encoding',*/}
-      {/*        defaultMessage: '编码',*/}
-      {/*      })}*/}
-      {/*      required={true}*/}
-      {/*      rules={[*/}
-      {/*        { required: true, message: '请输入编码' },*/}
-      {/*        { max: 64, message: '最多可输入64个字符' },*/}
-      {/*        {*/}
-      {/*          pattern: /^[a-zA-Z0-9`!@#$%^&*()_+\-={}|\\\]\[;':",.\/<>?]+$/,*/}
-      {/*          message: '请输入英文+数字+特殊字符(`!@#$%^&*()_+-={}|\\][;\':",./<>?)',*/}
-      {/*        },*/}
-      {/*      ]}*/}
-      {/*    >*/}
-      {/*      <Input />*/}
-      {/*    </Form.Item>*/}
-      {/*    <Form.Item*/}
-      {/*      name="name"*/}
-      {/*      label={intl.formatMessage({*/}
-      {/*        id: 'pages.table.name',*/}
-      {/*        defaultMessage: '名称',*/}
-      {/*      })}*/}
-      {/*      required={true}*/}
-      {/*      rules={[*/}
-      {/*        { required: true, message: '请输入名称' },*/}
-      {/*        { max: 64, message: '最多可输入64个字符' },*/}
-      {/*      ]}*/}
-      {/*    >*/}
-      {/*      <Input />*/}
-      {/*    </Form.Item>*/}
-      {/*  </Form>*/}
-      {/*</Modal>*/}
     </PageContainer>
   );
 });

+ 1 - 1
src/utils/const.ts

@@ -1,5 +1,5 @@
 class SystemConst {
-  static API_BASE = 'jetlinks';
+  static API_BASE = 'api';
 
   static SYSTEM_NAME = 'Jetlinks';
 

+ 6 - 1
src/utils/menu/index.ts

@@ -37,6 +37,9 @@ const extraRouteObj = {
       { code: 'Save2', name: '测试详情' },
     ],
   },
+  'device/Firmware': {
+    children: [{ code: 'Task', name: '升级任务' }],
+  },
   'link/Channel': {
     children: [
       {
@@ -73,6 +76,9 @@ const extraRouteObj = {
   'system/DataSource': {
     children: [{ code: 'Management', name: '管理' }],
   },
+  'system/Menu': {
+    children: [{ code: 'Setting', name: '菜单配置' }],
+  },
 };
 //额外路由
 export const extraRouteArr = [
@@ -183,7 +189,6 @@ export const handleRoutes = (routes?: MenuItem[], level = 1): MenuItem[] => {
     ? routes.map((item) => {
         // 判断当前是否有额外子路由
         const extraRoutes = extraRouteObj[item.code];
-
         if (extraRoutes) {
           if (extraRoutes.children) {
             const eRoutes = findExtraRoutes(item.code, extraRoutes.children, item.url);

+ 5 - 9
src/utils/menu/router.ts

@@ -16,8 +16,6 @@ export enum MENUS_CODE {
   'device/Category' = 'device/Category',
   'device/Command' = 'device/Command',
   'device/DataSource' = 'device/DataSource',
-  'device/Firmware/Save' = 'device/Firmware/Save',
-  'device/Firmware' = 'device/Firmware',
   'device/Instance' = 'device/Instance',
   'device/Location' = 'device/Location',
   'device/Product/Save' = 'device/Product/Save',
@@ -84,6 +82,7 @@ export enum MENUS_CODE {
   'system/Department/Member' = 'system/Department/Member',
   'system/Department' = 'system/Department',
   'system/Menu' = 'system/Menu',
+  'system/Menu/Setting' = 'system/Menu/Setting',
   'system/OpenAPI' = 'system/OpenAPI',
   'system/Permission' = 'system/Permission',
   'system/Role/Detail' = 'system/Role/Detail',
@@ -101,12 +100,9 @@ export enum MENUS_CODE {
   'visualization/Category' = 'visualization/Category',
   'visualization/Configuration' = 'visualization/Configuration',
   'visualization/Screen' = 'visualization/Screen',
-  'device/Firmware/Detail/History' = 'device/Firmware/Detail/History',
-  'device/Firmware/Detail/Task/Detail' = 'device/Firmware/Detail/Task/Detail',
-  'device/Firmware/Detail/Task/Release' = 'device/Firmware/Detail/Task/Release',
-  'device/Firmware/Detail/Task/Save' = 'device/Firmware/Detail/Task/Save',
-  'device/Firmware/Detail/Task' = 'device/Firmware/Detail/Task',
-  'device/Firmware/Detail' = 'device/Firmware/Detail',
+  'device/Firmware' = 'device/Firmware',
+  'device/Firmware/Task' = 'device/Firmware/Task',
+  'device/Firmware/Task/Detail' = 'device/Firmware/Task/Detail',
   'device/Instance/Detail/Config/Tags' = 'device/Instance/Detail/Config/Tags',
   'device/Instance/Detail/Config' = 'device/Instance/Detail/Config',
   'device/Instance/Detail/Functions' = 'device/Instance/Detail/Functions',
@@ -168,7 +164,7 @@ export const getDetailNameByCode = {
   'system/Menu/Detail': '菜单详情',
   'device/Product/Detail': '产品详情',
   'device/Instance/Detail': '设备详情',
-  'device/Firmware/Detail': '固件详情',
+  'device/Firmware/Task/Detail': '详情',
   'system/Department/Detail': '部门详情',
   'system/Role/Detail': '权限配置',
   'link/Type/Detail': '网络组件详情',

+ 39 - 2
yarn.lock

@@ -7842,6 +7842,13 @@ css-blank-pseudo@^0.1.4:
   dependencies:
     postcss "^7.0.5"
 
+css-box-model@^1.2.0:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
+  integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
+  dependencies:
+    tiny-invariant "^1.0.6"
+
 css-has-pseudo@^0.10.0:
   version "0.10.0"
   resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz#3c642ab34ca242c59c41a125df9105841f6966ee"
@@ -16167,6 +16174,11 @@ quickselect@^2.0.0:
   resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018"
   integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==
 
+raf-schd@^4.0.2:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
+  integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
+
 raf@^3.1.0, raf@^3.3.2, raf@^3.4.0, raf@^3.4.1:
   version "3.4.1"
   resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
@@ -16783,6 +16795,19 @@ react-base16-styling@^0.6.0:
     lodash.flow "^3.3.0"
     pure-color "^1.2.0"
 
+react-beautiful-dnd@^13.1.0:
+  version "13.1.0"
+  resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz#ec97c81093593526454b0de69852ae433783844d"
+  integrity sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==
+  dependencies:
+    "@babel/runtime" "^7.9.2"
+    css-box-model "^1.2.0"
+    memoize-one "^5.1.1"
+    raf-schd "^4.0.2"
+    react-redux "^7.2.0"
+    redux "^4.0.4"
+    use-memo-one "^1.1.1"
+
 react-color@2.17.1:
   version "2.17.1"
   resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.17.1.tgz#f114811c83f5d80a1bd1b80466c2f7ddcc58da9d"
@@ -17093,7 +17118,7 @@ react-redux@=4.4.10:
     loose-envify "^1.4.0"
     prop-types "^15.7.2"
 
-react-redux@^7.1.0:
+react-redux@^7.1.0, react-redux@^7.2.0:
   version "7.2.8"
   resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.8.tgz#a894068315e65de5b1b68899f9c6ee0923dd28de"
   integrity sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw==
@@ -17443,6 +17468,13 @@ redux@^4.0.0, redux@^4.0.1:
   dependencies:
     "@babel/runtime" "^7.9.2"
 
+redux@^4.0.4:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13"
+  integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==
+  dependencies:
+    "@babel/runtime" "^7.9.2"
+
 reflect-metadata@^0.1.13:
   version "0.1.13"
   resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
@@ -19748,7 +19780,7 @@ timers-ext@^0.1.7:
     es5-ext "~0.10.46"
     next-tick "1"
 
-tiny-invariant@^1.0.2:
+tiny-invariant@^1.0.2, tiny-invariant@^1.0.6:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9"
   integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==
@@ -20565,6 +20597,11 @@ use-media-antd-query@^1.0.6, use-media-antd-query@^1.1.0:
   resolved "https://registry.yarnpkg.com/use-media-antd-query/-/use-media-antd-query-1.1.0.tgz#f083ad7e292c1c0261b6bbfaac0edc3e0920d85d"
   integrity sha512-B6kKZwNV4R+l4Rl11sWO7HqOay9alzs1Vp1b4YJqjz33YxbltBCZtt/yxXxkXN9rc1S7OeEL/GbwC30Wmqhw6Q==
 
+use-memo-one@^1.1.1:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20"
+  integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==
+
 use-subscription@1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.4.1.tgz#edcbcc220f1adb2dd4fa0b2f61b6cc308e620069"