lind преди 3 години
родител
ревизия
2c73ca0c44
променени са 52 файла, в които са добавени 3437 реда и са изтрити 74 реда
  1. BIN
      public/images/channel/1.png
  2. BIN
      public/images/channel/2.png
  3. BIN
      public/images/channel/3.png
  4. BIN
      public/images/channel/4.png
  5. BIN
      public/images/channel/background.png
  6. BIN
      public/images/northbound/aliyun.png
  7. BIN
      public/images/northbound/aliyun1.jpg
  8. BIN
      public/images/northbound/aliyun2.png
  9. BIN
      public/images/northbound/图片44.png
  10. 0 0
      public/images/network/doeros.jpg
  11. 2 2
      src/app.tsx
  12. 10 0
      src/components/AMapComponent/APILoader.ts
  13. 17 7
      src/components/AMapComponent/PathSimplifier/index.tsx
  14. 20 9
      src/components/AMapComponent/amap.tsx
  15. 64 0
      src/components/AMapComponent/hooks/PlaceSearch.tsx
  16. 1 0
      src/components/AMapComponent/hooks/index.ts
  17. 36 0
      src/components/AMapComponent/hooks/typing.d.ts
  18. 50 0
      src/components/ProTableCard/CardItems/aliyun.tsx
  19. 3 1
      src/components/RightContent/AvatarDropdown.tsx
  20. 35 0
      src/pages/Northbound/AliCloud/Detail/index.less
  21. 381 0
      src/pages/Northbound/AliCloud/Detail/index.tsx
  22. 311 2
      src/pages/Northbound/AliCloud/index.tsx
  23. 56 0
      src/pages/Northbound/AliCloud/service.ts
  24. 17 0
      src/pages/Northbound/AliCloud/typings.d.ts
  25. 76 0
      src/pages/account/Center/bind/index.tsx
  26. 110 0
      src/pages/account/Center/edit/infoEdit.tsx
  27. 234 0
      src/pages/account/Center/edit/passwordEdit.tsx
  28. 34 0
      src/pages/account/Center/index.less
  29. 294 0
      src/pages/account/Center/index.tsx
  30. 66 0
      src/pages/account/Center/service.ts
  31. 21 0
      src/pages/account/Center/typings.d.ts
  32. 78 40
      src/pages/demo/AMap/index.tsx
  33. 70 0
      src/pages/device/Instance/Detail/MetadataLog/Property/AMap.tsx
  34. 15 3
      src/pages/device/Instance/Detail/MetadataLog/Property/Detail.tsx
  35. 78 7
      src/pages/device/Instance/Detail/MetadataLog/Property/index.tsx
  36. 1 1
      src/pages/device/Instance/Detail/Running/Property/FileComponent/index.tsx
  37. 17 1
      src/pages/device/Instance/Detail/Tags/Edit.tsx
  38. 100 0
      src/pages/device/Instance/Detail/Tags/location/AMap.tsx
  39. 51 0
      src/pages/device/Instance/Detail/Tags/location/GeoComponent.tsx
  40. 30 0
      src/pages/link/Channel/Opcua/index.less
  41. 241 0
      src/pages/link/Channel/Opcua/index.tsx
  42. 4 0
      src/pages/link/Channel/Opcua/typings.d.ts
  43. 16 0
      src/pages/rule-engine/Scene/index.tsx
  44. 27 0
      src/pages/system/Platforms/Api/basePage.tsx
  45. 16 0
      src/pages/system/Platforms/Api/index.tsx
  46. 227 0
      src/pages/system/Platforms/index.tsx
  47. 160 0
      src/pages/system/Platforms/password.tsx
  48. 419 0
      src/pages/system/Platforms/save.tsx
  49. 13 0
      src/pages/system/Platforms/service.ts
  50. 7 0
      src/pages/system/Platforms/typing.d.ts
  51. 23 1
      src/utils/menu/index.ts
  52. 6 0
      src/utils/menu/router.ts

BIN
public/images/channel/1.png


BIN
public/images/channel/2.png


BIN
public/images/channel/3.png


BIN
public/images/channel/4.png


BIN
public/images/channel/background.png


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


+ 2 - 2
src/app.tsx

@@ -13,7 +13,7 @@ import type { RequestOptionsInit } from 'umi-request';
 import ReconnectingWebSocket from 'reconnecting-websocket';
 import SystemConst from '@/utils/const';
 import { service as MenuService } from '@/pages/system/Menu';
-import getRoutes, { getMenus, handleRoutes, saveMenusCache } from '@/utils/menu';
+import getRoutes, { extraRouteArr, getMenus, handleRoutes, saveMenusCache } from '@/utils/menu';
 import { AIcon } from '@/components';
 
 const isDev = process.env.NODE_ENV === 'development';
@@ -264,7 +264,7 @@ export function render(oldRender: any) {
             url: '/demo',
           });
         }
-        extraRoutes = handleRoutes(res.result);
+        extraRoutes = handleRoutes([...extraRouteArr, ...res.result]);
         saveMenusCache(extraRoutes);
       }
       oldRender();

+ 10 - 0
src/components/AMapComponent/APILoader.ts

@@ -9,6 +9,16 @@ const buildScriptTag = (src: string): HTMLScriptElement => {
   return script;
 };
 
+export const getAMapPlugins = (type: string, map: any, callback: Function) => {
+  if (map) {
+    map.plugin([type], (...arg: any) => {
+      if (callback) {
+        callback(arg);
+      }
+    });
+  }
+};
+
 export const getAMapUiPromise = (version: string = '1.1'): Promise<any> => {
   if ((window as any).AMapUI) {
     return Promise.resolve();

+ 17 - 7
src/components/AMapComponent/PathSimplifier/index.tsx

@@ -10,7 +10,7 @@ interface PathSimplifierProps {
 }
 
 const PathSimplifier = (props: PathSimplifierProps) => {
-  const { pathData, __map__, onCreated, options } = props;
+  const { __map__, onCreated, options } = props;
 
   const pathSimplifierRef = useRef<PathSimplifier | null>(null);
   const [loading, setLoading] = useState(false);
@@ -28,16 +28,17 @@ const PathSimplifier = (props: PathSimplifierProps) => {
         map: __map__,
         ...options,
       });
-      if (pathData) {
-        pathSimplifierRef.current?.setData(
-          pathData.map((item) => ({ name: item.name || '路线', path: item.path })),
-        );
-        setLoading(true);
-      }
 
       if (onCreated) {
         onCreated(pathSimplifierRef.current!);
       }
+
+      if (props.pathData) {
+        pathSimplifierRef.current?.setData(
+          props.pathData.map((item) => ({ name: item.name || '路线', path: item.path })),
+        );
+        setLoading(true);
+      }
     },
     [props],
   );
@@ -71,6 +72,15 @@ const PathSimplifier = (props: PathSimplifierProps) => {
   };
 
   useEffect(() => {
+    if (pathSimplifierRef.current && props.pathData) {
+      pathSimplifierRef.current?.setData(
+        props.pathData.map((item) => ({ name: item.name || '路线', path: item.path })),
+      );
+      setLoading(true);
+    }
+  }, [props.pathData]);
+
+  useEffect(() => {
     if (__map__) {
       loadUI();
     }

+ 20 - 9
src/components/AMapComponent/amap.tsx

@@ -12,7 +12,7 @@ interface AMapProps extends Omit<MapProps, 'amapkey' | 'useAMapUI'> {
 }
 
 export default (props: AMapProps) => {
-  const { style, className, onInstanceCreated, ...extraProps } = props;
+  const { style, className, events, onInstanceCreated, ...extraProps } = props;
 
   const [uiLoading, setUiLoading] = useState(false);
 
@@ -27,6 +27,15 @@ export default (props: AMapProps) => {
     });
   };
 
+  const onCreated = (map: any) => {
+    if (onInstanceCreated) {
+      onInstanceCreated(map);
+    }
+    if (isOpenUi) {
+      getAMapUI();
+    }
+  };
+
   return (
     <div style={style || { width: '100%', height: '100%' }} className={className}>
       {amapKey ? (
@@ -35,14 +44,16 @@ export default (props: AMapProps) => {
           version={'2.0'}
           amapkey={amapKey}
           zooms={[3, 20]}
-          onInstanceCreated={(map: any) => {
-            if (onInstanceCreated) {
-              onInstanceCreated(map);
-            }
-            if (isOpenUi) {
-              getAMapUI();
-            }
-          }}
+          events={
+            events
+              ? {
+                  ...events!,
+                  created: onCreated,
+                }
+              : {
+                  created: onCreated,
+                }
+          }
           {...extraProps}
         >
           {isOpenUi ? (uiLoading ? props.children : null) : props.children}

+ 64 - 0
src/components/AMapComponent/hooks/PlaceSearch.tsx

@@ -0,0 +1,64 @@
+import { useEffect, useRef, useState } from 'react';
+import { getAMapPlugins } from '@/components/AMapComponent/APILoader';
+
+type DataType = {
+  label: string;
+  value: {
+    lat: number;
+    lng: number;
+  };
+  address: string;
+};
+const usePlaceSearch = (map: any) => {
+  const MSearch = useRef<PlaceSearch | null>(null);
+  const [data, setData] = useState<DataType[]>([]);
+
+  const initSearch = () => {
+    getAMapPlugins('AMap.PlaceSearch', map, () => {
+      MSearch.current = new (AMap as any).PlaceSearch({
+        pageSize: 10,
+        pageIndex: 1,
+      });
+    });
+  };
+
+  const onSearch = (value: string) => {
+    if (value && MSearch.current) {
+      MSearch.current.search(value, (status: string, result: any) => {
+        if (status === 'complete' && result.poiList && result.poiList.count) {
+          setData(
+            result.poiList.pois.map((item: any) => {
+              const lnglat: any = item.location || {};
+              return {
+                label: item.name,
+                address: item.address,
+                value: [lnglat.lng, lnglat.lat].toString(),
+                lnglat: {
+                  lng: lnglat.lng,
+                  lat: lnglat.lat,
+                },
+              };
+            }),
+          );
+        } else {
+          setData([]);
+        }
+      });
+    } else {
+      setData([]);
+    }
+  };
+
+  useEffect(() => {
+    if (map) {
+      initSearch();
+    }
+  }, [map]);
+
+  return {
+    data,
+    search: onSearch,
+  };
+};
+
+export default usePlaceSearch;

+ 1 - 0
src/components/AMapComponent/hooks/index.ts

@@ -0,0 +1 @@
+export { default as usePlaceSearch } from './PlaceSearch';

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

@@ -0,0 +1,36 @@
+interface PlaceSearchOptions {
+  pageSize: number;
+  pageIndex: number;
+  city?: string;
+  type?: string;
+  extensions?: string;
+}
+
+type resultCityType = {
+  name: string;
+  citycode: string;
+  adcode: string;
+  count: string;
+};
+
+type searchFnResult = {
+  info?: string;
+  keywordList?: string[];
+  cityList?: resultCityType[];
+  poiList?: {
+    pageIndex: number;
+    pageSize: number;
+    count: number;
+    pois: any[];
+  };
+};
+
+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>
+  );
+};

+ 3 - 1
src/components/RightContent/AvatarDropdown.tsx

@@ -8,6 +8,7 @@ import styles from './index.less';
 import type { MenuInfo } from 'rc-menu/lib/interface';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import Service from '@/pages/user/Login/service';
+import { getMenuPathByCode } from '@/utils/menu';
 
 export type GlobalHeaderRightProps = {
   menu?: boolean;
@@ -47,7 +48,8 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
         await loginOut();
         return;
       }
-      history.push(`/account/${key}`);
+      // console.log(key)
+      history.push(getMenuPathByCode('account/Center'));
     },
     [initialState, setInitialState],
   );

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

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

@@ -0,0 +1,381 @@
+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: '请选择网桥产品',
+        },
+        '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;

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

+ 76 - 0
src/pages/account/Center/bind/index.tsx

@@ -0,0 +1,76 @@
+import { Button, Card, Col, message, Row } from 'antd';
+import { useEffect, useState } from 'react';
+import Service from '@/pages/account/Center/service';
+
+export const service = new Service();
+
+const Bind = () => {
+  const [bindUser, setBindUser] = useState<any>();
+  const [user, setUser] = useState<any>();
+  const [code, setCode] = useState<string>('');
+
+  const bindUserInfo = (params: string) => {
+    service.bindUserInfo(params).then((res) => {
+      if (res.status === 200) {
+        setBindUser(res.result);
+      }
+    });
+  };
+  const getDetail = () => {
+    service.getUserDetail().subscribe((res) => {
+      setUser(res.result);
+    });
+  };
+
+  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];
+    setCode(params);
+    bindUserInfo(params);
+    getDetail();
+  }, []);
+  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);
+                      setTimeout(() => window.close(), 300);
+                    }
+                  } else {
+                    message.error('绑定失败');
+                  }
+                });
+              }}
+            >
+              立即绑定
+            </Button>
+          </Col>
+        </Row>
+      </Card>
+    </>
+  );
+};
+export default Bind;

+ 110 - 0
src/pages/account/Center/edit/infoEdit.tsx

@@ -0,0 +1,110 @@
+import { Col, Form, Input, Modal, Row } from 'antd';
+
+interface Props {
+  data: any;
+  save: Function;
+  close: Function;
+}
+
+const InfoEdit = (props: Props) => {
+  const [form] = Form.useForm();
+  const { data } = props;
+  const handleSave = async () => {
+    const formData = await form.validateFields();
+    console.log(formData);
+    props.save({
+      name: formData.name,
+      email: formData.email || '',
+      telephone: formData.telephone || '',
+    });
+  };
+
+  return (
+    <Modal
+      title="编辑"
+      visible
+      width="40vw"
+      destroyOnClose
+      onOk={handleSave}
+      onCancel={() => {
+        props.close();
+      }}
+    >
+      <Form
+        form={form}
+        layout="vertical"
+        initialValues={{
+          name: data.name,
+          username: data.username,
+          role: data?.roleList[0]?.name,
+          org: data?.orgList[0]?.name,
+          telephone: data.telephone,
+          email: data.email,
+        }}
+      >
+        <Row gutter={[24, 24]}>
+          <Col span={12}>
+            <Form.Item
+              label="姓名"
+              required
+              name="name"
+              rules={[
+                { type: 'string', max: 64 },
+                { required: true, message: '姓名必填' },
+              ]}
+            >
+              <Input placeholder="请输入姓名" />
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item label="用户名" name="username">
+              <Input placeholder="请输入用户名" disabled />
+            </Form.Item>
+          </Col>
+        </Row>
+        <Row gutter={[24, 24]}>
+          <Col span={12}>
+            <Form.Item label="角色" name="role">
+              <Input placeholder="请输入姓名" disabled />
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item label="部门" name="org">
+              <Input placeholder="请输入用户名" disabled />
+            </Form.Item>
+          </Col>
+        </Row>
+        <Row gutter={[24, 24]}>
+          <Col span={12}>
+            <Form.Item
+              label="手机号"
+              name="telephone"
+              rules={[
+                {
+                  pattern: /^1[3456789]\d{9}$/,
+                  message: '请输入正确手机号',
+                },
+              ]}
+            >
+              <Input placeholder="请输入手机号" />
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item
+              label="邮箱"
+              name="email"
+              rules={[
+                {
+                  type: 'email',
+                },
+              ]}
+            >
+              <Input placeholder="请输入邮箱" />
+            </Form.Item>
+          </Col>
+        </Row>
+      </Form>
+    </Modal>
+  );
+};
+export default InfoEdit;

+ 234 - 0
src/pages/account/Center/edit/passwordEdit.tsx

@@ -0,0 +1,234 @@
+import { Modal } from 'antd';
+import { createSchemaField } from '@formily/react';
+import { Form, FormItem, Input, Password } from '@formily/antd';
+import { ISchema } from '@formily/json-schema';
+import { useIntl } from 'umi';
+import { useMemo } from 'react';
+import { createForm } from '@formily/core';
+import { service } from '@/pages/account/Center';
+
+interface Props {
+  visible: boolean;
+  close: Function;
+  save: Function;
+}
+
+const PasswordEdit = (props: Props) => {
+  const intl = useIntl();
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Password,
+      Input,
+    },
+  });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      oldPassword: {
+        type: 'string',
+        title: '旧密码',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-component-props': {
+          checkStrength: true,
+          placeholder: '请输入旧密码',
+        },
+        required: true,
+        'x-validator': [
+          // {
+          //   max: 128,
+          //   message: '密码最多可输入128位',
+          // },
+          // {
+          //   min: 8,
+          //   message: '密码不能少于8位',
+          // },
+          {
+            required: true,
+            message: '请输入密码',
+          },
+          {
+            triggerType: 'onBlur',
+            validator: (value: string) => {
+              return new Promise((resolve) => {
+                service
+                  .validatePassword(value)
+                  .then((resp) => {
+                    if (resp.status === 200) {
+                      if (resp.result.passed) {
+                        resolve('');
+                      } else {
+                        resolve(resp.result.reason);
+                      }
+                    }
+                    resolve('');
+                  })
+                  .catch(() => {
+                    return '验证失败!';
+                  });
+              });
+            },
+          },
+        ],
+      },
+      newPassword: {
+        type: 'string',
+        title: intl.formatMessage({
+          id: 'pages.system.password',
+          defaultMessage: '密码',
+        }),
+        'x-decorator': 'FormItem',
+        'x-component': 'Password',
+        'x-component-props': {
+          checkStrength: true,
+          placeholder: '请输入密码',
+        },
+        required: true,
+
+        'x-reactions': [
+          {
+            dependencies: ['.confirmPassword'],
+            fulfill: {
+              state: {
+                selfErrors:
+                  '{{$deps[0] && $self.value && $self.value !==$deps[0] ? "两次密码输入不一致" : ""}}',
+              },
+            },
+          },
+        ],
+        name: 'password',
+        'x-validator': [
+          // {
+          //   max: 128,
+          //   message: '密码最多可输入128位',
+          // },
+          // {
+          //   min: 8,
+          //   message: '密码不能少于8位',
+          // },
+          {
+            required: true,
+            message: '请输入密码',
+          },
+          {
+            triggerType: 'onBlur',
+            validator: (value: string) => {
+              return new Promise((resolve) => {
+                service
+                  .validateField('password', value)
+                  .then((resp) => {
+                    if (resp.status === 200) {
+                      if (resp.result.passed) {
+                        resolve('');
+                      } else {
+                        resolve(resp.result.reason);
+                      }
+                    }
+                    resolve('');
+                  })
+                  .catch(() => {
+                    return '验证失败!';
+                  });
+              });
+            },
+          },
+        ],
+      },
+      confirmPassword: {
+        type: 'string',
+        title: intl.formatMessage({
+          id: 'pages.system.confirmPassword',
+          defaultMessage: '确认密码?',
+        }),
+        'x-decorator': 'FormItem',
+        'x-component': 'Password',
+        'x-component-props': {
+          checkStrength: true,
+          placeholder: '请再次输入密码',
+        },
+        'x-validator': [
+          // {
+          //   max: 128,
+          //   message: '密码最多可输入128位',
+          // },
+          // {
+          //   min: 8,
+          //   message: '密码不能少于8位',
+          // },
+          {
+            required: true,
+            message: '请输入确认密码',
+          },
+          {
+            triggerType: 'onBlur',
+            validator: (value: string) => {
+              return new Promise((resolve) => {
+                service
+                  .validateField('password', value)
+                  .then((resp) => {
+                    if (resp.status === 200) {
+                      if (resp.result.passed) {
+                        resolve('');
+                      } else {
+                        resolve(resp.result.reason);
+                      }
+                    }
+                    resolve('');
+                  })
+                  .catch(() => {
+                    return '验证失败!';
+                  });
+              });
+            },
+          },
+        ],
+        'x-reactions': [
+          {
+            dependencies: ['.password'],
+            fulfill: {
+              state: {
+                selfErrors:
+                  '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "两次密码输入不一致" : ""}}',
+              },
+            },
+          },
+        ],
+        'x-decorator-props': {},
+        name: 'confirmPassword',
+      },
+    },
+  };
+
+  const form = useMemo(() => createForm({}), [props.visible]);
+  return (
+    <Modal
+      title="重置密码"
+      visible
+      onCancel={() => props.close()}
+      onOk={async () => {
+        const value: { oldPassword: string; newPassword: string; confirmPassword: string } =
+          await form.submit();
+        console.log(value);
+        props.save({
+          oldPassword: value.oldPassword,
+          newPassword: value.newPassword,
+        });
+        // if (props.data.id) {
+        //   const resp = await service.resetPassword(props.data.id, value.confirmPassword);
+        //   if (resp.status === 200) {
+        //     message.success('操作成功');
+        //     props.close();
+        //   }
+        // }
+        // props.close();
+      }}
+    >
+      <Form form={form} layout="vertical">
+        <SchemaField schema={schema} />
+      </Form>
+    </Modal>
+  );
+};
+export default PasswordEdit;

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

@@ -0,0 +1,34 @@
+.info {
+  margin-top: 15px;
+}
+
+.top {
+  display: flex;
+  width: 100%;
+
+  .avatar {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: space-between;
+    width: 350px;
+    height: 200px;
+  }
+
+  .content {
+    width: 80%;
+    padding-top: 15px;
+  }
+
+  .action {
+    position: relative;
+    top: 15px;
+    right: 20px;
+  }
+}
+
+.bind {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}

+ 294 - 0
src/pages/account/Center/index.tsx

@@ -0,0 +1,294 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { Avatar, Button, Card, Col, Descriptions, Divider, message, Row, Upload } from 'antd';
+import { useEffect, useState } from 'react';
+import styles from './index.less';
+import { UploadProps } from 'antd/lib/upload';
+import Token from '@/utils/token';
+import SystemConst from '@/utils/const';
+import { EditOutlined, LockOutlined, UploadOutlined, UserOutlined } from '@ant-design/icons';
+import InfoEdit from './edit/infoEdit';
+import PasswordEdit from './edit/passwordEdit';
+import Service from '@/pages/account/Center/service';
+import moment from 'moment';
+
+export const service = new Service();
+
+const Center = () => {
+  const [data, setData] = useState<any>();
+  const [imageUrl, setImageUrl] = useState<string>('');
+  // const [loading, setLoading] = useState<boolean>(false)
+  const [infos, setInfos] = useState<boolean>(false);
+  const [password, setPassword] = useState<boolean>(false);
+  const [bindList, setBindList] = useState<any>([]);
+
+  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 uploadProps: UploadProps = {
+    showUploadList: false,
+    action: `/${SystemConst.API_BASE}/file/static`,
+    headers: {
+      'X-Access-Token': Token.get(),
+    },
+    beforeUpload(file) {
+      const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
+      if (!isJpgOrPng) {
+        message.error('请上传.png.jpg格式的文件');
+      }
+      return isJpgOrPng;
+    },
+    onChange(info) {
+      if (info.file.status === 'uploading') {
+        // setLoading(true);
+      }
+      if (info.file.status === 'done') {
+        setImageUrl(info.file.response.result);
+        service
+          .saveUserDetail({
+            name: data.name,
+            avatar: info.file.response.result,
+          })
+          .subscribe((res) => {
+            if (res.status === 200) {
+              setImageUrl(info.file.response.result);
+              message.success('上传成功');
+            }
+          });
+        // setLoading(false);
+      }
+    },
+  };
+
+  const getDetail = () => {
+    service.getUserDetail().subscribe((res) => {
+      setData(res.result);
+      setImageUrl(res.result.avatar);
+    });
+  };
+  const saveInfo = (parms: UserDetail) => {
+    service.saveUserDetail(parms).subscribe((res) => {
+      if (res.status === 200) {
+        message.success('保存成功');
+        getDetail();
+        setInfos(false);
+      } else {
+        message.success('保存失败');
+      }
+    });
+  };
+  const savePassword = (parms: { oldPassword: string; newPassword: string }) => {
+    service.savePassWord(parms).subscribe((res) => {
+      if (res.status === 200) {
+        message.success('保存成功');
+      }
+    });
+  };
+  const getBindInfo = () => {
+    service.bindInfo().then((res) => {
+      if (res.status === 200) {
+        console.log(res);
+        setBindList(res.result);
+      }
+    });
+  };
+  const unBind = (type: string, provider: string) => {
+    service.unbind(type, provider).then((res) => {
+      if (res.status === 200) {
+        message.success('解绑成功');
+        getBindInfo();
+      }
+    });
+  };
+
+  useEffect(() => {
+    getDetail();
+    getBindInfo();
+  }, []);
+
+  return (
+    <PageContainer>
+      <Card>
+        <div className={styles.top}>
+          <div className={styles.avatar}>
+            <div>
+              {data?.avatar ? (
+                <Avatar size={140} src={imageUrl} />
+              ) : (
+                <Avatar size={140} icon={<UserOutlined />} />
+              )}
+            </div>
+            <Upload {...uploadProps}>
+              <Button>
+                <UploadOutlined />
+                更换头像
+              </Button>
+            </Upload>
+          </div>
+          <div className={styles.content}>
+            <Descriptions column={4} layout="vertical">
+              <Descriptions.Item label="登录账号">{data?.username}</Descriptions.Item>
+              <Descriptions.Item label="账号ID">{data?.id}</Descriptions.Item>
+              <Descriptions.Item label="注册时间">
+                {moment(data?.createTime).format('YYYY-MM-DD HH:mm:ss')}
+              </Descriptions.Item>
+              <Descriptions.Item label="电话">{data?.telephone || '-'}</Descriptions.Item>
+              <Descriptions.Item label="姓名">{data?.name}</Descriptions.Item>
+              <Descriptions.Item label="角色">{data?.roleList[0]?.name || '-'}</Descriptions.Item>
+              <Descriptions.Item label="部门">{data?.orgList[0]?.name || '-'}</Descriptions.Item>
+              <Descriptions.Item label="邮箱">{data?.email || '-'}</Descriptions.Item>
+            </Descriptions>
+          </div>
+          <a>
+            {' '}
+            <EditOutlined
+              className={styles.action}
+              onClick={() => {
+                setInfos(true);
+              }}
+            />
+          </a>
+        </div>
+      </Card>
+      <Card
+        className={styles.info}
+        title={
+          <div style={{ fontSize: '22px' }}>
+            <Divider type="vertical" style={{ backgroundColor: '#2F54EB', width: 3 }} />
+            修改密码
+          </div>
+        }
+        extra={
+          <a>
+            {' '}
+            <EditOutlined
+              onClick={() => {
+                setPassword(true);
+              }}
+            />
+          </a>
+        }
+      >
+        <div style={{ display: 'flex', alignItems: 'flex-end' }}>
+          <div>
+            <LockOutlined
+              style={{
+                color: '#1d39c4',
+                fontSize: '70px',
+              }}
+            />
+          </div>
+          <div style={{ marginLeft: 5, color: 'rgba(0, 0, 0, 0.55)' }}>
+            安全性高的密码可以使帐号更安全。建议您定期更换密码,设置一个包含字母,符号或数字中至少两项且长度超过8位的密码
+          </div>
+        </div>
+      </Card>
+      <Card
+        className={styles.info}
+        title={
+          <div style={{ fontSize: '22px' }}>
+            <Divider type="vertical" style={{ backgroundColor: '#2F54EB', width: 3 }} />
+            绑定三方账号
+          </div>
+        }
+      >
+        <Row gutter={[24, 24]}>
+          {bindList.map((item: any) => (
+            <Col key={item.type}>
+              <Card
+                style={{
+                  background: `url(${bGroundMap.get(item.type)}) no-repeat`,
+                  backgroundSize: '100% 100%',
+                  width: 415,
+                }}
+              >
+                <div className={styles.bind}>
+                  <div>
+                    <img style={{ height: 56 }} src={iconMap.get(item.type)} />
+                  </div>
+                  <div>
+                    {item.bound ? (
+                      <div>
+                        <div style={{ fontSize: '22px' }}>绑定名:{item.others.name}</div>
+                        <div
+                          style={{
+                            fontSize: '14px',
+                            lineHeight: '20px',
+                            marginTop: '5px',
+                            color: '#00000073',
+                          }}
+                        >
+                          绑定时间: 2022-01-02 09:00:00
+                        </div>
+                      </div>
+                    ) : (
+                      <div style={{ fontSize: '22px' }}>{`${
+                        item.type === 'dingtalk' ? '钉钉' : '微信'
+                      }未绑定`}</div>
+                    )}
+                  </div>
+                  <div>
+                    {item.bound ? (
+                      <Button
+                        onClick={() => {
+                          unBind(item.type, item.provider);
+                        }}
+                      >
+                        解除绑定
+                      </Button>
+                    ) : (
+                      <Button
+                        type="primary"
+                        onClick={() => {
+                          const items: any = window.open(
+                            `/${SystemConst.API_BASE}/sso/${item.provider}/login`,
+                          );
+                          //  const items:any= window.open(`/#/account/Center/bind`);
+                          items!.onBindSuccess = (value: any) => {
+                            if (value.status === 200) {
+                              getBindInfo();
+                            }
+                          };
+                          // history.push(getMenuPathByCode('account/Center/bind'));
+                        }}
+                      >
+                        立即绑定
+                      </Button>
+                    )}
+                  </div>
+                </div>
+              </Card>
+            </Col>
+          ))}
+        </Row>
+      </Card>
+      {infos && (
+        <InfoEdit
+          data={data}
+          save={(item: any) => {
+            saveInfo(item);
+          }}
+          close={() => {
+            setInfos(false);
+          }}
+        />
+      )}
+      {password && (
+        <PasswordEdit
+          save={(item: any) => {
+            savePassword(item);
+          }}
+          visible={password}
+          close={() => {
+            setPassword(false);
+          }}
+        />
+      )}
+    </PageContainer>
+  );
+};
+export default Center;

+ 66 - 0
src/pages/account/Center/service.ts

@@ -0,0 +1,66 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import { defer, from } from 'rxjs';
+import { map } from 'rxjs/operators';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<UserItem> {
+  getUserDetail = (params?: any) =>
+    defer(() =>
+      from(
+        request(`/${SystemConst.API_BASE}/user/detail`, {
+          method: 'GET',
+          params,
+        }),
+      ),
+    ).pipe(map((item) => item));
+
+  saveUserDetail = (data: any) =>
+    defer(() =>
+      from(
+        request(`/${SystemConst.API_BASE}/user/detail`, {
+          method: 'PUT',
+          data,
+        }),
+      ),
+    ).pipe(map((item) => item));
+  savePassWord = (data: any) =>
+    defer(() =>
+      from(
+        request(`/${SystemConst.API_BASE}/user/passwd`, {
+          method: 'PUT',
+          data,
+        }),
+      ),
+    ).pipe(map((item) => item));
+
+  validateField = (type: 'username' | 'password', name: string) =>
+    request(`/${SystemConst.API_BASE}/user/${type}/_validate`, {
+      method: 'POST',
+      data: name,
+    });
+  validatePassword = (password: string) =>
+    request(`/${SystemConst.API_BASE}/user/me/password/_validate`, {
+      method: 'POST',
+      data: password,
+    });
+  bindInfo = (params?: any) =>
+    request(`/${SystemConst.API_BASE}/sso/me/bindings`, {
+      method: 'GET',
+      params,
+    });
+  bindUserInfo = (code: string) =>
+    request(`/${SystemConst.API_BASE}/sso/bind-code/${code}`, {
+      method: 'GET',
+    });
+  bind = (code: string) =>
+    request(`/${SystemConst.API_BASE}/sso/me/bind/${code}`, {
+      method: 'POST',
+    });
+  unbind = (type: string, provider: string) =>
+    request(`/${SystemConst.API_BASE}/sso/me/${type}/${provider}/unbind`, {
+      method: 'POST',
+    });
+}
+
+export default Service;

+ 21 - 0
src/pages/account/Center/typings.d.ts

@@ -0,0 +1,21 @@
+type UserItem = {
+  id: string;
+  name: string;
+  status: number;
+  username: string;
+  createTime: number;
+  email?: string;
+  telephone?: string;
+  avatar?: string;
+  description?: string;
+  orgList?: { id: string; name: string }[] | string[];
+  roleList?: { id: string; name: string }[] | string[];
+  orgIdList?: string[];
+  roleIdList?: string[];
+};
+type UserDetail = {
+  name: string;
+  emmail?: string;
+  telephone?: string;
+  avatar?: string;
+};

+ 78 - 40
src/pages/demo/AMap/index.tsx

@@ -1,52 +1,90 @@
 import { AMap, PathSimplifier } from '@/components';
+import { usePlaceSearch } from '@/components/AMapComponent/hooks';
+import { Marker } from 'react-amap';
 import { useState } from 'react';
+import { Select } from 'antd';
+import { debounce } from 'lodash';
 
 export default () => {
   const [speed] = useState(100000);
+  const [markerCenter, setMarkerCenter] = useState<any>({ longitude: 0, latitude: 0 });
+  const [map, setMap] = useState(null);
+
+  const { data, search } = usePlaceSearch(map);
+
+  const onSearch = (value: string) => {
+    search(value);
+  };
+  console.log(data);
+
+  const [pathData] = useState([
+    {
+      name: '线路1',
+      path: [
+        [116.405289, 39.904987],
+        [113.964458, 40.54664],
+        [111.47836, 41.135964],
+        [108.949297, 41.670904],
+        [106.380111, 42.149509],
+        [103.774185, 42.56996],
+        [101.135432, 42.930601],
+        [98.46826, 43.229964],
+        [95.777529, 43.466798],
+        [93.068486, 43.64009],
+        [90.34669, 43.749086],
+        [87.61792, 43.793308],
+      ],
+    },
+  ]);
+
   return (
-    <AMap
-      AMapUI
-      style={{
-        height: 500,
-        width: '100%',
-      }}
-    >
-      <PathSimplifier
-        pathData={[
-          {
-            name: '线路1',
-            path: [
-              [116.405289, 39.904987],
-              [113.964458, 40.54664],
-              [111.47836, 41.135964],
-              [108.949297, 41.670904],
-              [106.380111, 42.149509],
-              [103.774185, 42.56996],
-              [101.135432, 42.930601],
-              [98.46826, 43.229964],
-              [95.777529, 43.466798],
-              [93.068486, 43.64009],
-              [90.34669, 43.749086],
-              [87.61792, 43.793308],
-            ],
+    <div style={{ position: 'relative' }}>
+      <AMap
+        AMapUI
+        style={{
+          height: 500,
+          width: '100%',
+        }}
+        onInstanceCreated={setMap}
+        events={{
+          click: (e: any) => {
+            setMarkerCenter({
+              longitude: e.lnglat.lng,
+              latitude: e.lnglat.lat,
+            });
           },
-        ]}
+        }}
       >
-        <PathSimplifier.PathNavigator
-          speed={speed}
-          onStart={() => {
-            console.log('start');
-          }}
-          onCreate={(nav) => {
-            setTimeout(() => {
-              nav.pause();
-            }, 5000);
-            setTimeout(() => {
-              nav.resume(); // 恢复
-            }, 7000);
+        {markerCenter.longitude ? (
+          // @ts-ignore
+          <Marker position={markerCenter} />
+        ) : null}
+        <PathSimplifier pathData={pathData}>
+          <PathSimplifier.PathNavigator
+            speed={speed}
+            onCreate={(nav) => {
+              setTimeout(() => {
+                nav.pause();
+              }, 5000);
+              setTimeout(() => {
+                nav.resume(); // 恢复
+              }, 7000);
+            }}
+          />
+        </PathSimplifier>
+      </AMap>
+      <div style={{ position: 'absolute', top: 0 }}>
+        <Select
+          showSearch
+          options={data}
+          filterOption={false}
+          onSearch={debounce(onSearch, 300)}
+          style={{ width: 300 }}
+          onSelect={(key: string, node: any) => {
+            console.log(key, node);
           }}
         />
-      </PathSimplifier>
-    </AMap>
+      </div>
+    </div>
   );
 };

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

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

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

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

@@ -0,0 +1,241 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { Badge, Card, Col, Row } from 'antd';
+import styles from './index.less';
+import { PermissionButton } from '@/components';
+import {
+  DeleteOutlined,
+  EditOutlined,
+  LinkOutlined,
+  PlayCircleOutlined,
+  PlusOutlined,
+  StopOutlined,
+} from '@ant-design/icons';
+import { useIntl } from 'umi';
+import { useRef } from 'react';
+import SearchComponent from '@/components/SearchComponent';
+
+const Opcua = () => {
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+  // const [param, setParam] = useState({});
+  const { permission } = PermissionButton.usePermission('link/Channel/Opcua');
+
+  const iconMap = new Map();
+  iconMap.set('1', require('/public/images/channel/1.png'));
+  iconMap.set('2', require('/public/images/channel/2.png'));
+  iconMap.set('3', require('/public/images/channel/3.png'));
+  iconMap.set('4', require('/public/images/channel/4.png'));
+  const background = require('/public/images/channel/background.png');
+
+  const columns: ProColumns<any>[] = [
+    {
+      title: '通道名称',
+      dataIndex: 'name',
+    },
+    {
+      title: 'IP',
+      dataIndex: 'ip',
+    },
+    {
+      title: '端口',
+      dataIndex: 'local',
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      renderText: (state) => (
+        <Badge text={state?.text} status={state?.value === 'disabled' ? 'error' : 'success'} />
+      ),
+    },
+    {
+      title: '操作',
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [
+        <PermissionButton
+          isPermission={permission.update}
+          key="edit"
+          onClick={() => {
+            // setVisible(true);
+            // setCurrent(record);
+          }}
+          type={'link'}
+          style={{ padding: 0 }}
+          tooltip={{
+            title: intl.formatMessage({
+              id: 'pages.data.option.edit',
+              defaultMessage: '编辑',
+            }),
+          }}
+        >
+          <EditOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          type="link"
+          key={'action'}
+          style={{ padding: 0 }}
+          popConfirm={{
+            title: intl.formatMessage({
+              id: `pages.data.option.${
+                record.state.value !== 'notActive' ? '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();
+            },
+          }}
+          isPermission={permission.action}
+          tooltip={{
+            title: intl.formatMessage({
+              id: `pages.data.option.${
+                record.state.value !== 'notActive' ? 'disabled' : 'enabled'
+              }`,
+              defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
+            }),
+          }}
+        >
+          {record.state.value !== 'notActive' ? <StopOutlined /> : <PlayCircleOutlined />}
+        </PermissionButton>,
+        <PermissionButton
+          isPermission={permission.view}
+          style={{ padding: 0 }}
+          key="button"
+          type="link"
+          tooltip={{
+            title: '设备接入',
+          }}
+          onClick={() => {}}
+        >
+          <LinkOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          isPermission={permission.delete}
+          style={{ padding: 0 }}
+          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();
+              // }
+            },
+          }}
+          key="button"
+          type="link"
+        >
+          <DeleteOutlined />
+        </PermissionButton>,
+      ],
+    },
+  ];
+
+  const topCard = [
+    {
+      numeber: '1',
+      title: 'OPC UA通道',
+      text: '配置OPC UA通道',
+    },
+    {
+      numeber: '2',
+      title: '设备接入网关',
+      text: '创建OPC UA设备接入网关',
+    },
+    {
+      numeber: '3',
+      title: '创建产品',
+      text: '创建产品,并选择接入方式为OPC UA',
+    },
+    {
+      numeber: '4',
+      title: '添加设备',
+      text: '添加设备,单独为每一个设备进行数据点绑定',
+    },
+  ];
+  return (
+    <PageContainer>
+      <Card style={{ marginBottom: 10 }}>
+        <Row gutter={[24, 24]}>
+          {topCard.map((item) => (
+            <Col span={6}>
+              <Card>
+                <div className={styles.topCard}>
+                  <div
+                    style={{
+                      background: `url(${background}) no-repeat`,
+                      backgroundSize: '100% 100%',
+                      width: '56px',
+                      height: '56px',
+                    }}
+                  >
+                    <img src={iconMap.get(item.numeber)} className={styles.img} />
+                  </div>
+                  <div className={styles.text}>
+                    <p className={styles.p1}>{item.title}</p>
+                    <p className={styles.p2}>{item.text}</p>
+                  </div>
+                </div>
+              </Card>
+            </Col>
+          ))}
+        </Row>
+      </Card>
+
+      <SearchComponent<any>
+        field={columns}
+        target="opcua"
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          console.log(data);
+          // setParam(data);
+        }}
+      />
+      <ProTable<UserItem>
+        actionRef={actionRef}
+        // params={param}
+        columns={columns}
+        search={false}
+        headerTitle={
+          <PermissionButton
+            onClick={() => {
+              // setMode('add');
+            }}
+            isPermission={permission.add}
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.add',
+              defaultMessage: '新增',
+            })}
+          </PermissionButton>
+        }
+        // request={async (params) =>
+        //   service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        // }
+      />
+    </PageContainer>
+  );
+};
+export default Opcua;

+ 4 - 0
src/pages/link/Channel/Opcua/typings.d.ts

@@ -0,0 +1,4 @@
+type Item = {
+  id: string;
+  name: string;
+};

+ 16 - 0
src/pages/rule-engine/Scene/index.tsx

@@ -190,6 +190,21 @@ const Scene = () => {
         },
       },
       renderText: (record) => TriggerWayType[record],
+      valueType: 'select',
+      valueEnum: {
+        manual: {
+          text: '手动触发',
+          status: 'manual',
+        },
+        timer: {
+          text: '定时触发',
+          status: 'timer',
+        },
+        device: {
+          text: '设备触发',
+          status: 'device',
+        },
+      },
     },
     {
       dataIndex: 'description',
@@ -197,6 +212,7 @@ const Scene = () => {
         id: 'pages.system.description',
         defaultMessage: '说明',
       }),
+      hideInSearch: true,
     },
     {
       dataIndex: 'state',

+ 27 - 0
src/pages/system/Platforms/Api/basePage.tsx

@@ -0,0 +1,27 @@
+import { Button, Table } from 'antd';
+
+export default () => {
+  // const [selectKeys, setSelectKeys] = useState<string[]>([])
+
+  const save = () => {};
+
+  return (
+    <div>
+      <Table
+        columns={[
+          {
+            title: 'API',
+            dataIndex: 'name',
+          },
+          {
+            title: '说明',
+            dataIndex: '',
+          },
+        ]}
+      />
+      <Button type={'primary'} onClick={save}>
+        保存
+      </Button>
+    </div>
+  );
+};

+ 16 - 0
src/pages/system/Platforms/Api/index.tsx

@@ -0,0 +1,16 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { Tree } from 'antd';
+import Table from './basePage';
+
+export default () => {
+  return (
+    <PageContainer>
+      <div>
+        <div>
+          <Tree />
+        </div>
+        <Table />
+      </div>
+    </PageContainer>
+  );
+};

+ 227 - 0
src/pages/system/Platforms/index.tsx

@@ -0,0 +1,227 @@
+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 { BadgeStatus, PermissionButton } from '@/components';
+import SearchComponent from '@/components/SearchComponent';
+import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import SaveModal from './save';
+import PasswordModal from './password';
+import Service from './service';
+import { message } from 'antd';
+
+export const service = new Service('platforms');
+
+export default () => {
+  const actionRef = useRef<ActionType>();
+  const intl = useIntl();
+  const [param, setParam] = useState({});
+  const [saveVisible, setSaveVisible] = useState(false);
+  const [passwordVisible, setPasswordVisible] = useState(false);
+
+  const { permission } = PermissionButton.usePermission('system/Platforms');
+
+  const deleteById = async (id: string) => {
+    const resp: any = await service.remove(id);
+    if (resp.status === 200) {
+      message.success('操作成功');
+      actionRef.current?.reload();
+    }
+  };
+
+  const columns: ProColumns<any>[] = [
+    {
+      dataIndex: 'name',
+      title: '名称',
+    },
+    {
+      dataIndex: 'accessName',
+      title: '用户名',
+    },
+    {
+      dataIndex: 'role',
+      title: '角色',
+    },
+    {
+      dataIndex: 'state',
+      title: intl.formatMessage({
+        id: 'pages.searchTable.titleStatus',
+        defaultMessage: '状态',
+      }),
+      width: 160,
+      valueType: 'select',
+      renderText: (record) =>
+        record ? (
+          <BadgeStatus
+            status={record.value}
+            text={record.text}
+            statusNames={{
+              started: StatusColorEnum.processing,
+              disable: StatusColorEnum.error,
+              notActive: StatusColorEnum.warning,
+            }}
+          />
+        ) : (
+          ''
+        ),
+      valueEnum: {
+        disable: {
+          text: '禁用',
+          status: 'offline',
+        },
+        started: {
+          text: '正常',
+          status: 'started',
+        },
+      },
+    },
+    {
+      dataIndex: 'description',
+      title: intl.formatMessage({
+        id: 'pages.system.description',
+        defaultMessage: '说明',
+      }),
+      hideInSearch: true,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (_, record) => [
+        <PermissionButton
+          key={'update'}
+          type={'link'}
+          style={{ padding: 0 }}
+          isPermission={permission.update}
+          tooltip={{
+            title: intl.formatMessage({
+              id: 'pages.data.option.edit',
+              defaultMessage: '编辑',
+            }),
+          }}
+          onClick={() => {}}
+        >
+          <EditOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          key={'empowerment'}
+          type={'link'}
+          style={{ padding: 0 }}
+          isPermission={permission.update}
+          tooltip={{
+            title: '赋权',
+          }}
+          onClick={() => {}}
+        >
+          <EditOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          key={'api'}
+          type={'link'}
+          style={{ padding: 0 }}
+          tooltip={{
+            title: '查看API',
+          }}
+          onClick={() => {}}
+        >
+          <EditOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          key={'password'}
+          type={'link'}
+          style={{ padding: 0 }}
+          isPermission={permission.action}
+          tooltip={{
+            title: '重置密码',
+          }}
+          onClick={() => {
+            setPasswordVisible(true);
+          }}
+        >
+          <EditOutlined />
+        </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: () => {
+              deleteById(record.id);
+            },
+          }}
+          tooltip={{
+            title:
+              record.state.value === 'started' ? <span>请先禁用,再删除</span> : <span>删除</span>,
+          }}
+          onClick={() => {}}
+        >
+          <DeleteOutlined />
+        </PermissionButton>,
+      ],
+    },
+  ];
+
+  return (
+    <PageContainer>
+      <SearchComponent
+        field={columns}
+        onSearch={async (data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTable
+        rowKey="id"
+        search={false}
+        params={param}
+        columns={columns}
+        actionRef={actionRef}
+        headerTitle={
+          <PermissionButton
+            key="button"
+            type="primary"
+            isPermission={permission.add}
+            onClick={() => {
+              setSaveVisible(true);
+            }}
+            icon={<PlusOutlined />}
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.add',
+              defaultMessage: '新增',
+            })}
+          </PermissionButton>
+        }
+      />
+      <SaveModal
+        visible={saveVisible}
+        onCancel={() => {
+          setSaveVisible(false);
+        }}
+        onReload={() => {
+          actionRef.current?.reload();
+        }}
+      />
+      <PasswordModal
+        visible={passwordVisible}
+        onCancel={() => {
+          setPasswordVisible(false);
+        }}
+        onReload={() => {
+          actionRef.current?.reload();
+        }}
+      />
+    </PageContainer>
+  );
+};

+ 160 - 0
src/pages/system/Platforms/password.tsx

@@ -0,0 +1,160 @@
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import { Form, FormGrid, FormItem, Password } from '@formily/antd';
+import { message, Modal } from 'antd';
+import { useMemo, useState } from 'react';
+import type { ISchema } from '@formily/json-schema';
+import { service } from '@/pages/system/Platforms/index';
+
+interface SaveProps {
+  visible: boolean;
+  data?: any;
+  onReload?: () => void;
+  onCancel?: () => void;
+}
+
+export default (props: SaveProps) => {
+  const [loading, setLoading] = useState(false);
+
+  const SchemaField = createSchemaField({
+    components: {
+      Form,
+      FormGrid,
+      FormItem,
+      Password,
+    },
+  });
+
+  const form = useMemo(
+    () =>
+      createForm({
+        validateFirst: true,
+        initialValues: props.data || { oath2: true },
+      }),
+    [props.data],
+  );
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      password: {
+        type: 'string',
+        title: '密码',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Password',
+        'x-component-props': {
+          placeholder: '请输入密码',
+        },
+        'x-decorator-props': {
+          gridSpan: 1,
+        },
+        'x-reactions': [
+          {
+            dependencies: ['.confirm_password'],
+            fulfill: {
+              state: {
+                selfErrors:
+                  '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "确认密码不匹配" : ""}}',
+              },
+            },
+          },
+        ],
+        'x-validator': [
+          {
+            max: 64,
+            message: '最多可输入64个字符',
+          },
+          {
+            required: true,
+            message: '请输入密码',
+          },
+        ],
+      },
+      confirm_password: {
+        type: 'string',
+        title: '确认密码',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Password',
+        'x-component-props': {
+          placeholder: '请再次输入密码',
+        },
+        'x-decorator-props': {
+          gridSpan: 1,
+        },
+        'x-reactions': [
+          {
+            dependencies: ['.password'],
+            fulfill: {
+              state: {
+                selfErrors:
+                  '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "确认密码不匹配" : ""}}',
+              },
+            },
+          },
+        ],
+        'x-validator': [
+          {
+            max: 64,
+            message: '最多可输入64个字符',
+          },
+          {
+            required: true,
+            message: '请输入确认密码',
+          },
+        ],
+      },
+      id: {
+        type: 'string',
+        'x-hidden': true,
+      },
+    },
+  };
+
+  /**
+   * 关闭Modal
+   * @param type 是否需要刷新外部table数据
+   * @param id 传递上级部门id,用于table展开父节点
+   */
+  const modalClose = () => {
+    if (props.onCancel) {
+      props.onCancel();
+    }
+  };
+
+  const saveData = async () => {
+    // setLoading(true)
+    const data: any = await form.submit();
+    console.log(data);
+    if (data) {
+      setLoading(true);
+      const resp = await service.update(data);
+      setLoading(false);
+      if (resp.status === 200) {
+        if (props.onReload) {
+          props.onReload();
+        }
+        modalClose();
+        message.success('操作成功');
+      }
+    }
+  };
+
+  return (
+    <Modal
+      maskClosable={false}
+      visible={props.visible}
+      destroyOnClose={true}
+      confirmLoading={loading}
+      onOk={saveData}
+      onCancel={modalClose}
+      width={880}
+      title={'重置密码'}
+    >
+      <Form form={form} layout={'vertical'}>
+        <SchemaField schema={schema} />
+      </Form>
+    </Modal>
+  );
+};

+ 419 - 0
src/pages/system/Platforms/save.tsx

@@ -0,0 +1,419 @@
+import { createForm, Field } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import {
+  Checkbox,
+  Form,
+  FormGrid,
+  FormItem,
+  Input,
+  NumberPicker,
+  Password,
+  Radio,
+  Select,
+  Switch,
+  TreeSelect,
+} 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';
+
+interface SaveProps {
+  visible: boolean;
+  data?: any;
+  onReload?: () => void;
+  onCancel?: () => void;
+}
+
+export default (props: SaveProps) => {
+  const [loading, setLoading] = useState(false);
+  const { permission: deptPermission } = usePermissions('system/Department');
+
+  const SchemaField = createSchemaField({
+    components: {
+      Checkbox,
+      Form,
+      FormGrid,
+      FormItem,
+      Input,
+      NumberPicker,
+      Select,
+      Switch,
+      TreeSelect,
+      Password,
+      Radio,
+    },
+    scope: {
+      icon(name: any) {
+        return React.createElement(ICONS[name]);
+      },
+    },
+  });
+
+  const getRole = () => service.queryRoleList();
+
+  const useAsyncDataSource = (api: any) => (field: Field) => {
+    field.loading = true;
+    api(field).then(
+      action.bound!((resp: Response<any>) => {
+        field.dataSource = resp.result?.map((item: Record<string, unknown>) => ({
+          ...item,
+          label: item.name,
+          value: item.id,
+        }));
+        field.loading = false;
+      }),
+    );
+  };
+
+  const form = useMemo(
+    () =>
+      createForm({
+        validateFirst: true,
+        initialValues: props.data || { oath2: true },
+      }),
+    [props.data],
+  );
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      grid: {
+        type: 'void',
+        'x-component': 'FormGrid',
+        'x-component-props': {
+          maxColumns: 2,
+          minColumns: 1,
+          columnGap: 12,
+        },
+        properties: {
+          name: {
+            type: 'string',
+            title: '名称',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-component-props': {
+              placeholder: '请输入名称',
+            },
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            required: true,
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入名称',
+              },
+            ],
+          },
+          clientId: {
+            type: 'string',
+            title: 'clientId',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-component-props': {
+              disabled: true,
+            },
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            required: true,
+          },
+          secureKey: {
+            type: 'string',
+            title: 'secureKey',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-component-props': {
+              placeholder: '请输入secureKey',
+            },
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            required: true,
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入secureKey',
+              },
+            ],
+          },
+          accessName: {
+            type: 'string',
+            title: '用户名',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-component-props': {
+              placeholder: '请输入用户名',
+            },
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            required: true,
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入用户名',
+              },
+            ],
+          },
+          password: {
+            type: 'string',
+            title: '密码',
+            required: true,
+            'x-decorator': 'FormItem',
+            'x-component': 'Password',
+            'x-component-props': {
+              placeholder: '请输入密码',
+            },
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            'x-reactions': [
+              {
+                dependencies: ['.confirm_password'],
+                fulfill: {
+                  state: {
+                    selfErrors:
+                      '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "确认密码不匹配" : ""}}',
+                  },
+                },
+              },
+            ],
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入密码',
+              },
+            ],
+          },
+          confirm_password: {
+            type: 'string',
+            title: '确认密码',
+            required: true,
+            'x-decorator': 'FormItem',
+            'x-component': 'Password',
+            'x-component-props': {
+              placeholder: '请再次输入密码',
+            },
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            'x-reactions': [
+              {
+                dependencies: ['.password'],
+                fulfill: {
+                  state: {
+                    selfErrors:
+                      '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "确认密码不匹配" : ""}}',
+                  },
+                },
+              },
+            ],
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入确认密码',
+              },
+            ],
+          },
+          roleIdList: {
+            type: 'string',
+            title: '角色',
+            required: true,
+            'x-decorator': 'FormItem',
+            'x-component': 'Select',
+            'x-component-props': {
+              mode: 'multiple',
+              showArrow: true,
+              placeholder: '请选择角色',
+              filterOption: (input: string, option: any) =>
+                option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+            },
+            'x-decorator-props': {
+              gridSpan: 2,
+              addonAfter: (
+                <PermissionButton
+                  type="link"
+                  style={{ padding: 0 }}
+                  isPermission={deptPermission.add}
+                  onClick={() => {
+                    const tab: any = window.open(`${origin}/#/system/role?save=true`);
+                    tab!.onTabSaveSuccess = (value: any) => {
+                      form.setFieldState('roleIdList', async (state) => {
+                        state.dataSource = await getRole().then((resp) =>
+                          resp.result?.map((item: Record<string, unknown>) => ({
+                            ...item,
+                            label: item.name,
+                            value: item.id,
+                          })),
+                        );
+                        state.value = [...(state.value || []), value.id];
+                      });
+                    };
+                  }}
+                >
+                  <PlusOutlined />
+                </PermissionButton>
+              ),
+            },
+            'x-reactions': ['{{useAsyncDataSource(getRole)}}'],
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择角色',
+              },
+            ],
+          },
+          oath2: {
+            type: 'boolean',
+            title: '开启OAth2',
+            required: true,
+            'x-decorator': 'FormItem',
+            'x-component': 'Radio.Group',
+            'x-component-props': {
+              optionType: 'button',
+              buttonStyle: 'solid',
+            },
+            'x-decorator-props': {
+              gridSpan: 2,
+              tooltip: '免密授权',
+            },
+            enum: [
+              { label: '启用', value: true },
+              { label: '关闭', value: false },
+            ],
+          },
+          redirectUrl: {
+            type: 'boolean',
+            title: 'redirectUrl',
+            required: true,
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-component-props': {
+              placeholder: '请输入redirectUrl',
+            },
+            'x-decorator-props': {
+              gridSpan: 2,
+              tooltip: '授权后自动跳转的页面地址',
+            },
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入redirectUrl',
+              },
+            ],
+          },
+          ipAddress: {
+            type: 'string',
+            title: 'IP白名单',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input.TextArea',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入IP白名单,多个地址回车分隔,不填默认均可访问',
+              rows: 3,
+            },
+          },
+          description: {
+            type: 'string',
+            title: '说明',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input.TextArea',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              rows: 3,
+              placeholder: '请输入说明',
+              showCount: true,
+              maxLength: 200,
+            },
+          },
+          id: {
+            type: 'string',
+            'x-hidden': true,
+          },
+        },
+      },
+    },
+  };
+
+  /**
+   * 关闭Modal
+   * @param type 是否需要刷新外部table数据
+   * @param id 传递上级部门id,用于table展开父节点
+   */
+  const modalClose = () => {
+    if (props.onCancel) {
+      props.onCancel();
+    }
+  };
+
+  const saveData = async () => {
+    // setLoading(true)
+    const data: any = await form.submit();
+    console.log(data);
+    if (data) {
+      setLoading(true);
+      const resp = data.id ? await service.update(data) : await service.save(data);
+      setLoading(false);
+      if (resp.status === 200) {
+        if (props.onReload) {
+          props.onReload();
+        }
+        modalClose();
+        message.success('操作成功');
+      }
+    }
+  };
+
+  return (
+    <Modal
+      maskClosable={false}
+      visible={props.visible}
+      destroyOnClose={true}
+      confirmLoading={loading}
+      onOk={saveData}
+      onCancel={modalClose}
+      width={880}
+      title={props.data && props.data.id ? '编辑' : '新增'}
+    >
+      <Form form={form} layout={'vertical'}>
+        <SchemaField schema={schema} scope={{ useAsyncDataSource, getRole }} />
+      </Form>
+    </Modal>
+  );
+};

+ 13 - 0
src/pages/system/Platforms/service.ts

@@ -0,0 +1,13 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<platformsType> {
+  queryRoleList = (params?: any) =>
+    request(`${SystemConst.API_BASE}/role/_query/no-paging?paging=false`, {
+      method: 'GET',
+      params,
+    });
+}
+
+export default Service;

+ 7 - 0
src/pages/system/Platforms/typing.d.ts

@@ -0,0 +1,7 @@
+type platformsType = {
+  name: string;
+  accessName: string;
+  role: string;
+  state: any;
+  description: string;
+};

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

@@ -40,7 +40,29 @@ const extraRouteObj = {
     children: [{ code: 'AMap', name: '地图' }],
   },
 };
-
+//额外路由
+export const extraRouteArr = [
+  {
+    code: 'account',
+    id: 'accout',
+    name: '个人中心',
+    url: '/account',
+    hideInMenu: true,
+    children: [
+      {
+        code: 'account/Center',
+        name: '基本设置',
+        url: '/account/center',
+      },
+      {
+        code: 'account/Center/bind',
+        name: '第三方页面',
+        url: '/account/center/bind',
+        hideInMenu: true,
+      },
+    ],
+  },
+];
 /**
  * 根据url获取映射的组件
  * @param files

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

@@ -35,6 +35,7 @@ export enum MENUS_CODE {
   'link/Certificate' = 'link/Certificate',
   'link/Gateway' = 'link/Gateway',
   'link/Opcua' = 'link/Opcua',
+  'link/Channal/Opcua' = 'link/Channal/Opcua',
   'link/Protocol/Debug' = 'link/Protocol/Debug',
   'link/Protocol' = 'link/Protocol',
   'link/Type' = 'link/Type',
@@ -109,9 +110,13 @@ export enum MENUS_CODE {
   'system/Menu/Detail' = 'system/Menu/Detail',
   'system/Department/Detail' = 'system/Department/Detail',
   'link/Type/Detail' = 'link/Type/Detail',
+  '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',
 }
 
 export type MENUS_CODE_TYPE = keyof typeof MENUS_CODE | string;
@@ -151,4 +156,5 @@ export const getDetailNameByCode = {
   'link/AccessConfig/Detail': '配置详情',
   'media/Stream/Detail': '流媒体详情',
   'rule-engine/Alarm/Log/Detail': '告警日志',
+  'Northbound/AliCloud/Detail': '阿里云详情',
 };