Просмотр исходного кода

feat(merge): merge sc

feat: 阿里云
Lind 3 лет назад
Родитель
Сommit
5498000cdf

BIN
public/images/northbound/aliyun.png


BIN
public/images/northbound/aliyun1.jpg


BIN
public/images/northbound/aliyun2.png


BIN
public/images/northbound/图片44.png


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


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

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

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

@@ -0,0 +1,382 @@
+import { PermissionButton, TitleComponent } from '@/components';
+import { PageContainer } from '@ant-design/pro-layout';
+import {
+  ArrayItems,
+  Form,
+  FormButtonGroup,
+  FormGrid,
+  FormItem,
+  Input,
+  Select,
+  ArrayCollapse,
+} from '@formily/antd';
+import type { Field } from '@formily/core';
+import { onFieldValueChange } from '@formily/core';
+import { createForm } from '@formily/core';
+import { createSchemaField, observer } from '@formily/react';
+import { Card, Col, Row, Image, message } 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: '请选择网桥产品',
+        },
+        '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: '请选择阿里云产品',
+                  },
+                  '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: '请选择平台产品',
+                  },
+                  '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;

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

@@ -0,0 +1,55 @@
+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;
+};

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

+ 14 - 2
src/pages/device/Instance/Detail/MetadataLog/Property/Detail.tsx

@@ -1,5 +1,5 @@
 import { Modal, Input } from 'antd';
-// import ReactMarkdown from "react-markdown";
+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();
       }}

+ 76 - 6
src/pages/device/Instance/Detail/MetadataLog/Property/index.tsx

@@ -1,6 +1,16 @@
-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';
@@ -9,6 +19,7 @@ import { Axis, Chart, Geom, Legend, Tooltip, Slider } 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 +44,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 +75,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 +189,6 @@ const PropertyLog = (props: Props) => {
   };
 
   useEffect(() => {
-    console.log(data);
     if (visible) {
       handleSearch(
         {
@@ -179,7 +227,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 +365,8 @@ const PropertyLog = (props: Props) => {
       visible={visible}
       onCancel={() => close()}
       onOk={() => close()}
-      width="45vw"
+      destroyOnClose={true}
+      width="50vw"
     >
       <div style={{ marginBottom: '20px' }}>
         <Space>
@@ -430,6 +479,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 +502,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;

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

@@ -114,6 +114,7 @@ export enum MENUS_CODE {
   'account/Center/bind' = 'account/Center/bind',
   'Northbound/DuerOS' = 'Northbound/DuerOS',
   'Northbound/AliCloud' = 'Northbound/AliCloud',
+  'Northbound/AliCloud/Detail' = 'Northbound/AliCloud/Detail',
   'system/Platforms' = 'system/Platforms',
 }
 
@@ -154,4 +155,5 @@ export const getDetailNameByCode = {
   'link/AccessConfig/Detail': '配置详情',
   'media/Stream/Detail': '流媒体详情',
   'rule-engine/Alarm/Log/Detail': '告警日志',
+  'Northbound/AliCloud/Detail': '阿里云详情',
 };