core.tsx 9.46 KB
import * as React from 'react';
import { useCallback, useEffect, useImperativeHandle, useState } from 'react';
import { Empty, Input, Spin, Tooltip, Tree } from 'antd';
import './core.less';
import { getOrgTree } from './service';
import { cloneDeep } from 'lodash-es';
import { PartitionOutlined } from '@ant-design/icons';
import {QxBaseIcon} from '@qx/common';

type OrgCoreProps = {
  cRef?: any;
  max?: number;
  multiple?: boolean;
  params?: any;
  placeholder?: string;
  showLevel?: number;
  hasInclude?: boolean;
  checkStrictly?: boolean;
  selectFirstNode?: boolean;
  onSelect?: (selectedKeys: string[], selectedData: any[], include?: boolean) => void;
  request: any;
};

interface OrgModel {
  name: string;
  id: string;
  code: string;
  pid: string;
  visible?: boolean;
  disabled?: boolean;
  children?: OrgModel[];
}

const IncludeNode: React.FC<{ onChange: (include: boolean) => void }> = (props) => {
  const [include, setInclude] = useState(true);
  return include ? (
    <Tooltip title={'不包含子类'} getPopupContainer={(triggerNode) => triggerNode}>
      <a
        className={'qx-org-tree__include ant-typography'}
        onClick={(e) => {
          e.stopPropagation();
          props.onChange(!include);
          setInclude(!include);
        }}
      >
        {/*<PartitionOutlined className={` ${include ? 'active' : ''}`} />*/}
        <PartitionOutlined />
      </a>
    </Tooltip>
  ) : (
    <Tooltip title={'包含子类'} getPopupContainer={(triggerNode) => triggerNode}>
      <span
        className={'qx-org-tree__include'}
        onClick={(e) => {
          e.stopPropagation();
          props.onChange(!include);
          setInclude(!include);
        }}
      >
        {/*<PartitionOutlined className={` ${include ? 'active' : ''}`} />*/}
        <PartitionOutlined />
      </span>
    </Tooltip>
  );
};

const OrgCore: React.FC<OrgCoreProps> = (props) => {
  const [loading, setLoading] = useState<boolean>(true); //请求loading

  const [data, setData] = useState<OrgModel>(); //存储原始数据
  const [treeData, setTreeData] = useState<any[]>([]); //存储树节点数据
  const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
  const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
  const [selectedData, setSelectedData] = useState<OrgModel[]>([]);

  const [keywords, setKeywords] = useState<string>('');

  const [include, setInclude] = useState(true);

  useImperativeHandle(props.cRef, () => ({
    // 暴露给父组件
    remove: (index: number) => {
      let _selectedKeys: string[] = [];
      let _selectedData: OrgModel[] = [];
      if (selectedKeys && selectedKeys.length > 0) {
        _selectedKeys = [...selectedKeys];
        _selectedData = [...selectedData];
        _selectedKeys.splice(index, 1);
        _selectedData.splice(index, 1);
        setSelectedData(_selectedData);
        setSelectedKeys(_selectedKeys);
      }
    },
    emptySelect: () => {
      setSelectedData([]);
      setSelectedKeys([]);
    },
    setSelected: (ids: string[], users?: any[]) => {
      setSelectedKeys(ids);
      if (users) {
        setSelectedData(users);
      }
    },
  }));

  const onSelect = props.onSelect ? props.onSelect : () => {};

  const generateTreeData = useCallback((_data: OrgModel[], _keywords?: string): OrgModel[] => {
    const _treeNode: any[] = [];
    _data.map((item) => {
      if (typeof item.visible === 'boolean' && !item.visible) {
        return;
      }
      const _item: OrgModel = {
        ...item,
        children: [],
      };
      if (item.children) {
        _item.children = generateTreeData(item.children, _keywords);
      }
      _treeNode.push(_item);
    });
    return _treeNode;
  }, []);

  const getFirstNode = (nodes) => {
    if (!nodes.disabled) {
      return nodes;
    }
    if (nodes.children) {
      for (let i = 0; i < nodes.children.length; i++) {
        if (getFirstNode(nodes.children[i])) {
          return getFirstNode(nodes.children[i]);
        }
      }
    }
    return null;
  };

  const requestOrg = useCallback(() => {
    setLoading(true);
    getOrgTree(props.request, props.params || [])
      .then((res: OrgModel) => {
        if (res) {
          //默认展开三级
          const _expandedKeys = [];
          const firstNode = getFirstNode(res);

          if (firstNode) {
            _expandedKeys.push(firstNode.code);
          }

          if (res.children) {
            res.children.map((item) => {
              _expandedKeys.push(item.code);
            });
          }

          if (props.selectFirstNode && firstNode.code) {
            onSelect([firstNode.code], [{ id: firstNode.code, name: firstNode.name }], include);
            setSelectedKeys([firstNode.code]);
          } else {
            // onSelect([], [], include);
            // setSelectedKeys([]);
          }

          setExpandedKeys(_expandedKeys);
          setData(res);
          setTreeData([res]);

          setLoading(false);
        }
      })
      .catch(() => {
        onSelect([], [], include);
        setSelectedKeys([]);

        setExpandedKeys([]);
        setTreeData([]);

        setLoading(false);
      });
  }, []);

  useEffect(() => {
    requestOrg();
  }, [requestOrg]);

  const handleSelect = (_selectedKeys: any[]) => {
    if (props.multiple) {
      return;
    }
    if (_selectedKeys && _selectedKeys.length > 0) {
      // @ts-ignore
      setSelectedKeys(_selectedKeys);
      onSelect(_selectedKeys, [], include);
    }
  };

  const handleMultiSelect = (check: any, e: any) => {
    //e :{ checked: boolean; checkedNodes: [] }
    const keys: string[] = check.checked;
    //TODO 单选多选处理方式不一样
    //console.log(props.max, keys, check, e);
    const count = keys.length;
    let _selectKeys = [];
    let _selectData = [];
    if (props.max === 1 && count > 0) {
      _selectKeys = [keys[count - 1]];
      _selectData = [e.checkedNodes[count - 1]];
    } else {
      _selectKeys = [...keys];
      _selectData = [...e.checkedNodes];
    }
    setSelectedKeys(_selectKeys);
    setSelectedData(_selectData);

    onSelect(
      _selectKeys,
      _selectData.map((item) => {
        return { id: item.code, name: item.name };
      }),
      include,
    );
  };

  const filter = (word: string) => {
    setKeywords(word);
    const traverse = function (node: OrgModel) {
      const childNodes = node.children || [];

      if (node.name.indexOf(word) > -1) {
        node.visible = true;
      }
      childNodes.forEach((child) => {
        child.visible = child.name.indexOf(word) > -1;

        traverse(child);
      });

      if (!node.visible && childNodes.length) {
        node.visible = childNodes.some((child) => child.visible);
      }
    };

    if (data) {
      const _data = cloneDeep(data);
      if (word != '') {
        traverse(_data);
      }

      setTreeData(generateTreeData([_data], word));
    }
  };

  const handleSearch = (e: React.KeyboardEvent<HTMLInputElement>) => {
    e.stopPropagation();
    // @ts-ignore
    filter(e.target.value.trim());
  };
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // @ts-ignore
    if (e.type === 'click' && e.target.value === '' && data) {
      //如果是清空
      filter('');
    }
  };

  const renderTitle = (nodeData: any) => {
    let title = nodeData.name;
    if (keywords) {
      const index = title.indexOf(keywords);
      if (index > -1) {
        title = (
          <>
            {title.substr(0, index)}
            <span className={'qx-keywords-highlight'}>{keywords}</span>
            {title.substr(index + keywords.length)}
          </>
        );
      }
    }
    if (nodeData.pid === '*' && props.hasInclude) {
      return (
        <div>
          {title}{' '}
          <IncludeNode
            onChange={(value: boolean) => {
              setInclude(value);
              onSelect(selectedKeys, selectedData, value);
            }}
          />
        </div>
      );
    }
    return title;
  };

  return (
    <div className={'qx-search-tree__wrap'}>
      <Input
        className={'qx-selector-sub-search'}
        placeholder={props.placeholder || '请输入部门名称,按回车键搜索'}
        allowClear
        prefix={<QxBaseIcon type={'icon-app-search-line'} />}
        onChange={(e) => {
          handleChange(e);
        }}
        onPressEnter={(e) => {
          handleSearch(e);
        }}
      />
      <div className={`qx-search-tree ${props.max === 1 ? 'qx-search-tree--radio' : null}`}>
        {!loading ? (
          treeData.length > 0 ? (
            <Tree
              blockNode
              checkable={props.multiple}
              fieldNames={{
                title: 'name',
                key: 'code',
                children: 'children',
              }}
              titleRender={(nodeData) => renderTitle(nodeData)}
              defaultExpandedKeys={expandedKeys}
              checkStrictly={props.checkStrictly}
              selectedKeys={props.multiple ? [] : selectedKeys}
              treeData={treeData}
              onSelect={handleSelect}
              onCheck={handleMultiSelect}
              checkedKeys={props.multiple ? selectedKeys : []}
              selectable={!props.multiple}
            />
          ) : (
            <Empty style={{ paddingTop: '30px' }} />
          )
        ) : null}
      </div>
      <Spin
        style={{ width: '100%', marginTop: '40px' }}
        spinning={loading}
        // indicator={<LoadingOutlined style={{ fontSize: 24, marginTop: '40px' }} spin />}
      />
    </div>
  );
};

export default OrgCore;