Lind 3 лет назад
Родитель
Сommit
1e0512e7fa
40 измененных файлов с 1511 добавлено и 1185 удалено
  1. 38 0
      src/components/BadgeStatus/index.tsx
  2. 12 10
      src/components/BaseCrud/save/index.tsx
  3. 37 16
      src/components/ProTableCard/CardItems/device.tsx
  4. 39 6
      src/components/ProTableCard/index.less
  5. 12 7
      src/components/ProTableCard/index.tsx
  6. 5 4
      src/components/Upload/Image/index.less
  7. 4 2
      src/components/Upload/Image/index.tsx
  8. 1 0
      src/components/index.ts
  9. 4 0
      src/global.less
  10. 1 0
      src/locales/zh-CN/pages.ts
  11. 76 80
      src/pages/device/Instance/index.tsx
  12. 1 0
      src/pages/device/Instance/typings.d.ts
  13. 55 0
      src/pages/link/AccessConfig/index.less
  14. 135 0
      src/pages/link/AccessConfig/index.tsx
  15. 53 0
      src/pages/link/Protocol/FileUpload/index.tsx
  16. 89 139
      src/pages/link/Protocol/index.tsx
  17. 18 0
      src/pages/link/Type/service.ts
  18. 225 102
      src/pages/system/Menu/Detail/edit.tsx
  19. 16 9
      src/pages/system/Menu/Detail/index.tsx
  20. 16 0
      src/pages/system/Menu/components/Title.tsx
  21. 1 1
      src/pages/system/Menu/components/permission.less
  22. 26 0
      src/pages/system/Menu/components/title.less
  23. 83 79
      src/pages/system/Menu/index.tsx
  24. 3 0
      src/pages/system/Menu/service.ts
  25. 1 0
      src/pages/system/Menu/typing.d.ts
  26. 0 107
      src/pages/system/Role/Edit/Info/index.tsx
  27. 241 0
      src/pages/system/Role/Edit/Permission/Allocate/MenuPermission.tsx
  28. 16 0
      src/pages/system/Role/Edit/Permission/Allocate/index.less
  29. 94 0
      src/pages/system/Role/Edit/Permission/Allocate/index.tsx
  30. 0 126
      src/pages/system/Role/Edit/Permission/DataPermission.tsx
  31. 0 188
      src/pages/system/Role/Edit/Permission/MenuPermission.tsx
  32. 10 15
      src/pages/system/Role/Edit/Permission/index.less
  33. 86 234
      src/pages/system/Role/Edit/Permission/index.tsx
  34. 37 11
      src/pages/system/Role/Edit/UserManage/BindUser.tsx
  35. 63 35
      src/pages/system/Role/Edit/UserManage/index.tsx
  36. 1 10
      src/pages/system/Role/Edit/index.tsx
  37. 3 3
      src/pages/system/Role/index.tsx
  38. 2 1
      src/pages/user/Login/index.tsx
  39. 5 0
      src/style/common.less
  40. 2 0
      src/utils/menu.ts

+ 38 - 0
src/components/BadgeStatus/index.tsx

@@ -0,0 +1,38 @@
+import { Badge } from 'antd';
+
+/**
+ * 状态色
+ */
+export enum StatusColorEnum {
+  'success' = 'success',
+  'error' = 'error',
+  'processing' = 'processing',
+  'warning' = 'warning',
+  'default' = 'default',
+}
+
+export type StatusColorType = keyof typeof StatusColorEnum;
+
+export interface BadgeStatusProps {
+  text: string;
+  status: string | number;
+  /**
+   * 自定义status值颜色
+   * @example {
+   *   1: 'success',
+   *   0: 'error'
+   * }
+   */
+  statusNames?: Record<string | number, StatusColorType>;
+}
+
+export default (props: BadgeStatusProps) => {
+  const handleStatusColor = (): StatusColorType | undefined => {
+    if ('statusNames' in props) {
+      return props.statusNames![props.status];
+    }
+    return StatusColorEnum['default'];
+  };
+
+  return <Badge status={handleStatusColor()} text={props.text} />;
+};

+ 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>

+ 37 - 16
src/components/ProTableCard/CardItems/device.tsx

@@ -1,24 +1,45 @@
-import { Card } from 'antd';
-import { EditOutlined, EllipsisOutlined, SettingOutlined } from '@ant-design/icons';
+import { Avatar, Card } from 'antd';
+import React from 'react';
+import type { DeviceInstance } from '@/pages/device/Instance/typings';
+import { BadgeStatus } from '@/components';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import '@/style/common.less';
 
-export interface DeviceCardProps {
-  id: string;
-  name: string;
-  photoUrl?: string;
+export interface DeviceCardProps extends DeviceInstance {
+  actions?: React.ReactNode[];
+  avatarSize?: number;
 }
 
 export default (props: DeviceCardProps) => {
   return (
-    <Card
-      style={{ width: 280 }}
-      cover={null}
-      actions={[
-        <SettingOutlined key="setting" />,
-        <EditOutlined key="edit" />,
-        <EllipsisOutlined key="ellipsis" />,
-      ]}
-    >
-      <div>{props.name}</div>
+    <Card style={{ width: 340 }} cover={null} actions={props.actions}>
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <Avatar size={props.avatarSize || 64} src={props.photoUrl} />
+        </div>
+        <div className={'card-item-body'}>
+          <div className={'card-item-header'}>
+            <span className={'card-item-header-name ellipsis'}>{props.name}</span>
+            <BadgeStatus
+              status={props.state.value}
+              text={props.state.text}
+              statusNames={{
+                online: StatusColorEnum.success,
+                offline: StatusColorEnum.error,
+                notActive: StatusColorEnum.processing,
+              }}
+            />
+          </div>
+          <div className={'card-item-content'}>
+            <label>设备类型:</label>
+            <span className={'ellipsis'}>{props.deviceType ? props.deviceType.text : '--'}</span>
+          </div>
+          <div className={'card-item-content'}>
+            <label>产品名称:</label>
+            <span className={'ellipsis'}>{props.productName || '--'}</span>
+          </div>
+        </div>
+      </div>
     </Card>
   );
 };

+ 39 - 6
src/components/ProTableCard/index.less

@@ -18,13 +18,46 @@
   }
 
   .pro-table-card-items {
-    display: flex;
-    flex-wrap: wrap;
-    padding-bottom: 32px;
+    display: grid;
+    grid-gap: 26px;
+    grid-template-columns: repeat(auto-fit, 340px);
+    //display: flex;
+    //flex-wrap: wrap;
+    padding-bottom: 38px;
 
-    > div {
-      margin-right: 14px;
-      margin-bottom: 14px;
+    .pro-table-card-item {
+      display: flex;
+
+      .card-item-avatar {
+        margin-right: 16px;
+      }
+
+      .card-item-body {
+        display: flex;
+        flex-direction: column;
+        flex-grow: 1;
+        width: 0;
+
+        .card-item-header {
+          display: flex;
+          width: 100%;
+          margin-bottom: 12px;
+
+          .card-item-header-name {
+            flex: 1;
+            font-weight: bold;
+            font-size: 16px;
+          }
+        }
+
+        .card-item-content {
+          display: flex;
+
+          > span {
+            flex: 1;
+          }
+        }
+      }
     }
   }
 

+ 12 - 7
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 { Pagination, Space } from 'antd';
+import { Empty, Pagination, Space } from 'antd';
 import { AppstoreOutlined, BarsOutlined } from '@ant-design/icons';
 import classNames from 'classnames';
 import './index.less';
@@ -13,6 +13,8 @@ enum ModelEnum {
   CARD = 'CARD',
 }
 
+const Default_Size = 5;
+
 type ModelType = keyof typeof ModelEnum;
 
 interface ProTableCardProps<T> {
@@ -31,7 +33,7 @@ const ProTableCard = <
   const [total, setTotal] = useState<number | undefined>(0);
   const [current, setCurrent] = useState(1); // 当前页
   const [pageIndex, setPageIndex] = useState(0);
-  const [pageSize, setPageSize] = useState(10); // 每页条数
+  const [pageSize, setPageSize] = useState(Default_Size * 2); // 每页条数
 
   /**
    * 处理 Card
@@ -40,11 +42,11 @@ const ProTableCard = <
   const handleCard = (dataSource: readonly T[] | undefined): JSX.Element => {
     return (
       <div className={'pro-table-card-items'}>
-        {dataSource
-          ? dataSource.map((item) =>
-              cardRender && isFunction(cardRender) ? cardRender(item) : null,
-            )
-          : null}
+        {dataSource ? (
+          dataSource.map((item) => (cardRender && isFunction(cardRender) ? cardRender(item) : null))
+        ) : (
+          <Empty />
+        )}
       </div>
     );
   };
@@ -61,6 +63,7 @@ const ProTableCard = <
             pageSize,
           } as any
         }
+        options={model === ModelEnum.CARD ? false : props.options}
         request={async (param, sort, filter) => {
           if (request) {
             const resp = await request(param, sort, filter);
@@ -86,6 +89,7 @@ const ProTableCard = <
           },
           pageSize: pageSize,
           current: current,
+          pageSizeOptions: [Default_Size * 2, Default_Size * 4, 50, 100],
         }}
         toolBarRender={(action, row) => {
           const oldBar = toolBarRender ? toolBarRender(action, row) : [];
@@ -139,6 +143,7 @@ const ProTableCard = <
             setPageIndex(page - 1);
             setPageSize(size);
           }}
+          pageSizeOptions={[Default_Size * 2, Default_Size * 4, 50, 100]}
           pageSize={pageSize}
           showTotal={(num) => {
             const minSize = pageIndex * pageSize + 1;

+ 5 - 4
src/components/Upload/Image/index.less

@@ -16,8 +16,8 @@
 
   .upload-image-border {
     position: relative;
-    width: @with;
-    height: @height;
+    //width: @with;
+    //height: @height;
     overflow: hidden;
     //border-radius: 50%;
     border: @border;
@@ -35,6 +35,7 @@
       flex-direction: column;
       width: @with;
       height: @height;
+      padding: 8px;
       background-color: rgba(#000, 0.06);
       cursor: pointer;
 
@@ -53,8 +54,8 @@
       }
 
       .upload-image {
-        width: 144px;
-        height: 138px;
+        width: 100%;
+        height: 100%;
         //border-radius: 50%;
         background-repeat: no-repeat;
         background-position: center;

+ 4 - 2
src/components/Upload/Image/index.tsx

@@ -1,5 +1,5 @@
 import { message, Upload } from 'antd';
-import { useEffect, useState } from 'react';
+import React, { useEffect, useState } from 'react';
 import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
 import SystemConst from '@/utils/const';
 import Token from '@/utils/token';
@@ -19,6 +19,7 @@ interface UploadImageProps {
    * 图片大小限制, 单位 M,默认 4 M
    */
   size?: number;
+  style?: React.CSSProperties;
 }
 
 export default ({ onChange, value, ...extraProps }: UploadImageProps) => {
@@ -70,7 +71,7 @@ export default ({ onChange, value, ...extraProps }: UploadImageProps) => {
           beforeUpload={beforeUpload}
           {...extraProps}
         >
-          <div className={'upload-image-content'}>
+          <div className={'upload-image-content'} style={extraProps.style}>
             {values ? (
               <>
                 {/*<img width={120} height={120} src={values} />*/}
@@ -89,6 +90,7 @@ export default ({ onChange, value, ...extraProps }: UploadImageProps) => {
             )}
           </div>
         </Upload>
+        {extraProps.disabled && <div className={'upload-loading-mask'} />}
         {values && loading ? (
           <div className={'upload-loading-mask'}>
             {loading ? <LoadingOutlined style={{ fontSize: 28 }} /> : null}

+ 1 - 0
src/components/index.ts

@@ -1,3 +1,4 @@
 export { default as RadioCard } from './RadioCard';
 export { default as UploadImage } from './Upload/Image';
 export { default as ProTableCard } from './ProTableCard';
+export { default as BadgeStatus } from './BadgeStatus';

+ 4 - 0
src/global.less

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

+ 1 - 0
src/locales/zh-CN/pages.ts

@@ -146,6 +146,7 @@ export default {
   'pages.system.menu.detail': '基本信息',
   'pages.system.menu.buttons': '按钮管理',
   'pages.system.menu.root': '菜单权限',
+  'page.system.menu.sort': '排序',
   // 系统设置-第三方平台
   'pages.system.openApi': '第三方平台',
   'pages.system.openApi.username': '用户名',

+ 76 - 80
src/pages/device/Instance/index.tsx

@@ -65,13 +65,79 @@ const Instance = () => {
   const [bindKeys, setBindKeys] = useState<any[]>([]);
   const intl = useIntl();
 
+  const tools = (record: DeviceInstance) => [
+    <Link
+      onClick={() => {
+        InstanceModel.current = record;
+      }}
+      to={`/device/instance/detail/${record.id}`}
+      key="link"
+    >
+      <Tooltip
+        title={intl.formatMessage({
+          id: 'pages.data.option.detail',
+          defaultMessage: '查看',
+        })}
+        key={'detail'}
+      >
+        <EyeOutlined />
+      </Tooltip>
+    </Link>,
+    <a href={record.id} target="_blank" rel="noopener noreferrer" key="view">
+      <Popconfirm
+        title={intl.formatMessage({
+          id: 'pages.data.option.disabled.tips',
+          defaultMessage: '确认禁用?',
+        })}
+        onConfirm={async () => {
+          if (record.state.value !== 'notActive') {
+            await service.undeployDevice(record.id);
+          } else {
+            await service.deployDevice(record.id);
+          }
+          message.success(
+            intl.formatMessage({
+              id: 'pages.data.option.success',
+              defaultMessage: '操作成功!',
+            }),
+          );
+          actionRef.current?.reload();
+        }}
+      >
+        <Tooltip
+          title={intl.formatMessage({
+            id: `pages.data.option.${record.state.value !== 'notActive' ? 'disabled' : 'enabled'}`,
+            defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
+          })}
+        >
+          {record.state.value !== 'notActive' ? <StopOutlined /> : <CheckCircleOutlined />}
+        </Tooltip>
+      </Popconfirm>
+    </a>,
+
+    <a key={'delete'}>
+      <Popconfirm
+        title="确认删除"
+        onConfirm={async () => {
+          await service.remove(record.id);
+          message.success(
+            intl.formatMessage({
+              id: 'pages.data.option.success',
+              defaultMessage: '操作成功!',
+            }),
+          );
+          actionRef.current?.reload();
+        }}
+      >
+        <Tooltip title={'删除'}>
+          <DeleteOutlined />
+        </Tooltip>
+      </Popconfirm>
+    </a>,
+  ];
+
   const columns: ProColumns<DeviceInstance>[] = [
     {
-      dataIndex: 'index',
-      valueType: 'indexBorder',
-      width: 48,
-    },
-    {
       title: 'ID',
       dataIndex: 'id',
     },
@@ -152,78 +218,7 @@ const Instance = () => {
       valueType: 'option',
       align: 'center',
       width: 200,
-      render: (text, record) => [
-        <Link
-          onClick={() => {
-            InstanceModel.current = record;
-          }}
-          to={`/device/instance/detail/${record.id}`}
-          key="link"
-        >
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.data.option.detail',
-              defaultMessage: '查看',
-            })}
-            key={'detail'}
-          >
-            <EyeOutlined />
-          </Tooltip>
-        </Link>,
-        <a href={record.id} target="_blank" rel="noopener noreferrer" key="view">
-          <Popconfirm
-            title={intl.formatMessage({
-              id: 'pages.data.option.disabled.tips',
-              defaultMessage: '确认禁用?',
-            })}
-            onConfirm={async () => {
-              if (record.state.value !== 'notActive') {
-                await service.undeployDevice(record.id);
-              } else {
-                await service.deployDevice(record.id);
-              }
-              message.success(
-                intl.formatMessage({
-                  id: 'pages.data.option.success',
-                  defaultMessage: '操作成功!',
-                }),
-              );
-              actionRef.current?.reload();
-            }}
-          >
-            <Tooltip
-              title={intl.formatMessage({
-                id: `pages.data.option.${
-                  record.state.value !== 'notActive' ? 'disabled' : 'enabled'
-                }`,
-                defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
-              })}
-            >
-              {record.state.value !== 'notActive' ? <StopOutlined /> : <CheckCircleOutlined />}
-            </Tooltip>
-          </Popconfirm>
-        </a>,
-
-        <a key={'delete'}>
-          <Popconfirm
-            title="确认删除"
-            onConfirm={async () => {
-              await service.remove(record.id);
-              message.success(
-                intl.formatMessage({
-                  id: 'pages.data.option.success',
-                  defaultMessage: '操作成功!',
-                }),
-              );
-              actionRef.current?.reload();
-            }}
-          >
-            <Tooltip title={'删除'}>
-              <DeleteOutlined />
-            </Tooltip>
-          </Popconfirm>
-        </a>,
-      ],
+      render: (text, record) => tools(record),
     },
   ];
 
@@ -369,12 +364,13 @@ const Instance = () => {
             setBindKeys(selectedRows.map((item) => item.id));
           },
         }}
-        toolBarRender={() => [
+        headerTitle={[
           <Button
             onClick={() => {
               setVisible(true);
               setCurrent({});
             }}
+            style={{ marginRight: 12 }}
             key="button"
             icon={<PlusOutlined />}
             type="primary"
@@ -388,11 +384,11 @@ const Instance = () => {
             <Button>批量操作</Button>
           </Dropdown>,
         ]}
-        cardRender={(item) => <DeviceCard {...item} />}
+        cardRender={(record) => <DeviceCard {...record} actions={tools(record)} />}
       />
       <Save
         data={current}
-        model={!current ? 'add' : 'edit'}
+        model={!Object.keys(current).length ? 'add' : 'edit'}
         close={() => {
           setVisible(false);
         }}

+ 1 - 0
src/pages/device/Instance/typings.d.ts

@@ -37,6 +37,7 @@ export type DeviceInstance = {
   onlineTime: string | number;
   offlineTime: string | number;
   tags: any;
+  photoUrl: string;
 };
 
 type Unit = {

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

@@ -0,0 +1,55 @@
+.content {
+  display: flex;
+  width: 90%;
+  margin: 0 78px;
+
+  .server {
+    width: calc(50% - 78px);
+
+    :global {
+      .ant-badge-status-text {
+        color: rgba(0, 0, 0, 0.55);
+      }
+    }
+  }
+
+  .procotol {
+    width: calc(50% - 78px);
+  }
+}
+
+.desc {
+  margin-top: 10px;
+  color: rgba(0, 0, 0, 0.55);
+  font-weight: 400;
+  font-size: 13px;
+}
+
+.title {
+  margin-bottom: 10px;
+  font-weight: 600;
+  font-size: 14px;
+}
+
+:global {
+  .ant-list-item-meta-avatar {
+    width: 64px !important;
+    height: 64px !important;
+  }
+}
+
+.images {
+  width: 136px;
+  height: 108px;
+  color: white;
+  font-size: 18px;
+  line-height: 108px;
+  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);
+}

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

@@ -0,0 +1,135 @@
+import SearchComponent from '@/components/SearchComponent';
+import { CheckCircleOutlined, DeleteOutlined, EditOutlined, StopOutlined } from '@ant-design/icons';
+import { PageContainer } from '@ant-design/pro-layout';
+import ProList from '@jetlinks/pro-list';
+import { Badge, Card } from 'antd';
+import styles from './index.less';
+
+const AccessConfig = () => {
+  const dataSource = [
+    {
+      name: 'MQTT-官方协议接入',
+      avatar: 'MQTT',
+      state: 0,
+      describe: ' 我是一条测试的描述我是一条测试的描述我是一条测试的描述我是一条测试',
+    },
+    {
+      name: 'Modbus-TCP',
+      avatar: 'Modbus-TCP',
+      state: 0,
+      describe: ' 我是一条测试的描述我是一条测试的描述我是一条测试',
+    },
+    {
+      name: 'Modbus-TCP',
+      avatar: 'Modbus',
+      state: 1,
+      describe: ' 我是一条测试的描述我是一条测试的描述',
+    },
+    {
+      name: 'MQTT-官方协议接入',
+      avatar: 'MQTT',
+      state: 0,
+      describe: ' 我是一条测试的描述',
+    },
+  ];
+
+  return (
+    <PageContainer>
+      <Card>
+        <SearchComponent field={[]} pattern={'simple'} onSearch={() => {}} />
+        <ProList<any>
+          pagination={{
+            defaultPageSize: 8,
+            showSizeChanger: false,
+          }}
+          showActions="always"
+          rowKey="name"
+          dataSource={dataSource}
+          grid={{ gutter: 16, column: 2 }}
+          showExtra="always"
+          metas={{
+            title: {
+              dataIndex: 'name',
+              render: (text, row) => (
+                <div style={{ fontSize: 16 }}>
+                  <div>
+                    {text}
+                    {/* <a style={{ marginLeft: '10px' }}>
+                      <EditOutlined />
+                    </a> */}
+                    <Badge
+                      color={row.state !== 1 ? 'red' : 'green'}
+                      text={row.state !== 1 ? '禁用' : '正常'}
+                      style={{ marginLeft: '20px' }}
+                    />
+                  </div>
+                  <div className={styles.desc}>{row.describe}</div>
+                </div>
+              ),
+            },
+            avatar: {
+              render: (text, reocrd) => <div className={styles.images}>{reocrd.avatar}</div>,
+            },
+            subTitle: {
+              render: () => <div></div>,
+            },
+            content: {
+              render: (text, row) => (
+                <div className={styles.content}>
+                  <div className={styles.server}>
+                    <div className={styles.title}>MQTT服务</div>
+                    <p>
+                      <div>
+                        <Badge color={'green'} text={'mqtt://192.1.1:8080'} />
+                      </div>
+                      <div>
+                        <Badge color={'green'} text={'mqtt://192.1.1:8080'} />
+                      </div>
+                      <div>
+                        <Badge color={'red'} text={'mqtt://192.1.1:8080'} />
+                      </div>
+                    </p>
+                  </div>
+                  <div className={styles.procotol}>
+                    <div className={styles.title}>官方协议1.0</div>
+                    <p style={{ color: 'rgba(0, 0, 0, .55)' }}>
+                      {row.describe}
+                      {row.describe}
+                    </p>
+                  </div>
+                </div>
+              ),
+            },
+            actions: {
+              render: (text, row) => [
+                <a key="edit">
+                  <EditOutlined />
+                  编辑
+                </a>,
+                <a key="warning">
+                  {row.state === 1 ? (
+                    <span>
+                      <StopOutlined />
+                      禁用
+                    </span>
+                  ) : (
+                    <span>
+                      <CheckCircleOutlined />
+                      启用
+                    </span>
+                  )}
+                </a>,
+                <a key="remove">
+                  <DeleteOutlined />
+                  删除
+                </a>,
+              ],
+            },
+          }}
+        />
+      </Card>
+    </PageContainer>
+  );
+};
+
+export default AccessConfig;

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

+ 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;

+ 225 - 102
src/pages/system/Menu/Detail/edit.tsx

@@ -1,20 +1,40 @@
-import { Form, Input, InputNumber, Button, message } from 'antd';
+import {
+  Button,
+  Card,
+  Col,
+  Form,
+  Input,
+  InputNumber,
+  message,
+  Radio,
+  Row,
+  Select,
+  Switch,
+  Tooltip,
+  TreeSelect,
+} from 'antd';
 import Permission from '@/pages/system/Menu/components/permission';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { useEffect, useState } from 'react';
 import { service } from '@/pages/system/Menu';
 import { useRequest } from 'umi';
 import type { MenuItem } from '@/pages/system/Menu/typing';
-import { debounce } from 'lodash';
+// import { debounce } from 'lodash';
+import Title from '../components/Title';
+import { UploadImage } from '@/components';
+import { QuestionCircleFilled } from '@ant-design/icons';
 
 type EditProps = {
   data: MenuItem;
-  onLoad: () => void;
+  onLoad: (id: string) => void;
 };
 
 export default (props: EditProps) => {
   const intl = useIntl();
   const [disabled, setDisabled] = useState(true);
+  const [show, setShow] = useState(true);
+  const [accessSupport, setAccessSupport] = useState('unsupported');
+
   const [form] = Form.useForm();
 
   const { data: permissions, run: queryPermissions } = useRequest(service.queryPermission, {
@@ -22,6 +42,16 @@ export default (props: EditProps) => {
     formatResult: (response) => response.result.data,
   });
 
+  const { data: menuThree, run: queryMenuThree } = useRequest(service.queryMenuThree, {
+    manual: true,
+    formatResult: (response) => response.result,
+  });
+
+  const { data: assetsType, run: queryAssetsType } = useRequest(service.queryAssetsType, {
+    manual: true,
+    formatResult: (response) => response.result,
+  });
+
   const saveData = async () => {
     const formData = await form.validateFields();
     if (formData) {
@@ -29,126 +59,219 @@ export default (props: EditProps) => {
       if (response.status === 200) {
         message.success('操作成功!');
         setDisabled(true);
-        props.onLoad();
+        props.onLoad(response.result.id);
       } else {
         message.error('操作失败!');
       }
     }
   };
 
-  const filterThree = (e: any) => {
-    const _data: any = {
-      paging: false,
-    };
-    if (e.target.value) {
-      _data.terms = [{ column: 'name', value: e.target.value }];
-    }
-    queryPermissions(_data);
-  };
+  // const filterThree = (e: any) => {
+  //   const _data: any = {
+  //     paging: false,
+  //   };
+  //   if (e.target.value) {
+  //     _data.terms = [{ column: 'name', value: e.target.value }];
+  //   }
+  //   queryPermissions(_data);
+  // };
 
   useEffect(() => {
     queryPermissions({ paging: false });
+    queryMenuThree({ paging: false });
+    queryAssetsType();
     /* eslint-disable */
   }, []);
 
   useEffect(() => {
     if (form) {
-      form.setFieldsValue(props.data);
+      form.setFieldsValue({
+        ...props.data,
+        accessSupport: props.data.accessSupport ? props.data.accessSupport.value : 'unsupported',
+      });
+      setAccessSupport(props.data.accessSupport ? props.data.accessSupport.value : 'unsupported');
     }
+    setDisabled(!!props.data.id);
     /* eslint-disable */
   }, [props.data]);
 
   return (
     <div>
-      <Form form={form} labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
-        <Form.Item
-          name="code"
-          label={intl.formatMessage({
-            id: 'page.system.menu.encoding',
-            defaultMessage: '编码',
-          })}
-          required={true}
-          rules={[{ required: true, message: '该字段是必填字段' }]}
-        >
-          <Input disabled={disabled} />
-        </Form.Item>
-        <Form.Item
-          name="name"
-          label={intl.formatMessage({
-            id: 'pages.table.name',
-            defaultMessage: '名称',
-          })}
-          required={true}
-          rules={[{ required: true, message: '该字段是必填字段' }]}
-        >
-          <Input disabled={disabled} />
-        </Form.Item>
-        <Form.Item
-          name="url"
-          label={intl.formatMessage({
-            id: 'page.system.menu.url',
-            defaultMessage: '页面地址',
-          })}
-          required={true}
-          rules={[{ required: true, message: '该字段是必填字段' }]}
-        >
-          <Input disabled={disabled} />
-        </Form.Item>
-        <Form.Item
-          label={intl.formatMessage({
-            id: 'page.system.menu.permissions',
-            defaultMessage: '权限',
-          })}
-        >
-          <Input disabled={disabled} onChange={debounce(filterThree, 300)} />
-          <Form.Item name="permissions">
-            <Permission
-              title={intl.formatMessage({
-                id: 'page.system.menu.permissions.operate',
-                defaultMessage: '操作权限',
-              })}
-              disabled={disabled}
-              data={permissions}
-            />
-          </Form.Item>
-        </Form.Item>
-        <Form.Item
-          name="sortIndex"
-          label={intl.formatMessage({
-            id: 'page.system.menu.sort',
-            defaultMessage: '排序说明',
-          })}
-        >
-          <InputNumber style={{ width: '100%' }} disabled={disabled} />
-        </Form.Item>
-        <Form.Item
-          name="describe"
-          label={intl.formatMessage({
-            id: 'pages.table.describe',
-            defaultMessage: '描述',
-          })}
-        >
-          <Input.TextArea disabled={disabled} />
-        </Form.Item>
-        <Form.Item name="id" hidden={true}>
-          <Input />
-        </Form.Item>
+      <Form form={form} layout={'vertical'}>
+        <Card>
+          <Title title={'基本信息'} />
+          <Row>
+            <Col span={3}>
+              <Form.Item name={'icon'} label={'菜单图标'} required={true}>
+                <UploadImage disabled={disabled} style={{ width: 140, height: 130 }} />
+              </Form.Item>
+            </Col>
+            <Col span={21}>
+              <Row gutter={[24, 0]}>
+                <Col span={12}>
+                  <Form.Item
+                    name="name"
+                    label={intl.formatMessage({
+                      id: 'pages.table.name',
+                      defaultMessage: '名称',
+                    })}
+                    required={true}
+                    rules={[{ required: true, message: '该字段是必填字段' }]}
+                  >
+                    <Input disabled={disabled} />
+                  </Form.Item>
+                </Col>
+                <Col span={12}>
+                  <Form.Item
+                    name="code"
+                    label={intl.formatMessage({
+                      id: 'page.system.menu.encoding',
+                      defaultMessage: '编码',
+                    })}
+                    required={true}
+                    rules={[{ required: true, message: '该字段是必填字段' }]}
+                  >
+                    <Input disabled={disabled} />
+                  </Form.Item>
+                </Col>
+              </Row>
+              <Row gutter={[24, 0]}>
+                <Col span={12}>
+                  <Form.Item
+                    name="url"
+                    label={intl.formatMessage({
+                      id: 'page.system.menu.url',
+                      defaultMessage: '页面地址',
+                    })}
+                    required={true}
+                    rules={[
+                      { required: true, message: '该字段是必填字段' },
+                      { max: 120, message: '最多可输入120字符' },
+                    ]}
+                  >
+                    <Input disabled={disabled} />
+                  </Form.Item>
+                </Col>
+                <Col span={12}>
+                  <Form.Item
+                    name="sortIndex"
+                    label={intl.formatMessage({
+                      id: 'page.system.menu.sort',
+                      defaultMessage: '排序',
+                    })}
+                  >
+                    <InputNumber style={{ width: '100%' }} disabled={disabled} />
+                  </Form.Item>
+                </Col>
+              </Row>
+            </Col>
+          </Row>
+        </Card>
+        <Card style={{ marginTop: 24 }}>
+          <Title
+            title={'权限配置'}
+            toolbarRender={
+              <Switch
+                disabled={disabled}
+                checkedChildren="开启"
+                unCheckedChildren="关闭"
+                checked={show}
+                onChange={(checked) => {
+                  setShow(checked);
+                }}
+              />
+            }
+          />
+          {show && (
+            <Row gutter={[0, 10]}>
+              <Col span={24}>
+                <Form.Item
+                  label={'数据权限控制'}
+                  tooltip={'此菜单页面数据所对应的资产类型'}
+                  name={'accessSupport'}
+                >
+                  <Radio.Group
+                    onChange={(e) => {
+                      setAccessSupport(e.target.value);
+                    }}
+                    disabled={disabled}
+                  >
+                    <Radio value={'unsupported'}>不支持</Radio>
+                    <Radio value={'support'}>支持</Radio>
+                    <Radio value={'indirect'}>
+                      间接控制
+                      <Tooltip
+                        placement="topLeft"
+                        title={'此菜单内的数据基于其他菜单的数据权限控制'}
+                      >
+                        <QuestionCircleFilled style={{ marginLeft: 8 }} />
+                      </Tooltip>
+                    </Radio>
+                  </Radio.Group>
+                </Form.Item>
+                {accessSupport === 'support' && (
+                  <Form.Item name={'assetType'}>
+                    <Select
+                      style={{ width: 500 }}
+                      disabled={disabled}
+                      options={
+                        assetsType
+                          ? assetsType.map((item: any) => ({ label: item.name, value: item.id }))
+                          : []
+                      }
+                    />
+                  </Form.Item>
+                )}
+                {accessSupport === 'indirect' && (
+                  <Form.Item name={'indirectMenus'}>
+                    <TreeSelect
+                      style={{ width: 400 }}
+                      disabled={disabled}
+                      multiple
+                      fieldNames={{ label: 'name', value: 'id' }}
+                      treeData={menuThree}
+                    />
+                  </Form.Item>
+                )}
+                <Form.Item
+                  label={intl.formatMessage({
+                    id: 'page.system.menu.permissions',
+                    defaultMessage: '权限',
+                  })}
+                  name="permissions"
+                >
+                  {/*<Input disabled={disabled} onChange={debounce(filterThree, 300)} style={{ width: 300 }}/>*/}
+                  {/*<Form.Item name='permissions'>*/}
+                  <Permission
+                    title={intl.formatMessage({
+                      id: 'page.system.menu.permissions.operate',
+                      defaultMessage: '操作权限',
+                    })}
+                    disabled={disabled}
+                    data={permissions}
+                  />
+                  {/*</Form.Item>*/}
+                </Form.Item>
+              </Col>
+            </Row>
+          )}
+          <Button
+            type="primary"
+            onClick={() => {
+              if (disabled) {
+                setDisabled(false);
+              } else {
+                saveData();
+              }
+            }}
+          >
+            {intl.formatMessage({
+              id: `pages.data.option.${disabled ? 'edit' : 'save'}`,
+              defaultMessage: '编辑',
+            })}
+          </Button>
+        </Card>
       </Form>
-      <Button
-        type="primary"
-        onClick={() => {
-          if (disabled) {
-            setDisabled(false);
-          } else {
-            saveData();
-          }
-        }}
-      >
-        {intl.formatMessage({
-          id: `pages.data.option.${disabled ? 'edit' : 'save'}`,
-          defaultMessage: '编辑',
-        })}
-      </Button>
     </div>
   );
 };

+ 16 - 9
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;
@@ -15,6 +14,7 @@ type LocationType = {
 export default () => {
   const intl = useIntl();
   const [tabKey, setTabKey] = useState('detail');
+  const [pId, setPid] = useState<string | null>(null);
   const location = useLocation<LocationType>();
 
   const { data, run: queryData } = useRequest(service.queryDetail, {
@@ -27,12 +27,16 @@ export default () => {
   /**
    * 获取当前菜单详情
    */
-  const queryDetail = () => {
+  const queryDetail = (editId?: string) => {
     const params = new URLSearchParams(location.search);
-    const id = params.get('id');
+    const id = editId || params.get('id');
+    const _pId = params.get('pId');
     if (id) {
       queryData(id);
     }
+    if (_pId) {
+      setPid(_pId);
+    }
   };
 
   useEffect(() => {
@@ -63,11 +67,14 @@ export default () => {
       }}
     >
       {tabKey === 'detail' ? (
-        <div style={{ background: '#fff', padding: '16px 24px' }}>
-          <div style={{ width: 660 }}>
-            {' '}
-            <BaseDetail data={data} onLoad={queryDetail} />{' '}
-          </div>
+        <div style={{ padding: '16px 24px' }}>
+          <BaseDetail
+            data={{
+              ...data,
+              parentId: pId,
+            }}
+            onLoad={queryDetail}
+          />
         </div>
       ) : (
         <Buttons data={data} onLoad={queryDetail} />

+ 16 - 0
src/pages/system/Menu/components/Title.tsx

@@ -0,0 +1,16 @@
+import React from 'react';
+import './title.less';
+
+interface TitleProps {
+  title: string | React.ReactNode;
+  toolbarRender?: React.ReactNode;
+}
+
+export default (props: TitleProps) => {
+  return (
+    <div className={'descriptions-title'}>
+      <span>{props.title}</span>
+      {props.toolbarRender}
+    </div>
+  );
+};

+ 1 - 1
src/pages/system/Menu/components/permission.less

@@ -2,7 +2,7 @@
 @border: 1px solid @border-color-base;
 
 .permission-container {
-  margin-top: 20px;
+  //margin-top: 20px;
   border: @border;
 
   .permission-header {

+ 26 - 0
src/pages/system/Menu/components/title.less

@@ -0,0 +1,26 @@
+@import '~antd/lib/style/themes/variable';
+
+.descriptions-title {
+  position: relative;
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+  padding: 4px 0 4px 12px;
+  font-weight: bold;
+  font-size: 16px;
+
+  > span {
+    margin-right: 12px;
+  }
+
+  &::before {
+    position: absolute;
+    top: 5px;
+    left: 0;
+    width: 4px;
+    height: calc(100% - 10px);
+    background-color: @primary-color-hover;
+    border-radius: 2px;
+    content: ' ';
+  }
+}

+ 83 - 79
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, Modal, Form, Input } from 'antd';
+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';
@@ -39,7 +39,7 @@ export default observer(() => {
   const intl = useIntl();
 
   const [param, setParam] = useState({});
-  const [form] = Form.useForm();
+  // const [form] = Form.useForm();
   const history = useHistory();
 
   const deleteItem = async (id: string) => {
@@ -58,10 +58,13 @@ export default observer(() => {
   /**
    * 跳转详情页
    * @param id
+   * @param pId
    */
-  const pageJump = (id: string) => {
+  const pageJump = (id?: string, pId?: string) => {
     // 跳转详情
-    history.push(`${getMenuPathByCode(MENUS_CODE['system/Menu/Detail'])}?id=${id}`);
+    history.push(
+      `${getMenuPathByCode(MENUS_CODE['system/Menu/Detail'])}?id=${id || ''}&pId=${pId || ''}`,
+    );
   };
 
   const columns: ProColumns<MenuItem>[] = [
@@ -145,7 +148,8 @@ export default observer(() => {
             State.current = {
               parentId: record.id,
             };
-            State.visible = true;
+            // State.visible = true;
+            pageJump('', record.id);
           }}
         >
           <Tooltip
@@ -192,29 +196,29 @@ export default observer(() => {
     });
   };
 
-  const modalCancel = () => {
-    State.current = {};
-    State.visible = false;
-    form.resetFields();
-  };
+  // const modalCancel = () => {
+  //   State.current = {};
+  //   State.visible = false;
+  //   form.resetFields();
+  // };
 
-  const saveData = async () => {
-    const formData = await form.validateFields();
-    if (formData) {
-      const _data = {
-        ...formData,
-        parentId: State.current.parentId,
-      };
-      const response: any = await service.save(_data);
-      if (response.status === 200) {
-        message.success('操作成功!');
-        modalCancel();
-        pageJump(response.result.id);
-      } else {
-        message.error('操作成功!');
-      }
-    }
-  };
+  // const saveData = async () => {
+  //   const formData = await form.validateFields();
+  //   if (formData) {
+  //     const _data = {
+  //       ...formData,
+  //       parentId: State.current.parentId,
+  //     };
+  //     const response: any = await service.save(_data);
+  //     if (response.status === 200) {
+  //       message.success('操作成功!');
+  //       modalCancel();
+  //       pageJump(response.result.id);
+  //     } else {
+  //       message.error('操作成功!');
+  //     }
+  //   }
+  // };
 
   return (
     <PageContainer>
@@ -242,7 +246,7 @@ export default observer(() => {
         toolBarRender={() => [
           <Button
             onClick={() => {
-              State.visible = true;
+              pageJump();
             }}
             key="button"
             icon={<PlusOutlined />}
@@ -259,53 +263,53 @@ export default observer(() => {
           defaultMessage: '菜单列表',
         })}
       />
-      <Modal
-        title={intl.formatMessage({
-          id: State.current.parentId
-            ? 'pages.system.menu.option.addChildren'
-            : 'pages.data.option.add',
-          defaultMessage: '新增',
-        })}
-        visible={State.visible}
-        width={660}
-        onOk={saveData}
-        onCancel={modalCancel}
-      >
-        <Form form={form} labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
-          <Form.Item
-            name="code"
-            label={intl.formatMessage({
-              id: 'page.system.menu.encoding',
-              defaultMessage: '编码',
-            })}
-            required={true}
-            rules={[
-              { required: true, message: '请输入编码' },
-              { max: 64, message: '最多可输入64个字符' },
-              {
-                pattern: /^[a-zA-Z0-9`!@#$%^&*()_+\-={}|\\\]\[;':",.\/<>?]+$/,
-                message: '请输入英文+数字+特殊字符(`!@#$%^&*()_+-={}|\\][;\':",./<>?)',
-              },
-            ]}
-          >
-            <Input />
-          </Form.Item>
-          <Form.Item
-            name="name"
-            label={intl.formatMessage({
-              id: 'pages.table.name',
-              defaultMessage: '名称',
-            })}
-            required={true}
-            rules={[
-              { required: true, message: '请输入名称' },
-              { max: 64, message: '最多可输入64个字符' },
-            ]}
-          >
-            <Input />
-          </Form.Item>
-        </Form>
-      </Modal>
+      {/*<Modal*/}
+      {/*  title={intl.formatMessage({*/}
+      {/*    id: State.current.parentId*/}
+      {/*      ? 'pages.system.menu.option.addChildren'*/}
+      {/*      : 'pages.data.option.add',*/}
+      {/*    defaultMessage: '新增',*/}
+      {/*  })}*/}
+      {/*  visible={State.visible}*/}
+      {/*  width={660}*/}
+      {/*  onOk={saveData}*/}
+      {/*  onCancel={modalCancel}*/}
+      {/*>*/}
+      {/*  <Form form={form} labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>*/}
+      {/*    <Form.Item*/}
+      {/*      name="code"*/}
+      {/*      label={intl.formatMessage({*/}
+      {/*        id: 'page.system.menu.encoding',*/}
+      {/*        defaultMessage: '编码',*/}
+      {/*      })}*/}
+      {/*      required={true}*/}
+      {/*      rules={[*/}
+      {/*        { required: true, message: '请输入编码' },*/}
+      {/*        { max: 64, message: '最多可输入64个字符' },*/}
+      {/*        {*/}
+      {/*          pattern: /^[a-zA-Z0-9`!@#$%^&*()_+\-={}|\\\]\[;':",.\/<>?]+$/,*/}
+      {/*          message: '请输入英文+数字+特殊字符(`!@#$%^&*()_+-={}|\\][;\':",./<>?)',*/}
+      {/*        },*/}
+      {/*      ]}*/}
+      {/*    >*/}
+      {/*      <Input />*/}
+      {/*    </Form.Item>*/}
+      {/*    <Form.Item*/}
+      {/*      name="name"*/}
+      {/*      label={intl.formatMessage({*/}
+      {/*        id: 'pages.table.name',*/}
+      {/*        defaultMessage: '名称',*/}
+      {/*      })}*/}
+      {/*      required={true}*/}
+      {/*      rules={[*/}
+      {/*        { required: true, message: '请输入名称' },*/}
+      {/*        { max: 64, message: '最多可输入64个字符' },*/}
+      {/*      ]}*/}
+      {/*    >*/}
+      {/*      <Input />*/}
+      {/*    </Form.Item>*/}
+      {/*  </Form>*/}
+      {/*</Modal>*/}
     </PageContainer>
   );
 });

+ 3 - 0
src/pages/system/Menu/service.ts

@@ -18,6 +18,9 @@ class Service extends BaseService<MenuItem> {
     request(`${SystemConst.API_BASE}/permission/_query`, { method: 'POST', data });
 
   queryDetail = (id: string) => request(`${this.uri}/${id}`, { method: 'GET' });
+
+  // 资产类型
+  queryAssetsType = () => request(`${SystemConst.API_BASE}/asset/types`, { method: 'GET' });
 }
 
 export default Service;

+ 1 - 0
src/pages/system/Menu/typing.d.ts

@@ -59,6 +59,7 @@ export type MenuItem = {
   createTime: number;
   redirect?: string;
   children?: MenuItem[];
+  accessSupport?: { text: string; value: string };
 };
 
 /**

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

+ 5 - 0
src/style/common.less

@@ -0,0 +1,5 @@
+.ellipsis {
+  overflow: hidden;
+  white-space: nowrap; //文本不会换行
+  text-overflow: ellipsis; //文本溢出显示省略号
+}

+ 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',