Showing
25 changed files
with
3509 additions
and
0 deletions
src/qx-group-selector/src/core.tsx
0 → 100644
| 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; | ... | ... |
src/qx-group-selector/src/service.ts
0 → 100644
src/qx-org-selector/index.ts
0 → 100644
| 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; | ... | ... |
src/qx-org-selector/src/core.less
0 → 100644
| 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 | +} | ... | ... |
src/qx-org-selector/src/core.tsx
0 → 100644
| 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; | ... | ... |
src/qx-org-selector/src/dialog.tsx
0 → 100644
| 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; | ... | ... |
src/qx-org-selector/src/input.tsx
0 → 100644
| 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; | ... | ... |
src/qx-org-selector/src/service.ts
0 → 100644
src/qx-org-selector/src/style.less
0 → 100644
| 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 | +} | ... | ... |
src/qx-pos-selector/index.ts
0 → 100644
| 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; | ... | ... |
src/qx-pos-selector/src/core.tsx
0 → 100644
| 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; | ... | ... |
src/qx-pos-selector/src/dialog.tsx
0 → 100644
| 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; | ... | ... |
src/qx-pos-selector/src/input.tsx
0 → 100644
| 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; | ... | ... |
src/qx-pos-selector/src/service.ts
0 → 100644
src/qx-pos-selector/src/style.less
0 → 100644
| 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 | +} | ... | ... |
src/qx-user-selector/index.md
0 → 100644
| 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 | | | ... | ... |
src/qx-user-selector/index.ts
0 → 100644
| 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 | +} | ... | ... |
src/qx-user-selector/src/components/role.tsx
0 → 100644
| 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; | ... | ... |
src/qx-user-selector/src/dialog.tsx
0 → 100644
| 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; | ... | ... |
src/qx-user-selector/src/input.tsx
0 → 100644
| 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; | ... | ... |
src/qx-user-selector/src/service.ts
0 → 100644
| 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 | +} | ... | ... |
src/qx-user-selector/src/style.less
0 → 100644
| 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 | +} | ... | ... |