瀏覽代碼

feat: merge

xieyonghong 3 年之前
父節點
當前提交
f9536f8104
共有 50 個文件被更改,包括 2630 次插入273 次删除
  1. 16 0
      config/routes.ts
  2. 7 7
      package.json
  3. 二進制
      public/images/cloud/dueros.png
  4. 二進制
      public/images/northbound/aliyun.png
  5. 二進制
      public/images/northbound/aliyun1.jpg
  6. 二進制
      public/images/northbound/aliyun2.png
  7. 二進制
      public/images/northbound/图片44.png
  8. 0 0
      public/images/network/doeros.jpg
  9. 2 0
      src/components/AMapComponent/hooks/typing.d.ts
  10. 50 0
      src/components/ProTableCard/CardItems/aliyun.tsx
  11. 56 0
      src/components/ProTableCard/CardItems/duerOs.tsx
  12. 6 0
      src/components/ProTableCard/CardItems/noticeTemplate.tsx
  13. 35 0
      src/pages/Northbound/AliCloud/Detail/index.less
  14. 390 0
      src/pages/Northbound/AliCloud/Detail/index.tsx
  15. 311 2
      src/pages/Northbound/AliCloud/index.tsx
  16. 56 0
      src/pages/Northbound/AliCloud/service.ts
  17. 17 0
      src/pages/Northbound/AliCloud/typings.d.ts
  18. 31 0
      src/pages/Northbound/DuerOS/Detail/Doc.tsx
  19. 533 0
      src/pages/Northbound/DuerOS/Detail/index.tsx
  20. 69 46
      src/pages/Northbound/DuerOS/index.tsx
  21. 21 0
      src/pages/Northbound/DuerOS/service.ts
  22. 22 3
      src/pages/Northbound/DuerOS/types.d.ts
  23. 12 0
      src/pages/account/Center/bind/index.less
  24. 56 37
      src/pages/account/Center/bind/index.tsx
  25. 1 1
      src/pages/account/Center/edit/infoEdit.tsx
  26. 1 1
      src/pages/account/Center/edit/passwordEdit.tsx
  27. 5 0
      src/pages/account/Center/index.less
  28. 25 11
      src/pages/account/Center/index.tsx
  29. 11 0
      src/pages/cloud/DuerOS/Save/index.tsx
  30. 69 14
      src/pages/cloud/DuerOS/index.tsx
  31. 30 8
      src/pages/cloud/DuerOS/typings.d.ts
  32. 70 0
      src/pages/device/Instance/Detail/MetadataLog/Property/AMap.tsx
  33. 15 3
      src/pages/device/Instance/Detail/MetadataLog/Property/Detail.tsx
  34. 78 7
      src/pages/device/Instance/Detail/MetadataLog/Property/index.tsx
  35. 1 1
      src/pages/device/Instance/Detail/Running/Property/FileComponent/index.tsx
  36. 17 1
      src/pages/device/Instance/Detail/Tags/Edit.tsx
  37. 100 0
      src/pages/device/Instance/Detail/Tags/location/AMap.tsx
  38. 51 0
      src/pages/device/Instance/Detail/Tags/location/GeoComponent.tsx
  39. 6 0
      src/pages/link/Channel/Opcua/Access/index.tsx
  40. 256 0
      src/pages/link/Channel/Opcua/Save/index.tsx
  41. 4 0
      src/pages/link/Channel/Opcua/index.less
  42. 72 53
      src/pages/link/Channel/Opcua/index.tsx
  43. 26 0
      src/pages/link/Channel/Opcua/service.ts
  44. 7 1
      src/pages/link/Channel/Opcua/typings.d.ts
  45. 1 1
      src/pages/link/Type/Detail/index.tsx
  46. 1 1
      src/pages/system/Platforms/index.tsx
  47. 3 3
      src/pages/system/Platforms/save.tsx
  48. 20 6
      src/utils/menu/index.ts
  49. 5 1
      src/utils/menu/router.ts
  50. 65 65
      yarn.lock

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

+ 7 - 7
package.json

@@ -62,13 +62,13 @@
     "@ant-design/pro-descriptions": "^1.6.8",
     "@ant-design/pro-form": "^1.18.3",
     "@ant-design/pro-layout": "^6.27.2",
-    "@formily/antd": "2.0.19",
-    "@formily/core": "2.0.19",
-    "@formily/json-schema": "2.0.19",
-    "@formily/react": "2.0.19",
-    "@formily/reactive": "2.0.19",
-    "@formily/reactive-react": "2.0.19",
-    "@formily/shared": "2.0.19",
+    "@formily/antd": "2.1.2",
+    "@formily/core": "2.1.2",
+    "@formily/json-schema": "2.1.2",
+    "@formily/react": "2.1.2",
+    "@formily/reactive": "2.1.2",
+    "@formily/reactive-react": "2.1.2",
+    "@formily/shared": "2.1.2",
     "@jetlinks/pro-list": "^1.10.8",
     "@jetlinks/pro-table": "^2.63.11",
     "@liveqing/liveplayer": "^2.6.4",

二進制
public/images/cloud/dueros.png


二進制
public/images/northbound/aliyun.png


二進制
public/images/northbound/aliyun1.jpg


二進制
public/images/northbound/aliyun2.png


二進制
public/images/northbound/图片44.png


+ 0 - 0
public/images/network/doeros.jpg


+ 2 - 0
src/components/AMapComponent/hooks/typing.d.ts

@@ -5,6 +5,7 @@ interface PlaceSearchOptions {
   type?: string;
   extensions?: string;
 }
+
 type resultCityType = {
   name: string;
   citycode: string;
@@ -28,6 +29,7 @@ type searchFn = (status: string, result: searchFnResult) => void;
 
 interface PlaceSearch {
   new (options: PlaceSearchOptions);
+
   search: (keyword: string, callback: searchFn) => void;
   searchInBounds: (keyword: string, bounds: number[], callback: searchFn) => void;
   searchNearBy: (keyword: string, center: any, radius: number) => void;

+ 50 - 0
src/components/ProTableCard/CardItems/aliyun.tsx

@@ -0,0 +1,50 @@
+import React from 'react';
+import { TableCard } from '@/components';
+import '@/style/common.less';
+import '../index.less';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+
+export interface AliyunCardProps extends AliCloudType {
+  detail?: React.ReactNode;
+  actions?: React.ReactNode[];
+  avatarSize?: number;
+}
+
+const defaultImage = require('/public/images/northbound/aliyun.png');
+
+export default (props: AliyunCardProps) => {
+  return (
+    <TableCard
+      detail={props.detail}
+      actions={props.actions}
+      status={props?.state?.value}
+      statusText={props?.state?.text}
+      statusNames={{
+        enabled: StatusColorEnum.processing,
+        disabled: StatusColorEnum.error,
+      }}
+      showMask={false}
+    >
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <img width={88} height={88} src={defaultImage} alt={''} />
+        </div>
+        <div className={'card-item-body'}>
+          <div className={'card-item-header'}>
+            <span className={'card-item-header-name ellipsis'}>{props?.name}</span>
+          </div>
+          <div className={'card-item-content'}>
+            <div>
+              <label>网桥产品</label>
+              <div className={'ellipsis'}>{props?.bridgeProductName || '--'}</div>
+            </div>
+            <div>
+              <label>说明</label>
+              <div className={'ellipsis'}>{props?.description || '--'}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </TableCard>
+  );
+};

+ 56 - 0
src/components/ProTableCard/CardItems/duerOs.tsx

@@ -0,0 +1,56 @@
+import React from 'react';
+import { TableCard } from '@/components';
+import '@/style/common.less';
+import '../index.less';
+import { Tooltip } from 'antd';
+import { DuerOSItem } from '@/pages/cloud/DuerOS/typings';
+
+export interface DuerOSProps extends DuerOSItem {
+  detail?: React.ReactNode;
+  action?: React.ReactNode[];
+  avatarSize?: number;
+}
+
+export const duerOS = require('/public/images/cloud/dueros.png');
+
+export default (props: DuerOSProps) => {
+  return (
+    <TableCard
+      actions={props.action}
+      // detail={props.detail}
+      showStatus={false}
+      // status={props.state?.value}
+      // statusText={props.state?.text}
+      // statusNames={{
+      //   enabled: StatusColorEnum.success,
+      //   disabled: StatusColorEnum.error,
+      // }}
+      showMask={false}
+    >
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <img width={88} height={88} src={duerOS} alt={''} />
+        </div>
+        <div className={'card-item-body'}>
+          <div className={'card-item-header'}>
+            <span className={'card-item-header-name ellipsis'}>{props?.name}</span>
+          </div>
+          <div className={'card-item-content'}>
+            <div>
+              <label>产品</label>
+              <div className={'ellipsis'}>
+                <Tooltip title={props?.name || ''}>{props?.name || ''}</Tooltip>
+              </div>
+            </div>
+            <div>
+              <label>设备类型</label>
+              <div className={'ellipsis'}>
+                <Tooltip title={props.applianceType}>{props.applianceType}</Tooltip>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </TableCard>
+  );
+};

+ 6 - 0
src/components/ProTableCard/CardItems/noticeTemplate.tsx

@@ -27,6 +27,9 @@ export const imgMap = {
   sms: {
     aliyunSms: require('/public/images/notice/sms.png'),
   },
+  webhook: {
+    http: require('/public/images/notice/webhook.png'),
+  },
 };
 
 export const typeList = {
@@ -47,6 +50,9 @@ export const typeList = {
   email: {
     embedded: '默认',
   },
+  webhook: {
+    http: 'webhook',
+  },
 };
 
 export default (props: NoticeCardProps) => {

+ 35 - 0
src/pages/Northbound/AliCloud/Detail/index.less

@@ -0,0 +1,35 @@
+.doc {
+  height: 750px;
+  padding: 24px;
+  overflow-y: auto;
+  color: rgba(#000, 0.8);
+  font-size: 14px;
+  background-color: #fafafa;
+
+  .url {
+    padding: 8px 16px;
+    color: #2f54eb;
+    background-color: rgba(#a7bdf7, 0.2);
+  }
+
+  h1 {
+    margin: 16px 0;
+    color: rgba(#000, 0.85);
+    font-weight: bold;
+    font-size: 14px;
+
+    &:first-child {
+      margin-top: 0;
+    }
+  }
+
+  h2 {
+    margin: 6px 0;
+    color: rgba(0, 0, 0, 0.8);
+    font-size: 14px;
+  }
+
+  .image {
+    margin: 16px 0;
+  }
+}

+ 390 - 0
src/pages/Northbound/AliCloud/Detail/index.tsx

@@ -0,0 +1,390 @@
+import { PermissionButton, TitleComponent } from '@/components';
+import { PageContainer } from '@ant-design/pro-layout';
+import {
+  ArrayCollapse,
+  ArrayItems,
+  Form,
+  FormButtonGroup,
+  FormGrid,
+  FormItem,
+  Input,
+  Select,
+} from '@formily/antd';
+import type { Field } from '@formily/core';
+import { createForm, onFieldValueChange } from '@formily/core';
+import { createSchemaField, observer } from '@formily/react';
+import { Card, Col, Image, message, Row } from 'antd';
+import { useEffect, useMemo, useState } from 'react';
+import { useParams } from 'umi';
+import { useAsyncDataSource } from '@/utils/util';
+import './index.less';
+import { service } from '@/pages/Northbound/AliCloud';
+import usePermissions from '@/hooks/permission';
+
+const Detail = observer(() => {
+  const params = useParams<{ id: string }>();
+  const [dataList, setDataList] = useState<any[]>([]);
+  const [productList, setProductList] = useState<any[]>([]);
+
+  const form = useMemo(
+    () =>
+      createForm({
+        validateFirst: true,
+        effects() {
+          onFieldValueChange('accessConfig.*', async (field, f) => {
+            const regionId = field.query('accessConfig.regionId').value();
+            const accessKeyId = field.query('accessConfig.accessKeyId').value();
+            const accessSecret = field.query('accessConfig.accessSecret').value();
+            if (regionId && accessKeyId && accessSecret) {
+              const response = await service.getAliyunProductsList({
+                regionId,
+                accessKeyId,
+                accessSecret,
+              });
+              f.setFieldState(field.query('bridgeProductKey'), (state) => {
+                state.dataSource = response;
+                setDataList(response);
+              });
+            } else {
+              f.setFieldState(field.query('bridgeProductKey'), (state) => {
+                state.dataSource = [];
+                setDataList([]);
+              });
+            }
+          });
+        },
+      }),
+    [],
+  );
+
+  useEffect(() => {
+    if (params.id) {
+      service.detail(params.id).then((resp) => {
+        if (resp.status === 200) {
+          form.setValues(resp.result);
+        }
+      });
+    }
+  }, [params.id]);
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      FormGrid,
+      Input,
+      Select,
+      ArrayItems,
+      ArrayCollapse,
+    },
+  });
+
+  const queryRegionsList = () => service.getRegionsList();
+
+  const queryProductList = (f: Field) => {
+    const items = form.getValuesIn('mappings')?.map((i: any) => i?.productId) || [];
+    const checked = [...items];
+    const index = checked.findIndex((i) => i === f.value);
+    checked.splice(index, 1);
+    if (productList?.length > 0) {
+      return new Promise((resolve) => {
+        const list = productList.filter((j: any) => !checked.includes(j.value));
+        resolve(list);
+      });
+    } else {
+      return service.getProductsList({ paging: false }).then((resp) => {
+        setProductList(resp);
+        return resp.filter((j: any) => !checked.includes(j.value));
+      });
+    }
+  };
+
+  const queryAliyunProductList = (f: Field) => {
+    const items = form.getValuesIn('mappings')?.map((i: any) => i?.productKey) || [];
+    const checked = [...items];
+    const index = checked.findIndex((i) => i === f.value);
+    checked.splice(index, 1);
+    if (dataList?.length > 0) {
+      return new Promise((resolve) => {
+        const list = dataList.filter((j: any) => !checked.includes(j.value));
+        resolve(list);
+      });
+    } else {
+      const accessConfig = form.getValuesIn('accessConfig') || {};
+      return service.getAliyunProductsList(accessConfig).then((resp) => {
+        setDataList(resp);
+        return resp.filter((j: any) => !checked.includes(j.value));
+      });
+    }
+  };
+
+  const schema: any = {
+    type: 'object',
+    properties: {
+      name: {
+        type: 'string',
+        title: '名称',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-component-props': {
+          placeholder: '请输入名称',
+        },
+        'x-validator': [
+          {
+            max: 64,
+            message: '最多可输入64个字符',
+          },
+        ],
+      },
+      accessConfig: {
+        type: 'object',
+        properties: {
+          regionId: {
+            type: 'string',
+            title: '服务地址',
+            required: true,
+            'x-decorator': 'FormItem',
+            'x-component': 'Select',
+            'x-component-props': {
+              placeholder: '请选择服务地址',
+              showSearch: true,
+              filterOption: (input: string, option: any) =>
+                option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+            },
+            'x-decorator-props': {
+              tooltip: '阿里云内部给每台机器设置的唯一编号',
+            },
+            'x-reactions': ['{{useAsyncDataSource(queryRegionsList)}}'],
+          },
+          accessKeyId: {
+            type: 'string',
+            title: 'accessKey',
+            required: true,
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-component-props': {
+              placeholder: '请输入accessKey',
+            },
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+            ],
+            'x-decorator-props': {
+              tooltip: '用于程序通知方式调用云服务API的用户标识',
+            },
+          },
+          accessSecret: {
+            type: 'string',
+            title: 'accessSecret',
+            required: true,
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-component-props': {
+              placeholder: '请输入accessSecret',
+            },
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+            ],
+            'x-decorator-props': {
+              tooltip: '用于程序通知方式调用云服务费API的秘钥标识',
+            },
+          },
+        },
+      },
+      bridgeProductKey: {
+        type: 'string',
+        title: '网桥产品',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Select',
+        'x-component-props': {
+          placeholder: '请选择网桥产品',
+          showSearch: true,
+          filterOption: (input: string, option: any) =>
+            option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+        },
+        'x-decorator-props': {
+          tooltip: '物联网平台对应的阿里云产品',
+        },
+      },
+      mappings: {
+        type: 'array',
+        required: true,
+        'x-component': 'ArrayCollapse',
+        title: '产品映射',
+        items: {
+          type: 'object',
+          required: true,
+          'x-component': 'ArrayCollapse.CollapsePanel',
+          'x-component-props': {
+            header: '产品映射',
+          },
+          properties: {
+            grid: {
+              type: 'void',
+              'x-component': 'FormGrid',
+              'x-component-props': {
+                minColumns: [24],
+                maxColumns: [24],
+              },
+              properties: {
+                type: 'object',
+                productKey: {
+                  type: 'string',
+                  'x-decorator': 'FormItem',
+                  title: '阿里云产品',
+                  required: true,
+                  'x-component': 'Select',
+                  'x-component-props': {
+                    placeholder: '请选择阿里云产品',
+                    showSearch: true,
+                    filterOption: (input: string, option: any) =>
+                      option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+                  },
+                  'x-decorator-props': {
+                    gridSpan: 12,
+                    tooltip: '阿里云物联网平台产品标识',
+                  },
+                  'x-reactions': ['{{useAsyncDataSource(queryAliyunProductList)}}'],
+                },
+                productId: {
+                  type: 'string',
+                  title: '平台产品',
+                  required: true,
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Select',
+                  'x-decorator-props': {
+                    gridSpan: 12,
+                  },
+                  'x-component-props': {
+                    placeholder: '请选择平台产品',
+                    showSearch: true,
+                    filterOption: (input: string, option: any) =>
+                      option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+                  },
+                  'x-reactions': ['{{useAsyncDataSource(queryProductList)}}'],
+                },
+              },
+            },
+            remove: {
+              type: 'void',
+              'x-component': 'ArrayCollapse.Remove',
+            },
+          },
+        },
+        properties: {
+          addition: {
+            type: 'void',
+            title: '添加',
+            'x-component': 'ArrayCollapse.Addition',
+          },
+        },
+      },
+      description: {
+        title: '说明',
+        'x-component': 'Input.TextArea',
+        'x-decorator': 'FormItem',
+        'x-component-props': {
+          rows: 3,
+          showCount: true,
+          maxLength: 200,
+          placeholder: '请输入说明',
+        },
+      },
+    },
+  };
+
+  const handleSave = async () => {
+    const data: any = await form.submit();
+    const product = dataList.find((item) => item?.value === data?.bridgeProductKey);
+    data.bridgeProductName = product?.label || '';
+    const response: any = data.id ? await service.update(data) : await service.save(data);
+    if (response.status === 200) {
+      message.success('保存成功');
+      history.back();
+    }
+  };
+
+  const { getOtherPermission } = usePermissions('Northbound/AliCloud');
+
+  return (
+    <PageContainer>
+      <Card>
+        <Row gutter={24}>
+          <Col span={14}>
+            <TitleComponent data={'基本信息'} />
+            <Form form={form} layout="vertical" onAutoSubmit={console.log}>
+              <SchemaField
+                schema={schema}
+                scope={{
+                  useAsyncDataSource,
+                  queryRegionsList,
+                  queryProductList,
+                  queryAliyunProductList,
+                }}
+              />
+              <FormButtonGroup.Sticky>
+                <FormButtonGroup.FormItem>
+                  <PermissionButton
+                    type="primary"
+                    isPermission={getOtherPermission(['add', 'update'])}
+                    onClick={() => handleSave()}
+                  >
+                    保存
+                  </PermissionButton>
+                </FormButtonGroup.FormItem>
+              </FormButtonGroup.Sticky>
+            </Form>
+          </Col>
+          <Col span={10}>
+            <div className="doc">
+              <div className="url">
+                阿里云物联网平台:
+                <a
+                  style={{ wordBreak: 'break-all' }}
+                  href="https://help.aliyun.com/document_detail/87368.html"
+                >
+                  https://help.aliyun.com/document_detail/87368.html
+                </a>
+              </div>
+              <h1>1. 概述</h1>
+              <div>
+                在特定场景下,设备无法直接接入阿里云物联网平台时,您可先将设备接入物联网云平台,再使用阿里云“云云对接SDK”,快速构建桥接服务,搭建物联网平台与阿里云物联网平台的双向数据通道。
+              </div>
+              <div className={'image'}>
+                <Image width="100%" src={require('/public/images/northbound/aliyun2.png')} />
+              </div>
+              <h1>2.配置说明</h1>
+              <div>
+                <h2> 1、服务地址</h2>
+                <div>
+                  阿里云内部给每台机器设置的唯一编号。请根据购买的阿里云服务器地址进行选择。
+                </div>
+                <h2> 2、AccesskeyID/Secret</h2>
+                <div>
+                  用于程序通知方式调用云服务费API的用户标识和秘钥获取路径:“阿里云管理控制台”--“用户头像”--“”--“AccessKey管理”--“查看”
+                </div>
+                <div className={'image'}>
+                  <Image width="100%" src={require('/public/images/northbound/aliyun1.jpg')} />
+                </div>
+                <h2> 3. 网桥产品</h2>
+                <div>
+                  物联网平台对于阿里云物联网平台,是一个网关设备,需要映射到阿里云物联网平台的具体产品
+                </div>
+                <h2> 4. 产品映射</h2>
+                <div>将阿里云物联网平台中的产品实例与物联网平台的产品实例进行关联</div>
+              </div>
+            </div>
+          </Col>
+        </Row>
+      </Card>
+    </PageContainer>
+  );
+});
+
+export default Detail;

+ 311 - 2
src/pages/Northbound/AliCloud/index.tsx

@@ -1,5 +1,314 @@
 import { PageContainer } from '@ant-design/pro-layout';
+import SearchComponent from '@/components/SearchComponent';
+import { useRef, useState } from 'react';
+import { history } from 'umi';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { PermissionButton, ProTableCard } from '@/components';
+import {
+  DeleteOutlined,
+  EditOutlined,
+  ExclamationCircleFilled,
+  PlayCircleOutlined,
+  PlusOutlined,
+  StopOutlined,
+} from '@ant-design/icons';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+import AliyunCard from '@/components/ProTableCard/CardItems/aliyun';
+import Service from './service';
+import { Badge, message } from 'antd';
 
-export default () => {
-  return <PageContainer>AliCloud</PageContainer>;
+export const service = new Service('device/aliyun/bridge');
+
+const AliCloud = () => {
+  const actionRef = useRef<ActionType>();
+  const intl = useIntl();
+  const [searchParams, setSearchParams] = useState<any>({});
+
+  const { permission } = PermissionButton.usePermission('Northbound/AliCloud');
+
+  const Tools = (record: any, type: 'card' | 'table') => {
+    return [
+      <PermissionButton
+        key={'update'}
+        type={'link'}
+        style={{ padding: 0 }}
+        isPermission={permission.update}
+        tooltip={
+          type === 'table'
+            ? {
+                title: intl.formatMessage({
+                  id: 'pages.data.option.edit',
+                  defaultMessage: '编辑',
+                }),
+              }
+            : undefined
+        }
+        onClick={() => {}}
+      >
+        <EditOutlined />
+        {type !== 'table' &&
+          intl.formatMessage({
+            id: 'pages.data.option.edit',
+            defaultMessage: '编辑',
+          })}
+      </PermissionButton>,
+      <PermissionButton
+        key={'action'}
+        type={'link'}
+        style={{ padding: 0 }}
+        isPermission={permission.action}
+        popConfirm={{
+          title: intl.formatMessage({
+            id: `pages.data.option.${
+              record?.state?.value !== 'disabled' ? 'disabled' : 'enabled'
+            }.tips`,
+            defaultMessage: '确认禁用?',
+          }),
+          onConfirm: async () => {
+            const resp =
+              record?.state?.value !== 'disabled'
+                ? await service._disable(record.id)
+                : await service._enable(record.id);
+            if (resp.status === 200) {
+              message.success('操作成功!');
+              actionRef.current?.reload?.();
+            } else {
+              message.error('操作失败!');
+            }
+          },
+        }}
+        tooltip={{
+          title: intl.formatMessage({
+            id: `pages.data.option.${record?.state?.value !== 'disabled' ? 'disabled' : 'enabled'}`,
+            defaultMessage: '启用',
+          }),
+        }}
+      >
+        {record?.state?.value !== 'disabled' ? <StopOutlined /> : <PlayCircleOutlined />}
+      </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: () => {},
+        }}
+        tooltip={{
+          title:
+            record.state.value === 'started' ? <span>请先禁用,再删除</span> : <span>删除</span>,
+        }}
+      >
+        <DeleteOutlined />
+      </PermissionButton>,
+    ];
+  };
+
+  const columns: ProColumns<AliCloudType>[] = [
+    {
+      title: '名称',
+      dataIndex: 'name',
+    },
+    {
+      title: '网桥产品',
+      dataIndex: 'bridgeProductName',
+    },
+    {
+      title: '说明',
+      dataIndex: 'description',
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      render: (text: any) => (
+        <span>
+          <Badge status={text.value === 'disabled' ? 'error' : 'success'} text={text.text} />
+        </span>
+      ),
+      valueType: 'select',
+      valueEnum: {
+        disabled: {
+          text: '停用',
+          status: 'disabled',
+        },
+        enabled: {
+          text: '正常',
+          status: 'enabled',
+        },
+      },
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => Tools(record, 'table'),
+    },
+  ];
+
+  return (
+    <PageContainer>
+      <SearchComponent<AliCloudType>
+        field={columns}
+        target="aliyun"
+        onSearch={(data) => {
+          actionRef.current?.reload?.();
+          setSearchParams(data);
+        }}
+      />
+      <div style={{ backgroundColor: 'white', width: '100%', height: 60, padding: 20 }}>
+        <div
+          style={{
+            padding: 10,
+            width: '100%',
+            color: 'rgba(0, 0, 0, 0.55)',
+            backgroundColor: '#f6f6f6',
+          }}
+        >
+          <ExclamationCircleFilled style={{ marginRight: 10 }} />
+          将平台产品与设备数据通过API的方式同步到阿里云物联网平台
+        </div>
+      </div>
+      <ProTableCard<AliCloudType>
+        rowKey="id"
+        search={false}
+        columns={columns}
+        actionRef={actionRef}
+        params={searchParams}
+        options={{ fullScreen: true }}
+        request={(params) =>
+          service.query({
+            ...params,
+            sorts: [
+              {
+                name: 'createTime',
+                order: 'desc',
+              },
+            ],
+          })
+        }
+        pagination={{ pageSize: 10 }}
+        headerTitle={[
+          <PermissionButton
+            onClick={() => {
+              const url = `${getMenuPathByParams(MENUS_CODE['Northbound/AliCloud/Detail'])}`;
+              history.push(url);
+            }}
+            style={{ marginRight: 12 }}
+            isPermission={permission.add}
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.add',
+              defaultMessage: '新增',
+            })}
+          </PermissionButton>,
+        ]}
+        cardRender={(record) => (
+          <AliyunCard
+            {...record}
+            actions={[
+              <PermissionButton
+                type={'link'}
+                onClick={() => {
+                  const url = `${getMenuPathByParams(
+                    MENUS_CODE['Northbound/AliCloud/Detail'],
+                    record.id,
+                  )}`;
+                  history.push(url);
+                }}
+                key={'edit'}
+                isPermission={permission.update}
+              >
+                <EditOutlined />
+                {intl.formatMessage({
+                  id: 'pages.data.option.edit',
+                  defaultMessage: '编辑',
+                })}
+              </PermissionButton>,
+              <PermissionButton
+                key={'action'}
+                type={'link'}
+                style={{ padding: 0 }}
+                isPermission={permission.action}
+                popConfirm={{
+                  title: intl.formatMessage({
+                    id: `pages.data.option.${
+                      record?.state?.value !== 'disabled' ? 'disabled' : 'enabled'
+                    }.tips`,
+                    defaultMessage: '确认禁用?',
+                  }),
+                  onConfirm: async () => {
+                    const resp =
+                      record?.state?.value !== 'disabled'
+                        ? await service._disable(record.id)
+                        : await service._enable(record.id);
+                    if (resp.status === 200) {
+                      message.success('操作成功!');
+                      actionRef.current?.reload?.();
+                    } else {
+                      message.error('操作失败!');
+                    }
+                  },
+                }}
+              >
+                {record?.state?.value !== 'disabled' ? <StopOutlined /> : <PlayCircleOutlined />}
+                {intl.formatMessage({
+                  id: `pages.data.option.${
+                    record?.state?.value !== 'disabled' ? 'disabled' : 'enabled'
+                  }`,
+                  defaultMessage: record?.state?.value !== 'disabled' ? '禁用' : '启用',
+                })}
+              </PermissionButton>,
+              <PermissionButton
+                key="delete"
+                isPermission={permission.delete}
+                type={'link'}
+                style={{ padding: 0 }}
+                tooltip={
+                  record?.state?.value !== 'disabled'
+                    ? { title: intl.formatMessage({ id: 'pages.device.instance.deleteTip' }) }
+                    : undefined
+                }
+                disabled={record?.state?.value !== 'disabled'}
+                popConfirm={{
+                  title: intl.formatMessage({
+                    id: 'pages.data.option.remove.tips',
+                  }),
+                  disabled: record?.state?.value !== 'disabled',
+                  onConfirm: async () => {
+                    if (record?.state?.value === 'disabled') {
+                      await service.remove(record.id);
+                      message.success(
+                        intl.formatMessage({
+                          id: 'pages.data.option.success',
+                          defaultMessage: '操作成功!',
+                        }),
+                      );
+                      actionRef.current?.reload();
+                    } else {
+                      message.error(intl.formatMessage({ id: 'pages.device.instance.deleteTip' }));
+                    }
+                  },
+                }}
+              >
+                <DeleteOutlined />
+              </PermissionButton>,
+            ]}
+          />
+        )}
+      />
+    </PageContainer>
+  );
 };
+
+export default AliCloud;

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

@@ -0,0 +1,56 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<AliCloudType> {
+  // 获取服务地址的下拉列表
+  public getRegionsList = (params?: any) =>
+    request(`/${SystemConst.API_BASE}/device/aliyun/bridge/regions`, {
+      method: 'GET',
+      params,
+    }).then((resp: any) => {
+      return (resp?.result || []).map((item: any) => ({
+        label: item.name,
+        value: item.id,
+      }));
+    });
+  // 产品映射中的阿里云产品下拉列表
+  public getAliyunProductsList = (data?: any) =>
+    request(`/${SystemConst.API_BASE}/device/aliyun/bridge/products/_query`, {
+      method: 'POST',
+      data,
+    }).then((resp: any) => {
+      return (resp?.result?.data || []).map((item: any) => ({
+        label: item?.productName,
+        value: item?.productKey,
+      }));
+    });
+
+  // 产品下拉列表
+  public getProductsList = (data?: any) =>
+    request(`/${SystemConst.API_BASE}/device-product/_query/no-paging`, {
+      method: 'POST',
+      data,
+    }).then((resp: any) => {
+      return (resp?.result || []).map((item: any) => ({
+        label: item.name,
+        value: item.id,
+      }));
+    });
+
+  // 启用
+  public _enable = (id: string, data?: any) =>
+    request(`/${SystemConst.API_BASE}/device/aliyun/bridge/${id}/enable`, {
+      method: 'POST',
+      data,
+    });
+
+  // 禁用
+  public _disable = (id: string, data?: any) =>
+    request(`/${SystemConst.API_BASE}/device/aliyun/bridge/${id}/disable`, {
+      method: 'POST',
+      data,
+    });
+}
+
+export default Service;

+ 17 - 0
src/pages/Northbound/AliCloud/typings.d.ts

@@ -0,0 +1,17 @@
+type AliCloudType = {
+  id: string;
+  name: string;
+  bridgeProductKey: string;
+  bridgeProductName: string;
+  accessConfig: {
+    regionId: string;
+    accessKeyId: string;
+    accessSecret: string;
+  };
+  state?: {
+    text: string;
+    value: string;
+  };
+  mappings: any[];
+  description?: string;
+};

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

+ 533 - 0
src/pages/Northbound/DuerOS/Detail/index.tsx

@@ -0,0 +1,533 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { createSchemaField } from '@formily/react';
+import { ISchema } from '@formily/json-schema';
+import { Card, Col, message, Row } from 'antd';
+import {
+  ArrayCollapse,
+  ArrayTable,
+  Form,
+  FormButtonGroup,
+  FormGrid,
+  FormItem,
+  Input,
+  PreviewText,
+  Select,
+} from '@formily/antd';
+import { PermissionButton } from '@/components';
+import { useMemo } from 'react';
+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({
+    components: {
+      FormGrid,
+      FormItem,
+      Input,
+      Select,
+      ArrayTable,
+      ArrayCollapse,
+      PreviewText,
+    },
+  });
+
+  const { id } = useParams<{ id: string }>();
+
+  const getProduct = () =>
+    service.getProduct().then((resp) => {
+      Store.set('product-list', resp.result);
+      return resp.result;
+    });
+
+  const getTypes = () =>
+    service.getTypes().then((resp) => {
+      Store.set('product-types', resp.result);
+      return resp.result;
+    });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      name: {
+        title: '名称',
+        type: 'string',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-decorator-props': {
+          gridSpan: 1,
+        },
+        required: true,
+        'x-component-props': {
+          placeholder: '请输入名称',
+        },
+        name: 'name',
+      },
+      layout: {
+        type: 'void',
+        'x-decorator': 'FormGrid',
+        'x-decorator-props': {
+          maxColumns: 2,
+          minColumns: 2,
+          columnGap: 24,
+        },
+        properties: {
+          id: {
+            title: '产品',
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Select',
+            'x-component-props': {
+              placeholder: '请选择产品',
+              fieldNames: {
+                label: 'name',
+                value: 'id',
+              },
+              showSearch: true,
+              showArrow: true,
+              filterOption: (input: string, option: any) =>
+                option.name.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+            },
+            'x-reactions': '{{useAsyncDataSource(getProduct)}}',
+            required: true,
+          },
+          applianceType: {
+            title: '设备类型',
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Select',
+            'x-component-props': {
+              placeholder: '请选择产品',
+              fieldNames: {
+                label: 'name',
+                value: 'id',
+              },
+            },
+            'x-reactions': '{{useAsyncDataSource(getTypes)}}',
+            required: true,
+          },
+        },
+      },
+      actionMappings: {
+        type: 'array',
+        title: '动作映射',
+        'x-component': 'ArrayCollapse',
+        'x-decorator': 'FormItem',
+        items: {
+          type: 'object',
+          'x-component': 'ArrayCollapse.CollapsePanel',
+          'x-component-props': {
+            header: '动作',
+          },
+          properties: {
+            index: {
+              type: 'void',
+              'x-component': 'ArrayCollapse.Index',
+            },
+            layout: {
+              type: 'void',
+              'x-decorator': 'FormGrid',
+              'x-decorator-props': {
+                maxColumns: 2,
+                minColumns: 2,
+                columnGap: 24,
+              },
+              properties: {
+                action: {
+                  title: '动作',
+                  'x-component': 'Select',
+                  'x-decorator': 'FormItem',
+                  'x-decorator-props': {
+                    layout: 'vertical',
+                    labelAlign: 'left',
+                  },
+                  required: true,
+                  'x-component-props': {
+                    fieldNames: {
+                      label: 'name',
+                      value: 'id',
+                    },
+                  },
+                },
+                actionType: {
+                  title: '操作',
+                  'x-component': 'Select',
+                  'x-decorator': 'FormItem',
+                  'x-decorator-props': {
+                    layout: 'vertical',
+                    labelAlign: 'left',
+                  },
+                  enum: [
+                    { label: '下发指令', value: 'command' },
+                    { label: '获取历史数据', value: 'latestData' },
+                  ],
+                },
+                command: {
+                  type: 'object',
+                  properties: {
+                    messageType: {
+                      type: 'string',
+                      title: '指令类型',
+                      'x-decorator-props': {
+                        layout: 'vertical',
+                        labelAlign: 'left',
+                        gridSpan: 2,
+                      },
+                      'x-component': 'Select',
+                      'x-decorator': 'FormItem',
+                      enum: [
+                        { label: '读取属性', value: 'READ_PROPERTY' },
+                        { label: '修改属性', value: 'WRITE_PROPERTY' },
+                        { label: '调用功能', value: 'INVOKE_FUNCTION' },
+                      ],
+                      'x-reactions': {
+                        dependencies: ['..actionType'],
+                        fulfill: {
+                          state: {
+                            visible: '{{$deps[0]==="command"}}',
+                          },
+                        },
+                      },
+                    },
+                    message: {
+                      type: 'object',
+                      properties: {
+                        properties: {
+                          title: '属性',
+                          'x-component': 'Select',
+                          'x-decorator': 'FormItem',
+                          'x-decorator-props': {
+                            layout: 'vertical',
+                            labelAlign: 'left',
+                          },
+                          'x-component-props': {
+                            fieldNames: {
+                              label: 'name',
+                              value: 'id',
+                            },
+                          },
+                          'x-reactions': [
+                            {
+                              dependencies: ['..messageType', '...actionType'],
+                              fulfill: {
+                                state: {
+                                  visible:
+                                    '{{["READ_PROPERTY","WRITE_PROPERTY"].includes($deps[0])||$deps[1]==="latestData"}}',
+                                },
+                              },
+                            },
+                            {
+                              dependencies: ['..messageType', '...actionType'],
+                              fulfill: {
+                                state: {
+                                  decoratorProps: {
+                                    gridSpan:
+                                      '{{($deps[0]==="READ_PROPERTY"||$deps[1]==="latestData")?2:1}}',
+                                  },
+                                },
+                              },
+                            },
+                          ],
+                        },
+                        value: {
+                          title: '值',
+                          'x-component': 'Input',
+                          'x-decorator': 'FormItem',
+                          'x-decorator-props': {
+                            layout: 'vertical',
+                            labelAlign: 'left',
+                          },
+                          'x-reactions': {
+                            dependencies: ['..messageType'],
+                            fulfill: {
+                              state: {
+                                visible: '{{["WRITE_PROPERTY"].includes($deps[0])}}',
+                              },
+                            },
+                          },
+                        },
+                        functionId: {
+                          title: '功能',
+                          'x-component': 'Select',
+                          'x-decorator': 'FormItem',
+                          'x-decorator-props': {
+                            layout: 'vertical',
+                            labelAlign: 'left',
+                            gridSpan: 2,
+                          },
+                          'x-component-props': {
+                            fieldNames: {
+                              label: 'name',
+                              value: 'id',
+                            },
+                          },
+                          'x-reactions': {
+                            dependencies: ['..messageType'],
+                            fulfill: {
+                              state: {
+                                visible: '{{["INVOKE_FUNCTION"].includes($deps[0])}}',
+                              },
+                            },
+                          },
+                        },
+                        inputs: {
+                          title: '参数列表',
+                          type: 'array',
+                          'x-component': 'ArrayTable',
+                          'x-decorator': 'FormItem',
+                          'x-decorator-props': {
+                            layout: 'vertical',
+                            labelAlign: 'left',
+                            gridSpan: 2,
+                          },
+                          'x-component-props': {
+                            pagination: { pageSize: 10 },
+                          },
+                          items: {
+                            type: 'object',
+                            properties: {
+                              column1: {
+                                type: 'void',
+                                'x-component': 'ArrayTable.Column',
+                                'x-component-props': { width: 50, title: '参数名称' },
+                                properties: {
+                                  name: {
+                                    type: 'string',
+                                    'x-component': 'PreviewText.Input',
+                                  },
+                                },
+                              },
+                              column2: {
+                                type: 'void',
+                                'x-component': 'ArrayTable.Column',
+                                'x-component-props': { width: 50, title: '类型' },
+                                properties: {
+                                  valueType: {
+                                    type: 'string',
+                                    'x-component': 'PreviewText.Input',
+                                  },
+                                },
+                              },
+                              column3: {
+                                type: 'void',
+                                'x-component': 'ArrayTable.Column',
+                                'x-component-props': { width: 50, title: '值' },
+                                properties: {
+                                  value: {
+                                    type: 'string',
+                                    'x-component': 'Input',
+                                  },
+                                },
+                              },
+                            },
+                          },
+                          'x-reactions': {
+                            dependencies: ['..messageType'],
+                            fulfill: {
+                              state: {
+                                visible: '{{["INVOKE_FUNCTION"].includes($deps[0])}}',
+                              },
+                            },
+                          },
+                        },
+                      },
+                    },
+                  },
+                },
+              },
+            },
+            remove: {
+              type: 'void',
+              'x-component': 'ArrayCollapse.Remove',
+            },
+          },
+        },
+        properties: {
+          addition: {
+            type: 'void',
+            title: '新增动作',
+            'x-component': 'ArrayCollapse.Addition',
+          },
+        },
+      },
+      propertyMappings: {
+        title: '属性映射',
+        type: 'array',
+        'x-component': 'ArrayCollapse',
+        'x-decorator': 'FormItem',
+        items: {
+          type: 'object',
+          'x-component': 'ArrayCollapse.CollapsePanel',
+          'x-component-props': {
+            header: '动作',
+          },
+          properties: {
+            index: {
+              type: 'void',
+              'x-component': 'ArrayCollapse.Index',
+            },
+            layout: {
+              type: 'void',
+              'x-decorator': 'FormGrid',
+              'x-decorator-props': {
+                maxColumns: 2,
+                minColumns: 2,
+                columnGap: 24,
+              },
+              properties: {
+                source: {
+                  title: 'DuerOS属性',
+                  'x-component': 'Select',
+                  'x-decorator': 'FormItem',
+                  'x-decorator-props': {
+                    layout: 'vertical',
+                    labelAlign: 'left',
+                  },
+                  'x-component-props': {
+                    fieldNames: {
+                      label: 'name',
+                      value: 'id',
+                    },
+                  },
+                },
+                target: {
+                  title: '平台属性',
+                  'x-component': 'Select',
+                  'x-decorator': 'FormItem',
+                  'x-decorator-props': {
+                    layout: 'vertical',
+                    labelAlign: 'left',
+                  },
+                  'x-component-props': {
+                    fieldNames: {
+                      label: 'name',
+                      value: 'id',
+                    },
+                    mode: 'tags',
+                  },
+                },
+              },
+            },
+            remove: {
+              type: 'void',
+              'x-component': 'ArrayCollapse.Remove',
+            },
+          },
+        },
+        properties: {
+          addition: {
+            type: 'void',
+            title: '新增属性',
+            'x-component': 'ArrayCollapse.Addition',
+          },
+        },
+      },
+    },
+  };
+
+  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 findapplianceType = (_id: string) => {
+    if (!_id) return;
+    const _productTypes = Store.get('product-types');
+    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('applianceType').value();
+            (field as Field).setDataSource(findapplianceType(productType)?.actions);
+          });
+          onFieldReact('actionMappings.*.layout.command.message.properties', (field) => {
+            const product = field.query('id').value();
+            (field as Field).setDataSource(findProductMetadata(product)?.properties);
+          });
+          onFieldReact('actionMappings.*.layout.command.message.functionId', (field) => {
+            const product = field.query('id').value();
+            (field as Field).setDataSource(findProductMetadata(product)?.functions);
+          });
+          onFieldValueChange(
+            'actionMappings.*.layout.command.message.functionId',
+            (field, form1) => {
+              const functionId = field.value;
+              if (!functionId) return;
+              const product = field.query('id').value();
+              const _functionList = findProductMetadata(product)?.functions;
+              const _function =
+                _functionList && _functionList.find((item: any) => item.id === functionId);
+              form1.setFieldState(field.query('.function'), (state) => {
+                state.value = _function?.inputs.map((item: any) => ({
+                  ...item,
+                  valueType: item?.valueType?.type,
+                }));
+              });
+            },
+          );
+          onFieldReact('propertyMappings.*.layout.source', (field) => {
+            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 className={'page-title-show'}>
+      <Card>
+        <Row>
+          <Col span={12}>
+            <Form layout="vertical" form={form}>
+              <SchemaField schema={schema} scope={{ useAsyncDataSource, getTypes, getProduct }} />
+              <FormButtonGroup.Sticky>
+                <FormButtonGroup.FormItem>
+                  <PermissionButton isPermission={true} type="primary" onClick={handleSave}>
+                    保存
+                  </PermissionButton>
+                </FormButtonGroup.FormItem>
+              </FormButtonGroup.Sticky>
+            </Form>
+          </Col>
+          <Col span={10} push={2}>
+            <Doc />
+          </Col>
+        </Row>
+      </Card>
+    </PageContainer>
+  );
+};
+
+export default Save;

+ 69 - 46
src/pages/Northbound/DuerOS/index.tsx

@@ -3,14 +3,20 @@ 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, StopOutlined } from '@ant-design/icons';
+import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
 import { useIntl } from '@@/plugin-locale/localeExports';
+import { message, Space } from 'antd';
+import { DuerOSItem } from '@/pages/Northbound/DuerOS/types';
+import DuerOSCard from '@/components/ProTableCard/CardItems/duerOs';
+import { history } from '@@/core/history';
+import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+import Service from './service';
 
+export const service = new Service('dueros/product');
 export default () => {
   const actionRef = useRef<ActionType>();
   const intl = useIntl();
   const [searchParams, setSearchParams] = useState<any>({});
-
   const { permission } = PermissionButton.usePermission('Northbound/DuerOS');
 
   const Tools = (record: any, type: 'card' | 'table') => {
@@ -30,7 +36,9 @@ export default () => {
               }
             : undefined
         }
-        onClick={() => {}}
+        onClick={() => {
+          history.push(getMenuPathByParams(MENUS_CODE['Northbound/DuerOS/Detail'], record.id));
+        }}
       >
         <EditOutlined />
         {type !== 'table' &&
@@ -40,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 />
@@ -94,12 +69,37 @@ export default () => {
     ];
   };
 
-  const columns: ProColumns<DuerOSType>[] = [
+  const columns: ProColumns<DuerOSItem>[] = [
     {
+      title: intl.formatMessage({
+        id: 'pages.table.name',
+        defaultMessage: '名称',
+      }),
       dataIndex: 'name',
     },
     {
       title: intl.formatMessage({
+        id: 'pages.cloud.duerOS.applianceType',
+        defaultMessage: '设备类型',
+      }),
+      dataIndex: 'applianceType',
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.cloud.duerOS.manufacturerName',
+        defaultMessage: '厂家名称',
+      }),
+      dataIndex: 'manufacturerName',
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.cloud.duerOS.version',
+        defaultMessage: '动作数量',
+      }),
+      dataIndex: 'version',
+    },
+    {
+      title: intl.formatMessage({
         id: 'pages.data.option',
         defaultMessage: '操作',
       }),
@@ -112,22 +112,45 @@ export default () => {
 
   return (
     <PageContainer>
-      <SearchComponent<DuerOSType>
+      <SearchComponent<DuerOSItem>
         field={columns}
-        target="device-instance"
+        target="northbound-dueros"
         onSearch={(data) => {
           actionRef.current?.reset?.();
           setSearchParams(data);
         }}
       />
-      <ProTableCard<DuerOSType>
+      <ProTableCard<DuerOSItem>
         rowKey="id"
         search={false}
         columns={columns}
         actionRef={actionRef}
         params={searchParams}
         options={{ fullScreen: true }}
+        request={(params) =>
+          service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        }
+        cardRender={(record) => <DuerOSCard {...record} action={Tools(record, 'card')} />}
+        headerTitle={
+          <Space>
+            <PermissionButton
+              isPermission={true}
+              onClick={() => {
+                history.push(getMenuPathByParams(MENUS_CODE['Northbound/DuerOS/Detail']));
+              }}
+              key="button"
+              icon={<PlusOutlined />}
+              type="primary"
+            >
+              {intl.formatMessage({
+                id: 'pages.data.option.add',
+                defaultMessage: '新增',
+              })}
+            </PermissionButton>
+          </Space>
+        }
       />
+      {/*<Save/>*/}
     </PageContainer>
   );
 };

+ 21 - 0
src/pages/Northbound/DuerOS/service.ts

@@ -0,0 +1,21 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+import { DuerOSItem } from '@/pages/Northbound/DuerOS/types';
+
+class Service extends BaseService<DuerOSItem> {
+  public getTypes = () =>
+    request(`${this.uri}/types`, {
+      method: 'GET',
+    });
+
+  public getProduct = () =>
+    request(`/${SystemConst.API_BASE}/device-product/_query/no-paging`, {
+      method: 'POST',
+      data: {
+        paging: false,
+      },
+    });
+}
+
+export default Service;

+ 22 - 3
src/pages/Northbound/DuerOS/types.d.ts

@@ -1,4 +1,23 @@
-type DuerOSType = {
-  id: string;
-  name: string;
+import { BaseItem } from '@/utils/typings';
+
+type PropertyMapping = {
+  source: string;
+  target: string[];
 };
+
+type ActionMapping = {
+  action: string;
+  actionType: string;
+  command: {
+    message: Record<string, any>;
+    messageType: string;
+  };
+};
+type DuerOSItem = {
+  version: number;
+  manufacturerName: string;
+  autoReportProperty: boolean;
+  applianceType: string;
+  actionMappings: ActionMapping[];
+  propertyMappings: PropertyMapping[];
+} & BaseItem;

+ 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>
     </>
   );

+ 1 - 1
src/pages/account/Center/edit/infoEdit.tsx

@@ -1,4 +1,4 @@
-import { Modal, Form, Input, Col, Row } from 'antd';
+import { Col, Form, Input, Modal, Row } from 'antd';
 
 interface Props {
   data: any;

+ 1 - 1
src/pages/account/Center/edit/passwordEdit.tsx

@@ -1,6 +1,6 @@
 import { Modal } from 'antd';
 import { createSchemaField } from '@formily/react';
-import { Form, FormItem, Password, Input } from '@formily/antd';
+import { Form, FormItem, Input, Password } from '@formily/antd';
 import { ISchema } from '@formily/json-schema';
 import { useIntl } from 'umi';
 import { useMemo } from 'react';

+ 5 - 0
src/pages/account/Center/index.less

@@ -1,9 +1,11 @@
 .info {
   margin-top: 15px;
 }
+
 .top {
   display: flex;
   width: 100%;
+
   .avatar {
     display: flex;
     flex-direction: column;
@@ -12,16 +14,19 @@
     width: 350px;
     height: 200px;
   }
+
   .content {
     width: 80%;
     padding-top: 15px;
   }
+
   .action {
     position: relative;
     top: 15px;
     right: 20px;
   }
 }
+
 .bind {
   display: flex;
   align-items: center;

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

@@ -1,5 +1,16 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import { Card, Upload, message, Avatar, Button, Descriptions, Divider, Col, Row } 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'));
                         }}
                       >
                         立即绑定

+ 11 - 0
src/pages/cloud/DuerOS/Save/index.tsx

@@ -0,0 +1,11 @@
+import { Modal } from 'antd';
+
+const Save = () => {
+  return (
+    <Modal title="新增" visible>
+      。。。
+    </Modal>
+  );
+};
+
+export default Save;

+ 69 - 14
src/pages/cloud/DuerOS/index.tsx

@@ -1,12 +1,14 @@
 import BaseService from '@/utils/BaseService';
-import type { ProColumns, ActionType } from '@jetlinks/pro-table';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import { PageContainer } from '@ant-design/pro-layout';
-import BaseCrud from '@/components/BaseCrud';
-import { useRef } from 'react';
-import { Tooltip } from 'antd';
-import { EditOutlined, MinusOutlined } from '@ant-design/icons';
+import { useRef, useState } from 'react';
+import { Space, Tooltip } from 'antd';
+import { DeleteOutlined, EditOutlined, MinusOutlined, PlusOutlined } from '@ant-design/icons';
 import type { DuerOSItem } from '@/pages/cloud/DuerOS/typings';
 import { useIntl } from '@@/plugin-locale/localeExports';
+import SearchComponent from '@/components/SearchComponent';
+import { PermissionButton, ProTableCard } from '@/components';
+import DuerOSCard from '@/components/ProTableCard/CardItems/duerOs';
 
 export const service = new BaseService<DuerOSItem>('dueros/product');
 const DuerOS = () => {
@@ -82,19 +84,72 @@ const DuerOS = () => {
       ],
     },
   ];
-  const schema = {};
+  // const schema = {};
 
+  const [param, setParam] = useState({});
   return (
     <PageContainer>
-      <BaseCrud<DuerOSItem>
-        columns={columns}
-        service={service}
-        title={intl.formatMessage({
-          id: 'pages.cloud.duerOS',
-          defaultMessage: 'DuerOS',
-        })}
-        schema={schema}
+      <SearchComponent
+        field={columns}
+        onSearch={(data) => {
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTableCard<any>
         actionRef={actionRef}
+        rowKey="id"
+        search={false}
+        params={param}
+        columns={columns}
+        request={(params) =>
+          service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        }
+        cardRender={(record) => (
+          <DuerOSCard
+            {...record}
+            action={[
+              <PermissionButton>
+                <EditOutlined />
+                编辑
+              </PermissionButton>,
+              <PermissionButton
+                type="link"
+                popConfirm={{
+                  disabled: record.state?.value !== 'disabled',
+                  title: '确认删除?',
+                  onConfirm: async () => {
+                    await service.remove(record.id);
+                    actionRef.current?.reset?.();
+                  },
+                }}
+                isPermission={true}
+                key="delete"
+              >
+                <DeleteOutlined />
+              </PermissionButton>,
+            ]}
+          />
+        )}
+        headerTitle={
+          <Space>
+            <PermissionButton
+              isPermission={true}
+              onClick={() => {
+                // setCurrent(undefined);
+                // setVisible(true);
+              }}
+              key="button"
+              icon={<PlusOutlined />}
+              type="primary"
+            >
+              {intl.formatMessage({
+                id: 'pages.data.option.add',
+                defaultMessage: '新增',
+              })}
+            </PermissionButton>
+          </Space>
+        }
       />
     </PageContainer>
   );

+ 30 - 8
src/pages/cloud/DuerOS/typings.d.ts

@@ -1,15 +1,37 @@
 import type { BaseItem } from '@/utils/typings';
 
-type Action = {
-  arg: unknown[];
-} & BaseItem;
+// type Action = {
+//   arg: unknown[];
+// } & BaseItem;
+//
+// type Mode = BaseItem;
+//
+// type Property = BaseItem;
 
-type Mode = BaseItem;
+// type DuerOSItem = {
+//   actions?: Action[];
+//   modes?: Mode[];
+//   properties?: Property[];
+// } & BaseItem;
 
-type Property = BaseItem;
+type PropertyMapping = {
+  source: string;
+  target: string[];
+};
 
+type ActionMapping = {
+  action: string;
+  actionType: string;
+  command: {
+    message: Record<string, any>;
+    messageType: string;
+  };
+};
 type DuerOSItem = {
-  actions: Action[];
-  modes: Mode[];
-  properties: Property[];
+  version: number;
+  manufacturerName: string;
+  autoReportProperty: boolean;
+  applianceType: string;
+  actionMappings: ActionMapping[];
+  propertyMappings: PropertyMapping[];
 } & BaseItem;

+ 70 - 0
src/pages/device/Instance/Detail/MetadataLog/Property/AMap.tsx

@@ -0,0 +1,70 @@
+import { AMap, PathSimplifier } from '@/components';
+import { Button, Space } from 'antd';
+import { useEffect, useRef, useState } from 'react';
+
+interface Props {
+  value: any;
+  name: string;
+}
+
+export default (props: Props) => {
+  const [speed] = useState(1000000);
+  const PathNavigatorRef = useRef<PathNavigator | null>(null);
+  const [dataSource, setDataSource] = useState<any>({});
+
+  useEffect(() => {
+    const list: any[] = [];
+    (props?.value?.data || []).forEach((item: any) => {
+      list.push([item.value.lon, item.value.lat]);
+    });
+    setDataSource({
+      name: props?.name || '',
+      path: [...list],
+    });
+  }, [props.value]);
+  return (
+    <div style={{ position: 'relative' }}>
+      <div style={{ position: 'absolute', right: 0, top: 5, zIndex: 999 }}>
+        <Space>
+          <Button
+            type="primary"
+            onClick={() => {
+              if (PathNavigatorRef.current) {
+                PathNavigatorRef.current.start();
+              }
+            }}
+          >
+            开始动画
+          </Button>
+          <Button
+            type="primary"
+            onClick={() => {
+              if (PathNavigatorRef.current) {
+                PathNavigatorRef.current.stop();
+              }
+            }}
+          >
+            停止动画
+          </Button>
+        </Space>
+      </div>
+      <AMap
+        AMapUI
+        style={{
+          height: 500,
+          width: '100%',
+        }}
+      >
+        <PathSimplifier pathData={[dataSource]}>
+          <PathSimplifier.PathNavigator
+            speed={speed}
+            isAuto={false}
+            onCreate={(nav) => {
+              PathNavigatorRef.current = nav;
+            }}
+          />
+        </PathSimplifier>
+      </AMap>
+    </div>
+  );
+};

+ 15 - 3
src/pages/device/Instance/Detail/MetadataLog/Property/Detail.tsx

@@ -1,5 +1,5 @@
-import { Modal, Input } from 'antd';
-// import ReactMarkdown from "react-markdown";
+import { Input, Modal } from 'antd';
+import ReactJson from 'react-json-view';
 
 interface Props {
   close: () => void;
@@ -15,7 +15,18 @@ const Detail = (props: Props) => {
       return (
         <div>
           <div>自定义属性</div>
-          {JSON.stringify(value)}
+          <div>
+            {
+              // @ts-ignore
+              <ReactJson
+                displayObjectSize={false}
+                displayDataTypes={false}
+                style={{ marginTop: 10 }}
+                name={false}
+                src={value}
+              />
+            }
+          </div>
         </div>
       );
     } else {
@@ -32,6 +43,7 @@ const Detail = (props: Props) => {
     <Modal
       title="详情"
       visible
+      destroyOnClose={true}
       onOk={() => {
         props.close();
       }}

+ 78 - 7
src/pages/device/Instance/Detail/MetadataLog/Property/index.tsx

@@ -1,14 +1,26 @@
-import { service } from '@/pages/device/Instance';
+import { InstanceModel, service } from '@/pages/device/Instance';
 import { useParams } from 'umi';
-import { DatePicker, Modal, Radio, Select, Space, Table, Tabs } from 'antd';
+import {
+  DatePicker,
+  Modal,
+  Popconfirm,
+  Radio,
+  Select,
+  Space,
+  Table,
+  Tabs,
+  Tooltip as ATooltip,
+} from 'antd';
 import type { PropertyMetadata } from '@/pages/device/Product/typings';
 import encodeQuery from '@/utils/encodeQuery';
 import { useEffect, useState } from 'react';
 import moment from 'moment';
-import { Axis, Chart, Geom, Legend, Tooltip, Slider } from 'bizcharts';
+import { Axis, Chart, Geom, Legend, Slider, Tooltip } from 'bizcharts';
 import FileComponent from '../../Running/Property/FileComponent';
 import { DownloadOutlined, SearchOutlined } from '@ant-design/icons';
 import Detail from './Detail';
+import AMap from './AMap';
+
 interface Props {
   visible: boolean;
   close: () => void;
@@ -33,6 +45,8 @@ const PropertyLog = (props: Props) => {
   const [detailVisible, setDetailVisible] = useState<boolean>(false);
   const [current, setCurrent] = useState<any>('');
 
+  const [geoList, setGeoList] = useState<any[]>([]);
+
   const columns = [
     {
       title: '时间',
@@ -62,13 +76,49 @@ const PropertyLog = (props: Props) => {
               }}
             />
           ) : (
-            <DownloadOutlined />
+            <ATooltip title="下载">
+              <Popconfirm
+                title="确认修改"
+                onConfirm={() => {
+                  const type = (record?.value || '').split('.').pop();
+                  const downloadUrl = record.value;
+                  const downNode = document.createElement('a');
+                  downNode.href = downloadUrl;
+                  downNode.download = `${InstanceModel.detail.name}-${data.name}${moment(
+                    new Date().getTime(),
+                  ).format('YYYY-MM-DD-HH-mm-ss')}.${type}`;
+                  downNode.style.display = 'none';
+                  document.body.appendChild(downNode);
+                  downNode.click();
+                  document.body.removeChild(downNode);
+                }}
+              >
+                <DownloadOutlined />
+              </Popconfirm>
+            </ATooltip>
           )}
         </a>
       ),
     },
   ];
 
+  const geoColumns = [
+    {
+      title: '时间',
+      dataIndex: 'timestamp',
+      key: 'timestamp',
+      render: (text: any) => <span>{text ? moment(text).format('YYYY-MM-DD HH:mm:ss') : ''}</span>,
+    },
+    {
+      title: '位置',
+      dataIndex: 'value',
+      key: 'value',
+      render: (text: any, record: any) => (
+        <FileComponent type="table" value={{ formatValue: record.value }} data={data} />
+      ),
+    },
+  ];
+
   const tabList = [
     {
       tab: '列表',
@@ -140,7 +190,6 @@ const PropertyLog = (props: Props) => {
   };
 
   useEffect(() => {
-    console.log(data);
     if (visible) {
       handleSearch(
         {
@@ -179,7 +228,7 @@ const PropertyLog = (props: Props) => {
               );
             }}
             dataSource={dataSource?.data || []}
-            columns={columns}
+            columns={data?.valueType?.type === 'geoPoint' ? geoColumns : columns}
             pagination={{
               pageSize: dataSource?.pageSize || 10,
               showSizeChanger: true,
@@ -317,7 +366,8 @@ const PropertyLog = (props: Props) => {
       visible={visible}
       onCancel={() => close()}
       onOk={() => close()}
-      width="45vw"
+      destroyOnClose={true}
+      width="50vw"
     >
       <div style={{ marginBottom: '20px' }}>
         <Space>
@@ -430,6 +480,22 @@ const PropertyLog = (props: Props) => {
               });
             }
           }
+          if (key === 'geo') {
+            service
+              .getPropertyData(
+                params.id,
+                encodeQuery({
+                  paging: false,
+                  terms: { property: data.id, timestamp$BTW: start && end ? [start, end] : [] },
+                  sorts: { timestamp: 'desc' },
+                }),
+              )
+              .then((resp) => {
+                if (resp.status === 200) {
+                  setGeoList(resp.result);
+                }
+              });
+          }
         }}
       >
         {tabList.map((item) => (
@@ -437,6 +503,11 @@ const PropertyLog = (props: Props) => {
             {renderComponent(item.key)}
           </Tabs.TabPane>
         ))}
+        {data?.valueType?.type === 'geoPoint' && (
+          <Tabs.TabPane tab="轨迹" key="geo">
+            <AMap value={geoList} name={data.name} />
+          </Tabs.TabPane>
+        )}
       </Tabs>
       {detailVisible && (
         <Detail

+ 1 - 1
src/pages/device/Instance/Detail/Running/Property/FileComponent/index.tsx

@@ -53,7 +53,7 @@ const FileComponent = (props: Props) => {
           <img src={imgMap.get(flag) || imgMap.get('other')} />
         </div>
       );
-    } else if (data?.valueType?.type === 'object') {
+    } else if (data?.valueType?.type === 'object' || data?.valueType?.type === 'geoPoint') {
       return (
         <div className={props.type === 'card' ? styles.other : {}}>
           {JSON.stringify(value?.formatValue)}

+ 17 - 1
src/pages/device/Instance/Detail/Tags/Edit.tsx

@@ -4,6 +4,7 @@ import { InstanceModel, service } from '@/pages/device/Instance';
 import { ArrayTable, FormItem, Input } from '@formily/antd';
 import { message, Modal } from 'antd';
 import { useIntl } from 'umi';
+import GeoComponent from './location/GeoComponent';
 
 interface Props {
   close: () => void;
@@ -25,6 +26,7 @@ const Edit = (props: Props) => {
       FormItem,
       Input,
       ArrayTable,
+      GeoComponent,
     },
   });
 
@@ -51,7 +53,6 @@ const Edit = (props: Props) => {
                   type: 'string',
                   'x-decorator': 'FormItem',
                   'x-component': 'Input',
-                  // 'x-disabled': true
                 },
               },
             },
@@ -84,10 +85,25 @@ const Edit = (props: Props) => {
                 }),
               },
               properties: {
+                type: {
+                  type: 'string',
+                  name: '类型',
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Input',
+                  'x-hidden': true,
+                },
                 value: {
                   type: 'string',
                   'x-decorator': 'FormItem',
                   'x-component': 'Input',
+                  'x-reactions': {
+                    dependencies: ['.type'],
+                    fulfill: {
+                      state: {
+                        componentType: '{{$deps[0]==="geoPoint"?"GeoComponent":"Input"}}',
+                      },
+                    },
+                  },
                 },
               },
             },

+ 100 - 0
src/pages/device/Instance/Detail/Tags/location/AMap.tsx

@@ -0,0 +1,100 @@
+import { AMap } from '@/components';
+import usePlaceSearch from '@/components/AMapComponent/hooks/PlaceSearch';
+import { Input, Modal, Select } from 'antd';
+import { debounce } from 'lodash';
+import { Marker } from 'react-amap';
+import { useEffect, useState } from 'react';
+
+interface Props {
+  value: any;
+  close: () => void;
+  ok: (data: any) => void;
+}
+
+export default (props: Props) => {
+  const [markerCenter, setMarkerCenter] = useState<any>({ longitude: 0, latitude: 0 });
+  const [map, setMap] = useState(null);
+
+  const { data, search } = usePlaceSearch(map);
+
+  const [value, setValue] = useState<any>(props.value);
+
+  const onSearch = (value1: string) => {
+    search(value1);
+  };
+
+  useEffect(() => {
+    setValue(props.value);
+    const list = props?.value.split(',') || [];
+    if (!!props.value && list.length === 2) {
+      setMarkerCenter({
+        longitude: list[0],
+        latitude: list[1],
+      });
+    }
+  }, [props.value]);
+  return (
+    <Modal
+      visible
+      title="地理位置"
+      width={'55vw'}
+      onCancel={() => props.close()}
+      onOk={() => {
+        props.ok(value);
+      }}
+    >
+      <div style={{ position: 'relative' }}>
+        <div
+          style={{
+            position: 'absolute',
+            width: 300,
+            padding: 10,
+            right: 5,
+            top: 5,
+            zIndex: 999,
+            backgroundColor: 'white',
+          }}
+        >
+          <Select
+            showSearch
+            options={data}
+            filterOption={false}
+            onSearch={debounce(onSearch, 300)}
+            style={{ width: '100%', marginBottom: 10 }}
+            onSelect={(key: string, node: any) => {
+              setValue(key);
+              setMarkerCenter({
+                longitude: node.lnglat.lng,
+                latitude: node.lnglat.lat,
+              });
+            }}
+          />
+          <Input value={value} readOnly />
+        </div>
+        <AMap
+          AMapUI
+          style={{
+            height: 500,
+            width: '100%',
+          }}
+          center={markerCenter.longitude ? markerCenter : undefined}
+          onInstanceCreated={setMap}
+          events={{
+            click: (e: any) => {
+              setValue(`${e.lnglat.lng},${e.lnglat.lat}`);
+              setMarkerCenter({
+                longitude: e.lnglat.lng,
+                latitude: e.lnglat.lat,
+              });
+            },
+          }}
+        >
+          {markerCenter.longitude ? (
+            // @ts-ignore
+            <Marker position={markerCenter} />
+          ) : null}
+        </AMap>
+      </div>
+    </Modal>
+  );
+};

+ 51 - 0
src/pages/device/Instance/Detail/Tags/location/GeoComponent.tsx

@@ -0,0 +1,51 @@
+import { EnvironmentOutlined } from '@ant-design/icons';
+import { Input } from 'antd';
+import { useEffect, useState } from 'react';
+import AMap from './AMap';
+
+interface Props {
+  value: string;
+  onChange: (value: string) => void;
+}
+
+const GeoComponent = (props: Props) => {
+  const [visible, setVisible] = useState<boolean>(false);
+  const [value, setValue] = useState<any>(props?.value);
+
+  useEffect(() => {
+    setValue(props?.value);
+  }, [props.value]);
+
+  return (
+    <div>
+      <Input
+        addonAfter={
+          <EnvironmentOutlined
+            onClick={() => {
+              setVisible(true);
+            }}
+          />
+        }
+        value={value}
+        onChange={(e) => {
+          setValue(e.target.value);
+          props.onChange(e.target.value);
+        }}
+      />
+      {visible && (
+        <AMap
+          value={value}
+          close={() => {
+            setVisible(false);
+          }}
+          ok={(param) => {
+            props.onChange(param);
+            setValue(param);
+            setVisible(false);
+          }}
+        />
+      )}
+    </div>
+  );
+};
+export default GeoComponent;

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

+ 4 - 0
src/pages/link/Channel/Opcua/index.less

@@ -1,13 +1,16 @@
 .topCard {
   display: flex;
   align-items: center;
+
   .img {
     position: relative;
     top: 10px;
     left: 20px;
   }
+
   .text {
     margin-left: 24px;
+
     .p1 {
       height: 22px;
       margin-bottom: 7px;
@@ -15,6 +18,7 @@
       font-size: 18px;
       line-height: 22px;
     }
+
     .p2 {
       height: 20px;
       color: rgba(0, 0, 0, 0.75);

+ 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 { Card, Col, Row, Badge } 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;
+  };
 };

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

@@ -658,7 +658,7 @@ const Save = observer(() => {
                 'x-component': 'ArrayCollapse',
                 'x-decorator': 'FormItem',
                 items: {
-                  type: 'void',
+                  type: 'object',
                   'x-component': 'ArrayCollapse.CollapsePanel',
                   'x-component-props': {
                     header: '配置信息',

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

@@ -1,8 +1,8 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
 import { useRef, useState } from 'react';
 import { useIntl } from '@@/plugin-locale/localeExports';
-import ProTable from '@jetlinks/pro-table';
 import { BadgeStatus, PermissionButton } from '@/components';
 import SearchComponent from '@/components/SearchComponent';
 import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';

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

@@ -7,22 +7,22 @@ import {
   FormItem,
   Input,
   NumberPicker,
+  Password,
+  Radio,
   Select,
   Switch,
   TreeSelect,
-  Password,
-  Radio,
 } from '@formily/antd';
 import { message, Modal } from 'antd';
 import React, { useMemo, useState } from 'react';
 import * as ICONS from '@ant-design/icons';
+import { PlusOutlined } from '@ant-design/icons';
 import type { ISchema } from '@formily/json-schema';
 import { PermissionButton } from '@/components';
 import usePermissions from '@/hooks/permission';
 import { action } from '@formily/reactive';
 import { Response } from '@/utils/typings';
 import { service } from '@/pages/system/Platforms/index';
-import { PlusOutlined } from '@ant-design/icons';
 
 interface SaveProps {
   visible: boolean;

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

@@ -36,6 +36,20 @@ const extraRouteObj = {
       { code: 'Save2', name: '测试详情' },
     ],
   },
+  'link/Channel': {
+    children: [
+      {
+        code: 'Opcua',
+        name: 'OPC UA',
+        children: [
+          {
+            code: 'Access',
+            name: '数据点绑定',
+          },
+        ],
+      },
+    ],
+  },
   demo: {
     children: [{ code: 'AMap', name: '地图' }],
   },
@@ -57,12 +71,12 @@ export const extraRouteArr = [
         name: '基本设置',
         url: '/account/center',
       },
-      {
-        code: 'account/Center/bind',
-        name: '第三方页面',
-        url: '/account/center/bind',
-        hideInMenu: true,
-      },
+      // {
+      //   code: 'account/Center/bind',
+      //   name: '第三方页面',
+      //   url: '/account/center/bind',
+      //   hideInMenu: true,
+      // },
     ],
   },
 ];

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

@@ -35,7 +35,8 @@ export enum MENUS_CODE {
   'link/Certificate' = 'link/Certificate',
   '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',
@@ -113,7 +114,9 @@ export enum MENUS_CODE {
   'account/Center' = 'account/Center',
   'account/Center/bind' = 'account/Center/bind',
   'Northbound/DuerOS' = 'Northbound/DuerOS',
+  'Northbound/DuerOS/Detail' = 'Northbound/DuerOS/Detail',
   'Northbound/AliCloud' = 'Northbound/AliCloud',
+  'Northbound/AliCloud/Detail' = 'Northbound/AliCloud/Detail',
   'system/Platforms' = 'system/Platforms',
 }
 
@@ -154,4 +157,5 @@ export const getDetailNameByCode = {
   'link/AccessConfig/Detail': '配置详情',
   'media/Stream/Detail': '流媒体详情',
   'rule-engine/Alarm/Log/Detail': '告警日志',
+  'Northbound/AliCloud/Detail': '阿里云详情',
 };

+ 65 - 65
yarn.lock

@@ -2849,85 +2849,85 @@
   resolved "https://registry.yarnpkg.com/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz#2dc8c57044de0340eb53a7ba602e59abf80dc799"
   integrity sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==
 
-"@formily/antd@2.0.19":
-  version "2.0.19"
-  resolved "https://registry.yarnpkg.com/@formily/antd/-/antd-2.0.19.tgz#7419807965d5d1f39324b46e0be4f6aae04ca267"
-  integrity sha512-pxybyq2zWS4Ki56oY7227yjonVN7mnFiaIXSy/NVRD5wXxUBzOvrFA+4LiJuFGv0vzUkmSBbFCBkcDb/8TRZXQ==
-  dependencies:
-    "@formily/core" "2.0.19"
-    "@formily/grid" "2.0.19"
-    "@formily/json-schema" "2.0.19"
-    "@formily/react" "2.0.19"
-    "@formily/reactive" "2.0.19"
-    "@formily/reactive-react" "2.0.19"
-    "@formily/shared" "2.0.19"
+"@formily/antd@2.1.2":
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/@formily/antd/-/antd-2.1.2.tgz#48978aa1bd44bbbe370c3d4638300b06780b350a"
+  integrity sha512-o1EmUBXpCxTnNHEEyGOsxalp6KY1/YQNlOC+b/oE9skPuqr77/FMtVESYuD3aX4SseeZTOQzhH2JIRcU2/Hksw==
+  dependencies:
+    "@formily/core" "2.1.2"
+    "@formily/grid" "2.1.2"
+    "@formily/json-schema" "2.1.2"
+    "@formily/react" "2.1.2"
+    "@formily/reactive" "2.1.2"
+    "@formily/reactive-react" "2.1.2"
+    "@formily/shared" "2.1.2"
     classnames "^2.2.6"
     react-sortable-hoc "^1.11.0"
     react-sticky-box "^0.9.3"
 
-"@formily/core@2.0.19":
-  version "2.0.19"
-  resolved "https://registry.yarnpkg.com/@formily/core/-/core-2.0.19.tgz#359bef69964b623d8468934e4cc396de4eb03173"
-  integrity sha512-VsqWJKc2jhjzPgu4SKN5EVJeRrEwu+mAvsSo5bdDeKDTQ3b9+L9TTpUF8Q4t9NvZshK+gMAfvdCYNnb5hUqSnw==
+"@formily/core@2.1.2":
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/@formily/core/-/core-2.1.2.tgz#e11078a56e332c2a482e14e2e56ee3268522f8bf"
+  integrity sha512-K+7azEGhlB58UgibxvF4FQETfp3JLlI3YDIpwvyajCVd1Wbcrq4aLlv8TMkzuFIz0enJZRKSd56lzxMQtE0LPQ==
   dependencies:
-    "@formily/reactive" "2.0.19"
-    "@formily/shared" "2.0.19"
-    "@formily/validator" "2.0.19"
+    "@formily/reactive" "2.1.2"
+    "@formily/shared" "2.1.2"
+    "@formily/validator" "2.1.2"
 
-"@formily/grid@2.0.19":
-  version "2.0.19"
-  resolved "https://registry.yarnpkg.com/@formily/grid/-/grid-2.0.19.tgz#b0b3aa45f027fd23c5918f1490597aeea7b2b90d"
-  integrity sha512-x2s1EVAkiGx6rdFr333gsNJjpwS9yLHIECvvStqWcTfBHlszrFxtPyAa1rYf0RCvjBMWq0EE6p2o6VIVqKVOtw==
+"@formily/grid@2.1.2":
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/@formily/grid/-/grid-2.1.2.tgz#b524f5b5ec3768dcb24a4c7ff83b05044f2221e0"
+  integrity sha512-t5aFqGW1qVkbZzvHS5rSvCswUP0zXjswYzSM9yqo2C8q9IFV+hW7Wp1ilq1nP485ejiSVtdnsufVNuOa7qk7Sg==
   dependencies:
-    "@formily/reactive" "2.0.19"
+    "@formily/reactive" "2.1.2"
     "@juggle/resize-observer" "^3.3.1"
 
-"@formily/json-schema@2.0.19":
-  version "2.0.19"
-  resolved "https://registry.yarnpkg.com/@formily/json-schema/-/json-schema-2.0.19.tgz#e14167060a07abd54759bb5ee17c6679156b866f"
-  integrity sha512-BTcEZwcGM/up6VKEVZ4wulD4hI5fYBb8n5SgRnaezSJbHECK23p8Yh13Qj4h1GFbQbnCWr6FVYFvqBSAc8tyOQ==
+"@formily/json-schema@2.1.2":
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/@formily/json-schema/-/json-schema-2.1.2.tgz#79fb995aeaea6278fef4715a973cd8675d2e7eec"
+  integrity sha512-RroEee9QD1F4xYCV4vS5bb3U6by/xmcxidvKGEjFYTcf99UxJXhS0g2fh+6LfYQTQoIaD/+xFc8pUxzMxIgteg==
   dependencies:
-    "@formily/core" "2.0.19"
-    "@formily/reactive" "2.0.19"
-    "@formily/shared" "2.0.19"
+    "@formily/core" "2.1.2"
+    "@formily/reactive" "2.1.2"
+    "@formily/shared" "2.1.2"
 
-"@formily/path@2.0.19":
-  version "2.0.19"
-  resolved "https://registry.yarnpkg.com/@formily/path/-/path-2.0.19.tgz#391abd170fd68048a4f59568b7b150c68fd36785"
-  integrity sha512-uiNyq0Vrls7ie8/odP7ZVybNBOFgwJVQ68XXIzq4ZPrki0uSyoVAn5CrCkNP94PdqOjN8/gjP4sQo6eSXvPnvQ==
+"@formily/path@2.1.2":
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/@formily/path/-/path-2.1.2.tgz#6f1ad63df22f7aa6c63e521a70f90ba536e37028"
+  integrity sha512-jFBWhx2FaKBkEXaGJr5Y9qs5V7ce+Fw7o6zIrAsyEhJHx1CdvwmbTzGpYOOPDtidMzvHVAsvLHkSpeaHfryZdw==
 
-"@formily/react@2.0.19":
-  version "2.0.19"
-  resolved "https://registry.yarnpkg.com/@formily/react/-/react-2.0.19.tgz#44d0afeb4eff2b62f555664aa620cb9ecff1bec2"
-  integrity sha512-R6FE/pX1u06nORiWX7hNgb8idMcZdd+ozvZu1iupgDqAespWz6axl24OOKWH56+JU/uXDRXG8dvGKds5rjctvQ==
-  dependencies:
-    "@formily/core" "2.0.19"
-    "@formily/json-schema" "2.0.19"
-    "@formily/reactive" "2.0.19"
-    "@formily/reactive-react" "2.0.19"
-    "@formily/shared" "2.0.19"
-    "@formily/validator" "2.0.19"
+"@formily/react@2.1.2":
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/@formily/react/-/react-2.1.2.tgz#f39fca6e4435b057eb4236b304cb51c284b3a5d5"
+  integrity sha512-FhJ1y5RZ5yhLOYcxhIFv6f8yjs1Uz8naufvLMPA1Cz+Kxxvt1q0XSIRxg/8GsY7UjboWbPo83ykhT2QmHd6vkA==
+  dependencies:
+    "@formily/core" "2.1.2"
+    "@formily/json-schema" "2.1.2"
+    "@formily/reactive" "2.1.2"
+    "@formily/reactive-react" "2.1.2"
+    "@formily/shared" "2.1.2"
+    "@formily/validator" "2.1.2"
     hoist-non-react-statics "^3.3.2"
 
-"@formily/reactive-react@2.0.19":
-  version "2.0.19"
-  resolved "https://registry.yarnpkg.com/@formily/reactive-react/-/reactive-react-2.0.19.tgz#0526cc22346d62c1809eefbdbac988a1845e581e"
-  integrity sha512-Laz3O/oSCIA4qKQ4fIMsyUQjS4XtD00nUvXSXIZhGdTkZW09Spq8zv7wd+0V6REEKIH6urTtC8htpBQN8W3fww==
+"@formily/reactive-react@2.1.2":
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/@formily/reactive-react/-/reactive-react-2.1.2.tgz#d6d6017d628f67640aecb23669f5474a88f9508e"
+  integrity sha512-vl9Wslexfs/6kWZiCl3zjipk6KALciNupoit2ueHjTXxWgE6ChZsnjfoMs7/sZmuN02D8UAAHQJTqdJA14AQ+Q==
   dependencies:
-    "@formily/reactive" "2.0.19"
+    "@formily/reactive" "2.1.2"
     hoist-non-react-statics "^3.3.2"
 
-"@formily/reactive@2.0.19":
-  version "2.0.19"
-  resolved "https://registry.yarnpkg.com/@formily/reactive/-/reactive-2.0.19.tgz#4498b4e70c466bfee9b9dda8639ffe6f10d5d7ca"
-  integrity sha512-gEpiEITdrRHGc+cf/0lalw4gTcES+8axdAxC0mZRMHfJ8iSZnFs369AGxiWdElUK9NNVLfEmSuU60op6XCQhrg==
+"@formily/reactive@2.1.2":
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/@formily/reactive/-/reactive-2.1.2.tgz#7350c63a3f0ac20a23ad88d418601faf0da7a63c"
+  integrity sha512-9L7hAznjbQqY0QsSvAfz9ksmcJbNVP1pJSNkqID9ZMI7bVsmoxKLNXIlT5sw0jnlSpKStW7mQAzjRdivbUxTwQ==
 
-"@formily/shared@2.0.19":
-  version "2.0.19"
-  resolved "https://registry.yarnpkg.com/@formily/shared/-/shared-2.0.19.tgz#602ce0738fe39fb0773accc3345329ba3b0cbbac"
-  integrity sha512-1zKNZLKoEEH31Y9+rBXdByHVsUModWyshkPj7fsZv0KkaObn/wV2WUCKLQW4c4Hn1y+yojPH//8SD2oOZ4wZXw==
+"@formily/shared@2.1.2":
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/@formily/shared/-/shared-2.1.2.tgz#ce0a5f7a971a2b97046b29f0f11c5e0525e9efe8"
+  integrity sha512-WIWwFtPyGMqtCXu2F3a98K+RDsgb0B5kuH3cBD90wc4TPasCv75DMRg/EEP0USVs6WC9J8wIWSqqmYosP/vIFA==
   dependencies:
-    "@formily/path" "2.0.19"
+    "@formily/path" "2.1.2"
     camel-case "^4.1.1"
     lower-case "^2.0.1"
     no-case "^3.0.4"
@@ -2935,12 +2935,12 @@
     pascal-case "^3.1.1"
     upper-case "^2.0.1"
 
-"@formily/validator@2.0.19":
-  version "2.0.19"
-  resolved "https://registry.yarnpkg.com/@formily/validator/-/validator-2.0.19.tgz#4d14191b6ab92b0298a59b42964cab008ef7f551"
-  integrity sha512-KS9g0WXKR77ET+3blKGxDL2w4e8gp0z5kkd5BDm7bIUmfNb67rTuSaacs+8MbOuckt09B7qU1nzOekXkskaRNw==
+"@formily/validator@2.1.2":
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/@formily/validator/-/validator-2.1.2.tgz#425905aafe5340300328a40d146534e198326575"
+  integrity sha512-3XvswpgVJUXM3VimUNez7BWqgfSeuFG7TSkyMi2LKu0JSBO1/RLq5D9Ps69BnA16RJGQijdX0pePtsVNzu9JFw==
   dependencies:
-    "@formily/shared" "2.0.19"
+    "@formily/shared" "2.1.2"
 
 "@hapi/address@^2.1.2":
   version "2.1.4"