Showing
7 changed files
with
519 additions
and
0 deletions
@@ -6,6 +6,8 @@ export * from './qx-tags-input'; | @@ -6,6 +6,8 @@ export * from './qx-tags-input'; | ||
6 | export * from './qx-user-selector'; | 6 | export * from './qx-user-selector'; |
7 | export * from './qx-org-selector'; | 7 | export * from './qx-org-selector'; |
8 | export * from './qx-form-select'; | 8 | export * from './qx-form-select'; |
9 | +export * from './qx-pos-selector'; | ||
10 | +export * from './qx-role-selector'; | ||
9 | export * from './qx-app-selector'; | 11 | export * from './qx-app-selector'; |
10 | export * from './utils'; | 12 | export * from './utils'; |
11 | export * from './qx-field'; | 13 | export * from './qx-field'; |
src/qx-role-selector/index.ts
0 → 100644
1 | +import QxRoleSelectorInput from './src/input'; | ||
2 | +import QxRoleSelectorDialog from './src/dialog'; | ||
3 | +import React from 'react'; | ||
4 | + | ||
5 | +interface QxRoleSelectorType extends React.FC { | ||
6 | + Dialog: typeof QxRoleSelectorDialog; | ||
7 | +} | ||
8 | + | ||
9 | +export const QxRoleSelector = QxRoleSelectorInput as QxRoleSelectorType; | ||
10 | + | ||
11 | +QxRoleSelector.Dialog = QxRoleSelectorDialog; |
src/qx-role-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, Radio, Spin } from 'antd'; | ||
4 | +import Menu from 'antd/es/menu'; | ||
5 | +import { SearchOutlined } from '@ant-design/icons'; | ||
6 | +import { getRoles, getRolesByAppCode } from './service'; | ||
7 | +import { cloneDeep } from "lodash-es"; | ||
8 | +// import _ from 'lodash'; | ||
9 | + | ||
10 | +type PosCoreProps = { | ||
11 | + appId?: string; | ||
12 | + cRef?: any; | ||
13 | + multiple?: boolean; | ||
14 | + placeholder?: string; | ||
15 | + params?: any; | ||
16 | + onSelect?: (selectedKeys: string[], selectedData: RoleModel[]) => void; | ||
17 | + request: any; | ||
18 | + isRole?: boolean; // 是否为设计时角色权限 | ||
19 | + appCode?: string; | ||
20 | +}; | ||
21 | + | ||
22 | +export interface RoleModel { | ||
23 | + id: string; | ||
24 | + name: string; | ||
25 | + appId?: string; | ||
26 | + code: string; | ||
27 | + visible?: boolean; | ||
28 | + disabled?: boolean; | ||
29 | + child: RoleModel[]; | ||
30 | +} | ||
31 | + | ||
32 | +const RoleSelCore: React.FC<PosCoreProps> = ({ | ||
33 | + appId, | ||
34 | + cRef, | ||
35 | + multiple, | ||
36 | + placeholder, | ||
37 | + onSelect, | ||
38 | + isRole, | ||
39 | + appCode, | ||
40 | + ...props | ||
41 | +}) => { | ||
42 | + const [loading, setLoading] = useState<boolean>(true); | ||
43 | + | ||
44 | + const [data, setData] = useState<RoleModel[]>([]); | ||
45 | + | ||
46 | + const [selectedData, setSelectedData] = useState<RoleModel[]>([]); | ||
47 | + const [selectedKeys, setSelectedKeys] = useState<string[]>(); | ||
48 | + const [expandedKeys, setExpandedKeys] = useState<string[]>([]); | ||
49 | + const [keywords, setKeywords] = useState<string>(''); | ||
50 | + | ||
51 | + const requestData = useCallback(() => { | ||
52 | + setLoading(true); | ||
53 | + let _getRole = getRoles; | ||
54 | + if (isRole) { | ||
55 | + _getRole = getRolesByAppCode; | ||
56 | + } | ||
57 | + // todo 当前为指定appCode,待接口修改为appId传入(不用appId了) | ||
58 | + _getRole(props.request, appCode || '').then((res) => { | ||
59 | + const roles: RoleModel[] = res.child || []; | ||
60 | + if (roles) { | ||
61 | + const _expandedKeys: string[] = []; | ||
62 | + roles.forEach((item) => { | ||
63 | + _expandedKeys.push(item.id); | ||
64 | + }); | ||
65 | + setExpandedKeys(_expandedKeys); | ||
66 | + setData(roles); | ||
67 | + setLoading(false); | ||
68 | + } | ||
69 | + }); | ||
70 | + }, []); | ||
71 | + | ||
72 | + useEffect(() => { | ||
73 | + requestData(); | ||
74 | + }, [requestData]); | ||
75 | + | ||
76 | + useEffect(() => { | ||
77 | + const isOnSelect = onSelect ? onSelect : () => { }; | ||
78 | + if (selectedKeys) { | ||
79 | + isOnSelect(selectedKeys, selectedData); | ||
80 | + } | ||
81 | + }, [onSelect, selectedKeys, selectedData]); | ||
82 | + | ||
83 | + const handleSelect = (selectData: { selectedKeys: string[] }) => { | ||
84 | + //单选走这里 | ||
85 | + if (!multiple) { | ||
86 | + setSelectedKeys(selectData.selectedKeys); | ||
87 | + } | ||
88 | + }; | ||
89 | + useImperativeHandle(cRef, () => ({ | ||
90 | + // 暴露给父组件 | ||
91 | + remove: (index: number) => { | ||
92 | + let _selectedKeys: string[] = []; | ||
93 | + let _selectedData: RoleModel[] = []; | ||
94 | + if (selectedKeys && selectedKeys.length > 0) { | ||
95 | + _selectedKeys = [...selectedKeys]; | ||
96 | + _selectedData = [...selectedData]; | ||
97 | + _selectedKeys.splice(index, 1); | ||
98 | + _selectedData.splice(index, 1); | ||
99 | + setSelectedData(_selectedData); | ||
100 | + setSelectedKeys(_selectedKeys); | ||
101 | + } | ||
102 | + }, | ||
103 | + emptySelect: () => { | ||
104 | + setSelectedData([]); | ||
105 | + setSelectedKeys([]); | ||
106 | + }, | ||
107 | + })); | ||
108 | + const handleMultiSelect = (checked: boolean, item: RoleModel) => { | ||
109 | + let _selectedKeys: string[] = []; | ||
110 | + let _selectedData: RoleModel[] = []; | ||
111 | + if (selectedKeys) { | ||
112 | + _selectedKeys = [...selectedKeys]; | ||
113 | + _selectedData = [...selectedData]; | ||
114 | + } | ||
115 | + // console.log(checked, item.id, _selectedKeys); | ||
116 | + | ||
117 | + if (checked) { | ||
118 | + _selectedKeys.push(item.id); | ||
119 | + _selectedData.push(item); | ||
120 | + } else { | ||
121 | + const index = _selectedKeys.indexOf(item.id); | ||
122 | + if (index > -1) { | ||
123 | + _selectedKeys.splice(index, 1); | ||
124 | + _selectedData.splice(index, 1); | ||
125 | + } | ||
126 | + } | ||
127 | + | ||
128 | + setSelectedData(_selectedData); | ||
129 | + setSelectedKeys(_selectedKeys); | ||
130 | + }; | ||
131 | + | ||
132 | + //多选走这里 | ||
133 | + const filter = (word: string) => { | ||
134 | + setKeywords(word); | ||
135 | + const traverse = function (node: any) { | ||
136 | + const childNodes = node.child || []; | ||
137 | + | ||
138 | + childNodes.forEach((child) => { | ||
139 | + child.visible = child.name.indexOf(word) > -1; | ||
140 | + | ||
141 | + traverse(child); | ||
142 | + }); | ||
143 | + | ||
144 | + if (!node.visible && childNodes.length) { | ||
145 | + node.visible = childNodes.some((child) => child.visible); | ||
146 | + } | ||
147 | + }; | ||
148 | + | ||
149 | + if (data) { | ||
150 | + const _data = cloneDeep(data); | ||
151 | + _data.forEach((item) => { | ||
152 | + traverse(item); | ||
153 | + }); | ||
154 | + setData(_data); | ||
155 | + } | ||
156 | + }; | ||
157 | + | ||
158 | + const handleSearch = (e: React.KeyboardEvent<HTMLInputElement>) => { | ||
159 | + e.stopPropagation(); | ||
160 | + // @ts-ignore | ||
161 | + filter(e.target.value.trim()); | ||
162 | + }; | ||
163 | + const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
164 | + // @ts-ignore | ||
165 | + if (e.type === 'click' && e.target.value === '' && data) { | ||
166 | + //如果是清空 | ||
167 | + filter(''); | ||
168 | + } | ||
169 | + }; | ||
170 | + | ||
171 | + const renderText = (text: string) => { | ||
172 | + let title = <> {text}</>; | ||
173 | + if (keywords) { | ||
174 | + const index = text.indexOf(keywords); | ||
175 | + if (index > -1) { | ||
176 | + title = ( | ||
177 | + <> | ||
178 | + {text.substr(0, index)} | ||
179 | + <span className={'qx-keywords-highlight'}>{keywords}</span> | ||
180 | + {text.substr(index + keywords.length)} | ||
181 | + </> | ||
182 | + ); | ||
183 | + } | ||
184 | + } | ||
185 | + return title; | ||
186 | + }; | ||
187 | + | ||
188 | + return ( | ||
189 | + <div className={'qx-search-menus__wrap'}> | ||
190 | + <Input | ||
191 | + // style={{ display: 'none' }} | ||
192 | + className={'qx-selector-sub-search'} | ||
193 | + placeholder={placeholder || '请输入角色名称,按回车键搜索'} | ||
194 | + allowClear | ||
195 | + prefix={<SearchOutlined />} | ||
196 | + onChange={(e) => { | ||
197 | + handleChange(e); | ||
198 | + }} | ||
199 | + onPressEnter={(e) => { | ||
200 | + handleSearch(e); | ||
201 | + }} | ||
202 | + /> | ||
203 | + <div className="qx-search-menus"> | ||
204 | + {expandedKeys.length ? ( | ||
205 | + <Menu | ||
206 | + mode={'inline'} | ||
207 | + onSelect={handleSelect} | ||
208 | + selectedKeys={multiple ? [] : selectedKeys} | ||
209 | + multiple={!!multiple} | ||
210 | + defaultOpenKeys={expandedKeys} | ||
211 | + > | ||
212 | + {data.map((item) => { | ||
213 | + if (typeof item.visible === 'boolean' && !item.visible) { | ||
214 | + return null; | ||
215 | + } | ||
216 | + if (item.child) { | ||
217 | + return ( | ||
218 | + <Menu.SubMenu key={item.id} title={item.name}> | ||
219 | + {item.child.map((child) => { | ||
220 | + return typeof child.visible === 'boolean' && !child.visible ? null : ( | ||
221 | + <Menu.Item key={child.id} disabled={child.disabled}> | ||
222 | + {multiple ? ( | ||
223 | + <Checkbox | ||
224 | + checked={selectedKeys && selectedKeys.indexOf(child.id) > -1} | ||
225 | + disabled={child.disabled} | ||
226 | + onChange={(e) => { | ||
227 | + handleMultiSelect(e.target.checked, child); | ||
228 | + }} | ||
229 | + > | ||
230 | + {renderText(child.name)} | ||
231 | + </Checkbox> | ||
232 | + ) : isRole ? ( | ||
233 | + <Radio | ||
234 | + checked={ | ||
235 | + selectedKeys?.length && selectedKeys[0].indexOf(child.id) > -1 | ||
236 | + } | ||
237 | + disabled={child.disabled} | ||
238 | + > | ||
239 | + {renderText(child.name)} | ||
240 | + </Radio> | ||
241 | + ) : ( | ||
242 | + renderText(child.name) | ||
243 | + )} | ||
244 | + </Menu.Item> | ||
245 | + ); | ||
246 | + })} | ||
247 | + </Menu.SubMenu> | ||
248 | + ); | ||
249 | + } | ||
250 | + return null; | ||
251 | + })} | ||
252 | + </Menu> | ||
253 | + ) : null} | ||
254 | + | ||
255 | + <Spin | ||
256 | + spinning={loading} | ||
257 | + style={{ width: '100%', marginTop: '40px' }} | ||
258 | + // indicator={<LoadingOutlined style={{ fontSize: 24, marginTop: '40px' }} spin />} | ||
259 | + /> | ||
260 | + {!loading && data.length === 0 ? <Empty style={{ paddingTop: '30px' }} /> : null} | ||
261 | + </div> | ||
262 | + </div> | ||
263 | + ); | ||
264 | +}; | ||
265 | + | ||
266 | +export default RoleSelCore; |
src/qx-role-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 { RoleModel } from './core'; | ||
6 | +import RoleSelCore from './core'; | ||
7 | +// import { RequestMethod } from 'umi-request'; | ||
8 | + | ||
9 | +type RoleSelectorDialogProps = { | ||
10 | + appId?: string; | ||
11 | + title?: string; | ||
12 | + visible: boolean; | ||
13 | + onCancel: () => void; | ||
14 | + data?: []; | ||
15 | + multiple?: boolean; | ||
16 | + onOk: (selectedKeys: string[], selectedData: RoleModel[]) => void; | ||
17 | + request: AnalyserOptions; | ||
18 | + isRole?: boolean; // 是否为设计时角色权限 | ||
19 | + appCode?: string; | ||
20 | +}; | ||
21 | + | ||
22 | +const RoleSelectorDialog: React.FC<RoleSelectorDialogProps> = (props) => { | ||
23 | + const [selectedData, setSelectedData] = useState<RoleModel[]>([]); | ||
24 | + const [selectedKeys, setSelectedKeys] = useState<string[]>([]); | ||
25 | + const coreRef = useRef({ | ||
26 | + // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
27 | + remove: (index: number) => { | ||
28 | + // console.log(index) | ||
29 | + }, | ||
30 | + emptySelect: () => {}, | ||
31 | + }); | ||
32 | + | ||
33 | + const handleOk = () => { | ||
34 | + props.onOk(selectedKeys, selectedData); | ||
35 | + coreRef.current.emptySelect(); | ||
36 | + setSelectedKeys([]); | ||
37 | + setSelectedData([]); | ||
38 | + }; | ||
39 | + const handleCancel = () => { | ||
40 | + props.onCancel(); | ||
41 | + coreRef.current.emptySelect(); | ||
42 | + setSelectedKeys([]); | ||
43 | + setSelectedData([]); | ||
44 | + }; | ||
45 | + | ||
46 | + const handleSelectPos = (keys: string[], datas: RoleModel[]) => { | ||
47 | + setSelectedKeys(keys); | ||
48 | + setSelectedData(datas); | ||
49 | + }; | ||
50 | + | ||
51 | + const handleRemove = (index: number) => { | ||
52 | + coreRef.current.remove(index); | ||
53 | + }; | ||
54 | + | ||
55 | + return ( | ||
56 | + <Modal | ||
57 | + title={props.title || '选择角色'} | ||
58 | + width={560} | ||
59 | + visible={props.visible} | ||
60 | + className={'qx-role-selector__dialog'} | ||
61 | + onOk={handleOk} | ||
62 | + onCancel={handleCancel} | ||
63 | + > | ||
64 | + {props.multiple ? ( | ||
65 | + <div className={'qx-role-selected__temp'}> | ||
66 | + {(selectedData || []).map((item: RoleModel, index: number) => ( | ||
67 | + <Tag closable color={'blue'} key={item.id} onClose={() => handleRemove(index)}> | ||
68 | + {item.name} | ||
69 | + </Tag> | ||
70 | + ))} | ||
71 | + </div> | ||
72 | + ) : null} | ||
73 | + <div className={'qx-role-selector__content'}> | ||
74 | + {props.visible ? ( | ||
75 | + <RoleSelCore | ||
76 | + request={props.request} | ||
77 | + cRef={coreRef} | ||
78 | + // multiple | ||
79 | + multiple={!props?.isRole} | ||
80 | + onSelect={handleSelectPos} | ||
81 | + appId={props.appId} | ||
82 | + isRole={props?.isRole} | ||
83 | + appCode={props?.appCode} | ||
84 | + /> | ||
85 | + ) : null} | ||
86 | + </div> | ||
87 | + </Modal> | ||
88 | + ); | ||
89 | +}; | ||
90 | + | ||
91 | +export default RoleSelectorDialog; |
src/qx-role-selector/src/input.tsx
0 → 100644
1 | +import React, { useEffect, useState } from 'react'; | ||
2 | +import { Checkbox, Input, Tag } from 'antd'; | ||
3 | +import { ApartmentOutlined } from '@ant-design/icons'; | ||
4 | +import PosSelectorDialog from './dialog'; | ||
5 | +// import { RequestMethod } from 'umi-request'; | ||
6 | + | ||
7 | +export type QxRoleSelectorProps = { | ||
8 | + onChange?: (data: any) => void; | ||
9 | + defaultValue?: any; | ||
10 | + disabled?: boolean; | ||
11 | + multiple?: boolean; | ||
12 | + readOnly?: boolean; | ||
13 | + name?: string; | ||
14 | + data?: []; | ||
15 | + request: any; | ||
16 | +}; | ||
17 | + | ||
18 | +/** | ||
19 | + * 表单设计器(XRender) | ||
20 | + * @constructor | ||
21 | + */ | ||
22 | +const QxRoleSelector: React.FC<QxRoleSelectorProps> = (props) => { | ||
23 | + const [selectOrgs, setSelectOrgs] = useState([]); | ||
24 | + const [visible, setVisible] = useState(false); | ||
25 | + const [value, setValue] = useState<string | string[]>(); | ||
26 | + | ||
27 | + useEffect(() => { | ||
28 | + setValue(props.defaultValue); | ||
29 | + }, []); | ||
30 | + | ||
31 | + // getUserList() | ||
32 | + const handleOk = (keys: [], data: []) => { | ||
33 | + let _value: string[] | string = keys; | ||
34 | + if (!props.multiple && keys && keys.length > 0) { | ||
35 | + // @ts-ignore | ||
36 | + _value = keys[0]; | ||
37 | + } | ||
38 | + setValue(_value); | ||
39 | + setSelectOrgs(data); | ||
40 | + setVisible(false); | ||
41 | + if (props.onChange) { | ||
42 | + props.onChange(_value); | ||
43 | + } | ||
44 | + }; | ||
45 | + | ||
46 | + const handleCancel = () => { | ||
47 | + setVisible(false); | ||
48 | + }; | ||
49 | + const handleRemove = (index: number) => { | ||
50 | + let _value: string | string[] = ''; | ||
51 | + let _selected: string[] = []; | ||
52 | + if (props.multiple) { | ||
53 | + _value = [...value]; | ||
54 | + _selected = [...selectOrgs]; | ||
55 | + _value.splice(index, 1); | ||
56 | + _selected.splice(index, 1); | ||
57 | + } | ||
58 | + | ||
59 | + setValue(_value); | ||
60 | + setSelectOrgs(_selected); | ||
61 | + | ||
62 | + if (props.onChange) { | ||
63 | + props.onChange(_value); | ||
64 | + } | ||
65 | + }; | ||
66 | + | ||
67 | + return ( | ||
68 | + <> | ||
69 | + {props.name ? ( | ||
70 | + props.multiple && typeof value !== 'string' ? ( | ||
71 | + <Checkbox.Group name={props.name} value={value} style={{ display: 'none' }} /> | ||
72 | + ) : ( | ||
73 | + <Input style={{ display: 'none' }} value={value} /> | ||
74 | + ) | ||
75 | + ) : null} | ||
76 | + <div | ||
77 | + className={'qx-user-selector ant-input'} | ||
78 | + style={{ minHeight: '33px' }} | ||
79 | + onClick={() => setVisible(true)} | ||
80 | + > | ||
81 | + <ApartmentOutlined style={{ paddingRight: '5px', color: '#999' }} /> | ||
82 | + {selectOrgs.map((org: { title: string; key: string }, index) => ( | ||
83 | + <Tag closable color={'blue'} key={org.key} onClose={() => handleRemove(index)}> | ||
84 | + {org.title} | ||
85 | + </Tag> | ||
86 | + ))} | ||
87 | + </div> | ||
88 | + {!props.readOnly && ( | ||
89 | + <PosSelectorDialog | ||
90 | + visible={visible} | ||
91 | + multiple={props.multiple} | ||
92 | + data={props.data} | ||
93 | + onOk={handleOk} | ||
94 | + request={props.request} | ||
95 | + onCancel={handleCancel} | ||
96 | + /> | ||
97 | + )} | ||
98 | + </> | ||
99 | + ); | ||
100 | +}; | ||
101 | + | ||
102 | +export default QxRoleSelector; |
src/qx-role-selector/src/service.ts
0 → 100644
1 | +/** | ||
2 | + * 获取选人组件中的角色 | ||
3 | + */ | ||
4 | +export function getRoles(request: any, appCode: string) { | ||
5 | + return request.get(`/qx-apaas-uc/role/authTree`); | ||
6 | +} | ||
7 | + | ||
8 | +/** | ||
9 | + * 获取选人组件中的角色(设计时角色控制) | ||
10 | + */ | ||
11 | +export function getRolesByAppCode(request: any, appCode: string) { | ||
12 | + return request.get(`/qx-apaas-lowcode/appRole/${appCode}/authTree`); | ||
13 | +} |
src/qx-role-selector/src/style.less
0 → 100644
1 | +.qx-role-selector__dialog { | ||
2 | + .ant-modal-body { | ||
3 | + display: flex; | ||
4 | + flex-direction: column; | ||
5 | + padding: 0; | ||
6 | + | ||
7 | + > .ant-row { | ||
8 | + flex: 1; | ||
9 | + } | ||
10 | + } | ||
11 | + | ||
12 | + .ant-checkbox-wrapper { | ||
13 | + width: 100%; | ||
14 | + } | ||
15 | +} | ||
16 | + | ||
17 | +.qx-role-selected__temp { | ||
18 | + display: flex; | ||
19 | + flex-wrap: wrap; | ||
20 | + align-items: center; | ||
21 | + height: 60px; | ||
22 | + padding: 5px; | ||
23 | + overflow: auto; | ||
24 | + border-bottom: 1px solid #f0f0f0; | ||
25 | + | ||
26 | + .ant-tag { | ||
27 | + margin: 1px 2px; | ||
28 | + } | ||
29 | +} | ||
30 | + | ||
31 | +.qx-role-selector__content { | ||
32 | + height: 300px; | ||
33 | + overflow: auto; | ||
34 | +} |