Przeglądaj źródła

feat(product): product category

feat: 产品分类
Lind 3 lat temu
rodzic
commit
83b8a5d729

+ 19 - 0
src/components/CheckButton/index.less

@@ -0,0 +1,19 @@
+.box {
+  display: flex;
+  .item {
+    width: 30px;
+    height: 30px;
+    font-size: 20px;
+    line-height: 30px;
+    text-align: center;
+    border-top: 1px solid lightgray;
+    border-bottom: 1px solid lightgray;
+    cursor: pointer;
+  }
+  .left {
+    border-left: 1px solid lightgray;
+  }
+  .right {
+    border-right: 1px solid lightgray;
+  }
+}

+ 38 - 0
src/components/CheckButton/index.tsx

@@ -0,0 +1,38 @@
+import { AppstoreFilled, UnorderedListOutlined } from '@ant-design/icons';
+import classnames from 'classnames';
+import styles from './index.less';
+interface Props {
+  value: boolean;
+  change: (value: boolean) => void;
+}
+
+const CheckButton = (props: Props) => {
+  const activeStyle = {
+    border: '1px solid #1d39c4',
+    color: '#1d39c4',
+  };
+
+  return (
+    <div className={styles.box}>
+      <div
+        className={classnames(styles.item, styles.left)}
+        style={props.value ? activeStyle : {}}
+        onClick={() => {
+          props.change(true);
+        }}
+      >
+        <AppstoreFilled />
+      </div>
+      <div
+        className={classnames(styles.item, styles.right)}
+        style={!props.value ? activeStyle : {}}
+        onClick={() => {
+          props.change(false);
+        }}
+      >
+        <UnorderedListOutlined />
+      </div>
+    </div>
+  );
+};
+export default CheckButton;

+ 1 - 0
src/locales/en-US/pages.ts

@@ -210,6 +210,7 @@ export default {
   'pages.device.category.key': 'Key',
   'pages.device.category.name': 'Classification Name',
   'pages.device.category.addClass': 'Add Subclasses',
+  'pages.device.category.sortIndex': 'Sort',
   // 设备管理-设备
   'pages.device.instance': 'Instance Manage',
   'pages.device.instance.registrationTime': 'Registration Time',

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

@@ -220,6 +220,7 @@ export default {
   'pages.device.category.key': '标识',
   'pages.device.category.name': '分类名称',
   'pages.device.category.addClass': '添加子分类',
+  'pages.device.category.sortIndex': '分类排序',
   // 设备管理-设备
   'pages.device.instance': '设备',
   'pages.device.instance.registrationTime': '注册时间',

+ 26 - 24
src/pages/device/Category/Save/index.tsx

@@ -61,7 +61,10 @@ const Save = (props: Props) => {
   });
 
   const save = async () => {
-    const value = await form.submit();
+    const value: CategoryItem = await form.submit();
+    if (!!state.parentId) {
+      value.parentId = state.parentId;
+    }
     const resp = props.data.id
       ? await service.update(value as CategoryItem)
       : ((await service.save(value as any)) as Response<CategoryItem>);
@@ -76,23 +79,23 @@ const Save = (props: Props) => {
   const schema: ISchema = {
     type: 'object',
     properties: {
-      parentId: {
-        title: '上级分类',
-        'x-decorator': 'FormItem',
-        'x-component': 'Input',
-        name: 'parentId',
-        'x-disabled': true,
-        'x-visible': !!state.parentId,
-        'x-value': state.parentId,
-      },
-      id: {
-        title: 'ID',
-        'x-decorator': 'FormItem',
-        'x-component': 'Input',
-        required: true,
-        name: 'id',
-        'x-disabled': !!props.data.id,
-      },
+      // parentId: {
+      //   title: '上级分类',
+      //   'x-decorator': 'FormItem',
+      //   'x-component': 'Input',
+      //   name: 'parentId',
+      //   'x-disabled': true,
+      //   'x-visible': !!state.parentId,
+      //   'x-value': state.parentId,
+      // },
+      // id: {
+      //   title: 'ID',
+      //   'x-decorator': 'FormItem',
+      //   'x-component': 'Input',
+      //   required: true,
+      //   name: 'id',
+      //   'x-disabled': !!props.data.id,
+      // },
       name: {
         title: intl.formatMessage({
           id: 'pages.table.name',
@@ -103,15 +106,14 @@ const Save = (props: Props) => {
         required: true,
         name: 'name',
       },
-      key: {
+      sortIndex: {
         title: intl.formatMessage({
-          id: 'pages.device.category.key',
-          defaultMessage: '标识',
+          id: 'pages.device.category.sortIndex',
+          defaultMessage: '排序',
         }),
         'x-decorator': 'FormItem',
-        'x-component': 'Input',
-        required: true,
-        name: 'name',
+        'x-component': 'NumberPicker',
+        name: 'sortIndex',
       },
       description: {
         type: 'string',

+ 26 - 22
src/pages/device/Category/index.tsx

@@ -1,9 +1,9 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import Service from '@/pages/device/Category/service';
 import type { ProColumns } from '@jetlinks/pro-table';
-import { EditOutlined, MinusOutlined, PlusOutlined } from '@ant-design/icons';
+import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
 import { Button, message, Popconfirm, Tooltip } from 'antd';
-import { useRef } from 'react';
+import { useRef, useState } from 'react';
 import type { ActionType } from '@jetlinks/pro-table';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import ProTable from '@jetlinks/pro-table';
@@ -11,6 +11,7 @@ import Save from '@/pages/device/Category/Save';
 import { model } from '@formily/reactive';
 import { observer } from '@formily/react';
 import type { Response } from '@/utils/typings';
+import SearchComponent from '@/components/SearchComponent';
 
 export const service = new Service('device/category');
 
@@ -25,35 +26,27 @@ export const state = model<{
 });
 const Category = observer(() => {
   const actionRef = useRef<ActionType>();
+  const [param, setParam] = useState({});
 
   const intl = useIntl();
 
   const columns: ProColumns<CategoryItem>[] = [
     {
       title: intl.formatMessage({
-        id: 'pages.device.category.id',
-        defaultMessage: '分类ID',
-      }),
-      align: 'left',
-      width: 400,
-      dataIndex: 'id',
-      sorter: true,
-    },
-    {
-      title: intl.formatMessage({
-        id: 'pages.device.category.key',
-        defaultMessage: '标识',
-      }),
-      align: 'left',
-      dataIndex: 'key',
-    },
-    {
-      title: intl.formatMessage({
         id: 'pages.device.category.name',
         defaultMessage: '分类名称',
       }),
       dataIndex: 'name',
+    },
+    {
+      title: '分类排序',
+      dataIndex: 'sortIndex',
       align: 'center',
+      // render: (text) => (
+      //   <Space>{text}<EditOutlined onClick={() => {
+
+      //   }} /></Space>
+      // )
     },
     {
       title: intl.formatMessage({
@@ -74,6 +67,7 @@ const Category = observer(() => {
       align: 'center',
       render: (text, record) => [
         <a
+          key={'edit'}
           onClick={() => {
             state.visible = true;
             state.current = record;
@@ -89,6 +83,7 @@ const Category = observer(() => {
           </Tooltip>
         </a>,
         <a
+          key={'add-next'}
           onClick={() => {
             state.visible = true;
             state.parentId = record.id;
@@ -104,6 +99,7 @@ const Category = observer(() => {
           </Tooltip>
         </a>,
         <Popconfirm
+          key={'delete'}
           onConfirm={async () => {
             const resp = (await service.remove(record.id)) as Response<any>;
             if (resp.status === 200) {
@@ -122,7 +118,7 @@ const Category = observer(() => {
                 defaultMessage: '删除',
               })}
             >
-              <MinusOutlined />
+              <DeleteOutlined />
             </Tooltip>
           </a>
         </Popconfirm>,
@@ -132,9 +128,17 @@ const Category = observer(() => {
 
   return (
     <PageContainer>
+      <SearchComponent
+        field={columns}
+        onSearch={(data) => {
+          setParam(data);
+        }}
+        target="category"
+      />
       <ProTable
+        params={param}
+        search={false}
         request={async (params) => {
-          delete params.pageIndex;
           const response = await service.queryTree({ paging: false, ...params });
           return {
             code: response.message,

+ 1 - 1
src/pages/device/Category/service.ts

@@ -3,7 +3,7 @@ import { request } from '@@/plugin-request/request';
 
 class Service extends BaseService<CategoryItem> {
   queryTree = (params?: Record<string, any>) =>
-    request(`${this.uri}/_tree`, { params, method: 'GET' });
+    request(`${this.uri}/_tree`, { data: params, method: 'POST' });
 }
 
 export default Service;

+ 134 - 27
src/pages/device/Instance/Detail/MetadataLog/Property/index.tsx

@@ -1,10 +1,10 @@
-import ProTable from '@jetlinks/pro-table';
 import { service } from '@/pages/device/Instance';
 import { useParams } from 'umi';
-import { Drawer } from 'antd';
-import encodeQuery from '@/utils/encodeQuery';
+import { DatePicker, Modal, Radio, Space, Table } from 'antd';
 import type { PropertyMetadata } from '@/pages/device/Product/typings';
-import columns from '@/pages/device/Instance/Detail/MetadataLog/columns';
+import encodeQuery from '@/utils/encodeQuery';
+import { useEffect, useState } from 'react';
+import moment from 'moment';
 
 interface Props {
   visible: boolean;
@@ -15,35 +15,142 @@ interface Props {
 const PropertyLog = (props: Props) => {
   const params = useParams<{ id: string }>();
   const { visible, close, data } = props;
+  const [dataSource, setDataSource] = useState<any>({});
+  const [start, setStart] = useState<number>(moment().startOf('day').valueOf());
+  const [end, setEnd] = useState<number>(new Date().getTime());
+  const [radioValue, setRadioValue] = useState<undefined | 'today' | 'week' | 'month'>('today');
+  const [dateValue, setDateValue] = useState<any>(undefined);
+
+  const columns = [
+    {
+      title: '时间',
+      dataIndex: 'timestamp',
+      key: 'timestamp',
+      render: (text: any) => <span>{text ? moment(text).format('YYYY-MM-DD HH:mm:ss') : ''}</span>,
+    },
+    {
+      title: '自定义属性',
+      dataIndex: 'formatValue',
+      key: 'formatValue',
+    },
+  ];
+
+  const handleSearch = (param: any, startTime?: number, endTime?: number) => {
+    service
+      .getPropertyData(
+        params.id,
+        encodeQuery({
+          ...param,
+          terms: {
+            property: data.id,
+            timestamp$BTW: startTime && endTime ? [startTime, endTime] : [],
+          },
+          sorts: { timestamp: 'desc' },
+        }),
+      )
+      .then((resp) => {
+        if (resp.status === 200) {
+          setDataSource(resp.result);
+        }
+      });
+  };
+
+  useEffect(() => {
+    if (visible) {
+      handleSearch(
+        {
+          pageSize: 10,
+          pageIndex: 0,
+        },
+        start,
+        end,
+      );
+    }
+  }, [visible]);
 
   return (
-    <Drawer title={data.name} visible={visible} onClose={() => close()} width="45vw">
-      <ProTable
+    <Modal title="详情" visible={visible} onCancel={() => close()} width="45vw">
+      <div style={{ marginBottom: '20px' }}>
+        <Space>
+          <Radio.Group
+            value={radioValue}
+            buttonStyle="solid"
+            onChange={(e) => {
+              const value = e.target.value;
+              setRadioValue(value);
+              let st: number = 0;
+              const et = new Date().getTime();
+              if (value === 'today') {
+                st = moment().startOf('day').valueOf();
+              } else if (value === 'week') {
+                st = moment().subtract(6, 'days').valueOf();
+              } else if (value === 'month') {
+                st = moment().subtract(29, 'days').valueOf();
+              }
+              setDateValue(undefined);
+              setStart(st);
+              setEnd(et);
+              handleSearch(
+                {
+                  pageSize: 10,
+                  pageIndex: 0,
+                },
+                st,
+                et,
+              );
+            }}
+          >
+            <Radio.Button value="today">今日</Radio.Button>
+            <Radio.Button value="week">近一周</Radio.Button>
+            <Radio.Button value="month">近一月</Radio.Button>
+          </Radio.Group>
+          <DatePicker.RangePicker
+            value={dateValue}
+            showTime
+            onChange={(dates: any) => {
+              if (dates) {
+                setRadioValue(undefined);
+                setDateValue(dates);
+                const st = dates[0]?.valueOf();
+                const et = dates[1]?.valueOf();
+                setStart(st);
+                setEnd(et);
+                handleSearch(
+                  {
+                    pageSize: 10,
+                    pageIndex: 0,
+                  },
+                  st,
+                  et,
+                );
+              }
+            }}
+          />
+        </Space>
+      </div>
+
+      <Table
         size="small"
-        toolBarRender={false}
-        request={async (param) =>
-          service.getPropertyData(
-            params.id,
-            encodeQuery({
-              ...param,
-              terms: { property: data.id },
-              sorts: { timestamp: 'desc' },
-            }),
-          )
-        }
+        rowKey={'id'}
+        onChange={(page) => {
+          handleSearch(
+            {
+              pageSize: page.pageSize,
+              pageIndex: Number(page.current) - 1 || 0,
+            },
+            start,
+            end,
+          );
+        }}
+        dataSource={dataSource?.data || []}
+        columns={columns}
         pagination={{
-          pageSize: 15,
+          pageSize: dataSource?.pageSize || 10,
+          showSizeChanger: true,
+          total: dataSource?.total || 0,
         }}
-        columns={[
-          ...columns,
-          {
-            dataIndex: 'formatValue',
-            title: '数据',
-            // copyable: true,
-          },
-        ]}
       />
-    </Drawer>
+    </Modal>
   );
 };
 export default PropertyLog;

+ 70 - 0
src/pages/device/Instance/Detail/Running/Property/EditProperty.tsx

@@ -0,0 +1,70 @@
+import { Alert, message, Modal } from 'antd';
+import { Input, FormItem } from '@formily/antd';
+import { createForm } from '@formily/core';
+import { FormProvider, createSchemaField } from '@formily/react';
+import { service } from '@/pages/device/Instance';
+import { useParams } from 'umi';
+import type { PropertyMetadata } from '@/pages/device/Product/typings';
+
+interface Props {
+  visible: boolean;
+  data: Partial<PropertyMetadata>;
+  onCancel: () => void;
+}
+const EditProperty = (props: Props) => {
+  const { visible, data } = props;
+  const params = useParams<{ id: string }>();
+
+  const SchemaField = createSchemaField({
+    components: {
+      Input,
+      FormItem,
+    },
+  });
+
+  const form = createForm();
+  const schema = {
+    type: 'object',
+    properties: {
+      propertyValue: {
+        type: 'string',
+        title: '自定义属性',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+      },
+    },
+  };
+
+  const handleSetPropertyValue = async (propertyValue: string) => {
+    const resp = await service.setProperty(params.id, { [`${data.id}`]: propertyValue });
+    if (resp.status === 200) {
+      message.success('操作成功');
+    }
+    props.onCancel();
+  };
+  return (
+    <Modal
+      title="编辑"
+      visible={visible}
+      onOk={async () => {
+        const values: any = await form.submit();
+        if (!!values) {
+          handleSetPropertyValue(values?.propertyValue);
+        }
+      }}
+      onCancel={() => {
+        props.onCancel();
+      }}
+    >
+      <Alert message="当数据来源为设备时,填写的值将下发到设备" type="warning" showIcon />
+      <div style={{ marginTop: '30px' }}>
+        <FormProvider form={form}>
+          <SchemaField schema={schema} />
+        </FormProvider>
+      </div>
+    </Modal>
+  );
+};
+
+export default EditProperty;

src/pages/device/Instance/Detail/Running/Property.tsx → src/pages/device/Instance/Detail/Running/Property/PropertyCard copy.tsx


+ 85 - 0
src/pages/device/Instance/Detail/Running/Property/PropertyCard.tsx

@@ -0,0 +1,85 @@
+import { EditOutlined, SyncOutlined, UnorderedListOutlined } from '@ant-design/icons';
+import { Divider, message, Spin, Tooltip } from 'antd';
+import ProCard from '@ant-design/pro-card';
+import type { PropertyMetadata } from '@/pages/device/Product/typings';
+import { useState } from 'react';
+import { service } from '@/pages/device/Instance';
+import { useParams } from 'umi';
+import PropertyLog from '@/pages/device/Instance/Detail/MetadataLog/Property';
+import EditProperty from '@/pages/device/Instance/Detail/Running/Property/EditProperty';
+
+interface Props {
+  data: Partial<PropertyMetadata>;
+  value: any;
+}
+
+const Property = (props: Props) => {
+  const { data, value } = props;
+
+  const params = useParams<{ id: string }>();
+
+  const [loading, setLoading] = useState<boolean>(false);
+  const refreshProperty = async () => {
+    setLoading(true);
+    if (!data.id) return;
+    const resp = await service.getProperty(params.id, data.id);
+    setLoading(false);
+    if (resp.status === 200) {
+      message.success('操作成功');
+    }
+  };
+
+  const [visible, setVisible] = useState<boolean>(false);
+  const [editVisible, setEditVisible] = useState<boolean>(false);
+
+  return (
+    <ProCard
+      title={`${data?.name}`}
+      extra={
+        <>
+          {(data.expands?.readOnly === false || data.expands?.readOnly === 'false') && (
+            <>
+              <Tooltip placement="top" title="设置属性至设备">
+                <EditOutlined
+                  onClick={() => {
+                    setEditVisible(true);
+                  }}
+                />
+              </Tooltip>
+              <Divider type="vertical" />
+            </>
+          )}
+          <Tooltip placement="top" title="获取最新属性值">
+            <SyncOutlined onClick={refreshProperty} />
+          </Tooltip>
+          <Divider type="vertical" />
+          <Tooltip placement="top" title="详情">
+            <UnorderedListOutlined
+              onClick={() => {
+                setVisible(true);
+              }}
+            />
+          </Tooltip>
+        </>
+      }
+      bordered
+      hoverable
+      colSpan={{ xs: 12, sm: 8, md: 6, lg: 6, xl: 6 }}
+    >
+      <Spin spinning={loading}>
+        <div style={{ height: 60, fontWeight: 600, fontSize: '30px' }}>
+          {value?.formatValue || ''}
+        </div>
+      </Spin>
+      <EditProperty
+        visible={editVisible}
+        onCancel={() => {
+          setEditVisible(false);
+        }}
+        data={data}
+      />
+      <PropertyLog data={data} visible={visible} close={() => setVisible(false)} />
+    </ProCard>
+  );
+};
+export default Property;

+ 7 - 0
src/pages/device/Instance/Detail/Running/Property/index.less

@@ -0,0 +1,7 @@
+.page {
+  :global {
+    .ant-pagination-item {
+      display: none;
+    }
+  }
+}

+ 244 - 0
src/pages/device/Instance/Detail/Running/Property/index.tsx

@@ -0,0 +1,244 @@
+import { Col, Input, message, Pagination, Row, Space, Table } from 'antd';
+import CheckButton from '@/components/CheckButton';
+import { useEffect, useState } from 'react';
+import type { PropertyMetadata } from '@/pages/device/Product/typings';
+import PropertyCard from './PropertyCard';
+import { EditOutlined, SyncOutlined, UnorderedListOutlined } from '@ant-design/icons';
+import { InstanceModel, service } from '@/pages/device/Instance';
+import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
+import { map } from 'rxjs/operators';
+import EditProperty from './EditProperty';
+import { useParams } from 'umi';
+import PropertyLog from '../../MetadataLog/Property';
+import moment from 'moment';
+import styles from './index.less';
+
+interface Props {
+  data: Partial<PropertyMetadata>[];
+}
+
+const ColResponsiveProps = {
+  xs: 24,
+  sm: 12,
+  md: 12,
+  lg: 12,
+  xl: 6,
+  style: { marginBottom: 24 },
+};
+
+const Property = (props: Props) => {
+  const { data } = props;
+  const device = InstanceModel.detail;
+  const params = useParams<{ id: string }>();
+  const [subscribeTopic] = useSendWebsocketMessage();
+  const [visible, setVisible] = useState<boolean>(false);
+  const [infoVisible, setInfoVisible] = useState<boolean>(false);
+  const [currentInfo, setCurrentInfo] = useState<any>({});
+  const [propertyValue, setPropertyValue] = useState<any>({});
+  const [propertyList, setPropertyList] = useState<any[]>(data || []);
+  const [dataSource, setDataSource] = useState<any>({
+    total: data.length,
+    data: (data || []).slice(0, 8),
+    pageSize: 8,
+    currentPage: 0,
+  });
+
+  const [check, setCheck] = useState<boolean>(true);
+
+  const refreshProperty = async (id: string) => {
+    if (!id) return;
+    const resp = await service.getProperty(params.id, id);
+    if (resp.status === 200) {
+      message.success('操作成功');
+    }
+  };
+
+  const columns = [
+    {
+      title: '名称',
+      dataIndex: 'name',
+      key: 'name',
+    },
+    {
+      title: '值',
+      dataIndex: 'value',
+      key: 'value',
+      render: (text: any, record: any) => (
+        <span>{propertyValue[record.id]?.formatValue || '--'}</span>
+      ),
+    },
+    {
+      title: '更新时间',
+      dataIndex: 'time',
+      key: 'time',
+      render: (text: any, record: any) => (
+        <span>{moment(propertyValue[record.id]?.timestamp).format('YYYY-MM-DD HH:mm:ss')}</span>
+      ),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (text: any, record: any) => (
+        <Space size="middle" style={{ color: '#1d39c4' }}>
+          {(record.expands?.readOnly === false || record.expands?.readOnly === 'false') && (
+            <EditOutlined
+              onClick={() => {
+                setVisible(true);
+              }}
+            />
+          )}
+          <SyncOutlined
+            onClick={() => {
+              refreshProperty(record?.id);
+            }}
+          />
+          <UnorderedListOutlined
+            onClick={() => {
+              setCurrentInfo(record);
+              setInfoVisible(true);
+            }}
+          />
+        </Space>
+      ),
+    },
+  ];
+
+  /**
+   * 订阅属性数据
+   */
+  const subscribeProperty = () => {
+    const id = `instance-info-property-${device.id}-${device.productId}-${dataSource.data
+      .map((i: PropertyMetadata) => i.id)
+      .join('-')}`;
+    const topic = `/dashboard/device/${device.productId}/properties/realTime`;
+    subscribeTopic!(id, topic, {
+      deviceId: device.id,
+      properties: dataSource.data.map((i: PropertyMetadata) => i.id),
+      history: 1,
+    })
+      ?.pipe(map((res) => res.payload))
+      .subscribe((payload: any) => {
+        const { value } = payload;
+        propertyValue[value.property] = value;
+        setPropertyValue({ ...propertyValue });
+      });
+  };
+
+  const getDashboard = () => {
+    const param = [
+      {
+        dashboard: 'device',
+        object: device.productId,
+        measurement: 'properties',
+        dimension: 'history',
+        params: {
+          deviceId: device.id,
+          history: 1,
+          properties: dataSource.data.map((i: PropertyMetadata) => i.id),
+        },
+      },
+    ];
+
+    service.propertyRealTime(param).subscribe({
+      next: (resp) => {
+        propertyValue[resp.property] = resp.list[0];
+        setPropertyValue({ ...propertyValue });
+      },
+    });
+  };
+
+  useEffect(() => {
+    if (dataSource.data.length > 0) {
+      getDashboard();
+      subscribeProperty();
+    }
+  }, [dataSource]);
+
+  return (
+    <div>
+      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
+        <Space>
+          <Input.Search
+            allowClear
+            placeholder="请输入名称"
+            onSearch={(value: string) => {
+              if (!!value) {
+                const list = data.filter((item) => {
+                  return (
+                    item.name && item.name.toLowerCase().indexOf(value.toLocaleLowerCase()) !== -1
+                  );
+                });
+                setPropertyList(list);
+                setDataSource({
+                  total: list.length,
+                  data: (list || []).slice(0, 8),
+                  pageSize: 8,
+                  currentPage: 0,
+                });
+              } else {
+                setPropertyList(data);
+                setDataSource({
+                  total: data.length,
+                  data: (data || []).slice(0, 8),
+                  pageSize: 8,
+                  currentPage: 0,
+                });
+              }
+            }}
+            style={{ width: 300 }}
+          />
+          {/* <Checkbox onChange={() => {
+
+                    }}>仅显示当前有数据的属性</Checkbox> */}
+        </Space>
+        <CheckButton
+          value={check}
+          change={(value: boolean) => {
+            setCheck(value);
+          }}
+        />
+      </div>
+      <div style={{ marginTop: '20px' }}>
+        {check ? (
+          <Row gutter={[16, 16]}>
+            {dataSource.data.map((item: any) => (
+              <Col {...ColResponsiveProps} key={item.id}>
+                <PropertyCard data={item} value={item?.id ? propertyValue[item?.id] : '--'} />
+              </Col>
+            ))}
+          </Row>
+        ) : (
+          <Table pagination={false} columns={columns} dataSource={dataSource.data} rowKey="id" />
+        )}
+        <div
+          style={{ marginTop: '20px', width: '100%', display: 'flex', justifyContent: 'flex-end' }}
+        >
+          <Pagination
+            className={styles.page}
+            defaultCurrent={1}
+            total={dataSource.total}
+            showSizeChanger
+            pageSize={dataSource.pageSize}
+            onChange={(page: number, size: number) => {
+              setDataSource({
+                total: propertyList.length,
+                data: (propertyList || []).slice((page - 1) * size, page * size),
+                pageSize: size,
+                currentPage: page - 1,
+              });
+            }}
+          />
+        </div>
+      </div>
+      <EditProperty
+        data={currentInfo}
+        visible={visible}
+        onCancel={() => {
+          setVisible(false);
+        }}
+      />
+      <PropertyLog data={currentInfo} visible={infoVisible} close={() => setInfoVisible(false)} />
+    </div>
+  );
+};
+export default Property;

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

@@ -1,182 +1,187 @@
-import { InstanceModel, service } from '@/pages/device/Instance';
-import { Card, Col, Row } from 'antd';
-import type {
-  DeviceMetadata,
-  EventMetadata,
-  ObserverMetadata,
-  PropertyMetadata,
-} from '@/pages/device/Product/typings';
-import { useIntl } from '@@/plugin-locale/localeExports';
-import { useCallback, useEffect, useState } from 'react';
+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';
+// 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 },
-};
+// const ColResponsiveProps = {
+//   xs: 24,
+//   sm: 12,
+//   md: 12,
+//   lg: 12,
+//   xl: 6,
+//   style: { marginBottom: 24 },
+// };
 
 const Running = () => {
-  const intl = useIntl();
+  // const intl = useIntl();
   const metadata = JSON.parse(InstanceModel.detail.metadata as string) as DeviceMetadata;
 
-  const device = InstanceModel.detail;
-  const [subscribeTopic] = useSendWebsocketMessage();
+  // 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 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 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,
-        },
-      },
-    ];
+  // 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);
-        }
-      },
-    });
-  };
+  //   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);
-        }
-      });
-  };
+  // /**
+  //  * 订阅事件数据
+  //  */
+  // 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();
+    // 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 [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]);
+  // 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>
+    // <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.TabPane tab="属性" key="1">
+          <Property data={metadata?.properties || {}} />
+        </Tabs.TabPane>
+        <Tabs.TabPane tab="事件1" key="2">
+          Content of Tab Pane 2
+        </Tabs.TabPane>
+      </Tabs>
+    </Card>
   );
 };
 export default Running;