فهرست منبع

feat: 新增物联网卡-仪表盘

xieyonghong 3 سال پیش
والد
کامیت
12e3a08123

+ 1 - 0
src/components/index.ts

@@ -16,3 +16,4 @@ export { default as GeoPoint } from './GeoPoint';
 export { default as MetadataJsonInput } from './FormItems/MetadataJsonInput';
 export { default as Ellipsis } from './Ellipsis';
 export { default as FDatePicker } from './FDatePicker';
+export { default as DashBoard } from './DashBoard';

+ 130 - 0
src/pages/iot-card/CardManagement/BindDevice.tsx

@@ -0,0 +1,130 @@
+import { Badge, message, Modal } from 'antd';
+import SearchComponent from '@/components/SearchComponent';
+import type { ProColumns } from '@jetlinks/pro-table';
+import type { DeviceInstance } from '@/pages/device/Instance/typings';
+import moment from 'moment';
+import { statusMap } from '@/pages/device/Instance';
+import { service } from './index';
+import { useCallback, useRef, useState } from 'react';
+import ProTable from '@jetlinks/pro-table';
+import type { ActionType } from '@jetlinks/pro-table';
+
+type BindDeviceType = {
+  cardId: string;
+  onCancel: () => void;
+  onOk: () => void;
+};
+
+const BindDevice = (props: BindDeviceType) => {
+  const actionRef = useRef<ActionType>();
+  const [searchParams, setSearchParams] = useState<any>({});
+  const [bindKey, setBindKey] = useState<any>('');
+  const [loading, setLoading] = useState(false);
+
+  const columns: ProColumns<DeviceInstance>[] = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      width: 300,
+      ellipsis: true,
+      fixed: 'left',
+    },
+    {
+      title: '设备名称',
+      dataIndex: 'name',
+      ellipsis: true,
+      width: 200,
+    },
+    {
+      title: '注册时间',
+      dataIndex: 'registryTime',
+      width: '200px',
+      valueType: 'dateTime',
+      render: (_: any, row) => {
+        return row.registryTime ? moment(row.registryTime).format('YYYY-MM-DD HH:mm:ss') : '';
+      },
+      sorter: true,
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      width: '90px',
+      valueType: 'select',
+      renderText: (record) =>
+        record ? <Badge status={statusMap.get(record.value)} text={record.text} /> : '',
+      valueEnum: {
+        notActive: {
+          text: '禁用',
+          status: 'notActive',
+        },
+        offline: {
+          text: '离线',
+          status: 'offline',
+        },
+        online: {
+          text: '在线',
+          status: 'online',
+        },
+      },
+      filterMultiple: false,
+    },
+  ];
+
+  const submit = useCallback(async () => {
+    setLoading(true);
+    const resp = await service.bind(props.cardId, bindKey[0]);
+    setLoading(false);
+    if (resp.status === 200) {
+      message.success('操作成功');
+      props?.onOk();
+    }
+  }, [bindKey]);
+
+  return (
+    <Modal
+      title={'选择设备'}
+      width={1000}
+      visible={true}
+      confirmLoading={loading}
+      onCancel={props.onCancel}
+      onOk={submit}
+    >
+      <SearchComponent<DeviceInstance>
+        field={columns}
+        target="iot-card-bind-device"
+        onSearch={(data) => {
+          actionRef.current?.reset?.();
+          setSearchParams(data);
+        }}
+      />
+      <ProTable
+        columns={columns}
+        actionRef={actionRef}
+        params={searchParams}
+        request={(params) =>
+          service.queryUnbounded({
+            ...params,
+            sorts: [
+              {
+                name: 'createTime',
+                order: 'desc',
+              },
+            ],
+          })
+        }
+        rowKey="id"
+        search={false}
+        pagination={{ pageSize: 10 }}
+        rowSelection={{
+          type: 'radio',
+          selectedRowKeys: [bindKey],
+          onSelect: (record) => {
+            setBindKey(record.id);
+          },
+        }}
+      />
+    </Modal>
+  );
+};
+
+export default BindDevice;

+ 21 - 0
src/pages/iot-card/CardManagement/Detail/index.less

@@ -0,0 +1,21 @@
+.iot-card-detail-total-item {
+  padding: 20px;
+  color: rgba(0, 0, 0, 0.64);
+  font-size: 14px;
+  background: #fcfcfc;
+  border: 1px solid #e0e4e8;
+
+  .detail-total-item-content {
+    font-size: 22px;
+
+    .detail-total-item-value {
+      margin-right: 4px;
+      color: #323130;
+      font-size: 32px;
+    }
+  }
+
+  &:not(:last-child) {
+    margin-bottom: 16px;
+  }
+}

+ 423 - 0
src/pages/iot-card/CardManagement/Detail/index.tsx

@@ -0,0 +1,423 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { Card, Col, Descriptions, Row } from 'antd';
+import { EditOutlined } from '@ant-design/icons';
+import { PermissionButton, DashBoard } from '@/components';
+import SaveModal from '../SaveModal';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import type { CardManagement } from '@/pages/iot-card/CardManagement/typing';
+import { useParams } from 'umi';
+import { service } from '../index';
+import type { EChartsOption } from 'echarts';
+import Echarts from '@/components/DashBoard/echarts';
+import moment from 'moment';
+import './index.less';
+
+const DefaultEchartsOptions: any = {
+  yAxis: {
+    type: 'value',
+    show: false,
+  },
+  grid: {
+    top: '5%',
+    left: '2%',
+    bottom: 20,
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow',
+    },
+  },
+};
+
+const CardDetail = () => {
+  const [visible, setVisible] = useState(false);
+  const [detail, setDetail] = useState<Partial<CardManagement>>({});
+  const { permission } = PermissionButton.usePermission('device/Instance');
+  const [options, setOptions] = useState<EChartsOption>({});
+  const [dayOptions, setDayOptions] = useState<EChartsOption>({});
+  const [monthOptions, setMonthOptions] = useState<EChartsOption>({});
+  const [yearOptions, setYearOptions] = useState<EChartsOption>({});
+  const [dayTotal, setTotal] = useState(0);
+  const [monthTotal, setMonthTotal] = useState(0);
+  const [yearTotal, setYearTotal] = useState(0);
+  const echartsRef = useRef<any>();
+  const params = useParams<{ id: string }>();
+
+  const getDetail = useCallback(
+    (id?: string) => {
+      if (id) {
+        service.detail(id).then((resp) => {
+          if (resp.status === 200) {
+            setDetail(resp.result);
+          }
+        });
+      }
+    },
+    [params.id],
+  );
+
+  const getData = useCallback(
+    (start: number, end: number): Promise<{ xValue: any[]; data: any[] }> => {
+      return new Promise((resolve) => {
+        service
+          .queryFlow(start, end, {
+            orderBy: 'date',
+            terms: [
+              {
+                column: 'cardId',
+                termType: 'eq',
+                value: params.id,
+              },
+            ],
+          })
+          .then((resp) => {
+            if (resp.status === 200) {
+              const sortArray = resp.result.sort(
+                (a: any, b: any) => new Date(a.date).getTime() - new Date(b.date).getTime(),
+              );
+              resolve({
+                xValue: sortArray.map((item: any) => item.date),
+                data: sortArray.map((item: any) => item.value),
+              });
+            }
+          });
+      });
+    },
+    [params.id],
+  );
+
+  const getDataTotal = useCallback(() => {
+    if (!params.id) return;
+    const dTime = [
+      moment().subtract(1, 'days').startOf('day').valueOf(),
+      moment().subtract(1, 'days').endOf('day').valueOf(),
+    ];
+    const mTime = [moment().startOf('month').valueOf(), moment().endOf('month').valueOf()];
+    const yTime = [moment().startOf('year').valueOf(), moment().endOf('year').valueOf()];
+    const grid: any = {
+      top: '2%',
+      left: '0',
+      right: 0,
+      bottom: 0,
+    };
+    getData(dTime[0], dTime[1]).then((resp) => {
+      setTotal(resp.data.reduce((r, n) => r + n, 0));
+      setDayOptions({
+        ...DefaultEchartsOptions,
+        grid,
+        xAxis: {
+          type: 'category',
+          data: resp.xValue,
+          show: false,
+        },
+        series: [
+          {
+            name: '流量',
+            data: resp.data,
+            type: 'line',
+            color: '#FBA500',
+            areaStyle: {
+              color: {
+                type: 'linear',
+                x: 0,
+                y: 0,
+                x2: 0,
+                y2: 1,
+                colorStops: [
+                  {
+                    offset: 0,
+                    color: '#FBA500', // 100% 处的颜色
+                  },
+                  {
+                    offset: 1,
+                    color: '#FFFFFF', //   0% 处的颜色
+                  },
+                ],
+                global: false, // 缺省为 false
+              },
+            },
+          },
+        ],
+      });
+    });
+    getData(mTime[0], mTime[1]).then((resp) => {
+      setMonthTotal(resp.data.reduce((r, n) => r + n, 0));
+      setMonthOptions({
+        ...DefaultEchartsOptions,
+        grid,
+        xAxis: {
+          type: 'category',
+          data: resp.xValue,
+          show: false,
+        },
+        series: [
+          {
+            name: '流量',
+            data: resp.data,
+            type: 'line',
+            color: '#498BEF',
+            areaStyle: {
+              color: {
+                type: 'linear',
+                x: 0,
+                y: 0,
+                x2: 0,
+                y2: 1,
+                colorStops: [
+                  {
+                    offset: 0,
+                    color: '#498BEF', // 100% 处的颜色
+                  },
+                  {
+                    offset: 1,
+                    color: '#FFFFFF', //   0% 处的颜色
+                  },
+                ],
+                global: false, // 缺省为 false
+              },
+            },
+          },
+        ],
+      });
+    });
+    getData(yTime[0], yTime[1]).then((resp) => {
+      setYearTotal(resp.data.reduce((r, n) => r + n, 0));
+      setYearOptions({
+        ...DefaultEchartsOptions,
+        grid,
+        xAxis: {
+          type: 'category',
+          data: resp.xValue,
+          show: false,
+        },
+        series: [
+          {
+            name: '流量',
+            data: resp.data,
+            type: 'line',
+            color: '#58E1D3',
+            areaStyle: {
+              color: {
+                type: 'linear',
+                x: 0,
+                y: 0,
+                x2: 0,
+                y2: 1,
+                colorStops: [
+                  {
+                    offset: 0,
+                    color: '#58E1D3', // 100% 处的颜色
+                  },
+                  {
+                    offset: 1,
+                    color: '#FFFFFF', //   0% 处的颜色
+                  },
+                ],
+                global: false, // 缺省为 false
+              },
+            },
+          },
+        ],
+      });
+    });
+  }, [params.id]);
+
+  const getEcharts = useCallback(
+    (data: any) => {
+      if (!params.id) return;
+      getData(data.time.start, data.time.end).then((resp) => {
+        setOptions({
+          ...DefaultEchartsOptions,
+          xAxis: {
+            type: 'category',
+            data: resp.xValue,
+          },
+          series: [
+            {
+              name: '流量',
+              data: resp.data,
+              type: 'line',
+              color: '#498BEF',
+              areaStyle: {
+                color: {
+                  type: 'linear',
+                  x: 0,
+                  y: 0,
+                  x2: 0,
+                  y2: 1,
+                  colorStops: [
+                    {
+                      offset: 0,
+                      color: '#498BEF', // 100% 处的颜色
+                    },
+                    {
+                      offset: 1,
+                      color: '#FFFFFF', //   0% 处的颜色
+                    },
+                  ],
+                  global: false, // 缺省为 false
+                },
+              },
+            },
+          ],
+        });
+      });
+    },
+    [params.id],
+  );
+
+  useEffect(() => {
+    getDetail(params.id || '');
+
+    getEcharts(echartsRef?.current?.getValues());
+    getDataTotal();
+  }, [params.id]);
+
+  return (
+    <PageContainer>
+      {visible && (
+        <SaveModal
+          type={'edit'}
+          data={detail}
+          onCancel={() => {
+            setVisible(false);
+          }}
+          onOk={() => {
+            getDetail();
+          }}
+        />
+      )}
+      <Row gutter={[24, 24]}>
+        <Col span={24}>
+          <Card>
+            <Descriptions
+              size="small"
+              column={3}
+              bordered
+              title={[
+                <span key={1}>基本信息</span>,
+                <PermissionButton
+                  isPermission={permission.update}
+                  key={2}
+                  type={'link'}
+                  onClick={() => {
+                    setVisible(true);
+                  }}
+                >
+                  <EditOutlined />
+                  编辑
+                </PermissionButton>,
+              ]}
+            >
+              <Descriptions.Item label={'卡号'}>{detail.id}</Descriptions.Item>
+              <Descriptions.Item label={'ICCID'}>{detail.iccId}</Descriptions.Item>
+              <Descriptions.Item label={'绑定设备'}>{detail.deviceName}</Descriptions.Item>
+              <Descriptions.Item label={'运营商'}>{detail.operatorName}</Descriptions.Item>
+              <Descriptions.Item label={'类型'}>{detail.cardType?.text}</Descriptions.Item>
+              <Descriptions.Item label={'激活日期'}>
+                {detail.activationDate
+                  ? moment(detail.activationDate).format('YYYY-MM-DD HH:mm:ss')
+                  : ''}
+              </Descriptions.Item>
+              <Descriptions.Item label={'更新时间'}>
+                {detail.updateTime ? moment(detail.updateTime).format('YYYY-MM-DD HH:mm:ss') : ''}
+              </Descriptions.Item>
+              <Descriptions.Item label={'总流量'}>
+                {detail.totalFlow ? detail.totalFlow.toFixed(2) + ' M' : ''}
+              </Descriptions.Item>
+              <Descriptions.Item label={'使用流量'}>
+                {detail.usedFlow ? detail.usedFlow.toFixed(2) + ' M' : ''}
+              </Descriptions.Item>
+              <Descriptions.Item label={'剩余流量'}>
+                {detail.residualFlow ? detail.residualFlow.toFixed(2) + ' M' : ''}
+              </Descriptions.Item>
+              <Descriptions.Item label={'状态'}>{detail?.cardState?.text}</Descriptions.Item>
+              <Descriptions.Item label={'说明'}>{detail?.describe}</Descriptions.Item>
+            </Descriptions>
+          </Card>
+        </Col>
+        <Col span={24}>
+          <Row gutter={24}>
+            <Col flex={'auto'}>
+              <DashBoard
+                ref={echartsRef}
+                title={'流量统计'}
+                options={options}
+                defaultTime={'week'}
+                showTime={true}
+                showTimeTool={true}
+                height={500}
+                onParamsChange={getEcharts}
+              />
+            </Col>
+            <Col flex={'480px'}>
+              <Card>
+                <div
+                  style={{
+                    marginBottom: 20,
+                    color: '#323130',
+                    fontSize: 16,
+                    fontWeight: 600,
+                  }}
+                >
+                  数据统计
+                </div>
+                <div className={'iot-card-detail-total-item'}>
+                  <Row>
+                    <Col flex={'auto'}>
+                      <div className={'detail-total-item-name'}>昨日流量消耗</div>
+                      <div className={'detail-total-item-content'}>
+                        <span className={'detail-total-item-value'}>{dayTotal}</span>
+                        <span>M</span>
+                      </div>
+                    </Col>
+                    <Col flex={'240px'}>
+                      <div style={{ height: 83 }}>
+                        <Echarts options={dayOptions} />
+                      </div>
+                    </Col>
+                  </Row>
+                </div>
+                <div className={'iot-card-detail-total-item'}>
+                  <Row>
+                    <Col flex={'auto'}>
+                      <div className={'detail-total-item-name'}>当月流量消耗</div>
+                      <div className={'detail-total-item-content'}>
+                        <span className={'detail-total-item-value'}>{monthTotal}</span>
+                        <span>M</span>
+                      </div>
+                    </Col>
+                    <Col flex={'240px'}>
+                      <div style={{ height: 83 }}>
+                        <Echarts options={monthOptions} />
+                      </div>
+                    </Col>
+                  </Row>
+                </div>
+                <div className={'iot-card-detail-total-item'}>
+                  <Row>
+                    <Col flex={'auto'}>
+                      <div className={'detail-total-item-name'}>本年流量消耗</div>
+                      <div className={'detail-total-item-content'}>
+                        <span className={'detail-total-item-value'}>{yearTotal}</span>
+                        <span>M</span>
+                      </div>
+                    </Col>
+                    <Col flex={'240px'}>
+                      <div style={{ height: 84 }}>
+                        <Echarts options={yearOptions} />
+                      </div>
+                    </Col>
+                  </Row>
+                </div>
+              </Card>
+            </Col>
+          </Row>
+        </Col>
+      </Row>
+    </PageContainer>
+  );
+};
+
+export default CardDetail;

+ 21 - 6
src/pages/iot-card/CardManagement/ExportModal.tsx

@@ -1,7 +1,8 @@
 import { Modal, Radio, Space } from 'antd';
-import { useRef } from 'react';
+import { useRef, useState } from 'react';
 import { service } from './index';
-import { downloadFile } from '@/utils/util';
+import { downloadFileByUrl } from '@/utils/util';
+import moment from 'moment';
 
 type ExportModalType = {
   onCancel: () => void;
@@ -10,19 +11,33 @@ type ExportModalType = {
 
 const ExportModal = (props: ExportModalType) => {
   const type = useRef<string>('xlsx');
+  const [loading, setLoading] = useState(false);
 
   const downloadFileFn = async () => {
+    setLoading(true);
+
     service._export(type.current, props.keys).then((res) => {
-      if (res.status === 200) {
-        const blob = new Blob([res.data], { type: type.current });
+      setLoading(false);
+      if (res) {
+        const blob = new Blob([res]);
         const url = URL.createObjectURL(blob);
-        downloadFile(url);
+        downloadFileByUrl(
+          url,
+          `物联卡管理-${moment(new Date()).format('YYYY/MM/DD HH:mm:ss')}`,
+          type.current,
+        );
       }
     });
   };
 
   return (
-    <Modal title={'导出'} visible={true} onCancel={props.onCancel} onOk={downloadFileFn}>
+    <Modal
+      title={'导出'}
+      visible={true}
+      onCancel={props.onCancel}
+      onOk={downloadFileFn}
+      confirmLoading={loading}
+    >
       <div style={{ paddingLeft: 30 }}>
         <Space>
           <span>文件格式:</span>

+ 19 - 12
src/pages/iot-card/CardManagement/ImportModal.tsx

@@ -1,4 +1,4 @@
-import { Button, Form, message, Modal, Radio, Select, Upload } from 'antd';
+import { Button, Form, message, Modal, Radio, Select, Space, Upload } from 'antd';
 import { useCallback, useEffect, useState } from 'react';
 import { useRequest } from 'ahooks';
 import { service } from '@/pages/iot-card/CardManagement/index';
@@ -9,26 +9,30 @@ import { CheckOutlined } from '@ant-design/icons';
 
 type ImportModalType = {
   onCancel: () => void;
+  onOk: () => void;
 };
 
 const ImportModal = (props: ImportModalType) => {
   const [fileType, setFileType] = useState('xlsx');
   const [configId, setConfigId] = useState('');
   const [total, setTotal] = useState(0);
+  const [loading, setLoading] = useState(false);
 
   const { data: platformList, run: platformRun } = useRequest(service.queryPlatformNoPage, {
-    manual: false,
+    manual: true,
     formatResult(result) {
-      return result.data;
+      return result.result;
     },
   });
 
   const submitData = useCallback(
     (result: any) => {
       service._import(configId, { fileUrl: result }).then((resp) => {
+        setLoading(false);
         if (resp.status === 200) {
           setTotal(resp.result.total);
           message.success('导入成功');
+          props.onOk();
         } else {
           message.error(resp.message || '导入失败');
         }
@@ -38,8 +42,9 @@ const ImportModal = (props: ImportModalType) => {
   );
 
   const fileChange = (info: any) => {
+    setLoading(true);
     if (info.file.status === 'done') {
-      const resp = info.file.result || { result: '' };
+      const resp = info.file.response || { result: '' };
       submitData(resp.result);
     }
   };
@@ -102,21 +107,23 @@ const ImportModal = (props: ImportModalType) => {
                 showUploadList={false}
                 onChange={fileChange}
               >
-                <Button>上传文件</Button>
+                <Button loading={loading}>上传文件</Button>
               </Upload>
             </Form.Item>
             <Form.Item label={'下载模板'}>
-              <Button icon={'file'} onClick={() => downFileFn('xlsx')}>
-                .xlsx
-              </Button>
-              <Button icon={'file'} onClick={() => downFileFn('csv')}>
-                .csv
-              </Button>
+              <Space>
+                <Button icon={'file'} onClick={() => downFileFn('xlsx')}>
+                  .xlsx
+                </Button>
+                <Button icon={'file'} onClick={() => downFileFn('csv')}>
+                  .csv
+                </Button>
+              </Space>
             </Form.Item>
           </>
         )}
       </Form>
-      {total && (
+      {!!total && (
         <div>
           <CheckOutlined style={{ color: '#2F54EB', marginRight: 8 }} />
           已完成 总数量 <span style={{ color: '#2F54EB' }}>{total}</span>

+ 5 - 5
src/pages/iot-card/CardManagement/SaveModal.tsx

@@ -16,9 +16,9 @@ const Save = (props: SaveType) => {
   const [form] = Form.useForm();
 
   const { data: platformList, run: platformRun } = useRequest(service.queryPlatformNoPage, {
-    manual: false,
+    manual: true,
     formatResult(result) {
-      return result.data;
+      return result.result;
     },
   });
 
@@ -27,9 +27,9 @@ const Save = (props: SaveType) => {
       sorts: [{ name: 'createTime', order: 'desc' }],
       terms: [{ column: 'state', value: 'enabled' }],
     });
-    // if (props.type === 'edit' && form) {
-    //   form.setFieldsValue(props.data)
-    // }
+    if (props.type === 'edit' && form) {
+      form.setFieldsValue(props.data);
+    }
   }, []);
 
   const submit = async () => {

+ 347 - 88
src/pages/iot-card/CardManagement/index.tsx

@@ -1,21 +1,36 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import SearchComponent from '@/components/SearchComponent';
-import { useRef, useState } from 'react';
+import { useRef, useState, useEffect } from 'react';
 import type { ActionType } from '@jetlinks/pro-table';
-import { PermissionButton, ProTableCard } from '@/components';
+import { PermissionButton } from '@/components';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import type { ProColumns } from '@jetlinks/pro-table';
 import type { CardManagement } from './typing';
-import { Button, Dropdown, Menu } from 'antd';
+import { Button, Dropdown, Menu, message, Popconfirm } from 'antd';
 import {
   ExportOutlined,
   PlusOutlined,
   ImportOutlined,
   CheckCircleOutlined,
+  StopOutlined,
+  PoweroffOutlined,
+  SwapOutlined,
+  DeleteOutlined,
+  EditOutlined,
+  LinkOutlined,
+  EyeOutlined,
 } from '@ant-design/icons';
+import ProTable from '@jetlinks/pro-table';
+import Service from './service';
 import SaveModal from './SaveModal';
 import ExportModal from '@/pages/iot-card/CardManagement/ExportModal';
-import Service from './service';
+import ImportModal from '@/pages/iot-card/CardManagement/ImportModal';
+import BindDeviceModal from '@/pages/iot-card/CardManagement/BindDevice';
+import moment from 'moment';
+import { useDomFullHeight } from '@/hooks';
+import { onlyMessage } from '@/utils/util';
+import { useHistory, useLocation } from 'umi';
+import { getMenuPathByParams } from '@/utils/menu';
 
 export const service = new Service('network/card');
 
@@ -25,10 +40,22 @@ const CardManagementNode = () => {
   const [visible, setVisible] = useState<boolean>(false);
   const [exportVisible, setExportVisible] = useState<boolean>(false); // 导出
   const [importVisible, setImportVisible] = useState<boolean>(false); // 导入
+  const [bindDeviceVisible, setBindDeviceVisible] = useState<boolean>(false); // 绑定设备
   const [current, setCurrent] = useState<Partial<CardManagement>>({});
   const [bindKeys, setBindKeys] = useState<any[]>([]);
+  const { minHeight } = useDomFullHeight(`.iot-card-management`, 24);
   const { permission } = PermissionButton.usePermission('device/Instance');
   const intl = useIntl();
+  const history = useHistory();
+  const location = useLocation();
+
+  useEffect(() => {
+    const { state } = location;
+    if (state && state.save) {
+      setVisible(true);
+      setCurrent({});
+    }
+  }, [location]);
 
   const columns: ProColumns<CardManagement>[] = [
     {
@@ -48,71 +75,239 @@ const CardManagementNode = () => {
       title: '绑定设备',
       dataIndex: 'deviceId',
       ellipsis: true,
+      width: 200,
+      hideInSearch: true,
+      render: (_, record) => record.deviceName,
     },
     {
       title: '平台对接',
       dataIndex: 'platformConfigId',
+      width: 200,
+      valueType: 'select',
+      request: () =>
+        service
+          .queryPlatformNoPage({
+            sorts: [{ name: 'createTime', order: 'desc' }],
+            terms: [{ column: 'state', value: 'enabled' }],
+          })
+          .then((resp) => resp.result.map((item: any) => ({ label: item.name, value: item.id }))),
     },
     {
       title: '运营商',
       dataIndex: 'operatorName',
+      width: 120,
+      valueType: 'select',
+      request: async () => {
+        return [
+          { label: '移动', value: '移动' },
+          { label: '电信', value: '电信' },
+          { label: '联通', value: '联通' },
+        ];
+      },
     },
     {
       title: '类型',
       dataIndex: 'cardType',
+      width: 120,
+      valueType: 'select',
+      render(_, record) {
+        return record.cardType.text;
+      },
+      request: async () => {
+        return [
+          { label: '年卡', value: 'year' },
+          { label: '季卡', value: 'season' },
+          { label: '月卡', value: 'month' },
+          { label: '其他', value: 'other' },
+        ];
+      },
     },
     {
       title: '总流量',
       dataIndex: 'totalFlow',
+      width: 120,
+      render: (_, record) => (record.totalFlow ? record.totalFlow.toFixed(2) + ' M' : ''),
     },
     {
       title: '使用流量',
       dataIndex: 'usedFlow',
+      width: 120,
+      render: (_, record) => (record.usedFlow ? record.usedFlow.toFixed(2) + ' M' : ''),
     },
     {
       title: '剩余流量',
       dataIndex: 'residualFlow',
+      width: 120,
+      render: (_, record) => (record.residualFlow ? record.residualFlow.toFixed(2) + ' M' : ''),
     },
     {
       title: '激活日期',
       dataIndex: 'activationDate',
+      width: 200,
+      render: (_, record) =>
+        record.activationDate ? moment(record.activationDate).format('YYYY-MM-DD HH:mm:ss') : '',
     },
     {
       title: '更新时间',
       dataIndex: 'updateTime',
+      width: 200,
+      render: (_, record) =>
+        record.updateTime ? moment(record.updateTime).format('YYYY-MM-DD HH:mm:ss') : '',
     },
     {
       title: '状态',
       dataIndex: 'cardStateType',
-      valueEnum: {
-        using: {
-          text: '正常',
-          status: 'using',
-        },
-        toBeActivated: {
-          text: '未激活',
-          status: 'using',
-        },
-        deactivate: {
-          text: '停机',
-          status: 'using',
-        },
+      width: 180,
+      valueType: 'select',
+      render(_, record) {
+        return record.cardStateType?.text;
+      },
+      request: async () => {
+        return [
+          { label: '正常', value: 'using' },
+          { label: '未激活', value: 'toBeActivated' },
+          { label: '停机', value: 'deactivate' },
+        ];
       },
     },
     {
       title: '操作',
       valueType: 'option',
-      width: 120,
+      width: 200,
       fixed: 'right',
       render: (_, record) => {
-        console.log(_, record);
-        return [];
+        return [
+          <PermissionButton
+            style={{ padding: 0 }}
+            type="link"
+            isPermission={permission.update}
+            key="editable"
+            onClick={() => {
+              setCurrent(record);
+              setVisible(true);
+            }}
+            tooltip={{
+              title: intl.formatMessage({
+                id: 'pages.data.option.edit',
+                defaultMessage: '编辑',
+              }),
+            }}
+          >
+            <EditOutlined />
+          </PermissionButton>,
+          <PermissionButton
+            style={{ padding: 0 }}
+            type="link"
+            isPermission={true}
+            key="view"
+            onClick={() => {
+              const url = getMenuPathByParams('iot-card/CardManagement/Detail', record.id);
+              history.push(url);
+            }}
+            tooltip={{
+              title: '查看',
+            }}
+          >
+            <EyeOutlined />
+          </PermissionButton>,
+          <PermissionButton
+            type="link"
+            key="bindDevice"
+            style={{ padding: 0 }}
+            isPermission={permission.delete}
+            tooltip={{ title: '绑定设备' }}
+            onClick={() => {
+              setBindDeviceVisible(true);
+            }}
+          >
+            <LinkOutlined />
+          </PermissionButton>,
+          record.cardStateType.value === 'toBeActivated' ? (
+            <PermissionButton
+              type="link"
+              key="activation"
+              style={{ padding: 0 }}
+              isPermission={permission.active}
+              tooltip={{ title: '激活' }}
+              popConfirm={{
+                title: '确认激活?',
+                onConfirm: async () => {
+                  service.changeDeploy(record.id).then((resp) => {
+                    if (resp.status === 200) {
+                      message.success('操作成功');
+                      actionRef.current?.reload();
+                    }
+                  });
+                },
+              }}
+            >
+              <CheckCircleOutlined />
+            </PermissionButton>
+          ) : (
+            <PermissionButton
+              type="link"
+              key="activation"
+              style={{ padding: 0 }}
+              isPermission={permission.action}
+              tooltip={{ title: record.cardStateType.value === 'deactivate' ? '复机' : '停用' }}
+              popConfirm={{
+                title: record.cardStateType.value === 'deactivate' ? '确认复机?' : '确认停用?',
+                onConfirm: async () => {
+                  if (record.cardStateType.value === 'deactivate') {
+                    service.resumption(record.id).then((resp) => {
+                      if (resp.status === 200) {
+                        message.success('操作成功');
+                        actionRef.current?.reload();
+                      }
+                    });
+                  } else {
+                    service.unDeploy(record.id).then((resp) => {
+                      if (resp.status === 200) {
+                        message.success('操作成功');
+                        actionRef.current?.reload();
+                      }
+                    });
+                  }
+                },
+              }}
+            >
+              {record.cardStateType.value === 'deactivate' ? (
+                <PoweroffOutlined />
+              ) : (
+                <StopOutlined />
+              )}
+            </PermissionButton>
+          ),
+          <PermissionButton
+            type="link"
+            key="delete"
+            style={{ padding: 0 }}
+            isPermission={permission.delete}
+            tooltip={{ title: '删除' }}
+          >
+            <Popconfirm
+              onConfirm={async () => {
+                const resp: any = await service.remove(record.id);
+                if (resp.status === 200) {
+                  onlyMessage(
+                    intl.formatMessage({
+                      id: 'pages.data.option.success',
+                      defaultMessage: '操作成功!',
+                    }),
+                  );
+                  actionRef.current?.reload();
+                }
+              }}
+              title="确认删除?"
+            >
+              <DeleteOutlined />
+            </Popconfirm>
+          </PermissionButton>,
+        ];
       },
     },
   ];
 
-  console.log(exportVisible, importVisible);
-
   const menu = (
     <Menu>
       <Menu.Item key="1">
@@ -125,7 +320,7 @@ const CardManagementNode = () => {
           }}
           style={{ width: '100%' }}
         >
-          导出
+          批量导出
         </PermissionButton>
       </Menu.Item>
       <Menu.Item key="2">
@@ -138,87 +333,126 @@ const CardManagementNode = () => {
           }}
           style={{ width: '100%' }}
         >
-          导入
+          批量导入
         </PermissionButton>
       </Menu.Item>
-      {bindKeys.length > 0 && (
-        <Menu.Item key="3">
-          <PermissionButton
-            isPermission={permission.action}
-            icon={<CheckCircleOutlined />}
-            type="primary"
-            ghost
-            popConfirm={{
-              title: '确认激活吗?',
-              onConfirm: async () => {},
-            }}
-            style={{ width: '100%' }}
-          >
-            激活
-          </PermissionButton>
-        </Menu.Item>
-      )}
-      {bindKeys.length > 0 && (
-        <Menu.Item key="4">
-          <PermissionButton
-            isPermission={permission.stop}
-            icon={<CheckCircleOutlined />}
-            type="primary"
-            ghost
-            popConfirm={{
-              title: '确认停用吗?',
-              onConfirm: async () => {},
-            }}
-            style={{ width: '100%' }}
-          >
-            停用
-          </PermissionButton>
-        </Menu.Item>
-      )}
-      {bindKeys.length > 0 && (
-        <Menu.Item key="5">
-          <PermissionButton
-            isPermission={permission.restart}
-            icon={<CheckCircleOutlined />}
-            type="primary"
-            ghost
-            popConfirm={{
-              title: '确认复机吗?',
-              onConfirm: async () => {},
-            }}
-            style={{ width: '100%' }}
-          >
-            复机
-          </PermissionButton>
-        </Menu.Item>
-      )}
-      <Menu.Item key="6">
+      <Menu.Item key="3">
         <PermissionButton
-          isPermission={permission.sync}
+          isPermission={permission.active}
           icon={<CheckCircleOutlined />}
+          type="default"
+          popConfirm={{
+            title: '确认激活吗?',
+            onConfirm: async () => {
+              if (bindKeys.length >= 10 && bindKeys.length <= 100) {
+                service.changeDeployBatch(bindKeys).then((res) => {
+                  if (res.status === 200) {
+                    message.success('操作成功');
+                  }
+                });
+              } else {
+                message.warn('仅支持同一个运营商下且最少10条数据,最多100条数据');
+              }
+            },
+          }}
+          style={{ width: '100%' }}
+        >
+          批量激活
+        </PermissionButton>
+      </Menu.Item>
+      <Menu.Item key="4">
+        <PermissionButton
+          isPermission={permission.action}
+          icon={<StopOutlined />}
           type="primary"
           ghost
           popConfirm={{
-            title: '确认同步物联卡状态?',
-            onConfirm: async () => {},
+            title: '确认停用吗?',
+            onConfirm: async () => {
+              if (bindKeys.length >= 10 && bindKeys.length <= 100) {
+                service.unDeployBatch(bindKeys).then((res) => {
+                  if (res.status === 200) {
+                    message.success('操作成功');
+                  }
+                });
+              } else {
+                message.warn('仅支持同一个运营商下且最少10条数据,最多100条数据');
+              }
+            },
           }}
+          style={{ width: '100%' }}
         >
-          同步状态
+          批量停用
         </PermissionButton>
       </Menu.Item>
-      {bindKeys.length > 0 && (
+      <Menu.Item key="5">
         <PermissionButton
-          isPermission={permission.delete}
-          icon={<CheckCircleOutlined />}
+          isPermission={permission.action}
+          icon={<PoweroffOutlined />}
           type="primary"
           ghost
           popConfirm={{
-            title: '确认删除吗?',
-            onConfirm: async () => {},
+            title: '确认复机吗?',
+            onConfirm: async () => {
+              if (bindKeys.length >= 10 && bindKeys.length <= 100) {
+                service.resumptionBatch(bindKeys).then((res) => {
+                  if (res.status === 200) {
+                    message.success('操作成功');
+                  }
+                });
+              } else {
+                message.warn('仅支持同一个运营商下且最少10条数据,最多100条数据');
+              }
+            },
           }}
+          style={{ width: '100%' }}
         >
-          批量删除
+          批量复机
         </PermissionButton>
+      </Menu.Item>
+      <Menu.Item key="6">
+        <PermissionButton
+          isPermission={permission.sync}
+          icon={<SwapOutlined />}
+          type="primary"
+          ghost
+          popConfirm={{
+            title: '确认同步物联卡状态?',
+            onConfirm: async () => {
+              service.sync().then((res) => {
+                if (res.status === 200) {
+                  actionRef?.current?.reload();
+                  message.success('同步状态成功');
+                }
+              });
+            },
+          }}
+        >
+          同步状态
+        </PermissionButton>
+      </Menu.Item>
+      {bindKeys.length > 0 && (
+        <Menu.Item>
+          <PermissionButton
+            isPermission={permission.delete}
+            icon={<DeleteOutlined />}
+            type="default"
+            popConfirm={{
+              title: '确认删除吗?',
+              onConfirm: async () => {
+                service.removeCards(bindKeys).then((res) => {
+                  if (res.status === 200) {
+                    setBindKeys([]);
+                    message.success('操作成功');
+                    actionRef?.current?.reload();
+                  }
+                });
+              },
+            }}
+          >
+            批量删除
+          </PermissionButton>
+        </Menu.Item>
       )}
     </Menu>
   );
@@ -227,12 +461,13 @@ const CardManagementNode = () => {
     <PageContainer>
       {visible && (
         <SaveModal
-          type={'add'}
+          type={current.id ? 'edit' : 'add'}
           onCancel={() => {
             setVisible(false);
           }}
           data={current}
           onOk={() => {
+            setCurrent({});
             setVisible(false);
             actionRef.current?.reload();
           }}
@@ -246,6 +481,28 @@ const CardManagementNode = () => {
           }}
         />
       )}
+      {importVisible && (
+        <ImportModal
+          onCancel={() => {
+            setImportVisible(false);
+          }}
+          onOk={() => {
+            actionRef.current?.reload();
+          }}
+        />
+      )}
+      {bindDeviceVisible && (
+        <BindDeviceModal
+          cardId={current.id!}
+          onCancel={() => {
+            setBindDeviceVisible(false);
+          }}
+          onOk={() => {
+            setBindDeviceVisible(false);
+            actionRef.current?.reload();
+          }}
+        />
+      )}
       <SearchComponent<CardManagement>
         field={columns}
         target="iot-card-management"
@@ -254,12 +511,14 @@ const CardManagementNode = () => {
           setSearchParams(data);
         }}
       />
-      <ProTableCard<CardManagement>
+      <ProTable<CardManagement>
         columns={columns}
         scroll={{ x: 1366 }}
+        tableStyle={{ minHeight }}
         actionRef={actionRef}
         params={searchParams}
         options={{ fullScreen: true }}
+        tableClassName={'iot-card-management'}
         columnEmptyText={''}
         request={(params) =>
           service.query({

+ 18 - 3
src/pages/iot-card/CardManagement/service.ts

@@ -20,11 +20,20 @@ class Service extends BaseService<CardManagement> {
   unbind = (cardId: string) => this.GET(`${this.uri}/${cardId}/_unbind`);
 
   recharge = (data: any) => this.POST(`${this.uri}/_recharge`, data);
-
+  // 激活待激活物联卡
+  changeDeploy = (cardId: string) => this.GET(`/${this.uri}/${cardId}/_activation`);
+  // 停用已激活物联卡
   unDeploy = (cardId: string) => this.GET(`${this.uri}/${cardId}/_deactivate`);
-
+  // 复机已停机物联卡
   resumption = (cardId: string) => this.GET(`${this.uri}/${cardId}/_resumption`);
 
+  // 激活待激活物联卡
+  changeDeployBatch = (data: any) => this.GET(`/${this.uri}/_activation/_bitch`, data);
+  // 停用已激活物联卡
+  unDeployBatch = (data: any) => this.GET(`${this.uri}/_deactivate/_bitch`, data);
+  // 复机已停机物联卡
+  resumptionBatch = (data: any) => this.GET(`${this.uri}/_resumption/_bitch`, data);
+
   sync = () => this.GET(`${this.uri}/state/_sync`);
 
   listOnelinkNoPaging = (data: any) => this.POST(`${basePath}/onelink/_query/no-paging`, data);
@@ -36,7 +45,7 @@ class Service extends BaseService<CardManagement> {
 
   // 根据id批量导出
   _export = (format: string, params: any) =>
-    this.POST(`${this.uri}/download.${format}/_query`, {}, params, { responseType: 'blob' });
+    this.POST(`${this.uri}/download.${format}/_query`, params, {}, { responseType: 'blob' });
 
   // 批量删除物联卡
   removeCards = (data: any) => this.POST(`${this.uri}/batch/_delete`, data);
@@ -55,6 +64,12 @@ class Service extends BaseService<CardManagement> {
   queryUnbounded = (data: any) => this.POST(`${this.uri}/unbounded/device/_query`, data);
 
   queryCardNoPage = () => this.GET(`${this.uri}/_query/no-paging?paging=false`);
+
+  // 查询特定天数流量数据
+  queryFlow = (beginTime: number, endTime: number, data: any) =>
+    this.POST(`${basePath}/network/flow/_query/${beginTime}/${endTime}`, data);
+  // 查询对应状态物联卡数量
+  queryState = (status: string) => this.GET(`${this.uri}/${status}/state/_count`);
 }
 
 export default Service;

+ 7 - 5
src/pages/iot-card/CardManagement/typing.d.ts

@@ -3,13 +3,15 @@ export type CardManagement = {
   name: string;
   iccId: string;
   deviceId: string;
+  deviceName: string;
   platformConfigId: string;
   operatorName: string;
-  cardType: string;
-  totalFlow: string;
-  usedFlow: string;
-  residualFlow: string;
+  cardType: any;
+  totalFlow: number;
+  usedFlow: number;
+  residualFlow: number;
   activationDate: string;
   updateTime: string;
-  cardStateType: string;
+  cardStateType: any;
+  describe: string;
 };

+ 39 - 0
src/pages/iot-card/Dashboard/index.less

@@ -0,0 +1,39 @@
+.rankingList {
+  padding: 0;
+  list-style: none;
+}
+
+.topName {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 16px;
+  font-weight: bold;
+  font-size: 16px;
+}
+
+.rankItem {
+  display: flex;
+  justify-content: space-between;
+  padding: 16px 0;
+}
+
+.number {
+  padding: 4px;
+  color: #fff;
+  background-color: #d1d1d1;
+
+  :nth-child(1) {
+    color: #e50012;
+    background-color: rgba(#e50012, 0.5);
+  }
+
+  :nth-child(2) {
+    color: #fba500;
+    background-color: rgba(#fba500, 0.5);
+  }
+
+  :nth-child(3) {
+    color: #597ef7;
+    background-color: rgba(#597ef7, 0.5);
+  }
+}

+ 361 - 1
src/pages/iot-card/Dashboard/index.tsx

@@ -1,4 +1,364 @@
+import DashBoard, { DashBoardTopCard } from '@/components/DashBoard';
+import Echarts from '@/components/DashBoard/echarts';
+import { Card, Col, DatePicker, Row, Progress, Empty } from 'antd';
+import moment from 'moment';
+import styles from './index.less';
+import { PageContainer } from '@ant-design/pro-layout';
+import { useEffect, useRef, useState } from 'react';
+import { service } from '@/pages/iot-card/CardManagement';
+import type { EChartsOption } from 'echarts';
+
+const DefaultEchartsOptions: any = {
+  yAxis: {
+    type: 'value',
+    show: false,
+  },
+  grid: {
+    top: '5%',
+    left: '2%',
+    bottom: 20,
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow',
+    },
+  },
+};
+
 const Dashboard = () => {
-  return <>仪表盘</>;
+  const [options, setOptions] = useState<EChartsOption>({});
+  const [dayOptions, setDayOptions] = useState<EChartsOption>({});
+  const [monthOptions, setMonthOptions] = useState<EChartsOption>({});
+  const [yearOptions, setYearOptions] = useState<EChartsOption>({});
+  const [dayTotal, setDayTotal] = useState(0);
+  const [monthTotal, setMonthTotal] = useState(0);
+  const [yearTotal, setYearTotal] = useState(0);
+  const [topList, setTopList] = useState([]);
+  const [topTotal, setTotal] = useState(0);
+  const echartsRef = useRef<any>();
+
+  const getData = (start: number, end: number): Promise<{ xValue: any[]; data: any[] }> => {
+    return new Promise((resolve) => {
+      service
+        .queryFlow(start, end, {
+          orderBy: 'date',
+        })
+        .then((resp) => {
+          if (resp.status === 200) {
+            const sortArray = resp.result.sort(
+              (a: any, b: any) => new Date(a.date).getTime() - new Date(b.date).getTime(),
+            );
+            resolve({
+              xValue: sortArray.map((item: any) => item.date),
+              data: sortArray.map((item: any) => item.value),
+            });
+          }
+        });
+    });
+  };
+
+  const getDataTotal = () => {
+    const dTime = [
+      moment().subtract(1, 'days').startOf('day').valueOf(),
+      moment().subtract(1, 'days').endOf('day').valueOf(),
+    ];
+    const mTime = [moment().startOf('month').valueOf(), moment().endOf('month').valueOf()];
+    const yTime = [moment().startOf('year').valueOf(), moment().endOf('year').valueOf()];
+    const grid: any = {
+      top: '2%',
+      left: '0',
+      right: 0,
+      bottom: 0,
+    };
+    getData(dTime[0], dTime[1]).then((resp) => {
+      setDayTotal(resp.data.reduce((r, n) => r + n, 0));
+      setDayOptions({
+        ...DefaultEchartsOptions,
+        grid,
+        xAxis: {
+          type: 'category',
+          data: resp.xValue,
+          show: false,
+        },
+        series: [
+          {
+            name: '流量',
+            data: resp.data,
+            type: 'line',
+            color: '#FBA500',
+            areaStyle: {
+              color: {
+                type: 'linear',
+                x: 0,
+                y: 0,
+                x2: 0,
+                y2: 1,
+                colorStops: [
+                  {
+                    offset: 0,
+                    color: '#FBA500', // 100% 处的颜色
+                  },
+                  {
+                    offset: 1,
+                    color: '#FFFFFF', //   0% 处的颜色
+                  },
+                ],
+                global: false, // 缺省为 false
+              },
+            },
+          },
+        ],
+      });
+    });
+    getData(mTime[0], mTime[1]).then((resp) => {
+      setMonthTotal(resp.data.reduce((r, n) => r + n, 0));
+      setMonthOptions({
+        ...DefaultEchartsOptions,
+        grid,
+        xAxis: {
+          type: 'category',
+          data: resp.xValue,
+          show: false,
+        },
+        series: [
+          {
+            name: '流量',
+            data: resp.data,
+            type: 'line',
+            color: '#498BEF',
+            areaStyle: {
+              color: {
+                type: 'linear',
+                x: 0,
+                y: 0,
+                x2: 0,
+                y2: 1,
+                colorStops: [
+                  {
+                    offset: 0,
+                    color: '#498BEF', // 100% 处的颜色
+                  },
+                  {
+                    offset: 1,
+                    color: '#FFFFFF', //   0% 处的颜色
+                  },
+                ],
+                global: false, // 缺省为 false
+              },
+            },
+          },
+        ],
+      });
+    });
+    getData(yTime[0], yTime[1]).then((resp) => {
+      setYearTotal(resp.data.reduce((r, n) => r + n, 0));
+      setYearOptions({
+        ...DefaultEchartsOptions,
+        grid,
+        xAxis: {
+          type: 'category',
+          data: resp.xValue,
+          show: false,
+        },
+        series: [
+          {
+            name: '流量',
+            data: resp.data,
+            type: 'line',
+            color: '#58E1D3',
+            areaStyle: {
+              color: {
+                type: 'linear',
+                x: 0,
+                y: 0,
+                x2: 0,
+                y2: 1,
+                colorStops: [
+                  {
+                    offset: 0,
+                    color: '#58E1D3', // 100% 处的颜色
+                  },
+                  {
+                    offset: 1,
+                    color: '#FFFFFF', //   0% 处的颜色
+                  },
+                ],
+                global: false, // 缺省为 false
+              },
+            },
+          },
+        ],
+      });
+    });
+  };
+
+  const getEcharts = (data: any) => {
+    getData(data.time.start, data.time.end).then((resp) => {
+      setOptions({
+        ...DefaultEchartsOptions,
+        xAxis: {
+          type: 'category',
+          data: resp.xValue,
+        },
+        series: [
+          {
+            name: '流量统计',
+            data: resp.data,
+            type: 'line',
+            color: '#498BEF',
+            areaStyle: {
+              color: {
+                type: 'linear',
+                x: 0,
+                y: 0,
+                x2: 0,
+                y2: 1,
+                colorStops: [
+                  {
+                    offset: 0,
+                    color: '#498BEF', // 100% 处的颜色
+                  },
+                  {
+                    offset: 1,
+                    color: '#FFFFFF', //   0% 处的颜色
+                  },
+                ],
+                global: false, // 缺省为 false
+              },
+            },
+          },
+        ],
+      });
+    });
+  };
+
+  const getTopRang = (star: number, end: number) => {
+    service
+      .queryFlow(star, end, {
+        orderBy: 'usage',
+      })
+      .then((resp) => {
+        if (resp.status === 200) {
+          const arr = resp.result.slice(0, 10).sort((a: any, b: any) => b.value - a.value);
+          setTotal(arr.length ? arr[0].value : 0);
+          setTopList(arr);
+        }
+      });
+  };
+
+  useEffect(() => {
+    getDataTotal();
+
+    getEcharts(echartsRef?.current?.getValues());
+
+    const dTime = [
+      moment().subtract(1, 'days').startOf('day').valueOf(),
+      moment().subtract(6, 'days').endOf('day').valueOf(),
+    ];
+    getTopRang(dTime[0], dTime[1]);
+  }, []);
+
+  return (
+    <PageContainer>
+      <div className={'iot-card-dash-board'}>
+        <DashBoardTopCard>
+          <DashBoardTopCard.Item
+            title="今日流量消耗"
+            value={
+              <>
+                <span>{dayTotal.toFixed(2)}</span>
+                <span>M</span>
+              </>
+            }
+            footer={false}
+            span={8}
+          >
+            <Echarts options={dayOptions} />
+          </DashBoardTopCard.Item>
+          <DashBoardTopCard.Item
+            title="当月流量消耗"
+            value={
+              <>
+                <span>{monthTotal.toFixed(2)}</span>
+                <span>M</span>
+              </>
+            }
+            footer={false}
+            span={8}
+          >
+            <Echarts options={monthOptions} />
+          </DashBoardTopCard.Item>
+          <DashBoardTopCard.Item
+            title="本年流量消耗"
+            value={
+              <>
+                <span>{yearTotal.toFixed(2)}</span>
+                <span>M</span>
+              </>
+            }
+            footer={false}
+            span={8}
+          >
+            <Echarts options={yearOptions} />
+          </DashBoardTopCard.Item>
+        </DashBoardTopCard>
+      </div>
+      <Row gutter={24}>
+        <Col flex={'auto'}>
+          <Card>
+            <DashBoard
+              title="流量统计"
+              height={560}
+              showTimeTool={true}
+              ref={echartsRef}
+              options={options}
+              onParamsChange={getEcharts}
+              defaultTime={'week'}
+            />
+          </Card>
+        </Col>
+        <Col flex={'480px'}>
+          <Card>
+            <div className={styles.topName} style={{ height: 50 }}>
+              <span>流量使用TOP10</span>
+              <div>
+                {
+                  // @ts-ignore
+                  <DatePicker.RangePicker
+                    defaultPickerValue={[
+                      moment().subtract(1, 'days').startOf('day'),
+                      moment().subtract(1, 'days').endOf('day'),
+                    ]}
+                    onChange={(dates) => {
+                      getTopRang(dates?.[0].valueOf(), dates?.[1].valueOf());
+                    }}
+                  />
+                }
+              </div>
+            </div>
+            <div className={styles.rankingList} style={{ height: 490 }}>
+              {topList.length ? (
+                topList.map((item: any, index) => {
+                  return (
+                    <div className={styles.rankItem} key={item.cardNum}>
+                      <div className={styles.number}>{index + 1}</div>
+                      <div>{item.cardNum}</div>
+                      <div>
+                        <Progress strokeLinecap="butt" percent={(item.value / topTotal) * 100} />
+                      </div>
+                      <div className={styles.total}>{item?.value?.toFixed(2)} M</div>
+                    </div>
+                  );
+                })
+              ) : (
+                <Empty />
+              )}
+            </div>
+          </Card>
+        </Col>
+      </Row>
+    </PageContainer>
+  );
 };
 export default Dashboard;

+ 11 - 0
src/utils/util.ts

@@ -33,6 +33,17 @@ export const downloadFile = (url: string, params?: Record<string, any>) => {
   formElement.submit();
   document.body.removeChild(formElement);
 };
+
+export const downloadFileByUrl = (url: string, name: string, type: string) => {
+  const downNode = document.createElement('a');
+  downNode.style.display = 'none';
+  downNode.download = `${name}.${type}`;
+  downNode.href = url;
+  document.body.appendChild(downNode);
+  downNode.click();
+  document.body.removeChild(downNode);
+};
+
 /**
  * 把数据下载成JSON
  * @param record