Showing
7 changed files
with
519 additions
and
0 deletions
... | ... | @@ -6,6 +6,8 @@ export * from './qx-tags-input'; |
6 | 6 | export * from './qx-user-selector'; |
7 | 7 | export * from './qx-org-selector'; |
8 | 8 | export * from './qx-form-select'; |
9 | +export * from './qx-pos-selector'; | |
10 | +export * from './qx-role-selector'; | |
9 | 11 | export * from './qx-app-selector'; |
10 | 12 | export * from './utils'; |
11 | 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 | +} | ... | ... |