浏览代码

feat: 边缘网关接入方式

100011797 3 年之前
父节点
当前提交
91cea25b9a

二进制
public/images/access/edge.png


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

@@ -1,18 +1,10 @@
-import { Badge, Button, Card, Col, Empty, Form, Input, Row, Steps, Table, Tooltip } from 'antd';
+import { Button, Card, Steps } from 'antd';
 import { useEffect, useState } from 'react';
 import styles from './index.less';
-import { service } from '@/pages/link/AccessConfig';
-import encodeQuery from '@/utils/encodeQuery';
-import { useHistory } from 'umi';
-import ReactMarkdown from 'react-markdown';
-import { getButtonPermission, getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
-import { CheckOutlined, InfoCircleOutlined } from '@ant-design/icons';
-import TitleComponent from '@/components/TitleComponent';
-import { Ellipsis, PermissionButton } from '@/components';
 import { useDomFullHeight } from '@/hooks';
-import { onlyMessage } from '@/utils/util';
-import { descriptionList, MetworkTypeMapping, ProcotoleMapping } from './data';
-import classNames from 'classnames';
+import Network from '@/pages/link/AccessConfig/Detail/components/Network';
+import Protocol from '@/pages/link/AccessConfig/Detail/components/Protocol';
+import Finish from '@/pages/link/AccessConfig/Detail/components/Finish';
 
 interface Props {
   change: () => void;
@@ -22,671 +14,68 @@ interface Props {
 }
 
 const Access = (props: Props) => {
-  const [form] = Form.useForm();
   const { minHeight } = useDomFullHeight(`.access`);
-  const history = useHistory();
-
-  const [current, setCurrent] = useState<number>(0);
-  const [networkList, setNetworkList] = useState<any[]>([]);
-  const [procotolList, setProcotolList] = useState<any[]>([]);
-  const [allProcotolList, setAllProcotolList] = useState<any[]>([]);
-  const [procotolCurrent, setProcotolCurrent] = useState<string>('');
-  const [networkCurrent, setNetworkCurrent] = useState<string>('');
-  const [config, setConfig] = useState<any>();
-  const networkPermission = PermissionButton.usePermission('link/Type').permission;
-  const protocolPermission = PermissionButton.usePermission('link/Protocol').permission;
+  const [current, setCurrent] = useState<number>(props.provider?.id !== 'child-device' ? 0 : 1);
+  const [network, setNetwork] = useState<string>(props.data?.channelId);
+  const [protocol, setProtocol] = useState<string>(props.data?.protocol);
   const [steps, setSteps] = useState<string[]>(['网络组件', '消息协议', '完成']);
 
-  const queryNetworkList = (id: string, params?: any) => {
-    service.getNetworkList(MetworkTypeMapping.get(id), params).then((resp) => {
-      if (resp.status === 200) {
-        setNetworkList(resp.result);
-      }
-    });
-  };
-
-  const queryProcotolList = (id?: string, params?: any) => {
-    service
-      .getProtocolList(
-        ProcotoleMapping.get(id),
-        encodeQuery({
-          ...params,
-          sorts: { createTime: 'desc' },
-        }),
-      )
-      .then((resp) => {
-        if (resp.status === 200) {
-          setProcotolList(resp.result);
-          setAllProcotolList(resp.result);
-        }
-      });
-  };
-
   useEffect(() => {
-    if (props.provider?.id && !props.data?.id) {
+    if (props.provider?.id) {
       if (props.provider?.id !== 'child-device') {
         setSteps(['网络组件', '消息协议', '完成']);
-        queryNetworkList(props.provider?.id, {
-          include: networkCurrent || '',
-        });
         setCurrent(0);
       } else {
         setSteps(['消息协议', '完成']);
         setCurrent(1);
-        queryProcotolList(props.provider?.id);
       }
     }
   }, [props.provider]);
 
-  useEffect(() => {
-    if (props.data?.id) {
-      setProcotolCurrent(props.data?.protocol);
-      form.setFieldsValue({
-        name: props.data?.name,
-        description: props.data?.description,
-      });
-      if (props.data?.provider !== 'child-device') {
-        setCurrent(0);
-        setSteps(['网络组件', '消息协议', '完成']);
-        setNetworkCurrent(props.data?.channelId);
-        queryNetworkList(props.data?.provider, {
-          include: props.data?.channelId,
-        });
-      } else {
-        setSteps(['消息协议', '完成']);
-        setCurrent(1);
-        queryProcotolList(props.data?.provider);
-      }
-    }
-  }, [props.data]);
-
-  const next = () => {
-    if (current === 0) {
-      if (!networkCurrent) {
-        onlyMessage('请选择网络组件!', 'error');
-      } else {
-        queryProcotolList(props.provider?.id);
-        setCurrent(current + 1);
-      }
-    }
-    if (current === 1) {
-      if (!procotolCurrent) {
-        onlyMessage('请选择消息协议!', 'error');
-      } else {
-        if (props.provider?.channel !== 'child-device') {
-          service
-            .getConfigView(procotolCurrent, ProcotoleMapping.get(props.provider?.id))
-            .then((resp) => {
-              if (resp.status === 200) {
-                setConfig(resp.result);
-              }
-            });
-        } else {
-          service.getChildConfigView(procotolCurrent).then((resp) => {
-            if (resp.status === 200) {
-              setConfig(resp.result);
-            }
-          });
-        }
-        setCurrent(current + 1);
-      }
-    }
-  };
-
   const prev = () => {
     setCurrent(current - 1);
   };
 
-  const columnsMQTT: any[] = [
-    {
-      title: '分组',
-      dataIndex: 'group',
-      key: 'group',
-      ellipsis: true,
-      align: 'center',
-      width: 100,
-      onCell: (record: any, index: number) => {
-        const list = (config?.routes || []).sort((a: any, b: any) => a - b) || [];
-        const arr = list.filter((res: any) => {
-          // 这里gpsNumber是我需要判断的字段名(相同就合并)
-          return res?.group == record?.group;
-        });
-        if (index == 0 || list[index - 1]?.group != record?.group) {
-          return { rowSpan: arr.length };
-        } else {
-          return { rowSpan: 0 };
-        }
-      },
-    },
-    {
-      title: 'topic',
-      dataIndex: 'topic',
-      key: 'topic',
-      render: (text: any) => (
-        <Tooltip placement="topLeft" title={text}>
-          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
-            {text}
-          </div>
-        </Tooltip>
-      ),
-    },
-    {
-      title: '上下行',
-      dataIndex: 'stream',
-      key: 'stream',
-      ellipsis: true,
-      align: 'center',
-      width: 100,
-      render: (text: any, record: any) => {
-        const list = [];
-        if (record?.upstream) {
-          list.push('上行');
-        }
-        if (record?.downstream) {
-          list.push('下行');
-        }
-        return <span>{list.join(',')}</span>;
-      },
-    },
-    {
-      title: '说明',
-      dataIndex: 'description',
-      key: 'description',
-      render: (text: any) => (
-        <Tooltip placement="topLeft" title={text}>
-          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
-            {text}
-          </div>
-        </Tooltip>
-      ),
-    },
-  ];
-
-  const columnsHTTP: any[] = [
-    {
-      title: '分组',
-      dataIndex: 'group',
-      key: 'group',
-      ellipsis: true,
-      width: 100,
-      onCell: (record: any, index: number) => {
-        const list = (config?.routes || []).sort((a: any, b: any) => a - b) || [];
-        const arr = list.filter((res: any) => {
-          // 这里gpsNumber是我需要判断的字段名(相同就合并)
-          return res?.group == record?.group;
-        });
-        if (index == 0 || list[index - 1]?.group != record?.group) {
-          return { rowSpan: arr.length };
-        } else {
-          return { rowSpan: 0 };
-        }
-      },
-    },
-    {
-      title: '地址',
-      dataIndex: 'address',
-      key: 'address',
-      render: (text: any) => (
-        <Tooltip placement="topLeft" title={text}>
-          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
-            {text}
-          </div>
-        </Tooltip>
-      ),
-    },
-    {
-      title: '示例',
-      dataIndex: 'example',
-      key: 'example',
-      render: (text: any) => (
-        <Tooltip placement="topLeft" title={text}>
-          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
-            {text}
-          </div>
-        </Tooltip>
-      ),
-    },
-    {
-      title: '说明',
-      dataIndex: 'description',
-      key: 'description',
-      render: (text: any) => (
-        <Tooltip placement="topLeft" title={text}>
-          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
-            {text}
-          </div>
-        </Tooltip>
-      ),
-    },
-  ];
+  const next = () => {
+    setCurrent(current + 1);
+  };
 
   const renderSteps = (cur: number) => {
     switch (cur) {
       case 0:
         return (
-          <div>
-            <div className={styles.alert}>
-              <InfoCircleOutlined style={{ marginRight: 10 }} />
-              选择与设备通信的网络组件
-            </div>
-            <div className={styles.search}>
-              <Input.Search
-                key={'network'}
-                placeholder="请输入名称"
-                allowClear
-                onSearch={(value: string) => {
-                  queryNetworkList(
-                    props.provider?.id,
-                    encodeQuery({
-                      include: networkCurrent || '',
-                      terms: {
-                        name$LIKE: `%${value}%`,
-                      },
-                    }),
-                  );
-                }}
-                style={{ width: 500, margin: '20px 0' }}
-              />
-              {!props.view && (
-                <PermissionButton
-                  isPermission={networkPermission.add}
-                  onClick={() => {
-                    const url = getMenuPathByCode(MENUS_CODE['link/Type/Detail']);
-                    const tab: any = window.open(
-                      `${origin}/#${url}?type=${MetworkTypeMapping.get(props.provider?.id) || ''}`,
-                    );
-                    tab!.onTabSaveSuccess = (value: any) => {
-                      if (value.status === 200) {
-                        setNetworkCurrent(value.result?.id);
-                        queryNetworkList(props.provider?.id, {
-                          include: networkCurrent || '',
-                        });
-                      }
-                    };
-                  }}
-                  key="button"
-                  type="primary"
-                >
-                  新增
-                </PermissionButton>
-              )}
-            </div>
-            {networkList.length > 0 ? (
-              <Row gutter={[16, 16]}>
-                {networkList.map((item) => (
-                  <Col key={item.id} span={8}>
-                    <Card
-                      className={classNames(
-                        styles.cardRender,
-                        networkCurrent === item.id ? styles.checked : '',
-                      )}
-                      hoverable
-                      onClick={() => {
-                        setNetworkCurrent(item.id);
-                      }}
-                    >
-                      <div className={styles.title}>
-                        <Ellipsis title={item.name} tooltip={{ placement: 'topLeft' }} />
-                        {/*<Tooltip placement="topLeft" title={item.name}>*/}
-                        {/*  {item.name}*/}
-                        {/*</Tooltip>*/}
-                      </div>
-                      <div className={styles.cardContent}>
-                        <Tooltip
-                          placement="topLeft"
-                          title={
-                            item.addresses?.length > 1 ? (
-                              <div>
-                                {[...item.addresses].map((i: any) => (
-                                  <div key={i.address}>
-                                    <Badge color={i.health === -1 ? 'red' : 'green'} />
-                                    {i.address}
-                                  </div>
-                                ))}
-                              </div>
-                            ) : (
-                              ''
-                            )
-                          }
-                        >
-                          <div
-                            style={{
-                              width: '100%',
-                              height: '20px',
-                              display: 'flex',
-                              flexDirection: 'column',
-                              alignItems: 'center',
-                              justifyContent: 'center',
-                            }}
-                          >
-                            {item.addresses.slice(0, 1).map((i: any) => (
-                              <div className={styles.item} key={i.address}>
-                                <Badge color={i.health === -1 ? 'red' : 'green'} text={i.address} />
-                                {item.addresses?.length > 1 && '...'}
-                              </div>
-                            ))}
-                          </div>
-                        </Tooltip>
-                        <Ellipsis
-                          title={item?.description || descriptionList[props.provider?.id]}
-                          tooltip={{ placement: 'topLeft' }}
-                          titleClassName={styles.desc}
-                        />
-                        {/*<div className={styles.desc}>*/}
-                        {/*  <Tooltip*/}
-                        {/*    placement="topLeft"*/}
-                        {/*    title={item?.description || descriptionList[props.provider?.id]}*/}
-                        {/*  >*/}
-                        {/*    {item?.description || descriptionList[props.provider?.id]}*/}
-                        {/*  </Tooltip>*/}
-                        {/*  */}
-                        {/*</div>*/}
-                      </div>
-                      <div className={styles.checkedIcon}>
-                        <div>
-                          <CheckOutlined />
-                        </div>
-                      </div>
-                    </Card>
-                  </Col>
-                ))}
-              </Row>
-            ) : (
-              <Empty
-                style={{ marginTop: '10%', marginBottom: '10%' }}
-                description={
-                  <span>
-                    暂无数据
-                    {getButtonPermission('link/Type', ['add']) ? (
-                      '请联系管理员进行配置'
-                    ) : (
-                      <Button
-                        type="link"
-                        onClick={() => {
-                          const url = getMenuPathByCode(MENUS_CODE['link/Type/Detail']);
-                          const tab: any = window.open(`${origin}/#${url}`);
-                          tab!.onTabSaveSuccess = (value: any) => {
-                            if (value.status === 200) {
-                              setNetworkCurrent(value.result?.id);
-                              queryNetworkList(props.provider?.id, {
-                                include: networkCurrent || '',
-                              });
-                            }
-                          };
-                        }}
-                      >
-                        创建接入方式
-                      </Button>
-                    )}
-                  </span>
-                }
-              />
-            )}
-          </div>
+          <Network
+            next={(param) => {
+              setNetwork(param);
+              next();
+            }}
+            data={network}
+            provider={props.provider}
+            view={props.view}
+          />
         );
       case 1:
         return (
-          <div>
-            <div className={styles.alert}>
-              <InfoCircleOutlined style={{ marginRight: 10 }} />
-              使用选择的消息协议,对网络组件通信数据进行编解码、认证等操作
-            </div>
-            <div className={styles.search}>
-              <Input.Search
-                key={'protocol'}
-                allowClear
-                placeholder="请输入名称"
-                onSearch={(value: string) => {
-                  if (value) {
-                    const list = allProcotolList.filter((i) => {
-                      return (
-                        i?.name && i.name.toLocaleLowerCase().includes(value.toLocaleLowerCase())
-                      );
-                    });
-                    setProcotolList(list);
-                  } else {
-                    setProcotolList(allProcotolList);
-                  }
-                }}
-                style={{ width: 500, margin: '20px 0' }}
-              />
-              {!props.view && (
-                <PermissionButton
-                  isPermission={protocolPermission.add}
-                  onClick={() => {
-                    const url = getMenuPathByCode(MENUS_CODE[`link/Protocol`]);
-                    const tab: any = window.open(`${origin}/#${url}?save=true`);
-                    tab!.onTabSaveSuccess = (resp: any) => {
-                      if (resp.status === 200) {
-                        setProcotolCurrent(resp.result?.id);
-                        queryProcotolList(props.provider?.id);
-                      }
-                    };
-                  }}
-                  key="button"
-                  type="primary"
-                >
-                  新增
-                </PermissionButton>
-              )}
-            </div>
-            {procotolList.length > 0 ? (
-              <Row gutter={[16, 16]}>
-                {procotolList.map((item) => (
-                  <Col key={item.id} span={8}>
-                    <Card
-                      // className={styles.cardRender}
-                      className={classNames(
-                        styles.cardRender,
-                        procotolCurrent === item.id ? styles.checked : '',
-                      )}
-                      // style={{
-                      //   width: '100%',
-                      //   borderColor:
-                      //     procotolCurrent === item.id ? 'var(--ant-primary-color-active)' : '',
-                      // }}
-                      hoverable
-                      onClick={() => {
-                        if (!props.data.id) {
-                          setProcotolCurrent(item.id);
-                        }
-                      }}
-                    >
-                      <div style={{ height: '45px' }}>
-                        <div className={styles.title}>
-                          <Tooltip title={item.name}>{item.name}</Tooltip>
-                        </div>
-                        <div className={styles.desc}>
-                          <Tooltip placement="topLeft" title={item.description}>
-                            {item.description}
-                          </Tooltip>
-                        </div>
-                      </div>
-                      <div className={styles.checkedIcon}>
-                        <div>
-                          <CheckOutlined />
-                        </div>
-                      </div>
-                    </Card>
-                  </Col>
-                ))}
-              </Row>
-            ) : (
-              <Empty
-                style={{ marginTop: '10%', marginBottom: '10%' }}
-                description={
-                  <span>
-                    暂无数据
-                    {getButtonPermission('link/Protocol', ['add']) ? (
-                      '请联系管理员进行配置'
-                    ) : props.view ? (
-                      ''
-                    ) : (
-                      <Button
-                        type="link"
-                        onClick={() => {
-                          const url = getMenuPathByCode(MENUS_CODE[`link/Protocol`]);
-                          const tab: any = window.open(`${origin}/#${url}?save=true`);
-                          tab!.onTabSaveSuccess = (resp: any) => {
-                            if (resp.status === 200) {
-                              setProcotolCurrent(resp.result?.id);
-                              queryProcotolList(props.provider?.id);
-                            }
-                          };
-                        }}
-                      >
-                        去新增
-                      </Button>
-                    )}
-                  </span>
-                }
-              />
-            )}
-          </div>
+          <Protocol
+            dt={props.data}
+            data={protocol}
+            provider={props.provider}
+            prev={prev}
+            next={(param) => {
+              setProtocol(param);
+              next();
+            }}
+          />
         );
       case 2:
         return (
-          <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 placeholder="请输入名称" />
-                  </Form.Item>
-                  <Form.Item name="description" label="说明">
-                    <Input.TextArea showCount maxLength={200} placeholder="请输入说明" />
-                  </Form.Item>
-                </Form>
-                <div className={styles.action} style={{ marginTop: 50 }}>
-                  <Button style={{ margin: '0 8px' }} onClick={() => prev()}>
-                    上一步
-                  </Button>
-                  {!props.view && (
-                    <Button
-                      type="primary"
-                      disabled={
-                        !!props.data.id
-                          ? getButtonPermission('link/AccessConfig', ['update'])
-                          : getButtonPermission('link/AccessConfig', ['add'])
-                      }
-                      onClick={async () => {
-                        try {
-                          const values = await form.validateFields();
-                          // 编辑还是保存
-                          if (!props.data?.id) {
-                            service
-                              .save({
-                                name: values.name,
-                                description: values.description,
-                                provider: props.provider.id,
-                                protocol: procotolCurrent,
-                                transport:
-                                  props.provider?.id === 'child-device'
-                                    ? 'Gateway'
-                                    : ProcotoleMapping.get(props.provider.id),
-                                channel: 'network', // 网络组件
-                                channelId: networkCurrent,
-                              })
-                              .then((resp: any) => {
-                                if (resp.status === 200) {
-                                  onlyMessage('操作成功!');
-                                  history.goBack();
-                                  if ((window as any).onTabSaveSuccess) {
-                                    (window as any).onTabSaveSuccess(resp);
-                                    setTimeout(() => window.close(), 300);
-                                  }
-                                }
-                              });
-                          } else {
-                            service
-                              .update({
-                                ...props.data,
-                                name: values.name,
-                                description: values.description,
-                                protocol: procotolCurrent,
-                                channel: 'network', // 网络组件
-                                channelId: networkCurrent,
-                              })
-                              .then((resp: any) => {
-                                if (resp.status === 200) {
-                                  onlyMessage('操作成功!');
-                                  history.goBack();
-                                  if ((window as any).onTabSaveSuccess) {
-                                    (window as any).onTabSaveSuccess(resp);
-                                    setTimeout(() => window.close(), 300);
-                                  }
-                                }
-                              });
-                          }
-                        } catch (errorInfo) {
-                          console.error('Failed:', errorInfo);
-                        }
-                      }}
-                    >
-                      保存
-                    </Button>
-                  )}
-                </div>
-              </div>
-            </Col>
-            <Col span={12}>
-              <div className={styles.config}>
-                <div className={styles.item}>
-                  <div className={styles.title}>接入方式</div>
-                  <div className={styles.context}>{props.provider?.name}</div>
-                  <div className={styles.context}>{props.provider?.description}</div>
-                </div>
-                <div className={styles.item}>
-                  <div className={styles.title}>消息协议</div>
-                  <div className={styles.context}>
-                    {procotolList.find((i) => i.id === procotolCurrent)?.name}
-                  </div>
-                  {config?.document && (
-                    <div className={styles.context}>
-                      {<ReactMarkdown>{config?.document}</ReactMarkdown>}
-                    </div>
-                  )}
-                </div>
-                <div className={styles.item}>
-                  <div className={styles.title}>网络组件</div>
-                  {(networkList.find((i) => i.id === networkCurrent)?.addresses || []).length > 0
-                    ? (networkList.find((i) => i.id === networkCurrent)?.addresses || []).map(
-                        (item: any) => (
-                          <div key={item.address}>
-                            <Badge
-                              color={item.health === -1 ? 'red' : 'green'}
-                              text={item.address}
-                            />
-                          </div>
-                        ),
-                      )
-                    : ''}
-                </div>
-                {config?.routes && config?.routes?.length > 0 && (
-                  <div className={styles.item}>
-                    <div style={{ fontWeight: '600', marginBottom: 10 }}>
-                      {props.data?.provider === 'mqtt-server-gateway' ||
-                      props.data?.provider === 'mqtt-client-gateway'
-                        ? 'topic'
-                        : 'URL信息'}
-                    </div>
-                    <Table
-                      bordered
-                      dataSource={config?.routes || []}
-                      columns={config.id === 'MQTT' ? columnsMQTT : columnsHTTP}
-                      pagination={false}
-                      scroll={{ y: 300 }}
-                    />
-                  </div>
-                )}
-              </div>
-            </Col>
-          </Row>
+          <Finish
+            prev={prev}
+            data={props.data}
+            type={'network'}
+            provider={props.provider}
+            config={{ network, protocol }}
+          />
         );
       default:
         return null;
@@ -700,8 +89,6 @@ const Access = (props: Props) => {
           type="link"
           onClick={() => {
             props.change();
-            setNetworkCurrent('');
-            setProcotolCurrent('');
           }}
         >
           返回
@@ -716,18 +103,6 @@ const Access = (props: Props) => {
           </Steps>
         </div>
         <div className={styles.content}>{renderSteps(current)}</div>
-        <div className={styles.action}>
-          {current === 1 && props.provider.id !== 'child-device' && (
-            <Button style={{ margin: '0 8px' }} onClick={() => prev()}>
-              上一步
-            </Button>
-          )}
-          {(current === 0 || current === 1) && (
-            <Button type="primary" onClick={() => next()}>
-              下一步
-            </Button>
-          )}
-        </div>
       </div>
     </Card>
   );

+ 5 - 6
src/pages/link/AccessConfig/Detail/Channel/index.tsx

@@ -1,7 +1,7 @@
 import { Button, Card, Col, Form, Input, Row } from 'antd';
 import { useEffect, useState } from 'react';
 import { service } from '@/pages/link/AccessConfig';
-import { ProcotoleMapping } from '../Cloud/Protocol';
+import { ProtocolMapping } from '../data';
 import TitleComponent from '@/components/TitleComponent';
 import { getButtonPermission } from '@/utils/menu';
 import ReactMarkdown from 'react-markdown';
@@ -20,7 +20,7 @@ const Media = (props: Props) => {
   const [config, setConfig] = useState<any>({});
   const history = useHistory();
 
-  const procotol = props.provider.id === 'modbus-tcp' ? 'modbus-tcp' : 'opc-ua';
+  const protocol = props.provider.id === 'modbus-tcp' ? 'modbus-tcp' : 'opc-ua';
   const name = props.provider.id === 'modbus-tcp' ? 'Modbus' : 'OPCUA';
 
   useEffect(() => {
@@ -31,8 +31,7 @@ const Media = (props: Props) => {
   }, [props.data]);
 
   useEffect(() => {
-    console.log(ProcotoleMapping);
-    service.getConfigView(procotol, ProcotoleMapping.get(props.provider?.id)).then((resp) => {
+    service.getConfigView(protocol, ProtocolMapping.get(props.provider?.id)).then((resp) => {
       if (resp.status === 200) {
         setConfig(resp.result);
       }
@@ -84,7 +83,7 @@ const Media = (props: Props) => {
                           ...props.data,
                           ...values,
                           provider: props.provider?.id,
-                          protocol: procotol,
+                          protocol: protocol,
                           transport: props.provider.id === 'modbus-tcp' ? 'MODBUS_TCP' : 'OPC_UA',
                           channel: props.provider.id === 'modbus-tcp' ? 'modbus' : 'opc-ua',
                         };
@@ -114,7 +113,7 @@ const Media = (props: Props) => {
               <div>
                 <p>接入方式:{props.provider?.name || ''}</p>
                 {props.provider?.description && <p>{props.provider?.description || ''}</p>}
-                <p>消息协议:{procotol}</p>
+                <p>消息协议:{protocol}</p>
                 {config?.document && (
                   <div>{<ReactMarkdown>{config?.document}</ReactMarkdown> || ''}</div>
                 )}

+ 0 - 141
src/pages/link/AccessConfig/Detail/Cloud/Finish/index.tsx

@@ -1,141 +0,0 @@
-import { TitleComponent } from '@/components';
-import { getButtonPermission } from '@/utils/menu';
-import { Button, Col, Form, Input, Row } from 'antd';
-import { service } from '@/pages/link/AccessConfig';
-import { useHistory } from 'umi';
-import { useEffect, useState } from 'react';
-import ReactMarkdown from 'react-markdown';
-import { ProcotoleMapping } from '../Protocol';
-import { onlyMessage } from '@/utils/util';
-
-interface Props {
-  prev: () => void;
-  data: any;
-  config: any;
-  provider: any;
-  procotol: string;
-  view?: boolean;
-}
-
-const Finish = (props: Props) => {
-  const [form] = Form.useForm();
-  const history = useHistory();
-  const [config, setConfig] = useState<any>({});
-
-  useEffect(() => {
-    form.setFieldsValue({
-      name: props.data.name,
-      description: props.data.description,
-    });
-  }, [props.data]);
-
-  useEffect(() => {
-    service.getConfigView(props.procotol, ProcotoleMapping.get(props.provider?.id)).then((resp) => {
-      if (resp.status === 200) {
-        setConfig(resp.result);
-      }
-    });
-  }, [props.procotol, props.provider]);
-
-  return (
-    <Row gutter={24}>
-      <Col span={12}>
-        <div>
-          <TitleComponent data={'基本信息'} />
-          <Form name="basic" layout="vertical" form={form}>
-            <Form.Item label="名称" name="name" rules={[{ required: true, message: '请输入名称' }]}>
-              <Input placeholder="请输入名称" />
-            </Form.Item>
-            <Form.Item name="description" label="说明">
-              <Input.TextArea showCount maxLength={200} placeholder="请输入说明" />
-            </Form.Item>
-          </Form>
-          <div style={{ marginTop: 50 }}>
-            <Button
-              style={{ margin: '0 8px' }}
-              onClick={() => {
-                props.prev();
-              }}
-            >
-              上一步
-            </Button>
-            {!props.view && (
-              <Button
-                type="primary"
-                disabled={
-                  !!props.data.id
-                    ? getButtonPermission('link/AccessConfig', ['update'])
-                    : getButtonPermission('link/AccessConfig', ['add'])
-                }
-                onClick={async () => {
-                  try {
-                    const values = await form.validateFields();
-                    const param = {
-                      ...props.data,
-                      ...values,
-                      provider: props.provider.id,
-                      protocol: props.procotol,
-                      transport: 'HTTP_SERVER',
-                      configuration: {
-                        ...props.config,
-                      },
-                    };
-                    const resp: any = await service[!props.data?.id ? 'save' : 'update'](param);
-                    if (resp.status === 200) {
-                      onlyMessage('操作成功!');
-                      history.goBack();
-                      if ((window as any).onTabSaveSuccess) {
-                        (window as any).onTabSaveSuccess(resp);
-                        setTimeout(() => window.close(), 300);
-                      }
-                    }
-                  } catch (errorInfo) {
-                    console.error('Failed:', errorInfo);
-                  }
-                }}
-              >
-                保存
-              </Button>
-            )}
-          </div>
-        </div>
-      </Col>
-      <Col span={12}>
-        <div style={{ marginLeft: 10 }}>
-          <TitleComponent data={'配置概览'} />
-          <div>
-            <p>接入方式:{props.provider?.name || ''}</p>
-            {props.provider?.description && <p>{props.provider?.description || ''}</p>}
-            <p>消息协议:{props.procotol}</p>
-            {config?.document && (
-              <div>{<ReactMarkdown>{config?.document}</ReactMarkdown> || ''}</div>
-            )}
-          </div>
-          <TitleComponent data={'设备接入指引'} />
-          <div>
-            <p>
-              1、创建类型为{props?.provider?.id === 'OneNet' ? 'OneNet' : 'CTWing'}的设备接入网关
-            </p>
-            <p>
-              2、创建产品,并选中接入方式为
-              {props?.provider?.id === 'OneNet'
-                ? 'OneNet'
-                : 'CTWing,选中后需填写CTWing平台中的产品ID、Master-APIkey。'}
-            </p>
-            {props?.provider?.id === 'OneNet' ? (
-              <p>
-                3、添加设备,为每一台设备设置唯一的IMEI、IMSI码(需与OneNet平台中填写的值一致,若OneNet平台没有对应的设备,将会通过OneNet平台提供的LWM2M协议自动创建)
-              </p>
-            ) : (
-              <p>
-                3、添加设备,为每一台设备设置唯一的IMEI、SN、IMSI、PSK码(需与CTWingt平台中填写的值一致,若CTWing平台没有对应的设备,将会通过CTWing平台提供的LWM2M协议自动创建)
-              </p>
-            )}
-          </div>
-        </div>
-      </Col>
-    </Row>
-  );
-};
-
-export default Finish;

+ 0 - 18
src/pages/link/AccessConfig/Detail/Cloud/Protocol/index.less

@@ -1,18 +0,0 @@
-.search {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-}
-.alert {
-  height: 40px;
-  padding-left: 10px;
-  color: rgba(0, 0, 0, 0.55);
-  line-height: 40px;
-  background-color: #f6f6f6;
-}
-
-.cardRender {
-  width: 100%;
-  background: url('/images/access.png') no-repeat;
-  background-size: 100% 100%;
-}

+ 0 - 162
src/pages/link/AccessConfig/Detail/Cloud/Protocol/index.tsx

@@ -1,162 +0,0 @@
-import { getButtonPermission, getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
-import { Button, Card, Col, Empty, Input, message, Row, Space } from 'antd';
-import { useEffect, useState } from 'react';
-import { service } from '@/pages/link/AccessConfig';
-import styles from './index.less';
-import PermissionButton from '@/components/PermissionButton';
-import encodeQuery from '@/utils/encodeQuery';
-
-export const ProcotoleMapping = new Map();
-ProcotoleMapping.set('websocket-server', 'WebSocket');
-ProcotoleMapping.set('http-server-gateway', 'HTTP');
-ProcotoleMapping.set('udp-device-gateway', 'UDP');
-ProcotoleMapping.set('coap-server-gateway', 'COAP');
-ProcotoleMapping.set('mqtt-client-gateway', 'MQTT');
-ProcotoleMapping.set('mqtt-server-gateway', 'MQTT');
-ProcotoleMapping.set('tcp-server-gateway', 'TCP');
-ProcotoleMapping.set('child-device', '');
-ProcotoleMapping.set('OneNet', 'HTTP');
-ProcotoleMapping.set('Ctwing', 'HTTP');
-ProcotoleMapping.set('modbus-tcp', 'MODBUS_TCP');
-ProcotoleMapping.set('opc-ua', 'OPC_UA');
-
-interface Props {
-  provider: any;
-  data: string;
-  prev: () => void;
-  next: (data: string) => void;
-  view?: boolean;
-}
-
-const Protocol = (props: Props) => {
-  const [procotolList, setProcotolList] = useState<any[]>([]);
-  const [procotolCurrent, setProcotolCurrent] = useState<string>('');
-  const protocolPermission = PermissionButton.usePermission('link/Protocol').permission;
-
-  const queryProcotolList = (id?: string, params?: any) => {
-    service.getProtocolList(ProcotoleMapping.get(id), params).then((resp) => {
-      if (resp.status === 200) {
-        setProcotolList(resp.result);
-      }
-    });
-  };
-
-  useEffect(() => {
-    queryProcotolList(props.provider?.id);
-  }, [props.provider]);
-
-  useEffect(() => {
-    setProcotolCurrent(props.data);
-  }, [props.data]);
-
-  return (
-    <div>
-      <div className={styles.search}>
-        <Input.Search
-          key={'protocol'}
-          placeholder="请输入名称"
-          onSearch={(value: string) => {
-            queryProcotolList(
-              props.provider?.id,
-              encodeQuery({
-                terms: {
-                  name$LIKE: `%${value}%`,
-                },
-              }),
-            );
-          }}
-          style={{ width: 500, margin: '20px 0' }}
-        />
-        {!props.view && (
-          <PermissionButton
-            isPermission={protocolPermission.add}
-            onClick={() => {
-              const url = getMenuPathByCode(MENUS_CODE[`link/Protocol`]);
-              const tab: any = window.open(`${origin}/#${url}?save=true`);
-              tab!.onTabSaveSuccess = (resp: any) => {
-                if (resp.status === 200) {
-                  queryProcotolList(props.provider?.id);
-                }
-              };
-            }}
-            key="button"
-            type="primary"
-          >
-            新增
-          </PermissionButton>
-        )}
-      </div>
-      {procotolList.length > 0 ? (
-        <Row gutter={[16, 16]}>
-          {procotolList.map((item) => (
-            <Col key={item.id} span={8}>
-              <Card
-                className={styles.cardRender}
-                style={{
-                  width: '100%',
-                  borderColor: procotolCurrent === item.id ? 'var(--ant-primary-color-active)' : '',
-                }}
-                hoverable
-                onClick={() => {
-                  setProcotolCurrent(item.id);
-                }}
-              >
-                <div style={{ height: '45px' }}>
-                  <div className={styles.title}>{item.name || ''}</div>
-                  <div className={styles.desc}>{item.description || ''}</div>
-                </div>
-              </Card>
-            </Col>
-          ))}
-        </Row>
-      ) : (
-        <Empty
-          description={
-            <span>
-              暂无数据
-              {getButtonPermission('link/Protocol', ['add']) ? (
-                '请联系管理员进行配置'
-              ) : props.view ? (
-                ''
-              ) : (
-                <Button
-                  type="link"
-                  onClick={() => {
-                    const url = getMenuPathByCode(MENUS_CODE[`link/Protocol`]);
-                    const tab: any = window.open(`${origin}/#${url}?save=true`);
-                    tab!.onTabSaveSuccess = (resp: any) => {
-                      if (resp.status === 200) {
-                        queryProcotolList(props.provider?.id);
-                      }
-                    };
-                  }}
-                >
-                  去新增
-                </Button>
-              )}
-            </span>
-          }
-        />
-      )}
-      <Space style={{ marginTop: 20 }}>
-        <Button style={{ margin: '0 8px' }} onClick={() => props.prev()}>
-          上一步
-        </Button>
-        <Button
-          type="primary"
-          onClick={() => {
-            if (!procotolCurrent) {
-              message.error('请选择消息协议!');
-            } else {
-              props.next(procotolCurrent);
-            }
-          }}
-        >
-          下一步
-        </Button>
-      </Space>
-    </div>
-  );
-};
-
-export default Protocol;

+ 13 - 10
src/pages/link/AccessConfig/Detail/Cloud/index.tsx

@@ -2,10 +2,10 @@ import { Button, Card, Steps } from 'antd';
 import { useEffect, useState } from 'react';
 import styles from './index.less';
 import { InfoCircleOutlined } from '@ant-design/icons';
-import OneNet from './OneNet';
-import CTWing from './CTWing';
-import Protocol from './Protocol';
-import Finish from './Finish';
+import OneNet from '../components/OneNet';
+import CTWing from '../components/CTWing';
+import Protocol from '../components/Protocol';
+import Finish from '../components/Finish';
 
 interface Props {
   change: () => void;
@@ -18,7 +18,7 @@ const Cloud = (props: Props) => {
   const [current, setCurrent] = useState<number>(0);
   const [steps] = useState<string[]>(['接入配置', '消息协议', '完成']);
   const [config, setConfig] = useState<any>({});
-  const [procotolCurrent, setProcotolCurrent] = useState<string>('');
+  const [protocolCurrent, setProtocolCurrent] = useState<string>('');
 
   const prev = () => {
     setCurrent(current - 1);
@@ -32,7 +32,7 @@ const Cloud = (props: Props) => {
   useEffect(() => {
     setCurrent(0);
     setConfig(props.data?.configuration || {});
-    setProcotolCurrent(props.data?.protocol);
+    setProtocolCurrent(props.data?.protocol);
   }, [props.data]);
 
   const renderSteps = (cur: number) => {
@@ -63,11 +63,11 @@ const Cloud = (props: Props) => {
             </div>
             <div style={{ marginTop: 10 }}>
               <Protocol
-                data={procotolCurrent}
+                data={protocolCurrent}
                 provider={props.provider}
                 view={props.view}
                 next={(param: string) => {
-                  setProcotolCurrent(param);
+                  setProtocolCurrent(param);
                   setCurrent(current + 1);
                 }}
                 prev={prev}
@@ -78,11 +78,14 @@ const Cloud = (props: Props) => {
       case 2:
         return (
           <Finish
-            procotol={procotolCurrent}
             provider={props.provider}
             data={props.data}
-            config={config}
+            config={{
+              config,
+              protocol: protocolCurrent,
+            }}
             prev={prev}
+            type={'cloud'}
             view={props.view}
           />
         );

+ 81 - 0
src/pages/link/AccessConfig/Detail/Edge/index.less

@@ -0,0 +1,81 @@
+.box {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin: 20px 30px;
+}
+
+.steps {
+  width: 100%;
+}
+
+.content {
+  width: 100%;
+  margin: 20px 0;
+}
+
+.action {
+  width: 100%;
+}
+
+.title {
+  width: 100%;
+  overflow: hidden;
+  font-weight: 800;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+.desc {
+  width: 100%;
+  margin-top: 10px;
+  overflow: hidden;
+  color: rgba(0, 0, 0, 0.55);
+  font-weight: 400;
+  font-size: 13px;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+.cardContent {
+  display: flex;
+  flex-direction: column;
+  margin-top: 5px;
+  color: rgba(0, 0, 0, 0.55);
+
+  .item {
+    width: 100%;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+}
+
+.config {
+  padding: 10px 20px 20px 20px;
+  color: rgba(0, 0, 0, 0.8);
+  background: rgba(0, 0, 0, 0.04);
+
+  .title {
+    width: 100%;
+    margin: 10px 0;
+    font-weight: 600;
+  }
+
+  .item {
+    margin-bottom: 10px;
+
+    .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;
+}

+ 95 - 0
src/pages/link/AccessConfig/Detail/Edge/index.tsx

@@ -0,0 +1,95 @@
+import { Button, Card, Steps } from 'antd';
+import styles from './index.less';
+import { useEffect, useState } from 'react';
+import Network from '../components/Network';
+import Finish from '../components/Finish';
+
+interface Props {
+  change: () => void;
+  data: any;
+  provider: any;
+  view?: boolean;
+}
+
+const Edge = (props: Props) => {
+  const { provider } = props;
+  const [current, setCurrent] = useState<number>(provider?.id === 'edge-child-device' ? 1 : 0);
+  const [steps] = useState<string[]>(['网络组件', '完成']);
+  const [network, setNetwork] = useState<string>(props.data?.channelId);
+
+  useEffect(() => {
+    setCurrent(provider?.id === 'edge-child-device' ? 1 : 0);
+  }, [provider]);
+
+  useEffect(() => {
+    console.log(props.data);
+    setNetwork(props.data?.channelId);
+  }, [props.data]);
+
+  const prev = () => {
+    setCurrent(current - 1);
+  };
+
+  const next = () => {
+    setCurrent(current + 1);
+  };
+
+  const renderSteps = (cur: number) => {
+    switch (cur) {
+      case 0:
+        return (
+          <Network
+            provider={props.provider}
+            data={network}
+            view={props.view}
+            next={(param) => {
+              setNetwork(param);
+              next();
+            }}
+          />
+        );
+      case 1:
+        return (
+          <Finish
+            provider={props.provider}
+            data={props.data}
+            config={{ network, protocol: 'official-edge-protocol' }}
+            prev={prev}
+            type={'edge'}
+            view={props.view}
+          />
+        );
+      default:
+        return null;
+    }
+  };
+
+  return (
+    <Card>
+      {!props.data?.id && (
+        <Button
+          type="link"
+          onClick={() => {
+            props.change();
+          }}
+        >
+          返回
+        </Button>
+      )}
+      <div className={styles.box}>
+        <div className={styles.steps}>
+          {provider?.id !== 'edge-child-device' && (
+            <Steps size="small" current={current}>
+              {steps.map((item) => (
+                <Steps.Step key={item} title={item} />
+              ))}
+            </Steps>
+          )}
+        </div>
+        <div className={styles.content}>{renderSteps(current)}</div>
+      </div>
+    </Card>
+  );
+};
+
+export default Edge;

+ 11 - 39
src/pages/link/AccessConfig/Detail/Provider/index.tsx

@@ -17,6 +17,7 @@ const Provider = (props: Props) => {
     const network: any[] = [];
     const cloud: any[] = [];
     const channel: any[] = [];
+    const edge: any[] = [];
     (props?.data || []).map((item: any) => {
       if (item.id === 'fixed-media' || item.id === 'gb28181-2016') {
         media.push(item);
@@ -24,6 +25,8 @@ const Provider = (props: Props) => {
         cloud.push(item);
       } else if (item.id === 'modbus-tcp' || item.id === 'opc-ua') {
         channel.push(item);
+      } else if (item.id === 'official-edge-gateway' || item.id === 'edge-child-device') {
+        edge.push(item);
       } else {
         network.push(item);
       }
@@ -68,6 +71,11 @@ const Provider = (props: Props) => {
           list: [...channel],
           title: '通道类设备接入',
         },
+        {
+          type: 'edge',
+          list: [...edge],
+          title: '官方接入',
+        },
       ]);
     }
   }, [props.data]);
@@ -87,47 +95,11 @@ const Provider = (props: Props) => {
   backMap.set('OneNet', require('/public/images/access/onenet.png'));
   backMap.set('gb28181-2016', require('/public/images/access/gb28181.png'));
   backMap.set('mqtt-client-gateway', require('/public/images/access/mqtt-broke.png'));
+  backMap.set('edge-child-device', require('/public/images/access/child-device.png'));
+  backMap.set('official-edge-gateway', require('/public/images/access/edge.png'));
 
   return (
     <div>
-      {/* {dataSource.map((i) => (
-        <Card key={i.type} style={{ marginTop: 20 }}>
-          <TitleComponent data={i.title} />
-          <Row gutter={[24, 24]}>
-            {(i?.list || []).map((item: any) => (
-              <Col key={item.name} span={12}>
-                <div className={styles.provider}>
-                  <div className={styles.box}>
-                    <div className={styles.left}>
-                      <div className={styles.images}>
-                        <img src={backMap.get(item.id)} />
-                      </div>
-                      <div className={styles.context}>
-                        <div style={{ fontWeight: 600 }}>{item.name}</div>
-                        <div className={styles.desc}>
-                          <Tooltip title={item?.description || ''}>
-                            {item?.description || ''}
-                          </Tooltip>
-                        </div>
-                      </div>
-                    </div>
-                    <div style={{ width: '70px' }}>
-                      <Button
-                        type="primary"
-                        onClick={() => {
-                          props.change(item, i.type);
-                        }}
-                      >
-                        接入
-                      </Button>
-                    </div>
-                  </div>
-                </div>
-              </Col>
-            ))}
-          </Row>
-        </Card>
-      ))} */}
       {dataSource.map((i) => {
         if (i.list && i.list.length !== 0) {
           return (
@@ -135,7 +107,7 @@ const Provider = (props: Props) => {
               <TitleComponent data={i.title} />
               <Row gutter={[24, 24]}>
                 {(i?.list || []).map((item: any) => (
-                  <Col key={item.name} span={12}>
+                  <Col key={item.id} span={12}>
                     <div className={styles.provider}>
                       <div className={styles.box}>
                         <div className={styles.left}>

src/pages/link/AccessConfig/Detail/Cloud/CTWing/index.less → src/pages/link/AccessConfig/Detail/components/CTWing/index.less


src/pages/link/AccessConfig/Detail/Cloud/CTWing/index.tsx → src/pages/link/AccessConfig/Detail/components/CTWing/index.tsx


+ 378 - 0
src/pages/link/AccessConfig/Detail/components/Finish/index.tsx

@@ -0,0 +1,378 @@
+import { TitleComponent } from '@/components';
+import { getButtonPermission } from '@/utils/menu';
+import { Badge, Button, Col, Form, Input, Row, Table, Tooltip } from 'antd';
+import { service } from '@/pages/link/AccessConfig';
+import { useHistory } from 'umi';
+import { useEffect, useState } from 'react';
+import ReactMarkdown from 'react-markdown';
+import { onlyMessage } from '@/utils/util';
+import styles from '@/pages/link/AccessConfig/Detail/Access/index.less';
+import { Store } from 'jetlinks-store';
+import { ProtocolMapping } from '@/pages/link/AccessConfig/Detail/data';
+
+interface Props {
+  prev: () => void;
+  data: any;
+  config: any;
+  provider: any;
+  view?: boolean;
+  type: 'network' | 'edge' | 'cloud';
+}
+
+const Finish = (props: Props) => {
+  const [form] = Form.useForm();
+  const history = useHistory();
+  const [config, setConfig] = useState<any>({});
+
+  const protocolList = Store.get('allProtocolList') || [];
+  const networkList = Store.get('network') || [];
+
+  const columnsMQTT: any[] = [
+    {
+      title: '分组',
+      dataIndex: 'group',
+      key: 'group',
+      ellipsis: true,
+      align: 'center',
+      width: 100,
+      onCell: (record: any, index: number) => {
+        const list = (config?.routes || []).sort((a: any, b: any) => a - b) || [];
+        const arr = list.filter((res: any) => {
+          // 这里gpsNumber是我需要判断的字段名(相同就合并)
+          return res?.group == record?.group;
+        });
+        if (index == 0 || list[index - 1]?.group != record?.group) {
+          return { rowSpan: arr.length };
+        } else {
+          return { rowSpan: 0 };
+        }
+      },
+    },
+    {
+      title: 'topic',
+      dataIndex: 'topic',
+      key: 'topic',
+      render: (text: any) => (
+        <Tooltip placement="topLeft" title={text}>
+          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
+            {text}
+          </div>
+        </Tooltip>
+      ),
+    },
+    {
+      title: '上下行',
+      dataIndex: 'stream',
+      key: 'stream',
+      ellipsis: true,
+      align: 'center',
+      width: 100,
+      render: (text: any, record: any) => {
+        const list = [];
+        if (record?.upstream) {
+          list.push('上行');
+        }
+        if (record?.downstream) {
+          list.push('下行');
+        }
+        return <span>{list.join(',')}</span>;
+      },
+    },
+    {
+      title: '说明',
+      dataIndex: 'description',
+      key: 'description',
+      render: (text: any) => (
+        <Tooltip placement="topLeft" title={text}>
+          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
+            {text}
+          </div>
+        </Tooltip>
+      ),
+    },
+  ];
+
+  const columnsHTTP: any[] = [
+    {
+      title: '分组',
+      dataIndex: 'group',
+      key: 'group',
+      ellipsis: true,
+      width: 100,
+      onCell: (record: any, index: number) => {
+        const list = (config?.routes || []).sort((a: any, b: any) => a - b) || [];
+        const arr = list.filter((res: any) => {
+          // 这里gpsNumber是我需要判断的字段名(相同就合并)
+          return res?.group == record?.group;
+        });
+        if (index == 0 || list[index - 1]?.group != record?.group) {
+          return { rowSpan: arr.length };
+        } else {
+          return { rowSpan: 0 };
+        }
+      },
+    },
+    {
+      title: '地址',
+      dataIndex: 'address',
+      key: 'address',
+      render: (text: any) => (
+        <Tooltip placement="topLeft" title={text}>
+          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
+            {text}
+          </div>
+        </Tooltip>
+      ),
+    },
+    {
+      title: '示例',
+      dataIndex: 'example',
+      key: 'example',
+      render: (text: any) => (
+        <Tooltip placement="topLeft" title={text}>
+          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
+            {text}
+          </div>
+        </Tooltip>
+      ),
+    },
+    {
+      title: '说明',
+      dataIndex: 'description',
+      key: 'description',
+      render: (text: any) => (
+        <Tooltip placement="topLeft" title={text}>
+          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
+            {text}
+          </div>
+        </Tooltip>
+      ),
+    },
+  ];
+
+  useEffect(() => {
+    form.setFieldsValue({
+      name: props.data.name,
+      description: props.data.description,
+    });
+  }, [props.data]);
+
+  useEffect(() => {
+    if (props.type === 'network') {
+      if (props.provider?.channel !== 'child-device') {
+        service
+          .getConfigView(props.config.protocol, ProtocolMapping.get(props.provider?.id))
+          .then((resp) => {
+            if (resp.status === 200) {
+              setConfig(resp.result);
+            }
+          });
+      } else {
+        service.getChildConfigView(props.config.protocol).then((resp) => {
+          if (resp.status === 200) {
+            setConfig(resp.result);
+          }
+        });
+      }
+    }
+  }, [props.config.protocol, props.provider]);
+
+  const renderRightContent = (provider: string) => {
+    if (provider === 'OneNet' || provider === 'Ctwing') {
+      return (
+        <div style={{ marginLeft: 10 }}>
+          <TitleComponent data={'配置概览'} />
+          <div>
+            <p>接入方式:{props.provider?.name || ''}</p>
+            {props.provider?.description && <p>{props.provider?.description || ''}</p>}
+            <p>消息协议:{props.config.protocol}</p>
+            {config?.document && (
+              <div>{<ReactMarkdown>{config?.document}</ReactMarkdown> || ''}</div>
+            )}
+          </div>
+          <TitleComponent data={'设备接入指引'} />
+          <div>
+            <p>
+              1、创建类型为{props?.provider?.id === 'OneNet' ? 'OneNet' : 'CTWing'}的设备接入网关
+            </p>
+            <p>
+              2、创建产品,并选中接入方式为
+              {props?.provider?.id === 'OneNet'
+                ? 'OneNet'
+                : 'CTWing,选中后需填写CTWing平台中的产品ID、Master-APIkey。'}
+            </p>
+            {props?.provider?.id === 'OneNet' ? (
+              <p>
+                3、添加设备,为每一台设备设置唯一的IMEI、IMSI码(需与OneNet平台中填写的值一致,若OneNet平台没有对应的设备,将会通过OneNet平台提供的LWM2M协议自动创建)
+              </p>
+            ) : (
+              <p>
+                3、添加设备,为每一台设备设置唯一的IMEI、SN、IMSI、PSK码(需与CTWingt平台中填写的值一致,若CTWing平台没有对应的设备,将会通过CTWing平台提供的LWM2M协议自动创建)
+              </p>
+            )}
+          </div>
+        </div>
+      );
+    } else if (provider === 'official-edge-gateway' || provider === 'edge-child-device') {
+      return (
+        <div>
+          <TitleComponent data={'配置概览'} />
+          <div>
+            <p>接入方式:{props.provider?.name || ''}</p>
+            {props.provider?.description && <p>{props.provider?.description || ''}</p>}
+            <p>消息协议:{props.config.protocol}</p>
+            {config?.document && (
+              <div>{<ReactMarkdown>{config?.document}</ReactMarkdown> || ''}</div>
+            )}
+          </div>
+        </div>
+      );
+    } else {
+      return (
+        <div className={styles.config}>
+          <div className={styles.item}>
+            <div className={styles.title}>接入方式</div>
+            <div className={styles.context}>{props.provider?.name}</div>
+            <div className={styles.context}>{props.provider?.description}</div>
+          </div>
+          <div className={styles.item}>
+            <div className={styles.title}>消息协议</div>
+            <div className={styles.context}>
+              {protocolList.find((i: any) => i.id === props?.config.protocol)?.name}
+            </div>
+            {config?.document && (
+              <div className={styles.context}>
+                {<ReactMarkdown>{config?.document}</ReactMarkdown>}
+              </div>
+            )}
+          </div>
+          <div className={styles.item}>
+            <div className={styles.title}>网络组件</div>
+            {(networkList.find((i: any) => i.id === props.config?.network)?.addresses || []).length
+              ? (networkList.find((i: any) => i.id === props.config?.network)?.addresses || []).map(
+                  (item: any) => (
+                    <div key={item.address}>
+                      <Badge color={item.health === -1 ? 'red' : 'green'} text={item.address} />
+                    </div>
+                  ),
+                )
+              : ''}
+          </div>
+          {config?.routes && config?.routes?.length > 0 && (
+            <div className={styles.item}>
+              <div style={{ fontWeight: '600', marginBottom: 10 }}>
+                {props.data?.provider === 'mqtt-server-gateway' ||
+                props.data?.provider === 'mqtt-client-gateway'
+                  ? 'topic'
+                  : 'URL信息'}
+              </div>
+              <Table
+                bordered
+                dataSource={config?.routes || []}
+                columns={config.id === 'MQTT' ? columnsMQTT : columnsHTTP}
+                pagination={false}
+                scroll={{ y: 300 }}
+              />
+            </div>
+          )}
+        </div>
+      );
+    }
+  };
+
+  return (
+    <Row gutter={24}>
+      <Col span={12}>
+        <div>
+          <TitleComponent data={'基本信息'} />
+          <Form name="basic" layout="vertical" form={form}>
+            <Form.Item label="名称" name="name" rules={[{ required: true, message: '请输入名称' }]}>
+              <Input placeholder="请输入名称" />
+            </Form.Item>
+            <Form.Item name="description" label="说明">
+              <Input.TextArea showCount maxLength={200} placeholder="请输入说明" />
+            </Form.Item>
+          </Form>
+          <div style={{ marginTop: 50 }}>
+            {props.provider?.id !== 'edge-child-device' && (
+              <Button
+                style={{ margin: '0 8px' }}
+                onClick={() => {
+                  props.prev();
+                }}
+              >
+                上一步
+              </Button>
+            )}
+            {!props.view && (
+              <Button
+                type="primary"
+                disabled={
+                  !!props.data.id
+                    ? getButtonPermission('link/AccessConfig', ['update'])
+                    : getButtonPermission('link/AccessConfig', ['add'])
+                }
+                onClick={async () => {
+                  try {
+                    const values = await form.validateFields();
+                    let param: any = {};
+                    if (props.type === 'network') {
+                      param = {
+                        name: values.name,
+                        description: values.description,
+                        provider: props.provider.id,
+                        protocol: props.config.protocol,
+                        transport:
+                          props.provider?.id === 'child-device'
+                            ? 'Gateway'
+                            : ProtocolMapping.get(props.provider.id),
+                        channel: 'network', // 网络组件
+                        channelId: props.config.network,
+                      };
+                    } else if (props.type === 'cloud') {
+                      param = {
+                        ...props.data,
+                        ...values,
+                        provider: props.provider.id,
+                        protocol: props.config.protocol,
+                        transport: 'HTTP_SERVER',
+                        configuration: {
+                          ...props.config,
+                        },
+                      };
+                    } else {
+                      param = {
+                        name: values.name,
+                        description: values.description,
+                        provider: props.provider.id,
+                        protocol: props.config.protocol,
+                        transport: ProtocolMapping.get(props.provider.id),
+                        channelId: props?.config?.network,
+                      };
+                    }
+                    const resp: any = await service[!props.data?.id ? 'save' : 'update'](param);
+                    if (resp.status === 200) {
+                      onlyMessage('操作成功!');
+                      history.goBack();
+                      if ((window as any).onTabSaveSuccess) {
+                        (window as any).onTabSaveSuccess(resp);
+                        setTimeout(() => window.close(), 300);
+                      }
+                    }
+                  } catch (errorInfo) {
+                    console.error('Failed:', errorInfo);
+                  }
+                }}
+              >
+                保存
+              </Button>
+            )}
+          </div>
+        </div>
+      </Col>
+      <Col span={12}>{renderRightContent(props.provider.id)}</Col>
+    </Row>
+  );
+};
+
+export default Finish;

+ 103 - 0
src/pages/link/AccessConfig/Detail/components/Network/index.less

@@ -0,0 +1,103 @@
+@import '~antd/es/style/themes/default.less';
+.network {
+  position: relative;
+  width: 100%;
+}
+
+.content {
+  height: 500px;
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+.title {
+  width: calc(100% - 88px);
+  overflow: hidden;
+  font-weight: 800;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+.desc {
+  margin-top: 10px;
+  color: rgba(0, 0, 0, 0.55);
+  font-weight: 400;
+  font-size: 13px;
+}
+
+.cardContent {
+  display: flex;
+  flex-direction: column;
+  margin-top: 5px;
+  color: rgba(0, 0, 0, 0.55);
+
+  .item {
+    width: 100%;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+}
+
+.search {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.alert {
+  height: 40px;
+  padding-left: 10px;
+  color: rgba(0, 0, 0, 0.55);
+  line-height: 40px;
+  background-color: #f6f6f6;
+}
+
+.cardRender {
+  width: 100%;
+  overflow: hidden;
+  background: url('/images/access.png') no-repeat;
+  background-size: 100% 100%;
+
+  .checkedIcon {
+    position: absolute;
+    right: -22px;
+    bottom: -22px;
+    z-index: 2;
+    display: none;
+    width: 44px;
+    height: 44px;
+    color: #fff;
+    background-color: red;
+    background-color: @primary-color-active;
+    transform: rotate(-45deg);
+
+    > div {
+      position: relative;
+      height: 100%;
+      transform: rotate(45deg);
+
+      > span {
+        position: absolute;
+        top: 6px;
+        left: 6px;
+        font-size: 12px;
+      }
+    }
+  }
+  &.checked {
+    position: relative;
+    color: #2f54eb;
+    border-color: #2f54eb;
+
+    .checkedIcon {
+      display: block;
+    }
+  }
+}
+
+.action {
+  width: 100%;
+  margin-top: 20px;
+  background: white;
+}

+ 210 - 0
src/pages/link/AccessConfig/Detail/components/Network/index.tsx

@@ -0,0 +1,210 @@
+import styles from './index.less';
+import { CheckOutlined, InfoCircleOutlined } from '@ant-design/icons';
+import { Badge, Button, Card, Col, Empty, Input, Row, Tooltip } from 'antd';
+import encodeQuery from '@/utils/encodeQuery';
+import { Ellipsis, PermissionButton } from '@/components';
+import { getButtonPermission, getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
+import classNames from 'classnames';
+import { useEffect, useState } from 'react';
+import { service } from '@/pages/link/AccessConfig';
+import { NetworkTypeMapping, descriptionList } from '@/pages/link/AccessConfig/Detail/data';
+import { onlyMessage } from '@/utils/util';
+import { Store } from 'jetlinks-store';
+
+interface Props {
+  next: (data: string) => void;
+  data: string;
+  provider: any;
+  view?: boolean;
+}
+
+const Network = (props: Props) => {
+  const [networkList, setNetworkList] = useState<any[]>([]);
+  const networkPermission = PermissionButton.usePermission('link/Type').permission;
+  const [networkCurrent, setNetworkCurrent] = useState<string>(props.data);
+
+  const queryNetworkList = (id: string, params?: any) => {
+    service.getNetworkList(NetworkTypeMapping.get(id), params).then((resp) => {
+      if (resp.status === 200) {
+        setNetworkList(resp.result);
+        Store.set('network', resp.result);
+      }
+    });
+  };
+
+  useEffect(() => {
+    queryNetworkList(props.provider?.id);
+  }, [props.provider?.id]);
+
+  useEffect(() => {
+    setNetworkCurrent(props.data);
+  }, [props.data]);
+
+  return (
+    <div className={styles.network}>
+      <div className={styles.alert}>
+        <InfoCircleOutlined style={{ marginRight: 10 }} />
+        选择与设备通信的网络组件
+      </div>
+      <div className={styles.search}>
+        <Input.Search
+          key={'network'}
+          placeholder="请输入名称"
+          allowClear
+          onSearch={(value: string) => {
+            queryNetworkList(
+              props.provider?.id,
+              encodeQuery({
+                include: networkCurrent || '',
+                terms: {
+                  name$LIKE: `%${value}%`,
+                },
+              }),
+            );
+          }}
+          style={{ width: 500, margin: '20px 0' }}
+        />
+        {!props.view && (
+          <PermissionButton
+            isPermission={networkPermission.add}
+            onClick={() => {
+              const url = getMenuPathByCode(MENUS_CODE['link/Type/Detail']);
+              const tab: any = window.open(
+                `${origin}/#${url}?type=${NetworkTypeMapping.get(props.provider?.id) || ''}`,
+              );
+              tab!.onTabSaveSuccess = (value: any) => {
+                if (value.status === 200) {
+                  setNetworkCurrent(value.result?.id);
+                  queryNetworkList(props.provider?.id, {
+                    include: networkCurrent || '',
+                  });
+                }
+              };
+            }}
+            key="button"
+            type="primary"
+          >
+            新增
+          </PermissionButton>
+        )}
+      </div>
+      <div className={styles.content}>
+        {networkList.length ? (
+          <Row gutter={[16, 16]}>
+            {networkList.map((item) => (
+              <Col key={item.id} span={8}>
+                <Card
+                  className={classNames(
+                    styles.cardRender,
+                    networkCurrent === item.id ? styles.checked : '',
+                  )}
+                  hoverable
+                  onClick={() => {
+                    setNetworkCurrent(item.id);
+                  }}
+                >
+                  <div className={styles.title}>
+                    <Ellipsis title={item.name} tooltip={{ placement: 'topLeft' }} />
+                  </div>
+                  <div className={styles.cardContent}>
+                    <Tooltip
+                      placement="topLeft"
+                      title={
+                        item.addresses?.length > 1 ? (
+                          <div>
+                            {[...item.addresses].map((i: any) => (
+                              <div key={i.address}>
+                                <Badge color={i.health === -1 ? 'red' : 'green'} />
+                                {i.address}
+                              </div>
+                            ))}
+                          </div>
+                        ) : (
+                          ''
+                        )
+                      }
+                    >
+                      <div
+                        style={{
+                          width: '100%',
+                          height: '20px',
+                          display: 'flex',
+                          flexDirection: 'column',
+                          alignItems: 'center',
+                          justifyContent: 'center',
+                        }}
+                      >
+                        {item.addresses.slice(0, 1).map((i: any) => (
+                          <div className={styles.item} key={i.address}>
+                            <Badge color={i.health === -1 ? 'red' : 'green'} text={i.address} />
+                            {item.addresses?.length > 1 && '...'}
+                          </div>
+                        ))}
+                      </div>
+                    </Tooltip>
+                    <Ellipsis
+                      title={item?.description || descriptionList[props.provider?.id]}
+                      tooltip={{ placement: 'topLeft' }}
+                      titleClassName={styles.desc}
+                    />
+                  </div>
+                  <div className={styles.checkedIcon}>
+                    <div>
+                      <CheckOutlined />
+                    </div>
+                  </div>
+                </Card>
+              </Col>
+            ))}
+          </Row>
+        ) : (
+          <Empty
+            style={{ marginTop: '10%', marginBottom: '10%' }}
+            description={
+              <span>
+                暂无数据
+                {getButtonPermission('link/Type', ['add']) ? (
+                  '请联系管理员进行配置'
+                ) : (
+                  <Button
+                    type="link"
+                    onClick={() => {
+                      const url = getMenuPathByCode(MENUS_CODE['link/Type/Detail']);
+                      const tab: any = window.open(`${origin}/#${url}`);
+                      tab!.onTabSaveSuccess = (value: any) => {
+                        if (value.status === 200) {
+                          setNetworkCurrent(value.result?.id);
+                          queryNetworkList(props.provider?.id, {
+                            include: networkCurrent || '',
+                          });
+                        }
+                      };
+                    }}
+                  >
+                    创建接入方式
+                  </Button>
+                )}
+              </span>
+            }
+          />
+        )}
+      </div>
+      <div className={styles.action}>
+        <Button
+          type="primary"
+          onClick={() => {
+            if (!!networkCurrent) {
+              props.next(networkCurrent);
+            } else {
+              onlyMessage('请选择网络组件!', 'error');
+            }
+          }}
+        >
+          下一步
+        </Button>
+      </div>
+    </div>
+  );
+};
+
+export default Network;

src/pages/link/AccessConfig/Detail/Cloud/OneNet/index.less → src/pages/link/AccessConfig/Detail/components/OneNet/index.less


src/pages/link/AccessConfig/Detail/Cloud/OneNet/index.tsx → src/pages/link/AccessConfig/Detail/components/OneNet/index.tsx


+ 75 - 0
src/pages/link/AccessConfig/Detail/components/Protocol/index.less

@@ -0,0 +1,75 @@
+@import '~antd/es/style/themes/default.less';
+
+.protocol {
+  position: relative;
+  width: 100%;
+}
+
+.content {
+  height: 500px;
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+.search {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.alert {
+  height: 40px;
+  padding-left: 10px;
+  color: rgba(0, 0, 0, 0.55);
+  line-height: 40px;
+  background-color: #f6f6f6;
+}
+
+.cardRender {
+  width: 100%;
+  overflow: hidden;
+  background: url('/images/access.png') no-repeat;
+  background-size: 100% 100%;
+
+  .checkedIcon {
+    position: absolute;
+    right: -22px;
+    bottom: -22px;
+    z-index: 2;
+    display: none;
+    width: 44px;
+    height: 44px;
+    color: #fff;
+    background-color: red;
+    background-color: @primary-color-active;
+    transform: rotate(-45deg);
+
+    > div {
+      position: relative;
+      height: 100%;
+      transform: rotate(45deg);
+
+      > span {
+        position: absolute;
+        top: 6px;
+        left: 6px;
+        font-size: 12px;
+      }
+    }
+  }
+  &.checked {
+    position: relative;
+    color: #2f54eb;
+    border-color: #2f54eb;
+
+    .checkedIcon {
+      display: block;
+    }
+  }
+}
+
+.action {
+  position: absolute;
+  bottom: 0;
+  width: 100%;
+  background: white;
+}

+ 189 - 0
src/pages/link/AccessConfig/Detail/components/Protocol/index.tsx

@@ -0,0 +1,189 @@
+import { getButtonPermission, getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
+import { Button, Card, Col, Empty, Input, Row, Space, Tooltip } from 'antd';
+import { useEffect, useState } from 'react';
+import { service } from '@/pages/link/AccessConfig';
+import styles from './index.less';
+import PermissionButton from '@/components/PermissionButton';
+import { ProtocolMapping } from '../../data';
+import { onlyMessage } from '@/utils/util';
+import { CheckOutlined, InfoCircleOutlined } from '@ant-design/icons';
+import classNames from 'classnames';
+import { Store } from 'jetlinks-store';
+import encodeQuery from '@/utils/encodeQuery';
+
+interface Props {
+  provider: any;
+  data: string;
+  prev: () => void;
+  next: (data: string) => void;
+  view?: boolean;
+  dt?: any;
+}
+
+const Protocol = (props: Props) => {
+  const [protocolList, setProtocolList] = useState<any[]>([]);
+  const [allProtocolList, setAllProtocolList] = useState<any[]>([]);
+  const [protocolCurrent, setProtocolCurrent] = useState<string>('');
+  const protocolPermission = PermissionButton.usePermission('link/Protocol').permission;
+
+  const queryProtocolList = (id?: string, params?: any) => {
+    service
+      .getProtocolList(
+        ProtocolMapping.get(id),
+        encodeQuery({
+          ...params,
+          sorts: { createTime: 'desc' },
+        }),
+      )
+      .then((resp) => {
+        if (resp.status === 200) {
+          setProtocolList(resp.result);
+          setAllProtocolList(resp.result);
+          Store.set('allProtocolList', resp.result);
+        }
+      });
+  };
+
+  useEffect(() => {
+    queryProtocolList(props.provider?.id);
+  }, [props.provider]);
+
+  useEffect(() => {
+    setProtocolCurrent(props.data);
+  }, [props.data]);
+
+  return (
+    <div className={styles.protocol}>
+      <div className={styles.alert}>
+        <InfoCircleOutlined style={{ marginRight: 10 }} />
+        使用选择的消息协议,对网络组件通信数据进行编解码、认证等操作
+      </div>
+      <div className={styles.search}>
+        <Input.Search
+          key={'protocol'}
+          allowClear
+          placeholder="请输入名称"
+          onSearch={(value: string) => {
+            if (value) {
+              const list = allProtocolList.filter((i) => {
+                return i?.name && i.name.toLocaleLowerCase().includes(value.toLocaleLowerCase());
+              });
+              setProtocolList(list);
+            } else {
+              setProtocolList(allProtocolList);
+            }
+          }}
+          style={{ width: 500, margin: '20px 0' }}
+        />
+        {!props.view && (
+          <PermissionButton
+            isPermission={protocolPermission.add}
+            onClick={() => {
+              const url = getMenuPathByCode(MENUS_CODE[`link/Protocol`]);
+              const tab: any = window.open(`${origin}/#${url}?save=true`);
+              tab!.onTabSaveSuccess = (resp: any) => {
+                if (resp.status === 200) {
+                  setProtocolCurrent(resp.result?.id);
+                  queryProtocolList(props.provider?.id);
+                }
+              };
+            }}
+            key="button"
+            type="primary"
+          >
+            新增
+          </PermissionButton>
+        )}
+      </div>
+      <div className={styles.content}>
+        {protocolList.length ? (
+          <Row gutter={[16, 16]}>
+            {protocolList.map((item) => (
+              <Col key={item.id} span={8}>
+                <Card
+                  className={classNames(
+                    styles.cardRender,
+                    protocolCurrent === item.id ? styles.checked : '',
+                  )}
+                  hoverable
+                  onClick={() => {
+                    if (!props.dt?.id) {
+                      setProtocolCurrent(item.id);
+                    }
+                  }}
+                >
+                  <div style={{ height: '45px' }}>
+                    <div className={styles.title}>
+                      <Tooltip title={item.name}>{item.name}</Tooltip>
+                    </div>
+                    <div className={styles.desc}>
+                      <Tooltip placement="topLeft" title={item.description}>
+                        {item.description}
+                      </Tooltip>
+                    </div>
+                  </div>
+                  <div className={styles.checkedIcon}>
+                    <div>
+                      <CheckOutlined />
+                    </div>
+                  </div>
+                </Card>
+              </Col>
+            ))}
+          </Row>
+        ) : (
+          <Empty
+            style={{ marginTop: '10%', marginBottom: '10%' }}
+            description={
+              <span>
+                暂无数据
+                {getButtonPermission('link/Protocol', ['add']) ? (
+                  '请联系管理员进行配置'
+                ) : props.view ? (
+                  ''
+                ) : (
+                  <Button
+                    type="link"
+                    onClick={() => {
+                      const url = getMenuPathByCode(MENUS_CODE[`link/Protocol`]);
+                      const tab: any = window.open(`${origin}/#${url}?save=true`);
+                      tab!.onTabSaveSuccess = (resp: any) => {
+                        if (resp.status === 200) {
+                          setProtocolCurrent(resp.result?.id);
+                          queryProtocolList(props.provider?.id);
+                        }
+                      };
+                    }}
+                  >
+                    去新增
+                  </Button>
+                )}
+              </span>
+            }
+          />
+        )}
+      </div>
+      <div className={styles.action}>
+        <Space style={{ marginTop: 20 }}>
+          <Button style={{ margin: '0 8px' }} onClick={() => props.prev()}>
+            上一步
+          </Button>
+          <Button
+            type="primary"
+            onClick={() => {
+              if (!protocolCurrent) {
+                onlyMessage('请选择消息协议!', 'error');
+              } else {
+                props.next(protocolCurrent);
+              }
+            }}
+          >
+            下一步
+          </Button>
+        </Space>
+      </div>
+    </div>
+  );
+};
+
+export default Protocol;

+ 42 - 0
src/pages/link/AccessConfig/Detail/data.ts

@@ -0,0 +1,42 @@
+export const ProtocolMapping = new Map();
+ProtocolMapping.set('websocket-server', 'WebSocket');
+ProtocolMapping.set('http-server-gateway', 'HTTP');
+ProtocolMapping.set('udp-device-gateway', 'UDP');
+ProtocolMapping.set('coap-server-gateway', 'COAP');
+ProtocolMapping.set('mqtt-client-gateway', 'MQTT');
+ProtocolMapping.set('mqtt-server-gateway', 'MQTT');
+ProtocolMapping.set('tcp-server-gateway', 'TCP');
+ProtocolMapping.set('child-device', '');
+ProtocolMapping.set('OneNet', 'HTTP');
+ProtocolMapping.set('Ctwing', 'HTTP');
+ProtocolMapping.set('modbus-tcp', 'MODBUS_TCP');
+ProtocolMapping.set('opc-ua', 'OPC_UA');
+ProtocolMapping.set('edge-child-device', 'EdgeGateway');
+ProtocolMapping.set('official-edge-gateway', 'MQTT');
+
+export const NetworkTypeMapping = new Map();
+NetworkTypeMapping.set('websocket-server', 'WEB_SOCKET_SERVER');
+NetworkTypeMapping.set('http-server-gateway', 'HTTP_SERVER');
+NetworkTypeMapping.set('udp-device-gateway', 'UDP');
+NetworkTypeMapping.set('coap-server-gateway', 'COAP_SERVER');
+NetworkTypeMapping.set('mqtt-client-gateway', 'MQTT_CLIENT');
+NetworkTypeMapping.set('mqtt-server-gateway', 'MQTT_SERVER');
+NetworkTypeMapping.set('tcp-server-gateway', 'TCP_SERVER');
+NetworkTypeMapping.set('official-edge-gateway', 'MQTT_SERVER');
+
+export const descriptionList = {
+  'udp-device-gateway':
+    'UDP可以让设备无需建立连接就可以与平台传输数据。在允许一定程度丢包的情况下,提供轻量化且简单的连接。',
+  'tcp-server-gateway':
+    'TCP服务是一种面向连接的、可靠的、基于字节流的传输层通信协议。设备可通过TCP服务与平台进行长链接,实时更新状态并发送消息。可自定义多种粘拆包规则,处理传输过程中可能发生的粘拆包问题。',
+  'websocket-server':
+    'WebSocket是一种在单个TCP连接上进行全双工通信的协议,允许服务端主动向客户端推送数据。设备通过WebSocket服务与平台进行长链接,实时更新状态并发送消息,且可以发布订阅消息',
+  'mqtt-client-gateway':
+    'MQTT是ISO 标准下基于发布/订阅范式的消息协议,具有轻量、简单、开放和易于实现的特点。平台使用指定的ID接入其他远程平台,订阅消息。也可添加用户名和密码校验。可设置最大消息长度。可统一设置共享的订阅前缀。',
+  'http-server-gateway':
+    'HTTP服务是一个简单的请求-响应的基于TCP的无状态协议。设备通过HTTP服务与平台进行灵活的短链接通信,仅支持设备和平台之间单对单的请求-响应模式',
+  'mqtt-server-gateway':
+    'MQTT是ISO 标准下基于发布/订阅范式的消息协议,具有轻量、简单、开放和易于实现的特点。提供MQTT的服务端,以供设备以长链接的方式接入平台。设备使用唯一的ID,也可添加用户名和密码校验。可设置最大消息长度。',
+  'coap-server-gateway':
+    'CoAP是针对只有少量的内存空间和有限的计算能力提供的一种基于UDP的协议。便于低功耗或网络受限的设备与平台通信,仅支持设备和平台之间单对单的请求-响应模式。',
+};

+ 65 - 49
src/pages/link/AccessConfig/Detail/index.tsx

@@ -1,13 +1,13 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import { useEffect, useState } from 'react';
-// import { useLocation } from 'umi';
+import { service } from '@/pages/link/AccessConfig';
+import { Spin } from 'antd';
 import Access from './Access';
 import Provider from './Provider';
 import Media from './Media';
-import { service } from '@/pages/link/AccessConfig';
-import { Spin } from 'antd';
 import Cloud from './Cloud';
 import Channel from './Channel';
+import Edge from './Edge';
 import { useLocation } from '@/hooks';
 
 const Detail = () => {
@@ -17,57 +17,62 @@ const Detail = () => {
   const [loading, setLoading] = useState<boolean>(true);
   const [data, setData] = useState<any>({});
   const [provider, setProvider] = useState<any>({});
-  const [type, setType] = useState<'media' | 'network' | 'cloud' | 'channel' | undefined>(
+  const [type, setType] = useState<'media' | 'network' | 'cloud' | 'channel' | 'edge' | undefined>(
     undefined,
   );
-
   const [dataSource, setDataSource] = useState<any[]>([]);
-
   useEffect(() => {
-    setLoading(true);
-    const _params = new URLSearchParams(location.search);
-    const id = _params.get('id') || undefined;
-    const paramsType = _params.get('type');
+    if (Object.keys(location).length) {
+      setLoading(true);
+      const _params = new URLSearchParams(location.search);
+      const id = _params.get('id') || undefined;
+      const paramsType = _params.get('type');
 
-    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 if (
-              response.result?.provider === 'Ctwing' ||
-              response.result?.provider === 'OneNet'
-            ) {
-              setType('cloud');
-            } else if (
-              response.result?.provider === 'modbus-tcp' ||
-              response.result?.provider === 'opc-ua'
-            ) {
-              setType('channel');
-            } else {
-              setType('network');
-            }
-          });
-        } else if (paramsType) {
-          setType('media');
-          setProvider(resp.result.find((item: any) => item.id === paramsType));
-          setData({});
-          setVisible(false);
-        } else {
-          setVisible(true);
+      service.getProviders().then((resp) => {
+        if (resp.status === 200) {
+          setDataSource(resp.result);
+          if (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 if (
+                response.result?.provider === 'Ctwing' ||
+                response.result?.provider === 'OneNet'
+              ) {
+                setType('cloud');
+              } else if (
+                response.result?.provider === 'modbus-tcp' ||
+                response.result?.provider === 'opc-ua'
+              ) {
+                setType('channel');
+              } else if (
+                response.result?.provider === 'official-edge-gateway' ||
+                response.result?.provider === 'edge-child-device'
+              ) {
+                setType('edge');
+              } else {
+                setType('network');
+              }
+            });
+          } else if (paramsType) {
+            setType('media');
+            setProvider(resp.result.find((item: any) => item.id === paramsType));
+            setData({});
+            setVisible(false);
+          } else {
+            setVisible(true);
+          }
+          setLoading(false);
         }
-        setLoading(false);
-      }
-    });
+      });
+    }
   }, [location]);
 
   useEffect(() => {
@@ -122,6 +127,17 @@ const Detail = () => {
             }}
           />
         );
+      case 'edge':
+        return (
+          <Edge
+            data={data}
+            provider={provider}
+            view={view}
+            change={() => {
+              setVisible(true);
+            }}
+          />
+        );
       default:
         return null;
     }
@@ -133,7 +149,7 @@ const Detail = () => {
         {visible ? (
           <Provider
             data={dataSource}
-            change={(param: any, typings: 'media' | 'network' | 'cloud' | 'channel') => {
+            change={(param: any, typings: 'media' | 'network' | 'cloud' | 'channel' | 'edge') => {
               setType(typings);
               setProvider(param);
               setData({});