Procházet zdrojové kódy

feat(merge): merge xyh

lind před 3 roky
rodič
revize
3620b48aab

+ 4 - 4
config/proxy.ts

@@ -9,10 +9,10 @@
 export default {
   dev: {
     '/jetlinks': {
-      // target: 'http://192.168.33.222:8844/',
-      // ws: 'ws://192.168.33.222:8844/',
-      target: 'http://120.79.18.123:8844/',
-      ws: 'ws://120.79.18.123:8844/',
+      target: 'http://192.168.32.44:8844/',
+      ws: 'ws://192.168.32.44:8844/',
+      // target: 'http://120.79.18.123:8844/',
+      // ws: 'ws://120.79.18.123:8844/',
       // target: 'http://192.168.66.5:8844/',
       // ws: 'ws://192.168.66.5:8844/',
       // ws: 'ws://demo.jetlinks.cn/jetlinks',

+ 54 - 141
src/components/Player/ScreenPlayer.tsx

@@ -1,12 +1,15 @@
-import { useCallback, useEffect, useRef, useState, useImperativeHandle, forwardRef } from 'react';
+import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
 import classNames from 'classnames';
 import LivePlayer from './index';
-import { Button, Dropdown, Empty, Input, Menu, Popover, Radio, Tooltip } from 'antd';
+import { Button, Dropdown, Empty, Menu, message, Popconfirm, Popover, Radio, Tooltip } from 'antd';
+import { createSchemaField } from '@formily/react';
+import { Form, FormItem, Input } from '@formily/antd';
 import { useFullscreen } from 'ahooks';
 import './index.less';
 import { DeleteOutlined, QuestionCircleOutlined } from '@ant-design/icons';
 import Service from './service';
 import MediaTool from '@/components/Player/mediaTool';
+import { createForm } from '@formily/core';
 
 type Player = {
   id?: string;
@@ -37,19 +40,27 @@ interface ScreenProps {
 
 const service = new Service();
 
-const DEFAULT_SAVE_CODE = 'screen_save';
+const DEFAULT_SAVE_CODE = 'screen-save';
 
 export default forwardRef((props: ScreenProps, ref) => {
   const [screen, setScreen] = useState(1);
   const [players, setPlayers] = useState<Player[]>([]);
   const [playerActive, setPlayerActive] = useState(0);
   const [historyList, setHistoryList] = useState<any>([]);
-  const [historyTitle, setHistoryTitle] = useState('');
   const [visible, setVisible] = useState(false);
 
   const fullscreenRef = useRef(null);
   const [isFullscreen, { setFull }] = useFullscreen(fullscreenRef);
 
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+    },
+  });
+
+  const historyForm = createForm();
+
   const replaceVideo = useCallback(
     (id: string, channelId: string, url: string) => {
       players[playerActive] = { id, url, channelId, key: 'time_' + new Date().getTime() };
@@ -86,8 +97,9 @@ export default forwardRef((props: ScreenProps, ref) => {
   };
 
   const saveHistory = useCallback(async () => {
+    const historyValue = await historyForm.submit<{ alias: string }>();
     const param = {
-      name: historyTitle,
+      name: historyValue.alias,
       content: JSON.stringify({
         screen: screen,
         players: players,
@@ -97,8 +109,11 @@ export default forwardRef((props: ScreenProps, ref) => {
     if (resp.status === 200) {
       setVisible(false);
       getHistory();
+      message.success('保存成功!');
+    } else {
+      message.error('保存失败');
     }
-  }, [players, historyTitle, screen]);
+  }, [players, screen, historyForm]);
 
   const screenChange = (index: number) => {
     const arr = new Array(index)
@@ -141,11 +156,19 @@ export default forwardRef((props: ScreenProps, ref) => {
               }}
             >
               {item.name}
-              <DeleteOutlined
-                onClick={() => {
-                  deleteHistory(item.id);
+              <Popconfirm
+                title={'确认删除'}
+                onConfirm={(e) => {
+                  e?.stopPropagation();
+                  deleteHistory(item.key);
                 }}
-              />
+              >
+                <DeleteOutlined
+                  onClick={(e) => {
+                    e.stopPropagation();
+                  }}
+                />
+              </Popconfirm>
             </Menu.Item>
           );
         })
@@ -210,11 +233,26 @@ export default forwardRef((props: ScreenProps, ref) => {
               <div className={'screen-tool-save'}>
                 <Popover
                   content={
-                    <div style={{ width: 300 }}>
-                      <Input.TextArea
-                        rows={3}
-                        onChange={(e) => {
-                          setHistoryTitle(e.target.value);
+                    <Form style={{ width: '217px' }} form={historyForm}>
+                      <SchemaField
+                        schema={{
+                          type: 'object',
+                          properties: {
+                            alias: {
+                              'x-decorator': 'FormItem',
+                              'x-component': 'Input.TextArea',
+                              'x-validator': [
+                                {
+                                  max: 64,
+                                  message: '最多可输入64个字符',
+                                },
+                                {
+                                  required: true,
+                                  message: '请输入名称',
+                                },
+                              ],
+                            },
+                          },
                         }}
                       />
                       <Button
@@ -224,7 +262,7 @@ export default forwardRef((props: ScreenProps, ref) => {
                       >
                         保存
                       </Button>
-                    </div>
+                    </Form>
                   }
                   title="分屏名称"
                   trigger="click"
@@ -270,131 +308,6 @@ export default forwardRef((props: ScreenProps, ref) => {
           }
         }}
       />
-      {/*<div className={'live-player-tools'}>*/}
-      {/*  <div className={'direction'}>*/}
-      {/*    <div*/}
-      {/*      className={'direction-item up'}*/}
-      {/*      onMouseDown={() => {*/}
-      {/*        const { id, channelId } = players[playerActive];*/}
-      {/*        if (id && channelId && props.onMouseDown) {*/}
-      {/*          props.onMouseDown(id, channelId, 'UP');*/}
-      {/*        }*/}
-      {/*      }}*/}
-      {/*      onMouseUp={() => {*/}
-      {/*        const { id, channelId } = players[playerActive];*/}
-      {/*        if (props.onMouseUp && id && channelId) {*/}
-      {/*          props.onMouseUp(id, channelId, 'UP');*/}
-      {/*        }*/}
-      {/*      }}*/}
-      {/*    >*/}
-      {/*      <CaretUpOutlined className={'direction-icon'} />*/}
-      {/*    </div>*/}
-      {/*    <div*/}
-      {/*      className={'direction-item right'}*/}
-      {/*      onMouseDown={() => {*/}
-      {/*        const { id, channelId } = players[playerActive];*/}
-      {/*        if (props.onMouseDown && id && channelId) {*/}
-      {/*          props.onMouseDown(id, channelId, 'RIGHT');*/}
-      {/*        }*/}
-      {/*      }}*/}
-      {/*      onMouseUp={() => {*/}
-      {/*        const { id, channelId } = players[playerActive];*/}
-      {/*        if (props.onMouseUp && id && channelId) {*/}
-      {/*          props.onMouseUp(id, channelId, 'RIGHT');*/}
-      {/*        }*/}
-      {/*      }}*/}
-      {/*    >*/}
-      {/*      <CaretRightOutlined className={'direction-icon'} />*/}
-      {/*    </div>*/}
-      {/*    <div*/}
-      {/*      className={'direction-item left'}*/}
-      {/*      onMouseDown={() => {*/}
-      {/*        const { id, channelId } = players[playerActive];*/}
-      {/*        if (props.onMouseDown && id && channelId) {*/}
-      {/*          props.onMouseDown(id, channelId, 'LEFT');*/}
-      {/*        }*/}
-      {/*      }}*/}
-      {/*      onMouseUp={() => {*/}
-      {/*        const { id, channelId } = players[playerActive];*/}
-      {/*        if (props.onMouseUp && id && channelId) {*/}
-      {/*          props.onMouseUp(id, channelId, 'LEFT');*/}
-      {/*        }*/}
-      {/*      }}*/}
-      {/*    >*/}
-      {/*      <CaretLeftOutlined className={'direction-icon'} />*/}
-      {/*    </div>*/}
-      {/*    <div*/}
-      {/*      className={'direction-item down'}*/}
-      {/*      onMouseDown={() => {*/}
-      {/*        const { id, channelId } = players[playerActive];*/}
-      {/*        if (props.onMouseDown && id && channelId) {*/}
-      {/*          props.onMouseDown(id, channelId, 'DOWN');*/}
-      {/*        }*/}
-      {/*      }}*/}
-      {/*      onMouseUp={() => {*/}
-      {/*        const { id, channelId } = players[playerActive];*/}
-      {/*        if (props.onMouseUp && id && channelId) {*/}
-      {/*          props.onMouseUp(id, channelId, 'DOWN');*/}
-      {/*        }*/}
-      {/*      }}*/}
-      {/*    >*/}
-      {/*      <CaretDownOutlined className={'direction-icon'} />*/}
-      {/*    </div>*/}
-      {/*    <div*/}
-      {/*      className={'direction-audio'}*/}
-      {/*      // onMouseDown={() => {*/}
-      {/*      //   const { id, channelId } = players[playerActive];*/}
-      {/*      //   if (props.onMouseDown && id && channelId) {*/}
-      {/*      //     props.onMouseDown(id, channelId, 'AUDIO');*/}
-      {/*      //   }*/}
-      {/*      // }}*/}
-      {/*      // onMouseUp={() => {*/}
-      {/*      //   const { id, channelId } = players[playerActive];*/}
-      {/*      //   if (props.onMouseUp && id && channelId) {*/}
-      {/*      //     props.onMouseUp(id, channelId, 'AUDIO');*/}
-      {/*      //   }*/}
-      {/*      // }}*/}
-      {/*    >*/}
-      {/*      /!*<AudioOutlined />*!/*/}
-      {/*    </div>*/}
-      {/*  </div>*/}
-      {/*  <div className={'zoom'}>*/}
-      {/*    <div*/}
-      {/*      className={'zoom-item zoom-in'}*/}
-      {/*      onMouseDown={() => {*/}
-      {/*        const { id, channelId } = players[playerActive];*/}
-      {/*        if (props.onMouseDown && id && channelId) {*/}
-      {/*          props.onMouseDown(id, channelId, 'ZOOM_IN');*/}
-      {/*        }*/}
-      {/*      }}*/}
-      {/*      onMouseUp={() => {*/}
-      {/*        const { id, channelId } = players[playerActive];*/}
-      {/*        if (props.onMouseUp && id && channelId) {*/}
-      {/*          props.onMouseUp(id, channelId, 'ZOOM_IN');*/}
-      {/*        }*/}
-      {/*      }}*/}
-      {/*    >*/}
-      {/*      <PlusOutlined />*/}
-      {/*    </div>*/}
-      {/*    <div*/}
-      {/*      className={'zoom-item zoom-out'}*/}
-      {/*      onMouseDown={() => {*/}
-      {/*        const { id, channelId } = players[playerActive];*/}
-      {/*        if (props.onMouseDown && id && channelId) {*/}
-      {/*          props.onMouseDown(id, channelId, 'ZOOM_OUT');*/}
-      {/*        }*/}
-      {/*      }}*/}
-      {/*      onMouseUp={() => {*/}
-      {/*        const { id, channelId } = players[playerActive];*/}
-      {/*        if (props.onMouseUp && id && channelId) {*/}
-      {/*          props.onMouseUp(id, channelId, 'ZOOM_OUT');*/}
-      {/*        }*/}
-      {/*      }}*/}
-      {/*    >*/}
-      {/*      <MinusOutlined />*/}
-      {/*    </div>*/}
-      {/*  </div>*/}
-      {/*</div>*/}
     </div>
   );
 });

+ 3 - 1
src/components/ProTableCard/CardItems/mediaDevice.tsx

@@ -8,13 +8,15 @@ import '../index.less';
 export interface ProductCardProps extends DeviceItem {
   detail?: React.ReactNode;
   actions?: React.ReactNode[];
+  showMask?: boolean;
 }
+
 const defaultImage = require('/public/images/device-media.png');
 
 export default (props: ProductCardProps) => {
   return (
     <TableCard
-      showMask={false}
+      showMask={props.showMask}
       detail={props.detail}
       actions={props.actions}
       status={props.state.value}

+ 4 - 4
src/components/ProTableCard/index.tsx

@@ -63,11 +63,11 @@ const ProTableCard = <
   };
 
   const windowChange = () => {
-    if (window.innerWidth < 1600) {
+    if (window.innerWidth <= 1366) {
+      setColumn(props.gridColumn && props.gridColumn < 2 ? props.gridColumn : 2);
+    } else if (window.innerWidth > 1366 && window.innerWidth <= 1600) {
       setColumn(props.gridColumn && props.gridColumn < 3 ? props.gridColumn : 3);
-    }
-
-    if (window.innerWidth > 1600) {
+    } else if (window.innerWidth > 1600) {
       setColumn(props.gridColumn && props.gridColumn < 4 ? props.gridColumn : 4);
     }
   };

+ 2 - 2
src/components/SearchComponent/index.tsx

@@ -506,8 +506,8 @@ const SearchComponent = <T extends Record<string, any>>(props: Props<T>) => {
                   'x-component': 'Input.TextArea',
                   'x-validator': [
                     {
-                      max: 50,
-                      message: '最多可输入50个字符',
+                      max: 64,
+                      message: '最多可输入64个字符',
                     },
                   ],
                 },

+ 87 - 0
src/components/SipSelectComponent/index.tsx

@@ -0,0 +1,87 @@
+import { Select } from 'antd';
+import { useEffect, useState } from 'react';
+
+interface SipSelectComponentProps {
+  onChange?: (data: any) => void;
+  value?: {
+    host?: string;
+    port?: number;
+  };
+  transport?: 'UDP' | 'TCP';
+  data: any[];
+}
+
+const SipSelectComponent = (props: SipSelectComponentProps) => {
+  const { value, onChange, transport } = props;
+  const [data, setData] = useState<{ host?: string; port?: number } | undefined>(value);
+  const [list, setList] = useState<any[]>([]);
+
+  useEffect(() => {
+    setData(value);
+  }, [value]);
+
+  useEffect(() => {
+    setData(undefined);
+  }, [transport]);
+
+  return (
+    <div style={{ display: 'flex', alignItems: 'center' }}>
+      <Select
+        showSearch
+        value={data?.host}
+        style={{ marginRight: 10 }}
+        placeholder="请选择IP地址"
+        optionFilterProp="children"
+        onChange={(e) => {
+          if (onChange) {
+            const item = {
+              port: undefined,
+              host: e,
+            };
+            setData(item);
+            onChange(item);
+            const dt: any = props.data.find((i) => i.host === e);
+            setList(dt?.ports[transport || ''] || []);
+          }
+        }}
+        filterOption={(input: string, option: any) =>
+          String(option?.children)?.toLowerCase()?.indexOf(String(input).toLowerCase()) >= 0
+        }
+      >
+        {(props.data || []).map((item: any) => (
+          <Select.Option key={item.host} value={item.host}>
+            {item.host}
+          </Select.Option>
+        ))}
+      </Select>
+      <Select
+        showSearch
+        style={{ maxWidth: 100 }}
+        value={data?.port}
+        placeholder="请选择端口"
+        optionFilterProp="children"
+        onChange={(e: number) => {
+          if (onChange) {
+            const item = {
+              ...data,
+              port: e,
+            };
+            setData(item);
+            onChange(item);
+          }
+        }}
+        filterOption={(input: string, option: any) =>
+          String(option?.children)?.toLowerCase()?.indexOf(String(input).toLowerCase()) >= 0
+        }
+      >
+        {list.map((item) => (
+          <Select.Option key={item} value={item}>
+            {item}
+          </Select.Option>
+        ))}
+      </Select>
+    </div>
+  );
+};
+
+export default SipSelectComponent;

+ 2 - 2
src/locales/zh-CN/pages.ts

@@ -378,9 +378,9 @@ export default {
   'pages.media.config': '基本配置',
   'pages.media.device.transport': '信令传输',
   'pages.media.device.streamMode': '流传输模式',
-  'pages.media.device.channelNumber': '通道数',
+  'pages.media.device.channelNumber': '通道数',
   'pages.media.device.port': '端口',
-  'pages.media.device.manufacturer': '设备厂家',
+  'pages.media.device.manufacturer': '厂商',
   'pages.media.device.model': '型号',
   'pages.media.device.firmware': '固件版本',
   'pages.media.device': '视频设备',

+ 34 - 14
src/pages/device/Instance/Detail/MetadataMap/EditableTable/index.tsx

@@ -1,6 +1,7 @@
 import React, { useContext, useEffect, useState } from 'react';
 import { Form, Input, message, Pagination, Select, Table } from 'antd';
 import { service } from '@/pages/device/Instance';
+import { QuestionCircleOutlined } from '@ant-design/icons';
 
 const EditableContext: any = React.createContext(null);
 
@@ -58,7 +59,14 @@ const EditableCell = ({
   if (editable) {
     childNode = (
       <Form.Item style={{ margin: 0 }} name={dataIndex}>
-        <Select onChange={save}>
+        <Select
+          onChange={save}
+          showSearch
+          optionFilterProp="children"
+          filterOption={(input: string, option: any) =>
+            (option?.children || '').toLowerCase()?.indexOf(input.toLowerCase()) >= 0
+          }
+        >
           {list.map((item: any) => (
             <Select.Option key={item?.id} value={item?.id}>
               {item?.id}
@@ -231,19 +239,31 @@ const EditableTable = (props: Props) => {
 
   return (
     <div>
-      <Input.Search
-        placeholder="请输入物模型属性名"
-        allowClear
-        style={{ width: 300, marginBottom: 20 }}
-        onSearch={(e: string) => {
-          setValue(e);
-          handleSearch({
-            name: e,
-            pageIndex: 0,
-            pageSize: 10,
-          });
-        }}
-      />
+      <div style={{ display: 'flex', alignItems: 'center', marginBottom: 10 }}>
+        <Input.Search
+          placeholder="请输入物模型属性名"
+          allowClear
+          style={{ width: 300, marginRight: 10 }}
+          onSearch={(e: string) => {
+            setValue(e);
+            handleSearch({
+              name: e,
+              pageIndex: 0,
+              pageSize: 10,
+            });
+          }}
+        />
+        <div>
+          <div style={{ color: 'rgba(0, 0, 0, .65)' }}>
+            <QuestionCircleOutlined style={{ margin: 5 }} />
+            该设备已脱离产品物模型映射,修改产品物模型映射对该设备物模型映射无影响
+          </div>
+          <div style={{ color: 'rgba(0, 0, 0, .65)' }}>
+            <QuestionCircleOutlined style={{ margin: 5 }} />
+            设备会默认继承产品的物模型映射,修改设备物模型映射后将脱离产品物模型映射
+          </div>
+        </div>
+      </div>
       <Table
         components={components}
         rowClassName={() => 'editable-row'}

+ 1 - 0
src/pages/device/Product/Detail/Access/AccessConfig/index.tsx

@@ -92,6 +92,7 @@ const AccessConfig = (props: Props) => {
               protocolName: currrent.protocolDetail.name,
               accessId: currrent.id,
               accessName: currrent.name,
+              accessProvider: currrent.provider,
               messageProtocol: currrent.protocol,
             })
             .then((resp) => {

+ 54 - 51
src/pages/device/Product/index.tsx

@@ -170,20 +170,20 @@ const Product = observer(() => {
         />
       </Tooltip>
     </Button>,
-    <Button
-      disabled={getButtonPermission('device/Product', ['action'])}
-      style={{ padding: 0 }}
-      type={'link'}
+    <Popconfirm
+      key={'state'}
+      title={intl.formatMessage({
+        id: `pages.data.option.${record.state ? 'disabled' : 'enabled'}.tips`,
+        defaultMessage: '是否启用?',
+      })}
+      onConfirm={() => {
+        changeDeploy(record.id, record.state ? 'undeploy' : 'deploy');
+      }}
     >
-      <Popconfirm
-        key={'state'}
-        title={intl.formatMessage({
-          id: `pages.data.option.${record.state ? 'disabled' : 'enabled'}.tips`,
-          defaultMessage: '是否启用?',
-        })}
-        onConfirm={() => {
-          changeDeploy(record.id, record.state ? 'undeploy' : 'deploy');
-        }}
+      <Button
+        disabled={getButtonPermission('device/Product', ['action'])}
+        style={{ padding: 0 }}
+        type={'link'}
       >
         <Tooltip
           title={intl.formatMessage({
@@ -193,9 +193,10 @@ const Product = observer(() => {
         >
           {record.state ? <StopOutlined /> : <PlayCircleOutlined />}
         </Tooltip>
-      </Popconfirm>
-    </Button>,
+      </Button>
+    </Popconfirm>,
     <Button
+      key="unBindUser"
       disabled={getButtonPermission('device/Product', ['delete'])}
       type={'link'}
       style={{ padding: 0 }}
@@ -280,6 +281,7 @@ const Product = observer(() => {
       <SearchComponent
         field={columns}
         onSearch={searchFn}
+        target="device-produce"
         // onReset={() => {
         //   // 重置分页及搜索参数
         //   actionRef.current?.reset?.();
@@ -424,53 +426,54 @@ const Product = observer(() => {
                   defaultMessage: '下载',
                 })}
               </Button>,
-              <Button
-                style={{ padding: 0 }}
-                type={'link'}
-                disabled={getButtonPermission('device/Product', ['action'])}
+              <Popconfirm
+                key={'state'}
+                title={intl.formatMessage({
+                  id: `pages.data.option.${record.state ? 'disabled' : 'enabled'}.tips`,
+                  defaultMessage: '是否启用?',
+                })}
+                onConfirm={() => {
+                  changeDeploy(record.id, record.state ? 'undeploy' : 'deploy');
+                }}
               >
-                <Popconfirm
-                  key={'state'}
-                  title={intl.formatMessage({
-                    id: `pages.data.option.${record.state ? 'disabled' : 'enabled'}.tips`,
-                    defaultMessage: '是否启用?',
-                  })}
-                  onConfirm={() => {
-                    changeDeploy(record.id, record.state ? 'undeploy' : 'deploy');
-                  }}
+                <Button
+                  style={{ padding: 0 }}
+                  type={'link'}
+                  disabled={getButtonPermission('device/Product', ['action'])}
                 >
                   {record.state ? <StopOutlined /> : <PlayCircleOutlined />}
                   {intl.formatMessage({
                     id: `pages.data.option.${record.state ? 'disabled' : 'enabled'}`,
                     defaultMessage: record.state ? '禁用' : '启用',
                   })}
-                </Popconfirm>
-              </Button>,
-              <Button
-                type={'link'}
-                style={{ padding: 0 }}
+                </Button>
+              </Popconfirm>,
+              <Popconfirm
+                key="delete"
                 disabled={getButtonPermission('device/Product', ['delete'])}
+                title={intl.formatMessage({
+                  id:
+                    record.state === 1
+                      ? 'pages.device.productDetail.deleteTip'
+                      : 'page.table.isDelete',
+                  defaultMessage: '是否删除?',
+                })}
+                onConfirm={async () => {
+                  if (record.state === 0) {
+                    await deleteItem(record.id);
+                  } else {
+                    message.error('已发布的产品不能进行删除操作');
+                  }
+                }}
               >
-                <Popconfirm
-                  key="delete"
-                  title={intl.formatMessage({
-                    id:
-                      record.state === 1
-                        ? 'pages.device.productDetail.deleteTip'
-                        : 'page.table.isDelete',
-                    defaultMessage: '是否删除?',
-                  })}
-                  onConfirm={async () => {
-                    if (record.state === 0) {
-                      await deleteItem(record.id);
-                    } else {
-                      message.error('已发布的产品不能进行删除操作');
-                    }
-                  }}
+                <Button
+                  type={'link'}
+                  style={{ padding: 0 }}
+                  disabled={getButtonPermission('device/Product', ['delete'])}
                 >
                   <DeleteOutlined />
-                </Popconfirm>
-              </Button>,
+                </Button>
+              </Popconfirm>,
             ]}
           />
         )}

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

@@ -27,6 +27,7 @@ export type ProductItem = {
   accessId?: string;
   accessName?: string;
   photoUrl?: string;
+  accessProvider?: string;
 };
 
 export type ConfigProperty = {

+ 39 - 33
src/pages/link/AccessConfig/Detail/Media/index.tsx

@@ -25,6 +25,7 @@ import { testIP } from '@/utils/util';
 type LocationType = {
   id?: string;
 };
+
 interface Props {
   change: () => void;
   data: any;
@@ -50,6 +51,42 @@ const Media = (props: Props) => {
     },
   ];
 
+  const initConfig = (tconfiguration: any) => {
+    if (tconfiguration?.shareCluster) {
+      const hostPort = { ...tconfiguration?.hostPort };
+      setConfiguration({
+        ...tconfiguration,
+        hostPort: {
+          sip: {
+            port: hostPort?.port,
+            host: hostPort?.host,
+          },
+          public: {
+            port: hostPort?.publicPort,
+            host: hostPort?.publicHost,
+          },
+        },
+      });
+    } else {
+      const cluster: any[] = [];
+      (tconfiguration?.cluster || []).forEach((item: any) => {
+        cluster.push({
+          clusterNodeId: item?.clusterNodeId || '',
+          enabled: true,
+          sip: {
+            port: item?.port,
+            host: item?.host,
+          },
+          public: {
+            port: item?.publicPort,
+            host: item?.publicHost,
+          },
+        });
+      });
+      setConfiguration({ ...tconfiguration, cluster });
+    }
+  };
+
   const BasicRender = () => {
     const SchemaField = createSchemaField({
       components: {
@@ -399,6 +436,7 @@ const Media = (props: Props) => {
                 <Button
                   style={{ margin: '0 8px' }}
                   onClick={() => {
+                    initConfig(configuration);
                     setCurrent(0);
                   }}
                 >
@@ -461,39 +499,7 @@ const Media = (props: Props) => {
         description: props.data?.description,
       });
       if (props?.provider?.id !== 'fixed-media') {
-        if (props.data?.configuration?.shareCluster) {
-          const hostPort = { ...props.data?.configuration?.hostPort };
-          setConfiguration({
-            ...props.data?.configuration,
-            hostPort: {
-              sip: {
-                port: hostPort.port,
-                host: hostPort.host,
-              },
-              public: {
-                port: hostPort.publicPort,
-                host: hostPort.publicHost,
-              },
-            },
-          });
-        } else {
-          const cluster: any[] = [];
-          (props.data?.configuration?.cluster || []).forEach((item: any) => {
-            cluster.push({
-              clusterNodeId: item?.clusterNodeId || '',
-              enabled: true,
-              sip: {
-                port: item?.port,
-                host: item?.host,
-              },
-              public: {
-                port: item?.publicPort,
-                host: item?.publicHost,
-              },
-            });
-          });
-          setConfiguration({ ...props.data?.configuration, cluster });
-        }
+        initConfig(props.data?.configuration);
       }
     }
   }, [props.data]);

+ 14 - 10
src/pages/link/Protocol/index.tsx

@@ -23,9 +23,9 @@ const Protocol = () => {
   const modifyState = async (id: string, type: 'deploy' | 'un-deploy') => {
     const resp = await service.modifyState(id, type);
     if (resp.status === 200) {
-      message.success('插件发布成功!');
+      message.success('操作成功!');
     } else {
-      message.error('插件发布失败!');
+      message.error('操作失败!');
     }
     actionRef.current?.reload();
   };
@@ -141,14 +141,18 @@ const Protocol = () => {
                 defaultMessage: '确认删除?',
               })}
               onConfirm={async () => {
-                await service.remove(record.id);
-                message.success(
-                  intl.formatMessage({
-                    id: 'pages.data.option.success',
-                    defaultMessage: '操作成功!',
-                  }),
-                );
-                actionRef.current?.reload();
+                const resp: any = await service.remove(record.id);
+                if (resp.status === 200) {
+                  message.success(
+                    intl.formatMessage({
+                      id: 'pages.data.option.success',
+                      defaultMessage: '操作成功!',
+                    }),
+                  );
+                  actionRef.current?.reload();
+                } else {
+                  message.error(resp?.message || '操作失败');
+                }
               }}
             >
               <Tooltip

+ 8 - 4
src/pages/media/Cascade/Channel/BindChannel/index.tsx

@@ -80,10 +80,14 @@ const BindChannel = (props: Props) => {
       visible
       onCancel={props.close}
       onOk={async () => {
-        const resp = await service.bindChannel(props.data, selectedRowKey);
-        if (resp.status === 200) {
-          message.success('操作成功!');
-          props.close();
+        if (selectedRowKey.length > 0) {
+          const resp = await service.bindChannel(props.data, selectedRowKey);
+          if (resp.status === 200) {
+            message.success('操作成功!');
+            props.close();
+          }
+        } else {
+          message.error('请勾选数据');
         }
       }}
       width={1200}

+ 64 - 8
src/pages/media/Cascade/Channel/index.tsx

@@ -1,11 +1,11 @@
 import { service } from '@/pages/media/Cascade';
 import SearchComponent from '@/components/SearchComponent';
-import { DisconnectOutlined } from '@ant-design/icons';
+import { DisconnectOutlined, EditOutlined } from '@ant-design/icons';
 import { PageContainer } from '@ant-design/pro-layout';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
-import { Button, message, Popconfirm, Space, Tooltip } from 'antd';
-import { useRef, useState } from 'react';
 import ProTable from '@jetlinks/pro-table';
+import { Button, Input, message, Popconfirm, Popover, Space, Tooltip } from 'antd';
+import { useRef, useState } from 'react';
 import { useIntl, useLocation } from 'umi';
 import BindChannel from './BindChannel';
 import BadgeStatus, { StatusColorEnum } from '@/components/BadgeStatus';
@@ -18,15 +18,46 @@ const Channel = () => {
   const [visible, setVisible] = useState<boolean>(false);
   const [selectedRowKey, setSelectedRowKey] = useState<string[]>([]);
   const id = location?.query?.id || '';
+  const [data, setData] = useState<string>('');
 
-  const unbind = async (data: string[]) => {
-    const resp = await service.unbindChannel(id, data);
+  const unbind = async (list: string[]) => {
+    const resp = await service.unbindChannel(id, list);
     if (resp.status === 200) {
       actionRef.current?.reload();
       message.success('操作成功!');
     }
   };
 
+  const content = (record: any) => {
+    return (
+      <div>
+        <Input
+          value={data}
+          placeholder="请输入国标ID"
+          onChange={(e) => {
+            setData(e.target.value);
+          }}
+        />
+        <Button
+          type="primary"
+          style={{ marginTop: 10, width: '100%' }}
+          onClick={async () => {
+            if (!!data) {
+              const resp: any = service.editBindInfo(record.id, { gbChannelId: data });
+              if (resp.status === 200) {
+                actionRef.current?.reload();
+              }
+            } else {
+              message.error('请输入国标ID');
+            }
+          }}
+        >
+          保存
+        </Button>
+      </div>
+    );
+  };
+
   const columns: ProColumns<any>[] = [
     {
       dataIndex: 'deviceName',
@@ -39,6 +70,22 @@ const Channel = () => {
     {
       dataIndex: 'channelId',
       title: '国标ID',
+      tooltip: '国标级联有18位、20位两种格式。在当前页面修改不会修改视频设备-通道页面中的国标ID',
+      render: (text: any, record: any) => (
+        <span>
+          {text}
+          <Popover trigger="click" content={content(record)} title="编辑通道ID">
+            <a
+              style={{ marginLeft: 10 }}
+              onClick={() => {
+                setData('');
+              }}
+            >
+              <EditOutlined />
+            </a>
+          </Popover>
+        </span>
+      ),
     },
     {
       dataIndex: 'address',
@@ -90,11 +137,11 @@ const Channel = () => {
       <SearchComponent<any>
         field={columns}
         target="unbind-channel"
-        onSearch={(data) => {
+        onSearch={(params) => {
           actionRef.current?.reload();
           setParam({
             ...param,
-            terms: data?.terms ? [...data?.terms] : [],
+            terms: params?.terms ? [...params?.terms] : [],
           });
         }}
       />
@@ -148,7 +195,16 @@ const Channel = () => {
           >
             绑定通道
           </Button>,
-          <Button onClick={() => {}} key="unbind">
+          <Button
+            onClick={() => {
+              if (selectedRowKey.length > 0) {
+                unbind(selectedRowKey);
+              } else {
+                message.error('请先选择需要解绑的通道列表');
+              }
+            }}
+            key="unbind"
+          >
             批量解绑
           </Button>,
         ]}

+ 57 - 14
src/pages/media/Cascade/Save/index.tsx

@@ -15,6 +15,7 @@ import {
   Tooltip,
 } from 'antd';
 import SipComponent from '@/components/SipComponent';
+import SipSelectComponent from '@/components/SipSelectComponent';
 import { testIP } from '@/utils/util';
 import { useEffect, useState } from 'react';
 import { service } from '../index';
@@ -25,10 +26,14 @@ const Save = () => {
   const [form] = Form.useForm();
   const [clusters, setClusters] = useState<any[]>([]);
   const id = location?.query?.id || '';
+  const [list, setList] = useState<any[]>([]);
+  const [transport, setTransport] = useState<'UDP' | 'TCP'>('UDP');
 
   const checkSIP = (_: any, value: { host: string; port: number }) => {
-    if (!value || !value.host) {
-      return Promise.reject(new Error('请输入API HOST'));
+    if (!value) {
+      return Promise.resolve();
+    } else if (!value.host) {
+      return Promise.reject(new Error('请输入IP 地址'));
     } else if (value?.host && !testIP(value.host)) {
       return Promise.reject(new Error('请输入正确的IP地址'));
     } else if (!value?.port) {
@@ -38,13 +43,27 @@ const Save = () => {
     }
     return Promise.resolve();
   };
-
+  const checkLocalSIP = (_: any, value: { host: string; port: number }) => {
+    if (!value) {
+      return Promise.resolve();
+    } else if (!value.host) {
+      return Promise.reject(new Error('请选择IP地址'));
+    } else if (!value?.port) {
+      return Promise.reject(new Error('请选择端口'));
+    }
+    return Promise.resolve();
+  };
   useEffect(() => {
     service.queryClusters().then((resp) => {
       if (resp.status === 200) {
         setClusters(resp.result);
       }
     });
+    service.queryResources().then((resp) => {
+      if (resp.status === 200) {
+        setList(resp.result);
+      }
+    });
     if (!!id) {
       service.detail(id).then((resp) => {
         if (resp.status === 200) {
@@ -69,6 +88,13 @@ const Save = () => {
     }
   }, []);
 
+  const keepValidator = (_: any, value: any) => {
+    if ((!value && value !== 0) || (Number(value) >= 1 && Number(value) <= 10000)) {
+      return Promise.resolve();
+    }
+    return Promise.reject(new Error('请输入1~10000之间的数字'));
+  };
+
   return (
     <PageContainer>
       <Card>
@@ -80,6 +106,8 @@ const Save = () => {
             proxyStream: false,
             sipConfigs: {
               transport: 'UDP',
+              keepaliveInterval: 60,
+              registerInterval: 3000,
             },
           }}
           onFinish={async (values: any) => {
@@ -141,9 +169,9 @@ const Save = () => {
                   </span>
                 }
                 name={['sipConfigs', 'clusterNodeId']}
-                rules={[{ required: true, message: '请选择信令服务配置' }]}
+                rules={[{ required: true, message: '请选择集群节点' }]}
               >
-                <Select placeholder="请选择信令服务配置">
+                <Select placeholder="请选择集群节点">
                   {clusters.map((item) => (
                     <Select.Option key={item.id} value={item.id}>
                       {item.name}
@@ -174,9 +202,9 @@ const Save = () => {
               <Form.Item
                 label="上级SIP域"
                 name={['sipConfigs', 'domain']}
-                rules={[{ required: true, message: '请输入上级SIP域' }]}
+                rules={[{ required: true, message: '请输入上级平台SIP域' }]}
               >
-                <Input placeholder="请输入上级SIP域" />
+                <Input placeholder="请输入上级平台SIP域" />
               </Form.Item>
             </Col>
             <Col span={12}>
@@ -192,9 +220,9 @@ const Save = () => {
               <Form.Item
                 label="本地SIP ID"
                 name={['sipConfigs', 'localSipId']}
-                rules={[{ required: true, message: '请输入本地SIP ID' }]}
+                rules={[{ required: true, message: '请输入网关侧的SIP ID' }]}
               >
-                <Input placeholder="请输入本地SIP ID" />
+                <Input placeholder="网关侧的SIP ID" />
               </Form.Item>
             </Col>
             <Col span={12}>
@@ -203,7 +231,13 @@ const Save = () => {
                 name={['sipConfigs', 'transport']}
                 rules={[{ required: true, message: '请选择传输协议' }]}
               >
-                <Radio.Group optionType="button" buttonStyle="solid">
+                <Radio.Group
+                  optionType="button"
+                  buttonStyle="solid"
+                  onChange={(e) => {
+                    setTransport(e.target.value);
+                  }}
+                >
                   <Radio.Button value="UDP">UDP</Radio.Button>
                   <Radio.Button value="TCP">TCP</Radio.Button>
                 </Radio.Group>
@@ -220,9 +254,12 @@ const Save = () => {
                   </span>
                 }
                 name={['sipConfigs', 'local']}
-                rules={[{ required: true, message: '请输入SIP本地地址' }, { validator: checkSIP }]}
+                rules={[
+                  { required: true, message: '请输入SIP本地地址' },
+                  { validator: checkLocalSIP },
+                ]}
               >
-                <SipComponent />
+                <SipSelectComponent data={list} transport={transport} />
               </Form.Item>
             </Col>
             <Col span={12}>
@@ -274,7 +311,10 @@ const Save = () => {
               <Form.Item
                 label="心跳周期(秒)"
                 name={['sipConfigs', 'keepaliveInterval']}
-                rules={[{ required: true, message: '请输入心跳周期' }]}
+                rules={[
+                  { required: true, message: '请输入心跳周期' },
+                  { validator: keepValidator },
+                ]}
               >
                 <InputNumber placeholder="请输入心跳周期" style={{ width: '100%' }} />
               </Form.Item>
@@ -283,7 +323,10 @@ const Save = () => {
               <Form.Item
                 label="注册间隔(秒)"
                 name={['sipConfigs', 'registerInterval']}
-                rules={[{ required: true, message: '请输入注册间隔' }]}
+                rules={[
+                  { required: true, message: '请输入注册间隔' },
+                  { validator: keepValidator },
+                ]}
               >
                 <InputNumber placeholder="请输入注册间隔" style={{ width: '100%' }} />
               </Form.Item>

+ 94 - 77
src/pages/media/Cascade/index.tsx

@@ -35,6 +35,7 @@ const Cascade = () => {
   const tools = (record: CascadeItem) => [
     <Button
       type={'link'}
+      key={'edit'}
       style={{ padding: 0 }}
       disabled={getButtonPermission('media/Cascade', ['update', 'view'])}
       onClick={() => {
@@ -50,10 +51,12 @@ const Cascade = () => {
         key={'edit'}
       >
         <EditOutlined />
+        编辑
       </Tooltip>
     </Button>,
     <Button
       type={'link'}
+      key={'channel'}
       style={{ padding: 0 }}
       onClick={() => {
         const url = getMenuPathByCode(MENUS_CODE[`media/Cascade/Channel`]);
@@ -62,9 +65,10 @@ const Cascade = () => {
     >
       <Tooltip title={'选择通道'} key={'channel'}>
         <LinkOutlined />
+        选择通道
       </Tooltip>
     </Button>,
-    <Button type={'link'}>
+    <Button type={'link'} key={'share'} disabled={record.status.value === 'disabled'}>
       <Popconfirm
         key={'share'}
         title="确认共享!"
@@ -75,11 +79,13 @@ const Cascade = () => {
       >
         <Tooltip title={'共享'}>
           <ShareAltOutlined />
+          共享
         </Tooltip>
       </Popconfirm>
     </Button>,
     <Button
       type={'link'}
+      key={'operate'}
       style={{ padding: 0 }}
       disabled={getButtonPermission('media/Cascade', ['action'])}
     >
@@ -100,13 +106,24 @@ const Cascade = () => {
         }}
       >
         <Tooltip title={record.status.value === 'disabled' ? '启用' : '禁用'}>
-          {record.status.value === 'disabled' ? <CheckCircleOutlined /> : <StopOutlined />}
+          {record.status.value === 'disabled' ? (
+            <span>
+              <CheckCircleOutlined /> 启用
+            </span>
+          ) : (
+            <span>
+              {' '}
+              <StopOutlined />
+              禁用
+            </span>
+          )}
         </Tooltip>
       </Popconfirm>
     </Button>,
     <Button
       type={'link'}
       style={{ padding: 0 }}
+      key={'delete'}
       disabled={getButtonPermission('media/Cascade', ['delete'])}
     >
       <Popconfirm
@@ -143,11 +160,13 @@ const Cascade = () => {
     {
       dataIndex: 'sipConfigs[0].sipId',
       title: '上级SIP ID',
+      hideInSearch: true,
       render: (text: any, record: any) => record.sipConfigs[0].sipId,
     },
     {
       dataIndex: 'sipConfigs[0].publicHost',
       title: '上级SIP 地址',
+      hideInSearch: true,
       render: (text: any, record: any) => record.sipConfigs[0].publicHost,
     },
     {
@@ -207,84 +226,83 @@ const Cascade = () => {
       }),
       valueType: 'option',
       align: 'center',
-      width: 200,
       render: (text, record) => [
-        <Button
-          type="link"
-          style={{ padding: 0 }}
-          disabled={getButtonPermission('media/Cascade', ['view', 'update'])}
+        <Tooltip
           key={'edit'}
-          onClick={() => {
-            const url = getMenuPathByCode(MENUS_CODE[`media/Cascade/Save`]);
-            history.push(url + `?id=${record.id}`);
-          }}
+          title={intl.formatMessage({
+            id: 'pages.data.option.edit',
+            defaultMessage: '编辑',
+          })}
         >
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.data.option.edit',
-              defaultMessage: '编辑',
-            })}
+          <Button
+            type="link"
+            style={{ padding: 0 }}
+            disabled={getButtonPermission('media/Cascade', ['view', 'update'])}
+            onClick={() => {
+              const url = getMenuPathByCode(MENUS_CODE[`media/Cascade/Save`]);
+              history.push(url + `?id=${record.id}`);
+            }}
           >
             <EditOutlined />
-          </Tooltip>
-        </Button>,
-        <Button
-          type="link"
-          style={{ padding: 0 }}
-          key={'channel'}
-          onClick={() => {
-            const url = getMenuPathByCode(MENUS_CODE[`media/Cascade/Channel`]);
-            history.push(url + `?id=${record.id}`);
-          }}
-        >
-          <Tooltip title={'选择通道'}>
-            <LinkOutlined />
-          </Tooltip>
-        </Button>,
-        <Button type="link" style={{ padding: 0 }}>
-          <Popconfirm
-            key={'share'}
-            onConfirm={() => {
-              setVisible(true);
-              setCurrent(record);
+          </Button>
+        </Tooltip>,
+        <Tooltip title={'选择通道'} key={'channel'}>
+          <Button
+            type="link"
+            style={{ padding: 0 }}
+            onClick={() => {
+              const url = getMenuPathByCode(MENUS_CODE[`media/Cascade/Channel`]);
+              history.push(url + `?id=${record.id}`);
             }}
-            title={'确认共享'}
           >
-            <Tooltip title={'共享'}>
+            <LinkOutlined />
+          </Button>
+        </Tooltip>,
+        <Tooltip title={'共享'} key={'share'}>
+          <Button type="link" style={{ padding: 0 }} disabled={record.status.value === 'disabled'}>
+            <Popconfirm
+              onConfirm={() => {
+                setVisible(true);
+                setCurrent(record);
+              }}
+              title={'确认共享'}
+            >
               <ShareAltOutlined />
-            </Tooltip>
-          </Popconfirm>
-        </Button>,
-        <Button
-          type="link"
-          style={{ padding: 0 }}
-          disabled={getButtonPermission('media/Cascade', ['action'])}
-        >
-          <Popconfirm
-            key={'able'}
-            title={record.status.value === 'disabled' ? '确认启用' : '确认禁用'}
-            onConfirm={async () => {
-              let resp: any = undefined;
-              if (record.status.value === 'disabled') {
-                resp = await service.enabled(record.id);
-              } else {
-                resp = await service.disabled(record.id);
-              }
-              if (resp?.status === 200) {
-                message.success('操作成功!');
-                actionRef.current?.reset?.();
-              }
-            }}
+            </Popconfirm>
+          </Button>
+        </Tooltip>,
+        <Tooltip title={record.status.value === 'disabled' ? '启用' : '禁用'} key={'operate'}>
+          <Button
+            type="link"
+            style={{ padding: 0 }}
+            disabled={getButtonPermission('media/Cascade', ['action'])}
           >
-            <Tooltip title={record.status.value === 'disabled' ? '启用' : '禁用'}>
+            <Popconfirm
+              key={'able'}
+              title={record.status.value === 'disabled' ? '确认启用' : '确认禁用'}
+              onConfirm={async () => {
+                let resp: any = undefined;
+                if (record.status.value === 'disabled') {
+                  resp = await service.enabled(record.id);
+                } else {
+                  resp = await service.disabled(record.id);
+                }
+                if (resp?.status === 200) {
+                  message.success('操作成功!');
+                  actionRef.current?.reset?.();
+                }
+              }}
+            >
               {record.status.value === 'disabled' ? <CheckCircleOutlined /> : <StopOutlined />}
-            </Tooltip>
-          </Popconfirm>
-        </Button>,
-        <Button
-          type="link"
-          style={{ padding: 0 }}
-          disabled={getButtonPermission('media/Cascade', ['delete'])}
+            </Popconfirm>
+          </Button>
+        </Tooltip>,
+        <Tooltip
+          key={'delete'}
+          title={intl.formatMessage({
+            id: 'pages.data.option.remove',
+            defaultMessage: '删除',
+          })}
         >
           <Popconfirm
             title={'确认删除'}
@@ -297,16 +315,15 @@ const Cascade = () => {
               }
             }}
           >
-            <Tooltip
-              title={intl.formatMessage({
-                id: 'pages.data.option.remove',
-                defaultMessage: '删除',
-              })}
+            <Button
+              type="link"
+              style={{ padding: 0 }}
+              disabled={getButtonPermission('media/Cascade', ['delete'])}
             >
               <DeleteOutlined />
-            </Tooltip>
+            </Button>
           </Popconfirm>
-        </Button>,
+        </Tooltip>,
       ],
     },
   ];

+ 11 - 0
src/pages/media/Cascade/service.ts

@@ -61,6 +61,17 @@ class Service extends BaseService<CascadeItem> {
       method: 'POST',
       data,
     });
+  // 编辑绑定信息
+  editBindInfo = (id: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/media/gb28181-cascade/binding/${id}`, {
+      method: 'PUT',
+      data,
+    });
+  //
+  queryResources = () =>
+    request(`/${SystemConst.API_BASE}/network/resources/alive/_all`, {
+      method: 'GET',
+    });
 }
 
 export default Service;

+ 4 - 2
src/pages/media/Device/Playback/index.tsx

@@ -2,13 +2,13 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import LivePlayer from '@/components/Player';
 import { useCallback, useEffect, useState } from 'react';
-import { Select, Calendar, Empty, List, Tooltip } from 'antd';
+import { Calendar, Empty, List, Select, Tooltip } from 'antd';
 import { useLocation } from 'umi';
 import Service from './service';
 import './index.less';
 import { recordsItemType } from '@/pages/media/Device/Playback/typings';
-import * as moment from 'moment';
 import type { Moment } from 'moment';
+import * as moment from 'moment';
 import classNames from 'classnames';
 import {
   CloudDownloadOutlined,
@@ -65,6 +65,8 @@ export default () => {
         } else {
           setHistoryList(list);
         }
+      } else {
+        setHistoryList([]);
       }
     }
   };

+ 5 - 1
src/pages/media/Device/Save/SaveProduct.tsx

@@ -24,7 +24,10 @@ export default (props: SaveProps) => {
   useEffect(() => {
     if (visible) {
       getProviderList({
-        terms: [{ column: 'provider', value: props.type }],
+        terms: [
+          { column: 'provider', value: props.type },
+          { column: 'state', value: 'enabled' },
+        ],
       });
     }
   }, [visible]);
@@ -45,6 +48,7 @@ export default (props: SaveProps) => {
   const onSubmit = async () => {
     const formData = await form.validateFields();
     if (formData) {
+      formData.deviceType = 'device';
       setLoading(true);
       const resp = await service.saveProduct(formData);
       if (resp.status === 200) {

+ 76 - 16
src/pages/media/Device/index.tsx

@@ -6,17 +6,23 @@ import { Button, message, Popconfirm, Tooltip } from 'antd';
 import {
   DeleteOutlined,
   EditOutlined,
+  EyeOutlined,
+  PartitionOutlined,
   PlusOutlined,
   SyncOutlined,
-  PartitionOutlined,
 } from '@ant-design/icons';
 import type { DeviceItem } from '@/pages/media/Device/typings';
-import { useIntl, useHistory } from 'umi';
+import { useHistory, useIntl } from 'umi';
 import { BadgeStatus, ProTableCard } from '@/components';
 import { StatusColorEnum } from '@/components/BadgeStatus';
 import SearchComponent from '@/components/SearchComponent';
 import MediaDevice from '@/components/ProTableCard/CardItems/mediaDevice';
-import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
+import {
+  getButtonPermission,
+  getMenuPathByCode,
+  getMenuPathByParams,
+  MENUS_CODE,
+} from '@/utils/menu';
 import Service from './service';
 import Save from './Save';
 
@@ -68,6 +74,7 @@ const Device = () => {
   const updateChannel = async (id: string) => {
     const resp = await service.updateChannels(id);
     if (resp.status === 200) {
+      actionRef.current?.reload();
       message.success('通道更新成功');
     } else {
       message.error('通道更新失败');
@@ -97,8 +104,9 @@ const Device = () => {
       dataIndex: 'channelNumber',
       title: intl.formatMessage({
         id: 'pages.media.device.channelNumber',
-        defaultMessage: '通道数',
+        defaultMessage: '通道数',
       }),
+      valueType: 'digit',
     },
     {
       dataIndex: 'manufacturer',
@@ -155,17 +163,22 @@ const Device = () => {
             defaultMessage: '编辑',
           })}
         >
-          <a
+          <Button
+            disabled={getButtonPermission('media/Device', 'update')}
+            style={{ padding: 0 }}
+            type={'link'}
             onClick={() => {
               setCurrent(record);
               setVisible(true);
             }}
           >
             <EditOutlined />
-          </a>
+          </Button>
         </Tooltip>,
         <Tooltip key={'viewChannel'} title="查看通道">
-          <a
+          <Button
+            style={{ padding: 0 }}
+            type={'link'}
             onClick={() => {
               history.push(
                 `${getMenuPathByCode(MENUS_CODE['media/Device/Channel'])}?id=${record.id}&type=${
@@ -175,13 +188,37 @@ const Device = () => {
             }}
           >
             <PartitionOutlined />
-          </a>
+          </Button>
         </Tooltip>,
-        <Tooltip key={'updateChannel'} title="更新通道">
+        <Tooltip key={'deviceDetail'} title={'查看'}>
           <Button
-            style={{ padding: '4px' }}
+            style={{ padding: 0 }}
             type={'link'}
-            disabled={record.state.value === 'offline'}
+            onClick={() => {
+              history.push(
+                `${getMenuPathByParams(MENUS_CODE['device/Instance/Detail'], record.id)}`,
+              );
+            }}
+          >
+            <EyeOutlined />
+          </Button>
+        </Tooltip>,
+        <Tooltip
+          key={'updateChannel'}
+          title={
+            record.provider === providerType['fixed-media']
+              ? '接入方式为固定地址时不支持更新通道'
+              : '更新通道'
+          }
+        >
+          <Button
+            style={{ padding: 0 }}
+            type={'link'}
+            disabled={
+              getButtonPermission('media/Device', 'action') ||
+              record.state.value === 'offline' ||
+              record.provider === providerType['fixed-media']
+            }
             onClick={() => {
               updateChannel(record.id);
             }}
@@ -209,8 +246,10 @@ const Device = () => {
           >
             <Button
               type={'link'}
-              style={{ padding: '4px' }}
-              disabled={record.state.value !== 'offline'}
+              style={{ padding: 0 }}
+              disabled={
+                getButtonPermission('media/Device', 'delete') || record.state.value !== 'offline'
+              }
             >
               <DeleteOutlined />
             </Button>
@@ -222,7 +261,7 @@ const Device = () => {
 
   return (
     <PageContainer>
-      <SearchComponent field={columns} onSearch={searchFn} />
+      <SearchComponent field={columns} onSearch={searchFn} target="media-device" />
       <ProTableCard<DeviceItem>
         columns={columns}
         actionRef={actionRef}
@@ -250,6 +289,7 @@ const Device = () => {
             key="button"
             icon={<PlusOutlined />}
             type="primary"
+            disabled={getButtonPermission('media/Device', 'add')}
           >
             {intl.formatMessage({
               id: 'pages.data.option.add',
@@ -260,9 +300,22 @@ const Device = () => {
         cardRender={(record) => (
           <MediaDevice
             {...record}
+            detail={
+              <div
+                style={{ fontSize: 18, padding: 8 }}
+                onClick={() => {
+                  history.push(
+                    `${getMenuPathByParams(MENUS_CODE['device/Instance/Detail'], record.id)}`,
+                  );
+                }}
+              >
+                <EyeOutlined />
+              </div>
+            }
             actions={[
               <Button
                 key="edit"
+                disabled={getButtonPermission('media/Device', 'update')}
                 onClick={() => {
                   setCurrent(record);
                   setVisible(true);
@@ -291,7 +344,11 @@ const Device = () => {
               </Button>,
               <Button
                 key={'updateChannel'}
-                disabled={record.state.value === 'offline'}
+                disabled={
+                  getButtonPermission('media/Device', 'action') ||
+                  record.state.value === 'offline' ||
+                  record.provider === providerType['fixed-media']
+                }
                 onClick={() => {
                   updateChannel(record.id);
                 }}
@@ -319,7 +376,10 @@ const Device = () => {
                 <Button
                   type={'link'}
                   style={{ padding: 0 }}
-                  disabled={record.state.value !== 'offline'}
+                  disabled={
+                    getButtonPermission('media/Device', 'delete') ||
+                    record.state.value !== 'offline'
+                  }
                 >
                   <DeleteOutlined />
                 </Button>

+ 7 - 0
src/pages/media/Stream/Detail/index.tsx

@@ -18,6 +18,7 @@ import { useParams } from 'umi';
 import { QuestionCircleOutlined } from '@ant-design/icons';
 import SipComponent from '@/components/SipComponent';
 import { testIP } from '@/utils/util';
+
 interface RTPComponentProps {
   onChange?: (data: any) => void;
   value?: {
@@ -169,6 +170,9 @@ const Detail = () => {
   }, [params.id]);
 
   const checkSIP = (_: any, value: { host: string; port: number }) => {
+    if (!value) {
+      return Promise.resolve();
+    }
     if (!value || !value.host) {
       return Promise.reject(new Error('请输入API HOST'));
     } else if (value?.host && !testIP(value.host)) {
@@ -194,6 +198,9 @@ const Detail = () => {
       dynamicRtpPortRange: number[];
     },
   ) => {
+    if (!value) {
+      return Promise.resolve();
+    }
     if (!value || !value.rtpIp) {
       return Promise.reject(new Error('请输入RTP IP'));
     } else if (value?.rtpIp && !testIP(value.rtpIp)) {

+ 16 - 6
src/pages/system/Menu/Detail/buttons.tsx

@@ -8,6 +8,7 @@ import { DeleteOutlined, EditOutlined, PlusOutlined, SearchOutlined } from '@ant
 import type { MenuButtonInfo, MenuItem } from '@/pages/system/Menu/typing';
 import Permission from '@/pages/system/Menu/components/permission';
 import { useRequest } from '@@/plugin-request/request';
+import { getButtonPermission } from '@/utils/menu';
 
 type ButtonsProps = {
   data: MenuItem;
@@ -168,14 +169,17 @@ export default (props: ButtonsProps) => {
       align: 'center',
       width: 240,
       render: (_, record) => [
-        <a
+        <Button
           key="edit"
+          type={'link'}
+          style={{ padding: 0 }}
           onClick={() => {
             form.setFieldsValue(record);
             setId(record.id);
             setDisabled(false);
             setVisible(true);
           }}
+          disabled={getButtonPermission('system/Menu', 'update')}
         >
           <Tooltip
             title={intl.formatMessage({
@@ -185,9 +189,11 @@ export default (props: ButtonsProps) => {
           >
             <EditOutlined />
           </Tooltip>
-        </a>,
-        <a
+        </Button>,
+        <Button
           key="view"
+          type={'link'}
+          style={{ padding: 0 }}
           onClick={() => {
             form.setFieldsValue(record);
             setId(record.id);
@@ -203,7 +209,7 @@ export default (props: ButtonsProps) => {
           >
             <SearchOutlined />
           </Tooltip>
-        </a>,
+        </Button>,
         <Popconfirm
           key="unBindUser"
           title={intl.formatMessage({
@@ -220,9 +226,13 @@ export default (props: ButtonsProps) => {
               defaultMessage: '删除',
             })}
           >
-            <a key="delete">
+            <Button
+              disabled={getButtonPermission('system/Menu', 'delete')}
+              type={'link'}
+              style={{ padding: 0 }}
+            >
               <DeleteOutlined />
-            </a>
+            </Button>
           </Tooltip>
         </Popconfirm>,
       ],

+ 0 - 1
src/pages/system/Menu/index.tsx

@@ -137,7 +137,6 @@ export default observer(() => {
           onClick={() => {
             pageJump(record.id, record.parentId || '');
           }}
-          disabled={getButtonPermission('system/Menu', ['view', 'update'])}
         >
           <Tooltip
             title={intl.formatMessage({