Showing
12 changed files
with
1437 additions
and
53 deletions
| @@ -54,6 +54,7 @@ | @@ -54,6 +54,7 @@ | ||
| 54 | "devDependencies": { | 54 | "devDependencies": { |
| 55 | "@commitlint/cli": "^17.1.2", | 55 | "@commitlint/cli": "^17.1.2", |
| 56 | "@commitlint/config-conventional": "^17.1.0", | 56 | "@commitlint/config-conventional": "^17.1.0", |
| 57 | + "@qx/utils": "0.0.58", | ||
| 57 | "@types/lodash-es": "^4.17.8", | 58 | "@types/lodash-es": "^4.17.8", |
| 58 | "@types/react": "^18.0.0", | 59 | "@types/react": "^18.0.0", |
| 59 | "@types/react-dom": "^18.0.0", | 60 | "@types/react-dom": "^18.0.0", |
| @@ -69,6 +70,7 @@ | @@ -69,6 +70,7 @@ | ||
| 69 | "prettier-plugin-organize-imports": "^3.0.0", | 70 | "prettier-plugin-organize-imports": "^3.0.0", |
| 70 | "prettier-plugin-packagejson": "^2.2.18", | 71 | "prettier-plugin-packagejson": "^2.2.18", |
| 71 | "react": "^18.0.0", | 72 | "react": "^18.0.0", |
| 73 | + "react-cookies": ">=0.1.1", | ||
| 72 | "react-dom": "^18.0.0", | 74 | "react-dom": "^18.0.0", |
| 73 | "stylelint": "^14.9.1" | 75 | "stylelint": "^14.9.1" |
| 74 | }, | 76 | }, |
src/qx-app-selector/index.md
0 → 100644
| 1 | +### 选择应用 | ||
| 2 | + | ||
| 3 | +```tsx | ||
| 4 | +import { createRequest } from '@qx/utils'; | ||
| 5 | +import React, { useState } from 'react'; | ||
| 6 | +import { QxAppSelector } from './index'; | ||
| 7 | + | ||
| 8 | +export default () => { | ||
| 9 | + const [visible, setVisible] = useState(false); | ||
| 10 | + const props = { | ||
| 11 | + flag: 'join', | ||
| 12 | + item: { | ||
| 13 | + flag: 'SINGLE', | ||
| 14 | + currentId: '', | ||
| 15 | + appId: 'n7S2Gz9GJ4knP9w2AOb', | ||
| 16 | + }, | ||
| 17 | + onchange: () => {}, | ||
| 18 | + modalProps: { | ||
| 19 | + width: 550, | ||
| 20 | + destroyOnClose: true, | ||
| 21 | + visible: visible, | ||
| 22 | + onOk: () => setVisible(false), | ||
| 23 | + onCancel: () => setVisible(false), | ||
| 24 | + }, | ||
| 25 | + }; | ||
| 26 | + | ||
| 27 | + return ( | ||
| 28 | + <> | ||
| 29 | + <a onClick={() => setVisible(true)}>点我一下</a> | ||
| 30 | + <QxAppSelector {...props} request={createRequest()} /> | ||
| 31 | + </> | ||
| 32 | + ); | ||
| 33 | +}; | ||
| 34 | +``` | ||
| 35 | + | ||
| 36 | +<API></API> |
src/qx-app-selector/index.tsx
0 → 100644
| 1 | +import { DownOutlined, SearchOutlined } from '@ant-design/icons'; | ||
| 2 | +import { | ||
| 3 | + Input, | ||
| 4 | + Menu, | ||
| 5 | + // message, | ||
| 6 | + Modal, | ||
| 7 | + Popover, | ||
| 8 | + Radio, | ||
| 9 | + Select, | ||
| 10 | + // TreeSelect, | ||
| 11 | + Typography, | ||
| 12 | +} from 'antd'; | ||
| 13 | +import _ from 'lodash'; | ||
| 14 | +import React, { useEffect, useMemo, useState } from 'react'; | ||
| 15 | +import { | ||
| 16 | + getAggreList, | ||
| 17 | + getAppList, | ||
| 18 | + getDatasetListByAppid, | ||
| 19 | + getFunList, | ||
| 20 | +} from './service'; | ||
| 21 | +// import { FRWidgetProps } from '@/packages/qx-form-generator/src'; | ||
| 22 | +import type { ModalProps } from 'antd'; | ||
| 23 | + | ||
| 24 | +import './styles.less'; | ||
| 25 | + | ||
| 26 | +const { Title, Text } = Typography; | ||
| 27 | +/** | ||
| 28 | + * 应用选择器 | ||
| 29 | + * @constructor | ||
| 30 | + */ | ||
| 31 | +type QxAppSelectProps = { | ||
| 32 | + item?: Record<string, unknown>; | ||
| 33 | + modalProps: ModalProps; | ||
| 34 | + title?: string; | ||
| 35 | + code?: string; | ||
| 36 | + name?: string; | ||
| 37 | + onChange?: any; | ||
| 38 | + getConfig?: (appCode: string, funCode: string) => any; | ||
| 39 | + flow_add?: boolean; | ||
| 40 | + from?: string; | ||
| 41 | + flag?: string; | ||
| 42 | + clearData?: boolean; | ||
| 43 | + haveChildren?: boolean; | ||
| 44 | + clearDataSourceNode?: boolean; | ||
| 45 | + /** | ||
| 46 | + * 控制表单类型是否显示. | ||
| 47 | + * | ||
| 48 | + * 默认值 true | ||
| 49 | + */ | ||
| 50 | + showTableTypeTab?: boolean; | ||
| 51 | + /** | ||
| 52 | + * 默认表单类型 | ||
| 53 | + * | ||
| 54 | + * 默认值 single | ||
| 55 | + */ | ||
| 56 | + defaultTableType?: 'single' | 'join'; | ||
| 57 | + chartCustom?: boolean; | ||
| 58 | + nodeFrom?: 'flow_query_single'; | ||
| 59 | + request: any; | ||
| 60 | +}; | ||
| 61 | + | ||
| 62 | +export const QxAppSelector: React.FC<QxAppSelectProps> = ({ | ||
| 63 | + showTableTypeTab = true, | ||
| 64 | + defaultTableType = 'single', | ||
| 65 | + chartCustom = false, | ||
| 66 | + request, | ||
| 67 | + ...props | ||
| 68 | +}) => { | ||
| 69 | + const [appList, setAppList] = useState<any[]>([]); //应用列表 | ||
| 70 | + const [formList, setFormList] = useState<any[]>([]); //表单列表 | ||
| 71 | + const [appItem, setAppItem] = useState<any>({}); //选择的应用 | ||
| 72 | + const [formItem, setFormItem] = useState<any>({}); //选择的表单 | ||
| 73 | + const [tableType, setTableType] = useState<string>(defaultTableType); //单个表单or聚合表 | ||
| 74 | + const [aggreList, setAggreList] = useState<any[]>([]); //聚合表数据源 | ||
| 75 | + const [aggreValue, setAggreValue] = useState<string>(); //聚合表选择的值 | ||
| 76 | + const isDeveloperMode = | ||
| 77 | + window.sessionStorage.getItem('DEVELOPER_MODE') === '1'; | ||
| 78 | + | ||
| 79 | + useEffect(() => { | ||
| 80 | + const _item = props.item || {}; | ||
| 81 | + let targetItem: any = {}; | ||
| 82 | + getAppList(request, { from: props.from }).then((res: any) => { | ||
| 83 | + // eslint-disable-next-line array-callback-return | ||
| 84 | + res.map((item: any) => { | ||
| 85 | + if (item.code === _item['currentId']) { | ||
| 86 | + item.name = item.name + '(本应用)'; | ||
| 87 | + } | ||
| 88 | + if (item.code === _item['appId']) { | ||
| 89 | + targetItem = { ...item }; | ||
| 90 | + } | ||
| 91 | + }); | ||
| 92 | + setAppList(res); | ||
| 93 | + setAppItem(targetItem); | ||
| 94 | + if (!targetItem || !targetItem.code) { | ||
| 95 | + return; | ||
| 96 | + } | ||
| 97 | + if (props?.haveChildren) { | ||
| 98 | + getFunList(request, targetItem.code, { hasChild: true }).then((re) => { | ||
| 99 | + setFormList(re); | ||
| 100 | + }); | ||
| 101 | + } else { | ||
| 102 | + getFunList(request, targetItem.code).then((re) => { | ||
| 103 | + setFormList(re); | ||
| 104 | + }); | ||
| 105 | + } | ||
| 106 | + if (_item.flag === 'JOIN') { | ||
| 107 | + setFormItem({}); | ||
| 108 | + //聚合表 | ||
| 109 | + setTableType('join'); // 单选按钮切到'聚合表' | ||
| 110 | + if (!aggreList.length) { | ||
| 111 | + // 如果聚合表没有加载数据源 | ||
| 112 | + const datasetApi = chartCustom | ||
| 113 | + ? getDatasetListByAppid(request, targetItem.code) | ||
| 114 | + : getAggreList(request); | ||
| 115 | + datasetApi.then((datasetRes: any) => { | ||
| 116 | + // eslint-disable-next-line array-callback-return | ||
| 117 | + datasetRes.map((item: any) => { | ||
| 118 | + item.label = item.name; | ||
| 119 | + item.value = item.code; | ||
| 120 | + }); | ||
| 121 | + setAggreList(datasetRes); | ||
| 122 | + // console.log('聚合表数据源',res); | ||
| 123 | + const index = datasetRes.findIndex( | ||
| 124 | + (o: any) => o.code === String(_item['code']), | ||
| 125 | + ); //判断聚合表是否已被删除 | ||
| 126 | + setAggreValue(index === -1 ? undefined : String(_item['code'])); | ||
| 127 | + }); | ||
| 128 | + } else { | ||
| 129 | + const index = aggreList.findIndex( | ||
| 130 | + (o: any) => o.code === String(_item['code']), | ||
| 131 | + ); | ||
| 132 | + setAggreValue(index === -1 ? undefined : String(_item['code'])); | ||
| 133 | + } | ||
| 134 | + } else { | ||
| 135 | + setTableType('single'); | ||
| 136 | + setFormItem(_item); | ||
| 137 | + setAggreValue(undefined); | ||
| 138 | + } | ||
| 139 | + }); | ||
| 140 | + }, [props.item]); | ||
| 141 | + | ||
| 142 | + const formChange = (item: any) => { | ||
| 143 | + setFormItem(item); | ||
| 144 | + }; | ||
| 145 | + | ||
| 146 | + const appChange = (item: any) => { | ||
| 147 | + setAppItem(item); | ||
| 148 | + setFormItem({}); | ||
| 149 | + if (props?.haveChildren) { | ||
| 150 | + getFunList(request, item.code, { hasChild: true }).then((re: any) => { | ||
| 151 | + setFormList(re); | ||
| 152 | + }); | ||
| 153 | + } else { | ||
| 154 | + getFunList(request, item.code).then((res: any) => { | ||
| 155 | + setFormList(res); | ||
| 156 | + }); | ||
| 157 | + const datasetApi = chartCustom | ||
| 158 | + ? getDatasetListByAppid(request, item.code) | ||
| 159 | + : getAggreList(request); | ||
| 160 | + datasetApi.then((datasetRes: any) => { | ||
| 161 | + // eslint-disable-next-line array-callback-return | ||
| 162 | + datasetRes.map((_item: any) => { | ||
| 163 | + _item.label = _item.name; | ||
| 164 | + _item.value = _item.code; | ||
| 165 | + }); | ||
| 166 | + setAggreList(datasetRes); | ||
| 167 | + setAggreValue(undefined); | ||
| 168 | + }); | ||
| 169 | + } | ||
| 170 | + }; | ||
| 171 | + | ||
| 172 | + const radioChange = (e: any, data: any) => { | ||
| 173 | + setTableType(e.target.value); | ||
| 174 | + if (chartCustom) { | ||
| 175 | + if (e.target.value === 'join' && !aggreList.length) { | ||
| 176 | + const datasetApi = chartCustom | ||
| 177 | + ? getDatasetListByAppid(request, data?.code || '') | ||
| 178 | + : getAggreList(request); | ||
| 179 | + datasetApi.then((res: any) => { | ||
| 180 | + if (res && !!res.length) { | ||
| 181 | + // eslint-disable-next-line array-callback-return | ||
| 182 | + res.map((item: any) => { | ||
| 183 | + item.label = item.name; | ||
| 184 | + item.value = item.code; | ||
| 185 | + }); | ||
| 186 | + setAggreList(res); | ||
| 187 | + } else { | ||
| 188 | + setAggreList([]); | ||
| 189 | + } | ||
| 190 | + }); | ||
| 191 | + } | ||
| 192 | + } else { | ||
| 193 | + if (e.target.value === 'join' && !aggreList.length) { | ||
| 194 | + getAggreList(request).then((res: any) => { | ||
| 195 | + // eslint-disable-next-line array-callback-return | ||
| 196 | + res.map((item: any) => { | ||
| 197 | + item.label = item.name; | ||
| 198 | + item.value = item.code; | ||
| 199 | + }); | ||
| 200 | + setAggreList(res); | ||
| 201 | + }); | ||
| 202 | + } | ||
| 203 | + } | ||
| 204 | + }; | ||
| 205 | + const aggreValueChange = (e: any) => { | ||
| 206 | + setAggreValue(e); | ||
| 207 | + }; | ||
| 208 | + const handleGetConfig = () => { | ||
| 209 | + const appCode = appItem.extract; | ||
| 210 | + const funCode = formItem.extract?.code; | ||
| 211 | + if (!appCode || !funCode) return; | ||
| 212 | + //props.getConfig 当前为undefined--布尔判断为false | ||
| 213 | + if (props.getConfig) props.getConfig(appCode, funCode); | ||
| 214 | + }; | ||
| 215 | + | ||
| 216 | + // 只要选的是用户中心 都过滤掉 用户与部门管理 | ||
| 217 | + const workFormList = useMemo(() => { | ||
| 218 | + if (appItem?.extract === 'uc') { | ||
| 219 | + return formList?.filter( | ||
| 220 | + (o: any) => !['org', 'user'].includes(o.extract?.code), | ||
| 221 | + ); | ||
| 222 | + } | ||
| 223 | + return formList; | ||
| 224 | + }, [appItem?.extract, formList, props.nodeFrom]); | ||
| 225 | + | ||
| 226 | + /* 表单组件 */ | ||
| 227 | + const RelItemOption: React.FC<any> = (riProps) => { | ||
| 228 | + const [visible, setVisible] = useState<boolean>(false); | ||
| 229 | + const [list, setList] = useState<any>(riProps.list); | ||
| 230 | + const [item, setItem] = useState<any>(); | ||
| 231 | + useEffect(() => { | ||
| 232 | + if (riProps.item) { | ||
| 233 | + setItem(riProps.item); | ||
| 234 | + } | ||
| 235 | + | ||
| 236 | + if (riProps.list) { | ||
| 237 | + setList(riProps.list); | ||
| 238 | + } | ||
| 239 | + }, [riProps]); | ||
| 240 | + | ||
| 241 | + const filter = (word: string) => { | ||
| 242 | + if (list) { | ||
| 243 | + const _data = _.cloneDeep(list); | ||
| 244 | + _data.forEach((it: Item) => { | ||
| 245 | + it.deleted = !(it.name.indexOf(word) > -1); | ||
| 246 | + }); | ||
| 247 | + | ||
| 248 | + setList(_data); | ||
| 249 | + } | ||
| 250 | + }; | ||
| 251 | + | ||
| 252 | + const handleChange = (_keyword: string) => { | ||
| 253 | + filter(_keyword.trim()); | ||
| 254 | + }; | ||
| 255 | + | ||
| 256 | + const handleVisibleChange = (_visible: boolean) => { | ||
| 257 | + setVisible(_visible); | ||
| 258 | + }; | ||
| 259 | + | ||
| 260 | + const menus = () => { | ||
| 261 | + return ( | ||
| 262 | + <div className={'qx-search-menus__wrap'}> | ||
| 263 | + <Input | ||
| 264 | + className={'qx-selector-sub-search'} | ||
| 265 | + placeholder={'输入名称,回车搜索'} | ||
| 266 | + allowClear | ||
| 267 | + prefix={<SearchOutlined />} | ||
| 268 | + onChange={(e) => { | ||
| 269 | + handleChange(e.target.value); | ||
| 270 | + }} | ||
| 271 | + /> | ||
| 272 | + <div> | ||
| 273 | + <Menu mode={'inline'} selectedKeys={riProps?.item?.code}> | ||
| 274 | + {list && list.length > 0 ? ( | ||
| 275 | + <> | ||
| 276 | + {list.map((it: any) => { | ||
| 277 | + return !it.deleted ? ( | ||
| 278 | + <Menu.Item | ||
| 279 | + style={{ width: '100%' }} | ||
| 280 | + onClick={() => { | ||
| 281 | + riProps.onChange(it); | ||
| 282 | + setVisible(false); | ||
| 283 | + }} | ||
| 284 | + key={it.code} | ||
| 285 | + > | ||
| 286 | + {it.name} | ||
| 287 | + </Menu.Item> | ||
| 288 | + ) : null; | ||
| 289 | + })} | ||
| 290 | + </> | ||
| 291 | + ) : null} | ||
| 292 | + </Menu> | ||
| 293 | + </div> | ||
| 294 | + </div> | ||
| 295 | + ); | ||
| 296 | + }; | ||
| 297 | + | ||
| 298 | + return ( | ||
| 299 | + <> | ||
| 300 | + <Title level={5} style={{ margin: '20px 0 6px' }}> | ||
| 301 | + {riProps.title} | ||
| 302 | + </Title> | ||
| 303 | + <div style={{ width: '100%', marginBottom: '20px' }}> | ||
| 304 | + <Popover | ||
| 305 | + content={menus} | ||
| 306 | + open={visible} | ||
| 307 | + trigger={'click'} | ||
| 308 | + onOpenChange={handleVisibleChange} | ||
| 309 | + placement="bottomLeft" | ||
| 310 | + overlayClassName={'qx-fg-select-overlay'} | ||
| 311 | + getPopupContainer={(triggerNode) => triggerNode} | ||
| 312 | + > | ||
| 313 | + <div | ||
| 314 | + className={`ant-input qx-fr-input--fake select-source`} | ||
| 315 | + style={{ | ||
| 316 | + height: '36px', | ||
| 317 | + }} | ||
| 318 | + > | ||
| 319 | + <div> | ||
| 320 | + {item?.name ? ( | ||
| 321 | + item.name | ||
| 322 | + ) : ( | ||
| 323 | + <Text type="secondary">{riProps.placeholder}</Text> | ||
| 324 | + )} | ||
| 325 | + </div> | ||
| 326 | + <DownOutlined /> | ||
| 327 | + </div> | ||
| 328 | + </Popover> | ||
| 329 | + </div> | ||
| 330 | + </> | ||
| 331 | + ); | ||
| 332 | + }; | ||
| 333 | + /* 聚合表组件 */ | ||
| 334 | + const AggreOption: React.FC<any> = (_props: any) => { | ||
| 335 | + return ( | ||
| 336 | + <> | ||
| 337 | + <Title level={5} style={{ margin: '20px 0 6px' }}> | ||
| 338 | + {_props.title} | ||
| 339 | + </Title> | ||
| 340 | + <Select | ||
| 341 | + allowClear | ||
| 342 | + style={{ width: '100%', height: '36px' }} | ||
| 343 | + placeholder={_props.placeholder || '请选择数据源'} | ||
| 344 | + options={_props.aggreList} | ||
| 345 | + value={_props.aggreValue} | ||
| 346 | + onChange={_props.onChange} | ||
| 347 | + dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} | ||
| 348 | + getPopupContainer={(triggerNode) => triggerNode} | ||
| 349 | + /> | ||
| 350 | + </> | ||
| 351 | + ); | ||
| 352 | + }; | ||
| 353 | + | ||
| 354 | + /** | ||
| 355 | + * 表单类型选择 | ||
| 356 | + */ | ||
| 357 | + const TableTypeNode: React.FC<any> = (_typeProps: any) => { | ||
| 358 | + return ( | ||
| 359 | + <> | ||
| 360 | + {showTableTypeTab && ( | ||
| 361 | + <Radio.Group | ||
| 362 | + onChange={(e: any) => _typeProps.radioChange(e, _typeProps.appData)} | ||
| 363 | + value={_typeProps.radioType} | ||
| 364 | + defaultValue={_typeProps.defaultRadioType} | ||
| 365 | + optionType="button" | ||
| 366 | + style={{ | ||
| 367 | + width: '100%', | ||
| 368 | + textAlign: 'center', | ||
| 369 | + display: props.flag === 'join' ? '' : 'none', | ||
| 370 | + height: '36px', | ||
| 371 | + }} | ||
| 372 | + > | ||
| 373 | + <Radio.Button | ||
| 374 | + value="single" | ||
| 375 | + style={{ | ||
| 376 | + width: '50%', | ||
| 377 | + height: '36px', | ||
| 378 | + }} | ||
| 379 | + > | ||
| 380 | + 表单 | ||
| 381 | + </Radio.Button> | ||
| 382 | + <Radio.Button | ||
| 383 | + value="join" | ||
| 384 | + style={{ | ||
| 385 | + width: '50%', | ||
| 386 | + height: '36px', | ||
| 387 | + }} | ||
| 388 | + > | ||
| 389 | + 聚合表 | ||
| 390 | + </Radio.Button> | ||
| 391 | + </Radio.Group> | ||
| 392 | + )} | ||
| 393 | + </> | ||
| 394 | + ); | ||
| 395 | + }; | ||
| 396 | + | ||
| 397 | + return ( | ||
| 398 | + <> | ||
| 399 | + <Modal | ||
| 400 | + bodyStyle={{ padding: 0 }} | ||
| 401 | + className={'qx-query-designer'} | ||
| 402 | + keyboard={false} | ||
| 403 | + maskClosable={false} | ||
| 404 | + {...props.modalProps} | ||
| 405 | + onOk={ | ||
| 406 | + tableType === 'single' | ||
| 407 | + ? (e) => { | ||
| 408 | + // eslint-disable-next-line @typescript-eslint/no-unused-expressions | ||
| 409 | + props?.modalProps.onOk && props.modalProps.onOk(e); | ||
| 410 | + // 把应用id传出来 | ||
| 411 | + props.onChange({ | ||
| 412 | + ...formItem, | ||
| 413 | + appId: appItem.code, | ||
| 414 | + flag: 'SINGLE', | ||
| 415 | + appCode: appItem.extract, | ||
| 416 | + }); | ||
| 417 | + handleGetConfig(); | ||
| 418 | + } | ||
| 419 | + : (e) => { | ||
| 420 | + // eslint-disable-next-line @typescript-eslint/no-unused-expressions | ||
| 421 | + props?.modalProps.onOk && props.modalProps.onOk(e); | ||
| 422 | + // 把选择的聚合表的id传出来 | ||
| 423 | + const aggreItem = aggreList.filter( | ||
| 424 | + (ex: any) => ex.value === aggreValue, | ||
| 425 | + )[0]; | ||
| 426 | + props.onChange({ | ||
| 427 | + ...aggreItem, | ||
| 428 | + appId: appItem.code, | ||
| 429 | + flag: 'JOIN', | ||
| 430 | + appCode: appItem.extract, | ||
| 431 | + }); | ||
| 432 | + // handleGetConfig() | ||
| 433 | + } | ||
| 434 | + } | ||
| 435 | + title={props.title || '选择关联表单'} | ||
| 436 | + > | ||
| 437 | + {chartCustom ? ( | ||
| 438 | + <div style={{ width: '90%', margin: '10px auto' }}> | ||
| 439 | + <div style={{ margin: '20px auto' }}> | ||
| 440 | + <RelItemOption | ||
| 441 | + title={'应用'} | ||
| 442 | + list={appList} | ||
| 443 | + onChange={appChange} | ||
| 444 | + item={appItem} | ||
| 445 | + placeholder={'请选择应用'} | ||
| 446 | + /> | ||
| 447 | + <> | ||
| 448 | + <Title level={5} style={{ margin: '20px 0 6px' }}> | ||
| 449 | + 数据类型 | ||
| 450 | + </Title> | ||
| 451 | + <TableTypeNode | ||
| 452 | + radioType={tableType} | ||
| 453 | + defaultRadioType={defaultTableType} | ||
| 454 | + appData={appItem} | ||
| 455 | + radioChange={radioChange} | ||
| 456 | + /> | ||
| 457 | + </> | ||
| 458 | + {tableType === 'single' ? ( | ||
| 459 | + <div style={{ margin: '20px auto' }}> | ||
| 460 | + <RelItemOption | ||
| 461 | + title={'表单'} | ||
| 462 | + list={formList} | ||
| 463 | + onChange={formChange} | ||
| 464 | + item={formItem} | ||
| 465 | + placeholder={'请选择表单'} | ||
| 466 | + /> | ||
| 467 | + </div> | ||
| 468 | + ) : ( | ||
| 469 | + <div style={{ margin: '20px auto' }}> | ||
| 470 | + <AggreOption | ||
| 471 | + aggreList={aggreList} | ||
| 472 | + aggreValue={aggreValue} | ||
| 473 | + onChange={aggreValueChange} | ||
| 474 | + title={'聚合表'} | ||
| 475 | + placeholder={'请选择聚合表'} | ||
| 476 | + /> | ||
| 477 | + </div> | ||
| 478 | + )} | ||
| 479 | + </div> | ||
| 480 | + </div> | ||
| 481 | + ) : ( | ||
| 482 | + <div style={{ width: '90%', margin: '10px auto' }}> | ||
| 483 | + {props.title === '选择数据源' ? null : ( | ||
| 484 | + <Text type="secondary"> | ||
| 485 | + 在表单中显示关联的记录。如:订单关联客户 | ||
| 486 | + </Text> | ||
| 487 | + )} | ||
| 488 | + <TableTypeNode | ||
| 489 | + radioType={tableType} | ||
| 490 | + defaultRadioType={defaultTableType} | ||
| 491 | + appData={appItem} | ||
| 492 | + radioChange={radioChange} | ||
| 493 | + /> | ||
| 494 | + {tableType === 'single' ? ( | ||
| 495 | + <div style={{ margin: '20px auto' }}> | ||
| 496 | + <RelItemOption | ||
| 497 | + title={'应用'} | ||
| 498 | + // 非开发者模式下 过滤掉用户中心选项--钉钉2022.10.11 | ||
| 499 | + list={ | ||
| 500 | + isDeveloperMode | ||
| 501 | + ? appList | ||
| 502 | + : appList?.filter((o) => o?.extract !== 'uc') | ||
| 503 | + } | ||
| 504 | + onChange={appChange} | ||
| 505 | + item={appItem} | ||
| 506 | + placeholder={'请选择应用'} | ||
| 507 | + /> | ||
| 508 | + <RelItemOption | ||
| 509 | + title={'工作表'} | ||
| 510 | + list={workFormList} | ||
| 511 | + onChange={formChange} | ||
| 512 | + item={formItem} | ||
| 513 | + placeholder={'请选择工作表'} | ||
| 514 | + /> | ||
| 515 | + </div> | ||
| 516 | + ) : ( | ||
| 517 | + <div style={{ margin: '20px auto' }}> | ||
| 518 | + <AggreOption | ||
| 519 | + aggreList={aggreList} | ||
| 520 | + aggreValue={aggreValue} | ||
| 521 | + onChange={aggreValueChange} | ||
| 522 | + title={'数据表'} | ||
| 523 | + /> | ||
| 524 | + </div> | ||
| 525 | + )} | ||
| 526 | + </div> | ||
| 527 | + )} | ||
| 528 | + </Modal> | ||
| 529 | + </> | ||
| 530 | + ); | ||
| 531 | +}; | ||
| 532 | + | ||
| 533 | +type Item = { | ||
| 534 | + name: string; | ||
| 535 | + code?: string; | ||
| 536 | + deleted?: boolean; | ||
| 537 | +}; |
src/qx-app-selector/service.ts
0 → 100644
| 1 | +/** | ||
| 2 | + * 应用选项 | ||
| 3 | + */ | ||
| 4 | +export function getAppList( | ||
| 5 | + request: any, | ||
| 6 | + params?: { keyword?: string; code?: string; from?: string }, | ||
| 7 | +) { | ||
| 8 | + return request.get(`/qx-apaas-lowcode/app/option`, { params }); | ||
| 9 | +} | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * 功能信息 | ||
| 13 | + */ | ||
| 14 | +export function getFunInfo(request: any, funId: string) { | ||
| 15 | + return request.get(`/qx-apaas-lowcode/app/form/${funId}`); | ||
| 16 | +} | ||
| 17 | + | ||
| 18 | +/** | ||
| 19 | + * 表单选项 | ||
| 20 | + */ | ||
| 21 | +export function getFunList( | ||
| 22 | + request: any, | ||
| 23 | + appId: string, | ||
| 24 | + params?: { keyword?: string; code?: string; id?: string; hasChild?: boolean }, | ||
| 25 | +) { | ||
| 26 | + return request.get(`/qx-apaas-lowcode/app/${appId}/option`, { params }); | ||
| 27 | +} | ||
| 28 | + | ||
| 29 | +//获取聚合表的所有选项 | ||
| 30 | +export function getAggreList( | ||
| 31 | + request: any, | ||
| 32 | + params?: { keyword?: string; code?: string }, | ||
| 33 | +) { | ||
| 34 | + return request.get(`/qx-apaas-lowcode/dataset/join/option`, { params }); | ||
| 35 | +} | ||
| 36 | + | ||
| 37 | +// 获取应用下 聚合表的所有选项 | ||
| 38 | +export function getDatasetListByAppid(request: any, appId: string) { | ||
| 39 | + return request.get(`/qx-apaas-lowcode/dataset/${appId}/option`); | ||
| 40 | +} |
src/qx-app-selector/styles.less
0 → 100644
| 1 | +.ant-input.select-source { | ||
| 2 | + display: flex; | ||
| 3 | + flex-direction: row; | ||
| 4 | + align-items: center; | ||
| 5 | + justify-content: space-between; | ||
| 6 | + min-height: 32px; | ||
| 7 | + padding-top: 2px; | ||
| 8 | + padding-bottom: 2px; | ||
| 9 | + | ||
| 10 | + .ant-tag { | ||
| 11 | + margin: 1px; | ||
| 12 | + } | ||
| 13 | + | ||
| 14 | + > .anticon { | ||
| 15 | + padding: 0 6px; | ||
| 16 | + color: rgba(0, 0, 0, 25%); | ||
| 17 | + font-size: 12px; | ||
| 18 | + } | ||
| 19 | +} |
src/qx-form-select/index.md
0 → 100644
| 1 | +### 选择表单 | ||
| 2 | + | ||
| 3 | +```tsx | ||
| 4 | +import { createRequest } from '@qx/utils'; | ||
| 5 | +import React, { useState } from 'react'; | ||
| 6 | +import { QxFormSelect } from './index'; | ||
| 7 | + | ||
| 8 | +export default () => { | ||
| 9 | + const [value, setValue] = useState({}); | ||
| 10 | + const options = [ | ||
| 11 | + { | ||
| 12 | + name: '地址', | ||
| 13 | + code: 'UdXaoSj9EHYr3wZL253', | ||
| 14 | + extract: { | ||
| 15 | + code: 'o72gf', | ||
| 16 | + isTree: false, | ||
| 17 | + appName: 'LT-表单', | ||
| 18 | + type: 'form', | ||
| 19 | + }, | ||
| 20 | + }, | ||
| 21 | + { | ||
| 22 | + name: '测试关联记录按钮', | ||
| 23 | + code: 'ZRLa6NjJ98u3lFVLSur', | ||
| 24 | + extract: { | ||
| 25 | + code: 'vwxxw', | ||
| 26 | + isTree: false, | ||
| 27 | + appName: 'LT-表单', | ||
| 28 | + type: 'form', | ||
| 29 | + }, | ||
| 30 | + }, | ||
| 31 | + { | ||
| 32 | + name: '测试关联记录权限', | ||
| 33 | + code: 'SRqC0JJmcjKquqnn228', | ||
| 34 | + extract: { | ||
| 35 | + code: '60jte', | ||
| 36 | + isTree: false, | ||
| 37 | + appName: 'LT-表单', | ||
| 38 | + type: 'form', | ||
| 39 | + }, | ||
| 40 | + }, | ||
| 41 | + { | ||
| 42 | + name: '子表', | ||
| 43 | + code: 'GYsqE8yphn1amjVM2OY', | ||
| 44 | + extract: { | ||
| 45 | + code: 'oi0d2', | ||
| 46 | + isTree: false, | ||
| 47 | + appName: 'LT-表单', | ||
| 48 | + type: 'form', | ||
| 49 | + }, | ||
| 50 | + }, | ||
| 51 | + { | ||
| 52 | + name: '关联基础', | ||
| 53 | + code: '2wN04K7nF0fHjRbOAvu', | ||
| 54 | + extract: { | ||
| 55 | + code: '0jpbl', | ||
| 56 | + isTree: false, | ||
| 57 | + appName: 'LT-表单', | ||
| 58 | + type: 'form', | ||
| 59 | + }, | ||
| 60 | + }, | ||
| 61 | + { | ||
| 62 | + name: '关联记录', | ||
| 63 | + code: 's6k5W5aovjtiU7kvqVl', | ||
| 64 | + extract: { | ||
| 65 | + code: 'zbynu', | ||
| 66 | + isTree: false, | ||
| 67 | + appName: 'LT-表单', | ||
| 68 | + type: 'form', | ||
| 69 | + }, | ||
| 70 | + }, | ||
| 71 | + { | ||
| 72 | + name: '关联记录-关联记录', | ||
| 73 | + code: 'hfbs4k6Lbs4WV7tOept', | ||
| 74 | + extract: { | ||
| 75 | + code: 'fz7nl', | ||
| 76 | + isTree: false, | ||
| 77 | + appName: 'LT-表单', | ||
| 78 | + type: 'form', | ||
| 79 | + }, | ||
| 80 | + }, | ||
| 81 | + { | ||
| 82 | + name: '关联属性-关联记录', | ||
| 83 | + code: 'WPjXCOemcBxoXqeEX6x', | ||
| 84 | + extract: { | ||
| 85 | + code: 's585f', | ||
| 86 | + isTree: false, | ||
| 87 | + appName: 'LT-表单', | ||
| 88 | + type: 'form', | ||
| 89 | + }, | ||
| 90 | + }, | ||
| 91 | + { | ||
| 92 | + name: '子表-关联记录', | ||
| 93 | + code: 'H3moABKMnhVqjqcVbsa', | ||
| 94 | + extract: { | ||
| 95 | + code: 'snfhq', | ||
| 96 | + isTree: false, | ||
| 97 | + appName: 'LT-表单', | ||
| 98 | + type: 'form', | ||
| 99 | + }, | ||
| 100 | + }, | ||
| 101 | + { | ||
| 102 | + name: '关联记录1', | ||
| 103 | + code: 'PQspcwgyiewytF9iizd', | ||
| 104 | + extract: { | ||
| 105 | + code: 'f53ql', | ||
| 106 | + isTree: false, | ||
| 107 | + appName: 'LT-表单', | ||
| 108 | + type: 'form', | ||
| 109 | + }, | ||
| 110 | + }, | ||
| 111 | + { | ||
| 112 | + name: '富文本', | ||
| 113 | + code: 'On7QbrZv9u1qtVgMAGm', | ||
| 114 | + extract: { | ||
| 115 | + code: 'tmh23', | ||
| 116 | + isTree: false, | ||
| 117 | + appName: 'LT-表单', | ||
| 118 | + type: 'form', | ||
| 119 | + }, | ||
| 120 | + }, | ||
| 121 | + { | ||
| 122 | + name: '关联记录删除', | ||
| 123 | + code: 'MdUFSBxWOYKXSYGSqSk', | ||
| 124 | + extract: { | ||
| 125 | + code: 'uxenf', | ||
| 126 | + isTree: false, | ||
| 127 | + appName: 'LT-表单', | ||
| 128 | + type: 'form', | ||
| 129 | + }, | ||
| 130 | + }, | ||
| 131 | + { | ||
| 132 | + name: '1', | ||
| 133 | + code: 'HAJ8dEcoPLnjx81Uxjc', | ||
| 134 | + extract: { | ||
| 135 | + code: '3xtcr', | ||
| 136 | + isTree: false, | ||
| 137 | + appName: 'LT-表单', | ||
| 138 | + type: 'form', | ||
| 139 | + }, | ||
| 140 | + }, | ||
| 141 | + { | ||
| 142 | + name: '基础表单', | ||
| 143 | + code: 'oOY4njEfrHx7PlSlFRf', | ||
| 144 | + extract: { | ||
| 145 | + code: 'fd3eb', | ||
| 146 | + isTree: false, | ||
| 147 | + appName: 'LT-表单', | ||
| 148 | + type: 'form', | ||
| 149 | + }, | ||
| 150 | + }, | ||
| 151 | + { | ||
| 152 | + name: '关联记录', | ||
| 153 | + code: 'HaIdxngReF8WtKclakp', | ||
| 154 | + extract: { | ||
| 155 | + code: 'qbroi', | ||
| 156 | + isTree: false, | ||
| 157 | + appName: 'LT-表单', | ||
| 158 | + type: 'form', | ||
| 159 | + }, | ||
| 160 | + }, | ||
| 161 | + { | ||
| 162 | + name: '筛选', | ||
| 163 | + code: 'amWz1TlerTCrysLhKdD', | ||
| 164 | + extract: { | ||
| 165 | + code: 'ionr7', | ||
| 166 | + isTree: false, | ||
| 167 | + appName: 'LT-表单', | ||
| 168 | + type: 'form', | ||
| 169 | + }, | ||
| 170 | + }, | ||
| 171 | + { | ||
| 172 | + name: '子表', | ||
| 173 | + code: 'JV5IeD1Xc4MtwWdbPhB', | ||
| 174 | + extract: { | ||
| 175 | + code: 'x4lh4', | ||
| 176 | + isTree: false, | ||
| 177 | + appName: 'LT-表单', | ||
| 178 | + type: 'form', | ||
| 179 | + }, | ||
| 180 | + }, | ||
| 181 | + { | ||
| 182 | + name: '附件图片', | ||
| 183 | + code: 'jCfTNFw7tjERB5jnnWv', | ||
| 184 | + extract: { | ||
| 185 | + code: 'pqqko', | ||
| 186 | + isTree: false, | ||
| 187 | + appName: 'LT-表单', | ||
| 188 | + type: 'form', | ||
| 189 | + }, | ||
| 190 | + }, | ||
| 191 | + { | ||
| 192 | + name: '日期时间', | ||
| 193 | + code: 'WruqGjaxMj0jLsZ6Ufr', | ||
| 194 | + extract: { | ||
| 195 | + code: 'z15we', | ||
| 196 | + isTree: false, | ||
| 197 | + appName: 'LT-表单', | ||
| 198 | + type: 'form', | ||
| 199 | + }, | ||
| 200 | + }, | ||
| 201 | + { | ||
| 202 | + name: '选人员部门', | ||
| 203 | + code: 'wqsubyh5kJFsxXuHYyQ', | ||
| 204 | + extract: { | ||
| 205 | + code: '0h42r', | ||
| 206 | + isTree: false, | ||
| 207 | + appName: 'LT-表单', | ||
| 208 | + type: 'form', | ||
| 209 | + }, | ||
| 210 | + }, | ||
| 211 | + { | ||
| 212 | + name: '数值', | ||
| 213 | + code: 'HEsh8KhnUToggsWuboT', | ||
| 214 | + extract: { | ||
| 215 | + code: 'bb8ev', | ||
| 216 | + isTree: false, | ||
| 217 | + appName: 'LT-表单', | ||
| 218 | + type: 'form', | ||
| 219 | + }, | ||
| 220 | + }, | ||
| 221 | + { | ||
| 222 | + name: '多字段', | ||
| 223 | + code: 'deTV0Jjc1prqYZTERfN', | ||
| 224 | + extract: { | ||
| 225 | + code: 'hxaxj', | ||
| 226 | + isTree: false, | ||
| 227 | + appName: 'LT-表单', | ||
| 228 | + type: 'form', | ||
| 229 | + }, | ||
| 230 | + }, | ||
| 231 | + ]; | ||
| 232 | + | ||
| 233 | + return ( | ||
| 234 | + <QxFormSelect | ||
| 235 | + options={options} | ||
| 236 | + value={value} | ||
| 237 | + onChange={(datasource) => { | ||
| 238 | + setValue(datasource); | ||
| 239 | + }} | ||
| 240 | + request={createRequest()} | ||
| 241 | + /> | ||
| 242 | + ); | ||
| 243 | +}; | ||
| 244 | +``` | ||
| 245 | + | ||
| 246 | +<API></API> |
src/qx-form-select/index.tsx
0 → 100644
| 1 | +import { BlockOutlined } from '@ant-design/icons'; | ||
| 2 | +import { useSetState } from 'ahooks'; | ||
| 3 | +import { Button } from 'antd'; | ||
| 4 | +import cls from 'classnames'; | ||
| 5 | +import React, { useCallback, useRef } from 'react'; | ||
| 6 | +import { QxAppSelector } from '../qx-app-selector'; | ||
| 7 | +import type { InputSelectProps } from '../qx-input-select'; | ||
| 8 | +import { QxInputSelect } from '../qx-input-select'; | ||
| 9 | + | ||
| 10 | +import './styles.less'; | ||
| 11 | + | ||
| 12 | +const prefix = 'qx-datasource-select'; | ||
| 13 | + | ||
| 14 | +/** | ||
| 15 | + * 选择数据源 | ||
| 16 | + */ | ||
| 17 | +export const QxFormSelect: React.FC<FormSelectProps> = (props) => { | ||
| 18 | + const { | ||
| 19 | + className, | ||
| 20 | + options = [], | ||
| 21 | + onChange, | ||
| 22 | + value, | ||
| 23 | + loading, | ||
| 24 | + appId, | ||
| 25 | + request, | ||
| 26 | + disabled, | ||
| 27 | + } = props; | ||
| 28 | + | ||
| 29 | + const [state, setState] = useSetState<FormSelectState>({ | ||
| 30 | + visible: false, | ||
| 31 | + modalVisible: false, | ||
| 32 | + }); | ||
| 33 | + | ||
| 34 | + const inputSelectRef = useRef<any>(); | ||
| 35 | + | ||
| 36 | + const handleChange = (val: any) => { | ||
| 37 | + if (!val?.code) return; | ||
| 38 | + function onOk() { | ||
| 39 | + if (val?.code) { | ||
| 40 | + onChange?.(val); | ||
| 41 | + setState({ | ||
| 42 | + visible: false, | ||
| 43 | + }); | ||
| 44 | + } | ||
| 45 | + } | ||
| 46 | + onOk(); | ||
| 47 | + }; | ||
| 48 | + | ||
| 49 | + const onOpenOther = useCallback(() => { | ||
| 50 | + inputSelectRef.current?.closeDropdown(); | ||
| 51 | + setState({ | ||
| 52 | + modalVisible: true, | ||
| 53 | + }); | ||
| 54 | + }, []); | ||
| 55 | + | ||
| 56 | + return ( | ||
| 57 | + <div className={cls(prefix, className)}> | ||
| 58 | + <QxInputSelect | ||
| 59 | + ref={inputSelectRef} | ||
| 60 | + value={value?.name} | ||
| 61 | + defaultValue={value?.name} | ||
| 62 | + placeholder="请选择数据源" | ||
| 63 | + prefix={<BlockOutlined style={{ color: '#52c41a' }} />} | ||
| 64 | + options={options} | ||
| 65 | + onChange={handleChange} | ||
| 66 | + dropdownProps={{ | ||
| 67 | + showSearch: true, | ||
| 68 | + loading, | ||
| 69 | + renderBottom: ( | ||
| 70 | + <Button | ||
| 71 | + className={`${prefix}__dropdown-bottom`} | ||
| 72 | + type="link" | ||
| 73 | + onClick={onOpenOther} | ||
| 74 | + > | ||
| 75 | + 其他应用 | ||
| 76 | + </Button> | ||
| 77 | + ), | ||
| 78 | + }} | ||
| 79 | + disabled={disabled} | ||
| 80 | + /> | ||
| 81 | + | ||
| 82 | + {state.modalVisible ? ( | ||
| 83 | + <QxAppSelector | ||
| 84 | + title="选择数据源" | ||
| 85 | + item={{ | ||
| 86 | + flag: 'SINGLE', | ||
| 87 | + currentId: '', | ||
| 88 | + appId: appId, | ||
| 89 | + }} | ||
| 90 | + onChange={handleChange} | ||
| 91 | + flag="join" | ||
| 92 | + showTableTypeTab={false} | ||
| 93 | + modalProps={{ | ||
| 94 | + width: 550, | ||
| 95 | + destroyOnClose: true, | ||
| 96 | + visible: state.modalVisible, | ||
| 97 | + onOk: () => setState({ modalVisible: false }), | ||
| 98 | + onCancel: () => setState({ modalVisible: false }), | ||
| 99 | + }} | ||
| 100 | + request={request} | ||
| 101 | + /> | ||
| 102 | + ) : null} | ||
| 103 | + </div> | ||
| 104 | + ); | ||
| 105 | +}; | ||
| 106 | + | ||
| 107 | +interface FormSelectProps extends InputSelectProps { | ||
| 108 | + value?: any; | ||
| 109 | + loading?: boolean; | ||
| 110 | + appId?: string; | ||
| 111 | + request?: any; | ||
| 112 | + disabled?: boolean; | ||
| 113 | +} | ||
| 114 | +interface FormSelectState { | ||
| 115 | + visible: boolean; | ||
| 116 | + modalVisible: boolean; | ||
| 117 | +} |
src/qx-form-select/styles.less
0 → 100644
src/qx-input-select/dropdown-content.tsx
0 → 100644
| 1 | +import { SearchOutlined } from '@ant-design/icons'; | ||
| 2 | +import { useScroll } from 'ahooks'; | ||
| 3 | +import { Empty, Input, Menu, Spin } from 'antd'; | ||
| 4 | +import cls from 'classnames'; | ||
| 5 | +import { debounce } from 'lodash'; | ||
| 6 | +import React, { | ||
| 7 | + memo, | ||
| 8 | + useCallback, | ||
| 9 | + useEffect, | ||
| 10 | + useMemo, | ||
| 11 | + useRef, | ||
| 12 | + useState, | ||
| 13 | +} from 'react'; | ||
| 14 | +import './styles.less'; | ||
| 15 | +const prefix = 'qx-input-select-dropdown'; | ||
| 16 | + | ||
| 17 | +const listItemHeight = 40; | ||
| 18 | + | ||
| 19 | +const defaultProps = { | ||
| 20 | + fieldNames: { | ||
| 21 | + label: 'name', | ||
| 22 | + value: 'code', | ||
| 23 | + }, | ||
| 24 | +}; | ||
| 25 | + | ||
| 26 | +const DropdownContent = React.forwardRef<any, DropdownContentProps>( | ||
| 27 | + (props, forwardRef) => { | ||
| 28 | + const { | ||
| 29 | + options = [], | ||
| 30 | + fieldNames = defaultProps.fieldNames, | ||
| 31 | + onPopupScroll, | ||
| 32 | + onChange, | ||
| 33 | + className, | ||
| 34 | + style, | ||
| 35 | + renderBottom, | ||
| 36 | + showSearch = false, | ||
| 37 | + onSearch: parentSearch, | ||
| 38 | + placeholder, | ||
| 39 | + listHeight = 260, | ||
| 40 | + loading, | ||
| 41 | + } = props; | ||
| 42 | + | ||
| 43 | + const [filteredOpts, setFilteredOpts] = useState<any[]>(options || []); | ||
| 44 | + const ref = useRef(null); | ||
| 45 | + const position = useScroll(ref); | ||
| 46 | + const isEmpty = !filteredOpts.length; | ||
| 47 | + const fieldLabel = fieldNames.label || 'name'; | ||
| 48 | + const fieldValue = fieldNames.value || 'code'; | ||
| 49 | + | ||
| 50 | + const computedOptions = useMemo( | ||
| 51 | + () => | ||
| 52 | + options?.map((opt) => ({ | ||
| 53 | + label: opt[fieldLabel], | ||
| 54 | + key: opt[fieldValue], | ||
| 55 | + })), | ||
| 56 | + [options], | ||
| 57 | + ); | ||
| 58 | + | ||
| 59 | + const handleSearch = useCallback( | ||
| 60 | + debounce((e) => { | ||
| 61 | + const val = e.target?.value; | ||
| 62 | + const newOpts = val | ||
| 63 | + ? computedOptions.filter((opt) => { | ||
| 64 | + return opt.label?.includes(val); | ||
| 65 | + }) | ||
| 66 | + : computedOptions; | ||
| 67 | + setFilteredOpts(newOpts); | ||
| 68 | + }, 800), | ||
| 69 | + [computedOptions], | ||
| 70 | + ); | ||
| 71 | + | ||
| 72 | + const onSearch = (e: any) => { | ||
| 73 | + e.persist(); | ||
| 74 | + if (parentSearch) { | ||
| 75 | + parentSearch?.(e.target?.value); | ||
| 76 | + } else { | ||
| 77 | + handleSearch(e); | ||
| 78 | + } | ||
| 79 | + }; | ||
| 80 | + | ||
| 81 | + const findSelected = (key: string) => | ||
| 82 | + filteredOpts?.find((opt) => opt.key === key); | ||
| 83 | + | ||
| 84 | + const handleChange = ({ key }: any) => { | ||
| 85 | + const result = findSelected(key); | ||
| 86 | + onChange?.({ | ||
| 87 | + ...result, | ||
| 88 | + [fieldLabel]: result?.label, | ||
| 89 | + [fieldValue]: result?.key, | ||
| 90 | + }); | ||
| 91 | + }; | ||
| 92 | + | ||
| 93 | + useEffect(() => { | ||
| 94 | + const length = options?.length; | ||
| 95 | + | ||
| 96 | + if (length < 6) return; | ||
| 97 | + | ||
| 98 | + if (position) { | ||
| 99 | + const _listHeight = length * listItemHeight; | ||
| 100 | + | ||
| 101 | + const isBottom = _listHeight - _listHeight <= position.top; | ||
| 102 | + | ||
| 103 | + onPopupScroll?.(position, isBottom); | ||
| 104 | + } | ||
| 105 | + }, [position, options]); | ||
| 106 | + | ||
| 107 | + useEffect(() => { | ||
| 108 | + if (Array.isArray(computedOptions)) { | ||
| 109 | + setFilteredOpts(computedOptions); | ||
| 110 | + } | ||
| 111 | + }, [computedOptions]); | ||
| 112 | + | ||
| 113 | + return ( | ||
| 114 | + <div ref={forwardRef} className={cls(prefix, className)} style={style}> | ||
| 115 | + {showSearch ? ( | ||
| 116 | + <Input | ||
| 117 | + placeholder={placeholder || '输入名称搜索'} | ||
| 118 | + prefix={<SearchOutlined />} | ||
| 119 | + className={`${prefix}__input`} | ||
| 120 | + onChange={onSearch} | ||
| 121 | + /> | ||
| 122 | + ) : null} | ||
| 123 | + | ||
| 124 | + {loading ? ( | ||
| 125 | + <Spin spinning={loading} /> | ||
| 126 | + ) : ( | ||
| 127 | + <div | ||
| 128 | + ref={ref} | ||
| 129 | + className={`${prefix}__list`} | ||
| 130 | + style={{ height: listHeight }} | ||
| 131 | + > | ||
| 132 | + {isEmpty ? ( | ||
| 133 | + <Empty | ||
| 134 | + className={`${prefix}__list-empty`} | ||
| 135 | + image={Empty.PRESENTED_IMAGE_SIMPLE} | ||
| 136 | + /> | ||
| 137 | + ) : ( | ||
| 138 | + <Menu | ||
| 139 | + className={cls(`${prefix}__list-content`, { | ||
| 140 | + [`${prefix}__list-content--has-bottom`]: !!renderBottom, | ||
| 141 | + })} | ||
| 142 | + items={filteredOpts} | ||
| 143 | + onClick={handleChange} | ||
| 144 | + /> | ||
| 145 | + )} | ||
| 146 | + </div> | ||
| 147 | + )} | ||
| 148 | + <div className={`${prefix}__list-content-bottom`}>{renderBottom}</div> | ||
| 149 | + </div> | ||
| 150 | + ); | ||
| 151 | + }, | ||
| 152 | +); | ||
| 153 | + | ||
| 154 | +interface FieldNames { | ||
| 155 | + label: string; | ||
| 156 | + value: string; | ||
| 157 | + options?: any; | ||
| 158 | +} | ||
| 159 | + | ||
| 160 | +export interface DropdownContentOptions { | ||
| 161 | + name: string | React.ReactNode; | ||
| 162 | + code: string | number; | ||
| 163 | +} | ||
| 164 | + | ||
| 165 | +export interface DropdownContentProps { | ||
| 166 | + placeholder?: string; | ||
| 167 | + options?: DropdownContentOptions[]; | ||
| 168 | + fieldNames?: FieldNames; | ||
| 169 | + onPopupScroll?: ( | ||
| 170 | + options: { left: number; top: number }, | ||
| 171 | + isBottom: boolean, | ||
| 172 | + ) => void; | ||
| 173 | + onChange?: (val: any) => void; | ||
| 174 | + onOpenOther?: () => void; | ||
| 175 | + showSearch?: boolean; | ||
| 176 | + onSearch?: (val: string) => void; | ||
| 177 | + renderBottom?: React.ReactNode; | ||
| 178 | + listHeight?: number; | ||
| 179 | + loading?: boolean; | ||
| 180 | + className?: string; | ||
| 181 | + style?: React.CSSProperties; | ||
| 182 | +} | ||
| 183 | + | ||
| 184 | +export default memo(DropdownContent); |
src/qx-input-select/index.tsx
0 → 100644
| 1 | +import { DownOutlined, UpOutlined } from '@ant-design/icons'; | ||
| 2 | +import { useSetState } from 'ahooks'; | ||
| 3 | +import { Dropdown, Input } from 'antd'; | ||
| 4 | +import type { InputProps } from 'antd/lib/input'; | ||
| 5 | +import cls from 'classnames'; | ||
| 6 | +import React, { useImperativeHandle } from 'react'; | ||
| 7 | +import type { | ||
| 8 | + DropdownContentOptions, | ||
| 9 | + DropdownContentProps, | ||
| 10 | +} from './dropdown-content'; | ||
| 11 | +import DropdownContent from './dropdown-content'; | ||
| 12 | + | ||
| 13 | +import './styles.less'; | ||
| 14 | + | ||
| 15 | +const prefix = 'qx-input-select'; | ||
| 16 | + | ||
| 17 | +/** | ||
| 18 | + * 下拉选择 | ||
| 19 | + */ | ||
| 20 | +export const QxInputSelect = React.forwardRef<any, InputSelectProps>( | ||
| 21 | + (props, ref) => { | ||
| 22 | + const { | ||
| 23 | + className, | ||
| 24 | + options = [], | ||
| 25 | + dropdownProps, | ||
| 26 | + onChange, | ||
| 27 | + disabled, | ||
| 28 | + ...rest | ||
| 29 | + } = props; | ||
| 30 | + | ||
| 31 | + const [state, setState] = useSetState<InputSelectState>({ | ||
| 32 | + visible: false, | ||
| 33 | + }); | ||
| 34 | + | ||
| 35 | + const inputSuffix = ( | ||
| 36 | + <div className={`${prefix}__input-suffix`}> | ||
| 37 | + {state.visible ? <UpOutlined /> : <DownOutlined />} | ||
| 38 | + </div> | ||
| 39 | + ); | ||
| 40 | + | ||
| 41 | + const handleChange = (val: DropdownContentOptions) => { | ||
| 42 | + onChange?.(val); | ||
| 43 | + setState({ visible: false }); | ||
| 44 | + }; | ||
| 45 | + | ||
| 46 | + /** | ||
| 47 | + * 菜单显示状态改变时调用 | ||
| 48 | + */ | ||
| 49 | + const onVisibleChange = (visible: boolean) => { | ||
| 50 | + setState({ visible }); | ||
| 51 | + }; | ||
| 52 | + | ||
| 53 | + useImperativeHandle(ref, () => ({ | ||
| 54 | + closeDropdown: () => { | ||
| 55 | + setState({ visible: false }); | ||
| 56 | + }, | ||
| 57 | + openDropdown: () => { | ||
| 58 | + setState({ visible: true }); | ||
| 59 | + }, | ||
| 60 | + })); | ||
| 61 | + | ||
| 62 | + return ( | ||
| 63 | + <div className={cls(prefix, className)}> | ||
| 64 | + <Dropdown | ||
| 65 | + visible={state.visible} | ||
| 66 | + destroyPopupOnHide | ||
| 67 | + trigger={['click']} | ||
| 68 | + className={`${prefix}__dropdown`} | ||
| 69 | + overlay={() => ( | ||
| 70 | + <DropdownContent | ||
| 71 | + options={options} | ||
| 72 | + onChange={handleChange} | ||
| 73 | + {...dropdownProps} | ||
| 74 | + /> | ||
| 75 | + )} | ||
| 76 | + getPopupContainer={(triggerNode) => triggerNode} | ||
| 77 | + onVisibleChange={onVisibleChange} | ||
| 78 | + disabled={disabled} | ||
| 79 | + > | ||
| 80 | + <Input | ||
| 81 | + placeholder="请选择" | ||
| 82 | + readOnly | ||
| 83 | + suffix={inputSuffix} | ||
| 84 | + onClick={() => setState({ visible: !state.visible })} | ||
| 85 | + {...rest} | ||
| 86 | + className={`${prefix}__input`} | ||
| 87 | + /> | ||
| 88 | + </Dropdown> | ||
| 89 | + </div> | ||
| 90 | + ); | ||
| 91 | + }, | ||
| 92 | +); | ||
| 93 | + | ||
| 94 | +export interface InputSelectProps extends Omit<InputProps, 'onChange'> { | ||
| 95 | + onChange?: (args: DropdownContentOptions) => void; | ||
| 96 | + options?: DropdownContentOptions[]; | ||
| 97 | + dropdownProps?: DropdownContentProps; | ||
| 98 | + disabled?: boolean; | ||
| 99 | +} | ||
| 100 | +export interface InputSelectState { | ||
| 101 | + visible: boolean; | ||
| 102 | +} |
src/qx-input-select/styles.less
0 → 100644
| 1 | +@import '~@qx/ui/src/style/variable.less'; | ||
| 2 | + | ||
| 3 | +@prefix: ~'qx-input-select'; | ||
| 4 | + | ||
| 5 | +.@{prefix} { | ||
| 6 | + &-dropdown { | ||
| 7 | + position: relative; | ||
| 8 | + width: 100%; | ||
| 9 | + padding: 10px 10px 50px; | ||
| 10 | + overflow: hidden; | ||
| 11 | + background-color: #fff; | ||
| 12 | + box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 12%), | ||
| 13 | + 0 6px 16px 0 rgba(0, 0, 0, 8%), 0 9px 28px 8px rgba(0, 0, 0, 5%); | ||
| 14 | + | ||
| 15 | + &__list { | ||
| 16 | + min-height: 150px; | ||
| 17 | + max-height: 260px; | ||
| 18 | + margin-top: 5px; | ||
| 19 | + overflow-y: auto; | ||
| 20 | + } | ||
| 21 | + | ||
| 22 | + &__list-empty { | ||
| 23 | + margin-top: 15%; | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + &__list-content { | ||
| 27 | + width: 100%; | ||
| 28 | + | ||
| 29 | + :global { | ||
| 30 | + .ant-menu-item { | ||
| 31 | + margin-bottom: 0; | ||
| 32 | + | ||
| 33 | + &:hover { | ||
| 34 | + background-color: #f5f5f5; | ||
| 35 | + } | ||
| 36 | + } | ||
| 37 | + } | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + &__list-content-bottom { | ||
| 41 | + position: absolute; | ||
| 42 | + bottom: 0; | ||
| 43 | + width: 100%; | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + &__input { | ||
| 47 | + :global { | ||
| 48 | + .anticon { | ||
| 49 | + padding: 0 5px; | ||
| 50 | + color: silver; | ||
| 51 | + } | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + padding-bottom: 5px !important; | ||
| 55 | + padding-left: 0 !important; | ||
| 56 | + border: none !important; | ||
| 57 | + border-bottom: 1px solid #e6eaf2 !important; | ||
| 58 | + outline: none !important; | ||
| 59 | + box-shadow: none !important; | ||
| 60 | + | ||
| 61 | + &:hover, | ||
| 62 | + &:focus, | ||
| 63 | + &:active { | ||
| 64 | + border-color: @B8; | ||
| 65 | + } | ||
| 66 | + } | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + &__input { | ||
| 70 | + :global { | ||
| 71 | + input { | ||
| 72 | + cursor: default; | ||
| 73 | + } | ||
| 74 | + } | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + &__input-suffix { | ||
| 78 | + color: silver; | ||
| 79 | + } | ||
| 80 | +} |
| 1 | -## 选人组件 | 1 | +### 选人组件 |
| 2 | 2 | ||
| 3 | ```tsx | 3 | ```tsx |
| 4 | /** | 4 | /** |
| 5 | * debug: true | 5 | * debug: true |
| 6 | */ | 6 | */ |
| 7 | -import React from 'react'; | ||
| 8 | import { QxUserSelector } from '@qx/common'; | 7 | import { QxUserSelector } from '@qx/common'; |
| 9 | -// import request from 'umi-request'; | 8 | +import React from 'react'; |
| 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') | ||
| 19 | + ? url | ||
| 20 | + : `http://10.9.1.180/qx-api${url}`; | ||
| 21 | + return { | ||
| 22 | + url: fullUrl, | ||
| 23 | + options: { | ||
| 24 | + ...options, | ||
| 25 | + ...{ | ||
| 26 | + headers: { ...headers, ...(options.customHeaders || {}) }, | ||
| 27 | + isInternal: true, | ||
| 28 | + timeout: 30000, | ||
| 29 | + }, | ||
| 30 | + }, | ||
| 31 | + }; | ||
| 32 | +}); | ||
| 33 | + | ||
| 34 | +request.interceptors.response.use(async (response, options) => { | ||
| 35 | + if (response.status !== 200) { | ||
| 36 | + return Promise.reject(response); | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + if (!response.headers.get('content-type')?.includes('application/json')) { | ||
| 40 | + return response.blob(); | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + const body = await response.clone().json(); | ||
| 44 | + | ||
| 45 | + // 按正常逻辑处理"文件上传"系列接口 | ||
| 46 | + const fsUploadApis = ['/file/checkFile', '/file/uploadByExist', '/fss/file/']; | ||
| 47 | + const isFsUploadApis = fsUploadApis.filter( | ||
| 48 | + (api: string) => options.url.indexOf(api) !== -1, | ||
| 49 | + ); | ||
| 50 | + if (isFsUploadApis.length > 0) { | ||
| 51 | + return Promise.resolve(body || null); | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + if (body.success) { | ||
| 55 | + return Promise.resolve(body.data || null); | ||
| 56 | + } | ||
| 10 | 57 | ||
| 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 | -// }); | 58 | + console.error('网络请求出错:', body.msg); |
| 31 | 59 | ||
| 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 | -// }); | 60 | + return Promise.reject(body); |
| 61 | +}); | ||
| 58 | 62 | ||
| 59 | export default () => { | 63 | export default () => { |
| 60 | return ( | 64 | return ( |
| 61 | <div> | 65 | <div> |
| 62 | <QxUserSelector | 66 | <QxUserSelector |
| 63 | - // request={request} | 67 | + request={request} |
| 64 | params={{ | 68 | params={{ |
| 65 | org: [{ relType: 'APPOINT_ORG', relIds: [''] }], | 69 | org: [{ relType: 'APPOINT_ORG', relIds: [''] }], |
| 66 | pos: null, | 70 | pos: null, |
| 67 | - range: ['ORG:MubDrwZm8IMxuLDU9FM', 'ORG:a0WZVI96GAdoI5g9IwX', 'ORG:QPLEku2yJU8hmbpLTtg'], | 71 | + range: [ |
| 72 | + 'ORG:MubDrwZm8IMxuLDU9FM', | ||
| 73 | + 'ORG:a0WZVI96GAdoI5g9IwX', | ||
| 74 | + 'ORG:QPLEku2yJU8hmbpLTtg', | ||
| 75 | + ], | ||
| 68 | }} | 76 | }} |
| 69 | /> | 77 | /> |
| 70 | <br /> | 78 | <br /> |
| 71 | - <QxUserSelector /> | 79 | + <QxUserSelector request={request} /> |
| 72 | <br /> | 80 | <br /> |
| 73 | <QxUserSelector | 81 | <QxUserSelector |
| 74 | readOnly | 82 | readOnly |
| 75 | value={['1212']} | 83 | value={['1212']} |
| 76 | defaultData={[{ id: '1212', name: '邢晴晴' }]} | 84 | defaultData={[{ id: '1212', name: '邢晴晴' }]} |
| 77 | - // request={request} | 85 | + request={request} |
| 78 | /> | 86 | /> |
| 79 | </div> | 87 | </div> |
| 80 | ); | 88 | ); |