Przeglądaj źródła

feat: 边缘网关

100011797 3 lat temu
rodzic
commit
0e64573a85

+ 9 - 3
src/pages/device/Instance/Import/index.tsx

@@ -5,7 +5,7 @@ import { Badge, Button, Checkbox, Modal, Radio, Space, Upload } from 'antd';
 import 'antd/lib/tree-select/style/index.less';
 import { useEffect, useState } from 'react';
 import { service } from '@/pages/device/Instance';
-import type { DeviceInstance } from '../typings';
+// import type { DeviceInstance } from '../typings';
 import FUpload from '@/components/Upload';
 import { UploadOutlined } from '@ant-design/icons';
 import SystemConst from '@/utils/const';
@@ -17,7 +17,8 @@ import encodeQuery from '@/utils/encodeQuery';
 interface Props {
   visible: boolean;
   close: () => void;
-  data: Partial<DeviceInstance>;
+  type?: string;
+  // data: Partial<DeviceInstance>;
 }
 
 const FileFormat = (props: any) => {
@@ -177,7 +178,12 @@ const Import = (props: Props) => {
 
   useEffect(() => {
     service
-      .getProductList(encodeQuery({ terms: { state: 1 }, sorts: { createTime: 'desc' } }))
+      .getProductList(
+        encodeQuery({
+          terms: { state: 1, accessProvider: props?.type },
+          sorts: { createTime: 'desc' },
+        }),
+      )
       .then((resp) => {
         if (resp.status === 200) {
           const list = resp.result.map((item: { name: any; id: any }) => ({

+ 2 - 1
src/pages/device/Instance/index.tsx

@@ -870,7 +870,8 @@ const Instance = () => {
         visible={exportVisible}
       />
       <Import
-        data={current}
+        // data={current}
+        type={''}
         close={() => {
           setImportVisible(false);
           actionRef.current?.reload();

+ 5 - 0
src/pages/device/Instance/service.ts

@@ -385,6 +385,11 @@ class Service extends BaseService<DeviceInstance> {
     request(`/${SystemConst.API_BASE}/gateway/device/providers`, {
       method: 'GET',
     });
+  // 边缘网关
+  public restPassword = (id: string) =>
+    request(`/${SystemConst.API_BASE}/edge/operations/${id}/auth-user-password-reset/invoke`, {
+      method: 'POST',
+    });
 }
 
 export default Service;

+ 291 - 0
src/pages/edge/Device/Save/index.tsx

@@ -0,0 +1,291 @@
+import { Col, Form, Input, Modal, Row, Select } from 'antd';
+import { service } from '@/pages/device/Instance';
+import { useEffect, useState } from 'react';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { UploadImage } from '@/components';
+import { debounce } from 'lodash';
+import encodeQuery from '@/utils/encodeQuery';
+import { onlyMessage } from '@/utils/util';
+import { PlusOutlined } from '@ant-design/icons';
+import SaveProductModal from '@/pages/media/Device/Save/SaveProduct';
+import { DeviceInstance } from '@/pages/device/Instance/typings';
+
+interface Props {
+  close: (data: DeviceInstance | undefined) => void;
+  reload?: () => void;
+  model?: 'add' | 'edit';
+  data?: Partial<DeviceInstance>;
+}
+
+const defaultImage = '/images/device-type-3-big.png';
+
+const Save = (props: Props) => {
+  const { close, data } = props;
+  const [productList, setProductList] = useState<any[]>([]);
+  const [loading, setLoading] = useState(false);
+  const [visible, setVisible] = useState(false);
+  const [form] = Form.useForm();
+
+  useEffect(() => {
+    if (data && Object.keys(data).length) {
+      form.setFieldsValue({ ...data });
+    } else {
+      form.setFieldsValue({
+        photoUrl: defaultImage,
+      });
+    }
+  }, [data]);
+
+  const intl = useIntl();
+
+  useEffect(() => {
+    service
+      .getProductList(
+        encodeQuery({
+          sorts: {
+            createTime: 'desc',
+          },
+          terms: {
+            state: 1,
+            accessProvider: 'official-edge-gateway',
+          },
+        }),
+      )
+      .then((resp: any) => {
+        if (resp.status === 200) {
+          const list = resp.result.map((item: { name: any; id: any }) => ({
+            label: item.name,
+            value: item.id,
+          }));
+          setProductList(list);
+        }
+      });
+  }, []);
+
+  const intlFormat = (
+    id: string,
+    defaultMessage: string,
+    paramsID?: string,
+    paramsMessage?: string,
+  ) => {
+    const paramsObj: Record<string, string> = {};
+    if (paramsID) {
+      const paramsMsg = intl.formatMessage({
+        id: paramsID,
+        defaultMessage: paramsMessage,
+      });
+      paramsObj.name = paramsMsg;
+    }
+    return intl.formatMessage(
+      {
+        id,
+        defaultMessage,
+      },
+      paramsObj,
+    );
+  };
+
+  const handleSave = async () => {
+    const values = await form.validateFields();
+    if (values) {
+      if (values.id === '') {
+        delete values.id;
+      }
+      setLoading(true);
+      const resp = (await service.update(values)) as any;
+      setLoading(false);
+      if (resp.status === 200) {
+        onlyMessage('保存成功');
+        if (props.reload) {
+          props.reload();
+        }
+        props.close(values);
+        form.resetFields();
+      }
+    }
+  };
+
+  const vailId = (_: any, value: any, callback: Function) => {
+    if (props.model === 'add' && value) {
+      service.isExists(value).then((resp: any) => {
+        if (resp.status === 200 && resp.result) {
+          callback(
+            intl.formatMessage({
+              id: 'pages.form.tip.existsID',
+              defaultMessage: 'ID重复',
+            }),
+          );
+        } else {
+          callback();
+        }
+      });
+    } else {
+      callback();
+    }
+  };
+
+  return (
+    <Modal
+      maskClosable={false}
+      open
+      onCancel={() => {
+        form.resetFields();
+        close(undefined);
+      }}
+      width="580px"
+      title={intl.formatMessage({
+        id: `pages.data.option.${props.model || 'add'}`,
+        defaultMessage: '新增',
+      })}
+      confirmLoading={loading}
+      onOk={handleSave}
+    >
+      <Form
+        form={form}
+        layout={'vertical'}
+        labelAlign={'right'}
+        labelCol={{
+          style: { width: 100 },
+        }}
+      >
+        <Row>
+          <Col span={8}>
+            <Form.Item name={'photoUrl'}>
+              <UploadImage />
+            </Form.Item>
+          </Col>
+          <Col span={16}>
+            <Form.Item
+              label={'ID'}
+              name={'id'}
+              tooltip={intlFormat('pages.form.tooltip.id', '若不填写,系统将自动生成唯一ID')}
+              rules={[
+                {
+                  pattern: /^[a-zA-Z0-9_\-]+$/,
+                  message: intlFormat('pages.form.tip.id', '请输入英文或者数字或者-或者_'),
+                },
+                {
+                  max: 64,
+                  message: intlFormat('pages.form.tip.max64', '最多输入64个字符'),
+                },
+                {
+                  validator: debounce(vailId, 300),
+                },
+              ]}
+            >
+              <Input
+                disabled={props.model === 'edit'}
+                placeholder={`${intlFormat('pages.form.tip.input', '请输入')}ID`}
+              />
+            </Form.Item>
+            <Form.Item
+              label={intlFormat('pages.table.name', '名称')}
+              name={'name'}
+              rules={[
+                {
+                  required: true,
+                  message: intlFormat(
+                    'pages.form.tip.input.props',
+                    '请输入名称',
+                    'pages.table.name',
+                    '名称',
+                  ),
+                },
+                {
+                  max: 64,
+                  message: intl.formatMessage({
+                    id: 'pages.form.tip.max64',
+                    defaultMessage: '最多输入64个字符',
+                  }),
+                },
+              ]}
+              required
+            >
+              <Input
+                placeholder={
+                  intlFormat('pages.form.tip.input', '请输入') +
+                  intlFormat('pages.table.name', '名称')
+                }
+              />
+            </Form.Item>
+          </Col>
+        </Row>
+        <Row>
+          <Col span={22}>
+            <Form.Item
+              label={'所属产品'}
+              name={'productId'}
+              rules={[
+                {
+                  required: true,
+                  message: '请选择所属产品',
+                },
+              ]}
+              // tooltip={'只能选择“正常”状态的产品'}
+              required
+            >
+              <Select
+                showSearch
+                allowClear
+                options={productList}
+                disabled={props.model === 'edit'}
+                onSelect={(_: any, node: any) => {
+                  form.setFieldsValue({
+                    productName: node.label,
+                  });
+                }}
+                placeholder={'请选择所属产品'}
+                filterOption={(input, option) => option.label.includes(input)}
+              />
+            </Form.Item>
+            <Form.Item hidden={true} name={'productName'}>
+              <Input />
+            </Form.Item>
+          </Col>
+          <Col span={2} style={{ margin: '35px 0 0 0' }}>
+            <a
+              style={{ marginLeft: 10 }}
+              onClick={() => {
+                setVisible(true);
+              }}
+            >
+              <PlusOutlined />
+            </a>
+          </Col>
+        </Row>
+        <Row>
+          <Col span={24}>
+            <Form.Item label={intlFormat('pages.table.description', '说明')} name={'description'}>
+              <Input.TextArea
+                placeholder={
+                  intlFormat('pages.form.tip.input', '请输入') +
+                  intlFormat('pages.table.description', '说明')
+                }
+                rows={4}
+                style={{ width: '100%' }}
+                maxLength={200}
+                showCount={true}
+              />
+            </Form.Item>
+          </Col>
+        </Row>
+      </Form>
+      <SaveProductModal
+        visible={visible}
+        type={'official-edge-gateway'}
+        close={() => {
+          setVisible(false);
+        }}
+        reload={(productId: string, name: string) => {
+          form.setFieldsValue({ productId });
+          productList.push({
+            id: productId,
+            name,
+          });
+          setProductList([...productList]);
+        }}
+      />
+    </Modal>
+  );
+};
+export default Save;

+ 478 - 0
src/pages/edge/Device/index.tsx

@@ -0,0 +1,478 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { DeviceInstance } from '@/pages/device/Instance/typings';
+import SearchComponent from '@/components/SearchComponent';
+import { ActionType, ProColumns } from '@jetlinks/pro-table';
+import moment from 'moment';
+import { Badge, Button, Tooltip } from 'antd';
+import { service as categoryService } from '@/pages/device/Category';
+import { InstanceModel, service, statusMap } from '@/pages/device/Instance';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import {
+  ControlOutlined,
+  DeleteOutlined,
+  EditOutlined,
+  EyeOutlined,
+  ImportOutlined,
+  PlayCircleOutlined,
+  PlusOutlined,
+  RedoOutlined,
+  StopOutlined,
+} from '@ant-design/icons';
+import { PermissionButton, ProTableCard } from '@/components';
+import { useRef, useState } from 'react';
+import DeviceCard from '@/components/ProTableCard/CardItems/device';
+import { onlyMessage } from '@/utils/util';
+import Save from './Save';
+import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+import { useHistory } from 'umi';
+import Import from '@/pages/device/Instance/Import';
+
+export default () => {
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+  const [searchParams, setSearchParams] = useState<any>({});
+  const [current, setCurrent] = useState<Partial<DeviceInstance>>({});
+  const [visible, setVisible] = useState<boolean>(false);
+  const history = useHistory<Record<string, string>>();
+  const [importVisible, setImportVisible] = useState<boolean>(false);
+  const { permission } = PermissionButton.usePermission('edge/Device');
+
+  const tools = (record: DeviceInstance, type: 'card' | 'list') => [
+    type === 'list' && (
+      <Button
+        type={'link'}
+        style={{ padding: 0 }}
+        key={'detail'}
+        onClick={() => {
+          InstanceModel.current = record;
+          const url = getMenuPathByParams(MENUS_CODE['device/Instance/Detail'], record.id);
+          history.push(url);
+        }}
+      >
+        <Tooltip
+          title={intl.formatMessage({
+            id: 'pages.data.option.detail',
+            defaultMessage: '查看',
+          })}
+        >
+          <EyeOutlined />
+        </Tooltip>
+      </Button>
+    ),
+    <PermissionButton
+      type={'link'}
+      isPermission={permission.update}
+      onClick={() => {
+        setCurrent(record);
+        setVisible(true);
+      }}
+      tooltip={{
+        title: type === 'list' ? '编辑' : '',
+      }}
+      style={{ padding: 0 }}
+      key={'edit'}
+    >
+      <EditOutlined />
+      {type === 'list' ? '' : '编辑'}
+    </PermissionButton>,
+    <PermissionButton
+      type={'link'}
+      onClick={() => {
+        onlyMessage('暂未开发', 'error');
+      }}
+      isPermission={permission.setting}
+      style={{ padding: 0 }}
+      key={'control'}
+    >
+      <ControlOutlined />
+      {type === 'list' ? '' : '远程控制'}
+    </PermissionButton>,
+    <PermissionButton
+      type={'link'}
+      tooltip={{
+        title: type !== 'list' ? '' : '重置密码',
+      }}
+      style={{ padding: 0 }}
+      isPermission={permission.password}
+      key={'reset'}
+      popConfirm={{
+        title: '确认重置密码?',
+        onConfirm: () => {
+          service.restPassword(record.id).then((resp: any) => {
+            if (resp.status === 200) {
+              onlyMessage(
+                intl.formatMessage({
+                  id: 'pages.data.option.success',
+                  defaultMessage: '操作成功!',
+                }),
+              );
+              actionRef.current?.reload();
+            }
+          });
+        },
+      }}
+    >
+      <RedoOutlined />
+      {type === 'list' ? '' : '重置密码'}
+    </PermissionButton>,
+    <PermissionButton
+      type={'link'}
+      key={'state'}
+      style={{ padding: 0 }}
+      popConfirm={{
+        title: intl.formatMessage({
+          id: `pages.data.option.${
+            record.state.value !== 'notActive' ? 'disabled' : 'enabled'
+          }.tips`,
+          defaultMessage: '确认禁用?',
+        }),
+        onConfirm: () => {
+          if (record.state.value !== 'notActive') {
+            service.undeployDevice(record.id).then((resp: any) => {
+              if (resp.status === 200) {
+                onlyMessage(
+                  intl.formatMessage({
+                    id: 'pages.data.option.success',
+                    defaultMessage: '操作成功!',
+                  }),
+                );
+                actionRef.current?.reload();
+              }
+            });
+          } else {
+            service.deployDevice(record.id).then((resp: any) => {
+              if (resp.status === 200) {
+                onlyMessage(
+                  intl.formatMessage({
+                    id: 'pages.data.option.success',
+                    defaultMessage: '操作成功!',
+                  }),
+                );
+                actionRef.current?.reload();
+              }
+            });
+          }
+        },
+      }}
+      isPermission={permission.action}
+      tooltip={{
+        title: intl.formatMessage({
+          id: `pages.data.option.${record.state.value !== 'notActive' ? 'disabled' : 'enabled'}`,
+          defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
+        }),
+      }}
+    >
+      {record.state.value !== 'notActive' ? <StopOutlined /> : <PlayCircleOutlined />}
+      {record.state.value !== 'notActive'
+        ? type === 'list'
+          ? ''
+          : '禁用'
+        : type === 'list'
+        ? ''
+        : '启用'}
+    </PermissionButton>,
+    <PermissionButton
+      type={'link'}
+      key={'delete'}
+      style={{ padding: 0 }}
+      isPermission={permission.delete}
+      tooltip={
+        record.state.value !== 'notActive'
+          ? { title: intl.formatMessage({ id: 'pages.device.instance.deleteTip' }) }
+          : undefined
+      }
+      disabled={record.state.value !== 'notActive'}
+      popConfirm={{
+        title: intl.formatMessage({
+          id: 'pages.data.option.remove.tips',
+        }),
+        disabled: record.state.value !== 'notActive',
+        onConfirm: async () => {
+          if (record.state.value === 'notActive') {
+            await service.remove(record.id);
+            onlyMessage(
+              intl.formatMessage({
+                id: 'pages.data.option.success',
+                defaultMessage: '操作成功!',
+              }),
+            );
+            actionRef.current?.reload();
+          } else {
+            onlyMessage(intl.formatMessage({ id: 'pages.device.instance.deleteTip' }), 'error');
+          }
+        },
+      }}
+    >
+      <DeleteOutlined />
+    </PermissionButton>,
+  ];
+  const columns: ProColumns<DeviceInstance>[] = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      width: 200,
+      ellipsis: true,
+      fixed: 'left',
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.table.deviceName',
+        defaultMessage: '设备名称',
+      }),
+      dataIndex: 'name',
+      ellipsis: true,
+      width: 200,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.table.productName',
+        defaultMessage: '产品名称',
+      }),
+      dataIndex: 'productId',
+      width: 200,
+      ellipsis: true,
+      valueType: 'select',
+      request: async () => {
+        const res = await service.getProductList();
+        if (res.status === 200) {
+          return res.result.map((pItem: any) => ({ label: pItem.name, value: pItem.id }));
+        }
+        return [];
+      },
+      render: (_, row) => row.productName,
+      filterMultiple: true,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.device.instance.registrationTime',
+        defaultMessage: '注册时间',
+      }),
+      dataIndex: 'registryTime',
+      width: '200px',
+      valueType: 'dateTime',
+      render: (_: any, row) => {
+        return row.registryTime ? moment(row.registryTime).format('YYYY-MM-DD HH:mm:ss') : '';
+      },
+      sorter: true,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.searchTable.titleStatus',
+        defaultMessage: '状态',
+      }),
+      dataIndex: 'state',
+      width: '90px',
+      valueType: 'select',
+      renderText: (record) =>
+        record ? <Badge status={statusMap.get(record.value)} text={record.text} /> : '',
+      valueEnum: {
+        notActive: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.notActive',
+            defaultMessage: '禁用',
+          }),
+          status: 'notActive',
+        },
+        offline: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.offLine',
+            defaultMessage: '离线',
+          }),
+          status: 'offline',
+        },
+        online: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.onLine',
+            defaultMessage: '在线',
+          }),
+          status: 'online',
+        },
+      },
+      filterMultiple: false,
+    },
+    {
+      dataIndex: 'classifiedId',
+      title: '产品分类',
+      valueType: 'treeSelect',
+      hideInTable: true,
+      fieldProps: {
+        fieldNames: {
+          label: 'name',
+          value: 'id',
+        },
+      },
+      request: () =>
+        categoryService
+          .queryTree({
+            paging: false,
+          })
+          .then((resp: any) => resp.result),
+    },
+    {
+      dataIndex: 'productId$product-info',
+      title: '接入方式',
+      valueType: 'select',
+      hideInTable: true,
+      request: () =>
+        service.queryGatewayList().then((resp: any) =>
+          resp.result.map((item: any) => ({
+            label: item.name,
+            value: `accessId is ${item.id}`,
+          })),
+        ),
+    },
+    {
+      dataIndex: 'deviceType',
+      title: '设备类型',
+      valueType: 'select',
+      hideInTable: true,
+      valueEnum: {
+        device: {
+          text: '直连设备',
+          status: 'device',
+        },
+        childrenDevice: {
+          text: '网关子设备',
+          status: 'childrenDevice',
+        },
+        gateway: {
+          text: '网关设备',
+          status: 'gateway',
+        },
+      },
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.table.description',
+        defaultMessage: '说明',
+      }),
+      dataIndex: 'describe',
+      width: '15%',
+      ellipsis: true,
+      hideInSearch: true,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
+      width: 250,
+      fixed: 'right',
+      render: (text, record) => tools(record, 'list'),
+    },
+  ];
+
+  return (
+    <PageContainer>
+      <SearchComponent<DeviceInstance>
+        field={columns}
+        target="edge-device"
+        onSearch={(data) => {
+          actionRef.current?.reset?.();
+          setSearchParams(data);
+        }}
+      />
+      <ProTableCard<DeviceInstance>
+        columns={columns}
+        scroll={{ x: 1366 }}
+        actionRef={actionRef}
+        params={searchParams}
+        options={{ fullScreen: true }}
+        columnEmptyText={''}
+        request={(params) =>
+          service.query({
+            ...params,
+            terms: [
+              ...(params?.terms || []),
+              {
+                terms: [
+                  {
+                    column: 'productId$product-info',
+                    value: 'accessProvider is official-edge-gateway',
+                  },
+                ],
+                type: 'and',
+              },
+            ],
+            sorts: [
+              {
+                name: 'createTime',
+                order: 'desc',
+              },
+            ],
+          })
+        }
+        rowKey="id"
+        search={false}
+        pagination={{ pageSize: 10 }}
+        headerTitle={[
+          <PermissionButton
+            onClick={() => {
+              setVisible(true);
+              setCurrent({});
+            }}
+            style={{ marginRight: 12 }}
+            isPermission={permission.add}
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.add',
+              defaultMessage: '新增',
+            })}
+          </PermissionButton>,
+          <PermissionButton
+            isPermission={permission.import}
+            icon={<ImportOutlined />}
+            onClick={() => {
+              setImportVisible(true);
+            }}
+          >
+            导入
+          </PermissionButton>,
+        ]}
+        cardRender={(record) => (
+          <DeviceCard
+            {...record}
+            detail={
+              <div
+                style={{ padding: 8, fontSize: 24 }}
+                onClick={() => {
+                  InstanceModel.current = record;
+                  const url = getMenuPathByParams(MENUS_CODE['device/Instance/Detail'], record.id);
+                  history.push(url);
+                }}
+              >
+                <EyeOutlined />
+              </div>
+            }
+            actions={tools(record, 'card')}
+          />
+        )}
+      />
+      {visible && (
+        <Save
+          data={current}
+          model={!Object.keys(current).length ? 'add' : 'edit'}
+          close={() => {
+            setVisible(false);
+          }}
+          reload={() => {
+            actionRef.current?.reload();
+          }}
+        />
+      )}
+      <Import
+        // data={current}
+        type={'official-edge-gateway'}
+        close={() => {
+          setImportVisible(false);
+          actionRef.current?.reload();
+        }}
+        visible={importVisible}
+      />
+    </PageContainer>
+  );
+};

+ 23 - 0
src/pages/edge/Resource/Issue/index.tsx

@@ -0,0 +1,23 @@
+import { Modal } from 'antd';
+interface Props {
+  data: any;
+  cancel: () => void;
+}
+
+export default (props: Props) => {
+  return (
+    <Modal
+      open
+      title={'下发'}
+      onOk={() => {
+        props.cancel();
+      }}
+      onCancel={() => {
+        props.cancel();
+      }}
+      width={700}
+    >
+      下发
+    </Modal>
+  );
+};

+ 44 - 0
src/pages/edge/Resource/Save/index.tsx

@@ -0,0 +1,44 @@
+import { Modal } from 'antd';
+import MonacoEditor from 'react-monaco-editor';
+import { useState } from 'react';
+interface Props {
+  data: any;
+  cancel: () => void;
+}
+
+export default (props: Props) => {
+  const [monacoValue, setMonacoValue] = useState<string>(props.data);
+
+  const editorDidMountHandle = (editor: any) => {
+    editor.getAction('editor.action.formatDocument').run();
+    editor.onDidContentSizeChange?.(() => {
+      editor.getAction('editor.action.formatDocument').run();
+    });
+  };
+
+  return (
+    <Modal
+      open
+      title={'编辑'}
+      onOk={() => {
+        props.cancel();
+      }}
+      onCancel={() => {
+        props.cancel();
+      }}
+      width={700}
+    >
+      <MonacoEditor
+        width={'100%'}
+        height={400}
+        theme="vs-dark"
+        language={'json'}
+        value={monacoValue}
+        onChange={(newValue) => {
+          setMonacoValue(newValue);
+        }}
+        editorDidMount={editorDidMountHandle}
+      />
+    </Modal>
+  );
+};

+ 394 - 0
src/pages/edge/Resource/index.tsx

@@ -0,0 +1,394 @@
+import { PageContainer } from '@ant-design/pro-layout';
+// import {DeviceInstance} from "@/pages/device/Instance/typings";
+// import SearchComponent from "@/components/SearchComponent";
+// import {ActionType, ProColumns} from "@jetlinks/pro-table";
+// import moment from "moment";
+// import {Badge, Button, Tooltip} from "antd";
+// import {service as categoryService} from "@/pages/device/Category";
+// import {InstanceModel, service, statusMap} from "@/pages/device/Instance";
+// import {useIntl} from "@@/plugin-locale/localeExports";
+// import {
+//   DeleteOutlined,
+//   DownSquareOutlined,
+//   EditOutlined,
+//   EyeOutlined,
+//   PlayCircleOutlined,
+//   StopOutlined,
+// } from "@ant-design/icons";
+// import {PermissionButton, ProTableCard} from "@/components";
+// import {useRef, useState} from "react";
+// import DeviceCard from "@/components/ProTableCard/CardItems/device";
+// import {onlyMessage} from "@/utils/util";
+// import {getMenuPathByParams, MENUS_CODE} from "@/utils/menu";
+// import {useHistory} from "umi";
+// import Save from './Save';
+
+export default () => {
+  // const intl = useIntl();
+  // const actionRef = useRef<ActionType>();
+  // const [searchParams, setSearchParams] = useState<any>({});
+  // const [current, setCurrent] = useState<Partial<DeviceInstance>>({});
+  // const [visible, setVisible] = useState<boolean>(false);
+  // const history = useHistory<Record<string, string>>();
+
+  // const tools = (record: DeviceInstance, type: 'card' | 'list') => [
+  //   type === 'list' && <Button
+  //     type={'link'}
+  //     style={{ padding: 0 }}
+  //     key={'detail'}
+  //     onClick={() => {
+  //       InstanceModel.current = record;
+  //       const url = getMenuPathByParams(MENUS_CODE['device/Instance/Detail'], record.id);
+  //       history.push(url);
+  //     }}
+  //   >
+  //     <Tooltip
+  //       title={intl.formatMessage({
+  //         id: 'pages.data.option.detail',
+  //         defaultMessage: '查看',
+  //       })}
+  //     >
+  //       <EyeOutlined />
+  //     </Tooltip>
+  //   </Button>,
+  //   <PermissionButton
+  //     type={'link'}
+  //     isPermission={true}
+  //     onClick={() => {
+  //       setCurrent(record);
+  //       setVisible(true);
+  //     }}
+  //     tooltip={{
+  //       title: type === 'list' ? '编辑' : '',
+  //     }}
+  //     style={{ padding: 0 }}
+  //     key={'edit'}
+  //   >
+  //     <EditOutlined />
+  //     {type === 'list' ? '' : '编辑'}
+  //   </PermissionButton>,
+  //   <PermissionButton
+  //     type={'link'}
+  //     onClick={() => {
+  //     }}
+  //     tooltip={{
+  //       title: type !== 'list' ? '' : '下发'
+  //     }}
+  //     style={{ padding: 0 }}
+  //     isPermission={true}
+  //     key={'reset'}
+  //   >
+  //     <DownSquareOutlined />
+  //     {type === 'list' ? '' : '下发'}
+  //   </PermissionButton>,
+  //   <PermissionButton
+  //     type={'link'}
+  //     key={'state'}
+  //     style={{ padding: 0 }}
+  //     popConfirm={{
+  //       title: intl.formatMessage({
+  //         id: `pages.data.option.${
+  //           record.state.value !== 'notActive' ? 'disabled' : 'enabled'
+  //         }.tips`,
+  //         defaultMessage: '确认禁用?',
+  //       }),
+  //       onConfirm: () => {
+  //         if (record.state.value !== 'notActive') {
+  //           service.undeployDevice(record.id).then((resp: any) => {
+  //             if (resp.status === 200) {
+  //               onlyMessage(
+  //                 intl.formatMessage({
+  //                   id: 'pages.data.option.success',
+  //                   defaultMessage: '操作成功!',
+  //                 }),
+  //               );
+  //               actionRef.current?.reload();
+  //             }
+  //           });
+  //         } else {
+  //           service.deployDevice(record.id).then((resp: any) => {
+  //             if (resp.status === 200) {
+  //               onlyMessage(
+  //                 intl.formatMessage({
+  //                   id: 'pages.data.option.success',
+  //                   defaultMessage: '操作成功!',
+  //                 }),
+  //               );
+  //               actionRef.current?.reload();
+  //             }
+  //           });
+  //         }
+  //       },
+  //     }}
+  //     isPermission={true}
+  //     tooltip={{
+  //       title: intl.formatMessage({
+  //         id: `pages.data.option.${record.state.value !== 'notActive' ? 'disabled' : 'enabled'}`,
+  //         defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
+  //       }),
+  //     }}
+  //   >
+  //     {record.state.value !== 'notActive' ? <StopOutlined /> : <PlayCircleOutlined />}
+  //     {record.state.value !== 'notActive' ? (type === 'list' ? '' : '禁用') : (type === 'list' ? '' : '启用')}
+  //   </PermissionButton>,
+  //   <PermissionButton
+  //     type={'link'}
+  //     key={'delete'}
+  //     style={{ padding: 0 }}
+  //     isPermission={true}
+  //     tooltip={
+  //       record.state.value !== 'notActive'
+  //         ? { title: intl.formatMessage({ id: 'pages.device.instance.deleteTip' }) }
+  //         : undefined
+  //     }
+  //     disabled={record.state.value !== 'notActive'}
+  //     popConfirm={{
+  //       title: intl.formatMessage({
+  //         id: 'pages.data.option.remove.tips',
+  //       }),
+  //       disabled: record.state.value !== 'notActive',
+  //       onConfirm: async () => {
+  //         if (record.state.value === 'notActive') {
+  //           await service.remove(record.id);
+  //           onlyMessage(
+  //             intl.formatMessage({
+  //               id: 'pages.data.option.success',
+  //               defaultMessage: '操作成功!',
+  //             }),
+  //           );
+  //           actionRef.current?.reload();
+  //         } else {
+  //           onlyMessage(intl.formatMessage({ id: 'pages.device.instance.deleteTip' }), 'error');
+  //         }
+  //       },
+  //     }}
+  //   >
+  //     <DeleteOutlined />
+  //   </PermissionButton>,
+  // ];
+  // const columns: ProColumns<DeviceInstance>[] = [
+  //   {
+  //     title: 'ID',
+  //     dataIndex: 'id',
+  //     width: 200,
+  //     ellipsis: true,
+  //     fixed: 'left',
+  //   },
+  //   {
+  //     title: intl.formatMessage({
+  //       id: 'pages.table.deviceName',
+  //       defaultMessage: '设备名称',
+  //     }),
+  //     dataIndex: 'name',
+  //     ellipsis: true,
+  //     width: 200,
+  //   },
+  //   {
+  //     title: intl.formatMessage({
+  //       id: 'pages.table.productName',
+  //       defaultMessage: '产品名称',
+  //     }),
+  //     dataIndex: 'productId',
+  //     width: 200,
+  //     ellipsis: true,
+  //     valueType: 'select',
+  //     request: async () => {
+  //       const res = await service.getProductList();
+  //       if (res.status === 200) {
+  //         return res.result.map((pItem: any) => ({ label: pItem.name, value: pItem.id }));
+  //       }
+  //       return [];
+  //     },
+  //     render: (_, row) => row.productName,
+  //     filterMultiple: true,
+  //   },
+  //   {
+  //     title: intl.formatMessage({
+  //       id: 'pages.device.instance.registrationTime',
+  //       defaultMessage: '注册时间',
+  //     }),
+  //     dataIndex: 'registryTime',
+  //     width: '200px',
+  //     valueType: 'dateTime',
+  //     render: (_: any, row) => {
+  //       return row.registryTime ? moment(row.registryTime).format('YYYY-MM-DD HH:mm:ss') : '';
+  //     },
+  //     sorter: true,
+  //   },
+  //   {
+  //     title: intl.formatMessage({
+  //       id: 'pages.searchTable.titleStatus',
+  //       defaultMessage: '状态',
+  //     }),
+  //     dataIndex: 'state',
+  //     width: '90px',
+  //     valueType: 'select',
+  //     renderText: (record) =>
+  //       record ? <Badge status={statusMap.get(record.value)} text={record.text} /> : '',
+  //     valueEnum: {
+  //       notActive: {
+  //         text: intl.formatMessage({
+  //           id: 'pages.device.instance.status.notActive',
+  //           defaultMessage: '禁用',
+  //         }),
+  //         status: 'notActive',
+  //       },
+  //       offline: {
+  //         text: intl.formatMessage({
+  //           id: 'pages.device.instance.status.offLine',
+  //           defaultMessage: '离线',
+  //         }),
+  //         status: 'offline',
+  //       },
+  //       online: {
+  //         text: intl.formatMessage({
+  //           id: 'pages.device.instance.status.onLine',
+  //           defaultMessage: '在线',
+  //         }),
+  //         status: 'online',
+  //       },
+  //     },
+  //     filterMultiple: false,
+  //   },
+  //   {
+  //     dataIndex: 'classifiedId',
+  //     title: '产品分类',
+  //     valueType: 'treeSelect',
+  //     hideInTable: true,
+  //     fieldProps: {
+  //       fieldNames: {
+  //         label: 'name',
+  //         value: 'id',
+  //       },
+  //     },
+  //     request: () =>
+  //       categoryService
+  //         .queryTree({
+  //           paging: false,
+  //         })
+  //         .then((resp: any) => resp.result),
+  //   },
+  //   {
+  //     dataIndex: 'productId$product-info',
+  //     title: '接入方式',
+  //     valueType: 'select',
+  //     hideInTable: true,
+  //     request: () =>
+  //       service.queryGatewayList().then((resp: any) =>
+  //         resp.result.map((item: any) => ({
+  //           label: item.name,
+  //           value: `accessId is ${item.id}`,
+  //         })),
+  //       ),
+  //   },
+  //   {
+  //     dataIndex: 'deviceType',
+  //     title: '设备类型',
+  //     valueType: 'select',
+  //     hideInTable: true,
+  //     valueEnum: {
+  //       device: {
+  //         text: '直连设备',
+  //         status: 'device',
+  //       },
+  //       childrenDevice: {
+  //         text: '网关子设备',
+  //         status: 'childrenDevice',
+  //       },
+  //       gateway: {
+  //         text: '网关设备',
+  //         status: 'gateway',
+  //       },
+  //     },
+  //   },
+  //   {
+  //     title: intl.formatMessage({
+  //       id: 'pages.table.description',
+  //       defaultMessage: '说明',
+  //     }),
+  //     dataIndex: 'describe',
+  //     width: '15%',
+  //     ellipsis: true,
+  //     hideInSearch: true,
+  //   },
+  //   {
+  //     title: intl.formatMessage({
+  //       id: 'pages.data.option',
+  //       defaultMessage: '操作',
+  //     }),
+  //     valueType: 'option',
+  //     width: 250,
+  //     fixed: 'right',
+  //     render: (text, record) => tools(record, 'list'),
+  //   },
+  // ];
+
+  return (
+    <PageContainer>
+      开发中。。。。
+      {/*<SearchComponent<DeviceInstance>*/}
+      {/*  field={columns}*/}
+      {/*  target="edge-resource"*/}
+      {/*  onSearch={(data) => {*/}
+      {/*    actionRef.current?.reset?.();*/}
+      {/*    setSearchParams(data);*/}
+      {/*  }}*/}
+      {/*/>*/}
+      {/*<ProTableCard<DeviceInstance>*/}
+      {/*  columns={columns}*/}
+      {/*  scroll={{ x: 1366 }}*/}
+      {/*  actionRef={actionRef}*/}
+      {/*  params={searchParams}*/}
+      {/*  options={{ fullScreen: true }}*/}
+      {/*  columnEmptyText={''}*/}
+      {/*  request={(params) =>*/}
+      {/*    service.query({*/}
+      {/*      ...params,*/}
+      {/*      terms: [*/}
+      {/*        ...(params?.terms || []),*/}
+      {/*        {*/}
+      {/*          terms: [*/}
+      {/*            {*/}
+      {/*              column: 'productId$product-info',*/}
+      {/*              value: 'accessProvider is official-edge-gateway',*/}
+      {/*            },*/}
+      {/*          ],*/}
+      {/*          type: 'and',*/}
+      {/*        },*/}
+      {/*      ],*/}
+      {/*      sorts: [*/}
+      {/*        {*/}
+      {/*          name: 'createTime',*/}
+      {/*          order: 'desc',*/}
+      {/*        },*/}
+      {/*      ],*/}
+      {/*    })*/}
+      {/*  }*/}
+      {/*  rowKey="id"*/}
+      {/*  search={false}*/}
+      {/*  pagination={{ pageSize: 10 }}*/}
+      {/*  cardRender={(record) => (*/}
+      {/*    <DeviceCard*/}
+      {/*      {...record}*/}
+      {/*      detail={*/}
+      {/*        <div*/}
+      {/*          style={{ padding: 8, fontSize: 24 }}*/}
+      {/*          onClick={() => {*/}
+      {/*            InstanceModel.current = record;*/}
+      {/*            const url = getMenuPathByParams(MENUS_CODE['device/Instance/Detail'], record.id);*/}
+      {/*            history.push(url);*/}
+      {/*          }}*/}
+      {/*        >*/}
+      {/*          <EyeOutlined />*/}
+      {/*        </div>*/}
+      {/*      }*/}
+      {/*      actions={tools(record, 'card')}*/}
+      {/*    />*/}
+      {/*  )}*/}
+      {/*/>*/}
+      {/*{*/}
+      {/*  visible && <Save data={current} cancel={() => {setVisible(false)}} />*/}
+      {/*}*/}
+    </PageContainer>
+  );
+};

+ 0 - 0
src/pages/edge/Resource/service.ts


+ 74 - 0
src/pages/edge/Resource/typings.d.ts

@@ -0,0 +1,74 @@
+export type DeviceInstance = {
+  id: string;
+  name: string;
+  describe: string;
+  description: string;
+  productId: string;
+  productName: string;
+  protocolName: string;
+  security: any;
+  deriveMetadata: string;
+  metadata: string;
+  binds: any;
+  state: {
+    value: string;
+    text: string;
+  };
+  creatorId: string;
+  creatorName: string;
+  createTime: number;
+  registryTime: string;
+  disabled?: boolean;
+  aloneConfiguration?: boolean;
+  deviceType: {
+    value: string;
+    text: string;
+  };
+  transportProtocol: string;
+  messageProtocol: string;
+  orgId: string;
+  orgName: string;
+  configuration: Record<string, any>;
+  relations?: any[];
+  cachedConfiguration: any;
+  transport: string;
+  protocol: string;
+  address: string;
+  registerTime: number;
+  onlineTime: string | number;
+  offlineTime: string | number;
+  tags: any;
+  photoUrl: string;
+  independentMetadata?: boolean;
+  accessProvider?: string;
+  accessId?: string;
+  features?: any[];
+  parentId?: string;
+  classifiedName?: string;
+};
+
+type Unit = {
+  id: string;
+  name: string;
+  symbol: string;
+  text: string;
+  type: string;
+  value: string;
+  description: string;
+};
+
+type PropertyData = {
+  data: {
+    value?:
+      | {
+          formatValue: string;
+          property: string;
+          value: any;
+        }
+      | any;
+    timeString: string;
+    timestamp: number;
+    formatValue: string;
+    property: string;
+  };
+};

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

@@ -2318,6 +2318,110 @@ export default [
           },
         ],
       },
+      {
+        code: 'edge',
+        name: '边缘网关',
+        owner: 'iot',
+        sortIndex: 8,
+        url: '/iot/edge',
+        icon: 'icon-zidingyiguize',
+        permissions: [],
+        buttons: [],
+        children: [
+          {
+            code: 'edge/Device',
+            name: '网关设备',
+            owner: 'iot',
+            sortIndex: 1,
+            url: '/iot/edge/Devic',
+            icon: 'icon-changjingliandong',
+            showPage: ['device-instance'],
+            permissions: [],
+            buttons: [
+              {
+                id: 'view',
+                name: '查看',
+                permissions: [
+                  {
+                    permission: 'device-instance',
+                    actions: ['query'],
+                  },
+                ],
+              },
+              {
+                id: 'action',
+                name: '启/禁用',
+                permissions: [
+                  {
+                    permission: 'device-instance',
+                    actions: ['query', 'save'],
+                  },
+                ],
+              },
+              {
+                id: 'delete',
+                name: '删除',
+                permissions: [
+                  {
+                    permission: 'device-instance',
+                    actions: ['query', 'delete'],
+                  },
+                ],
+              },
+              {
+                id: 'update',
+                name: '编辑',
+                permissions: [
+                  {
+                    permission: 'rule-instance',
+                    actions: ['query', 'save'],
+                  },
+                ],
+              },
+              {
+                id: 'add',
+                name: '新增',
+                permissions: [
+                  {
+                    permission: 'rule-instance',
+                    actions: ['query', 'save'],
+                  },
+                ],
+              },
+              {
+                id: 'import',
+                name: '导入',
+                permissions: [
+                  {
+                    permission: 'rule-instance',
+                    actions: ['save'],
+                  },
+                ],
+              },
+              {
+                id: 'setting',
+                name: '远程控制',
+                permissions: [
+                  {
+                    permission: 'rule-instance',
+                    actions: ['save'],
+                  },
+                ],
+              },
+              {
+                id: 'password',
+                name: '重置密码',
+                permissions: [
+                  {
+                    permission: 'rule-instance',
+                    actions: ['save'],
+                  },
+                ],
+              },
+            ],
+          },
+        ],
+      },
     ],
   },
 

+ 2 - 2
src/utils/menu/router.ts

@@ -30,8 +30,6 @@ export enum MENUS_CODE {
   'device/components/Metadata/Cat' = 'device/components/Metadata/Cat',
   'device/components/Metadata/Import' = 'device/components/Metadata/Import',
   'device/components/Metadata' = 'device/components/Metadata',
-  'edge/Device' = 'edge/Device',
-  'edge/Product' = 'edge/Product',
   'link/Certificate' = 'link/Certificate',
   'link/Certificate/Detail' = 'link/Certificate/Detail',
   'link/Gateway' = 'link/Gateway',
@@ -42,6 +40,8 @@ export enum MENUS_CODE {
   'link/DataCollect/Dashboard' = 'link/DataCollect/Dashboard',
   'link/DataCollect/DataGathering' = 'link/DataCollect/DataGathering',
   'link/DataCollect/IntegratedQuery' = 'link/DataCollect/IntegratedQuery',
+  'edge/Device' = 'edge/Device',
+  'edge/Resource' = 'edge/Resource',
   'Log' = 'Log',
   'media/Cascade' = 'media/Cascade',
   'media/Cascade/Save' = 'media/Cascade/Save',