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

feat(metadata): import metadata

Lind 4 лет назад
Родитель
Сommit
a5b7dbb173

+ 1 - 0
src/locales/zh-CN/pages.ts

@@ -202,6 +202,7 @@ export default {
   'pages.device.productDetail.alarmLog.deviceName': '设备名称',
   'pages.device.productDetail.setting': '应用配置',
   'pages.device.productDetail.disable': '停用',
+  'pages.device.productDetail.enabled': '启用',
   // 设备管理-产品分类
   'pages.device.category': '产品分类',
   'pages.device.category.id': '分类ID',

+ 22 - 17
src/pages/device/Product/Detail/Metadata/Base/Edit/index.tsx

@@ -1,4 +1,4 @@
-import { Button, Drawer, message } from 'antd';
+import { Drawer, Dropdown, Menu, message } from 'antd';
 import { createSchemaField } from '@formily/react';
 import MetadataModel from '@/pages/device/Product/Detail/Metadata/Base/model';
 import type { Field, IFieldState } from '@formily/core';
@@ -26,7 +26,7 @@ import {
 import { useCallback, useEffect, useState } from 'react';
 import { productModel, service } from '@/pages/device/Product';
 import { Store } from 'jetlinks-store';
-import type { MetadataItem, MetadataType, UnitType } from '@/pages/device/Product/typings';
+import type { MetadataItem, UnitType } from '@/pages/device/Product/typings';
 
 import JsonParam from '@/components/Metadata/JsonParam';
 import ArrayParam from '@/components/Metadata/ArrayParam';
@@ -506,7 +506,9 @@ const Edit = () => {
     });
   }, [getUnits]);
 
-  const saveMetadata = async (type: MetadataType, params: MetadataItem) => {
+  const saveMetadata = async (deploy?: boolean) => {
+    const params = (await form.submit()) as MetadataItem;
+    const { type } = MetadataModel;
     const product = productModel.current;
     if (!product) return;
     const metadata = JSON.parse(product.metadata) as DeviceMetadata;
@@ -525,13 +527,26 @@ const Edit = () => {
     const result = await service.saveProduct(product);
     if (result.status === 200) {
       message.success('操作成功!');
+      Store.set(SystemConst.REFRESH_METADATA_TABLE, true);
+      console.log(deploy, 'dep');
+      if (deploy) {
+        // 不阻塞主流程。发布更新通知
+        Store.set('product-deploy', deploy);
+      }
       MetadataModel.edit = false;
       MetadataModel.item = {};
-      Store.set(SystemConst.REFRESH_METADATA_TABLE, true);
     } else {
       message.error('操作失败!');
     }
   };
+
+  const menu = (
+    <Menu>
+      <Menu.Item key="1" onClick={() => saveMetadata(true)}>
+        保存并生效
+      </Menu.Item>
+    </Menu>
+  );
   return (
     <Drawer
       width="25vw"
@@ -551,19 +566,9 @@ const Edit = () => {
       zIndex={1000}
       placement={'right'}
       extra={
-        <Button
-          type="primary"
-          onClick={async () => {
-            const data = (await form.submit()) as MetadataItem;
-            const { type } = MetadataModel;
-            await saveMetadata(type, data);
-          }}
-        >
-          {intl.formatMessage({
-            id: 'pages.device.productDetail.metadata.saveData',
-            defaultMessage: '保存数据',
-          })}
-        </Button>
+        <Dropdown.Button type="primary" onClick={() => saveMetadata()} overlay={menu}>
+          保存数据
+        </Dropdown.Button>
       }
     >
       <Form form={form} layout="vertical" size="small">

+ 2 - 0
src/pages/device/Product/Detail/Metadata/Base/model.ts

@@ -6,11 +6,13 @@ type MetadataModelType = {
   edit: boolean;
   type: MetadataType;
   action: 'edit' | 'add';
+  import: boolean;
 };
 const MetadataModel = model<MetadataModelType>({
   item: undefined,
   edit: false,
   type: 'events',
   action: 'add',
+  import: false,
 });
 export default MetadataModel;

+ 130 - 0
src/pages/device/Product/Detail/Metadata/Import/index.tsx

@@ -0,0 +1,130 @@
+import { Modal } from 'antd';
+import { createSchemaField } from '@formily/react';
+import { createForm } from '@formily/core';
+import { Form, FormItem, Input, Select } from '@formily/antd';
+import type { ISchema } from '@formily/json-schema';
+import FMonacoEditor from '@/components/FMonacoEditor';
+import Upload from '@/components/Upload/Upload';
+
+interface Props {
+  visible: boolean;
+  close: () => void;
+}
+
+const Import = (props: Props) => {
+  const form = createForm({
+    initialValues: {},
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Select,
+      FMonacoEditor,
+      Upload,
+    },
+  });
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      type: {
+        title: '导入方式',
+        'x-decorator': 'FormItem',
+        'x-component': 'Select',
+        enum: [
+          { label: '拷贝产品', value: 'copy' },
+          { label: '导入物模型', value: 'import' },
+        ],
+      },
+      device: {
+        title: '选择设备',
+        'x-decorator': 'FormItem',
+        'x-component': 'Select',
+        enum: [],
+        'x-visible': false,
+        'x-reactions': {
+          dependencies: ['.type'],
+          fulfill: {
+            state: {
+              visible: "{{$deps[0]==='copy'}}",
+            },
+          },
+        },
+      },
+      upload: {
+        title: '快速导入',
+        'x-decorator': 'FormItem',
+        'x-component': 'Upload',
+      },
+      metadata: {
+        title: '类型',
+        'x-decorator': 'FormItem',
+        'x-component': 'Select',
+        enum: [
+          {
+            label: 'Jetlinks物模型',
+            value: 'jetlinks',
+          },
+          {
+            label: '阿里云物模型TSL',
+            value: 'aliyun-tsl',
+          },
+        ],
+        'x-visible': false,
+        default: 'jetlinks',
+        'x-reactions': {
+          dependencies: ['.type'],
+          fulfill: {
+            state: {
+              visible: "{{$deps[0]==='import'}}",
+            },
+          },
+        },
+      },
+      editor: {
+        title: '物模型',
+        'x-decorator': 'FormItem',
+        'x-component': 'FMonacoEditor',
+        'x-component-props': {
+          height: 200,
+          theme: 'vs',
+          language: 'json',
+        },
+        'x-visible': false,
+        'x-reactions': {
+          dependencies: ['.type'],
+          fulfill: {
+            state: {
+              visible: "{{$deps[0]==='import'}}",
+            },
+          },
+        },
+      },
+    },
+  };
+
+  return (
+    <Modal title="导入物模型" visible={props.visible} onCancel={() => props.close()}>
+      <div style={{ background: 'rgb(236, 237, 238)' }}>
+        <p style={{ padding: 10 }}>
+          <span style={{ color: '#f5222d' }}>注</span>
+          :导入的物模型会覆盖原来的属性、功能、事件、标签,请谨慎操作。
+          <br />
+          物模型格式请参考文档:
+          <a
+            target="_blank"
+            href="http://doc.jetlinks.cn/basics-guide/device-manager.html#%E8%AE%BE%E5%A4%87%E5%9E%8B%E5%8F%B7"
+          >
+            设备型号
+          </a>
+        </p>
+      </div>
+      <Form form={form} layout="vertical">
+        <SchemaField schema={schema} />
+      </Form>
+    </Modal>
+  );
+};
+
+export default Import;

+ 62 - 56
src/pages/device/Product/Detail/Metadata/index.tsx

@@ -2,67 +2,73 @@ import { observer } from '@formily/react';
 import { Button, Space, Tabs } from 'antd';
 import BaseMetadata from '@/pages/device/Product/Detail/Metadata/Base';
 import { useIntl } from '@@/plugin-locale/localeExports';
+import Import from '@/pages/device/Product/Detail/Metadata/Import';
+import { useState } from 'react';
 
 const Metadata = observer(() => {
   const intl = useIntl();
+  const [visible, setVisible] = useState<boolean>(false);
   return (
-    <Tabs
-      tabBarExtraContent={
-        <Space>
-          <Button>
-            {intl.formatMessage({
-              id: 'pages.device.productDetail.metadata.quickImport',
-              defaultMessage: '快速导入',
-            })}
-          </Button>
-          <Button>
-            {intl.formatMessage({
-              id: 'pages.device.productDetail.metadata',
-              defaultMessage: '物模型',
-            })}{' '}
-            TSL
-          </Button>
-        </Space>
-      }
-      destroyInactiveTabPane
-    >
-      <Tabs.TabPane
-        tab={intl.formatMessage({
-          id: 'pages.device.productDetail.metadata.propertyDefinition',
-          defaultMessage: '属性定义',
-        })}
-        key="properties"
+    <>
+      <Tabs
+        tabBarExtraContent={
+          <Space>
+            <Button onClick={() => setVisible(true)}>
+              {intl.formatMessage({
+                id: 'pages.device.productDetail.metadata.quickImport',
+                defaultMessage: '快速导入',
+              })}
+            </Button>
+            <Button>
+              {intl.formatMessage({
+                id: 'pages.device.productDetail.metadata',
+                defaultMessage: '物模型',
+              })}
+              TSL
+            </Button>
+          </Space>
+        }
+        destroyInactiveTabPane
       >
-        <BaseMetadata type={'properties'} />
-      </Tabs.TabPane>
-      <Tabs.TabPane
-        tab={intl.formatMessage({
-          id: 'pages.device.productDetail.metadata.functionDefinition',
-          defaultMessage: '功能定义',
-        })}
-        key="functions"
-      >
-        <BaseMetadata type={'functions'} />
-      </Tabs.TabPane>
-      <Tabs.TabPane
-        tab={intl.formatMessage({
-          id: 'pages.device.productDetail.metadata.eventDefinition',
-          defaultMessage: '事件定义',
-        })}
-        key="events"
-      >
-        <BaseMetadata type={'events'} />
-      </Tabs.TabPane>
-      <Tabs.TabPane
-        tab={intl.formatMessage({
-          id: 'pages.device.productDetail.metadata.tagDefinition',
-          defaultMessage: '标签定义',
-        })}
-        key="tags"
-      >
-        <BaseMetadata type={'tags'} />
-      </Tabs.TabPane>
-    </Tabs>
+        <Tabs.TabPane
+          tab={intl.formatMessage({
+            id: 'pages.device.productDetail.metadata.propertyDefinition',
+            defaultMessage: '属性定义',
+          })}
+          key="properties"
+        >
+          <BaseMetadata type={'properties'} />
+        </Tabs.TabPane>
+        <Tabs.TabPane
+          tab={intl.formatMessage({
+            id: 'pages.device.productDetail.metadata.functionDefinition',
+            defaultMessage: '功能定义',
+          })}
+          key="functions"
+        >
+          <BaseMetadata type={'functions'} />
+        </Tabs.TabPane>
+        <Tabs.TabPane
+          tab={intl.formatMessage({
+            id: 'pages.device.productDetail.metadata.eventDefinition',
+            defaultMessage: '事件定义',
+          })}
+          key="events"
+        >
+          <BaseMetadata type={'events'} />
+        </Tabs.TabPane>
+        <Tabs.TabPane
+          tab={intl.formatMessage({
+            id: 'pages.device.productDetail.metadata.tagDefinition',
+            defaultMessage: '标签定义',
+          })}
+          key="tags"
+        >
+          <BaseMetadata type={'tags'} />
+        </Tabs.TabPane>
+      </Tabs>
+      <Import visible={visible} close={() => setVisible(false)} />
+    </>
   );
 });
 export default Metadata;

+ 135 - 75
src/pages/device/Product/Detail/index.tsx

@@ -1,37 +1,50 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import { history, useParams } from 'umi';
-import { Button, Card, Descriptions, Space, Tabs, Badge } from 'antd';
+import { Button, Card, Descriptions, Space, Tabs, Badge, message, Spin } from 'antd';
 import BaseInfo from '@/pages/device/Product/Detail/BaseInfo';
 import { observer } from '@formily/react';
 import { productModel, service } from '@/pages/device/Product';
-import { useEffect } from 'react';
+import { useCallback, useEffect, useState } from 'react';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import Metadata from '@/pages/device/Product/Detail/Metadata';
 import Alarm from '@/pages/device/Product/Detail/Alarm';
 import type { DeviceMetadata } from '@/pages/device/Product/typings';
 import DB from '@/db';
+import { Link } from 'umi';
+import { Store } from 'jetlinks-store';
 
 const ProductDetail = observer(() => {
   const intl = useIntl();
+
   const statusMap = {
-    1: (
-      <Badge
-        status="processing"
-        text={intl.formatMessage({
-          id: 'pages.system.tenant.assetInformation.published',
-          defaultMessage: '已发布',
-        })}
-      />
-    ),
-    0: (
-      <Badge
-        status="error"
-        text={intl.formatMessage({
-          id: 'pages.system.tenant.assetInformation.unpublished',
-          defaultMessage: '未发布',
-        })}
-      />
-    ),
+    1: {
+      key: 'disable',
+      name: '停用',
+      action: 'undeploy',
+      component: (
+        <Badge
+          status="processing"
+          text={intl.formatMessage({
+            id: 'pages.system.tenant.assetInformation.published',
+            defaultMessage: '已发布',
+          })}
+        />
+      ),
+    },
+    0: {
+      key: 'enabled',
+      name: '启用',
+      action: 'deploy',
+      component: (
+        <Badge
+          status="error"
+          text={intl.formatMessage({
+            id: 'pages.system.tenant.assetInformation.unpublished',
+            defaultMessage: '未发布',
+          })}
+        />
+      ),
+    },
   };
   const param = useParams<{ id: string }>();
 
@@ -96,71 +109,118 @@ const ProductDetail = observer(() => {
       DB.updateSchema(schema);
     };
   }, [param.id]);
+
+  const [loading, setLoading] = useState<boolean>(false);
+
+  const changeDeploy = useCallback(
+    (state: 'deploy' | 'undeploy') => {
+      setLoading(true);
+      // 似乎没有必要重新获取当前产品信息,暂时做前端数据修改
+      service.changeDeploy(param.id, state).subscribe({
+        next: async () => {
+          const item = productModel.current;
+          // 重新应用的话。就不执行更新操作。
+          if (item) {
+            if (!(item.state === 1 && state === 'deploy')) {
+              item.state = item.state > 0 ? item.state - 1 : item.state + 1;
+            }
+          }
+          productModel.current = item;
+          message.success('操作成功');
+        },
+        error: async () => {
+          message.success('操作失败');
+        },
+        complete: () => {
+          setLoading(false);
+        },
+      });
+    },
+    [param.id],
+  );
+
+  useEffect(() => {
+    const subscription = Store.subscribe('product-deploy', () => {
+      changeDeploy('deploy');
+    });
+    return subscription.unsubscribe;
+  }, [changeDeploy, param.id]);
+
   return (
     <PageContainer
       onBack={() => history.goBack()}
       extraContent={<Space size={24} />}
       content={
-        <Descriptions size="small" column={2}>
-          <Descriptions.Item
-            label={intl.formatMessage({
-              id: 'pages.device.category',
-              defaultMessage: '产品ID',
-            })}
-          >
-            {productModel.current?.id}
-          </Descriptions.Item>
-          <Descriptions.Item
-            label={intl.formatMessage({
-              id: 'pages.table.productName',
-              defaultMessage: '产品名称',
-            })}
-          >
-            {productModel.current?.name}
-          </Descriptions.Item>
-          <Descriptions.Item
-            label={intl.formatMessage({
-              id: 'pages.device.productDetail.classifiedName',
-              defaultMessage: '所属品类',
-            })}
-          >
-            {productModel.current?.classifiedName}
-          </Descriptions.Item>
-          <Descriptions.Item
-            label={intl.formatMessage({
-              id: 'pages.device.productDetail.protocolName',
-              defaultMessage: '消息协议',
-            })}
-          >
-            {productModel.current?.protocolName}
-          </Descriptions.Item>
-          <Descriptions.Item
-            label={intl.formatMessage({
-              id: 'pages.device.productDetail.transportProtocol',
-              defaultMessage: '链接协议',
-            })}
-          >
-            {productModel.current?.transportProtocol}
-          </Descriptions.Item>
-          <Descriptions.Item
-            label={intl.formatMessage({
-              id: 'pages.device.productDetail.createTime',
-              defaultMessage: '创建时间',
-            })}
-          >
-            {productModel.current?.createTime}
-          </Descriptions.Item>
-        </Descriptions>
+        <Spin spinning={loading}>
+          <Descriptions size="small" column={2}>
+            <Descriptions.Item
+              label={intl.formatMessage({
+                id: 'pages.device.category',
+                defaultMessage: '产品ID',
+              })}
+            >
+              {productModel.current?.id}
+            </Descriptions.Item>
+            <Descriptions.Item
+              label={intl.formatMessage({
+                id: 'pages.table.productName',
+                defaultMessage: '产品名称',
+              })}
+            >
+              {productModel.current?.name}
+            </Descriptions.Item>
+            <Descriptions.Item
+              label={intl.formatMessage({
+                id: 'pages.device.productDetail.classifiedName',
+                defaultMessage: '所属品类',
+              })}
+            >
+              {productModel.current?.classifiedName}
+            </Descriptions.Item>
+            <Descriptions.Item
+              label={intl.formatMessage({
+                id: 'pages.device.productDetail.protocolName',
+                defaultMessage: '消息协议',
+              })}
+            >
+              {productModel.current?.protocolName}
+            </Descriptions.Item>
+            <Descriptions.Item
+              label={intl.formatMessage({
+                id: 'pages.device.productDetail.transportProtocol',
+                defaultMessage: '链接协议',
+              })}
+            >
+              {productModel.current?.transportProtocol}
+            </Descriptions.Item>
+            <Descriptions.Item label={'设备数量'}>
+              <Link to={'/device/instance'}> {productModel.current?.count}</Link>
+            </Descriptions.Item>
+            <Descriptions.Item
+              label={intl.formatMessage({
+                id: 'pages.device.productDetail.createTime',
+                defaultMessage: '创建时间',
+              })}
+            >
+              {productModel.current?.createTime}
+            </Descriptions.Item>
+          </Descriptions>
+        </Spin>
       }
       extra={[
-        statusMap[productModel.current?.state || 0],
-        <Button key="2">
+        statusMap[productModel.current?.state || 0].component,
+        <Button
+          key="2"
+          onClick={() => {
+            changeDeploy(statusMap[productModel.current?.state || 0].action);
+          }}
+        >
           {intl.formatMessage({
-            id: 'pages.device.productDetail.disable',
-            defaultMessage: '停用',
+            id: `pages.device.productDetail.${statusMap[productModel.current?.state || 0].key}`,
+            defaultMessage: statusMap[productModel.current?.state || 1].name,
           })}
         </Button>,
-        <Button key="1" type="primary">
+        <Button key="1" type="primary" onClick={() => changeDeploy('deploy')}>
           {intl.formatMessage({
             id: 'pages.device.productDetail.setting',
             defaultMessage: '应用配置',

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

@@ -29,6 +29,7 @@ export const productModel = model<{
 }>({
   current: undefined,
 });
+
 const Product = observer(() => {
   const intl = useIntl();
   const status = {
@@ -124,7 +125,6 @@ const Product = observer(() => {
                   >
                     <div>ID</div>
                     <Typography.Paragraph copyable={{ text: row.id }}>
-                      {' '}
                       {row.id}
                     </Typography.Paragraph>
                   </div>
@@ -204,8 +204,8 @@ const Product = observer(() => {
                 >
                   <a key="download">
                     <DownloadOutlined
-                      onClick={() => {
-                        message.success(
+                      onClick={async () => {
+                        await message.success(
                           `${intl.formatMessage({
                             id: 'pages.data.option.download',
                             defaultMessage: '下载',

+ 12 - 0
src/pages/device/Product/service.ts

@@ -65,6 +65,18 @@ class Service extends BaseService<ProductItem> {
       method: 'PATCH',
       data,
     });
+
+  public changeDeploy = (id: string, state: 'deploy' | 'undeploy') =>
+    defer(() =>
+      from(
+        request(`${this.uri}/${id}/${state}`, {
+          method: 'POST',
+        }),
+      ),
+    ).pipe(
+      filter((resp) => resp.status === 200),
+      map((resp) => resp.result),
+    );
 }
 
 export default Service;