瀏覽代碼

feat: merge

xieyonghong 3 年之前
父節點
當前提交
06fd520640
共有 47 個文件被更改,包括 1293 次插入466 次删除
  1. 2 0
      src/components/FRuleEditor/Advance/index.tsx
  2. 1 0
      src/components/FSelectDevice/index.tsx
  3. 1 0
      src/components/FSelectDevices/index.tsx
  4. 1 0
      src/pages/device/Category/Save/index.tsx
  5. 8 1
      src/pages/device/Command/cat/index.tsx
  6. 2 1
      src/pages/device/Command/create/index.tsx
  7. 7 1
      src/pages/device/Firmware/Detail/Task/Detail/index.tsx
  8. 1 0
      src/pages/device/Firmware/Detail/Task/Release/index.tsx
  9. 1 0
      src/pages/device/Firmware/Detail/Task/Save/index.tsx
  10. 1 0
      src/pages/device/Firmware/Save/index.tsx
  11. 1 0
      src/pages/device/Instance/Detail/ChildDevice/BindChildDevice/index.tsx
  12. 2 2
      src/pages/device/Instance/Detail/Functions/form.tsx
  13. 7 1
      src/pages/device/Instance/Detail/MetadataLog/Event/index.tsx
  14. 7 1
      src/pages/device/Instance/Detail/MetadataLog/Property/index.tsx
  15. 3 2
      src/pages/device/Instance/Detail/Running/Property/EditProperty.tsx
  16. 26 21
      src/pages/device/Instance/Detail/Running/Property/index.tsx
  17. 1 1
      src/pages/device/Instance/Detail/Running/index.tsx
  18. 1 0
      src/pages/device/Instance/Export/index.tsx
  19. 1 0
      src/pages/device/Instance/Import/index.tsx
  20. 1 0
      src/pages/device/Instance/Process/index.tsx
  21. 1 0
      src/pages/device/Instance/Save/index.tsx
  22. 89 0
      src/pages/device/Product/Detail/Access/index.less
  23. 426 0
      src/pages/device/Product/Detail/Access/index.tsx
  24. 2 1
      src/pages/device/Product/Detail/PropertyImport/index.tsx
  25. 40 34
      src/pages/device/Product/Detail/index.tsx
  26. 1 0
      src/pages/device/Product/Save/index.tsx
  27. 6 0
      src/pages/device/Product/service.ts
  28. 1 0
      src/pages/device/components/Alarm/Edit/index.tsx
  29. 1 0
      src/pages/device/components/Alarm/Record/index.tsx
  30. 66 21
      src/pages/device/components/Metadata/Base/Edit/index.tsx
  31. 13 2
      src/pages/device/components/Metadata/Base/columns.ts
  32. 6 1
      src/pages/device/components/Metadata/Cat/index.tsx
  33. 1 0
      src/pages/device/components/Metadata/Import/index.tsx
  34. 213 102
      src/pages/link/AccessConfig/Detail/Access/index.tsx
  35. 25 5
      src/pages/link/AccessConfig/Detail/index.tsx
  36. 58 40
      src/pages/link/AccessConfig/index.less
  37. 158 151
      src/pages/link/AccessConfig/index.tsx
  38. 1 1
      src/pages/link/AccessConfig/service.ts
  39. 2 1
      src/pages/link/Protocol/Debug/index.tsx
  40. 0 1
      src/pages/link/Protocol/FileUpload/index.tsx
  41. 54 40
      src/pages/link/Protocol/index.tsx
  42. 19 7
      src/pages/link/Type/Save/index.tsx
  43. 7 3
      src/pages/link/Type/index.tsx
  44. 4 3
      src/pages/system/Menu/Detail/buttons.tsx
  45. 2 1
      src/pages/system/OpenAPI/index.tsx
  46. 3 3
      src/pages/system/Tenant/index.tsx
  47. 19 18
      src/pages/system/User/Save/index.tsx

+ 2 - 0
src/components/FRuleEditor/Advance/index.tsx

@@ -13,10 +13,12 @@ const Advance = (props: Props) => {
   const { model, onChange } = props;
   return (
     <Modal
+      maskClosable={false}
       visible={model === 'advance'}
       width="70vw"
       title="设置属性规则"
       onCancel={() => onChange('simple')}
+      onOk={() => onChange('simple')}
     >
       <div className={styles.box}>
         <div className={styles.left}>

+ 1 - 0
src/components/FSelectDevice/index.tsx

@@ -85,6 +85,7 @@ const FSelectDevice = connect((props: Props) => {
       />
       {visible && (
         <Modal
+          maskClosable={false}
           visible
           title="选择设备"
           width="80vw"

+ 1 - 0
src/components/FSelectDevices/index.tsx

@@ -85,6 +85,7 @@ const FSelectDevices = connect((props: Props) => {
       />
       {visible && (
         <Modal
+          maskClosable={false}
           visible
           title="选择设备"
           width="80vw"

+ 1 - 0
src/pages/device/Category/Save/index.tsx

@@ -133,6 +133,7 @@ const Save = (props: Props) => {
 
   return (
     <Modal
+      maskClosable={false}
       title={intl.formatMessage({
         id: `pages.data.option.${props.data.id ? 'edit' : 'add'}`,
         defaultMessage: '新增',

+ 8 - 1
src/pages/device/Command/cat/index.tsx

@@ -11,7 +11,14 @@ interface Props {
 const Cat = (props: Props) => {
   const { close, data, visible } = props;
   return (
-    <Modal width="40vw" visible={visible} onCancel={() => close()} footer={null} title="查看指令">
+    <Modal
+      maskClosable={false}
+      width="40vw"
+      visible={visible}
+      onCancel={() => close()}
+      footer={null}
+      title="查看指令"
+    >
       下发指令:
       <MonacoEditor
         height={300}

+ 2 - 1
src/pages/device/Command/create/index.tsx

@@ -1,7 +1,7 @@
 import type { Field } from '@formily/core';
 import { createForm, onFieldValueChange } from '@formily/core';
 import { createSchemaField } from '@formily/react';
-import { Form, Input, FormItem, Select, Space, ArrayTable } from '@formily/antd';
+import { ArrayTable, Form, FormItem, Input, Select, Space } from '@formily/antd';
 import { action } from '@formily/reactive';
 import type { ISchema } from '@formily/json-schema';
 import { service } from '@/pages/device/Command';
@@ -243,6 +243,7 @@ const Create = (props: Props) => {
   };
   return (
     <Modal
+      maskClosable={false}
       onOk={sendCommand}
       onCancel={() => close()}
       width="50vw"

+ 7 - 1
src/pages/device/Firmware/Detail/Task/Detail/index.tsx

@@ -64,7 +64,13 @@ const Detail = (props: Props) => {
   }, [state.taskItem]);
 
   return (
-    <Modal width="30vw" visible={props.visible} onCancel={() => props.close()} title="任务详情">
+    <Modal
+      maskClosable={false}
+      width="30vw"
+      visible={props.visible}
+      onCancel={() => props.close()}
+      title="任务详情"
+    >
       <Row gutter={16}>
         {Object.keys(count)
           .reduce((previousValue: any[], currentValue) => {

+ 1 - 0
src/pages/device/Firmware/Detail/Task/Release/index.tsx

@@ -74,6 +74,7 @@ const Release = (props: Props) => {
   };
   return (
     <Modal
+      maskClosable={false}
       title="发布任务"
       onOk={save}
       visible={props.visible}

+ 1 - 0
src/pages/device/Firmware/Detail/Task/Save/index.tsx

@@ -69,6 +69,7 @@ const Save = (props: Props) => {
   };
   return (
     <Modal
+      maskClosable={false}
       onOk={save}
       width="40vw"
       visible={props.visible}

+ 1 - 0
src/pages/device/Firmware/Save/index.tsx

@@ -120,6 +120,7 @@ const Save = (props: Props) => {
 
   return (
     <Modal
+      maskClosable={false}
       width="50vw"
       title="新增固件版本"
       onCancel={() => close()}

+ 1 - 0
src/pages/device/Instance/Detail/ChildDevice/BindChildDevice/index.tsx

@@ -90,6 +90,7 @@ const BindChildDevice = (props: Props) => {
 
   return (
     <Modal
+      maskClosable={false}
       title="绑定子设备"
       visible={visible}
       width={1000}

+ 2 - 2
src/pages/device/Instance/Detail/Functions/form.tsx

@@ -1,6 +1,6 @@
 import type { FunctionMetadata } from '@/pages/device/Product/typings';
-import React, { useState, useEffect, useRef } from 'react';
-import { Input, Button, DatePicker, Select, InputNumber } from 'antd';
+import React, { useEffect, useRef, useState } from 'react';
+import { Button, DatePicker, Input, InputNumber, Select } from 'antd';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import type { ProColumns } from '@jetlinks/pro-table';
 import { EditableProTable } from '@jetlinks/pro-table';

+ 7 - 1
src/pages/device/Instance/Detail/MetadataLog/Event/index.tsx

@@ -37,7 +37,13 @@ const EventLog = (props: Props) => {
         ];
 
   return (
-    <Drawer title={data.name} visible={visible} onClose={() => close()} width="45vw">
+    <Drawer
+      maskClosable={false}
+      title={data.name}
+      visible={visible}
+      onClose={() => close()}
+      width="45vw"
+    >
       <ProTable
         size="small"
         rowKey="id"

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

@@ -69,7 +69,13 @@ const PropertyLog = (props: Props) => {
   }, [visible]);
 
   return (
-    <Modal title="详情" visible={visible} onCancel={() => close()} width="45vw">
+    <Modal
+      maskClosable={false}
+      title="详情"
+      visible={visible}
+      onCancel={() => close()}
+      width="45vw"
+    >
       <div style={{ marginBottom: '20px' }}>
         <Space>
           <Radio.Group

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

@@ -1,7 +1,7 @@
 import { Alert, message, Modal } from 'antd';
-import { Input, FormItem } from '@formily/antd';
+import { FormItem, Input } from '@formily/antd';
 import { createForm } from '@formily/core';
-import { FormProvider, createSchemaField } from '@formily/react';
+import { createSchemaField, FormProvider } from '@formily/react';
 import { service } from '@/pages/device/Instance';
 import { useParams } from 'umi';
 import type { PropertyMetadata } from '@/pages/device/Product/typings';
@@ -45,6 +45,7 @@ const EditProperty = (props: Props) => {
   };
   return (
     <Modal
+      maskClosable={false}
       title="编辑"
       visible={visible}
       onOk={async () => {

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

@@ -28,6 +28,7 @@ const ColResponsiveProps = {
 
 const Property = (props: Props) => {
   const { data } = props;
+  console.log(data);
   const device = InstanceModel.detail;
   const params = useParams<{ id: string }>();
   const [subscribeTopic] = useSendWebsocketMessage();
@@ -193,9 +194,6 @@ const Property = (props: Props) => {
             }}
             style={{ width: 300 }}
           />
-          {/* <Checkbox onChange={() => {
-
-                    }}>仅显示当前有数据的属性</Checkbox> */}
         </Space>
         <CheckButton
           value={check}
@@ -216,25 +214,32 @@ const Property = (props: Props) => {
         ) : (
           <Table pagination={false} columns={columns} dataSource={dataSource.data} rowKey="id" />
         )}
-        <div
-          style={{ marginTop: '20px', width: '100%', display: 'flex', justifyContent: 'flex-end' }}
-        >
-          <Pagination
-            className={styles.page}
-            defaultCurrent={1}
-            total={dataSource.total}
-            showSizeChanger
-            pageSize={dataSource.pageSize}
-            onChange={(page: number, size: number) => {
-              setDataSource({
-                total: propertyList.length,
-                data: (propertyList || []).slice((page - 1) * size, page * size),
-                pageSize: size,
-                currentPage: page - 1,
-              });
+        {dataSource.data.length > 0 && (
+          <div
+            style={{
+              marginTop: '20px',
+              width: '100%',
+              display: 'flex',
+              justifyContent: 'flex-end',
             }}
-          />
-        </div>
+          >
+            <Pagination
+              className={styles.page}
+              defaultCurrent={1}
+              total={dataSource.total}
+              showSizeChanger
+              pageSize={dataSource.pageSize}
+              onChange={(page: number, size: number) => {
+                setDataSource({
+                  total: propertyList.length,
+                  data: (propertyList || []).slice((page - 1) * size, page * size),
+                  pageSize: size,
+                  currentPage: page - 1,
+                });
+              }}
+            />
+          </div>
+        )}
       </div>
       <EditProperty
         data={currentInfo}

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

@@ -11,7 +11,7 @@ const Running = () => {
     <Card>
       <Tabs defaultActiveKey="1" tabPosition="left" style={{ height: 600 }}>
         <Tabs.TabPane tab="属性" key="1">
-          <Property data={metadata?.properties || {}} />
+          <Property data={metadata?.properties || []} />
         </Tabs.TabPane>
         {metadata.events?.map((item) => (
           <Tabs.TabPane tab={item.name} key={item.id}>

+ 1 - 0
src/pages/device/Instance/Export/index.tsx

@@ -102,6 +102,7 @@ const Export = (props: Props) => {
   };
   return (
     <Modal
+      maskClosable={false}
       visible={visible}
       onCancel={() => close()}
       width="35vw"

+ 1 - 0
src/pages/device/Instance/Import/index.tsx

@@ -254,6 +254,7 @@ const Import = (props: Props) => {
 
   return (
     <Modal
+      maskClosable={false}
       visible={visible}
       onCancel={() => close()}
       width="35vw"

+ 1 - 0
src/pages/device/Instance/Process/index.tsx

@@ -64,6 +64,7 @@ const Process = (props: Props) => {
   }, []);
   return (
     <Modal
+      maskClosable={false}
       title="当前进度"
       visible
       confirmLoading={flag}

+ 1 - 0
src/pages/device/Instance/Save/index.tsx

@@ -101,6 +101,7 @@ const Save = (props: Props) => {
 
   return (
     <Modal
+      maskClosable={false}
       visible={visible}
       onCancel={() => {
         form.resetFields();

+ 89 - 0
src/pages/device/Product/Detail/Access/index.less

@@ -0,0 +1,89 @@
+.box {
+  display: flex;
+  justify-content: space-between;
+}
+
+.images {
+  width: 64px;
+  height: 64px;
+  color: white;
+  font-size: 18px;
+  line-height: 64px;
+  text-align: center;
+  background: linear-gradient(
+    128.453709216706deg,
+    rgba(255, 255, 255, 1) 4%,
+    rgba(113, 187, 255, 1) 43%,
+    rgba(24, 144, 255, 1) 100%
+  );
+  border: 1px solid rgba(242, 242, 242, 1);
+  border-radius: 50%;
+}
+
+.content {
+  display: flex;
+  flex-direction: column;
+  width: calc(100% - 80px);
+}
+
+.top {
+  display: flex;
+}
+
+.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;
+}
+
+.title {
+  font-weight: 600;
+  font-size: 14px;
+}
+
+.container {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  height: 90px;
+  margin-top: 10px;
+}
+
+.server {
+  width: 50%;
+  margin-right: 20px;
+}
+
+.procotol {
+  display: -webkit-box;
+  width: 50%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 4;
+}
+
+:global {
+  .ant-pagination-item {
+    display: none;
+  }
+}
+
+.config {
+  width: 48%;
+  .title {
+    width: 100%;
+    margin-bottom: 10px;
+    font-weight: 600;
+  }
+  .title::before {
+    margin-right: 10px;
+    background-color: #2810ff;
+    content: '|';
+  }
+}

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

@@ -0,0 +1,426 @@
+import {
+  Alert,
+  Badge,
+  Button,
+  Card,
+  Col,
+  Descriptions,
+  message,
+  Pagination,
+  Row,
+  Table,
+  Tooltip,
+} from 'antd';
+import { service } from '@/pages/link/AccessConfig';
+import styles from './index.less';
+import { useEffect, useState } from 'react';
+import Service from '@/pages/device/Product/service';
+import { productModel } from '@/pages/device/Product';
+import type { ProColumns } from '@jetlinks/pro-table';
+import SearchComponent from '@/components/SearchComponent';
+import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
+import { useHistory } from 'umi';
+
+const Access = () => {
+  const service1 = new Service('device-product');
+  const [currrent, setCurrrent] = useState<any>({
+    id: productModel.current?.accessId,
+    name: productModel.current?.accessName,
+    protocol: productModel.current?.messageProtocol,
+    transport: productModel.current?.transportProtocol,
+    protocolDetail: {
+      name: productModel.current?.protocolName,
+    },
+  });
+  const [visible, setVisible] = useState<boolean>(true);
+  const [config, setConfig] = useState<any>();
+  const [access, setAccess] = useState<any>();
+  const [providers, setProviders] = useState<any[]>([]);
+  const [networkList, setNetworkList] = useState<any[]>([]);
+
+  const MetworkTypeMapping = new Map();
+  MetworkTypeMapping.set('websocket-server', 'WEB_SOCKET_SERVER');
+  MetworkTypeMapping.set('http-server-gateway', 'HTTP_SERVER');
+  MetworkTypeMapping.set('udp-device-gateway', 'UDP');
+  MetworkTypeMapping.set('coap-server-gateway', 'COAP_SERVER');
+  MetworkTypeMapping.set('mqtt-client-gateway', 'MQTT_CLIENT');
+  MetworkTypeMapping.set('mqtt-server-gateway', 'MQTT_SERVER');
+  MetworkTypeMapping.set('tcp-server-gateway', 'TCP_SERVER');
+
+  const [param, setParam] = useState<any>({ pageSize: 10 });
+  const history = useHistory();
+
+  const columns: ProColumns<any>[] = [
+    {
+      title: '名称',
+      dataIndex: 'name',
+    },
+  ];
+
+  const queryNetworkList = (id: string) => {
+    service.getNetworkList(MetworkTypeMapping.get(id)).then((resp) => {
+      if (resp.status === 200) {
+        setNetworkList(resp.result);
+      }
+    });
+  };
+
+  const [dataSource, setDataSource] = useState<any>({
+    data: [],
+    pageSize: 10,
+    pageIndex: 0,
+    total: 0,
+  });
+
+  const handleSearch = (params: any) => {
+    setParam(params);
+    service
+      .queryList({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+      .then((resp) => {
+        setDataSource(resp.result);
+      });
+  };
+
+  const queryProviders = () => {
+    service.getProviders().then((resp) => {
+      if (resp.status === 200) {
+        setProviders(resp.result);
+      }
+    });
+  };
+
+  const queryAccess = (id: string) => {
+    service.queryList({ pageSize: 1000 }).then((resp) => {
+      const dt = resp.result.data.find((i: any) => i.id === id);
+      setAccess(dt);
+      if (dt) {
+        queryNetworkList(dt?.provider);
+      }
+    });
+  };
+  const columnsMQTT: any[] = [
+    {
+      title: '分组',
+      dataIndex: 'group',
+      key: 'group',
+      ellipsis: true,
+      align: 'center',
+      render: (text: any) => (
+        <Tooltip placement="top" title={text}>
+          {text}
+        </Tooltip>
+      ),
+    },
+    {
+      title: 'qos',
+      dataIndex: 'qos',
+      key: 'qos',
+      ellipsis: true,
+      align: 'center',
+    },
+    {
+      title: '地址',
+      dataIndex: 'address',
+      key: 'address',
+      ellipsis: true,
+      align: 'center',
+      render: (text: any) => (
+        <Tooltip placement="top" title={text}>
+          {text}
+        </Tooltip>
+      ),
+    },
+    {
+      title: 'topic',
+      dataIndex: 'topic',
+      key: 'topic',
+      ellipsis: true,
+      align: 'center',
+      render: (text: any) => (
+        <Tooltip placement="top" title={text}>
+          {text}
+        </Tooltip>
+      ),
+    },
+    {
+      title: '说明',
+      dataIndex: 'description',
+      key: 'description',
+      ellipsis: true,
+      align: 'center',
+      render: (text: any) => (
+        <Tooltip placement="top" title={text}>
+          {text}
+        </Tooltip>
+      ),
+    },
+  ];
+
+  const columnsHTTP: any[] = [
+    {
+      title: '地址',
+      dataIndex: 'address',
+      key: 'address',
+      ellipsis: true,
+      align: 'center',
+      render: (text: any) => (
+        <Tooltip placement="top" title={text}>
+          {text}
+        </Tooltip>
+      ),
+    },
+    {
+      title: '分组',
+      dataIndex: 'group',
+      key: 'group',
+      ellipsis: true,
+      align: 'center',
+      render: (text: any) => (
+        <Tooltip placement="top" title={text}>
+          {text}
+        </Tooltip>
+      ),
+    },
+    {
+      title: '示例',
+      dataIndex: 'example',
+      key: 'example',
+      ellipsis: true,
+      align: 'center',
+      render: (text: any) => (
+        <Tooltip placement="top" title={text}>
+          {text}
+        </Tooltip>
+      ),
+    },
+    {
+      title: '说明',
+      dataIndex: 'description',
+      key: 'description',
+      ellipsis: true,
+      align: 'center',
+      render: (text: any) => (
+        <Tooltip placement="top" title={text}>
+          {text}
+        </Tooltip>
+      ),
+    },
+  ];
+
+  const getDetail = (messageProtocol: string, transportProtocol: string) => {
+    service.getConfigView(messageProtocol, transportProtocol).then((resp) => {
+      if (resp.status === 200) {
+        setConfig(resp.result);
+      }
+    });
+  };
+
+  useEffect(() => {
+    queryProviders();
+    setVisible(!!productModel.current?.accessId);
+    if (productModel.current?.accessId) {
+      getDetail(
+        productModel.current?.messageProtocol || '',
+        productModel.current?.transportProtocol || '',
+      );
+      queryAccess(productModel.current?.accessId);
+    } else {
+      handleSearch(param);
+    }
+  }, []);
+
+  return (
+    <div>
+      {!visible ? (
+        <div style={{ padding: '20px 0' }}>
+          <Alert message="请选择使用的设备接入网关,用以提供设备接入能力" type="info" showIcon />
+          <SearchComponent
+            field={columns}
+            pattern={'simple'}
+            onSearch={(data: any) => {
+              const dt = {
+                pageSize: 10,
+                terms: [...data.terms],
+              };
+              handleSearch(dt);
+            }}
+            onReset={() => {
+              handleSearch({ pageSize: 10 });
+            }}
+          />
+          <Button
+            type="primary"
+            onClick={() => {
+              history.push(`${getMenuPathByCode(MENUS_CODE['link/AccessConfig/Detail'])}`);
+            }}
+          >
+            新增
+          </Button>
+          <Row gutter={[16, 16]} style={{ marginTop: 10 }}>
+            {dataSource.data.map((item: any) => (
+              <Col key={item.name} span={12}>
+                <Card
+                  style={{
+                    width: '100%',
+                    border: currrent?.id === item.id ? '1px solid #1d39c4' : '',
+                  }}
+                  hoverable
+                  onClick={() => {
+                    setCurrrent(item);
+                  }}
+                >
+                  <div className={styles.box}>
+                    <div className={styles.images}>{item.name}</div>
+                    <div className={styles.content}>
+                      <div className={styles.header}>
+                        <div className={styles.top}>
+                          <div className={styles.title}>{item.name}</div>
+                          <div className={styles.status}>
+                            <Badge
+                              color={item.state.value === 'disabled' ? 'red' : 'green'}
+                              text={item.state.text}
+                              style={{ marginLeft: '20px' }}
+                            />
+                          </div>
+                        </div>
+                        <div className={styles.desc}>这里是接入方式的解释说明</div>
+                      </div>
+                      <div className={styles.container}>
+                        <div className={styles.server}>
+                          <div className={styles.title}>{item?.channelInfo?.name}</div>
+                          <p>
+                            {item.channelInfo?.addresses.map((i: any) => (
+                              <div key={i.address}>
+                                <Badge color={i.health === -1 ? 'red' : 'green'} text={i.address} />
+                              </div>
+                            ))}
+                          </p>
+                        </div>
+                        <div className={styles.procotol}>
+                          <div className={styles.title}>{item?.protocolDetail?.name}</div>
+                          <p style={{ color: 'rgba(0, 0, 0, .55)' }}>{item.description}</p>
+                        </div>
+                      </div>
+                    </div>
+                  </div>
+                </Card>
+              </Col>
+            ))}
+          </Row>
+          <div style={{ display: 'flex', marginTop: 20, justifyContent: 'flex-end' }}>
+            <Pagination
+              showSizeChanger
+              size="small"
+              className={'pro-table-card-pagination'}
+              total={dataSource?.total || 0}
+              current={dataSource?.pageIndex + 1}
+              onChange={(page, size) => {
+                handleSearch({
+                  ...param,
+                  pageIndex: page - 1,
+                  pageSize: size,
+                });
+              }}
+              pageSizeOptions={[10, 20, 50, 100]}
+              pageSize={dataSource?.pageSize}
+              showTotal={(num) => {
+                const minSize = dataSource?.pageIndex * dataSource?.pageSize + 1;
+                const MaxSize = (dataSource?.pageIndex + 1) * dataSource?.pageSize;
+                return `第 ${minSize} - ${MaxSize > num ? num : MaxSize} 条/总共 ${num} 条`;
+              }}
+            />
+          </div>
+          <div style={{ marginTop: '20px' }}>
+            <Button
+              type="primary"
+              onClick={() => {
+                if (!!currrent) {
+                  service1
+                    .update({
+                      ...productModel.current,
+                      transportProtocol: currrent.transport,
+                      protocolName: currrent.protocolDetail.name,
+                      accessId: currrent.id,
+                      accessName: currrent.name,
+                      messageProtocol: currrent.protocol,
+                    })
+                    .then((resp) => {
+                      if (resp.status === 200) {
+                        service1.detail(productModel.current?.id || '').then((res) => {
+                          if (res.status === 200) {
+                            productModel.current = { ...res.result };
+                            message.success('操作成功!');
+                            setVisible(true);
+                            getDetail(res.result.messageProtocol, res.result.transportProtocol);
+                            queryAccess(res.result.accessId);
+                          }
+                        });
+                      }
+                    });
+                } else {
+                  message.success('请选择接入方式');
+                }
+              }}
+            >
+              保存
+            </Button>
+          </div>
+        </div>
+      ) : (
+        <div className={styles.config}>
+          <div className={styles.title}>配置概览</div>
+          <Descriptions column={1}>
+            <Descriptions.Item label="接入方式">
+              {providers.find((i) => i.id === access?.provider)?.name || ''}
+            </Descriptions.Item>
+            <Descriptions.Item>
+              {providers.find((i) => i.id === access?.provider)?.description || ''}
+            </Descriptions.Item>
+            <Descriptions.Item label="消息协议">
+              {access?.protocolDetail?.name || ''}
+            </Descriptions.Item>
+            <Descriptions.Item>{access?.protocolDetail?.description || ''}</Descriptions.Item>
+            <Descriptions.Item label="网络组件">
+              {(networkList.find((i) => i.id === access?.channelId)?.addresses || []).map(
+                (item: any) => (
+                  <div key={item.address}>
+                    <Badge
+                      color={item.health === -1 ? 'red' : 'green'}
+                      text={item.address}
+                      style={{ marginLeft: '20px' }}
+                    />
+                  </div>
+                ),
+              )}
+            </Descriptions.Item>
+          </Descriptions>
+          {config?.routes && config?.routes?.length > 0 && (
+            <div>
+              <div>路由信息:</div>
+              <Table
+                dataSource={config?.routes || []}
+                columns={config.id === 'MQTT' ? columnsMQTT : columnsHTTP}
+                pagination={false}
+                scroll={{ x: 500 }}
+              />
+            </div>
+          )}
+
+          <div style={{ marginTop: '20px' }}>
+            <Button
+              type="primary"
+              onClick={() => {
+                setVisible(false);
+                handleSearch({ pageSize: 10 });
+              }}
+            >
+              编辑
+            </Button>
+          </div>
+        </div>
+      )}
+    </div>
+  );
+};
+
+export default Access;

+ 2 - 1
src/pages/device/Product/Detail/PropertyImport/index.tsx

@@ -177,8 +177,9 @@ const PropertyImport = (props: Props) => {
 
   return (
     <Modal
+      maskClosable={false}
       visible
-      onCancel={() => close()}
+      onCancel={() => (MetadataModel.importMetadata = false)}
       width="35vw"
       title="导入属性"
       onOk={() => {

+ 40 - 34
src/pages/device/Product/Detail/index.tsx

@@ -7,7 +7,8 @@ import { productModel, service } from '@/pages/device/Product';
 import { useCallback, useEffect, useState } from 'react';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import Metadata from '@/pages/device/components/Metadata';
-import Alarm from '@/pages/device/components/Alarm';
+// import Alarm from '@/pages/device/components/Alarm';
+import Access from '@/pages/device/Product/Detail/Access';
 import type { DeviceMetadata } from '@/pages/device/Product/typings';
 import { Store } from 'jetlinks-store';
 import MetadataAction from '@/pages/device/components/Metadata/DataBaseAction';
@@ -170,47 +171,52 @@ const ProductDetail = observer(() => {
           </Tabs.TabPane>
           <Tabs.TabPane
             tab={
-              <Tooltip
-                placement="right"
-                title={
-                  <div>
-                    <p>
-                      属性:
-                      <br />
-                      用于描述设备运行时具体信息和状态。
-                      例如,环境监测设备所读取的当前环境温度、智能灯开关状态、电风扇风力等级等。
-                    </p>
-                    功能:
-                    <br />
-                    <p>
-                      指设备可供外部调用的指令或方法。功能调用中可设置输入和输出参数。输入参数是服务执行时的参数,输出参数是服务执行后的结果。
-                      相比于属性,功能可通过一条指令实现更复杂的业务逻辑,例如执行某项特定的任务。
-                      功能分为异步和同步两种调用方式。
-                    </p>
-                    <p> 事件:</p>
-                    <p>
-                      设备运行时,主动上报给云端的信息,一般包含需要被外部感知和处理的信息、告警和故障。事件中可包含多个输出参数。
-                      例如,某项任务完成后的通知信息;设备发生故障时的温度、时间信息;设备告警时的运行状态等。
-                    </p>
-                    <p> 标签:</p>
-                    <p>
-                      统一为设备添加拓展字段,添加后将在设备信息页显示。可用于对设备基本信息描述的补充。
-                    </p>
-                  </div>
-                }
-              >
+              <>
                 {intl.formatMessage({
                   id: 'pages.device.productDetail.metadata',
                   defaultMessage: '物模型',
                 })}
-                <QuestionCircleOutlined />
-              </Tooltip>
+                <Tooltip
+                  placement="right"
+                  title={
+                    <div>
+                      <p>
+                        属性:
+                        <br />
+                        用于描述设备运行时具体信息和状态。
+                        例如,环境监测设备所读取的当前环境温度、智能灯开关状态、电风扇风力等级等。
+                      </p>
+                      功能:
+                      <br />
+                      <p>
+                        指设备可供外部调用的指令或方法。功能调用中可设置输入和输出参数。输入参数是服务执行时的参数,输出参数是服务执行后的结果。
+                        相比于属性,功能可通过一条指令实现更复杂的业务逻辑,例如执行某项特定的任务。
+                        功能分为异步和同步两种调用方式。
+                      </p>
+                      <p> 事件:</p>
+                      <p>
+                        设备运行时,主动上报给云端的信息,一般包含需要被外部感知和处理的信息、告警和故障。事件中可包含多个输出参数。
+                        例如,某项任务完成后的通知信息;设备发生故障时的温度、时间信息;设备告警时的运行状态等。
+                      </p>
+                      <p> 标签:</p>
+                      <p>
+                        统一为设备添加拓展字段,添加后将在设备信息页显示。可用于对设备基本信息描述的补充。
+                      </p>
+                    </div>
+                  }
+                >
+                  <QuestionCircleOutlined style={{ marginLeft: '5px' }} />
+                </Tooltip>
+              </>
             }
             key="metadata"
           >
             <Metadata type="product" />
           </Tabs.TabPane>
-          <Tabs.TabPane
+          <Tabs.TabPane tab={'设备接入'} key="access">
+            <Access />
+          </Tabs.TabPane>
+          {/* <Tabs.TabPane
             tab={intl.formatMessage({
               id: 'pages.device.productDetail.alarm',
               defaultMessage: '告警设置',
@@ -218,7 +224,7 @@ const ProductDetail = observer(() => {
             key="alarm"
           >
             <Alarm type="product" />
-          </Tabs.TabPane>
+          </Tabs.TabPane> */}
         </Tabs>
       </Card>
     </PageContainer>

+ 1 - 0
src/pages/device/Product/Save/index.tsx

@@ -102,6 +102,7 @@ const Save = (props: Props) => {
 
   return (
     <Modal
+      maskClosable={false}
       visible={visible}
       onCancel={() => {
         form.resetFields();

+ 6 - 0
src/pages/device/Product/service.ts

@@ -134,6 +134,12 @@ class Service extends BaseService<ProductItem> {
       method: 'GET',
     });
 
+  public getNetwork = (params: any) =>
+    request(`/${SystemConst.API_BASE}/network/config/_query/`, {
+      method: 'GET',
+      params,
+    });
+
   //上传物模型属性
   public importProductProperty = (productId: string, fileUrl: string) =>
     request(

+ 1 - 0
src/pages/device/components/Alarm/Edit/index.tsx

@@ -310,6 +310,7 @@ const EditAlarm = (props: Props) => {
   };
   return (
     <Drawer
+      maskClosable={false}
       title="编辑告警"
       visible={visible}
       onClose={() => close()}

+ 1 - 0
src/pages/device/components/Alarm/Record/index.tsx

@@ -120,6 +120,7 @@ const Record = (props: Props) => {
         search={false}
       />
       <Modal
+        maskClosable={false}
         title="处理告警"
         visible={visible}
         onCancel={() => setVisible(false)}

+ 66 - 21
src/pages/device/components/Metadata/Base/Edit/index.tsx

@@ -2,7 +2,7 @@ import { Button, Drawer, Dropdown, Menu, message } from 'antd';
 import { createSchemaField, observer } from '@formily/react';
 import MetadataModel from '../model';
 import type { Field, IFieldState } from '@formily/core';
-import { createForm } from '@formily/core';
+import { createForm, registerValidateRules } from '@formily/core';
 import {
   ArrayItems,
   Editable,
@@ -133,6 +133,13 @@ const Edit = observer((props: Props) => {
     },
   });
 
+  registerValidateRules({
+    validateId(value) {
+      if (!value) return '';
+      const reg = new RegExp('^\\w{3,20}$');
+      return reg.exec(value) ? '' : 'ID只能由数字、26个英文字母或者下划线组成';
+    },
+  });
   const valueTypeConfig = {
     type: 'object',
     'x-index': 4,
@@ -142,7 +149,16 @@ const Edit = observer((props: Props) => {
         required: true,
         'x-decorator': 'FormItem',
         'x-component': 'Select',
-        enum: DataTypeList,
+        default: MetadataModel.type === 'events' ? 'object' : null,
+        enum:
+          MetadataModel.type === 'events'
+            ? [
+                {
+                  value: 'object',
+                  label: 'object(结构体)',
+                },
+              ]
+            : DataTypeList,
       },
       unit: {
         title: intl.formatMessage({
@@ -152,6 +168,12 @@ const Edit = observer((props: Props) => {
         'x-decorator': 'FormItem',
         'x-component': 'Select',
         'x-visible': false,
+        'x-component-props': {
+          showSearch: true,
+          showArrow: true,
+          filterOption: (input: string, option: any) =>
+            option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+        },
         'x-reactions': [
           {
             dependencies: ['.type'],
@@ -329,7 +351,11 @@ const Edit = observer((props: Props) => {
         },
         {
           required: true,
-          message: '请输入ID',
+          message: '请输入标识',
+        },
+        {
+          validateId: true,
+          message: 'ID只能由数字、26个英文字母或者下划线组成',
         },
       ],
     },
@@ -388,6 +414,24 @@ const Edit = observer((props: Props) => {
             'x-component': 'Select',
             enum: PropertySource,
           },
+          'virtualRule.script': {
+            type: 'string',
+            'x-component': 'FRuleEditor',
+            'x-visible': false,
+            'x-reactions': [
+              {
+                dependencies: ['..source', 'id'],
+                fulfill: {
+                  state: {
+                    visible: '{{$deps[0]==="rule"}}',
+                  },
+                  schema: {
+                    'x-component-props.property': '{{$deps[1]}}',
+                  },
+                },
+              },
+            ],
+          },
           virtualRule: {
             type: 'object',
             title: '规则配置',
@@ -402,24 +446,24 @@ const Edit = observer((props: Props) => {
               },
             },
             properties: {
-              script: {
-                type: 'string',
-                'x-component': 'FRuleEditor',
-                'x-visible': false,
-                'x-reactions': [
-                  {
-                    dependencies: ['.source', '..id'],
-                    fulfill: {
-                      state: {
-                        visible: '{{$deps[0]==="rule"}}',
-                      },
-                      schema: {
-                        'x-component-props.property': '{{$deps[1]}}',
-                      },
-                    },
-                  },
-                ],
-              },
+              // script: {
+              //   type: 'string',
+              //   'x-component': 'FRuleEditor',
+              //   'x-visible': false,
+              //   'x-reactions': [
+              //     {
+              //       dependencies: ['..source', '..id'],
+              //       fulfill: {
+              //         state: {
+              //           visible: '{{$deps[0]==="rule"}}',
+              //         },
+              //         schema: {
+              //           'x-component-props.property': '{{$deps[1]}}',
+              //         },
+              //       },
+              //     },
+              //   ],
+              // },
 
               windowType: {
                 type: 'string',
@@ -764,6 +808,7 @@ const Edit = observer((props: Props) => {
   return (
     <>
       <Drawer
+        maskClosable={false}
         width="25vw"
         visible
         title={`${intl.formatMessage({

+ 13 - 2
src/pages/device/components/Metadata/Base/columns.ts

@@ -25,7 +25,15 @@ const BaseColumns: ProColumns<MetadataItem>[] = [
 const EventColumns: ProColumns<MetadataItem>[] = BaseColumns.concat([
   {
     title: '事件级别',
-    dataIndex: 'expands.level',
+    dataIndex: 'expands',
+    render: (text: any) => {
+      const map = {
+        ordinary: '普通',
+        warn: '警告',
+        urgent: '紧急',
+      };
+      return map[text?.level] || '-';
+    },
   },
 ]);
 
@@ -33,6 +41,7 @@ const FunctionColumns: ProColumns<MetadataItem>[] = BaseColumns.concat([
   {
     title: '是否异步',
     dataIndex: 'async',
+    render: (text) => (text ? '是' : '否'),
   },
   {
     title: '是否只读',
@@ -44,7 +53,8 @@ const FunctionColumns: ProColumns<MetadataItem>[] = BaseColumns.concat([
 const PropertyColumns: ProColumns<MetadataItem>[] = BaseColumns.concat([
   {
     title: '数据类型',
-    dataIndex: 'dataType',
+    dataIndex: 'valueType',
+    render: (text: any) => text?.type,
   },
   {
     title: '是否只读',
@@ -57,6 +67,7 @@ const TagColumns: ProColumns<MetadataItem>[] = BaseColumns.concat([
   {
     title: '数据类型',
     dataIndex: 'valueType',
+    render: (text: any) => text?.type,
   },
   {
     title: '是否只读',

+ 6 - 1
src/pages/device/components/Metadata/Cat/index.tsx

@@ -33,7 +33,12 @@ const Cat = (props: Props) => {
     }
   };
   return (
-    <Drawer title="查看物模型" onClose={() => props.close()} visible={props.visible}>
+    <Drawer
+      maskClosable={false}
+      title="查看物模型"
+      onClose={() => props.close()}
+      visible={props.visible}
+    >
       <div style={{ background: 'rgb(236, 237, 238)' }}>
         <p style={{ padding: 10 }}>
           物模型是对设备在云端的功能描述,包括设备的属性、服务和事件。物联网平台通过定义一种物的描述语言来描述物模型,称之为

+ 1 - 0
src/pages/device/components/Metadata/Import/index.tsx

@@ -188,6 +188,7 @@ const Import = (props: Props) => {
   };
   return (
     <Modal
+      maskClosable={false}
       title="导入物模型"
       destroyOnClose
       visible={props.visible}

+ 213 - 102
src/pages/link/AccessConfig/Detail/Access/index.tsx

@@ -11,6 +11,8 @@ import {
   message,
   Row,
   Steps,
+  Table,
+  Tooltip,
 } from 'antd';
 import { useEffect, useState } from 'react';
 import styles from './index.less';
@@ -22,6 +24,7 @@ import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
 interface Props {
   change: () => void;
   data: any;
+  access: any;
 }
 const Access = (props: Props) => {
   const [form] = Form.useForm();
@@ -31,9 +34,10 @@ const Access = (props: Props) => {
   const [current, setCurrent] = useState<number>(0);
   const [networkList, setNetworkList] = useState<any[]>([]);
   const [procotolList, setProcotolList] = useState<any[]>([]);
-  const [procotolCurrent, setProcotolCurrent] = useState<string>('');
-  const [networkCurrent, setNetworkCurrent] = useState<string>('');
-  // const [config, setConfig] = useState<any>();
+  const [procotolCurrent, setProcotolCurrent] = useState<string>(props.access?.protocol || '');
+  const [networkCurrent, setNetworkCurrent] = useState<string>(props.access?.channelId || '');
+  const [config, setConfig] = useState<any>();
+  const [providers, setProviders] = useState<any[]>([]);
 
   const MetworkTypeMapping = new Map();
   MetworkTypeMapping.set('websocket-server', 'WEB_SOCKET_SERVER');
@@ -54,17 +58,29 @@ const Access = (props: Props) => {
   ProcotoleMapping.set('tcp-server-gateway', 'TCP');
 
   const queryNetworkList = (params?: any) => {
-    service.getNetworkList(MetworkTypeMapping.get(props.data?.id), params).then((resp) => {
-      if (resp.status === 200) {
-        setNetworkList(resp.result);
-      }
-    });
+    service
+      .getNetworkList(MetworkTypeMapping.get(props.data?.id || props.access?.provider), params)
+      .then((resp) => {
+        if (resp.status === 200) {
+          setNetworkList(resp.result);
+        }
+      });
   };
 
   const queryProcotolList = (params?: any) => {
-    service.getProtocolList(ProcotoleMapping.get(props.data?.id), params).then((resp) => {
+    service
+      .getProtocolList(ProcotoleMapping.get(props.data?.id || props.access?.provider), params)
+      .then((resp) => {
+        if (resp.status === 200) {
+          setProcotolList(resp.result);
+        }
+      });
+  };
+
+  const queryProviders = () => {
+    service.getProviders().then((resp) => {
       if (resp.status === 200) {
-        setProcotolList(resp.result);
+        setProviders(resp.result);
       }
     });
   };
@@ -76,6 +92,14 @@ const Access = (props: Props) => {
     }
   }, [props.data]);
 
+  useEffect(() => {
+    form.setFieldsValue({
+      name: props.access?.name,
+      description: props.access?.name,
+    });
+    queryProviders();
+  }, [props.access]);
+
   const next = () => {
     if (current === 0) {
       if (!networkCurrent) {
@@ -90,10 +114,13 @@ const Access = (props: Props) => {
         message.error('请选择消息协议!');
       } else {
         service
-          .getConfigView(procotolCurrent, ProcotoleMapping.get(props.data?.id))
+          .getConfigView(
+            procotolCurrent,
+            ProcotoleMapping.get(props.data?.id || props.access?.provider),
+          )
           .then((resp) => {
             if (resp.status === 200) {
-              // setConfig(resp.result)
+              setConfig(resp.result);
             }
           });
         setCurrent(current + 1);
@@ -117,53 +144,114 @@ const Access = (props: Props) => {
     },
   ];
 
-  // const columns = [
-  //     {
-  //         title: '姓名',
-  //         dataIndex: 'name',
-  //         key: 'name',
-  //     },
-  //     {
-  //         title: '年龄',
-  //         dataIndex: 'age',
-  //         key: 'age',
-  //     },
-  //     {
-  //         title: '住址',
-  //         dataIndex: 'address',
-  //         key: 'address',
-  //     },
-  //     {
-  //         title: '姓名',
-  //         dataIndex: 'name',
-  //         key: 'name',
-  //     },
-  //     {
-  //         title: '年龄',
-  //         dataIndex: 'age',
-  //         key: 'age',
-  //     },
-  //     {
-  //         title: '住址',
-  //         dataIndex: 'address',
-  //         key: 'address',
-  //     },
-  // ];
+  const columnsMQTT: any[] = [
+    {
+      title: '分组',
+      dataIndex: 'group',
+      key: 'group',
+      ellipsis: true,
+      align: 'center',
+      render: (text: any) => (
+        <Tooltip placement="top" title={text}>
+          {text}
+        </Tooltip>
+      ),
+    },
+    {
+      title: 'qos',
+      dataIndex: 'qos',
+      key: 'qos',
+      ellipsis: true,
+      align: 'center',
+    },
+    {
+      title: '地址',
+      dataIndex: 'address',
+      key: 'address',
+      ellipsis: true,
+      align: 'center',
+      render: (text: any) => (
+        <Tooltip placement="top" title={text}>
+          {text}
+        </Tooltip>
+      ),
+    },
+    {
+      title: 'topic',
+      dataIndex: 'topic',
+      key: 'topic',
+      ellipsis: true,
+      align: 'center',
+      render: (text: any) => (
+        <Tooltip placement="top" title={text}>
+          {text}
+        </Tooltip>
+      ),
+    },
+    {
+      title: '说明',
+      dataIndex: 'description',
+      key: 'description',
+      ellipsis: true,
+      align: 'center',
+      render: (text: any) => (
+        <Tooltip placement="top" title={text}>
+          {text}
+        </Tooltip>
+      ),
+    },
+  ];
 
-  // const dataSource = [
-  //     {
-  //         key: '1',
-  //         name: '胡彦斌',
-  //         age: 32,
-  //         address: '西湖区湖底公园1号',
-  //     },
-  //     {
-  //         key: '2',
-  //         name: '胡彦祖',
-  //         age: 42,
-  //         address: '西湖区湖底公园1号',
-  //     },
-  // ];
+  const columnsHTTP: any[] = [
+    {
+      title: '地址',
+      dataIndex: 'address',
+      key: 'address',
+      ellipsis: true,
+      align: 'center',
+      render: (text: any) => (
+        <Tooltip placement="top" title={text}>
+          {text}
+        </Tooltip>
+      ),
+    },
+    {
+      title: '分组',
+      dataIndex: 'group',
+      key: 'group',
+      ellipsis: true,
+      align: 'center',
+      render: (text: any) => (
+        <Tooltip placement="top" title={text}>
+          {text}
+        </Tooltip>
+      ),
+    },
+    {
+      title: '示例',
+      dataIndex: 'example',
+      key: 'example',
+      ellipsis: true,
+      align: 'center',
+      render: (text: any) => (
+        <Tooltip placement="top" title={text}>
+          {text}
+        </Tooltip>
+      ),
+    },
+    {
+      title: '说明',
+      dataIndex: 'description',
+      key: 'description',
+      ellipsis: true,
+      align: 'center',
+      render: (text: any) => (
+        <Tooltip placement="top" title={text}>
+          {text}
+        </Tooltip>
+      ),
+    },
+  ];
 
   const renderSteps = (cur: number) => {
     switch (cur) {
@@ -210,16 +298,10 @@ const Access = (props: Props) => {
                     >
                       <div className={styles.title}>{item.name}</div>
                       <div className={styles.cardContent}>
-                        <div style={{ width: '40%' }}>
-                          <div className={styles.item}>
-                            {MetworkTypeMapping.get(props.data?.id)}
-                          </div>
-                          <div className={styles.item}>共享配置</div>
-                        </div>
-                        <div style={{ width: '60%' }}>
+                        <div style={{ width: '100%', height: '50px' }}>
                           {item.addresses.slice(0, 2).map((i: any) => (
                             <div className={styles.item} key={i.address}>
-                              公网: {i.address}
+                              <Badge color={i.health === -1 ? 'red' : 'green'} text={i.address} />
                             </div>
                           ))}
                         </div>
@@ -296,7 +378,7 @@ const Access = (props: Props) => {
                       }}
                     >
                       <div className={styles.title}>{item.name}</div>
-                      <div className={styles.desc}>这里是协议包中的协议说明</div>
+                      <div className={styles.desc}>{item.description}</div>
                     </Card>
                   </Col>
                 ))}
@@ -344,32 +426,41 @@ const Access = (props: Props) => {
             <div className={styles.config}>
               <div className={styles.title}>配置概览</div>
               <Descriptions column={1}>
-                <Descriptions.Item label="接入方式">{props.data?.name || ''}</Descriptions.Item>
+                <Descriptions.Item label="接入方式">
+                  {props.data?.name || providers.find((i) => i.id === props.access?.provider)?.name}
+                </Descriptions.Item>
                 <Descriptions.Item>{props.data?.description || ''}</Descriptions.Item>
                 <Descriptions.Item label="消息协议">
                   {procotolList.find((i) => i.id === procotolCurrent)?.name || ''}
                 </Descriptions.Item>
                 <Descriptions.Item>
-                  {procotolList.find((i) => i.id === procotolCurrent)?.description ||
-                    '----缺少描述呀----'}
+                  {procotolList.find((i) => i.id === procotolCurrent)?.description || ''}
                 </Descriptions.Item>
                 <Descriptions.Item label="网络组件">
                   {(networkList.find((i) => i.id === networkCurrent)?.addresses || []).map(
                     (item: any) => (
-                      <Badge
-                        key={item.address}
-                        color={item.health === -1 ? 'red' : 'green'}
-                        text={item.address}
-                        style={{ marginLeft: '20px' }}
-                      />
+                      <div key={item.address}>
+                        <Badge
+                          color={item.health === -1 ? 'red' : 'green'}
+                          text={item.address}
+                          style={{ marginLeft: '20px' }}
+                        />
+                      </div>
                     ),
                   )}
                 </Descriptions.Item>
               </Descriptions>
-              {/* <div>
-                            <div>路由信息</div>
-                            <Table dataSource={dataSource} columns={columns} pagination={false} />
-                        </div> */}
+              {config?.routes && config?.routes?.length > 0 && (
+                <div>
+                  <div>路由信息:</div>
+                  <Table
+                    dataSource={config?.routes || []}
+                    columns={config.id === 'MQTT' ? columnsMQTT : columnsHTTP}
+                    pagination={false}
+                    scroll={{ x: 500 }}
+                  />
+                </div>
+              )}
             </div>
           </div>
         );
@@ -384,6 +475,8 @@ const Access = (props: Props) => {
         type="link"
         onClick={() => {
           props.change();
+          setNetworkCurrent('');
+          setProcotolCurrent('');
         }}
       >
         返回
@@ -398,6 +491,11 @@ const Access = (props: Props) => {
         </div>
         <div className={styles.content}>{renderSteps(current)}</div>
         <div className={styles.action}>
+          {current > 0 && (
+            <Button style={{ margin: '0 8px' }} onClick={() => prev()}>
+              上一步
+            </Button>
+          )}
           {current < steps.length - 1 && (
             <Button type="primary" onClick={() => next()}>
               下一步
@@ -409,23 +507,41 @@ const Access = (props: Props) => {
               onClick={async () => {
                 try {
                   const values = await form.validateFields();
-                  const params = {
-                    name: values.name,
-                    description: values.description,
-                    provider: props.data.id,
-                    protocol: procotolCurrent,
-                    transport: ProcotoleMapping.get(props.data.id),
-                    channel: 'network', // 网络组件
-                    channelId: networkCurrent,
-                  };
-                  service.save(params).then((resp: any) => {
-                    if (resp.status === 200) {
-                      message.success('操作成功!');
-                      setCurrent(0);
-                      setNetworkCurrent('');
-                      setProcotolCurrent('');
-                    }
-                  });
+                  // 编辑还是保存
+                  if (!!props.access.id) {
+                    const params = {
+                      name: values.name,
+                      description: values.description,
+                      provider: props.data.id,
+                      protocol: procotolCurrent,
+                      transport: ProcotoleMapping.get(props.data.id),
+                      channel: 'network', // 网络组件
+                      channelId: networkCurrent,
+                    };
+                    service.save(params).then((resp: any) => {
+                      if (resp.status === 200) {
+                        message.success('操作成功!');
+                        history.push(`${getMenuPathByCode(MENUS_CODE['link/AccessConfig'])}`);
+                      }
+                    });
+                  } else {
+                    const params = {
+                      id: props.access?.id,
+                      name: values.name,
+                      description: values.description,
+                      provider: props.data.id,
+                      protocol: procotolCurrent,
+                      transport: ProcotoleMapping.get(props.data.id),
+                      channel: 'network', // 网络组件
+                      channelId: networkCurrent,
+                    };
+                    service.update(params).then((resp: any) => {
+                      if (resp.status === 200) {
+                        message.success('操作成功!');
+                        history.push(`${getMenuPathByCode(MENUS_CODE['link/AccessConfig'])}`);
+                      }
+                    });
+                  }
                 } catch (errorInfo) {
                   console.error('Failed:', errorInfo);
                 }
@@ -434,11 +550,6 @@ const Access = (props: Props) => {
               保存
             </Button>
           )}
-          {current > 0 && (
-            <Button style={{ margin: '0 8px' }} onClick={() => prev()}>
-              上一步
-            </Button>
-          )}
         </div>
       </div>
     </Card>

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

@@ -1,24 +1,44 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
+import { useLocation } from 'umi';
 import Access from './Access';
 import Provider from './Provider';
+import { service } from '@/pages/link/AccessConfig';
+
+type LocationType = {
+  id?: string;
+};
 
 const Detail = () => {
   const [visible, setVisible] = useState<boolean>(true);
-  const [id, setId] = useState<any>({});
+  const [data, setData] = useState<any>({});
+  const [config, setConfig] = useState<any>({});
+
+  const location = useLocation<LocationType>();
+
+  useEffect(() => {
+    const params = new URLSearchParams(location.search);
+    if (params.get('id')) {
+      service.detail(params.get('id') || '').then((resp) => {
+        setConfig(resp.result);
+        setVisible(false);
+      });
+    }
+  }, []);
 
   return (
     <PageContainer>
       {visible ? (
         <Provider
-          change={(data: string) => {
-            setId(data);
+          change={(param: any) => {
+            setData(param);
             setVisible(false);
           }}
         />
       ) : (
         <Access
-          data={id}
+          data={data}
+          access={config}
           change={() => {
             setVisible(true);
           }}

+ 58 - 40
src/pages/link/AccessConfig/index.less

@@ -1,31 +1,46 @@
+.box {
+  display: flex;
+  justify-content: space-between;
+}
+
+.images {
+  width: 64px;
+  height: 64px;
+  color: white;
+  font-size: 18px;
+  line-height: 64px;
+  text-align: center;
+  background: linear-gradient(
+    128.453709216706deg,
+    rgba(255, 255, 255, 1) 4%,
+    rgba(113, 187, 255, 1) 43%,
+    rgba(24, 144, 255, 1) 100%
+  );
+  border: 1px solid rgba(242, 242, 242, 1);
+  border-radius: 50%;
+}
+
 .content {
   display: flex;
-  width: 90%;
-  height: 100px;
-  margin: 0 78px;
-  overflow: hidden;
-  .server {
-    width: calc(50% - 78px);
+  flex-direction: column;
+  width: calc(100% - 80px);
+}
+
+.top {
+  display: flex;
+  justify-content: space-between;
 
-    :global {
-      .ant-badge-status-text {
-        color: rgba(0, 0, 0, 0.55);
-      }
-    }
+  .left {
+    display: flex;
   }
 
-  .procotol {
-    display: -webkit-box;
-    width: calc(50% - 78px);
-    overflow: hidden;
-    text-overflow: ellipsis;
-    -webkit-box-orient: vertical;
-    -webkit-line-clamp: 4;
+  .action a {
+    margin: 0 5px;
   }
 }
 
 .desc {
-  width: 50%;
+  width: 100%;
   margin-top: 10px;
   overflow: hidden;
   color: rgba(0, 0, 0, 0.55);
@@ -33,34 +48,37 @@
   font-size: 13px;
   white-space: nowrap;
   text-overflow: ellipsis;
-  background-color: antiquewhite;
 }
 
 .title {
-  margin-bottom: 10px;
   font-weight: 600;
   font-size: 14px;
 }
 
-:global {
-  .ant-list-item-meta-avatar {
-    width: 64px !important;
-    height: 64px !important;
-  }
+.container {
+  display: flex;
+  width: 100%;
+  height: 90px;
+  margin-top: 10px;
 }
 
-.images {
-  width: 64px;
-  height: 64px;
-  color: white;
-  font-size: 18px;
-  line-height: 64px;
-  text-align: center;
-  background: linear-gradient(
-    128.453709216706deg,
-    rgba(255, 255, 255, 1) 4%,
-    rgba(113, 187, 255, 1) 43%,
-    rgba(24, 144, 255, 1) 100%
-  );
-  border: 1px solid rgba(242, 242, 242, 1);
+.server {
+  align-items: center;
+  width: 50%;
+  margin-right: 20px;
+}
+
+.procotol {
+  display: -webkit-box;
+  width: 50%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 4;
+}
+
+:global {
+  .ant-pagination-item {
+    display: none;
+  }
 }

+ 158 - 151
src/pages/link/AccessConfig/index.tsx

@@ -2,10 +2,9 @@ import SearchComponent from '@/components/SearchComponent';
 import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
 import { CheckCircleOutlined, DeleteOutlined, EditOutlined, StopOutlined } from '@ant-design/icons';
 import { PageContainer } from '@ant-design/pro-layout';
-import ProList from '@jetlinks/pro-list';
 import type { ProColumns } from '@jetlinks/pro-table';
-import { Badge, Button, Card, message, Popconfirm } from 'antd';
-import { useState } from 'react';
+import { Badge, Button, Card, Col, message, Pagination, Popconfirm, Row } from 'antd';
+import { useEffect, useState } from 'react';
 import { useHistory } from 'umi';
 import styles from './index.less';
 import Service from './service';
@@ -14,41 +13,35 @@ export const service = new Service('gateway/device');
 
 const AccessConfig = () => {
   const history = useHistory();
-  const [param, setParam] = useState({});
-  // const actionRef = useRef<ActionType>();
+  const [param, setParam] = useState<any>({ pageSize: 10 });
 
   const columns: ProColumns<any>[] = [
     {
       title: '名称',
       dataIndex: 'name',
     },
-    // {
-    //   title: '状态',
-    //   dataIndex: 'state',
-    //   align: 'center',
-    //   valueType: 'select',
-    //   valueEnum: {
-    //     // 1: {
-    //     //   text: intl.formatMessage({
-    //     //     id: 'pages.searchTable.titleStatus.normal',
-    //     //     defaultMessage: '正常',
-    //     //   }),
-    //     //   status: 1,
-    //     // },
-    //     // 0: {
-    //     //   text: intl.formatMessage({
-    //     //     id: 'pages.searchTable.titleStatus.disable',
-    //     //     defaultMessage: '禁用',
-    //     //   }),
-    //     //   status: 0,
-    //     // },
-    //   },
-    //   render: (text, record) => (
-    //     <Badge status={record.status === 1 ? 'success' : 'error'} text={text} />
-    //   ),
-    // },
   ];
 
+  const [dataSource, setDataSource] = useState<any>({
+    data: [],
+    pageSize: 10,
+    pageIndex: 0,
+    total: 0,
+  });
+
+  const handleSearch = (params: any) => {
+    setParam(params);
+    service
+      .queryList({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+      .then((resp) => {
+        setDataSource(resp.result);
+      });
+  };
+
+  useEffect(() => {
+    handleSearch(param);
+  }, []);
+
   return (
     <PageContainer>
       <Card>
@@ -56,12 +49,14 @@ const AccessConfig = () => {
           field={columns}
           pattern={'simple'}
           onSearch={(data: any) => {
-            setParam(data);
-            // actionRef.current?.reset?.();
+            const dt = {
+              pageSize: 10,
+              terms: [...data.terms],
+            };
+            handleSearch(dt);
           }}
           onReset={() => {
-            setParam({});
-            // actionRef.current?.reset?.();
+            handleSearch({ pageSize: 10 });
           }}
         />
         <div style={{ width: '100%', display: 'flex', justifyContent: 'flex-end' }}>
@@ -74,126 +69,138 @@ const AccessConfig = () => {
             新增
           </Button>
         </div>
-        <ProList<any>
-          pagination={{
-            defaultPageSize: 8,
-            showSizeChanger: false,
-          }}
-          showActions="always"
-          rowKey="id"
-          // actionRef={actionRef}
-          request={async (data) =>
-            service.queryList({ ...param, ...data, sorts: [{ name: 'createTime', order: 'desc' }] })
-          }
-          grid={{ gutter: 16, column: 2 }}
-          showExtra="always"
-          metas={{
-            title: {
-              dataIndex: 'name',
-              render: (text, row) => (
-                <div style={{ fontSize: 16, width: '70%' }}>
-                  <div>
-                    {text}
-                    <Badge
-                      color={row.state.value === 'disabled' ? 'red' : 'green'}
-                      text={row.state.text}
-                      style={{ marginLeft: '20px' }}
-                    />
-                  </div>
-                  <div className={styles.desc}>{row.describe}</div>
-                </div>
-              ),
-            },
-            avatar: {
-              render: (text, reocrd) => <div className={styles.images}>{reocrd.name}</div>,
-            },
-            subTitle: {
-              render: () => <div></div>,
-            },
-            content: {
-              render: (text, row) => (
-                <div className={styles.content}>
-                  <div className={styles.server}>
-                    <div className={styles.title}>{row?.channelInfo?.name}</div>
-                    <p>
-                      {row.channelInfo?.addresses.map((item: any) => (
-                        <div key={item.address}>
-                          <Badge color={'green'} text={item.address} />
+        <Row gutter={[16, 16]} style={{ marginTop: 10 }}>
+          {dataSource.data.map((item: any) => (
+            <Col key={item.name} span={12}>
+              <Card hoverable>
+                <div className={styles.box}>
+                  <div className={styles.images}>{item.name}</div>
+                  <div className={styles.content}>
+                    <div className={styles.header}>
+                      <div className={styles.top}>
+                        <div className={styles.left}>
+                          <div className={styles.title}>{item.name}</div>
+                          <div className={styles.status}>
+                            <Badge
+                              color={item.state.value === 'disabled' ? 'red' : 'green'}
+                              text={item.state.text}
+                              style={{ marginLeft: '20px' }}
+                            />
+                          </div>
                         </div>
-                      ))}
-                    </p>
-                  </div>
-                  <div className={styles.procotol}>
-                    <div className={styles.title}>{row?.protocolDetail?.name}</div>
-                    <p style={{ color: 'rgba(0, 0, 0, .55)' }}>{row.description}</p>
+                        <div className={styles.action}>
+                          <a
+                            key="edit"
+                            onClick={() => {
+                              history.push(
+                                `${getMenuPathByCode(MENUS_CODE['link/AccessConfig/Detail'])}?id=${
+                                  item.id
+                                }`,
+                              );
+                            }}
+                          >
+                            <EditOutlined />
+                            编辑
+                          </a>
+                          <a key="warning">
+                            <Popconfirm
+                              title={`确认${item.state.value !== 'disabled' ? '禁用' : '启用'}`}
+                              onConfirm={() => {
+                                if (item.state.value !== 'disabled') {
+                                  service.shutDown(item.id).then((resp) => {
+                                    if (resp.status === 200) {
+                                      message.success('操作成功!');
+                                      handleSearch(param);
+                                    }
+                                  });
+                                } else {
+                                  service.startUp(item.id).then((resp) => {
+                                    if (resp.status === 200) {
+                                      message.success('操作成功!');
+                                      handleSearch(param);
+                                    }
+                                  });
+                                }
+                              }}
+                            >
+                              {item.state.value !== 'disabled' ? (
+                                <span>
+                                  <StopOutlined />
+                                  禁用
+                                </span>
+                              ) : (
+                                <span>
+                                  <CheckCircleOutlined />
+                                  启用
+                                </span>
+                              )}
+                            </Popconfirm>
+                          </a>
+                          <a key="remove">
+                            <Popconfirm
+                              title={'确认删除?'}
+                              onConfirm={() => {
+                                service.remove(item.id).then((resp: any) => {
+                                  if (resp.status === 200) {
+                                    message.success('操作成功!');
+                                    handleSearch(param);
+                                  }
+                                });
+                              }}
+                            >
+                              <DeleteOutlined />
+                              删除
+                            </Popconfirm>
+                          </a>
+                        </div>
+                      </div>
+                      <div className={styles.desc}>这里是接入方式的解释说明</div>
+                    </div>
+                    <div className={styles.container}>
+                      <div className={styles.server}>
+                        <div className={styles.title}>{item?.channelInfo?.name}</div>
+                        <p>
+                          {item.channelInfo?.addresses.map((i: any) => (
+                            <div key={i.address}>
+                              <Badge color={i.health === -1 ? 'red' : 'green'} text={i.address} />
+                            </div>
+                          ))}
+                        </p>
+                      </div>
+                      <div className={styles.procotol}>
+                        <div className={styles.title}>{item?.protocolDetail?.name}</div>
+                        <p style={{ color: 'rgba(0, 0, 0, .55)' }}>{item.description}</p>
+                      </div>
+                    </div>
                   </div>
                 </div>
-              ),
-            },
-            actions: {
-              render: (text, row) => [
-                <a
-                  key="edit"
-                  onClick={() => {
-                    history.push(
-                      `${getMenuPathByCode(MENUS_CODE['link/AccessConfig/Detail'])}?id=${row.id}`,
-                    );
-                  }}
-                >
-                  <EditOutlined />
-                  编辑
-                </a>,
-                <a key="warning">
-                  <Popconfirm
-                    title={`确认${row.state.value !== 'disabled' ? '禁用' : '启用'}`}
-                    onConfirm={() => {
-                      if (row.state.value !== 'disabled') {
-                        service.shutDown(row.id).then((resp) => {
-                          if (resp.status === 200) {
-                            message.success('操作成功!');
-                          }
-                        });
-                      } else {
-                        service.startUp(row.id).then((resp) => {
-                          if (resp.status === 200) {
-                            message.success('操作成功!');
-                          }
-                        });
-                      }
-                    }}
-                  >
-                    {row.state.value !== 'disabled' ? (
-                      <span>
-                        <StopOutlined />
-                        禁用
-                      </span>
-                    ) : (
-                      <span>
-                        <CheckCircleOutlined />
-                        启用
-                      </span>
-                    )}
-                  </Popconfirm>
-                </a>,
-                <a key="remove">
-                  <Popconfirm
-                    title={'确认删除?'}
-                    onConfirm={() => {
-                      service.remove(row.id).then((resp: any) => {
-                        if (resp.status === 200) {
-                          message.success('操作成功!');
-                        }
-                      });
-                    }}
-                  >
-                    <DeleteOutlined />
-                    删除
-                  </Popconfirm>
-                </a>,
-              ],
-            },
-          }}
-        />
+              </Card>
+            </Col>
+          ))}
+        </Row>
+        <div style={{ display: 'flex', marginTop: 20, justifyContent: 'flex-end' }}>
+          <Pagination
+            showSizeChanger
+            size="small"
+            className={'pro-table-card-pagination'}
+            total={dataSource?.total || 0}
+            current={dataSource?.pageIndex + 1}
+            onChange={(page, size) => {
+              handleSearch({
+                ...param,
+                pageIndex: page - 1,
+                pageSize: size,
+              });
+            }}
+            pageSizeOptions={[10, 20, 50, 100]}
+            pageSize={dataSource?.pageSize}
+            showTotal={(num) => {
+              const minSize = dataSource?.pageIndex * dataSource?.pageSize + 1;
+              const MaxSize = (dataSource?.pageIndex + 1) * dataSource?.pageSize;
+              return `第 ${minSize} - ${MaxSize > num ? num : MaxSize} 条/总共 ${num} 条`;
+            }}
+          />
+        </div>
       </Card>
     </PageContainer>
   );

+ 1 - 1
src/pages/link/AccessConfig/service.ts

@@ -14,7 +14,7 @@ class Service extends BaseService<AccessItem> {
       method: 'POST',
     });
   public shutDown = (id: string) =>
-    request(`/${SystemConst.API_BASE}/gateway/device/${id}/__shutdown`, {
+    request(`/${SystemConst.API_BASE}/gateway/device/${id}/_shutdown`, {
       method: 'POST',
     });
   public getProviders = () =>

+ 2 - 1
src/pages/link/Protocol/Debug/index.tsx

@@ -3,7 +3,7 @@ import type { ISchema } from '@formily/json-schema';
 import type { Field } from '@formily/core';
 import { createForm } from '@formily/core';
 import { createSchemaField } from '@formily/react';
-import { Input, Radio, Select, FormItem, Form, Space } from '@formily/antd';
+import { Form, FormItem, Input, Radio, Select, Space } from '@formily/antd';
 import FMonacoEditor from '@/components/FMonacoEditor';
 import type { ProtocolItem } from '@/pages/link/Protocol/typings';
 import { service } from '@/pages/link/Protocol';
@@ -169,6 +169,7 @@ const Debug = (props: Props) => {
 
   return (
     <Modal
+      maskClosable={false}
       title="调试"
       width="60vw"
       onCancel={() => close()}

+ 0 - 1
src/pages/link/Protocol/FileUpload/index.tsx

@@ -37,7 +37,6 @@ const FileUpload = connect((props: Props) => {
         <Input
           style={{ width: 'calc(100% - 100px)' }}
           value={url}
-          readOnly
           onClick={(e) => {
             e.preventDefault();
             e.stopPropagation();

+ 54 - 40
src/pages/link/Protocol/index.tsx

@@ -2,18 +2,14 @@ import { PageContainer } from '@ant-design/pro-layout';
 import type { ProtocolItem } from '@/pages/link/Protocol/typings';
 import { useRef } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
-import { message, Popconfirm, Tag, Tooltip } from 'antd';
-import {
-  CloudSyncOutlined,
-  DeleteOutlined,
-  EditOutlined,
-  PlayCircleOutlined,
-} from '@ant-design/icons';
+import { Badge, message, Popconfirm, Tooltip } from 'antd';
+import { CheckCircleOutlined, DeleteOutlined, EditOutlined, StopOutlined } from '@ant-design/icons';
 import BaseCrud from '@/components/BaseCrud';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import type { ISchema } from '@formily/json-schema';
 import { CurdModel } from '@/components/BaseCrud/model';
 import Service from '@/pages/link/Protocol/service';
+import { onFormMount, registerValidateRules } from '@formily/core';
 
 export const service = new Service('protocol');
 const Protocol = () => {
@@ -56,8 +52,9 @@ const Protocol = () => {
     {
       dataIndex: 'state',
       title: '状态',
-      renderText: (text) =>
-        text === 1 ? <Tag color="#108ee9">正常</Tag> : <Tag color="#F50">禁用</Tag>,
+      renderText: (text) => (
+        <Badge color={text !== 1 ? 'red' : 'green'} text={text !== 1 ? '未发布' : '已发布'} />
+      ),
     },
     {
       dataIndex: 'description',
@@ -90,55 +87,61 @@ const Protocol = () => {
         </a>,
         record.state !== 1 && (
           <a key="publish">
-            <Popconfirm title="发布?" onConfirm={() => modifyState(record.id, 'deploy')}>
+            <Popconfirm title="确认发布?" onConfirm={() => modifyState(record.id, 'deploy')}>
               <Tooltip title="发布">
-                <PlayCircleOutlined />
+                <CheckCircleOutlined />
               </Tooltip>
             </Popconfirm>
           </a>
         ),
         record.state === 1 && (
           <a key="reload">
-            <Popconfirm title="重新发布?" onConfirm={() => modifyState(record.id, 'deploy')}>
-              <Tooltip title="重新发布">
-                <CloudSyncOutlined />
+            <Popconfirm title="确认撤销?" onConfirm={() => modifyState(record.id, 'un-deploy')}>
+              <Tooltip title="撤销">
+                <StopOutlined />
               </Tooltip>
             </Popconfirm>
           </a>
         ),
-        record.state !== 1 && (
-          <a key="delete">
-            <Popconfirm
+        <a key="delete">
+          <Popconfirm
+            title={intl.formatMessage({
+              id: 'pages.data.option.remove.tips',
+              defaultMessage: '确认删除?',
+            })}
+            onConfirm={async () => {
+              await service.remove(record.id);
+              message.success(
+                intl.formatMessage({
+                  id: 'pages.data.option.success',
+                  defaultMessage: '操作成功!',
+                }),
+              );
+              actionRef.current?.reload();
+            }}
+          >
+            <Tooltip
               title={intl.formatMessage({
-                id: 'pages.data.option.remove.tips',
-                defaultMessage: '确认删除?',
+                id: 'pages.data.option.remove',
+                defaultMessage: '删除',
               })}
-              onConfirm={async () => {
-                await service.remove(record.id);
-                message.success(
-                  intl.formatMessage({
-                    id: 'pages.data.option.success',
-                    defaultMessage: '操作成功!',
-                  }),
-                );
-                actionRef.current?.reload();
-              }}
             >
-              <Tooltip
-                title={intl.formatMessage({
-                  id: 'pages.data.option.remove',
-                  defaultMessage: '删除',
-                })}
-              >
-                <DeleteOutlined />
-              </Tooltip>
-            </Popconfirm>
-          </a>
-        ),
+              <DeleteOutlined />
+            </Tooltip>
+          </Popconfirm>
+        </a>,
       ],
     },
   ];
 
+  registerValidateRules({
+    validateId(value) {
+      if (!value) return '';
+      const reg = new RegExp('^\\w{3,20}$');
+      return reg.exec(value) ? '' : 'ID只能由数字、26个英文字母或者下划线组成';
+    },
+  });
+
   const schema: ISchema = {
     type: 'object',
     properties: {
@@ -166,6 +169,10 @@ const Protocol = () => {
                 max: 64,
                 message: '最多可输入64个字符',
               },
+              {
+                validateId: true,
+                message: 'ID只能由数字、26个英文字母或者下划线组成',
+              },
             ],
           },
           name: {
@@ -283,6 +290,13 @@ const Protocol = () => {
         modelConfig={{ width: '550px' }}
         schema={schema}
         actionRef={actionRef}
+        formEffect={() => {
+          onFormMount((form) => {
+            form.setFieldState('id', (state) => {
+              state.disabled = CurdModel.model === 'edit';
+            });
+          });
+        }}
       />
       {/* {visible && <Debug data={current} close={() => setVisible(!visible)} />} */}
     </PageContainer>

+ 19 - 7
src/pages/link/Type/Save/index.tsx

@@ -32,8 +32,6 @@ import { Store } from 'jetlinks-store';
  * @param type
  */
 const filterConfigByType = (data: any[], type: string) => {
-  // UDP、TCP_SERVER、WEB_SOCKET_SERVER、HTTP_SERVER、MQTT_SERVER、COAP_SERVER
-
   const tcpList = ['TCP_SERVER', 'WEB_SOCKET_SERVER', 'HTTP_SERVER', 'MQTT_SERVER'];
   const udpList = ['UDP', 'COAP_SERVER'];
 
@@ -54,7 +52,6 @@ const filterConfigByType = (data: any[], type: string) => {
 const Save = observer(() => {
   // const param = useParams<{ id: string }>();
 
-  // const [config, setConfig] = useState<any[]>([]);
   const configRef = useRef([]);
 
   useEffect(() => {
@@ -105,6 +102,10 @@ const Save = observer(() => {
         effects() {
           onFieldValueChange('type', (field, f) => {
             const value = (field as Field).value;
+            f.deleteValuesIn('configuration');
+            f.deleteValuesIn('cluster');
+            f.clearErrors();
+
             const _host = filterConfigByType(_.cloneDeep(configRef.current), value);
             f.setFieldState('grid.configuration.panel1.layout2.host', (state) => {
               state.dataSource = _host.map((item) => ({ label: item.host, value: item.host }));
@@ -123,10 +124,16 @@ const Save = observer(() => {
               state.dataSource = _host?.ports.map((p: any) => ({ label: p, value: p }));
             });
           });
-          onFieldValueChange('shareCluster', (field) => {
+          onFieldValueChange('shareCluster', (field, f5) => {
             const value = (field as Field).value;
-            if (!value) {
-              // false 获取独立配置的信息
+            if (value) {
+              // 共享配置
+              f5.setFieldState('grid.configuration.panel1.layout2.host', (state) => {
+                state.value = '0.0.0.0';
+                state.disabled = true;
+              });
+            } else {
+              // 独立配置
             }
           });
           onFieldValueChange('grid.cluster.cluster.*.layout2.serverId', async (field, f3) => {
@@ -153,7 +160,8 @@ const Save = observer(() => {
   );
 
   useEffect(() => {
-    Store.subscribe('current-network-data', (data) => {
+    const subscription = Store.subscribe('current-network-data', (data) => {
+      if (!data) return;
       form.readPretty = true;
       const _data = _.cloneDeep(data);
       // 处理一下集群模式数据
@@ -162,6 +170,10 @@ const Save = observer(() => {
       }
       form.setValues(_data);
     });
+    return () => {
+      subscription.unsubscribe();
+      Store.set('current-network-data', undefined);
+    };
   }, []);
 
   const SchemaField = createSchemaField({

+ 7 - 3
src/pages/link/Type/index.tsx

@@ -57,6 +57,7 @@ const Network = () => {
       dataIndex: 'configuration',
       title: '详情',
       renderText: (text, record) => {
+        console.log(record, '详情');
         if (record.shareCluster) {
           const publicHost = record.configuration.publicHost;
           const publicPort = record.configuration.publicPort;
@@ -66,11 +67,14 @@ const Network = () => {
             </>
           );
         } else {
-          const publicHost = record.cluster?.[0]?.configuration?.publicHost;
-          const publicPort = record.cluster?.[0]?.configuration?.publicPort;
+          const log = record.cluster?.map(
+            (item) => `${item.configuration.publicHost}:${item.configuration.publicPort}`,
+          );
           return (
             <>
-              公网: {publicHost}:{publicPort}
+              {log.map((item) => (
+                <div key={item}>公网:{item}</div>
+              ))}
             </>
           );
         }

+ 4 - 3
src/pages/system/Menu/Detail/buttons.tsx

@@ -1,10 +1,10 @@
-import { Form, Input, Button, message, Modal, Popconfirm, Tooltip } from 'antd';
+import { Button, Form, Input, message, Modal, Popconfirm, Tooltip } from 'antd';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { useCallback, useEffect, useState } from 'react';
 import { service } from '@/pages/system/Menu';
-import ProTable from '@jetlinks/pro-table';
 import type { ProColumns } from '@jetlinks/pro-table';
-import { SearchOutlined, PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
+import ProTable from '@jetlinks/pro-table';
+import { DeleteOutlined, EditOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons';
 import type { MenuButtonInfo, MenuItem } from '@/pages/system/Menu/typing';
 import Permission from '@/pages/system/Menu/components/permission';
 import { useRequest } from '@@/plugin-request/request';
@@ -249,6 +249,7 @@ export default (props: ButtonsProps) => {
         ]}
       />
       <Modal
+        maskClosable={false}
         width={660}
         visible={visible}
         title={handleTitle()}

+ 2 - 1
src/pages/system/OpenAPI/index.tsx

@@ -1,6 +1,6 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import React, { useRef } from 'react';
-import type { ProColumns, ActionType } from '@jetlinks/pro-table';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import type { OpenApiItem } from '@/pages/system/OpenAPI/typings';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { CurdModel } from '@/components/BaseCrud/model';
@@ -329,6 +329,7 @@ const OpenAPI: React.FC = observer(() => {
         actionRef={actionRef}
       />
       <Drawer
+        maskClosable={false}
         title={intl.formatMessage({
           id: 'pages.data.option.authorize',
           defaultMessage: '授权',

+ 3 - 3
src/pages/system/Tenant/index.tsx

@@ -1,7 +1,6 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import type { ProColumns, ActionType } from '@jetlinks/pro-table';
-import type { TenantDetail } from '@/pages/system/Tenant/typings';
-import type { TenantItem } from '@/pages/system/Tenant/typings';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import type { TenantDetail, TenantItem } from '@/pages/system/Tenant/typings';
 import BaseCrud from '@/components/BaseCrud';
 import { useRef } from 'react';
 import { Avatar, Drawer, Tooltip } from 'antd';
@@ -238,6 +237,7 @@ const Tenant = observer(() => {
         actionRef={actionRef}
       />
       <Drawer
+        maskClosable={false}
         title={intl.formatMessage({
           id: 'pages.data.option.authorize',
           defaultMessage: '授权',

+ 19 - 18
src/pages/system/User/Save/index.tsx

@@ -1,4 +1,4 @@
-import { message, Modal } from 'antd';
+import { message, Modal, TreeSelect as ATreeSelect } from 'antd';
 import { useIntl } from 'umi';
 import type { Field } from '@formily/core';
 import { createForm } from '@formily/core';
@@ -103,22 +103,6 @@ const Save = (props: Props) => {
             required: true,
             message: '请输入姓名',
           },
-          {
-            triggerType: 'onBlur',
-            validator: (value: string) => {
-              return new Promise((resolve) => {
-                service
-                  .validateField('username', value)
-                  .then((resp) => {
-                    console.log(resp);
-                    resolve('');
-                  })
-                  .catch(() => {
-                    return '验证失败!';
-                  });
-              });
-            },
-          },
         ],
         // required: true,
       },
@@ -142,6 +126,22 @@ const Save = (props: Props) => {
             required: true,
             message: '请输入用户名',
           },
+          {
+            triggerType: 'onBlur',
+            validator: (value: string) => {
+              return new Promise((resolve) => {
+                service
+                  .validateField('username', value)
+                  .then((resp) => {
+                    console.log(resp);
+                    resolve('');
+                  })
+                  .catch(() => {
+                    return '验证失败!';
+                  });
+              });
+            },
+          },
         ],
         name: 'username',
         required: true,
@@ -266,6 +266,7 @@ const Save = (props: Props) => {
         'x-component-props': {
           multiple: true,
           showArrow: true,
+          showCheckedStrategy: ATreeSelect.SHOW_ALL,
           filterOption: (input: string, option: any) =>
             option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
           fieldNames: {
@@ -329,7 +330,7 @@ const Save = (props: Props) => {
       onOk={save}
       width="35vw"
     >
-      <Form form={form} labelCol={4} wrapperCol={18}>
+      <Form form={form} layout="vertical">
         <SchemaField schema={schema} scope={{ useAsyncDataSource, getRole, getOrg }} />
       </Form>
     </Modal>