Commit a184fb354a8146c13bb07c1a02029673b70634a5

Authored by 李婷
1 parent 2ffa0e05

feat: 选人选部门

  1 +import * as React from 'react';
  2 +import { useCallback, useEffect, useImperativeHandle, useState } from 'react';
  3 +import { Checkbox, Empty, Input, Spin } from 'antd';
  4 +import Menu from 'antd/es/menu';
  5 +import { SearchOutlined } from '@ant-design/icons';
  6 +import { getGroups } from './service';
  7 +import _ from 'lodash';
  8 +
  9 +type GroupCoreProps = {
  10 + appId?: string;
  11 + cRef?: any;
  12 + multiple?: boolean;
  13 + placeholder?: string;
  14 + params?: any;
  15 + onSelect?: (selectedKeys: string[], selectedData: GroupModel[]) => void;
  16 + request: any;
  17 +};
  18 +
  19 +export interface GroupModel {
  20 + id: string;
  21 + name: string;
  22 + appId?: string;
  23 + code: string;
  24 + categoryId: string;
  25 + visible?: boolean;
  26 + groupList: GroupModel[];
  27 +}
  28 +
  29 +const GroupSelCore: React.FC<GroupCoreProps> = ({
  30 + appId,
  31 + cRef,
  32 + multiple,
  33 + placeholder,
  34 + onSelect,
  35 + ...props
  36 +}) => {
  37 + const [loading, setLoading] = useState<boolean>(true);
  38 +
  39 + const [data, setData] = useState<GroupModel[]>([]);
  40 +
  41 + const [selectedData, setSelectedData] = useState<GroupModel[]>([]);
  42 + const [selectedKeys, setSelectedKeys] = useState<string[]>();
  43 + const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
  44 + const [keywords, setKeywords] = useState<string>('');
  45 +
  46 + const requestData = useCallback(() => {
  47 + setLoading(true);
  48 + // todo 当前为指定appCode,待接口修改为appId传入(不用appId了)
  49 + getGroups(props.request).then((res) => {
  50 + const groups: GroupModel[] = res;
  51 + let _selectKey = '';
  52 + if (groups) {
  53 + const _expandedKeys: string[] = [];
  54 + groups.forEach((item) => {
  55 + _expandedKeys.push(item.id);
  56 + if (!_selectKey && item.groupList && item.groupList.length > 0) {
  57 + _selectKey = item.groupList[0].id;
  58 + }
  59 + });
  60 + if (_selectKey && !multiple) {
  61 + setSelectedKeys([_selectKey]);
  62 + }
  63 + setExpandedKeys(_expandedKeys);
  64 + setData(groups);
  65 + setLoading(false);
  66 + }
  67 + });
  68 + }, []);
  69 +
  70 + useEffect(() => {
  71 + requestData();
  72 + }, [requestData]);
  73 +
  74 + useEffect(() => {
  75 + const isOnSelect = onSelect ? onSelect : () => {};
  76 + if (selectedKeys) {
  77 + isOnSelect(selectedKeys, selectedData);
  78 + }
  79 + }, [onSelect, selectedKeys, selectedData]);
  80 +
  81 + const handleSelect = (selectData: { selectedKeys: string[] }) => {
  82 + //单选走这里
  83 + if (!multiple) {
  84 + setSelectedKeys(selectData.selectedKeys);
  85 + }
  86 + };
  87 + useImperativeHandle(cRef, () => ({
  88 + // 暴露给父组件
  89 + remove: (index: number) => {
  90 + let _selectedKeys: string[] = [];
  91 + let _selectedData: GroupModel[] = [];
  92 + if (selectedKeys && selectedKeys.length > 0) {
  93 + _selectedKeys = [...selectedKeys];
  94 + _selectedData = [...selectedData];
  95 + _selectedKeys.splice(index, 1);
  96 + _selectedData.splice(index, 1);
  97 + setSelectedData(_selectedData);
  98 + setSelectedKeys(_selectedKeys);
  99 + }
  100 + },
  101 + emptySelect: () => {
  102 + setSelectedData([]);
  103 + setSelectedKeys([]);
  104 + },
  105 + }));
  106 + const handleMultiSelect = (checked: boolean, item: GroupModel) => {
  107 + let _selectedKeys: string[] = [];
  108 + let _selectedData: GroupModel[] = [];
  109 + if (selectedKeys) {
  110 + _selectedKeys = [...selectedKeys];
  111 + _selectedData = [...selectedData];
  112 + }
  113 +
  114 + if (checked) {
  115 + _selectedKeys.push(item.id);
  116 + _selectedData.push(item);
  117 + } else {
  118 + const index = _selectedKeys.indexOf(item.id);
  119 + if (index > -1) {
  120 + _selectedKeys.splice(index, 1);
  121 + _selectedData.splice(index, 1);
  122 + }
  123 + }
  124 +
  125 + setSelectedData(_selectedData);
  126 + setSelectedKeys(_selectedKeys);
  127 + };
  128 +
  129 + //多选走这里
  130 + const filter = (word: string) => {
  131 + setKeywords(word);
  132 + const traverse = function (node: any) {
  133 + const childNodes = node.groupList || [];
  134 +
  135 + childNodes.forEach((child) => {
  136 + child.visible = child.name.indexOf(word) > -1;
  137 +
  138 + traverse(child);
  139 + });
  140 +
  141 + if (!node.visible && childNodes.length) {
  142 + node.visible = childNodes.some((child) => child.visible);
  143 + }
  144 + };
  145 +
  146 + if (data) {
  147 + const _data = _.cloneDeep(data);
  148 + _data.forEach((item) => {
  149 + traverse(item);
  150 + });
  151 + setData(_data);
  152 + }
  153 + };
  154 +
  155 + const handleSearch = (e: React.KeyboardEvent<HTMLInputElement>) => {
  156 + e.stopPropagation();
  157 + // @ts-ignore
  158 + filter(e.target.value.trim());
  159 + };
  160 + const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  161 + // @ts-ignore
  162 + if (e.type === 'click' && e.target.value === '' && data) {
  163 + //如果是清空
  164 + filter('');
  165 + }
  166 + };
  167 +
  168 + const renderText = (text: string) => {
  169 + let title = <> {text}</>;
  170 + if (keywords) {
  171 + const index = text.indexOf(keywords);
  172 + if (index > -1) {
  173 + title = (
  174 + <>
  175 + {text.substr(0, index)}
  176 + <span className={'qx-keywords-highlight'}>{keywords}</span>
  177 + {text.substr(index + keywords.length)}
  178 + </>
  179 + );
  180 + }
  181 + }
  182 + return title;
  183 + };
  184 +
  185 + return (
  186 + <div className={'qx-search-menus__wrap'}>
  187 + <Input
  188 + // style={{ display: 'none' }}
  189 + className={'qx-selector-sub-search'}
  190 + placeholder={placeholder || '请输入群组名称,按回车键搜索'}
  191 + allowClear
  192 + prefix={<SearchOutlined />}
  193 + onChange={(e) => {
  194 + handleChange(e);
  195 + }}
  196 + onPressEnter={(e) => {
  197 + handleSearch(e);
  198 + }}
  199 + />
  200 + <div className="qx-search-menus">
  201 + {expandedKeys.length ? (
  202 + <Menu
  203 + mode={'inline'}
  204 + onSelect={handleSelect}
  205 + selectedKeys={multiple ? [] : selectedKeys}
  206 + multiple={!!multiple}
  207 + defaultOpenKeys={expandedKeys}
  208 + >
  209 + {data.map((item) => {
  210 + if (typeof item.visible === 'boolean' && !item.visible) {
  211 + return null;
  212 + }
  213 + if (item.groupList) {
  214 + return (
  215 + <Menu.SubMenu key={item.id} title={item.name}>
  216 + {item.groupList.map((child) => {
  217 + return typeof child.visible === 'boolean' && !child.visible ? null : (
  218 + <Menu.Item key={child.id}>
  219 + {multiple ? (
  220 + <Checkbox
  221 + checked={selectedKeys && selectedKeys.indexOf(child.id) > -1}
  222 + onChange={(e) => {
  223 + handleMultiSelect(e.target.checked, child);
  224 + }}
  225 + >
  226 + {renderText(child.name)}
  227 + </Checkbox>
  228 + ) : (
  229 + renderText(child.name)
  230 + )}
  231 + </Menu.Item>
  232 + );
  233 + })}
  234 + </Menu.SubMenu>
  235 + );
  236 + }
  237 + return null;
  238 + })}
  239 + </Menu>
  240 + ) : null}
  241 +
  242 + <Spin
  243 + spinning={loading}
  244 + style={{ width: '100%', marginTop: '40px' }}
  245 + // indicator={<LoadingOutlined style={{ fontSize: 24, marginTop: '40px' }} spin />}
  246 + />
  247 + {!loading && data.length === 0 ? <Empty style={{ paddingTop: '30px' }} /> : null}
  248 + </div>
  249 + </div>
  250 + );
  251 +};
  252 +
  253 +export default GroupSelCore;
... ...
  1 +
  2 +/**
  3 + * 获取群组
  4 + */
  5 +export function getGroups(request: any) {
  6 + return request.post(`/qx-apaas-uc/selectUser/getGroups`);
  7 +}
... ...
  1 +import React from 'react';
  2 +import QxOrgSelectorInput from './src/input';
  3 +import QxOrgSelectorDialog from './src/dialog';
  4 +
  5 +interface QxOrgSelectorType extends React.FC {
  6 + Dialog: typeof QxOrgSelectorDialog;
  7 +}
  8 +
  9 +export const QxOrgSelector = QxOrgSelectorInput as QxOrgSelectorType;
  10 +
  11 +QxOrgSelector.Dialog = QxOrgSelectorDialog;
... ...
  1 +.qx-search-tree__wrap {
  2 + height: 100%;
  3 + overflow: auto;
  4 +}
  5 +
  6 +.qx-search-tree {
  7 + padding: 0 0 0 10px;
  8 +
  9 + .ant-tree .ant-tree-node-content-wrapper.ant-tree-node-selected {
  10 + // TODO 主题色更改 颜色暂时写死
  11 + color: #1764ff;
  12 + background-color: #e6f7ff;
  13 + }
  14 +}
  15 +
  16 +.qx-search-tree--radio {
  17 + .ant-tree-checkbox-inner {
  18 + border-radius: 50%;
  19 + }
  20 +}
... ...
  1 +import * as React from 'react';
  2 +import { useCallback, useEffect, useImperativeHandle, useState } from 'react';
  3 +import { Empty, Input, Spin, Tooltip, Tree } from 'antd';
  4 +import './core.less';
  5 +import { getOrgTree } from './service';
  6 +import _ from 'lodash';
  7 +import { PartitionOutlined, SearchOutlined } from '@ant-design/icons';
  8 +
  9 +type OrgCoreProps = {
  10 + cRef?: any;
  11 + max?: number;
  12 + multiple?: boolean;
  13 + params?: any;
  14 + placeholder?: string;
  15 + showLevel?: number;
  16 + hasInclude?: boolean;
  17 + checkStrictly?: boolean;
  18 + selectFirstNode?: boolean;
  19 + onSelect?: (selectedKeys: string[], selectedData: any[], include?: boolean) => void;
  20 + request: any;
  21 +};
  22 +
  23 +interface OrgModel {
  24 + name: string;
  25 + id: string;
  26 + code: string;
  27 + pid: string;
  28 + visible?: boolean;
  29 + disabled?: boolean;
  30 + children?: OrgModel[];
  31 +}
  32 +
  33 +const IncludeNode: React.FC<{ onChange: (include: boolean) => void }> = (props) => {
  34 + const [include, setInclude] = useState(true);
  35 + return include ? (
  36 + <Tooltip title={'不包含子类'} getPopupContainer={(triggerNode) => triggerNode}>
  37 + <a
  38 + className={'qx-org-tree__include ant-typography'}
  39 + onClick={(e) => {
  40 + e.stopPropagation();
  41 + props.onChange(!include);
  42 + setInclude(!include);
  43 + }}
  44 + >
  45 + {/*<PartitionOutlined className={` ${include ? 'active' : ''}`} />*/}
  46 + <PartitionOutlined />
  47 + </a>
  48 + </Tooltip>
  49 + ) : (
  50 + <Tooltip title={'包含子类'} getPopupContainer={(triggerNode) => triggerNode}>
  51 + <span
  52 + className={'qx-org-tree__include'}
  53 + onClick={(e) => {
  54 + e.stopPropagation();
  55 + props.onChange(!include);
  56 + setInclude(!include);
  57 + }}
  58 + >
  59 + {/*<PartitionOutlined className={` ${include ? 'active' : ''}`} />*/}
  60 + <PartitionOutlined />
  61 + </span>
  62 + </Tooltip>
  63 + );
  64 +};
  65 +
  66 +const OrgCore: React.FC<OrgCoreProps> = (props) => {
  67 + const [loading, setLoading] = useState<boolean>(true); //请求loading
  68 +
  69 + const [data, setData] = useState<OrgModel>(); //存储原始数据
  70 + const [treeData, setTreeData] = useState<any[]>([]); //存储树节点数据
  71 + const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
  72 + const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
  73 + const [selectedData, setSelectedData] = useState<OrgModel[]>([]);
  74 +
  75 + const [keywords, setKeywords] = useState<string>('');
  76 +
  77 + const [include, setInclude] = useState(true);
  78 +
  79 + useImperativeHandle(props.cRef, () => ({
  80 + // 暴露给父组件
  81 + remove: (index: number) => {
  82 + let _selectedKeys: string[] = [];
  83 + let _selectedData: OrgModel[] = [];
  84 + if (selectedKeys && selectedKeys.length > 0) {
  85 + _selectedKeys = [...selectedKeys];
  86 + _selectedData = [...selectedData];
  87 + _selectedKeys.splice(index, 1);
  88 + _selectedData.splice(index, 1);
  89 + setSelectedData(_selectedData);
  90 + setSelectedKeys(_selectedKeys);
  91 + }
  92 + },
  93 + emptySelect: () => {
  94 + setSelectedData([]);
  95 + setSelectedKeys([]);
  96 + },
  97 + setSelected: (ids: string[], users?: any[]) => {
  98 + setSelectedKeys(ids);
  99 + if (users) {
  100 + setSelectedData(users);
  101 + }
  102 + },
  103 + }));
  104 +
  105 + const onSelect = props.onSelect ? props.onSelect : () => {};
  106 +
  107 + const generateTreeData = useCallback((_data: OrgModel[], _keywords?: string): OrgModel[] => {
  108 + const _treeNode: any[] = [];
  109 + _data.map((item) => {
  110 + if (typeof item.visible === 'boolean' && !item.visible) {
  111 + return;
  112 + }
  113 + const _item: OrgModel = {
  114 + ...item,
  115 + children: [],
  116 + };
  117 + if (item.children) {
  118 + _item.children = generateTreeData(item.children, _keywords);
  119 + }
  120 + _treeNode.push(_item);
  121 + });
  122 + return _treeNode;
  123 + }, []);
  124 +
  125 + const getFirstNode = (nodes) => {
  126 + if (!nodes.disabled) {
  127 + return nodes;
  128 + }
  129 + if (nodes.children) {
  130 + for (let i = 0; i < nodes.children.length; i++) {
  131 + if (getFirstNode(nodes.children[i])) {
  132 + return getFirstNode(nodes.children[i]);
  133 + }
  134 + }
  135 + }
  136 + return null;
  137 + };
  138 +
  139 + const requestOrg = useCallback(() => {
  140 + setLoading(true);
  141 + getOrgTree(props.request, props.params || [])
  142 + .then((res: OrgModel) => {
  143 + if (res) {
  144 + //默认展开三级
  145 + const _expandedKeys = [];
  146 + const firstNode = getFirstNode(res);
  147 +
  148 + if (firstNode) {
  149 + _expandedKeys.push(firstNode.code);
  150 + }
  151 +
  152 + if (res.children) {
  153 + res.children.map((item) => {
  154 + _expandedKeys.push(item.code);
  155 + });
  156 + }
  157 +
  158 + if (props.selectFirstNode && firstNode.code) {
  159 + onSelect([firstNode.code], [{ id: firstNode.code, name: firstNode.name }], include);
  160 + setSelectedKeys([firstNode.code]);
  161 + } else {
  162 + // onSelect([], [], include);
  163 + // setSelectedKeys([]);
  164 + }
  165 +
  166 + setExpandedKeys(_expandedKeys);
  167 + setData(res);
  168 + setTreeData([res]);
  169 +
  170 + setLoading(false);
  171 + }
  172 + })
  173 + .catch(() => {
  174 + onSelect([], [], include);
  175 + setSelectedKeys([]);
  176 +
  177 + setExpandedKeys([]);
  178 + setTreeData([]);
  179 +
  180 + setLoading(false);
  181 + });
  182 + }, []);
  183 +
  184 + useEffect(() => {
  185 + requestOrg();
  186 + }, [requestOrg]);
  187 +
  188 + const handleSelect = (_selectedKeys: any[]) => {
  189 + if (props.multiple) {
  190 + return;
  191 + }
  192 + if (_selectedKeys && _selectedKeys.length > 0) {
  193 + // @ts-ignore
  194 + setSelectedKeys(_selectedKeys);
  195 + onSelect(_selectedKeys, [], include);
  196 + }
  197 + };
  198 +
  199 + const handleMultiSelect = (check: any, e: any) => {
  200 + //e :{ checked: boolean; checkedNodes: [] }
  201 + const keys: string[] = check.checked;
  202 + //TODO 单选多选处理方式不一样
  203 + //console.log(props.max, keys, check, e);
  204 + const count = keys.length;
  205 + let _selectKeys = [];
  206 + let _selectData = [];
  207 + if (props.max === 1 && count > 0) {
  208 + _selectKeys = [keys[count - 1]];
  209 + _selectData = [e.checkedNodes[count - 1]];
  210 + } else {
  211 + _selectKeys = [...keys];
  212 + _selectData = [...e.checkedNodes];
  213 + }
  214 + setSelectedKeys(_selectKeys);
  215 + setSelectedData(_selectData);
  216 +
  217 + onSelect(
  218 + _selectKeys,
  219 + _selectData.map((item) => {
  220 + return { id: item.code, name: item.name };
  221 + }),
  222 + include,
  223 + );
  224 + };
  225 +
  226 + const filter = (word: string) => {
  227 + setKeywords(word);
  228 + const traverse = function (node: OrgModel) {
  229 + const childNodes = node.children || [];
  230 +
  231 + if (node.name.indexOf(word) > -1) {
  232 + node.visible = true;
  233 + }
  234 + childNodes.forEach((child) => {
  235 + child.visible = child.name.indexOf(word) > -1;
  236 +
  237 + traverse(child);
  238 + });
  239 +
  240 + if (!node.visible && childNodes.length) {
  241 + node.visible = childNodes.some((child) => child.visible);
  242 + }
  243 + };
  244 +
  245 + if (data) {
  246 + const _data = _.cloneDeep(data);
  247 + if (word != '') {
  248 + traverse(_data);
  249 + }
  250 +
  251 + setTreeData(generateTreeData([_data], word));
  252 + }
  253 + };
  254 +
  255 + const handleSearch = (e: React.KeyboardEvent<HTMLInputElement>) => {
  256 + e.stopPropagation();
  257 + // @ts-ignore
  258 + filter(e.target.value.trim());
  259 + };
  260 + const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  261 + // @ts-ignore
  262 + if (e.type === 'click' && e.target.value === '' && data) {
  263 + //如果是清空
  264 + filter('');
  265 + }
  266 + };
  267 +
  268 + const renderTitle = (nodeData: any) => {
  269 + let title = nodeData.name;
  270 + if (keywords) {
  271 + const index = title.indexOf(keywords);
  272 + if (index > -1) {
  273 + title = (
  274 + <>
  275 + {title.substr(0, index)}
  276 + <span className={'qx-keywords-highlight'}>{keywords}</span>
  277 + {title.substr(index + keywords.length)}
  278 + </>
  279 + );
  280 + }
  281 + }
  282 + if (nodeData.pid === '*' && props.hasInclude) {
  283 + return (
  284 + <div>
  285 + {title}{' '}
  286 + <IncludeNode
  287 + onChange={(value: boolean) => {
  288 + setInclude(value);
  289 + onSelect(selectedKeys, selectedData, value);
  290 + }}
  291 + />
  292 + </div>
  293 + );
  294 + }
  295 + return title;
  296 + };
  297 +
  298 + return (
  299 + <div className={'qx-search-tree__wrap'}>
  300 + <Input
  301 + className={'qx-selector-sub-search'}
  302 + placeholder={props.placeholder || '请输入部门名称,按回车键搜索'}
  303 + allowClear
  304 + prefix={<SearchOutlined />}
  305 + onChange={(e) => {
  306 + handleChange(e);
  307 + }}
  308 + onPressEnter={(e) => {
  309 + handleSearch(e);
  310 + }}
  311 + />
  312 + <div className={`qx-search-tree ${props.max === 1 ? 'qx-search-tree--radio' : null}`}>
  313 + {!loading ? (
  314 + treeData.length > 0 ? (
  315 + <Tree
  316 + blockNode
  317 + checkable={props.multiple}
  318 + fieldNames={{
  319 + title: 'name',
  320 + key: 'code',
  321 + children: 'children',
  322 + }}
  323 + titleRender={(nodeData) => renderTitle(nodeData)}
  324 + defaultExpandedKeys={expandedKeys}
  325 + checkStrictly={props.checkStrictly}
  326 + selectedKeys={props.multiple ? [] : selectedKeys}
  327 + treeData={treeData}
  328 + onSelect={handleSelect}
  329 + onCheck={handleMultiSelect}
  330 + checkedKeys={props.multiple ? selectedKeys : []}
  331 + selectable={!props.multiple}
  332 + />
  333 + ) : (
  334 + <Empty style={{ paddingTop: '30px' }} />
  335 + )
  336 + ) : null}
  337 + </div>
  338 + <Spin
  339 + style={{ width: '100%', marginTop: '40px' }}
  340 + spinning={loading}
  341 + // indicator={<LoadingOutlined style={{ fontSize: 24, marginTop: '40px' }} spin />}
  342 + />
  343 + </div>
  344 + );
  345 +};
  346 +
  347 +export default OrgCore;
... ...
  1 +import * as React from 'react';
  2 +import { useEffect, useRef, useState } from 'react';
  3 +import { Modal, Tag } from 'antd';
  4 +import OrgCore from './core';
  5 +
  6 +type OrgSelectorDialogProps = {
  7 + title?: string;
  8 + visible: boolean;
  9 + onCancel: () => void;
  10 + data?: [];
  11 + max?: number;
  12 + multiple?: boolean; //默认不是多选
  13 + request: any;
  14 + checkStrictly?: boolean;
  15 + selectedData?: { id: string; name: string }[]; //已选组织数据
  16 + onOk: (selectedKeys: string[], selectedData: any[]) => void;
  17 + modalClassName?: string | undefined; // 弹框类名自定义 用于自定义以及覆盖样式
  18 +};
  19 +
  20 +const OrgSelectorDialog: React.FC<OrgSelectorDialogProps> = (props) => {
  21 + const [selectedData, setSelectedData] = useState<any[]>(props?.selectedData || []);
  22 + const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
  23 + const orgCoreRef = useRef<{
  24 + remove: (index: number) => void;
  25 + emptySelect: () => void;
  26 + setSelected: (selectedKeys: string[], selectedData: any[]) => void;
  27 + }>();
  28 +
  29 + useEffect(() => {
  30 + if (props.visible) {
  31 + orgCoreRef.current?.emptySelect();
  32 + const keys = props?.selectedData?.map((data) => data.id) || [];
  33 + const data = props?.selectedData || [];
  34 + setSelectedKeys(keys);
  35 + setSelectedData(data);
  36 + orgCoreRef.current?.setSelected(keys, data);
  37 + }
  38 + }, [props.visible]);
  39 +
  40 + useEffect(() => {
  41 + //console.log(selectedData);
  42 + }, [selectedData]);
  43 +
  44 + const handleOk = () => {
  45 + props.onOk(selectedKeys, selectedData);
  46 + };
  47 + const handleCancel = () => {
  48 + props.onCancel();
  49 + };
  50 +
  51 + const handleSelectOrg = (keys: string[], select: any[]) => {
  52 + setSelectedKeys(keys);
  53 + setSelectedData(select);
  54 + };
  55 + return (
  56 + <Modal
  57 + title={props.title || '选择部门'}
  58 + width={560}
  59 + visible={props.visible}
  60 + className={'qx-org-selector__dialog'}
  61 + onOk={handleOk}
  62 + onCancel={handleCancel}
  63 + wrapClassName={props?.modalClassName || ''}
  64 + >
  65 + <div className={'qx-org-selected__temp'}>
  66 + {selectedData &&
  67 + selectedData.map(
  68 + (item: { key?: string; title?: string; code?: string; name?: string }) => (
  69 + <Tag closable color={'blue'} key={item.key || item.code}>
  70 + {item.title || item.name}
  71 + </Tag>
  72 + ),
  73 + )}
  74 + </div>
  75 + <div className={'qx-org-selector__content'}>
  76 + {props.visible ? (
  77 + <OrgCore
  78 + request={props.request}
  79 + cRef={orgCoreRef}
  80 + multiple
  81 + max={props.max}
  82 + checkStrictly={props.checkStrictly}
  83 + params={props?.data || []}
  84 + onSelect={handleSelectOrg}
  85 + />
  86 + ) : null}
  87 + </div>
  88 + </Modal>
  89 + );
  90 +};
  91 +
  92 +export default OrgSelectorDialog;
... ...
  1 +import React, { useEffect, useImperativeHandle, useState } from 'react';
  2 +
  3 +import { Checkbox, Input, Tag } from 'antd';
  4 +import { ApartmentOutlined } from '@ant-design/icons';
  5 +import './style.less';
  6 +import OrgSelectorDialog from './dialog';
  7 +
  8 +export type QxOrgSelectorProps = {
  9 + onChange?: (data: string | string[], infos?: BaseOrg[]) => void;
  10 + onMounted?: () => void;
  11 + defaultValue?: any;
  12 + disabled?: boolean;
  13 + multiple?: boolean;
  14 + readOnly?: boolean;
  15 + name?: string;
  16 + max?: number;
  17 + request: any;
  18 + value?: string | string[];
  19 + defaultData?: any;
  20 + data?: []; //请求body参数
  21 + cRef?: any;
  22 +};
  23 +
  24 +type BaseOrg = {
  25 + id: string;
  26 + name: string;
  27 +};
  28 +/**
  29 + * 表单设计器(XRender)
  30 + * @constructor
  31 + */
  32 +const QxOrgSelector: React.FC<QxOrgSelectorProps> = (props) => {
  33 + const [selectOrgs, setSelectOrgs] = useState([]);
  34 + const [visible, setVisible] = useState(false);
  35 + const [value, setValue] = useState<string | string[]>();
  36 +
  37 + useEffect(() => {
  38 + setValue(props.defaultValue);
  39 + if (props?.onMounted) {
  40 + props?.onMounted();
  41 + }
  42 + }, []);
  43 +
  44 + useEffect(() => {
  45 + setValue(props.value);
  46 + //如果value
  47 + if (!props.value) {
  48 + setSelectOrgs([]);
  49 + }
  50 + }, [props.value]);
  51 +
  52 + useImperativeHandle(props.cRef, function () {
  53 + return {
  54 + // 暴露给父组件
  55 + clear: () => {
  56 + setSelectOrgs([]);
  57 + setValue(undefined);
  58 + },
  59 + /* //方法触发增加部门
  60 + addOrgs: (orgs: BaseOrg[]) => {
  61 + const addIds: string[] = selectOrgs.map(org => org.id);
  62 + const waits = orgs.filter((org) => {
  63 + return org.id && !addIds.includes(org.id);
  64 + });
  65 + setSelectOrgs([...selectOrgs, ...waits]);
  66 + },*/
  67 + //设置部门
  68 + setOrgs: (orgs: BaseOrg[]) => {
  69 + if (JSON.stringify(orgs) === JSON.stringify(selectOrgs)) {
  70 + return;
  71 + }
  72 + setSelectOrgs(orgs);
  73 + const ids = orgs.map((user) => user.id);
  74 + if (JSON.stringify(ids) === JSON.stringify(value)) {
  75 + return;
  76 + }
  77 + props.onChange(ids, orgs);
  78 + },
  79 + };
  80 + });
  81 +
  82 + //TODO 默认值待优化
  83 + useEffect(() => {
  84 + let _orgs = [];
  85 + let ids;
  86 + if (props.defaultData) {
  87 + if (props.multiple || Array.isArray(props.defaultData)) {
  88 + _orgs = props.defaultData;
  89 + ids = [];
  90 + props.defaultData.map((item) => {
  91 + ids.push(item.id);
  92 + });
  93 + } else {
  94 + _orgs = [props.defaultData];
  95 + ids = props.defaultData.id;
  96 + }
  97 + }
  98 + setSelectOrgs(_orgs);
  99 +
  100 + if (ids && ids.length > 0 && !props.value) {
  101 + props.onChange(ids, _orgs);
  102 + }
  103 + }, [JSON.stringify(props.defaultData)]);
  104 +
  105 + // getUserList()
  106 + const handleOk = (keys: [], data: []) => {
  107 + let _value: [] | string = keys;
  108 + if (!props.multiple && keys && keys.length > 0) {
  109 + // @ts-ignore
  110 + _value = keys[0];
  111 + }
  112 + setValue(_value);
  113 +
  114 + setSelectOrgs(data);
  115 + setVisible(false);
  116 + if (props.onChange) {
  117 + props.onChange(_value, data);
  118 + }
  119 + };
  120 +
  121 + const handleCancel = () => {
  122 + setVisible(false);
  123 + };
  124 +
  125 + const handleRemove = (index: number) => {
  126 + let _value: string | string[] = '';
  127 + let _selected = [];
  128 + if (props.multiple) {
  129 + // @ts-ignore
  130 + _value = [...value];
  131 + _selected = [...selectOrgs];
  132 + _value.splice(index, 1);
  133 + _selected.splice(index, 1);
  134 + }
  135 +
  136 + setValue(_value);
  137 + setSelectOrgs(_selected);
  138 +
  139 + if (props.onChange) {
  140 + props.onChange(_value, _selected);
  141 + }
  142 + };
  143 +
  144 + return (
  145 + <>
  146 + {props.name ? (
  147 + props.multiple && typeof value !== 'string' ? (
  148 + <Checkbox.Group name={props.name} value={value} style={{ display: 'none' }} />
  149 + ) : (
  150 + <Input style={{ display: 'none' }} value={value} />
  151 + )
  152 + ) : null}
  153 + <div
  154 + className={
  155 + 'qx-org-selector ant-input ' +
  156 + `${props.readOnly ? 'qx-org-selector--readonly' : 'qx-org-selector--edit'}`
  157 + }
  158 + style={{ minHeight: '32px', paddingTop: 3, paddingBottom: 3 }}
  159 + onClick={() => setVisible(true)}
  160 + >
  161 + {props.readOnly ? null : (
  162 + <ApartmentOutlined
  163 + style={{
  164 + paddingRight: '5px',
  165 + paddingLeft: 4,
  166 + paddingTop: 6,
  167 + verticalAlign: 'top',
  168 + color: '#999',
  169 + }}
  170 + />
  171 + )}
  172 + {selectOrgs.map(
  173 + (org: { title?: string; key?: string; name?: string; id?: string }, index: number) => (
  174 + <Tag
  175 + closable={!props.readOnly}
  176 + color={'blue'}
  177 + key={org.key || org.id}
  178 + onClose={() => handleRemove(index)}
  179 + style={{
  180 + maxWidth: `calc(100% - ${!props.readOnly && index === 0 ? 32 : 8}px)`,
  181 + }}
  182 + >
  183 + <span
  184 + style={{
  185 + display: 'inline-block',
  186 + maxWidth: `calc(100% - ${!props.readOnly ? 15 : 0}px)`,
  187 + textOverflow: 'ellipsis',
  188 + overflow: 'hidden',
  189 + height: 20,
  190 + // lineHeight: '20px',
  191 + }}
  192 + title={org.title || org.name}
  193 + >
  194 + {org.title || org.name}
  195 + </span>
  196 + </Tag>
  197 + ),
  198 + )}
  199 + </div>
  200 + {props.readOnly ? null : (
  201 + <OrgSelectorDialog
  202 + key={visible + ''}
  203 + visible={visible}
  204 + multiple
  205 + checkStrictly
  206 + selectedData={selectOrgs}
  207 + data={props.data}
  208 + max={props.max}
  209 + request={props.request}
  210 + onOk={handleOk}
  211 + onCancel={handleCancel}
  212 + />
  213 + )}
  214 + </>
  215 + );
  216 +};
  217 +
  218 +export default QxOrgSelector;
... ...
  1 +/*获取选人组件中的部门树*/
  2 +
  3 +export function getOrgTree(
  4 + request: any,
  5 + data: { relType: string; relIds?: []; includeChild?: boolean }[],
  6 +) {
  7 + return request.post(`/qx-apaas-uc/selectUser/getOrgTree`, { data });
  8 +}
... ...
  1 +.qx-org-selector {
  2 + padding-left: 8px;
  3 + line-height: 0;
  4 + .ant-tag {
  5 + height: 22px;
  6 + margin: 1px 4px;
  7 + font-size: 0;
  8 + vertical-align: top;
  9 + > span {
  10 + font-size: 12px;
  11 + }
  12 + > .anticon-close {
  13 + transform: translateY(-4px);
  14 + }
  15 + }
  16 + &.qx-org-selector--edit {
  17 + .ant-tag {
  18 + &:first-child {
  19 + margin-left: 0;
  20 + }
  21 + }
  22 + }
  23 +
  24 + &.qx-org-selector--readonly {
  25 + padding-right: 0;
  26 + padding-left: 0;
  27 + background-color: transparent;
  28 + border-color: transparent;
  29 + &.ant-input:hover {
  30 + border-color: transparent;
  31 + }
  32 + }
  33 +
  34 + &.qx-org-selector--readonly > .ant-tag {
  35 + &:last-child {
  36 + margin-right: 0;
  37 + }
  38 + }
  39 +}
  40 +
  41 +.qx-org-selector__dialog {
  42 + //.ant-popover-arrow {
  43 + // display: none;
  44 + //}
  45 +
  46 + .ant-modal-body {
  47 + display: flex;
  48 + flex-direction: column;
  49 + padding: 0;
  50 +
  51 + > .ant-row {
  52 + flex: 1;
  53 + }
  54 + }
  55 +
  56 + .qx-selector-sub-search.ant-input-affix-wrapper {
  57 + margin-bottom: 8px;
  58 + border-bottom: 1px solid #f0f0f0;
  59 + }
  60 +}
  61 +
  62 +.qx-org-selected__temp {
  63 + //background-color: #fafafa;
  64 + display: flex;
  65 + flex-wrap: wrap;
  66 + //align-items: flex-start;
  67 + align-items: center;
  68 + height: 60px;
  69 + padding: 5px;
  70 + overflow: auto;
  71 + border-bottom: 1px solid #f0f0f0;
  72 +
  73 + .ant-tag {
  74 + margin: 1px 2px;
  75 + }
  76 +}
  77 +
  78 +.qx-org-selector__content {
  79 + height: 380px;
  80 + overflow: auto;
  81 +}
  82 +
  83 +.qx-org-tree__include {
  84 + margin-left: 10px;
  85 + color: #999;
  86 + font-size: 16px;
  87 +
  88 + &:hover {
  89 + color: #333;
  90 + }
  91 +
  92 + .active {
  93 + color: #1890ff;
  94 + }
  95 +}
... ...
  1 +import QxPosSelectorInput from './src/input';
  2 +import QxPosSelectorDialog from './src/dialog';
  3 +import React from 'react';
  4 +
  5 +interface QxPosSelectorType extends React.FC {
  6 + Dialog: typeof QxPosSelectorDialog;
  7 +}
  8 +
  9 +export const QxPosSelector = QxPosSelectorInput as QxPosSelectorType;
  10 +
  11 +QxPosSelector.Dialog = QxPosSelectorDialog;
... ...
  1 +import * as React from 'react';
  2 +import { useCallback, useEffect, useImperativeHandle, useState } from 'react';
  3 +import { Checkbox, Empty, Input, Spin } from 'antd';
  4 +import { getPositions } from './service';
  5 +import _ from 'lodash';
  6 +import { SearchOutlined } from '@ant-design/icons';
  7 +import Menu from 'antd/es/menu';
  8 +
  9 +type PosCoreProps = {
  10 + cRef?: any;
  11 + multiple?: boolean;
  12 + placeholder?: string;
  13 + params?: any;
  14 + onSelect?: (selectedKeys: string[], selectedData: PosModel[]) => void;
  15 + request: any;
  16 +};
  17 +
  18 +export interface PosModel {
  19 + id: string;
  20 + name: string;
  21 + category?: boolean;
  22 + positionList?: PosModel[];
  23 + visible?: boolean;
  24 +}
  25 +
  26 +const PosCore: React.FC<PosCoreProps> = (props) => {
  27 + const [loading, setLoading] = useState<boolean>(true); //请求loading
  28 +
  29 + const [data, setData] = useState<PosModel[]>([]); //存储原始数据
  30 + const [expandedKeys, setExpandedKeys] = useState<string[]>();
  31 +
  32 + const [keywords, setKeywords] = useState<string>('');
  33 +
  34 + const [selectedData, setSelectedData] = useState<PosModel[]>([]);
  35 + const [selectedKeys, setSelectedKeys] = useState<string[]>();
  36 +
  37 + const requestData = useCallback(() => {
  38 + setLoading(true);
  39 + getPositions(props.request, props.params || {})
  40 + .then((res: PosModel[]) => {
  41 + if (res) {
  42 + const _extendKeys: string[] = [];
  43 + let _selectKey = '';
  44 + res.map((item) => {
  45 + if (!_selectKey && item.positionList && item.positionList.length > 0) {
  46 + _selectKey = item.positionList[0].id;
  47 + }
  48 + _extendKeys.push(item.id);
  49 + });
  50 + if (_selectKey && !props.multiple) {
  51 + setSelectedKeys([_selectKey]);
  52 + }
  53 + setExpandedKeys(_extendKeys);
  54 +
  55 + setData(res);
  56 +
  57 + setLoading(false);
  58 + }
  59 + })
  60 + .catch(() => {
  61 + setLoading(false);
  62 + });
  63 + }, []);
  64 +
  65 + useEffect(() => {
  66 + requestData();
  67 + }, [requestData]);
  68 +
  69 + useEffect(() => {
  70 + const onSelect = props.onSelect ? props.onSelect : () => {};
  71 + if (selectedKeys) {
  72 + onSelect(selectedKeys, selectedData);
  73 + }
  74 + }, [props.onSelect, selectedKeys, selectedData]);
  75 +
  76 + const handleSelect = (selectData: { selectedKeys: string[] }) => {
  77 + //单选走这里
  78 + if (!props.multiple) {
  79 + setSelectedKeys(selectData.selectedKeys);
  80 + }
  81 + };
  82 + useImperativeHandle(props.cRef, () => ({
  83 + // 暴露给父组件
  84 + remove: (index: number) => {
  85 + let _selectedKeys: string[] = [];
  86 + let _selectedData: PosModel[] = [];
  87 + if (selectedKeys && selectedKeys.length > 0) {
  88 + _selectedKeys = [...selectedKeys];
  89 + _selectedData = [...selectedData];
  90 + _selectedKeys.splice(index, 1);
  91 + _selectedData.splice(index, 1);
  92 + setSelectedData(_selectedData);
  93 + setSelectedKeys(_selectedKeys);
  94 + }
  95 + },
  96 + emptySelect: () => {
  97 + setSelectedData([]);
  98 + setSelectedKeys([]);
  99 + },
  100 + }));
  101 + const handleMultiSelect = (checked: boolean, item: PosModel) => {
  102 + let _selectedKeys: string[] = [];
  103 + let _selectedData: PosModel[] = [];
  104 + if (selectedKeys) {
  105 + _selectedKeys = [...selectedKeys];
  106 + _selectedData = [...selectedData];
  107 + }
  108 + //console.log(checked, item.id, _selectedKeys);
  109 +
  110 + if (checked) {
  111 + _selectedKeys.push(item.id);
  112 + _selectedData.push(item);
  113 + } else {
  114 + const index = _selectedKeys.indexOf(item.id);
  115 + if (index > -1) {
  116 + _selectedKeys.splice(index, 1);
  117 + _selectedData.splice(index, 1);
  118 + }
  119 + }
  120 +
  121 + setSelectedData(_selectedData);
  122 + setSelectedKeys(_selectedKeys);
  123 + /*
  124 + if (props.onSelect) {
  125 + props.onSelect(_selectedKeys, _selectedData);
  126 + }*/
  127 + };
  128 + //多选走这里
  129 + const filter = (word: string) => {
  130 + setKeywords(word);
  131 + const traverse = function (node: PosModel) {
  132 + const childNodes = node.positionList || [];
  133 +
  134 + childNodes.forEach((child) => {
  135 + child.visible = child.name.indexOf(word) > -1;
  136 +
  137 + traverse(child);
  138 + });
  139 +
  140 + if (!node.visible && childNodes.length) {
  141 + node.visible = childNodes.some((child) => child.visible);
  142 + }
  143 + };
  144 +
  145 + if (data) {
  146 + const _data = _.cloneDeep(data);
  147 + _data.forEach((item) => {
  148 + traverse(item);
  149 + });
  150 + setData(_data);
  151 + }
  152 + };
  153 +
  154 + const handleSearch = (e: React.KeyboardEvent<HTMLInputElement>) => {
  155 + e.stopPropagation();
  156 + // @ts-ignore
  157 + filter(e.target.value.trim());
  158 + };
  159 + const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  160 + // @ts-ignore
  161 + if (e.type === 'click' && e.target.value === '' && data) {
  162 + //如果是清空
  163 + filter('');
  164 + }
  165 + };
  166 + const renderText = (text: string) => {
  167 + let title = <> {text}</>;
  168 + if (keywords) {
  169 + const index = text.indexOf(keywords);
  170 + if (index > -1) {
  171 + title = (
  172 + <>
  173 + {text.substr(0, index)}
  174 + <span className={'qx-keywords-highlight'}>{keywords}</span>
  175 + {text.substr(index + keywords.length)}
  176 + </>
  177 + );
  178 + }
  179 + }
  180 + return title;
  181 + };
  182 +
  183 + return (
  184 + <div className={'qx-search-menus__wrap'}>
  185 + <Input
  186 + className={'qx-selector-sub-search'}
  187 + placeholder={props.placeholder || '请输入岗位名称,按回车键搜索'}
  188 + allowClear
  189 + prefix={<SearchOutlined />}
  190 + onChange={(e) => {
  191 + handleChange(e);
  192 + }}
  193 + onPressEnter={(e) => {
  194 + handleSearch(e);
  195 + }}
  196 + />
  197 + <div className="qx-search-menus">
  198 + {expandedKeys ? (
  199 + <Menu
  200 + mode={'inline'}
  201 + onSelect={handleSelect}
  202 + selectedKeys={props.multiple ? [] : selectedKeys}
  203 + multiple={!!props.multiple}
  204 + defaultOpenKeys={expandedKeys}
  205 + >
  206 + {data.map((item: PosModel) => {
  207 + if (typeof item.visible === 'boolean' && !item.visible) {
  208 + return null;
  209 + }
  210 + if (item.positionList) {
  211 + return (
  212 + <Menu.SubMenu key={item.id} title={item.name}>
  213 + {item.positionList.map((child) => {
  214 + return typeof child.visible === 'boolean' && !child.visible ? null : (
  215 + <Menu.Item key={child.id}>
  216 + {props.multiple ? (
  217 + <Checkbox
  218 + checked={selectedKeys && selectedKeys.indexOf(child.id) > -1}
  219 + onChange={(e) => {
  220 + handleMultiSelect(e.target.checked, child);
  221 + }}
  222 + >
  223 + {renderText(child.name)}
  224 + </Checkbox>
  225 + ) : (
  226 + renderText(child.name)
  227 + )}
  228 + </Menu.Item>
  229 + );
  230 + })}
  231 + </Menu.SubMenu>
  232 + );
  233 + }
  234 + return null;
  235 + })}
  236 + </Menu>
  237 + ) : null}
  238 + <Spin
  239 + spinning={loading}
  240 + style={{ width: '100%', marginTop: '40px' }}
  241 + // indicator={<LoadingOutlined style={{ fontSize: 24, marginTop: '40px' }} spin />}
  242 + />
  243 + {!loading && data.length === 0 ? <Empty style={{ paddingTop: '30px' }} /> : null}
  244 + </div>
  245 + </div>
  246 + );
  247 +};
  248 +
  249 +export default PosCore;
... ...
  1 +import * as React from 'react';
  2 +import { useRef, useState } from 'react';
  3 +import { Modal, Tag } from 'antd';
  4 +import './style.less';
  5 +import type { PosModel } from './core';
  6 +import PosCore from './core';
  7 +
  8 +type PosSelectorDialogProps = {
  9 + title?: string;
  10 + visible: boolean;
  11 + onCancel: () => void;
  12 + data?: [];
  13 + multiple?: boolean; //默认不是多选
  14 + request: any;
  15 + onOk: (selectedKeys: string[], selectedData: PosModel[]) => void;
  16 +};
  17 +
  18 +const PosSelectorDialog: React.FC<PosSelectorDialogProps> = (props) => {
  19 + const [selectedData, setSelectedData] = useState<PosModel[]>([]);
  20 + const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
  21 + const posCoreRef = useRef({
  22 + // eslint-disable-next-line @typescript-eslint/no-unused-vars
  23 + remove: (index: number) => {
  24 + // console.log(index)
  25 + },
  26 + emptySelect: () => {},
  27 + });
  28 +
  29 + const handleOk = () => {
  30 + props.onOk(selectedKeys, selectedData);
  31 + posCoreRef.current.emptySelect();
  32 + setSelectedKeys([]);
  33 + setSelectedData([]);
  34 + };
  35 + const handleCancel = () => {
  36 + props.onCancel();
  37 + posCoreRef.current.emptySelect();
  38 + setSelectedKeys([]);
  39 + setSelectedData([]);
  40 + };
  41 +
  42 + const handleSelectPos = (keys: string[], datas: PosModel[]) => {
  43 + setSelectedKeys(keys);
  44 + setSelectedData(datas);
  45 + };
  46 +
  47 + const handleRemove = (index: number) => {
  48 + posCoreRef.current.remove(index);
  49 + };
  50 +
  51 + return props.visible ? (
  52 + <Modal
  53 + title={props.title || '选择岗位'}
  54 + width={560}
  55 + visible={props.visible}
  56 + className={'qx-pos-selector__dialog'}
  57 + onOk={handleOk}
  58 + onCancel={handleCancel}
  59 + >
  60 + {props.multiple ? (
  61 + <div className={'qx-pos-selected__temp'}>
  62 + {(selectedData || []).map((item: PosModel, index: number) => (
  63 + <Tag closable color={'blue'} key={item.id} onClose={() => handleRemove(index)}>
  64 + {item.name}
  65 + </Tag>
  66 + ))}
  67 + </div>
  68 + ) : null}
  69 + <div className={'qx-pos-selector__content'}>
  70 + <PosCore
  71 + request={props.request}
  72 + cRef={posCoreRef}
  73 + multiple
  74 + params={{}}
  75 + onSelect={handleSelectPos}
  76 + />
  77 + </div>
  78 + </Modal>
  79 + ) : null;
  80 +};
  81 +
  82 +export default PosSelectorDialog;
... ...
  1 +import React, { useEffect, useState } from 'react';
  2 +
  3 +import { Checkbox, Input, Tag } from 'antd';
  4 +import { ApartmentOutlined } from '@ant-design/icons';
  5 +/*
  6 +import './index.less';
  7 +*/
  8 +import PosSelectorDialog from './dialog';
  9 +
  10 +export type QxPosSelectorProps = {
  11 + onChange?: (data: any) => void;
  12 + defaultValue?: any;
  13 + disabled?: boolean;
  14 + multiple?: boolean;
  15 + readOnly?: boolean;
  16 + name?: string;
  17 + data?: []; //请求body参数
  18 + request: any;
  19 +};
  20 +
  21 +/**
  22 + * 表单设计器(XRender)
  23 + * @constructor
  24 + */
  25 +const QxPosSelector: React.FC<QxPosSelectorProps> = (props) => {
  26 + const [selectOrgs, setSelectOrgs] = useState([]);
  27 + const [visible, setVisible] = useState(false);
  28 + const [value, setValue] = useState<string | string[]>();
  29 +
  30 + useEffect(() => {
  31 + setValue(props.defaultValue);
  32 + }, []);
  33 +
  34 + // getUserList()
  35 + const handleOk = (keys: [], data: []) => {
  36 + let _value: [] | string = keys;
  37 + if (!props.multiple && keys && keys.length > 0) {
  38 + // @ts-ignore
  39 + _value = keys[0];
  40 + }
  41 + setValue(_value);
  42 + setSelectOrgs(data);
  43 + setVisible(false);
  44 + if (props.onChange) {
  45 + props.onChange(_value);
  46 + }
  47 + };
  48 +
  49 + const handleCancel = () => {
  50 + setVisible(false);
  51 + };
  52 + const handleRemove = (index: number) => {
  53 + let _value: string | string[] = '';
  54 + let _selected = [];
  55 + if (props.multiple) {
  56 + // @ts-ignore
  57 + _value = [...value];
  58 + _selected = [...selectOrgs];
  59 + _value.splice(index, 1);
  60 + _selected.splice(index, 1);
  61 + }
  62 +
  63 + setValue(_value);
  64 + setSelectOrgs(_selected);
  65 +
  66 + if (props.onChange) {
  67 + props.onChange(_value);
  68 + }
  69 + };
  70 +
  71 + return (
  72 + <>
  73 + {props.name ? (
  74 + props.multiple && typeof value !== 'string' ? (
  75 + <Checkbox.Group name={props.name} value={value} style={{ display: 'none' }} />
  76 + ) : (
  77 + <Input style={{ display: 'none' }} value={value} />
  78 + )
  79 + ) : null}
  80 + <div
  81 + className={'qx-user-selector ant-input'}
  82 + style={{ minHeight: '33px' }}
  83 + onClick={() => setVisible(true)}
  84 + >
  85 + <ApartmentOutlined style={{ paddingRight: '5px', color: '#999' }} />
  86 + {selectOrgs.map((org: { title: string; key: string }, index) => (
  87 + <Tag closable color={'blue'} key={org.key} onClose={() => handleRemove(index)}>
  88 + {org.title}
  89 + </Tag>
  90 + ))}
  91 + </div>
  92 + {props.readOnly ? null : (
  93 + <PosSelectorDialog
  94 + visible={visible}
  95 + multiple={props.multiple}
  96 + data={props.data}
  97 + onOk={handleOk}
  98 + request={props.request}
  99 + onCancel={handleCancel}
  100 + />
  101 + )}
  102 + </>
  103 + );
  104 +};
  105 +
  106 +export default QxPosSelector;
... ...
  1 +/*获取选人组件中的岗位*/
  2 +
  3 +export function getPositions(request: any, data: any) {
  4 + return request.post(`/qx-apaas-uc/selectUser/getPositions`, { data });
  5 +}
... ...
  1 +.qx-pos-selector__dialog {
  2 + //.ant-popover-arrow {
  3 + // display: none;
  4 + //}
  5 +
  6 + .ant-modal-body {
  7 + display: flex;
  8 + flex-direction: column;
  9 + padding: 0;
  10 +
  11 + > .ant-row {
  12 + flex: 1;
  13 + }
  14 + }
  15 +
  16 + .ant-checkbox-wrapper {
  17 + width: 100%;
  18 + }
  19 +}
  20 +
  21 +.qx-pos-selected__temp {
  22 + //background-color: #fafafa;
  23 + display: flex;
  24 + flex-wrap: wrap;
  25 + //align-items: flex-start;
  26 + align-items: center;
  27 + height: 60px;
  28 + padding: 5px;
  29 + overflow: auto;
  30 + border-bottom: 1px solid #f0f0f0;
  31 +
  32 + .ant-tag {
  33 + margin: 1px 2px;
  34 + }
  35 +}
  36 +
  37 +.qx-pos-selector__content {
  38 + height: 300px;
  39 + overflow: auto;
  40 +}
... ...
  1 +## 选人组件
  2 +
  3 +```tsx
  4 +/**
  5 + * debug: true
  6 + */
  7 +import React from 'react';
  8 +import { QxUserSelector } from '@qx/common';
  9 +// import request from 'umi-request';
  10 +
  11 +// request.interceptors.request.use((url, options) => {
  12 +// if (url.startsWith('/api/')) {
  13 +// return { url, options };
  14 +// }
  15 +// const headers = { Authorization: 'dev_session:ZGuqjkCF3GMzorijXw7' };
  16 +// // headers['SERVER-IP'] = '192.168.181.112';
  17 +//
  18 +// const fullUrl = url.startsWith('http') ? url : `http://10.9.1.180/qx-api${url}`;
  19 +// return {
  20 +// url: fullUrl,
  21 +// options: {
  22 +// ...options,
  23 +// ...{
  24 +// headers: { ...headers, ...(options.customHeaders || {}) },
  25 +// isInternal: true,
  26 +// timeout: 30000,
  27 +// },
  28 +// },
  29 +// };
  30 +// });
  31 +
  32 +// request.interceptors.response.use(async (response, options) => {
  33 +// if (response.status !== 200) {
  34 +// return Promise.reject(response);
  35 +// }
  36 +//
  37 +// if (!response.headers.get('content-type')?.includes('application/json')) {
  38 +// return response.blob();
  39 +// }
  40 +//
  41 +// const body = await response.clone().json();
  42 +//
  43 +// // 按正常逻辑处理"文件上传"系列接口
  44 +// const fsUploadApis = ['/file/checkFile', '/file/uploadByExist', '/fss/file/'];
  45 +// const isFsUploadApis = fsUploadApis.filter((api: string) => options.url.indexOf(api) !== -1);
  46 +// if (isFsUploadApis.length > 0) {
  47 +// return Promise.resolve(body || null);
  48 +// }
  49 +//
  50 +// if (body.success) {
  51 +// return Promise.resolve(body.data || null);
  52 +// }
  53 +//
  54 +// console.error('网络请求出错:', body.msg);
  55 +//
  56 +// return Promise.reject(body);
  57 +// });
  58 +
  59 +export default () => {
  60 + return (
  61 + <div>
  62 + <QxUserSelector
  63 + // request={request}
  64 + params={{
  65 + org: [{ relType: 'APPOINT_ORG', relIds: [''] }],
  66 + pos: null,
  67 + range: ['ORG:MubDrwZm8IMxuLDU9FM', 'ORG:a0WZVI96GAdoI5g9IwX', 'ORG:QPLEku2yJU8hmbpLTtg'],
  68 + }}
  69 + />
  70 + <br />
  71 + <QxUserSelector />
  72 + <br />
  73 + <QxUserSelector
  74 + readOnly
  75 + value={['1212']}
  76 + defaultData={[{ id: '1212', name: '邢晴晴' }]}
  77 + // request={request}
  78 + />
  79 + </div>
  80 + );
  81 +};
  82 +```
  83 +
  84 +## API
  85 +
  86 +| 参数 | 说明 | 类型 | 默认值 |
  87 +| ------------ | ------------------------ | ------------------ | ------ |
  88 +| onChange | 选的人变化时的回调 | function(value) | - |
  89 +| defaultValue | 默认值 | string[] | - |
  90 +| disabled | 禁用 | bool | - |
  91 +| multiple | 是否多选 | bool | - |
  92 +| max | 最多选几个,ps:没有控制 | number | - |
  93 +| readOnly | 只读 | bool | - |
  94 +| value | | string[] \| string | |
... ...
  1 +import QxUserSelectorInput from './src/input';
  2 +import QxUserSelectorDialog from './src/dialog';
  3 +import React from 'react';
  4 +
  5 +interface QxUserSelectorType extends React.FC {
  6 + Dialog: typeof QxUserSelectorDialog;
  7 +}
  8 +
  9 +export const QxUserSelector = QxUserSelectorInput as QxUserSelectorType;
  10 +
  11 +QxUserSelector.Dialog = QxUserSelectorDialog;
... ...
  1 +import React, { useEffect, useMemo, useState } from 'react';
  2 +import { LeftOutlined, RightOutlined } from '@ant-design/icons/lib';
  3 +import { InputNumber, Popover, Select } from 'antd';
  4 +import './style.less';
  5 +
  6 +interface QxPaginationProps {
  7 + total: number;
  8 + pageNum: number;
  9 + pageSize?: number;
  10 + pageSizeOptions?: number[];
  11 + onChange: (page: number, pageSize: number) => void;
  12 +}
  13 +
  14 +const QxPagination: React.FC<QxPaginationProps> = (props) => {
  15 + const { pageNum, total, onChange, pageSizeOptions } = { ...props };
  16 + const [current, setCurrent] = useState<number>(1);
  17 + const [pageSize, setPageSize] = useState(props.pageSize || 10);
  18 +
  19 + useEffect(() => {
  20 + setCurrent(pageNum);
  21 + }, [pageNum]);
  22 +
  23 + const onPageSizeChange = (value: number) => {
  24 + setCurrent(1);
  25 + setPageSize(value);
  26 + onChange(1, value);
  27 + };
  28 +
  29 + const pageTotal = Math.ceil(total / pageSize);
  30 +
  31 + const pageContent = useMemo(() => {
  32 + return (
  33 + <ul className={'qx-pagination__more'}>
  34 + <li>共计 {total} 条</li>
  35 + <li>
  36 + 跳转
  37 + <InputNumber
  38 + min={1}
  39 + max={pageTotal}
  40 + value={current}
  41 + style={{ width: '66px', margin: '0 5px' }}
  42 + onChange={(value) => {
  43 + setCurrent(value || 1);
  44 + onChange(value || 1, pageSize);
  45 + }}
  46 + />
  47 +
  48 + </li>
  49 + <li>
  50 + 每页
  51 + <Select
  52 + onChange={onPageSizeChange}
  53 + value={pageSize}
  54 + style={{ width: '80px', margin: '0 5px' }}
  55 + >
  56 + {(pageSizeOptions || [10, 20, 50, 100]).map((item) => (
  57 + <Select.Option key={item} value={item}>
  58 + {item}条
  59 + </Select.Option>
  60 + ))}
  61 + </Select>
  62 + </li>
  63 + </ul>
  64 + );
  65 + }, [total, pageTotal, current, pageSize, pageSizeOptions]);
  66 +
  67 + return (
  68 + <ul
  69 + className={'ant-pagination ant-pagination-simple mini qx-pagination '}
  70 + style={{ whiteSpace: 'nowrap' }}
  71 + >
  72 + <li className={`ant-pagination-prev ${current === 1 ? 'ant-pagination-disabled' : null}`}>
  73 + <button
  74 + className="ant-pagination-item-link"
  75 + disabled={current <= 1}
  76 + type={'button'}
  77 + onClick={() => {
  78 + setCurrent(current - 1);
  79 + onChange(current - 1, pageSize);
  80 + }}
  81 + >
  82 + <LeftOutlined />
  83 + </button>
  84 + </li>
  85 + <Popover
  86 + overlayClassName={'qx-pagination__overlay'}
  87 + content={pageContent}
  88 + trigger={total === 0 ? '' : 'click'}
  89 + placement={'bottom'}
  90 + >
  91 + <li className={'ant-pagination-simple-pager'}>
  92 + {current}
  93 + <span className="ant-pagination-slash">/</span>
  94 + {pageTotal}
  95 + </li>
  96 + </Popover>
  97 + <li
  98 + className={`ant-pagination-next ${
  99 + current >= pageTotal ? 'ant-pagination-disabled' : null
  100 + }`}
  101 + >
  102 + <button
  103 + className="ant-pagination-item-link"
  104 + disabled={current >= pageTotal}
  105 + type={'button'}
  106 + onClick={() => {
  107 + setCurrent(current + 1);
  108 + onChange(current + 1, pageSize);
  109 + }}
  110 + >
  111 + <RightOutlined />
  112 + </button>
  113 + </li>
  114 + </ul>
  115 + );
  116 +};
  117 +export default QxPagination;
... ...
  1 +.qx-pagination {
  2 + > .anticon {
  3 + color: rgba(0, 0, 0, 0.25);
  4 + }
  5 +
  6 + .ant-pagination-simple-pager {
  7 + cursor: pointer;
  8 +
  9 + &:hover {
  10 + color: #007ef3;
  11 + }
  12 + }
  13 +}
  14 +
  15 +.qx-pagination__more {
  16 + margin: 0;
  17 + padding: 5px;
  18 +
  19 + > li {
  20 + margin-bottom: 5px;
  21 + }
  22 +}
... ...
  1 +import * as React from 'react';
  2 +import { useCallback, useEffect, useState } from 'react';
  3 +import { Empty, Input, Spin} from 'antd';
  4 +import { getAllRole } from '../service';
  5 +import {
  6 + SearchOutlined,
  7 +} from '@ant-design/icons';
  8 +import Menu from 'antd/es/menu';
  9 +
  10 +type RoleProps = {
  11 + params: { appId: string };
  12 + placeholder?: string;
  13 + onSelect?: (selectedKeys: string[]) => void;
  14 + request: any;
  15 +};
  16 +
  17 +interface roleModel {
  18 + id: string;
  19 + pId?: string;
  20 + orgName: string;
  21 + name: string;
  22 + manage?: boolean;
  23 + child: roleModel[];
  24 + visible?: boolean;
  25 + // category?: boolean;
  26 + // scopeList?: roleModel[];
  27 + // visible?: boolean;
  28 + // relType?: string;
  29 + // relId?: string;
  30 +}
  31 +
  32 +const Role: React.FC<RoleProps> = (props) => {
  33 + const [loading, setLoading] = useState<boolean>(true); //请求loading
  34 + const [keywords, setKeywords] = useState<string>('');
  35 +
  36 + const [data, setData] = useState<any[]>([]); //存储原始数据
  37 + const [expandedKeys, setExpandedKeys] = useState<string[]>();
  38 +
  39 + const [selectedKeys, setSelectedKeys] = useState<string[]>();
  40 +
  41 + // const generateMenus = function (_data: roleModel[], keywords?: string, pId?: string) {
  42 + // const menus: JSX.Element[] = [];
  43 + // _data.map((item: roleModel) => {
  44 + // if (typeof item.visible === 'boolean' && !item.visible) {
  45 + // return;
  46 + // }
  47 + // if (item.scopeList) {
  48 + // let title = <>{item.name}</>;
  49 + // if (keywords) {
  50 + // const index = item.name.indexOf(keywords);
  51 + // if (index > -1) {
  52 + // title = (
  53 + // <>
  54 + // {item.name.substr(0, index)}
  55 + // <span className={'qx-keywords-highlight'}>{keywords}</span>
  56 + // {item.name.substr(index + keywords.length)}
  57 + // </>
  58 + // );
  59 + // }
  60 + // }
  61 + // menus.push(
  62 + // <Menu.SubMenu key={item.id} title={title}>
  63 + // {generateMenus(item.scopeList || [], keywords, item.id).map((m) => m)}
  64 + // </Menu.SubMenu>,
  65 + // );
  66 + // } else {
  67 + // menus.push(
  68 + // <Menu.Item key={`${item.relType}:${item.relType === 'USER' ? pId : item.relId}`}>
  69 + // <span style={{ opacity: 0.5, marginRight: '10px' }}>
  70 + // {item.relType === 'USER' ? <UserOutlined /> : null}
  71 + // {item.relType === 'ORG' ? <ApartmentOutlined /> : null}
  72 + // {item.relType === 'POSITION' ? <IdcardOutlined /> : null}
  73 + // </span>
  74 + // {item.orgName}
  75 + // </Menu.Item>,
  76 + // );
  77 + // }
  78 + // });
  79 + // return menus;
  80 + // };
  81 +
  82 + const requestData = useCallback(() => {
  83 + setLoading(true);
  84 + getAllRole(props.request).then((res: any) => {
  85 + if (res.child) {
  86 + const _extendKeys: string[] = [];
  87 + // let _selectedData: roleModel | undefined;
  88 + // eslint-disable-next-line array-callback-return
  89 + res.child.map((item: any, index: number) => {
  90 + // if (!_selectedData && item.scopeList && item.scopeList.length > 0) {
  91 + // _selectedData = item.scopeList[0];
  92 + // _selectedData.pId = item.id;
  93 + // }
  94 + _extendKeys.push(item.id);
  95 + if (index === 0) {
  96 + if (item.child && item.child.length) {
  97 + const key: string[] = [item.child[0].id]
  98 + setSelectedKeys(key)
  99 + // @ts-ignore
  100 + props.onSelect(key)
  101 + }
  102 + }
  103 + });
  104 + // if (_selectedData) {
  105 + // setSelectedKeys([
  106 + // _selectedData.relType +
  107 + // ':' +
  108 + // (_selectedData.relType === 'USER' ? _selectedData.pId : _selectedData.id),
  109 + // ]);
  110 + // }
  111 + setExpandedKeys(_extendKeys);
  112 +
  113 + setData(res.child);
  114 + // setMenusData(generateMenus(res));
  115 +
  116 + setLoading(false);
  117 + }
  118 + });
  119 + }, []);
  120 +
  121 + useEffect(() => {
  122 + requestData();
  123 + }, []);
  124 +
  125 + useEffect(() => {
  126 + const onSelect = props.onSelect ? props.onSelect : () => {};
  127 + //console.log('selectedKeys', selectedKeys);
  128 + if (selectedKeys) {
  129 + onSelect(selectedKeys);
  130 + }
  131 + }, [props.onSelect, selectedKeys]);
  132 +
  133 + const handleSelect = (selectData: { selectedKeys: string[] }) => {
  134 + setSelectedKeys(selectData.selectedKeys);
  135 + };
  136 +
  137 + const filter = (word: string) => {
  138 + setKeywords(word)
  139 + // eslint-disable-next-line @typescript-eslint/no-unused-vars
  140 + const traverse = function (node: roleModel) {
  141 + node.visible = node.name.indexOf(word) > -1;
  142 + };
  143 +
  144 + // if (data) {
  145 + // const _data = _.cloneDeep(data);
  146 + // if (word != '') {
  147 + // _data.forEach((item) => {
  148 + // traverse(item);
  149 + // });
  150 + // }
  151 + // setMenusData(generateMenus(_data, word));
  152 + // }
  153 + };
  154 +
  155 + const handleSearch = (e: React.KeyboardEvent<HTMLInputElement>) => {
  156 + e.stopPropagation();
  157 + // @ts-ignore
  158 + filter(e.target.value.trim());
  159 + };
  160 + const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  161 + // @ts-ignore
  162 + if (e.type === 'click' && e.target.value === '' && data) {
  163 + //如果是清空
  164 + filter('');
  165 + }
  166 + };
  167 +
  168 + const renderText = (text: string) => {
  169 + let title = <> {text}</>;
  170 + if (keywords) {
  171 + const index = text.indexOf(keywords);
  172 + if (index > -1) {
  173 + title = (
  174 + <>
  175 + {text.substr(0, index)}
  176 + <span className={'qx-keywords-highlight'}>{keywords}</span>
  177 + {text.substr(index + keywords.length)}
  178 + </>
  179 + );
  180 + }
  181 + }
  182 + return title;
  183 + };
  184 +
  185 + return (
  186 + <div className={'qx-search-menus__wrap'}>
  187 + <Input
  188 + className={'qx-selector-sub-search'}
  189 + placeholder={props.placeholder || '请输入角色名称,按回车键搜索'}
  190 + allowClear
  191 + prefix={<SearchOutlined />}
  192 + onChange={(e) => {
  193 + handleChange(e);
  194 + }}
  195 + onPressEnter={(e) => {
  196 + handleSearch(e);
  197 + }}
  198 + />
  199 + <div className="qx-search-menus">
  200 + {expandedKeys ? (
  201 + <Menu
  202 + mode={'inline'}
  203 + onSelect={handleSelect}
  204 + selectedKeys={selectedKeys}
  205 + defaultOpenKeys={expandedKeys}
  206 + >
  207 + {data.map((item) => {
  208 + if (typeof item.visible === 'boolean' && !item.visible) {
  209 + return null;
  210 + }
  211 + if (item.child) {
  212 + return (
  213 + <Menu.SubMenu key={item.id} title={item.name}>
  214 + {item.child.map((child) => {
  215 + return typeof child.visible === 'boolean' && !child.visible ? null : (
  216 + <Menu.Item key={child.id}>
  217 + {renderText(child.name)}
  218 + </Menu.Item>
  219 + );
  220 + })}
  221 + </Menu.SubMenu>
  222 + );
  223 + }
  224 + return null;
  225 + })}
  226 + </Menu>
  227 + ) : null}
  228 + <Spin
  229 + style={{ width: '100%', marginTop: '40px' }}
  230 + spinning={loading}
  231 + // indicator={<LoadingOutlined style={{ fontSize: 24, marginTop: '40px' }} spin />}
  232 + />
  233 + {!loading && data.length === 0 ? <Empty style={{ paddingTop: '30px' }} /> : null}
  234 + </div>
  235 + </div>
  236 + );
  237 +};
  238 +
  239 +export default Role;
... ...
  1 +import * as React from 'react';
  2 +import { useEffect, useImperativeHandle, useRef, useState } from 'react';
  3 +import type { SearchUserData } from '../service';
  4 +import { searchUser } from '../service';
  5 +import { Checkbox, Empty, Input, Spin } from 'antd';
  6 +// import { InputRef } from 'antd/es/input';
  7 +import QxPagination from './qx-pagination';
  8 +import type { CheckboxChangeEvent } from 'antd/es/checkbox';
  9 +import { SearchOutlined } from '@ant-design/icons';
  10 +// import type { InputRef } from 'antd/lib/input';
  11 +
  12 +type UserItem = { id: string; name: string; code: string; relName?: string };
  13 +
  14 +type UserListProps = {
  15 + cRef: any;
  16 + max?: number; //最多选几个
  17 + onSelect: (selectedKeys: string[], selectedData: UserItem[]) => void;
  18 + request: any;
  19 +};
  20 +
  21 +const UserList: React.FC<UserListProps> = (props) => {
  22 + const [pageParams, setPageParams] = useState<SearchUserData>({
  23 + pageSize: 10,
  24 + pageNum: 1,
  25 + relType: 'ORG',
  26 + });
  27 +
  28 + const [userData, setUserData] = useState({ list: [], total: 0, pageNum: 0 });
  29 +
  30 + const [selectUsers, setSelectUsers] = useState<UserItem[]>([]);
  31 + const [selectIds, setSelectIds] = useState<string[]>([]);
  32 +
  33 + const [checkAll, setCheckAll] = useState<boolean>(false);
  34 +
  35 + const [loading, setLoading] = useState<boolean>(true);
  36 +
  37 + const inputRef = useRef<any>();
  38 +
  39 + useEffect(() => {}, []);
  40 +
  41 + const getUserData = (params: any) => {
  42 + setLoading(true);
  43 + const _pageParams = { ...pageParams, ...params };
  44 + setPageParams(_pageParams);
  45 + searchUser(props.request, _pageParams)
  46 + .then((res) => {
  47 + setUserData(res);
  48 + setLoading(false);
  49 + })
  50 + .catch(() => {
  51 + setLoading(false);
  52 + });
  53 + };
  54 +
  55 + // 暴露给父组件
  56 + useImperativeHandle(props.cRef, () => ({
  57 + focusSearch: () => {
  58 + setTimeout(() => {
  59 + inputRef.current.focus({ cursor: 'end' });
  60 + }, 200);
  61 + },
  62 + searchUser: (params: SearchUserData) => {
  63 + getUserData(params);
  64 + },
  65 + remove: (index: number) => {
  66 + const _selectedIds = [...selectIds];
  67 + const _selectUsers = [...selectUsers];
  68 + _selectedIds.splice(index, 1);
  69 + _selectUsers.splice(index, 1);
  70 + setSelectIds(_selectedIds);
  71 + setSelectUsers(_selectUsers);
  72 + },
  73 + setSelected: (ids: string[], users?: UserItem[]) => {
  74 + setSelectIds(ids);
  75 + if (users) {
  76 + setSelectUsers(users);
  77 + }
  78 + },
  79 + }));
  80 +
  81 + useEffect(() => {
  82 + if (!selectIds || selectIds.length < userData.list.length) {
  83 + setCheckAll(false);
  84 + } else {
  85 + setCheckAll(userData.list.some((item: UserItem) => selectIds.indexOf(item.id) > -1));
  86 + }
  87 + }, [selectIds, userData]);
  88 +
  89 + const pageUser = (pageNum: number, pageSize: number) => {
  90 + getUserData({ pageNum, pageSize });
  91 + };
  92 +
  93 + const onSelectAll = (e: CheckboxChangeEvent) => {
  94 + const checked = e.target.checked;
  95 + setCheckAll(checked);
  96 + const _selectedIds = [...selectIds];
  97 + const _selectUsers = [...selectUsers];
  98 + // eslint-disable-next-line array-callback-return
  99 + userData.list.map((item: UserItem) => {
  100 + const index = _selectedIds.indexOf(item.id);
  101 + if (checked) {
  102 + if (index === -1) {
  103 + _selectedIds.push(item.id);
  104 + _selectUsers.push(item);
  105 + }
  106 + } else if (index > -1) {
  107 + _selectedIds.splice(index, 1);
  108 + _selectUsers.splice(index, 1);
  109 + }
  110 + });
  111 + setSelectIds(_selectedIds);
  112 + setSelectUsers(_selectUsers);
  113 +
  114 + props.onSelect(_selectedIds, _selectUsers);
  115 + };
  116 +
  117 + const onSelectItem = (e: CheckboxChangeEvent, item: UserItem) => {
  118 + let _selectedIds = [...selectIds];
  119 + let _selectUsers = [...selectUsers];
  120 + if (props.max === 1) {
  121 + _selectedIds = [item.id];
  122 + _selectUsers = [item];
  123 + } else {
  124 + const index = _selectedIds.indexOf(item.id);
  125 + if (index > -1) {
  126 + _selectedIds.splice(index, 1);
  127 + _selectUsers.splice(index, 1);
  128 + } else {
  129 + _selectedIds.push(item.id);
  130 + _selectUsers.push(item);
  131 + }
  132 + }
  133 +
  134 + setSelectIds(_selectedIds);
  135 + setSelectUsers(_selectUsers);
  136 +
  137 + props.onSelect(_selectedIds, _selectUsers);
  138 + };
  139 +
  140 + const dealText = (
  141 + value: string,
  142 + maxLen: number,
  143 + beforeLen: number,
  144 + afterLen: number,
  145 + separator: string,
  146 + ) => {
  147 + if (value && value.length > maxLen) {
  148 + return value.substr(0, beforeLen) + separator + value.substr(value.length - afterLen);
  149 + }
  150 + return value;
  151 + };
  152 +
  153 + const userTitle = (user: UserItem) => {
  154 + const afterText = [];
  155 + afterText.push(dealText(user.code, 10, 3, 4, '...'));
  156 + if (user.relName) {
  157 + afterText.push(dealText(user.relName, 8, 4, 4, '...'));
  158 + }
  159 + return (
  160 + <div className={'qx-user-selector__item'}>
  161 + {user.name} <span style={{ color: '#666' }}> ({afterText.join(', ')})</span>
  162 + </div>
  163 + );
  164 + };
  165 + const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  166 + // @ts-ignore
  167 + if (e.type === 'click' && e.target.value === '') {
  168 + //如果是清空
  169 + getUserData({
  170 + pageNum: 1,
  171 + keywords: '',
  172 + });
  173 + }
  174 + };
  175 +
  176 + return (
  177 + <>
  178 + <Spin
  179 + style={{ width: '100%' }}
  180 + spinning={loading}
  181 + // indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
  182 + >
  183 + <div className={'qx-user-selector__right__header'}>
  184 + {props.max === 1 ? (
  185 + <span />
  186 + ) : (
  187 + <Checkbox checked={checkAll} onChange={onSelectAll}>
  188 + 全选
  189 + </Checkbox>
  190 + )}
  191 + <div className={`qx-selector-tab__search qx-selector-tab__search--expanded`}>
  192 + <Input
  193 + ref={inputRef}
  194 + placeholder="输入姓名或工号,回车搜索"
  195 + onPressEnter={(e) => {
  196 + // @ts-ignore
  197 + const value = e.target.value;
  198 + getUserData({
  199 + pageNum: 1,
  200 + keywords: value ? value.trim() : value,
  201 + });
  202 + }}
  203 + onChange={(e) => handleChange(e)}
  204 + allowClear
  205 + prefix={<SearchOutlined style={{ color: 'rgba(0,0,0,.45)' }} />}
  206 + />
  207 + </div>
  208 + <QxPagination
  209 + pageNum={(userData?.list?.length && userData?.pageNum) || 0}
  210 + pageSize={pageParams.pageSize}
  211 + total={userData.total}
  212 + onChange={pageUser}
  213 + />
  214 + </div>
  215 +
  216 + {userData.list.length > 0 ? (
  217 + <ul
  218 + className={`qx-user-selector__list ${
  219 + props.max === 1 ? 'qx-user-selector__list--radio' : null
  220 + }`}
  221 + >
  222 + {userData.list.map((user: UserItem) => (
  223 + <li key={user.id}>
  224 + <Checkbox
  225 + checked={selectIds.indexOf(user.id) > -1}
  226 + onChange={(e) => {
  227 + onSelectItem(e, user);
  228 + }}
  229 + >
  230 + {userTitle(user)}
  231 + </Checkbox>
  232 + </li>
  233 + ))}
  234 + </ul>
  235 + ) : null}
  236 + {!loading && userData.list.length === 0 ? <Empty style={{ marginTop: '100px' }} /> : null}
  237 + </Spin>
  238 + </>
  239 + );
  240 +};
  241 +
  242 +export default UserList;
... ...
  1 +import * as React from 'react';
  2 +import { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
  3 +import type { SearchUserData } from './service';
  4 +import { Col, Modal, Row, Tabs, Tag, Tooltip } from 'antd';
  5 +import { InfoCircleOutlined, UserOutlined } from '@ant-design/icons';
  6 +import OrgCore from '../../qx-org-selector/src/core';
  7 +import PosCore from '../../qx-pos-selector/src/core';
  8 +import GroupCore from '../../qx-group-selector/src/core';
  9 +import Role from './components/role';
  10 +import UserList from './components/user-list';
  11 +
  12 +export type UserItem = { id: string; name: string; code?: string; relName?: string };
  13 +
  14 +type UserSelectorDialogProps = {
  15 + appId?: string; //如果按角色查人,必须有appId
  16 + title?: string;
  17 + visible: boolean;
  18 + onCancel: () => void;
  19 + multiple?: boolean; //默认不是多选
  20 + max?: number;
  21 + onOk: (selectedKeys: string[], selectedData: UserItem[]) => void;
  22 + selectedData?: UserItem[]; //已选人员数据
  23 + params?: { org: any; pos: any; user: any; range: string[] } | null; //请求body参数
  24 + showRole?: boolean; //是否按角色筛选
  25 + request: any;
  26 + dRef?: any;
  27 + modalClassName?: string | undefined; // 弹框类名自定义 用于自定义以及覆盖样式
  28 +};
  29 +const SELECTOR_TABS = {
  30 + ORG: '按部门',
  31 + POSITION: '按岗位',
  32 + GROUP: '按群组',
  33 + ROLE: '按角色',
  34 +};
  35 +
  36 +const UserSelectorDialog: React.FC<UserSelectorDialogProps> = (props) => {
  37 + const [currentTab, setCurrentTab] = useState(props?.params?.range ? '' : 'ORG');
  38 + const [range, setRange] = useState({ org: true, pos: true, group: !props?.params?.range });
  39 +
  40 + const pageParamRef = useRef({
  41 + pageNum: 1,
  42 + relType: 'ORG',
  43 + relId: null,
  44 + });
  45 +
  46 + const userListRef = useRef({
  47 + // eslint-disable-next-line @typescript-eslint/no-unused-vars
  48 + searchUser: (data: SearchUserData) => {},
  49 + // eslint-disable-next-line @typescript-eslint/no-unused-vars
  50 + remove: (index: number, id: string) => {},
  51 + // eslint-disable-next-line @typescript-eslint/no-unused-vars
  52 + setSelected: (ids: string[], data?: UserItem[]) => {},
  53 + focusSearch: () => {},
  54 + });
  55 +
  56 + const [selectUsers, setSelectUsers] = useState<UserItem[]>([]);
  57 + const [selectIds, setSelectIds] = useState<string[]>([]);
  58 +
  59 + useEffect(() => {
  60 + if (props?.params?.range && props?.params?.range?.length > 0) {
  61 + let hasOrg = false;
  62 + let hasPos = false;
  63 + if (props?.params?.org) {
  64 + hasOrg = true;
  65 + }
  66 + if (props?.params?.pos) {
  67 + hasPos = true;
  68 + }
  69 +
  70 + if (!hasOrg && hasPos) {
  71 + setCurrentTab('POSITION');
  72 + } else {
  73 + setCurrentTab('ORG');
  74 + }
  75 +
  76 + setRange({ org: hasOrg, pos: hasPos, group: false });
  77 + }
  78 + }, [props?.params]);
  79 +
  80 + // useEffect(() => {
  81 + // if (props.selectedData) {
  82 + // const selectedData = [...props.selectedData];
  83 + // const keys: string[] = [];
  84 + // selectedData?.forEach((item) => {
  85 + // keys.push(item.id);
  86 + // });
  87 + // setSelectIds(keys);
  88 + // setSelectUsers(selectedData);
  89 +
  90 + // userListRef.current.setSelected(keys, selectedData);
  91 + // }
  92 + // }, [props.selectedData]);
  93 +
  94 + useEffect(() => {
  95 + if (props.visible) {
  96 + const selectedData = Array.isArray(props.selectedData) ? [...props.selectedData] : [];
  97 + const keys: string[] = [];
  98 + selectedData?.forEach((item) => {
  99 + keys.push(item.id);
  100 + });
  101 + setSelectIds(keys);
  102 + setSelectUsers(selectedData);
  103 + userListRef.current.setSelected(keys, selectedData);
  104 + userListRef.current.focusSearch();
  105 + }
  106 + }, [props.visible]);
  107 +
  108 + const getUserData = (params: any) => {
  109 + const _param = { ...pageParamRef.current, ...params };
  110 + pageParamRef.current = _param;
  111 + userListRef.current.searchUser(_param);
  112 + };
  113 +
  114 + const handleOk = () => {
  115 + props.onOk(selectIds, selectUsers);
  116 + };
  117 + const handleCancel = () => {
  118 + props.onCancel();
  119 + };
  120 +
  121 + const changeTabs = (activeKey: string | string[]) => {
  122 + if (typeof activeKey !== 'string') {
  123 + return;
  124 + }
  125 + setCurrentTab(activeKey);
  126 + };
  127 +
  128 + const handleSelectOrg = (keys: string[], selectedData: any[], include?: boolean) => {
  129 + getUserData({
  130 + pageNum: 1,
  131 + includeChild: !!include,
  132 + relType: 'ORG',
  133 + relId: keys[0] + '',
  134 + });
  135 + };
  136 +
  137 + const handleSelectPos = (keys: string[]) => {
  138 + getUserData({
  139 + pageNum: 1,
  140 + relType: 'POSITION',
  141 + relId: keys[0] + '',
  142 + });
  143 + };
  144 +
  145 + const handleSelectRole = (selectedKeys: string[]) => {
  146 + // const selectKey = selectedKeys[0];
  147 + // const keyArr = selectKey.split(':');
  148 + getUserData({
  149 + pageNum: 1,
  150 + relType: 'ROLE',
  151 + relId: selectedKeys[0],
  152 + });
  153 + };
  154 +
  155 + const handleSelectGroup = (selectedKeys: string[]) => {
  156 + getUserData({
  157 + pageNum: 1,
  158 + relType: 'GROUP',
  159 + relId: selectedKeys[0],
  160 + });
  161 + };
  162 +
  163 + const removeUser = (index: number) => {
  164 + const _selectUsersKey = [...selectIds];
  165 + const _selectUsers = [...selectUsers];
  166 + const key = _selectUsersKey.splice(index, 1);
  167 + _selectUsers.splice(index, 1);
  168 + setSelectIds(_selectUsersKey);
  169 + setSelectUsers(_selectUsers);
  170 +
  171 + userListRef.current.remove(index, key[0]);
  172 + };
  173 +
  174 + const dialogTitle = useMemo(() => {
  175 + return (
  176 + <div
  177 + style={{
  178 + display: 'flex',
  179 + alignItems: 'center',
  180 + justifyContent: 'space-between',
  181 + marginRight: '34px',
  182 + }}
  183 + >
  184 + <div>
  185 + <UserOutlined /> {props.title || '选择人员'}
  186 + </div>
  187 + <Tooltip
  188 + placement="top"
  189 + title={
  190 + <div>
  191 + 搜索规则
  192 + <br />
  193 + 1、在按部门状态下,搜索的是人员的:姓名,工号或岗位 <br />
  194 + 2、在按岗位的状态,搜索的是人员的姓名,工号和部门名称
  195 + <br />
  196 + {props.showRole ? (
  197 + <>
  198 + 3、按角色的状态下搜索的是人员的名称,工号和岗位名称,部门名称
  199 + <br />
  200 + </>
  201 + ) : null}
  202 + </div>
  203 + }
  204 + getPopupContainer={(triggerNode) => triggerNode}
  205 + >
  206 + <InfoCircleOutlined />
  207 + </Tooltip>
  208 + </div>
  209 + );
  210 + }, [props.title, props.showRole]);
  211 +
  212 + const onSelectUser = (keys: string[], data: UserItem[]) => {
  213 + setSelectIds(keys);
  214 + setSelectUsers(data);
  215 + };
  216 +
  217 + useImperativeHandle(props.dRef, () => ({
  218 + onSelectUser,
  219 + }));
  220 +
  221 + return props.visible ? (
  222 + <Modal
  223 + title={dialogTitle}
  224 + width={700}
  225 + visible={props.visible}
  226 + className={'qx-user-selector__dialog'}
  227 + onOk={handleOk}
  228 + onCancel={handleCancel}
  229 + wrapClassName={props?.modalClassName || ''}
  230 + >
  231 + <div className={'qx-user-selected__temp'}>
  232 + {selectUsers
  233 + ? selectUsers.map((user: UserItem, index: number) => {
  234 + if (!user.name) {
  235 + return null;
  236 + }
  237 + return (
  238 + <Tag color="blue" key={user.id} closable onClose={() => removeUser(index)}>
  239 + {user.name}
  240 + </Tag>
  241 + );
  242 + })
  243 + : null}
  244 + </div>
  245 + <div className={'qx-selector-tab'} style={{ position: 'relative' }}>
  246 + <Tabs activeKey={currentTab} centered onChange={changeTabs}>
  247 + {Object.keys(SELECTOR_TABS).map((key) => {
  248 + if (key === 'ORG' && !range.org) {
  249 + console.log();
  250 + return null;
  251 + }
  252 + if (key === 'POSITION' && !range.pos) {
  253 + return null;
  254 + }
  255 + if (key === 'GROUP' && !range.group) {
  256 + return null;
  257 + }
  258 + if (key === 'ROLE' && (typeof props.showRole === 'undefined' || !props.showRole)) {
  259 + return null;
  260 + }
  261 + return <Tabs.TabPane tab={SELECTOR_TABS[key]} key={key} />;
  262 + })}
  263 + </Tabs>
  264 + </div>
  265 + <Row className={'qx-user-selector__content'}>
  266 + <Col span={10} className="qx-user-selector__left">
  267 + {currentTab === 'ORG' && props.visible && range.org ? (
  268 + <OrgCore
  269 + request={props.request}
  270 + selectFirstNode
  271 + hasInclude={true}
  272 + params={props.params && props.params.org}
  273 + onSelect={handleSelectOrg}
  274 + />
  275 + ) : null}
  276 + {currentTab === 'POSITION' && props.visible ? (
  277 + <PosCore
  278 + request={props.request}
  279 + params={props.params && props.params.pos}
  280 + onSelect={handleSelectPos}
  281 + />
  282 + ) : null}
  283 + {currentTab === 'ROLE' && props.visible && range.group ? (
  284 + <Role
  285 + request={props.request}
  286 + params={{ appId: props.appId || '' }}
  287 + onSelect={handleSelectRole}
  288 + />
  289 + ) : null}
  290 + {currentTab === 'GROUP' && props.visible ? (
  291 + // <Role
  292 + // request={props.request}
  293 + // params={{ appId: props.appId || '' }}
  294 + // onSelect={handleSelectRole}
  295 + // />
  296 + <GroupCore
  297 + request={props.request}
  298 + params={{ appId: props.appId || '' }}
  299 + onSelect={handleSelectGroup}
  300 + />
  301 + ) : null}
  302 + </Col>
  303 + <Col span={14} className="qx-user-selector__right">
  304 + <UserList
  305 + request={props.request}
  306 + cRef={userListRef}
  307 + onSelect={onSelectUser}
  308 + max={props.max}
  309 + />
  310 + </Col>
  311 + </Row>
  312 + </Modal>
  313 + ) : null;
  314 +};
  315 +
  316 +export default UserSelectorDialog;
... ...
  1 +import React, { useEffect, useState, useImperativeHandle, useRef, useMemo } from 'react';
  2 +
  3 +import { Tag, Button, Popover } from 'antd';
  4 +import { UserAddOutlined } from '@ant-design/icons';
  5 +import './style.less';
  6 +import type { UserItem } from './dialog';
  7 +import UserSelectorDialog from './dialog';
  8 +import { searchUserByAllType, SearchUserAllData } from './service';
  9 +import _ from 'lodash';
  10 +
  11 +export type QxUserSelectorProps = {
  12 + cRef?: any;
  13 + onChange?: (data: string | string[], users?: UserItem[]) => void;
  14 + onMounted?: () => void;
  15 + defaultValue?: any;
  16 + disabled?: boolean;
  17 + multiple?: boolean;
  18 + max?: number;
  19 + readOnly?: boolean;
  20 + value?: string | string[];
  21 + defaultData?: UserItem | UserItem[];
  22 + params?: any; //请求body参数
  23 + request: any;
  24 +};
  25 +
  26 +type BaseUser = {
  27 + id: string;
  28 + name: string;
  29 +};
  30 +
  31 +/**
  32 + * 选人组件
  33 + * @param props
  34 + * @constructor
  35 + */
  36 +const QxUserSelector: React.FC<QxUserSelectorProps> = (props) => {
  37 + //弹框是否可见
  38 + const [visible, setVisible] = useState(false);
  39 + const [popVisible, setPopVisible] = useState(false);
  40 + const [popWidth, setPopWidth] = useState(100);
  41 + //常用联系人
  42 + const [favorites, setFavorites] = useState([]);
  43 + //模糊搜索到的人
  44 + const [userList, setUserList] = useState(null);
  45 + //已选择的人员信息
  46 + const [selectUsers, setSelectUsers] = useState<BaseUser[]>([]);
  47 + //存储人员信息,减少重复请求 {[id]:BaseUser}
  48 + const [selectTmpUsersMap, setTmpUsersMap] = useState<Record<string, BaseUser>>({});
  49 +
  50 + const [value, setValue] = useState<string | string[]>();
  51 +
  52 + const qxUserSelectorInputRef = useRef<HTMLDivElement>();
  53 + const inputRef = useRef<HTMLInputElement>();
  54 +
  55 + useEffect(() => {
  56 + setValue(props.defaultValue);
  57 + if (props?.onMounted) {
  58 + props?.onMounted();
  59 + }
  60 + }, []);
  61 + //console.log('QxUserSelector', props.value)
  62 +
  63 + useImperativeHandle(props.cRef, function () {
  64 + return {
  65 + // 暴露给父组件
  66 + clear: () => {
  67 + setSelectUsers([]);
  68 + setValue(undefined);
  69 + },
  70 + /*//方法触发增加人
  71 + addUsers: (users: BaseUser[]) => {
  72 + const addIds: string[] = selectUsers.map(user => user.id);
  73 + const waitUsers = users.filter((user) => {
  74 + return user.id && !addIds.includes(user.id);
  75 + });
  76 + setSelectUsers([...selectUsers, ...waitUsers]);
  77 + },*/
  78 + //设置人员
  79 + setUsers: (users: BaseUser[]) => {
  80 + if (JSON.stringify(users) === JSON.stringify(selectUsers)) {
  81 + return;
  82 + }
  83 + setSelectUsers(users);
  84 + const ids = users.map((user) => user.id);
  85 + if (JSON.stringify(ids) === JSON.stringify(value)) {
  86 + return;
  87 + }
  88 + props.onChange(ids, users);
  89 + },
  90 + };
  91 + });
  92 +
  93 + const userId = window.localStorage.getItem('userId');
  94 + const handleFavorites = (data?: UserItem[]) => {
  95 + //如果限制了选择范围,则不能选择常用联系人
  96 + if (props?.params?.range) {
  97 + return;
  98 + }
  99 +
  100 + if (!userId) {
  101 + return;
  102 + }
  103 +
  104 + const userName = window.localStorage.getItem('userName');
  105 + const cropCode = window.localStorage.getItem('corpCode');
  106 + const favoritesStorage = window.localStorage.getItem(`${cropCode}_${userId}_favorites`);
  107 + let _favoritesData = [];
  108 + let ids = [];
  109 + if (favoritesStorage) {
  110 + _favoritesData = JSON.parse(favoritesStorage);
  111 + ids = _favoritesData.map((item) => item?.data?.id);
  112 + }
  113 + if (data) {
  114 + data.forEach((item) => {
  115 + const index = ids.indexOf(item.id);
  116 + if (index > -1) {
  117 + const favoritesDatum = _favoritesData[index];
  118 + favoritesDatum.count += 1;
  119 + favoritesDatum.data = item;
  120 + _favoritesData.splice(index, 1);
  121 + _favoritesData.unshift(favoritesDatum);
  122 + } else {
  123 + _favoritesData.unshift({ count: 1, data: item });
  124 + }
  125 + });
  126 + }
  127 +
  128 + if (_favoritesData.length === 0 && userId) {
  129 + _favoritesData[0] = {
  130 + count: 1,
  131 + data: { id: userId, name: userName || '我自己' },
  132 + };
  133 + }
  134 + if (_favoritesData.length > 0) {
  135 + // _favoritesData.sort((a, b) => b.count - a.count);
  136 + _favoritesData = _favoritesData.splice(0, 5);
  137 + }
  138 +
  139 + window.localStorage.setItem(`${cropCode}_${userId}_favorites`, JSON.stringify(_favoritesData));
  140 + setFavorites(_favoritesData.map((item) => item.data).filter((o) => o.name !== '系统管理员')); // 如果有重名过滤怎么办 TODO
  141 + };
  142 +
  143 + //TODO 默认值待优化
  144 + useEffect(() => {
  145 + let _users: BaseUser[] = [];
  146 + const _maps = {};
  147 + let ids: string[];
  148 + if (props.defaultData) {
  149 + if (props.multiple && Array.isArray(props.defaultData)) {
  150 + _users = props.defaultData;
  151 + ids = [];
  152 + props.defaultData.map((item: BaseUser) => {
  153 + ids.push(item.id);
  154 + _maps[item.id] = item;
  155 + });
  156 + } else if (Array.isArray(props.defaultData)) {
  157 + _users = props.defaultData;
  158 + ids = [];
  159 + props.defaultData.map((item: BaseUser) => {
  160 + ids.push(item.id);
  161 + _maps[item.id] = item;
  162 + });
  163 + } else {
  164 + // @ts-ignore
  165 + _users = [props.defaultData];
  166 + // @ts-ignore
  167 + ids = props.defaultData.id;
  168 + }
  169 + }
  170 + setSelectUsers(_users);
  171 + setTmpUsersMap(_maps);
  172 + if (ids && ids.length > 0 && !props.value && props.onChange) {
  173 + props.onChange(ids, _users);
  174 + }
  175 +
  176 + handleFavorites();
  177 + }, [JSON.stringify(props.defaultData)]);
  178 +
  179 + useEffect(() => {
  180 + setValue(props.value);
  181 + if (!props.value) {
  182 + setSelectUsers([]);
  183 + }
  184 + }, [props.value]);
  185 +
  186 + // getUserList()
  187 + const handleOk = (keys: string[], data: UserItem[]) => {
  188 + let _value: string[] | string = keys;
  189 + if (!props.multiple && keys && keys.length > 0) {
  190 + _value = keys[0];
  191 + }
  192 + setValue(_value);
  193 + setSelectUsers([...data]);
  194 + setVisible(false);
  195 +
  196 + handleFavorites(data);
  197 +
  198 + if (props.onChange) {
  199 + props.onChange(_value, data);
  200 + }
  201 + };
  202 +
  203 + const handleAdd = (user: UserItem) => {
  204 + if ((value || []).indexOf(user?.id) > -1) {
  205 + return;
  206 + }
  207 + let _value: string | string[] = '';
  208 + let _selectUsers: UserItem[] = [];
  209 + if (props.multiple) {
  210 + if (props.max === 1) {
  211 + _value = [user.id];
  212 + _selectUsers = [user];
  213 + } else {
  214 + _value = value ? [...value, user.id] : [user.id];
  215 + _selectUsers = [...selectUsers, user];
  216 + }
  217 + } else {
  218 + _value = user.id;
  219 + _selectUsers = [user];
  220 + }
  221 + setValue(_value);
  222 + setSelectUsers(_selectUsers);
  223 +
  224 + handleFavorites([user]);
  225 +
  226 + if (props.onChange) {
  227 + props.onChange(_value, _selectUsers);
  228 + }
  229 + //setPopVisible(false);
  230 +
  231 + //如果是单选,则直接关闭pop
  232 + if (props.max === 1 || !props.multiple || typeof value === 'string') {
  233 + setPopVisible(false);
  234 + }
  235 + };
  236 +
  237 + const handleCancel = () => {
  238 + setVisible(false);
  239 + };
  240 +
  241 + const handleRemove = (index: number) => {
  242 + let _value: string | string[] = '';
  243 + let _selectUsers: UserItem[] = [];
  244 + if (props.multiple && Array.isArray(value)) {
  245 + _value = [...value];
  246 + _selectUsers = [...selectUsers];
  247 + _value.splice(index, 1);
  248 + _selectUsers.splice(index, 1);
  249 + }
  250 +
  251 + setValue(_value);
  252 + setSelectUsers(_selectUsers);
  253 +
  254 + if (props.onChange) {
  255 + props.onChange(_value, _selectUsers);
  256 + }
  257 + };
  258 +
  259 + useEffect(() => {
  260 + if (popVisible && qxUserSelectorInputRef?.current) {
  261 + setPopWidth(qxUserSelectorInputRef?.current?.clientWidth);
  262 + }
  263 + if (!popVisible && inputRef?.current) {
  264 + inputRef.current.value = '';
  265 + setUserList(null);
  266 + }
  267 + }, [popVisible]);
  268 +
  269 + const handleSearch = _.debounce((_keywords: string | undefined) => {
  270 + if (!_keywords) {
  271 + setUserList(null);
  272 + return;
  273 + }
  274 + const __keywords: string = _keywords.trim();
  275 + if (!__keywords) {
  276 + setUserList(null);
  277 + return;
  278 + }
  279 +
  280 + const params: SearchUserAllData = { pageSize: 5, keywords: __keywords };
  281 + if (props?.params?.range) {
  282 + params.range = props?.params?.range;
  283 + }
  284 + searchUserByAllType(props.request, params).then((res) => {
  285 + setUserList(res?.list || []);
  286 + });
  287 + }, 500);
  288 +
  289 + const userDropContent = useMemo(() => {
  290 + return (
  291 + <div className={'qx-user-selector--input__drop'} style={{ width: popWidth + 'px' }}>
  292 + <dl className={'qx-user-selector-pop-list'}>
  293 + {userList ? (
  294 + userList.length == 0 ? (
  295 + <dd className={'qx-user-selector-pop-empty'}>没有匹配到任何结果</dd>
  296 + ) : (
  297 + <>
  298 + <dt>您可能想找</dt>
  299 + {userList.map((item) => {
  300 + return (
  301 + <dd
  302 + key={item.id}
  303 + onClick={() => handleAdd(item)}
  304 + className={(value || []).indexOf(item.id) > -1 ? 'disabled' : null}
  305 + >
  306 + {item?.name}
  307 + {item?.code ? (
  308 + <span className={'qx-user-selector-code'}>({item?.code})</span>
  309 + ) : null}
  310 + </dd>
  311 + );
  312 + })}
  313 + </>
  314 + )
  315 + ) : null}
  316 + {favorites?.length > 0 ? (
  317 + <>
  318 + <dt>最近联系人</dt>
  319 + {favorites.map((item) => {
  320 + return (
  321 + <dd
  322 + key={item.id}
  323 + onClick={() => handleAdd(item)}
  324 + className={`text_over ${
  325 + (value || []).indexOf(item.id) > -1 ? 'disabled' : null
  326 + }`}
  327 + >
  328 + {item?.name}
  329 + {item?.code ? (
  330 + <span className={'qx-user-selector-code'}>({item?.code})</span>
  331 + ) : null}
  332 + </dd>
  333 + );
  334 + })}
  335 + </>
  336 + ) : null}
  337 + <a
  338 + className={'qx-user-selector-more ant-typography'}
  339 + style={{ paddingLeft: 10 }}
  340 + onClick={() => {
  341 + setPopVisible(false);
  342 + setVisible(true);
  343 + }}
  344 + >
  345 + 加载更多
  346 + </a>
  347 + </dl>
  348 + </div>
  349 + );
  350 + }, [popWidth, favorites, props?.params, userList, value]);
  351 +
  352 + const handleKeyDown = (e) => {
  353 + const { which } = e;
  354 + // Remove value by `backspace`
  355 + if (which === 8 && e.target.value === '') {
  356 + if (Array.isArray(value) && value.length > 0) {
  357 + const _value: string[] = [...value];
  358 + let _selectUsers: UserItem[] = [];
  359 +
  360 + _selectUsers = [...selectUsers];
  361 +
  362 + _value.pop();
  363 + _selectUsers.pop();
  364 +
  365 + setValue(_value);
  366 + setSelectUsers(_selectUsers);
  367 +
  368 + if (props.onChange) {
  369 + props.onChange(_value, _selectUsers);
  370 + }
  371 + } else if (!Array.isArray(value) && value) {
  372 + setValue([]);
  373 + setSelectUsers([]);
  374 +
  375 + if (props.onChange) {
  376 + props.onChange('');
  377 + }
  378 + }
  379 + }
  380 + };
  381 +
  382 + return (
  383 + <>
  384 + <Popover
  385 + content={userDropContent}
  386 + placement={'bottom'}
  387 + visible={
  388 + ((props?.params?.range || !userId) && !userList) || props.readOnly ? false : popVisible
  389 + }
  390 + trigger={'click'}
  391 + overlayClassName={'qx-user-selector--input__pop'}
  392 + onVisibleChange={(v: boolean) => setPopVisible(v)}
  393 + >
  394 + <div
  395 + className={
  396 + 'qx-user-selector--input ant-input' +
  397 + `${props.readOnly ? ' qx-user-selector--readonly' : ''}`
  398 + }
  399 + style={{ minHeight: '32px' }}
  400 + ref={qxUserSelectorInputRef}
  401 + onClick={() => setPopVisible(true)}
  402 + >
  403 + <div
  404 + className={
  405 + 'qx-user-selector--div ' + `${props?.readOnly ? '' : 'qx-user-selector-overflow'}`
  406 + }
  407 + >
  408 + {selectUsers.map((user: { name: string; id: string }, index: number) => {
  409 + if (!user.name) {
  410 + return null;
  411 + }
  412 + return (
  413 + <Tag
  414 + color={'blue'}
  415 + closable={!props.readOnly}
  416 + key={user.id}
  417 + onClose={() => handleRemove(index)}
  418 + style={{
  419 + maxWidth: `calc(100%)`,
  420 + height: '22px',
  421 + }}
  422 + >
  423 + <span
  424 + style={{
  425 + display: 'inline-block',
  426 + maxWidth: `calc(100% - ${!props.readOnly ? 15 : 0}px)`,
  427 + textOverflow: 'ellipsis',
  428 + overflow: 'hidden',
  429 + height: 20,
  430 + lineHeight: '20px',
  431 + }}
  432 + title={user.name}
  433 + >
  434 + {user.name}
  435 + </span>
  436 + </Tag>
  437 + );
  438 + })}
  439 + {props.readOnly ? null : (
  440 + <div className="qx-select-selection-search">
  441 + <input
  442 + ref={inputRef}
  443 + type="text"
  444 + disabled={props.readOnly || props.disabled}
  445 + className={'qx-user-input__box'}
  446 + placeholder={'搜索'}
  447 + onKeyDown={(e) => handleKeyDown(e)}
  448 + onChange={(e) => {
  449 + handleSearch(e.target?.value);
  450 + }}
  451 + onFocus={() => {
  452 + setPopVisible(true);
  453 + }}
  454 + onClick={(e) => {
  455 + e.stopPropagation();
  456 + }}
  457 + maxLength={20}
  458 + />
  459 + <Button
  460 + className={'qx-user-input__icon'}
  461 + size={'small'}
  462 + onClick={(e) => {
  463 + e.stopPropagation();
  464 + setPopVisible(false);
  465 + setVisible(true);
  466 + }}
  467 + type={'text'}
  468 + icon={<UserAddOutlined style={{ color: '#40A9FC' }} />}
  469 + />
  470 + </div>
  471 + )}
  472 + </div>
  473 + </div>
  474 + </Popover>
  475 + {!props.readOnly ? (
  476 + <UserSelectorDialog
  477 + key={visible + ''}
  478 + visible={visible}
  479 + multiple={props.multiple}
  480 + selectedData={selectUsers}
  481 + params={props.params}
  482 + request={props.request}
  483 + onOk={handleOk}
  484 + max={props.max}
  485 + onCancel={handleCancel}
  486 + />
  487 + ) : null}
  488 + </>
  489 + );
  490 +};
  491 +
  492 +export default QxUserSelector;
... ...
  1 +export type SearchUserData = {
  2 + keywords?: string;
  3 + pageNum: number;
  4 + pageSize: number;
  5 + relType: string;
  6 + relId?: string;
  7 + relIds?: string[];
  8 + includeChild?: boolean;
  9 +};
  10 +
  11 +type getUserData = {
  12 + pageNum?: number;
  13 + pageSize?: number;
  14 + keywords?: string;
  15 +};
  16 +
  17 +export type SearchUserAllData = {
  18 + pageNum?: number;
  19 + pageSize?: number;
  20 + keywords?: string;
  21 + range?: string[];
  22 +};
  23 +
  24 +/*获取选人组件待选人员*/
  25 +export function searchUserByAllType(request: any, data: SearchUserAllData) {
  26 + return request.post(`/qx-apaas-uc/selectUser/searchUserByAllType`, { data });
  27 +}
  28 +
  29 +/*获取选人组件待选人员*/
  30 +export function searchUser(request: any, data: SearchUserData) {
  31 + return request.post(`/qx-apaas-uc/selectUser/searchUser`, { data });
  32 +}
  33 +
  34 +/*根据appId获取角色下关联类型分组*/
  35 +export function getListAllRole(request: any, appId: string) {
  36 + return request.get(`/qx-apaas-uc/role/listAllRole/${appId}`);
  37 +}
  38 +
  39 +/*获取所有角色*/
  40 +export function getAllRole(request: any) {
  41 + return request.get(`/qx-apaas-uc/role/authTree`);
  42 +}
  43 +
  44 +/*根据角色获取人员*/
  45 +export function getUserByRole(request: any, data: getUserData) {
  46 + return request.post(`/qx-apaas-uc/roleScope/searchUser`, { data });
  47 +}
... ...
  1 +.qx-user-input__box {
  2 + padding-left: 0;
  3 + background-color: transparent;
  4 + border: none;
  5 + outline: none;
  6 +
  7 + &::placeholder {
  8 + color: #bfbfbf;
  9 + }
  10 +}
  11 +
  12 +.qx-user-selector--input {
  13 + &.ant-input {
  14 + box-sizing: border-box;
  15 + padding: 3px 32px 3px 11px;
  16 + .ant-tag {
  17 + margin: 1px 8px 1px 0;
  18 + vertical-align: top;
  19 + }
  20 + }
  21 +
  22 + &.qx-user-selector--readonly {
  23 + padding-right: 0;
  24 + padding-left: 0;
  25 + background-color: transparent;
  26 + border-color: transparent;
  27 +
  28 + &.ant-input:hover {
  29 + border-color: transparent;
  30 + }
  31 + }
  32 +}
  33 +
  34 +.qx-user-selector-overflow {
  35 + display: flex;
  36 + flex-wrap: wrap;
  37 + .ant-tag {
  38 + height: 22px;
  39 + margin: 1px 4px;
  40 + font-size: 0;
  41 + > span {
  42 + font-size: 12px;
  43 + }
  44 + > .anticon-close {
  45 + transform: translateY(-4px);
  46 + }
  47 + }
  48 +}
  49 +
  50 +.qx-user-input__icon {
  51 + position: absolute !important;
  52 + top: 50%;
  53 + right: 10px;
  54 + transform: translateY(-50%);
  55 +}
  56 +
  57 +.qx-select-selection-search {
  58 + flex: 1;
  59 + min-width: 52px;
  60 + overflow: hidden;
  61 +
  62 + .qx-user-input__box {
  63 + width: 100%;
  64 + }
  65 + &.qx-user-selector--readonly {
  66 + padding-right: 0;
  67 + padding-left: 0;
  68 + background-color: transparent;
  69 + border-color: transparent;
  70 + }
  71 +
  72 + .qx-user-selector--div > .ant-tag {
  73 + &:last-child {
  74 + margin-right: 0;
  75 + }
  76 + }
  77 +}
  78 +
  79 +.qx-user-selector--input__drop {
  80 + max-height: 300px;
  81 + margin-top: -10px;
  82 + padding: 10px;
  83 + overflow: auto;
  84 +}
  85 +.qx-user-selector--input__pop {
  86 + .ant-popover-arrow {
  87 + display: none;
  88 + }
  89 + .ant-popover-inner-content {
  90 + padding: 0;
  91 + }
  92 +}
  93 +
  94 +.qx-user-selector-pop-list {
  95 + margin-bottom: 0;
  96 + > dt {
  97 + margin-top: 5px;
  98 + margin-bottom: 7px;
  99 + padding-left: 2px;
  100 + color: #999;
  101 + font-weight: normal;
  102 + font-size: 14px;
  103 + border-bottom: 1px solid #efefef;
  104 + &:first-child {
  105 + margin-top: 0;
  106 + }
  107 + }
  108 + > dd {
  109 + margin: 0;
  110 + padding: 4px 10px;
  111 + color: #333;
  112 + font-size: 14px;
  113 + line-height: 1.2;
  114 + cursor: pointer;
  115 + &.qx-user-selector-more {
  116 + margin-top: 5px;
  117 + color: #4ba9ff;
  118 + &:hover {
  119 + color: #4ba9ff;
  120 + }
  121 + }
  122 + &.disabled {
  123 + color: #ccc;
  124 + cursor: default;
  125 + &:hover {
  126 + color: #ccc;
  127 + }
  128 + .qx-user-selector-code {
  129 + color: #ccc;
  130 + }
  131 + }
  132 +
  133 + &:hover {
  134 + color: #000;
  135 + background-color: #fafafa;
  136 + }
  137 + &.qx-user-selector-pop-empty {
  138 + padding: 10px;
  139 + color: #aaa;
  140 + text-align: center;
  141 + background-color: #fbfbfb;
  142 + cursor: default;
  143 + &:hover {
  144 + color: #aaa;
  145 + }
  146 + }
  147 + }
  148 +}
  149 +
  150 +.qx-user-selector-code {
  151 + color: rgb(102, 102, 102);
  152 +}
  153 +.qx-user-selector__dialog {
  154 + .ant-modal-body {
  155 + display: flex;
  156 + flex-direction: column;
  157 + padding: 0;
  158 +
  159 + > .ant-row {
  160 + flex: 1;
  161 + }
  162 + }
  163 +
  164 + .ant-tabs-tab-btn {
  165 + min-width: 60px;
  166 + text-align: center;
  167 + }
  168 +
  169 + .ant-tabs-tabpane-active {
  170 + padding-top: 0 !important;
  171 + }
  172 +
  173 + .ant-tabs-nav {
  174 + margin-bottom: 0;
  175 + }
  176 +}
  177 +
  178 +.qx-user-selected__temp {
  179 + //background-color: #fafafa;
  180 + display: flex;
  181 + flex-wrap: wrap;
  182 + align-items: center;
  183 + height: 60px;
  184 + padding: 5px;
  185 + overflow: auto;
  186 + border-bottom: 1px solid #f0f0f0;
  187 +
  188 + .ant-tag {
  189 + margin: 1px 2px;
  190 + }
  191 +}
  192 +
  193 +.qx-user-selector__content {
  194 + height: 380px;
  195 +}
  196 +
  197 +@contentHeight: 408px;
  198 +.qx-user-selector__left {
  199 + height: @contentHeight;
  200 + overflow: hidden;
  201 + border-right: 1px solid #f0f0f0;
  202 +
  203 + .qx-user-selector__collapse {
  204 + border: none;
  205 + border-radius: 0;
  206 +
  207 + .ant-collapse-content-box {
  208 + height: 440px;
  209 + overflow: auto;
  210 + }
  211 + }
  212 +
  213 + .ant-collapse-content {
  214 + border-color: #f0f0f0;
  215 + }
  216 +
  217 + .ant-collapse > .ant-collapse-item {
  218 + border-color: #f0f0f0;
  219 + }
  220 +}
  221 +
  222 +.qx-user-selector__right {
  223 + display: flex;
  224 + flex-direction: column;
  225 + height: @contentHeight;
  226 + border-left: 1px solid #f0f0f0;
  227 + border-left: none;
  228 +
  229 + &__header {
  230 + display: flex;
  231 + align-items: center;
  232 + justify-content: space-between;
  233 + height: 40px;
  234 + padding: 0 10px;
  235 + border-bottom: 1px solid #f0f0f0;
  236 +
  237 + .ant-checkbox-wrapper {
  238 + white-space: nowrap;
  239 + }
  240 + }
  241 +
  242 + > .ant-table-wrapper {
  243 + flex: 1;
  244 + }
  245 +
  246 + .ant-spin-nested-loading,
  247 + .ant-spin-container {
  248 + height: 100%;
  249 + }
  250 +
  251 + .ant-spin-container {
  252 + display: flex;
  253 + flex-direction: column;
  254 + }
  255 +
  256 + .ant-table {
  257 + flex: 1;
  258 + }
  259 +
  260 + .ant-table-pagination.ant-pagination {
  261 + margin: 0;
  262 + padding: 11px;
  263 + border-top: 1px solid #f0f0f0;
  264 + }
  265 +}
  266 +
  267 +.qx-user-selector__user-search {
  268 + padding: 7px 10px;
  269 + border-bottom: 1px solid #f0f0f0;
  270 +}
  271 +
  272 +.qx-selector-tab {
  273 + position: relative;
  274 +
  275 + &__search {
  276 + /*
  277 + position: absolute;
  278 + */
  279 + display: flex;
  280 + align-items: center;
  281 + width: 6%;
  282 + padding: 0 15px 0 0;
  283 + background-color: #fff;
  284 + cursor: pointer;
  285 +
  286 + .ant-input-prefix {
  287 + margin-right: 10px;
  288 + }
  289 +
  290 + .ant-input-affix-wrapper {
  291 + padding-left: 0;
  292 + border: none;
  293 +
  294 + &-focus,
  295 + &-focused {
  296 + box-shadow: none;
  297 + }
  298 + }
  299 +
  300 + &.qx-selector-tab__search--expanded {
  301 + flex: 1;
  302 +
  303 + > .anticon {
  304 + visibility: visible;
  305 + opacity: 0.25;
  306 +
  307 + &:hover {
  308 + opacity: 0.75;
  309 + }
  310 + }
  311 + }
  312 +
  313 + > .anticon {
  314 + visibility: hidden;
  315 + }
  316 + }
  317 +}
  318 +
  319 +.qx-user-selector__list {
  320 + flex: 1;
  321 + margin: 0;
  322 + padding: 0;
  323 + overflow: auto;
  324 +
  325 + &--radio {
  326 + .ant-checkbox-inner {
  327 + border-radius: 50%;
  328 + }
  329 +
  330 + .ant-checkbox-checked::after {
  331 + border-radius: 50%;
  332 + }
  333 + }
  334 +
  335 + > li {
  336 + padding: 0 10px;
  337 + line-height: 36px;
  338 + white-space: nowrap;
  339 + cursor: pointer;
  340 +
  341 + &:hover {
  342 + background-color: #fafafa;
  343 + }
  344 +
  345 + .ant-checkbox-wrapper,
  346 + .ant-checkbox + span {
  347 + width: 98%;
  348 + }
  349 + }
  350 +}
  351 +
  352 +.qx-user-selector__item {
  353 + //text-overflow: ellipsis;
  354 +}
  355 +
  356 +.qx-selector-sub-search {
  357 + &.ant-input-affix-wrapper {
  358 + height: 40px;
  359 + border: none;
  360 + border-radius: 0;
  361 +
  362 + &-focus,
  363 + &-focused {
  364 + box-shadow: none;
  365 + }
  366 + }
  367 +
  368 + .ant-input-prefix {
  369 + opacity: 0.25;
  370 + }
  371 +
  372 + .ant-input {
  373 + height: 32px;
  374 + border: none !important;
  375 + //border-bottom: 1px solid #f0f0f0 !important;
  376 + border-radius: 0;
  377 + box-shadow: none;
  378 + }
  379 +}
  380 +
  381 +.text_over {
  382 + overflow: hidden;
  383 + white-space: nowrap;
  384 + text-overflow: ellipsis;
  385 +}
... ...