Explorar o código

feat(merge): merge sc

feat: 国标级联
Lind %!s(int64=3) %!d(string=hai) anos
pai
achega
78df17e826

+ 1 - 1
src/components/ProTableCard/CardItems/cascade.tsx

@@ -34,7 +34,7 @@ export default (props: CascadeCardProps) => {
           <div className={'card-item-header'}>
           <div className={'card-item-header'}>
             <span className={'card-item-header-name ellipsis'}>{props.name}</span>
             <span className={'card-item-header-name ellipsis'}>{props.name}</span>
           </div>
           </div>
-          <div>通道数量: 5</div>
+          <div>通道数量: {props?.count || 0}</div>
           <div>
           <div>
             <Badge
             <Badge
               status={props.onlineStatus?.value === 'offline' ? 'error' : 'success'}
               status={props.onlineStatus?.value === 'offline' ? 'error' : 'success'}

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

@@ -57,7 +57,7 @@ const System = () => {
         id: 'pages.log.system.logContent',
         id: 'pages.log.system.logContent',
         defaultMessage: '日志内容',
         defaultMessage: '日志内容',
       }),
       }),
-      dataIndex: 'exceptionStack',
+      dataIndex: 'message',
       ellipsis: true,
       ellipsis: true,
     },
     },
     {
     {

+ 1 - 1
src/pages/device/Instance/Detail/Running/Property/index.tsx

@@ -125,7 +125,7 @@ const Property = (props: Props) => {
       ?.pipe(map((res) => res.payload))
       ?.pipe(map((res) => res.payload))
       .subscribe((payload: any) => {
       .subscribe((payload: any) => {
         const { value } = payload;
         const { value } = payload;
-        propertyValue[value.property] = value;
+        propertyValue[value.property] = { ...payload, ...value };
         setPropertyValue({ ...propertyValue });
         setPropertyValue({ ...propertyValue });
       });
       });
   };
   };

+ 54 - 67
src/pages/device/Instance/Detail/index.tsx

@@ -2,7 +2,7 @@ import { PageContainer } from '@ant-design/pro-layout';
 import { InstanceModel, service } from '@/pages/device/Instance';
 import { InstanceModel, service } from '@/pages/device/Instance';
 import { history, useParams } from 'umi';
 import { history, useParams } from 'umi';
 import { Badge, Button, Card, Descriptions, Divider, message, Tooltip } from 'antd';
 import { Badge, Button, Card, Descriptions, Divider, message, Tooltip } from 'antd';
-import { useEffect, useState } from 'react';
+import { ReactNode, useEffect, useState } from 'react';
 import { observer } from '@formily/react';
 import { observer } from '@formily/react';
 import Log from '@/pages/device/Instance/Detail/Log';
 import Log from '@/pages/device/Instance/Detail/Log';
 // import Alarm from '@/pages/device/components/Alarm';
 // import Alarm from '@/pages/device/components/Alarm';
@@ -27,47 +27,8 @@ deviceStatus.set('notActive', <Badge status="processing" text={'未启用'} />);
 const InstanceDetail = observer(() => {
 const InstanceDetail = observer(() => {
   const intl = useIntl();
   const intl = useIntl();
   const [tab, setTab] = useState<string>('detail');
   const [tab, setTab] = useState<string>('detail');
-  const getDetail = (id: string) => {
-    service.detail(id).then((response) => {
-      InstanceModel.detail = response?.result;
-      // 写入物模型数据
-      const metadata: DeviceMetadata = JSON.parse(response.result?.metadata || '{}');
-      MetadataAction.insert(metadata);
-    });
-  };
   const params = useParams<{ id: string }>();
   const params = useParams<{ id: string }>();
 
 
-  const [subscribeTopic] = useSendWebsocketMessage();
-
-  useEffect(() => {
-    if (subscribeTopic) {
-      subscribeTopic(
-        `instance-editor-info-status-${params.id}`,
-        `/dashboard/device/status/change/realTime`,
-        {
-          deviceId: params.id,
-        },
-        // @ts-ignore
-      ).subscribe((data: any) => {
-        const payload = data.payload;
-        const state = payload.value.type;
-        InstanceModel.detail.state = {
-          value: state,
-          text: '',
-        };
-      });
-    }
-  }, []);
-
-  useEffect(() => {
-    Store.subscribe(SystemConst.REFRESH_DEVICE, () => {
-      MetadataAction.clean();
-      setTimeout(() => {
-        getDetail(params.id);
-      }, 200);
-    });
-    // return subscription.unsubscribe();
-  }, []);
   const resetMetadata = async () => {
   const resetMetadata = async () => {
     const resp = await service.deleteMetadata(params.id);
     const resp = await service.deleteMetadata(params.id);
     if (resp.status === 200) {
     if (resp.status === 200) {
@@ -78,7 +39,7 @@ const InstanceDetail = observer(() => {
       }, 400);
       }, 400);
     }
     }
   };
   };
-  const list = [
+  const baseList = [
     {
     {
       key: 'detail',
       key: 'detail',
       tab: intl.formatMessage({
       tab: intl.formatMessage({
@@ -131,33 +92,59 @@ const InstanceDetail = observer(() => {
       }),
       }),
       component: <Log />,
       component: <Log />,
     },
     },
-    // 产品类型为网关的情况下才显示此模块
-    {
-      key: 'child-device',
-      tab: '子设备',
-      component: <ChildDevice />,
-    },
-    // {
-    //   key: 'alarm',
-    //   tab: intl.formatMessage({
-    //     id: 'pages.device.instanceDetail.alarm',
-    //     defaultMessage: '告警设置',
-    //   }),
-    //   component: (
-    //     <Card>
-    //       <Alarm type="device" />
-    //     </Card>
-    //   ),
-    // },
-    // {
-    //   key: 'visualization',
-    //   tab: intl.formatMessage({
-    //     id: 'pages.device.instanceDetail.visualization',
-    //     defaultMessage: '可视化',
-    //   }),
-    //   component: <div>开发中...</div>,
-    // },
   ];
   ];
+  const [list, setList] = useState<{ key: string; tab: string; component: ReactNode }[]>(baseList);
+
+  const getDetail = (id: string) => {
+    service.detail(id).then((response) => {
+      InstanceModel.detail = response?.result;
+      const datalist = [...baseList];
+      if (response.result.deviceType?.value === 'gateway') {
+        // 产品类型为网关的情况下才显示此模块
+        datalist.push({
+          key: 'child-device',
+          tab: '子设备',
+          component: <ChildDevice />,
+        });
+      }
+      setList(datalist);
+      // 写入物模型数据
+      const metadata: DeviceMetadata = JSON.parse(response.result?.metadata || '{}');
+      MetadataAction.insert(metadata);
+    });
+  };
+
+  const [subscribeTopic] = useSendWebsocketMessage();
+
+  useEffect(() => {
+    if (subscribeTopic) {
+      subscribeTopic(
+        `instance-editor-info-status-${params.id}`,
+        `/dashboard/device/status/change/realTime`,
+        {
+          deviceId: params.id,
+        },
+        // @ts-ignore
+      ).subscribe((data: any) => {
+        const payload = data.payload;
+        const state = payload.value.type;
+        InstanceModel.detail.state = {
+          value: state,
+          text: '',
+        };
+      });
+    }
+  }, []);
+
+  useEffect(() => {
+    Store.subscribe(SystemConst.REFRESH_DEVICE, () => {
+      MetadataAction.clean();
+      setTimeout(() => {
+        getDetail(params.id);
+      }, 200);
+    });
+    // return subscription.unsubscribe();
+  }, []);
 
 
   useEffect(() => {
   useEffect(() => {
     if (!InstanceModel.current && !params.id) {
     if (!InstanceModel.current && !params.id) {

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

@@ -28,7 +28,7 @@ const AccessConfig = (props: Props) => {
   });
   });
   const [param, setParam] = useState<any>({ pageSize: 4 });
   const [param, setParam] = useState<any>({ pageSize: 4 });
 
 
-  const [currrent] = useState<any>({
+  const [currrent, setCurrrent] = useState<any>({
     id: productModel.current?.accessId,
     id: productModel.current?.accessId,
     name: productModel.current?.accessName,
     name: productModel.current?.accessName,
     protocol: productModel.current?.messageProtocol,
     protocol: productModel.current?.messageProtocol,
@@ -147,11 +147,11 @@ const AccessConfig = (props: Props) => {
             span={12}
             span={12}
             // style={{
             // style={{
             //   width: '100%',
             //   width: '100%',
-            //   borderColor: currrent?.id === item.id ? 'var(--ant-primary-color-active)' : ''
-            // }}
-            // onClick={() => {
-            //   setCurrrent(item);
+            //   borderColor: currrent?.id === item.id ? 'var(--ant-primary-color-active)' : 'red',
             // }}
             // }}
+            onClick={() => {
+              setCurrrent(item);
+            }}
           >
           >
             <TableCard
             <TableCard
               showMask={false}
               showMask={false}
@@ -174,13 +174,13 @@ const AccessConfig = (props: Props) => {
                   <div className={styles.container}>
                   <div className={styles.container}>
                     <div className={styles.server}>
                     <div className={styles.server}>
                       <div className={styles.subTitle}>{item?.channelInfo?.name || '--'}</div>
                       <div className={styles.subTitle}>{item?.channelInfo?.name || '--'}</div>
-                      <p>
+                      <div style={{ width: '100%' }}>
                         {item.channelInfo?.addresses.map((i: any) => (
                         {item.channelInfo?.addresses.map((i: any) => (
-                          <div key={i.address}>
+                          <p key={i.address}>
                             <Badge color={i.health === -1 ? 'red' : 'green'} text={i.address} />
                             <Badge color={i.health === -1 ? 'red' : 'green'} text={i.address} />
-                          </div>
+                          </p>
                         ))}
                         ))}
-                      </p>
+                      </div>
                     </div>
                     </div>
                     <div className={styles.procotol}>
                     <div className={styles.procotol}>
                       <div className={styles.subTitle}>{item?.protocolDetail?.name || '--'}</div>
                       <div className={styles.subTitle}>{item?.protocolDetail?.name || '--'}</div>

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

@@ -33,6 +33,8 @@ const Access = () => {
   MetworkTypeMapping.set('mqtt-client-gateway', 'MQTT_CLIENT');
   MetworkTypeMapping.set('mqtt-client-gateway', 'MQTT_CLIENT');
   MetworkTypeMapping.set('mqtt-server-gateway', 'MQTT_SERVER');
   MetworkTypeMapping.set('mqtt-server-gateway', 'MQTT_SERVER');
   MetworkTypeMapping.set('tcp-server-gateway', 'TCP_SERVER');
   MetworkTypeMapping.set('tcp-server-gateway', 'TCP_SERVER');
+  MetworkTypeMapping.set('fixed-media', 'TCP_CLIENT');
+  MetworkTypeMapping.set('gb28181-2016', 'UDP');
 
 
   const [configVisible, setConfigVisible] = useState<boolean>(false);
   const [configVisible, setConfigVisible] = useState<boolean>(false);
 
 

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

@@ -530,7 +530,7 @@ const Media = (props: Props) => {
         </Button>
         </Button>
       )}
       )}
       {props?.provider?.id === 'fixed-media' ? (
       {props?.provider?.id === 'fixed-media' ? (
-        FinishRender()
+        <div style={{ margin: '20px 30px' }}>{FinishRender()}</div>
       ) : (
       ) : (
         <div className={styles.box}>
         <div className={styles.box}>
           <div className={styles.steps}>
           <div className={styles.steps}>

+ 0 - 2
src/pages/link/Protocol/index.tsx

@@ -267,7 +267,6 @@ const Protocol = () => {
                   dependencies: ['..type'],
                   dependencies: ['..type'],
                   fulfill: {
                   fulfill: {
                     state: {
                     state: {
-                      value: '',
                       visible: '{{["jar","local"].includes($deps[0])}}',
                       visible: '{{["jar","local"].includes($deps[0])}}',
                       componentType: '{{$deps[0]==="jar"?"FileUpload":"Input"}}',
                       componentType: '{{$deps[0]==="jar"?"FileUpload":"Input"}}',
                       componentProps: '{{$deps[0]==="jar"?{type:"file", accept: ".jar, .zip"}:{}}}',
                       componentProps: '{{$deps[0]==="jar"?{type:"file", accept: ".jar, .zip"}:{}}}',
@@ -354,7 +353,6 @@ const Protocol = () => {
           </>
           </>
         }
         }
       />
       />
-      {/* {visible && <Debug data={current} close={() => setVisible(!visible)} />} */}
     </PageContainer>
     </PageContainer>
   );
   );
 };
 };

+ 158 - 0
src/pages/media/Cascade/Channel/BindChannel/index.tsx

@@ -0,0 +1,158 @@
+import SearchComponent from '@/components/SearchComponent';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
+import { message, Modal, Space } from 'antd';
+import { useRef, useState } from 'react';
+import { service } from '@/pages/media/Cascade';
+import { useIntl } from 'umi';
+import BadgeStatus, { StatusColorEnum } from '@/components/BadgeStatus';
+
+interface Props {
+  data: string;
+  close: () => void;
+}
+
+const BindChannel = (props: Props) => {
+  const [param, setParam] = useState<any>({
+    pageIndex: 0,
+    pageSize: 10,
+    terms: [
+      {
+        column: 'id',
+        termType: 'cascade_channel$not',
+        value: props.data,
+        type: 'and',
+      },
+      {
+        column: 'catalogType',
+        termType: 'eq',
+        value: 'device',
+        type: 'and',
+      },
+    ],
+    sorts: [
+      {
+        name: 'name',
+        order: 'asc',
+      },
+    ],
+  });
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+  const [selectedRowKey, setSelectedRowKey] = useState<string[]>([]);
+
+  const columns: ProColumns<any>[] = [
+    {
+      dataIndex: 'deviceName',
+      title: '设备名称',
+    },
+    {
+      dataIndex: 'name',
+      title: '通道名称',
+    },
+    {
+      dataIndex: 'address',
+      title: '安装地址',
+    },
+    {
+      dataIndex: 'manufacturer',
+      title: '厂商',
+    },
+    {
+      dataIndex: 'status',
+      title: '在线状态',
+      render: (text: any, record: any) => (
+        <BadgeStatus
+          status={record.status?.value}
+          text={record.status?.text}
+          statusNames={{
+            online: StatusColorEnum.success,
+            offline: StatusColorEnum.error,
+          }}
+        />
+      ),
+    },
+  ];
+
+  return (
+    <Modal
+      title={'绑定通道'}
+      visible
+      onCancel={props.close}
+      onOk={async () => {
+        const resp = await service.bindChannel(props.data, selectedRowKey);
+        if (resp.status === 200) {
+          message.success('操作成功!');
+          props.close();
+        }
+      }}
+      width={1200}
+    >
+      <SearchComponent<any>
+        field={columns}
+        target="bind-channel"
+        enableSave={false}
+        onSearch={(data) => {
+          actionRef.current?.reload();
+          const terms = [
+            {
+              column: 'id',
+              termType: 'cascade_channel$not',
+              value: props.data,
+              type: 'and',
+            },
+            {
+              column: 'catalogType',
+              termType: 'eq',
+              value: 'device',
+              type: 'and',
+            },
+          ];
+          setParam({
+            ...param,
+            terms: data?.terms ? [...data?.terms, ...terms] : [...terms],
+          });
+        }}
+      />
+      <ProTable<any>
+        actionRef={actionRef}
+        params={param}
+        columns={columns}
+        search={false}
+        headerTitle={'通道列表'}
+        request={async (params) =>
+          service.queryChannel({ ...params, sorts: [{ name: 'name', order: 'desc' }] })
+        }
+        rowKey="id"
+        rowSelection={{
+          selectedRowKeys: selectedRowKey,
+          onChange: (keys) => {
+            setSelectedRowKey(keys as string[]);
+          },
+        }}
+        tableAlertRender={({ selectedRowKeys, onCleanSelected }) => (
+          <Space size={24}>
+            <span>
+              {intl.formatMessage({
+                id: 'pages.bindUser.bindTheNewUser.selected',
+                defaultMessage: '已选',
+              })}{' '}
+              {selectedRowKeys?.length}{' '}
+              {intl.formatMessage({
+                id: 'pages.bindUser.bindTheNewUser.item',
+                defaultMessage: '项',
+              })}
+              <a style={{ marginLeft: 8 }} onClick={onCleanSelected}>
+                {intl.formatMessage({
+                  id: 'pages.bindUser.bindTheNewUser.deselect',
+                  defaultMessage: '取消选择',
+                })}
+              </a>
+            </span>
+          </Space>
+        )}
+      />
+    </Modal>
+  );
+};
+export default BindChannel;

+ 169 - 0
src/pages/media/Cascade/Channel/index.tsx

@@ -0,0 +1,169 @@
+import { service } from '@/pages/media/Cascade';
+import SearchComponent from '@/components/SearchComponent';
+import { DisconnectOutlined } 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 { useIntl, useLocation } from 'umi';
+import BindChannel from './BindChannel';
+import BadgeStatus, { StatusColorEnum } from '@/components/BadgeStatus';
+
+const Channel = () => {
+  const location: any = useLocation();
+  const actionRef = useRef<ActionType>();
+  const [param, setParam] = useState({});
+  const intl = useIntl();
+  const [visible, setVisible] = useState<boolean>(false);
+  const [selectedRowKey, setSelectedRowKey] = useState<string[]>([]);
+  const id = location?.query?.id || '';
+
+  const unbind = async (data: string[]) => {
+    const resp = await service.unbindChannel(id, data);
+    if (resp.status === 200) {
+      actionRef.current?.reload();
+      message.success('操作成功!');
+    }
+  };
+
+  const columns: ProColumns<any>[] = [
+    {
+      dataIndex: 'deviceName',
+      title: '设备名称',
+    },
+    {
+      dataIndex: 'name',
+      title: '通道名称',
+    },
+    {
+      dataIndex: 'channelId',
+      title: '国标ID',
+    },
+    {
+      dataIndex: 'address',
+      title: '安装地址',
+    },
+    {
+      dataIndex: 'manufacturer',
+      title: '厂商',
+    },
+    {
+      dataIndex: 'status',
+      title: '在线状态',
+      render: (text: any, record: any) => (
+        <BadgeStatus
+          status={record.status?.value}
+          text={record.status?.text}
+          statusNames={{
+            online: StatusColorEnum.success,
+            offline: StatusColorEnum.error,
+          }}
+        />
+      ),
+    },
+    {
+      title: '操作',
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text: any, record: any) => [
+        <Popconfirm
+          key={'unbinds'}
+          title="确认解绑"
+          onConfirm={() => {
+            unbind([record.id]);
+          }}
+        >
+          <a>
+            <Tooltip title={'解绑'}>
+              <DisconnectOutlined />
+            </Tooltip>
+          </a>
+        </Popconfirm>,
+      ],
+    },
+  ];
+
+  return (
+    <PageContainer>
+      <SearchComponent<any>
+        field={columns}
+        target="unbind-channel"
+        onSearch={(data) => {
+          actionRef.current?.reload();
+          setParam({
+            ...param,
+            terms: data?.terms ? [...data?.terms] : [],
+          });
+        }}
+      />
+      <ProTable<any>
+        actionRef={actionRef}
+        params={param}
+        columns={columns}
+        search={false}
+        headerTitle={'通道列表'}
+        request={async (params) =>
+          service.queryBindChannel(id, {
+            ...params,
+            sorts: [{ name: 'createTime', order: 'desc' }],
+          })
+        }
+        rowKey="id"
+        rowSelection={{
+          selectedRowKeys: selectedRowKey,
+          onChange: (selectedRowKeys) => {
+            setSelectedRowKey(selectedRowKeys as string[]);
+          },
+        }}
+        tableAlertRender={({ selectedRowKeys, onCleanSelected }) => (
+          <Space size={24}>
+            <span>
+              {intl.formatMessage({
+                id: 'pages.bindUser.bindTheNewUser.selected',
+                defaultMessage: '已选',
+              })}{' '}
+              {selectedRowKeys.length}{' '}
+              {intl.formatMessage({
+                id: 'pages.bindUser.bindTheNewUser.item',
+                defaultMessage: '项',
+              })}
+              <a style={{ marginLeft: 8 }} onClick={onCleanSelected}>
+                {intl.formatMessage({
+                  id: 'pages.bindUser.bindTheNewUser.deselect',
+                  defaultMessage: '取消选择',
+                })}
+              </a>
+            </span>
+          </Space>
+        )}
+        toolBarRender={() => [
+          <Button
+            onClick={() => {
+              setVisible(true);
+            }}
+            key="bind"
+            type="primary"
+          >
+            绑定通道
+          </Button>,
+          <Button onClick={() => {}} key="unbind">
+            批量解绑
+          </Button>,
+        ]}
+      />
+      {visible && (
+        <BindChannel
+          data={id}
+          close={() => {
+            setVisible(false);
+            actionRef.current?.reload();
+          }}
+        />
+      )}
+    </PageContainer>
+  );
+};
+
+export default Channel;

+ 84 - 0
src/pages/media/Cascade/Publish/index.tsx

@@ -0,0 +1,84 @@
+import SystemConst from '@/utils/const';
+import Token from '@/utils/token';
+import { downloadObject } from '@/utils/util';
+import { Col, Input, Modal, Row } from 'antd';
+import { EventSourcePolyfill } from 'event-source-polyfill';
+import { useEffect, useState } from 'react';
+
+interface Props {
+  data: any;
+  close: () => void;
+}
+
+const Publish = (props: Props) => {
+  const activeAPI = `/${SystemConst.API_BASE}/media/gb28181-cascade/${
+    props.data.id
+  }/bindings/publish?:X_Access_Token=${Token.get()}`;
+  const [count, setCount] = useState<number>(0);
+  const [countErr, setCountErr] = useState<number>(0);
+  const [flag, setFlag] = useState<boolean>(true);
+  const [errMessage, setErrMessage] = useState<string>('');
+
+  const getData = () => {
+    let dt = 0;
+    const source = new EventSourcePolyfill(activeAPI);
+    source.onmessage = (e: any) => {
+      const res = JSON.parse(e.data);
+      if (res.success) {
+        const temp = res.result.total;
+        dt += temp;
+        setCount(dt);
+        // setCountErr(0);
+      } else {
+        setCountErr(0);
+        setErrMessage(res.message);
+      }
+    };
+    source.onerror = () => {
+      setFlag(false);
+      source.close();
+    };
+    source.onopen = () => {};
+  };
+
+  useEffect(() => {
+    getData();
+  }, []);
+
+  return (
+    <Modal
+      title={'推送'}
+      maskClosable={false}
+      visible
+      onCancel={props.close}
+      onOk={props.close}
+      width={900}
+    >
+      <Row gutter={24} style={{ marginBottom: 20 }}>
+        <Col span={8}>
+          <div>成功: {count}</div>
+          <div>
+            失败: {countErr}
+            <a
+              style={{ marginLeft: 20 }}
+              onClick={() => {
+                downloadObject(JSON.parse(errMessage || '{}'), props.data.name + '-推送失败');
+              }}
+            >
+              下载
+            </a>
+          </div>
+        </Col>
+        <Col span={8}>推送通道数量: {props.data?.count || 0}</Col>
+        <Col span={8}>已推送通道数量: {countErr + count}</Col>
+      </Row>
+      {flag && (
+        <div>
+          <Input.TextArea rows={10} value={errMessage} />
+        </div>
+      )}
+    </Modal>
+  );
+};
+
+export default Publish;

+ 303 - 0
src/pages/media/Cascade/Save/index.tsx

@@ -0,0 +1,303 @@
+import TitleComponent from '@/components/TitleComponent';
+import { QuestionCircleOutlined } from '@ant-design/icons';
+import { PageContainer } from '@ant-design/pro-layout';
+import {
+  Button,
+  Card,
+  Col,
+  Form,
+  Input,
+  InputNumber,
+  message,
+  Radio,
+  Row,
+  Select,
+  Tooltip,
+} from 'antd';
+import SipComponent from '@/components/SipComponent';
+import { testIP } from '@/utils/util';
+import { useEffect, useState } from 'react';
+import { service } from '../index';
+import { useLocation } from 'umi';
+
+const Save = () => {
+  const location: any = useLocation();
+  const [form] = Form.useForm();
+  const [clusters, setClusters] = useState<any[]>([]);
+  const id = location?.query?.id || '';
+
+  const checkSIP = (_: any, value: { host: string; port: number }) => {
+    if (!value) {
+      return Promise.reject(new Error('请输入SIP'));
+    } else if (Number(value.port) < 1 || Number(value.port) > 65535) {
+      return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
+    } else if (!testIP(value.host)) {
+      return Promise.reject(new Error('请输入正确的IP地址'));
+    }
+    return Promise.resolve();
+  };
+
+  useEffect(() => {
+    service.queryClusters().then((resp) => {
+      if (resp.status === 200) {
+        setClusters(resp.result);
+      }
+    });
+    if (!!id) {
+      service.detail(id).then((resp) => {
+        if (resp.status === 200) {
+          const sipConfigs = resp.result?.sipConfigs[0];
+          const data = {
+            ...resp.result,
+            sipConfigs: {
+              ...sipConfigs,
+              public: {
+                host: sipConfigs.remoteAddress,
+                port: sipConfigs.remotePort,
+              },
+              local: {
+                host: sipConfigs.host,
+                port: sipConfigs.port,
+              },
+            },
+          };
+          form.setFieldsValue(data);
+        }
+      });
+    }
+  }, []);
+
+  return (
+    <PageContainer>
+      <Card>
+        <Form
+          name="cascade"
+          layout="vertical"
+          form={form}
+          initialValues={{
+            proxyStream: false,
+            sipConfigs: {
+              transport: 'UDP',
+            },
+          }}
+          onFinish={async (values: any) => {
+            const sipConfigs = {
+              ...values.sipConfigs,
+              remoteAddress: values.sipConfigs.public.host,
+              remotePort: values.sipConfigs.public.port,
+              host: values.sipConfigs.local.host,
+              port: values.sipConfigs.local.port,
+            };
+            delete values.sipConfigs;
+            delete sipConfigs.public;
+            delete sipConfigs.local;
+            const param = { ...values, sipConfigs: [sipConfigs] };
+            let resp = undefined;
+            if (id) {
+              resp = await service.update({ ...param, id });
+            } else {
+              resp = await service.save(param);
+            }
+            if (resp && resp.status === 200) {
+              message.success('操作成功!');
+              history.back();
+            }
+          }}
+        >
+          <Row gutter={24}>
+            <TitleComponent data={'基本信息'} />
+            <Col span={12}>
+              <Form.Item
+                label="名称"
+                name="name"
+                rules={[{ required: true, message: '请输入名称' }]}
+              >
+                <Input placeholder="请输入名称" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label={<span>代理视频流</span>}
+                name="proxyStream"
+                rules={[{ required: true, message: '请选择代理视频流' }]}
+              >
+                <Radio.Group optionType="button" buttonStyle="solid">
+                  <Radio.Button value={true}>启用</Radio.Button>
+                  <Radio.Button value={false}>禁用</Radio.Button>
+                </Radio.Group>
+              </Form.Item>
+            </Col>
+            <TitleComponent data={'信令服务配置'} />
+            <Col span={12}>
+              <Form.Item
+                label={
+                  <span>
+                    集群节点
+                    <Tooltip title="使用此集群节点级联到上级平台">
+                      <QuestionCircleOutlined />
+                    </Tooltip>
+                  </span>
+                }
+                name={['sipConfigs', 'clusterNodeId']}
+                rules={[{ required: true, message: '请选择信令服务配置' }]}
+              >
+                <Select placeholder="请选择信令服务配置">
+                  {clusters.map((item) => (
+                    <Select.Option key={item.id} value={item.id}>
+                      {item.name}
+                    </Select.Option>
+                  ))}
+                </Select>
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="信令名称"
+                name={['sipConfigs', 'name']}
+                rules={[{ required: true, message: '请输入信令名称' }]}
+              >
+                <Input placeholder="请输入信令名称" />
+              </Form.Item>
+            </Col>
+            <Col span={24}>
+              <Form.Item
+                label="上级SIP ID"
+                name={['sipConfigs', 'sipId']}
+                rules={[{ required: true, message: '请输入上级SIP ID' }]}
+              >
+                <Input placeholder="请输入上级SIP ID" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="上级SIP域"
+                name={['sipConfigs', 'domain']}
+                rules={[{ required: true, message: '请输入上级SIP域' }]}
+              >
+                <Input placeholder="请输入上级SIP域" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="上级SIP 地址"
+                name={['sipConfigs', 'public']}
+                rules={[{ required: true, message: '请输入上级SIP 地址' }, { validator: checkSIP }]}
+              >
+                <SipComponent />
+              </Form.Item>
+            </Col>
+            <Col span={24}>
+              <Form.Item
+                label="本地SIP ID"
+                name={['sipConfigs', 'localSipId']}
+                rules={[{ required: true, message: '请输入本地SIP ID' }]}
+              >
+                <Input placeholder="请输入本地SIP ID" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="传输协议"
+                name={['sipConfigs', 'transport']}
+                rules={[{ required: true, message: '请选择传输协议' }]}
+              >
+                <Radio.Group optionType="button" buttonStyle="solid">
+                  <Radio.Button value="UDP">UDP</Radio.Button>
+                  <Radio.Button value="TCP">TCP</Radio.Button>
+                </Radio.Group>
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label={
+                  <span>
+                    SIP本地地址
+                    <Tooltip title="使用指定的网卡和端口进行请求">
+                      <QuestionCircleOutlined />
+                    </Tooltip>
+                  </span>
+                }
+                name={['sipConfigs', 'local']}
+                rules={[{ required: true, message: '请输入SIP本地地址' }, { validator: checkSIP }]}
+              >
+                <SipComponent />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="用户"
+                name={['sipConfigs', 'user']}
+                rules={[{ required: true, message: '请输入用户' }]}
+              >
+                <Input placeholder="请输入用户" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="接入密码"
+                name={['sipConfigs', 'password']}
+                rules={[{ required: true, message: '请输入接入密码' }]}
+              >
+                <Input.Password placeholder="请输入接入密码" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="厂商"
+                name={['sipConfigs', 'manufacturer']}
+                rules={[{ required: true, message: '请输入厂商' }]}
+              >
+                <Input placeholder="请输入厂商" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="型号"
+                name={['sipConfigs', 'model']}
+                rules={[{ required: true, message: '请输入型号' }]}
+              >
+                <Input placeholder="请输入型号" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="版本号"
+                name={['sipConfigs', 'firmware']}
+                rules={[{ required: true, message: '请输入版本号' }]}
+              >
+                <Input placeholder="请输入版本号" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="心跳周期(秒)"
+                name={['sipConfigs', 'keepaliveInterval']}
+                rules={[{ required: true, message: '请输入心跳周期' }]}
+              >
+                <InputNumber placeholder="请输入心跳周期" style={{ width: '100%' }} />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="注册间隔(秒)"
+                name={['sipConfigs', 'registerInterval']}
+                rules={[{ required: true, message: '请输入注册间隔' }]}
+              >
+                <InputNumber placeholder="请输入注册间隔" style={{ width: '100%' }} />
+              </Form.Item>
+            </Col>
+            <Col span={24}>
+              <Form.Item>
+                <Button type="primary" htmlType="submit">
+                  保存
+                </Button>
+              </Form.Item>
+            </Col>
+          </Row>
+        </Form>
+      </Card>
+    </PageContainer>
+  );
+};
+
+export default Save;

+ 297 - 56
src/pages/media/Cascade/index.tsx

@@ -1,25 +1,130 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import { PageContainer } from '@ant-design/pro-layout';
-import BaseService from '@/utils/BaseService';
-import { useRef } from 'react';
+import { useRef, useState } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
-import { Tooltip } from 'antd';
-import { ArrowDownOutlined, BugOutlined, EditOutlined, MinusOutlined } from '@ant-design/icons';
-import BaseCrud from '@/components/BaseCrud';
+import { Badge, Button, message, Popconfirm, Tooltip } from 'antd';
+import {
+  CheckCircleOutlined,
+  DeleteOutlined,
+  EditOutlined,
+  LinkOutlined,
+  PlusOutlined,
+  ShareAltOutlined,
+  StopOutlined,
+} from '@ant-design/icons';
 import type { CascadeItem } from '@/pages/media/Cascade/typings';
 import type { CascadeItem } from '@/pages/media/Cascade/typings';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { useIntl } from '@@/plugin-locale/localeExports';
+import SearchComponent from '@/components/SearchComponent';
+import { ProTableCard } from '@/components';
+import CascadeCard from '@/components//ProTableCard/CardItems/cascade';
+import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
+import { useHistory } from 'umi';
+import Service from './service';
+import Publish from './Publish';
+import { lastValueFrom } from 'rxjs';
+
+export const service = new Service('media/gb28181-cascade');
 
 
-export const service = new BaseService<CascadeItem>('media/gb28181-cascade');
 const Cascade = () => {
 const Cascade = () => {
   const intl = useIntl();
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
   const actionRef = useRef<ActionType>();
+  const [searchParams, setSearchParams] = useState<any>({});
+  const history = useHistory<Record<string, string>>();
+  const [visible, setVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<Partial<CascadeItem>>();
+
+  const tools = (record: CascadeItem) => [
+    <Tooltip
+      title={intl.formatMessage({
+        id: 'pages.data.option.edit',
+        defaultMessage: '编辑',
+      })}
+      key={'edit'}
+    >
+      <Button
+        type={'link'}
+        style={{ padding: 0 }}
+        onClick={() => {
+          const url = getMenuPathByCode(MENUS_CODE[`media/Cascade/Save`]);
+          history.push(url + `?id=${record.id}`);
+        }}
+      >
+        <EditOutlined />
+      </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}`);
+        }}
+      >
+        <LinkOutlined />
+      </Button>
+    </Tooltip>,
+    <Popconfirm
+      key={'share'}
+      title="确认共享!"
+      onConfirm={() => {
+        setCurrent(record);
+        setVisible(true);
+      }}
+    >
+      <Tooltip title={'共享'}>
+        <Button type={'link'}>
+          <ShareAltOutlined />
+        </Button>
+      </Tooltip>
+    </Popconfirm>,
+    <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?.();
+        }
+      }}
+    >
+      <Button type={'link'} style={{ padding: 0 }}>
+        <Tooltip title={record.status.value === 'disabled' ? '启用' : '禁用'}>
+          {record.status.value === 'disabled' ? <CheckCircleOutlined /> : <StopOutlined />}
+        </Tooltip>
+      </Button>
+    </Popconfirm>,
+    <Popconfirm
+      title={'确认删除'}
+      key={'delete'}
+      onConfirm={async () => {
+        const resp: any = await service.remove(record.id);
+        if (resp.status === 200) {
+          message.success('操作成功!');
+          actionRef.current?.reset?.();
+        }
+      }}
+    >
+      <Tooltip
+        title={intl.formatMessage({
+          id: 'pages.data.option.remove',
+          defaultMessage: '删除',
+        })}
+      >
+        <Button type={'link'} style={{ padding: 0 }}>
+          <DeleteOutlined />
+        </Button>
+      </Tooltip>
+    </Popconfirm>,
+  ];
 
 
   const columns: ProColumns<CascadeItem>[] = [
   const columns: ProColumns<CascadeItem>[] = [
     {
     {
-      dataIndex: 'index',
-      valueType: 'indexBorder',
-      width: 48,
-    },
-    {
       dataIndex: 'name',
       dataIndex: 'name',
       title: intl.formatMessage({
       title: intl.formatMessage({
         id: 'pages.table.name',
         id: 'pages.table.name',
@@ -27,19 +132,64 @@ const Cascade = () => {
       }),
       }),
     },
     },
     {
     {
-      dataIndex: 'networkType',
-      title: intl.formatMessage({
-        id: 'pages.table.type',
-        defaultMessage: '类型',
-      }),
+      dataIndex: 'sipConfigs[0].sipId',
+      title: '上级SIP ID',
+      render: (text: any, record: any) => record.sipConfigs[0].sipId,
     },
     },
     {
     {
-      dataIndex: 'state',
+      dataIndex: 'sipConfigs[0].publicHost',
+      title: '上级SIP 地址',
+      render: (text: any, record: any) => record.sipConfigs[0].publicHost,
+    },
+    {
+      dataIndex: 'count',
+      title: '通道数量',
+      hideInSearch: true,
+    },
+    {
+      dataIndex: 'status',
       title: intl.formatMessage({
       title: intl.formatMessage({
         id: 'pages.searchTable.titleStatus',
         id: 'pages.searchTable.titleStatus',
         defaultMessage: '状态',
         defaultMessage: '状态',
       }),
       }),
-      render: (text, record) => record.status.value,
+      render: (text: any, record: any) => (
+        <Badge
+          status={record.status?.value === 'disabled' ? 'error' : 'success'}
+          text={record.status?.text}
+        />
+      ),
+      valueType: 'select',
+      valueEnum: {
+        disabled: {
+          text: '已停止',
+          status: 'disabled',
+        },
+        enabled: {
+          text: '已启动',
+          status: 'enabled',
+        },
+      },
+    },
+    {
+      dataIndex: 'onlineStatus',
+      title: '级联状态',
+      render: (text: any, record: any) => (
+        <Badge
+          status={record.onlineStatus?.value === 'offline' ? 'error' : 'success'}
+          text={record.onlineStatus?.text}
+        />
+      ),
+      valueType: 'select',
+      valueEnum: {
+        online: {
+          text: '在线',
+          status: 'online',
+        },
+        offline: {
+          text: '离线',
+          status: 'offline',
+        },
+      },
     },
     },
     {
     {
       title: intl.formatMessage({
       title: intl.formatMessage({
@@ -50,7 +200,13 @@ const Cascade = () => {
       align: 'center',
       align: 'center',
       width: 200,
       width: 200,
       render: (text, record) => [
       render: (text, record) => [
-        <a onClick={() => console.log(record)}>
+        <a
+          key={'edit'}
+          onClick={() => {
+            const url = getMenuPathByCode(MENUS_CODE[`media/Cascade/Save`]);
+            history.push(url + `?id=${record.id}`);
+          }}
+        >
           <Tooltip
           <Tooltip
             title={intl.formatMessage({
             title={intl.formatMessage({
               id: 'pages.data.option.edit',
               id: 'pages.data.option.edit',
@@ -60,54 +216,139 @@ const Cascade = () => {
             <EditOutlined />
             <EditOutlined />
           </Tooltip>
           </Tooltip>
         </a>,
         </a>,
-        <a>
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.data.option.remove',
-              defaultMessage: '删除',
-            })}
-          >
-            <MinusOutlined />
-          </Tooltip>
-        </a>,
-        <a>
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.data.option.download',
-              defaultMessage: '下载配置',
-            })}
-          >
-            <ArrowDownOutlined />
-          </Tooltip>
-        </a>,
-        <a>
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.notice.option.debug',
-              defaultMessage: '调试',
-            })}
-          >
-            <BugOutlined />
+        <a
+          key={'channel'}
+          onClick={() => {
+            const url = getMenuPathByCode(MENUS_CODE[`media/Cascade/Channel`]);
+            history.push(url + `?id=${record.id}`);
+          }}
+        >
+          <Tooltip title={'选择通道'}>
+            <LinkOutlined />
           </Tooltip>
           </Tooltip>
         </a>,
         </a>,
+        <Popconfirm
+          key={'share'}
+          onConfirm={() => {
+            setVisible(true);
+            setCurrent(record);
+          }}
+          title={'确认共享'}
+        >
+          <a>
+            <Tooltip title={'共享'}>
+              <ShareAltOutlined />
+            </Tooltip>
+          </a>
+        </Popconfirm>,
+        <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?.();
+            }
+          }}
+        >
+          <a>
+            <Tooltip title={record.status.value === 'disabled' ? '启用' : '禁用'}>
+              {record.status.value === 'disabled' ? <CheckCircleOutlined /> : <StopOutlined />}
+            </Tooltip>
+          </a>
+        </Popconfirm>,
+        <Popconfirm
+          title={'确认删除'}
+          key={'delete'}
+          onConfirm={async () => {
+            const resp: any = await service.remove(record.id);
+            if (resp.status === 200) {
+              message.success('操作成功!');
+              actionRef.current?.reset?.();
+            }
+          }}
+        >
+          <a>
+            <Tooltip
+              title={intl.formatMessage({
+                id: 'pages.data.option.remove',
+                defaultMessage: '删除',
+              })}
+            >
+              <DeleteOutlined />
+            </Tooltip>
+          </a>
+        </Popconfirm>,
       ],
       ],
     },
     },
   ];
   ];
 
 
-  const schema = {};
-
   return (
   return (
     <PageContainer>
     <PageContainer>
-      <BaseCrud
+      <SearchComponent<CascadeItem>
+        field={columns}
+        target="media-cascade"
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setSearchParams(data);
+        }}
+      />
+      <ProTableCard<CascadeItem>
         columns={columns}
         columns={columns}
-        service={service}
-        title={intl.formatMessage({
-          id: 'pages.media.cascade',
-          defaultMessage: '模拟测试',
-        })}
-        schema={schema}
         actionRef={actionRef}
         actionRef={actionRef}
+        params={searchParams}
+        options={{ fullScreen: true }}
+        request={async (params = {}) => {
+          return await lastValueFrom(
+            service.queryZipCount({
+              ...params,
+              sorts: [
+                {
+                  name: 'createTime',
+                  order: 'desc',
+                },
+              ],
+            }),
+          );
+        }}
+        rowKey="id"
+        search={false}
+        pagination={{ pageSize: 10 }}
+        headerTitle={[
+          <Button
+            onClick={() => {
+              const url = getMenuPathByCode(MENUS_CODE[`media/Cascade/Save`]);
+              history.push(url);
+            }}
+            style={{ marginRight: 12 }}
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.add',
+              defaultMessage: '新增',
+            })}
+          </Button>,
+        ]}
+        gridColumn={2}
+        cardRender={(record) => <CascadeCard {...record} actions={tools(record)} />}
       />
       />
+      {visible && (
+        <Publish
+          data={current}
+          close={() => {
+            setVisible(false);
+          }}
+        />
+      )}
     </PageContainer>
     </PageContainer>
   );
   );
 };
 };

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

@@ -0,0 +1,66 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+import type { CascadeItem } from './typings';
+import { concatMap, from, toArray } from 'rxjs';
+import { map } from 'rxjs/operators';
+import type { PageResult, Response } from '@/utils/typings';
+import _ from 'lodash';
+
+class Service extends BaseService<CascadeItem> {
+  queryBindChannel = (id: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/media/gb28181-cascade/${id}/bindings/_query`, {
+      method: 'POST',
+      data,
+    });
+
+  queryZipCount = (params: any) =>
+    from(this.query(params)).pipe(
+      concatMap((i: Response<CascadeItem>) =>
+        from((i.result as PageResult)?.data).pipe(
+          concatMap((t: CascadeItem) =>
+            from(this.queryBindChannel(t.id, {})).pipe(
+              map((count: any) => ({ ...t, count: count.result?.total || 0 })),
+            ),
+          ),
+          toArray(),
+          map((data) => _.set(i, 'result.data', data) as any),
+        ),
+      ),
+    );
+
+  queryClusters = () =>
+    request(`/${SystemConst.API_BASE}/network/resources/clusters`, {
+      method: 'GET',
+    });
+
+  enabled = (id: string) =>
+    request(`/${SystemConst.API_BASE}/media/gb28181-cascade/${id}/_enabled`, {
+      method: 'POST',
+    });
+
+  disabled = (id: string) =>
+    request(`/${SystemConst.API_BASE}/media/gb28181-cascade/${id}/_disabled`, {
+      method: 'POST',
+    });
+
+  queryChannel = (data: any) =>
+    request(`/${SystemConst.API_BASE}/media/channel/_query`, {
+      method: 'POST',
+      data,
+    });
+
+  bindChannel = (id: string, data: string[]) =>
+    request(`/${SystemConst.API_BASE}/media/gb28181-cascade/${id}/_bind`, {
+      method: 'POST',
+      data,
+    });
+
+  unbindChannel = (id: string, data: string[]) =>
+    request(`/${SystemConst.API_BASE}/media/gb28181-cascade/${id}/_unbind`, {
+      method: 'POST',
+      data,
+    });
+}
+
+export default Service;

+ 1 - 0
src/pages/media/Cascade/typings.d.ts

@@ -29,4 +29,5 @@ type CascadeItem = {
   proxyStream: boolean;
   proxyStream: boolean;
   sipConfigs: Partial<SipConfig>[];
   sipConfigs: Partial<SipConfig>[];
   status: State;
   status: State;
+  count?: number;
 } & BaseItem;
 } & BaseItem;

+ 15 - 69
src/pages/media/Stream/Detail/index.tsx

@@ -16,64 +16,8 @@ import { useEffect, useState } from 'react';
 import { service, StreamModel } from '@/pages/media/Stream';
 import { service, StreamModel } from '@/pages/media/Stream';
 import { useParams } from 'umi';
 import { useParams } from 'umi';
 import { QuestionCircleOutlined } from '@ant-design/icons';
 import { QuestionCircleOutlined } from '@ant-design/icons';
-
-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])$/;
-
-// API host
-interface APIComponentProps {
-  onChange?: (data: any) => void;
-  value?: {
-    apiHost?: string;
-    apiPort?: number;
-  };
-}
-
-const APIComponent = (props: APIComponentProps) => {
-  const { value, onChange } = props;
-  const [data, setData] = useState<{ apiHost?: string; apiPort?: number } | undefined>(value);
-
-  useEffect(() => {
-    setData(value);
-  }, [value]);
-
-  return (
-    <div style={{ display: 'flex', alignItems: 'center' }}>
-      <Input
-        onChange={(e) => {
-          if (onChange) {
-            const item = {
-              ...data,
-              apiHost: e.target.value,
-            };
-            setData(item);
-            onChange(item);
-          }
-        }}
-        value={data?.apiHost}
-        style={{ marginRight: 10 }}
-        placeholder="请输入API Host"
-      />
-      <InputNumber
-        style={{ minWidth: 150 }}
-        value={data?.apiPort}
-        min={1}
-        max={65535}
-        onChange={(e: number) => {
-          if (onChange) {
-            const item = {
-              ...data,
-              apiPort: e,
-            };
-            setData(item);
-            onChange(item);
-          }
-        }}
-      />
-    </div>
-  );
-};
-
+import SipComponent from '@/components/SipComponent';
+import { testIP } from '@/utils/util';
 interface RTPComponentProps {
 interface RTPComponentProps {
   onChange?: (data: any) => void;
   onChange?: (data: any) => void;
   value?: {
   value?: {
@@ -224,11 +168,12 @@ const Detail = () => {
     }
     }
   }, [params.id]);
   }, [params.id]);
 
 
-  const checkAPI = (_: any, value: { apiHost: string; apiPort: number }) => {
-    if (Number(value.apiPort) < 1 || Number(value.apiPort) > 65535) {
+  const checkSIP = (_: any, value: { host: string; port: number }) => {
+    if (!value || !value.host) {
+      return Promise.reject(new Error('请输入API HOST'));
+    } else if ((value?.port && Number(value.port) < 1) || Number(value.port) > 65535) {
       return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
       return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
-    }
-    if (!re.test(value.apiHost)) {
+    } else if (value?.host && !testIP(value.host)) {
       return Promise.reject(new Error('请输入正确的IP地址'));
       return Promise.reject(new Error('请输入正确的IP地址'));
     }
     }
     return Promise.resolve();
     return Promise.resolve();
@@ -242,10 +187,11 @@ const Detail = () => {
       dynamicRtpPortRange: number[];
       dynamicRtpPortRange: number[];
     },
     },
   ) => {
   ) => {
-    if (!re.test(value.rtpIp)) {
+    if (!value || !value.rtpIp) {
+      return Promise.reject(new Error('请输入RTP IP'));
+    } else if (value.rtpIp && !testIP(value.rtpIp)) {
       return Promise.reject(new Error('请输入正确的IP地址'));
       return Promise.reject(new Error('请输入正确的IP地址'));
-    }
-    if (value.dynamicRtpPort) {
+    } else if (value.dynamicRtpPort) {
       if (value.dynamicRtpPortRange) {
       if (value.dynamicRtpPortRange) {
         if (value.dynamicRtpPortRange?.[0]) {
         if (value.dynamicRtpPortRange?.[0]) {
           if (
           if (
@@ -272,7 +218,7 @@ const Detail = () => {
         }
         }
       }
       }
     } else {
     } else {
-      if (Number(value.rtpPort) < 1 || Number(value.rtpPort) > 65535) {
+      if ((value.rtpPort && Number(value.rtpPort) < 1) || Number(value.rtpPort) > 65535) {
         return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
         return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
       }
       }
     }
     }
@@ -370,9 +316,9 @@ const Detail = () => {
                   </span>
                   </span>
                 }
                 }
                 name="api"
                 name="api"
-                rules={[{ required: true, message: '请输入API Host' }, { validator: checkAPI }]}
+                rules={[{ validator: checkSIP }]}
               >
               >
-                <APIComponent />
+                <SipComponent />
               </Form.Item>
               </Form.Item>
             </Col>
             </Col>
             <Col span={24}>
             <Col span={24}>
@@ -389,7 +335,7 @@ const Detail = () => {
                   </span>
                   </span>
                 }
                 }
                 name="rtp"
                 name="rtp"
-                rules={[{ required: true, message: '请输入RTP IP' }, { validator: checkRIP }]}
+                rules={[{ validator: checkRIP }]}
               >
               >
                 <RTPComponent />
                 <RTPComponent />
               </Form.Item>
               </Form.Item>

+ 13 - 4
src/pages/rule-engine/Instance/index.tsx

@@ -11,13 +11,14 @@ import {
   PlusOutlined,
   PlusOutlined,
   StopOutlined,
   StopOutlined,
 } from '@ant-design/icons';
 } from '@ant-design/icons';
-import { Badge, Button, message, Popconfirm, Tooltip } from 'antd';
+import { Button, message, Popconfirm, Tooltip } from 'antd';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import SearchComponent from '@/components/SearchComponent';
 import SearchComponent from '@/components/SearchComponent';
-import { ProTableCard } from '@/components';
+import { BadgeStatus, ProTableCard } from '@/components';
 import RuleInstanceCard from '@/components/ProTableCard/CardItems/ruleInstance';
 import RuleInstanceCard from '@/components/ProTableCard/CardItems/ruleInstance';
 import Save from '@/pages/rule-engine/Instance/Save';
 import Save from '@/pages/rule-engine/Instance/Save';
 import SystemConst from '@/utils/const';
 import SystemConst from '@/utils/const';
+import { StatusColorEnum } from '@/components/BadgeStatus';
 
 
 export const service = new Service('rule-engine/instance');
 export const service = new Service('rule-engine/instance');
 
 
@@ -139,8 +140,16 @@ const Instance = () => {
     {
     {
       dataIndex: 'state',
       dataIndex: 'state',
       title: '状态',
       title: '状态',
-      render: (text: any) => (
-        <Badge color={text?.value === 'stopped' ? 'red' : 'green'} text={text?.text} />
+      render: (text: any, record: any) => (
+        <BadgeStatus
+          status={record.state?.value}
+          text={record.state?.text}
+          statusNames={{
+            started: StatusColorEnum.success,
+            stopped: StatusColorEnum.error,
+            disable: StatusColorEnum.processing,
+          }}
+        />
       ),
       ),
       valueType: 'select',
       valueType: 'select',
       valueEnum: {
       valueEnum: {

+ 1 - 0
src/pages/system/Role/Detail/Permission/index.tsx

@@ -76,6 +76,7 @@ const Permission = () => {
             .subscribe((resp) => {
             .subscribe((resp) => {
               if (resp.status === 200) {
               if (resp.status === 200) {
                 message.success('操作成功');
                 message.success('操作成功');
+                history.goBack();
               }
               }
             });
             });
         }}
         }}