Quellcode durchsuchen

feat(merge): merge

xieyonghong vor 4 Jahren
Ursprung
Commit
70d190fcce
38 geänderte Dateien mit 2049 neuen und 990 gelöschten Zeilen
  1. 12 10
      src/components/BaseCrud/save/index.tsx
  2. 1 1
      src/components/ProTableCard/CardItems/device.tsx
  3. 1 1
      src/components/ProTableCard/index.tsx
  4. 4 0
      src/global.less
  5. 71 0
      src/pages/link/AccessConfig/Detail/Access/index.less
  6. 448 0
      src/pages/link/AccessConfig/Detail/Access/index.tsx
  7. 27 0
      src/pages/link/AccessConfig/Detail/Provider/index.less
  8. 74 0
      src/pages/link/AccessConfig/Detail/Provider/index.tsx
  9. 31 0
      src/pages/link/AccessConfig/Detail/index.tsx
  10. 66 0
      src/pages/link/AccessConfig/index.less
  11. 202 0
      src/pages/link/AccessConfig/index.tsx
  12. 40 0
      src/pages/link/AccessConfig/service.ts
  13. 19 0
      src/pages/link/AccessConfig/typings.d.ts
  14. 53 0
      src/pages/link/Protocol/FileUpload/index.tsx
  15. 89 139
      src/pages/link/Protocol/index.tsx
  16. 207 49
      src/pages/link/Type/Save/index.tsx
  17. 113 44
      src/pages/link/Type/index.tsx
  18. 18 0
      src/pages/link/Type/service.ts
  19. 2 1
      src/pages/link/Type/typings.d.ts
  20. 3 0
      src/pages/link/service.ts
  21. 6 6
      src/pages/system/Menu/Detail/edit.tsx
  22. 1 2
      src/pages/system/Menu/Detail/index.tsx
  23. 4 4
      src/pages/system/Menu/index.tsx
  24. 0 107
      src/pages/system/Role/Edit/Info/index.tsx
  25. 241 0
      src/pages/system/Role/Edit/Permission/Allocate/MenuPermission.tsx
  26. 16 0
      src/pages/system/Role/Edit/Permission/Allocate/index.less
  27. 94 0
      src/pages/system/Role/Edit/Permission/Allocate/index.tsx
  28. 0 126
      src/pages/system/Role/Edit/Permission/DataPermission.tsx
  29. 0 188
      src/pages/system/Role/Edit/Permission/MenuPermission.tsx
  30. 10 15
      src/pages/system/Role/Edit/Permission/index.less
  31. 86 234
      src/pages/system/Role/Edit/Permission/index.tsx
  32. 37 11
      src/pages/system/Role/Edit/UserManage/BindUser.tsx
  33. 63 35
      src/pages/system/Role/Edit/UserManage/index.tsx
  34. 1 10
      src/pages/system/Role/Edit/index.tsx
  35. 3 3
      src/pages/system/Role/index.tsx
  36. 2 3
      src/pages/system/User/Save/index.tsx
  37. 2 1
      src/pages/user/Login/index.tsx
  38. 2 0
      src/utils/menu.ts

+ 12 - 10
src/components/BaseCrud/save/index.tsx

@@ -1,23 +1,24 @@
 import React, { useEffect, useState } from 'react';
 import { message, Modal, Spin } from 'antd';
 import {
-  NumberPicker,
+  ArrayItems,
+  ArrayTable,
   Editable,
   Form,
+  FormGrid,
   FormItem,
+  FormTab,
   Input,
+  NumberPicker,
   Password,
-  Upload,
   PreviewText,
-  FormTab,
+  Radio,
   Select,
-  ArrayTable,
-  Switch,
-  FormGrid,
-  ArrayItems,
   Space,
-  Radio,
+  Switch,
+  Upload,
 } from '@formily/antd';
+import type { Form as Form1 } from '@formily/core';
 import { createForm } from '@formily/core';
 import { createSchemaField } from '@formily/react';
 import * as ICONS from '@ant-design/icons';
@@ -30,8 +31,8 @@ import { CurdModel } from '@/components/BaseCrud/model';
 import type { ISchemaFieldProps } from '@formily/react/lib/types';
 import type { ModalProps } from 'antd/lib/modal/Modal';
 import FUpload from '@/components/Upload';
+import FileUpload from '@/pages/link/Protocol/FileUpload';
 import FMonacoEditor from '@/components/FMonacoEditor';
-import type { Form as Form1 } from '@formily/core';
 import FBraftEditor from '@/components/FBraftEditor';
 
 interface Props<T> {
@@ -83,6 +84,7 @@ const Save = <T extends Record<string, any>>(props: Props<T>) => {
       Editable,
       NumberPicker,
       FUpload,
+      FileUpload,
       FMonacoEditor,
       ArrayItems,
       Space,
@@ -133,7 +135,7 @@ const Save = <T extends Record<string, any>>(props: Props<T>) => {
     >
       <Spin spinning={modelConfig?.loading || false}>
         <PreviewText.Placeholder value="-">
-          <Form form={customForm || form} labelCol={4} wrapperCol={18}>
+          <Form form={customForm || form} layout={'vertical'}>
             <SchemaField schema={schema} {...schemaConfig} />
           </Form>
         </PreviewText.Placeholder>

+ 1 - 1
src/components/ProTableCard/CardItems/device.tsx

@@ -1,4 +1,4 @@
-import { Card, Avatar } from 'antd';
+import { Avatar, Card } from 'antd';
 import React from 'react';
 import type { DeviceInstance } from '@/pages/device/Instance/typings';
 import { BadgeStatus } from '@/components';

+ 1 - 1
src/components/ProTableCard/index.tsx

@@ -3,7 +3,7 @@ import ProTable from '@jetlinks/pro-table';
 import type { ParamsType } from '@ant-design/pro-provider';
 import React, { useState } from 'react';
 import { isFunction } from 'lodash';
-import { Space, Pagination, Empty } from 'antd';
+import { Empty, Pagination, Space } from 'antd';
 import { AppstoreOutlined, BarsOutlined } from '@ant-design/icons';
 import classNames from 'classnames';
 import './index.less';

+ 4 - 0
src/global.less

@@ -55,3 +55,7 @@ ol {
     min-height: 100vh;
   }
 }
+
+// .ant-formily-item-colon {
+//   display: none;
+// }

+ 71 - 0
src/pages/link/AccessConfig/Detail/Access/index.less

@@ -0,0 +1,71 @@
+.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 {
+  font-weight: 600;
+}
+
+.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;
+  margin-top: 10px;
+  color: rgba(0, 0, 0, 0.55);
+  .item {
+    width: 100%;
+    margin: 5px 0;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+}
+
+.view {
+  display: flex;
+  justify-content: space-between;
+  .info,
+  .config {
+    width: 48%;
+    .title {
+      width: 100%;
+      margin-bottom: 10px;
+      font-weight: 600;
+    }
+    .title::before {
+      margin-right: 10px;
+      background-color: #2810ff;
+      content: '|';
+    }
+  }
+}
+
+.search {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}

+ 448 - 0
src/pages/link/AccessConfig/Detail/Access/index.tsx

@@ -0,0 +1,448 @@
+import {
+  Alert,
+  Badge,
+  Button,
+  Card,
+  Col,
+  Descriptions,
+  Empty,
+  Form,
+  Input,
+  message,
+  Row,
+  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 { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
+
+interface Props {
+  change: () => void;
+  data: any;
+}
+const Access = (props: Props) => {
+  const [form] = Form.useForm();
+
+  const history = useHistory();
+
+  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 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 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');
+
+  const queryNetworkList = (params?: any) => {
+    service.getNetworkList(MetworkTypeMapping.get(props.data?.id), params).then((resp) => {
+      if (resp.status === 200) {
+        setNetworkList(resp.result);
+      }
+    });
+  };
+
+  const queryProcotolList = (params?: any) => {
+    service.getProtocolList(ProcotoleMapping.get(props.data?.id), params).then((resp) => {
+      if (resp.status === 200) {
+        setProcotolList(resp.result);
+      }
+    });
+  };
+
+  useEffect(() => {
+    if (props.data) {
+      queryNetworkList();
+      setCurrent(0);
+    }
+  }, [props.data]);
+
+  const next = () => {
+    if (current === 0) {
+      if (!networkCurrent) {
+        message.error('请选择网络组件!');
+      } else {
+        queryProcotolList();
+        setCurrent(current + 1);
+      }
+    }
+    if (current === 1) {
+      if (!procotolCurrent) {
+        message.error('请选择消息协议!');
+      } else {
+        service
+          .getConfigView(procotolCurrent, ProcotoleMapping.get(props.data?.id))
+          .then((resp) => {
+            if (resp.status === 200) {
+              // setConfig(resp.result)
+            }
+          });
+        setCurrent(current + 1);
+      }
+    }
+  };
+
+  const prev = () => {
+    setCurrent(current - 1);
+  };
+
+  const steps = [
+    {
+      title: '网络组件',
+    },
+    {
+      title: '消息协议',
+    },
+    {
+      title: '完成',
+    },
+  ];
+
+  // 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 dataSource = [
+  //     {
+  //         key: '1',
+  //         name: '胡彦斌',
+  //         age: 32,
+  //         address: '西湖区湖底公园1号',
+  //     },
+  //     {
+  //         key: '2',
+  //         name: '胡彦祖',
+  //         age: 42,
+  //         address: '西湖区湖底公园1号',
+  //     },
+  // ];
+
+  const renderSteps = (cur: number) => {
+    switch (cur) {
+      case 0:
+        return (
+          <div>
+            <Alert message="选择与设备通信的网络组件" type="warning" showIcon />
+            <div className={styles.search}>
+              <Input.Search
+                placeholder="请输入名称"
+                onSearch={(value: string) => {
+                  queryNetworkList(
+                    encodeQuery({
+                      terms: {
+                        name$LIKE: `%${value}%`,
+                      },
+                    }),
+                  );
+                }}
+                style={{ width: 500, margin: '20px 0' }}
+              />
+              <Button
+                type="primary"
+                onClick={() => {
+                  history.push(`${getMenuPathByCode(MENUS_CODE['link/Type/Save'])}`);
+                }}
+              >
+                新增
+              </Button>
+            </div>
+            {networkList.length > 0 ? (
+              <Row gutter={[16, 16]}>
+                {networkList.map((item) => (
+                  <Col key={item.name} span={8}>
+                    <Card
+                      style={{
+                        width: '100%',
+                        border: networkCurrent === item.id ? '1px solid #1d39c4' : '',
+                      }}
+                      hoverable
+                      onClick={() => {
+                        setNetworkCurrent(item.id);
+                      }}
+                    >
+                      <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%' }}>
+                          {item.addresses.slice(0, 2).map((i: any) => (
+                            <div className={styles.item} key={i.address}>
+                              公网: {i.address}
+                            </div>
+                          ))}
+                        </div>
+                      </div>
+                    </Card>
+                  </Col>
+                ))}
+              </Row>
+            ) : (
+              <Empty
+                image="https://gw.alipayobjects.com/zos/antfincdn/ZHrcdLPrvN/empty.svg"
+                imageStyle={{
+                  height: 60,
+                }}
+                description={
+                  <span>
+                    暂无数据{' '}
+                    <a
+                      onClick={() => {
+                        history.push(`${getMenuPathByCode(MENUS_CODE['link/Type/Save'])}`);
+                      }}
+                    >
+                      创建接入方式
+                    </a>
+                  </span>
+                }
+              />
+            )}
+          </div>
+        );
+      case 1:
+        return (
+          <div>
+            <Alert
+              message="使用选择的消息协议,对网络组件通信数据进行编解码、认证等操作"
+              type="warning"
+              showIcon
+            />
+            <div className={styles.search}>
+              <Input.Search
+                placeholder="请输入名称"
+                onSearch={(value: string) => {
+                  queryProcotolList(
+                    encodeQuery({
+                      terms: {
+                        name$LIKE: `%${value}%`,
+                      },
+                    }),
+                  );
+                }}
+                style={{ width: 500, margin: '20px 0' }}
+              />
+              <Button
+                type="primary"
+                onClick={() => {
+                  history.push(`${getMenuPathByCode(MENUS_CODE['link/Protocol'])}`);
+                }}
+              >
+                新增
+              </Button>
+            </div>
+            {procotolList.length > 0 ? (
+              <Row gutter={[16, 16]}>
+                {procotolList.map((item) => (
+                  <Col key={item.name} span={8}>
+                    <Card
+                      style={{
+                        width: '100%',
+                        border: procotolCurrent === item.id ? '1px solid #1d39c4' : '',
+                      }}
+                      hoverable
+                      onClick={() => {
+                        setProcotolCurrent(item.id);
+                      }}
+                    >
+                      <div className={styles.title}>{item.name}</div>
+                      <div className={styles.desc}>这里是协议包中的协议说明</div>
+                    </Card>
+                  </Col>
+                ))}
+              </Row>
+            ) : (
+              <Empty
+                image="https://gw.alipayobjects.com/zos/antfincdn/ZHrcdLPrvN/empty.svg"
+                imageStyle={{
+                  height: 60,
+                }}
+                description={
+                  <span>
+                    暂无数据
+                    <a
+                      onClick={() => {
+                        history.push(`${getMenuPathByCode(MENUS_CODE['link/Protocol'])}`);
+                      }}
+                    >
+                      去新增
+                    </a>
+                  </span>
+                }
+              />
+            )}
+          </div>
+        );
+      case 2:
+        return (
+          <div className={styles.view}>
+            <div className={styles.info}>
+              <div className={styles.title}>基本信息</div>
+              <Form name="basic" layout="vertical" form={form}>
+                <Form.Item
+                  label="名称"
+                  name="name"
+                  rules={[{ required: true, message: '请输入名称' }]}
+                >
+                  <Input />
+                </Form.Item>
+                <Form.Item name="description" label="说明">
+                  <Input.TextArea showCount maxLength={200} />
+                </Form.Item>
+              </Form>
+            </div>
+            <div className={styles.config}>
+              <div className={styles.title}>配置概览</div>
+              <Descriptions column={1}>
+                <Descriptions.Item label="接入方式">{props.data?.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 ||
+                    '----缺少描述呀----'}
+                </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' }}
+                      />
+                    ),
+                  )}
+                </Descriptions.Item>
+              </Descriptions>
+              {/* <div>
+                            <div>路由信息</div>
+                            <Table dataSource={dataSource} columns={columns} pagination={false} />
+                        </div> */}
+            </div>
+          </div>
+        );
+      default:
+        return null;
+    }
+  };
+
+  return (
+    <Card>
+      <Button
+        type="link"
+        onClick={() => {
+          props.change();
+        }}
+      >
+        返回
+      </Button>
+      <div className={styles.box}>
+        <div className={styles.steps}>
+          <Steps size="small" current={current}>
+            {steps.map((item) => (
+              <Steps.Step key={item.title} title={item.title} />
+            ))}
+          </Steps>
+        </div>
+        <div className={styles.content}>{renderSteps(current)}</div>
+        <div className={styles.action}>
+          {current < steps.length - 1 && (
+            <Button type="primary" onClick={() => next()}>
+              下一步
+            </Button>
+          )}
+          {current === steps.length - 1 && (
+            <Button
+              type="primary"
+              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('');
+                    }
+                  });
+                } catch (errorInfo) {
+                  console.error('Failed:', errorInfo);
+                }
+              }}
+            >
+              保存
+            </Button>
+          )}
+          {current > 0 && (
+            <Button style={{ margin: '0 8px' }} onClick={() => prev()}>
+              上一步
+            </Button>
+          )}
+        </div>
+      </div>
+    </Card>
+  );
+};
+
+export default Access;

+ 27 - 0
src/pages/link/AccessConfig/Detail/Provider/index.less

@@ -0,0 +1,27 @@
+.images {
+  width: 64px;
+  height: 64px;
+  color: white;
+  font-size: 12px;
+  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%;
+}
+
+.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;
+}

+ 74 - 0
src/pages/link/AccessConfig/Detail/Provider/index.tsx

@@ -0,0 +1,74 @@
+import { Button, Card, Col, Empty, Row } from 'antd';
+import { service } from '@/pages/link/AccessConfig';
+import { useEffect, useState } from 'react';
+import styles from './index.less';
+
+interface Props {
+  change: (id: string) => void;
+}
+
+const Provider = (props: Props) => {
+  const [dataSource, setDataSource] = useState<any[]>([]);
+
+  const handleSearch = () => {
+    service.getProviders().then((resp) => {
+      if (resp.status === 200) {
+        setDataSource(resp.result);
+      }
+    });
+  };
+
+  useEffect(() => {
+    handleSearch();
+  }, []);
+
+  return (
+    <Card style={{ padding: '20px' }}>
+      {dataSource.length > 0 ? (
+        <Row gutter={[16, 16]}>
+          {dataSource.map((item) => (
+            <Col key={item.name} span={12}>
+              <Card style={{ width: '100%' }} hoverable>
+                <div
+                  style={{
+                    width: '100%',
+                    display: 'flex',
+                    alignItems: 'center',
+                    justifyContent: 'space-between',
+                  }}
+                >
+                  <div
+                    style={{
+                      display: 'flex',
+                      width: 'calc(100% - 70px)',
+                    }}
+                  >
+                    <div className={styles.images}>{item.name}</div>
+                    <div style={{ margin: '10px', width: 'calc(100% - 84px)' }}>
+                      <div style={{ fontWeight: 600 }}>{item.name}</div>
+                      <div className={styles.desc}>{item.description}</div>
+                    </div>
+                  </div>
+                  <div style={{ width: '70px' }}>
+                    <Button
+                      type="primary"
+                      onClick={() => {
+                        props.change(item);
+                      }}
+                    >
+                      接入
+                    </Button>
+                  </div>
+                </div>
+              </Card>
+            </Col>
+          ))}
+        </Row>
+      ) : (
+        <Empty />
+      )}
+    </Card>
+  );
+};
+
+export default Provider;

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

@@ -0,0 +1,31 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { useState } from 'react';
+import Access from './Access';
+import Provider from './Provider';
+
+const Detail = () => {
+  const [visible, setVisible] = useState<boolean>(true);
+  const [id, setId] = useState<any>({});
+
+  return (
+    <PageContainer>
+      {visible ? (
+        <Provider
+          change={(data: string) => {
+            setId(data);
+            setVisible(false);
+          }}
+        />
+      ) : (
+        <Access
+          data={id}
+          change={() => {
+            setVisible(true);
+          }}
+        />
+      )}
+    </PageContainer>
+  );
+};
+
+export default Detail;

+ 66 - 0
src/pages/link/AccessConfig/index.less

@@ -0,0 +1,66 @@
+.content {
+  display: flex;
+  width: 90%;
+  height: 100px;
+  margin: 0 78px;
+  overflow: hidden;
+  .server {
+    width: calc(50% - 78px);
+
+    :global {
+      .ant-badge-status-text {
+        color: rgba(0, 0, 0, 0.55);
+      }
+    }
+  }
+
+  .procotol {
+    display: -webkit-box;
+    width: calc(50% - 78px);
+    overflow: hidden;
+    text-overflow: ellipsis;
+    -webkit-box-orient: vertical;
+    -webkit-line-clamp: 4;
+  }
+}
+
+.desc {
+  width: 50%;
+  margin-top: 10px;
+  overflow: hidden;
+  color: rgba(0, 0, 0, 0.55);
+  font-weight: 400;
+  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;
+  }
+}
+
+.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);
+}

+ 202 - 0
src/pages/link/AccessConfig/index.tsx

@@ -0,0 +1,202 @@
+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 { useHistory } from 'umi';
+import styles from './index.less';
+import Service from './service';
+
+export const service = new Service('gateway/device');
+
+const AccessConfig = () => {
+  const history = useHistory();
+  const [param, setParam] = useState({});
+  // const actionRef = useRef<ActionType>();
+
+  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} />
+    //   ),
+    // },
+  ];
+
+  return (
+    <PageContainer>
+      <Card>
+        <SearchComponent
+          field={columns}
+          pattern={'simple'}
+          onSearch={(data: any) => {
+            setParam(data);
+            // actionRef.current?.reset?.();
+          }}
+          onReset={() => {
+            setParam({});
+            // actionRef.current?.reset?.();
+          }}
+        />
+        <div style={{ width: '100%', display: 'flex', justifyContent: 'flex-end' }}>
+          <Button
+            type="primary"
+            onClick={() => {
+              history.push(`${getMenuPathByCode(MENUS_CODE['link/AccessConfig/Detail'])}`);
+            }}
+          >
+            新增
+          </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} />
+                        </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>
+                </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>
+    </PageContainer>
+  );
+};
+
+export default AccessConfig;

+ 40 - 0
src/pages/link/AccessConfig/service.ts

@@ -0,0 +1,40 @@
+import { request } from 'umi';
+import BaseService from '@/utils/BaseService';
+import SystemConst from '@/utils/const';
+import type { AccessItem } from './typings';
+
+class Service extends BaseService<AccessItem> {
+  public queryList = (data: any) =>
+    request(`/${SystemConst.API_BASE}/gateway/device/detail/_query`, {
+      method: 'POST',
+      data,
+    });
+  public startUp = (id: string) =>
+    request(`/${SystemConst.API_BASE}/gateway/device/${id}/_startup`, {
+      method: 'POST',
+    });
+  public shutDown = (id: string) =>
+    request(`/${SystemConst.API_BASE}/gateway/device/${id}/__shutdown`, {
+      method: 'POST',
+    });
+  public getProviders = () =>
+    request(`/${SystemConst.API_BASE}/gateway/device/providers`, {
+      method: 'GET',
+    });
+  public getNetworkList = (networkType: string, params?: any) =>
+    request(`/${SystemConst.API_BASE}/network/config/${networkType}/_alive`, {
+      method: 'GET',
+      params,
+    });
+  public getProtocolList = (transport: string, params?: any) =>
+    request(`/${SystemConst.API_BASE}/protocol/supports/${transport}`, {
+      method: 'GET',
+      params,
+    });
+  public getConfigView = (id: string, transport: string) =>
+    request(`/${SystemConst.API_BASE}/protocol/${id}/transport/${transport}`, {
+      method: 'GET',
+    });
+}
+
+export default Service;

+ 19 - 0
src/pages/link/AccessConfig/typings.d.ts

@@ -0,0 +1,19 @@
+import type { BaseItem } from '@/utils/typings';
+
+type AccessItem = {
+  id: string | undefined;
+  name: string;
+  description: string;
+  provider: string;
+  protocol: string;
+  transport: string;
+  channel: string;
+  channelId: string;
+  state: {
+    text: string;
+    value: string;
+  };
+  channelInfo: Record<string, any>;
+  protocolDetail: Record<string, any>;
+  transportDetail: Record<string, any>;
+} & BaseItem;

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

@@ -0,0 +1,53 @@
+import SystemConst from '@/utils/const';
+import Token from '@/utils/token';
+import { useState } from 'react';
+import { connect } from '@formily/react';
+import { Button, Input, Upload } from 'antd';
+import type { UploadChangeParam } from 'antd/lib/upload/interface';
+
+interface Props {
+  value: string;
+  onChange: (value: string) => void;
+  accept?: string;
+}
+
+const FileUpload = connect((props: Props) => {
+  const [url, setUrl] = useState<string>(props?.value);
+
+  const handleChange = (info: UploadChangeParam) => {
+    if (info.file.status === 'done') {
+      info.file.url = info.file.response?.result;
+      setUrl(info.file.response?.result);
+      props.onChange(info.file.response?.result);
+    }
+  };
+
+  return (
+    <Upload
+      accept={props?.accept || '*'}
+      listType={'text'}
+      action={`/${SystemConst.API_BASE}/file/static`}
+      headers={{
+        'X-Access-Token': Token.get(),
+      }}
+      onChange={handleChange}
+      showUploadList={false}
+    >
+      <Input.Group compact>
+        <Input
+          style={{ width: 'calc(100% - 100px)' }}
+          value={url}
+          readOnly
+          onClick={(e) => {
+            e.preventDefault();
+            e.stopPropagation();
+          }}
+        />
+        <Button shape="round" style={{ width: '100px', textAlign: 'center' }} type="primary">
+          上传jar包
+        </Button>
+      </Input.Group>
+    </Upload>
+  );
+});
+export default FileUpload;

+ 89 - 139
src/pages/link/Protocol/index.tsx

@@ -1,14 +1,12 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import type { ProtocolItem } from '@/pages/link/Protocol/typings';
-import { useRef, useState } from 'react';
+import { useRef } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import { message, Popconfirm, Tag, Tooltip } from 'antd';
 import {
-  BugOutlined,
-  CloseOutlined,
   CloudSyncOutlined,
+  DeleteOutlined,
   EditOutlined,
-  MinusOutlined,
   PlayCircleOutlined,
 } from '@ant-design/icons';
 import BaseCrud from '@/components/BaseCrud';
@@ -16,14 +14,11 @@ 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 Debug from '@/pages/link/Protocol/Debug';
 
 export const service = new Service('protocol');
 const Protocol = () => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
-  const [visible, setVisible] = useState<boolean>(false);
-  const [current, setCurrent] = useState<Partial<ProtocolItem>>({});
 
   const modifyState = async (id: string, type: 'deploy' | 'un-deploy') => {
     const resp = await service.modifyState(id, type);
@@ -55,21 +50,18 @@ const Protocol = () => {
       }),
     },
     {
+      dataIndex: 'type',
+      title: '类型',
+    },
+    {
       dataIndex: 'state',
       title: '状态',
       renderText: (text) =>
         text === 1 ? <Tag color="#108ee9">正常</Tag> : <Tag color="#F50">禁用</Tag>,
     },
     {
-      dataIndex: 'type',
-      title: '类型',
-    },
-    {
-      dataIndex: 'provider',
-      title: intl.formatMessage({
-        id: 'pages.table.provider',
-        defaultMessage: '服务商',
-      }),
+      dataIndex: 'description',
+      title: '说明',
     },
     {
       title: intl.formatMessage({
@@ -114,31 +106,6 @@ const Protocol = () => {
             </Popconfirm>
           </a>
         ),
-        record.state === 1 && (
-          <a key="unDeploy">
-            <Popconfirm onConfirm={() => modifyState(record.id, 'un-deploy')} title="发布?">
-              <Tooltip title="取消发布">
-                <CloseOutlined />
-              </Tooltip>
-            </Popconfirm>
-          </a>
-        ),
-        <a
-          key="debug"
-          onClick={() => {
-            setVisible(true);
-            setCurrent(record);
-          }}
-        >
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.notice.option.debug',
-              defaultMessage: '调试',
-            })}
-          >
-            <BugOutlined />
-          </Tooltip>
-        </a>,
         record.state !== 1 && (
           <a key="delete">
             <Popconfirm
@@ -163,7 +130,7 @@ const Protocol = () => {
                   defaultMessage: '删除',
                 })}
               >
-                <MinusOutlined />
+                <DeleteOutlined />
               </Tooltip>
             </Popconfirm>
           </a>
@@ -179,141 +146,126 @@ const Protocol = () => {
         type: 'void',
         'x-component': 'FormGrid',
         'x-component-props': {
-          maxColumns: 2,
-          minColumns: 2,
+          maxColumns: 1,
+          minColumns: 1,
         },
         properties: {
           id: {
             title: 'ID',
             'x-component': 'Input',
             'x-decorator': 'FormItem',
-            required: true,
             'x-decorator-props': {
               gridSpan: 1,
             },
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入ID',
+              },
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+            ],
           },
           name: {
             title: '名称',
-            required: true,
             'x-component': 'Input',
             'x-decorator': 'FormItem',
             'x-decorator-props': {
               gridSpan: 1,
             },
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入名称',
+              },
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+            ],
           },
           type: {
             title: '类型',
             'x-component': 'Select',
             'x-decorator': 'FormItem',
-            required: true,
+            'x-decorator-props': {
+              tooltip: <div>jar:上传协议jar包,文件格式支持.jar或.zip</div>,
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择类型',
+              },
+            ],
             enum: [
               { label: 'jar', value: 'jar' },
               { label: 'local', value: 'local' },
-              { label: 'script', value: 'script' },
+              // { label: 'script', value: 'script' },
             ],
           },
           configuration: {
             type: 'object',
             properties: {
-              provider: {
-                title: '类名',
-                'x-component': 'Input',
-                'x-decorator': 'FormItem',
-                'x-visible': false,
-                'x-reactions': {
-                  dependencies: ['..type'],
-                  fulfill: {
-                    state: {
-                      visible: '{{["jar","local"].includes($deps[0])}}',
-                    },
-                  },
-                },
-              },
-              '{url:location}': {
+              location: {
                 title: '文件地址',
-                'x-component': 'FUpload',
                 'x-decorator': 'FormItem',
-                'x-component-props': {
-                  type: 'file',
-                },
                 'x-visible': false,
-                'x-reactions': {
-                  dependencies: ['..type'],
-                  when: '{{$deps[0]==="script"}}',
-                  fulfill: {
-                    state: {
-                      visible: false,
-                    },
-                  },
-                  otherwise: {
-                    state: {
-                      visible: '{{["jar","local"].includes($deps[0])}}',
-                      componentType: '{{$deps[0]==="jar"?"FUpload":"Input"}}',
-                      componentProps: '{{$deps[0]==="jar"?{type:"file"}:{}}}',
-                    },
-                  },
-                },
-              },
-              protocol: {
-                title: '协议标识',
-                'x-component': 'Input',
-                'x-decorator': 'FormItem',
-              },
-              transport: {
-                title: '链接协议',
-                'x-component': 'Select',
-                'x-decorator': 'FormItem',
-                enum: [
-                  { label: 'MQTT', value: 'MQTT' },
-                  { label: 'UDP', value: 'UDP' },
-                  { label: 'CoAP', value: 'CoAP' },
-                  { label: 'TCP', value: 'TCP' },
-                  { label: 'HTTP', value: 'HTTP' },
-                  { label: 'HTTPS', value: 'HTTPS' },
-                ],
-              },
-              script: {
-                title: '脚本',
-                'x-component': 'FMonacoEditor',
-                'x-decorator': 'FormItem',
                 'x-decorator-props': {
-                  gridSpan: 2,
-                  labelCol: 2,
-                  wrapperCol: 22,
+                  tooltip: (
+                    <div>
+                      local:填写本地协议编译目录绝对地址,如:d:/workspace/protocol/target/classes
+                    </div>
+                  ),
                 },
-                default: `//解码,收到设备上行消息时
-codec.decoder(function (context) {
-  var message = context.getMessage();
-  return {
-    messageType:"REPORT_PROPERTY"//消息类型
-  };
-});
-
-//编码读取设备属性消息
-codec.encoder("READ_PROPERTY",function(context){
-  var message = context.getMessage();
-  var properties = message.properties;
-})`,
-                'x-component-props': {
-                  height: 200,
-                  theme: 'dark',
-                  language: 'javascript',
-                  editorDidMount: (editor1: any) => {
-                    editor1.onDidContentSizeChange?.(() => {
-                      editor1.getAction('editor.action.formatDocument').run();
-                    });
+                'x-validator': [
+                  {
+                    required: true,
+                    message: '请输入文件地址',
                   },
-                },
-                'x-visible': false,
+                ],
                 'x-reactions': {
                   dependencies: ['..type'],
                   fulfill: {
                     state: {
-                      visible: '{{$deps[0]==="script"}}',
+                      visible: '{{["jar","local"].includes($deps[0])}}',
+                      componentType: '{{$deps[0]==="jar"?"FileUpload":"Input"}}',
+                      componentProps: '{{$deps[0]==="jar"?{type:"file", accept: ".jar, .zip"}:{}}}',
                     },
                   },
                 },
               },
+              // provider: {
+              //   title: '类名',
+              //   'x-component': 'Input',
+              //   'x-decorator': 'FormItem',
+              //   'x-visible': false,
+              //   'x-validator': [
+              //     {
+              //       required: true,
+              //       message: '请选择类名',
+              //     },
+              //   ],
+              //   'x-reactions': {
+              //     dependencies: ['..type'],
+              //     fulfill: {
+              //       state: {
+              //         visible: '{{["jar","local"].includes($deps[0])}}',
+              //       },
+              //     },
+              //   },
+              // },
+            },
+          },
+          description: {
+            title: '说明',
+            'x-component': 'Input.TextArea',
+            'x-decorator': 'FormItem',
+            'x-component-props': {
+              rows: 3,
+              showCount: true,
+              maxLength: 200,
             },
           },
         },
@@ -326,15 +278,13 @@ codec.encoder("READ_PROPERTY",function(context){
       <BaseCrud
         columns={columns}
         service={service}
-        title={intl.formatMessage({
-          id: 'pages.link.protocol',
-          defaultMessage: '协议管理',
-        })}
-        modelConfig={{ width: '50vw' }}
+        title={'插件管理'}
+        search={false}
+        modelConfig={{ width: '550px' }}
         schema={schema}
         actionRef={actionRef}
       />
-      {visible && <Debug data={current} close={() => setVisible(!visible)} />}
+      {/* {visible && <Debug data={current} close={() => setVisible(!visible)} />} */}
     </PageContainer>
   );
 };

+ 207 - 49
src/pages/link/Type/Save/index.tsx

@@ -1,9 +1,9 @@
 import { PageContainer } from '@ant-design/pro-layout';
-// import { useParams } from 'umi';
-import { createSchemaField } from '@formily/react';
+import { createSchemaField, observer } from '@formily/react';
 import {
   ArrayCollapse,
   Form,
+  FormButtonGroup,
   FormCollapse,
   FormGrid,
   FormItem,
@@ -12,17 +12,19 @@ import {
   Password,
   Radio,
   Select,
+  Submit,
 } from '@formily/antd';
 import type { ISchema } from '@formily/json-schema';
 import { useEffect, useMemo, useRef } from 'react';
 import type { Field } from '@formily/core';
 import { createForm, onFieldValueChange } from '@formily/core';
-import { Card } from 'antd';
+import { Button, Card, message } from 'antd';
 import styles from './index.less';
 import { useAsyncDataSource } from '@/utils/util';
 import { service } from '..';
 import _ from 'lodash';
 import FAutoComplete from '@/components/FAutoComplete';
+import { Store } from 'jetlinks-store';
 
 /**
  *  根据类型过滤配置信息
@@ -30,34 +32,31 @@ import FAutoComplete from '@/components/FAutoComplete';
  * @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'];
+
+  let _temp = type;
+  if (tcpList.includes(type)) {
+    _temp = 'TCP';
+  } else if (udpList.includes(type)) {
+    _temp = 'UDP';
+  }
   // 只保留ports 包含type的数据
-  const _config = data.filter((item) => Object.keys(item.ports).includes(type));
+  const _config = data.filter((item) => Object.keys(item.ports).includes(_temp));
   // 只保留ports的type数据
   return _config.map((i) => {
-    i.ports = i.ports[type];
+    i.ports = i.ports[_temp];
     return i;
   });
 };
-const Save = () => {
+const Save = observer(() => {
   // const param = useParams<{ id: string }>();
 
   // const [config, setConfig] = useState<any[]>([]);
   const configRef = useRef([]);
 
-  const getResourcesClusters = () => {
-    // eslint-disable-next-line @typescript-eslint/no-use-before-define
-    const checked = form.getValuesIn('config')?.map((i: any) => i?.nodeName) || [];
-    return service.getResourceClusters().then((resp) => {
-      // 获取到已经选择的节点名称。然后过滤、通过form.values获取
-      return resp.result
-        ?.map((item: any) => ({
-          label: item.name,
-          value: item.id,
-        }))
-        .filter((j: any) => !checked.includes(j.value));
-    });
-  };
-
   useEffect(() => {
     service.getResourcesCurrent().then((resp) => {
       if (resp.status === 200) {
@@ -68,6 +67,26 @@ const Save = () => {
     });
   }, []);
 
+  const getResourcesClusters = () => {
+    // eslint-disable-next-line @typescript-eslint/no-use-before-define
+    const checked = form.getValuesIn('cluster')?.map((i: any) => i?.serverId) || [];
+    // cache resourcesCluster
+    if (Store.get('resources-cluster')?.length > 0) {
+      return new Promise((resolve) => {
+        resolve(Store.get('resources-cluster').filter((j: any) => !checked.includes(j.value)));
+      });
+    } else {
+      return service.getResourceClusters().then((resp) => {
+        const _data = resp.result?.map((item: any) => ({
+          label: item.name,
+          value: item.id,
+        }));
+        Store.set('resources-cluster', _data);
+        return _data.filter((j: any) => !checked.includes(j.value));
+      });
+    }
+  };
+
   const getResourceById = (id: string, type: string) =>
     service.getResourceClustersById(id).then((resp) => filterConfigByType(resp.result, type));
 
@@ -81,7 +100,8 @@ const Save = () => {
   const form = useMemo(
     () =>
       createForm({
-        initialValues: {},
+        readPretty: false,
+        // initialValues: {},
         effects() {
           onFieldValueChange('type', (field, f) => {
             const value = (field as Field).value;
@@ -90,7 +110,7 @@ const Save = () => {
               state.dataSource = _host.map((item) => ({ label: item.host, value: item.host }));
             });
             f.setFieldState('cluster.config.*.host', (state) => {
-              state.dataSource = _host.map((item) => item.host);
+              state.dataSource = _host.map((item) => ({ label: item.host, value: item.host }));
             });
           });
           onFieldValueChange('grid.configuration.panel1.layout2.host', (field, f1) => {
@@ -98,6 +118,7 @@ const Save = () => {
             const type = (field.query('type').take() as Field).value;
             const _port = filterConfigByType(_.cloneDeep(configRef.current), type);
             const _host = _port.find((item) => item.host === value);
+            console.log(_host, 'host');
             f1.setFieldState('grid.configuration.panel1.layout2.port', (state) => {
               state.dataSource = _host?.ports.map((p: any) => ({ label: p, value: p }));
             });
@@ -108,7 +129,7 @@ const Save = () => {
               // false 获取独立配置的信息
             }
           });
-          onFieldValueChange('grid.cluster.config.*.layout2.nodeName', async (field, f3) => {
+          onFieldValueChange('grid.cluster.cluster.*.layout2.serverId', async (field, f3) => {
             const value = (field as Field).value;
             const type = (field.query('type').take() as Field).value;
             const response = await getResourceById(value, type);
@@ -116,9 +137,9 @@ const Save = () => {
               state.dataSource = response.map((item) => ({ label: item.host, value: item.host }));
             });
           });
-          onFieldValueChange('grid.cluster.config.*.layout2.host', async (field, f4) => {
+          onFieldValueChange('grid.cluster.cluster.*.layout2.host', async (field, f4) => {
             const host = (field as Field).value;
-            const value = (field.query('.nodeName').take() as Field).value;
+            const value = (field.query('.serverId').take() as Field).value;
             const type = (field.query('type').take() as Field).value;
             const response = await getResourceById(value, type);
             const _ports = response.find((item) => item.host === host);
@@ -131,6 +152,18 @@ const Save = () => {
     [],
   );
 
+  useEffect(() => {
+    Store.subscribe('current-network-data', (data) => {
+      form.readPretty = true;
+      const _data = _.cloneDeep(data);
+      // 处理一下集群模式数据
+      if (!_data.shareCluster) {
+        _data.cluster = _data.cluster?.map((item: any) => ({ ...item.configuration }));
+      }
+      form.setValues(_data);
+    });
+  }, []);
+
   const SchemaField = createSchemaField({
     components: {
       FormItem,
@@ -165,7 +198,7 @@ const Save = () => {
       columnGap: 48,
     },
     properties: {
-      nodeName: {
+      serverId: {
         title: '节点名称',
         'x-component': 'Select',
         'x-decorator': 'FormItem',
@@ -178,7 +211,7 @@ const Save = () => {
         },
         'x-reactions': [
           {
-            dependencies: ['....shareCluster'],
+            dependencies: ['shareCluster'],
             fulfill: {
               state: {
                 visible: '{{!$deps[0]}}',
@@ -200,7 +233,14 @@ const Save = () => {
         },
         required: true,
         'x-reactions': {
-          //后台获取数据
+          dependencies: ['type'],
+          fulfill: {
+            state: {
+              // visible: '{{$deps[0]==="UDP"}}',
+              visible:
+                '{{["COAP_SERVER","MQTT_SERVER","WEB_SOCKET_SERVER","TCP_SERVER","UDP"].includes($deps[0])}}',
+            },
+          },
         },
         'x-validator': ['ipv4'],
       },
@@ -216,6 +256,16 @@ const Save = () => {
         type: 'number',
         'x-decorator': 'FormItem',
         'x-component': 'Select',
+        'x-reactions': {
+          dependencies: ['type'],
+          fulfill: {
+            state: {
+              // visible: '{{$deps[0]==="UDP"}}',
+              visible:
+                '{{["COAP_SERVER","MQTT_SERVER","WEB_SOCKET_SERVER","TCP_SERVER","UDP"].includes($deps[0])}}',
+            },
+          },
+        },
         'x-validator': [
           {
             max: 65535,
@@ -239,6 +289,16 @@ const Save = () => {
         'x-decorator': 'FormItem',
         'x-component': 'Input',
         'x-validator': ['ipv4'],
+        'x-reactions': {
+          dependencies: ['type'],
+          fulfill: {
+            state: {
+              // visible: '{{$deps[0]==="UDP"}}',
+              visible:
+                '{{["COAP_SERVER","MQTT_SERVER","WEB_SOCKET_SERVER","TCP_SERVER","UDP"].includes($deps[0])}}',
+            },
+          },
+        },
       },
       publicPort: {
         title: '公网端口',
@@ -251,6 +311,16 @@ const Save = () => {
         required: true,
         'x-decorator': 'FormItem',
         'x-component': 'NumberPicker',
+        'x-reactions': {
+          dependencies: ['type'],
+          fulfill: {
+            state: {
+              // visible: '{{$deps[0]==="UDP"}}',
+              visible:
+                '{{["COAP_SERVER","MQTT_SERVER","WEB_SOCKET_SERVER","TCP_SERVER","UDP"].includes($deps[0])}}',
+            },
+          },
+        },
         'x-validator': [
           {
             max: 65535,
@@ -262,6 +332,75 @@ const Save = () => {
           },
         ],
       },
+      mqttClient: {
+        type: 'void',
+        'x-reactions': {
+          dependencies: ['type'],
+          fulfill: {
+            state: {
+              // visible: '{{$deps[0]==="UDP"}}',
+              visible: '{{["MQTT_Client"].includes($deps[0])}}',
+            },
+          },
+        },
+        properties: {
+          remoteHost: {
+            title: '远程地址',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+          },
+          remotePort: {
+            title: '远程端口',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+          },
+          clientId: {
+            title: 'clientId',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+          },
+          username: {
+            title: '用户名',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+          },
+          password: {
+            title: '密码',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+          },
+          maxMessageSize: {
+            title: '最大消息长度',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+          },
+          topicPrefix: {
+            title: '订阅前缀',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+          },
+        },
+      },
+      maxMessageSize: {
+        title: '最大消息长度',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-decorator-props': {
+          gridSpan: 1,
+          labelAlign: 'left',
+          tooltip: '对外提供访问的地址,内网环境是填写服务器的内网IP地址',
+          layout: 'vertical',
+        },
+        'x-reactions': {
+          dependencies: ['type'],
+          fulfill: {
+            state: {
+              // visible: '{{$deps[0]==="UDP"}}',
+              visible: '{{["MQTT_SERVER","MQTT-Client"].includes($deps[0])}}',
+            },
+          },
+        },
+      },
       parserType: {
         // TCP
         required: true,
@@ -282,10 +421,11 @@ const Save = () => {
           { value: 'fixed_length', label: '固定长度' },
         ],
         'x-reactions': {
-          dependencies: ['....type'],
+          dependencies: ['type'],
           fulfill: {
             state: {
-              visible: '{{$deps[0]==="UDP"}}',
+              // visible: '{{$deps[0]==="UDP"}}',
+              visible: '{{["TCP_SERVER"].includes($deps[0])}}',
             },
           },
         },
@@ -355,22 +495,24 @@ const Save = () => {
             },
           },
           configuration: {
-            type: 'void',
-            'x-visible': false,
+            type: 'object',
+            // 'x-visible': false,
             'x-decorator': 'FormItem',
             'x-component': 'FormCollapse',
             'x-component-props': {
               formCollapse: '{{formCollapse}}',
               className: styles.configuration,
             },
-            'x-reactions': {
-              dependencies: ['.shareCluster'],
-              fulfill: {
-                state: {
-                  visible: '{{$deps[0]===true}}',
+            'x-reactions': [
+              {
+                dependencies: ['.shareCluster', 'type'],
+                fulfill: {
+                  state: {
+                    visible: '{{!!$deps[1]&&$deps[0]===true}}',
+                  },
                 },
               },
-            },
+            ],
             'x-decorator-props': {
               gridSpan: 3,
             },
@@ -391,26 +533,19 @@ const Save = () => {
               gridSpan: 3,
             },
             'x-reactions': {
-              dependencies: ['.shareCluster'],
+              dependencies: ['.shareCluster', 'type'],
               fulfill: {
                 state: {
-                  visible: '{{$deps[0]===false}}',
+                  visible: '{{!!$deps[1]&&$deps[0]===false}}',
                 },
               },
             },
             'x-visible': false,
             properties: {
-              config: {
+              cluster: {
                 type: 'array',
                 'x-component': 'ArrayCollapse',
                 'x-decorator': 'FormItem',
-                // maxItems: 2,
-                'x-validator': [
-                  {
-                    maxItems: 2,
-                    message: '集群节点已全部配置,请勿重复添加',
-                  },
-                ],
                 items: {
                   type: 'void',
                   'x-component': 'ArrayCollapse.CollapsePanel',
@@ -538,6 +673,20 @@ const Save = () => {
       },
     },
   };
+
+  const handleSave = async (data: any) => {
+    if (data.shareCluster === false) {
+      data.cluster = data.cluster?.map((item: any) => ({
+        serverId: item.serverId,
+        configuration: item,
+      }));
+    }
+    const response: any = data.id ? await service.update(data) : await service.save(data);
+    if (response.status === 200) {
+      message.success('保存成功');
+      history.back();
+    }
+  };
   return (
     <PageContainer onBack={() => history.back()}>
       <Card>
@@ -546,10 +695,19 @@ const Save = () => {
             schema={schema}
             scope={{ formCollapse, useAsyncDataSource, getSupports, getResourcesClusters }}
           />
+          <FormButtonGroup.Sticky>
+            <FormButtonGroup.FormItem>
+              {!form.readPretty ? (
+                <Submit onSubmit={handleSave}>保存</Submit>
+              ) : (
+                <Button onClick={() => (form.readPretty = false)}>编辑</Button>
+              )}
+            </FormButtonGroup.FormItem>
+          </FormButtonGroup.Sticky>
         </Form>
       </Card>
     </PageContainer>
   );
-};
+});
 
 export default Save;

+ 113 - 44
src/pages/link/Type/index.tsx

@@ -1,8 +1,14 @@
 import { useRef, useState } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
-import { Button, Tooltip } from 'antd';
-import { BugOutlined, EditOutlined, MinusOutlined, PlusOutlined } from '@ant-design/icons';
+import { Badge, Button, message, Popconfirm, Tooltip } from 'antd';
+import {
+  CloseCircleOutlined,
+  DeleteOutlined,
+  EyeOutlined,
+  PlayCircleOutlined,
+  PlusOutlined,
+} from '@ant-design/icons';
 import { PageContainer } from '@ant-design/pro-layout';
 import type { NetworkItem } from '@/pages/link/Type/typings';
 import { useIntl } from '@@/plugin-locale/localeExports';
@@ -10,20 +16,25 @@ import SearchComponent from '@/components/SearchComponent';
 import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
 import { history } from 'umi';
 import Service from '@/pages/link/service';
+import { Store } from 'jetlinks-store';
 
 export const service = new Service('network/config');
 
+/**
+ * 跳转详情页
+ * @param id
+ */
+const pageJump = (id?: string) => {
+  // 跳转详情
+  history.push(`${getMenuPathByParams(MENUS_CODE['link/Type/Save'], id)}`);
+};
+
 const Network = () => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
 
   const columns: ProColumns<NetworkItem>[] = [
     {
-      dataIndex: 'index',
-      valueType: 'indexBorder',
-      width: 48,
-    },
-    {
       dataIndex: 'name',
       title: intl.formatMessage({
         id: 'pages.table.name',
@@ -38,19 +49,49 @@ const Network = () => {
       }),
     },
     {
+      dataIndex: 'shareCluster',
+      title: '集群',
+      renderText: (text) => (text ? '共享配置' : '独立配置'),
+    },
+    {
+      dataIndex: 'configuration',
+      title: '详情',
+      renderText: (text, record) => {
+        if (record.shareCluster) {
+          const publicHost = record.configuration.publicHost;
+          const publicPort = record.configuration.publicPort;
+          return (
+            <>
+              公网: {publicHost}:{publicPort}
+            </>
+          );
+        } else {
+          const publicHost = record.cluster?.[0]?.configuration?.publicHost;
+          const publicPort = record.cluster?.[0]?.configuration?.publicPort;
+          return (
+            <>
+              公网: {publicHost}:{publicPort}
+            </>
+          );
+        }
+      },
+    },
+    {
       dataIndex: 'state',
       title: intl.formatMessage({
         id: 'pages.searchTable.titleStatus',
         defaultMessage: '状态',
       }),
-      render: (text, record) => record.state.value,
+      render: (text, record) => {
+        if (record.state.value === 'disabled') {
+          return <Badge color="lime" text="正常" />;
+        }
+        return <Badge color="red" text="禁用" />;
+      },
     },
     {
-      dataIndex: 'provider',
-      title: intl.formatMessage({
-        id: 'pages.table.provider',
-        defaultMessage: '服务商',
-      }),
+      dataIndex: 'description',
+      title: '说明',
     },
     {
       title: intl.formatMessage({
@@ -61,35 +102,72 @@ const Network = () => {
       align: 'center',
       width: 200,
       render: (text, record) => [
-        <a onClick={() => console.log(record)}>
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.data.option.edit',
-              defaultMessage: '编辑',
-            })}
-          >
-            <EditOutlined />
+        <a
+          key="edit"
+          onClick={() => {
+            Store.set('current-network-data', record);
+            pageJump(record.id);
+          }}
+        >
+          <Tooltip title="查看">
+            <EyeOutlined />
           </Tooltip>
         </a>,
-        <a>
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.data.option.remove',
-              defaultMessage: '删除',
-            })}
+        <a key="delete">
+          <Popconfirm
+            title="确认删除?"
+            onConfirm={async () => {
+              const response: any = await service.remove(record.id);
+              if (response.status === 200) {
+                message.success('删除成功');
+                actionRef.current?.reload();
+              }
+            }}
           >
-            <MinusOutlined />
-          </Tooltip>
+            <Tooltip
+              title={intl.formatMessage({
+                id: 'pages.data.option.remove',
+                defaultMessage: '删除',
+              })}
+            >
+              <DeleteOutlined />
+            </Tooltip>
+          </Popconfirm>
         </a>,
-        <a>
-          <Tooltip
+        <a key="changeState">
+          <Popconfirm
             title={intl.formatMessage({
-              id: 'pages.notice.option.debug',
-              defaultMessage: '调试',
+              id: `pages.data.option.${record.state.value}.tips`,
+              defaultMessage: `确认${record.state.value === 'enabled' ? '禁用' : '启用'}?`,
             })}
+            onConfirm={async () => {
+              // await service.update({
+              //   id: record.id,
+              //   status: record.status ? 0 : 1,
+              // });
+              const map = {
+                disabled: 'start',
+                enabled: 'shutdown',
+              };
+              await service.changeState(record.id, map[record.state.value]);
+              message.success(
+                intl.formatMessage({
+                  id: 'pages.data.option.success',
+                  defaultMessage: '操作成功!',
+                }),
+              );
+              actionRef.current?.reload();
+            }}
           >
-            <BugOutlined />
-          </Tooltip>
+            <Tooltip
+              title={intl.formatMessage({
+                id: `pages.data.option.${record.state.value}`,
+                defaultMessage: record.state.value === 'enabled' ? '禁用' : '启用',
+              })}
+            >
+              {record.state.value === 'disabled' ? <CloseCircleOutlined /> : <PlayCircleOutlined />}
+            </Tooltip>
+          </Popconfirm>
         </a>,
       ],
     },
@@ -97,15 +175,6 @@ const Network = () => {
 
   const [param, setParam] = useState({});
 
-  /**
-   * 跳转详情页
-   * @param id
-   */
-  const pageJump = (id?: string) => {
-    // 跳转详情
-    history.push(`${getMenuPathByParams(MENUS_CODE['link/Type/Save'], id)}`);
-  };
-
   return (
     <PageContainer>
       <SearchComponent

+ 18 - 0
src/pages/link/Type/service.ts

@@ -0,0 +1,18 @@
+import type { NetworkItem } from '@/pages/link/Type/typings';
+import { request } from 'umi';
+import BaseService from '@/utils/BaseService';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<NetworkItem> {
+  public _start = (id: string) =>
+    request(`/${SystemConst.API_BASE}/network/config/${id}/_start`, {
+      method: 'POST',
+    });
+
+  public _shutdown = (id: string) =>
+    request(`/${SystemConst.API_BASE}/network/config/${id}/_shutdown`, {
+      method: 'POST',
+    });
+}
+
+export default Service;

+ 2 - 1
src/pages/link/Type/typings.d.ts

@@ -1,4 +1,4 @@
-import type { BaseItem } from '@/utils/typings';
+import type { BaseItem, State } from '@/utils/typings';
 
 type NetworkItem = {
   shareCluster: boolean;
@@ -12,4 +12,5 @@ type NetworkItem = {
   createTime: number;
   creatorId: string;
   configuration: Record<string, any>;
+  cluster: any[];
 } & BaseItem;

+ 3 - 0
src/pages/link/service.ts

@@ -33,6 +33,9 @@ class Service extends BaseService<NetworkItem> {
     request(`${SystemConst.API_BASE}/network/resources/alive/_all`, {
       method: 'GET',
     });
+
+  changeState = (id: string, status: 'start' | 'shutdown') =>
+    request(`${SystemConst.API_BASE}/network/config/${id}/_${status}`, { method: 'POST' });
 }
 
 export default Service;

+ 6 - 6
src/pages/system/Menu/Detail/edit.tsx

@@ -1,17 +1,17 @@
 import {
+  Button,
+  Card,
+  Col,
   Form,
   Input,
   InputNumber,
-  Button,
   message,
-  Row,
-  Col,
-  Card,
-  Switch,
   Radio,
+  Row,
   Select,
-  TreeSelect,
+  Switch,
   Tooltip,
+  TreeSelect,
 } from 'antd';
 import Permission from '@/pages/system/Menu/components/permission';
 import { useIntl } from '@@/plugin-locale/localeExports';

+ 1 - 2
src/pages/system/Menu/Detail/index.tsx

@@ -4,9 +4,8 @@ import { useIntl } from '@@/plugin-locale/localeExports';
 import { useEffect, useState } from 'react';
 import BaseDetail from './edit';
 import Buttons from './buttons';
-import { useLocation } from 'umi';
+import { useLocation, useRequest } from 'umi';
 import { service } from '@/pages/system/Menu';
-import { useRequest } from 'umi';
 
 type LocationType = {
   id?: string;

+ 4 - 4
src/pages/system/Menu/index.tsx

@@ -1,15 +1,15 @@
 // 菜单管理
 import { PageContainer } from '@ant-design/pro-layout';
-import ProTable from '@jetlinks/pro-table';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
 import { useRef, useState } from 'react';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { Button, message, Popconfirm, Tooltip } from 'antd';
 import {
-  SearchOutlined,
-  PlusOutlined,
-  PlusCircleOutlined,
   DeleteOutlined,
+  PlusCircleOutlined,
+  PlusOutlined,
+  SearchOutlined,
 } from '@ant-design/icons';
 import { observer } from '@formily/react';
 import { model } from '@formily/reactive';

+ 0 - 107
src/pages/system/Role/Edit/Info/index.tsx

@@ -1,107 +0,0 @@
-import { Form, FormButtonGroup, FormItem, Input, Submit } from '@formily/antd';
-import { createSchemaField } from '@formily/react';
-import { Card, message, Spin } from 'antd';
-import { createForm } from '@formily/core';
-import { useEffect, useState } from 'react';
-import { service } from '@/pages/system/Role';
-import { useParams, history } from 'umi';
-
-const Info = () => {
-  const [loading, setLoading] = useState<boolean>(true);
-  const [type, setType] = useState<'edit' | 'disabled'>('disabled');
-  const [data, setData] = useState<RoleItem>();
-  const params = useParams<{ id: string }>();
-  const getDetail = async (id: string) => {
-    const res = await service.detail(id);
-    if (res.status === 200) {
-      setData(res.result);
-      setLoading(false);
-    }
-  };
-
-  useEffect(() => {
-    const { id } = params;
-    if (id) {
-      getDetail(id);
-    } else {
-      history.goBack();
-    }
-  }, [params, params.id]);
-
-  const SchemaField = createSchemaField({
-    components: {
-      Input,
-      FormItem,
-    },
-  });
-
-  const form = createForm({
-    validateFirst: true,
-    initialValues: {
-      id: data?.id,
-      name: data?.name,
-      description: data?.description,
-    },
-  });
-
-  const schema = {
-    type: 'object',
-    properties: {
-      name: {
-        type: 'string',
-        title: '名称',
-        required: true,
-        'x-disabled': type === 'disabled',
-        'x-decorator': 'FormItem',
-        'x-component': 'Input',
-      },
-      description: {
-        type: 'string',
-        title: '角色描述',
-        required: false,
-        'x-disabled': type === 'disabled',
-        'x-decorator': 'FormItem',
-        'x-component': 'Input.TextArea',
-      },
-    },
-  };
-
-  const save = async () => {
-    const values: RoleItem = await form.submit();
-    const resp = await service.modify(values.id, values);
-    if (resp.status === 200) {
-      message.success('操作成功!');
-      getDetail(values.id);
-      setType('disabled');
-    }
-  };
-
-  return (
-    <Card>
-      <div style={{ width: '500px' }}>
-        <Spin spinning={loading}>
-          <Form form={form} labelCol={5} wrapperCol={16}>
-            <SchemaField schema={schema} />
-            <FormButtonGroup.FormItem>
-              <Submit
-                block
-                size="large"
-                onClick={() => {
-                  if (type === 'edit') {
-                    save();
-                  } else {
-                    setType('edit');
-                  }
-                }}
-              >
-                {type === 'disabled' ? '编辑' : '保存'}
-              </Submit>
-            </FormButtonGroup.FormItem>
-          </Form>
-        </Spin>
-      </div>
-    </Card>
-  );
-};
-
-export default Info;

+ 241 - 0
src/pages/system/Role/Edit/Permission/Allocate/MenuPermission.tsx

@@ -0,0 +1,241 @@
+import { CaretDownOutlined, QuestionCircleOutlined } from '@ant-design/icons';
+import { Checkbox, Radio, Tooltip } from 'antd';
+import type { CheckboxValueType } from 'antd/lib/checkbox/Group';
+import _ from 'lodash';
+import { useEffect, useState } from 'react';
+
+interface Props {
+  value: any;
+  check?: boolean;
+  level?: number;
+  change: (data: any) => void;
+}
+
+const MenuPermission = (props: Props) => {
+  const [value, setValue] = useState<any>(props.value);
+  const [checkAll, setCheckAll] = useState<boolean>(props.value?.check === 1);
+  const [visible, setVisible] = useState<boolean>(value.id === 'menu-permission');
+  const [indeterminate, setIndeterminate] = useState<boolean>(props.value?.check === 2);
+
+  useEffect(() => {
+    setValue(props.value);
+    setCheckAll(props.value?.check === 1);
+    setIndeterminate(props.value?.check === 2);
+  }, [props.value]);
+
+  const checkAllData: any = (data: any[], check: boolean) => {
+    if (Array.isArray(data) && data.length > 0) {
+      return data.map((item) => {
+        const buttons = (item?.buttons || []).map((i: any) => {
+          return {
+            ...i,
+            enabled: check,
+          };
+        });
+        return {
+          ...item,
+          check: check ? 1 : 3, // 1: 全选 2: 只选了部分 3: 一个都没选
+          buttons: [...buttons],
+          children: item?.children ? checkAllData(item?.children || [], check) : [],
+        };
+      });
+    }
+    return [];
+  };
+
+  return (
+    <>
+      <div
+        style={{
+          display: 'flex',
+          alignItems: 'center',
+          paddingLeft: (props?.level || 0) * 10,
+          transition: 'background .3s',
+          borderBottom: '1px solid #f0f0f0',
+        }}
+        key={value?.id}
+      >
+        <div
+          style={{
+            width: 20,
+            textAlign: 'center',
+            height: 20,
+            transform: !visible ? 'rotate(-90deg)' : 'none',
+          }}
+        >
+          {value?.children && value?.children?.length > 0 && (
+            <CaretDownOutlined
+              onClick={() => {
+                setVisible(!visible);
+              }}
+            />
+          )}
+        </div>
+        <div
+          style={{
+            width: `calc(50% - ${(props?.level || 0) * 5}px)`,
+            borderRight: '1px solid #f0f0f0',
+          }}
+        >
+          <Checkbox
+            indeterminate={indeterminate}
+            checked={checkAll}
+            style={{
+              padding: '10px 0',
+              width: 250 - (props?.level || 0) * 10,
+              borderRight: '1px solid #f0f0f0',
+              marginRight: 20,
+              fontWeight: value.id === 'menu-permission' ? 600 : 400,
+            }}
+            onChange={(e) => {
+              setCheckAll(e.target.checked);
+              setIndeterminate(false);
+              const buttons = (value?.buttons || []).map((i: any) => {
+                return {
+                  ...i,
+                  enabled: e.target.checked,
+                };
+              });
+              console.log(e.target.checked);
+              props.change({
+                ...value,
+                check: e.target.checked ? 1 : 3, // 1: 全选 2: 只选了部分 3: 一个都没选
+                buttons: [...buttons],
+                children: checkAllData(value.children || [], e.target.checked),
+              });
+            }}
+          >
+            {value?.name}
+          </Checkbox>
+          <Checkbox.Group
+            name={value?.id}
+            value={_.map(
+              (value?.buttons || []).filter((i: any) => i?.enabled),
+              'id',
+            )}
+            onChange={(data: CheckboxValueType[]) => {
+              const buttons = value.buttons.map((i: any) => {
+                return {
+                  ...i,
+                  enabled: data.includes(i.id),
+                };
+              });
+              const clen = (value?.children || []).filter((i: any) => i.check !== 3).length;
+              let check: number = 3;
+              if (data.length + clen === 0) {
+                check = 3;
+              } else if (
+                data.length + clen <
+                value?.buttons.length + (value?.children.length || 0)
+              ) {
+                check = 2;
+              } else {
+                check = 1;
+              }
+              const d = {
+                ...value,
+                check,
+                buttons: [...buttons],
+              };
+              props.change(d);
+            }}
+            options={(value?.buttons || []).map((i: any) => ({
+              label: i.name,
+              value: i.id,
+              key: i.id,
+            }))}
+          />
+        </div>
+        <div
+          style={{
+            width: `calc(50% - ${(props?.level || 0) * 10}px - 20px)`,
+            padding: '10px 0 10px 20px',
+          }}
+        >
+          {value.id === 'menu-permission' ? (
+            <span style={{ fontWeight: value.id === 'menu-permission' ? 600 : 400 }}>
+              数据权限
+              <Tooltip title="勾选任意数据权限均能看到自己创建的数据权限">
+                <QuestionCircleOutlined />
+              </Tooltip>
+            </span>
+          ) : (
+            <div>
+              {value?.accessSupport?.value === 'unsupported' ? (
+                <div>{value?.accessDescription}</div>
+              ) : (
+                <Radio.Group
+                  defaultValue={value?.assetAccesses[0]?.supportId}
+                  value={
+                    _.map(
+                      (value?.assetAccesses || []).filter((i: any) => i?.enabled),
+                      'supportId',
+                    )[0]
+                  }
+                  onChange={(e) => {
+                    const access = (value?.assetAccesses || []).map((i: any) => {
+                      if (i.supportId === e.target.value) {
+                        return {
+                          ...i,
+                          enabled: true,
+                        };
+                      }
+                      return {
+                        ...i,
+                        enabled: false,
+                      };
+                    });
+                    const d = {
+                      ...value,
+                      assetAccesses: [...access],
+                    };
+                    props.change(d);
+                  }}
+                >
+                  {value?.assetAccesses.map((item: any) => (
+                    <Radio value={item?.supportId} key={item?.supportId}>
+                      {item?.name}
+                    </Radio>
+                  ))}
+                </Radio.Group>
+              )}
+            </div>
+          )}
+        </div>
+      </div>
+      {visible &&
+        value?.children &&
+        (value?.children || []).map((item: { id: string }) => (
+          <div key={item.id}>
+            <MenuPermission
+              level={(props?.level || 0) + 1}
+              value={item}
+              change={(data: any) => {
+                const children = (value?.children || []).map((i: any) => {
+                  if (data.id === i.id) {
+                    return data;
+                  }
+                  return i;
+                });
+                let check: number = 3;
+                const blen = value.buttons?.length || 0;
+                const bblen = (value?.buttons || []).filter((i: any) => i.enabled).length || 0;
+                const clen = children.length || 0;
+                const cclen = (children || []).filter((i: any) => i.check !== 3).length || 0;
+                const cclen1 = (children || []).filter((i: any) => i.check === 1).length || 0;
+                if (clen + blen > 0 && clen + blen === cclen1 + bblen) {
+                  check = 1;
+                } else if (cclen + bblen === 0) {
+                  check = 3;
+                } else {
+                  check = 2;
+                }
+                props.change({ ...value, check, children });
+              }}
+            />
+          </div>
+        ))}
+    </>
+  );
+};
+export default MenuPermission;

+ 16 - 0
src/pages/system/Role/Edit/Permission/Allocate/index.less

@@ -0,0 +1,16 @@
+// .rolePermission {
+//   :global {
+//     .ant-table-cell {
+//       .ant-form-item {
+//         margin: 0;
+//       }
+//     }
+//     .ant-table-cell-with-append {
+//       display: flex;
+//       align-items: center;
+//     }
+//     .ant-table-row-expand-icon {
+//       margin-top: 0;
+//     }
+//   }
+// }

+ 94 - 0
src/pages/system/Role/Edit/Permission/Allocate/index.tsx

@@ -0,0 +1,94 @@
+import { useEffect, useState } from 'react';
+import MenuPermission from './MenuPermission';
+
+interface Props {
+  onChange?: (data: any) => void;
+  value?: any;
+}
+
+const Allocate = (props: Props) => {
+  const [dataSource, setDataSource] = useState<any>({
+    id: 'menu-permission',
+    buttons: [],
+    name: '菜单权限',
+    children: [],
+  });
+
+  const getDataList: any = (data1: any[]) => {
+    if (Array.isArray(data1) && data1.length > 0) {
+      return data1.map((item) => {
+        const children = getDataList(item.children || []) || [];
+        let check: number = 3;
+        const blen = item.buttons?.length || 0;
+        const bblen = (item?.buttons || []).filter((i: any) => i.enabled).length || 0;
+        const clen = children.length || 0;
+        const cclen = (children || []).filter((i: any) => i.granted).length || 0;
+        const cclen1 = (children || []).filter((i: any) => i.check === 1).length || 0;
+        if (clen + blen > 0 && clen + blen === cclen1 + bblen) {
+          check = 1;
+        } else if (cclen + bblen === 0 && !item.granted) {
+          check = 3;
+        } else if (clen + blen === 0 && item.granted) {
+          check = 1;
+        } else {
+          check = 2;
+        }
+
+        return {
+          ...item,
+          check,
+          children,
+        };
+      });
+    }
+    return [];
+  };
+
+  useEffect(() => {
+    if (props?.value) {
+      if (!props.value?.check) {
+        const children = getDataList(props.value?.children || []) || [];
+        let check: number = 3;
+        const clen = children.length || 0;
+        const cclen = (children || []).filter((i: any) => i.granted).length || 0;
+        const cclen1 = (children || []).filter((i: any) => i.check === 1).length || 0;
+        if (clen > 0 && clen === cclen1) {
+          check = 1;
+        } else if (cclen === 0) {
+          check = 3;
+        } else {
+          check = 2;
+        }
+        setDataSource({
+          // 重新初始化
+          id: 'menu-permission',
+          buttons: [],
+          check,
+          name: '菜单权限',
+          children,
+        });
+      } else {
+        setDataSource(props.value);
+      }
+    }
+  }, [props.value]);
+
+  return (
+    <div style={{ border: '1px solid #f0f0f0', paddingBottom: 10 }}>
+      <div style={{ overflowY: 'scroll', maxHeight: '500px' }}>
+        <MenuPermission
+          key={'menu-permission'}
+          value={dataSource}
+          level={1}
+          change={(data: any) => {
+            setDataSource(data);
+            if (props.onChange) {
+              props.onChange(data);
+            }
+          }}
+        />
+      </div>
+    </div>
+  );
+};
+export default Allocate;

+ 0 - 126
src/pages/system/Role/Edit/Permission/DataPermission.tsx

@@ -1,126 +0,0 @@
-import { useEffect, useState } from 'react';
-import type { TableColumnsType } from 'antd';
-import { Form } from 'antd';
-import { Checkbox, Select, Table } from 'antd';
-import Service from '@/pages/system/Role/service';
-import _ from 'lodash';
-
-interface Props {
-  initialValues?: any;
-  data: any;
-  change: (data: any) => void;
-}
-
-const DataPermission = (props: Props) => {
-  const service = new Service('role-permissions-data');
-  const [form] = Form.useForm();
-  const [typeList, setTypeList] = useState<any[]>([]);
-  const [dimensionsList, setDimensionsList] = useState<any>({});
-
-  const menuAssetsTypes = (data: any) => {
-    service.queryAssetTypeList(data).subscribe((resp) => {
-      if (resp.status === 200) {
-        setTypeList(resp.result);
-      }
-    });
-  };
-
-  useEffect(() => {
-    if (typeList.length > 0) {
-      typeList.map((item) => {
-        service.queryAssetsList(item.id).subscribe((resp) => {
-          if (resp.status === 200) {
-            dimensionsList[item.id] = resp.result;
-            setDimensionsList({ ...dimensionsList });
-          } else {
-            dimensionsList[item.id] = undefined;
-            setDimensionsList({ ...dimensionsList });
-          }
-        });
-      });
-    }
-  }, [typeList]);
-
-  useEffect(() => {
-    if (Array.isArray(props.data) && props.data.length > 0) {
-      menuAssetsTypes(props.data);
-    }
-  }, [props.data]);
-
-  useEffect(() => {
-    props.initialValues.map((item: { dimensions: any[]; assetType: string }) => {
-      const type = _.map(item?.dimensions || [], 'dimensionType');
-      form.setFieldsValue({
-        [item.assetType]: {
-          value: true,
-          type,
-        },
-      });
-    });
-  }, [props.initialValues]);
-
-  const dataColumns: TableColumnsType<PermissionItem> = [
-    {
-      title: '数据权限',
-      dataIndex: 'name',
-      render: (text: string, record) => (
-        <div style={{ display: 'flex', alignItems: 'center' }}>
-          <Form.Item name={[`${record.id}`, 'value']} valuePropName="checked">
-            <Checkbox
-              style={{ width: '150px' }}
-              onChange={(e) => {
-                form.setFieldsValue({
-                  [`${record.id}`]: {
-                    value: e.target.checked,
-                    type: form.getFieldValue(`${record.id}`).type,
-                  },
-                });
-                props.change(form.getFieldsValue());
-              }}
-            >
-              {record.name}
-            </Checkbox>
-          </Form.Item>
-          <Form.Item name={[record.id, 'type']}>
-            <Select
-              style={{ width: '300px' }}
-              showSearch
-              placeholder="请选择"
-              mode="multiple"
-              onChange={(value: string) => {
-                form.setFieldsValue({
-                  [`${record.id}`]: {
-                    type: value,
-                    value: form.getFieldValue(`${record.id}`).value,
-                  },
-                });
-                props.change(form.getFieldsValue());
-              }}
-              filterOption={(input, option: any) =>
-                option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
-              }
-            >
-              {(dimensionsList[record.id] || []).map((item: { id: string; name: string }) => (
-                <Select.Option key={item.id} value={item.id}>
-                  {item.name}
-                </Select.Option>
-              ))}
-            </Select>
-          </Form.Item>
-        </div>
-      ),
-    },
-  ];
-
-  return (
-    <div>
-      <div>
-        {/* <Input.Search enterButton placeholder="请输入权限名称" onSearch={() => { }} style={{ width: 300, marginBottom: '15px' }} /> */}
-        <Form form={form} wrapperCol={{ span: 20 }} labelCol={{ span: 3 }}>
-          <Table rowKey="id" pagination={false} columns={dataColumns} dataSource={typeList} />
-        </Form>
-      </div>
-    </div>
-  );
-};
-export default DataPermission;

+ 0 - 188
src/pages/system/Role/Edit/Permission/MenuPermission.tsx

@@ -1,188 +0,0 @@
-import { CaretDownOutlined } from '@ant-design/icons';
-import { Checkbox } from 'antd';
-import type { CheckboxValueType } from 'antd/lib/checkbox/Group';
-import _ from 'lodash';
-import { useEffect, useState } from 'react';
-
-interface Props {
-  value: any;
-  initialValues: any;
-  check?: boolean;
-  change: (data: any) => void;
-}
-
-const MenuPermission = (props: Props) => {
-  const { value } = props;
-  const [checkAll, setCheckAll] = useState<boolean>(
-    value.buttons?.length > 0 || value.children?.length > 0
-      ? (props.initialValues?.buttons?.length || 0) +
-          ((props.initialValues?.children || []).filter((item: any) => item.check)?.length || 0) ===
-          (value.buttons?.length || 0) + (value.children?.length || 0)
-      : props.initialValues.check,
-  );
-  const [menuList, setMenuList] = useState<any[]>(props.initialValues?.buttons || []);
-  const [visible, setVisible] = useState<boolean>(true);
-  const [children, setChildren] = useState<any>({});
-  const [initialChildrenValues, setInitialChildrenValues] = useState<any[]>(
-    props.initialValues?.children || [],
-  );
-  const [indeterminate, setIndeterminate] = useState<boolean>(
-    props.initialValues?.indeterminate || false,
-  );
-
-  useEffect(() => {
-    if (props.initialValues && Object.keys(props.initialValues).length > 0) {
-      setMenuList(props.initialValues?.buttons || []);
-      setIndeterminate(props.initialValues?.indeterminate);
-      setCheckAll(props?.initialValues?.check);
-      setInitialChildrenValues(props.initialValues?.children || []);
-    } else {
-      setMenuList([]);
-      setInitialChildrenValues([]);
-      setIndeterminate(false);
-      setCheckAll(false);
-    }
-  }, [props.initialValues]);
-
-  const getInitValues = (list: any[]) => {
-    if (Array.isArray(list) && list.length > 0) {
-      return list.map((item) => {
-        let child: any[] = [];
-        if (item.children && item.children.length > 0) {
-          child = getInitValues(item.children);
-        }
-        return {
-          id: item.id,
-          check: true,
-          indeterminate: false,
-          buttons: item.buttons && item.buttons.length > 0 ? _.map(item.buttons, 'id') : [],
-          children: child,
-        };
-      });
-    }
-    return [];
-  };
-
-  useEffect(() => {
-    const list = initialChildrenValues.map((item) => {
-      if (item.id === children.id) return children;
-      return item;
-    });
-    const flag = list.find((i) => i.id === children.id);
-    if (!flag) {
-      list.push(children);
-    }
-    const lenB = menuList?.length || 0;
-    const ilen: number = (list || []).filter((i: any) => i.indeterminate || i.check)?.length || 0;
-    const clen: number = (list || []).filter((i: any) => i.check)?.length || 0;
-    const check = clen + lenB === (value?.children?.length || 0) + (value?.buttons?.length || 0);
-    setIndeterminate((ilen > 0 || lenB > 0) && !check);
-    setCheckAll(check);
-    props.change({
-      id: value.id,
-      indeterminate: (ilen > 0 || lenB > 0) && !check,
-      check: check,
-      children: [...list],
-      buttons: [...menuList],
-    });
-  }, [children]);
-
-  return (
-    <div key={value?.id} style={{ margin: '10px 0' }}>
-      <div
-        style={{
-          display: 'flex',
-          padding: '10px 0',
-          alignItems: 'center',
-          borderBottom: '1px solid #f0f0f0',
-          transition: 'background .3s',
-        }}
-      >
-        <div
-          style={{
-            marginRight: '10px',
-            width: '15px',
-            transform: !visible ? 'rotate(-90deg)' : 'none',
-          }}
-        >
-          {value?.children && value?.children?.length > 0 && (
-            <CaretDownOutlined
-              onClick={() => {
-                setVisible(!visible);
-              }}
-            />
-          )}
-        </div>
-        <div>
-          <Checkbox
-            indeterminate={indeterminate}
-            checked={checkAll}
-            style={{ width: '200px' }}
-            onChange={(e) => {
-              setCheckAll(e.target.checked);
-              setIndeterminate(false);
-              const data = e.target.checked ? (value?.buttons || []).map((i: any) => i.id) : [];
-              setMenuList([...data]);
-              const initialData = e.target.checked ? [...getInitValues(value?.children || [])] : [];
-              props.change({
-                id: value.id,
-                check: e.target.checked,
-                indeterminate: false,
-                children: [...initialData],
-                buttons: [...data],
-              });
-            }}
-          >
-            {value?.name}
-          </Checkbox>
-        </div>
-        <div>
-          <Checkbox.Group
-            name={value?.id}
-            value={menuList}
-            onChange={(data: CheckboxValueType[]) => {
-              const len = (value.buttons?.length || 0) + (value.children?.length || 0);
-              const lenB = data?.length || 0;
-              const lenC = initialChildrenValues?.length || 0;
-              setIndeterminate(lenB + lenC < len);
-              setCheckAll(lenB + lenC === len);
-              setMenuList([...data]);
-              props.change({
-                id: value.id,
-                check: lenB + lenC === len,
-                indeterminate: !(lenB + lenC === len),
-                children: [...initialChildrenValues],
-                buttons: [...data],
-              });
-            }}
-            options={(value?.buttons || []).map((i: any) => ({
-              label: i.name,
-              value: i.id,
-              key: i.id,
-            }))}
-          />
-        </div>
-      </div>
-      {visible && value?.children && (
-        <div style={{ paddingLeft: '20px' }}>
-          {(value?.children || []).map((item: { id: string }) => (
-            <div key={item.id}>
-              <MenuPermission
-                initialValues={
-                  (initialChildrenValues || []).find((i: any) => i.id === item.id) || {}
-                }
-                value={item}
-                change={(data: any) => {
-                  if (Object.keys(data).length > 0) {
-                    setChildren(data);
-                  }
-                }}
-              />
-            </div>
-          ))}
-        </div>
-      )}
-    </div>
-  );
-};
-export default MenuPermission;

+ 10 - 15
src/pages/system/Role/Edit/Permission/index.less

@@ -1,16 +1,11 @@
-.rolePermission {
-  :global {
-    .ant-table-cell {
-      .ant-form-item {
-        margin: 0;
-      }
-    }
-    .ant-table-cell-with-append {
-      display: flex;
-      align-items: center;
-    }
-    .ant-table-row-expand-icon {
-      margin-top: 0;
-    }
-  }
+.title {
+  width: 100%;
+  margin-bottom: 10px;
+  font-weight: 600;
+}
+
+.title::before {
+  margin-right: 10px;
+  background-color: #2810ff;
+  content: '|';
 }

+ 86 - 234
src/pages/system/Role/Edit/Permission/index.tsx

@@ -1,259 +1,111 @@
+import { Button, Card, Col, Form, Input, message, Row } from 'antd';
+import Allocate from '@/pages/system/Role/Edit/Permission/Allocate';
 import { useEffect, useState } from 'react';
-import { Button, Card, message, Steps } from 'antd';
-import Service from '@/pages/system/Role/service';
+import { history, useParams } from 'umi';
+import { service } from '@/pages/system/Role';
 import styles from './index.less';
-import DataPermission from './DataPermission';
-import MenuPermission from './MenuPermission';
-import encodeQuery from '@/utils/encodeQuery';
-import { useParams } from 'umi';
-import _ from 'lodash';
 
 const Permission = () => {
-  const service = new Service('role');
   const params = useParams<{ id: string }>();
-  const [current, setCurrent] = useState<number>(0);
-  const [dataSource, setDataSource] = useState<any[]>([]);
-  const [initialValues, setInitialValues] = useState<any>({});
-  const [menuPermissions, setMenuPermissions] = useState<any[]>([]);
-  const [dataPermissions, setDataPermissions] = useState<any[]>([]);
-  const [info, setInfo] = useState<RoleItem>();
-
+  const [form] = Form.useForm();
+  const [data, setData] = useState<RoleItem>();
   const getDetail = async (id: string) => {
     const res = await service.detail(id);
     if (res.status === 200) {
-      setInfo(res.result);
-      setDataPermissions(res.result?.dataAccess || []);
-    }
-  };
-
-  const handleSearch = (data: any) => {
-    service.queryMenuTreeList(encodeQuery(data)).subscribe((resp) => {
-      if (resp.status === 200) {
-        setDataSource(resp.result);
-      }
-    });
-  };
-
-  const breadthQuery = (tree: any[], id: string) => {
-    let stark: any[] = [];
-    stark = stark.concat(tree);
-    while (stark.length) {
-      const temp = stark.shift();
-      if (temp.children) {
-        stark = stark.concat(temp.children);
-      }
-      if (temp.id === id) {
-        return temp;
-      }
-    }
-    return undefined;
-  };
-
-  const initToMenu = (initList: any[], list: any[]) => {
-    if (Array.isArray(initList) && initList.length > 0) {
-      return initList.map((item) => {
-        const data = breadthQuery(list, item.id);
-        if ((item?.children.length > 0 || item?.buttons.length > 0 || item.check) && data) {
-          const dt: any = { ...data, buttons: [], children: [] };
-          if (item?.children && item?.children?.length > 0) {
-            dt.children = initToMenu(item.children, list);
-          }
-          if (
-            item?.buttons &&
-            item?.buttons?.length > 0 &&
-            data?.buttons &&
-            data?.buttons?.length > 0
-          ) {
-            const buttons = data.buttons.filter((i: any) => item.buttons.includes(i.id));
-            dt.buttons = [...buttons];
-          }
-          return dt;
+      setData(res.result);
+      service.queryGrantTree('role', id).subscribe((resp) => {
+        if (resp.status === 200) {
+          form.setFieldsValue({
+            ...res.result,
+            permission: {
+              id: 'menu-permission',
+              buttons: [],
+              name: '菜单权限',
+              children: [...resp.result],
+            },
+          });
         }
       });
     }
-    return [];
   };
 
-  const initToPermission = (list: any[]): any[] => {
-    if (Array.isArray(list) && list.length > 0) {
-      return list.map((item) => {
-        const data = breadthQuery(dataSource, item.id);
-        const ilen: number =
-          (initToPermission(item.children) || []).filter((i: any) => i.indeterminate || i.check)
-            ?.length || 0;
-        const clen: number =
-          (initToPermission(item.children) || []).filter((i: any) => i.check)?.length || 0;
-        const check =
-          clen + (item?.buttons?.length || 0) ===
-          (data?.children?.length || 0) + (data?.buttons?.length || 0);
+  useEffect(() => {
+    const { id } = params;
+    if (id) {
+      getDetail(id);
+    } else {
+      history.goBack();
+    }
+  }, [params, params.id]);
+
+  const getDataList: any = (data1: any[]) => {
+    if (Array.isArray(data1) && data1.length > 0) {
+      return data1.map((item) => {
+        const check = item.check;
+        delete item.check;
         return {
-          id: item.id,
-          check: check,
-          indeterminate: (ilen > 0 || item?.buttons?.length > 0) && !check,
-          buttons: _.map(item.buttons || [], 'id') || [],
-          children: initToPermission(item.children) || [],
+          ...item,
+          granted: check !== 3,
+          children: item?.children ? getDataList(item.children) : [],
         };
       });
     }
     return [];
   };
 
-  const initialMenu = (id: string) => {
-    service.queryGrantTree('role', id).subscribe((resp) => {
-      if (resp.status === 200) {
-        const data = initToPermission(resp.result);
-        const len = data.filter((i: any) => i.indeterminate)?.length;
-        const lenC = data.filter((i: any) => i.check)?.length;
-        const d = {
-          id: 'menu-permission',
-          check: dataSource.length === lenC,
-          children: [...data],
-          indeterminate: len > 0,
-          buttons: [],
-        };
-        setInitialValues(d);
-      }
-    });
-  };
-
-  useEffect(() => {
-    handleSearch({ paging: false });
-    if (params?.id) {
-      getDetail(params.id);
-    }
-  }, []);
-
-  useEffect(() => {
-    if (dataSource.length > 0) {
-      initialMenu(params.id);
-    }
-  }, [dataSource]);
-
   return (
-    <Card className={styles.rolePermission}>
-      <Steps current={current}>
-        <Steps.Step title="菜单权限" />
-        <Steps.Step title="数据权限" />
-      </Steps>
-      <div style={{ marginTop: '15px' }}>
-        {current === 0 && (
-          <div className={styles.rolePermission}>
-            {/* <Input.Search enterButton placeholder="请输入权限名称" onSearch={() => { }} style={{ width: 300, marginBottom: '15px' }} /> */}
-            <div style={{ border: '1px solid #f0f0f0' }}>
-              <div
-                style={{
-                  textAlign: 'center',
-                  fontSize: '15px',
-                  width: '100%',
-                  paddingLeft: '10px',
-                  backgroundColor: '#fafafa',
-                  height: '55px',
-                  lineHeight: '55px',
-                  fontWeight: 600,
-                }}
-              >
-                菜单权限
-              </div>
-              <div style={{ padding: '0 20px', overflowY: 'scroll', height: '500px' }}>
-                <MenuPermission
-                  initialValues={initialValues}
-                  key={'menu-permission'}
-                  value={{
-                    id: 'menu-permission',
-                    buttons: [],
-                    name: '菜单权限',
-                    children: [...dataSource],
-                  }}
-                  change={(data: any) => {
-                    setInitialValues(data);
-                  }}
-                />
-              </div>
-            </div>
-          </div>
-        )}
-        {current === 1 && (
-          <DataPermission
-            initialValues={info?.dataAccess || []}
-            data={menuPermissions}
-            change={(data: any) => {
-              const dataAccess: any[] = [];
-              Object.keys(data).forEach((key) => {
-                if (data[key].value) {
-                  const dimensions = (data[key]?.type || []).map((i: string) => {
-                    return { dimensionType: i };
-                  });
-                  dataAccess.push({
-                    assetType: key,
-                    dimensions,
-                  });
-                }
-              });
-              setDataPermissions(dataAccess);
-            }}
-          />
-        )}
-      </div>
-      <div style={{ marginTop: '15px' }}>
-        {current === 0 && (
-          <Button
-            type="primary"
-            onClick={() => {
-              const data = initToMenu(initialValues.children, dataSource).filter((i) => i);
-              if (data.length > 0) {
-                setCurrent(1);
-                setMenuPermissions(data);
-              } else {
-                message.error('请选择菜单权限!');
-              }
-            }}
-          >
-            下一步
-          </Button>
-        )}
-        {current === 1 && (
-          <>
-            <Button
-              style={{ margin: '0 8px' }}
-              onClick={() => {
-                setCurrent(0);
-                initialMenu(params.id);
-              }}
+    <Form
+      layout="vertical"
+      form={form}
+      onFinish={async (values: any) => {
+        await service.update({
+          ...data,
+          name: values?.name,
+          description: values?.description || '',
+        });
+        service
+          .saveGrantTree('role', params?.id, {
+            menus: getDataList([...values.permission?.children]),
+          })
+          .subscribe((resp) => {
+            if (resp.status === 200) {
+              message.success('操作成功');
+            }
+          });
+      }}
+    >
+      <Card>
+        <div className={styles.title}>基本信息</div>
+        <Row>
+          <Col span={14}>
+            <Form.Item
+              label="名称"
+              name="name"
+              rules={[{ required: true, message: '请输入名称!' }]}
             >
-              上一步
-            </Button>
-            <Button
-              type="primary"
-              onClick={async () => {
-                const res = await service.modify(params?.id, {
-                  id: params?.id || '',
-                  name: info?.name,
-                  description: info?.description,
-                  dataAccess: [...dataPermissions],
-                });
-                if (res.status === 200) {
-                  getDetail(params.id);
-                }
-                service
-                  .saveGrantTree('role', params.id, {
-                    merge: true,
-                    priority: 0,
-                    menus: [...menuPermissions],
-                  })
-                  .subscribe((resp) => {
-                    if (resp.status === 200) {
-                      message.success('操作成功!');
-                      initialMenu(params.id);
-                    }
-                  });
-              }}
-            >
-              保存
-            </Button>
-          </>
-        )}
-      </div>
-    </Card>
+              <Input />
+            </Form.Item>
+          </Col>
+          <Col span={14}>
+            <Form.Item label="说明" name="description">
+              <Input.TextArea showCount maxLength={200} />
+            </Form.Item>
+          </Col>
+        </Row>
+      </Card>
+      <Card style={{ marginTop: 20 }}>
+        <div className={styles.title}>权限分配</div>
+        <Form.Item label="权限" name="permission" rules={[{ required: true }]}>
+          <Allocate />
+        </Form.Item>
+        <Form.Item>
+          <Button type="primary" htmlType="submit">
+            保存
+          </Button>
+        </Form.Item>
+      </Card>
+    </Form>
   );
 };
+
 export default Permission;

+ 37 - 11
src/pages/system/Role/Edit/UserManage/BindUser.tsx

@@ -4,8 +4,9 @@ import { message, Modal } from 'antd';
 import { useRef, useState } from 'react';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { service } from '@/pages/system/User/index';
-import encodeQuery from '@/utils/encodeQuery';
 import Service from '@/pages/system/Role/service';
+import SearchComponent from '@/components/SearchComponent';
+
 interface Props {
   visible: boolean;
   data: any;
@@ -17,7 +18,9 @@ const BindUser = (props: Props) => {
   const intl = useIntl();
   const actionRef = useRef<any>();
   const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
-  const columns: ProColumns<RoleItem>[] = [
+  const [param, setParam] = useState<any>({ terms: [] });
+
+  const columns: ProColumns<UserItem>[] = [
     {
       dataIndex: 'index',
       valueType: 'indexBorder',
@@ -75,8 +78,25 @@ const BindUser = (props: Props) => {
         props.cancel();
       }}
     >
+      <SearchComponent<UserItem>
+        field={columns}
+        target="user"
+        pattern={'simple'}
+        onSearch={(data) => {
+          // console.log(data);
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+        onReset={() => {
+          // 重置分页及搜索参数
+          actionRef.current?.reset?.();
+          setParam({});
+        }}
+      />
       <ProTable
         actionRef={actionRef}
+        search={false}
         rowSelection={{
           selectedRowKeys: selectedRowKeys,
           onChange: (key) => {
@@ -86,16 +106,22 @@ const BindUser = (props: Props) => {
         pagination={{
           pageSize: 10,
         }}
-        request={async (param: any) => {
-          const response = await service.query(
-            encodeQuery({
-              pageSize: param.pageSize,
-              pageIndex: param.current,
-              terms: {
-                'id$in-dimension$role$not': props.data.id,
+        request={async (params: any) => {
+          const response = await service.query({
+            pageSize: params.pageSize,
+            pageIndex: params.current,
+            terms: [
+              ...(param?.terms || []),
+              {
+                terms: [
+                  {
+                    column: 'id$in-dimension$role$not',
+                    value: props.data.id,
+                  },
+                ],
               },
-            }),
-          );
+            ],
+          });
           return {
             result: { data: response.result.data },
             success: true,

+ 63 - 35
src/pages/system/Role/Edit/UserManage/index.tsx

@@ -1,14 +1,15 @@
-import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
-import type { ProColumns, ActionType } from '@jetlinks/pro-table';
-import { Button, Card, message, Popconfirm, Space, Tooltip } from 'antd';
+import { DisconnectOutlined, PlusOutlined } from '@ant-design/icons';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
+import { Badge, Button, Card, message, Popconfirm, Space, Tooltip } from 'antd';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { useRef, useState } from 'react';
-import ProTable from '@jetlinks/pro-table';
 import BindUser from './BindUser';
 import { service } from '@/pages/system/User/index';
-import encodeQuery from '@/utils/encodeQuery';
 import { useParams } from 'umi';
 import Service from '@/pages/system/Role/service';
+import moment from 'moment';
+import SearchComponent from '@/components/SearchComponent';
 
 const UserManage = () => {
   const roleService = new Service('role');
@@ -17,6 +18,8 @@ const UserManage = () => {
   const actionRef = useRef<ActionType>();
   const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
   const [bindUserVisible, setBindUserVisible] = useState<boolean>(false);
+  const [param, setParam] = useState<any>({ terms: [] });
+
   const unBindUser = (id: string, ids: string[]) => {
     roleService.unbindUser(id, ids).subscribe((resp) => {
       if (resp.status === 200) {
@@ -25,41 +28,45 @@ const UserManage = () => {
       }
     });
   };
-  const columns: ProColumns<RoleItem>[] = [
+  const columns: ProColumns<UserItem>[] = [
     {
       dataIndex: 'index',
       valueType: 'indexBorder',
       width: 48,
     },
     {
-      title: intl.formatMessage({
-        id: 'pages.table.name',
-        defaultMessage: '名称',
-      }),
+      title: '姓名',
       dataIndex: 'name',
-      // copyable: true,
       ellipsis: true,
-      tip: intl.formatMessage({
-        id: 'pages.system.userName.tips',
-        defaultMessage: '用户名过长会自动收缩',
-      }),
-      formItemProps: {
-        rules: [
-          {
-            required: true,
-            message: '此项为必填项',
-          },
-        ],
-      },
+      align: 'center',
     },
     {
       title: intl.formatMessage({
         id: 'pages.system.username',
         defaultMessage: '用户名',
       }),
+      align: 'center',
       dataIndex: 'username',
-      filters: true,
-      onFilter: true,
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createTime',
+      ellipsis: true,
+      width: '200px',
+      align: 'center',
+      render: (text: any) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      ellipsis: true,
+      align: 'center',
+      render: (text, record) => (
+        <Badge
+          status={record?.status === 1 ? 'success' : 'error'}
+          text={record?.status === 1 ? '正常' : '禁用'}
+        />
+      ),
     },
     {
       title: intl.formatMessage({
@@ -78,7 +85,7 @@ const UserManage = () => {
             }}
           >
             <Tooltip title={'解绑'}>
-              <MinusOutlined />
+              <DisconnectOutlined />
             </Tooltip>
           </Popconfirm>
         </a>,
@@ -87,8 +94,23 @@ const UserManage = () => {
   ];
   return (
     <Card>
+      <SearchComponent<UserItem>
+        field={columns}
+        target="user"
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+        onReset={() => {
+          // 重置分页及搜索参数
+          actionRef.current?.reset?.();
+          setParam({});
+        }}
+      />
       <ProTable
         actionRef={actionRef}
+        search={false}
         tableAlertOptionRender={() => (
           <Space size={16}>
             <a
@@ -122,16 +144,22 @@ const UserManage = () => {
         pagination={{
           pageSize: 10,
         }}
-        request={async (param: any) => {
-          const response = await service.query(
-            encodeQuery({
-              pageSize: param.pageSize,
-              pageIndex: param.current,
-              terms: {
-                'id$in-dimension$role': params.id,
+        request={async (data: any) => {
+          const response = await service.query({
+            pageSize: data.pageSize,
+            pageIndex: data.current,
+            terms: [
+              {
+                terms: [
+                  {
+                    column: 'id$in-dimension$role',
+                    value: params.id,
+                  },
+                ],
               },
-            }),
-          );
+              ...(param?.terms || []),
+            ],
+          });
           return {
             result: { data: response.result.data },
             success: true,

+ 1 - 10
src/pages/system/Role/Edit/index.tsx

@@ -4,23 +4,14 @@ import { useState } from 'react';
 import { history } from 'umi';
 import UserManage from '@/pages/system/Role/Edit/UserManage';
 import Permission from '@/pages/system/Role/Edit/Permission';
-import Info from '@/pages/system/Role/Edit/Info';
 import { useIntl } from '@@/plugin-locale/localeExports';
 
 const RoleEdit = observer(() => {
   const intl = useIntl();
-  const [tab, setTab] = useState<string>('baseInfo');
+  const [tab, setTab] = useState<string>('permission');
 
   const list = [
     {
-      key: 'baseInfo',
-      tab: intl.formatMessage({
-        id: 'pages.system.role.access.baseInfo',
-        defaultMessage: '基本信息',
-      }),
-      component: <Info />,
-    },
-    {
       key: 'permission',
       tab: intl.formatMessage({
         id: 'pages.system.role.access.permission',

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

@@ -2,9 +2,9 @@ import { PageContainer } from '@ant-design/pro-layout';
 import React, { useEffect, useRef } from 'react';
 import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
 import { message, Popconfirm, Tooltip } from 'antd';
-import type { ProColumns, ActionType } from '@jetlinks/pro-table';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import BaseCrud from '@/components/BaseCrud';
-import BaseService from '@/utils/BaseService';
+import Service from './service';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { observer } from '@formily/react';
 import { Link, useLocation } from 'umi';
@@ -12,7 +12,7 @@ import { Store } from 'jetlinks-store';
 import SystemConst from '@/utils/const';
 import { CurdModel } from '@/components/BaseCrud/model';
 
-export const service = new BaseService<RoleItem>('role');
+export const service = new Service('role');
 
 const Role: React.FC = observer(() => {
   const intl = useIntl();

+ 2 - 3
src/pages/system/User/Save/index.tsx

@@ -280,10 +280,9 @@ const Save = (props: Props) => {
               onClick={() => {
                 const tab: any = window.open(`${origin}/#/system/department?save=true`);
                 tab!.onTabSaveSuccess = (value: any) => {
+                  console.log(value, 'value');
                   form.setFieldState('orgIdList', (state) => {
-                    state.dataSource = state.dataSource?.concat([
-                      { label: value.name, value: value.id },
-                    ]);
+                    state.dataSource = state.dataSource?.concat({ name: value.name, id: value.id });
                   });
                 };
               }}

+ 2 - 1
src/pages/user/Login/index.tsx

@@ -7,7 +7,7 @@ import Service from '@/pages/user/Login/service';
 import { createForm } from '@formily/core';
 import { createSchemaField } from '@formily/react';
 import { Form, FormItem, Input, Password, Submit } from '@formily/antd';
-import { filter, mergeMap } from 'rxjs/operators';
+import { catchError, filter, mergeMap } from 'rxjs/operators';
 import * as ICONS from '@ant-design/icons';
 import { useModel } from '@@/plugin-model/useModel';
 import SystemConst from '@/utils/const';
@@ -62,6 +62,7 @@ const Login: React.FC = () => {
       .pipe(
         filter((r) => r.enabled),
         mergeMap(Service.getCaptcha),
+        catchError(() => message.error('服务端挂了!')),
       )
       .subscribe(setCaptcha);
   };

+ 2 - 0
src/utils/menu.ts

@@ -63,6 +63,8 @@ export const MENUS_CODE = {
   'link/Protocol': 'link/Protocol',
   'link/Type': 'link/Type',
   'link/Type/Save': 'link/Type/Save',
+  'link/AccessConfig': 'link/AccessConfig',
+  'link/AccessConfig/Detail': 'link/AccessConfig/Detail',
   'log/Access': 'log/Access',
   'log/System': 'log/System',
   'media/Cascade': 'media/Cascade',