Kaynağa Gözat

feat(merge): merge xyh

feat: merge
Lind 3 yıl önce
ebeveyn
işleme
4aba31b524
34 değiştirilmiş dosya ile 1204 ekleme ve 167 silme
  1. 7 7
      package.json
  2. BIN
      public/images/alarm/background.png
  3. BIN
      public/images/alarm/device.png
  4. BIN
      public/images/alarm/log.png
  5. BIN
      public/images/alarm/org.png
  6. BIN
      public/images/alarm/other.png
  7. BIN
      public/images/alarm/product.png
  8. BIN
      public/images/metadata-map.png
  9. BIN
      public/images/notice/doc/template/weixin-official/02-mini-Program-Appid.png
  10. 4 0
      src/pages/device/Instance/Detail/Info/index.tsx
  11. 23 12
      src/pages/device/Instance/Detail/MetadataMap/EditableTable/index.tsx
  12. 160 0
      src/pages/device/Instance/Detail/Reation/Edit.tsx
  13. 88 0
      src/pages/device/Instance/Detail/Reation/index.tsx
  14. 14 0
      src/pages/device/Instance/service.ts
  15. 1 0
      src/pages/device/Instance/typings.d.ts
  16. 224 1
      src/pages/device/components/Metadata/Base/Edit/index.tsx
  17. 24 2
      src/pages/device/components/Metadata/Cat/index.tsx
  18. 1 1
      src/pages/device/components/Metadata/index.tsx
  19. 31 25
      src/pages/link/AccessConfig/Detail/Access/index.tsx
  20. 1 1
      src/pages/link/AccessConfig/Detail/Provider/index.tsx
  21. 3 2
      src/pages/link/AccessConfig/service.ts
  22. 34 20
      src/pages/notice/Template/Debug/index.tsx
  23. 31 14
      src/pages/notice/Template/Detail/doc/WeixinApp.tsx
  24. 1 0
      src/pages/notice/Template/Detail/index.tsx
  25. 27 4
      src/pages/notice/Template/Log/index.tsx
  26. 1 0
      src/pages/notice/Template/index.tsx
  27. 1 0
      src/pages/notice/Template/typings.d.ts
  28. 32 1
      src/pages/rule-engine/Alarm/Log/model.ts
  29. 251 0
      src/pages/system/Relationship/Save/index.tsx
  30. 146 0
      src/pages/system/Relationship/index.tsx
  31. 12 0
      src/pages/system/Relationship/service.ts
  32. 12 0
      src/pages/system/Relationship/typings.d.ts
  33. 2 3
      src/pages/system/User/ResetPassword/index.tsx
  34. 73 74
      yarn.lock

+ 7 - 7
package.json

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

BIN
public/images/alarm/background.png


BIN
public/images/alarm/device.png


BIN
public/images/alarm/log.png


BIN
public/images/alarm/org.png


BIN
public/images/alarm/other.png


BIN
public/images/alarm/product.png


BIN
public/images/metadata-map.png


BIN
public/images/notice/doc/template/weixin-official/02-mini-Program-Appid.png


+ 4 - 0
src/pages/device/Instance/Detail/Info/index.tsx

@@ -4,6 +4,7 @@ import moment from 'moment';
 import { observer } from '@formily/react';
 import { observer } from '@formily/react';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import Config from '@/pages/device/Instance/Detail/Config';
 import Config from '@/pages/device/Instance/Detail/Config';
+import Reation from '@/pages/device/Instance/Detail/Reation';
 import Save from '../../Save';
 import Save from '../../Save';
 import { useState } from 'react';
 import { useState } from 'react';
 import type { DeviceInstance } from '../../typings';
 import type { DeviceInstance } from '../../typings';
@@ -115,6 +116,9 @@ const Info = observer(() => {
         </Descriptions>
         </Descriptions>
         <Config />
         <Config />
         {InstanceModel.detail?.tags && InstanceModel.detail?.tags.length > 0 && <Tags />}
         {InstanceModel.detail?.tags && InstanceModel.detail?.tags.length > 0 && <Tags />}
+        {InstanceModel.detail?.relations && InstanceModel.detail?.relations.length > 0 && (
+          <Reation />
+        )}
       </Card>
       </Card>
       <Save
       <Save
         model={'edit'}
         model={'edit'}

+ 23 - 12
src/pages/device/Instance/Detail/MetadataMap/EditableTable/index.tsx

@@ -27,6 +27,7 @@ interface EditableCellProps {
   dataIndex: string;
   dataIndex: string;
   record: any;
   record: any;
   list: any[];
   list: any[];
+  properties: any[];
   handleSave: (record: any) => void;
   handleSave: (record: any) => void;
 }
 }
 
 
@@ -37,15 +38,17 @@ const EditableCell = ({
   dataIndex,
   dataIndex,
   record,
   record,
   list,
   list,
+  properties,
   handleSave,
   handleSave,
   ...restProps
   ...restProps
 }: EditableCellProps) => {
 }: EditableCellProps) => {
   const form: any = useContext(EditableContext);
   const form: any = useContext(EditableContext);
+  const [temp, setTemp] = useState<any>({});
 
 
   const save = async () => {
   const save = async () => {
     try {
     try {
       const values = await form.validateFields();
       const values = await form.validateFields();
-      handleSave({ ...record, metadataId: values?.metadataId });
+      handleSave({ ...record, originalId: values?.originalId });
     } catch (errInfo) {
     } catch (errInfo) {
       console.log('Save failed:', errInfo);
       console.log('Save failed:', errInfo);
     }
     }
@@ -54,6 +57,7 @@ const EditableCell = ({
   useEffect(() => {
   useEffect(() => {
     if (record) {
     if (record) {
       form.setFieldsValue({ [dataIndex]: record[dataIndex] });
       form.setFieldsValue({ [dataIndex]: record[dataIndex] });
+      setTemp(properties.find((i) => i.id === record.originalId));
     }
     }
   }, [record]);
   }, [record]);
 
 
@@ -71,6 +75,11 @@ const EditableCell = ({
           }
           }
         >
         >
           <Select.Option value={record.metadataId}>使用原始属性</Select.Option>
           <Select.Option value={record.metadataId}>使用原始属性</Select.Option>
+          {record.originalId !== record.metadataId && (
+            <Select.Option value={record.originalId}>
+              {temp?.name}({temp?.id})
+            </Select.Option>
+          )}
           {list.length > 0 &&
           {list.length > 0 &&
             list.map((item: any) => (
             list.map((item: any) => (
               <Select.Option key={item?.id} value={item?.id}>
               <Select.Option key={item?.id} value={item?.id}>
@@ -98,7 +107,7 @@ const EditableTable = (props: Props) => {
     },
     },
     {
     {
       title: '设备上报属性',
       title: '设备上报属性',
-      dataIndex: 'metadataId',
+      dataIndex: 'originalId',
       width: '30%',
       width: '30%',
       editable: true,
       editable: true,
     },
     },
@@ -136,7 +145,7 @@ const EditableTable = (props: Props) => {
     },
     },
   };
   };
 
 
-  const initData = async () => {
+  const initData = async (lists: any[]) => {
     let resp = null;
     let resp = null;
     if (props.type === 'device') {
     if (props.type === 'device') {
       resp = await service.queryDeviceMetadata(props.data.id);
       resp = await service.queryDeviceMetadata(props.data.id);
@@ -147,10 +156,10 @@ const EditableTable = (props: Props) => {
       const data = resp.result;
       const data = resp.result;
       const obj: any = {};
       const obj: any = {};
       data.map((i: any) => {
       data.map((i: any) => {
-        obj[i?.originalId] = i;
+        obj[i?.metadataId] = i;
       });
       });
-      if (protocolMetadata.length > 0) {
-        setPmList(protocolMetadata.filter((i) => !_.map(data, 'metadataId').includes(i.id)));
+      if (lists.length > 0) {
+        setPmList(lists.filter((i) => !_.map(data, 'originalId').includes(i.id)));
       } else {
       } else {
         setPmList([]);
         setPmList([]);
       }
       }
@@ -184,8 +193,9 @@ const EditableTable = (props: Props) => {
         )
         )
         .then((resp) => {
         .then((resp) => {
           if (resp.status === 200) {
           if (resp.status === 200) {
-            setProtocolMetadata(JSON.parse(resp.result || '{}')?.properties || []);
-            initData();
+            const list = JSON.parse(resp.result || '{}')?.properties || [];
+            setProtocolMetadata(list);
+            initData(list);
           }
           }
         });
         });
     }
     }
@@ -195,21 +205,21 @@ const EditableTable = (props: Props) => {
     const newData = [...dataSource.data];
     const newData = [...dataSource.data];
     const index = newData.findIndex((item) => row.id === item.id);
     const index = newData.findIndex((item) => row.id === item.id);
     const item = newData[index];
     const item = newData[index];
-    if (item?.metadataId !== row?.metadataId) {
+    if (item?.originalId !== row?.originalId) {
       const resp = await service[
       const resp = await service[
         props.type === 'device' ? 'saveDeviceMetadata' : 'saveProductMetadata'
         props.type === 'device' ? 'saveDeviceMetadata' : 'saveProductMetadata'
       ](props.data?.id, [
       ](props.data?.id, [
         {
         {
           metadataType: 'property',
           metadataType: 'property',
-          metadataId: row.metadataId === row.id ? row.metadataId : row.id,
-          originalId: row.metadataId === row.id ? row.id : '',
+          metadataId: row.metadataId,
+          originalId: row.metadataId !== row.originalId ? row.originalId : '',
           others: {},
           others: {},
         },
         },
       ]);
       ]);
       if (resp.status === 200) {
       if (resp.status === 200) {
         message.success('操作成功!');
         message.success('操作成功!');
         // 刷新
         // 刷新
-        initData();
+        initData(protocolMetadata);
       }
       }
     }
     }
   };
   };
@@ -253,6 +263,7 @@ const EditableTable = (props: Props) => {
         dataIndex: col.dataIndex,
         dataIndex: col.dataIndex,
         title: col.title,
         title: col.title,
         list: pmList,
         list: pmList,
+        properties: protocolMetadata,
         handleSave: handleSave,
         handleSave: handleSave,
       }),
       }),
     };
     };

+ 160 - 0
src/pages/device/Instance/Detail/Reation/Edit.tsx

@@ -0,0 +1,160 @@
+import type { Field } from '@formily/core';
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import { InstanceModel, service } from '@/pages/device/Instance';
+import type { ISchema } from '@formily/json-schema';
+import { Form, FormGrid, FormItem, Select, PreviewText } from '@formily/antd';
+import { useParams } from 'umi';
+import { Button, Drawer, message, Space } from 'antd';
+import { action } from '@formily/reactive';
+import type { Response } from '@/utils/typings';
+import { useEffect, useState } from 'react';
+
+interface Props {
+  close: () => void;
+  data: any[];
+}
+
+const Edit = (props: Props) => {
+  const { data } = props;
+  const params = useParams<{ id: string }>();
+  const id = InstanceModel.detail?.id || params?.id;
+  const [initData, setInitData] = useState<any>({});
+
+  const getUsers = () => service.queryUserListNopaging();
+
+  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: JSON.stringify({
+            id: item.id,
+            name: item.name,
+          }),
+        }));
+        field.loading = false;
+      }),
+    );
+  };
+
+  const form = createForm({
+    validateFirst: true,
+    initialValues: initData,
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Select,
+      FormGrid,
+      PreviewText,
+    },
+  });
+
+  const configToSchema = (list: any[]) => {
+    const config = {};
+    list.forEach((item) => {
+      config[item.relation] = {
+        type: 'string',
+        title: item.relationName,
+        'x-decorator': 'FormItem',
+        'x-component': 'Select',
+        'x-component-props': {
+          placeholder: '请选择关联方',
+          showSearch: true,
+          showArrow: true,
+          mode: 'multiple',
+          filterOption: (input: string, option: any) =>
+            option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+        },
+        'x-reactions': ['{{useAsyncDataSource(getUsers)}}'],
+      };
+    });
+    return config;
+  };
+
+  const renderConfigCard = () => {
+    const itemSchema: ISchema = {
+      type: 'object',
+      properties: {
+        grid: {
+          type: 'void',
+          'x-component': 'FormGrid',
+          'x-component-props': {
+            minColumns: [1],
+            maxColumns: [1],
+          },
+          properties: configToSchema(data),
+        },
+      },
+    };
+
+    return (
+      <>
+        <PreviewText.Placeholder value="-">
+          <Form form={form} layout="vertical">
+            <SchemaField schema={itemSchema} scope={{ useAsyncDataSource, getUsers }} />
+          </Form>
+        </PreviewText.Placeholder>
+      </>
+    );
+  };
+
+  useEffect(() => {
+    const obj: any = {};
+    (props?.data || []).map((item: any) => {
+      obj[item.relation] = [...(item?.related || []).map((i: any) => JSON.stringify(i))];
+    });
+    setInitData(obj);
+  }, [props.data]);
+
+  return (
+    <Drawer
+      title="编辑"
+      placement="right"
+      onClose={() => {
+        props.close();
+      }}
+      visible
+      extra={
+        <Space>
+          <Button
+            type="primary"
+            onClick={async () => {
+              const values = (await form.submit()) as any;
+              if (Object.keys(values).length > 0) {
+                const param: any[] = [];
+                Object.keys(values).forEach((key) => {
+                  const item = data.find((i) => i.relation === key);
+                  const items = (values[key] || []).map((i: string) => JSON.parse(i));
+                  if (item) {
+                    param.push({
+                      relatedType: 'user',
+                      relation: item.relation,
+                      description: '',
+                      related: [...items],
+                    });
+                  }
+                });
+                const resp = await service.saveRelations(id || '', param);
+                if (resp.status === 200) {
+                  message.success('操作成功!');
+                  props.close();
+                }
+              }
+            }}
+          >
+            保存
+          </Button>
+        </Space>
+      }
+    >
+      {renderConfigCard()}
+    </Drawer>
+  );
+};
+
+export default Edit;

+ 88 - 0
src/pages/device/Instance/Detail/Reation/index.tsx

@@ -0,0 +1,88 @@
+import { Descriptions, Tooltip } from 'antd';
+import { InstanceModel, service } from '@/pages/device/Instance';
+import { useEffect, useState } from 'react';
+import { history, useParams } from 'umi';
+import { EditOutlined, QuestionCircleOutlined } from '@ant-design/icons';
+import Edit from './Edit';
+import { PermissionButton } from '@/components';
+import _ from 'lodash';
+
+const Reation = () => {
+  const params = useParams<{ id: string }>();
+  useEffect(() => {
+    const id = InstanceModel.current?.id || params.id;
+    if (id) {
+      service.getConfigMetadata(id).then((response) => {
+        InstanceModel.config = response?.result;
+      });
+    } else {
+      history.goBack();
+    }
+  }, []);
+
+  const [data, setData] = useState<any[]>([]);
+  const [visible, setVisible] = useState<boolean>(false);
+  const { permission } = PermissionButton.usePermission('device/Instance');
+
+  const id = InstanceModel.detail?.id || params?.id;
+
+  const getDetail = () => {
+    service.detail(id || '').then((resp) => {
+      if (resp.status === 200) {
+        InstanceModel.detail = { id, ...resp.result };
+      }
+    });
+  };
+
+  useEffect(() => {
+    if (id) {
+      setData(InstanceModel.detail?.relations || []);
+    }
+  }, [id]);
+
+  return (
+    <div style={{ width: '100%', marginTop: '20px' }}>
+      <Descriptions
+        style={{ marginBottom: 20 }}
+        bordered
+        column={3}
+        size="small"
+        title={
+          <span>
+            关系信息
+            <PermissionButton
+              isPermission={permission.update}
+              type="link"
+              onClick={async () => {
+                setVisible(true);
+              }}
+            >
+              <EditOutlined />
+              编辑
+              <Tooltip title={`管理设备与其他业务的关联关系,关系来源于关系配置`}>
+                <QuestionCircleOutlined />
+              </Tooltip>
+            </PermissionButton>
+          </span>
+        }
+      >
+        {(data || [])?.map((item: any) => (
+          <Descriptions.Item span={1} label={item.relationName} key={item.objectId}>
+            {_.map(item?.related || [], 'name').join(',')}
+          </Descriptions.Item>
+        ))}
+      </Descriptions>
+      {visible && (
+        <Edit
+          data={data || []}
+          close={() => {
+            setVisible(false);
+            getDetail();
+          }}
+        />
+      )}
+    </div>
+  );
+};
+
+export default Reation;

+ 14 - 0
src/pages/device/Instance/service.ts

@@ -251,6 +251,20 @@ class Service extends BaseService<DeviceInstance> {
 
 
   //接入方式
   //接入方式
   public queryGatewayList = () => request(`/${SystemConst.API_BASE}/gateway/device/providers`);
   public queryGatewayList = () => request(`/${SystemConst.API_BASE}/gateway/device/providers`);
+  // 保存设备关系
+  public saveRelations = (id: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/device/instance/${id}/relations`, {
+      method: 'PATCH',
+      data,
+    });
+  // 查询用户
+  public queryUserListNopaging = () =>
+    request(`/${SystemConst.API_BASE}/user/_query/no-paging`, {
+      method: 'POST',
+      data: {
+        paging: false,
+      },
+    });
 }
 }
 
 
 export default Service;
 export default Service;

+ 1 - 0
src/pages/device/Instance/typings.d.ts

@@ -29,6 +29,7 @@ export type DeviceInstance = {
   orgId: string;
   orgId: string;
   orgName: string;
   orgName: string;
   configuration: Record<string, any>;
   configuration: Record<string, any>;
+  relations?: any[];
   cachedConfiguration: any;
   cachedConfiguration: any;
   transport: string;
   transport: string;
   protocol: string;
   protocol: string;

+ 224 - 1
src/pages/device/components/Metadata/Base/Edit/index.tsx

@@ -2,11 +2,14 @@ import { Button, Drawer, Dropdown, Menu, message } from 'antd';
 import { createSchemaField, observer } from '@formily/react';
 import { createSchemaField, observer } from '@formily/react';
 import MetadataModel from '../model';
 import MetadataModel from '../model';
 import type { Field, IFieldState } from '@formily/core';
 import type { Field, IFieldState } from '@formily/core';
-import { createForm, registerValidateRules } from '@formily/core';
+import { createForm, onFieldReact, registerValidateRules } from '@formily/core';
 import {
 import {
   ArrayItems,
   ArrayItems,
+  Checkbox,
+  DatePicker,
   Editable,
   Editable,
   Form,
   Form,
+  FormGrid,
   FormItem,
   FormItem,
   FormLayout,
   FormLayout,
   Input,
   Input,
@@ -54,6 +57,44 @@ const Edit = observer((props: Props) => {
     () =>
     () =>
       createForm({
       createForm({
         initialValues: MetadataModel.item as Record<string, unknown>,
         initialValues: MetadataModel.item as Record<string, unknown>,
+        effects: () => {
+          onFieldReact('expands.metrics.*.*', (field, form1) => {
+            console.log('指标配置');
+            const type = field.query('valueType.type').take() as Field;
+            console.log(type.value, 'value');
+
+            const componentMap = {
+              int: 'NumberPicker',
+              long: 'NumberPicker',
+              float: 'NumberPicker',
+              double: 'NumberPicker',
+              number: 'NumberPicker',
+              date: 'DatePicker',
+              boolean: 'Select',
+            };
+
+            form1.setFieldState('expands.metrics.*.edit.space.value.*', (state) => {
+              state.componentType = componentMap[type.value] || 'Input';
+              if (type.value === 'date') {
+                state.componentProps = {
+                  showTime: true,
+                };
+              } else if (type.value === 'boolean') {
+                state.componentType = 'Select';
+                // 获取 boolean配置的值
+                const values = form1.getValuesIn('valueType');
+                state.dataSource = [
+                  { label: values.trueText, value: values.trueValue },
+                  { label: values.falseText, value: values.falseValue },
+                ];
+              }
+            });
+          });
+          /// 处理Boolean 类型
+          // expands.metrics.0.edit.space.value.0 路径
+          // const metricsPath = field.query('expands.metrics.value.0');
+          // form.setValuesIn('expands.metrics.value.0', 'testtttt')
+        },
       }),
       }),
     [],
     [],
   );
   );
@@ -102,6 +143,9 @@ const Edit = observer((props: Props) => {
       BooleanEnum,
       BooleanEnum,
       ConfigParam,
       ConfigParam,
       FRuleEditor,
       FRuleEditor,
+      Checkbox,
+      FormGrid,
+      DatePicker,
     },
     },
     scope: {
     scope: {
       async asyncOtherConfig(field: Field) {
       async asyncOtherConfig(field: Field) {
@@ -616,6 +660,185 @@ const Edit = observer((props: Props) => {
             'x-component': 'ConfigParam',
             'x-component': 'ConfigParam',
             'x-reactions': '{{asyncOtherConfig}}',
             'x-reactions': '{{asyncOtherConfig}}',
           },
           },
+          // 指标
+          metrics: {
+            type: 'array',
+            'x-component': 'ArrayItems',
+            'x-decorator': 'FormItem',
+            title: '指标配置',
+            items: {
+              type: 'object',
+              'x-decorator': 'ArrayItems.Item',
+              properties: {
+                left: {
+                  type: 'void',
+                  'x-component': 'Space',
+                  properties: {
+                    sort: {
+                      type: 'void',
+                      'x-decorator': 'FormItem',
+                      'x-component': 'ArrayItems.SortHandle',
+                    },
+                    index: {
+                      type: 'void',
+                      'x-decorator': 'FormItem',
+                      'x-component': 'ArrayItems.Index',
+                    },
+                  },
+                },
+                edit: {
+                  type: 'void',
+                  'x-component': 'Editable.Popover',
+                  title: '指标数据',
+                  properties: {
+                    id: {
+                      // 标识
+                      title: '标识',
+                      'x-decorator': 'FormItem',
+                      'x-component': 'Input',
+                      'x-decorator-props': {
+                        labelAlign: 'left',
+                        layout: 'vertical',
+                      },
+                    },
+                    name: {
+                      // 名称
+                      title: '名称',
+                      'x-decorator': 'FormItem',
+                      'x-component': 'Input',
+                      'x-decorator-props': {
+                        labelAlign: 'left',
+                        layout: 'vertical',
+                      },
+                    },
+                    space: {
+                      type: 'void',
+                      title: '指标值',
+                      'x-decorator': 'FormItem',
+                      'x-component': 'FormGrid',
+                      'x-decorator-props': {
+                        labelAlign: 'left',
+                        layout: 'vertical',
+                      },
+                      'x-component-props': {
+                        maxColumns: 12,
+                        minColumns: 12,
+                      },
+                      properties: {
+                        'value[0]': {
+                          'x-decorator': 'FormItem',
+                          'x-component': 'Input',
+                          'x-decorator-props': {
+                            gridSpan: 5,
+                          },
+                          'x-reactions': {
+                            dependencies: ['..range', 'valueType.type'],
+                            fulfill: {
+                              state: {
+                                decoratorProps: {
+                                  gridSpan: '{{!!$deps[0]?5:$deps[1]==="boolean"?12:10}}',
+                                },
+                                componentType:
+                                  '{{["int","long","double","float"].includes($deps[1])?"NumberPicker":["date"].includes($deps[1])?"DatePicker":"Input"}}',
+                              },
+                            },
+                          },
+                          // 根据数据类型来渲染不同的组件
+                        },
+                        'value[1]': {
+                          title: '~',
+                          'x-decorator': 'FormItem',
+                          'x-component': 'Input',
+                          'x-decorator-props': {
+                            gridSpan: 5,
+                          },
+                          'x-reactions': [
+                            {
+                              dependencies: ['..range', 'valueType.type'],
+                              fulfill: {
+                                state: {
+                                  visible: '{{!!$deps[0]}}',
+                                  componentType:
+                                    '{{["int","long","double","float"].includes($deps[1])?"NumberPicker":["date"].includes($deps[1])?"DatePicker":"Input"}}',
+                                },
+                              },
+                            },
+                            {
+                              dependencies: ['valueType.type'],
+                              fulfill: {
+                                state: {
+                                  visible: '{{!$deps[0]==="boolean"}}',
+                                },
+                              },
+                            },
+                          ],
+                        },
+                        // 根据数据类型来渲染不同的组件
+                        range: {
+                          type: 'boolean',
+                          default: false,
+                          'x-decorator': 'FormItem',
+                          'x-component': 'Checkbox',
+                          'x-component-props': {
+                            children: '范围',
+                          },
+                          'x-decorator-props': {
+                            gridSpan: 2,
+                          },
+                          'x-reactions': {
+                            dependencies: ['valueType.type'],
+                            when: '{{$deps[0]==="boolean"}}',
+                            fulfill: {
+                              state: {
+                                visible: false,
+                                decoratorProps: {
+                                  gridSpan: 0,
+                                },
+                              },
+                            },
+                            otherwise: {
+                              state: {
+                                visible: true,
+                                decoratorProps: {
+                                  gridSpan: 2,
+                                },
+                              },
+                            },
+                          },
+                        },
+                      },
+                    },
+                  },
+                },
+                right: {
+                  type: 'void',
+                  'x-component': 'Space',
+                  properties: {
+                    remove: {
+                      type: 'void',
+                      'x-component': 'ArrayItems.Remove',
+                    },
+                  },
+                },
+              },
+            },
+            properties: {
+              addition: {
+                type: 'void',
+                title: '添加指标',
+                'x-component': 'ArrayItems.Addition',
+              },
+            },
+            'x-reactions': {
+              dependencies: ['valueType.type'],
+              fulfill: {
+                state: {
+                  visible:
+                    "{{['int','float','double','long','date','string','boolean'].includes($deps[0])}}",
+                },
+              },
+            },
+          },
         },
         },
       },
       },
     },
     },

+ 24 - 2
src/pages/device/components/Metadata/Cat/index.tsx

@@ -3,23 +3,45 @@ import { useEffect, useState } from 'react';
 import { productModel, service } from '@/pages/device/Product';
 import { productModel, service } from '@/pages/device/Product';
 import MonacoEditor from 'react-monaco-editor';
 import MonacoEditor from 'react-monaco-editor';
 import { observer } from '@formily/react';
 import { observer } from '@formily/react';
+import { InstanceModel } from '@/pages/device/Instance';
+import { useLocation } from 'umi';
+import InstanceService from '@/pages/device/Instance/service';
 
 
 interface Props {
 interface Props {
   visible: boolean;
   visible: boolean;
   close: () => void;
   close: () => void;
+  type: 'product' | 'device';
 }
 }
 
 
+const instanceService = new InstanceService('device-instance');
 const Cat = observer((props: Props) => {
 const Cat = observer((props: Props) => {
+  const location = useLocation<{ id: string }>();
   const [codecs, setCodecs] = useState<{ id: string; name: string }[]>();
   const [codecs, setCodecs] = useState<{ id: string; name: string }[]>();
-  const metadata = productModel.current?.metadata as string;
+  const metadataMap = {
+    product: productModel.current?.metadata as string,
+    device: InstanceModel.current?.metadata as string, // 有问题
+  };
+  const metadata = metadataMap[props.type];
   const [value, setValue] = useState(metadata);
   const [value, setValue] = useState(metadata);
+  const _path = location.pathname.split('/');
+  const id = _path[_path.length - 1];
   useEffect(() => {
   useEffect(() => {
     service.codecs().subscribe({
     service.codecs().subscribe({
       next: (data) => {
       next: (data) => {
         setCodecs([{ id: 'jetlinks', name: 'jetlinks' }].concat(data));
         setCodecs([{ id: 'jetlinks', name: 'jetlinks' }].concat(data));
       },
       },
     });
     });
-  }, []);
+
+    if (props.type === 'device' && id) {
+      instanceService.detail(id).then((resp) => {
+        if (resp.status === 200) {
+          InstanceModel.current = resp.result;
+          const _metadata = resp.result?.metadata;
+          setValue(_metadata);
+        }
+      });
+    }
+  }, [id]);
 
 
   const convertMetadata = (key: string) => {
   const convertMetadata = (key: string) => {
     if (key === 'alink') {
     if (key === 'alink') {

+ 1 - 1
src/pages/device/components/Metadata/index.tsx

@@ -94,7 +94,7 @@ const Metadata = observer((props: Props) => {
         </Tabs.TabPane>
         </Tabs.TabPane>
       </Tabs>
       </Tabs>
       <Import visible={visible} close={() => setVisible(false)} />
       <Import visible={visible} close={() => setVisible(false)} />
-      <Cat visible={cat} close={() => setCat(false)} />
+      <Cat visible={cat} close={() => setCat(false)} type={props.type} />
     </div>
     </div>
   );
   );
 });
 });

+ 31 - 25
src/pages/link/AccessConfig/Detail/Access/index.tsx

@@ -42,6 +42,7 @@ const Access = (props: Props) => {
   const [config, setConfig] = useState<any>();
   const [config, setConfig] = useState<any>();
   const networkPermission = PermissionButton.usePermission('link/Type').permission;
   const networkPermission = PermissionButton.usePermission('link/Type').permission;
   const protocolPermission = PermissionButton.usePermission('link/Protocol').permission;
   const protocolPermission = PermissionButton.usePermission('link/Protocol').permission;
+  const [steps, setSteps] = useState<string[]>(['网络组件', '消息协议', '完成']);
 
 
   const MetworkTypeMapping = new Map();
   const MetworkTypeMapping = new Map();
   MetworkTypeMapping.set('websocket-server', 'WEB_SOCKET_SERVER');
   MetworkTypeMapping.set('websocket-server', 'WEB_SOCKET_SERVER');
@@ -69,7 +70,7 @@ const Access = (props: Props) => {
     });
     });
   };
   };
 
 
-  const queryProcotolList = (id: string, params?: any) => {
+  const queryProcotolList = (id?: string, params?: any) => {
     service.getProtocolList(ProcotoleMapping.get(id), params).then((resp) => {
     service.getProtocolList(ProcotoleMapping.get(id), params).then((resp) => {
       if (resp.status === 200) {
       if (resp.status === 200) {
         setProcotolList(resp.result);
         setProcotolList(resp.result);
@@ -79,25 +80,39 @@ const Access = (props: Props) => {
 
 
   useEffect(() => {
   useEffect(() => {
     if (props.provider?.id && !props.data?.id) {
     if (props.provider?.id && !props.data?.id) {
-      queryNetworkList(props.provider?.id, {
-        include: networkCurrent || '',
-      });
-      setCurrent(0);
+      if (props.provider?.id !== 'child-device') {
+        setSteps(['网络组件', '消息协议', '完成']);
+        queryNetworkList(props.provider?.id, {
+          include: networkCurrent || '',
+        });
+        setCurrent(0);
+      } else {
+        setSteps(['消息协议', '完成']);
+        setCurrent(1);
+        queryProcotolList(props.provider?.id);
+      }
     }
     }
   }, [props.provider]);
   }, [props.provider]);
 
 
   useEffect(() => {
   useEffect(() => {
     if (props.data?.id) {
     if (props.data?.id) {
       setProcotolCurrent(props.data?.protocol);
       setProcotolCurrent(props.data?.protocol);
-      setNetworkCurrent(props.data?.channelId);
       form.setFieldsValue({
       form.setFieldsValue({
         name: props.data?.name,
         name: props.data?.name,
         description: props.data?.description,
         description: props.data?.description,
       });
       });
-      setCurrent(0);
-      queryNetworkList(props.data?.provider, {
-        include: props.data?.channelId,
-      });
+      if (props.data?.provider !== 'child-device') {
+        setCurrent(0);
+        setSteps(['网络组件', '消息协议', '完成']);
+        setNetworkCurrent(props.data?.channelId);
+        queryNetworkList(props.data?.provider, {
+          include: props.data?.channelId,
+        });
+      } else {
+        setSteps(['消息协议', '完成']);
+        setCurrent(1);
+        queryProcotolList(props.data?.provider);
+      }
     }
     }
   }, [props.data]);
   }, [props.data]);
 
 
@@ -130,18 +145,6 @@ const Access = (props: Props) => {
     setCurrent(current - 1);
     setCurrent(current - 1);
   };
   };
 
 
-  const steps = [
-    {
-      title: '网络组件',
-    },
-    {
-      title: '消息协议',
-    },
-    {
-      title: '完成',
-    },
-  ];
-
   const columnsMQTT: any[] = [
   const columnsMQTT: any[] = [
     {
     {
       title: '分组',
       title: '分组',
@@ -525,7 +528,10 @@ const Access = (props: Props) => {
                               description: values.description,
                               description: values.description,
                               provider: props.provider.id,
                               provider: props.provider.id,
                               protocol: procotolCurrent,
                               protocol: procotolCurrent,
-                              transport: ProcotoleMapping.get(props.provider.id),
+                              transport:
+                                props.provider?.id === 'child-device'
+                                  ? 'Gateway'
+                                  : ProcotoleMapping.get(props.provider.id),
                               channel: 'network', // 网络组件
                               channel: 'network', // 网络组件
                               channelId: networkCurrent,
                               channelId: networkCurrent,
                             })
                             })
@@ -648,13 +654,13 @@ const Access = (props: Props) => {
         <div className={styles.steps}>
         <div className={styles.steps}>
           <Steps size="small" current={current}>
           <Steps size="small" current={current}>
             {steps.map((item) => (
             {steps.map((item) => (
-              <Steps.Step key={item.title} title={item.title} />
+              <Steps.Step key={item} title={item} />
             ))}
             ))}
           </Steps>
           </Steps>
         </div>
         </div>
         <div className={styles.content}>{renderSteps(current)}</div>
         <div className={styles.content}>{renderSteps(current)}</div>
         <div className={styles.action}>
         <div className={styles.action}>
-          {current === 1 && (
+          {current === 1 && props.provider.id !== 'child-device' && (
             <Button style={{ margin: '0 8px' }} onClick={() => prev()}>
             <Button style={{ margin: '0 8px' }} onClick={() => prev()}>
               上一步
               上一步
             </Button>
             </Button>

+ 1 - 1
src/pages/link/AccessConfig/Detail/Provider/index.tsx

@@ -50,7 +50,7 @@ const Provider = (props: Props) => {
                     <div className={styles.images}>{item.name}</div>
                     <div className={styles.images}>{item.name}</div>
                     <div style={{ margin: '10px', width: 'calc(100% - 84px)' }}>
                     <div style={{ margin: '10px', width: 'calc(100% - 84px)' }}>
                       <div style={{ fontWeight: 600 }}>{item.name}</div>
                       <div style={{ fontWeight: 600 }}>{item.name}</div>
-                      <div className={styles.desc}>{item.description}</div>
+                      <div className={styles.desc}>{item.description || '--'}</div>
                     </div>
                     </div>
                   </div>
                   </div>
                   <div style={{ width: '70px' }}>
                   <div style={{ width: '70px' }}>

+ 3 - 2
src/pages/link/AccessConfig/service.ts

@@ -31,11 +31,12 @@ class Service extends BaseService<AccessItem> {
       method: 'GET',
       method: 'GET',
       params,
       params,
     });
     });
-  public getProtocolList = (transport: string, params?: any) =>
-    request(`/${SystemConst.API_BASE}/protocol/supports/${transport}`, {
+  public getProtocolList = (transport?: string, params?: any) => {
+    return request(`/${SystemConst.API_BASE}/protocol/supports/${transport ? transport : ''}`, {
       method: 'GET',
       method: 'GET',
       params,
       params,
     });
     });
+  };
   public getConfigView = (id: string, transport: string) =>
   public getConfigView = (id: string, transport: string) =>
     request(`/${SystemConst.API_BASE}/protocol/${id}/transport/${transport}`, {
     request(`/${SystemConst.API_BASE}/protocol/${id}/transport/${transport}`, {
       method: 'GET',
       method: 'GET',

+ 34 - 20
src/pages/notice/Template/Debug/index.tsx

@@ -38,27 +38,32 @@ const Debug = observer(() => {
             }
             }
           });
           });
 
 
-          onFieldReact('variableDefinitions.*.type', (field) => {
+          onFieldReact('variableDefinitions.*.id', (field) => {
             const value = (field as Field).value;
             const value = (field as Field).value;
-            const format = field.query('.value').take() as any;
-            switch (value) {
-              case 'date':
-                format.setComponent(DatePicker);
-                break;
-              case 'string':
-                format.setComponent(Input);
-                break;
-              case 'number':
-                format.setComponent(NumberPicker);
-                break;
-              case 'file':
-                format.setComponent(FUpload, {
-                  type: 'file',
-                });
-                break;
-              case 'other':
-                format.setComponent(Input);
-                break;
+            const format = field.query('.value').take() as Field;
+
+            if (format) {
+              switch (value) {
+                case 'date':
+                  format.setComponent(DatePicker, {
+                    showTime: true,
+                  });
+                  break;
+                case 'string':
+                  format.setComponent(Input);
+                  break;
+                case 'number':
+                  format.setComponent(NumberPicker, {});
+                  break;
+                case 'file':
+                  format.setComponent(FUpload, {
+                    type: 'file',
+                  });
+                  break;
+                case 'other':
+                  format.setComponent(Input);
+                  break;
+              }
             }
             }
           });
           });
         },
         },
@@ -83,6 +88,9 @@ const Debug = observer(() => {
       Select,
       Select,
       ArrayTable,
       ArrayTable,
       PreviewText,
       PreviewText,
+      NumberPicker,
+      DatePicker,
+      FUpload,
     },
     },
   });
   });
 
 
@@ -164,6 +172,12 @@ const Debug = observer(() => {
                   'x-decorator': 'FormItem',
                   'x-decorator': 'FormItem',
                   'x-component': 'Input',
                   'x-component': 'Input',
                 },
                 },
+                type: {
+                  'x-hidden': true,
+                  type: 'string',
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Input',
+                },
               },
               },
             },
             },
           },
           },

+ 31 - 14
src/pages/notice/Template/Detail/doc/WeixinApp.tsx

@@ -2,35 +2,52 @@ import { Image } from 'antd';
 import './index.less';
 import './index.less';
 
 
 const WeixinApp = () => {
 const WeixinApp = () => {
-  const agentId = require('/public/images/notice/doc/template/weixin-official/01-Agentid.jpg');
-  const appId = require('/public/images/notice/doc/template/weixin-official/02-mini-Program-Appid.jpg');
+  const appId = require('/public/images/notice/doc/template/weixin-official/02-mini-Program-Appid.png');
 
 
   return (
   return (
     <div className="doc">
     <div className="doc">
       <div className="url">
       <div className="url">
-        微信公众平台:<a href="https://mp.weixin.qq.com/">https://mp.weixin.qq.com/</a>
+        企业微信管理后台:<a href="https://work.weixin.qq.com">https://work.weixin.qq.com</a>
       </div>
       </div>
       <h1>1. 概述</h1>
       <h1>1. 概述</h1>
       <div>
       <div>
-        通知配置可以结合通知配置为告警消息通知提供支撑。也可以用于系统中其他自定义模块的调用
+        通知模板结合通知配置为告警消息通知提供支撑。通知模板只能调用同一类型的通知配置服务
       </div>
       </div>
-      <h1>2.通知配置说明</h1>
+      <h1>2.模板配置说明</h1>
       <div>
       <div>
-        <h2>1. AppID</h2>
-        <div>微信服务号的唯一专属编号。</div>
-        <div>获取路径:“微信公众平台”管理后台--“设置与开发”--“基本配置”</div>
-        <div className="image">
-          <Image width="100%" src={agentId} />
-        </div>
+        <h2>1. 绑定配置</h2>
+        <div>绑定通知配置</div>
+      </div>
+      <div>
+        <h2>2. 用户标签</h2>
+        <div>以标签的维度通知该标签下所有用户</div>
       </div>
       </div>
-      <h2>2. AppSecret</h2>
       <div>
       <div>
-        <div>公众号开发者身份的密码</div>
-        <div>获取路径:“微信公众平台”管理后台--“设置与开发”--“基本配置”</div>
+        <h2>3. 消息模板</h2>
+        <div>微信公众号中配置的消息模板</div>
+      </div>
+      <div>
+        <h2>4. 模板跳转链接</h2>
+        <div>点击消息之后进行页面跳转</div>
+      </div>
+      <div>
+        <h2>5. 跳转小程序Appid</h2>
+        <div>点击消息之后打开对应的小程序</div>
+      </div>
+      <div>
+        <h2>6. 跳转小程序具体路径</h2>
+        <div>点击消息之后跳转到小程序的具体页面</div>
         <div className="image">
         <div className="image">
           <Image width="100%" src={appId} />
           <Image width="100%" src={appId} />
         </div>
         </div>
       </div>
       </div>
+      <div>
+        <h2>7. 模板内容</h2>
+        <div>
+          支持填写带变量的动态模板。变量填写规范示例:${name}
+          。填写动态参数后,可对变量的名称、类型、格式进行配置,以便告警通知时填写。
+        </div>
+      </div>
     </div>
     </div>
   );
   );
 };
 };

+ 1 - 0
src/pages/notice/Template/Detail/index.tsx

@@ -247,6 +247,7 @@ const Detail = observer(() => {
                 format.setValue('string');
                 format.setValue('string');
                 break;
                 break;
               case 'string':
               case 'string':
+                console.log('string');
                 format.setComponent(PreviewText.Input);
                 format.setComponent(PreviewText.Input);
                 format.setValue('%s');
                 format.setValue('%s');
                 break;
                 break;

+ 27 - 4
src/pages/notice/Template/Log/index.tsx

@@ -1,4 +1,4 @@
-import { Modal } from 'antd';
+import { Badge, Modal } from 'antd';
 import { observer } from '@formily/react';
 import { observer } from '@formily/react';
 import { service, state } from '..';
 import { service, state } from '..';
 import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
 import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
@@ -15,6 +15,7 @@ const Log = observer(() => {
     {
     {
       dataIndex: 'id',
       dataIndex: 'id',
       title: 'id',
       title: 'id',
+      width: 200,
     },
     },
     {
     {
       dataIndex: 'sendTime',
       dataIndex: 'sendTime',
@@ -23,6 +24,30 @@ const Log = observer(() => {
     {
     {
       dataIndex: 'state',
       dataIndex: 'state',
       title: '状态',
       title: '状态',
+      renderText: (text: { value: string; text: string }, record) => {
+        return (
+          <>
+            <Badge status={text.value === 'success' ? 'success' : 'error'} text={text.text} />
+            {text.value !== 'success' && (
+              <a
+                key="info"
+                onClick={() => {
+                  Modal.info({
+                    title: '错误信息',
+                    width: '30vw',
+                    content: (
+                      <div style={{ height: '300px', overflowY: 'auto' }}>{record.errorStack}</div>
+                    ),
+                    onOk() {},
+                  });
+                }}
+              >
+                <InfoCircleOutlined />
+              </a>
+            )}
+          </>
+        );
+      },
     },
     },
     {
     {
       dataIndex: 'action',
       dataIndex: 'action',
@@ -34,9 +59,7 @@ const Log = observer(() => {
             Modal.info({
             Modal.info({
               title: '详情信息',
               title: '详情信息',
               width: '30vw',
               width: '30vw',
-              content: (
-                <div style={{ height: '300px', overflowY: 'auto' }}>{record.errorStack}</div>
-              ),
+              content: <div style={{ height: '300px', overflowY: 'auto' }}>{record.message}</div>,
               onOk() {},
               onOk() {},
             });
             });
           }}
           }}

+ 1 - 0
src/pages/notice/Template/index.tsx

@@ -315,6 +315,7 @@ const Template = observer(() => {
                     actionRef.current?.reset?.();
                     actionRef.current?.reset?.();
                   },
                   },
                 }}
                 }}
+                isPermission={templatePermission.delete}
                 key="delete"
                 key="delete"
               >
               >
                 <DeleteOutlined />
                 <DeleteOutlined />

+ 1 - 0
src/pages/notice/Template/typings.d.ts

@@ -16,4 +16,5 @@ type LogItem = {
   sendTime: number;
   sendTime: number;
   state: string;
   state: string;
   errorStack?: string;
   errorStack?: string;
+  message?: string;
 };
 };

+ 32 - 1
src/pages/rule-engine/Alarm/Log/model.ts

@@ -1,7 +1,38 @@
 import { model } from '@formily/reactive';
 import { model } from '@formily/reactive';
+import type { ProColumns } from '@jetlinks/pro-table';
 
 
 export const AlarmLogModel = model<{
 export const AlarmLogModel = model<{
-  tab: 'product' | 'device' | 'department' | 'other';
+  tab: string;
+  current: Partial<AlarmLogItem>;
+  solveVisible: boolean;
+  logVisible: boolean;
+  defaultLevel: {
+    level: number;
+    title: string;
+  }[];
+  columns: ProColumns<AlarmLogHistoryItem>[];
 }>({
 }>({
   tab: 'product',
   tab: 'product',
+  current: {},
+  solveVisible: false,
+  logVisible: false,
+  defaultLevel: [],
+  columns: [
+    {
+      dataIndex: 'alarmTime',
+      title: '告警时间',
+    },
+    {
+      dataIndex: 'alarmName',
+      title: '告警名称',
+    },
+    {
+      dataIndex: 'description',
+      title: '说明',
+    },
+    {
+      dataIndex: 'action',
+      title: '操作',
+    },
+  ],
 });
 });

+ 251 - 0
src/pages/system/Relationship/Save/index.tsx

@@ -0,0 +1,251 @@
+import { useIntl } from 'umi';
+import type { Field } from '@formily/core';
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import React from 'react';
+import * as ICONS from '@ant-design/icons';
+import { Form, FormGrid, FormItem, Input, Select } from '@formily/antd';
+import type { ISchema } from '@formily/json-schema';
+import { action } from '@formily/reactive';
+import type { Response } from '@/utils/typings';
+import { service } from '@/pages/system/Relationship';
+import { Modal } from '@/components';
+import { message } from 'antd';
+
+interface Props {
+  data: Partial<ReationItem>;
+  close: () => void;
+}
+
+const Save = (props: Props) => {
+  const intl = useIntl();
+
+  const getTypes = () => service.getTypes();
+
+  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: JSON.stringify({
+            objectType: item.id,
+            objectTypeName: item.name,
+          }),
+        }));
+        field.loading = false;
+      }),
+    );
+  };
+
+  const form = createForm({
+    validateFirst: true,
+    initialValues: props.data,
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Select,
+      FormGrid,
+    },
+    scope: {
+      icon(name: any) {
+        return React.createElement(ICONS[name]);
+      },
+    },
+  });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      layout: {
+        type: 'void',
+        'x-decorator': 'FormGrid',
+        'x-decorator-props': {
+          maxColumns: 2,
+          minColumns: 2,
+          columnGap: 24,
+        },
+        properties: {
+          name: {
+            title: '名称',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入名称',
+            },
+            name: 'name',
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入名称',
+              },
+            ],
+          },
+          relation: {
+            title: '标识',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            type: 'string',
+            'x-disabled': !!props.data?.id,
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-component-props': {
+              placeholder: '请输入标识',
+            },
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入标识',
+              },
+              // {
+              //   triggerType: 'onBlur',
+              //   // validator: (value: string) => {
+              //   //   return new Promise((resolve) => {
+              //   //     service
+              //   //       .validateField('username', value)
+              //   //       .then((resp) => {
+              //   //         if (resp.status === 200) {
+              //   //           if (resp.result.passed) {
+              //   //             resolve('');
+              //   //           } else {
+              //   //             resolve(model === 'edit' ? '' : resp.result.reason);
+              //   //           }
+              //   //         }
+              //   //         resolve('');
+              //   //       })
+              //   //       .catch(() => {
+              //   //         return '验证失败!';
+              //   //       });
+              //   //   });
+              //   // },
+              // },
+            ],
+            name: 'relation',
+            required: true,
+          },
+          object: {
+            title: '关联方',
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-disabled': !!props.data?.id,
+            'x-component': 'Select',
+            'x-component-props': {
+              placeholder: '请选择关联方',
+              showArrow: true,
+              filterOption: (input: string, option: any) =>
+                option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+            },
+            required: true,
+            'x-reactions': ['{{useAsyncDataSource(getTypes)}}'],
+          },
+          target: {
+            title: '被关联方',
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-disabled': !!props.data?.id,
+            'x-component': 'Select',
+            'x-component-props': {
+              placeholder: '请选择被关联方',
+            },
+            'x-reactions': {
+              dependencies: ['..object'],
+              fulfill: {
+                state: {
+                  dataSource:
+                    '{{JSON.parse($deps[0] || "{}").objectType==="device"?[{label: "用户", value: JSON.stringify({"targetType":"user", "targetTypeName": "用户"})}] : []}}',
+                },
+              },
+            },
+            required: true,
+          },
+          description: {
+            title: '说明',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input.TextArea',
+            'x-component-props': {
+              rows: 5,
+              placeholder: '请输入说明',
+            },
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-validator': [
+              {
+                max: 200,
+                message: '最多可输入200个字符',
+              },
+            ],
+          },
+        },
+      },
+    },
+  };
+
+  const save = async () => {
+    const value = await form.submit<any>();
+    const temp: any = {
+      ...props.data,
+      ...value,
+      ...JSON.parse(value?.object || '{}'),
+      ...JSON.parse(value?.target || '{}'),
+    };
+    delete temp.object;
+    delete temp.target;
+    const response: any = await service[!props.data?.id ? 'save' : 'update']({ ...temp });
+    if (response.status === 200) {
+      message.success(
+        intl.formatMessage({
+          id: 'pages.data.option.success',
+          defaultMessage: '操作成功',
+        }),
+      );
+      props.close();
+    } else {
+      message.error('操作失败!');
+    }
+  };
+
+  return (
+    <Modal
+      title={intl.formatMessage({
+        id: `pages.data.option.${props.data.id ? 'edit' : 'add'}`,
+        defaultMessage: '编辑',
+      })}
+      maskClosable={false}
+      visible
+      onCancel={props.close}
+      onOk={save}
+      width="35vw"
+      permissionCode={'system/Relationship'}
+      permission={['add', 'edit']}
+    >
+      <Form form={form} layout="vertical">
+        <SchemaField schema={schema} scope={{ useAsyncDataSource, getTypes }} />
+      </Form>
+    </Modal>
+  );
+};
+export default Save;

+ 146 - 0
src/pages/system/Relationship/index.tsx

@@ -0,0 +1,146 @@
+import SearchComponent from '@/components/SearchComponent';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
+import { useRef, useState } from 'react';
+import Service from '@/pages/system/Relationship/service';
+import { PageContainer } from '@ant-design/pro-layout';
+import { PermissionButton } from '@/components';
+import { useIntl } from 'umi';
+import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
+import { message } from 'antd';
+import Save from './Save';
+
+export const service = new Service('relation');
+
+const Relationship = () => {
+  const intl = useIntl();
+  const [param, setParam] = useState<any>({});
+  const [current, setCurrent] = useState<Partial<ReationItem>>({});
+  const [visible, setVisible] = useState<boolean>(false);
+  const actionRef = useRef<ActionType>();
+  const { permission } = PermissionButton.usePermission('system/Relationship');
+
+  const columns: ProColumns<ReationItem>[] = [
+    {
+      dataIndex: 'name',
+      title: '名称',
+      ellipsis: true,
+    },
+    {
+      dataIndex: 'objectTypeName',
+      title: '关联方',
+      ellipsis: true,
+    },
+    {
+      dataIndex: 'targetTypeName',
+      title: '被关联方',
+      ellipsis: true,
+    },
+    {
+      dataIndex: 'description',
+      title: '说明',
+      ellipsis: true,
+    },
+    {
+      title: '操作',
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [
+        <PermissionButton
+          isPermission={permission.update}
+          key="warning"
+          onClick={() => {
+            setVisible(true);
+            setCurrent(record);
+          }}
+          type={'link'}
+          style={{ padding: 0 }}
+          tooltip={{
+            title: intl.formatMessage({
+              id: 'pages.data.option.edit',
+              defaultMessage: '编辑',
+            }),
+          }}
+        >
+          <EditOutlined />
+        </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>,
+      ],
+    },
+  ];
+
+  return (
+    <PageContainer>
+      <SearchComponent<ReationItem>
+        field={columns}
+        target="relationship"
+        onSearch={(data) => {
+          actionRef.current?.reload();
+          setParam(data);
+        }}
+      />
+      <ProTable<ReationItem>
+        actionRef={actionRef}
+        params={param}
+        columns={columns}
+        search={false}
+        rowKey="id"
+        request={async (params) => {
+          return service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] });
+        }}
+        headerTitle={[
+          <PermissionButton
+            isPermission={permission.add}
+            key="add"
+            onClick={() => {
+              setVisible(true);
+              setCurrent({});
+            }}
+            type="primary"
+            tooltip={{
+              title: intl.formatMessage({
+                id: 'pages.data.option.add',
+                defaultMessage: '新增',
+              }),
+            }}
+          >
+            新增
+          </PermissionButton>,
+        ]}
+      />
+      {visible && (
+        <Save
+          data={current}
+          close={() => {
+            setVisible(false);
+            actionRef.current?.reload();
+          }}
+        />
+      )}
+    </PageContainer>
+  );
+};
+export default Relationship;

+ 12 - 0
src/pages/system/Relationship/service.ts

@@ -0,0 +1,12 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<ReationItem> {
+  getTypes = () =>
+    request(`/${SystemConst.API_BASE}/relation/types`, {
+      method: 'GET',
+    });
+}
+
+export default Service;

+ 12 - 0
src/pages/system/Relationship/typings.d.ts

@@ -0,0 +1,12 @@
+type ReationItem = {
+  id: string;
+  name: string;
+  objectType: string;
+  objectTypeName: string;
+  relation: string;
+  targetType: string;
+  targetTypeName: string;
+  createTime: number;
+  description?: string;
+  expands?: Record<string, any>;
+};

+ 2 - 3
src/pages/system/User/ResetPassword/index.tsx

@@ -108,7 +108,7 @@ const ResetPassword = (props: Props) => {
     },
     },
   };
   };
 
 
-  const form = useMemo(() => createForm({}), []);
+  const form = useMemo(() => createForm({}), [props.visible]);
   return (
   return (
     <Modal
     <Modal
       title="重置密码"
       title="重置密码"
@@ -122,9 +122,8 @@ const ResetPassword = (props: Props) => {
             message.success('操作成功');
             message.success('操作成功');
             props.close();
             props.close();
           }
           }
-        } else {
-          props.close();
         }
         }
+        props.close();
       }}
       }}
     >
     >
       <Form form={form} layout="vertical">
       <Form form={form} layout="vertical">

+ 73 - 74
yarn.lock

@@ -2778,86 +2778,85 @@
   resolved "https://registry.yarnpkg.com/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz#2dc8c57044de0340eb53a7ba602e59abf80dc799"
   resolved "https://registry.yarnpkg.com/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz#2dc8c57044de0340eb53a7ba602e59abf80dc799"
   integrity sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==
   integrity sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==
 
 
-"@formily/antd@2.0.0-rc.17":
-  version "2.0.0-rc.17"
-  resolved "https://registry.yarnpkg.com/@formily/antd/-/antd-2.0.0-rc.17.tgz#fe41ded1c387a2018c292cd37c17e13350ee1dfe"
-  integrity sha512-FLUnq56b43+va3NiH2bhLPSO2BhmAMMxRWvdMX2qfV7s6SG4TQ35hEQkxDPu3YUUKKDv/75kFdgAJIaIZwHcNA==
-  dependencies:
-    "@ant-design/icons" "^4.0.0"
-    "@formily/core" "2.0.0-rc.17"
-    "@formily/grid" "2.0.0-rc.17"
-    "@formily/json-schema" "2.0.0-rc.17"
-    "@formily/react" "2.0.0-rc.17"
-    "@formily/reactive" "2.0.0-rc.17"
-    "@formily/reactive-react" "2.0.0-rc.17"
-    "@formily/shared" "2.0.0-rc.17"
-    "@juggle/resize-observer" "^3.3.1"
+"@formily/antd@2.0.19":
+  version "2.0.19"
+  resolved "https://registry.yarnpkg.com/@formily/antd/-/antd-2.0.19.tgz#7419807965d5d1f39324b46e0be4f6aae04ca267"
+  integrity sha512-pxybyq2zWS4Ki56oY7227yjonVN7mnFiaIXSy/NVRD5wXxUBzOvrFA+4LiJuFGv0vzUkmSBbFCBkcDb/8TRZXQ==
+  dependencies:
+    "@formily/core" "2.0.19"
+    "@formily/grid" "2.0.19"
+    "@formily/json-schema" "2.0.19"
+    "@formily/react" "2.0.19"
+    "@formily/reactive" "2.0.19"
+    "@formily/reactive-react" "2.0.19"
+    "@formily/shared" "2.0.19"
     classnames "^2.2.6"
     classnames "^2.2.6"
     react-sortable-hoc "^1.11.0"
     react-sortable-hoc "^1.11.0"
     react-sticky-box "^0.9.3"
     react-sticky-box "^0.9.3"
 
 
-"@formily/core@2.0.0-rc.17":
-  version "2.0.0-rc.17"
-  resolved "https://registry.yarnpkg.com/@formily/core/-/core-2.0.0-rc.17.tgz#06880aa6be6f6f822050998662654748bce1627b"
-  integrity sha512-O+iahZipqv1iwqQW9KDLTSo0USVwFrwjUs3v6ToHWgaJHbOFY3rHtUjUBB00QN4cQsc7tANErx8+MawoV/fH8Q==
-  dependencies:
-    "@formily/reactive" "2.0.0-rc.17"
-    "@formily/shared" "2.0.0-rc.17"
-    "@formily/validator" "2.0.0-rc.17"
-
-"@formily/grid@2.0.0-rc.17":
-  version "2.0.0-rc.17"
-  resolved "https://registry.yarnpkg.com/@formily/grid/-/grid-2.0.0-rc.17.tgz#46603919f435cbf71b65f039a4f7a4c98db8cf77"
-  integrity sha512-1boauZbcyKFC+0Pom3xeOzbH5DvCBXs2u9xHklDoaQrxZZLPggtZLlo7Qxyx7zXfyKQRn910WkbzlWFIt2VaDg==
-  dependencies:
-    "@formily/reactive" "2.0.0-rc.17"
-
-"@formily/json-schema@2.0.0-rc.17":
-  version "2.0.0-rc.17"
-  resolved "https://registry.yarnpkg.com/@formily/json-schema/-/json-schema-2.0.0-rc.17.tgz#5638ad56c7c44da89ae33b60b8b1f78f548ed9e4"
-  integrity sha512-x7kqGGdXXS40a3xf7LvbjPvMsCEhUZCshoEfHbQaxKOq4Y+mtUCpFYwJMBr3xsJG6+Yid3IDqflL0yBHn9/SDA==
-  dependencies:
-    "@formily/core" "2.0.0-rc.17"
-    "@formily/reactive" "2.0.0-rc.17"
-    "@formily/shared" "2.0.0-rc.17"
-
-"@formily/path@2.0.0-rc.17":
-  version "2.0.0-rc.17"
-  resolved "https://registry.yarnpkg.com/@formily/path/-/path-2.0.0-rc.17.tgz#40ea317fc8f46fa9908c4407720d8bcfd7e5fae6"
-  integrity sha512-BOFI38udFlYC/q9DYHehwu9FfKOdW1KgIjXp0t/wFlwfiVGQ+B/KyKVSkFPzEocdK5Q3fkujs8kyGLoyJLfSHQ==
-
-"@formily/react@2.0.0-rc.17":
-  version "2.0.0-rc.17"
-  resolved "https://registry.yarnpkg.com/@formily/react/-/react-2.0.0-rc.17.tgz#a88d9fa4b30d08a5ac19fda7ae09841fab64d493"
-  integrity sha512-PLBZYzKHNAb8PSGJrIFBEHA8kB3+j3WN0ls6weo89RXqbcnmkLCmjs6Xa7Cx0KYFLSLkmUZySmrI+Y51w0ASJA==
-  dependencies:
-    "@formily/core" "2.0.0-rc.17"
-    "@formily/json-schema" "2.0.0-rc.17"
-    "@formily/reactive" "2.0.0-rc.17"
-    "@formily/reactive-react" "2.0.0-rc.17"
-    "@formily/shared" "2.0.0-rc.17"
-    "@formily/validator" "2.0.0-rc.17"
+"@formily/core@2.0.19":
+  version "2.0.19"
+  resolved "https://registry.yarnpkg.com/@formily/core/-/core-2.0.19.tgz#359bef69964b623d8468934e4cc396de4eb03173"
+  integrity sha512-VsqWJKc2jhjzPgu4SKN5EVJeRrEwu+mAvsSo5bdDeKDTQ3b9+L9TTpUF8Q4t9NvZshK+gMAfvdCYNnb5hUqSnw==
+  dependencies:
+    "@formily/reactive" "2.0.19"
+    "@formily/shared" "2.0.19"
+    "@formily/validator" "2.0.19"
+
+"@formily/grid@2.0.19":
+  version "2.0.19"
+  resolved "https://registry.yarnpkg.com/@formily/grid/-/grid-2.0.19.tgz#b0b3aa45f027fd23c5918f1490597aeea7b2b90d"
+  integrity sha512-x2s1EVAkiGx6rdFr333gsNJjpwS9yLHIECvvStqWcTfBHlszrFxtPyAa1rYf0RCvjBMWq0EE6p2o6VIVqKVOtw==
+  dependencies:
+    "@formily/reactive" "2.0.19"
+    "@juggle/resize-observer" "^3.3.1"
+
+"@formily/json-schema@2.0.19":
+  version "2.0.19"
+  resolved "https://registry.yarnpkg.com/@formily/json-schema/-/json-schema-2.0.19.tgz#e14167060a07abd54759bb5ee17c6679156b866f"
+  integrity sha512-BTcEZwcGM/up6VKEVZ4wulD4hI5fYBb8n5SgRnaezSJbHECK23p8Yh13Qj4h1GFbQbnCWr6FVYFvqBSAc8tyOQ==
+  dependencies:
+    "@formily/core" "2.0.19"
+    "@formily/reactive" "2.0.19"
+    "@formily/shared" "2.0.19"
+
+"@formily/path@2.0.19":
+  version "2.0.19"
+  resolved "https://registry.yarnpkg.com/@formily/path/-/path-2.0.19.tgz#391abd170fd68048a4f59568b7b150c68fd36785"
+  integrity sha512-uiNyq0Vrls7ie8/odP7ZVybNBOFgwJVQ68XXIzq4ZPrki0uSyoVAn5CrCkNP94PdqOjN8/gjP4sQo6eSXvPnvQ==
+
+"@formily/react@2.0.19":
+  version "2.0.19"
+  resolved "https://registry.yarnpkg.com/@formily/react/-/react-2.0.19.tgz#44d0afeb4eff2b62f555664aa620cb9ecff1bec2"
+  integrity sha512-R6FE/pX1u06nORiWX7hNgb8idMcZdd+ozvZu1iupgDqAespWz6axl24OOKWH56+JU/uXDRXG8dvGKds5rjctvQ==
+  dependencies:
+    "@formily/core" "2.0.19"
+    "@formily/json-schema" "2.0.19"
+    "@formily/reactive" "2.0.19"
+    "@formily/reactive-react" "2.0.19"
+    "@formily/shared" "2.0.19"
+    "@formily/validator" "2.0.19"
     hoist-non-react-statics "^3.3.2"
     hoist-non-react-statics "^3.3.2"
 
 
-"@formily/reactive-react@2.0.0-rc.17":
-  version "2.0.0-rc.17"
-  resolved "https://registry.yarnpkg.com/@formily/reactive-react/-/reactive-react-2.0.0-rc.17.tgz#fa7d86c83170f183c8180bc6a798797609e8839b"
-  integrity sha512-7rHZ1Az0cpqjLccmrwASJ68b6QxPzJ2mpTLYKf5jbmIINPB5mG0zziFPJLymY15ljAQ6jIyX15viOkDBSkedJA==
+"@formily/reactive-react@2.0.19":
+  version "2.0.19"
+  resolved "https://registry.yarnpkg.com/@formily/reactive-react/-/reactive-react-2.0.19.tgz#0526cc22346d62c1809eefbdbac988a1845e581e"
+  integrity sha512-Laz3O/oSCIA4qKQ4fIMsyUQjS4XtD00nUvXSXIZhGdTkZW09Spq8zv7wd+0V6REEKIH6urTtC8htpBQN8W3fww==
   dependencies:
   dependencies:
-    "@formily/reactive" "2.0.0-rc.17"
+    "@formily/reactive" "2.0.19"
     hoist-non-react-statics "^3.3.2"
     hoist-non-react-statics "^3.3.2"
 
 
-"@formily/reactive@2.0.0-rc.17":
-  version "2.0.0-rc.17"
-  resolved "https://registry.yarnpkg.com/@formily/reactive/-/reactive-2.0.0-rc.17.tgz#fcf752d2c6c14459580d08305efc0d56d9741278"
-  integrity sha512-xFLOFnd+O5t1TRmunlFJHpTTKObSjh7rxJW7IvO42OkrV1o2dUJ7TdDcsaZIHsHg9H/3tMFHzAtfGcprpIcYAA==
+"@formily/reactive@2.0.19":
+  version "2.0.19"
+  resolved "https://registry.yarnpkg.com/@formily/reactive/-/reactive-2.0.19.tgz#4498b4e70c466bfee9b9dda8639ffe6f10d5d7ca"
+  integrity sha512-gEpiEITdrRHGc+cf/0lalw4gTcES+8axdAxC0mZRMHfJ8iSZnFs369AGxiWdElUK9NNVLfEmSuU60op6XCQhrg==
 
 
-"@formily/shared@2.0.0-rc.17":
-  version "2.0.0-rc.17"
-  resolved "https://registry.yarnpkg.com/@formily/shared/-/shared-2.0.0-rc.17.tgz#c6e88df5652f376783130be908e428d3533093b5"
-  integrity sha512-+09L5mOP0MwOk5AOOiphNlZ1PPNYxPI/7pMthulyF3BIDSAJF9Odx3IGvGz+YthzD0fYpi3QX7ly2OuwAA3FhA==
+"@formily/shared@2.0.19":
+  version "2.0.19"
+  resolved "https://registry.yarnpkg.com/@formily/shared/-/shared-2.0.19.tgz#602ce0738fe39fb0773accc3345329ba3b0cbbac"
+  integrity sha512-1zKNZLKoEEH31Y9+rBXdByHVsUModWyshkPj7fsZv0KkaObn/wV2WUCKLQW4c4Hn1y+yojPH//8SD2oOZ4wZXw==
   dependencies:
   dependencies:
-    "@formily/path" "2.0.0-rc.17"
+    "@formily/path" "2.0.19"
     camel-case "^4.1.1"
     camel-case "^4.1.1"
     lower-case "^2.0.1"
     lower-case "^2.0.1"
     no-case "^3.0.4"
     no-case "^3.0.4"
@@ -2865,12 +2864,12 @@
     pascal-case "^3.1.1"
     pascal-case "^3.1.1"
     upper-case "^2.0.1"
     upper-case "^2.0.1"
 
 
-"@formily/validator@2.0.0-rc.17":
-  version "2.0.0-rc.17"
-  resolved "https://registry.yarnpkg.com/@formily/validator/-/validator-2.0.0-rc.17.tgz#e11bf27a5f5b14bed92dcdc8e5dbf2430965018d"
-  integrity sha512-srjQrfY8ubKaFjldb75lcHhBVgXKNY6Q1R6BvFr2Xogslbkriv2ct752Bix0YC+cFZ4elFwWyiOknSaupnzZRg==
+"@formily/validator@2.0.19":
+  version "2.0.19"
+  resolved "https://registry.yarnpkg.com/@formily/validator/-/validator-2.0.19.tgz#4d14191b6ab92b0298a59b42964cab008ef7f551"
+  integrity sha512-KS9g0WXKR77ET+3blKGxDL2w4e8gp0z5kkd5BDm7bIUmfNb67rTuSaacs+8MbOuckt09B7qU1nzOekXkskaRNw==
   dependencies:
   dependencies:
-    "@formily/shared" "2.0.0-rc.17"
+    "@formily/shared" "2.0.19"
 
 
 "@hapi/address@^2.1.2":
 "@hapi/address@^2.1.2":
   version "2.1.4"
   version "2.1.4"