Browse Source

fix: merge

wzyyy 3 năm trước cách đây
mục cha
commit
7323f9c9ba
35 tập tin đã thay đổi với 1607 bổ sung136 xóa
  1. 31 16
      src/components/ProTableCard/CardItems/Scene/index.tsx
  2. 1 1
      src/components/ProTableCard/CardItems/ruleInstance.tsx
  3. 8 1
      src/components/SearchComponent/index.tsx
  4. 9 3
      src/pages/device/Instance/Import/index.tsx
  5. 2 1
      src/pages/device/Instance/index.tsx
  6. 5 0
      src/pages/device/Instance/service.ts
  7. 291 0
      src/pages/edge/Device/Save/index.tsx
  8. 478 0
      src/pages/edge/Device/index.tsx
  9. 23 0
      src/pages/edge/Resource/Issue/index.tsx
  10. 44 0
      src/pages/edge/Resource/Save/index.tsx
  11. 394 0
      src/pages/edge/Resource/index.tsx
  12. 0 0
      src/pages/edge/Resource/service.ts
  13. 74 0
      src/pages/edge/Resource/typings.d.ts
  14. 6 3
      src/pages/link/DataCollect/components/Point/Save/modbus.tsx
  15. 30 30
      src/pages/link/DataCollect/components/Point/index.tsx
  16. 3 2
      src/pages/rule-engine/Scene/Save/action/ListItem/Item.tsx
  17. 1 1
      src/pages/rule-engine/Scene/Save/action/Modal/add.tsx
  18. 2 1
      src/pages/rule-engine/Scene/Save/action/TriggerAlarm/index.tsx
  19. 1 1
      src/pages/rule-engine/Scene/Save/action/notify/components/notifyType/index.less
  20. 1 1
      src/pages/rule-engine/Scene/Save/action/notify/index.tsx
  21. 11 19
      src/pages/rule-engine/Scene/Save/components/TimingTrigger/RangePicker.tsx
  22. 2 1
      src/pages/rule-engine/Scene/Save/components/TimingTrigger/index.less
  23. 1 1
      src/pages/rule-engine/Scene/Save/components/TimingTrigger/index.tsx
  24. 0 8
      src/pages/rule-engine/Scene/Save/components/TriggerWay/actionsType.tsx
  25. 1 2
      src/pages/rule-engine/Scene/Save/components/TriggerWay/index.less
  26. 29 14
      src/pages/rule-engine/Scene/Save/device/addModel.tsx
  27. 4 2
      src/pages/rule-engine/Scene/Save/device/device.tsx
  28. 1 0
      src/pages/rule-engine/Scene/Save/device/deviceList.tsx
  29. 1 1
      src/pages/rule-engine/Scene/Save/device/index.less
  30. 21 2
      src/pages/rule-engine/Scene/Save/device/index.tsx
  31. 1 0
      src/pages/rule-engine/Scene/Save/device/org.tsx
  32. 11 21
      src/pages/rule-engine/Scene/Save/device/product.tsx
  33. 14 2
      src/pages/rule-engine/Scene/Save/terms/branchItem.tsx
  34. 104 0
      src/pages/system/Menu/Setting/baseMenu.ts
  35. 2 2
      src/utils/menu/router.ts

+ 31 - 16
src/components/ProTableCard/CardItems/Scene/index.tsx

@@ -276,8 +276,12 @@ const actionRender = (action: ActionsType, index: number) => {
 };
 
 const conditionsRender = (when: any[], index: number) => {
-  if (when.length) {
-    return (when[index]?.terms || []).join('');
+  if (when.length && when[index]) {
+    let str: string = '';
+    (when[index]?.terms || []).map((i: any) => {
+      str += `${i?.terms[0] || ''}${i?.termType || ''}`;
+    });
+    return str;
   }
   return '';
 };
@@ -342,10 +346,16 @@ const ContentRender = (data: SceneCardProps) => {
                   <div className={styles['card-item-content-action-item-right']}>
                     <div className={styles['card-item-content-action-item-right-item']}>
                       {type === 'device' && (
-                        <div className={styles['right-item-left']}>
-                          <MyTooltip title={conditionsRender(data.options?.terms || [], index)}>
+                        <div
+                          className={styles['right-item-left']}
+                          style={{ width: item?.then && item?.then.length ? '15%' : '100%' }}
+                        >
+                          <MyTooltip
+                            placement={'topLeft'}
+                            title={conditionsRender(data.options?.when || [], index)}
+                          >
                             <div className={classNames(styles['trigger-conditions'], 'ellipsis')}>
-                              {conditionsRender(data.options?.terms || [], index)}
+                              {conditionsRender(data.options?.when || [], index)}
                             </div>
                           </MyTooltip>
                           {item.shakeLimit?.enabled && (
@@ -361,18 +371,23 @@ const ContentRender = (data: SceneCardProps) => {
                           )}
                         </div>
                       )}
-                      <div className={styles['right-item-right']}>
-                        {(item?.then || []).map((i: BranchesThen, _index: number) => (
-                          <div key={i?.key || _index} className={styles['right-item-right-item']}>
-                            <div className={styles['trigger-ways']}>
-                              {i.parallel ? '并行执行' : '串行执行'}
-                            </div>
-                            <div className={classNames(styles['right-item-right-item-contents'])}>
-                              {branchesActionRender(Array.isArray(i?.actions) ? i?.actions : [])}
+                      {item?.then && item?.then.length && (
+                        <div
+                          className={styles['right-item-right']}
+                          style={{ width: type === 'device' ? '85%' : '15%' }}
+                        >
+                          {(item?.then || []).map((i: BranchesThen, _index: number) => (
+                            <div key={i?.key || _index} className={styles['right-item-right-item']}>
+                              <div className={styles['trigger-ways']}>
+                                {i.parallel ? '并行执行' : '串行执行'}
+                              </div>
+                              <div className={classNames(styles['right-item-right-item-contents'])}>
+                                {branchesActionRender(Array.isArray(i?.actions) ? i?.actions : [])}
+                              </div>
                             </div>
-                          </div>
-                        ))}
-                      </div>
+                          ))}
+                        </div>
+                      )}
                     </div>
                   </div>
                 </div>

+ 1 - 1
src/components/ProTableCard/CardItems/ruleInstance.tsx

@@ -10,7 +10,7 @@ export interface RuleInstanceCardProps extends InstanceItem {
   avatarSize?: number;
 }
 
-const defaultImage = require('/public/images/device-type-3-big.png');
+const defaultImage = require('/public/images/scene/trigger-type/scene.png');
 
 export default (props: RuleInstanceCardProps) => {
   return (

+ 8 - 1
src/components/SearchComponent/index.tsx

@@ -72,6 +72,8 @@ interface Props<T> {
   model?: 'simple' | 'advance';
   enableSave?: boolean;
   initParam?: SearchTermsServer;
+  style?: React.CSSProperties;
+  bodyStyle?: React.CSSProperties;
 }
 
 const termType = [
@@ -790,7 +792,12 @@ const SearchComponent = <T extends Record<string, any>>(props: Props<T>) => {
   );
 
   return (
-    <Card bordered={false} className={styles.container}>
+    <Card
+      bordered={false}
+      className={styles.container}
+      style={props.style}
+      bodyStyle={props.bodyStyle}
+    >
       <Form
         form={form}
         className={styles.form}

+ 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;
+  };
+};

+ 6 - 3
src/pages/link/DataCollect/components/Point/Save/modbus.tsx

@@ -34,6 +34,9 @@ export default (props: Props) => {
       accessModes: props.data?.accessModes
         ? (props.data?.accessModes || []).map((item) => item.value)
         : [],
+      features: props.data?.features
+        ? (props.data?.features || []).map((item: any) => item?.value)
+        : [],
     });
   }, [props.data]);
 
@@ -155,9 +158,9 @@ export default (props: Props) => {
               placeholder: '请选择功能码',
             },
             enum: [
-              { label: '线圈寄存器', value: 'Coils' },
-              { label: '保存寄存器', value: 'HoldingRegisters' },
-              { label: '输入寄存器', value: 'DiscreteInputs' },
+              { label: '01线圈寄存器', value: 'Coils' },
+              { label: '03保存寄存器', value: 'HoldingRegisters' },
+              { label: '04输入寄存器', value: 'DiscreteInputs' },
             ],
             'x-validator': [
               {

+ 30 - 30
src/pages/link/DataCollect/components/Point/index.tsx

@@ -394,21 +394,21 @@ export default observer((props: Props) => {
             },
           },
         },
-        {
-          title: '状态',
-          dataIndex: 'state',
-          valueType: 'select',
-          valueEnum: {
-            enabled: {
-              text: '正常',
-              status: 'enabled',
-            },
-            disabled: {
-              text: '禁用',
-              status: 'disabled',
-            },
-          },
-        },
+        // {
+        //   title: '状态',
+        //   dataIndex: 'state',
+        //   valueType: 'select',
+        //   valueEnum: {
+        //     enabled: {
+        //       text: '正常',
+        //       status: 'enabled',
+        //     },
+        //     disabled: {
+        //       text: '禁用',
+        //       status: 'disabled',
+        //     },
+        //   },
+        // },
         {
           title: '运行状态',
           dataIndex: 'runningState',
@@ -473,21 +473,21 @@ export default observer((props: Props) => {
                   },
                 },
         },
-        {
-          title: '状态',
-          dataIndex: 'state',
-          valueType: 'select',
-          valueEnum: {
-            enabled: {
-              text: '正常',
-              status: 'enabled',
-            },
-            disabled: {
-              text: '禁用',
-              status: 'disabled',
-            },
-          },
-        },
+        // {
+        //   title: '状态',
+        //   dataIndex: 'state',
+        //   valueType: 'select',
+        //   valueEnum: {
+        //     enabled: {
+        //       text: '正常',
+        //       status: 'enabled',
+        //     },
+        //     disabled: {
+        //       text: '禁用',
+        //       status: 'disabled',
+        //     },
+        //   },
+        // },
         {
           title: '运行状态',
           dataIndex: 'runningState',

+ 3 - 2
src/pages/rule-engine/Scene/Save/action/ListItem/Item.tsx

@@ -186,10 +186,11 @@ export default (props: ItemProps) => {
     // console.log('props.data', props.data)
     if (props?.data?.alarm?.mode === 'trigger') {
       return (
-        <div>
+        <div className={'item-options-content'}>
           满足条件后将触发关联
           <a
-            onClick={() => {
+            onClick={(e) => {
+              e.stopPropagation();
               setTriggerVisible(true);
             }}
           >

+ 1 - 1
src/pages/rule-engine/Scene/Save/action/Modal/add.tsx

@@ -105,7 +105,7 @@ export default (props: Props) => {
     <Modal
       title="类型"
       open
-      width={800}
+      width={860}
       onCancel={() => {
         props.close();
       }}

+ 2 - 1
src/pages/rule-engine/Scene/Save/action/TriggerAlarm/index.tsx

@@ -143,11 +143,12 @@ export default (props: Props) => {
         props.close();
       }}
     >
-      <div>关联告警数量:{count}</div>
+      <div style={{ marginBottom: 24 }}>关联告警数量:{count}</div>
       <ProTable<ConfigurationItem>
         actionRef={actionRef}
         params={{}}
         columns={columns}
+        toolBarRender={false}
         search={false}
         rowKey={'id'}
         columnEmptyText={''}

+ 1 - 1
src/pages/rule-engine/Scene/Save/action/notify/components/notifyType/index.less

@@ -3,7 +3,7 @@
 .notify-type-warp {
   display: flex;
   flex-wrap: wrap;
-  gap: 8px;
+  gap: 16px 24px;
   width: 100%;
 
   .notify-type-item {

+ 1 - 1
src/pages/rule-engine/Scene/Save/action/notify/index.tsx

@@ -132,7 +132,7 @@ export default observer((props: Props) => {
     <Modal
       title={'执行动作'}
       open
-      width={800}
+      width={810}
       onCancel={() => {
         props.cancel();
         NotifyModel.current = 0;

+ 11 - 19
src/pages/rule-engine/Scene/Save/components/TimingTrigger/RangePicker.tsx

@@ -1,5 +1,6 @@
 import { TimePicker } from 'antd';
 import moment from 'moment';
+import type { FormInstance } from 'antd';
 
 type RangePickerValue = {
   from: string;
@@ -12,7 +13,8 @@ interface RangePickerProps {
   value?: RangePickerValue;
   onChange?: (value: RangePickerValue) => void;
   id?: string;
-  form?: any;
+  form?: FormInstance<any>;
+  name: (string | number)[];
 }
 export default (props: RangePickerProps) => {
   return (
@@ -26,24 +28,14 @@ export default (props: RangePickerProps) => {
       ]}
       onChange={(_, dateString) => {
         if (props.onChange) {
-          if (props.form.getFieldsValue().trigger.type === 'timer') {
-            const { every, unit } = props.form.getFieldsValue().trigger?.timer?.period;
-            props.onChange({
-              from: dateString[0],
-              to: dateString[1],
-              every: every,
-              unit: unit,
-            });
-          } else {
-            const { every, unit } =
-              props.form.getFieldsValue().trigger?.device?.operation?.timer?.period;
-            props.onChange({
-              from: dateString[0],
-              to: dateString[1],
-              every: every,
-              unit: unit,
-            });
-          }
+          const { every, unit } = props.form?.getFieldValue([...props.name]);
+          console.log(every, unit);
+          props.onChange({
+            from: dateString[0],
+            to: dateString[1],
+            every: every,
+            unit: unit,
+          });
         }
       }}
     />

+ 2 - 1
src/pages/rule-engine/Scene/Save/components/TimingTrigger/index.less

@@ -1,7 +1,7 @@
 .timer-when-warp {
   display: flex;
   flex-wrap: wrap;
-  gap: 16px;
+  gap: 8px 16px;
   padding: 16px;
   background: #fafafa;
 }
@@ -10,6 +10,7 @@
   width: 76px;
   padding: 6px 0;
   text-align: center;
+  background: #fff;
   border: 1px solid #e6e6e6;
   border-radius: 2px;
   cursor: pointer;

+ 1 - 1
src/pages/rule-engine/Scene/Save/components/TimingTrigger/index.tsx

@@ -129,7 +129,7 @@ export default (props: TimmingTriggerProps) => {
                 to: moment(new Date()).format('HH:mm:ss'),
               }}
             >
-              <RangePicker form={form} />
+              <RangePicker name={[...name, 'period']} form={form} />
             </Form.Item>
             <Form.Item> 每 </Form.Item>
             <Form.Item

+ 0 - 8
src/pages/rule-engine/Scene/Save/components/TriggerWay/actionsType.tsx

@@ -1,7 +1,6 @@
 import { useEffect, useState } from 'react';
 import classNames from 'classnames';
 import './index.less';
-// import { ParallelType } from '../../../typings';
 
 interface ActionsTypeProps {
   value?: string;
@@ -9,16 +8,9 @@ interface ActionsTypeProps {
   onChange?: (type: string) => void;
   onSelect?: (type: string) => void;
   disabled?: boolean;
-  // type: ParallelType;
   parallel: boolean; //并行true
 }
 
-export enum ActionsTypeEnum {
-  manual = 'manual',
-  timing = 'timer',
-  device = 'device',
-}
-
 const TypeList = [
   {
     label: '设备输出',

+ 1 - 2
src/pages/rule-engine/Scene/Save/components/TriggerWay/index.less

@@ -3,13 +3,12 @@
 .trigger-way-warp {
   display: flex;
   flex-wrap: wrap;
-  gap: 8px;
+  gap: 16px 24px;
   width: 100%;
 
   .trigger-way-item {
     display: flex;
     justify-content: space-between;
-    // width: 100%;
     padding: 16px;
     border: 1px solid #e0e4e8;
     border-radius: 2px;

+ 29 - 14
src/pages/rule-engine/Scene/Save/device/addModel.tsx

@@ -9,6 +9,9 @@ import Device from './device';
 import Type from './type';
 import { numberToString } from '../components/TimingTrigger/whenOption';
 import { timeUnitEnum } from '../components/TimingTrigger';
+import { Store } from 'jetlinks-store';
+import { FormModel } from '@/pages/rule-engine/Scene/Save';
+import { isEqual } from 'lodash';
 
 interface AddProps {
   options?: any;
@@ -43,7 +46,7 @@ const defaultModelData: Omit<DeviceModelProps, 'steps'> = {
   productDetail: {},
   deviceKeys: [],
   orgId: '',
-  selector: 'custom',
+  selector: 'fixed',
   metadata: {},
   operation: {
     operator: 'online',
@@ -80,12 +83,19 @@ export default observer((props: AddProps) => {
     TriggerDeviceModel.stepNumber = 0;
     return () => {
       Object.keys(defaultModelData).forEach((key) => {
-        TriggerDeviceModel[key] = defaultModelData[key];
+        if (
+          ['selector', 'productId', 'selectorValues', 'operation', 'deviceKeys', 'orgId'].includes(
+            key,
+          )
+        ) {
+          TriggerDeviceModel[key] = defaultModelData[key];
+        }
       });
     };
   }, []);
 
   useEffect(() => {
+    console.log('productPage', props.value);
     if (props.value) {
       TriggerDeviceModel.selector = props.value.selector;
       TriggerDeviceModel.productId = props.value.productId;
@@ -93,9 +103,11 @@ export default observer((props: AddProps) => {
       TriggerDeviceModel.selectorValues = props.value.selectorValues;
       TriggerDeviceModel.operation = props.value.operation;
       TriggerDeviceModel.deviceKeys =
-        props.value.selector === 'custom'
+        props.value.selector === 'fixed'
           ? props.value.selectorValues?.map((item) => item.value) || []
           : [];
+      TriggerDeviceModel.orgId =
+        props.value.selector === 'org' ? props.value.selectorValues?.[0].value : [];
     }
   }, [props.value]);
 
@@ -128,7 +140,7 @@ export default observer((props: AddProps) => {
       extraTime: undefined,
       action: TriggerDeviceModel.options.action,
     };
-    if (TriggerDeviceModel.selector === 'custom') {
+    if (TriggerDeviceModel.selector === 'fixed') {
       _options.name = TriggerDeviceModel.selectorValues?.map((item) => item.name).join(',');
     } else if (TriggerDeviceModel.selector === 'org') {
       _options.name = TriggerDeviceModel.selectorValues?.[0].name + '的';
@@ -184,7 +196,7 @@ export default observer((props: AddProps) => {
         onlyMessage('请选择产品', 'error');
       }
     } else if (TriggerDeviceModel.stepNumber === 1) {
-      if (TriggerDeviceModel.selector === 'custom' && !TriggerDeviceModel.selectorValues?.length) {
+      if (TriggerDeviceModel.selector === 'fixed' && !TriggerDeviceModel.selectorValues?.length) {
         onlyMessage('请选择设备', 'error');
         return;
       } else if (
@@ -204,15 +216,18 @@ export default observer((props: AddProps) => {
         _options.productPageSize = TriggerDeviceModel.productPageSize;
         _options.devicePage = TriggerDeviceModel.devicePage;
         _options.devicePageSize = TriggerDeviceModel.devicePageSize;
-        props.onSave?.(
-          {
-            operation: operationData,
-            selectorValues: TriggerDeviceModel.selectorValues,
-            selector: TriggerDeviceModel.selector!,
-            productId: TriggerDeviceModel.productId,
-          },
-          _options,
-        );
+        const saveData = {
+          operation: operationData,
+          selectorValues: TriggerDeviceModel.selectorValues,
+          selector: TriggerDeviceModel.selector!,
+          productId: TriggerDeviceModel.productId,
+        };
+        const isUpdate = isEqual(saveData, FormModel.current.trigger?.device);
+        Store.set('TriggerDeviceModel', {
+          update: !isUpdate,
+        });
+        console.log('isUpdate', isUpdate);
+        props.onSave?.(saveData, _options);
       }
     }
   };

+ 4 - 2
src/pages/rule-engine/Scene/Save/device/device.tsx

@@ -9,7 +9,7 @@ import OrgList from './org';
 const TypeList = [
   {
     label: '自定义',
-    value: 'custom',
+    value: 'fixed',
     image: require('/public/images/scene/device-custom.png'),
     tip: '自定义选择当前产品下的任意设备',
   },
@@ -34,6 +34,8 @@ export default observer(() => {
 
   useEffect(() => {
     if (form) {
+      console.log('TriggerDeviceModel.selector', TriggerDeviceModel.selector);
+
       form.setFieldsValue({ selector: TriggerDeviceModel.selector });
     }
   }, []);
@@ -51,7 +53,7 @@ export default observer(() => {
           <TopCard typeList={TypeList} />
         </Form.Item>
       </Form>
-      {selector === 'custom' ? <DeviceList /> : selector === 'org' ? <OrgList /> : null}
+      {selector === 'fixed' ? <DeviceList /> : selector === 'org' ? <OrgList /> : null}
     </div>
   );
 });

+ 1 - 0
src/pages/rule-engine/Scene/Save/device/deviceList.tsx

@@ -225,6 +225,7 @@ export default observer(() => {
         field={columns}
         model={'simple'}
         enableSave={false}
+        bodyStyle={{ padding: 0, paddingBottom: 16 }}
         onSearch={async (data) => {
           if (loading) {
             setSearchParam({

+ 1 - 1
src/pages/rule-engine/Scene/Save/device/index.less

@@ -14,7 +14,7 @@
 .trigger-way-warp {
   display: flex;
   flex-wrap: wrap;
-  gap: 8px;
+  gap: 16px 24px;
   width: 100%;
 
   .trigger-way-item {

+ 21 - 2
src/pages/rule-engine/Scene/Save/device/index.tsx

@@ -1,4 +1,4 @@
-import type { ReactChild } from 'react';
+import { ReactChild, useEffect } from 'react';
 import Terms from '@/pages/rule-engine/Scene/Save/terms';
 import { AddButton } from '@/pages/rule-engine/Scene/Save/components/Buttons';
 import { useState } from 'react';
@@ -7,16 +7,35 @@ import AddModel from './addModel';
 import { FormModel } from '@/pages/rule-engine/Scene/Save';
 import classNames from 'classnames';
 import { observer } from '@formily/reactive-react';
+import { service } from '@/pages/device/Product/index';
+import { Store } from 'jetlinks-store';
+import { TriggerDeviceModel } from './addModel';
+import { handleMetadata } from './product';
 
 const defaultDeviceValue = {
   productId: '',
-  selector: 'custom',
+  selector: 'fixed',
   selectorValues: [],
 };
 
 export default observer(() => {
   const [visible, setVisible] = useState(false);
 
+  useEffect(() => {
+    if (FormModel.current.trigger!.device?.productId) {
+      service.detail(FormModel.current.trigger!.device?.productId).then((res) => {
+        if (res.status === 200) {
+          TriggerDeviceModel.productDetail = res.result;
+          handleMetadata(res.result.metadata);
+        } else {
+          Store.set('TriggerDeviceModel', {
+            update: true,
+          });
+        }
+      });
+    }
+  }, [FormModel.current.trigger!.device?.productId]);
+
   const handleLabel = (options: any): ReactChild | ReactChild[] => {
     console.log('FormModel', options);
     if (!options || !Object.keys(options).length) return '点击配置设备触发';

+ 1 - 0
src/pages/rule-engine/Scene/Save/device/org.tsx

@@ -34,6 +34,7 @@ export default observer(() => {
         onSearch={(data) => {
           setParam(data);
         }}
+        bodyStyle={{ padding: 0, paddingBottom: 16 }}
         model={'simple'}
         target="scene-triggrt-device-category"
       />

+ 11 - 21
src/pages/rule-engine/Scene/Save/device/product.tsx

@@ -1,7 +1,7 @@
 import { ProTableCard } from '@/components';
 import SearchComponent from '@/components/SearchComponent';
 import type { ProductItem } from '@/pages/device/Product/typings';
-import { useEffect, useRef, useState } from 'react';
+import { useRef, useState } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import { service } from '@/pages/device/Product/index';
 import { SceneProductCard } from '@/components/ProTableCard/CardItems/product';
@@ -12,6 +12,14 @@ import { service as deptService } from '@/pages/system/Department';
 import { TriggerDeviceModel } from './addModel';
 import { observer } from '@formily/reactive-react';
 
+export const handleMetadata = (metadata?: string) => {
+  try {
+    TriggerDeviceModel.metadata = JSON.parse(metadata || '{}');
+  } catch (error) {
+    TriggerDeviceModel.metadata = {};
+  }
+};
+
 export default observer(() => {
   const actionRef = useRef<ActionType>();
   const intl = useIntl();
@@ -22,25 +30,6 @@ export default observer(() => {
 
   const [loading, setLoading] = useState(true);
 
-  const handleMetadata = (metadata?: string) => {
-    try {
-      TriggerDeviceModel.metadata = JSON.parse(metadata || '{}');
-    } catch (error) {
-      TriggerDeviceModel.metadata = {};
-    }
-  };
-
-  useEffect(() => {
-    if (TriggerDeviceModel.productId && !TriggerDeviceModel.productDetail.id) {
-      service.detail(TriggerDeviceModel.productId).then((res) => {
-        if (res.status === 200) {
-          TriggerDeviceModel.productDetail = res.result;
-          handleMetadata(res.result.metadata);
-        }
-      });
-    }
-  }, [TriggerDeviceModel.productId]);
-
   const columns: ProColumns<ProductItem>[] = [
     {
       title: 'ID',
@@ -219,6 +208,7 @@ export default observer(() => {
         field={columns}
         model={'simple'}
         enableSave={false}
+        bodyStyle={{ padding: 0, paddingBottom: 16 }}
         onSearch={async (data) => {
           if (loading) {
             setSearchParam({
@@ -260,7 +250,7 @@ export default observer(() => {
               // 初始化选择设备类型以及触发类型
               TriggerDeviceModel.deviceKeys = [];
               TriggerDeviceModel.orgId = '';
-              TriggerDeviceModel.selector = 'custom';
+              TriggerDeviceModel.selector = 'fixed';
               TriggerDeviceModel.operation = {
                 operator: 'online',
               };

+ 14 - 2
src/pages/rule-engine/Scene/Save/terms/branchItem.tsx

@@ -7,7 +7,7 @@ import Term from './term';
 import Actions from '@/pages/rule-engine/Scene/Save/action';
 import classNames from 'classnames';
 import { set } from 'lodash';
-
+import { Store } from 'jetlinks-store';
 interface BranchesItemProps {
   name: number;
   data: ActionBranchesProps;
@@ -18,6 +18,16 @@ interface BranchesItemProps {
 
 export default observer((props: BranchesItemProps) => {
   const [when, setWhen] = useState<TermsType[]>([]);
+  const [error, setError] = useState(false);
+
+  useEffect(() => {
+    Store.subscribe('TriggerDeviceModel', (data) => {
+      console.log('Store', data);
+      if (data.update) {
+        setError(true);
+      }
+    });
+  }, []);
 
   useEffect(() => {
     if (props.data.when) {
@@ -62,7 +72,9 @@ export default observer((props: BranchesItemProps) => {
   return (
     <div className="actions-terms-warp">
       <div className="actions-terms-title">{props.isFrist ? '当' : '否则'}</div>
-      <div className={classNames('actions-terms-options', { border: !props.isFrist })}>
+      <div
+        className={classNames('actions-terms-options', { border: !props.isFrist, error: error })}
+      >
         {!props.isFrist && props.data.when.length ? (
           <div className={classNames('terms-params-delete denger show')} onClick={props.onDelete}>
             <DeleteOutlined />

+ 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',