Bladeren bron

feat(设备通道): 添加设备通道

xieyonghong 3 jaren geleden
bovenliggende
commit
9fe43afc6c

+ 1 - 1
src/components/ProTableCard/index.less

@@ -41,7 +41,7 @@
 
 
         .card-item-header {
         .card-item-header {
           display: flex;
           display: flex;
-          width: 100%;
+          width: calc(100% - 86px);
           margin-bottom: 12px;
           margin-bottom: 12px;
 
 
           .card-item-header-name {
           .card-item-header-name {

+ 11 - 0
src/pages/media/Device/Channel/index.less

@@ -0,0 +1,11 @@
+.device-channel-warp {
+  display: flex;
+
+  .left {
+    width: 300px;
+  }
+
+  .right {
+    flex: 1;
+  }
+}

+ 138 - 0
src/pages/media/Device/Channel/index.tsx

@@ -0,0 +1,138 @@
+// 视频设备通道列表
+import { PageContainer } from '@ant-design/pro-layout';
+import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
+import SearchComponent from '@/components/SearchComponent';
+import './index.less';
+import { useRef, useState } from 'react';
+import { ChannelItem } from '@/pages/media/Device/Channel/typings';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { BadgeStatus } from '@/components';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import { Button, Tooltip } from 'antd';
+import { EditOutlined, PlusOutlined } from '@ant-design/icons';
+import Service from './service';
+
+export const service = new Service('media/device');
+
+export default () => {
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+  const [queryParam, setQueryParam] = useState({});
+  const [, setVisible] = useState<boolean>(false);
+  const [, setCurrent] = useState<ChannelItem>();
+
+  /**
+   * table 查询参数
+   * @param data
+   */
+  const searchFn = (data: any) => {
+    setQueryParam(data);
+  };
+
+  const columns: ProColumns<ChannelItem>[] = [
+    {
+      dataIndex: 'id',
+      title: 'ID',
+    },
+    {
+      dataIndex: 'name',
+      title: intl.formatMessage({
+        id: 'pages.table.name',
+        defaultMessage: '名称',
+      }),
+    },
+    {
+      dataIndex: 'manufacturer',
+      title: intl.formatMessage({
+        id: 'pages.media.device.manufacturer',
+        defaultMessage: '设备厂家',
+      }),
+    },
+    {
+      dataIndex: 'state',
+      title: intl.formatMessage({
+        id: 'pages.searchTable.titleStatus',
+        defaultMessage: '状态',
+      }),
+      render: (_, record) => (
+        <BadgeStatus
+          status={record.status}
+          statusNames={{
+            online: StatusColorEnum.success,
+            offline: StatusColorEnum.error,
+            notActive: StatusColorEnum.processing,
+          }}
+          text={record.status}
+        />
+      ),
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: () => [
+        <Tooltip
+          key="edit"
+          title={intl.formatMessage({
+            id: 'pages.data.option.edit',
+            defaultMessage: '编辑',
+          })}
+        >
+          <a onClick={() => {}}>
+            <EditOutlined />
+          </a>
+        </Tooltip>,
+      ],
+    },
+  ];
+
+  return (
+    <PageContainer>
+      <div className={'device-channel-warp'}>
+        <div className={'left'}></div>
+        <div className={'right'}>
+          <SearchComponent field={columns} onSearch={searchFn} />
+          <ProTable<ChannelItem>
+            columns={columns}
+            actionRef={actionRef}
+            options={{ fullScreen: true }}
+            params={queryParam}
+            request={(params = {}) =>
+              service.query({
+                ...params,
+                sorts: [
+                  {
+                    name: 'createTime',
+                    order: 'desc',
+                  },
+                ],
+              })
+            }
+            rowKey="id"
+            search={false}
+            headerTitle={[
+              <Button
+                onClick={() => {
+                  setCurrent(undefined);
+                  setVisible(true);
+                }}
+                key="button"
+                icon={<PlusOutlined />}
+                type="primary"
+              >
+                {intl.formatMessage({
+                  id: 'pages.data.option.add',
+                  defaultMessage: '新增',
+                })}
+              </Button>,
+            ]}
+          />
+        </div>
+      </div>
+    </PageContainer>
+  );
+};

+ 11 - 0
src/pages/media/Device/Channel/service.ts

@@ -0,0 +1,11 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import type { ChannelItem } from './typings';
+
+class Service extends BaseService<ChannelItem> {
+  //
+  queryTree = (id: string, data: any) =>
+    request(`${this.uri}/${id}/catalog/_query/tree`, { method: 'PATCH', data });
+}
+
+export default Service;

+ 111 - 0
src/pages/media/Device/Channel/typings.d.ts

@@ -0,0 +1,111 @@
+export type CatalogItemType = {
+  district?: string;
+  device?: string;
+  platform?: string;
+  user?: string;
+  platform_outer?: string;
+  ext?: string;
+};
+
+export interface CatalogItem {
+  id: string;
+  channelId: string;
+  deviceId: string;
+  name: string;
+  type: CatalogItemType;
+  createTime: number;
+  modifyTime: number;
+  children?: CatalogItem[];
+}
+
+export type ChannelStatusType =
+  | 'online'
+  | 'lost'
+  | 'defect'
+  | 'add'
+  | 'delete'
+  | 'update'
+  | 'offline';
+
+export type PtzType = 'unknown' | 'ball' | 'hemisphere' | 'fixed' | 'remoteControl';
+
+export type CatalogType = keyof CatalogItemType;
+
+export type ChannelType =
+  | 'dv_no_storage'
+  | 'dv_has_storage'
+  | 'dv_decoder'
+  | 'networking_monitor_server'
+  | 'media_proxy'
+  | 'web_access_server'
+  | 'video_management_server'
+  | 'network_matrix'
+  | 'network_controller'
+  | 'network_alarm_machine'
+  | 'dvr'
+  | 'video_server'
+  | 'encoder'
+  | 'decoder'
+  | 'video_switching_matrix'
+  | 'audio_switching_matrix'
+  | 'alarm_controller'
+  | 'nvr'
+  | 'hvr'
+  | 'camera'
+  | 'ipc'
+  | 'display'
+  | 'alarm_input'
+  | 'alarm_output'
+  | 'audio_input'
+  | 'audio_output'
+  | 'mobile_trans'
+  | 'other_outer'
+  | 'center_server'
+  | 'web_server'
+  | 'media_dispatcher'
+  | 'proxy_server'
+  | 'secure_server'
+  | 'alarm_server'
+  | 'database_server'
+  | 'gis_server'
+  | 'management_server'
+  | 'gateway_server'
+  | 'media_storage_server'
+  | 'signaling_secure_gateway'
+  | 'business_group'
+  | 'virtual_group'
+  | 'center_user'
+  | 'end_user'
+  | 'media_iap'
+  | 'media_ops'
+  | 'district'
+  | 'other';
+
+export interface ChannelItem {
+  id: string;
+  deviceId: string;
+  deviceName: string;
+  channelId: string;
+  name: string;
+  manufacturer: string;
+  model: string;
+  address: string;
+  provider: string;
+  status: ChannelStatusType;
+  others: object;
+  description: string;
+  parentChannelId: string;
+  subCount: integer;
+  civilCode: string;
+  ptzType: PtzType;
+  catalogType: CatalogType;
+  channelType: ChannelType;
+  catalogCode: string;
+  longitude: number;
+  latitude: number;
+  createTime: number;
+  modifyTime: number;
+  parentId: string;
+  gb28181ProxyStream: boolean;
+  gb28181ChannelId: string;
+}

+ 46 - 2
src/pages/media/Device/Save/ProviderSelect.tsx

@@ -1,4 +1,9 @@
 import classNames from 'classnames';
 import classNames from 'classnames';
+import { Badge } from 'antd';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import styles from '@/pages/link/AccessConfig/index.less';
+import { TableCard } from '@/components';
+import './providerSelect.less';
 
 
 interface ProviderProps {
 interface ProviderProps {
   value?: string;
   value?: string;
@@ -7,6 +12,8 @@ interface ProviderProps {
   onSelect?: (id: string, rowData: any) => void;
   onSelect?: (id: string, rowData: any) => void;
 }
 }
 
 
+const defaultImage = require('/public/images/device-access.png');
+
 export default (props: ProviderProps) => {
 export default (props: ProviderProps) => {
   return (
   return (
     <div className={'provider-list'}>
     <div className={'provider-list'}>
@@ -23,9 +30,46 @@ export default (props: ProviderProps) => {
                 }
                 }
               }}
               }}
               style={{ padding: 16 }}
               style={{ padding: 16 }}
-              className={classNames({ active: item.id === props.value })}
             >
             >
-              {item.name}
+              <TableCard
+                className={classNames({ active: item.id === props.value })}
+                showMask={false}
+                showTool={false}
+                status={item.state.value}
+                statusText={item.state.text}
+                statusNames={{
+                  enabled: StatusColorEnum.processing,
+                  disabled: StatusColorEnum.error,
+                }}
+              >
+                <div className={styles.context}>
+                  <div>
+                    <img width={88} height={88} src={defaultImage} alt={''} />
+                  </div>
+                  <div className={styles.card}>
+                    <div className={styles.header}>
+                      <div className={styles.title}>{item.name || '--'}</div>
+                      <div className={styles.desc}>{item.description || '--'}</div>
+                    </div>
+                    <div className={styles.container}>
+                      <div className={styles.server}>
+                        <div className={styles.subTitle}>{item?.channelInfo?.name || '--'}</div>
+                        <div style={{ width: '100%' }}>
+                          {item.channelInfo?.addresses.map((i: any, index: number) => (
+                            <p key={i.address + `_address${index}`}>
+                              <Badge color={i.health === -1 ? 'red' : 'green'} text={i.address} />
+                            </p>
+                          ))}
+                        </div>
+                      </div>
+                      <div className={styles.procotol}>
+                        <div className={styles.subTitle}>{item?.protocolDetail?.name || '--'}</div>
+                        <p>{item.protocolDetail?.description || '--'}</p>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </TableCard>
             </div>
             </div>
           ))
           ))
         : null}
         : null}

+ 16 - 14
src/pages/media/Device/Save/index.tsx

@@ -63,10 +63,10 @@ export default (props: SaveProps) => {
   const handleSave = async () => {
   const handleSave = async () => {
     const formData = await form.validateFields();
     const formData = await form.validateFields();
     if (formData) {
     if (formData) {
-      const { type, ...extraFormData } = formData;
+      const { provider, ...extraFormData } = formData;
       setLoading(true);
       setLoading(true);
       const resp =
       const resp =
-        type === DefaultAccessType
+        provider === DefaultAccessType
           ? await service.saveGB(extraFormData)
           ? await service.saveGB(extraFormData)
           : await service.saveFixed(extraFormData);
           : await service.saveFixed(extraFormData);
       setLoading(false);
       setLoading(false);
@@ -207,20 +207,22 @@ export default (props: SaveProps) => {
                     disabled={props.model === 'edit'}
                     disabled={props.model === 'edit'}
                     options={productList}
                     options={productList}
                     placeholder={'请选择所属产品'}
                     placeholder={'请选择所属产品'}
-                    style={{ width: 'calc(100% - 36px)' }}
+                    style={{ width: props.model === 'edit' ? '100%' : 'calc(100% - 36px)' }}
                   />
                   />
                 </Form.Item>
                 </Form.Item>
-                <Form.Item noStyle>
-                  <Button
-                    type={'link'}
-                    style={{ padding: '4px 10px' }}
-                    onClick={() => {
-                      setProductVisible(true);
-                    }}
-                  >
-                    <PlusOutlined />
-                  </Button>
-                </Form.Item>
+                {props.model !== 'edit' && (
+                  <Form.Item noStyle>
+                    <Button
+                      type={'link'}
+                      style={{ padding: '4px 10px' }}
+                      onClick={() => {
+                        setProductVisible(true);
+                      }}
+                    >
+                      <PlusOutlined />
+                    </Button>
+                  </Form.Item>
+                )}
               </Form.Item>
               </Form.Item>
             </Col>
             </Col>
             {accessType === DefaultAccessType && (
             {accessType === DefaultAccessType && (

+ 8 - 0
src/pages/media/Device/Save/providerSelect.less

@@ -1,4 +1,12 @@
+@import '~antd/es/style/themes/default.less';
+
 .provider-list {
 .provider-list {
   max-height: 450px;
   max-height: 450px;
   overflow-y: auto;
   overflow-y: auto;
+
+  .active {
+    .card-warp {
+      border-color: @primary-color-active;
+    }
+  }
 }
 }

+ 152 - 122
src/pages/media/Device/index.tsx

@@ -4,14 +4,12 @@ import { useRef, useState } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import { Button, message, Popconfirm, Tooltip } from 'antd';
 import { Button, message, Popconfirm, Tooltip } from 'antd';
 import {
 import {
-  ArrowDownOutlined,
-  BugOutlined,
   DeleteOutlined,
   DeleteOutlined,
   EditOutlined,
   EditOutlined,
   EyeOutlined,
   EyeOutlined,
-  MinusOutlined,
   PlusOutlined,
   PlusOutlined,
   SyncOutlined,
   SyncOutlined,
+  PartitionOutlined,
 } from '@ant-design/icons';
 } from '@ant-design/icons';
 import type { DeviceItem } from '@/pages/media/Device/typings';
 import type { DeviceItem } from '@/pages/media/Device/typings';
 import { useIntl, useHistory } from 'umi';
 import { useIntl, useHistory } from 'umi';
@@ -25,6 +23,11 @@ import Save from './Save';
 
 
 export const service = new Service('media/device');
 export const service = new Service('media/device');
 
 
+const providerType = {
+  'gb28181-2016': 'GB/T28181',
+  'fixed-media': '固定地址',
+};
+
 const Device = () => {
 const Device = () => {
   const intl = useIntl();
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
   const actionRef = useRef<ActionType>();
@@ -33,11 +36,44 @@ const Device = () => {
   const [queryParam, setQueryParam] = useState({});
   const [queryParam, setQueryParam] = useState({});
   const history = useHistory<Record<string, string>>();
   const history = useHistory<Record<string, string>>();
 
 
+  /**
+   * table 查询参数
+   * @param data
+   */
+  const searchFn = (data: any) => {
+    setQueryParam(data);
+  };
+
+  const deleteItem = async (id: string) => {
+    const response: any = await service.remove(id);
+    if (response.status === 200) {
+      message.success(
+        intl.formatMessage({
+          id: 'pages.data.option.success',
+          defaultMessage: '操作成功!',
+        }),
+      );
+    }
+    actionRef.current?.reload();
+  };
+
+  /**
+   * 更新通道
+   * @param id 视频设备ID
+   */
+  const updateChannel = async (id: string) => {
+    const resp = await service.updateChannels(id);
+    if (resp.status === 200) {
+      message.success('通道更新成功');
+    } else {
+      message.error('通道更新失败');
+    }
+  };
+
   const columns: ProColumns<DeviceItem>[] = [
   const columns: ProColumns<DeviceItem>[] = [
     {
     {
-      dataIndex: 'index',
-      valueType: 'indexBorder',
-      width: 48,
+      dataIndex: 'id',
+      title: 'ID',
     },
     },
     {
     {
       dataIndex: 'name',
       dataIndex: 'name',
@@ -46,66 +82,41 @@ const Device = () => {
         defaultMessage: '名称',
         defaultMessage: '名称',
       }),
       }),
     },
     },
-    // {
-    //   dataIndex: 'transport',
-    //   title: intl.formatMessage({
-    //     id: 'pages.media.device.transport',
-    //     defaultMessage: '信令传输',
-    //   }),
-    // },
-    // {
-    //   dataIndex: 'streamMode',
-    //   title: intl.formatMessage({
-    //     id: 'pages.media.device.streamMode',
-    //     defaultMessage: '流传输模式',
-    //   }),
-    // },
-    // {
-    //   dataIndex: 'channelNumber',
-    //   title: intl.formatMessage({
-    //     id: 'pages.media.device.channelNumber',
-    //     defaultMessage: '通道数',
-    //   }),
-    // },
-    // {
-    //   dataIndex: 'host',
-    //   title: 'IP',
-    // },
-    // {
-    //   dataIndex: '端口',
-    //   title: intl.formatMessage({
-    //     id: 'pages.media.device.port',
-    //     defaultMessage: '端口',
-    //   }),
-    // },
-    // {
-    //   dataIndex: 'manufacturer',
-    //   title: intl.formatMessage({
-    //     id: 'pages.media.device.manufacturer',
-    //     defaultMessage: '设备厂家',
-    //   }),
-    // },
-    // {
-    //   dataIndex: 'model',
-    //   title: intl.formatMessage({
-    //     id: 'pages.media.device.model',
-    //     defaultMessage: '型号',
-    //   }),
-    // },
-    // {
-    //   dataIndex: 'firmware',
-    //   title: intl.formatMessage({
-    //     id: 'pages.media.device.firmware',
-    //     defaultMessage: '固件版本',
-    //   }),
-    // },
-    // {
-    //   dataIndex: 'networkType',
-    //   title: intl.formatMessage({
-    //     id: 'pages.table.type',
-    //     defaultMessage: '类型',
-    //   }),
-    // },
+    {
+      dataIndex: 'provider',
+      title: '接入方式',
+      render: (_, row) => {
+        return providerType[row.provider];
+      },
+    },
+    {
+      dataIndex: 'channelNumber',
+      title: intl.formatMessage({
+        id: 'pages.media.device.channelNumber',
+        defaultMessage: '通道数',
+      }),
+    },
+    {
+      dataIndex: 'manufacturer',
+      title: intl.formatMessage({
+        id: 'pages.media.device.manufacturer',
+        defaultMessage: '设备厂家',
+      }),
+    },
+    {
+      dataIndex: 'model',
+      title: intl.formatMessage({
+        id: 'pages.media.device.model',
+        defaultMessage: '型号',
+      }),
+    },
+    {
+      dataIndex: 'firmware',
+      title: intl.formatMessage({
+        id: 'pages.media.device.firmware',
+        defaultMessage: '固件版本',
+      }),
+    },
     {
     {
       dataIndex: 'state',
       dataIndex: 'state',
       title: intl.formatMessage({
       title: intl.formatMessage({
@@ -133,71 +144,74 @@ const Device = () => {
       align: 'center',
       align: 'center',
       width: 200,
       width: 200,
       render: (text, record) => [
       render: (text, record) => [
-        <a onClick={() => console.log(record)}>
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.data.option.edit',
-              defaultMessage: '编辑',
-            })}
+        <Tooltip
+          key="edit"
+          title={intl.formatMessage({
+            id: 'pages.data.option.edit',
+            defaultMessage: '编辑',
+          })}
+        >
+          <a
+            onClick={() => {
+              setCurrent(record);
+              setVisible(true);
+            }}
           >
           >
             <EditOutlined />
             <EditOutlined />
-          </Tooltip>
-        </a>,
-        <a>
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.data.option.remove',
-              defaultMessage: '删除',
-            })}
+          </a>
+        </Tooltip>,
+        <Tooltip key={'viewChannel'} title="查看通道">
+          <a
+            onClick={() => {
+              history.push(`${getMenuPathByParams(MENUS_CODE['media/Device/Channel'], record.id)}`);
+            }}
           >
           >
-            <MinusOutlined />
-          </Tooltip>
-        </a>,
-        <a>
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.data.option.download',
-              defaultMessage: '下载配置',
-            })}
+            <PartitionOutlined />
+          </a>
+        </Tooltip>,
+        <Tooltip key={'updateChannel'} title="更新通道">
+          <Button
+            style={{ padding: '4px' }}
+            type={'link'}
+            disabled={record.state.value === 'offline'}
+            onClick={() => {
+              updateChannel(record.id);
+            }}
           >
           >
-            <ArrowDownOutlined />
-          </Tooltip>
-        </a>,
-        <a>
-          <Tooltip
+            <SyncOutlined />
+          </Button>
+        </Tooltip>,
+        <Tooltip key={'updateChannel'} title="删除">
+          <Popconfirm
+            key="delete"
             title={intl.formatMessage({
             title={intl.formatMessage({
-              id: 'pages.notice.option.debug',
-              defaultMessage: '调试',
+              id:
+                record.state.value === 'offline'
+                  ? 'pages.device.productDetail.deleteTip'
+                  : 'page.table.isDelete',
+              defaultMessage: '是否删除?',
             })}
             })}
+            onConfirm={async () => {
+              if (record.state.value !== 'offline') {
+                await deleteItem(record.id);
+              } else {
+                message.error('在线设备不能进行删除操作');
+              }
+            }}
           >
           >
-            <BugOutlined />
-          </Tooltip>
-        </a>,
+            <Button
+              type={'link'}
+              style={{ padding: '4px' }}
+              disabled={record.state.value !== 'offline'}
+            >
+              <DeleteOutlined />
+            </Button>
+          </Popconfirm>
+        </Tooltip>,
       ],
       ],
     },
     },
   ];
   ];
 
 
-  /**
-   * table 查询参数
-   * @param data
-   */
-  const searchFn = (data: any) => {
-    setQueryParam(data);
-  };
-
-  const deleteItem = async (id: string) => {
-    const response: any = await service.remove(id);
-    if (response.status === 200) {
-      message.success(
-        intl.formatMessage({
-          id: 'pages.data.option.success',
-          defaultMessage: '操作成功!',
-        }),
-      );
-    }
-    actionRef.current?.reload();
-  };
-
   return (
   return (
     <PageContainer>
     <PageContainer>
       <SearchComponent field={columns} onSearch={searchFn} />
       <SearchComponent field={columns} onSearch={searchFn} />
@@ -266,8 +280,24 @@ const Device = () => {
                   defaultMessage: '编辑',
                   defaultMessage: '编辑',
                 })}
                 })}
               </Button>,
               </Button>,
-              <Button key={'viewChannel'}>查看通道</Button>,
-              <Button key={'updateChannel'} disabled={record.state.value === 'offline'}>
+              <Button
+                key={'viewChannel'}
+                onClick={() => {
+                  history.push(
+                    `${getMenuPathByParams(MENUS_CODE['media/Device/Channel'], record.id)}`,
+                  );
+                }}
+              >
+                <PartitionOutlined />
+                查看通道
+              </Button>,
+              <Button
+                key={'updateChannel'}
+                disabled={record.state.value === 'offline'}
+                onClick={() => {
+                  updateChannel(record.id);
+                }}
+              >
                 <SyncOutlined />
                 <SyncOutlined />
                 更新通道
                 更新通道
               </Button>,
               </Button>,

+ 3 - 0
src/pages/media/Device/service.ts

@@ -10,6 +10,9 @@ class Service extends BaseService<DeviceItem> {
   // 新增固定地址接入的设备
   // 新增固定地址接入的设备
   saveFixed = (data?: any) => request(`${this.uri}/fixed-url`, { method: 'PATCH', data });
   saveFixed = (data?: any) => request(`${this.uri}/fixed-url`, { method: 'PATCH', data });
 
 
+  // 更新通道
+  updateChannels = (id: string) => request(`${this.uri}/${id}/channels/_sync`, { method: 'POST' });
+
   // 快速添加产品
   // 快速添加产品
   saveProduct = (data?: any) =>
   saveProduct = (data?: any) =>
     request(`/${SystemConst.API_BASE}/device/product`, { method: 'POST', data });
     request(`/${SystemConst.API_BASE}/device/product`, { method: 'POST', data });

+ 6 - 0
src/utils/menu/index.ts

@@ -22,6 +22,12 @@ const extraRouteObj = {
       { code: 'Channel', name: '选择通道' },
       { code: 'Channel', name: '选择通道' },
     ],
     ],
   },
   },
+  'media/Device': {
+    children: [
+      { code: 'Channel', name: '通道列表' },
+      { code: 'Playback', name: '回放' },
+    ],
+  },
 };
 };
 
 
 /**
 /**

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

@@ -45,6 +45,7 @@ export const MENUS_CODE = {
   'media/Cascade/Channel': 'media/Cascade/Channel',
   'media/Cascade/Channel': 'media/Cascade/Channel',
   'media/Config': 'media/Config',
   'media/Config': 'media/Config',
   'media/Device': 'media/Device',
   'media/Device': 'media/Device',
+  'media/Device/Channel': 'media/Device/Channel',
   'media/Reveal': 'media/Reveal',
   'media/Reveal': 'media/Reveal',
   'media/Stream': 'media/Stream',
   'media/Stream': 'media/Stream',
   'media/Stream/Detail': 'media/Stream/Detail',
   'media/Stream/Detail': 'media/Stream/Detail',