Преглед изворни кода

feat(merge): merge sc

feat: 运行状态和设备接入配置修改
Lind пре 3 година
родитељ
комит
b423088c0d
42 измењених фајлова са 1547 додато и 123 уклоњено
  1. BIN
      public/images/network/CTWing.jpg
  2. BIN
      public/images/network/OneNet.jpg
  3. BIN
      public/images/network/doeros.jpg
  4. BIN
      public/images/running/doc.png
  5. BIN
      public/images/running/docx.png
  6. BIN
      public/images/running/flv.png
  7. BIN
      public/images/running/jpg.png
  8. BIN
      public/images/running/mp3.png
  9. BIN
      public/images/running/mp4.png
  10. BIN
      public/images/running/mvb.png
  11. BIN
      public/images/running/other.png
  12. BIN
      public/images/running/pdf.png
  13. BIN
      public/images/running/png.png
  14. BIN
      public/images/running/ppt.png
  15. BIN
      public/images/running/pptx.png
  16. BIN
      public/images/running/rmvb.png
  17. BIN
      public/images/running/swf.png
  18. BIN
      public/images/running/tiff.png
  19. BIN
      public/images/running/txt.png
  20. BIN
      public/images/running/wma.png
  21. BIN
      public/images/running/xls.png
  22. BIN
      public/images/running/xlsx.png
  23. 47 0
      src/pages/device/Instance/Detail/MetadataLog/Property/Detail.tsx
  24. 319 34
      src/pages/device/Instance/Detail/MetadataLog/Property/index.tsx
  25. 40 0
      src/pages/device/Instance/Detail/Running/Property/FileComponent/Detail.tsx
  26. 25 0
      src/pages/device/Instance/Detail/Running/Property/FileComponent/index.less
  27. 87 0
      src/pages/device/Instance/Detail/Running/Property/FileComponent/index.tsx
  28. 2 3
      src/pages/device/Instance/Detail/Running/Property/PropertyCard.tsx
  29. 2 1
      src/pages/device/Instance/Detail/Running/Property/index.tsx
  30. 11 0
      src/pages/device/Instance/service.ts
  31. 129 0
      src/pages/link/AccessConfig/Detail/Channel/index.tsx
  32. 23 0
      src/pages/link/AccessConfig/Detail/Cloud/CTWing/index.less
  33. 97 0
      src/pages/link/AccessConfig/Detail/Cloud/CTWing/index.tsx
  34. 134 0
      src/pages/link/AccessConfig/Detail/Cloud/Finish/index.tsx
  35. 23 0
      src/pages/link/AccessConfig/Detail/Cloud/OneNet/index.less
  36. 130 0
      src/pages/link/AccessConfig/Detail/Cloud/OneNet/index.tsx
  37. 18 0
      src/pages/link/AccessConfig/Detail/Cloud/Protocol/index.less
  38. 157 0
      src/pages/link/AccessConfig/Detail/Cloud/Protocol/index.tsx
  39. 81 0
      src/pages/link/AccessConfig/Detail/Cloud/index.less
  40. 117 0
      src/pages/link/AccessConfig/Detail/Cloud/index.tsx
  41. 69 83
      src/pages/link/AccessConfig/Detail/Provider/index.tsx
  42. 36 2
      src/pages/link/AccessConfig/Detail/index.tsx

BIN
public/images/network/CTWing.jpg


BIN
public/images/network/OneNet.jpg


BIN
public/images/network/doeros.jpg


BIN
public/images/running/doc.png


BIN
public/images/running/docx.png


BIN
public/images/running/flv.png


BIN
public/images/running/jpg.png


BIN
public/images/running/mp3.png


BIN
public/images/running/mp4.png


BIN
public/images/running/mvb.png


BIN
public/images/running/other.png


BIN
public/images/running/pdf.png


BIN
public/images/running/png.png


BIN
public/images/running/ppt.png


BIN
public/images/running/pptx.png


BIN
public/images/running/rmvb.png


BIN
public/images/running/swf.png


BIN
public/images/running/tiff.png


BIN
public/images/running/txt.png


BIN
public/images/running/wma.png


BIN
public/images/running/xls.png


BIN
public/images/running/xlsx.png


+ 47 - 0
src/pages/device/Instance/Detail/MetadataLog/Property/Detail.tsx

@@ -0,0 +1,47 @@
+import { Modal, Input } from 'antd';
+// import ReactMarkdown from "react-markdown";
+
+interface Props {
+  close: () => void;
+  value: any;
+  type: string;
+}
+
+const Detail = (props: Props) => {
+  const { value, type } = props;
+
+  const renderValue = () => {
+    if (type === 'object') {
+      return (
+        <div>
+          <div>自定义属性</div>
+          {JSON.stringify(value)}
+        </div>
+      );
+    } else {
+      return (
+        <div>
+          <div>自定义属性</div>
+          <Input value={value} disabled />
+        </div>
+      );
+    }
+  };
+
+  return (
+    <Modal
+      title="详情"
+      visible
+      onOk={() => {
+        props.close();
+      }}
+      onCancel={() => {
+        props.close();
+      }}
+    >
+      {renderValue()}
+    </Modal>
+  );
+};
+
+export default Detail;

+ 319 - 34
src/pages/device/Instance/Detail/MetadataLog/Property/index.tsx

@@ -1,11 +1,14 @@
 import { service } from '@/pages/device/Instance';
 import { useParams } from 'umi';
-import { DatePicker, Modal, Radio, Space, Table } from 'antd';
+import { DatePicker, Modal, Radio, Select, Space, Table, Tabs } from 'antd';
 import type { PropertyMetadata } from '@/pages/device/Product/typings';
 import encodeQuery from '@/utils/encodeQuery';
 import { useEffect, useState } from 'react';
 import moment from 'moment';
-
+import { Axis, Chart, Geom, Legend, Tooltip, Slider } from 'bizcharts';
+import FileComponent from '../../Running/Property/FileComponent';
+import { DownloadOutlined, SearchOutlined } from '@ant-design/icons';
+import Detail from './Detail';
 interface Props {
   visible: boolean;
   close: () => void;
@@ -15,11 +18,20 @@ interface Props {
 const PropertyLog = (props: Props) => {
   const params = useParams<{ id: string }>();
   const { visible, close, data } = props;
+  const list = ['int', 'float', 'double', 'long'];
   const [dataSource, setDataSource] = useState<any>({});
   const [start, setStart] = useState<number>(moment().startOf('day').valueOf());
   const [end, setEnd] = useState<number>(new Date().getTime());
   const [radioValue, setRadioValue] = useState<undefined | 'today' | 'week' | 'month'>('today');
   const [dateValue, setDateValue] = useState<any>(undefined);
+  const [chartsList, setChartsList] = useState<any>([]);
+  const [cycle, setCycle] = useState<string>(
+    list.includes(data.valueType?.type || '') ? '*' : '1m',
+  );
+  const [agg, setAgg] = useState<string>('AVG');
+  const [tab, setTab] = useState<string>('table');
+  const [detailVisible, setDetailVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<any>('');
 
   const columns = [
     {
@@ -29,9 +41,42 @@ const PropertyLog = (props: Props) => {
       render: (text: any) => <span>{text ? moment(text).format('YYYY-MM-DD HH:mm:ss') : ''}</span>,
     },
     {
-      title: '自定义属性',
-      dataIndex: 'formatValue',
-      key: 'formatValue',
+      title: <span>{data.valueType?.type !== 'file' ? '自定义属性' : '文件内容'}</span>,
+      dataIndex: 'value',
+      key: 'value',
+      render: (text: any, record: any) => (
+        <FileComponent type="table" value={{ formatValue: record.value }} data={data} />
+      ),
+    },
+    {
+      title: '操作',
+      dataIndex: 'action',
+      key: 'action',
+      render: (text: any, record: any) => (
+        <a>
+          {data.valueType?.type !== 'file' ? (
+            <SearchOutlined
+              onClick={() => {
+                setDetailVisible(true);
+                setCurrent(record.value);
+              }}
+            />
+          ) : (
+            <DownloadOutlined />
+          )}
+        </a>
+      ),
+    },
+  ];
+
+  const tabList = [
+    {
+      tab: '列表',
+      key: 'table',
+    },
+    {
+      tab: '图表',
+      key: 'charts',
     },
   ];
 
@@ -55,7 +100,47 @@ const PropertyLog = (props: Props) => {
       });
   };
 
+  const queryChartsList = async (startTime?: number, endTime?: number) => {
+    const resp = await service.queryPropertieList(params.id, data.id || '', {
+      paging: false,
+      terms: [
+        {
+          column: 'timestamp$BTW',
+          value: startTime && endTime ? [startTime, endTime] : [],
+          type: 'and',
+        },
+      ],
+    });
+    if (resp.status === 200) {
+      const dataList: any[] = [];
+      resp.result.data.forEach((i: any) => {
+        dataList.push({
+          year: moment(i.timestamp).format('YYYY-MM-DD HH:mm:ss'),
+          value: i.value,
+          type: data?.name || '',
+        });
+      });
+      setChartsList(dataList);
+    }
+  };
+
+  const queryChartsAggList = async (datas: any) => {
+    const resp = await service.queryPropertieInfo(params.id, datas);
+    if (resp.status === 200) {
+      const dataList: any[] = [];
+      resp.result.forEach((i: any) => {
+        dataList.push({
+          year: moment(i.time).format('YYYY-MM-DD HH:mm:ss'),
+          value: Number(i[data.id || '']),
+          type: data?.name || '',
+        });
+      });
+      setChartsList(dataList);
+    }
+  };
+
   useEffect(() => {
+    console.log(data);
     if (visible) {
       handleSearch(
         {
@@ -68,6 +153,162 @@ const PropertyLog = (props: Props) => {
     }
   }, [visible]);
 
+  const scale = {
+    value: { min: 0 },
+    year: {
+      range: [0, 0.96],
+      type: 'timeCat',
+    },
+  };
+
+  const renderComponent = (type: string) => {
+    switch (type) {
+      case 'table':
+        return (
+          <Table
+            size="small"
+            rowKey={'id'}
+            onChange={(page) => {
+              handleSearch(
+                {
+                  pageSize: page.pageSize,
+                  pageIndex: Number(page.current) - 1 || 0,
+                },
+                start,
+                end,
+              );
+            }}
+            dataSource={dataSource?.data || []}
+            columns={columns}
+            pagination={{
+              pageSize: dataSource?.pageSize || 10,
+              showSizeChanger: true,
+              total: dataSource?.total || 0,
+            }}
+          />
+        );
+      case 'charts':
+        return (
+          <div>
+            <div style={{ margin: '10 0', display: 'flex' }}>
+              <div style={{ marginRight: 20 }}>
+                统计周期:
+                <Select
+                  value={cycle}
+                  style={{ width: 120 }}
+                  onChange={(value: string) => {
+                    setCycle(value);
+                    if (cycle === '*') {
+                      queryChartsList(start, end);
+                    } else {
+                      queryChartsAggList({
+                        columns: [
+                          {
+                            property: data.id,
+                            alias: data.id,
+                            agg: agg,
+                          },
+                        ],
+                        query: {
+                          interval: value,
+                          format: 'yyyy-MM-dd HH:mm:ss',
+                          from: start,
+                          to: end,
+                        },
+                      });
+                    }
+                  }}
+                >
+                  {list.includes(data.valueType?.type || '') && (
+                    <Select.Option value="*">实际值</Select.Option>
+                  )}
+                  <Select.Option value="1m">按分钟统计</Select.Option>
+                  <Select.Option value="1h">按小时统计</Select.Option>
+                  <Select.Option value="1d">按天统计</Select.Option>
+                  <Select.Option value="1w">按周统计</Select.Option>
+                  <Select.Option value="1M">按月统计</Select.Option>
+                </Select>
+              </div>
+              {cycle !== '*' && list.includes(data.valueType?.type || '') && (
+                <div>
+                  统计规则:
+                  <Select
+                    defaultValue="AVG"
+                    style={{ width: 120 }}
+                    onChange={(value: string) => {
+                      setAgg(value);
+                      queryChartsAggList({
+                        columns: [
+                          {
+                            property: data.id,
+                            alias: data.id,
+                            agg: value,
+                          },
+                        ],
+                        query: {
+                          interval: cycle,
+                          format: 'yyyy-MM-dd HH:mm:ss',
+                          from: start,
+                          to: end,
+                        },
+                      });
+                    }}
+                  >
+                    <Select.Option value="AVG">平均值</Select.Option>
+                    <Select.Option value="MAX">最大值</Select.Option>
+                    <Select.Option value="MIN">最小值</Select.Option>
+                    <Select.Option value="COUNT">总数</Select.Option>
+                  </Select>
+                </div>
+              )}
+            </div>
+            <div style={{ paddingTop: 15 }}>
+              <Chart height={400} data={chartsList} scale={scale} autoFit>
+                <Legend />
+                <Axis name="year" />
+                <Axis
+                  name="value"
+                  label={{
+                    formatter: (val) => parseFloat(val).toLocaleString(),
+                  }}
+                />
+                <Tooltip showCrosshairs shared />
+                <Geom
+                  type="line"
+                  tooltip={[
+                    'value*type',
+                    (value, name) => {
+                      return {
+                        value: value,
+                        name,
+                      };
+                    },
+                  ]}
+                  position="year*value"
+                  size={2}
+                />
+                <Geom
+                  type="point"
+                  tooltip={false}
+                  position="year*value"
+                  size={4}
+                  shape={'circle'}
+                  style={{
+                    stroke: '#fff',
+                    lineWidth: 1,
+                  }}
+                />
+                <Geom type="area" position="year*value" shape={'circle'} tooltip={false} />
+                <Slider />
+              </Chart>
+            </div>
+          </div>
+        );
+      default:
+        return null;
+    }
+  };
+
   // @ts-ignore
   return (
     <Modal
@@ -98,14 +339,36 @@ const PropertyLog = (props: Props) => {
               setDateValue(undefined);
               setStart(st);
               setEnd(et);
-              handleSearch(
-                {
-                  pageSize: 10,
-                  pageIndex: 0,
-                },
-                st,
-                et,
-              );
+              if (tab === 'charts') {
+                if (list.includes(data.valueType?.type || '')) {
+                  queryChartsList(st, et);
+                } else {
+                  queryChartsAggList({
+                    columns: [
+                      {
+                        property: data.id,
+                        alias: data.id,
+                        agg,
+                      },
+                    ],
+                    query: {
+                      interval: cycle,
+                      format: 'yyyy-MM-dd HH:mm:ss',
+                      from: st,
+                      to: et,
+                    },
+                  });
+                }
+              } else {
+                handleSearch(
+                  {
+                    pageSize: 10,
+                    pageIndex: 0,
+                  },
+                  st,
+                  et,
+                );
+              }
             }}
             style={{ minWidth: 220 }}
           >
@@ -140,28 +403,50 @@ const PropertyLog = (props: Props) => {
           }
         </Space>
       </div>
-
-      <Table
-        size="small"
-        rowKey={'id'}
-        onChange={(page) => {
-          handleSearch(
-            {
-              pageSize: page.pageSize,
-              pageIndex: Number(page.current) - 1 || 0,
-            },
-            start,
-            end,
-          );
-        }}
-        dataSource={dataSource?.data || []}
-        columns={columns}
-        pagination={{
-          pageSize: dataSource?.pageSize || 10,
-          showSizeChanger: true,
-          total: dataSource?.total || 0,
+      <Tabs
+        activeKey={tab}
+        onChange={(key: string) => {
+          setTab(key);
+          if (key === 'charts' && !!data.valueType?.type) {
+            if (list.includes(data.valueType?.type)) {
+              queryChartsList(start, end);
+            } else {
+              setCycle('1m');
+              setAgg('COUNT');
+              queryChartsAggList({
+                columns: [
+                  {
+                    property: data.id,
+                    alias: data.id,
+                    agg: 'COUNT',
+                  },
+                ],
+                query: {
+                  interval: '1m',
+                  format: 'yyyy-MM-dd HH:mm:ss',
+                  from: start,
+                  to: end,
+                },
+              });
+            }
+          }
         }}
-      />
+      >
+        {tabList.map((item) => (
+          <Tabs.TabPane tab={item.tab} key={item.key}>
+            {renderComponent(item.key)}
+          </Tabs.TabPane>
+        ))}
+      </Tabs>
+      {detailVisible && (
+        <Detail
+          close={() => {
+            setDetailVisible(false);
+          }}
+          value={current}
+          type={data.valueType?.type || ''}
+        />
+      )}
     </Modal>
   );
 };

+ 40 - 0
src/pages/device/Instance/Detail/Running/Property/FileComponent/Detail.tsx

@@ -0,0 +1,40 @@
+import LivePlayer from '@/components/Player';
+import { Modal, Image } from 'antd';
+
+interface Props {
+  close: () => void;
+  value: any;
+  type: string;
+}
+
+const Detail = (props: Props) => {
+  const { value, type } = props;
+
+  const renderValue = () => {
+    if (['jpg', 'png', 'tiff'].includes(type)) {
+      return <Image src={value?.formatValue} />;
+    } else if (value?.formatValue.indexOf('https') !== -1) {
+      return <p>域名为https时,不支持访问http地址</p>;
+    } else if (['flv', 'm3u8', 'mp4'].includes(type)) {
+      return <LivePlayer live={false} url={value?.formatValue} />;
+    }
+    return <p>当前仅支持播放.mp4,.flv,.m3u8格式的视频</p>;
+  };
+
+  return (
+    <Modal
+      title="详情"
+      visible
+      onOk={() => {
+        props.close();
+      }}
+      onCancel={() => {
+        props.close();
+      }}
+    >
+      {renderValue()}
+    </Modal>
+  );
+};
+
+export default Detail;

+ 25 - 0
src/pages/device/Instance/Detail/Running/Property/FileComponent/index.less

@@ -0,0 +1,25 @@
+.value {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  height: 60px;
+
+  .other {
+    width: 100%;
+    overflow: hidden;
+    color: #323130;
+    font-weight: 700;
+    font-size: 24px;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+
+  .img {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 60px;
+    height: 100%;
+    border: 1px solid rgba(0, 0, 0, 0.08);
+  }
+}

+ 87 - 0
src/pages/device/Instance/Detail/Running/Property/FileComponent/index.tsx

@@ -0,0 +1,87 @@
+import type { PropertyMetadata } from '@/pages/device/Product/typings';
+import styles from './index.less';
+import Detail from './Detail';
+import { useState } from 'react';
+
+interface Props {
+  data: Partial<PropertyMetadata>;
+  value: any;
+  type: 'card' | 'table';
+}
+
+const imgMap = new Map<any, any>();
+imgMap.set('txt', require('/public/images/running/txt.png'));
+imgMap.set('doc', require('/public/images/running/doc.png'));
+imgMap.set('xls', require('/public/images/running/xls.png'));
+imgMap.set('ppt', require('/public/images/running/ppt.png'));
+imgMap.set('docx', require('/public/images/running/docx.png'));
+imgMap.set('xlsx', require('/public/images/running/xlsx.png'));
+imgMap.set('pptx', require('/public/images/running/pptx.png'));
+imgMap.set('jpg', require('/public/images/running/jpg.png'));
+imgMap.set('png', require('/public/images/running/png.png'));
+imgMap.set('pdf', require('/public/images/running/pdf.png'));
+imgMap.set('tiff', require('/public/images/running/tiff.png'));
+imgMap.set('swf', require('/public/images/running/swf.png'));
+imgMap.set('flv', require('/public/images/running/flv.png'));
+imgMap.set('rmvb', require('/public/images/running/rmvb.png'));
+imgMap.set('mp4', require('/public/images/running/mp4.png'));
+imgMap.set('mvb', require('/public/images/running/mvb.png'));
+imgMap.set('wma', require('/public/images/running/wma.png'));
+imgMap.set('mp3', require('/public/images/running/mp3.png'));
+imgMap.set('other', require('/public/images/running/other.png'));
+
+const FileComponent = (props: Props) => {
+  const { data, value } = props;
+  const [type, setType] = useState<string>('other');
+  const [visible, setVisible] = useState<boolean>(false);
+
+  const renderValue = () => {
+    if (!value?.formatValue) {
+      return <div className={props.type === 'card' ? styles.other : {}}>--</div>;
+    } else if (data?.valueType?.type === 'file') {
+      const flag: string = value?.formatValue.split('.').pop() || 'other';
+      return (
+        <div
+          className={styles.img}
+          onClick={() => {
+            if (['jpg', 'png', 'tiff', 'flv', 'm3u8', 'mp4', 'rmvb', 'mvb'].includes(flag)) {
+              setType(flag);
+              setVisible(true);
+            }
+          }}
+        >
+          <img src={imgMap.get(flag) || imgMap.get('other')} />
+        </div>
+      );
+    } else if (data?.valueType?.type === 'object') {
+      return (
+        <div className={props.type === 'card' ? styles.other : {}}>
+          {JSON.stringify(value?.formatValue)}
+        </div>
+      );
+    } else {
+      return (
+        <div className={props.type === 'card' ? styles.other : {}}>
+          {String(value?.formatValue)}
+        </div>
+      );
+    }
+  };
+
+  return (
+    <div className={styles.value}>
+      {renderValue()}
+      {visible && (
+        <Detail
+          type={type}
+          value={value}
+          close={() => {
+            setVisible(false);
+          }}
+        />
+      )}
+    </div>
+  );
+};
+
+export default FileComponent;

+ 2 - 3
src/pages/device/Instance/Detail/Running/Property/PropertyCard.tsx

@@ -14,6 +14,7 @@ import EditProperty from '@/pages/device/Instance/Detail/Running/Property/EditPr
 import moment from 'moment';
 import Indicators from './Indicators';
 import './PropertyCard.less';
+import FileComponent from './FileComponent';
 
 interface Props {
   data: Partial<PropertyMetadata>;
@@ -88,9 +89,7 @@ const Property = (props: Props) => {
       <Spin spinning={loading}>
         <div>
           <div>{renderTitle(data?.name || '')}</div>
-          <div className="value" style={{ fontWeight: 700, fontSize: '24px', color: '#323130' }}>
-            {value?.formatValue || '--'}
-          </div>
+          <FileComponent type="card" value={value} data={data} />
           <div style={{ marginTop: 10 }}>
             <div style={{ color: 'rgba(0, 0, 0, .65)', fontSize: 12 }}>更新时间</div>
             <div style={{ marginTop: 5, fontSize: 16, color: 'black' }} className="value">

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

@@ -12,6 +12,7 @@ import { useParams } from 'umi';
 import PropertyLog from '../../MetadataLog/Property';
 import moment from 'moment';
 import styles from './index.less';
+import FileComponent from './FileComponent';
 
 interface Props {
   data: Partial<PropertyMetadata>[];
@@ -64,7 +65,7 @@ const Property = (props: Props) => {
       dataIndex: 'value',
       key: 'value',
       render: (text: any, record: any) => (
-        <span>{propertyValue[record.id]?.formatValue || '--'}</span>
+        <FileComponent type="table" value={propertyValue[record.id]} data={record} />
       ),
     },
     {

+ 11 - 0
src/pages/device/Instance/service.ts

@@ -292,6 +292,17 @@ class Service extends BaseService<DeviceInstance> {
     request(`/${SystemConst.API_BASE}/device-instance/${deviceId}/metric/property/${propertyId}`, {
       method: 'GET',
     });
+  //聚合查询设备属性
+  public queryPropertieInfo = (deviceId: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance/${deviceId}/agg/_query`, {
+      method: 'POST',
+      data,
+    });
+  public queryPropertieList = (deviceId: string, property: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance/${deviceId}/property/${property}/_query`, {
+      method: 'POST',
+      data,
+    });
 }
 
 export default Service;

+ 129 - 0
src/pages/link/AccessConfig/Detail/Channel/index.tsx

@@ -0,0 +1,129 @@
+import { Button, Card, Col, Form, Input, message, Row } from 'antd';
+import { useEffect, useState } from 'react';
+import { service } from '@/pages/link/AccessConfig';
+import { ProcotoleMapping } from '../Cloud/Protocol';
+import TitleComponent from '@/components/TitleComponent';
+import { getButtonPermission } from '@/utils/menu';
+import ReactMarkdown from 'react-markdown';
+import { useHistory } from 'umi';
+
+interface Props {
+  change: () => void;
+  data: any;
+  provider: any;
+}
+
+const Media = (props: Props) => {
+  const [form] = Form.useForm();
+  const [config, setConfig] = useState<any>({});
+  const history = useHistory();
+
+  const procotol = props.provider.id === 'modbus-tcp' ? 'modbus-tcp' : 'opc-ua';
+  const name = props.provider.id === 'modbus-tcp' ? 'Modbus' : 'OPCUA';
+
+  useEffect(() => {
+    form.setFieldsValue({
+      name: props.data.name,
+      description: props.data.description,
+    });
+  }, [props.data]);
+
+  useEffect(() => {
+    console.log(ProcotoleMapping);
+    service.getConfigView(procotol, ProcotoleMapping.get(props.provider?.id)).then((resp) => {
+      if (resp.status === 200) {
+        setConfig(resp.result);
+      }
+    });
+  }, [props.provider]);
+
+  return (
+    <Card>
+      {!props.data?.id && (
+        <Button
+          type="link"
+          onClick={() => {
+            props.change();
+          }}
+        >
+          返回
+        </Button>
+      )}
+      <div style={{ margin: '20px 30px' }}>
+        <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
+                  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: procotol,
+                        transport: props.provider.id === 'modbus-tcp' ? 'MODBUS_TCP' : 'OPC_UA',
+                        channel: props.provider.id === 'modbus-tcp' ? 'modbus' : 'opc-ua',
+                      };
+                      const resp: any = await service[!props.data?.id ? 'save' : 'update'](param);
+                      if (resp.status === 200) {
+                        message.success('操作成功!');
+                        history.goBack();
+                      }
+                    } 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>消息协议:{procotol}</p>
+                {config?.document && (
+                  <div>{<ReactMarkdown>{config?.document}</ReactMarkdown> || '--'}</div>
+                )}
+              </div>
+              <TitleComponent data={'设备接入指引'} />
+              <div>
+                <p>1、配置{name}通道</p>
+                <p>2、创建{name}设备接入网关</p>
+                <p>3、创建产品,并选中接入方式为{name}</p>
+                <p>4、添加设备,单独为每一个设备进行数据点绑定</p>
+              </div>
+            </div>
+          </Col>
+        </Row>
+      </div>
+    </Card>
+  );
+};
+
+export default Media;

+ 23 - 0
src/pages/link/AccessConfig/Detail/Cloud/CTWing/index.less

@@ -0,0 +1,23 @@
+.doc {
+  height: 550px;
+  padding: 24px;
+  overflow-y: auto;
+  color: rgba(#000, 0.8);
+  font-size: 14px;
+  background-color: #fafafa;
+
+  h1 {
+    margin: 16px 0;
+    color: rgba(#000, 0.85);
+    font-weight: bold;
+    font-size: 14px;
+
+    &:first-child {
+      margin-top: 0;
+    }
+  }
+
+  .image {
+    margin: 16px 0;
+  }
+}

+ 97 - 0
src/pages/link/AccessConfig/Detail/Cloud/CTWing/index.tsx

@@ -0,0 +1,97 @@
+import { Button, Col, Form, Input, Row, Image } from 'antd';
+import { useEffect } from 'react';
+import styles from './index.less';
+
+interface Props {
+  next: (data: any) => void;
+  data: any;
+}
+
+const CTWing = (props: Props) => {
+  const [form] = Form.useForm();
+  const img = require('/public/images/network/CTWing.jpg');
+
+  useEffect(() => {
+    form.setFieldsValue({
+      ...props.data,
+      apiAddress: 'https://ag-api.ctwing.cn/',
+    });
+  }, [props.data]);
+
+  return (
+    <Row gutter={24}>
+      <Col span={16}>
+        <Form
+          name="CTWing"
+          layout="vertical"
+          form={form}
+          initialValues={{
+            apiAddress: 'https://ag-api.ctwing.cn/',
+          }}
+          onFinish={(values: any) => {
+            props.next(values);
+          }}
+        >
+          <Row gutter={24}>
+            <Col span={12}>
+              <Form.Item
+                label="接口地址"
+                name="apiAddress"
+                rules={[{ required: true, message: '请输入接口地址' }]}
+              >
+                <Input disabled placeholder="请输入接口地址" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="appKey"
+                name="appKey"
+                rules={[{ required: true, message: '请输入appKey' }]}
+              >
+                <Input placeholder="请输入appKey" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="appSecret"
+                name="appSecret"
+                rules={[{ required: true, message: '请输入appSecret' }]}
+              >
+                <Input placeholder="请输入appSecret" />
+              </Form.Item>
+            </Col>
+            <Col span={24}>
+              <Form.Item name="description" label="说明">
+                <Input.TextArea showCount maxLength={200} placeholder="请输入说明" />
+              </Form.Item>
+            </Col>
+            <Col>
+              <Form.Item>
+                <Button type="primary" htmlType="submit">
+                  下一步
+                </Button>
+              </Form.Item>
+            </Col>
+          </Row>
+        </Form>
+      </Col>
+      <Col span={8}>
+        <div className={styles.doc}>
+          <h1>操作指引:</h1>
+          <div>1、创建类型为CTWing的设备接入网关</div>
+          <div>2、创建产品,并选中接入方式为CTWing</div>
+          <div>
+            3、添加设备,为每一台设备设置唯一的IMEI、SN、PSK码(需与CTWingt平台中填写的值一致,若CTWing平台没有对应的设备,将会通过CTWing平台提供的LWM2M协议自动创建)
+          </div>
+          <div className={styles.image}>
+            <Image width="100%" src={img} />
+          </div>
+          <h1>配置说明</h1>
+          <div>1.请将CTWing的AEP平台-应用管理中的App Key和App Secret复制到当前页面</div>
+        </div>
+      </Col>
+    </Row>
+  );
+};
+
+export default CTWing;

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

@@ -0,0 +1,134 @@
+import { TitleComponent } from '@/components';
+import { getButtonPermission } from '@/utils/menu';
+import { Button, Col, Form, Input, message, 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';
+
+interface Props {
+  prev: () => void;
+  data: any;
+  config: any;
+  provider: any;
+  procotol: string;
+}
+
+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>
+            <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) {
+                    message.success('操作成功!');
+                    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'}
+            </p>
+            {props?.provider?.id === 'OneNet' ? (
+              <p>
+                3、添加设备,为每一台设备设置唯一的IMEI、IMSI码(需与OneNet平台中填写的值一致,若OneNet平台没有对应的设备,将会通过OneNet平台提供的LWM2M协议自动创建)
+              </p>
+            ) : (
+              <p>
+                3、添加设备,为每一台设备设置唯一的IMEI、SN、PSK码(需与CTWingt平台中填写的值一致,若CTWing平台没有对应的设备,将会通
+              </p>
+            )}
+          </div>
+        </div>
+      </Col>
+    </Row>
+  );
+};
+
+export default Finish;

+ 23 - 0
src/pages/link/AccessConfig/Detail/Cloud/OneNet/index.less

@@ -0,0 +1,23 @@
+.doc {
+  height: 550px;
+  padding: 24px;
+  overflow-y: auto;
+  color: rgba(#000, 0.8);
+  font-size: 14px;
+  background-color: #fafafa;
+
+  h1 {
+    margin: 16px 0;
+    color: rgba(#000, 0.85);
+    font-weight: bold;
+    font-size: 14px;
+
+    &:first-child {
+      margin-top: 0;
+    }
+  }
+
+  .image {
+    margin: 16px 0;
+  }
+}

+ 130 - 0
src/pages/link/AccessConfig/Detail/Cloud/OneNet/index.tsx

@@ -0,0 +1,130 @@
+import { QuestionCircleOutlined } from '@ant-design/icons';
+import { Button, Col, Form, Input, Row, Tooltip, Image } from 'antd';
+import { useEffect } from 'react';
+import styles from './index.less';
+
+interface Props {
+  next: (data: any) => void;
+  data: any;
+}
+
+const OneNet = (props: Props) => {
+  const img = require('/public/images/network/OneNet.jpg');
+
+  const [form] = Form.useForm();
+
+  useEffect(() => {
+    form.setFieldsValue({
+      ...props.data,
+      apiAddress: 'https://ag-api.ctwing.cn/',
+    });
+  }, [props.data]);
+
+  return (
+    <Row gutter={24}>
+      <Col span={16}>
+        <Form
+          name="onenet"
+          layout="vertical"
+          form={form}
+          initialValues={{
+            apiAddress: 'https://api.heclouds.com/',
+          }}
+          onFinish={(values: any) => {
+            props.next(values);
+          }}
+        >
+          <Row gutter={24}>
+            <Col span={24}>
+              <Form.Item
+                label={
+                  <span>
+                    接口地址
+                    <Tooltip title={`同步物联网平台设备数据到OneNet`}>
+                      <QuestionCircleOutlined />
+                    </Tooltip>
+                  </span>
+                }
+                name="apiAddress"
+                rules={[{ required: true, message: '请输入接口地址' }]}
+              >
+                <Input disabled placeholder="请输入接口地址" />
+              </Form.Item>
+            </Col>
+            <Col span={24}>
+              <Form.Item
+                label={<span>apiKey</span>}
+                name="apiKey"
+                rules={[{ required: true, message: '请输入apiKey' }]}
+              >
+                <Input placeholder="请输入apiKey" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label={
+                  <span>
+                    通知Token
+                    <Tooltip title={`接收OneNet推送的Token地址`}>
+                      <QuestionCircleOutlined />
+                    </Tooltip>
+                  </span>
+                }
+                name="validateToken"
+                rules={[{ required: true, message: '请输入通知Token' }]}
+              >
+                <Input placeholder="请输入通知Token" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label={
+                  <span>
+                    aesKey
+                    <Tooltip title={`OneNet 端生成的消息加密key`}>
+                      <QuestionCircleOutlined />
+                    </Tooltip>
+                  </span>
+                }
+                name="aesKey"
+              >
+                <Input placeholder="请输入aesKey" />
+              </Form.Item>
+            </Col>
+            <Col span={24}>
+              <Form.Item name="description" label="说明">
+                <Input.TextArea showCount maxLength={200} placeholder="请输入说明" />
+              </Form.Item>
+            </Col>
+            <Col>
+              <Form.Item>
+                <Button type="primary" htmlType="submit">
+                  下一步
+                </Button>
+              </Form.Item>
+            </Col>
+          </Row>
+        </Form>
+      </Col>
+      <Col span={8}>
+        <div className={styles.doc}>
+          <h1>操作指引:</h1>
+          <div>1、创建类型为OneNet的设备接入网关</div>
+          <div>2、创建产品,并选中接入方式为OneNet</div>
+          <div>
+            3、添加设备,为每一台设备设置唯一的IMEI、IMSI码(需与OneNet平台中填写的值一致,若OneNet平台没有对应的设备,将会通过OneNet平台提供的LWM2M协议自动创建)
+          </div>
+          <div className={styles.image}>
+            <Image width="100%" src={img} />
+          </div>
+          <h1>配置说明</h1>
+          <div>1.接口地址需要与OneNet数据推送配置中地址一致</div>
+          <div>2.通知Token需要与OneNet数据推送配置中Token一致</div>
+          <div>3.aesKey需要与OneNet数据推送配置中aesKey一致</div>
+        </div>
+      </Col>
+    </Row>
+  );
+};
+
+export default OneNet;

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

@@ -0,0 +1,18 @@
+.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%;
+}

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

@@ -0,0 +1,157 @@
+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', 'OneNet');
+ProcotoleMapping.set('Ctwing', 'Ctwing');
+ProcotoleMapping.set('modbus-tcp', 'MODBUS_TCP');
+ProcotoleMapping.set('opc-ua', 'OPC_UA');
+
+interface Props {
+  provider: any;
+  data: string;
+  prev: () => void;
+  next: (data: string) => void;
+}
+
+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' }}
+        />
+        <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']) ? (
+                '请联系管理员进行配置'
+              ) : (
+                <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;

+ 81 - 0
src/pages/link/AccessConfig/Detail/Cloud/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;
+}

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

@@ -0,0 +1,117 @@
+import { Button, Card, Steps } from 'antd';
+import { useEffect, useState } from 'react';
+import styles from './index.less';
+import { ExclamationCircleFilled } from '@ant-design/icons';
+import OneNet from './OneNet';
+import CTWing from './CTWing';
+import Protocol from './Protocol';
+import Finish from './Finish';
+
+interface Props {
+  change: () => void;
+  data: any;
+  provider: any;
+}
+
+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 prev = () => {
+    setCurrent(current - 1);
+  };
+
+  const next = (param: any) => {
+    setConfig(param);
+    setCurrent(current + 1);
+  };
+
+  useEffect(() => {
+    setCurrent(0);
+    setConfig(props.data?.configuration || {});
+    setProcotolCurrent(props.data?.protocol);
+  }, [props.data]);
+
+  const renderSteps = (cur: number) => {
+    switch (cur) {
+      case 0:
+        return (
+          <div>
+            <div className={styles.alert}>
+              <ExclamationCircleFilled style={{ marginRight: 10 }} />
+              通过{props?.provider?.id === 'OneNet' ? 'OneNet' : 'CTWing'}
+              平台的HTTP推送服务进行数据接入
+            </div>
+            <div style={{ marginTop: 10 }}>
+              {props?.provider?.id === 'OneNet' ? (
+                <OneNet data={config} next={(param: any) => next(param)} />
+              ) : (
+                <CTWing data={config} next={(param: any) => next(param)} />
+              )}
+            </div>
+          </div>
+        );
+      case 1:
+        return (
+          <div>
+            <div className={styles.alert}>
+              <ExclamationCircleFilled style={{ marginRight: 10 }} />
+              只能选择HTTP通信方式的协议
+            </div>
+            <div style={{ marginTop: 10 }}>
+              <Protocol
+                data={procotolCurrent}
+                provider={props.provider}
+                next={(param: string) => {
+                  setProcotolCurrent(param);
+                  setCurrent(current + 1);
+                }}
+                prev={prev}
+              />
+            </div>
+          </div>
+        );
+      case 2:
+        return (
+          <Finish
+            procotol={procotolCurrent}
+            provider={props.provider}
+            data={props.data}
+            config={config}
+            prev={prev}
+          />
+        );
+      default:
+        return null;
+    }
+  };
+
+  return (
+    <Card>
+      {!props.data?.id && (
+        <Button
+          type="link"
+          onClick={() => {
+            props.change();
+          }}
+        >
+          返回
+        </Button>
+      )}
+      <div className={styles.box}>
+        <div className={styles.steps}>
+          <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 Cloud;

+ 69 - 83
src/pages/link/AccessConfig/Detail/Provider/index.tsx

@@ -1,6 +1,6 @@
+import { useEffect, useState } from 'react';
 import { TitleComponent } from '@/components';
 import { Button, Card, Col, Row } from 'antd';
-import { useEffect, useState } from 'react';
 import styles from './index.less';
 
 interface Props {
@@ -10,109 +10,95 @@ interface Props {
 
 const Provider = (props: Props) => {
   const [dataSource, setDataSource] = useState<any[]>([]);
-  const [mediaSource, setMediaSource] = useState<any[]>([]);
 
   useEffect(() => {
     const media: any[] = [];
-    const data: any = [];
+    const network: any[] = [];
+    const cloud: any[] = [];
+    const channel: any[] = [];
     (props?.data || []).map((item: any) => {
       if (item.id === 'fixed-media' || item.id === 'gb28181-2016') {
         media.push(item);
+      } else if (item.id === 'OneNet' || item.id === 'Ctwing') {
+        cloud.push(item);
+      } else if (item.id === 'modbus-tcp' || item.id === 'opc-ua') {
+        channel.push(item);
       } else {
-        data.push(item);
+        network.push(item);
       }
     });
-    setDataSource(data);
-    setMediaSource(media);
+
+    setDataSource([
+      {
+        type: 'network',
+        list: [...network],
+        title: '自定义设备接入',
+      },
+      {
+        type: 'media',
+        list: [...media],
+        title: '视频类设备接入',
+      },
+      {
+        type: 'cloud',
+        list: [...cloud],
+        title: '云平台接入',
+      },
+      {
+        type: 'channel',
+        list: [...channel],
+        title: '通道类设备接入',
+      },
+    ]);
   }, [props.data]);
 
   return (
-    <>
-      <Card>
-        <TitleComponent data={'自定义设备接入'} />
-        <Row gutter={[16, 16]}>
-          {dataSource.map((item) => (
-            <Col key={item.name} span={12}>
-              <Card style={{ width: '100%' }} hoverable>
-                <div
-                  style={{
-                    width: '100%',
-                    display: 'flex',
-                    alignItems: 'center',
-                    justifyContent: 'space-between',
-                  }}
-                >
+    <div>
+      {dataSource.map((i) => (
+        <Card key={i.type} style={{ marginTop: 20 }}>
+          <TitleComponent data={i.title} />
+          <Row gutter={[16, 16]}>
+            {(i?.list || []).map((item: any) => (
+              <Col key={item.name} span={12}>
+                <Card style={{ width: '100%' }} hoverable>
                   <div
                     style={{
+                      width: '100%',
                       display: 'flex',
-                      width: 'calc(100% - 70px)',
+                      alignItems: 'center',
+                      justifyContent: 'space-between',
                     }}
                   >
-                    <div className={styles.images}>{item.name}</div>
-                    <div style={{ margin: '10px', width: 'calc(100% - 84px)' }}>
-                      <div style={{ fontWeight: 600 }}>{item.name}</div>
-                      <div className={styles.desc}>{item?.description || '--'}</div>
-                    </div>
-                  </div>
-                  <div style={{ width: '70px' }}>
-                    <Button
-                      type="primary"
-                      onClick={() => {
-                        props.change(item, 'network');
+                    <div
+                      style={{
+                        display: 'flex',
+                        width: 'calc(100% - 70px)',
                       }}
                     >
-                      接入
-                    </Button>
-                  </div>
-                </div>
-              </Card>
-            </Col>
-          ))}
-        </Row>
-      </Card>
-      <Card style={{ marginTop: 20 }}>
-        <TitleComponent data={'视频类设备接入'} />
-        <Row gutter={[16, 16]}>
-          {mediaSource.map((item) => (
-            <Col key={item.name} span={12}>
-              <Card style={{ width: '100%' }} hoverable>
-                <div
-                  style={{
-                    width: '100%',
-                    display: 'flex',
-                    alignItems: 'center',
-                    justifyContent: 'space-between',
-                  }}
-                >
-                  <div
-                    style={{
-                      display: 'flex',
-                      width: 'calc(100% - 70px)',
-                    }}
-                  >
-                    <div className={styles.images}>{item.name}</div>
-                    <div style={{ margin: '10px', width: 'calc(100% - 84px)' }}>
-                      <div style={{ fontWeight: 600 }}>{item.name}</div>
-                      <div className={styles.desc}>{item.description || '--'}</div>
+                      <div className={styles.images}>{item.name}</div>
+                      <div style={{ margin: '10px', width: 'calc(100% - 84px)' }}>
+                        <div style={{ fontWeight: 600 }}>{item.name}</div>
+                        <div className={styles.desc}>{item?.description || '--'}</div>
+                      </div>
+                    </div>
+                    <div style={{ width: '70px' }}>
+                      <Button
+                        type="primary"
+                        onClick={() => {
+                          props.change(item, i.type);
+                        }}
+                      >
+                        接入
+                      </Button>
                     </div>
                   </div>
-                  <div style={{ width: '70px' }}>
-                    <Button
-                      type="primary"
-                      onClick={() => {
-                        props.change(item, 'media');
-                      }}
-                    >
-                      接入
-                    </Button>
-                  </div>
-                </div>
-              </Card>
-            </Col>
-          ))}
-        </Row>
-      </Card>
-    </>
+                </Card>
+              </Col>
+            ))}
+          </Row>
+        </Card>
+      ))}
+    </div>
   );
 };
 

+ 36 - 2
src/pages/link/AccessConfig/Detail/index.tsx

@@ -6,6 +6,8 @@ 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';
 
 type LocationType = {
   id?: string;
@@ -17,7 +19,9 @@ const Detail = () => {
   const [loading, setLoading] = useState<boolean>(true);
   const [data, setData] = useState<any>({});
   const [provider, setProvider] = useState<any>({});
-  const [type, setType] = useState<'media' | 'network' | undefined>(undefined);
+  const [type, setType] = useState<'media' | 'network' | 'cloud' | 'channel' | undefined>(
+    undefined,
+  );
 
   const [dataSource, setDataSource] = useState<any[]>([]);
 
@@ -41,6 +45,16 @@ const Detail = () => {
               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');
             }
@@ -80,6 +94,26 @@ const Detail = () => {
             }}
           />
         );
+      case 'cloud':
+        return (
+          <Cloud
+            data={data}
+            provider={provider}
+            change={() => {
+              setVisible(true);
+            }}
+          />
+        );
+      case 'channel':
+        return (
+          <Channel
+            data={data}
+            provider={provider}
+            change={() => {
+              setVisible(true);
+            }}
+          />
+        );
       default:
         return null;
     }
@@ -91,7 +125,7 @@ const Detail = () => {
         {visible ? (
           <Provider
             data={dataSource}
-            change={(param: any, typings: 'media' | 'network') => {
+            change={(param: any, typings: 'media' | 'network' | 'cloud' | 'channel') => {
               setType(typings);
               setProvider(param);
               setData({});