xieyonghong 3 лет назад
Родитель
Сommit
c9c5ead310
28 измененных файлов с 777 добавлено и 423 удалено
  1. 1 9
      src/pages/device/Instance/Detail/ChildDevice/BindChildDevice/index.tsx
  2. 1 8
      src/pages/device/Instance/Detail/ChildDevice/index.tsx
  3. 45 76
      src/pages/device/Instance/Detail/Config/Tags/index.tsx
  4. 181 119
      src/pages/device/Instance/Detail/Config/index.tsx
  5. 108 21
      src/pages/device/Instance/Detail/Info/index.tsx
  6. 1 1
      src/pages/device/Instance/Detail/Running/index.tsx
  7. 18 5
      src/pages/device/Instance/Detail/index.tsx
  8. 12 27
      src/pages/device/Instance/Export/index.tsx
  9. 11 21
      src/pages/device/Instance/Import/index.tsx
  10. 18 16
      src/pages/device/Instance/Save/index.tsx
  11. 18 23
      src/pages/device/Instance/index.tsx
  12. 10 0
      src/pages/device/Instance/service.ts
  13. 60 26
      src/pages/device/Product/Detail/PropertyImport/index.tsx
  14. 19 7
      src/pages/device/Product/Detail/index.tsx
  15. 14 10
      src/pages/device/Product/Save/index.tsx
  16. 4 4
      src/pages/device/Product/index.tsx
  17. 65 25
      src/pages/device/components/Metadata/Base/Edit/index.tsx
  18. 37 7
      src/pages/device/components/Metadata/Base/index.tsx
  19. 4 0
      src/pages/device/components/Metadata/Import/index.tsx
  20. 86 0
      src/pages/device/components/Metadata/metadata.ts
  21. 19 3
      src/pages/system/User/Save/index.tsx
  22. 6 0
      src/pages/system/User/serivce.ts
  23. 4 0
      src/utils/const.ts
  24. 30 0
      src/utils/util.ts
  25. 0 2
      tests/beforeTest.js
  26. 2 7
      tests/getBrowser.js
  27. 0 3
      tests/run-tests.js
  28. 3 3
      yarn.lock

+ 1 - 9
src/pages/device/Instance/Detail/ChildDevice/BindChildDevice/index.tsx

@@ -4,7 +4,7 @@ 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 { InstanceModel, service } from '@/pages/device/Instance';
+import { InstanceModel, service, statusMap } from '@/pages/device/Instance';
 import { useIntl } from 'umi';
 import moment from 'moment';
 
@@ -17,14 +17,6 @@ interface Props {
 const BindChildDevice = (props: Props) => {
   const { visible } = props;
   const intl = useIntl();
-  const statusMap = new Map();
-
-  statusMap.set('在线', 'success');
-  statusMap.set('离线', 'error');
-  statusMap.set('未激活', 'processing');
-  statusMap.set('online', 'success');
-  statusMap.set('offline', 'error');
-  statusMap.set('notActive', 'processing');
 
   const actionRef = useRef<ActionType>();
   const [searchParams, setSearchParams] = useState<any>({});

+ 1 - 8
src/pages/device/Instance/Detail/ChildDevice/index.tsx

@@ -4,7 +4,7 @@ import type { LogItem } from '@/pages/device/Instance/Detail/Log/typings';
 import { Badge, Button, Card, message, Popconfirm, Tooltip } from 'antd';
 import { DisconnectOutlined, SearchOutlined } from '@ant-design/icons';
 import { useIntl } from '@@/plugin-locale/localeExports';
-import { InstanceModel, service } from '@/pages/device/Instance';
+import { InstanceModel, service, statusMap } from '@/pages/device/Instance';
 import { useRef, useState } from 'react';
 import SearchComponent from '@/components/SearchComponent';
 import BindChildDevice from './BindChildDevice';
@@ -14,13 +14,6 @@ import { Link } from 'umi';
 const ChildDevice = () => {
   const intl = useIntl();
   const [visible, setVisible] = useState<boolean>(false);
-  const statusMap = new Map();
-  statusMap.set('在线', 'success');
-  statusMap.set('离线', 'error');
-  statusMap.set('未激活', 'processing');
-  statusMap.set('online', 'success');
-  statusMap.set('offline', 'error');
-  statusMap.set('notActive', 'processing');
 
   const actionRef = useRef<ActionType>();
   const [searchParams, setSearchParams] = useState<any>({});

+ 45 - 76
src/pages/device/Instance/Detail/Config/Tags/index.tsx

@@ -1,8 +1,10 @@
-import { createSchemaField, FormProvider, observer } from '@formily/react';
+import { createSchemaField, FormProvider } from '@formily/react';
 import { Editable, FormItem, Input, ArrayTable } from '@formily/antd';
 import { createForm } from '@formily/core';
-import { Card } from 'antd';
+import { Card, message } from 'antd';
 import { useIntl } from '@@/plugin-locale/localeExports';
+import { InstanceModel, service } from '@/pages/device/Instance';
+import { useEffect, useState } from 'react';
 
 const SchemaField = createSchemaField({
   components: {
@@ -12,14 +14,29 @@ const SchemaField = createSchemaField({
     ArrayTable,
   },
 });
-const form = createForm();
 
-const Tags = observer(() => {
+const Tags = () => {
   const intl = useIntl();
+  const [tags, setTags] = useState<any[]>([]);
+
+  const tag = InstanceModel.detail?.tags;
+
+  useEffect(() => {
+    if (tag) {
+      setTags([...tag] || []);
+    }
+  }, [tag]);
+
+  const form = createForm({
+    initialValues: {
+      tags: tags,
+    },
+  });
+
   const schema = {
     type: 'object',
     properties: {
-      array: {
+      tags: {
         type: 'array',
         'x-decorator': 'FormItem',
         'x-component': 'ArrayTable',
@@ -33,34 +50,17 @@ const Tags = observer(() => {
             column1: {
               type: 'void',
               'x-component': 'ArrayTable.Column',
-              'x-component-props': {
-                width: 50,
-                title: intl.formatMessage({
-                  id: 'pages.device.instanceDetail.detail.sort',
-                  defaultMessage: '排序',
-                }),
-                align: 'center',
-              },
-              properties: {
-                sort: {
-                  type: 'void',
-                  'x-component': 'ArrayTable.SortHandle',
-                },
-              },
-            },
-            column3: {
-              type: 'void',
-              'x-component': 'ArrayTable.Column',
               'x-component-props': { width: 200, title: 'ID' },
               properties: {
-                a1: {
+                key: {
                   type: 'string',
-                  'x-decorator': 'Editable',
+                  'x-decorator': 'FormItem',
                   'x-component': 'Input',
+                  'x-disabled': true,
                 },
               },
             },
-            column4: {
+            column2: {
               type: 'void',
               'x-component': 'ArrayTable.Column',
               'x-component-props': {
@@ -71,14 +71,14 @@ const Tags = observer(() => {
                 }),
               },
               properties: {
-                a2: {
+                name: {
                   type: 'string',
-                  'x-decorator': 'FormItem',
+                  'x-decorator': 'Editable',
                   'x-component': 'Input',
                 },
               },
             },
-            column5: {
+            column3: {
               type: 'void',
               'x-component': 'ArrayTable.Column',
               'x-component-props': {
@@ -89,56 +89,13 @@ const Tags = observer(() => {
                 }),
               },
               properties: {
-                a3: {
+                value: {
                   type: 'string',
-                  'x-decorator': 'FormItem',
+                  'x-decorator': 'Editable',
                   'x-component': 'Input',
                 },
               },
             },
-            column6: {
-              type: 'void',
-              'x-component': 'ArrayTable.Column',
-              'x-component-props': {
-                title: intl.formatMessage({
-                  id: 'pages.data.option',
-                  defaultMessage: '操作',
-                }),
-                dataIndex: 'operations',
-                width: 200,
-                fixed: 'right',
-              },
-              properties: {
-                item: {
-                  type: 'void',
-                  'x-component': 'FormItem',
-                  properties: {
-                    remove: {
-                      type: 'void',
-                      'x-component': 'ArrayTable.Remove',
-                    },
-                    moveDown: {
-                      type: 'void',
-                      'x-component': 'ArrayTable.MoveDown',
-                    },
-                    moveUp: {
-                      type: 'void',
-                      'x-component': 'ArrayTable.MoveUp',
-                    },
-                  },
-                },
-              },
-            },
-          },
-        },
-        properties: {
-          add: {
-            type: 'void',
-            'x-component': 'ArrayTable.Addition',
-            title: intl.formatMessage({
-              id: 'pages.device.instanceDetail.detail.value',
-              defaultMessage: '添加标签',
-            }),
           },
         },
       },
@@ -151,7 +108,18 @@ const Tags = observer(() => {
         defaultMessage: '标签',
       })}
       extra={
-        <a>
+        <a
+          onClick={async () => {
+            const values = (await form.submit()) as any;
+            if (values?.tags) {
+              const resp = await service.saveTags(InstanceModel.detail?.id || '', values.tags);
+              if (resp.status === 200) {
+                InstanceModel.detail = { ...InstanceModel.detail, tags: values.tags };
+                message.success('操作成功!');
+              }
+            }
+          }}
+        >
           {intl.formatMessage({
             id: 'pages.device.instanceDetail.save',
             defaultMessage: '保存',
@@ -164,5 +132,6 @@ const Tags = observer(() => {
       </FormProvider>
     </Card>
   );
-});
+};
+
 export default Tags;

+ 181 - 119
src/pages/device/Instance/Detail/Config/index.tsx

@@ -1,127 +1,189 @@
-import { Descriptions, Card, Button } from 'antd';
+import { Card, Divider, Empty, message, Popconfirm, Space, Tooltip } from 'antd';
 import { InstanceModel, service } from '@/pages/device/Instance';
-import moment from 'moment';
-import { observer } from '@formily/react';
-import { useIntl } from '@@/plugin-locale/localeExports';
-import { EditOutlined } from '@ant-design/icons';
-import { useState } from 'react';
-import { useParams } from 'umi';
-import Save from '@/pages/device/Instance/Save';
+import { useEffect, useState } from 'react';
+import { createSchemaField } from '@formily/react';
+import type { ConfigMetadata, ConfigProperty } from '@/pages/device/Product/typings';
+import type { ISchema } from '@formily/json-schema';
+import { Form, FormGrid, FormItem, FormLayout, Input, Password, PreviewText } from '@formily/antd';
+import { createForm } from '@formily/core';
+import { history, useParams } from 'umi';
+import Tags from '@/pages/device/Instance/Detail/Config/Tags';
+import Icon from '@ant-design/icons';
 
-const Info = observer(() => {
-  const intl = useIntl();
-  const [visible, setVisible] = useState(false);
-  const param = useParams<{ id: string }>();
+const componentMap = {
+  string: 'Input',
+  password: 'Password',
+};
 
-  const getDetailInfo = async () => {
-    const res = await service.detail(param.id);
-    if (res.status === 200) {
-      InstanceModel.detail = res.result;
+const Config = () => {
+  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 [metadata, setMetadata] = useState<ConfigMetadata[]>([]);
+  const [state, setState] = useState<boolean>(false);
+
+  const form = createForm({
+    validateFirst: true,
+    readPretty: state,
+    initialValues: InstanceModel.detail?.configuration,
+  });
+
+  const id = InstanceModel.detail?.id;
+
+  useEffect(() => {
+    if (id) {
+      service.getConfigMetadata(id).then((config) => {
+        setMetadata(config?.result);
+      });
+      setState(
+        !!(
+          InstanceModel.detail?.configuration &&
+          Object.keys(InstanceModel.detail?.configuration).length > 0
+        ),
+      );
+    }
+
+    return () => {};
+  }, [id]);
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Password,
+      FormGrid,
+      PreviewText,
+    },
+  });
+
+  const configToSchema = (data: ConfigProperty[]) => {
+    const config = {};
+    data.forEach((item) => {
+      config[item.property] = {
+        type: 'string',
+        title: item.name,
+        'x-decorator': 'FormItem',
+        'x-component': componentMap[item.type.type],
+        'x-decorator-props': {
+          tooltip: item.description,
+        },
+      };
+    });
+    return config;
+  };
+
+  const renderConfigCard = () => {
+    return metadata ? (
+      metadata?.map((item) => {
+        const itemSchema: ISchema = {
+          type: 'object',
+          properties: {
+            grid: {
+              type: 'void',
+              'x-component': 'FormGrid',
+              'x-component-props': {
+                minColumns: [2],
+                maxColumns: [2],
+              },
+              properties: configToSchema(item.properties),
+            },
+          },
+        };
+
+        return (
+          <>
+            <Divider />
+            <Card
+              title={item.name}
+              extra={
+                <Space>
+                  <a
+                    onClick={async () => {
+                      const values = (await form.submit()) as any;
+                      const resp = await service.modify(id || '', {
+                        id,
+                        configuration: { ...values },
+                      });
+                      if (resp.status === 200) {
+                        InstanceModel.detail = {
+                          ...InstanceModel.detail,
+                          configuration: { ...values },
+                        };
+                        setState(!state);
+                      }
+                    }}
+                  >
+                    {state ? '编辑' : '保存'}
+                  </a>
+                  {InstanceModel.detail.state?.value !== 'notActive' && (
+                    <Popconfirm
+                      title="确认重新应用该配置?"
+                      onConfirm={async () => {
+                        const resp = await service.deployDevice(id || '');
+                        if (resp.status === 200) {
+                          message.success('操作成功');
+                        }
+                      }}
+                    >
+                      <a>应用配置</a>
+                      <Tooltip title="修改配置后需重新应用后才能生效。">
+                        <Icon type="question-circle-o" />
+                      </Tooltip>
+                    </Popconfirm>
+                  )}
+                  {InstanceModel.detail?.aloneConfiguration && (
+                    <Popconfirm
+                      title="确认恢复默认配置?"
+                      onConfirm={async () => {
+                        const resp = await service.configurationReset(id || '');
+                        if (resp.status === 200) {
+                          message.success('恢复默认配置成功');
+                        }
+                      }}
+                    >
+                      <a>恢复默认</a>
+                      <Tooltip
+                        title={`该设备单独编辑过配置信息,点击此将恢复成默认的配置信息,请谨慎操作。`}
+                      >
+                        <Icon type="question-circle-o" />
+                      </Tooltip>
+                    </Popconfirm>
+                  )}
+                </Space>
+              }
+            >
+              <PreviewText.Placeholder value="-">
+                <Form form={form}>
+                  <FormLayout labelCol={6} wrapperCol={16}>
+                    <SchemaField schema={itemSchema} />
+                  </FormLayout>
+                </Form>
+              </PreviewText.Placeholder>
+            </Card>
+          </>
+        );
+      })
+    ) : (
+      <Empty />
+    );
   };
 
   return (
-    <Card>
-      <Descriptions
-        size="small"
-        column={3}
-        bordered
-        title={[
-          <span key={1}>
-            {intl.formatMessage({
-              id: 'pages.device.instanceDetail.info',
-              defaultMessage: '设备信息',
-            })}
-          </span>,
-          <Button
-            key={2}
-            type={'link'}
-            onClick={() => {
-              setVisible(true);
-            }}
-          >
-            <EditOutlined />
-          </Button>,
-        ]}
-      >
-        <Descriptions.Item
-          label={intl.formatMessage({
-            id: 'pages.device.instanceDetail.deviceType',
-            defaultMessage: '设备类型',
-          })}
-        >
-          {InstanceModel.detail?.deviceType?.text}
-        </Descriptions.Item>
-        <Descriptions.Item
-          label={intl.formatMessage({
-            id: 'pages.device.instanceDetail.transportProtocol',
-            defaultMessage: '链接协议',
-          })}
-        >
-          {InstanceModel.detail?.protocolName}
-        </Descriptions.Item>
-        <Descriptions.Item
-          label={intl.formatMessage({
-            id: 'pages.device.instanceDetail.protocolName',
-            defaultMessage: '消息协议',
-          })}
-        >
-          {InstanceModel.detail?.transport}
-        </Descriptions.Item>
-        <Descriptions.Item
-          label={intl.formatMessage({
-            id: 'pages.device.instanceDetail.IPAddress',
-            defaultMessage: 'IP地址',
-          })}
-        >
-          {InstanceModel.detail?.address}
-        </Descriptions.Item>
-        <Descriptions.Item
-          label={intl.formatMessage({
-            id: 'pages.device.instanceDetail.createTime',
-            defaultMessage: '创建时间',
-          })}
-        >
-          {moment(InstanceModel.detail?.createTime).format('YYYY-MM-DD HH:mm:ss')}
-        </Descriptions.Item>
-        <Descriptions.Item
-          label={intl.formatMessage({
-            id: 'pages.device.instanceDetail.registerTime',
-            defaultMessage: '注册时间',
-          })}
-        >
-          {InstanceModel.detail?.createTime}
-        </Descriptions.Item>
-        <Descriptions.Item
-          label={intl.formatMessage({
-            id: 'pages.device.instanceDetail.lastTimeOnline',
-            defaultMessage: '最后上线时间',
-          })}
-        >
-          {InstanceModel.detail?.createTime}
-        </Descriptions.Item>
-        <Descriptions.Item label={''}>{}</Descriptions.Item>
-        <Descriptions.Item label={''}>{}</Descriptions.Item>
-        <Descriptions.Item
-          label={intl.formatMessage({
-            id: 'pages.table.description',
-            defaultMessage: '说明',
-          })}
-          span={3}
-        >
-          {InstanceModel.detail?.description}
-        </Descriptions.Item>
-      </Descriptions>
-      <Save
-        data={InstanceModel.detail}
-        model={'edit'}
-        close={() => {
-          setVisible(false);
-        }}
-        reload={getDetailInfo}
-        visible={visible}
-      />
-    </Card>
+    <>
+      {renderConfigCard()}
+      <Divider />
+      <Tags />
+    </>
   );
-});
-export default Info;
+};
+
+export default Config;

+ 108 - 21
src/pages/device/Instance/Detail/Info/index.tsx

@@ -1,37 +1,124 @@
-import { Descriptions } from 'antd';
+import { Card, Descriptions } from 'antd';
 import { InstanceModel } from '@/pages/device/Instance';
+import moment from 'moment';
 import { observer } from '@formily/react';
 import { useIntl } from '@@/plugin-locale/localeExports';
-import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
-import { useHistory } from 'umi';
+import Config from '@/pages/device/Instance/Detail/Config';
+import Save from '../../Save';
+import { useState } from 'react';
+import type { DeviceInstance } from '../../typings';
 
 const Info = observer(() => {
   const intl = useIntl();
-  const history = useHistory();
+  const [visible, setVisible] = useState<boolean>(false);
+
   return (
     <>
-      <Descriptions size="small" column={5}>
-        <Descriptions.Item label={'ID'}>{InstanceModel.detail?.id}</Descriptions.Item>
-        <Descriptions.Item
-          label={intl.formatMessage({
-            id: 'pages.device.firmware.productName',
-            defaultMessage: '链接协议',
-          })}
-        >
+      <Card
+        title={'设备信息'}
+        extra={
           <a
             onClick={() => {
-              history.push({
-                pathname: `${getMenuPathByCode(MENUS_CODE['device/Product'])}`,
-                state: {
-                  id: InstanceModel.detail?.productId,
-                },
-              });
+              setVisible(true);
             }}
           >
-            {InstanceModel.detail?.productName}
+            编辑
           </a>
-        </Descriptions.Item>
-      </Descriptions>
+        }
+      >
+        <Descriptions size="small" column={3} bordered>
+          <Descriptions.Item
+            label={intl.formatMessage({
+              id: 'pages.table.deviceId',
+              defaultMessage: '设备ID',
+            })}
+          >
+            {InstanceModel.detail?.id}
+          </Descriptions.Item>
+          <Descriptions.Item
+            label={intl.formatMessage({
+              id: 'pages.table.productName',
+              defaultMessage: '产品名称',
+            })}
+          >
+            {InstanceModel.detail?.name}
+          </Descriptions.Item>
+          <Descriptions.Item
+            label={intl.formatMessage({
+              id: 'pages.device.instanceDetail.deviceType',
+              defaultMessage: '设备类型',
+            })}
+          >
+            {InstanceModel.detail?.deviceType?.text}
+          </Descriptions.Item>
+          <Descriptions.Item
+            label={intl.formatMessage({
+              id: 'pages.device.instanceDetail.transportProtocol',
+              defaultMessage: '链接协议',
+            })}
+          >
+            {InstanceModel.detail?.protocolName}
+          </Descriptions.Item>
+          <Descriptions.Item
+            label={intl.formatMessage({
+              id: 'pages.device.instanceDetail.protocolName',
+              defaultMessage: '消息协议',
+            })}
+          >
+            {InstanceModel.detail?.transport}
+          </Descriptions.Item>
+          <Descriptions.Item
+            label={intl.formatMessage({
+              id: 'pages.device.instanceDetail.createTime',
+              defaultMessage: '创建时间',
+            })}
+          >
+            {moment(InstanceModel.detail?.createTime).format('YYYY-MM-DD HH:mm:ss')}
+          </Descriptions.Item>
+          <Descriptions.Item
+            label={intl.formatMessage({
+              id: 'pages.device.instanceDetail.registerTime',
+              defaultMessage: '注册时间',
+            })}
+          >
+            {moment(InstanceModel.detail?.registerTime).format('YYYY-MM-DD HH:mm:ss')}
+          </Descriptions.Item>
+          <Descriptions.Item
+            label={intl.formatMessage({
+              id: 'pages.device.instanceDetail.lastTimeOnline',
+              defaultMessage: '最后上线时间',
+            })}
+          >
+            {InstanceModel.detail?.onlineTime
+              ? moment(InstanceModel.detail?.onlineTime).format('YYYY-MM-DD HH:mm:ss')
+              : '--'}
+          </Descriptions.Item>
+          <Descriptions.Item
+            label={intl.formatMessage({
+              id: 'pages.table.description',
+              defaultMessage: '说明',
+            })}
+          >
+            {InstanceModel.detail?.description}
+          </Descriptions.Item>
+        </Descriptions>
+      </Card>
+      <Config />
+      <Save
+        model={'edit'}
+        data={{ ...InstanceModel?.detail, describe: InstanceModel?.detail?.description || '' }}
+        close={(data: DeviceInstance | undefined) => {
+          setVisible(false);
+          if (data) {
+            InstanceModel.detail = {
+              ...InstanceModel.detail,
+              name: data?.name,
+              description: data?.describe,
+            };
+          }
+        }}
+        visible={visible}
+      />
     </>
   );
 });

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

@@ -13,7 +13,7 @@ const Running = () => {
         <Tabs.TabPane tab="属性" key="1">
           <Property data={metadata?.properties || {}} />
         </Tabs.TabPane>
-        {metadata.events.map((item) => (
+        {metadata.events?.map((item) => (
           <Tabs.TabPane tab={item.name} key={item.id}>
             <Event data={item} />
           </Tabs.TabPane>

+ 18 - 5
src/pages/device/Instance/Detail/index.tsx

@@ -5,7 +5,6 @@ import { Badge, Button, Card, Divider, message, Tooltip } from 'antd';
 import { useEffect, useState } from 'react';
 import { statusMap } from '@/pages/device/Product';
 import { observer } from '@formily/react';
-import Config from '@/pages/device/Instance/Detail/Config';
 import Log from '@/pages/device/Instance/Detail/Log';
 import Alarm from '@/pages/device/components/Alarm';
 import Info from '@/pages/device/Instance/Detail/Info';
@@ -16,6 +15,8 @@ import { useIntl } from '@@/plugin-locale/localeExports';
 import Metadata from '../../components/Metadata';
 import type { DeviceMetadata } from '@/pages/device/Product/typings';
 import MetadataAction from '@/pages/device/components/Metadata/DataBaseAction';
+import { Store } from 'jetlinks-store';
+import SystemConst from '@/utils/const';
 
 export const deviceStatus = new Map();
 deviceStatus.set('online', <Badge status="success" text={'在线'} />);
@@ -34,11 +35,23 @@ const InstanceDetail = observer(() => {
   };
   const params = useParams<{ id: string }>();
 
+  useEffect(() => {
+    Store.subscribe(SystemConst.REFRESH_DEVICE, () => {
+      MetadataAction.clean();
+      setTimeout(() => {
+        getDetail(params.id);
+      }, 200);
+    });
+    // return subscription.unsubscribe();
+  }, []);
   const resetMetadata = async () => {
     const resp = await service.deleteMetadata(params.id);
     if (resp.status === 200) {
       message.success('操作成功');
-      getDetail(params.id);
+      Store.set(SystemConst.REFRESH_DEVICE, true);
+      setTimeout(() => {
+        Store.set(SystemConst.REFRESH_METADATA_TABLE, true);
+      }, 400);
     }
   };
   const list = [
@@ -46,9 +59,9 @@ const InstanceDetail = observer(() => {
       key: 'detail',
       tab: intl.formatMessage({
         id: 'pages.device.instanceDetail.detail',
-        defaultMessage: '配置信息',
+        defaultMessage: '实例信息',
       }),
-      component: <Config />,
+      component: <Info />,
     },
     {
       key: 'running',
@@ -138,7 +151,7 @@ const InstanceDetail = observer(() => {
       onBack={history.goBack}
       onTabChange={setTab}
       tabList={list}
-      content={<Info />}
+      // content={<Info />}
       title={
         <>
           {InstanceModel.detail.name}

+ 12 - 27
src/pages/device/Instance/Export/index.tsx

@@ -1,4 +1,4 @@
-import { FormItem, FormLayout, Select, Radio } from '@formily/antd';
+import { FormItem, FormLayout, Radio, Select } from '@formily/antd';
 import { createForm } from '@formily/core';
 import { createSchemaField, FormProvider } from '@formily/react';
 import { Alert, Modal } from 'antd';
@@ -7,8 +7,8 @@ import { useEffect, useState } from 'react';
 import { service } from '@/pages/device/Instance';
 import type { DeviceInstance } from '../typings';
 import SystemConst from '@/utils/const';
-import Token from '@/utils/token';
 import encodeQuery from '@/utils/encodeQuery';
+import { downloadFile } from '@/utils/util';
 
 interface Props {
   visible: boolean;
@@ -60,6 +60,9 @@ const Export = (props: Props) => {
             'x-decorator': 'FormItem',
             'x-component': 'Select',
             enum: [...productList],
+            'x-component-props': {
+              allowClear: true,
+            },
           },
           fileType: {
             title: '文件格式',
@@ -87,31 +90,15 @@ const Export = (props: Props) => {
   };
   const downloadTemplate = async () => {
     const values = (await form.submit()) as any;
-    const formElement = document.createElement('form');
-    formElement.style.display = 'display:none;';
-    formElement.method = 'GET';
+    const params = encodeQuery(props.data);
     if (values.product) {
-      formElement.action = `/${SystemConst.API_BASE}/device/instance/${values.product}/export.${values.fileType}`;
+      downloadFile(
+        `/${SystemConst.API_BASE}/device/instance/${values.product}/export.${values.fileType}`,
+        params,
+      );
     } else {
-      formElement.action = `/${SystemConst.API_BASE}/device/instance/export.${values.fileType}`;
+      downloadFile(`/${SystemConst.API_BASE}/device/instance/export.${values.fileType}`, params);
     }
-    const params = encodeQuery(props.data);
-    Object.keys(params).forEach((key: string) => {
-      const inputElement = document.createElement('input');
-      inputElement.type = 'hidden';
-      inputElement.name = key;
-      inputElement.value = params[key];
-      formElement.appendChild(inputElement);
-    });
-    const inputElement = document.createElement('input');
-    inputElement.type = 'hidden';
-    inputElement.name = ':X_Access_Token';
-    inputElement.value = Token.get();
-    formElement.appendChild(inputElement);
-
-    document.body.appendChild(formElement);
-    formElement.submit();
-    document.body.removeChild(formElement);
   };
   return (
     <Modal
@@ -119,9 +106,7 @@ const Export = (props: Props) => {
       onCancel={() => close()}
       width="35vw"
       title="导出"
-      onOk={() => {
-        downloadTemplate();
-      }}
+      onOk={downloadTemplate}
     >
       <Alert
         message="不勾选产品,默认导出所有设备的基础数据,勾选单个产品可导出下属的详细数据"

+ 11 - 21
src/pages/device/Instance/Import/index.tsx

@@ -11,11 +11,12 @@ import { UploadOutlined } from '@ant-design/icons';
 import SystemConst from '@/utils/const';
 import Token from '@/utils/token';
 import { EventSourcePolyfill } from 'event-source-polyfill';
+import { downloadFile } from '@/utils/util';
 
 interface Props {
   visible: boolean;
   close: () => void;
-  data?: DeviceInstance;
+  data: Partial<DeviceInstance>;
 }
 
 const FileFormat = (props: any) => {
@@ -60,28 +61,13 @@ const FileFormat = (props: any) => {
   );
 };
 
-const downloadTemplate = (type: string, product: string) => {
-  const formElement = document.createElement('form');
-  formElement.style.display = 'display:none;';
-  formElement.method = 'GET';
-  formElement.action = `/${SystemConst.API_BASE}/device-instance/${product}/template.${type}`;
-  const inputElement = document.createElement('input');
-  inputElement.type = 'hidden';
-  inputElement.name = ':X_Access_Token';
-  inputElement.value = Token.get();
-  formElement.appendChild(inputElement);
-  document.body.appendChild(formElement);
-  formElement.submit();
-  document.body.removeChild(formElement);
-};
-
 const NormalUpload = (props: any) => {
   const [importLoading, setImportLoading] = useState(false);
   const [flag, setFlag] = useState<boolean>(true);
   const [count, setCount] = useState<number>(0);
   const [errMessage, setErrMessage] = useState<string>('');
 
-  const submitData = (fileUrl: string) => {
+  const submitData = async (fileUrl: string) => {
     if (!!fileUrl) {
       setCount(0);
       setErrMessage('');
@@ -123,11 +109,11 @@ const NormalUpload = (props: any) => {
           headers={{
             'X-Access-Token': Token.get(),
           }}
-          onChange={(info) => {
+          onChange={async (info) => {
             if (info.file.status === 'done') {
               message.success('上传成功');
               const resp: any = info.file.response || { result: '' };
-              submitData(resp?.result || '');
+              await submitData(resp?.result || '');
             }
           }}
           showUploadList={false}
@@ -139,7 +125,9 @@ const NormalUpload = (props: any) => {
           <a
             style={{ marginLeft: 10 }}
             onClick={() => {
-              downloadTemplate('xlsx', props.product);
+              const url = `/${SystemConst.API_BASE}/device-instance/${props.product}/template.xlsx`;
+              downloadFile(url);
+              // downloadTemplate('xlsx', props.product);
             }}
           >
             .xlsx
@@ -147,7 +135,9 @@ const NormalUpload = (props: any) => {
           <a
             style={{ marginLeft: 10 }}
             onClick={() => {
-              downloadTemplate('csv', props.product);
+              const url = `/${SystemConst.API_BASE}/device-instance/${props.product}/template.csv`;
+              downloadFile(url);
+              // downloadTemplate('csv', props.product);
             }}
           >
             .csv

+ 18 - 16
src/pages/device/Instance/Save/index.tsx

@@ -9,8 +9,8 @@ import { debounce } from 'lodash';
 
 interface Props {
   visible: boolean;
-  close: () => void;
-  reload: () => void;
+  close: (data: DeviceInstance | undefined) => void;
+  reload?: () => void;
   model?: 'add' | 'edit';
   data?: Partial<DeviceInstance>;
 }
@@ -78,24 +78,26 @@ const Save = (props: Props) => {
         if (props.reload) {
           props.reload();
         }
-        props.close();
+        props.close(resp.result);
       }
     }
   };
 
   const vailId = (_: any, value: any, callback: Function) => {
-    service.isExists(value).then((resp) => {
-      if (resp.status === 200 && resp.result) {
-        callback(
-          intl.formatMessage({
-            id: 'pages.form.tip.existsID',
-            defaultMessage: 'ID重复',
-          }),
-        );
-      } else {
-        callback();
-      }
-    });
+    if (props.model === 'add') {
+      service.isExists(value).then((resp) => {
+        if (resp.status === 200 && resp.result) {
+          callback(
+            intl.formatMessage({
+              id: 'pages.form.tip.existsID',
+              defaultMessage: 'ID重复',
+            }),
+          );
+        } else {
+          callback();
+        }
+      });
+    }
   };
 
   return (
@@ -103,7 +105,7 @@ const Save = (props: Props) => {
       visible={visible}
       onCancel={() => {
         form.resetFields();
-        close();
+        close(undefined);
       }}
       width="30vw"
       title={intl.formatMessage({

+ 18 - 23
src/pages/device/Instance/index.tsx

@@ -10,9 +10,9 @@ import {
   CheckCircleOutlined,
   DeleteOutlined,
   ExportOutlined,
+  EyeOutlined,
   ImportOutlined,
   PlusOutlined,
-  SearchOutlined,
   StopOutlined,
   SyncOutlined,
 } from '@ant-design/icons';
@@ -24,12 +24,11 @@ import Save from './Save';
 import Export from './Export';
 import Import from './Import';
 import Process from './Process';
-import encodeQuery from '@/utils/encodeQuery';
 import SearchComponent from '@/components/SearchComponent';
 import SystemConst from '@/utils/const';
 import Token from '@/utils/token';
 
-const statusMap = new Map();
+export const statusMap = new Map();
 statusMap.set('在线', 'success');
 statusMap.set('离线', 'error');
 statusMap.set('未激活', 'processing');
@@ -38,14 +37,14 @@ statusMap.set('offline', 'error');
 statusMap.set('notActive', 'processing');
 
 export const InstanceModel = model<{
-  current: DeviceInstance | undefined;
+  current: Partial<DeviceInstance>;
   detail: Partial<DeviceInstance>;
   config: any;
   metadataItem: MetadataItem;
   params: Set<string>; // 处理无限循环Card
   active?: string; // 当前编辑的Card
 }>({
-  current: undefined,
+  current: {},
   detail: {},
   config: {},
   metadataItem: {},
@@ -60,7 +59,7 @@ const Instance = () => {
   const [operationVisible, setOperationVisible] = useState<boolean>(false);
   const [type, setType] = useState<'active' | 'sync'>('active');
   const [api, setApi] = useState<string>('');
-  const [current, setCurrent] = useState<DeviceInstance>();
+  const [current, setCurrent] = useState<Partial<DeviceInstance>>({});
   const [searchParams, setSearchParams] = useState<any>({});
   const [bindKeys, setBindKeys] = useState<any[]>([]);
   const intl = useIntl();
@@ -167,23 +166,9 @@ const Instance = () => {
             })}
             key={'detail'}
           >
-            <SearchOutlined />
+            <EyeOutlined />
           </Tooltip>
         </Link>,
-        // <a key="editable" onClick={() => {
-        //   setVisible(true)
-        //   setCurrent(record)
-        // }}>
-        //   <Tooltip
-        //     title={intl.formatMessage({
-        //       id: 'pages.data.option.edit',
-        //       defaultMessage: '编辑',
-        //     })}
-        //   >
-        //     <EditOutlined />
-        //   </Tooltip>
-        // </a>,
-
         <a href={record.id} target="_blank" rel="noopener noreferrer" key="view">
           <Popconfirm
             title={intl.formatMessage({
@@ -363,7 +348,17 @@ const Instance = () => {
         actionRef={actionRef}
         params={searchParams}
         options={{ fullScreen: true }}
-        request={(params) => service.query(encodeQuery({ ...params, sorts: { id: 'ascend' } }))}
+        request={(params) =>
+          service.query({
+            ...params,
+            sorts: [
+              {
+                name: 'id',
+                order: 'ascend',
+              },
+            ],
+          })
+        }
         rowKey="id"
         search={false}
         pagination={{ pageSize: 10 }}
@@ -377,7 +372,7 @@ const Instance = () => {
           <Button
             onClick={() => {
               setVisible(true);
-              setCurrent(undefined);
+              setCurrent({});
             }}
             key="button"
             icon={<PlusOutlined />}

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

@@ -149,6 +149,16 @@ class Service extends BaseService<DeviceInstance> {
       method: 'POST',
       data,
     });
+  public configurationReset = (deviceId: string) =>
+    request(`/${SystemConst.API_BASE}/device-instance/${deviceId}/configuration/_reset`, {
+      method: 'PUT',
+    });
+
+  public saveTags = (deviceId: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance/${deviceId}/tag`, {
+      method: 'PATCH',
+      data,
+    });
 }
 
 export default Service;

+ 60 - 26
src/pages/device/Product/Detail/PropertyImport/index.tsx

@@ -1,34 +1,55 @@
-import { Modal } from 'antd';
+import { Button, message, Modal, Space, Upload } from 'antd';
 import MetadataModel from '@/pages/device/components/Metadata/Base/model';
 import { FormItem, FormLayout, Radio } from '@formily/antd';
 import { createForm, onFieldValueChange } from '@formily/core';
 import { createSchemaField, FormProvider } from '@formily/react';
-import { Button, message, Space, Upload } from 'antd';
 import 'antd/lib/tree-select/style/index.less';
 import { UploadOutlined } from '@ant-design/icons';
 import SystemConst from '@/utils/const';
 import Token from '@/utils/token';
 import { useParams } from 'umi';
-import { service } from '../..';
-
-const downloadTemplate = (type: string, productId: string) => {
-  const formElement = document.createElement('form');
-  formElement.style.display = 'display:none;';
-  formElement.method = 'GET';
-  formElement.action = `/${SystemConst.API_BASE}/device/product/${productId}/property-metadata/template.${type}`;
-  const inputElement = document.createElement('input');
-  inputElement.type = 'hidden';
-  inputElement.name = ':X_Access_Token';
-  inputElement.value = Token.get();
-  formElement.appendChild(inputElement);
-  document.body.appendChild(formElement);
-  formElement.submit();
-  document.body.removeChild(formElement);
-};
+import { productModel, service } from '../..';
+import { downloadFile } from '@/utils/util';
+import type { DeviceMetadata, ProductItem } from '@/pages/device/Product/typings';
+import { Store } from 'jetlinks-store';
+import { asyncUpdateMedata, updateMetadata } from '@/pages/device/components/Metadata/metadata';
+import { InstanceModel } from '@/pages/device/Instance';
 
 const NormalUpload = (props: any) => {
   const param = useParams<{ id: string }>();
-  console.log(props?.fileType);
+
+  const typeMap = new Map<string, any>();
+
+  typeMap.set('product', productModel.current);
+  typeMap.set('device', InstanceModel.detail);
+
+  const mergeMetadata = async (url: string) => {
+    if (!url) return;
+    // 解析物模型
+    const r = await service.importProductProperty(param.id, url);
+    const _metadata = JSON.parse(r.result || '{}') as DeviceMetadata;
+
+    const target = typeMap.get(props.type);
+
+    const _data = updateMetadata('properties', _metadata.properties, target) as ProductItem;
+    // const resp = await service.update(_product);
+    const resp = await asyncUpdateMedata(props.type, _data);
+    console.log(resp);
+    if (resp.status === 200) {
+      message.success('操作成功');
+      // 刷新物模型
+
+      if (props.type === 'product') {
+        Store.set(SystemConst.GET_METADATA, true);
+      } else if (props.type === 'device') {
+        Store.set(SystemConst.REFRESH_DEVICE, true);
+      }
+      setTimeout(() => {
+        Store.set(SystemConst.REFRESH_METADATA_TABLE, true);
+      }, 300);
+    }
+    MetadataModel.importMetadata = false;
+  };
 
   return (
     <div>
@@ -39,13 +60,17 @@ const NormalUpload = (props: any) => {
           headers={{
             'X-Access-Token': Token.get(),
           }}
-          onChange={(info) => {
+          onChange={async (info) => {
             if (info.file.status === 'done') {
               message.success('上传成功');
               const resp: any = info.file.response || { result: '' };
-              service.importProductProperty(param.id, resp?.result).then(() => {
-                // 更新产品物模型属性信息
-              });
+              await mergeMetadata(resp?.result);
+              // service.importProductProperty(param.id, resp?.result).then((r) => {
+              //   console.log(r, 'resp');
+              //   const _metadata = JSON.parse(r.result || '{}') as DeviceMetadata;
+              //
+              //   // 更新产品物模型属性信息
+              // });
             }
           }}
           showUploadList={false}
@@ -57,7 +82,9 @@ const NormalUpload = (props: any) => {
           <a
             style={{ marginLeft: 10 }}
             onClick={() => {
-              downloadTemplate('xlsx', param?.id);
+              const url = `/${SystemConst.API_BASE}/device/product/${param?.id}/property-metadata/template.xlsx`;
+              downloadFile(url);
+              // downloadTemplate('xlsx', param?.id);
             }}
           >
             .xlsx
@@ -65,7 +92,9 @@ const NormalUpload = (props: any) => {
           <a
             style={{ marginLeft: 10 }}
             onClick={() => {
-              downloadTemplate('csv', param?.id);
+              const url = `/${SystemConst.API_BASE}/device/product/${param.id}/property-metadata/template.csv`;
+              // downloadTemplate('csv', param?.id);
+              downloadFile(url);
             }}
           >
             .csv
@@ -76,7 +105,11 @@ const NormalUpload = (props: any) => {
   );
 };
 
-const PropertyImport = () => {
+interface Props {
+  type: 'product' | 'device';
+}
+
+const PropertyImport = (props: Props) => {
   const SchemaField = createSchemaField({
     components: {
       Radio,
@@ -92,6 +125,7 @@ const PropertyImport = () => {
         form.setFieldState('*(upload)', (state) => {
           state.componentProps = {
             fileType: field.value,
+            type: props.type,
           };
         });
       });

+ 19 - 7
src/pages/device/Product/Detail/index.tsx

@@ -25,6 +25,7 @@ import MetadataAction from '@/pages/device/components/Metadata/DataBaseAction';
 import { QuestionCircleOutlined } from '@ant-design/icons';
 import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
 import encodeQuery from '@/utils/encodeQuery';
+import SystemConst from '@/utils/const';
 
 const ProductDetail = observer(() => {
   const intl = useIntl();
@@ -61,25 +62,36 @@ const ProductDetail = observer(() => {
   };
   const param = useParams<{ id: string }>();
 
+  const initMetadata = () => {
+    service.getProductDetail(param?.id).subscribe((data) => {
+      if (data.metadata) {
+        const metadata: DeviceMetadata = JSON.parse(data.metadata);
+        productModel.current = data;
+        MetadataAction.insert(metadata);
+      }
+    });
+  };
   useEffect(() => {
+    const subscription = Store.subscribe(SystemConst.GET_METADATA, () => {
+      MetadataAction.clean();
+      setTimeout(() => {
+        initMetadata();
+        Store.set(SystemConst.REFRESH_METADATA_TABLE, true);
+      }, 300);
+    });
     if (!productModel.current) {
       history.goBack();
     } else {
-      service.getProductDetail(param?.id).subscribe((data) => {
-        if (data.metadata) {
-          const metadata: DeviceMetadata = JSON.parse(data.metadata);
-          MetadataAction.insert(metadata);
-        }
-      });
+      initMetadata();
       service.instanceCount(encodeQuery({ terms: { productId: param?.id } })).then((res: any) => {
         if (res.status === 200) {
           productModel.current!.count = res.result;
         }
       });
     }
-
     return () => {
       MetadataAction.clean();
+      subscription.unsubscribe();
     };
   }, [param.id]);
 

+ 14 - 10
src/pages/device/Product/Save/index.tsx

@@ -83,17 +83,21 @@ const Save = (props: Props) => {
   };
 
   const vailId = (_: any, value: any, callback: Function) => {
-    service.existsID(value).then((res) => {
-      if (res.status === 200 && res.result) {
-        callback(
-          intl.formatMessage({
-            id: 'pages.form.tip.existsID',
-            defaultMessage: 'ID重复',
-          }),
-        );
-      }
+    if (props.model === 'add') {
+      service.existsID(value).then((res) => {
+        if (res.status === 200 && res.result) {
+          callback(
+            intl.formatMessage({
+              id: 'pages.form.tip.existsID',
+              defaultMessage: 'ID重复',
+            }),
+          );
+        }
+        callback();
+      });
+    } else {
       callback();
-    });
+    }
   };
 
   return (

+ 4 - 4
src/pages/device/Product/index.tsx

@@ -190,7 +190,7 @@ const Product = observer(() => {
           key={'state'}
           title={intl.formatMessage({
             id: `pages.data.option.${record.state ? 'disabled' : 'enabled'}.tips`,
-            defaultMessage: '是否删除该菜单',
+            defaultMessage: '是否删除?',
           })}
           onConfirm={() => {
             changeDeploy(record.id, record.state ? 'undeploy' : 'deploy');
@@ -209,10 +209,10 @@ const Product = observer(() => {
           key="unBindUser"
           title={intl.formatMessage({
             id: 'page.system.menu.table.delete',
-            defaultMessage: '是否删除该菜单',
+            defaultMessage: '是否删除?',
           })}
-          onConfirm={() => {
-            deleteItem(record.id);
+          onConfirm={async () => {
+            await deleteItem(record.id);
           }}
         >
           <Tooltip

+ 65 - 25
src/pages/device/components/Metadata/Base/Edit/index.tsx

@@ -27,7 +27,7 @@ import { useMemo } from 'react';
 import { productModel } from '@/pages/device/Product';
 import { service } from '@/pages/device/components/Metadata';
 import { Store } from 'jetlinks-store';
-import type { DeviceMetadata, MetadataItem } from '@/pages/device/Product/typings';
+import type { MetadataItem } from '@/pages/device/Product/typings';
 
 import JsonParam from '@/components/Metadata/JsonParam';
 import ArrayParam from '@/components/Metadata/ArrayParam';
@@ -39,10 +39,10 @@ import { lastValueFrom } from 'rxjs';
 import SystemConst from '@/utils/const';
 import DB from '@/db';
 import _ from 'lodash';
-import { useParams } from 'umi';
 import { InstanceModel } from '@/pages/device/Instance';
 import FRuleEditor from '@/components/FRuleEditor';
 import { action } from '@formily/reactive';
+import { asyncUpdateMedata, updateMetadata } from '../../metadata';
 
 interface Props {
   type: 'product' | 'device';
@@ -322,6 +322,16 @@ const Edit = observer((props: Props) => {
       'x-component': 'Input',
       'x-disabled': MetadataModel.action === 'edit',
       'x-index': 0,
+      'x-validator': [
+        {
+          max: 64,
+          message: '最多可输入64个字符',
+        },
+        {
+          required: true,
+          message: '请输入ID',
+        },
+      ],
     },
     name: {
       title: intl.formatMessage({
@@ -332,6 +342,16 @@ const Edit = observer((props: Props) => {
       'x-decorator': 'FormItem',
       'x-component': 'Input',
       'x-index': 1,
+      'x-validator': [
+        {
+          max: 64,
+          message: '最多可输入64个字符',
+        },
+        {
+          required: true,
+          message: '请输入姓名',
+        },
+      ],
     },
     description: {
       title: intl.formatMessage({
@@ -341,6 +361,12 @@ const Edit = observer((props: Props) => {
       'x-decorator': 'FormItem',
       'x-component': 'Input.TextArea',
       'x-index': 100,
+      'x-validator': [
+        {
+          max: 200,
+          message: '最多可输入200个字符',
+        },
+      ],
     },
   } as any;
   const propertySchema: ISchema = {
@@ -665,12 +691,12 @@ const Edit = observer((props: Props) => {
     );
   };
 
-  const param = useParams<{ id: string }>();
+  // const param = useParams<{ id: string }>();
   const typeMap = new Map<string, any>();
 
   typeMap.set('product', productModel.current);
   typeMap.set('device', InstanceModel.detail);
-  const saveMap = new Map<string, Promise<any>>();
+  // const saveMap = new Map<string, Promise<any>>();
   const { type } = MetadataModel;
 
   const saveMetadata = async (deploy?: boolean) => {
@@ -678,29 +704,43 @@ const Edit = observer((props: Props) => {
 
     if (!typeMap.get(props.type)) return;
 
-    const metadata = JSON.parse(typeMap.get(props.type).metadata || '{}') as DeviceMetadata;
-    const config = (metadata[type] || []) as MetadataItem[];
-    const index = config.findIndex((item) => item.id === params.id);
-    if (index > -1) {
-      config[index] = params;
-      DB.getDB().table(`${type}`).update(params.id, params);
-    } else {
-      config.push(params);
-      DB.getDB().table(`${type}`).add(params, params.id);
-    }
+    // const metadata = JSON.parse(typeMap.get(props.type).metadata || '{}') as DeviceMetadata;
+    // const config = (metadata[type] || []) as MetadataItem[];
+    // const index = config.findIndex((item) => item.id === params.id);
+    // if (index > -1) {
+    //   config[index] = params;
+    //   DB.getDB().table(`${type}`).update(params.id, params);
+    // } else {
+    //   config.push(params);
+    //   DB.getDB().table(`${type}`).add(params, params.id);
+    // }
 
-    if (props.type === 'product') {
-      const product = typeMap.get('product');
-      // product.metadata = JSON.stringify(metadata);
-      // @ts-ignore
-      metadata[type] = config;
-      product.metadata = JSON.stringify(metadata);
-      saveMap.set('product', service.saveProductMetadata(product));
-    } else {
-      saveMap.set('device', service.saveDeviceMetadata(param.id, metadata));
-    }
+    const updateDB = (t: 'add' | 'update', item: MetadataItem) => {
+      switch (t) {
+        case 'add':
+          DB.getDB().table(`${type}`).add(item, item.id);
+          return;
+        case 'update':
+          DB.getDB().table(`${type}`).update(item.id, item);
+          return;
+      }
+    };
 
-    const result = await saveMap.get(props.type);
+    console.log(typeMap.get(props.type), 'log');
+    const _data = updateMetadata(type, [params], typeMap.get(props.type), updateDB);
+    // console.log(params, JSON.parse(_data.metadata));
+    // if (props.type === 'product') {
+    //   // const product = typeMap.get('product');
+    //   // @ts-ignore
+    //   // metadata[type] = config;
+    //   // product.metadata = JSON.stringify(metadata);
+    //   saveMap.set('product', service.saveProductMetadata(_data));
+    // } else {
+    //   saveMap.set('device', service.saveDeviceMetadata(param.id, { metadata: _data.metadata }));
+    // }
+    //
+    // const result = await saveMap.get(props.type);
+    const result = await asyncUpdateMedata(props.type, _data);
     if (result.status === 200) {
       message.success('操作成功!');
       Store.set(SystemConst.REFRESH_METADATA_TABLE, true);

+ 37 - 7
src/pages/device/components/Metadata/Base/index.tsx

@@ -5,7 +5,7 @@ import { useParams } from 'umi';
 import DB from '@/db';
 import type { MetadataItem, MetadataType } from '@/pages/device/Product/typings';
 import MetadataMapping from './columns';
-import { Button, Popconfirm, Tooltip } from 'antd';
+import { Button, message, Popconfirm, Tooltip } from 'antd';
 import { DeleteOutlined, EditOutlined, ImportOutlined, PlusOutlined } from '@ant-design/icons';
 import Edit from './Edit';
 import { observer } from '@formily/react';
@@ -14,6 +14,9 @@ import { Store } from 'jetlinks-store';
 import SystemConst from '@/utils/const';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import PropertyImport from '@/pages/device/Product/Detail/PropertyImport';
+import { productModel } from '@/pages/device/Product';
+import { InstanceModel } from '@/pages/device/Instance';
+import { asyncUpdateMedata, removeMetadata } from '../metadata';
 
 interface Props {
   type: MetadataType;
@@ -27,6 +30,26 @@ const BaseMetadata = observer((props: Props) => {
 
   const [loading, setLoading] = useState<boolean>(true);
   const [data, setData] = useState<MetadataItem[]>([]);
+  const typeMap = new Map<string, any>();
+
+  typeMap.set('product', productModel.current);
+  typeMap.set('device', InstanceModel.detail);
+
+  const removeItem = async (record: MetadataItem) => {
+    const removeDB = () => {
+      return DB.getDB().table(`${type}`).delete(record.id!);
+    };
+    const _currentData = removeMetadata(type, [record], typeMap.get(target), removeDB);
+    const result = await asyncUpdateMedata(target, _currentData);
+    if (result.status === 200) {
+      message.success('操作成功!');
+      Store.set(SystemConst.REFRESH_METADATA_TABLE, true);
+      MetadataModel.edit = false;
+      MetadataModel.item = {};
+    } else {
+      message.error('操作失败!');
+    }
+  };
 
   const actions: ProColumns<MetadataItem>[] = [
     {
@@ -49,7 +72,12 @@ const BaseMetadata = observer((props: Props) => {
           </Tooltip>
         </a>,
         <a key="delete">
-          <Popconfirm title="确认删除?" onConfirm={async () => {}}>
+          <Popconfirm
+            title="确认删除?"
+            onConfirm={async () => {
+              await removeItem(record);
+            }}
+          >
             <Tooltip title="删除">
               <DeleteOutlined />
             </Tooltip>
@@ -69,10 +97,12 @@ const BaseMetadata = observer((props: Props) => {
   }, [initData]);
 
   useEffect(() => {
-    const subscription = Store.subscribe(SystemConst.REFRESH_METADATA_TABLE, async (flag) => {
-      if (flag) {
-        await initData();
-      }
+    const subscription = Store.subscribe(SystemConst.REFRESH_METADATA_TABLE, (flag) => {
+      setTimeout(async () => {
+        if (flag) {
+          await initData();
+        }
+      }, 300);
     });
     return () => subscription.unsubscribe();
   }, []);
@@ -142,7 +172,7 @@ const BaseMetadata = observer((props: Props) => {
           </Button>,
         ]}
       />
-      {MetadataModel.importMetadata && <PropertyImport />}
+      {MetadataModel.importMetadata && <PropertyImport type={target} />}
       {MetadataModel.edit && <Edit type={target} />}
     </>
   );

+ 4 - 0
src/pages/device/components/Metadata/Import/index.tsx

@@ -8,6 +8,8 @@ import FMonacoEditor from '@/components/FMonacoEditor';
 import FUpload from '@/components/Upload';
 import { service } from '@/pages/device/Product';
 import { useParams } from 'umi';
+import { Store } from 'jetlinks-store';
+import SystemConst from '@/utils/const';
 
 interface Props {
   visible: boolean;
@@ -180,7 +182,9 @@ const Import = (props: Props) => {
     } else {
       await service.modify(param.id, { metadata: data[data.type] });
     }
+    Store.set(SystemConst.GET_METADATA, true);
     message.success('导入成功');
+    props.close();
   };
   return (
     <Modal

+ 86 - 0
src/pages/device/components/Metadata/metadata.ts

@@ -0,0 +1,86 @@
+import type {
+  DeviceMetadata,
+  MetadataItem,
+  MetadataType,
+  ProductItem,
+} from '@/pages/device/Product/typings';
+import type { DeviceInstance } from '@/pages/device/Instance/typings';
+import { service } from '@/pages/device/components/Metadata/index';
+
+/**
+ * 更新物模型
+ * @param type 物模型类型 events
+ * @param item 物模型数据 【{a},{b},{c}】
+ // * @param target product、device
+ * @param data product 、device [{event:[1,2,3]]
+ * @param onEvent 数据更新回调:更新数据库、发送事件等操作
+ *
+ */
+export const updateMetadata = (
+  type: MetadataType,
+  item: MetadataItem[],
+  // target: 'product' | 'device',
+  data: ProductItem | DeviceInstance,
+  onEvent?: (type: 'update' | 'add', item: MetadataItem) => void,
+): ProductItem | DeviceInstance => {
+  if (!data) return data;
+  const metadata = JSON.parse(data.metadata || '{}') as DeviceMetadata;
+  const config = (metadata[type] || []) as MetadataItem[];
+  if (item.length > 0) {
+    item.forEach((i) => {
+      const index = config.findIndex((c) => c.id === i.id);
+      if (index > -1) {
+        config[index] = i;
+        onEvent?.('update', i);
+      } else {
+        config.push(i);
+        onEvent?.('add', i);
+      }
+    });
+  } else {
+    console.warn('未触发物模型修改');
+  }
+  // @ts-ignore
+  metadata[type] = config;
+  data.metadata = JSON.stringify(metadata);
+  return data;
+};
+
+/**
+ * 删除物模型数据
+ * @param type 物模型类型
+ * @param item 删除的数据
+ * @param data 设备/产品数据
+ * @param onEvent 回调
+ */
+export const removeMetadata = (
+  type: MetadataType,
+  item: MetadataItem[],
+  data: ProductItem | DeviceInstance,
+  onEvent?: (type: 'remove', item: MetadataItem) => void,
+): ProductItem | DeviceInstance => {
+  const metadata = JSON.parse(data.metadata || '{}') as DeviceMetadata;
+  const config = (metadata[type] || []) as MetadataItem[];
+  // @ts-ignore
+  metadata[type] = config.filter((i) => !item.map((r) => r.id).includes(i.id));
+  onEvent?.('remove', item);
+  data.metadata = JSON.stringify(metadata);
+  return data;
+};
+
+/**
+ * 保存物模型数据到服务器
+ * @param type 类型
+ * @param data 数据
+ */
+export const asyncUpdateMedata = (
+  type: 'product' | 'device',
+  data: ProductItem | DeviceInstance,
+): Promise<any> => {
+  switch (type) {
+    case 'product':
+      return service.saveProductMetadata(data);
+    case 'device':
+      return service.saveDeviceMetadata(data.id, JSON.parse(data.metadata || '{}'));
+  }
+};

+ 19 - 3
src/pages/system/User/Save/index.tsx

@@ -5,9 +5,9 @@ import { createForm } from '@formily/core';
 import { createSchemaField } from '@formily/react';
 import React, { useEffect, useState } from 'react';
 import * as ICONS from '@ant-design/icons';
+import { PlusOutlined } from '@ant-design/icons';
 import { Form, FormItem, Input, Password, Select, Switch, TreeSelect } from '@formily/antd';
 import type { ISchema } from '@formily/json-schema';
-import { PlusOutlined } from '@ant-design/icons';
 import { action } from '@formily/reactive';
 import type { Response } from '@/utils/typings';
 import { service } from '@/pages/system/User';
@@ -96,13 +96,29 @@ const Save = (props: Props) => {
         name: 'name',
         'x-validator': [
           {
-            max: 50,
-            message: '最多可输入50个字符',
+            max: 64,
+            message: '最多可输入64个字符',
           },
           {
             required: true,
             message: '请输入姓名',
           },
+          {
+            triggerType: 'onBlur',
+            validator: (value: string) => {
+              return new Promise((resolve) => {
+                service
+                  .validateField('username', value)
+                  .then((resp) => {
+                    console.log(resp);
+                    resolve('');
+                  })
+                  .catch(() => {
+                    return '验证失败!';
+                  });
+              });
+            },
+          },
         ],
         // required: true,
       },

+ 6 - 0
src/pages/system/User/serivce.ts

@@ -36,6 +36,12 @@ class Service extends BaseService<UserItem> {
       data,
     });
   };
+
+  validateField = (type: 'username' | 'password', name: string) =>
+    request(`/${SystemConst.API_BASE}/user/${type}/_validate`, {
+      method: 'POST',
+      data: name,
+    });
 }
 
 export default Service;

+ 4 - 0
src/utils/const.ts

@@ -22,6 +22,10 @@ class SystemConst {
   static REFRESH_METADATA = 'refresh_metadata';
 
   static REFRESH_METADATA_TABLE = 'refresh_metadata_table';
+
+  static GET_METADATA = 'get_metadata';
+
+  static REFRESH_DEVICE = 'refresh_device';
 }
 
 export default SystemConst;

+ 30 - 0
src/utils/util.ts

@@ -1,8 +1,38 @@
 import moment from 'moment';
 import type { Field, FieldDataSource } from '@formily/core';
 import { action } from '@formily/reactive';
+import Token from '@/utils/token';
 
 /**
+ * 下载文件
+ * @param url 下载链接
+ * @param params 参数
+ */
+export const downloadFile = (url: string, params?: Record<string, any>) => {
+  const formElement = document.createElement('form');
+  formElement.style.display = 'display:none;';
+  formElement.method = 'GET';
+  formElement.action = url;
+  // 添加参数
+  if (params) {
+    Object.keys(params).forEach((key: string) => {
+      const inputElement = document.createElement('input');
+      inputElement.type = 'hidden';
+      inputElement.name = key;
+      inputElement.value = params[key];
+      formElement.appendChild(inputElement);
+    });
+  }
+  const inputElement = document.createElement('input');
+  inputElement.type = 'hidden';
+  inputElement.name = ':X_Access_Token';
+  inputElement.value = Token.get();
+  formElement.appendChild(inputElement);
+  document.body.appendChild(formElement);
+  formElement.submit();
+  document.body.removeChild(formElement);
+};
+/**
  * 把数据下载成JSON
  * @param record
  * @param fileName

+ 0 - 2
tests/beforeTest.js

@@ -1,5 +1,4 @@
 /* eslint-disable global-require */
-/* eslint-disable import/no-extraneous-dependencies */
 const { execSync } = require('child_process');
 const { join } = require('path');
 const findChrome = require('carlo/lib/find_chrome');
@@ -19,7 +18,6 @@ const installPuppeteer = () => {
 
 const initPuppeteer = async () => {
   try {
-    // eslint-disable-next-line import/no-unresolved
     const findChromePath = await findChrome({});
     const { executablePath } = findChromePath;
     console.log(`🧲 find you browser in ${executablePath}`);

+ 2 - 7
tests/getBrowser.js

@@ -1,12 +1,10 @@
 /* eslint-disable global-require */
-/* eslint-disable import/no-extraneous-dependencies */
 const findChrome = require('carlo/lib/find_chrome');
 
 const getBrowser = async () => {
   try {
-    // eslint-disable-next-line import/no-unresolved
     const puppeteer = require('puppeteer');
-    const browser = await puppeteer.launch({
+    return await puppeteer.launch({
       args: [
         '--disable-gpu',
         '--disable-dev-shm-usage',
@@ -15,17 +13,15 @@ const getBrowser = async () => {
         '--no-sandbox',
       ],
     });
-    return browser;
   } catch (error) {
     // console.log(error)
   }
 
   try {
-    // eslint-disable-next-line import/no-unresolved
     const puppeteer = require('puppeteer-core');
     const findChromePath = await findChrome({});
     const { executablePath } = findChromePath;
-    const browser = await puppeteer.launch({
+    return await puppeteer.launch({
       executablePath,
       args: [
         '--disable-gpu',
@@ -35,7 +31,6 @@ const getBrowser = async () => {
         '--no-sandbox',
       ],
     });
-    return browser;
   } catch (error) {
     console.log('🧲 no find chrome');
   }

+ 0 - 3
tests/run-tests.js

@@ -1,8 +1,5 @@
-/* eslint-disable eslint-comments/disable-enable-pair */
 /* eslint-disable @typescript-eslint/no-var-requires */
-/* eslint-disable eslint-comments/no-unlimited-disable */
 const { spawn } = require('child_process');
-// eslint-disable-next-line import/no-extraneous-dependencies
 const { kill } = require('cross-port-killer');
 
 const env = Object.create(process.env);

+ 3 - 3
yarn.lock

@@ -385,7 +385,7 @@
 
 "@ant-design/pro-utils@1.35.2":
   version "1.35.2"
-  resolved "https://registry.npmmirror.com/@ant-design/pro-utils/-/pro-utils-1.35.2.tgz#789b4c5d4d63085e02ab24c2ae68c6084bc9a8f1"
+  resolved "https://registry.npmjs.org/@ant-design/pro-utils/-/pro-utils-1.35.2.tgz#789b4c5d4d63085e02ab24c2ae68c6084bc9a8f1"
   integrity sha512-LZfMr821QHD54A9PyRKRl2Xy4FYxJ9gIXMrq4kqTJMBeZr9W7xkx2ZjoeFJqFKnrY3xAf1GzRgz1dMO+VIbc3g==
   dependencies:
     "@ant-design/icons" "^4.3.0"
@@ -3348,7 +3348,7 @@
 
 "@types/event-source-polyfill@^1.0.0":
   version "1.0.0"
-  resolved "https://registry.npmmirror.com/@types/event-source-polyfill/-/event-source-polyfill-1.0.0.tgz#f93f13433f750c8ea0e3cfa69c72e3c7393e0585"
+  resolved "https://registry.npmjs.org/@types/event-source-polyfill/-/event-source-polyfill-1.0.0.tgz#f93f13433f750c8ea0e3cfa69c72e3c7393e0585"
   integrity sha512-b8O8/rg7NIW0iJ8i9MNDBZqPljHA+b7AjC3QFqH3dSyW6vgrl3oBgyIv5dw2fibh5enHHDkkPZG5PHza7U4NRw==
 
 "@types/express-serve-static-core@^4.17.18":
@@ -7906,7 +7906,7 @@ event-emitter@^0.3.5:
 
 event-source-polyfill@^1.0.25:
   version "1.0.25"
-  resolved "https://registry.npmmirror.com/event-source-polyfill/-/event-source-polyfill-1.0.25.tgz#d8bb7f99cb6f8119c2baf086d9f6ee0514b6d9c8"
+  resolved "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.25.tgz#d8bb7f99cb6f8119c2baf086d9f6ee0514b6d9c8"
   integrity sha512-hQxu6sN1Eq4JjoI7ITdQeGGUN193A2ra83qC0Ltm9I2UJVAten3OFVN6k5RX4YWeCS0BoC8xg/5czOCIHVosQg==
 
 eventemitter3@^4.0.0, eventemitter3@^4.0.4: