Kaynağa Gözat

feat(metadata): device metadata ui design

Lind 4 yıl önce
ebeveyn
işleme
3555d0ddd7

+ 129 - 0
src/pages/device/Instance/Detail/Config/Tags/index.tsx

@@ -0,0 +1,129 @@
+import { createSchemaField, FormProvider, observer } from '@formily/react';
+import { Editable, FormItem, Input, ArrayTable } from '@formily/antd';
+import { createForm } from '@formily/core';
+import { Card } from 'antd';
+
+const SchemaField = createSchemaField({
+  components: {
+    FormItem,
+    Editable,
+    Input,
+    ArrayTable,
+  },
+});
+const form = createForm();
+
+const schema = {
+  type: 'object',
+  properties: {
+    array: {
+      type: 'array',
+      'x-decorator': 'FormItem',
+      'x-component': 'ArrayTable',
+      'x-component-props': {
+        pagination: { pageSize: 10 },
+        scroll: { x: '100%' },
+      },
+      items: {
+        type: 'object',
+        properties: {
+          column1: {
+            type: 'void',
+            'x-component': 'ArrayTable.Column',
+            'x-component-props': { width: 50, title: '排序', 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: {
+                type: 'string',
+                'x-decorator': 'Editable',
+                'x-component': 'Input',
+              },
+            },
+          },
+          column4: {
+            type: 'void',
+            'x-component': 'ArrayTable.Column',
+            'x-component-props': { width: 200, title: '名称' },
+            properties: {
+              a2: {
+                type: 'string',
+                'x-decorator': 'FormItem',
+                'x-component': 'Input',
+              },
+            },
+          },
+          column5: {
+            type: 'void',
+            'x-component': 'ArrayTable.Column',
+            'x-component-props': { width: 200, title: '值' },
+            properties: {
+              a3: {
+                type: 'string',
+                'x-decorator': 'FormItem',
+                'x-component': 'Input',
+              },
+            },
+          },
+          column6: {
+            type: 'void',
+            'x-component': 'ArrayTable.Column',
+            'x-component-props': {
+              title: '操作',
+              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: '添加标签',
+        },
+      },
+    },
+  },
+};
+
+const Tags = observer(() => {
+  return (
+    <Card title="标签" extra={<a>保存</a>}>
+      <FormProvider form={form}>
+        <SchemaField schema={schema} />
+      </FormProvider>
+    </Card>
+  );
+});
+export default Tags;

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

@@ -1,7 +1,18 @@
-import { Card, Divider } from 'antd';
+import { Card, Divider, Empty } from 'antd';
 import { InstanceModel, service } from '@/pages/device/Instance';
-import { useEffect } from 'react';
-import { observer } from '@formily/react';
+import { useEffect, useState } from 'react';
+import { createSchemaField, observer } 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 } from 'umi';
+import Tags from '@/pages/device/Instance/Detail/Config/Tags';
+
+const componentMap = {
+  string: 'Input',
+  password: 'Password',
+};
 
 const Config = observer(() => {
   useEffect(() => {
@@ -9,13 +20,101 @@ const Config = observer(() => {
       service.getConfigMetadata(InstanceModel.current.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.current?.id;
+
+  useEffect(() => {
+    if (id) {
+      service.getConfigMetadata(id).then((config) => {
+        setMetadata(config?.result);
+      });
+    }
+
+    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 (
+          <Card
+            title={item.name}
+            extra={<a onClick={() => setState(!state)}>{state ? '编辑' : '保存'}</a>}
+          >
+            <PreviewText.Placeholder value="-">
+              <Form form={form}>
+                <FormLayout labelCol={6} wrapperCol={16}>
+                  <SchemaField schema={itemSchema} />
+                </FormLayout>
+              </Form>
+            </PreviewText.Placeholder>
+          </Card>
+        );
+      })
+    ) : (
+      <Empty />
+    );
+  };
+
   return (
     <>
-      <Card title="配置">{JSON.stringify(InstanceModel.config)}</Card>
+      {renderConfigCard()}
       <Divider />
-      <Card title="标签">{JSON.stringify(InstanceModel.detail.tags)}</Card>
+      <Tags />
     </>
   );
 });

+ 3 - 2
src/pages/device/Instance/Detail/Info/index.tsx

@@ -1,8 +1,9 @@
 import { Descriptions } from 'antd';
 import { InstanceModel } from '@/pages/device/Instance';
 import moment from 'moment';
+import { observer } from '@formily/react';
 
-const Info = () => {
+const Info = observer(() => {
   return (
     <>
       <Descriptions size="small" column={3}>
@@ -24,5 +25,5 @@ const Info = () => {
       </Descriptions>
     </>
   );
-};
+});
 export default Info;

+ 99 - 0
src/pages/device/Instance/Detail/Metadata/ItemDetail/index.tsx

@@ -0,0 +1,99 @@
+import { InstanceModel } from '@/pages/device/Instance';
+import { observer } from '@formily/react';
+
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import {
+  Form,
+  FormItem,
+  FormLayout,
+  Input,
+  Select,
+  Cascader,
+  DatePicker,
+  FormGrid,
+  ArrayItems,
+  Editable,
+  Radio,
+} from '@formily/antd';
+import type { MetadataItem } from '@/pages/device/Product/typings';
+
+const ItemDetail = observer(() => {
+  const form = createForm<MetadataItem>({
+    validateFirst: true,
+    initialValues: InstanceModel.metadataItem,
+  });
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      FormGrid,
+      FormLayout,
+      Input,
+      DatePicker,
+      Cascader,
+      Select,
+      ArrayItems,
+      Editable,
+      Radio,
+    },
+  });
+
+  const schema = {
+    type: 'object',
+    properties: {
+      id: {
+        type: 'string',
+        title: '属性标识',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+      },
+      name: {
+        type: 'string',
+        title: '属性名称',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+      },
+      dataType: {
+        type: 'string',
+        title: '数据类型',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+      },
+      readOnly: {
+        type: 'string',
+        title: '只读',
+        enum: [
+          {
+            label: '是',
+            value: 1,
+          },
+          {
+            label: '否',
+            value: 2,
+          },
+        ],
+        'x-decorator': 'FormItem',
+        'x-component': 'Radio.Group',
+      },
+      description: {
+        type: 'string',
+        required: true,
+        title: '描述',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input.TextArea',
+      },
+    },
+  };
+  return (
+    <>
+      <Form form={form} labelCol={5} wrapperCol={16} onAutoSubmit={console.log} size="small">
+        <SchemaField schema={schema} />
+      </Form>
+    </>
+  );
+});
+
+export default ItemDetail;

+ 53 - 0
src/pages/device/Instance/Detail/Metadata/ItemList/index.tsx

@@ -0,0 +1,53 @@
+import { MetadataItem } from '@/pages/device/Product/typings';
+import { InstanceModel } from '@/pages/device/Instance';
+import { Popconfirm, Tooltip } from 'antd';
+import { CloseCircleOutlined } from '@ant-design/icons';
+import ProList from '@jetlinks/pro-list';
+
+interface Props {
+  metadata: Partial<MetadataItem>;
+}
+
+const ItemList = (props: Props) => {
+  const { metadata } = props;
+  return (
+    <ProList
+      rowKey="id"
+      bordered={true}
+      showActions="hover"
+      onRow={(record: MetadataItem) => {
+        return {
+          onClick: () => {
+            InstanceModel.metadataItem = record;
+          },
+        };
+      }}
+      dataSource={metadata.properties}
+      metas={{
+        title: {
+          dataIndex: 'name',
+        },
+        id: {
+          dataIndex: 'id',
+        },
+        actions: {
+          render: (text, row) => [
+            <Popconfirm
+              onConfirm={() => {
+                console.log(row);
+              }}
+              title="确认删除?"
+            >
+              <Tooltip title="删除">
+                <a>
+                  <CloseCircleOutlined />
+                </a>
+              </Tooltip>
+            </Popconfirm>,
+          ],
+        },
+      }}
+    ></ProList>
+  );
+};
+export default ItemList;

+ 126 - 0
src/pages/device/Instance/Detail/Metadata/ItemParam/index.tsx

@@ -0,0 +1,126 @@
+import { createSchemaField, observer } from '@formily/react';
+import type { IFieldState } from '@formily/core';
+import { createForm } from '@formily/core';
+import {
+  FormItem,
+  Form,
+  FormGrid,
+  FormLayout,
+  Input,
+  DatePicker,
+  Cascader,
+  Select,
+  ArrayItems,
+  Editable,
+  Radio,
+} from '@formily/antd';
+import { service } from '@/pages/device/Instance';
+import type { Unit } from '@/pages/device/Instance/typings';
+import type { ISchema } from '@formily/json-schema';
+
+const ItemParam = observer(() => {
+  const form = createForm({
+    validateFirst: true,
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      FormGrid,
+      FormLayout,
+      Input,
+      DatePicker,
+      Cascader,
+      Select,
+      ArrayItems,
+      Editable,
+      Radio,
+    },
+    scope: {
+      fetchUnits: async (field: IFieldState) => {
+        const unit = await service.getUnits();
+        // eslint-disable-next-line no-param-reassign
+        field.dataSource = unit.result?.map((item: Unit) => ({
+          label: item.text,
+          value: item.id,
+        }));
+      },
+    },
+  });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      id: {
+        type: 'string',
+        title: '参数标识',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+      },
+      name: {
+        type: 'string',
+        title: '参数名称',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+      },
+      type: {
+        type: 'string',
+        title: '数据类型',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Select',
+        enum: [
+          { label: 'int(整数型)', value: 'int' },
+          { label: 'long(长整数型)', value: 'long' },
+          { label: 'double(双精度浮点数)', value: 'double' },
+          { label: 'float(单精度浮点数)', value: 'float' },
+          { label: 'text(字符串)', value: 'string' },
+          { label: 'bool(布尔型)', value: 'boolean' },
+          { label: 'date(时间型)', value: 'date' },
+          { label: 'enum(枚举)', value: 'enum' },
+          { label: 'array(数组)', value: 'array' },
+          { label: 'object(结构体)', value: 'object' },
+          { label: 'geoPoint(地理位置)', value: 'geoPoint' },
+        ],
+      },
+      scale: {
+        type: 'string',
+        title: '精度',
+        'x-display': 'none',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-reactions': {
+          dependencies: ['type'],
+          fulfill: {
+            state: {
+              display: '{{$deps[0]==="int"?"visible"∑:"none"}}',
+            },
+          },
+        },
+      },
+      unit: {
+        type: 'string',
+        title: '单位',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Select',
+        'x-reactions': '{{fetchUnits}}',
+      },
+      description: {
+        type: 'string',
+        title: '描述',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input.TextArea',
+      },
+    },
+  };
+  return (
+    <Form form={form} labelCol={8} wrapperCol={13} size="small">
+      <SchemaField schema={schema} />
+    </Form>
+  );
+});
+export default ItemParam;

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

@@ -1,7 +1,92 @@
 import { observer } from '@formily/react';
+import ProCard from '@ant-design/pro-card';
+import { Col, Input, Row } from 'antd';
 import { InstanceModel } from '@/pages/device/Instance';
+import type { DeviceMetadata } from '@/pages/device/Product/typings';
+import ItemList from '@/pages/device/Instance/Detail/Metadata/ItemList';
+import ItemDetail from '@/pages/device/Instance/Detail/Metadata/ItemDetail';
+import ItemParam from '@/pages/device/Instance/Detail/Metadata/ItemParam';
 
 const Metadata = observer(() => {
-  return <div>{JSON.stringify(JSON.parse(InstanceModel.detail.metadata as string))}</div>;
+  const metadata = JSON.parse(InstanceModel.detail.metadata as string) as DeviceMetadata;
+  return (
+    <ProCard
+      tabs={{
+        tabPosition: 'left',
+      }}
+    >
+      <ProCard.TabPane tab="属性" key="property" style={{ overflowX: 'auto' }}>
+        <ProCard gutter={[16, 16]} style={{ height: '50vh' }}>
+          <ProCard
+            bordered={true}
+            colSpan={5}
+            extra={
+              <Row>
+                <Col span={18}>
+                  <Input.Search size="small" />
+                </Col>
+                <Col span={2} />
+                <Col span={4} style={{ alignItems: 'center' }}>
+                  <a>新增</a>
+                </Col>
+              </Row>
+            }
+            style={{ height: '40vh', marginRight: 10 }}
+          >
+            <ItemList metadata={metadata} />
+          </ProCard>
+          <ProCard
+            extra={<a>保存</a>}
+            bordered={true}
+            colSpan={7}
+            style={{ height: '40vh', marginRight: 10 }}
+          >
+            <ItemDetail />
+          </ProCard>
+          <ProCard
+            extra={<a>保存</a>}
+            bordered={true}
+            colSpan={7}
+            style={{ height: '40vh', marginRight: 10 }}
+          >
+            <ItemParam />
+          </ProCard>
+          <ProCard
+            extra={<a>保存</a>}
+            bordered={true}
+            colSpan={7}
+            style={{ height: '40vh', marginRight: 10 }}
+          >
+            <ItemParam />
+          </ProCard>
+          <ProCard
+            extra={<a>保存</a>}
+            bordered={true}
+            colSpan={7}
+            style={{ height: '40vh', marginRight: 10 }}
+          >
+            <ItemParam />
+          </ProCard>
+          <ProCard
+            extra={<a>保存</a>}
+            bordered={true}
+            colSpan={7}
+            style={{ height: '40vh', marginRight: 10, display: '' }}
+          >
+            <ItemParam />
+          </ProCard>
+        </ProCard>
+      </ProCard.TabPane>
+      <ProCard.TabPane tab="事件" key="events">
+        事件
+      </ProCard.TabPane>
+      <ProCard.TabPane tab="功能" key="functions">
+        功能
+      </ProCard.TabPane>
+      <ProCard.TabPane tab="标签" key="tags">
+        标签
+      </ProCard.TabPane>
+    </ProCard>
+  );
 });
 export default Metadata;

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

@@ -1,11 +1,55 @@
 import { InstanceModel } from '@/pages/device/Instance';
+import ProCard from '@ant-design/pro-card';
+import { SyncOutlined } from '@ant-design/icons';
+import { Badge, Col, message, Row } from 'antd';
+import type { DeviceMetadata } from '@/pages/device/Product/typings';
 
 const Running = () => {
+  const metadata = JSON.parse(InstanceModel.detail.metadata as string) as DeviceMetadata;
   return (
-    <div>
-      运行状态
-      {JSON.stringify(JSON.parse(InstanceModel.detail.metadata as string))}
-    </div>
+    <ProCard style={{ marginTop: 8 }} gutter={[16, 16]} wrap>
+      <ProCard
+        title="设备状态"
+        extra={<SyncOutlined onClick={() => message.success('刷新')} />}
+        layout="default"
+        bordered
+        headerBordered
+        colSpan={{ xs: 12, sm: 8, md: 6, lg: 6, xl: 6 }}
+      >
+        <div style={{ height: 60 }}>
+          <Row gutter={[16, 16]}>
+            <Col span={24}>
+              <Badge status="success" text={<span style={{ fontSize: 25 }}>在线</span>} />
+            </Col>
+            <Col span={24}>上线时间: 2021-8-20 12:20:33</Col>
+          </Row>
+        </div>
+      </ProCard>
+      {metadata.properties.map((item) => (
+        <ProCard
+          title={item.name}
+          extra={<SyncOutlined onClick={() => message.success('刷新')} />}
+          layout="center"
+          bordered
+          headerBordered
+          colSpan={{ xs: 12, sm: 8, md: 6, lg: 6, xl: 6 }}
+        >
+          <div style={{ height: 60 }}>{`${item.name}-属性`}</div>
+        </ProCard>
+      ))}
+      {metadata.events.map((item) => (
+        <ProCard
+          title={item.name}
+          extra={<SyncOutlined onClick={() => message.success('刷新')} />}
+          layout="center"
+          bordered
+          headerBordered
+          colSpan={{ xs: 12, sm: 8, md: 6, lg: 6, xl: 6 }}
+        >
+          <div style={{ height: 60 }}>{`${item.name}-事件`}</div>
+        </ProCard>
+      ))}
+    </ProCard>
   );
 };
 export default Running;

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

@@ -73,7 +73,7 @@ const InstanceDetail = observer(() => {
 
   return (
     <PageContainer
-      onBack={() => history.goBack()}
+      onBack={history.goBack}
       onTabChange={setTab}
       tabList={list}
       content={<Info />}

+ 3 - 0
src/pages/device/Instance/index.tsx

@@ -16,6 +16,7 @@ import { useIntl } from '@@/plugin-locale/localeExports';
 import { CurdModel } from '@/components/BaseCrud/model';
 import { model } from '@formily/reactive';
 import Service from '@/pages/device/Instance/service';
+import type { MetadataItem } from '@/pages/device/Product/typings';
 
 const statusMap = new Map();
 statusMap.set('在线', 'success');
@@ -29,10 +30,12 @@ export const InstanceModel = model<{
   current: DeviceInstance | undefined;
   detail: Partial<DeviceInstance>;
   config: any;
+  metadataItem: MetadataItem;
 }>({
   current: undefined,
   detail: {},
   config: {},
+  metadataItem: {},
 });
 export const service = new Service('device/instance');
 const Instance = () => {

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

@@ -1,12 +1,15 @@
 import BaseService from '@/utils/BaseService';
 import { request } from 'umi';
 import type { DeviceInstance } from '@/pages/device/Instance/typings';
+import SystemConst from '@/utils/const';
 
 class Service extends BaseService<DeviceInstance> {
   public detail = (id: string) => request(`${this.uri}/${id}/detail`, { method: 'GET' });
 
   public getConfigMetadata = (id: string) =>
     request(`${this.uri}/${id}/config-metadata`, { method: 'GET' });
+
+  public getUnits = () => request(`/${SystemConst.API_BASE}/protocol/units`, { method: 'GET' });
 }
 
 export default Service;

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

@@ -37,3 +37,13 @@ export type DeviceInstance = {
   onlineTime: string | number;
   tags: any;
 };
+
+type Unit = {
+  id: string;
+  name: string;
+  symbol: string;
+  text: string;
+  type: string;
+  value: string;
+  description: string;
+};

+ 61 - 0
src/pages/device/Product/typings.d.ts

@@ -37,3 +37,64 @@ export type ConfigMetadata = {
   scopes: any[];
   properties: ConfigProperty[];
 };
+
+export type DeviceMetadata = {
+  events: Partial<EventMetadata>[];
+  properties: Partial<PropertyMetadata>[];
+  functions: Partial<FunctionMetadata>[];
+  tags: Partial<TagMetadata>[];
+};
+export type MetadataItem = Partial<EventMetadata | PropertyMetadata | FunctionMetadata> &
+  Record<string, any>;
+
+type EventMetadata = {
+  id: string;
+  name: string;
+  expands?: {
+    eventType?: string;
+    level?: string;
+  } & Record<string, any>;
+  valueType: {
+    type: string;
+    properties: {
+      id: string;
+      name: string;
+      dataType: string;
+      valueType: {
+        type: string;
+      } & Record<any, any>;
+    }[];
+  };
+  description: string;
+};
+type FunctionMetadata = {
+  id: string;
+  name: string;
+  async: boolean;
+  output: Record<any, any>;
+  inputs: ({
+    id: string;
+    name: string;
+    valueType: {
+      type: string;
+    } & Record<any, any>;
+  } & Record<string, any>)[];
+};
+type PropertyMetadata = {
+  id: string;
+  name: string;
+  dataType?: string;
+  valueType: {
+    type: string;
+  } & Record<any, any>;
+  expands: Record<string, any>;
+  description?: string;
+};
+type TagMetadata = {
+  id: string;
+  name: string;
+  valueType: {
+    type: string;
+  } & Record<string, any>;
+  expands: Record<string, any>;
+};

+ 1 - 1
src/pages/user/Login/index.tsx

@@ -127,7 +127,7 @@ const Login: React.FC = () => {
   };
 
   const doLogin = async (data: LoginParam) =>
-    Service.login({ verifyKey: captcha.key, ...data }).subscribe({
+    Service.login({ expires: -1, verifyKey: captcha.key, ...data }).subscribe({
       next: async (userInfo: UserInfo) => {
         message.success(
           intl.formatMessage({