Explorar el Código

fix(菜单管理): 删除多余console

xieyonghong hace 3 años
padre
commit
a70f62e1f2

+ 4 - 0
package.json

@@ -71,6 +71,7 @@
     "@formily/shared": "2.0.0-rc.17",
     "@jetlinks/pro-list": "^1.10.8",
     "@jetlinks/pro-table": "^2.63.10",
+    "@types/react-syntax-highlighter": "^13.5.2",
     "@umijs/route-utils": "^1.0.36",
     "ahooks": "^2.10.9",
     "antd": "^4.18.8",
@@ -88,8 +89,11 @@
     "react-dev-inspector": "^1.1.1",
     "react-dom": "^17.0.0",
     "react-helmet-async": "^1.0.4",
+    "react-markdown": "^8.0.0",
     "react-monaco-editor": "^0.46.0",
+    "react-syntax-highlighter": "^15.4.5",
     "reconnecting-websocket": "^4.4.0",
+    "remark-gfm": "^3.0.1",
     "rxjs": "^7.2.0",
     "rxjs-websockets": "8",
     "umi": "^3.5.0",

+ 0 - 1
src/app.tsx

@@ -215,7 +215,6 @@ export function patchRoutes(routes: any) {
 }
 
 export function render(oldRender: any) {
-  console.log(history.location.pathname !== loginPath && history.location.pathname !== '/');
   if (history.location.pathname !== loginPath && history.location.pathname !== '/') {
     MenuService.queryMenuThree({ paging: false }).then((res) => {
       if (res.status === 200) {

+ 37 - 0
src/components/FRuleEditor/index.less

@@ -0,0 +1,37 @@
+.box {
+  border: 1px solid lightgray;
+
+  .top {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+    width: 100%;
+    border-bottom: 1px solid lightgray;
+
+    .left {
+      display: flex;
+      align-items: center;
+      width: 200px;
+      margin: 0 5px;
+
+      span {
+        display: inline-block;
+        height: 40px;
+        margin: 0 10px;
+        line-height: 40px;
+        cursor: pointer;
+      }
+    }
+
+    .right {
+      display: flex;
+      align-items: center;
+      width: 100px;
+      margin: 0 5px;
+
+      span {
+        margin: 0 5px;
+      }
+    }
+  }
+}

+ 137 - 0
src/components/FRuleEditor/index.tsx

@@ -0,0 +1,137 @@
+import MonacoEditor from 'react-monaco-editor';
+import styles from './index.less';
+import { Dropdown, Menu, message, Tooltip } from 'antd';
+import { BugOutlined, FullscreenOutlined, MoreOutlined, PlusOutlined } from '@ant-design/icons';
+
+const symbolList = [
+  {
+    key: 'add',
+    value: '+',
+  },
+  {
+    key: 'subtract',
+    value: '-',
+  },
+  {
+    key: 'multiply',
+    value: '*',
+  },
+  {
+    key: 'divide',
+    value: '/',
+  },
+  {
+    key: 'parentheses',
+    value: '()',
+  },
+  {
+    key: 'cubic',
+    value: '^',
+  },
+  {
+    key: 'dayu',
+    value: '>',
+  },
+  {
+    key: 'dayudengyu',
+    value: '>=',
+  },
+  {
+    key: 'dengyudengyu',
+    value: '==',
+  },
+  {
+    key: 'xiaoyudengyu',
+    value: '<=',
+  },
+  {
+    key: 'xiaoyu',
+    value: '<',
+  },
+  {
+    key: 'jiankuohao',
+    value: '<>',
+  },
+  {
+    key: 'andand',
+    value: '&&',
+  },
+  {
+    key: 'huohuo',
+    value: '||',
+  },
+  {
+    key: 'fei',
+    value: '!',
+  },
+  {
+    key: 'and',
+    value: '&',
+  },
+  {
+    key: 'huo',
+    value: '|',
+  },
+  {
+    key: 'bolang',
+    value: '~',
+  },
+];
+const FRuleEditor = () => {
+  return (
+    <div className={styles.box}>
+      <div className={styles.top}>
+        <div className={styles.left}>
+          {symbolList
+            .filter((t, i) => i <= 3)
+            .map((item) => (
+              <span key={item.key} onClick={() => message.success(`插入数据${item.value}`)}>
+                {item.value}
+              </span>
+            ))}
+          <span>
+            <Dropdown
+              overlay={
+                <Menu>
+                  {symbolList
+                    .filter((t, i) => i > 6)
+                    .map((item) => (
+                      <Menu.Item
+                        key={item.key}
+                        onClick={async () => {
+                          message.success(`选中了这个${item.value}`);
+                        }}
+                      >
+                        {item.value}
+                      </Menu.Item>
+                    ))}
+                </Menu>
+              }
+            >
+              <a className="ant-dropdown-link" onClick={(e) => e.preventDefault()}>
+                <MoreOutlined />
+              </a>
+            </Dropdown>
+          </span>
+        </div>
+        <div className={styles.right}>
+          <span>
+            <Tooltip title="进行调试">
+              <BugOutlined />
+            </Tooltip>
+          </span>
+          <span>
+            <Tooltip title="快速添加">
+              <PlusOutlined />
+            </Tooltip>
+          </span>
+          <span>
+            <FullscreenOutlined />
+          </span>
+        </div>
+      </div>
+      <MonacoEditor language={'javascript'} height={300} />
+    </div>
+  );
+};
+export default FRuleEditor;

+ 6 - 0
src/pages/device/Product/index.tsx

@@ -21,6 +21,9 @@ import ProTable from '@jetlinks/pro-table';
 import { lastValueFrom } from 'rxjs';
 import encodeQuery from '@/utils/encodeQuery';
 import Save from '@/pages/device/Product/Save';
+import Edit from '@/pages/device/components/Metadata/Base/Edit';
+// import Operator from '@/pages/device/components/Operator';
+// import Debug from '@/pages/device/components/Debug';
 
 export const service = new Service('device-product');
 export const statusMap = {
@@ -206,6 +209,9 @@ const Product = observer(() => {
         }}
         visible={visible}
       />
+      <Edit type={'product'} />
+      {/*<Operator />*/}
+      {/*<Debug />*/}
     </PageContainer>
   );
 });

+ 75 - 0
src/pages/device/components/Debug/index.less

@@ -0,0 +1,75 @@
+.container {
+  display: flex;
+  width: 100%;
+  height: 340px;
+
+  .left {
+    flex: auto;
+    border: 1px solid lightgray;
+
+    .header {
+      display: flex;
+      align-items: center;
+      width: 100%;
+      height: 40px;
+      border-bottom: 1px solid lightgray;
+      //justify-content: space-around;
+
+      div {
+        display: flex;
+        //width: 100%;
+        align-items: center;
+        justify-content: flex-start;
+        height: 100%;
+
+        .title {
+          margin: 0 10px;
+          font-weight: 600;
+          font-size: 16px;
+        }
+
+        .description {
+          margin-left: 10px;
+          color: lightgray;
+          font-size: 12px;
+        }
+      }
+
+      .action {
+        width: 150px;
+        font-size: 14px;
+      }
+    }
+  }
+
+  .right {
+    flex: auto;
+    border: 1px solid lightgray;
+    border-left: none;
+
+    .header {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      width: 100%;
+      height: 40px;
+      border-bottom: 1px solid lightgray;
+
+      .title {
+        display: flex;
+
+        div {
+          margin: 0 10px;
+        }
+      }
+
+      .action {
+        display: flex;
+
+        div {
+          margin: 0 10px;
+        }
+      }
+    }
+  }
+}

+ 122 - 0
src/pages/device/components/Debug/index.tsx

@@ -0,0 +1,122 @@
+import { Modal } from 'antd';
+import styles from './index.less';
+import { createSchemaField, FormProvider } from '@formily/react';
+import { ArrayTable, FormItem, Input, Select } from '@formily/antd';
+import { useMemo } from 'react';
+import { createForm } from '@formily/core';
+import type { ISchema } from '@formily/json-schema';
+
+const Debug = () => {
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Select,
+      ArrayTable,
+    },
+  });
+  const form = useMemo(() => createForm(), []);
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      array: {
+        type: 'array',
+        'x-decorator': 'FormItem',
+        'x-component': 'ArrayTable',
+        'x-component-props': {
+          pagination: false,
+        },
+        items: {
+          type: 'object',
+          properties: {
+            column1: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': {
+                title: '字段1',
+              },
+              properties: {
+                t1: {
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Input',
+                },
+              },
+            },
+            column2: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': {
+                title: '字段2',
+              },
+              properties: {
+                t2: {
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Input',
+                },
+              },
+            },
+            column3: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': {
+                title: '字段3',
+              },
+              properties: {
+                t2: {
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Input',
+                },
+              },
+            },
+          },
+        },
+        properties: {
+          add: {
+            type: 'void',
+            'x-component': 'ArrayTable.Addition',
+            title: '添加条目',
+          },
+        },
+      },
+    },
+  };
+  return (
+    <Modal visible width="40vw" title="debug">
+      <div className={styles.container}>
+        <div className={styles.left}>
+          <div className={styles.header}>
+            <div>
+              <div className={styles.title}>
+                属性赋值
+                <div className={styles.description}>请对上方规则使用的属性进行赋值</div>
+              </div>
+            </div>
+          </div>
+          <FormProvider form={form}>
+            <SchemaField schema={schema} />
+          </FormProvider>
+        </div>
+        <div className={styles.right}>
+          <div className={styles.header}>
+            <div className={styles.title}>
+              <div>运行结果</div>
+            </div>
+
+            <div className={styles.action}>
+              <div>
+                <a>开始运行</a>
+                {/*<a>停止运行</a>*/}
+              </div>
+              <div>
+                <a>清空</a>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </Modal>
+  );
+};
+
+export default Debug;

+ 2 - 1
src/pages/device/components/Metadata/Base/Edit/index.tsx

@@ -600,7 +600,8 @@ const Edit = (props: Props) => {
       }
     >
       <Form form={form} layout="vertical" size="small">
-        <SchemaField schema={metadataTypeMapping[MetadataModel.type].schema} />
+        <SchemaField schema={metadataTypeMapping.properties.schema} />
+        {/*<SchemaField schema={metadataTypeMapping[MetadataModel.type].schema} />*/}
       </Form>
     </Drawer>
   );

+ 38 - 0
src/pages/device/components/Operator/index.less

@@ -0,0 +1,38 @@
+.border {
+  margin-top: 10px;
+  padding: 10px;
+  border-top: 1px solid lightgray;
+}
+
+.box {
+  width: 100%;
+
+  .explain {
+    .border;
+  }
+
+  .tree {
+    .border;
+
+    height: 350px;
+    overflow-y: auto;
+
+    .node {
+      display: flex;
+      justify-content: space-between;
+      width: 220px;
+
+      .add {
+        display: none;
+      }
+
+      &:hover .add {
+        display: block;
+      }
+
+      .parent {
+        display: none;
+      }
+    }
+  }
+}

+ 83 - 0
src/pages/device/components/Operator/index.tsx

@@ -0,0 +1,83 @@
+import { Input, Modal, Tree } from 'antd';
+import Service from '@/pages/device/components/Operator/service';
+import { useEffect, useRef, useState } from 'react';
+import styles from './index.less';
+import ReactMarkdown from 'react-markdown';
+import { treeFilter } from '@/utils/tree';
+import type { OperatorItem } from '@/pages/device/components/Operator/typings';
+
+const service = new Service();
+
+interface Props {
+  onChange: (value: any) => void;
+  data: any;
+}
+
+const Operator = (props: Props) => {
+  const [data, setData] = useState<OperatorItem[]>([]);
+  const [item, setItem] = useState<Partial<OperatorItem>>({});
+  const dataRef = useRef<OperatorItem[]>([]);
+  const getData = async () => {
+    // TODO 从物模型中获取属性数据
+    const properties = {
+      id: 'property',
+      name: '属性',
+      description: '',
+      code: '',
+      children: [
+        {
+          id: 'test',
+          name: '测试数据',
+        },
+      ],
+    };
+    const response = await service.getOperator();
+    if (response.status === 200) {
+      setData([properties, ...response.result]);
+      dataRef.current = [properties, ...response.result];
+    }
+  };
+  useEffect(() => {
+    getData();
+  }, []);
+
+  const search = (value: string) => {
+    if (value) {
+      const nodes = treeFilter(dataRef.current, value, 'name') as OperatorItem[];
+      setData(nodes);
+    } else {
+      setData(dataRef.current);
+    }
+  };
+  return (
+    <Modal visible={true} title="快速添加">
+      <div className={styles.box}>
+        <Input.Search onSearch={search} allowClear placeholder="搜索关键字" />
+        <Tree
+          className={styles.tree}
+          onSelect={(k, info) => {
+            setItem(info.node as unknown as OperatorItem);
+          }}
+          fieldNames={{
+            title: 'name',
+            key: 'id',
+          }}
+          titleRender={(node) => (
+            <div className={styles.node}>
+              <div>{node.name}</div>
+              <div className={node.children?.length > 0 ? styles.parent : styles.add}>
+                <a onClick={() => props.onChange(item.code)}>添加</a>
+              </div>
+            </div>
+          )}
+          autoExpandParent={true}
+          treeData={data}
+        />
+        <div className={styles.explain}>
+          <ReactMarkdown>{item.description || ''}</ReactMarkdown>
+        </div>
+      </div>
+    </Modal>
+  );
+};
+export default Operator;

+ 11 - 0
src/pages/device/components/Operator/service.ts

@@ -0,0 +1,11 @@
+import BaseService from '@/utils/BaseService';
+import type { ProductItem } from '@/pages/device/Product/typings';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<ProductItem> {
+  public getOperator = () =>
+    request(`/${SystemConst.API_BASE}/property-calculate-rule/description`, { method: 'GET' });
+}
+
+export default Service;

+ 10 - 0
src/pages/device/components/Operator/typings.d.ts

@@ -0,0 +1,10 @@
+import type { TreeNode } from '@/utils/tree';
+
+interface OperatorItem extends TreeNode {
+  id: string;
+  name: string;
+  key: string;
+  description: string;
+  code: string;
+  children: OperatorItem[];
+}

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

@@ -0,0 +1,126 @@
+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.queryDimensionsList(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;

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

@@ -0,0 +1,188 @@
+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;

+ 16 - 0
src/pages/system/Role/Edit/Permission/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;
+    }
+  }
+}

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

@@ -0,0 +1,259 @@
+import { useEffect, useState } from 'react';
+import { Button, Card, message, Steps } from 'antd';
+import Service from '@/pages/system/Role/service';
+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 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;
+        }
+      });
+    }
+    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);
+        return {
+          id: item.id,
+          check: check,
+          indeterminate: (ilen > 0 || item?.buttons?.length > 0) && !check,
+          buttons: _.map(item.buttons || [], 'id') || [],
+          children: initToPermission(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);
+              }}
+            >
+              上一步
+            </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>
+  );
+};
+export default Permission;

+ 9 - 9
src/pages/system/Role/Edit/index.tsx

@@ -3,7 +3,7 @@ import { PageContainer } from '@ant-design/pro-layout';
 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 Permission from '@/pages/system/Role/Edit/Permission';
 import Info from '@/pages/system/Role/Edit/Info';
 import { useIntl } from '@@/plugin-locale/localeExports';
 
@@ -20,14 +20,14 @@ const RoleEdit = observer(() => {
       }),
       component: <Info />,
     },
-    // {
-    //     key: 'permission',
-    //     tab: intl.formatMessage({
-    //         id: 'pages.system.role.access.permission',
-    //         defaultMessage: '权限分配',
-    //     }),
-    //     component: <Permission />,
-    // },
+    {
+      key: 'permission',
+      tab: intl.formatMessage({
+        id: 'pages.system.role.access.permission',
+        defaultMessage: '权限分配',
+      }),
+      component: <Permission />,
+    },
     {
       key: 'userManagement',
       tab: intl.formatMessage({

+ 61 - 0
src/pages/system/Role/service.ts

@@ -14,6 +14,67 @@ class Service extends BaseService<RoleItem> {
         }),
       ),
     ).pipe(map((item) => item));
+  queryMenuTreeList = (data: any) =>
+    defer(() =>
+      from(
+        request(`/${SystemConst.API_BASE}/menu/_all/tree`, {
+          method: 'POST',
+          data,
+        }),
+      ),
+    ).pipe(map((item) => item));
+  queryPermissionsList = (data: any) =>
+    defer(() =>
+      from(
+        request(`/${SystemConst.API_BASE}/menu/permissions`, {
+          method: 'POST',
+          data,
+        }),
+      ),
+    ).pipe(map((item) => item));
+  queryAssetTypeList = (data: any) =>
+    defer(() =>
+      from(
+        request(`/${SystemConst.API_BASE}/menu/asset-types`, {
+          method: 'POST',
+          data,
+        }),
+      ),
+    ).pipe(map((item) => item));
+  queryDimensionsList = (type: string) =>
+    defer(() =>
+      from(
+        request(`/${SystemConst.API_BASE}/asset/${type}/dimensions`, {
+          method: 'GET',
+        }),
+      ),
+    ).pipe(map((item) => item));
+  queryGrantTree = (targetType: string, targetId: string) =>
+    defer(() =>
+      from(
+        request(`/${SystemConst.API_BASE}/menu/${targetType}/${targetId}/_grant/tree`, {
+          method: 'GET',
+        }),
+      ),
+    ).pipe(map((item) => item));
+  saveGrantTree = (targetType: string, targetId: string, data: any) =>
+    defer(() =>
+      from(
+        request(`/${SystemConst.API_BASE}/menu/${targetType}/${targetId}/_grant`, {
+          method: 'PUT',
+          data,
+        }),
+      ),
+    ).pipe(map((item) => item));
+  saveAutz = (data: any) =>
+    defer(() =>
+      from(
+        request(`/${SystemConst.API_BASE}/autz-setting/detail/_save`, {
+          method: 'POST',
+          data,
+        }),
+      ),
+    ).pipe(map((item) => item));
   bindUser = (roleId: string, params: any) =>
     defer(() =>
       from(

+ 1 - 0
src/pages/system/Role/typings.d.ts

@@ -5,4 +5,5 @@ type RoleItem = {
   path: string;
   sortIndex: number;
   description: string;
+  dataAccess?: any;
 };

+ 117 - 0
src/utils/tree.ts

@@ -0,0 +1,117 @@
+/**
+ 场景
+ 树形数据过滤, 并保留原有树形结构不变, 即如果有子集被选中,父级同样保留。
+ 思路
+ 对数据进行处理,根据过滤标识对匹配的数据添加标识。如visible:true
+ 对有标识的子集的父级添加标识visible:true
+ 根据visible标识对数据进行递归过滤,得到最后的数据
+ */
+
+import _ from 'lodash';
+
+export type TreeNode = {
+  id: string;
+  name: string;
+  children: TreeNode[];
+  visible?: boolean;
+} & Record<string, any>;
+
+/*
+ *	对表格数据进行处理
+ *	data 树形数据数组
+ *	filter 过滤参数值
+ *	filterType 过滤参数名
+ */
+export function treeFilter(data: TreeNode[], filter: string, filterType: string): TreeNode[] {
+  const _data = _.cloneDeep(data);
+  const traverse = (item: TreeNode[]) => {
+    item.forEach((child) => {
+      child.visible = filterMethod(filter, child, filterType);
+      if (child.children) traverse(child.children);
+      if (!child.visible && child.children?.length) {
+        const visible = !child.children.some((c) => c.visible);
+        child.visible = !visible;
+      }
+    });
+  };
+  traverse(_data);
+  return filterDataByVisible(_data);
+}
+
+// 根据传入的值进行数据匹配, 并返回匹配结果
+function filterMethod(val: string, data: TreeNode, filterType: string | number) {
+  return data[filterType].includes(val);
+}
+
+// 递归过滤符合条件的数据
+function filterDataByVisible(data: TreeNode[]) {
+  return data.filter((item) => {
+    if (item.children) {
+      item.children = filterDataByVisible(item.children);
+    }
+    return item.visible;
+  });
+}
+
+const mockData = [
+  {
+    children: [
+      {
+        children: [],
+        name: '加',
+        id: 'operator-1',
+      },
+      {
+        children: [],
+        name: '减',
+        id: 'operator-2',
+      },
+      {
+        children: [],
+        name: '乘',
+        id: 'operator-3',
+      },
+      {
+        children: [],
+        name: '除',
+        id: 'operator-4',
+      },
+      {
+        children: [],
+        name: '括号',
+        id: 'operator-5',
+      },
+      {
+        children: [],
+        name: '按位异或',
+        id: 'operator-6',
+      },
+    ],
+    name: '操作符',
+    id: 'operator',
+  },
+  {
+    children: [
+      {
+        children: [],
+        name: 'if',
+        id: 'if',
+      },
+      {
+        children: [],
+        name: 'for',
+        id: 'for',
+      },
+      {
+        children: [],
+        name: 'while',
+        id: 'while',
+      },
+    ],
+    name: '控制语句',
+    id: 'control',
+  },
+];
+const myTree = treeFilter(mockData, '操作', 'name');
+
+console.log(JSON.stringify(myTree), 'mytree');