Przeglądaj źródła

fix(merge): merge

xieyonghong 3 lat temu
rodzic
commit
dab4c8aa53

+ 1 - 1
package.json

@@ -70,7 +70,7 @@
     "@formily/reactive-react": "2.0.0-rc.17",
     "@formily/shared": "2.0.0-rc.17",
     "@jetlinks/pro-list": "^1.10.8",
-    "@jetlinks/pro-table": "^2.63.10",
+    "@jetlinks/pro-table": "^2.63.11",
     "@types/react-syntax-highlighter": "^13.5.2",
     "@umijs/route-utils": "^1.0.36",
     "ahooks": "^2.10.9",

+ 2 - 2
src/components/BaseCrud/index.tsx

@@ -1,11 +1,12 @@
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { Button, Dropdown } from 'antd';
+import type { ActionType, ProColumns, RequestData } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
-import type { ProColumns, ActionType, RequestData } from '@jetlinks/pro-table';
 
 import { EllipsisOutlined, PlusOutlined } from '@ant-design/icons';
 import type BaseService from '@/utils/BaseService';
 import * as React from 'react';
+import { useRef, useState } from 'react';
 import Save from '@/components/BaseCrud/save';
 import type { ISchema } from '@formily/json-schema';
 import { CurdModel } from '@/components/BaseCrud/model';
@@ -14,7 +15,6 @@ import type { ModalProps } from 'antd/lib/modal/Modal';
 import type { TablePaginationConfig } from 'antd/lib/table/interface';
 import type { Form } from '@formily/core';
 import SearchComponent from '@/components/SearchComponent';
-import { useRef, useState } from 'react';
 import type { ProFormInstance } from '@ant-design/pro-form';
 import type { SearchConfig } from '@ant-design/pro-form/lib/components/Submitter';
 

+ 1 - 0
src/components/SearchComponent/index.tsx

@@ -170,6 +170,7 @@ const SearchComponent = <T extends Record<string, any>>(props: Props<T>) => {
   const filterSearchTerm = (): EnumData[] =>
     field
       .filter((item) => item.dataIndex)
+      .filter((item) => !item.hideInSearch)
       .filter((item) => !['index', 'option'].includes(item.dataIndex as string))
       .map((i) => ({ label: i.title, value: i.dataIndex } as EnumData));
 

+ 171 - 0
src/pages/device/Instance/Detail/ChildDevice/BindChildDevice/index.tsx

@@ -0,0 +1,171 @@
+import { Badge, Button, Modal } from 'antd';
+import type { DeviceInstance } from '@/pages/device/Instance/typings';
+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 { useIntl } from 'umi';
+import moment from 'moment';
+
+interface Props {
+  visible: boolean;
+  data: Partial<DeviceInstance>;
+  onCancel: () => void;
+}
+
+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>({});
+  const [bindKeys, setBindKeys] = useState<any[]>([]);
+
+  const columns: ProColumns<DeviceInstance>[] = [
+    {
+      dataIndex: 'index',
+      valueType: 'indexBorder',
+      width: 48,
+    },
+    {
+      title: 'ID',
+      dataIndex: 'id',
+    },
+    {
+      title: '设备名称',
+      dataIndex: 'name',
+    },
+    {
+      title: '所属产品',
+      dataIndex: 'productName',
+    },
+    {
+      title: '注册时间',
+      dataIndex: 'registryTime',
+      width: '200px',
+      render: (text: any) => (!!text ? moment(text).format('YYYY-MM-DD HH:mm:ss') : '/'),
+      sorter: true,
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      renderText: (record) =>
+        record ? <Badge status={statusMap.get(record.value)} text={record.text} /> : '',
+      valueType: 'select',
+      valueEnum: {
+        notActive: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.notActive',
+            defaultMessage: '未启用',
+          }),
+          value: 'notActive',
+        },
+        offline: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.offLine',
+            defaultMessage: '离线',
+          }),
+          value: 'offline',
+        },
+        online: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.onLine',
+            defaultMessage: '在线',
+          }),
+          value: 'online',
+        },
+      },
+    },
+  ];
+
+  const submitBtn = async () => {
+    const resp = await service.bindDevice(InstanceModel.detail.id!, bindKeys);
+    if (resp.status === 200) {
+      props.onCancel();
+      setBindKeys([]);
+      actionRef.current?.reset?.();
+    }
+  };
+
+  return (
+    <Modal
+      title="绑定子设备"
+      visible={visible}
+      width={1000}
+      onOk={() => {
+        submitBtn();
+      }}
+      onCancel={() => {
+        props.onCancel();
+        setBindKeys([]);
+      }}
+      footer={[
+        <Button
+          key="back"
+          onClick={() => {
+            props.onCancel();
+            setBindKeys([]);
+            actionRef.current?.reset?.();
+          }}
+        >
+          取消
+        </Button>,
+        <Button
+          disabled={!(bindKeys.length > 0)}
+          key="submit"
+          type="primary"
+          onClick={() => {
+            submitBtn();
+          }}
+        >
+          确认
+        </Button>,
+      ]}
+    >
+      <SearchComponent<DeviceInstance>
+        field={[...columns]}
+        target="child-device-bind"
+        pattern={'simple'}
+        defaultParam={[{ column: 'parentId$isnull', value: '1' }]}
+        onSearch={(param) => {
+          actionRef.current?.reset?.();
+          setSearchParams(param);
+        }}
+        onReset={() => {
+          // 重置分页及搜索参数
+          actionRef.current?.reset?.();
+          setSearchParams({});
+        }}
+      />
+      <ProTable<DeviceInstance>
+        search={false}
+        columns={columns}
+        size="small"
+        rowSelection={{
+          selectedRowKeys: bindKeys,
+          onChange: (selectedRowKeys, selectedRows) => {
+            setBindKeys(selectedRows.map((item) => item.id));
+          },
+        }}
+        actionRef={actionRef}
+        params={searchParams}
+        rowKey="id"
+        toolBarRender={false}
+        pagination={{
+          pageSize: 10,
+        }}
+        request={(params) => service.query({ ...params })}
+      />
+    </Modal>
+  );
+};
+export default BindChildDevice;

+ 199 - 0
src/pages/device/Instance/Detail/ChildDevice/index.tsx

@@ -0,0 +1,199 @@
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
+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 { useRef, useState } from 'react';
+import SearchComponent from '@/components/SearchComponent';
+import BindChildDevice from './BindChildDevice';
+import moment from 'moment';
+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>({});
+  const [bindKeys, setBindKeys] = useState<any[]>([]);
+
+  const unBindSingleDevice = async (id: string) => {
+    const resp = await service.unbindDevice(InstanceModel.detail.id!, id, {});
+    if (resp.status === 200) {
+      actionRef.current?.reset?.();
+      message.success('操作成功!');
+    }
+  };
+
+  const columns: ProColumns<LogItem>[] = [
+    {
+      dataIndex: 'index',
+      valueType: 'indexBorder',
+      width: 48,
+    },
+    {
+      title: 'ID',
+      dataIndex: 'id',
+    },
+    {
+      title: '设备名称',
+      dataIndex: 'name',
+    },
+    {
+      title: '所属产品',
+      dataIndex: 'productName',
+    },
+    {
+      title: '注册时间',
+      dataIndex: 'registryTime',
+      width: '200px',
+      render: (text: any) => (!!text ? moment(text).format('YYYY-MM-DD HH:mm:ss') : '/'),
+      sorter: true,
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      renderText: (record) =>
+        record ? <Badge status={statusMap.get(record.value)} text={record.text} /> : '',
+      valueType: 'select',
+      valueEnum: {
+        notActive: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.notActive',
+            defaultMessage: '未启用',
+          }),
+          value: 'notActive',
+        },
+        offline: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.offLine',
+            defaultMessage: '离线',
+          }),
+          value: 'offline',
+        },
+        online: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.onLine',
+            defaultMessage: '在线',
+          }),
+          value: 'online',
+        },
+      },
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [
+        <Link to={`/device/instance/detail/${record.id}`} key="link">
+          <Tooltip
+            title={intl.formatMessage({
+              id: 'pages.data.option.detail',
+              defaultMessage: '查看',
+            })}
+            key={'detail'}
+          >
+            <SearchOutlined />
+          </Tooltip>
+        </Link>,
+        <a key="unbind">
+          <Popconfirm
+            onConfirm={() => {
+              unBindSingleDevice(record.id);
+            }}
+            title={'确认解绑吗?'}
+          >
+            <DisconnectOutlined />
+          </Popconfirm>
+        </a>,
+      ],
+    },
+  ];
+
+  return (
+    <Card>
+      <SearchComponent<LogItem>
+        field={[...columns]}
+        target="child-device"
+        pattern={'simple'}
+        defaultParam={[
+          { column: 'parentId', value: InstanceModel?.detail?.id || '', termType: 'eq' },
+        ]}
+        onSearch={(param) => {
+          actionRef.current?.reset?.();
+          setSearchParams(param);
+        }}
+        onReset={() => {
+          // 重置分页及搜索参数
+          actionRef.current?.reset?.();
+          setSearchParams({});
+        }}
+      />
+      <ProTable<LogItem>
+        search={false}
+        columns={columns}
+        size="small"
+        actionRef={actionRef}
+        params={searchParams}
+        rowKey="id"
+        rowSelection={{
+          selectedRowKeys: bindKeys,
+          onChange: (selectedRowKeys, selectedRows) => {
+            setBindKeys(selectedRows.map((item) => item.id));
+          },
+        }}
+        toolBarRender={() => [
+          <Button
+            onClick={() => {
+              setVisible(true);
+              actionRef.current?.reset?.();
+            }}
+            key="bind"
+            type="primary"
+          >
+            绑定
+          </Button>,
+          <Popconfirm
+            key="unbind"
+            onConfirm={async () => {
+              const resp = await service.unbindBatchDevice(InstanceModel.detail.id!, bindKeys);
+              if (resp.status === 200) {
+                message.success('操作成功!');
+                setBindKeys([]);
+                actionRef.current?.reset?.();
+              }
+            }}
+            title={'确认解绑吗?'}
+          >
+            <Button>批量解绑</Button>
+          </Popconfirm>,
+        ]}
+        pagination={{
+          pageSize: 10,
+        }}
+        request={(params) => service.query(params)}
+      />
+      <BindChildDevice
+        visible={visible}
+        data={{}}
+        onCancel={() => {
+          setVisible(false);
+        }}
+      />
+    </Card>
+  );
+};
+export default ChildDevice;

+ 12 - 0
src/pages/device/Instance/Detail/ChildDevice/typings.d.ts

@@ -0,0 +1,12 @@
+export type LogItem = {
+  id: string;
+  deviceId: string;
+  productId: string;
+  timestamp: number;
+  type: {
+    text: string;
+    value: string;
+  };
+  content: string;
+  createTime: number;
+};

+ 49 - 29
src/pages/device/Instance/Detail/Log/index.tsx

@@ -1,22 +1,26 @@
-import type { ProColumns } from '@jetlinks/pro-table';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
 import type { LogItem } from '@/pages/device/Instance/Detail/Log/typings';
-import { Modal, Tooltip } from 'antd';
-import { EyeOutlined } from '@ant-design/icons';
+import { Card, Modal, Tooltip } from 'antd';
+import { SearchOutlined } from '@ant-design/icons';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { InstanceModel, service } from '@/pages/device/Instance';
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
+import SearchComponent from '@/components/SearchComponent';
 
 const Log = () => {
   const intl = useIntl();
 
   const [type, setType] = useState<any>({});
+  const actionRef = useRef<ActionType>();
+  const [searchParams, setSearchParams] = useState<any>({});
+
   useEffect(() => {
     service.getLogType().then((resp) => {
       if (resp.status === 200) {
-        const list = (resp.result as { text: string; value: string }[]).reduce(
+        const list = (resp.result as { text: string; type: string }[]).reduce(
           (previousValue, currentValue) => {
-            previousValue[currentValue.value] = currentValue;
+            previousValue[currentValue.type] = currentValue;
             return previousValue;
           },
           {},
@@ -25,6 +29,7 @@ const Log = () => {
       }
     });
   }, []);
+
   const columns: ProColumns<LogItem>[] = [
     {
       dataIndex: 'index',
@@ -35,6 +40,7 @@ const Log = () => {
       title: '类型',
       dataIndex: 'type',
       renderText: (text) => text.text,
+      valueType: 'select',
       valueEnum: type,
     },
     {
@@ -46,17 +52,11 @@ const Log = () => {
       hideInSearch: true,
     },
     {
-      title: '时间',
-      dataIndex: 'timestamp',
-      defaultSortOrder: 'descend',
-      valueType: 'dateTimeRange',
-      sorter: true,
-      hideInTable: true,
-      search: {
-        transform: (value) => ({
-          timestamp$BTW: value.toString(),
-        }),
-      },
+      title: '内容',
+      dataIndex: 'content',
+      valueType: 'option',
+      ellipsis: true,
+      render: (text, record) => <span>{String(record.content)}</span>,
     },
     {
       title: intl.formatMessage({
@@ -79,7 +79,7 @@ const Log = () => {
             onClick={() => Modal.info({ title: '详细信息', content: <pre>{content}</pre> })}
           >
             <Tooltip title="查看">
-              <EyeOutlined />
+              <SearchOutlined />
             </Tooltip>
           </a>,
         ];
@@ -88,17 +88,37 @@ const Log = () => {
   ];
 
   return (
-    <ProTable<LogItem>
-      columns={columns}
-      defaultParams={{
-        deviceId: InstanceModel.detail.id,
-      }}
-      rowKey="id"
-      pagination={{
-        pageSize: 10,
-      }}
-      request={(params) => service.queryLog(InstanceModel.detail.id!, params)}
-    />
+    <Card>
+      <SearchComponent<LogItem>
+        field={[...columns]}
+        target="logs"
+        pattern={'simple'}
+        onSearch={(param) => {
+          actionRef.current?.reset?.();
+          setSearchParams(param);
+        }}
+        onReset={() => {
+          // 重置分页及搜索参数
+          actionRef.current?.reset?.();
+          setSearchParams({});
+        }}
+      />
+      <ProTable<LogItem>
+        search={false}
+        columns={columns}
+        size="small"
+        actionRef={actionRef}
+        params={searchParams}
+        toolBarRender={false}
+        rowKey="id"
+        pagination={{
+          pageSize: 10,
+        }}
+        request={async (params) => {
+          return service.queryLog(InstanceModel.detail.id!, params);
+        }}
+      />
+    </Card>
   );
 };
 export default Log;

+ 0 - 5
src/pages/device/Instance/Detail/MetadataLog/columns.ts

@@ -3,11 +3,6 @@ import moment from 'moment';
 
 const columns: ProColumns<MetadataLogData>[] = [
   {
-    dataIndex: 'index',
-    valueType: 'indexBorder',
-    width: 48,
-  },
-  {
     dataIndex: 'timestamp',
     title: '时间',
     sorter: true,

+ 0 - 82
src/pages/device/Instance/Detail/Running/Event.tsx

@@ -1,82 +0,0 @@
-import { SyncOutlined, UnorderedListOutlined } from '@ant-design/icons';
-import { Badge, Divider, Tooltip } from 'antd';
-import ProCard from '@ant-design/pro-card';
-import type { EventMetadata, ObserverMetadata } from '@/pages/device/Product/typings';
-import { useEffect, useRef, useState } from 'react';
-import { service } from '@/pages/device/Instance';
-import { useParams } from 'umi';
-import EventLog from '@/pages/device/Instance/Detail/MetadataLog/Event';
-
-interface Props {
-  data: Partial<EventMetadata> & ObserverMetadata;
-}
-
-const eventLevel = new Map();
-eventLevel.set('ordinary', <Badge status="processing" text="普通" />);
-eventLevel.set('warn', <Badge status="warning" text="警告" />);
-eventLevel.set('urgent', <Badge status="error" text="紧急" />);
-
-const Event = (props: Props) => {
-  const { data } = props;
-  const params = useParams<{ id: string }>();
-
-  const [count, setCount] = useState<number>(0);
-  const cacheCount = useRef<number>(count);
-
-  const [loading, setLoading] = useState<boolean>(false);
-  const initCount = () => {
-    setLoading(true);
-    if (data.id) {
-      service
-        .getEventCount(params.id, data.id, {
-          format: true,
-          pageSize: 1,
-        })
-        .then((resp) => {
-          if (resp.status === 200) {
-            setCount(resp.result?.total);
-            cacheCount.current = resp.result?.total;
-          }
-        })
-        .finally(() => setLoading(false));
-    }
-  };
-  useEffect(() => {
-    initCount();
-    data.subscribe((payload: unknown) => {
-      if (payload) {
-        cacheCount.current = cacheCount.current + 1;
-        setCount(cacheCount.current);
-      }
-    });
-  }, [data.id, params.id]);
-  const [visible, setVisible] = useState<boolean>(false);
-
-  return (
-    <ProCard
-      title={`${data.name}: ${count}`}
-      extra={
-        <>
-          <SyncOutlined onClick={initCount} />
-          <Divider type="vertical" />
-          <Tooltip placement="top" title="详情">
-            <UnorderedListOutlined
-              onClick={() => {
-                setVisible(true);
-              }}
-            />
-          </Tooltip>
-        </>
-      }
-      loading={loading}
-      layout="center"
-      bordered
-      headerBordered
-      colSpan={{ xs: 12, sm: 8, md: 6, lg: 6, xl: 6 }}
-    >
-      <div style={{ height: 60 }}>{eventLevel.get(data.expands?.level || 'warn')}</div>
-      <EventLog visible={visible} close={() => setVisible(false)} data={data} />
-    </ProCard>
-  );
-};
-export default Event;

+ 126 - 0
src/pages/device/Instance/Detail/Running/Event/index.tsx

@@ -0,0 +1,126 @@
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
+import { service } from '@/pages/device/Instance';
+import { useParams } from 'umi';
+import type { EventMetadata } from '@/pages/device/Product/typings';
+import SearchComponent from '@/components/SearchComponent';
+import moment from 'moment';
+import { Form, Modal } from 'antd';
+import { SearchOutlined } from '@ant-design/icons';
+import { useRef, useState } from 'react';
+import MonacoEditor from 'react-monaco-editor';
+
+interface Props {
+  data: Partial<EventMetadata>;
+}
+
+const EventLog = (props: Props) => {
+  const params = useParams<{ id: string }>();
+  const { data } = props;
+  const actionRef = useRef<ActionType>();
+  const [searchParams, setSearchParams] = useState<any>({});
+
+  const columns: ProColumns<MetadataLogData>[] = [
+    {
+      dataIndex: 'timestamp',
+      title: '时间',
+      sorter: true,
+      renderText: (text: string) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
+    },
+    {
+      dataIndex: 'option',
+      title: '操作',
+      render: (text, record) => [
+        <a
+          key={'option'}
+          onClick={() => {
+            for (const i in record) {
+              if (i.indexOf('_format') != -1) {
+                delete record[i];
+              }
+            }
+            Modal.info({
+              title: '详情',
+              width: 850,
+              content: (
+                <Form.Item wrapperCol={{ span: 22 }} labelCol={{ span: 2 }} label={data.name}>
+                  <MonacoEditor
+                    height={350}
+                    theme="vs"
+                    language="json"
+                    value={JSON.stringify(record, null, 2)}
+                  />
+                </Form.Item>
+              ),
+              okText: '关闭',
+              onOk() {},
+            });
+          }}
+        >
+          <SearchOutlined />
+        </a>,
+      ],
+    },
+  ];
+
+  const createColumn = (): ProColumns[] =>
+    data.valueType?.type === 'object'
+      ? data.valueType.properties.map(
+          (i: any) =>
+            ({
+              key: i.id,
+              title: i.name,
+              dataIndex: i.id,
+              renderText: (text) => (typeof text === 'object' ? JSON.stringify(text) : text),
+            } as ProColumns),
+        )
+      : [
+          {
+            title: '数据',
+            dataIndex: 'value',
+            ellipsis: true,
+            // render: (text) => text ? JSON.stringify(text) : ''
+          },
+        ];
+
+  return (
+    <div>
+      <SearchComponent<any>
+        field={[...createColumn(), ...columns]}
+        target="events"
+        pattern={'simple'}
+        onSearch={(param) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setSearchParams(param);
+        }}
+        onReset={() => {
+          // 重置分页及搜索参数
+          actionRef.current?.reset?.();
+          setSearchParams({});
+        }}
+      />
+      <ProTable
+        size="small"
+        rowKey="id"
+        actionRef={actionRef}
+        search={false}
+        params={searchParams}
+        toolBarRender={false}
+        request={async (param) => {
+          param.pageIndex = param.current - 1;
+          delete param.current;
+          delete param.total;
+          return service.getEventCount(params.id, data.id!, {
+            ...param,
+          });
+        }}
+        pagination={{
+          pageSize: 10,
+        }}
+        columns={[...createColumn(), ...columns]}
+      />
+    </div>
+  );
+};
+export default EventLog;

+ 13 - 7
src/pages/device/Instance/Detail/Running/Property/index.tsx

@@ -79,25 +79,31 @@ const Property = (props: Props) => {
       title: '操作',
       key: 'action',
       render: (text: any, record: any) => (
-        <Space size="middle" style={{ color: '#1d39c4' }}>
+        <Space size="middle">
           {(record.expands?.readOnly === false || record.expands?.readOnly === 'false') && (
-            <EditOutlined
+            <a
               onClick={() => {
                 setVisible(true);
               }}
-            />
+            >
+              <EditOutlined />
+            </a>
           )}
-          <SyncOutlined
+          <a
             onClick={() => {
               refreshProperty(record?.id);
             }}
-          />
-          <UnorderedListOutlined
+          >
+            <SyncOutlined />
+          </a>
+          <a
             onClick={() => {
               setCurrentInfo(record);
               setInfoVisible(true);
             }}
-          />
+          >
+            <UnorderedListOutlined />
+          </a>
         </Space>
       ),
     },

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

@@ -1,185 +1,23 @@
 import { InstanceModel } from '@/pages/device/Instance';
 import { Card, Tabs } from 'antd';
 import type { DeviceMetadata } from '@/pages/device/Product/typings';
-// import { useIntl } from '@@/plugin-locale/localeExports';
-import { useEffect } from 'react';
 import Property from '@/pages/device/Instance/Detail/Running/Property';
-// import Event from '@/pages/device/Instance/Detail/Running/Event';
-// import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
-// import { map } from 'rxjs/operators';
-// import moment from 'moment';
-// import { deviceStatus } from '@/pages/device/Instance/Detail';
-
-// const ColResponsiveProps = {
-//   xs: 24,
-//   sm: 12,
-//   md: 12,
-//   lg: 12,
-//   xl: 6,
-//   style: { marginBottom: 24 },
-// };
+import Event from '@/pages/device/Instance/Detail/Running/Event';
 
 const Running = () => {
-  // const intl = useIntl();
   const metadata = JSON.parse(InstanceModel.detail.metadata as string) as DeviceMetadata;
 
-  // const device = InstanceModel.detail;
-  // const [subscribeTopic] = useSendWebsocketMessage();
-
-  // const addObserver = (item: Record<string, any>) => {
-  //   item.listener = [];
-  //   item.subscribe = (callback: () => void) => {
-  //     item.listener.push(callback);
-  //   };
-  //   item.next = (data: any) => {
-  //     item.listener.forEach((element: any) => {
-  //       element(data);
-  //     });
-  //   };
-  //   return item;
-  // };
-  // metadata.events = metadata.events.map(addObserver);
-  // metadata.properties = metadata.properties.map(addObserver);
-  // const [propertiesList, setPropertiesList] = useState<string[]>(
-  //   metadata.properties.map((item: any) => item.id),
-  // );
-
-  /**
-   * 订阅属性数据
-   */
-  // const subscribeProperty = () => {
-  //   const id = `instance-info-property-${device.id}-${device.productId}-${propertiesList.join(
-  //     '-',
-  //   )}`;
-  //   const topic = `/dashboard/device/${device.productId}/properties/realTime`;
-  //   subscribeTopic!(id, topic, {
-  //     deviceId: device.id,
-  //     properties: propertiesList,
-  //     history: 0,
-  //   })
-  //     ?.pipe(map((res) => res.payload))
-  //     .subscribe((payload: any) => {
-  //       const property = metadata.properties.find(
-  //         (i) => i.id === payload.value.property,
-  //       ) as PropertyMetadata & ObserverMetadata;
-  //       if (property) {
-  //         property.next(payload);
-  //       }
-  //     });
-  // };
-
-  // const getDashboard = () => {
-  //   const params = [
-  //     {
-  //       dashboard: 'device',
-  //       object: device.productId,
-  //       measurement: 'properties',
-  //       dimension: 'history',
-  //       params: {
-  //         deviceId: device.id,
-  //         history: 15,
-  //         properties: propertiesList,
-  //       },
-  //     },
-  //   ];
-
-  //   service.propertyRealTime(params).subscribe({
-  //     next: (data) => {
-  //       const index = metadata.properties.findIndex((i) => i.id === data.property);
-  //       if (index > -1) {
-  //         const property = metadata.properties[index] as PropertyMetadata & ObserverMetadata;
-  //         property.list = data.list as Record<string, unknown>[];
-  //         property.next(data.list);
-  //       }
-  //     },
-  //   });
-  // };
-
-  // /**
-  //  * 订阅事件数据
-  //  */
-  // const subscribeEvent = () => {
-  //   const id = `instance-info-event-${device.id}-${device.productId}`;
-  //   const topic = `/dashboard/device/${device.productId}/events/realTime`;
-  //   subscribeTopic!(id, topic, { deviceId: device.id })
-  //     ?.pipe(map((res) => res.payload))
-  //     .subscribe((payload: any) => {
-  //       const event = metadata.events.find((i) => i.id === payload.value.event) as EventMetadata &
-  //         ObserverMetadata;
-  //       if (event) {
-  //         event.next(payload);
-  //       }
-  //     });
-  // };
-  useEffect(() => {
-    // subscribeProperty();
-    // subscribeEvent();
-    // getDashboard();
-  }, []);
-
-  // const [renderCount, setRenderCount] = useState<number>(15);
-  // window.onscroll = () => {
-  //   const a = document.documentElement.scrollTop;
-  //   const c = document.documentElement.scrollHeight;
-  //   const b = document.body.clientHeight;
-  //   if (a + b >= c - 50) {
-  //     const list: any = [];
-  //     metadata.properties.slice(renderCount, renderCount + 15).map((item) => {
-  //       list.push(item.id);
-  //     });
-  //     setPropertiesList([...list]);
-  //     setRenderCount(renderCount + 15);
-  //   }
-  // };
-
-  // const renderCard = useCallback(() => {
-  //   return [
-  //     ...metadata.properties.map((item) => (
-  //       <Col {...ColResponsiveProps} key={item.id}>
-  //         <Property data={item as Partial<PropertyMetadata> & ObserverMetadata} />
-  //       </Col>
-  //     )),
-  //     ...metadata.events.map((item) => (
-  //       <Col {...ColResponsiveProps} key={item.id}>
-  //         <Event data={item as Partial<EventMetadata> & ObserverMetadata} />
-  //       </Col>
-  //     )),
-  //   ].splice(0, renderCount);
-  // }, [device, renderCount]);
-
   return (
-    // <Row gutter={24}>
-    //   <Col {...ColResponsiveProps}>
-    //     <Card
-    //       title={intl.formatMessage({
-    //         id: 'pages.device.instanceDetail.running.status',
-    //         defaultMessage: '设备状态',
-    //       })}
-    //     >
-    //       <div style={{ height: 60 }}>
-    //         <Row gutter={[16, 16]}>
-    //           <Col span={24}>{deviceStatus.get(InstanceModel.detail.state?.value)}</Col>
-    //           <Col span={24}>
-    //             {device.state?.value === 'online' ? (
-    //               <span>上线时间:{moment(device?.onlineTime).format('YYYY-MM-DD HH:mm:ss')}</span>
-    //             ) : (
-    //               <span>离线时间:{moment(device?.offlineTime).format('YYYY-MM-DD HH:mm:ss')}</span>
-    //             )}
-    //           </Col>
-    //         </Row>
-    //       </div>
-    //     </Card>
-    //   </Col>
-    //   {renderCard()}
-    // </Row>
     <Card>
-      <Tabs defaultActiveKey="1" tabPosition="left">
+      <Tabs defaultActiveKey="1" tabPosition="left" style={{ height: 600 }}>
         <Tabs.TabPane tab="属性" key="1">
           <Property data={metadata?.properties || {}} />
         </Tabs.TabPane>
-        <Tabs.TabPane tab="事件1" key="2">
-          Content of Tab Pane 2
-        </Tabs.TabPane>
+        {metadata.events.map((item) => (
+          <Tabs.TabPane tab={item.name} key={item.id}>
+            <Event data={item} />
+          </Tabs.TabPane>
+        ))}
       </Tabs>
     </Card>
   );

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

@@ -11,6 +11,7 @@ import Alarm from '@/pages/device/components/Alarm';
 import Info from '@/pages/device/Instance/Detail/Info';
 import Functions from '@/pages/device/Instance/Detail/Functions';
 import Running from '@/pages/device/Instance/Detail/Running';
+import ChildDevice from '@/pages/device/Instance/Detail/ChildDevice';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import Metadata from '../../components/Metadata';
 import type { DeviceMetadata } from '@/pages/device/Product/typings';
@@ -32,16 +33,6 @@ const InstanceDetail = observer(() => {
     });
   };
   const params = useParams<{ id: string }>();
-  useEffect(() => {
-    if (!InstanceModel.current && !params.id) {
-      history.goBack();
-    } else {
-      getDetail(InstanceModel.current?.id || params.id);
-    }
-    return () => {
-      MetadataAction.clean();
-    };
-  }, [params.id]);
 
   const resetMetadata = async () => {
     const resp = await service.deleteMetadata(params.id);
@@ -103,6 +94,12 @@ const InstanceDetail = observer(() => {
       }),
       component: <Log />,
     },
+    // 产品类型为网关的情况下才显示此模块
+    {
+      key: 'child-device',
+      tab: '子设备',
+      component: <ChildDevice />,
+    },
     {
       key: 'alarm',
       tab: intl.formatMessage({
@@ -125,6 +122,17 @@ const InstanceDetail = observer(() => {
     },
   ];
 
+  useEffect(() => {
+    if (!InstanceModel.current && !params.id) {
+      history.goBack();
+    } else {
+      getDetail(InstanceModel.current?.id || params.id);
+    }
+    return () => {
+      MetadataAction.clean();
+    };
+  }, [params.id]);
+
   return (
     <PageContainer
       onBack={history.goBack}

+ 19 - 4
src/pages/device/Instance/service.ts

@@ -58,8 +58,8 @@ class Service extends BaseService<DeviceInstance> {
 
   public getEventCount = (deviceId: string, eventId: string, params: Record<string, unknown>) =>
     request(`/${SystemConst.API_BASE}/device/instance/${deviceId}/event/${eventId}`, {
-      method: 'GET',
-      params,
+      method: 'POST',
+      data: params,
     });
 
   public deleteMetadata = (deviceId: string) =>
@@ -75,14 +75,29 @@ class Service extends BaseService<DeviceInstance> {
 
   public queryLog = (deviceId: string, params: Record<string, unknown>) =>
     request(`/${SystemConst.API_BASE}/device-instance/${deviceId}/logs`, {
-      method: 'GET',
-      params,
+      method: 'POST',
+      data: params,
     });
 
   public getLogType = () =>
     request(`/${SystemConst.API_BASE}/dictionary/device-log-type/items`, {
       method: 'GET',
     });
+  public bindDevice = (deviceId: string, data: string[]) =>
+    request(`/${SystemConst.API_BASE}/device/gateway/${deviceId}/bind`, {
+      method: 'POST',
+      data,
+    });
+  public unbindDevice = (deviceId: string, childrenId: string, data: Record<string, unknown>) =>
+    request(`/${SystemConst.API_BASE}/device/gateway/${deviceId}/unbind/${childrenId}`, {
+      method: 'POST',
+      data,
+    });
+  public unbindBatchDevice = (deviceId: string, data: string[]) =>
+    request(`/${SystemConst.API_BASE}/device/gateway/${deviceId}/unbind`, {
+      method: 'POST',
+      data,
+    });
 }
 
 export default Service;

+ 2 - 2
src/pages/notice/Config/index.tsx

@@ -12,7 +12,7 @@ import { useMemo, useRef, useState } from 'react';
 import BaseCrud from '@/components/BaseCrud';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import type { ISchema } from '@formily/json-schema';
-import { downObject, useAsyncDataSource } from '@/utils/util';
+import { downloadObject, useAsyncDataSource } from '@/utils/util';
 import { CurdModel } from '@/components/BaseCrud/model';
 import Service from '@/pages/notice/Config/service';
 import { createForm, onFieldValueChange } from '@formily/core';
@@ -222,7 +222,7 @@ const Config = observer(() => {
             <EditOutlined />
           </Tooltip>
         </a>,
-        <a onClick={() => downObject(record, '通知配置')} key="download">
+        <a onClick={() => downloadObject(record, '通知配置')} key="download">
           <Tooltip
             title={intl.formatMessage({
               id: 'pages.data.option.download',

+ 6 - 24
src/pages/system/Permission/index.tsx

@@ -1,13 +1,13 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import React, { useEffect, useRef } from 'react';
 import {
-  EditOutlined,
   CloseCircleOutlined,
-  PlayCircleOutlined,
   DeleteOutlined,
+  EditOutlined,
+  PlayCircleOutlined,
 } from '@ant-design/icons';
-import { Menu, Tooltip, Popconfirm, message, Button, Upload, Badge } from 'antd';
-import type { ProColumns, ActionType } from '@jetlinks/pro-table';
+import { Badge, Button, Menu, message, Popconfirm, Tooltip, Upload } from 'antd';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import BaseCrud from '@/components/BaseCrud';
 import { CurdModel } from '@/components/BaseCrud/model';
@@ -17,9 +17,9 @@ import type { ISchema } from '@formily/json-schema';
 import Service from '@/pages/system/Permission/service';
 import { model } from '@formily/reactive';
 import { observer } from '@formily/react';
-import moment from 'moment';
 import SystemConst from '@/utils/const';
 import Token from '@/utils/token';
+import { downloadObject } from '@/utils/util';
 
 export const service = new Service('permission');
 
@@ -29,23 +29,6 @@ const defaultAction = [
   { action: 'delete', name: '删除', describe: '删除' },
 ];
 
-const downloadObject = (record: any, fileName: string) => {
-  // 创建隐藏的可下载链接
-  const eleLink = document.createElement('a');
-  eleLink.download = `${fileName}-${
-    record.name || moment(new Date()).format('YYYY/MM/DD HH:mm:ss')
-  }.json`;
-  eleLink.style.display = 'none';
-  // 字符内容转变成blob地址
-  const blob = new Blob([JSON.stringify(record)]);
-  eleLink.href = URL.createObjectURL(blob);
-  // 触发点击
-  document.body.appendChild(eleLink);
-  eleLink.click();
-  // 然后移除
-  document.body.removeChild(eleLink);
-};
-
 const PermissionModel = model<{
   assetsTypesList: { label: string; value: string }[];
 }>({
@@ -55,13 +38,12 @@ const Permission: React.FC = observer(() => {
   useEffect(() => {
     service.getAssetTypes().subscribe((resp) => {
       if (resp.status === 200) {
-        const list = resp.result.map((item: { name: string; id: string }) => {
+        PermissionModel.assetsTypesList = resp.result.map((item: { name: string; id: string }) => {
           return {
             label: item.name,
             value: item.id,
           };
         });
-        PermissionModel.assetsTypesList = list;
       }
     });
   }, []);

+ 8 - 6
src/pages/system/User/index.tsx

@@ -15,6 +15,7 @@ import { useIntl } from '@@/plugin-locale/localeExports';
 import { useRef, useState } from 'react';
 import Save from './Save';
 import { observer } from '@formily/react';
+import { request } from 'umi';
 
 export const service = new Service('user');
 
@@ -53,9 +54,12 @@ const User = observer(() => {
           },
         ],
       },
-      search: {
-        transform: (value) => ({ name$LIKE: value }),
-      },
+    },
+    {
+      title: '测试字段',
+      dataIndex: 'test',
+      align: 'center',
+      request: () => request('/jetlinks/dictionary/device-log-type/items'),
     },
     {
       title: intl.formatMessage({
@@ -78,9 +82,7 @@ const User = observer(() => {
           },
         ],
       },
-      search: {
-        transform: (value) => ({ username$LIKE: value }),
-      },
+      hideInSearch: false,
     },
     {
       title: intl.formatMessage({

+ 6 - 1
src/utils/util.ts

@@ -2,7 +2,12 @@ import moment from 'moment';
 import type { Field, FieldDataSource } from '@formily/core';
 import { action } from '@formily/reactive';
 
-export const downObject = (record: Record<string, unknown>, fileName: string) => {
+/**
+ * 把数据下载成JSON
+ * @param record
+ * @param fileName
+ */
+export const downloadObject = (record: Record<string, unknown>, fileName: string) => {
   // 创建隐藏的可下载链接
   const ghostLink = document.createElement('a');
   ghostLink.download = `${fileName}-${

+ 8 - 8
yarn.lock

@@ -175,7 +175,7 @@
     "@ant-design/pro-field" "1.32.3"
     "@ant-design/pro-form" "1.56.1"
     "@ant-design/pro-skeleton" "1.0.5"
-    "@ant-design/pro-utils" "1.35.1"
+    "@ant-design/pro-utils" "1.35.2"
     "@babel/runtime" "^7.16.3"
     rc-util "^5.0.6"
     use-json-comparison "^1.0.5"
@@ -220,7 +220,7 @@
   dependencies:
     "@ant-design/icons" "^4.2.1"
     "@ant-design/pro-provider" "1.6.0"
-    "@ant-design/pro-utils" "1.35.1"
+    "@ant-design/pro-utils" "1.35.2"
     "@babel/runtime" "^7.16.3"
     classnames "^2.2.6"
     lodash.tonumber "^4.0.3"
@@ -273,7 +273,7 @@
     "@ant-design/icons" "^4.2.1"
     "@ant-design/pro-field" "1.32.3"
     "@ant-design/pro-provider" "1.6.0"
-    "@ant-design/pro-utils" "1.35.1"
+    "@ant-design/pro-utils" "1.35.2"
     "@babel/runtime" "^7.16.3"
     "@umijs/use-params" "^1.0.9"
     classnames "^2.2.6"
@@ -369,7 +369,7 @@
     react-sortable-hoc "^2.0.0"
     swr "^1.2.0"
 
-"@ant-design/pro-utils@1.35.1", "@ant-design/pro-utils@1.35.1":
+"@ant-design/pro-utils@1.35.1":
   version "1.35.1"
   resolved "https://registry.npmjs.org/@ant-design/pro-utils/-/pro-utils-1.35.1.tgz#ab39815b7c013003be7a06b360ad4fbef61eb5c7"
   integrity sha512-XvxSnekFkYnu0Bh/crc3YJrB+TLzJjI5p60WvpdIh5OrYYVsJICP+U46cPHKIOaVfFviEVOEGRARPxCiNaLvAw==
@@ -2975,10 +2975,10 @@
     use-json-comparison "^1.0.5"
     use-media-antd-query "1.0.6"
 
-"@jetlinks/pro-table@^2.63.10":
-  version "2.63.10"
-  resolved "https://registry.npmjs.org/@jetlinks/pro-table/-/pro-table-2.63.10.tgz#7b07f908f23a5548a995cd6b251983ff0e69653b"
-  integrity sha512-mK9Jw8t/uW3i8ztckWns2St6iwDhPgBXfyaLdiwSNCf2nDQv0kiHJfnc6G1zRC+VbOPsSH+bD4LM/vzTss0COA==
+"@jetlinks/pro-table@^2.63.11":
+  version "2.63.11"
+  resolved "https://registry.npmjs.org/@jetlinks/pro-table/-/pro-table-2.63.11.tgz#fffce287276925184b4e751690e8a9bd83b8c5e8"
+  integrity sha512-tUFkFlCdQ19JyS8SV2umfJ+guA8fiICNi7gwvDufX/rYaFHDwLC55SQ12piDto5ee1VC42wN5D4ro0F45QrkIg==
   dependencies:
     "@ant-design/icons" "^4.1.0"
     "@ant-design/pro-field" "1.31.17"