xieyonghong 3 سال پیش
والد
کامیت
9da5395b11
40فایلهای تغییر یافته به همراه2963 افزوده شده و 1110 حذف شده
  1. 2 2
      config/proxy.ts
  2. BIN
      public/images/access/edge.png
  3. 10 1
      src/components/RadioCard/index.tsx
  4. 60 0
      src/pages/home/components/CardStatics.tsx
  5. 23 0
      src/pages/home/components/index.less
  6. 217 3
      src/pages/iot-card/Home/index.tsx
  7. 21 0
      src/pages/iot-card/Home/service.ts
  8. 415 0
      src/pages/iot-card/Platform/Detail/index.tsx
  9. 35 0
      src/pages/iot-card/Platform/doc/index.less
  10. 161 0
      src/pages/iot-card/Platform/doc/index.tsx
  11. 209 1
      src/pages/iot-card/Platform/index.tsx
  12. 12 0
      src/pages/iot-card/Platform/service.ts
  13. 35 0
      src/pages/iot-card/Recharge/detail.tsx
  14. 163 1
      src/pages/iot-card/Recharge/index.tsx
  15. 215 0
      src/pages/iot-card/Recharge/topUp.tsx
  16. 63 1
      src/pages/iot-card/Record/index.tsx
  17. 12 0
      src/pages/iot-card/Record/service.ts
  18. 37 662
      src/pages/link/AccessConfig/Detail/Access/index.tsx
  19. 5 6
      src/pages/link/AccessConfig/Detail/Channel/index.tsx
  20. 0 141
      src/pages/link/AccessConfig/Detail/Cloud/Finish/index.tsx
  21. 0 18
      src/pages/link/AccessConfig/Detail/Cloud/Protocol/index.less
  22. 0 162
      src/pages/link/AccessConfig/Detail/Cloud/Protocol/index.tsx
  23. 13 10
      src/pages/link/AccessConfig/Detail/Cloud/index.tsx
  24. 81 0
      src/pages/link/AccessConfig/Detail/Edge/index.less
  25. 95 0
      src/pages/link/AccessConfig/Detail/Edge/index.tsx
  26. 11 39
      src/pages/link/AccessConfig/Detail/Provider/index.tsx
  27. 0 0
      src/pages/link/AccessConfig/Detail/components/CTWing/index.less
  28. 0 0
      src/pages/link/AccessConfig/Detail/components/CTWing/index.tsx
  29. 378 0
      src/pages/link/AccessConfig/Detail/components/Finish/index.tsx
  30. 103 0
      src/pages/link/AccessConfig/Detail/components/Network/index.less
  31. 210 0
      src/pages/link/AccessConfig/Detail/components/Network/index.tsx
  32. 0 0
      src/pages/link/AccessConfig/Detail/components/OneNet/index.less
  33. 0 0
      src/pages/link/AccessConfig/Detail/components/OneNet/index.tsx
  34. 75 0
      src/pages/link/AccessConfig/Detail/components/Protocol/index.less
  35. 189 0
      src/pages/link/AccessConfig/Detail/components/Protocol/index.tsx
  36. 42 0
      src/pages/link/AccessConfig/Detail/data.ts
  37. 65 49
      src/pages/link/AccessConfig/Detail/index.tsx
  38. 0 12
      src/pages/notice/Config/Detail/index.tsx
  39. 4 2
      src/pages/oauth/index.tsx
  40. 2 0
      src/utils/menu/router.ts

+ 2 - 2
config/proxy.ts

@@ -17,8 +17,8 @@ export default {
       // 测试环境
       // target: 'http://120.77.179.54:8844/',
       // ws: 'ws://120.77.179.54:8844/',
-      // target: 'http://192.168.32.65:8850/',
-      // ws: 'ws://192.168.32.65:8850/',
+      // target: 'http://192.168.32.65:8844/',
+      // ws: 'ws://192.168.32.65:8844/',
       //v2环境
       // ws: 'ws://47.109.52.230:8844',
       // target: 'http://47.109.52.230:8844',

BIN
public/images/access/edge.png


+ 10 - 1
src/components/RadioCard/index.tsx

@@ -10,6 +10,7 @@ interface RadioCardItem {
   label: string;
   value: string;
   imgUrl?: string;
+  imgSize?: number[];
 }
 
 export interface RadioCardProps {
@@ -80,7 +81,15 @@ export default (props: RadioCardProps) => {
               }
             }}
           >
-            {item.imgUrl && <img width={32} height={32} src={item.imgUrl} alt={''} />}
+            {item.imgUrl && (
+              <img
+                width={32}
+                height={32}
+                src={item.imgUrl}
+                alt={''}
+                style={{ width: item.imgSize?.[0], height: item.imgSize?.[1] }}
+              />
+            )}
             <span>{item.label}</span>
             <div className={'checked-icon'}>
               <div>

+ 60 - 0
src/pages/home/components/CardStatics.tsx

@@ -0,0 +1,60 @@
+import Title from '@/pages/home/components/Title';
+import React from 'react';
+import './index.less';
+
+type StatisticsItem = {
+  name: string;
+  value: number | string;
+  children: React.ReactNode | string;
+  permission?: any;
+  node?: any;
+};
+
+interface StatisticsProps {
+  extra?: React.ReactNode | string;
+  style?: any;
+  height?: any;
+  data: StatisticsItem[];
+  title: string;
+}
+
+const defaultImage = require('/public/images/home/top-1.png');
+
+const CardStatistics = (props: StatisticsProps) => {
+  return (
+    <div className={'home-statistics'} style={{ height: props.height }}>
+      <Title title={props.title} extra={props.extra} />
+      <div className={'home-statistics-body'} style={props.style}>
+        {props.data.map((item) => (
+          <div className={'home-guide-item'} key={item.name}>
+            <div className={'item-english'}>{item.name}</div>
+            {item.node ? (
+              <div style={{ display: 'flex', marginTop: 15, width: '60%' }}>
+                {item.node.map((i: any) => (
+                  <div style={{ marginRight: 7 }}>
+                    <div style={{ fontSize: '14px', fontWeight: 'bold' }}>{i.value}</div>
+                    <div className={`state ${i.className}`}>{i.name}</div>
+                  </div>
+                ))}
+              </div>
+            ) : (
+              <div className={'item-title'}>{item.permission ? item.permission : item.value}</div>
+            )}
+
+            {typeof item.children === 'string' ? (
+              <div className={`item-index`}>
+                <img src={item.children || defaultImage} />
+              </div>
+            ) : (
+              <div className={'item-index-echarts'} style={{ height: 75, width: 110 }}>
+                {item.children}
+              </div>
+            )}
+          </div>
+        ))}
+      </div>
+    </div>
+  );
+};
+
+export default CardStatistics;

+ 23 - 0
src/pages/home/components/index.less

@@ -26,6 +26,29 @@
   background: linear-gradient(135.62deg, #f6f7fd 22.27%, rgba(255, 255, 255, 0.86) 91.82%);
   border-radius: 2px;
   box-shadow: 0 4px 18px #efefef;
+  .state {
+    position: relative;
+    padding-left: 8px;
+    &::before {
+      position: absolute;
+      top: 7px;
+      left: 0;
+      display: inline-block;
+      width: 6px;
+      height: 6px;
+      margin-right: 2px;
+      content: '';
+    }
+    &.normal::before {
+      background: #85a5ff;
+    }
+    &.notActive::before {
+      background: #f29b55;
+    }
+    &.stopped::before {
+      background: #c4c4c4;
+    }
+  }
 
   &.pointer {
     cursor: pointer;

+ 217 - 3
src/pages/iot-card/Home/index.tsx

@@ -1,4 +1,218 @@
-const Home = () => {
-  return <>首页</>;
+import { PageContainer } from '@ant-design/pro-layout';
+import { Col, message, Row } from 'antd';
+import { PermissionButton } from '@/components';
+import { getMenuPathByCode } from '@/utils/menu';
+import useHistory from '@/hooks/route/useHistory';
+import { useEffect, useRef, useState } from 'react';
+import { Body, Guide } from '@/pages/home/components';
+import CardStatistics from '@/pages/home/components/CardStatics';
+import Echarts from '@/components/DashBoard/echarts';
+import { EChartsOption } from 'echarts';
+import moment from 'moment';
+import Service from './service';
+
+export const service = new Service('');
+
+export default () => {
+  const dashBoardUrl = getMenuPathByCode('iot-card/Dashboard');
+  const platformUrl = getMenuPathByCode('iot-card/Platform/Detail');
+  const recordUrl = getMenuPathByCode('iot-card/Record');
+  const cardUrl = getMenuPathByCode('iot-card/CardManagement');
+
+  const [options, setOptions] = useState<EChartsOption>({});
+  const [cardOptions, setCardOptions] = useState<EChartsOption>({});
+  const currentSource = useRef(0);
+  const pieChartData = useRef([
+    {
+      key: 'using',
+      name: '正常',
+      value: 0,
+      className: 'normal',
+    },
+    {
+      key: 'toBeActivated',
+      name: '未激活',
+      value: 0,
+      className: 'notActive',
+    },
+    {
+      key: 'deactivate',
+      name: '停用',
+      value: 0,
+      className: 'stopped',
+    },
+  ]);
+
+  const { permission: paltformPermission } = PermissionButton.usePermission('iot-card/Platform');
+
+  const history = useHistory();
+
+  //昨日流量
+  const getTodayFlow = async () => {
+    const beginTime = moment().subtract(1, 'days').startOf('day').valueOf();
+    const endTime = moment().subtract(1, 'days').endOf('day').valueOf();
+    const res = await service.queryFlow(beginTime, endTime, { orderBy: 'date' });
+    if (res.status === 200) {
+      res.result.map((item: any) => {
+        currentSource.current += parseFloat(item.value.toFixed(2));
+      });
+    }
+  };
+
+  //15天流量
+  const get15DaysTrafficConsumption = async () => {
+    const beginTime = moment().subtract(15, 'days').startOf('day').valueOf();
+    const endTime = moment().subtract(1, 'days').endOf('day').valueOf();
+    const resp = await service.queryFlow(beginTime, endTime, { orderBy: 'date' });
+    if (resp.status === 200) {
+      setOptions({
+        tooltip: {},
+        xAxis: {
+          show: false,
+          data: resp.result.map((item: any) => item.date).reverse(),
+        },
+        yAxis: {
+          show: false,
+        },
+        series: [
+          {
+            name: '流量消耗',
+            type: 'bar',
+            color: '#FACD89',
+            // barWidth: '5%', // 设单柱状置宽度
+            showBackground: true, //设置柱状的背景虚拟
+            data: resp.result.map((m: any) => parseFloat(m.value.toFixed(2))).reverse(),
+          },
+        ],
+      });
+    }
+  };
+  //获取物联卡状态数据
+  const getStateCard = async () => {
+    Promise.all(
+      pieChartData.current.map((item) => {
+        const params = {
+          terms: [
+            {
+              terms: [
+                {
+                  column: 'cardStateType',
+                  termType: 'eq',
+                  value: item.key,
+                },
+              ],
+            },
+          ],
+        };
+        return service.list(params);
+      }),
+    ).then((res) => {
+      res.forEach((item, index) => {
+        if (item && item.status === 200) {
+          pieChartData.current[index].value = item.result.total;
+        }
+      });
+      setCardOptions({
+        tooltip: {
+          trigger: 'item',
+          formatter: '{b}: {c} ({d}%)',
+        },
+        color: ['#85a5ff', '#f29b55', '#c4c4c4'],
+        series: [
+          {
+            name: '',
+            type: 'pie',
+            avoidLabelOverlap: true, //是否启用防止标签重叠策略
+            radius: ['50%', '90%'],
+            center: ['50%', '50%'],
+            itemStyle: {
+              borderColor: 'rgba(255,255,255,1)',
+              borderWidth: 2,
+            },
+            label: {
+              show: false,
+            },
+            data: pieChartData.current,
+          },
+        ],
+      });
+    });
+  };
+
+  const guideList = [
+    {
+      key: 'EQUIPMENT',
+      name: '平台对接',
+      english: 'STEP1',
+      auth: !!paltformPermission.update,
+      url: platformUrl,
+    },
+    {
+      key: 'SCREEN',
+      name: '物联卡管理',
+      english: 'STEP2',
+      auth: !!cardUrl,
+      url: cardUrl,
+      param: { save: true },
+    },
+    {
+      key: 'CASCADE',
+      name: '操作记录',
+      english: 'STEP3',
+      auth: !!recordUrl,
+      url: recordUrl,
+    },
+  ];
+
+  useEffect(() => {
+    getTodayFlow();
+    get15DaysTrafficConsumption();
+    getStateCard();
+  }, []);
+
+  return (
+    <PageContainer>
+      <Row gutter={24}>
+        <Col span={14}>
+          <Guide title={'物联卡引导'} data={guideList} />
+        </Col>
+        <Col span={10}>
+          <CardStatistics
+            title={'基础统计'}
+            data={[
+              {
+                name: '昨日流量统计',
+                value: `${currentSource.current}M`,
+                children: <Echarts options={options} />,
+              },
+              {
+                name: '物联卡',
+                value: 0,
+                node: pieChartData.current,
+                children: <Echarts options={cardOptions} />,
+              },
+            ]}
+            extra={
+              <div style={{ fontSize: 14, fontWeight: 400 }}>
+                <a
+                  onClick={() => {
+                    if (!!dashBoardUrl) {
+                      history.push(`${dashBoardUrl}`);
+                    } else {
+                      message.warning('暂无权限,请联系管理员');
+                    }
+                  }}
+                >
+                  详情
+                </a>
+              </div>
+            }
+          />
+        </Col>
+        <Col span={24}>
+          <Body title={'平台架构图'} english={'PLATFORM ARCHITECTURE DIAGRAM'} />
+        </Col>
+      </Row>
+    </PageContainer>
+  );
 };
-export default Home;

+ 21 - 0
src/pages/iot-card/Home/service.ts

@@ -0,0 +1,21 @@
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+import BaseService from '@/utils/BaseService';
+
+class Service extends BaseService<any> {
+  queryFlow = (beginTime: any, endTime: any, data: any) =>
+    request(`${SystemConst.API_BASE}/network/flow/_query/${beginTime}/${endTime}`, {
+      method: 'POST',
+      data,
+    });
+  queryState = (status: any) =>
+    request(`${SystemConst.API_BASE}/network/card/${status}/state/_count`, {
+      method: 'GET',
+    });
+  list = (data: any) =>
+    request(`${SystemConst.API_BASE}/network/card/_query`, {
+      method: 'POST',
+      data,
+    });
+}
+export default Service;

+ 415 - 0
src/pages/iot-card/Platform/Detail/index.tsx

@@ -0,0 +1,415 @@
+import { RadioCard, TitleComponent } from '@/components';
+import { PageContainer } from '@ant-design/pro-layout';
+import { Form, FormButtonGroup, FormGrid, FormItem, Input } from '@formily/antd';
+import { createForm, Field, onFieldReact, onFormInit } from '@formily/core';
+import { createSchemaField, observer } from '@formily/react';
+import { Button, Card, Col, Row } from 'antd';
+import { useEffect, useMemo, useState } from 'react';
+import { useParams } from 'umi';
+import { onlyMessage, useAsyncDataSource } from '@/utils/util';
+import { service } from '../index';
+import { useModel } from '@@/plugin-model/useModel';
+import Doc from '../doc';
+
+const Detail = observer(() => {
+  const params = useParams<{ id: string }>();
+  const { initialState } = useModel('@@initialState');
+  const [docType, setDocType] = useState('');
+
+  const form = useMemo(
+    () =>
+      createForm({
+        validateFirst: true,
+        effects() {
+          onFormInit(async (form1) => {
+            if (params.id === ':id') return;
+            const resp = await service.detail(params.id);
+            if (resp.status === 200) {
+              form1.setValues(resp.result);
+            }
+          });
+          onFieldReact('operatorName', (field) => {
+            const value = (field as Field).value;
+            setDocType(value);
+            // console.log(value)
+          });
+        },
+      }),
+    [],
+  );
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      FormGrid,
+      Input,
+      RadioCard,
+    },
+  });
+
+  const schema: any = {
+    type: 'object',
+    properties: {
+      operatorName: {
+        title: '平台类型',
+        'x-component': 'RadioCard',
+        'x-decorator': 'FormItem',
+        'x-decorator-props': {
+          gridSpan: 1,
+        },
+        default: params.id === ':id' ? 'onelink' : undefined,
+        'x-component-props': {
+          model: 'singular',
+          itemStyle: {
+            display: 'flex',
+            flexDirection: 'column',
+            justifyContent: 'space-around',
+            minWidth: '130px',
+          },
+          options: [
+            {
+              label: '移动OneLink',
+              value: 'onelink',
+              imgUrl: require('/public/images/iot-card/onelink.png'),
+              imgSize: [78, 20],
+            },
+            {
+              label: '电信Ctwing',
+              value: 'ctwing',
+              imgUrl: require('/public/images/iot-card/ctwingcmp.png'),
+              imgSize: [52, 25],
+            },
+            {
+              label: '联通Unicom',
+              value: 'unicom',
+              imgUrl: require('/public/images/iot-card/unicom.png'),
+              imgSize: [56, 41],
+            },
+          ],
+        },
+        'x-validator': [
+          {
+            required: true,
+            message: '请选择类型',
+          },
+        ],
+      },
+      name: {
+        type: 'string',
+        title: '名称',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-component-props': {
+          placeholder: '请输入名称',
+        },
+        'x-validator': [
+          {
+            max: 64,
+            message: '最多可输入64个字符',
+          },
+          {
+            required: true,
+            message: '请输入名称',
+          },
+        ],
+      },
+      onelink: {
+        type: 'void',
+        'x-reactions': {
+          dependencies: ['.operatorName'],
+          fulfill: {
+            state: {
+              visible: '{{$deps[0] ==="onelink"}}',
+            },
+          },
+        },
+        properties: {
+          config: {
+            type: 'object',
+            properties: {
+              appId: {
+                type: 'string',
+                title: 'App ID',
+                required: true,
+                'x-decorator': 'FormItem',
+                'x-component': 'Input',
+                'x-component-props': {
+                  placeholder: '请输入App ID',
+                },
+                'x-validator': [
+                  {
+                    max: 64,
+                    message: '最多可输入64个字符',
+                  },
+                  {
+                    required: true,
+                    message: '请输入App ID',
+                  },
+                ],
+              },
+              passWord: {
+                type: 'string',
+                title: 'Password',
+                required: true,
+                'x-decorator': 'FormItem',
+                'x-component': 'Input',
+                'x-component-props': {
+                  placeholder: '请输入Password',
+                },
+                'x-validator': [
+                  {
+                    max: 64,
+                    message: '最多可输入64个字符',
+                  },
+                  {
+                    required: true,
+                    message: '请输入App ID',
+                  },
+                ],
+              },
+              apiAddr: {
+                type: 'string',
+                title: '接口地址',
+                required: true,
+                'x-decorator': 'FormItem',
+                'x-component': 'Input',
+                'x-component-props': {
+                  placeholder: '请输入接口地址',
+                },
+                'x-validator': [
+                  {
+                    max: 64,
+                    message: '最多可输入64个字符',
+                  },
+                  {
+                    required: true,
+                    message: '请输入接口地址',
+                  },
+                ],
+              },
+            },
+          },
+        },
+      },
+      ctwing: {
+        type: 'void',
+        'x-reactions': {
+          dependencies: ['.operatorName'],
+          fulfill: {
+            state: {
+              visible: '{{$deps[0] ==="ctwing"}}',
+            },
+          },
+        },
+
+        properties: {
+          config: {
+            type: 'object',
+            properties: {
+              userId: {
+                type: 'string',
+                title: '用户id',
+                required: true,
+                'x-decorator': 'FormItem',
+                'x-component': 'Input',
+                'x-component-props': {
+                  placeholder: '请输入用户id',
+                },
+                'x-validator': [
+                  {
+                    max: 64,
+                    message: '最多可输入64个字符',
+                  },
+                  {
+                    required: true,
+                    message: '请输入用户id',
+                  },
+                ],
+              },
+              passWord: {
+                type: 'string',
+                title: 'Password',
+                required: true,
+                'x-decorator': 'FormItem',
+                'x-component': 'Input',
+                'x-component-props': {
+                  placeholder: '请输入Password',
+                },
+                'x-validator': [
+                  {
+                    required: true,
+                    message: '请输入Password',
+                  },
+                ],
+              },
+              secretKey: {
+                type: 'string',
+                title: 'secretKey',
+                required: true,
+                'x-decorator': 'FormItem',
+                'x-component': 'Input',
+                'x-component-props': {
+                  placeholder: '请输入secretKey',
+                },
+                'x-validator': [
+                  {
+                    max: 64,
+                    message: '最多可输入64个字符',
+                  },
+                  {
+                    required: true,
+                    message: '请输入secretKey',
+                  },
+                ],
+              },
+            },
+          },
+        },
+      },
+      unicom: {
+        type: 'void',
+        'x-reactions': {
+          dependencies: ['.operatorName'],
+          fulfill: {
+            state: {
+              visible: '{{$deps[0] ==="unicom"}}',
+            },
+          },
+        },
+
+        properties: {
+          config: {
+            type: 'object',
+            properties: {
+              appId: {
+                type: 'string',
+                title: 'App ID',
+                required: true,
+                'x-decorator': 'FormItem',
+                'x-component': 'Input',
+                'x-component-props': {
+                  placeholder: '请输入App ID',
+                },
+                'x-validator': [
+                  {
+                    max: 64,
+                    message: '最多可输入64个字符',
+                  },
+                  {
+                    required: true,
+                    message: '请输入App ID',
+                  },
+                ],
+              },
+              appSecret: {
+                type: 'string',
+                title: 'App Secret',
+                required: true,
+                'x-decorator': 'FormItem',
+                'x-component': 'Input',
+                'x-component-props': {
+                  placeholder: '请输入App Secret',
+                },
+                'x-validator': [
+                  {
+                    required: true,
+                    message: '请输入App Secret',
+                  },
+                ],
+              },
+              openId: {
+                type: 'string',
+                title: '创建者ID',
+                required: true,
+                'x-decorator': 'FormItem',
+                'x-component': 'Input',
+                'x-component-props': {
+                  placeholder: '请输入创建者ID',
+                },
+                'x-validator': [
+                  {
+                    max: 64,
+                    message: '最多可输入64个字符',
+                  },
+                  {
+                    required: true,
+                    message: '请输入创建者ID',
+                  },
+                ],
+              },
+            },
+          },
+        },
+      },
+      explain: {
+        title: '说明',
+        'x-component': 'Input.TextArea',
+        'x-decorator': 'FormItem',
+        'x-component-props': {
+          rows: 3,
+          showCount: true,
+          maxLength: 200,
+          placeholder: '请输入说明',
+        },
+        'x-validator': [
+          {
+            max: 200,
+            message: '最多可输入200个字符',
+          },
+        ],
+      },
+    },
+  };
+
+  const handleSave = async () => {
+    const data: any = await form.submit();
+    const res: any = params.id === ':id' ? await service.save(data) : await service.update(data);
+    if (res.status === 200) {
+      onlyMessage('保存成功');
+    }
+    console.log(data);
+  };
+
+  useEffect(() => {
+    setTimeout(() => {
+      if (initialState?.settings?.title) {
+        document.title = `物联卡 - ${initialState?.settings?.title}`;
+      } else {
+        document.title = '物联卡';
+      }
+    }, 0);
+  }, []);
+
+  return (
+    <PageContainer>
+      <Card>
+        <Row gutter={24}>
+          <Col span={14}>
+            <TitleComponent data={'详情'} />
+            <Form form={form} layout="vertical">
+              <SchemaField
+                schema={schema}
+                scope={{
+                  useAsyncDataSource,
+                }}
+              />
+              <FormButtonGroup.Sticky>
+                <FormButtonGroup.FormItem>
+                  <Button type="primary" onClick={() => handleSave()}>
+                    保存
+                  </Button>
+                </FormButtonGroup.FormItem>
+              </FormButtonGroup.Sticky>
+            </Form>
+          </Col>
+          <Col span={10}>
+            <Doc type={docType} />
+          </Col>
+        </Row>
+      </Card>
+    </PageContainer>
+  );
+});
+
+export default Detail;

+ 35 - 0
src/pages/iot-card/Platform/doc/index.less

@@ -0,0 +1,35 @@
+.doc {
+  height: 1000px;
+  padding: 24px;
+  overflow-y: auto;
+  color: rgba(#000, 0.8);
+  font-size: 14px;
+  background-color: #fafafa;
+
+  .url {
+    padding: 8px 16px;
+    color: #2f54eb;
+    background-color: rgba(#a7bdf7, 0.2);
+  }
+
+  h1 {
+    margin: 16px 0;
+    color: rgba(#000, 0.85);
+    font-weight: bold;
+    font-size: 14px;
+
+    &:first-child {
+      margin-top: 0;
+    }
+  }
+
+  h2 {
+    margin: 6px 0;
+    color: rgba(0, 0, 0, 0.8);
+    font-size: 14px;
+  }
+
+  .image {
+    margin: 16px 0;
+  }
+}

+ 161 - 0
src/pages/iot-card/Platform/doc/index.tsx

@@ -0,0 +1,161 @@
+import './index.less';
+import { Image } from 'antd';
+interface Props {
+  type: 'onelink' | 'ctwing' | 'unicom' | any;
+}
+
+const Doc = (props: Props) => {
+  const { type } = props;
+
+  return (
+    <>
+      {type === 'onelink' && (
+        <div className="doc">
+          <div className="url">
+            中国移动物联卡能力开放平台:
+            <a
+              style={{ wordBreak: 'break-all' }}
+              href="https://api.iot.10086.cn/api/index.html#/login"
+              target={'_blank'}
+              rel="noreferrer"
+            >
+              https://api.iot.10086.cn/api/index.html#/login
+            </a>
+          </div>
+          <h1>1.概述</h1>
+          <p>平台对接通过API的方式与三方系统进行数据对接,为物联卡的管理提供数据交互支持。</p>
+          <h1>2.配置说明</h1>
+          <h2>1、APP ID</h2>
+          <p>
+            第三方应用唯一标识,中国移动物联网全网管理员在 OneLink
+            能力开放平台上分配并展示给集团客户。
+            <br />
+            获取路径:“中移物联卡能力开放平台”--“个人中心”--“客户信息”--“接入信息”
+          </p>
+          <div className={'image'}>
+            <Image width="100%" src={require('/public/images/iot-card/onelink-appid.png')} />
+          </div>
+          <h2>2、Password</h2>
+          <p>
+            API 接入秘钥,由中国移动物联网提供,集团客户从“OneLink 能力开放平台”获取。
+            <br />
+            获取路径:“中移物联卡能力开放平台”--“个人中心”--“客户信息”--“接入信息”
+          </p>
+          <div className={'image'}>
+            <Image width="100%" src={require('/public/images/iot-card/onelink-pass.png')} />
+          </div>
+          <h2>3、接口地址</h2>
+          <p>
+            https://api.iot.10086.cn/v5/ec/get/token
+            <br />
+            token后缀请根据实际情况填写
+            <br />
+            示例:https://api.iot.10086.cn/v5/authService?appid=xxx&password=xxx&transid=xxx
+          </p>
+        </div>
+      )}
+
+      {type === 'ctwing' && (
+        <div className="doc">
+          <div className="url">
+            5G连接管理平台:
+            <a
+              style={{ wordBreak: 'break-all' }}
+              href="https://cmp.ctwing.cn:4821/login"
+              target={'_blank'}
+              rel="noreferrer"
+            >
+              https://cmp.ctwing.cn:4821/login
+            </a>
+          </div>
+          <div>
+            <h1>1.概述</h1>
+            <p>平台对接通过API的方式与三方系统进行数据对接,为物联卡的管理提供数据交互支持。</p>
+            <h1>2.配置说明</h1>
+            <h2>1、用户 id</h2>
+            <p>
+              5G连接管理平台用户的唯一标识,用于身份识别。
+              <br />
+              获取路径:“5G连接管理平台”--“能力开放”--“API网关账号管理”
+            </p>
+            <div className={'image'}>
+              <Image width="100%" src={require('/public/images/iot-card/ctwing-id.png')} />
+            </div>
+
+            <h2>2、密码</h2>
+            <p>
+              用户id经加密之后的密码。
+              <br />
+              获取路径:“5G连接管理平台”--“能力开放”--“API网关账号管理”
+            </p>
+            <div className={'image'}>
+              <Image width="100%" src={require('/public/images/iot-card/ctwing-pass.png')} />
+            </div>
+
+            <h2>3、secretKey</h2>
+            <p>
+              APP secret唯一秘钥。
+              <br />
+              获取路径:“5G连接管理平台”--“能力开放”--“API网关账号管理”
+            </p>
+            <div className={'image'}>
+              <Image width="100%" src={require('/public/images/iot-card/ctwing-secret.png')} />
+            </div>
+          </div>
+        </div>
+      )}
+      {type === 'unicom' && (
+        <div className="doc">
+          <div className="url">
+            雁飞智连CMP平台:
+            <a
+              style={{ wordBreak: 'break-all' }}
+              href="  https://cmp.10646.cn/webframe/login"
+              target={'_blank'}
+              rel="noreferrer"
+            >
+              https://cmp.10646.cn/webframe/login
+            </a>
+          </div>
+
+          <div>
+            <h1>1.概述</h1>
+            <p>平台对接通过API的方式与三方系统进行数据对接,为物联卡的管理提供数据交互支持。</p>
+            <h1>2.配置说明</h1>
+            <h2>1、APP ID</h2>
+            <p>
+              第三方应用唯一标识。
+              <br />
+              获取路径:“雁飞智连CMP平台”--“我的应用”--“应用列表”
+            </p>
+            <div className={'image'}>
+              <Image width="100%" src={require('/public/images/iot-card/unicom-id.png')} />
+            </div>
+
+            <h2>2、App Secret</h2>
+            <p>
+              API 接入秘钥。
+              <br />
+              获取路径:“雁飞智连CMP平台”--“我的应用”--“应用列表”
+            </p>
+            <div className={'image'}>
+              <Image width="100%" src={require('/public/images/iot-card/unicom-secret.png')} />
+            </div>
+
+            <h2>3、创建者ID</h2>
+            <p>
+              接口参数中的 OpenId。
+              <br />
+              获取路径:“雁飞智连CMP平台”--“我的应用”--“应用列表”
+              <br />
+            </p>
+            <div className={'image'}>
+              <Image width="100%" src={require('/public/images/iot-card/unicom-openid.png')} />
+            </div>
+          </div>
+        </div>
+      )}
+    </>
+  );
+};
+export default Doc;

+ 209 - 1
src/pages/iot-card/Platform/index.tsx

@@ -1,4 +1,212 @@
+import { PermissionButton } from '@/components';
+import SearchComponent from '@/components/SearchComponent';
+import useDomFullHeight from '@/hooks/document/useDomFullHeight';
+import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+import { onlyMessage } from '@/utils/util';
+import {
+  DeleteOutlined,
+  EditOutlined,
+  PlayCircleOutlined,
+  PlusOutlined,
+  StopOutlined,
+} from '@ant-design/icons';
+import { PageContainer } from '@ant-design/pro-layout';
+import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { Badge } from 'antd';
+import { useRef, useState } from 'react';
+import Service from './service';
+import { useHistory } from '@/hooks';
+
+export const service = new Service('network/card/platform');
+
 const Platform = () => {
-  return <>平台对接</>;
+  const { minHeight } = useDomFullHeight(`.record`, 24);
+  const actionRef = useRef<ActionType>();
+  const [param, setParam] = useState({});
+  const history = useHistory();
+
+  const statusUpdate = async (data: any) => {
+    const res = await service.update(data);
+    if (res.status === 200) {
+      onlyMessage('操作成功');
+      actionRef.current?.reload();
+    }
+  };
+
+  const columns: ProColumns<any>[] = [
+    {
+      title: '名称',
+      dataIndex: 'name',
+      ellipsis: true,
+    },
+    {
+      title: '平台类型',
+      dataIndex: 'operatorName',
+      ellipsis: true,
+      valueType: 'select',
+      valueEnum: {
+        onelink: {
+          text: '移动OneLink',
+          status: 'onelink',
+        },
+        ctwing: {
+          text: '电信Ctwing',
+          status: 'ctwing',
+        },
+        unicom: {
+          text: '联通Unicom',
+          status: 'unicom',
+        },
+      },
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      ellipsis: true,
+      valueType: 'select',
+      valueEnum: {
+        enabled: {
+          text: '启用',
+          status: 'enabled',
+        },
+        disabled: {
+          text: '禁用',
+          status: 'disabled',
+        },
+      },
+      render: (_, record: any) => (
+        <Badge
+          status={record.state?.value === 'disabled' ? 'error' : 'success'}
+          text={record.state?.text}
+        />
+      ),
+    },
+    {
+      title: '说明',
+      dataIndex: 'explain',
+      ellipsis: true,
+      hideInSearch: true,
+    },
+    {
+      title: '操作',
+      valueType: 'option',
+      fixed: 'right',
+      render: (text, record) => [
+        <PermissionButton
+          isPermission={true}
+          key="edit"
+          onClick={() => {
+            const url = `${getMenuPathByParams(MENUS_CODE['iot-card/Platform/Detail'], record.id)}`;
+            history.push(url);
+          }}
+          type={'link'}
+          style={{ padding: 0 }}
+          tooltip={{
+            title: '编辑',
+          }}
+        >
+          <EditOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          isPermission={true}
+          key="action"
+          type={'link'}
+          style={{ padding: 0 }}
+          tooltip={{
+            title: record.state.value === 'enabled' ? '禁用' : '启用',
+          }}
+          popConfirm={{
+            title: `确认${record.state.value === 'enabled' ? '禁用' : '启用'}`,
+            onConfirm: () => {
+              if (record.state.value === 'enabled') {
+                statusUpdate({
+                  id: record.id,
+                  config: { ...record.config },
+                  state: 'disabled',
+                  operatorName: record.operatorName,
+                });
+              } else {
+                statusUpdate({
+                  id: record.id,
+                  config: { ...record.config },
+                  state: 'enabled',
+                  operatorName: record.operatorName,
+                });
+              }
+            },
+          }}
+        >
+          {record.state === 'enabled' ? <StopOutlined /> : <PlayCircleOutlined />}
+        </PermissionButton>,
+        <PermissionButton
+          isPermission={true}
+          tooltip={{
+            title: record.state.value !== 'enabled' ? '删除' : '请先禁用再删除',
+          }}
+          style={{ padding: 0 }}
+          disabled={record.state.value === 'enabled'}
+          popConfirm={{
+            title: '确认删除',
+            disabled: record.state.value === 'enabled',
+            onConfirm: async () => {
+              const res: any = await service.remove(record.id);
+              if (res.status === 200) {
+                onlyMessage('操作成功');
+                actionRef.current?.reload();
+              }
+            },
+          }}
+          key="delete"
+          type="link"
+        >
+          <DeleteOutlined />
+        </PermissionButton>,
+      ],
+    },
+  ];
+
+  return (
+    <PageContainer>
+      <SearchComponent
+        field={columns}
+        target="record"
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTable
+        actionRef={actionRef}
+        params={param}
+        columns={columns}
+        search={false}
+        rowKey="id"
+        tableClassName={'record'}
+        columnEmptyText={''}
+        tableStyle={{ minHeight }}
+        headerTitle={
+          <>
+            <PermissionButton
+              onClick={() => {
+                const url = `${getMenuPathByParams(MENUS_CODE['iot-card/Platform/Detail'])}`;
+                history.push(url);
+              }}
+              style={{ marginRight: 12 }}
+              isPermission={true}
+              key="button"
+              icon={<PlusOutlined />}
+              type="primary"
+            >
+              新增
+            </PermissionButton>
+          </>
+        }
+        request={async (params) =>
+          service.getList({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        }
+      />
+    </PageContainer>
+  );
 };
 export default Platform;

+ 12 - 0
src/pages/iot-card/Platform/service.ts

@@ -0,0 +1,12 @@
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+import BaseService from '@/utils/BaseService';
+
+class Service extends BaseService<any> {
+  getList = (data: any) =>
+    request(`${SystemConst.API_BASE}/network/card/platform/_query`, {
+      method: 'POST',
+      data,
+    });
+}
+export default Service;

+ 35 - 0
src/pages/iot-card/Recharge/detail.tsx

@@ -0,0 +1,35 @@
+import { Modal, Descriptions } from 'antd';
+import moment from 'moment';
+
+interface Props {
+  data: any;
+  close: any;
+}
+
+const Detail = (props: Props) => {
+  const { data } = props;
+  return (
+    <Modal
+      title={'详情'}
+      maskClosable={false}
+      visible
+      onCancel={props.close}
+      onOk={props.close}
+      width="35vw"
+    >
+      <Descriptions bordered column={2}>
+        <Descriptions.Item label="充值金额">{data.chargeMoney}</Descriptions.Item>
+        <Descriptions.Item label="账户id">{data?.rechargeId}</Descriptions.Item>
+        <Descriptions.Item label="平台对接">{data.configName}</Descriptions.Item>
+        <Descriptions.Item label="订单号">{data.orderNumber}</Descriptions.Item>
+        <Descriptions.Item label="支付方式">{data.paymentType}</Descriptions.Item>
+        <Descriptions.Item label="支付URL">{data.url ? data.url : ''}</Descriptions.Item>
+        <Descriptions.Item label="订单时间">
+          {data.createTime ? moment(data.createTime).format('YYYY-MM-DD HH:mm:ss') : '-'}
+        </Descriptions.Item>
+      </Descriptions>
+    </Modal>
+  );
+};
+
+export default Detail;

+ 163 - 1
src/pages/iot-card/Recharge/index.tsx

@@ -1,4 +1,166 @@
+import { PermissionButton } from '@/components';
+import SearchComponent from '@/components/SearchComponent';
+import useDomFullHeight from '@/hooks/document/useDomFullHeight';
+import { ExclamationCircleOutlined, EyeOutlined } from '@ant-design/icons';
+import { PageContainer } from '@ant-design/pro-layout';
+import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { Tooltip } from 'antd';
+import moment from 'moment';
+import { useRef, useState } from 'react';
+import Service from '../CardManagement/service';
+import Detail from './detail';
+import TopUp from './topUp';
+
+export const service = new Service('network/card');
+
 const Recharge = () => {
-  return <>充值管理</>;
+  const { minHeight } = useDomFullHeight(`.record`, 24);
+  const actionRef = useRef<ActionType>();
+  const [param, setParam] = useState({});
+  const [visible, setVisible] = useState<boolean>(false);
+  const [detail, setDetail] = useState<boolean>(false);
+  const [current, setCurrent] = useState<any>({});
+
+  const columns: ProColumns<any>[] = [
+    {
+      title: '充值金额',
+      dataIndex: 'chargeMoney',
+      ellipsis: true,
+    },
+    {
+      title: '支付方式',
+      dataIndex: 'paymentType',
+      ellipsis: true,
+      valueType: 'select',
+      valueEnum: {
+        ALIPAY_WAP: {
+          text: '支付宝手机网站支付',
+          status: 'ALIPAY_WAP',
+        },
+        ALIPAY_WEB: {
+          text: '支付宝网页及时到账支付',
+          status: 'ALIPAY_WEB',
+        },
+        WEIXIN_JSAPI: {
+          text: '微信公众号支付',
+          status: 'WEIXIN_JSAPI',
+        },
+        WEIXIN_NATIVE: {
+          text: '微信扫码支付',
+          status: 'WEIXIN_NATIVE',
+        },
+      },
+    },
+    {
+      title: '订单号',
+      dataIndex: 'orderNumber',
+      ellipsis: true,
+    },
+    {
+      title: '支付URL',
+      dataIndex: 'url',
+      ellipsis: true,
+      hideInSearch: true,
+    },
+    {
+      title: '订单时间',
+      dataIndex: 'createTime',
+      ellipsis: true,
+      valueType: 'dateTime',
+      render: (_: any, record) => {
+        return record.createTime ? moment(record.createTime).format('YYYY-MM-DD HH:mm:ss') : '';
+      },
+    },
+    {
+      title: '操作',
+      key: 'action',
+      fixed: 'right',
+      align: 'center',
+      width: 200,
+      hideInSearch: true,
+      render: (_, record) => [
+        <a
+          key="editable"
+          onClick={() => {
+            setDetail(true);
+            setCurrent(record);
+          }}
+        >
+          <Tooltip title="查看">
+            <EyeOutlined />
+          </Tooltip>
+        </a>,
+      ],
+    },
+  ];
+
+  return (
+    <PageContainer>
+      <SearchComponent
+        field={columns}
+        target="record"
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTable
+        actionRef={actionRef}
+        params={param}
+        columns={columns}
+        search={false}
+        rowKey="id"
+        tableClassName={'record'}
+        columnEmptyText={''}
+        tableStyle={{ minHeight }}
+        headerTitle={
+          <>
+            <PermissionButton
+              onClick={() => {
+                setVisible(true);
+              }}
+              isPermission={true}
+              key="button"
+              type="primary"
+            >
+              充值
+            </PermissionButton>
+            <div
+              style={{
+                paddingLeft: 24,
+                background: '#fff',
+                fontSize: 14,
+              }}
+            >
+              <span style={{ marginRight: 8, fontSize: 16 }}>
+                <ExclamationCircleOutlined />
+              </span>
+              本平台仅提供充值入口,具体充值结果需以运营商的充值结果为准
+            </div>
+          </>
+        }
+        request={async (params) =>
+          service.queryRechargeList({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        }
+      />
+      {visible && (
+        <TopUp
+          close={() => {
+            setVisible(false);
+            actionRef.current?.reload();
+          }}
+        />
+      )}
+      {detail && (
+        <Detail
+          data={current}
+          close={() => {
+            setDetail(false);
+          }}
+        />
+      )}
+    </PageContainer>
+  );
 };
 export default Recharge;

+ 215 - 0
src/pages/iot-card/Recharge/topUp.tsx

@@ -0,0 +1,215 @@
+import { createForm, Field } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import { Form, FormGrid, FormItem, Input, Select, NumberPicker } from '@formily/antd';
+import type { ISchema } from '@formily/json-schema';
+import { Modal } from '@/components';
+import { onlyMessage } from '@/utils/util';
+import { ExclamationCircleOutlined } from '@ant-design/icons';
+import { action } from '@formily/reactive';
+import { service } from './index';
+import { PaymentMethod } from '../data';
+
+interface Props {
+  close: () => void;
+}
+
+const TopUp = (props: Props) => {
+  const form = createForm({});
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Select,
+      FormGrid,
+      NumberPicker,
+    },
+  });
+  const useAsyncDataSource = (ser: (arg0: any) => Promise<any>) => (field: Field) => {
+    field.loading = true;
+    ser(field).then(
+      action.bound?.((data) => {
+        field.dataSource = (data.result || []).map((item: any) => ({
+          label: item.name,
+          value: item.id,
+        }));
+        field.loading = false;
+      }),
+    );
+  };
+
+  const queryProvidersList = () =>
+    service.queryPlatformNoPage({
+      paging: false,
+      terms: [
+        {
+          terms: [
+            {
+              column: 'operatorName',
+              termType: 'eq',
+              value: 'onelink',
+            },
+            {
+              column: 'state',
+              termType: 'eq',
+              value: 'enabled',
+              type: 'and',
+            },
+          ],
+        },
+      ],
+    });
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      layout: {
+        type: 'void',
+        'x-decorator': 'FormGrid',
+        'x-decorator-props': {
+          maxColumns: 2,
+          minColumns: 2,
+          columnGap: 24,
+        },
+        properties: {
+          configId: {
+            title: '平台对接',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Select',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入平台对接',
+            },
+            'x-reactions': '{{useAsyncDataSource(queryProvidersList)}}',
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请选择平台对接',
+              },
+            ],
+          },
+          rechargeId: {
+            title: '账户id',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入账户id',
+            },
+
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入账户id',
+              },
+            ],
+          },
+          chargeMoney: {
+            title: '充值金额',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'NumberPicker',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入充值金额',
+            },
+
+            'x-validator': [
+              {
+                min: 1,
+                message: '请输入1~500之间的数字',
+              },
+              {
+                max: 500,
+                message: '请输入1~500之间的数字',
+              },
+              {
+                required: true,
+                message: '请输入充值金额',
+              },
+            ],
+          },
+          paymentType: {
+            title: '支付方式',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Select',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请选择支付方式',
+            },
+            name: 'name',
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择支付方式',
+              },
+            ],
+            enum: PaymentMethod,
+          },
+        },
+      },
+    },
+  };
+
+  const save = async () => {
+    const value = await form.submit<any>();
+    const res: any = await service.recharge(value);
+    if (res.status === 200) {
+      if (res.result === '失败') {
+        onlyMessage('缴费失败', 'error');
+        props.close();
+      } else {
+        window.open(res.result);
+        props.close();
+      }
+    }
+  };
+
+  return (
+    <Modal
+      title={'充值'}
+      maskClosable={false}
+      visible
+      onCancel={props.close}
+      onOk={save}
+      width="35vw"
+    >
+      <div
+        style={{
+          padding: 5,
+          background: '#f6f6f6',
+          fontSize: 14,
+          color: '#00000091',
+          marginBottom: 10,
+        }}
+      >
+        <span style={{ fontSize: 16, marginRight: 5 }}>
+          <ExclamationCircleOutlined />
+        </span>
+        暂只支持移动OneLink平台
+      </div>
+      <Form form={form} layout="vertical">
+        <SchemaField schema={schema} scope={{ useAsyncDataSource, queryProvidersList }} />
+      </Form>
+    </Modal>
+  );
+};
+export default TopUp;

+ 63 - 1
src/pages/iot-card/Record/index.tsx

@@ -1,4 +1,66 @@
+import SearchComponent from '@/components/SearchComponent';
+import useDomFullHeight from '@/hooks/document/useDomFullHeight';
+import { PageContainer } from '@ant-design/pro-layout';
+import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
+import moment from 'moment';
+import { useRef, useState } from 'react';
+import Service from './service';
+
+export const service = new Service('');
+
 const Record = () => {
-  return <>操作记录</>;
+  const { minHeight } = useDomFullHeight(`.record`, 24);
+  const actionRef = useRef<ActionType>();
+  const [param, setParam] = useState({});
+
+  const columns: ProColumns<any>[] = [
+    {
+      dataIndex: 'cardId',
+      title: '卡号',
+    },
+    {
+      dataIndex: 'type',
+      title: '操作类型',
+    },
+    {
+      dataIndex: 'time',
+      title: '操作时间',
+      valueType: 'dateTime',
+      render: (_: any, record) => {
+        return record.time ? moment(record.time).format('YYYY-MM-DD HH:mm:ss') : '';
+      },
+    },
+    {
+      dataIndex: 'operator',
+      title: '操作人',
+    },
+  ];
+
+  return (
+    <PageContainer>
+      <SearchComponent
+        field={columns}
+        target="record"
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTable
+        actionRef={actionRef}
+        params={param}
+        columns={columns}
+        search={false}
+        rowKey="id"
+        tableClassName={'record'}
+        columnEmptyText={''}
+        tableStyle={{ minHeight }}
+        request={async (params) =>
+          service.getList({ ...params, sorts: [{ name: 'time', order: 'desc' }] })
+        }
+      />
+    </PageContainer>
+  );
 };
 export default Record;

+ 12 - 0
src/pages/iot-card/Record/service.ts

@@ -0,0 +1,12 @@
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+import BaseService from '@/utils/BaseService';
+
+class Service extends BaseService<any> {
+  getList = (data: any) =>
+    request(`${SystemConst.API_BASE}/network/card/stateOperate/_log`, {
+      method: 'POST',
+      data,
+    });
+}
+export default Service;

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

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

@@ -319,12 +319,6 @@ const Detail = observer(() => {
                 'x-component-props': {
                   placeholder: '请输入webhook',
                 },
-                'x-validator': [
-                  {
-                    max: 64,
-                    message: '最多可输入64个字符',
-                  },
-                ],
                 'x-reactions': {
                   dependencies: ['provider'],
                   fulfill: {
@@ -530,12 +524,6 @@ const Detail = observer(() => {
                 },
                 'x-component': 'Input',
                 'x-decorator': 'FormItem',
-                'x-validator': [
-                  {
-                    max: 64,
-                    message: '最多可输入64个字符',
-                  },
-                ],
               },
               headers: {
                 title: '请求头',

+ 4 - 2
src/pages/oauth/index.tsx

@@ -202,8 +202,10 @@ const Oauth = () => {
         const item = getQueryVariable('internal');
         if (items.redirect_uri) {
           const orgin = items.redirect_uri.split('/').slice(0, 3);
-          const url = `${orgin.join('/')}/%23/${items.redirect_uri?.split('redirect=')[1]}`;
-          redirectUrl = `${items.redirect_uri?.split('redirect=')[0]}redirect=${url}`;
+          // const url = `${orgin.join('/')}/%23/${items.redirect_uri?.split('redirect=')[1]}`;
+          // redirectUrl = `${items.redirect_uri?.split('redirect=')[0]}redirect=${url}`;
+          const url = `${orgin.join('/')}/%23/${items.redirect_uri?.split('redirect_uri=')[1]}`;
+          redirectUrl = `${items.redirect_uri?.split('redirect_uri=')[0]}?redirect=${url}`;
         }
         getLoginUser({
           ...items,

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

@@ -139,6 +139,7 @@ export enum MENUS_CODE {
   'system/License' = 'system/License',
   'iot-card/Home' = 'iot-card/Home',
   'iot-card/Platform' = 'iot-card/Platform',
+  'iot-card/Platform/Detail' = 'iot-card/Platform/Detail',
   'iot-card/Recharge' = 'iot-card/Recharge',
   'iot-card/Dashboard' = 'iot-card/Dashboard',
   'iot-card/CardManagement' = 'iot-card/CardManagement',
@@ -193,6 +194,7 @@ export const getDetailNameByCode = {
   'rule-engine/Alarm/Log/Detail': '告警日志',
   'Northbound/AliCloud/Detail': '阿里云详情',
   'link/Certificate/Detail': '证书详情',
+  'iot-card/Platform/Detail': '平台对接详情',
 };
 
 // 开源版路由