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