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 | +} | ... | ... |