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

feat(第三方平台): 添加列表页

xieyonghong 3 лет назад
Родитель
Сommit
c1598229ff

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

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

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

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

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

@@ -175,6 +175,21 @@ const Scene = () => {
       }),
       width: 120,
       renderText: (record) => TriggerWayType[record],
+      valueType: 'select',
+      valueEnum: {
+        manual: {
+          text: '手动触发',
+          status: 'manual',
+        },
+        timer: {
+          text: '定时触发',
+          status: 'timer',
+        },
+        device: {
+          text: '设备触发',
+          status: 'device',
+        },
+      },
     },
     {
       dataIndex: 'description',
@@ -182,6 +197,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 { useRef, useState } from 'react';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import ProTable from '@jetlinks/pro-table';
+import { BadgeStatus, PermissionButton } from '@/components';
+import SearchComponent from '@/components/SearchComponent';
+import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
+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,
+  Select,
+  Switch,
+  TreeSelect,
+  Password,
+  Radio,
+} from '@formily/antd';
+import { message, Modal } from 'antd';
+import React, { useMemo, useState } from 'react';
+import * as ICONS from '@ant-design/icons';
+import type { ISchema } from '@formily/json-schema';
+import { PermissionButton } from '@/components';
+import usePermissions from '@/hooks/permission';
+import { action } from '@formily/reactive';
+import { Response } from '@/utils/typings';
+import { service } from '@/pages/system/Platforms/index';
+import { PlusOutlined } from '@ant-design/icons';
+
+interface SaveProps {
+  visible: boolean;
+  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;
+};

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

@@ -111,6 +111,7 @@ export enum MENUS_CODE {
   'link/Type/Detail' = 'link/Type/Detail',
   'Northbound/DuerOS' = 'Northbound/DuerOS',
   'Northbound/AliCloud' = 'Northbound/AliCloud',
+  'system/Platforms' = 'system/Platforms',
 }
 
 export type MENUS_CODE_TYPE = keyof typeof MENUS_CODE | string;