فهرست منبع

feat: 设备日志管理和子设备管理

sun-chaochao 3 سال پیش
والد
کامیت
eb9499e84d

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

@@ -0,0 +1,158 @@
+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

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

@@ -0,0 +1,193 @@
+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 - 30
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,7 +40,8 @@ const Log = () => {
       title: '类型',
       dataIndex: 'type',
       renderText: (text) => text.text,
-      valueEnum: type,
+      valueType: 'select',
+      valueEnum: type
     },
     {
       title: '时间',
@@ -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,36 @@ 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;

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

@@ -9,8 +9,6 @@ import { Form, Modal } from 'antd';
 import { SearchOutlined } from '@ant-design/icons';
 import { useRef, useState } from 'react';
 import MonacoEditor from 'react-monaco-editor';
-import encodeQuery from '@/utils/encodeQuery';
-
 interface Props {
   data: Partial<EventMetadata>;
 }
@@ -115,9 +113,9 @@ const EventLog = (props: Props) => {
           return service.getEventCount(
             params.id,
             data.id!,
-            encodeQuery({
+            {
               ...param,
-            }),
+            }
           );
         }}
         pagination={{

+ 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;