Kaynağa Gözat

feat(device): add device\product\instance\datasource

Lind 4 yıl önce
ebeveyn
işleme
7f9dcd4e84

+ 102 - 0
src/pages/device/Alarm/index.tsx

@@ -0,0 +1,102 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import BaseService from '@/utils/BaseService';
+import { useRef } from 'react';
+import type { ProColumns, ActionType } from '@ant-design/pro-table';
+import moment from 'moment';
+import { Divider, Modal, Tag } from 'antd';
+import BaseCrud from '@/components/BaseCrud';
+
+const service = new BaseService<AlarmItem>('device/alarm/history');
+const Alarm = () => {
+  const actionRef = useRef<ActionType>();
+
+  const columns: ProColumns<AlarmItem>[] = [
+    {
+      title: '设备ID',
+      dataIndex: 'deviceId',
+    },
+    {
+      title: '设备名称',
+      dataIndex: 'deviceName',
+    },
+    {
+      title: '告警名称',
+      dataIndex: 'alarmName',
+    },
+    {
+      title: '告警时间',
+      dataIndex: 'alarmTime',
+      width: '300px',
+      render: (text: any) => (text ? moment(text).format('YYYY-MM-DD HH:mm:ss') : '/'),
+      sorter: true,
+      defaultSortOrder: 'descend',
+    },
+    {
+      title: '处理状态',
+      dataIndex: 'state',
+      align: 'center',
+      width: '100px',
+      render: (text) =>
+        text === 'solve' ? <Tag color="#87d068">已处理</Tag> : <Tag color="#f50">未处理</Tag>,
+    },
+    {
+      title: '操作',
+      width: '120px',
+      align: 'center',
+      render: (record: any) => (
+        <>
+          <a
+            onClick={() => {
+              let content: string;
+              try {
+                content = JSON.stringify(record.alarmData, null, 2);
+              } catch (error) {
+                content = record.alarmData;
+              }
+              Modal.confirm({
+                width: '40VW',
+                title: '告警数据',
+                content: (
+                  <pre>
+                    {content}
+                    {record.state === 'solve' && (
+                      <>
+                        <br />
+                        <br />
+                        <span style={{ fontSize: 16 }}>处理结果:</span>
+                        <br />
+                        <p>{record.description}</p>
+                      </>
+                    )}
+                  </pre>
+                ),
+                okText: '确定',
+                cancelText: '关闭',
+              });
+            }}
+          >
+            详情
+          </a>
+          {record.state !== 'solve' ? <Divider type="vertical" /> : ''}
+          {record.state !== 'solve' && <a onClick={() => {}}>处理</a>}
+        </>
+      ),
+    },
+  ];
+
+  const schema = {};
+
+  return (
+    <PageContainer>
+      <BaseCrud
+        columns={columns}
+        service={service}
+        title={'告警记录'}
+        schema={schema}
+        actionRef={actionRef}
+      />
+    </PageContainer>
+  );
+};
+
+export default Alarm;

+ 13 - 0
src/pages/device/Alarm/typings.d.ts

@@ -0,0 +1,13 @@
+type AlarmItem = {
+  alarmId: string;
+  alarmName: string;
+  alarmTime: number;
+  deviceId: string;
+  deviceName: string;
+  id: string;
+  productId: string;
+  productName: string;
+  state: string;
+  updateTime: number;
+  alarmData: Record<string, any>;
+};

+ 101 - 0
src/pages/device/Command/index.tsx

@@ -0,0 +1,101 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import BaseService from '@/utils/BaseService';
+import { useRef } from 'react';
+import type { ProColumns, ActionType } from '@ant-design/pro-table';
+import type { CommandItem } from '@/pages/device/Command/typings';
+import { Divider } from 'antd';
+import moment from 'moment';
+import BaseCrud from '@/components/BaseCrud';
+
+const service = new BaseService('device/message/task');
+const Command = () => {
+  const actionRef = useRef<ActionType>();
+
+  const columns: ProColumns<CommandItem>[] = [
+    {
+      title: '设备ID',
+      dataIndex: 'deviceId',
+    },
+    {
+      title: '设备名称',
+      dataIndex: 'deviceName',
+    },
+    {
+      title: '指令类型',
+      dataIndex: 'messageType',
+      filters: [
+        { text: '读取属性', value: 'READ_PROPERTY' },
+        { text: '设置属性', value: 'WRITE_PROPERTY' },
+        { text: '调用功能', value: 'INVOKE_FUNCTION' },
+      ],
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      filters: [
+        { text: '等待中', value: 'wait' },
+        { text: '发送失败', value: 'sendError' },
+        { text: '发送成功', value: 'success' },
+      ],
+      render: (value: any) => value.text,
+    },
+    {
+      title: '错误信息',
+      dataIndex: 'lastError',
+    },
+    {
+      title: '发送时间',
+      dataIndex: 'sendTimestamp',
+      render: (text: any) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
+      sorter: true,
+      defaultSortOrder: 'descend',
+    },
+    {
+      title: '操作',
+      render: (text, record) => (
+        <>
+          <a
+            onClick={() => {
+              // setVisible(true);
+              // setCurrent(record);
+            }}
+          >
+            查看指令
+          </a>
+          {record.state.value !== 'wait' && (
+            <>
+              <Divider type="vertical" />
+              <a
+                onClick={() => {
+                  // service.resend(encodeQueryParam({ terms: { id: record.id } })).subscribe(
+                  //   data => {
+                  //     message.success('操作成功');
+                  //   },
+                  //   () => {},
+                  //   () => handleSearch(searchParam),
+                  // );
+                }}
+              >
+                重新发送
+              </a>
+            </>
+          )}
+        </>
+      ),
+    },
+  ];
+
+  const schema = {};
+  return (
+    <PageContainer>
+      <BaseCrud<CommandItem>
+        columns={columns}
+        service={service}
+        title={'指令下发'}
+        schema={schema}
+        actionRef={actionRef}
+      />
+    </PageContainer>
+  );
+};
+export default Command;

+ 28 - 0
src/pages/device/Command/typings.d.ts

@@ -0,0 +1,28 @@
+export type CommandItem = {
+  id: string;
+  deviceId: string;
+  deviceName: string;
+  lastError: string;
+  lastErrorCode: string;
+  maxRetryTimes: number;
+  messageId: string;
+  messageType: string;
+  productId: string;
+  replyTimestamp: number;
+  retryTimes: number;
+  sendTimestamp: number;
+  serverId: string;
+  state: {
+    text: string;
+    value: string;
+  };
+  downstream: {
+    deviceId: string;
+    functionId: string;
+    headers: Record<string, any>;
+    inputs: Record<string, any>[];
+    messageId: string;
+    messageType: string;
+    timestamp: number;
+  };
+};

+ 6 - 0
src/pages/device/DataSource/index.tsx

@@ -0,0 +1,6 @@
+import { PageContainer } from '@ant-design/pro-layout';
+
+const DataSource = () => {
+  return <PageContainer>....</PageContainer>;
+};
+export default DataSource;

+ 0 - 0
src/pages/device/DataSource/typings.d.ts


+ 78 - 0
src/pages/device/Firmware/index.tsx

@@ -0,0 +1,78 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import BaseService from '@/utils/BaseService';
+import type { ProColumns, ActionType } from '@ant-design/pro-table';
+import { Divider, Popconfirm } from 'antd';
+import moment from 'moment';
+import { useRef } from 'react';
+import BaseCrud from '@/components/BaseCrud';
+
+const service = new BaseService('firmware');
+
+const Firmware = () => {
+  const actionRef = useRef<ActionType>();
+
+  const columns: ProColumns<FirmwareItem>[] = [
+    {
+      title: '固件名称',
+      dataIndex: 'name',
+    },
+    {
+      title: '固件版本',
+      dataIndex: 'version',
+    },
+    {
+      title: '所属产品',
+      dataIndex: 'productName',
+    },
+    {
+      title: '签名方式',
+      dataIndex: 'signMethod',
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createTime',
+      width: '200px',
+      align: 'center',
+      render: (text: any) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
+      sorter: true,
+      defaultSortOrder: 'descend',
+    },
+    {
+      title: '操作',
+      width: '300px',
+      align: 'center',
+      renderText: () => (
+        <>
+          <a
+            onClick={() => {
+              // router.push(`/device/firmware/save/${record.id}`);
+            }}
+          >
+            查看
+          </a>
+          <Divider type="vertical" />
+          <a onClick={() => {}}>编辑</a>
+          <Divider type="vertical" />
+          <Popconfirm title="确定删除?" onConfirm={() => {}}>
+            <a>删除</a>
+          </Popconfirm>
+        </>
+      ),
+    },
+  ];
+
+  const schema = {};
+
+  return (
+    <PageContainer>
+      <BaseCrud<FirmwareItem>
+        columns={columns}
+        service={service}
+        title={'固件升级'}
+        schema={schema}
+        actionRef={actionRef}
+      />
+    </PageContainer>
+  );
+};
+export default Firmware;

+ 13 - 0
src/pages/device/Firmware/typings.d.ts

@@ -0,0 +1,13 @@
+type FirmwareItem = {
+  createTime: number;
+  id: string;
+  name: string;
+  productId: string;
+  productName: string;
+  sign: string;
+  signMethod: string;
+  size: number;
+  url: string;
+  version: string;
+  versionOrder: number;
+};

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

@@ -0,0 +1,144 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import type { ProColumns, ActionType } from '@ant-design/pro-table';
+import type { DeviceInstance } from '@/pages/device/Instance/typings';
+import moment from 'moment';
+import { Badge, Divider, Popconfirm } from 'antd';
+import { useRef } from 'react';
+import BaseCrud from '@/components/BaseCrud';
+import BaseService from '@/utils/BaseService';
+
+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 service = new BaseService<DeviceInstance>('device/instance');
+const Instance = () => {
+  const actionRef = useRef<ActionType>();
+
+  const columns: ProColumns<DeviceInstance>[] = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+    },
+    {
+      title: '设备名称',
+      dataIndex: 'name',
+      ellipsis: true,
+    },
+    {
+      title: '产品名称',
+      dataIndex: 'productName',
+      ellipsis: true,
+    },
+    {
+      title: '注册时间',
+      dataIndex: 'registryTime',
+      width: '200px',
+      render: (text: any) => (text ? moment(text).format('YYYY-MM-DD HH:mm:ss') : '/'),
+      sorter: true,
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      width: '90px',
+      renderText: (record) =>
+        record ? <Badge status={statusMap.get(record.value)} text={record.text} /> : '',
+      filters: [
+        {
+          text: '未启用',
+          value: 'notActive',
+        },
+        {
+          text: '离线',
+          value: 'offline',
+        },
+        {
+          text: '在线',
+          value: 'online',
+        },
+      ],
+      filterMultiple: false,
+    },
+    {
+      title: '说明',
+      dataIndex: 'describe',
+      width: '15%',
+      ellipsis: true,
+    },
+    {
+      title: '操作',
+      width: '200px',
+      align: 'center',
+      render: (record: any) => (
+        <>
+          <a
+            onClick={() => {
+              // router.push(`/device/instance/save/${record.id}`);
+            }}
+          >
+            查看
+          </a>
+          <Divider type="vertical" />
+          <a
+            onClick={() => {
+              // setCurrentItem(record);
+              // setAddVisible(true);
+            }}
+          >
+            编辑
+          </a>
+          <Divider type="vertical" />
+          {record.state?.value === 'notActive' ? (
+            <span>
+              <Popconfirm
+                title="确认启用?"
+                onConfirm={() => {
+                  // changeDeploy(record);
+                }}
+              >
+                <a>启用</a>
+              </Popconfirm>
+              <Divider type="vertical" />
+              <Popconfirm
+                title="确认删除?"
+                onConfirm={() => {
+                  // deleteInstance(record);
+                }}
+              >
+                <a>删除</a>
+              </Popconfirm>
+            </span>
+          ) : (
+            <Popconfirm
+              title="确认禁用设备?"
+              onConfirm={() => {
+                // unDeploy(record);
+              }}
+            >
+              <a>禁用</a>
+            </Popconfirm>
+          )}
+        </>
+      ),
+    },
+  ];
+
+  const schema = {};
+
+  return (
+    <PageContainer>
+      <BaseCrud
+        columns={columns}
+        service={service}
+        title={'设备管理'}
+        schema={schema}
+        actionRef={actionRef}
+      />
+    </PageContainer>
+  );
+};
+export default Instance;

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

@@ -0,0 +1,39 @@
+export type DeviceInstance = {
+  id: string;
+  name: string;
+  describe: string;
+  description: string;
+  productId: string;
+  productName: string;
+  protocolName: string;
+  security: any;
+  deriveMetadata: string;
+  metadata: string;
+  binds: any;
+  state: {
+    value: string;
+    text: string;
+  };
+  creatorId: string;
+  creatorName: string;
+  createTime: number;
+  registryTime: string;
+  disabled?: boolean;
+  aloneConfiguration?: boolean;
+  deviceType: {
+    value: string;
+    text: string;
+  };
+  transportProtocol: string;
+  messageProtocol: string;
+  orgId: string;
+  orgName: string;
+  configuration: Record<string, any>;
+  cachedConfiguration: any;
+  transport: string;
+  protocol: string;
+  address: string;
+  registerTime: number;
+  onlineTime: string | number;
+  tags: any;
+};

+ 12 - 0
src/pages/device/Product/Detail/BaseInfo/index.tsx

@@ -0,0 +1,12 @@
+import { observer } from '@formily/react';
+import { productModel } from '@/pages/device/Product';
+
+const BaseInfo = observer(() => {
+  return (
+    <div>
+      基础信息
+      {JSON.stringify(productModel.current)}
+    </div>
+  );
+});
+export default BaseInfo;

+ 57 - 0
src/pages/device/Product/Detail/index.tsx

@@ -0,0 +1,57 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { history } from 'umi';
+import { Button, Card, Descriptions, Space, Tabs } from 'antd';
+import BaseInfo from '@/pages/device/Product/Detail/BaseInfo';
+import { observer } from '@formily/react';
+import { productModel, statusMap } from '@/pages/device/Product';
+import { useEffect } from 'react';
+
+const ProductDetail = observer(() => {
+  useEffect(() => {
+    if (!productModel.current) history.goBack();
+  }, []);
+  return (
+    <PageContainer
+      onBack={() => history.goBack()}
+      extraContent={<Space size={24}></Space>}
+      content={
+        <Descriptions size="small" column={2}>
+          <Descriptions.Item label="产品ID">{productModel.current?.id}</Descriptions.Item>
+          <Descriptions.Item label="产品名称">{productModel.current?.name}</Descriptions.Item>
+          <Descriptions.Item label="所属品类">
+            {productModel.current?.classifiedName}
+          </Descriptions.Item>
+          <Descriptions.Item label="消息协议">
+            {productModel.current?.protocolName}
+          </Descriptions.Item>
+          <Descriptions.Item label="链接协议">
+            {productModel.current?.transportProtocol}
+          </Descriptions.Item>
+          <Descriptions.Item label="创建时间">{productModel.current?.createTime}</Descriptions.Item>
+        </Descriptions>
+      }
+      extra={[
+        statusMap[productModel.current?.state || 0],
+        <Button key="2">停用</Button>,
+        <Button key="1" type="primary">
+          应用配置
+        </Button>,
+      ]}
+    >
+      <Card>
+        <Tabs defaultActiveKey={'base'}>
+          <Tabs.TabPane tab={'配置信息'} key={'base'}>
+            <BaseInfo />
+          </Tabs.TabPane>
+          <Tabs.TabPane tab={'物模型'} key={'metadata'}>
+            物模型
+          </Tabs.TabPane>
+          <Tabs.TabPane tab={'告警设置'} key={'alarm'}>
+            告警设置
+          </Tabs.TabPane>
+        </Tabs>
+      </Card>
+    </PageContainer>
+  );
+});
+export default ProductDetail;

+ 218 - 0
src/pages/device/Product/index.tsx

@@ -0,0 +1,218 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import ProList from '@ant-design/pro-list';
+import { Badge, Button, Card, message, Space, Tag, Tooltip } from 'antd';
+import type { ProductItem } from '@/pages/device/Product/typings';
+import {
+  CloseCircleOutlined,
+  DownloadOutlined,
+  EditOutlined,
+  EyeOutlined,
+  GoldOutlined,
+  MinusOutlined,
+  PlayCircleOutlined,
+} from '@ant-design/icons';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { lastValueFrom } from 'rxjs';
+import Service from '@/pages/device/Product/service';
+import { observer } from '@formily/react';
+import { Link } from '@umijs/preset-dumi/lib/theme';
+import { model } from '@formily/reactive';
+
+const service = new Service('device-product');
+export const statusMap = {
+  1: <Badge status="processing" text="已发布" />,
+  0: <Badge status="error" text="未发布" />,
+};
+export const productModel = model<{
+  current: ProductItem | undefined;
+}>({
+  current: undefined,
+});
+const Product = observer(() => {
+  const intl = useIntl();
+  return (
+    <PageContainer>
+      <Card>
+        <ProList<ProductItem>
+          toolBarRender={() => {
+            return [
+              <Button key="3" type="primary">
+                新建
+              </Button>,
+            ];
+          }}
+          search={{
+            filterType: 'light',
+          }}
+          rowKey={'id'}
+          headerTitle="产品列表"
+          request={async (params = {}) => {
+            return await lastValueFrom(service.list(params));
+            // console.log(ii, 'i')
+            // return await service.query(params);
+          }}
+          pagination={{
+            pageSize: 5,
+          }}
+          // showActions="hover"
+          metas={{
+            id: {
+              dataIndex: 'id',
+              title: 'ID',
+            },
+            title: {
+              dataIndex: 'name',
+              title: '名称',
+            },
+            avatar: {
+              dataIndex: 'id',
+              search: false,
+            },
+            description: {
+              dataIndex: 'classifiedName',
+              search: false,
+            },
+            subTitle: {
+              dataIndex: 'state',
+              render: (_, row) => <Space size={0}>{statusMap[row.state]}</Space>,
+              search: false,
+            },
+            content: {
+              search: false,
+              render: (_, row) => (
+                <div
+                  style={{
+                    flex: 1,
+                    display: 'flex',
+                    justifyContent: 'flex-start',
+                  }}
+                >
+                  <div
+                    style={{
+                      width: 200,
+                    }}
+                  >
+                    <div>ID</div>
+                    {row.id}
+                  </div>
+                  <div
+                    style={{
+                      width: 200,
+                    }}
+                  >
+                    <div>设备数量</div>
+                    <Badge
+                      showZero={true}
+                      className="site-badge-count-109"
+                      count={row.count}
+                      style={{ backgroundColor: '#52c41a' }}
+                    />
+                  </div>
+                  <div
+                    style={{
+                      width: 200,
+                    }}
+                  >
+                    <div>设备分类</div>
+                    <Tag icon={<GoldOutlined />} color="#55acee">
+                      {' '}
+                      {row.deviceType.text}
+                    </Tag>
+                  </div>
+                </div>
+              ),
+            },
+            actions: {
+              render: (_, record) => [
+                <Tooltip
+                  title={intl.formatMessage({
+                    id: 'pages.data.option.detail',
+                    defaultMessage: '查看',
+                  })}
+                  key={'detail'}
+                >
+                  <Link
+                    onClick={() => {
+                      productModel.current = record;
+                    }}
+                    to={`/device/product/detail/${record.id}`}
+                    key="link"
+                  >
+                    <EyeOutlined />
+                  </Link>
+                </Tooltip>,
+                <Tooltip
+                  title={intl.formatMessage({
+                    id: 'pages.data.option.edit',
+                    defaultMessage: '编辑',
+                  })}
+                  key={'edit'}
+                >
+                  <a key="warning">
+                    <EditOutlined />
+                  </a>
+                </Tooltip>,
+
+                <Tooltip
+                  title={intl.formatMessage({
+                    id: 'pages.data.option.download',
+                    defaultMessage: '下载',
+                  })}
+                  key={'download'}
+                >
+                  <a key="download">
+                    <DownloadOutlined
+                      onClick={() => {
+                        message.success('下载');
+                      }}
+                    />
+                  </a>
+                </Tooltip>,
+                <Tooltip
+                  title={intl.formatMessage({
+                    id: `pages.data.option.${record.state ? 'disable' : 'enable'}`,
+                    defaultMessage: record.state ? '禁用' : '启用',
+                  })}
+                  key={'state'}
+                >
+                  <a key="state">
+                    {record.state ? <CloseCircleOutlined /> : <PlayCircleOutlined />}
+                  </a>
+                </Tooltip>,
+                <Tooltip
+                  title={intl.formatMessage({
+                    id: 'pages.data.option.remove',
+                    defaultMessage: '删除',
+                  })}
+                  key={'remove'}
+                >
+                  <a key="delete">
+                    <MinusOutlined />
+                  </a>
+                </Tooltip>,
+              ],
+              search: false,
+            },
+            status: {
+              // 自己扩展的字段,主要用于筛选,不在列表中显示
+              title: '状态',
+              valueType: 'select',
+              valueEnum: {
+                all: { text: '全部', status: 'Default' },
+                0: {
+                  text: '已发布',
+                  status: 'Error',
+                },
+                1: {
+                  text: '未发布',
+                  status: 'Success',
+                },
+              },
+            },
+          }}
+        />
+      </Card>
+    </PageContainer>
+  );
+});
+export default Product;

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

@@ -0,0 +1,32 @@
+import BaseService from '@/utils/BaseService';
+import type { ProductItem } from '@/pages/device/Product/typings';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+import { from, toArray } from 'rxjs';
+import { map, mergeMap } from 'rxjs/operators';
+import encodeQuery from '@/utils/encodeQuery';
+import type { Response } from '@/utils/typings';
+import _ from 'lodash';
+
+class Service extends BaseService<ProductItem> {
+  public instanceCount = (params: Record<string, any>) => {
+    return request(`/${SystemConst.API_BASE}/device-instance/_count`, { params, method: 'GET' });
+  };
+
+  public list = (params: any) =>
+    from(this.query(params)).pipe(
+      mergeMap((i: Response<ProductItem>) =>
+        from(i.result.data as ProductItem[]).pipe(
+          mergeMap((t: ProductItem) =>
+            from(this.instanceCount(encodeQuery({ terms: { productId: t.id } }))).pipe(
+              map((count) => ({ ...t, count: count.result })),
+            ),
+          ),
+          toArray(),
+          map((data) => _.set(i, 'result.data', data) as any),
+        ),
+      ),
+    );
+}
+
+export default Service;

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

@@ -0,0 +1,20 @@
+export type ProductItem = {
+  id: string;
+  name: string;
+  classifiedId: string;
+  classifiedName: string;
+  configuration: Record<string, any>;
+  createTime: number;
+  creatorId: string;
+  deviceType: {
+    text: string;
+    value: string;
+  };
+  count?: number;
+  messageProtocol: string;
+  metadata: string;
+  orgId: string;
+  protocolName: string;
+  state: number;
+  transportProtocol: string;
+};