Bladeren bron

feat: merge

xieyonghong 4 jaren geleden
bovenliggende
commit
63cac266fd
31 gewijzigde bestanden met toevoegingen van 1190 en 428 verwijderingen
  1. 1 1
      config/config.ts
  2. 1 1
      config/defaultSettings.ts
  3. 48 0
      src/components/ProTableCard/CardItems/cascade.tsx
  4. 1 0
      src/components/ProTableCard/TableCard.tsx
  5. 57 0
      src/components/SipComponent/index.tsx
  6. 15 0
      src/components/TitleComponent/index.less
  7. 11 0
      src/components/TitleComponent/index.tsx
  8. 2 1
      src/pages/Log/System/index.tsx
  9. 14 5
      src/pages/device/Product/Detail/Access/index.tsx
  10. 1 1
      src/pages/link/AccessConfig/Detail/Access/index.tsx
  11. 22 15
      src/pages/link/AccessConfig/Detail/Media/index.less
  12. 251 85
      src/pages/link/AccessConfig/Detail/Media/index.tsx
  13. 12 20
      src/pages/link/AccessConfig/Detail/Provider/index.tsx
  14. 58 16
      src/pages/link/AccessConfig/Detail/index.tsx
  15. 0 5
      src/pages/link/AccessConfig/index.less
  16. 8 6
      src/pages/link/AccessConfig/index.tsx
  17. 5 0
      src/pages/link/AccessConfig/service.ts
  18. 4 4
      src/pages/media/Stream/Detail/index.tsx
  19. 25 15
      src/pages/media/Stream/index.tsx
  20. 8 0
      src/pages/notice/Config/Detail/index.less
  21. 223 0
      src/pages/notice/Config/Detail/index.tsx
  22. 162 128
      src/pages/notice/Config/index.tsx
  23. 157 96
      src/pages/notice/Template/Detail/index.tsx
  24. 16 11
      src/pages/notice/Template/index.tsx
  25. 1 1
      src/pages/notice/Template/typings.d.ts
  26. 63 0
      src/pages/notice/index.tsx
  27. 6 16
      src/pages/rule-engine/Instance/index.tsx
  28. 1 1
      src/pages/system/Menu/components/Icons/icon.ts
  29. 6 0
      src/utils/menu/index.ts
  30. 3 0
      src/utils/menu/router.ts
  31. 8 0
      src/utils/util.ts

+ 1 - 1
config/config.ts

@@ -21,7 +21,7 @@ export default defineConfig({
   },
   layout: {
     // https://umijs.org/zh-CN/plugins/plugin-layout
-    locale: true,
+    locale: false,
     siderWidth: 208,
     ...defaultSettings,
   },

+ 1 - 1
config/defaultSettings.ts

@@ -8,7 +8,7 @@ const Settings: LayoutSettings & {
   // 拂晓蓝
   // primaryColor: '#1890ff',
   // 极光绿
-  primaryColor: '#f5222d',
+  primaryColor: '#1d39c4',
   layout: 'mix',
   contentWidth: 'Fluid',
   splitMenus: true,

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

+ 8 - 0
src/pages/notice/Config/Detail/index.less

@@ -0,0 +1,8 @@
+.form {
+  :global {
+    .ant-radio-button-wrapper {
+      height: 100%;
+      margin-right: 15px;
+    }
+  }
+}

+ 223 - 0
src/pages/notice/Config/Detail/index.tsx

@@ -0,0 +1,223 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { createForm, onFieldValueChange } from '@formily/core';
+import { Card, Col, Input, Row } from 'antd';
+import { ISchema } from '@formily/json-schema';
+import { useMemo } from 'react';
+import { createSchemaField } from '@formily/react';
+import { FormButtonGroup, FormItem, Select, Submit, Switch, Form } from '@formily/antd';
+import styles from './index.less';
+import { service } from '@/pages/notice/Config';
+import { useAsyncDataSource } from '@/utils/util';
+import { useParams } from 'umi';
+import { typeList } from '@/pages/notice';
+
+const Detail = () => {
+  const { id } = useParams<{ id: string }>();
+
+  const form = useMemo(
+    () =>
+      createForm({
+        validateFirst: true,
+        effects() {
+          onFieldValueChange('type', async (field, f) => {
+            const type = field.value;
+            if (!type) return;
+            f.setFieldState('provider', (state) => {
+              state.value = undefined;
+              // state.dataSource = providerRef.current
+              //   .find((item) => type === item.id)
+              //   ?.providerInfos.map((i) => ({ label: i.name, value: i.id }));
+            });
+          });
+          onFieldValueChange('provider', async () => {
+            // eslint-disable-next-line @typescript-eslint/no-use-before-define
+            // currentType = field.value;
+            // await createSchema();
+          });
+        },
+      }),
+    [id],
+  );
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Select,
+      Switch,
+    },
+  });
+
+  const getTypes = async () =>
+    service.getTypes().then((resp) => {
+      return resp.result.map((item: NetworkType) => ({
+        label: item.name,
+        value: item.id,
+      }));
+    });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      name: {
+        title: '名称',
+        'x-component': 'Input',
+        'x-decorator': 'FormItem',
+      },
+      type: {
+        title: '分类',
+        'x-component': 'Input',
+        'x-value': id,
+      },
+      provider: {
+        title: '类型',
+        type: 'string',
+        'x-decorator': 'FormItem',
+        'x-component': 'Radio.Group',
+        'x-component-props': {
+          optionType: 'button',
+        },
+        required: true,
+        'x-visible': typeList[id]?.length > 0,
+        enum: typeList[id] || [],
+      },
+      configuration: {
+        type: 'object',
+        properties: {
+          weixin: {
+            type: 'void',
+            properties: {
+              corpId: {
+                title: 'corpID',
+                'x-component': 'Input',
+                'x-decorator': 'FormItem',
+                // 企业消息
+              },
+              corpSecret: {
+                title: 'corpSecret',
+                'x-component': 'Input',
+                'x-decorator': 'FormItem',
+                // 企业消息
+              },
+              AppId: {
+                title: 'appId',
+                'x-component': 'Input',
+                'x-decorator': 'FormItem',
+                // 服务号
+              },
+              AppSecret: {
+                title: 'appSecret',
+                'x-component': 'Input',
+                'x-decorator': 'FormItem',
+                // 服务号
+              },
+            },
+          },
+          dingTalk: {
+            type: 'void',
+            properties: {
+              AppKey: {
+                title: 'AppKey',
+                'x-component': 'Input',
+                'x-decorator': 'FormItem',
+                // 钉钉消息通知
+              },
+              AppSecret: {
+                title: 'AppSecret',
+                'x-component': 'Input',
+                'x-decorator': 'FormItem',
+                // 钉钉消息通知
+              },
+              Webhook: {
+                title: 'webHook',
+                'x-component': 'Input',
+                'x-decorator': 'FormItem',
+                // 群机器人
+              },
+            },
+          },
+          // 阿里云语音/短信
+          voiceOrSms: {
+            type: 'void',
+            properties: {
+              RegionId: {
+                title: 'regionId',
+                'x-component': 'Input',
+                'x-decorator': 'FormItem',
+              },
+              AccessKeyId: {
+                title: 'accessKeyId',
+                'x-component': 'Input',
+                'x-decorator': 'FormItem',
+              },
+              Secret: {
+                title: 'secret',
+                'x-component': 'Input',
+                'x-decorator': 'FormItem',
+              },
+            },
+          },
+          email: {
+            type: 'void',
+            properties: {
+              host: {
+                title: '服务器地址',
+                'x-component': 'Input',
+                'x-decorator': 'FormItem',
+              },
+              port: {
+                title: '端口',
+                'x-component': 'Input',
+                'x-decorator': 'FormItem',
+              },
+              enableSSL: {
+                title: '开启SSL',
+                'x-component': 'Input',
+                'x-decorator': 'FormItem',
+              },
+              sender: {
+                title: '发件人',
+                'x-component': 'Input',
+                'x-decorator': 'FormItem',
+              },
+              username: {
+                title: '用户名',
+                'x-component': 'Input',
+                'x-decorator': 'FormItem',
+              },
+              password: {
+                title: '密码',
+                'x-component': 'Input',
+                'x-decorator': 'FormItem',
+              },
+            },
+          },
+        },
+      },
+    },
+  };
+
+  return (
+    <PageContainer>
+      <Card>
+        <Row>
+          <Col span={10}>
+            <Form className={styles.form} form={form} layout={'vertical'}>
+              <SchemaField scope={{ useAsyncDataSource, getTypes }} schema={schema} />
+              <FormButtonGroup.Sticky>
+                <FormButtonGroup.FormItem>
+                  <Submit>保存</Submit>
+                </FormButtonGroup.FormItem>
+              </FormButtonGroup.Sticky>
+            </Form>
+          </Col>
+          <Col span={12} push={2}>
+            结果
+          </Col>
+        </Row>
+      </Card>
+    </PageContainer>
+  );
+};
+
+export default Detail;

+ 162 - 128
src/pages/notice/Config/index.tsx

@@ -6,17 +6,20 @@ import {
   BugOutlined,
   EditOutlined,
   MinusOutlined,
+  PlusOutlined,
 } from '@ant-design/icons';
-import { message, Popconfirm, Tooltip } from 'antd';
+import { Button, message, Popconfirm, Tooltip } from 'antd';
 import { useMemo, useRef, useState } from 'react';
-import BaseCrud from '@/components/BaseCrud';
 import { useIntl } from '@@/plugin-locale/localeExports';
-import type { ISchema } from '@formily/json-schema';
-import { downloadObject, useAsyncDataSource } from '@/utils/util';
+import { downloadObject } from '@/utils/util';
 import { CurdModel } from '@/components/BaseCrud/model';
 import Service from '@/pages/notice/Config/service';
 import { createForm, onFieldValueChange } from '@formily/core';
 import { observer } from '@formily/react';
+import SearchComponent from '@/components/SearchComponent';
+import ProTable from '@jetlinks/pro-table';
+import { history } from '@@/core/history';
+import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
 
 export const service = new Service('notifier/config');
 
@@ -26,112 +29,112 @@ const Config = observer(() => {
   const providerRef = useRef<NetworkType[]>([]);
   const oldTypeRef = useRef();
 
-  const [configSchema, setConfigSchema] = useState<ISchema>({});
-  const [loading, setLoading] = useState<boolean>(false);
+  // const [configSchema, setConfigSchema] = useState<ISchema>({});
+  // const [loading, setLoading] = useState<boolean>(false);
   const createSchema = async () => {
     // eslint-disable-next-line @typescript-eslint/no-use-before-define
     const DForm = form;
     if (!DForm?.values) return;
     DForm.setValuesIn('provider', DForm.values.provider);
-    const resp = await service.getMetadata(
-      DForm?.values?.type,
-      // eslint-disable-next-line @typescript-eslint/no-use-before-define
-      currentType || DForm.values?.provider,
-    );
-    const properties = resp.result?.properties as ConfigMetadata[];
-    setConfigSchema({
-      type: 'object',
-      properties: properties?.reduce((previousValue, currentValue) => {
-        if (currentValue.type?.type === 'array') {
-          // 单独处理邮件的其他配置功能
-          previousValue[currentValue.property] = {
-            type: 'array',
-            title: '其他配置',
-            'x-component': 'ArrayItems',
-            'x-decorator': 'FormItem',
-            items: {
-              type: 'object',
-              properties: {
-                space: {
-                  type: 'void',
-                  'x-component': 'Space',
-                  properties: {
-                    sort: {
-                      type: 'void',
-                      'x-decorator': 'FormItem',
-                      'x-component': 'ArrayItems.SortHandle',
-                    },
-                    name: {
-                      type: 'string',
-                      title: 'key',
-                      'x-decorator': 'FormItem',
-                      'x-component': 'Input',
-                    },
-                    value: {
-                      type: 'string',
-                      title: 'value',
-                      'x-decorator': 'FormItem',
-                      'x-component': 'Input',
-                    },
-                    description: {
-                      type: 'string',
-                      title: '备注',
-                      'x-decorator': 'FormItem',
-                      'x-component': 'Input',
-                    },
-                    remove: {
-                      type: 'void',
-                      'x-decorator': 'FormItem',
-                      'x-component': 'ArrayItems.Remove',
-                    },
-                  },
-                },
-              },
-            },
-            properties: {
-              add: {
-                type: 'void',
-                title: '添加条目',
-                'x-component': 'ArrayItems.Addition',
-              },
-            },
-          };
-        } else {
-          previousValue[currentValue.property] = {
-            title: currentValue.name,
-            type: 'string',
-            'x-component': 'Input',
-            'x-decorator': 'FormItem',
-          };
-        }
-        return previousValue;
-      }, {}),
-    });
-    DForm.setValues(CurdModel.current);
-    setLoading(false);
-  };
-  const schema: ISchema = {
-    type: 'object',
-    properties: {
-      name: {
-        title: '名称',
-        'x-component': 'Input',
-        'x-decorator': 'FormItem',
-      },
-      type: {
-        title: '类型',
-        'x-component': 'Select',
-        'x-decorator': 'FormItem',
-        'x-reactions': ['{{useAsyncDataSource(getTypes)}}'],
-      },
-      provider: {
-        title: '服务商',
-        'x-component': 'Select',
-        'x-decorator': 'FormItem',
-      },
-      configuration: configSchema,
-    },
+    // const resp = await service.getMetadata(
+    //   DForm?.values?.type,
+    //   // eslint-disable-next-line @typescript-eslint/no-use-before-define
+    //   currentType || DForm.values?.provider,
+    // );
+    // const properties = resp.result?.properties as ConfigMetadata[];
+    // setConfigSchema({
+    //   type: 'object',
+    //   properties: properties?.reduce((previousValue, currentValue) => {
+    //     if (currentValue.type?.type === 'array') {
+    //       // 单独处理邮件的其他配置功能
+    //       previousValue[currentValue.property] = {
+    //         type: 'array',
+    //         title: '其他配置',
+    //         'x-component': 'ArrayItems',
+    //         'x-decorator': 'FormItem',
+    //         items: {
+    //           type: 'object',
+    //           properties: {
+    //             space: {
+    //               type: 'void',
+    //               'x-component': 'Space',
+    //               properties: {
+    //                 sort: {
+    //                   type: 'void',
+    //                   'x-decorator': 'FormItem',
+    //                   'x-component': 'ArrayItems.SortHandle',
+    //                 },
+    //                 name: {
+    //                   type: 'string',
+    //                   title: 'key',
+    //                   'x-decorator': 'FormItem',
+    //                   'x-component': 'Input',
+    //                 },
+    //                 value: {
+    //                   type: 'string',
+    //                   title: 'value',
+    //                   'x-decorator': 'FormItem',
+    //                   'x-component': 'Input',
+    //                 },
+    //                 description: {
+    //                   type: 'string',
+    //                   title: '备注',
+    //                   'x-decorator': 'FormItem',
+    //                   'x-component': 'Input',
+    //                 },
+    //                 remove: {
+    //                   type: 'void',
+    //                   'x-decorator': 'FormItem',
+    //                   'x-component': 'ArrayItems.Remove',
+    //                 },
+    //               },
+    //             },
+    //           },
+    //         },
+    //         properties: {
+    //           add: {
+    //             type: 'void',
+    //             title: '添加条目',
+    //             'x-component': 'ArrayItems.Addition',
+    //           },
+    //         },
+    //       };
+    //     } else {
+    //       previousValue[currentValue.property] = {
+    //         title: currentValue.name,
+    //         type: 'string',
+    //         'x-component': 'Input',
+    //         'x-decorator': 'FormItem',
+    //       };
+    //     }
+    //     return previousValue;
+    //   }, {}),
+    // });
+    // DForm.setValues(CurdModel.current);
+    // setLoading(false);
   };
+  // const schema: ISchema = {
+  //   type: 'object',
+  //   properties: {
+  //     name: {
+  //       title: '名称',
+  //       'x-component': 'Input',
+  //       'x-decorator': 'FormItem',
+  //     },
+  //     type: {
+  //       title: '类型',
+  //       'x-component': 'Select',
+  //       'x-decorator': 'FormItem',
+  //       'x-reactions': ['{{useAsyncDataSource(getTypes)}}'],
+  //     },
+  //     provider: {
+  //       title: '服务商',
+  //       'x-component': 'Select',
+  //       'x-decorator': 'FormItem',
+  //     },
+  //     configuration: configSchema,
+  //   },
+  // };
 
   const formEvent = () => {
     onFieldValueChange('type', async (field, f) => {
@@ -206,7 +209,7 @@ const Config = observer(() => {
         <a
           key="edit"
           onClick={async () => {
-            setLoading(true);
+            // setLoading(true);
             CurdModel.update(record);
             form.setValues(record);
             await createSchema();
@@ -280,33 +283,64 @@ const Config = observer(() => {
     },
   ];
 
-  const getTypes = async () =>
-    service.getTypes().then((resp) => {
-      providerRef.current = resp.result;
-      return resp.result.map((item: NetworkType) => ({
-        label: item.name,
-        value: item.id,
-      }));
-    });
-
+  // const getTypes = async () =>
+  //   service.getTypes().then((resp) => {
+  //     providerRef.current = resp.result;
+  //     return resp.result.map((item: NetworkType) => ({
+  //       label: item.name,
+  //       value: item.id,
+  //     }));
+  //   });
+  const [param, setParam] = useState({});
   return (
     <PageContainer className={'page-title-show'}>
-      <BaseCrud
-        columns={columns}
-        service={service}
-        title={intl.formatMessage({
-          id: 'pages.notice.config',
-          defaultMessage: '通知配置',
-        })}
-        modelConfig={{
-          width: '50vw',
-          loading: loading,
+      <SearchComponent
+        field={columns}
+        onSearch={(data) => {
+          actionRef.current?.reset?.();
+          setParam(data);
         }}
-        schema={schema}
-        form={form}
-        schemaConfig={{ scope: { useAsyncDataSource, getTypes } }}
-        actionRef={actionRef}
       />
+      <ProTable<ConfigItem>
+        search={false}
+        params={param}
+        columns={columns}
+        headerTitle={'通知模版'}
+        toolBarRender={() => [
+          <Button
+            onClick={() => {
+              const id = (location as any).query?.id;
+              history.push(getMenuPathByParams(MENUS_CODE['notice/Config/Detail'], id));
+            }}
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.add',
+              defaultMessage: '新增',
+            })}
+          </Button>,
+        ]}
+        request={async (params) => service.query(params)}
+      />
+
+      {/*<BaseCrud*/}
+      {/*  columns={columns}*/}
+      {/*  service={service}*/}
+      {/*  title={intl.formatMessage({*/}
+      {/*    id: 'pages.notice.config',*/}
+      {/*    defaultMessage: '通知配置',*/}
+      {/*  })}*/}
+      {/*  modelConfig={{*/}
+      {/*    width: '50vw',*/}
+      {/*    loading: loading,*/}
+      {/*  }}*/}
+      {/*  schema={schema}*/}
+      {/*  form={form}*/}
+      {/*  schemaConfig={{ scope: { useAsyncDataSource, getTypes } }}*/}
+      {/*  actionRef={actionRef}*/}
+      {/*/>*/}
     </PageContainer>
   );
 });

+ 157 - 96
src/pages/notice/Template/Detail/index.tsx

@@ -5,6 +5,7 @@ import {
   FormButtonGroup,
   FormItem,
   Input,
+  NumberPicker,
   PreviewText,
   Radio,
   Select,
@@ -14,24 +15,18 @@ import {
 } from '@formily/antd';
 import type { Field } from '@formily/core';
 import { createForm, onFieldValueChange } from '@formily/core';
-import { createSchemaField } from '@formily/react';
+import { createSchemaField, observer } from '@formily/react';
 import type { ISchema } from '@formily/json-schema';
 import styles from './index.less';
-import { useMemo, useState } from 'react';
+import { useEffect, useMemo } from 'react';
 import FUpload from '@/components/Upload';
 import { useParams } from 'umi';
 import { PageContainer } from '@ant-design/pro-layout';
-import { Card, Col, Row } from 'antd';
+import { Card, Col, message, Row } from 'antd';
+import { typeList } from '@/pages/notice';
+import { service, state } from '@/pages/notice/Template';
 
-const createImageLabel = (image: string, text: string) => {
-  return (
-    <>
-      <img alt="" height="100px" src={image} />
-      <div style={{ textAlign: 'center' }}>{text}</div>
-    </>
-  );
-};
-const Detail = () => {
+const Detail = observer(() => {
   const { id } = useParams<{ id: string }>();
 
   // 正则提取${}里面的值
@@ -47,13 +42,49 @@ const Detail = () => {
               .match(pattern)
               ?.filter((i: string) => i)
               .map((item: string) => ({ id: item }));
-            form1.setValuesIn('variableDefinitions', idList);
+            if (form1.modified) {
+              form1.setValuesIn('variableDefinitions', idList);
+            }
+          });
+          onFieldValueChange('variableDefinitions.*.type', () => {
+            // const value = (field as Field).value;
+            // console.log(value, 'value');
+            // const format = field.query('.format').take() as DataField;
+            // console.log(field.query('.format'), field.query('.format').take(), 'values')
+            // switch (format.value) {
+            //   case 'date':
+            //     break;
+            //   case 'string':
+            //     format.setComponent(Input);
+            //     format.setDataSource([])
+            //     break;
+            //   case 'number':
+            //     format.setComponent(Input);
+            //     // format.setValue('%.xf');
+            //     break;
+            //   case 'file':
+            //     format.setComponent(Select);
+            //     format.setDataSource([
+            //       {label: '视频', value: 'video'},
+            //       {label: '图片', value: 'img'},
+            //       {label: '全部', value: 'any'},
+            //       {label: '', value: ''},
+            //     ])
+            //
+            //     break;
+            // }
           });
         },
       }),
     [id],
   );
 
+  useEffect(() => {
+    if (state.current) {
+      form.setValues(state.current);
+    }
+  }, []);
+
   const SchemaField = createSchemaField({
     components: {
       FormItem,
@@ -66,74 +97,46 @@ const Detail = () => {
       PreviewText,
       Space,
       FUpload,
+      NumberPicker,
     },
   });
 
-  const [result, setResult] = useState<any>({});
   const handleSave = async () => {
-    const data = await form.submit();
-    setResult(data);
-  };
+    const data: TemplateItem = await form.submit();
 
-  const typeList = {
-    weixin: [
-      {
-        label: createImageLabel(
-          'https://lf1-cdn-tos.bytegoofy.com/goofy/lark/passport/staticfiles/passport/OKR.png',
-          '企业消息',
-        ),
-        value: 'corpMessage',
-      },
-      {
-        label: createImageLabel(
-          'https://lf1-cdn-tos.bytegoofy.com/goofy/lark/passport/staticfiles/passport/Hire.png',
-          '服务号消息',
-        ),
-        value: 'officialMessage',
-      },
-    ],
-    dingTalk: [
-      {
-        label: createImageLabel(
-          'https://lf1-cdn-tos.bytegoofy.com/goofy/lark/passport/staticfiles/passport/OKR.png',
-          '钉钉消息',
-        ),
-        value: 'dingTalkMessage',
-      },
-      {
-        label: createImageLabel(
-          'https://lf1-cdn-tos.bytegoofy.com/goofy/lark/passport/staticfiles/passport/Hire.png',
-          '群机器人消息',
-        ),
-        value: 'dingTalkRobotWebHook',
-      },
-    ],
-    voice: [
-      {
-        label: createImageLabel(
-          'https://lf1-cdn-tos.bytegoofy.com/goofy/lark/passport/staticfiles/passport/OKR.png',
-          '阿里云语音',
-        ),
-        value: 'aliyun',
-      },
-    ],
-    sms: [
-      {
-        label: createImageLabel(
-          'https://lf1-cdn-tos.bytegoofy.com/goofy/lark/passport/staticfiles/passport/OKR.png',
-          '阿里云短信',
-        ),
-        value: 'aliyunSms',
-      },
-    ],
-    email: [],
+    // dingTalkRobotWebHook
+
+    // 提交的时候处理内容
+    // 钉钉机器人-->dingTalkRobotWebHook
+    // r如果是text 的话。template.message=>template.text.content
+    // 如果是markdown 的话。 template.message=>template.markdown.text
+    // 如果是link的话。 template.message =>template.markdown.text
+    if (data.provider === 'dingTalkRobotWebHook') {
+      const type = data.template.messageType;
+      // emplate.messageType
+      switch (type) {
+        case 'text':
+          data.template.text = {
+            content: data.template.message,
+          };
+          // data.template.text.content = data.template.message;
+          break;
+        case 'markdown':
+          data.template.markdown.text = data.template.message;
+          break;
+        case 'link':
+          data.template.link.text = data.template.message;
+      }
+    }
+
+    const response: any = await service.save(data);
+
+    if (response?.status === 200) {
+      message.success('保存成功');
+      history.back();
+    }
   };
 
-  // 提交的时候处理内容
-  // 钉钉机器人-->dingTalkRobotWebHook
-  // r如果是text 的话。template.message=>template.text.content
-  // 如果是markdown 的话。 template.message=>template.markdown.text
-  // 如果是link的话。 template.message =>template.markdown.text
   const schema: ISchema = {
     type: 'object',
     properties: {
@@ -154,6 +157,11 @@ const Detail = () => {
           },
         ],
       },
+      type: {
+        title: '类型',
+        'x-value': id,
+        'x-hidden': true,
+      },
       provider: {
         title: '类型',
         type: 'string',
@@ -184,7 +192,7 @@ const Detail = () => {
             type: 'void',
             'x-visible': id === 'weixin',
             properties: {
-              agentID: {
+              agentId: {
                 title: 'AgentId',
                 'x-component': 'Input',
                 'x-decorator': 'FormItem',
@@ -237,7 +245,7 @@ const Detail = () => {
               dingTalkMessage: {
                 type: 'void',
                 properties: {
-                  agentID: {
+                  agentId: {
                     title: 'AgentID',
                     'x-component': 'Input',
                     'x-decorator': 'FormItem',
@@ -329,7 +337,7 @@ const Detail = () => {
                         'x-component': 'Input',
                         'x-decorator': 'FormItem',
                       },
-                      picUrl: {
+                      '{url:picUrl}': {
                         title: '图片链接',
                         'x-component': 'FUpload',
                         'x-decorator': 'FormItem',
@@ -373,7 +381,7 @@ const Detail = () => {
             properties: {
               voice: {
                 'x-visible': id === 'voice',
-                type: 'object',
+                type: 'void',
                 properties: {
                   // ttsCode	String	语音-模版ID
                   // calledShowNumbers	String	语音-被叫显号
@@ -381,30 +389,86 @@ const Detail = () => {
                   // PlayTimes	String	语音-播放次数
                   ttsCode: {
                     title: '模版ID',
+                    'x-component': 'Input',
+                    'x-decorator': 'FormItem',
+                    'x-decorator-props': {
+                      tooltip: '请输入模版ID',
+                    },
+                    'x-component-props': {
+                      placeholder: '请输入模版ID',
+                    },
                   },
                   calledShowNumbers: {
                     title: '被叫号码',
+                    'x-component': 'Input',
+                    'x-decorator': 'FormItem',
+                    'x-decorator-props': {
+                      tooltip: '请输入calledShowNumbers',
+                    },
+                    'x-component-props': {
+                      placeholder: '请输入calledShowNumbers',
+                    },
                   },
-                  CalledNumber: {
+                  calledNumber: {
                     title: '被叫显号',
+                    'x-component': 'Input',
+                    'x-decorator': 'FormItem',
+                    'x-decorator-props': {
+                      tooltip: '请输入CalledNumber',
+                    },
+                    'x-component-props': {
+                      placeholder: '请输入CalledNumber',
+                    },
                   },
                   PlayTimes: {
                     title: '播放次数',
+                    'x-component': 'Input',
+                    'x-decorator': 'FormItem',
+                    'x-decorator-props': {
+                      tooltip: '请输入PlayTimes',
+                    },
+                    'x-component-props': {
+                      placeholder: '请输入PlayTimes',
+                    },
                   },
                 },
               },
               sms: {
                 'x-visible': id === 'sms',
-                type: 'object',
+                type: 'void',
                 properties: {
                   code: {
                     title: '模版ID',
+                    'x-component': 'Input',
+                    'x-decorator': 'FormItem',
+                    'x-decorator-props': {
+                      tooltip: '请输入模版ID',
+                    },
+                    'x-component-props': {
+                      placeholder: '请输入模版ID',
+                    },
                   },
                   phoneNumber: {
                     title: '收信人',
+                    'x-component': 'Input',
+                    'x-decorator': 'FormItem',
+                    'x-decorator-props': {
+                      tooltip: '请输入收信人',
+                    },
+                    'x-component-props': {
+                      placeholder: '请输入收信人',
+                    },
                   },
                   signName: {
                     title: '签名',
+                    'x-component': 'Input',
+                    'x-decorator': 'FormItem',
+                    'x-decorator-props': {
+                      tooltip: '请输入签名',
+                    },
+                    'x-component-props': {
+                      placeholder: '请输入签名',
+                    },
                   },
                   // code	String	短信-模板ID
                   // signName	String	短信-签名
@@ -487,27 +551,17 @@ const Detail = () => {
             column3: {
               type: 'void',
               'x-component': 'ArrayTable.Column',
-              'x-component-props': { title: '类型', width: '100px' },
+              'x-component-props': { title: '类型', width: '120px' },
               properties: {
                 type: {
                   type: 'string',
                   'x-decorator': 'FormItem',
                   'x-component': 'Select',
                   enum: [
+                    { label: '时间', value: 'date' },
+                    { label: '数字', value: 'number' },
                     { label: '字符串', value: 'string' },
-                    { label: '日期', value: 'date' },
-                    { label: 'int', value: 'int' },
-                    { label: '数组', value: 'array' },
-                    { label: '布尔', value: 'date' },
-                    { label: 'double', value: 'double' },
-                    { label: '枚举', value: 'enum' },
-                    { label: '浮点', value: 'float' },
-                    { label: 'long', value: 'long' },
-                    { label: '对象', value: 'object' },
                     { label: '文件', value: 'file' },
-                    { label: '密码', value: 'password' },
-                    { label: 'geoShape', value: 'geoShape' },
-                    { label: 'geoPoint', value: 'geoPoint' },
                   ],
                 },
               },
@@ -520,7 +574,14 @@ const Detail = () => {
                 format: {
                   type: 'string',
                   'x-decorator': 'FormItem',
-                  'x-component': 'Input',
+                  'x-component': 'Select',
+                  enum: [
+                    { label: 'String类型的UTC时间戳 (毫秒)', value: 'string' },
+                    { label: 'yyyy-MM-dd', value: 'yyyy-MM-dd' },
+                    { label: 'yyyy-MM-dd HH:mm:ss', value: 'yyyy-MM-dd HH:mm:ss' },
+                    { label: 'yyyy-MM-dd HH:mm:ss EE', value: 'yyyy-MM-dd HH:mm:ss EE' },
+                    { label: 'yyyy-MM-dd HH:mm:ss zzz', value: 'yyyy-MM-dd HH:mm:ss zzz' },
+                  ],
                 },
               },
             },
@@ -552,11 +613,11 @@ const Detail = () => {
             </Form>
           </Col>
           <Col span={12} push={2}>
-            {JSON.stringify(result, null, 2)}
+            这里是放描述信息的
           </Col>
         </Row>
       </Card>
     </PageContainer>
   );
-};
+});
 export default Detail;

+ 16 - 11
src/pages/notice/Template/index.tsx

@@ -7,8 +7,8 @@ import ProTable from '@jetlinks/pro-table';
 import {
   ArrowDownOutlined,
   BugOutlined,
+  DeleteOutlined,
   EditOutlined,
-  MinusOutlined,
   PlusOutlined,
 } from '@ant-design/icons';
 import { Button, Tooltip } from 'antd';
@@ -19,19 +19,20 @@ import SearchComponent from '@/components/SearchComponent';
 // import Detail from '@/pages/notice/Template/Detail';
 import { history, useLocation } from 'umi';
 import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+import { model } from '@formily/reactive';
 
 export const service = new Service('notifier/template');
+export const state = model<{
+  current?: TemplateItem;
+}>({});
 const Template = () => {
   const intl = useIntl();
+  const location = useLocation<{ id: string }>();
+  const id = (location as any).query?.id;
   const actionRef = useRef<ActionType>();
 
   const columns: ProColumns<TemplateItem>[] = [
     {
-      dataIndex: 'index',
-      valueType: 'indexBorder',
-      width: 48,
-    },
-    {
       dataIndex: 'name',
       title: intl.formatMessage({
         id: 'pages.table.name',
@@ -61,7 +62,13 @@ const Template = () => {
       align: 'center',
       width: 200,
       render: (text, record) => [
-        <a key="edit" onClick={() => console.log(record)}>
+        <a
+          key="edit"
+          onClick={() => {
+            state.current = record;
+            history.push(getMenuPathByParams(MENUS_CODE['notice/Template/Detail'], id));
+          }}
+        >
           <Tooltip
             title={intl.formatMessage({
               id: 'pages.data.option.edit',
@@ -78,7 +85,7 @@ const Template = () => {
               defaultMessage: '删除',
             })}
           >
-            <MinusOutlined />
+            <DeleteOutlined />
           </Tooltip>
         </a>,
         <a key="download">
@@ -600,8 +607,6 @@ const Template = () => {
   //   },
   // };
 
-  const location = useLocation<{ id: string }>();
-
   const [param, setParam] = useState({});
   return (
     <PageContainer className={'page-title-show'}>
@@ -623,7 +628,7 @@ const Template = () => {
         toolBarRender={() => [
           <Button
             onClick={() => {
-              const id = (location as any).query?.id;
+              state.current = undefined;
               history.push(getMenuPathByParams(MENUS_CODE['notice/Template/Detail'], id));
             }}
             key="button"

+ 1 - 1
src/pages/notice/Template/typings.d.ts

@@ -2,7 +2,7 @@ type TemplateItem = {
   id: string;
   name: string;
   type: string;
-  template: string;
+  template: any;
   provider: string;
   creatorId: string;
   createTime: number;

+ 63 - 0
src/pages/notice/index.tsx

@@ -7,6 +7,69 @@ import { observer } from '@formily/react';
 import { history } from 'umi';
 import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
 
+const createImageLabel = (image: string, text: string) => {
+  return (
+    <>
+      <img alt="" height="100px" src={image} />
+      <div style={{ textAlign: 'center' }}>{text}</div>
+    </>
+  );
+};
+
+export const typeList = {
+  weixin: [
+    {
+      label: createImageLabel(
+        'https://lf1-cdn-tos.bytegoofy.com/goofy/lark/passport/staticfiles/passport/OKR.png',
+        '企业消息',
+      ),
+      value: 'corpMessage',
+    },
+    {
+      label: createImageLabel(
+        'https://lf1-cdn-tos.bytegoofy.com/goofy/lark/passport/staticfiles/passport/Hire.png',
+        '服务号消息',
+      ),
+      value: 'officialMessage',
+    },
+  ],
+  dingTalk: [
+    {
+      label: createImageLabel(
+        'https://lf1-cdn-tos.bytegoofy.com/goofy/lark/passport/staticfiles/passport/OKR.png',
+        '钉钉消息',
+      ),
+      value: 'dingTalkMessage',
+    },
+    {
+      label: createImageLabel(
+        'https://lf1-cdn-tos.bytegoofy.com/goofy/lark/passport/staticfiles/passport/Hire.png',
+        '群机器人消息',
+      ),
+      value: 'dingTalkRobotWebHook',
+    },
+  ],
+  voice: [
+    {
+      label: createImageLabel(
+        'https://lf1-cdn-tos.bytegoofy.com/goofy/lark/passport/staticfiles/passport/OKR.png',
+        '阿里云语音',
+      ),
+      value: 'aliyun',
+    },
+  ],
+  sms: [
+    {
+      label: createImageLabel(
+        'https://lf1-cdn-tos.bytegoofy.com/goofy/lark/passport/staticfiles/passport/OKR.png',
+        '阿里云短信',
+      ),
+      value: 'aliyunSms',
+    },
+  ],
+  email: [],
+};
+
 const Type = observer(() => {
   const list = [
     {

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

+ 1 - 1
src/pages/system/Menu/components/Icons/icon.ts

@@ -22,4 +22,4 @@ export default [
   'icon-zhihuiyuanqu',
   'icon-Subtract',
   'icon-a-Frame2125',
-]
+];

+ 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: '选择通道' },
+    ],
+  },
 };
 
 /**

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

@@ -41,12 +41,15 @@ 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',
   'media/Stream': 'media/Stream',
   'media/Stream/Detail': 'media/Stream/Detail',
   'notice/Type': 'notice/Type',
+  'notice/Config': 'notice/Config',
   'media/SplitScreen': 'media/SplitScreen',
   'notice/Type/Config': 'notice/Config',
   'notice/Config/Detail': 'notice/Config/Detail',

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