فهرست منبع

feat(merge): merge sx

Lind 3 سال پیش
والد
کامیت
476e4ac539

+ 48 - 0
src/components/ProTableCard/CardItems/cascade.tsx

@@ -0,0 +1,48 @@
+import React from 'react';
+import type { CascadeItem } from '@/pages/media/Cascade/typings';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import { TableCard } from '@/components';
+import '@/style/common.less';
+import '../index.less';
+import { Badge } from 'antd';
+
+export interface CascadeCardProps extends CascadeItem {
+  detail?: React.ReactNode;
+  actions?: React.ReactNode[];
+  avatarSize?: number;
+}
+
+const defaultImage = require('/public/images/device-type-3-big.png');
+
+export default (props: CascadeCardProps) => {
+  return (
+    <TableCard
+      showMask={false}
+      actions={props.actions}
+      status={props.status.value}
+      statusText={props.status.text}
+      statusNames={{
+        enabled: StatusColorEnum.processing,
+        disabled: StatusColorEnum.error,
+      }}
+    >
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <img width={88} height={88} src={defaultImage} alt={''} />
+        </div>
+        <div className={'card-item-body'}>
+          <div className={'card-item-header'}>
+            <span className={'card-item-header-name ellipsis'}>{props.name}</span>
+          </div>
+          <div>通道数量: 5</div>
+          <div>
+            <Badge
+              status={props.onlineStatus?.value === 'offline' ? 'error' : 'success'}
+              text={`sip:${props.sipConfigs[0]?.sipId}@${props.sipConfigs[0]?.hostAndPort}`}
+            />
+          </div>
+        </div>
+      </div>
+    </TableCard>
+  );
+};

+ 1 - 0
src/components/ProTableCard/TableCard.tsx

@@ -25,6 +25,7 @@ function getAction(actions: React.ReactNode[]) {
           delete: item.key === 'delete',
           disabled: item.disabled,
         })}
+        key={item.key}
       >
         {item}
       </div>

+ 57 - 0
src/components/SipComponent/index.tsx

@@ -0,0 +1,57 @@
+import { Input, InputNumber } from 'antd';
+import { useEffect, useState } from 'react';
+
+interface SipComponentProps {
+  onChange?: (data: any) => void;
+  value?: {
+    host?: string;
+    port?: number;
+  };
+}
+
+const SipComponent = (props: SipComponentProps) => {
+  const { value, onChange } = props;
+  const [data, setData] = useState<{ host?: string; port?: number } | undefined>(value);
+
+  useEffect(() => {
+    setData(value);
+  }, [value]);
+
+  return (
+    <div style={{ display: 'flex', alignItems: 'center' }}>
+      <Input
+        onChange={(e) => {
+          if (onChange) {
+            const item = {
+              ...data,
+              host: e.target.value,
+            };
+            setData(item);
+            onChange(item);
+          }
+        }}
+        value={data?.host}
+        style={{ marginRight: 10 }}
+        placeholder="请输入"
+      />
+      <InputNumber
+        style={{ minWidth: 100 }}
+        value={data?.port}
+        min={1}
+        max={65535}
+        onChange={(e: number) => {
+          if (onChange) {
+            const item = {
+              ...data,
+              port: e,
+            };
+            setData(item);
+            onChange(item);
+          }
+        }}
+      />
+    </div>
+  );
+};
+
+export default SipComponent;

+ 15 - 0
src/components/TitleComponent/index.less

@@ -0,0 +1,15 @@
+@import '~antd/es/style/themes/default.less';
+
+.title {
+  width: 100%;
+  margin-bottom: 10px;
+  color: rgba(0, 0, 0, 0.8);
+  font-weight: 600;
+}
+
+.title::before {
+  margin-right: 10px;
+  color: @primary-color;
+  background-color: @primary-color;
+  content: '|';
+}

+ 11 - 0
src/components/TitleComponent/index.tsx

@@ -0,0 +1,11 @@
+import type { ReactNode } from 'react';
+import './index.less';
+
+interface TitleComponentProps {
+  data: ReactNode | string;
+}
+const TitleComponent = (props: TitleComponentProps) => {
+  return <div className="title">{props.data}</div>;
+};
+
+export default TitleComponent;

+ 2 - 1
src/pages/Log/System/index.tsx

@@ -65,9 +65,10 @@ const System = () => {
         id: 'pages.log.system.serviceName',
         defaultMessage: '服务名',
       }),
-      dataIndex: 'context.server',
+      dataIndex: 'server',
       width: 150,
       ellipsis: true,
+      render: (text, record) => record?.context?.server || '--',
     },
     {
       title: intl.formatMessage({

+ 14 - 5
src/pages/device/Product/Detail/Access/index.tsx

@@ -372,9 +372,11 @@ const Access = () => {
               <div className={styles.item}>
                 <div className={styles.title}>消息协议</div>
                 <div className={styles.context}>{access?.protocolDetail?.name || '--'}</div>
-                <div className={styles.context}>
-                  <ReactMarkdown>{config?.document || '--'}</ReactMarkdown>
-                </div>
+                {config?.document && (
+                  <div className={styles.context}>
+                    <ReactMarkdown>{config?.document || '--'}</ReactMarkdown>
+                  </div>
+                )}
               </div>
 
               <div className={styles.item}>
@@ -391,7 +393,7 @@ const Access = () => {
                         </div>
                       ),
                     )
-                  : '---'}
+                  : '暂无连接信息'}
               </div>
 
               <div className={styles.item}>
@@ -424,7 +426,14 @@ const Access = () => {
                   />
                 </div>
               ) : (
-                <Empty />
+                <Empty
+                  description={`暂无${
+                    access?.provider === 'mqtt-server-gateway' ||
+                    access?.provider === 'mqtt-client-gateway'
+                      ? 'topic'
+                      : 'URL信息'
+                  }`}
+                />
               )}
             </div>
           </Col>

+ 1 - 1
src/pages/link/AccessConfig/Detail/Access/index.tsx

@@ -629,7 +629,7 @@ const Access = (props: Props) => {
 
   return (
     <Card>
-      {props.data?.id && (
+      {!props.data?.id && (
         <Button
           type="link"
           onClick={() => {

+ 22 - 15
src/pages/link/AccessConfig/Detail/Media/index.less

@@ -51,24 +51,31 @@
   }
 }
 
-.view {
-  display: flex;
-  justify-content: space-between;
+.config {
+  padding: 10px 20px 20px 20px;
+  color: rgba(0, 0, 0, 0.8);
+  background: rgba(0, 0, 0, 0.04);
 
-  .info,
-  .config {
-    width: 48%;
+  .title {
+    width: 100%;
+    margin: 10px 0;
+    font-weight: 600;
+  }
 
-    .title {
-      width: 100%;
-      margin-bottom: 10px;
-      font-weight: 600;
-    }
+  .item {
+    margin-bottom: 10px;
 
-    .title::before {
-      margin-right: 10px;
-      background-color: #2810ff;
-      content: '|';
+    .context {
+      margin: 5px 0;
+      color: rgba(0, 0, 0, 0.8);
     }
   }
 }
+
+.alert {
+  height: 40px;
+  padding-left: 10px;
+  color: rgba(0, 0, 0, 0.55);
+  line-height: 40px;
+  background-color: #f6f6f6;
+}

+ 251 - 85
src/pages/link/AccessConfig/Detail/Media/index.tsx

@@ -1,4 +1,4 @@
-import { Alert, Button, Card, Form, Input, Steps } from 'antd';
+import { Button, Card, Col, Form, Input, message, Row, Steps } from 'antd';
 import { useEffect, useState } from 'react';
 import styles from './index.less';
 import {
@@ -14,15 +14,32 @@ import {
 } from '@formily/antd';
 import { createSchemaField } from '@formily/react';
 import type { ISchema } from '@formily/json-schema';
-import { createForm } from '@formily/core';
+import { createForm, registerValidateRules } from '@formily/core';
+import { service } from '@/pages/link/AccessConfig';
+import { useLocation } from 'umi';
+import SipComponent from '@/components/SipComponent';
+import TitleComponent from '@/components/TitleComponent';
+import { ExclamationCircleFilled } from '@ant-design/icons';
+import { testIP } from '@/utils/util';
 
+type LocationType = {
+  id?: string;
+};
 interface Props {
   change: () => void;
   data: any;
+  provider: any;
 }
 
 const Media = (props: Props) => {
   const [current, setCurrent] = useState<number>(0);
+  const [form] = Form.useForm();
+  const [configuration, setConfiguration] = useState<any>({});
+  const [clusters, setClusters] = useState<any[]>([]);
+
+  const location = useLocation<LocationType>();
+
+  const params = new URLSearchParams(location.search);
 
   const steps = [
     {
@@ -40,13 +57,36 @@ const Media = (props: Props) => {
         AInput,
         Select,
         Radio,
+        SipComponent,
         FormGrid,
         FormCollapse,
         ArrayCollapse,
       },
     });
 
-    const aform = createForm({});
+    const aform = createForm({
+      validateFirst: true,
+      initialValues: {
+        configuration: configuration,
+      },
+    });
+
+    registerValidateRules({
+      checkSIP(value: { host: string; port: number }) {
+        if (Number(value.port) < 1 || Number(value.port) > 65535) {
+          return {
+            type: 'error',
+            message: '端口请输入1~65535之间的正整数',
+          };
+        } else if (!testIP(value.host)) {
+          return {
+            type: 'error',
+            message: '请输入正确的IP地址',
+          };
+        }
+        return true;
+      },
+    });
 
     const clusterConfig: ISchema = {
       type: 'void',
@@ -57,20 +97,21 @@ const Media = (props: Props) => {
         columnGap: 48,
       },
       properties: {
-        serverId: {
+        clusterNodeId: {
           title: '节点名称',
-          'x-component': 'AInput',
+          'x-component': 'Select',
           'x-decorator': 'FormItem',
           'x-decorator-props': {
             gridSpan: 1,
             labelAlign: 'left',
             layout: 'vertical',
           },
+          enum: [...clusters],
         },
-        host: {
+        sip: {
           title: 'SIP 地址',
           'x-decorator': 'FormItem',
-          'x-component': 'AInput',
+          'x-component': 'SipComponent',
           'x-decorator-props': {
             gridSpan: 1,
             labelAlign: 'left',
@@ -78,9 +119,11 @@ const Media = (props: Props) => {
             tooltip: '绑定到服务器上的网卡地址,绑定到所有网卡:0.0.0.0',
           },
           required: true,
-          'x-validator': ['ipv4'],
+          'x-validator': {
+            checkSIP: true,
+          },
         },
-        publicHost: {
+        public: {
           title: '公网 Host',
           'x-decorator-props': {
             gridSpan: 1,
@@ -91,17 +134,10 @@ const Media = (props: Props) => {
           required: true,
           type: 'number',
           'x-decorator': 'FormItem',
-          'x-component': 'AInput',
-          'x-validator': [
-            {
-              max: 65535,
-              message: '请输入1-65535之间的整整数',
-            },
-            {
-              min: 1,
-              message: '请输入1-65535之间的整整数',
-            },
-          ],
+          'x-component': 'SipComponent',
+          'x-validator': {
+            checkSIP: true,
+          },
         },
       },
     };
@@ -118,26 +154,7 @@ const Media = (props: Props) => {
             columnGap: 48,
           },
           properties: {
-            name: {
-              title: '名称',
-              type: 'string',
-              'x-decorator': 'FormItem',
-              'x-component': 'AInput',
-              'x-decorator-props': {
-                gridSpan: 1,
-              },
-              'x-validator': [
-                {
-                  max: 64,
-                  message: '最多可输入64个字符',
-                },
-                {
-                  required: true,
-                  message: '请输入名称',
-                },
-              ],
-            },
-            domain: {
+            'configuration.domain': {
               title: 'SIP 域',
               type: 'string',
               'x-decorator': 'FormItem',
@@ -145,6 +162,9 @@ const Media = (props: Props) => {
               'x-decorator-props': {
                 gridSpan: 1,
               },
+              'x-component-props': {
+                placeholder: '请输入SIP 域',
+              },
               'x-validator': [
                 {
                   required: true,
@@ -152,13 +172,16 @@ const Media = (props: Props) => {
                 },
               ],
             },
-            sipId: {
+            'configuration.sipId': {
               title: 'SIP ID',
               type: 'string',
               'x-decorator': 'FormItem',
               'x-component': 'AInput',
+              'x-component-props': {
+                placeholder: '请输入SIP ID',
+              },
               'x-decorator-props': {
-                gridSpan: 2,
+                gridSpan: 1,
               },
               'x-validator': [
                 {
@@ -167,7 +190,7 @@ const Media = (props: Props) => {
                 },
               ],
             },
-            shareCluster: {
+            'configuration.shareCluster': {
               title: '集群',
               'x-decorator': 'FormItem',
               'x-component': 'Radio.Group',
@@ -183,7 +206,7 @@ const Media = (props: Props) => {
                   '共享配置:集群下所有节点共用同一配置\r\n' + '独立配置:集群下不同节点使用不同配置',
               },
             },
-            hostPort: {
+            'configuration.hostPort': {
               type: 'object',
               'x-decorator': 'FormItem',
               'x-reactions': [
@@ -207,33 +230,45 @@ const Media = (props: Props) => {
                     maxColumns: 2,
                     minColumns: 1,
                     columnGap: 48,
+                    style: {
+                      background: '#f6f6f6',
+                      padding: 10,
+                    },
                   },
                   properties: {
-                    host: {
+                    sip: {
                       title: 'SIP 地址',
-                      'x-component': 'AInput',
+                      'x-component': 'SipComponent',
                       'x-decorator': 'FormItem',
+                      required: true,
                       'x-decorator-props': {
                         gridSpan: 1,
                         labelAlign: 'left',
                         layout: 'vertical',
                       },
+                      'x-validator': {
+                        checkSIP: true,
+                      },
                     },
-                    publicHost: {
+                    public: {
                       title: '公网 Host',
-                      'x-component': 'AInput',
+                      'x-component': 'SipComponent',
                       'x-decorator': 'FormItem',
+                      required: true,
                       'x-decorator-props': {
                         gridSpan: 1,
                         labelAlign: 'left',
                         layout: 'vertical',
                       },
+                      'x-validator': {
+                        checkSIP: true,
+                      },
                     },
                   },
                 },
               },
             },
-            cluster: {
+            'configuration.cluster': {
               type: 'void',
               'x-decorator': 'FormItem',
               'x-decorator-props': {
@@ -288,13 +323,47 @@ const Media = (props: Props) => {
 
     return (
       <div>
-        <Alert message="配置设备信令参数" type="warning" showIcon />
+        <div className={styles.alert}>
+          <ExclamationCircleFilled style={{ marginRight: 10 }} />
+          配置设备信令参数
+        </div>
         <AForm form={aform} layout="vertical" style={{ padding: 30 }}>
           <SchemaField schema={schema} />
           <FormButtonGroup.Sticky>
             <FormButtonGroup.FormItem>
               <Button
-                onClick={() => {
+                onClick={async () => {
+                  const value = await aform.submit<any>();
+                  let param: any = {};
+                  if (value.configuration.shareCluster) {
+                    const hostPort = { ...value.configuration.hostPort };
+                    param = {
+                      ...value.configuration,
+                      hostPort: {
+                        host: hostPort.sip.host,
+                        port: hostPort.sip.port,
+                        publicHost: hostPort.public.host,
+                        publicPort: hostPort.public.port,
+                      },
+                    };
+                  } else {
+                    const cluster: any[] = [];
+                    value.configuration.cluster.forEach((item: any) => {
+                      cluster.push({
+                        clusterNodeId: item?.clusterNodeId || '',
+                        host: item?.sip?.host,
+                        port: item?.sip?.port,
+                        publicHost: item.public?.host,
+                        publicPort: item.public?.port,
+                        enabled: true,
+                      });
+                    });
+                    param = {
+                      ...value.configuration,
+                      cluster: cluster,
+                    };
+                  }
+                  setConfiguration(param);
                   setCurrent(1);
                 }}
               >
@@ -308,52 +377,149 @@ const Media = (props: Props) => {
   };
 
   const FinishRender = () => {
-    const [form] = Form.useForm();
     return (
-      <div className={styles.view}>
-        <div className={styles.info}>
-          <div className={styles.title}>基本信息</div>
-          <Form name="basic" layout="vertical" form={form}>
-            <Form.Item label="名称" name="name" rules={[{ required: true, message: '请输入名称' }]}>
-              <Input />
-            </Form.Item>
-            <Form.Item name="description" label="说明">
-              <Input.TextArea showCount maxLength={200} />
-            </Form.Item>
-          </Form>
-          <div className={styles.action}>
-            {props.data.id !== 'fixed-media' && (
+      <Row gutter={24}>
+        <Col span={12}>
+          <div className={styles.info}>
+            <TitleComponent data={'基本信息'} />
+            <Form name="basic" layout="vertical" form={form}>
+              <Form.Item
+                label="名称"
+                name="name"
+                rules={[{ required: true, message: '请输入名称' }]}
+              >
+                <Input />
+              </Form.Item>
+              <Form.Item name="description" label="说明">
+                <Input.TextArea showCount maxLength={200} />
+              </Form.Item>
+            </Form>
+            <div className={styles.action}>
+              {props?.provider?.id !== 'fixed-media' && (
+                <Button
+                  style={{ margin: '0 8px' }}
+                  onClick={() => {
+                    setCurrent(0);
+                  }}
+                >
+                  上一步
+                </Button>
+              )}
               <Button
-                style={{ margin: '0 8px' }}
-                onClick={() => {
-                  setCurrent(0);
+                type="primary"
+                onClick={async () => {
+                  const values = await form.validateFields();
+                  const param: any = {
+                    name: values.name,
+                    description: values.description,
+                  };
+                  if (props?.provider?.id === 'fixed-media') {
+                    param.provider = 'fixed-media';
+                    param.transport = 'URL';
+                    param.channel = 'fixed-media';
+                  } else {
+                    param.provider = 'gb28181-2016';
+                    param.transport = 'SIP';
+                    param.channel = 'gb28181';
+                    param.configuration = configuration;
+                  }
+                  let resp = undefined;
+                  if (!!params.get('id')) {
+                    resp = await service.update({ ...param, id: params.get('id') || '' });
+                  } else {
+                    resp = await service.save({ ...param });
+                  }
+                  if (resp.status === 200) {
+                    message.success('操作成功!');
+                    history.back();
+                  }
                 }}
               >
-                上一步
+                保存
               </Button>
-            )}
-            <Button type="primary" onClick={() => {}}>
-              保存
-            </Button>
+            </div>
           </div>
-        </div>
-        <div className={styles.config}>
-          <div className={styles.title}>接入方式</div>
-          <div>这里是接入方式说明</div>
-          <div className={styles.title}>消息协议</div>
-          <div>这里是接入方式说明</div>
-        </div>
-      </div>
+        </Col>
+        <Col span={12}>
+          <div className={styles.config}>
+            <div className={styles.title}>接入方式</div>
+            <div>{props.provider?.name}</div>
+            <div>{props.provider?.description}</div>
+            <div className={styles.title}>消息协议</div>
+            <div>{props.provider?.id === 'fixed-media' ? 'URL' : 'SIP'}</div>
+            <div>这里是消息协议说明</div>
+          </div>
+        </Col>
+      </Row>
     );
   };
 
   useEffect(() => {
-    console.log(props.data);
-  }, []);
+    if (props.data) {
+      form.setFieldsValue({
+        name: props.data?.name,
+        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 });
+        }
+      }
+    }
+  }, [props.data]);
+
+  useEffect(() => {
+    if (props?.provider?.id !== 'fixed-media') {
+      service.getClusters().then((resp: any) => {
+        if (resp.status === 200) {
+          const list = (resp?.result || []).map((item: any) => {
+            return {
+              label: item.id,
+              value: item.name,
+            };
+          });
+          setClusters(list);
+        }
+      });
+      setCurrent(0);
+    } else {
+      setCurrent(1);
+    }
+  }, [props?.provider?.id]);
 
   return (
     <Card>
-      {props.data?.id && (
+      {!props.data?.id && (
         <Button
           type="link"
           onClick={() => {
@@ -363,7 +529,7 @@ const Media = (props: Props) => {
           返回
         </Button>
       )}
-      {props.data.id === 'fixed-media' ? (
+      {props?.provider?.id === 'fixed-media' ? (
         FinishRender()
       ) : (
         <div className={styles.box}>

+ 12 - 20
src/pages/link/AccessConfig/Detail/Provider/index.tsx

@@ -1,9 +1,9 @@
 import { Button, Card, Col, Row } from 'antd';
-import { service } from '@/pages/link/AccessConfig';
 import { useEffect, useState } from 'react';
 import styles from './index.less';
 
 interface Props {
+  data: any[];
   change: (data: any, type: 'media' | 'network') => void;
 }
 
@@ -11,27 +11,19 @@ const Provider = (props: Props) => {
   const [dataSource, setDataSource] = useState<any[]>([]);
   const [mediaSource, setMediaSource] = useState<any[]>([]);
 
-  const handleSearch = () => {
-    service.getProviders().then((resp) => {
-      if (resp.status === 200) {
-        const media: any[] = [];
-        const data: any = [];
-        resp.result.map((item: any) => {
-          if (item.id === 'fixed-media' || item.id === 'gb28181-2016') {
-            media.push(item);
-          } else {
-            data.push(item);
-          }
-        });
-        setDataSource(data);
-        setMediaSource(media);
+  useEffect(() => {
+    const media: any[] = [];
+    const data: any = [];
+    (props?.data || []).map((item: any) => {
+      if (item.id === 'fixed-media' || item.id === 'gb28181-2016') {
+        media.push(item);
+      } else {
+        data.push(item);
       }
     });
-  };
-
-  useEffect(() => {
-    handleSearch();
-  }, []);
+    setDataSource(data);
+    setMediaSource(media);
+  }, [props.data]);
 
   return (
     <>

+ 58 - 16
src/pages/link/AccessConfig/Detail/index.tsx

@@ -1,9 +1,11 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
 import { useLocation } from 'umi';
 import Access from './Access';
 import Provider from './Provider';
 import Media from './Media';
+import { service } from '@/pages/link/AccessConfig';
+import { Spin } from 'antd';
 
 type LocationType = {
   id?: string;
@@ -11,9 +13,42 @@ type LocationType = {
 
 const Detail = () => {
   const location = useLocation<LocationType>();
-  const [visible, setVisible] = useState<boolean>(!new URLSearchParams(location.search).get('id'));
+  const [visible, setVisible] = useState<boolean>(false);
+  const [loading, setLoading] = useState<boolean>(true);
   const [data, setData] = useState<any>({});
-  const [type, setType] = useState<'media' | 'network'>('media');
+  const [provider, setProvider] = useState<any>({});
+  const [type, setType] = useState<'media' | 'network' | undefined>(undefined);
+
+  const [dataSource, setDataSource] = useState<any[]>([]);
+
+  useEffect(() => {
+    setLoading(true);
+    const id = new URLSearchParams(location.search).get('id') || undefined;
+    service.getProviders().then((resp) => {
+      if (resp.status === 200) {
+        setDataSource(resp.result);
+        if (new URLSearchParams(location.search).get('id')) {
+          setVisible(false);
+          service.detail(id || '').then((response) => {
+            setData(response.result);
+            const dt = resp.result.find((item: any) => item?.id === response.result?.provider);
+            setProvider(dt);
+            if (
+              response.result?.provider === 'fixed-media' ||
+              response.result?.provider === 'gb28181-2016'
+            ) {
+              setType('media');
+            } else {
+              setType('network');
+            }
+          });
+        } else {
+          setVisible(true);
+        }
+        setLoading(false);
+      }
+    });
+  }, []);
 
   const componentRender = () => {
     switch (type) {
@@ -30,28 +65,35 @@ const Detail = () => {
         return (
           <Media
             data={data}
+            provider={provider}
             change={() => {
               setVisible(true);
             }}
           />
         );
+      default:
+        return null;
     }
   };
 
   return (
-    <PageContainer>
-      {visible ? (
-        <Provider
-          change={(param: any, typings: 'media' | 'network') => {
-            setType(typings);
-            setData(param);
-            setVisible(false);
-          }}
-        />
-      ) : (
-        componentRender()
-      )}
-    </PageContainer>
+    <Spin spinning={loading}>
+      <PageContainer>
+        {visible ? (
+          <Provider
+            data={dataSource}
+            change={(param: any, typings: 'media' | 'network') => {
+              setType(typings);
+              setProvider(param);
+              setData({});
+              setVisible(false);
+            }}
+          />
+        ) : (
+          componentRender()
+        )}
+      </PageContainer>
+    </Spin>
   );
 };
 

+ 0 - 5
src/pages/link/AccessConfig/index.less

@@ -42,13 +42,11 @@
 .context {
   display: flex;
   width: 100%;
-
   .card {
     display: flex;
     flex-direction: column;
     width: 100%;
     margin-left: 10px;
-
     .header {
       .title {
         width: 90%;
@@ -58,7 +56,6 @@
         white-space: nowrap;
         text-overflow: ellipsis;
       }
-
       .desc {
         width: 100%;
         margin-top: 10px;
@@ -81,7 +78,6 @@
       .procotol {
         width: calc(50% - 20px);
         margin-right: 10px;
-
         .subTitle {
           width: 100%;
           overflow: hidden;
@@ -90,7 +86,6 @@
           white-space: nowrap;
           text-overflow: ellipsis;
         }
-
         p {
           width: 100%;
           overflow: hidden;

+ 8 - 6
src/pages/link/AccessConfig/index.tsx

@@ -95,7 +95,7 @@ const AccessConfig = () => {
         {dataSource?.data.length > 0 ? (
           <Row gutter={[16, 16]} style={{ marginTop: 10 }}>
             {(dataSource?.data || []).map((item: any) => (
-              <Col key={item.name} span={12}>
+              <Col key={item.id} span={12}>
                 <TableCard
                   showMask={false}
                   actions={[
@@ -155,6 +155,8 @@ const AccessConfig = () => {
                             if (resp.status === 200) {
                               message.success('操作成功!');
                               handleSearch(param);
+                            } else {
+                              message.error(resp.message);
                             }
                           });
                         }}
@@ -183,13 +185,13 @@ const AccessConfig = () => {
                       <div className={styles.container}>
                         <div className={styles.server}>
                           <div className={styles.subTitle}>{item?.channelInfo?.name || '--'}</div>
-                          <p>
-                            {item.channelInfo?.addresses.map((i: any) => (
-                              <div key={i.address}>
+                          <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} />
-                              </div>
+                              </p>
                             ))}
-                          </p>
+                          </div>
                         </div>
                         <div className={styles.procotol}>
                           <div className={styles.subTitle}>

+ 5 - 0
src/pages/link/AccessConfig/service.ts

@@ -35,6 +35,11 @@ class Service extends BaseService<AccessItem> {
     request(`/${SystemConst.API_BASE}/protocol/${id}/transport/${transport}`, {
       method: 'GET',
     });
+
+  public getClusters = () =>
+    request(`/${SystemConst.API_BASE}/network/resources/clusters`, {
+      method: 'GET',
+    });
 }
 
 export default Service;

+ 4 - 4
src/pages/media/Stream/Detail/index.tsx

@@ -225,7 +225,7 @@ const Detail = () => {
   }, [params.id]);
 
   const checkAPI = (_: any, value: { apiHost: string; apiPort: number }) => {
-    if (Number(value.apiPort) < 1 && Number(value.apiPort) > 65535) {
+    if (Number(value.apiPort) < 1 || Number(value.apiPort) > 65535) {
       return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
     }
     if (!re.test(value.apiHost)) {
@@ -249,7 +249,7 @@ const Detail = () => {
       if (value.dynamicRtpPortRange) {
         if (value.dynamicRtpPortRange?.[0]) {
           if (
-            Number(value.dynamicRtpPortRange?.[0]) < 1 &&
+            Number(value.dynamicRtpPortRange?.[0]) < 1 ||
             Number(value.dynamicRtpPortRange?.[0]) > 65535
           ) {
             return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
@@ -257,7 +257,7 @@ const Detail = () => {
         }
         if (value.dynamicRtpPortRange?.[1]) {
           if (
-            Number(value.dynamicRtpPortRange?.[1]) < 1 &&
+            Number(value.dynamicRtpPortRange?.[1]) < 1 ||
             Number(value.dynamicRtpPortRange?.[1]) > 65535
           ) {
             return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
@@ -272,7 +272,7 @@ const Detail = () => {
         }
       }
     } else {
-      if (Number(value.rtpPort) < 1 && Number(value.rtpPort) > 65535) {
+      if (Number(value.rtpPort) < 1 || Number(value.rtpPort) > 65535) {
         return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
       }
     }

+ 25 - 15
src/pages/media/Stream/index.tsx

@@ -29,11 +29,6 @@ const Stream = () => {
       title: '名称',
       ellipsis: true,
     },
-    {
-      dataIndex: 'description',
-      title: '说明',
-      ellipsis: true,
-    },
   ];
 
   const [dataSource, setDataSource] = useState<any>({
@@ -78,17 +73,18 @@ const Stream = () => {
         }}
       />
       <Card>
-        <Button
-          type="primary"
-          onClick={() => {
-            history.push(`${getMenuPathByParams(MENUS_CODE['media/Stream/Detail'])}`);
-            StreamModel.current = {};
-          }}
-        >
-          新增
-        </Button>
         {dataSource.data.length > 0 ? (
           <>
+            <Button
+              type="primary"
+              onClick={() => {
+                history.push(`${getMenuPathByParams(MENUS_CODE['media/Stream/Detail'])}`);
+                StreamModel.current = {};
+              }}
+            >
+              新增
+            </Button>
+
             <Row gutter={[16, 16]} style={{ marginTop: 10 }}>
               {(dataSource?.data || []).map((item: any) => (
                 <Col key={item.id} span={12}>
@@ -176,7 +172,21 @@ const Stream = () => {
             </div>
           </>
         ) : (
-          <Empty />
+          <Empty
+            description={
+              <span>
+                暂无数据,请先
+                <a
+                  onClick={() => {
+                    history.push(`${getMenuPathByParams(MENUS_CODE['media/Stream/Detail'])}`);
+                    StreamModel.current = {};
+                  }}
+                >
+                  新增流媒体服务
+                </a>
+              </span>
+            }
+          />
         )}
       </Card>
     </PageContainer>

+ 6 - 16
src/pages/rule-engine/Instance/index.tsx

@@ -88,7 +88,7 @@ const Instance = () => {
       <Tooltip
         title={intl.formatMessage({
           id: `pages.data.option.${record.state.value !== 'stopped' ? 'disabled' : 'enabled'}`,
-          defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
+          defaultMessage: record.state.value !== 'stopped' ? '禁用' : '启用',
         })}
       >
         <Button type={'link'} style={{ padding: 0 }}>
@@ -97,15 +97,10 @@ const Instance = () => {
       </Tooltip>
     </Popconfirm>,
     <Popconfirm
-      title={intl.formatMessage({
-        id:
-          record.state.value === 'notActive'
-            ? 'pages.data.option.remove.tips'
-            : 'pages.device.instance.deleteTip',
-      })}
+      title={record.state.value === 'stopped' ? '确认删除' : '未停止不能删除'}
       key={'delete'}
       onConfirm={async () => {
-        if (record.state.value === 'notActive') {
+        if (record.state.value === 'stopped') {
           await service.remove(record.id);
           message.success(
             intl.formatMessage({
@@ -115,7 +110,7 @@ const Instance = () => {
           );
           actionRef.current?.reload();
         } else {
-          message.error(intl.formatMessage({ id: 'pages.device.instance.deleteTip' }));
+          message.error('未停止不能删除');
         }
       }}
     >
@@ -234,7 +229,7 @@ const Instance = () => {
           <Tooltip
             title={intl.formatMessage({
               id: `pages.data.option.${record.state.value !== 'stopped' ? 'disabled' : 'enabled'}`,
-              defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
+              defaultMessage: record.state.value !== 'stopped' ? '禁用' : '启用',
             })}
           >
             <Button type={'link'} style={{ padding: 0 }}>
@@ -243,12 +238,7 @@ const Instance = () => {
           </Tooltip>
         </Popconfirm>,
         <Popconfirm
-          title={intl.formatMessage({
-            id:
-              record.state.value === 'stopped'
-                ? 'pages.data.option.remove.tips'
-                : 'pages.device.instance.deleteTip',
-          })}
+          title={record.state.value === 'stopped' ? '确认删除' : '未停止不能删除'}
           key={'delete'}
           onConfirm={async () => {
             if (record.state.value === 'stopped') {

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

@@ -16,6 +16,12 @@ const extraRouteObj = {
       { code: 'Template', name: '通知模板' },
     ],
   },
+  'media/Cascade': {
+    children: [
+      { code: 'Save', name: '新增' },
+      { code: 'Channel', name: '选择通道' },
+    ],
+  },
 };
 
 /**

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

@@ -41,6 +41,8 @@ export const MENUS_CODE = {
   'link/AccessConfig': 'link/AccessConfig',
   Log: 'Log',
   'media/Cascade': 'media/Cascade',
+  'media/Cascade/Save': 'media/Cascade/Save',
+  'media/Cascade/Channel': 'media/Cascade/Channel',
   'media/Config': 'media/Config',
   'media/Device': 'media/Device',
   'media/Reveal': 'media/Reveal',

+ 8 - 0
src/utils/util.ts

@@ -80,3 +80,11 @@ export const flattenArray: any = (arr: any[]) => {
     return result.concat(item, Array.isArray(item.children) ? flattenArray(item.children) : []);
   }, []);
 };
+/**
+ * 判断是否为正确的IP地址
+ */
+export const testIP = (str: string) => {
+  const re =
+    /^([0-9]|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.([0-9]|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.([0-9]|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.([0-9]|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])$/;
+  return re.test(str);
+};