xieyonghong 3 лет назад
Родитель
Сommit
69486c0f25

+ 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;

+ 6 - 6
src/pages/device/Instance/Detail/Diagnose/Message/index.tsx

@@ -1,7 +1,7 @@
 import TitleComponent from '@/components/TitleComponent';
 import './index.less';
 import Dialog from './Dialog';
-import { Button, Col, DatePicker, Empty, Input, InputNumber, Row, Select } from 'antd';
+import { Button, Col, Input, InputNumber, Row, Select, DatePicker, Empty } from 'antd';
 import { useEffect, useState } from 'react';
 import { InstanceModel, service } from '@/pages/device/Instance';
 import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
@@ -11,20 +11,21 @@ import { createForm, onFieldValueChange } from '@formily/core';
 import { createSchemaField, FormProvider } from '@formily/react';
 import {
   ArrayTable,
-  DatePicker as FDatePicker,
   FormItem,
   Input as FInput,
   PreviewText,
   Select as FSelect,
+  DatePicker as FDatePicker,
   Switch,
 } from '@formily/antd';
 import { randomString } from '@/utils/util';
 import Log from './Log';
-
 interface Props {
   onChange: (type: string) => void;
 }
 
+const DatePicker1: any = DatePicker;
+
 const Message = (props: Props) => {
   const [subscribeTopic] = useSendWebsocketMessage();
   const [dialogList, setDialogList] = useState<any[]>([]);
@@ -100,11 +101,10 @@ const Message = (props: Props) => {
           />
         );
       case 'date':
-        // @ts-ignore
         return (
-          <DatePicker
+          <DatePicker1
             style={{ width: '100%' }}
-            onChange={(value) => {
+            onChange={(value: any) => {
               setPropertyValue(value);
             }}
           />

+ 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) => {

+ 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 = {

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

@@ -50,6 +50,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 +435,7 @@ const Media = (props: Props) => {
                 <Button
                   style={{ margin: '0 8px' }}
                   onClick={() => {
+                    initConfig(configuration);
                     setCurrent(0);
                   }}
                 >
@@ -461,39 +498,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}

+ 63 - 7
src/pages/media/Cascade/Channel/index.tsx

@@ -1,9 +1,9 @@
 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 { Button, Input, message, Popconfirm, Popover, Space, Tooltip } from 'antd';
 import { useRef, useState } from 'react';
 import ProTable from '@jetlinks/pro-table';
 import { useIntl, useLocation } from 'umi';
@@ -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;

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

@@ -169,6 +169,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 +197,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)) {