sun-chaochao před 3 roky
rodič
revize
67b45d78b5
39 změnil soubory, kde provedl 990 přidání a 323 odebrání
  1. 16 0
      config/routes.ts
  2. 3 2
      src/components/ProTableCard/CardItems/duerOs.tsx
  3. 1 0
      src/pages/Northbound/AliCloud/service.ts
  4. 31 0
      src/pages/Northbound/DuerOS/Detail/Doc.tsx
  5. 46 28
      src/pages/Northbound/DuerOS/Detail/index.tsx
  6. 11 51
      src/pages/Northbound/DuerOS/index.tsx
  7. 12 0
      src/pages/account/Center/bind/index.less
  8. 56 37
      src/pages/account/Center/bind/index.tsx
  9. 25 11
      src/pages/account/Center/index.tsx
  10. 6 0
      src/pages/link/Channel/Opcua/Access/index.tsx
  11. 256 0
      src/pages/link/Channel/Opcua/Save/index.tsx
  12. 72 53
      src/pages/link/Channel/Opcua/index.tsx
  13. 26 0
      src/pages/link/Channel/Opcua/service.ts
  14. 7 1
      src/pages/link/Channel/Opcua/typings.d.ts
  15. 5 5
      src/pages/rule-engine/Scene/Save/action/VariableItems/builtIn.tsx
  16. 29 10
      src/pages/rule-engine/Scene/Save/action/VariableItems/user.tsx
  17. 2 0
      src/pages/rule-engine/Scene/Save/action/action.tsx
  18. 0 1
      src/pages/rule-engine/Scene/Save/action/device/AllDevice.tsx
  19. 6 1
      src/pages/rule-engine/Scene/Save/action/device/WriteProperty/index.tsx
  20. 14 6
      src/pages/rule-engine/Scene/Save/action/device/deviceModal.tsx
  21. 1 5
      src/pages/rule-engine/Scene/Save/action/device/functionCall.tsx
  22. 6 2
      src/pages/rule-engine/Scene/Save/action/device/index.tsx
  23. 6 1
      src/pages/rule-engine/Scene/Save/action/device/readProperty.tsx
  24. 1 1
      src/pages/rule-engine/Scene/Save/action/device/tagModal.tsx
  25. 6 3
      src/pages/rule-engine/Scene/Save/action/messageContent.tsx
  26. 8 2
      src/pages/rule-engine/Scene/Save/action/service.ts
  27. 4 4
      src/pages/rule-engine/Scene/Save/components/TimingTrigger/index.tsx
  28. 48 33
      src/pages/rule-engine/Scene/Save/index.tsx
  29. 8 8
      src/pages/rule-engine/Scene/Save/trigger/device.tsx
  30. 32 32
      src/pages/rule-engine/Scene/Save/trigger/index.tsx
  31. 1 0
      src/pages/rule-engine/Scene/Save/trigger/operation.tsx
  32. 57 8
      src/pages/system/Platforms/Api/basePage.tsx
  33. 20 0
      src/pages/system/Platforms/Api/index.less
  34. 15 5
      src/pages/system/Platforms/Api/index.tsx
  35. 119 0
      src/pages/system/Platforms/Api/leftTree.tsx
  36. 3 3
      src/pages/system/Platforms/index.tsx
  37. 6 9
      src/pages/system/Platforms/save.tsx
  38. 23 0
      src/utils/menu/index.ts
  39. 2 1
      src/utils/menu/router.ts

+ 16 - 0
config/routes.ts

@@ -15,6 +15,22 @@
       },
     ],
   },
+  {
+    path: '/account/center/bind',
+    layout: false,
+    routes: [
+      {
+        path: '/account/center/bind',
+        routes: [
+          {
+            name: 'bind',
+            path: '/account/center/bind',
+            component: './account/Center/bind',
+          },
+        ],
+      },
+    ],
+  },
   // {
   //   path: '/analysis',
   //   name: 'analysis',

+ 3 - 2
src/components/ProTableCard/CardItems/duerOs.tsx

@@ -17,14 +17,15 @@ export default (props: DuerOSProps) => {
   return (
     <TableCard
       actions={props.action}
-      detail={props.detail}
+      // detail={props.detail}
+      showStatus={false}
       // status={props.state?.value}
       // statusText={props.state?.text}
       // statusNames={{
       //   enabled: StatusColorEnum.success,
       //   disabled: StatusColorEnum.error,
       // }}
-      // showMask={false}
+      showMask={false}
     >
       <div className={'pro-table-card-item'}>
         <div className={'card-item-avatar'}>

+ 1 - 0
src/pages/Northbound/AliCloud/service.ts

@@ -1,6 +1,7 @@
 import BaseService from '@/utils/BaseService';
 import { request } from 'umi';
 import SystemConst from '@/utils/const';
+
 class Service extends BaseService<AliCloudType> {
   // 获取服务地址的下拉列表
   public getRegionsList = (params?: any) =>

+ 31 - 0
src/pages/Northbound/DuerOS/Detail/Doc.tsx

@@ -0,0 +1,31 @@
+const Doc = () => {
+  return (
+    <div className="doc">
+      <div className="url">
+        小度智能家居开放平台:
+        <a href="https://dueros.baidu.com/dbp/bot/index#/iotopenplatform">
+          https://dueros.baidu.com/dbp/bot/index#/iotopenplatform
+        </a>
+      </div>
+      <h1>1. 概述</h1>
+      <div>
+        DuerOS支持家居场景下的云端控制,该页面主要将平台的产品与DuerOS支持语音控制的产品进行映射,以到达小度平台控制本平台设备的目的。
+      </div>
+      <h1>2. 操作步骤</h1>
+      <div>
+        <h2>1、在百度小度技能平台创建技能,并授权。完成物联网平台与dueros的关联。</h2>
+        <h2>2、登录物联网平台,进行平台内产品与dueros产品的数据映射。</h2>
+        <h2>
+          3、智能家居用户通过物联网平台中的用户,登录小度APP,获取平台内当前用户的所属设备。获取后即可进行语音控制。
+        </h2>
+      </div>
+      <h1>3. 配置说明</h1>
+      <div>
+        <h2>
+          1、“设备类型”为dueros平台拟定的标准规范,设备类型将决定【动作映射】中“动作”的下拉选项,以及【属性映射】中“Dueros属性”的下拉选项
+        </h2>
+      </div>
+    </div>
+  );
+};
+export default Doc;

+ 46 - 28
src/pages/Northbound/DuerOS/Detail/index.tsx

@@ -1,7 +1,7 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import { createSchemaField } from '@formily/react';
 import { ISchema } from '@formily/json-schema';
-import { Card, Col, Row } from 'antd';
+import { Card, Col, message, Row } from 'antd';
 import {
   ArrayCollapse,
   ArrayTable,
@@ -15,10 +15,12 @@ import {
 } from '@formily/antd';
 import { PermissionButton } from '@/components';
 import { useMemo } from 'react';
-import { createForm, Field, onFieldReact, onFieldValueChange } from '@formily/core';
+import { createForm, Field, onFieldReact, onFieldValueChange, onFormInit } from '@formily/core';
 import { useAsyncDataSource } from '@/utils/util';
 import { service } from '..';
 import { Store } from 'jetlinks-store';
+import { useParams } from 'umi';
+import Doc from '@/pages/Northbound/DuerOS/Detail/Doc';
 
 const Save = () => {
   const SchemaField = createSchemaField({
@@ -33,6 +35,8 @@ const Save = () => {
     },
   });
 
+  const { id } = useParams<{ id: string }>();
+
   const getProduct = () =>
     service.getProduct().then((resp) => {
       Store.set('product-list', resp.result);
@@ -71,7 +75,7 @@ const Save = () => {
           columnGap: 24,
         },
         properties: {
-          product: {
+          id: {
             title: '产品',
             'x-decorator-props': {
               gridSpan: 1,
@@ -93,7 +97,7 @@ const Save = () => {
             'x-reactions': '{{useAsyncDataSource(getProduct)}}',
             required: true,
           },
-          deviceType: {
+          applianceType: {
             title: '设备类型',
             'x-decorator-props': {
               gridSpan: 1,
@@ -275,7 +279,7 @@ const Save = () => {
                             },
                           },
                         },
-                        function: {
+                        inputs: {
                           title: '参数列表',
                           type: 'array',
                           'x-component': 'ArrayTable',
@@ -408,6 +412,7 @@ const Save = () => {
                       label: 'name',
                       value: 'id',
                     },
+                    mode: 'tags',
                   },
                 },
               },
@@ -429,35 +434,39 @@ const Save = () => {
     },
   };
 
-  const handleSave = () => {};
-
-  const findProductMetadata = (id: string) => {
-    if (!id) return;
+  const findProductMetadata = (_id: string) => {
+    if (!_id) return;
     const _productList = Store.get('product-list');
-    const _product = _productList.find((item: any) => item.id === id);
-    return _product.metadata && JSON.parse(_product.metadata || '{}');
+    const _product = _productList?.find((item: any) => item.id === _id);
+    return _product?.metadata && JSON.parse(_product.metadata || '{}');
   };
 
-  const findDeviceType = (id: string) => {
-    if (!id) return;
+  const findapplianceType = (_id: string) => {
+    if (!_id) return;
     const _productTypes = Store.get('product-types');
-    return _productTypes.find((item: any) => item.id === id);
+    return _productTypes?.find((item: any) => item.id === _id);
   };
   const form = useMemo(
     () =>
       createForm({
         validateFirst: true,
         effects() {
+          onFormInit(async (form1) => {
+            await getTypes();
+            await getProduct();
+            const resp = await service.detail(id);
+            form1.setInitialValues(resp.result);
+          });
           onFieldReact('actionMappings.*.layout.action', (field) => {
-            const productType = field.query('deviceType').value();
-            (field as Field).setDataSource(findDeviceType(productType)?.actions);
+            const productType = field.query('applianceType').value();
+            (field as Field).setDataSource(findapplianceType(productType)?.actions);
           });
           onFieldReact('actionMappings.*.layout.command.message.properties', (field) => {
-            const product = field.query('product').value();
+            const product = field.query('id').value();
             (field as Field).setDataSource(findProductMetadata(product)?.properties);
           });
           onFieldReact('actionMappings.*.layout.command.message.functionId', (field) => {
-            const product = field.query('product').value();
+            const product = field.query('id').value();
             (field as Field).setDataSource(findProductMetadata(product)?.functions);
           });
           onFieldValueChange(
@@ -465,7 +474,7 @@ const Save = () => {
             (field, form1) => {
               const functionId = field.value;
               if (!functionId) return;
-              const product = field.query('product').value();
+              const product = field.query('id').value();
               const _functionList = findProductMetadata(product)?.functions;
               const _function =
                 _functionList && _functionList.find((item: any) => item.id === functionId);
@@ -477,35 +486,44 @@ const Save = () => {
               });
             },
           );
-          onFieldReact('propertyMappings.*.layout.target', (field) => {
-            const productType = field.query('deviceType').value();
-            (field as Field).setDataSource(findDeviceType(productType)?.properties);
-          });
           onFieldReact('propertyMappings.*.layout.source', (field) => {
-            const product = field.query('product').value();
+            const productType = field.query('applianceType').value();
+            (field as Field).setDataSource(findapplianceType(productType)?.properties);
+          });
+          onFieldReact('propertyMappings.*.layout.target', (field) => {
+            const product = field.query('id').value();
             (field as Field).setDataSource(findProductMetadata(product)?.properties);
           });
         },
       }),
     [],
   );
+
+  const handleSave = async () => {
+    const data: any = await form.submit();
+    await service.savePatch(data);
+    message.success('保存成功!');
+    history.back();
+  };
   return (
-    <PageContainer>
+    <PageContainer className={'page-title-show'}>
       <Card>
         <Row>
-          <Col span={10}>
+          <Col span={12}>
             <Form layout="vertical" form={form}>
               <SchemaField schema={schema} scope={{ useAsyncDataSource, getTypes, getProduct }} />
               <FormButtonGroup.Sticky>
                 <FormButtonGroup.FormItem>
-                  <PermissionButton type="primary" onClick={handleSave}>
+                  <PermissionButton isPermission={true} type="primary" onClick={handleSave}>
                     保存
                   </PermissionButton>
                 </FormButtonGroup.FormItem>
               </FormButtonGroup.Sticky>
             </Form>
           </Col>
-          <Col span={12} push={2}></Col>
+          <Col span={10} push={2}>
+            <Doc />
+          </Col>
         </Row>
       </Card>
     </PageContainer>

+ 11 - 51
src/pages/Northbound/DuerOS/index.tsx

@@ -3,15 +3,9 @@ import SearchComponent from '@/components/SearchComponent';
 import { useRef, useState } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import { PermissionButton, ProTableCard } from '@/components';
-import {
-  DeleteOutlined,
-  EditOutlined,
-  PlayCircleOutlined,
-  PlusOutlined,
-  StopOutlined,
-} from '@ant-design/icons';
+import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
 import { useIntl } from '@@/plugin-locale/localeExports';
-import { Space } from 'antd';
+import { message, Space } from 'antd';
 import { DuerOSItem } from '@/pages/Northbound/DuerOS/types';
 import DuerOSCard from '@/components/ProTableCard/CardItems/duerOs';
 import { history } from '@@/core/history';
@@ -42,7 +36,9 @@ export default () => {
               }
             : undefined
         }
-        onClick={() => {}}
+        onClick={() => {
+          history.push(getMenuPathByParams(MENUS_CODE['Northbound/DuerOS/Detail'], record.id));
+        }}
       >
         <EditOutlined />
         {type !== 'table' &&
@@ -52,53 +48,20 @@ export default () => {
           })}
       </PermissionButton>,
       <PermissionButton
-        key={'started'}
-        type={'link'}
-        style={{ padding: 0 }}
-        isPermission={permission.action}
-        popConfirm={{
-          title: intl.formatMessage({
-            id: `pages.data.option.${
-              record.state.value === 'started' ? 'disabled' : 'enabled'
-            }.tips`,
-            defaultMessage: '确认禁用?',
-          }),
-          onConfirm: async () => {},
-        }}
-        tooltip={
-          type === 'table'
-            ? {
-                title: intl.formatMessage({
-                  id: `pages.data.option.${
-                    record.state.value === 'started' ? 'disabled' : 'enabled'
-                  }`,
-                  defaultMessage: '启用',
-                }),
-              }
-            : undefined
-        }
-      >
-        {record.state.value === 'started' ? <StopOutlined /> : <PlayCircleOutlined />}
-        {type !== 'table' &&
-          intl.formatMessage({
-            id: `pages.data.option.${record.state.value === 'started' ? 'disabled' : 'enabled'}`,
-            defaultMessage: record.state.value === 'started' ? '禁用' : '启用',
-          })}
-      </PermissionButton>,
-      <PermissionButton
         key={'delete'}
         type={'link'}
         style={{ padding: 0 }}
         isPermission={permission.delete}
-        disabled={record.state.value === 'started'}
         popConfirm={{
           title: '确认删除?',
-          disabled: record.state.value === 'started',
-          onConfirm: () => {},
+          onConfirm: async () => {
+            await service.remove(record.id);
+            message.success('删除成功!');
+            actionRef.current?.reload();
+          },
         }}
         tooltip={{
-          title:
-            record.state.value === 'started' ? <span>请先禁用,再删除</span> : <span>删除</span>,
+          title: '删除',
         }}
       >
         <DeleteOutlined />
@@ -173,9 +136,6 @@ export default () => {
             <PermissionButton
               isPermission={true}
               onClick={() => {
-                // setCurrent(undefined);
-                // setVisible(true);
-                // state.current = record;
                 history.push(getMenuPathByParams(MENUS_CODE['Northbound/DuerOS/Detail']));
               }}
               key="button"

+ 12 - 0
src/pages/account/Center/bind/index.less

@@ -0,0 +1,12 @@
+.col {
+  display: flex;
+  justify-content: center;
+
+  .item {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: space-around;
+    width: 300px;
+  }
+}

+ 56 - 37
src/pages/account/Center/bind/index.tsx

@@ -1,6 +1,7 @@
-import { Button, Card, Col, message, Row } from 'antd';
+import { Avatar, Button, Card, Col, message, Row } from 'antd';
 import { useEffect, useState } from 'react';
 import Service from '@/pages/account/Center/service';
+import styles from './index.less';
 
 export const service = new Service();
 
@@ -9,6 +10,14 @@ const Bind = () => {
   const [user, setUser] = useState<any>();
   const [code, setCode] = useState<string>('');
 
+  const iconMap = new Map();
+  iconMap.set('dingtalk', require('/public/images/notice/dingtalk.png'));
+  iconMap.set('wechat-webapp', require('/public/images/notice/wechat.png'));
+
+  const bGroundMap = new Map();
+  bGroundMap.set('dingtalk', require('/public/images/notice/dingtalk-background.png'));
+  bGroundMap.set('wechat-webapp', require('/public/images/notice/wechat-background.png'));
+
   const bindUserInfo = (params: string) => {
     service.bindUserInfo(params).then((res) => {
       if (res.status === 200) {
@@ -23,9 +32,8 @@ const Bind = () => {
   };
 
   useEffect(() => {
-    // window.open('http://z.jetlinks.cn')
-    // const item = `http://pro.jetlinks.cn/#/user/login?sso=true&code=4decc08bcb87f3a4fbd74976fd86cd3d&redirect=http://pro.jetlinks.cn/jetlinks`;
     const params = window.location.href.split('?')[1].split('&')[1].split('=')[1];
+    // const params = 'b584032923c78d69e6148cf0e9312723'
     setCode(params);
     bindUserInfo(params);
     getDetail();
@@ -33,42 +41,53 @@ const Bind = () => {
   return (
     <>
       <Card>
-        <Row gutter={[24, 24]}>
-          <Col span={12}>
-            <Card title="个人信息">
-              <p>登录账号:{user?.name}</p>
-              <p>姓名:{user?.name}</p>
-            </Card>
-          </Col>
-          <Col span={12}>
-            <Card title="三方账号信息">
-              <p>类型:{bindUser?.type}</p>
-              <p>组织:{bindUser?.providerName}</p>
-            </Card>
-          </Col>
-        </Row>
-        <Row gutter={[24, 24]}>
-          <Col span={24} style={{ textAlign: 'center', marginTop: 20 }}>
-            <Button
-              type="primary"
-              onClick={() => {
-                service.bind(code).then((res) => {
-                  if (res.status === 200) {
-                    message.success('绑定成功');
-                    if ((window as any).onBindSuccess) {
-                      (window as any).onBindSuccess(res);
+        <div style={{ margin: '0 auto', width: 800 }}>
+          <Row>
+            <Col span={12} className={styles.col}>
+              <Card title="个人信息">
+                <div className={styles.item}>
+                  <div style={{ height: 100 }}>
+                    <Avatar size={90} src={user?.avatar} />
+                  </div>
+                  <p>登录账号:{user?.username}</p>
+                  <p>姓名:{user?.name}</p>
+                </div>
+              </Card>
+            </Col>
+            <Col span={12} className={styles.col}>
+              <Card title="三方账号信息">
+                <div className={styles.item}>
+                  <div style={{ height: 100 }}>
+                    <img style={{ height: 80 }} src={iconMap.get(bindUser?.type)} />
+                  </div>
+                  <p>组织:{bindUser?.providerName}</p>
+                  <p>名字:{bindUser?.result.others.name}</p>
+                </div>
+              </Card>
+            </Col>
+          </Row>
+          <Row>
+            <Col span={24} style={{ textAlign: 'center', marginTop: 20 }}>
+              <Button
+                type="primary"
+                onClick={() => {
+                  // window.close()
+                  service.bind(code).then((res) => {
+                    if (res.status === 200) {
+                      message.success('绑定成功');
+                      localStorage.setItem('onBind', 'true');
                       setTimeout(() => window.close(), 300);
+                    } else {
+                      message.error('绑定失败');
                     }
-                  } else {
-                    message.error('绑定失败');
-                  }
-                });
-              }}
-            >
-              立即绑定
-            </Button>
-          </Col>
-        </Row>
+                  });
+                }}
+              >
+                立即绑定
+              </Button>
+            </Col>
+          </Row>
+        </div>
       </Card>
     </>
   );

+ 25 - 11
src/pages/account/Center/index.tsx

@@ -1,5 +1,16 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import { Avatar, Button, Card, Col, Descriptions, Divider, message, Row, Upload } from 'antd';
+import {
+  Avatar,
+  Button,
+  Card,
+  Col,
+  Descriptions,
+  Divider,
+  message,
+  Popconfirm,
+  Row,
+  Upload,
+} from 'antd';
 import { useEffect, useState } from 'react';
 import styles from './index.less';
 import { UploadProps } from 'antd/lib/upload';
@@ -222,7 +233,7 @@ const Center = () => {
                             color: '#00000073',
                           }}
                         >
-                          绑定时间: 2022-01-02 09:00:00
+                          绑定时间: {moment(item.bindTime).format('YYYY-MM-DD HH:mm:ss')}
                         </div>
                       </div>
                     ) : (
@@ -233,27 +244,30 @@ const Center = () => {
                   </div>
                   <div>
                     {item.bound ? (
-                      <Button
-                        onClick={() => {
+                      <Popconfirm
+                        title="确认解除绑定嘛?"
+                        onConfirm={() => {
                           unBind(item.type, item.provider);
                         }}
                       >
-                        解除绑定
-                      </Button>
+                        <Button>解除绑定</Button>
+                      </Popconfirm>
                     ) : (
                       <Button
                         type="primary"
                         onClick={() => {
-                          const items: any = window.open(
+                          window.open(
                             `/${SystemConst.API_BASE}/sso/${item.provider}/login`,
+                            '',
+                            'width=700,height=500,left=500,top=300',
                           );
-                          //  const items:any= window.open(`/#/account/Center/bind`);
-                          items!.onBindSuccess = (value: any) => {
-                            if (value.status === 200) {
+                          // window.open(`/#/account/center/bind`,'','width=700,height=500,left=500,top=300');
+                          localStorage.setItem('onBind', 'false');
+                          window.onstorage = (e) => {
+                            if (e.newValue) {
                               getBindInfo();
                             }
                           };
-                          // history.push(getMenuPathByCode('account/Center/bind'));
                         }}
                       >
                         立即绑定

+ 6 - 0
src/pages/link/Channel/Opcua/Access/index.tsx

@@ -0,0 +1,6 @@
+import { PageContainer } from '@ant-design/pro-layout';
+
+const Access = () => {
+  return <PageContainer>Access</PageContainer>;
+};
+export default Access;

+ 256 - 0
src/pages/link/Channel/Opcua/Save/index.tsx

@@ -0,0 +1,256 @@
+import { useIntl } from 'umi';
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import { Form, FormGrid, FormItem, Input, Select } from '@formily/antd';
+import type { ISchema } from '@formily/json-schema';
+import { service } from '@/pages/link/Channel/Opcua';
+import { Modal } from '@/components';
+import { message } from 'antd';
+import { useEffect, useState } from 'react';
+
+interface Props {
+  data: Partial<OpaUa>;
+  close: () => void;
+}
+
+const Save = (props: Props) => {
+  const intl = useIntl();
+  const [policies, setPolicies] = useState<any>([]);
+  const [modes, setModes] = useState<any>([]);
+
+  const form = createForm({
+    validateFirst: true,
+    initialValues: {
+      ...props.data,
+      clientConfigs: props.data?.clientConfigs?.[0],
+    },
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Select,
+      FormGrid,
+    },
+  });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      layout: {
+        type: 'void',
+        'x-decorator': 'FormGrid',
+        'x-decorator-props': {
+          maxColumns: 2,
+          minColumns: 2,
+          columnGap: 24,
+        },
+        properties: {
+          name: {
+            title: '名称',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入名称',
+            },
+            name: 'name',
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入名称',
+              },
+            ],
+          },
+          'clientConfigs.endpoint': {
+            title: '服务地址',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-component-props': {
+              placeholder: '请输入服务地址',
+            },
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入服务地址',
+              },
+              {
+                pattern:
+                  '(opc.tcp|http|https|opc.http|opc.https|opc.ws|opc.wss)://([^:/]+|\\[.*])(:\\d+)?(/.*)?',
+                message: '格式错误(opc.tcp://127.0.0.1:49320)',
+              },
+            ],
+            name: 'endpoint',
+            required: true,
+          },
+          'clientConfigs.securityPolicy': {
+            title: '安全策略',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Select',
+            'x-component-props': {
+              placeholder: '请选择安全策略',
+              showArrow: true,
+              filterOption: (input: string, option: any) =>
+                option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+            },
+            required: true,
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择安全策略',
+              },
+            ],
+            enum: policies,
+          },
+          'clientConfigs.securityMode': {
+            title: '安全模式',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Select',
+            'x-component-props': {
+              placeholder: '请选择安全模式',
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择安全模式',
+              },
+            ],
+            required: true,
+            enum: modes,
+          },
+          'clientConfigs.username': {
+            title: '用户名',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            'x-component-props': {
+              placeholder: '请输入用户名',
+            },
+            name: 'name',
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+            ],
+          },
+          'clientConfigs.password': {
+            title: '密码',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入密码',
+            },
+            name: 'name',
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+            ],
+          },
+          description: {
+            title: '说明',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input.TextArea',
+            'x-component-props': {
+              rows: 5,
+              placeholder: '请输入说明',
+            },
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-validator': [
+              {
+                max: 200,
+                message: '最多可输入200个字符',
+              },
+            ],
+          },
+        },
+      },
+    },
+  };
+
+  const save = async () => {
+    const value = await form.submit<any>();
+    console.log(value);
+    const item = {
+      name: value.name,
+      description: value.description,
+      clientConfigs: [value.clientConfigs],
+    };
+    if (props.data.id) {
+      service.modify(props.data.id, item).then((res: any) => {
+        if (res.status === 200) {
+          message.success('保存成功');
+          props.close();
+        }
+      });
+    } else {
+      service.save(item).then((res: any) => {
+        if (res.status === 200) {
+          message.success('保存成功');
+          props.close();
+        }
+      });
+    }
+  };
+
+  useEffect(() => {
+    service.policies().then((res) => setPolicies(res.result));
+    service.modes().then((res) => setModes(res.result));
+    console.log(props.data.clientConfigs?.[0]);
+  }, []);
+  return (
+    <Modal
+      title={intl.formatMessage({
+        id: `pages.data.option.${props.data.id ? 'edit' : 'add'}`,
+        defaultMessage: '编辑',
+      })}
+      maskClosable={false}
+      visible
+      onCancel={props.close}
+      onOk={save}
+      width="35vw"
+      permissionCode={'system/Relationship'}
+      permission={['add', 'edit']}
+    >
+      <Form form={form} layout="vertical">
+        <SchemaField schema={schema} />
+      </Form>
+    </Modal>
+  );
+};
+export default Save;

+ 72 - 53
src/pages/link/Channel/Opcua/index.tsx

@@ -1,25 +1,32 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
-import { Badge, Card, Col, Row } from 'antd';
+import { Badge, Card, Col, message, Row } from 'antd';
 import styles from './index.less';
 import { PermissionButton } from '@/components';
+import { history, useIntl } from 'umi';
 import {
+  ControlOutlined,
   DeleteOutlined,
   EditOutlined,
-  LinkOutlined,
   PlayCircleOutlined,
   PlusOutlined,
   StopOutlined,
 } from '@ant-design/icons';
-import { useIntl } from 'umi';
-import { useRef } from 'react';
+import { useRef, useState } from 'react';
 import SearchComponent from '@/components/SearchComponent';
+import Service from './service';
+import Save from './Save';
+import { getMenuPathByCode } from '@/utils/menu';
+
+export const service = new Service('opc/client');
 
 const Opcua = () => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
-  // const [param, setParam] = useState({});
+  const [param, setParam] = useState({});
   const { permission } = PermissionButton.usePermission('link/Channel/Opcua');
+  const [visible, setVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<Partial<OpaUa>>({});
 
   const iconMap = new Map();
   iconMap.set('1', require('/public/images/channel/1.png'));
@@ -28,18 +35,19 @@ const Opcua = () => {
   iconMap.set('4', require('/public/images/channel/4.png'));
   const background = require('/public/images/channel/background.png');
 
-  const columns: ProColumns<any>[] = [
+  const columns: ProColumns<OpaUa>[] = [
     {
       title: '通道名称',
       dataIndex: 'name',
     },
     {
-      title: 'IP',
-      dataIndex: 'ip',
+      title: '服务地址',
+      // dataIndex: 'clientConfigs',
+      render: (_, record) => <>{record.clientConfigs?.[0].endpoint}</>,
     },
     {
-      title: '端口',
-      dataIndex: 'local',
+      title: '安全策略',
+      render: (_, record) => <>{record.clientConfigs?.[0].securityPolicy}</>,
     },
     {
       title: '状态',
@@ -58,8 +66,8 @@ const Opcua = () => {
           isPermission={permission.update}
           key="edit"
           onClick={() => {
-            // setVisible(true);
-            // setCurrent(record);
+            setVisible(true);
+            setCurrent(record);
           }}
           type={'link'}
           style={{ padding: 0 }}
@@ -79,48 +87,48 @@ const Opcua = () => {
           popConfirm={{
             title: intl.formatMessage({
               id: `pages.data.option.${
-                record.state.value !== 'notActive' ? 'disabled' : 'enabled'
+                record.state.value !== 'disabled' ? 'disabled' : 'enabled'
               }.tips`,
               defaultMessage: '确认禁用?',
             }),
             onConfirm: async () => {
-              // if (record.state.value !== 'notActive') {
-              //   await service.undeployDevice(record.id);
-              // } else {
-              //   await service.deployDevice(record.id);
-              // }
-              // message.success(
-              //   intl.formatMessage({
-              //     id: 'pages.data.option.success',
-              //     defaultMessage: '操作成功!',
-              //   }),
-              // );
-              // actionRef.current?.reload();
+              if (record.state.value === 'disabled') {
+                await service.enable(record.id);
+              } else {
+                await service.disable(record.id);
+              }
+              message.success(
+                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' ? '禁用' : '启用',
+              id: `pages.data.option.${record.state.value !== 'disabled' ? 'disabled' : 'enabled'}`,
+              defaultMessage: record.state.value !== 'disabled' ? '禁用' : '启用',
             }),
           }}
         >
-          {record.state.value !== 'notActive' ? <StopOutlined /> : <PlayCircleOutlined />}
+          {record.state.value !== 'disabled' ? <StopOutlined /> : <PlayCircleOutlined />}
         </PermissionButton>,
         <PermissionButton
           isPermission={permission.view}
           style={{ padding: 0 }}
-          key="button"
+          key="link"
           type="link"
           tooltip={{
             title: '设备接入',
           }}
-          onClick={() => {}}
+          onClick={() => {
+            history.push(`${getMenuPathByCode('link/Channel/Opcua/Access')}?id=${record.id}`);
+          }}
         >
-          <LinkOutlined />
+          <ControlOutlined />
         </PermissionButton>,
         <PermissionButton
           isPermission={permission.delete}
@@ -128,19 +136,19 @@ const Opcua = () => {
           popConfirm={{
             title: '确认删除',
             onConfirm: async () => {
-              // const resp: any = await service.remove(record.id);
-              // if (resp.status === 200) {
-              //   message.success(
-              //     intl.formatMessage({
-              //       id: 'pages.data.option.success',
-              //       defaultMessage: '操作成功!',
-              //     }),
-              //   );
-              //   actionRef.current?.reload();
-              // }
+              const resp: any = await service.remove(record.id);
+              if (resp.status === 200) {
+                message.success(
+                  intl.formatMessage({
+                    id: 'pages.data.option.success',
+                    defaultMessage: '操作成功!',
+                  }),
+                );
+                actionRef.current?.reload();
+              }
             },
           }}
-          key="button"
+          key="delete"
           type="link"
         >
           <DeleteOutlined />
@@ -176,7 +184,7 @@ const Opcua = () => {
       <Card style={{ marginBottom: 10 }}>
         <Row gutter={[24, 24]}>
           {topCard.map((item) => (
-            <Col span={6}>
+            <Col span={6} key={item.numeber}>
               <Card>
                 <div className={styles.topCard}>
                   <div
@@ -206,22 +214,24 @@ const Opcua = () => {
         onSearch={(data) => {
           // 重置分页数据
           actionRef.current?.reset?.();
-          console.log(data);
-          // setParam(data);
+          setParam(data);
         }}
       />
-      <ProTable<UserItem>
+      <ProTable<OpaUa>
         actionRef={actionRef}
-        // params={param}
+        params={param}
         columns={columns}
+        rowKey="id"
         search={false}
         headerTitle={
           <PermissionButton
             onClick={() => {
               // setMode('add');
+              setVisible(true);
+              setCurrent({});
             }}
             isPermission={permission.add}
-            key="button"
+            key="add"
             icon={<PlusOutlined />}
             type="primary"
           >
@@ -231,10 +241,19 @@ const Opcua = () => {
             })}
           </PermissionButton>
         }
-        // request={async (params) =>
-        //   service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
-        // }
+        request={async (params) =>
+          service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        }
       />
+      {visible && (
+        <Save
+          data={current}
+          close={() => {
+            setVisible(false);
+            actionRef.current?.reload();
+          }}
+        />
+      )}
     </PageContainer>
   );
 };

+ 26 - 0
src/pages/link/Channel/Opcua/service.ts

@@ -0,0 +1,26 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<OpaUa> {
+  enable = (id: string) =>
+    request(`${SystemConst.API_BASE}/opc/client/${id}/_enable`, {
+      method: 'POST',
+    });
+  disable = (id: string) =>
+    request(`${SystemConst.API_BASE}/opc/client/${id}/_disable`, {
+      method: 'POST',
+    });
+  policies = (params?: any) =>
+    request(`${SystemConst.API_BASE}/opc/client/security-policies`, {
+      method: 'GET',
+      params,
+    });
+  modes = (params?: any) =>
+    request(`${SystemConst.API_BASE}/opc/client/security-modes`, {
+      method: 'GET',
+      params,
+    });
+}
+
+export default Service;

+ 7 - 1
src/pages/link/Channel/Opcua/typings.d.ts

@@ -1,4 +1,10 @@
-type Item = {
+type OpaUa = {
   id: string;
   name: string;
+  clientConfigs?: Record<string, any>[];
+  description?: string;
+  state: {
+    text: string;
+    value: string;
+  };
 };

+ 5 - 5
src/pages/rule-engine/Scene/Save/action/VariableItems/builtIn.tsx

@@ -17,6 +17,7 @@ interface BuiltInProps {
   type?: string;
   notifyType?: string;
   onChange?: (value: ChangeType) => void;
+  trigger?: any;
 }
 
 export default (props: BuiltInProps) => {
@@ -35,12 +36,11 @@ export default (props: BuiltInProps) => {
   });
 
   useEffect(() => {
-    if (source === 'upper') {
-      getBuiltInList({
-        trigger: { type: props.type },
-      });
+    console.log(props.trigger);
+    if (source === 'upper' && props.trigger) {
+      getBuiltInList({ ...props.trigger });
     }
-  }, [source, props.type]);
+  }, [source, props.trigger]);
 
   useEffect(() => {
     setSource(props.value?.source);

+ 29 - 10
src/pages/rule-engine/Scene/Save/action/VariableItems/user.tsx

@@ -54,7 +54,11 @@ export default (props: UserProps) => {
     };
     const resp1 = await queryPlatformUsers();
     if (resp1.status === 200) {
-      _userList.platform = resp1.result.map((item: any) => ({ label: item.name, value: item.id }));
+      _userList.platform = resp1.result.map((item: any) => ({
+        label: item.name,
+        value: item.id,
+        username: item.username,
+      }));
     }
 
     const resp2 = await queryRelationUsers();
@@ -62,6 +66,7 @@ export default (props: UserProps) => {
       _userList.relation = resp2.result.map((item: any) => ({
         label: item.name,
         value: item.relation,
+        username: '',
       }));
     }
 
@@ -172,7 +177,7 @@ export default (props: UserProps) => {
   };
 
   const filterOption = (input: string, option: any) => {
-    return option.children ? option.children.toLowerCase().includes(input.toLowerCase()) : false;
+    return option.label ? option.label.toLowerCase().includes(input.toLowerCase()) : false;
   };
 
   const userSelect =
@@ -187,12 +192,16 @@ export default (props: UserProps) => {
         placeholder={'请选择收信人'}
         listHeight={200}
         filterOption={filterOption}
+        optionLabelProp="label"
       >
         {userList.platform.length ? (
           <Select.OptGroup label={'平台用户'}>
             {userList.platform.map((item: any) => (
-              <Select.Option value={item.value} isRelation={false}>
-                {item.label}
+              <Select.Option value={item.value} isRelation={false} label={item.label}>
+                <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+                  <span>{item.label}</span>
+                  <span style={{ color: '#cfcfcf' }}>{item.username}</span>
+                </div>
               </Select.Option>
             ))}
           </Select.OptGroup>
@@ -200,8 +209,11 @@ export default (props: UserProps) => {
         {userList.relation.length ? (
           <Select.OptGroup label={'关系用户'}>
             {userList.relation.map((item: any) => (
-              <Select.Option value={item.value} isRelation={true}>
-                {item.label}
+              <Select.Option value={item.value} isRelation={false} label={item.label}>
+                <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+                  <span>{item.label}</span>
+                  <span style={{ color: '#cfcfcf' }}>{item.username}</span>
+                </div>
               </Select.Option>
             ))}
           </Select.OptGroup>
@@ -237,12 +249,16 @@ export default (props: UserProps) => {
         placeholder={'请选择收信人'}
         listHeight={200}
         filterOption={filterOption}
+        optionLabelProp="label"
       >
         {userList.platform.length ? (
           <Select.OptGroup label={'平台用户'}>
             {userList.platform.map((item: any) => (
-              <Select.Option value={item.value} isRelation={false}>
-                {item.label}
+              <Select.Option value={item.value} isRelation={false} label={item.label}>
+                <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+                  <span>{item.label}</span>
+                  <span style={{ color: '#cfcfcf' }}>{item.username}</span>
+                </div>
               </Select.Option>
             ))}
           </Select.OptGroup>
@@ -250,8 +266,11 @@ export default (props: UserProps) => {
         {userList.relation.length ? (
           <Select.OptGroup label={'关系用户'}>
             {userList.relation.map((item: any) => (
-              <Select.Option value={item.value} isRelation={true}>
-                {item.label}
+              <Select.Option value={item.value} isRelation={true} label={item.label}>
+                <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+                  <span>{item.label}</span>
+                  <span style={{ color: '#cfcfcf' }}>{item.username}</span>
+                </div>
               </Select.Option>
             ))}
           </Select.OptGroup>

+ 2 - 0
src/pages/rule-engine/Scene/Save/action/action.tsx

@@ -25,6 +25,7 @@ interface ActionProps {
   triggerType: string;
   onRemove: () => void;
   actionItemData?: any;
+  trigger?: any;
 }
 
 export default observer((props: ActionProps) => {
@@ -265,6 +266,7 @@ export default observer((props: ActionProps) => {
           form={props.form}
           template={templateData}
           name={props.name}
+          trigger={props.trigger}
           notifyType={notifyType}
           triggerType={props.triggerType}
           configId={configId}

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

@@ -10,7 +10,6 @@ interface AllDeviceProps {
 
 export default (props: AllDeviceProps) => {
   useEffect(() => {
-    console.log(props.productId);
     queryAllDevice({
       terms: [{ column: 'productId', value: props.productId }],
       paging: false,

+ 6 - 1
src/pages/rule-engine/Scene/Save/action/device/WriteProperty/index.tsx

@@ -124,7 +124,12 @@ export default (props: WritePropertyProps) => {
       <Col span={4}>
         <Select
           value={propertiesKey}
-          options={props.properties}
+          options={props.properties.filter((item) => {
+            if (item.expands && item.expands.type) {
+              return item.expands.type.includes('write');
+            }
+            return false;
+          })}
           fieldNames={{ label: 'name', value: 'id' }}
           style={{ width: '100%' }}
           onSelect={(key: any) => {

+ 14 - 6
src/pages/rule-engine/Scene/Save/action/device/deviceModal.tsx

@@ -1,4 +1,4 @@
-import { Badge, Input, message, Modal } from 'antd';
+import { Badge, Button, Input, message, Modal } from 'antd';
 import { useEffect, useRef, useState } from 'react';
 import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
 import { DeviceItem } from '@/pages/system/Department/typings';
@@ -38,7 +38,6 @@ export default (props: DeviceModelProps) => {
   const [selectKeys, setSelectKeys] = useState<ChangeValueType[]>(props.value || []);
   const [searchParam, setSearchParam] = useState({});
   const [value, setValue] = useState<ChangeValueType[]>(props.value || []);
-  const oldAllSelect = useRef<any[]>([]);
 
   useEffect(() => {
     setValue(props.value || []);
@@ -159,21 +158,30 @@ export default (props: DeviceModelProps) => {
                 }
                 setSelectKeys(newSelectKeys);
               },
-              onSelectAll: (selected, selectedRows) => {
+              onSelectAll: (selected, _, changeRows) => {
                 let newSelectKeys = [...selectKeys];
                 if (selected) {
-                  oldAllSelect.current = selectedRows;
-                  selectedRows.forEach((item) => {
+                  changeRows.forEach((item) => {
                     newSelectKeys.push({ name: item.name, value: item.id });
                   });
                 } else {
                   newSelectKeys = newSelectKeys.filter((a) => {
-                    return !oldAllSelect.current.some((b) => b.id === a.value);
+                    return !changeRows.some((b) => b.id === a.value);
                   });
                 }
                 setSelectKeys(newSelectKeys);
               },
             }}
+            tableAlertOptionRender={() => (
+              <Button
+                type={'link'}
+                onClick={() => {
+                  setSelectKeys([]);
+                }}
+              >
+                取消选择
+              </Button>
+            )}
             request={(params) => queryDevice(params)}
             params={searchParam}
           ></ProTable>

+ 1 - 5
src/pages/rule-engine/Scene/Save/action/device/functionCall.tsx

@@ -24,10 +24,6 @@ export default (props: FunctionCallProps) => {
 
   useEffect(() => {
     setEditableRowKeys(props.functionData.map((d) => d.id));
-    console.log('functionData', props.functionData);
-    formRef.current?.setFieldsValue({
-      table: props.functionData,
-    });
   }, [props.functionData]);
 
   useEffect(() => {
@@ -45,7 +41,7 @@ export default (props: FunctionCallProps) => {
         }),
       });
     }
-  }, []);
+  }, [props.value, props.functionData]);
 
   const getItemNode = (record: any) => {
     const type = record.type;

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

@@ -185,7 +185,11 @@ export default (props: DeviceProps) => {
             initialValue={props.value ? props.value.selector : SourceEnum.fixed}
             {...props.restField}
           >
-            <Select options={sourceList} style={{ width: 120 }} />
+            <Select
+              options={sourceList}
+              style={{ width: 120 }}
+              onSelect={(key: string) => setSelector(key)}
+            />
           </Form.Item>
           {selector === SourceEnum.fixed && (
             <Form.Item
@@ -211,7 +215,7 @@ export default (props: DeviceProps) => {
               {...props.restField}
               rules={[{ required: true, message: '请选择关系人' }]}
             >
-              <Select style={{ width: 300 }} />
+              <Select style={{ width: '100%' }} placeholder={'请选择关系'} />
             </Form.Item>
           )}
         </ItemGroup>

+ 6 - 1
src/pages/rule-engine/Scene/Save/action/device/readProperty.tsx

@@ -10,7 +10,12 @@ export default (props: ReadPropertyProps) => {
   return (
     <Select
       value={props.value ? props.value[0] : undefined}
-      options={props.properties}
+      options={props.properties.filter((item) => {
+        if (item.expands && item.expands.type) {
+          return item.expands.type.includes('read');
+        }
+        return false;
+      })}
       fieldNames={{ label: 'name', value: 'id' }}
       style={{ width: '100%' }}
       onSelect={(key: any) => {

+ 1 - 1
src/pages/rule-engine/Scene/Save/action/device/tagModal.tsx

@@ -151,7 +151,7 @@ export default (props: TagModalProps) => {
     <>
       <Modal
         visible={visible}
-        title={'设备'}
+        title={'标签'}
         width={660}
         onOk={() => {
           const newValue = tagList

+ 6 - 3
src/pages/rule-engine/Scene/Save/action/messageContent.tsx

@@ -16,6 +16,7 @@ interface MessageContentProps {
   notifyType: string;
   triggerType: string;
   configId: string;
+  trigger?: any;
 }
 
 const rowGutter = 12;
@@ -44,8 +45,10 @@ export default (props: MessageContentProps) => {
         } else {
           rules.push({
             validator: async (_: any, value: any) => {
-              if (type === 'file' && !value) {
-                return Promise.reject(new Error('请输入' + item.name));
+              if (type === 'file' || type === 'link') {
+                if (!value) {
+                  return Promise.reject(new Error('请输入' + item.name));
+                }
               } else {
                 if (!value || !value.value) {
                   if (['date', 'org'].includes(type)) {
@@ -146,7 +149,7 @@ export default (props: MessageContentProps) => {
                       ) : type === 'link' ? (
                         <Input placeholder={'请输入' + item.name} />
                       ) : (
-                        <BuiltIn type={props.triggerType} data={item} />
+                        <BuiltIn type={props.triggerType} trigger={props.trigger} data={item} />
                       )}
                     </Form.Item>
                   </Col>

+ 8 - 2
src/pages/rule-engine/Scene/Save/action/service.ts

@@ -50,7 +50,10 @@ export const queryRelationUsers = () =>
 
 // 钉钉用户
 export const queryDingTalkUsers = (id: string) =>
-  request(`${SystemConst.API_BASE}/notifier/dingtalk/corp/${id}/users`, { method: 'GET' });
+  request(
+    `${SystemConst.API_BASE}/notifier/dingtalk/corp/${id}/users?sorts[0].name='name'&sorts[0].order=asc`,
+    { method: 'GET' },
+  );
 
 // 钉钉部门
 export const queryDingTalkDepartments = (id: string) =>
@@ -60,7 +63,10 @@ export const queryDingTalkDepartments = (id: string) =>
 
 // 微信用户
 export const queryWechatUsers = (id: string) =>
-  request(`${SystemConst.API_BASE}/notifier/wechat/corp/${id}/users`, { method: 'GET' });
+  request(
+    `${SystemConst.API_BASE}/notifier/wechat/corp/${id}/users?sorts[0].name='name'&sorts[0].order=asc`,
+    { method: 'GET' },
+  );
 
 // 微信部门
 export const queryWechatDepartments = (id: string) =>

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

@@ -193,7 +193,7 @@ export default (props: TimingTrigger) => {
                   value={
                     data.period?.from
                       ? [moment(data.period?.from, 'HH:mm:ss'), moment(data.period?.to, 'hh:mm:ss')]
-                      : undefined
+                      : [moment(new Date(), 'HH:mm:ss'), moment(new Date(), 'HH:mm:ss')]
                   }
                   onChange={(_, dateString) => {
                     onChange({
@@ -208,8 +208,8 @@ export default (props: TimingTrigger) => {
                 />
               ) : (
                 <TimePicker
-                  format={'hh:mm:ss'}
-                  value={data.once?.time ? moment(data.once?.time, 'hh:mm:ss') : undefined}
+                  format={'HH:mm:ss'}
+                  value={moment(data.once?.time || new Date(), 'HH:mm:ss')}
                   onChange={(_, dateString) => {
                     onChange({
                       ...data,
@@ -233,7 +233,7 @@ export default (props: TimingTrigger) => {
                     addonAfter={TimeTypeAfter}
                     style={{ flex: 1 }}
                     min={0}
-                    max={9999}
+                    max={59}
                     onChange={(e) => {
                       onChange({
                         ...data,

+ 48 - 33
src/pages/rule-engine/Scene/Save/index.tsx

@@ -35,7 +35,7 @@ type ShakeLimitType = {
 
 const DefaultShakeLimit = {
   enabled: false,
-  alarmFirst: true,
+  alarmFirst: false,
 };
 
 export let FormModel = model<FormModelType>({});
@@ -59,6 +59,7 @@ export default () => {
 
   const [requestParams, setRequestParams] = useState<any>(undefined);
   const [triggerValue, setTriggerValue] = useState<any>([]);
+  const [actionParams, setActionParams] = useState<any>(undefined);
 
   const [actionsData, setActionsData] = useState<any[]>([]);
   const [isEdit, setIsEdit] = useState(false);
@@ -72,10 +73,11 @@ export default () => {
         FormModel = _data;
         form.setFieldsValue(_data);
         setParallel(_data.parallel);
-        setShakeLimit(_data.shakeLimit || DefaultShakeLimit);
 
         setTriggerValue({ trigger: _data.terms || [] });
-
+        if (_data.trigger?.shakeLimit) {
+          setShakeLimit(_data.trigger?.shakeLimit || DefaultShakeLimit);
+        }
         if (_data.trigger?.device) {
           setRequestParams({ trigger: _data.trigger });
         }
@@ -95,7 +97,7 @@ export default () => {
     }
   }, [location]);
 
-  const saveData = async () => {
+  const saveData = useCallback(async () => {
     const formData = await form.validateFields();
     let triggerData = undefined;
     // 获取触发条件数据
@@ -107,6 +109,12 @@ export default () => {
     }
     console.log('save', formData);
     if (formData) {
+      if (shakeLimit.enabled) {
+        formData.trigger = {
+          ...formData.trigger,
+          shakeLimit: shakeLimit,
+        };
+      }
       setLoading(true);
       const resp = formData.id ? await service.updateScene(formData) : await service.save(formData);
 
@@ -126,7 +134,7 @@ export default () => {
         message.error(resp.message);
       }
     }
-  };
+  }, [shakeLimit]);
 
   const AntiShake = (
     <Space>
@@ -136,34 +144,36 @@ export default () => {
         checkedChildren="开启防抖"
         unCheckedChildren="关闭防抖"
         onChange={(e) => {
-          setShakeLimit({
+          const newShake = {
             ...shakeLimit,
             enabled: e,
-          });
-          form.setFieldsValue({ shakeLimit });
+          };
+          setShakeLimit(newShake);
         }}
       />
       {shakeLimit.enabled && (
         <>
           <InputNumber
             value={shakeLimit.time}
+            min={0}
             onChange={(e: number) => {
-              setShakeLimit({
+              const newShake = {
                 ...shakeLimit,
                 time: e,
-              });
-              form.setFieldsValue({ shakeLimit });
+              };
+              setShakeLimit(newShake);
             }}
           />
           <span> 秒内发生 </span>
           <InputNumber
             value={shakeLimit.threshold}
+            min={0}
             onChange={(e: number) => {
-              setShakeLimit({
+              const newShake = {
                 ...shakeLimit,
                 threshold: e,
-              });
-              form.setFieldsValue({ shakeLimit });
+              };
+              setShakeLimit(newShake);
             }}
           />
           <span>次及以上时,处理</span>
@@ -175,12 +185,11 @@ export default () => {
             ]}
             optionType="button"
             onChange={(e) => {
-              console.log(e);
-              setShakeLimit({
+              const newShake = {
                 ...shakeLimit,
                 alarmFirst: e.target.value,
-              });
-              form.setFieldsValue({ shakeLimit });
+              };
+              setShakeLimit(newShake);
             }}
           ></Radio.Group>
         </>
@@ -199,16 +208,21 @@ export default () => {
           preserve={false}
           className={'scene-save'}
           onValuesChange={(changeValue, allValues) => {
-            if (changeValue.trigger && changeValue.trigger.device) {
-              if (
-                changeValue.trigger.device.selectorValues ||
-                (changeValue.trigger.device.operation &&
-                  changeValue.trigger.device.operation.operator)
-              ) {
-                setTriggerValue([]);
-                setRequestParams({ trigger: allValues.trigger });
+            if (changeValue.trigger) {
+              if (changeValue.trigger.device) {
+                if (
+                  changeValue.trigger.device.selectorValues ||
+                  (changeValue.trigger.device.operation &&
+                    changeValue.trigger.device.operation.operator)
+                ) {
+                  setTriggerValue([]);
+                  setRequestParams({ trigger: allValues.trigger });
+                }
+              } else if (['timer', 'manual'].includes(changeValue.trigger.type)) {
+                setActionParams({ trigger: allValues.trigger });
               }
             }
+
             if (allValues.actions) {
               setActionsData(allValues.actions);
             }
@@ -380,6 +394,7 @@ export default () => {
                         form={form}
                         restField={restField}
                         name={name}
+                        trigger={actionParams}
                         triggerType={triggerType}
                         onRemove={() => remove(name)}
                         actionItemData={actionsData.length && actionsData[name]}
@@ -402,13 +417,13 @@ export default () => {
           >
             <Input.TextArea showCount maxLength={200} placeholder={'请输入说明'} rows={4} />
           </Form.Item>
-          {triggerType === TriggerWayType.device &&
-          requestParams &&
-          requestParams.trigger?.device?.productId ? (
-            <Form.Item hidden name={'shakeLimit'} initialValue={DefaultShakeLimit}>
-              <Input />
-            </Form.Item>
-          ) : null}
+          {/*{triggerType === TriggerWayType.device &&*/}
+          {/*requestParams &&*/}
+          {/*requestParams.trigger?.device?.productId ? (*/}
+          {/*  <Form.Item hidden name={['trigger','shakeLimit']}>*/}
+          {/*    <Input />*/}
+          {/*  </Form.Item>*/}
+          {/*) : null}*/}
           <Form.Item hidden name={'id'}>
             <Input />
           </Form.Item>

+ 8 - 8
src/pages/rule-engine/Scene/Save/trigger/device.tsx

@@ -362,14 +362,14 @@ export default (props: TriggerProps) => {
                 }
                 return false;
               })}
-              maxTagCount={0}
-              maxTagPlaceholder={(values) => {
-                return (
-                  <div style={{ maxWidth: 'calc(100% - 8px)' }}>
-                    {values.map((item) => item.label).toString()}
-                  </div>
-                );
-              }}
+              maxTagCount={'responsive'}
+              // maxTagPlaceholder={(values) => {
+              //   return (
+              //     <div style={{ maxWidth: 'calc(100% - 8px)' }}>
+              //       {values.map((item) => item.label).toString()}
+              //     </div>
+              //   );
+              // }}
               placeholder={'请选择属性'}
               style={{ width: '100%' }}
               fieldNames={{ label: 'name', value: 'id' }}

+ 32 - 32
src/pages/rule-engine/Scene/Save/trigger/index.tsx

@@ -98,6 +98,29 @@ export default observer((props: TriggerProps) => {
     [selector],
   );
 
+  useEffect(() => {
+    if (FormModel.trigger?.device?.operation?.functionId && functions.length) {
+      const fcItem: any = functions.find(
+        (_fcItem: any) => _fcItem.id === FormModel.trigger?.device?.operation?.functionId,
+      );
+      if (fcItem) {
+        const _properties = fcItem.valueType ? fcItem.valueType.properties : fcItem.inputs;
+        const array = [];
+        for (const datum of _properties) {
+          array.push({
+            id: datum.id,
+            name: datum.name,
+            type: datum.valueType ? datum.valueType.type : '-',
+            format: datum.valueType ? datum.valueType.format : undefined,
+            options: datum.valueType ? datum.valueType.elements : undefined,
+            value: undefined,
+          });
+        }
+        setFunctionItem(array);
+      }
+    }
+  }, [functions, FormModel]);
+
   const getProducts = async () => {
     const resp = await getProductList({ paging: false });
     if (resp && resp.status === 200) {
@@ -108,7 +131,7 @@ export default observer((props: TriggerProps) => {
         );
 
         if (productItem) {
-          productIdChange(FormModel.trigger!.device.productId, productItem.metadata);
+          await productIdChange(FormModel.trigger!.device.productId, productItem.metadata);
         }
       }
     }
@@ -133,10 +156,6 @@ export default observer((props: TriggerProps) => {
     }
   }, [props.value]);
 
-  useEffect(() => {
-    console.log('FormModel-device', FormModel);
-  }, [FormModel]);
-
   return (
     <div className={classNames(props.className)}>
       <Row gutter={24}>
@@ -186,6 +205,9 @@ export default observer((props: TriggerProps) => {
                       { label: '按部门', value: 'org' },
                     ]}
                     // fieldNames={{ label: 'name', value: 'id' }}
+                    onSelect={(key: string) => {
+                      setSelector(key);
+                    }}
                     style={{ width: 120 }}
                   />
                 </Form.Item>
@@ -306,21 +328,6 @@ export default observer((props: TriggerProps) => {
                   }}
                   style={{ width: '100%' }}
                   placeholder={'请选择功能'}
-                  onSelect={(_: any, data: any) => {
-                    const _properties = data.valueType ? data.valueType.properties : data.inputs;
-                    const array = [];
-                    for (const datum of _properties) {
-                      array.push({
-                        id: datum.id,
-                        name: datum.name,
-                        type: datum.valueType ? datum.valueType.type : '-',
-                        format: datum.valueType ? datum.valueType.format : undefined,
-                        options: datum.valueType ? datum.valueType.elements : undefined,
-                        value: undefined,
-                      });
-                    }
-                    setFunctionItem(array);
-                  }}
                   filterOption={(input: string, option: any) =>
                     option.name.toLowerCase().indexOf(input.toLowerCase()) >= 0
                   }
@@ -347,8 +354,8 @@ export default observer((props: TriggerProps) => {
             >
               <Operation
                 propertiesList={properties.filter((item) => {
-                  if (item.expands) {
-                    return item.expands.type ? item.expands.type.includes('write') : false;
+                  if (item.expands && item.expands.type) {
+                    return item.expands.type.includes('write');
                   }
                   return false;
                 })}
@@ -368,19 +375,12 @@ export default observer((props: TriggerProps) => {
               <Select
                 mode={'multiple'}
                 options={properties.filter((item) => {
-                  if (item.expands) {
-                    return item.expands.type ? item.expands.type.includes('read') : false;
+                  if (item.expands && item.expands.type) {
+                    return item.expands.type.includes('read');
                   }
                   return false;
                 })}
-                maxTagCount={0}
-                maxTagPlaceholder={(values) => {
-                  return (
-                    <div style={{ maxWidth: 'calc(100% - 8px)' }}>
-                      {values.map((item) => item.label).toString()}
-                    </div>
-                  );
-                }}
+                maxTagCount={'responsive'}
                 placeholder={'请选择属性'}
                 style={{ width: '100%' }}
                 fieldNames={{ label: 'name', value: 'id' }}

+ 1 - 0
src/pages/rule-engine/Scene/Save/trigger/operation.tsx

@@ -76,6 +76,7 @@ export default (props: OperatorProps) => {
             label: 'name',
             value: 'id',
           }}
+          maxTagCount={'responsive'}
           style={{ width: '100%' }}
           placeholder={'请选择属性'}
           onSelect={(id: any) => {

+ 57 - 8
src/pages/system/Platforms/Api/basePage.tsx

@@ -1,27 +1,76 @@
 import { Button, Table } from 'antd';
+import { useCallback, useEffect, useState } from 'react';
+import { service } from '../index';
 
-export default () => {
-  // const [selectKeys, setSelectKeys] = useState<string[]>([])
+interface TableProps {
+  parentId: string;
+  onJump: (id: string) => void;
+}
 
-  const save = () => {};
+export default (props: TableProps) => {
+  const [selectKeys, setSelectKeys] = useState<string[]>([]);
+  const [dataSource, setDataSource] = useState([]);
+
+  const queryData = async (pId: string) => {
+    const resp: any = service.queryRoleList(pId);
+    if (resp.status === 200) {
+      setDataSource(resp.result);
+    }
+  };
+
+  useEffect(() => {
+    queryData(props.parentId);
+  }, [props.parentId]);
+
+  const save = useCallback(async () => {}, [selectKeys]);
 
   return (
-    <div>
-      <Table
+    <div className={'platforms-api-table'}>
+      <Table<any>
         columns={[
           {
             title: 'API',
             dataIndex: 'name',
+            render: (text: string, record) => {
+              return (
+                <Button
+                  type={'link'}
+                  style={{ padding: 0 }}
+                  onClick={() => {
+                    props.onJump(record.id);
+                  }}
+                >
+                  {text}
+                </Button>
+              );
+            },
           },
           {
             title: '说明',
             dataIndex: '',
           },
         ]}
+        dataSource={dataSource}
+        rowSelection={{
+          selectedRowKeys: selectKeys,
+          onSelect: (record, selected) => {
+            if (selected) {
+              const newArr = [...selectKeys, record];
+              setSelectKeys(newArr);
+            } else {
+              setSelectKeys([...selectKeys.filter((key) => key !== record)]);
+            }
+          },
+          onSelectAll: (_, selectedRows) => {
+            setSelectKeys(selectedRows);
+          },
+        }}
       />
-      <Button type={'primary'} onClick={save}>
-        保存
-      </Button>
+      <div className={'platforms-api-save'}>
+        <Button type={'primary'} onClick={save}>
+          保存
+        </Button>
+      </div>
     </div>
   );
 };

+ 20 - 0
src/pages/system/Platforms/Api/index.less

@@ -0,0 +1,20 @@
+.platforms-api {
+  display: flex;
+  padding: 24px;
+  background-color: #fff;
+
+  .platforms-api-tree {
+    width: 320px;
+  }
+
+  .platforms-api-table {
+    display: flex;
+    flex-direction: column;
+    flex-grow: 1;
+    width: 0;
+
+    .platforms-api-save {
+      margin-top: 12px;
+    }
+  }
+}

+ 15 - 5
src/pages/system/Platforms/Api/index.tsx

@@ -1,15 +1,25 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import { Tree } from 'antd';
 import Table from './basePage';
+import Tree from './leftTree';
+import './index.less';
+import { useState } from 'react';
 
 export default () => {
+  const [jumpId, setJumpId] = useState('');
+  const [parentId, setParentId] = useState('');
+
   return (
     <PageContainer>
-      <div>
-        <div>
-          <Tree />
+      <div className={'platforms-api'}>
+        <div className={'platforms-api-tree'}>
+          <Tree
+            onSelect={(id) => {
+              setJumpId('');
+              setParentId(id);
+            }}
+          />
         </div>
-        <Table />
+        {!jumpId ? <Table parentId={parentId} onJump={setJumpId} /> : <></>}
       </div>
     </PageContainer>
   );

+ 119 - 0
src/pages/system/Platforms/Api/leftTree.tsx

@@ -0,0 +1,119 @@
+import { Tree } from 'antd';
+import React, { useState } from 'react';
+import { queryChannel } from '@/pages/media/SplitScreen/service';
+
+type LeftTreeType = {
+  onSelect: (id: string) => void;
+};
+
+interface DataNode {
+  name: string;
+  id: string;
+  isLeaf?: boolean;
+  icon?: React.ReactNode;
+  children?: DataNode[];
+}
+
+export default (props: LeftTreeType) => {
+  const [treeData, setTreeData] = useState<DataNode[]>([]);
+
+  /**
+   * 是否为子节点
+   * @param node
+   */
+  const isLeaf = (node: DataNode): boolean => {
+    if (node.children) {
+      return false;
+    }
+    return true;
+  };
+
+  const updateTreeData = (list: DataNode[], key: React.Key, children: DataNode[]): DataNode[] => {
+    return list.map((node) => {
+      if (node.id === key) {
+        return {
+          ...node,
+          children: node.children ? [...node.children, ...children] : children,
+        };
+      }
+
+      if (node.children) {
+        return {
+          ...node,
+          children: updateTreeData(node.children, key, children),
+        };
+      }
+      return node;
+    });
+  };
+
+  const getChildren = (key: React.Key, params: any): Promise<any> => {
+    return new Promise(async (resolve) => {
+      const resp = await queryChannel(params);
+      if (resp.status === 200) {
+        const { total, pageIndex, pageSize } = resp.result;
+        setTreeData((origin) => {
+          const data = updateTreeData(
+            origin,
+            key,
+            resp.result.data.map((item: DataNode) => ({
+              ...item,
+              isLeaf: isLeaf(item),
+            })),
+          );
+
+          if (total > (pageIndex + 1) * pageSize) {
+            setTimeout(() => {
+              getChildren(key, {
+                ...params,
+                pageIndex: params.pageIndex + 1,
+              });
+            }, 50);
+          }
+
+          return data;
+        });
+        resolve(resp.result);
+      }
+    });
+  };
+
+  const onLoadData = ({ key, children }: any): Promise<void> => {
+    return new Promise(async (resolve) => {
+      if (children) {
+        resolve();
+        return;
+      }
+      await getChildren(key, {
+        pageIndex: 0,
+        pageSize: 100,
+        terms: [
+          {
+            column: 'deviceId',
+            value: key,
+          },
+        ],
+      });
+      resolve();
+    });
+  };
+
+  return (
+    <Tree
+      showIcon
+      showLine={{ showLeafIcon: false }}
+      height={550}
+      fieldNames={{
+        title: 'name',
+        key: 'id',
+      }}
+      onSelect={(_, { node }: any) => {
+        if (props.onSelect && node.isLeaf) {
+          props.onSelect(node.id);
+        }
+      }}
+      loadData={onLoadData}
+      treeData={treeData}
+    />
+  );
+};

+ 3 - 3
src/pages/system/Platforms/index.tsx

@@ -12,7 +12,7 @@ import PasswordModal from './password';
 import Service from './service';
 import { message } from 'antd';
 
-export const service = new Service('platforms');
+export const service = new Service('api-client');
 
 export default () => {
   const actionRef = useRef<ActionType>();
@@ -37,11 +37,11 @@ export default () => {
       title: '名称',
     },
     {
-      dataIndex: 'accessName',
+      dataIndex: 'username',
       title: '用户名',
     },
     {
-      dataIndex: 'role',
+      dataIndex: 'roleIdList',
       title: '角色',
     },
     {

+ 6 - 9
src/pages/system/Platforms/save.tsx

@@ -23,6 +23,7 @@ import usePermissions from '@/hooks/permission';
 import { action } from '@formily/reactive';
 import { Response } from '@/utils/typings';
 import { service } from '@/pages/system/Platforms/index';
+import { randomString } from '@/utils/util';
 
 interface SaveProps {
   visible: boolean;
@@ -76,7 +77,7 @@ export default (props: SaveProps) => {
     () =>
       createForm({
         validateFirst: true,
-        initialValues: props.data || { oath2: true },
+        initialValues: props.data || { oath2: true, id: randomString() },
       }),
     [props.data],
   );
@@ -116,7 +117,7 @@ export default (props: SaveProps) => {
               },
             ],
           },
-          clientId: {
+          id: {
             type: 'string',
             title: 'clientId',
             'x-decorator': 'FormItem',
@@ -152,7 +153,7 @@ export default (props: SaveProps) => {
               },
             ],
           },
-          accessName: {
+          username: {
             type: 'string',
             title: '用户名',
             'x-decorator': 'FormItem',
@@ -291,7 +292,7 @@ export default (props: SaveProps) => {
               },
             ],
           },
-          oath2: {
+          enableOAuth2: {
             type: 'boolean',
             title: '开启OAth2',
             required: true,
@@ -334,7 +335,7 @@ export default (props: SaveProps) => {
               },
             ],
           },
-          ipAddress: {
+          ipWhiteList: {
             type: 'string',
             title: 'IP白名单',
             'x-decorator': 'FormItem',
@@ -362,10 +363,6 @@ export default (props: SaveProps) => {
               maxLength: 200,
             },
           },
-          id: {
-            type: 'string',
-            'x-hidden': true,
-          },
         },
       },
     },

+ 23 - 0
src/utils/menu/index.ts

@@ -36,9 +36,26 @@ const extraRouteObj = {
       { code: 'Save2', name: '测试详情' },
     ],
   },
+  'link/Channel': {
+    children: [
+      {
+        code: 'Opcua',
+        name: 'OPC UA',
+        children: [
+          {
+            code: 'Access',
+            name: '数据点绑定',
+          },
+        ],
+      },
+    ],
+  },
   demo: {
     children: [{ code: 'AMap', name: '地图' }],
   },
+  'system/Platforms': {
+    children: [{ code: 'Api', name: '赋权' }],
+  },
 };
 //额外路由
 export const extraRouteArr = [
@@ -70,6 +87,12 @@ export const extraRouteArr = [
         name: '通知记录',
         url: '/account/NotificationRecord',
       },
+      // {
+      //   code: 'account/Center/bind',
+      //   name: '第三方页面',
+      //   url: '/account/center/bind',
+      //   hideInMenu: true,
+      // },
     ],
   },
 ];

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

@@ -36,7 +36,8 @@ export enum MENUS_CODE {
   'link/Certificate/Detail' = 'link/Certificate/Detail',
   'link/Gateway' = 'link/Gateway',
   'link/Opcua' = 'link/Opcua',
-  'link/Channal/Opcua' = 'link/Channal/Opcua',
+  'link/Channel/Opcua' = 'link/Channel/Opcua',
+  'link/Channel/Opcua/Access' = 'link/Channel/Opcua/Access',
   'link/Protocol/Debug' = 'link/Protocol/Debug',
   'link/Protocol' = 'link/Protocol',
   'link/Type' = 'link/Type',