import type { ReactNode } from 'react'; import { useMemo } from 'react'; import React, { useState, useImperativeHandle, useRef, useContext, useCallback, useEffect, } from 'react'; import type { SelectProps } from 'antd'; import { Space, Spin, ConfigProvider } from 'antd'; import type { ProFieldRequestData, ProFieldValueEnumType, ProSchemaValueEnumMap, ProSchemaValueEnumObj, } from '@ant-design/pro-utils'; import { useDeepCompareEffect, useMountMergeState } from '@ant-design/pro-utils'; import useSWR, { mutate } from 'swr'; import { useIntl } from '@ant-design/pro-provider'; import LightSelect from './LightSelect'; import SearchSelect from './SearchSelect'; import type { ProFieldStatusType } from '../Status'; import TableStatus, { ProFieldBadgeColor } from '../Status'; import type { ProFieldFC } from '../../index'; import './index.less'; let testId = 0; export type FieldSelectProps = { text: string; /** 值的枚举,如果存在枚举,Search 中会生成 select */ valueEnum?: ProFieldValueEnumType; /** 从服务器读取选项 */ request?: ProFieldRequestData; /** 重新触发的时机 */ params?: any; /** 组件的全局设置 */ fieldProps?: any; }; export const ObjToMap = (value: ProFieldValueEnumType | undefined): ProSchemaValueEnumMap => { if (getType(value) === 'map') { return value as ProSchemaValueEnumMap; } return new Map(Object.entries(value || {})); }; /** * 转化 text 和 valueEnum 通过 type 来添加 Status * * @param text * @param valueEnum * @param pure 纯净模式,不增加 status */ export const proFieldParsingText = ( text: string | number | (string | number)[], valueEnumParams: ProFieldValueEnumType, ): React.ReactNode => { if (Array.isArray(text)) { return ( {text.map((value) => ( // @ts-ignore {proFieldParsingText(value, valueEnumParams)} ))} ); } const valueEnum = ObjToMap(valueEnumParams); if (!valueEnum.has(text) && !valueEnum.has(`${text}`)) { // @ts-ignore return text?.label || text; } const domText = (valueEnum.get(text) || valueEnum.get(`${text}`)) as { text: ReactNode; status: ProFieldStatusType; color?: string; }; if (!domText) { // @ts-ignore return text?.label || text; } const { status, color } = domText; const Status = TableStatus[status || 'Init']; // 如果类型存在优先使用类型 if (Status) { return {domText.text}; } // 如果不存在使用颜色 if (color) { return {domText.text}; } // 什么都没有使用 text return domText.text || domText; }; const Highlight: React.FC<{ label: string; words: string[]; }> = ({ label, words }) => { const REG_LIST = '.^$*+-?()[]{}\\|'; const { getPrefixCls } = useContext(ConfigProvider.ConfigContext); const lightCls = getPrefixCls('pro-select-item-option-content-light'); const optionCls = getPrefixCls('pro-select-item-option-content'); const reg = new RegExp( words .map((word) => { return word .split('') .map((w) => (REG_LIST.includes(w) ? `\\${w}` : w)) .join(''); }) .join('|'), 'gi', ); const token = label.replace(reg, '#@$&#'); const elements = token.split('#').map((x) => x[0] === '@' ? React.createElement( 'span', { className: lightCls, }, x.slice(1), ) : x, ); return React.createElement( 'div', { className: optionCls, }, ...elements, ); }; /** * 获取类型的 type * * @param obj */ function getType(obj: any) { // @ts-ignore const type = Object.prototype.toString .call(obj) .match(/^\[object (.*)\]$/)[1] .toLowerCase(); if (type === 'string' && typeof obj === 'object') return 'object'; // Let "new String('')" return 'object' if (obj === null) return 'null'; // PhantomJS has type "DOMWindow" for null if (obj === undefined) return 'undefined'; // PhantomJS has type "DOMWindow" for undefined return type; } /** * 递归筛选 item * * @param item * @param keyWords * @returns */ function filerByItem( item: { label: string; value: string; optionType: string; children: any[]; options: any[]; }, keyWords?: string, ) { if (!keyWords) return true; if ( item?.label?.toString().toLowerCase().includes(keyWords.toLowerCase()) || item?.value?.toString().toLowerCase().includes(keyWords.toLowerCase()) ) { return true; } if (item.optionType === 'optGroup' && (item.children || item.options)) { const findItem = [...(item.children || []), item.options || []].find((mapItem) => { return filerByItem(mapItem, keyWords); }); if (findItem) return true; } return false; } /** * 把 value 的枚举转化为数组 * * @param valueEnum */ export const proFieldParsingValueEnumToArray = ( valueEnumParams: ProFieldValueEnumType, ): { value: string | number; text: string; }[] => { const enumArray: { value: string | number; text: string; /** 是否禁用 */ disabled?: boolean; }[] = []; const valueEnum = ObjToMap(valueEnumParams); valueEnum.forEach((_, key) => { const value = (valueEnum.get(key) || valueEnum.get(`${key}`)) as { text: string; disabled?: boolean; }; if (!value) { return; } if (typeof value === 'object' && value?.text) { enumArray.push({ text: value?.text as unknown as string, value: key, disabled: value.disabled, }); return; } enumArray.push({ text: value as unknown as string, value: key, }); }); return enumArray; }; export const useFieldFetchData = ( props: FieldSelectProps & { proFieldKey?: React.Key; }, ): [boolean, SelectProps['options'], (keyWord?: string) => void, () => void] => { const [keyWords, setKeyWords] = useState(undefined); /** Key 是用来缓存请求的,如果不在是有问题 */ const [cacheKey] = useState(() => { if (props.proFieldKey) { return props.proFieldKey.toString(); } if (props.request) { testId += 1; return testId.toString(); } return 'no-fetch'; }); const proFieldKeyRef = useRef(cacheKey); const getOptionsFormValueEnum = useCallback((valueEnum) => { return proFieldParsingValueEnumToArray(ObjToMap(valueEnum)).map(({ value, text, ...rest }) => ({ label: text, value, key: value, ...rest, })); }, []); const [options, setOptions] = useMountMergeState['options']>( () => { if (props.valueEnum) { return getOptionsFormValueEnum(props.valueEnum); } return []; }, { value: props.fieldProps?.options, }, ); useDeepCompareEffect(() => { // 优先使用 fieldProps?.options if (!props.valueEnum || props.fieldProps?.options) return; setOptions(getOptionsFormValueEnum(props.valueEnum)); }, [props.valueEnum]); const [loading, setLoading] = useState(false); const fetchData = async () => { if (!props.request) { return []; } setLoading(true); const loadData = await props.request({ ...props.params, keyWords }, props); setLoading(false); return loadData; }; const key = useMemo(() => { if (!props.request) { return 'no-fetch'; } if (!props.params && !keyWords) { return proFieldKeyRef.current; } return [proFieldKeyRef.current, JSON.stringify({ ...props.params, keyWords })]; }, [keyWords, props.params, props.request]); const { data, mutate: setLocaleData } = useSWR(key, fetchData, { revalidateOnFocus: false, shouldRetryOnError: false, revalidateOnReconnect: false, }); return [ loading, props.request ? (data as SelectProps['options']) : options ?.map((item) => { if (typeof item === 'string') { return { label: item, value: item, }; } if (item?.optionType === 'optGroup' && (item.children || item.options)) { const childrenOptions = [...(item.children || []), ...(item.options || [])].filter( (mapItem) => { return filerByItem(mapItem, keyWords); }, ); return { ...item, children: childrenOptions, options: childrenOptions, }; } return item; }) ?.filter((item) => { if (!item) return false; if (!keyWords) return true; return filerByItem(item as any, keyWords); }), (fetchKeyWords?: string) => { setKeyWords(fetchKeyWords); mutate(key); }, () => { setKeyWords(undefined); setLocaleData(async () => [], false); }, ]; }; /** * 可以根据 valueEnum 来进行类型的设置 * * @param */ const FieldSelect: ProFieldFC = (props, ref) => { const { mode, valueEnum, render, renderFormItem, request, fieldProps, plain, children, light, proFieldKey, ...rest } = props; const inputRef = useRef(); const intl = useIntl(); const keyWordsRef = useRef(''); useEffect(() => { testId += 1; }, []); useEffect(() => { keyWordsRef.current = fieldProps?.searchValue; }, [fieldProps?.searchValue]); const [loading, options, fetchData, resetData] = useFieldFetchData(props); const size = useContext(ConfigProvider.SizeContext); useImperativeHandle(ref, () => ({ ...(inputRef.current || {}), fetchData: () => fetchData(), })); if (mode === 'read') { const optionsValueEnum: ProSchemaValueEnumObj = options?.length ? options?.reduce((pre: any, cur) => { return { ...pre, [cur.value]: cur.label }; }, {}) : undefined; // 如果有 label 直接就用 label // @ts-ignore if (rest.text?.label) { // @ts-ignore return rest.text?.label; } const dom = ( <> {proFieldParsingText( rest.text, ObjToMap(valueEnum || optionsValueEnum) as unknown as ProSchemaValueEnumObj, )} ); if (render) { return render(rest.text, { mode, ...fieldProps }, dom) || null; } return dom; } if (mode === 'edit' || mode === 'update') { const renderDom = () => { if (light) { return ( ); } return ( : fieldProps?.notFoundContent} fetchData={(keyWord) => { keyWordsRef.current = keyWord; fetchData(keyWord); }} resetData={resetData} optionItemRender={(item) => { if (typeof item.label === 'string' && keyWordsRef.current) { return ; } return item.label; }} {...rest} placeholder={intl.getMessage('tableForm.selectPlaceholder', '请选择')} {...fieldProps} options={options} /> ); }; const dom = renderDom(); if (renderFormItem) { return renderFormItem(rest.text, { mode, ...fieldProps, options }, dom) || null; } return dom; } return null; }; export default React.forwardRef(FieldSelect);