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