Quellcode durchsuchen

feat(system): add org

Lind vor 4 Jahren
Ursprung
Commit
962a0cc597

+ 47 - 0
src/pages/system/Org/NodeTemplate/index.tsx

@@ -0,0 +1,47 @@
+import styles from '../index.less';
+import { Avatar, Dropdown } from 'antd';
+import { SmallDashOutlined, UserOutlined } from '@ant-design/icons';
+import React from 'react';
+import type { OrgItem } from '@/pages/system/Org/typings';
+
+declare type OverlayFunc = () => React.ReactElement;
+
+interface Props {
+  data: Partial<OrgItem>;
+  action: React.ReactElement | OverlayFunc;
+}
+
+const NodeTemplate: React.FC<Props> = (props) => {
+  const { data, action } = props;
+  return (
+    <div className={styles.node}>
+      <div className={styles.top}>
+        <span className={styles.title}>{data.name}</span>
+        <Avatar size="small" icon={<UserOutlined />} />
+      </div>
+
+      <div className={styles.content}>
+        <div className={styles.item}>
+          {data.code !== null && (
+            <div>
+              <span className={styles.mark}>编码</span>
+              <span>{data.code}</span>
+            </div>
+          )}
+          <div>
+            <span className={styles.mark}>下级数量</span>
+            <span>{data?.children?.length || 0}</span>
+          </div>
+        </div>
+        <div className={styles.action}>
+          <Dropdown overlay={action}>
+            <a className="ant-dropdown-link" onClick={(e) => e.preventDefault()}>
+              <SmallDashOutlined />
+            </a>
+          </Dropdown>
+        </div>
+      </div>
+    </div>
+  );
+};
+export default NodeTemplate;

+ 76 - 0
src/pages/system/Org/Save/index.tsx

@@ -0,0 +1,76 @@
+import { Modal } from 'antd';
+import { createForm } from '@formily/core';
+import { createSchemaField, observer } from '@formily/react';
+import { Form, Input, FormItem } from '@formily/antd';
+import React from 'react';
+import type { ObsModel } from '@/pages/system/Org/typings';
+
+interface Props {
+  obs: ObsModel;
+}
+
+const Save: React.FC<Props> = observer((props) => {
+  const { obs } = props;
+  const form = createForm({});
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+    },
+  });
+
+  const schema = {
+    type: 'object',
+    properties: {
+      code: {
+        title: '编码',
+        type: 'string',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-component-props': {},
+        'x-decorator-props': {},
+        name: 'id',
+        required: true,
+      },
+      name: {
+        title: '名称',
+        type: 'string',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-component-props': {},
+        'x-decorator-props': {},
+        name: 'name',
+        required: true,
+      },
+      sort: {
+        title: '名称',
+        type: 'string',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-component-props': {},
+        'x-decorator-props': {},
+        name: 'name',
+        required: true,
+      },
+      describe: {
+        title: '描述',
+        type: 'string',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input.TextArea',
+        'x-component-props': {},
+        'x-decorator-props': {},
+        name: 'describe',
+      },
+    },
+  };
+
+  return (
+    <Modal title="编辑" visible={obs.edit} onCancel={obs.closeEdit}>
+      <Form form={form} labelCol={5} wrapperCol={16}>
+        <SchemaField schema={schema} />
+      </Form>
+    </Modal>
+  );
+});
+export default Save;

+ 70 - 0
src/pages/system/Org/index.less

@@ -0,0 +1,70 @@
+.orgContainer {
+  :global {
+    .orgchart {
+      background: #fff;
+    }
+    .orgchart-container {
+      border: none;
+    }
+  }
+}
+
+.node {
+  display: flex;
+  flex-direction: column;
+  min-width: 140px;
+  border: 1px solid #4c77bf;
+  border-radius: 3px;
+  transition: 0.3s;
+}
+
+.node:hover {
+  box-shadow: 0 0 10px 10px #b1d9ff;
+}
+
+.top {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 36px;
+  padding: 0 12px;
+  color: #fff;
+  font-size: 14px;
+  background: #4c77bf;
+}
+
+.title {
+  margin-right: 12px;
+  white-space: nowrap;
+}
+
+.content {
+  display: flex;
+  flex: 1 1;
+  align-items: center;
+  justify-content: space-between;
+  padding: 3px 12px;
+  background: #fff;
+}
+
+.item > div {
+  display: flex;
+}
+
+.mark {
+  display: flex;
+  justify-content: flex-start;
+  width: 50px;
+  margin-right: 10px;
+  color: rgba(0, 0, 0, 0.45);
+  font-size: 14px;
+  white-space: nowrap;
+}
+
+.action {
+  align-self: flex-end;
+  height: 100%;
+  padding-bottom: 3px;
+  vertical-align: bottom;
+  cursor: pointer;
+}

+ 153 - 0
src/pages/system/Org/index.tsx

@@ -0,0 +1,153 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import OrganizationChart from '@dabeng/react-orgchart';
+import styles from './index.less';
+import { Menu, message } from 'antd';
+import NodeTemplate from '@/pages/system/Org/NodeTemplate';
+import { model } from '@formily/reactive';
+import { observer } from '@formily/react';
+import { useEffect } from 'react';
+import Service from '@/pages/system/Org/service';
+import encodeQuery from '@/utils/encodeQuery';
+import type { ObsModel, OrgItem } from '@/pages/system/Org/typings';
+import Save from '@/pages/system/Org/Save';
+
+const obs = model<ObsModel>({
+  edit: false,
+  parentId: '',
+  data: {},
+  current: {},
+  authorize: true,
+
+  update(data: Partial<OrgItem>) {
+    this.current = data;
+    this.edit = true;
+    this.parentId = undefined;
+  },
+
+  addNext(parentData: Partial<OrgItem>) {
+    this.parentId = parentData.id;
+    this.edit = true;
+    this.current = {};
+  },
+
+  authorized(data: Partial<OrgItem>) {
+    this.current = data;
+    this.authorize = true;
+  },
+  closeEdit() {
+    this.current = {};
+    this.edit = false;
+  },
+});
+
+const service = new Service('organization');
+const Org = observer(() => {
+  const hitCenter = () => {
+    const orgChart = document.getElementsByClassName('orgchart-container')[0];
+    const { width } = orgChart.getBoundingClientRect();
+    orgChart.scrollLeft = width;
+  };
+
+  const query = async () => {
+    const response = await service.queryTree(
+      encodeQuery({
+        paging: false,
+        terms: { typeId: 'org' },
+      }),
+    );
+    obs.data = {
+      id: null,
+      name: '机构管理',
+      title: '组织架构',
+      children: response.result,
+    };
+    hitCenter();
+    return obs;
+  };
+
+  const remove = async (id: string) => {
+    await service.remove(id);
+    message.success('操作成功');
+  };
+  useEffect(() => {
+    query();
+  }, []);
+
+  const menu = (nodeData: any) => {
+    return nodeData.id === null ? (
+      <Menu>
+        <Menu.Item>
+          <a target="_blank" rel="noopener noreferrer" onClick={() => obs.addNext(nodeData)}>
+            添加下级
+          </a>
+        </Menu.Item>
+      </Menu>
+    ) : (
+      <Menu>
+        <Menu.Item>
+          <a
+            target="_blank"
+            rel="noopener noreferrer"
+            onClick={() => {
+              // setParentId(null);
+              // setCurrent(nodeData);
+              // setEdit(true);
+            }}
+          >
+            编辑
+          </a>
+        </Menu.Item>
+        <Menu.Item>
+          <a target="_blank" rel="noopener noreferrer" onClick={() => obs.addNext(nodeData)}>
+            添加下级
+          </a>
+        </Menu.Item>
+        <Menu.Item>
+          <a
+            target="_blank"
+            rel="noopener noreferrer"
+            onClick={() => {
+              // setCurrent(nodeData);
+              // setAutzVisible(true);
+            }}
+          >
+            权限分配
+          </a>
+        </Menu.Item>
+        <Menu.Item>
+          <a
+            target="_blank"
+            rel="noopener noreferrer"
+            onClick={() => {
+              // setCurrent(nodeData);
+              // setUserVisible(true);
+            }}
+          >
+            绑定用户
+          </a>
+        </Menu.Item>
+        <Menu.Item>
+          <a target="_blank" rel="noopener noreferrer" onClick={() => remove(nodeData.id)}>
+            删除
+          </a>
+        </Menu.Item>
+      </Menu>
+    );
+  };
+  return (
+    <PageContainer>
+      <div className={styles.orgContainer}>
+        <OrganizationChart
+          datasource={obs.data}
+          pan={true}
+          NodeTemplate={(nodeData: any) => (
+            <NodeTemplate data={nodeData.nodeData} action={menu(nodeData.nodeData)} />
+          )}
+        />
+      </div>
+      <Save obs={obs} />
+    </PageContainer>
+  );
+});
+
+export default Org;

+ 11 - 0
src/pages/system/Org/service.ts

@@ -0,0 +1,11 @@
+import BaseService from '@/utils/BaseService';
+import type { OrgItem } from '@/pages/system/Org/typings';
+import { request } from '@@/plugin-request/request';
+
+class Service extends BaseService<OrgItem> {
+  queryTree(params: any): Promise<any> {
+    return request(`${this.uri}/_all/tree`, { params, method: 'GET' });
+  }
+}
+
+export default Service;

+ 32 - 0
src/pages/system/Org/typings.d.ts

@@ -0,0 +1,32 @@
+export type OrgItem = {
+  id: string;
+  parentId: string;
+  path: string;
+  sortIndex: number;
+  level: number;
+  name: string;
+  describe: string;
+  permissionExpresion: string;
+  url: string;
+  icon: string;
+  status: number;
+  code?: string;
+  children?: OrgItem[];
+};
+
+export type ObsModel = {
+  data: Partial<{
+    id: null;
+    name: string;
+    title: string;
+    children: Partial<OrgItem>[];
+  }>;
+  current: Partial<OrgItem>;
+  parentId: string | undefined;
+  edit: boolean;
+  update: (data: Partial<OrgItem>) => void;
+  addNext: (parentData: Partial<OrgItem>) => void;
+  authorize: boolean;
+  authorized: (data: Partial<OrgItem>) => void;
+  closeEdit: () => void;
+};