Commit b9266262fbf940d84c0837303f37a1a4741a7b34

Authored by 李婷
1 parent 752ff835

feat: 选择表单

... ... @@ -54,6 +54,7 @@
54 54 "devDependencies": {
55 55 "@commitlint/cli": "^17.1.2",
56 56 "@commitlint/config-conventional": "^17.1.0",
  57 + "@qx/utils": "0.0.58",
57 58 "@types/lodash-es": "^4.17.8",
58 59 "@types/react": "^18.0.0",
59 60 "@types/react-dom": "^18.0.0",
... ... @@ -69,6 +70,7 @@
69 70 "prettier-plugin-organize-imports": "^3.0.0",
70 71 "prettier-plugin-packagejson": "^2.2.18",
71 72 "react": "^18.0.0",
  73 + "react-cookies": ">=0.1.1",
72 74 "react-dom": "^18.0.0",
73 75 "stylelint": "^14.9.1"
74 76 },
... ...
  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>
... ...
  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 +};
... ...
  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 +}
... ...
  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 +}
... ...
  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>
... ...
  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 +}
... ...
  1 +@prefix: ~'qx-datasource-select';
  2 +
  3 +.@{prefix} {
  4 + width: 100%;
  5 +
  6 + &__dropdown-bottom {
  7 + width: 100% !important;
  8 + height: 50px !important;
  9 + text-align: left !important;
  10 + background-color: #fff !important;
  11 + border-top: 1px solid #e6eaf2 !important;
  12 + }
  13 +}
... ...
  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);
... ...
  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 +}
... ...
  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 3 ```tsx
4 4 /**
5 5 * debug: true
6 6 */
7   -import React from 'react';
8 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 63 export default () => {
60 64 return (
61 65 <div>
62 66 <QxUserSelector
63   - // request={request}
  67 + request={request}
64 68 params={{
65 69 org: [{ relType: 'APPOINT_ORG', relIds: [''] }],
66 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 78 <br />
71   - <QxUserSelector />
  79 + <QxUserSelector request={request} />
72 80 <br />
73 81 <QxUserSelector
74 82 readOnly
75 83 value={['1212']}
76 84 defaultData={[{ id: '1212', name: '邢晴晴' }]}
77   - // request={request}
  85 + request={request}
78 86 />
79 87 </div>
80 88 );
... ...