lind 3 лет назад
Родитель
Сommit
bc96c6ecf5

BIN
public/images/media/dashboard-1.png


BIN
public/images/media/dashboard-2.png


BIN
public/images/media/dashboard-3.png


BIN
public/images/media/dashboard-4.png


+ 0 - 1
src/components/DashBoard/echarts.tsx

@@ -73,7 +73,6 @@ export default (props: EchartsProps) => {
   };
 
   const updateOptions = useCallback(() => {
-    console.log(chartsRef.current, props.options);
     if (chartsRef.current && props.options) {
       chartsRef.current.setOption(props.options);
     }

+ 76 - 0
src/components/DashBoard/index.less

@@ -37,3 +37,79 @@
   flex: 1;
   height: 100%;
 }
+
+.dash-board-top {
+  margin-bottom: 24px;
+
+  .dash-board-top-item {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    padding: 24px;
+    background-color: #fff;
+    border: 1px solid #e0e4e8;
+    border-radius: 2px;
+
+    .top-item-content {
+      display: flex;
+      flex-direction: column;
+      flex-grow: 1;
+
+      .content-left {
+        width: 50%;
+
+        .content-left-title {
+          color: rgba(0, 0, 0, 0.64);
+        }
+
+        .content-left-value {
+          padding: 12px 0;
+          color: #323130;
+          font-weight: bold;
+          font-size: 36px;
+        }
+      }
+
+      .content-right {
+        display: flex;
+        flex-grow: 1;
+        align-items: flex-end;
+        justify-content: flex-end;
+        width: 100%;
+        height: 0;
+
+        > * {
+          max-width: 100%;
+          max-height: 100%;
+        }
+      }
+
+      &.show-value {
+        flex-direction: row;
+
+        .content-left {
+          height: 100%;
+        }
+
+        .content-right {
+          width: 0;
+          height: 100%;
+        }
+      }
+    }
+  }
+
+  .top-item-footer {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding-top: 16px;
+    border-top: 1px solid #f0f0f0;
+
+    .footer-item-value {
+      color: #323130;
+      font-weight: bold;
+      font-size: 16px;
+    }
+  }
+}

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

@@ -2,5 +2,6 @@ import BaseCard from './baseCard';
 
 export { default as DashBoardEcharts } from './echarts';
 export { default as DashBoardHeader } from './header';
+export { default as DashBoardTopCard } from './topCard';
 
 export default BaseCard;

+ 4 - 4
src/components/DashBoard/timePicker.tsx

@@ -23,11 +23,11 @@ interface ExtraTimePickerProps extends Omit<DatePickerProps, 'onChange' | 'value
 export const getTimeByType = (type: TimeType) => {
   switch (type) {
     case TimeKey.week:
-      return moment().subtract(6, 'days').valueOf();
+      return moment().subtract(6, 'days').startOf('day').valueOf();
     case TimeKey.month:
-      return moment().subtract(29, 'days').valueOf();
+      return moment().subtract(29, 'days').startOf('day').valueOf();
     case TimeKey.year:
-      return moment().subtract(365, 'days').valueOf();
+      return moment().subtract(365, 'days').startOf('day').valueOf();
     default:
       return moment().startOf('day').valueOf();
   }
@@ -49,7 +49,7 @@ export default (props: ExtraTimePickerProps) => {
   };
 
   const timeChange = (type: TimeType) => {
-    const endTime = moment(new Date()).valueOf();
+    const endTime = moment(new Date()).endOf('day').valueOf();
     const startTime: number = getTimeByType(type);
     setRadioValue(type);
     change(startTime, endTime, type);

+ 72 - 2
src/components/DashBoard/topCard.tsx

@@ -1,3 +1,73 @@
-export default () => {
-  return <div></div>;
+import React from 'react';
+import { Badge, Col, Row } from 'antd';
+import classNames from 'classnames';
+import './index.less';
+
+interface TopCardProps {
+  children: React.ReactNode;
+  className?: string;
+  style?: React.CSSProperties;
+}
+
+interface FooterItem {
+  status?: 'error' | 'success' | 'warning';
+  title: string;
+  value: string | number;
+}
+
+interface CardItemProps {
+  span: number;
+  title: string;
+  value: any;
+  footer: false | FooterItem[];
+  showValue?: boolean;
+  children: React.ReactNode;
+}
+
+const CardItem = (props: CardItemProps) => {
+  return (
+    <Col span={props.span}>
+      <div className={'dash-board-top-item'}>
+        <div
+          className={classNames('top-item-content', { 'show-value': props.showValue !== false })}
+        >
+          <div className={'content-left'}>
+            <div className={'content-left-title'}>{props.title}</div>
+            {props.showValue !== false && <div className={'content-left-value'}>{props.value}</div>}
+          </div>
+          <div className={'content-right'}>{props.children}</div>
+        </div>
+        {props.footer !== false && props.footer.length ? (
+          <div className={'top-item-footer'}>
+            {props.footer.map((item) => {
+              return (
+                <>
+                  <div>
+                    {item.status ? (
+                      <Badge status={item.status} text={item.title} />
+                    ) : (
+                      <span>{item.title}</span>
+                    )}
+                  </div>
+                  <div className={'footer-item-value'}>{item.value}</div>
+                </>
+              );
+            })}
+          </div>
+        ) : null}
+      </div>
+    </Col>
+  );
 };
+
+const TopCard = (props: TopCardProps) => {
+  return (
+    <div className={classNames('dash-board-top', props.className)} style={props.style}>
+      <Row gutter={24}>{props.children}</Row>
+    </div>
+  );
+};
+
+TopCard.Item = CardItem;
+
+export default TopCard;

+ 168 - 90
src/pages/media/DashBoard/index.tsx

@@ -1,46 +1,57 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import { Badge, Card, Select } from 'antd';
-import DashBoard from '@/components/DashBoard';
+import DashBoard, { DashBoardTopCard } from '@/components/DashBoard';
 import { useRequest } from 'umi';
-import React, { useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
 import Service from './service';
 import './index.less';
 import encodeQuery from '@/utils/encodeQuery';
 import type { EChartsOption } from 'echarts';
+import moment from 'moment';
 
-interface TopCardProps {
-  url: string;
-  title: string;
-  total: number | string;
-  bottomRender: () => React.ReactNode;
-}
-
-const service = new Service('media/device');
-
-const TopCard = (props: TopCardProps) => {
-  return (
-    <div className={'top-card-item'}>
-      <div className={'top-card-top'}>
-        <div className={'top-card-top-left'}></div>
-        <div className={'top-card-top-right'}>
-          <div className={'top-card-title'}>{props.title}</div>
-          <div className={'top-card-total'}>{props.total}</div>
-        </div>
-      </div>
-      <div className={'top-card-bottom'}>{props.bottomRender()}</div>
-    </div>
-  );
-};
+const service = new Service('media');
 
 export default () => {
   const [deviceOnline, setDeviceOnline] = useState(0);
   const [deviceOffline, setDeviceOffline] = useState(0);
+  const [channelOnline, setChannelOnline] = useState(0);
+  const [channelOffline, setChannelOffline] = useState(0);
   const [options, setOptions] = useState<EChartsOption>({});
 
   const { data: deviceTotal } = useRequest(service.deviceCount, {
     formatResult: (res) => res.result,
   });
 
+  const { data: playObject } = useRequest(service.playingAgg, {
+    formatResult: (res) => res.result,
+  });
+
+  const { data: fileObject } = useRequest(service.fileAgg, {
+    formatResult: (res) => res.result,
+  });
+
+  const { data: channelTotal } = useRequest<any, any>(service.channelCount, {
+    formatResult: (res) => res.result,
+    defaultParams: {},
+  });
+
+  /**
+   * 通道数量
+   */
+  const channelStatus = async () => {
+    const onlineRes = await service.channelCount({
+      terms: [{ column: 'status', value: 'online' }],
+    });
+    if (onlineRes.status === 200) {
+      setChannelOnline(onlineRes.result);
+    }
+    const offlineRes = await service.channelCount({
+      terms: [{ column: 'status$not', value: 'online' }],
+    });
+    if (offlineRes.status === 200) {
+      setChannelOffline(offlineRes.result);
+    }
+  };
+
   /**
    * 设备数量
    */
@@ -56,90 +67,157 @@ export default () => {
   };
 
   const getEcharts = async (params: any) => {
-    // 请求数据
-    console.log(params);
-
-    setOptions({
-      xAxis: {
-        type: 'category',
-        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
-      },
-      yAxis: {
-        type: 'value',
+    const resp = await service.getMulti([
+      {
+        dashboard: 'media_stream',
+        object: 'play_count',
+        measurement: 'quantity',
+        dimension: 'agg',
+        group: 'playCount',
+        params: {
+          time: params.time.type === 'today' ? '1h' : '1d',
+          from: moment(params.time.start).format('YYYY-MM-DD HH:mm:ss'),
+          to: moment(params.time.end).format('YYYY-MM-DD HH:mm:ss'),
+          limit: 30,
+        },
       },
-      series: [
-        {
-          data: [150, 230, 224, 218, 135, 147, 260],
-          type: 'line',
+    ]);
+
+    if (resp.status === 200) {
+      const xData: string[] = [];
+      const sData: number[] = [];
+      resp.result.forEach((item: any) => {
+        xData.push(item.data.timeString);
+        sData.push(item.data.value);
+      });
+
+      setOptions({
+        xAxis: {
+          type: 'category',
+          data: xData,
         },
-      ],
-    });
+        yAxis: {
+          type: 'value',
+        },
+        grid: {
+          left: '2%',
+          right: '2%',
+          top: '2%',
+          bottom: '2%',
+        },
+        series: [
+          {
+            data: sData,
+            type: 'bar',
+          },
+        ],
+      });
+    }
+  };
+
+  const handleTimeFormat = (time: number) => {
+    let hour = 0;
+    let min = 0;
+    let sec = 0;
+    const timeStr = 'hh小时mm分钟ss秒';
+
+    if (time) {
+      if (time >= 6000) {
+        hour = Math.trunc(time / (60 * 60));
+      }
+
+      if (time >= 60) {
+        min = Math.trunc((time - hour * 60 * 60) / 60);
+      }
+
+      sec = time - hour * (60 * 60) - min * 60;
+    }
+
+    return timeStr
+      .replace('hh', hour.toString())
+      .replace('mm', min.toString())
+      .replace('ss', sec.toString());
   };
 
   useEffect(() => {
     deviceStatus();
+    channelStatus();
   }, []);
 
   return (
     <PageContainer>
       <div className={'media-dash-board'}>
-        <Card className={'top-card-items'} bodyStyle={{ display: 'flex', gap: 12 }}>
-          <TopCard
+        <DashBoardTopCard>
+          <DashBoardTopCard.Item
             title={'设备数量'}
-            total={deviceTotal}
-            url={''}
-            bottomRender={() => (
-              <>
-                <Badge status="error" text="在线" />{' '}
-                <span style={{ padding: '0 4px' }}>{deviceOnline}</span>
-                <Badge status="success" text="离线" />{' '}
-                <span style={{ padding: '0 4px' }}>{deviceOffline}</span>
-              </>
-            )}
-          />
-          <TopCard
+            value={deviceTotal}
+            footer={[
+              {
+                title: '在线',
+                value: deviceOnline,
+                status: 'success',
+              },
+              {
+                title: '离线',
+                value: deviceOffline,
+                status: 'error',
+              },
+            ]}
+            span={6}
+          >
+            <img src={require('/public/images/media/dashboard-1.png')} />
+          </DashBoardTopCard.Item>
+          <DashBoardTopCard.Item
             title={'通道数量'}
-            total={12}
-            url={''}
-            bottomRender={() => (
-              <>
-                <Badge status="error" text="在线" /> <span style={{ padding: '0 4px' }}>12</span>
-                <Badge status="success" text="离线" /> <span style={{ padding: '0 4px' }}>12</span>
-              </>
-            )}
-          />
-          <TopCard
+            value={channelTotal}
+            footer={[
+              {
+                title: '已连接',
+                value: channelOnline,
+                status: 'success',
+              },
+              {
+                title: '未连接',
+                value: channelOffline,
+                status: 'error',
+              },
+            ]}
+            span={6}
+          >
+            <img src={require('/public/images/media/dashboard-2.png')} />
+          </DashBoardTopCard.Item>
+          <DashBoardTopCard.Item
             title={'录像数量'}
-            total={12}
-            url={''}
-            bottomRender={() => <div>总时长: </div>}
-          />
-          <TopCard
+            value={fileObject ? fileObject.total : 0}
+            footer={[
+              {
+                title: '总时长',
+                value: handleTimeFormat(fileObject ? fileObject.duration : 0),
+              },
+            ]}
+            span={6}
+          >
+            <img src={require('/public/images/media/dashboard-3.png')} />
+          </DashBoardTopCard.Item>
+          <DashBoardTopCard.Item
             title={'播放中数量'}
-            total={12}
-            url={''}
-            bottomRender={() => <div>播放人数: </div>}
-          />
-        </Card>
+            value={playObject ? playObject.playerTotal : 0}
+            footer={[
+              {
+                title: '播放人数',
+                value: playObject ? playObject.playingTotal : 0,
+              },
+            ]}
+            span={6}
+          >
+            <img src={require('/public/images/media/dashboard-4.png')} />
+          </DashBoardTopCard.Item>
+        </DashBoardTopCard>
         <DashBoard
           className={'media-dash-board-body'}
           title={'播放数量(人次)'}
           options={options}
           height={500}
-          initialValues={{
-            test: '2',
-          }}
-          extraParams={{
-            key: 'test',
-            Children: (
-              <Select
-                options={[
-                  { label: '1', value: '1' },
-                  { label: '2', value: '2' },
-                ]}
-              ></Select>
-            ),
-          }}
           onParamsChange={getEcharts}
         />
       </div>

+ 11 - 3
src/pages/media/DashBoard/service.ts

@@ -1,12 +1,20 @@
 import BaseService from '@/utils/BaseService';
 import { request } from 'umi';
 import type { DeviceItem } from '@/pages/media/Device/typings';
+import SystemConst from '@/utils/const';
 
 class Service extends BaseService<DeviceItem> {
-  deviceCount = (data?: any) => request(`${this.uri}/_count`, { methods: 'GET', params: data });
+  deviceCount = (data?: any) =>
+    request(`${this.uri}/device/_count`, { methods: 'GET', params: data });
 
-  channelCount = (data?: any) =>
-    request(`${this.uri}/channel/_count`, { methods: 'POST', params: data });
+  channelCount = (data?: any) => request(`${this.uri}/channel/_count`, { method: 'POST', data });
+
+  playingAgg = () => request(`${this.uri}/channel/playing/agg`, { method: 'GET' });
+
+  fileAgg = () => request(`${this.uri}/record/file/agg`, { method: 'GET' });
+
+  getMulti = (data: any) =>
+    request(`/${SystemConst.API_BASE}/dashboard/_multi`, { method: 'POST', data });
 }
 
 export default Service;

+ 10 - 1
src/pages/media/Device/index.tsx

@@ -1,6 +1,6 @@
 // 视频设备列表
 import { PageContainer } from '@ant-design/pro-layout';
-import { useRef, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import { Button, message, Tooltip } from 'antd';
 import {
@@ -14,6 +14,7 @@ import {
 import type { DeviceItem } from '@/pages/media/Device/typings';
 import { useHistory, useIntl } from 'umi';
 import { BadgeStatus, PermissionButton, ProTableCard } from '@/components';
+import useLocation from '@/hooks/route/useLocation';
 import { StatusColorEnum } from '@/components/BadgeStatus';
 import SearchComponent from '@/components/SearchComponent';
 import MediaDevice from '@/components/ProTableCard/CardItems/mediaDevice';
@@ -41,6 +42,14 @@ const Device = () => {
   const [queryParam, setQueryParam] = useState({});
   const history = useHistory<Record<string, string>>();
   const { permission } = PermissionButton.usePermission('media/Device');
+  const location = useLocation();
+
+  useEffect(() => {
+    const { state } = location;
+    if (state && state.save) {
+      setVisible(true);
+    }
+  }, [location]);
 
   /**
    * table 查询参数

+ 154 - 0
src/pages/media/Home/deviceModal.tsx

@@ -0,0 +1,154 @@
+import { message, Modal } from 'antd';
+import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { service } from './index';
+import SearchComponent from '@/components/SearchComponent';
+import { DeviceItem } from '@/pages/media/Home/typings';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { BadgeStatus } from '@/components';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import useHistory from '@/hooks/route/useHistory';
+
+interface DeviceModalProps {
+  visible: boolean;
+  url?: string;
+  onCancel: () => void;
+}
+
+export default (props: DeviceModalProps) => {
+  const intl = useIntl();
+  const history = useHistory();
+
+  const actionRef = useRef<ActionType>();
+  const [searchParam, setSearchParam] = useState({});
+  const [deviceItem, setDeviceItem] = useState<any>({});
+
+  useEffect(() => {
+    if (!props.visible) {
+      setDeviceItem({});
+      setSearchParam({});
+    }
+  }, [props.visible]);
+
+  const cancel = () => {
+    if (props.onCancel) {
+      props.onCancel();
+    }
+  };
+
+  const jumpChannel = useCallback(() => {
+    if (deviceItem && deviceItem.id) {
+      history.push(`${props.url}?id=${deviceItem.id}&type=${deviceItem.provider}`);
+    } else {
+      message.warning('请选择设备');
+    }
+  }, [props.url, deviceItem]);
+
+  const columns: ProColumns<DeviceItem>[] = [
+    {
+      dataIndex: 'id',
+      title: 'ID',
+      width: 220,
+    },
+    {
+      dataIndex: 'name',
+      title: intl.formatMessage({
+        id: 'pages.table.name',
+        defaultMessage: '名称',
+      }),
+    },
+    {
+      dataIndex: 'channelNumber',
+      title: intl.formatMessage({
+        id: 'pages.media.device.channelNumber',
+        defaultMessage: '通道数量',
+      }),
+      valueType: 'digit',
+      hideInSearch: true,
+    },
+    {
+      dataIndex: 'state',
+      title: intl.formatMessage({
+        id: 'pages.searchTable.titleStatus',
+        defaultMessage: '状态',
+      }),
+      render: (_, record) => (
+        <BadgeStatus
+          status={record.state.value}
+          statusNames={{
+            online: StatusColorEnum.success,
+            offline: StatusColorEnum.error,
+            notActive: StatusColorEnum.processing,
+          }}
+          text={record.state.text}
+        />
+      ),
+      valueType: 'select',
+      valueEnum: {
+        offline: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.offLine',
+            defaultMessage: '离线',
+          }),
+          status: 'offline',
+        },
+        online: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.onLine',
+            defaultMessage: '在线',
+          }),
+          status: 'online',
+        },
+      },
+      filterMultiple: false,
+    },
+  ];
+
+  return (
+    <Modal
+      title={'选择设备'}
+      onCancel={cancel}
+      onOk={jumpChannel}
+      destroyOnClose={true}
+      maskClosable={false}
+      visible={props.visible}
+      width={800}
+    >
+      <SearchComponent<DeviceItem>
+        field={columns}
+        enableSave={false}
+        onSearch={async (data) => {
+          setSearchParam(data);
+        }}
+        target="media-home-device"
+      />
+      <ProTable<DeviceItem>
+        actionRef={actionRef}
+        columns={columns}
+        rowKey={'id'}
+        search={false}
+        request={(params) =>
+          service.query({
+            ...params,
+            sorts: [
+              {
+                name: 'createTime',
+                order: 'desc',
+              },
+            ],
+          })
+        }
+        rowSelection={{
+          type: 'radio',
+          selectedRowKeys: deviceItem.id ? [deviceItem.id] : undefined,
+          onSelect: (record) => {
+            console.log(record);
+            setDeviceItem(record);
+          },
+        }}
+        tableAlertOptionRender={() => false}
+        params={searchParam}
+      />
+    </Modal>
+  );
+};

+ 38 - 0
src/pages/media/Home/index.less

@@ -0,0 +1,38 @@
+.media-home {
+  .media-home-top {
+    & .media-guide,
+    & .media-statistics {
+      display: flex;
+      justify-content: space-between;
+      height: 150px;
+    }
+
+    .media-guide {
+      > div {
+        padding: 24px;
+        border: 1px solid rgba(#000, 0.4);
+      }
+    }
+
+    .media-statistics {
+      > div {
+        display: flex;
+        flex-grow: 1;
+      }
+    }
+  }
+
+  .media-home-content {
+    height: 350px;
+  }
+
+  .media-home-steps {
+    display: flex;
+    height: 150px;
+
+    > div {
+      padding: 24px;
+      border: 1px solid rgba(#000, 0.4);
+    }
+  }
+}

+ 150 - 0
src/pages/media/Home/index.tsx

@@ -0,0 +1,150 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { Card, Col, message, Row, Tooltip, Typography } from 'antd';
+import { PermissionButton } from '@/components';
+import { getMenuPathByCode } from '@/utils/menu';
+import useHistory from '@/hooks/route/useHistory';
+import { QuestionCircleOutlined } from '@ant-design/icons';
+import { useRequest } from 'umi';
+import Service from './service';
+import { useState } from 'react';
+import DeviceModal from './deviceModal';
+import './index.less';
+
+const permissionTip = '暂无权限,请联系管理员';
+
+export const service = new Service('media/device');
+
+export default () => {
+  const dashBoardUrl = getMenuPathByCode('media/DashBoard');
+  const deviceUrl = getMenuPathByCode('media/Device');
+  const channelUrl = getMenuPathByCode('media/Device/Channel');
+  const splitScreenUrl = getMenuPathByCode('media/SplitScreen');
+  const cascadeUrl = getMenuPathByCode('media/Cascade');
+
+  const [visible, setVisible] = useState(false);
+
+  const { permission: devicePermission } = PermissionButton.usePermission('media/Device');
+
+  const history = useHistory();
+
+  const { data: deviceTotal } = useRequest(service.deviceCount, {
+    formatResult: (res) => res.result,
+  });
+
+  const { data: channelTotal } = useRequest<any, any>(service.channelCount, {
+    formatResult: (res) => res.result,
+    defaultParams: {},
+  });
+
+  const addDevice = () => {
+    if (deviceUrl && devicePermission.add) {
+      history.push(deviceUrl, {
+        save: true,
+      });
+    } else {
+      message.warning(permissionTip);
+    }
+  };
+
+  const jumpSplitScreen = () => {
+    if (splitScreenUrl) {
+      history.push(splitScreenUrl);
+    } else {
+      message.warning(permissionTip);
+    }
+  };
+
+  const jumpCascade = () => {
+    if (cascadeUrl) {
+      history.push(cascadeUrl);
+    } else {
+      message.warning(permissionTip);
+    }
+  };
+
+  const jumpChannel = () => {
+    if (channelUrl) {
+      setVisible(true);
+    } else {
+      message.warning(permissionTip);
+    }
+  };
+
+  return (
+    <PageContainer>
+      <DeviceModal
+        visible={visible}
+        url={channelUrl}
+        onCancel={() => {
+          setVisible(false);
+        }}
+      />
+      <Card className={'media-home'}>
+        <Row gutter={[12, 12]}>
+          <Col span={14}>
+            <div className={'media-home-top'}>
+              <Typography.Title level={5}>视频中心引导</Typography.Title>
+              <div className={'media-guide'}>
+                <div onClick={addDevice}>添加视频设备</div>
+                <div onClick={jumpSplitScreen}>分屏展示</div>
+                <div onClick={jumpCascade}>国标级联</div>
+              </div>
+            </div>
+          </Col>
+          <Col span={10}>
+            <div className={'media-home-top'}>
+              <Typography.Title level={5}>
+                基础统计
+                <PermissionButton
+                  isPermission={!!dashBoardUrl}
+                  onClick={() => {
+                    history.push(dashBoardUrl);
+                  }}
+                  type={'link'}
+                >
+                  详情
+                </PermissionButton>
+              </Typography.Title>
+              <div className={'media-statistics'}>
+                <div>
+                  设备数量
+                  {deviceTotal}
+                </div>
+                <div>
+                  通道数量
+                  {channelTotal}
+                </div>
+              </div>
+            </div>
+          </Col>
+          <Col span={24}>
+            <Typography.Title level={5}>平台架构图</Typography.Title>
+            <div className={'media-home-content'}></div>
+          </Col>
+          <Col span={24}>
+            <Typography.Title level={5}>
+              <span style={{ paddingRight: 12 }}>视频设备管理推荐步骤</span>
+              <Tooltip title={'请根据业务需要对下述步骤进行选择性操作'}>
+                <QuestionCircleOutlined />
+              </Tooltip>
+            </Typography.Title>
+            <div className={'media-home-steps'}>
+              <div onClick={addDevice}>
+                添加视频设备
+                <div>根据视频设备的传输协议,在已创建的产品下添加对应的设备</div>
+              </div>
+              <div onClick={jumpChannel}>
+                查看通道
+                <div>查看设备下的通道数据,可以进行直播、录制等操作</div>
+              </div>
+              <div onClick={jumpSplitScreen}>
+                分屏展示
+                <div>对多个通道的视频流数据进行分屏展示</div>
+              </div>
+            </div>
+          </Col>
+        </Row>
+      </Card>
+    </PageContainer>
+  );
+};

+ 11 - 0
src/pages/media/Home/service.tsx

@@ -0,0 +1,11 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import type { DeviceItem } from '@/pages/media/Device/typings';
+
+class Service extends BaseService<DeviceItem> {
+  deviceCount = (data?: any) => request(`${this.uri}/_count`, { methods: 'GET', params: data });
+
+  channelCount = (data?: any) => request(`${this.uri}/channel/_count`, { method: 'POST', data });
+}
+
+export default Service;

+ 17 - 0
src/pages/media/Home/typings.d.ts

@@ -0,0 +1,17 @@
+import type { BaseItem, State } from '@/utils/typings';
+
+export type DeviceItem = {
+  photoUrl?: string;
+  channelNumber: number;
+  createTime: number;
+  firmware: string;
+  gatewayId: string;
+  host: string;
+  manufacturer: string;
+  model: string;
+  port: number;
+  provider: string;
+  state: State;
+  streamMode: string;
+  transport: string;
+} & BaseItem;

+ 17 - 6
src/pages/system/Platforms/Api/base.tsx

@@ -26,7 +26,14 @@ export const ApiModel = model<{
 
 interface ApiPageProps {
   showDebugger?: boolean;
+  /**
+   * true 只展示已赋权的接口
+   */
   isShowGranted?: boolean;
+  /**
+   * false:table暂时所有接口
+   */
+  isOpenGranted?: boolean;
 }
 
 export default observer((props: ApiPageProps) => {
@@ -60,12 +67,15 @@ export default observer((props: ApiPageProps) => {
     const param = new URLSearchParams(location.search);
     const code = param.get('code');
 
-    service.getApiGranted(code!).then((resp: any) => {
-      if (resp.status === 200) {
-        setGrantKeys(resp.result);
-      }
-    });
-  }, [location]);
+    if (props.isOpenGranted === false) {
+    } else {
+      service.getApiGranted(code!).then((resp: any) => {
+        if (resp.status === 200) {
+          setGrantKeys(resp.result);
+        }
+      });
+    }
+  }, [location, props.isOpenGranted]);
 
   useEffect(() => {
     initModel();
@@ -89,6 +99,7 @@ export default observer((props: ApiPageProps) => {
         <Table
           data={ApiModel.data}
           operations={operations}
+          isOpenGranted={props.isOpenGranted}
           isShowGranted={props.isShowGranted}
           grantKeys={GrantKeys}
         />

+ 6 - 2
src/pages/system/Platforms/Api/basePage.tsx

@@ -10,6 +10,8 @@ interface TableProps {
   // 是否只暂时已授权的接口
   isShowGranted?: boolean;
   //
+  isOpenGranted?: boolean;
+  //
   grantKeys: string[];
 }
 
@@ -48,14 +50,16 @@ export default (props: TableProps) => {
   }, [props.isShowGranted, selectKeys, props.data]);
 
   useEffect(() => {
-    if (!props.isShowGranted) {
+    if (props.isOpenGranted === false) {
+      setDataSource(props.data);
+    } else if (!props.isShowGranted) {
       if (props.data && props.data.length && props.operations) {
         getOperations(props.data, props.operations);
       } else {
         setDataSource([]);
       }
     }
-  }, [props.data, props.operations, props.isShowGranted]);
+  }, [props.data, props.operations, props.isShowGranted, props.isOpenGranted]);
 
   const save = useCallback(async () => {
     const param = new URLSearchParams(location.search);

+ 11 - 0
src/pages/system/Platforms/Setting/index.tsx

@@ -0,0 +1,11 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import ApiPage from '../Api/base';
+
+export default () => {
+  return (
+    <PageContainer>
+      <div>配置系统支持API赋权的范围</div>
+      <ApiPage showDebugger={true} isOpenGranted={false} />
+    </PageContainer>
+  );
+};

+ 1 - 0
src/utils/menu/router.ts

@@ -131,6 +131,7 @@ export enum MENUS_CODE {
   'system/Platforms' = 'system/Platforms',
   'system/Platforms/Api' = 'system/Platforms/Api',
   'system/Platforms/View' = 'system/Platforms/View',
+  'system/Platforms/Setting' = 'system/Platforms/Setting',
 }
 
 export type MENUS_CODE_TYPE = keyof typeof MENUS_CODE | string;