import React, { useMemo, useState, useCallback, useEffect, ForwardRefRenderFunction, useImperativeHandle, useRef, } from 'react'; import locale from 'antd/es/locale/zh_CN'; import ConfigProvider from 'antd/es/config-provider'; import Input from 'antd/es/input'; import 'antd/es/input/style/index'; import Select, { SelectProps } from 'antd/es/select'; import 'antd/es/select/style/index'; import classNames from 'classnames'; import Button from 'antd/es/button'; import 'antd/es/button/style/index'; import TreeSelect from 'antd/es/tree-select'; import 'antd/es/tree-select/style/index'; import Cascader, { CascaderOptionType, CascaderProps } from 'antd/es/cascader'; import 'antd/es/cascader/style/index'; import DatePicker from 'antd/es/date-picker'; import 'antd/es/date-picker/style/index'; import Checkbox, { CheckboxChangeEvent } from 'antd/es/checkbox'; import 'antd/es/checkbox/style/index'; import 'antd/es/popover/style/index'; import { RangeValue } from 'rc-picker/es/interface'; import { checkType } from '@jy-fe/utils'; import { XuiReactFilterIcon, XuiReactCloseIcon } from '@jy-fe/icons'; import { SearchFrameHandles, SearchFrameProps, NodeType, ValidatorMapType, } from './xui-search-frame.d'; const XuiSearchFrame: ForwardRefRenderFunction = ( { searchNode, onSearch, defaultValues }, ref, ) => { /** 渲染的数据 */ const [state, setState] = useState({} as any); /** 真正筛选的数据 */ const [searchState, setSearchState] = useState({} as any); /** 外部input焦点控制 */ const [visible, setVisible] = useState(false); const className = 'xui-ant__search-frame'; const searchFrameEl = useRef(null); /** 获取校验集 */ const validatorMap = useMemo(() => { const outerValidatorMap: ValidatorMapType = {}; const innerValidatorMap: ValidatorMapType = {}; searchNode.forEach((node: NodeType) => { const { key, hidden, validator } = node; if (validator) { if (hidden) { innerValidatorMap[key] = validator; } else { outerValidatorMap[key] = validator; } } }); return { outerValidatorMap, innerValidatorMap, }; }, [searchNode]); /** 解析外部keys,内部keys */ const analysis = useMemo(() => { const outerKeys: string[] = []; const innerKeys: string[] = []; const rangeKeys: string[] = []; searchNode.forEach((node: NodeType) => { const { key, hidden, type, disabledProps } = node; if (hidden) { innerKeys.push(key); } else { outerKeys.push(key); } if (type === 'rangeInput') { rangeKeys.push(key); } if (disabledProps) { innerKeys.push(disabledProps.curKey); } }); return { outerKeys, innerKeys, rangeKeys, }; }, [searchNode]); /** 获取已经筛选的keys */ const isSearchKeys = useMemo( () => Object.keys(searchState).filter(key => { const value = searchState[key]; if (checkType(value) === 'Number') { if (value || value === 0) { return true; } return false; } if (checkType(value) === 'Object') { if (Object.keys(value).length > 0) { return true; } return false; } if (checkType(value) === 'Array') { if (value.length > 0) { return true; } return false; } if (value) { return true; } return false; }), [searchState], ); /** 获取已经筛选的外部keys */ const isSearchOuterKeys = useMemo( () => isSearchKeys.filter(item => analysis.outerKeys.includes(item)), [isSearchKeys, analysis], ); /** 获取已经筛选的内部keys */ const isSearchInnerKeys = useMemo(() => { const currentIsSearchInnerKeys = isSearchKeys.filter( item => analysis.innerKeys.includes(item) || analysis.innerKeys.includes(item.replace('Low', '')) || analysis.innerKeys.includes(item.replace('High', '')), ); return Array.from( new Set(currentIsSearchInnerKeys.map(item => item.replace('Low', '').replace('High', ''))), ); }, [isSearchKeys, analysis]); /** 改变渲染的数据 */ const changeState = useCallback((newState: any, callback?: () => void) => { setState(newState); if (callback) { callback(); } }, []); const changeVisible = useCallback( (newVisible: boolean) => { if (newVisible) { setState(searchState); } setVisible(newVisible); }, [searchState], ); /** 查找 */ const search = useCallback( (key?: string, newState?: any) => { let flag = true; const currentState = newState || state; if (visible) { const validatorKeys = Object.keys(validatorMap.innerValidatorMap); for (let i = 0; i < validatorKeys.length; i++) { const f = validatorMap.innerValidatorMap[validatorKeys[i]]; if (f) { const currentKey = validatorKeys[i]; const callbackParam: { [key: string]: any } = {}; if (analysis.rangeKeys.includes(currentKey)) { callbackParam[`${currentKey}Low`] = currentState[`${currentKey}Low`]; callbackParam[`${currentKey}High`] = currentState[`${currentKey}High`]; } else { callbackParam[currentKey] = currentState[currentKey]; } const result = f(callbackParam); if (!result) { flag = false; break; } } } } else { const currentKey = key || ''; const callbackParam: { [key: string]: any } = {}; if (analysis.rangeKeys.includes(currentKey)) { callbackParam[`${currentKey}Low`] = currentState[`${currentKey}Low`]; callbackParam[`${currentKey}High`] = currentState[`${currentKey}High`]; } else { callbackParam[currentKey || ''] = currentState[currentKey]; } const validator = validatorMap.outerValidatorMap[currentKey]; if (validator) { flag = validator(callbackParam); } } if (flag) { let params = { ...searchState }; if (key) { const value = currentState[key]; if (value || value === 0) { params = { ...params, [key]: currentState[key], }; } else { delete params[key]; } } else { const { innerKeys } = analysis; Object.keys(currentState).forEach((itemKey: string) => { if ( innerKeys.includes(itemKey) || (itemKey.includes('Low') && innerKeys.includes(itemKey.replace('Low', ''))) || (itemKey.includes('High') && innerKeys.includes(itemKey.replace('High', ''))) ) { const value = currentState[itemKey]; if (value || value === 0) { params = { ...params, [itemKey]: currentState[itemKey], }; } else { delete params[itemKey]; } } }); } const resultParams = {}; Object.keys(params).forEach(pKey => { const pValue = params[pKey]; if ( !( (!pValue && pValue !== 0) || (checkType(pValue) === 'Array' && pValue.length === 0) || (checkType(pValue) === 'Object' && Object.keys(pValue).length === 0) ) ) { resultParams[pKey] = pValue; } }); setSearchState(resultParams); onSearch(resultParams, 'search'); changeVisible(false); } }, [ analysis, changeVisible, onSearch, searchState, state, validatorMap.innerValidatorMap, validatorMap.outerValidatorMap, visible, ], ); const dropdownClassName = `${className}--dropdown`; const classNamePopoverContentCol = `${className}--col`; const classNamePopoverContentColTitle = `${className}--col--title`; const classNamePopoverContentColContent = `${className}--col--content`; const classNamePopoverContentColContentWithCheckbox = `${className}--col--content--with-checkbox`; const classNamePopoverContentColCheckbox = `${className}--col--checkbox`; /** 渲染节点 */ const renderNode = useMemo(() => { const outerNode: (JSX.Element | null)[] = []; const innerNode: (JSX.Element | null)[] = []; searchNode.forEach((node: NodeType) => { let dom: any = null; const { hidden, type, label, extraProps, options = [], cascaderOptionMap, displayRender, key, placeholder, placeholders, getPopupContainer = () => document.body, onChange, disabledProps, } = node; if (type === 'input') { if (hidden) { dom = (
{label}
) => { const { value } = e.target; const newState = { ...state, [key]: value || undefined, }; changeState(newState); }} {...extraProps} />
); } else { dom = (
{label}
search(key)} onChange={(e: React.ChangeEvent) => { const { value } = e.target; const newState = { ...state, [key]: value || undefined, }; /** 点击清空按钮时触发 */ if (e.type === 'click' && value === '') { changeState(newState, () => search(key, newState)); } else { changeState(newState); } }} onBlur={() => { if (state[key] !== searchState[key]) { search(key); } }} {...extraProps} />
); } } else if (type === 'select' || type === 'multiple') { if (hidden) { const props: SelectProps = { dropdownClassName, value: state[key], placeholder, style: { width: '100%', }, getPopupContainer, allowClear: true, onChange: (value: string) => { let newState; // 批量选择为空时,不保留空数组 if (type === 'multiple' && Array.isArray(value) && !value.length) { newState = { ...state, [key]: undefined, }; } else { newState = { ...state, [key]: value, }; } changeState(newState); if (onChange) { onChange(value); } }, ...extraProps, }; if (type === 'multiple') { props.mode = 'multiple'; } dom = (
{label}
); } else { const props: SelectProps = { value: state[key], placeholder, style: { width: '100%', }, getPopupContainer, allowClear: true, onChange: (value: string) => { const newState = { ...state, [key]: value, }; changeState(newState, () => { search(key, newState); }); if (onChange) onChange(value); }, ...extraProps, }; if (type === 'multiple') { props.mode = 'multiple'; } dom = (
{label}
); } } else if (type === 'treeSelect' || type === 'treeSelectMultiple') { if (hidden) { const props: { [key: string]: any } = { dropdownClassName, value: state[key], placeholder, style: { width: '100%', }, getPopupContainer, allowClear: true, onChange: (value: string) => { const newState = { ...state, [key]: value, }; changeState(newState); if (onChange) { onChange(value); } }, ...extraProps, }; if (type === 'treeSelectMultiple') { props.multiple = true; } dom = (
{label}
{options}
); } else { const props: { [key: string]: any } = { value: state[key], placeholder, style: { width: '100%', }, getPopupContainer, allowClear: true, onChange: (value: string) => { const newState = { ...state, [key]: value, }; changeState(newState, () => { search(key, newState); }); if (onChange) onChange(value); }, ...extraProps, }; if (type === 'treeSelectMultiple') { props.multiple = true; } dom = (
{label}
{options}
); } } else if (type === 'rangeInput') { let check; if (disabledProps) { const { curKey, disabledKey, title } = disabledProps; check = ( { const value = e.target.checked; const newState = { ...state, [curKey]: value || undefined, [`${disabledKey}Low`]: undefined, [`${disabledKey}High`]: undefined, }; changeState(newState); if (onChange) onChange(value); }} > {title} ); } dom = (
{label}
) => { const { value } = e.target; const newState = { ...state, [`${key}Low`]: value, }; changeState(newState); }} disabled={disabledProps ? state[disabledProps.curKey] : null} />
) => { const { value } = e.target; const newState = { ...state, [`${key}High`]: value, }; changeState(newState); }} disabled={disabledProps ? state[disabledProps.curKey] : null} />
{check}
); } else if (type === 'cascader') { const props = { popupClassName: dropdownClassName, options: cascaderOptionMap, placeholder, getPopupContainer, value: state[key], style: { width: '100%', }, onChange: (value: string[]) => { const newState = { ...state, [key]: value, }; setState(newState); }, showSearch: { matchInputWidth: false, filter: (inputValue, path: CascaderOptionType[]) => path.some((option: CascaderOptionType) => { if (option && option.label && typeof option.label === 'string') { return option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1; } return false; }), }, ...extraProps, } as CascaderProps; if (displayRender) { props.displayRender = displayRender; } if (hidden) { const innerProps = { onChange: (value: React.ReactText[]) => { const newState = { ...state, [key]: value, }; setState(newState); }, }; dom = (
{label}
); } else { const outerProps = { onChange: (value: React.ReactText[]) => { const newState = { ...state, [key]: value, }; changeState(newState, () => { search(key, newState); }); }, }; dom = (
{label}
); } } else if (type === 'datePicker') { let check; if (disabledProps) { const { curKey, disabledKey, title } = disabledProps; check = ( { const value = e.target.checked; const newState = { ...state, [curKey]: value || undefined, [disabledKey]: undefined, }; changeState(newState); if (onChange) onChange(value); }} > {title} ); } dom = (
{label}
{ const newState = { ...state, [key]: value, }; setState(newState); }} disabled={disabledProps ? state[disabledProps.curKey] : null} {...extraProps} />
{check}
); } else if (type === 'monthPicker') { dom = (
{label}
{ const newState = { ...state, [key]: value, }; setState(newState); }} {...extraProps} />
); } else if (type === 'rangePicker') { let check; if (disabledProps) { const { curKey, disabledKey, title } = disabledProps; check = ( { const value = e.target.checked; const newState = { ...state, [curKey]: value || undefined, [disabledKey]: undefined, }; changeState(newState); if (onChange) onChange(value); }} > {title} ); } const props = { placeholder: placeholders, getPopupContainer, value: state[key], ...extraProps, }; if (hidden) { const innerProps = { disabled: disabledProps ? state[disabledProps.curKey] : null, dropdownClassName, onChange: (value: RangeValue) => { const newState = { ...state, [key]: value, }; setState(newState); }, }; dom = (
{label}
{check}
); } else { const outerProps = { onChange: (value: RangeValue) => { const newState = { ...state, [key]: value, }; changeState(newState, () => { search(key, newState); }); }, }; dom = (
{label}
); } } if (hidden) { innerNode.push(dom); } else { outerNode.push(dom); } }); return { outerNode, innerNode, }; }, [ searchNode, classNamePopoverContentCol, classNamePopoverContentColTitle, classNamePopoverContentColContent, state, changeState, isSearchOuterKeys, search, searchState, classNamePopoverContentColContentWithCheckbox, classNamePopoverContentColCheckbox, ]); /** 重置 */ const reset = useCallback(() => { const params = { ...searchState }; Object.keys(params).forEach(key => { if (!analysis.outerKeys.includes(key)) { delete params[key]; } }); setState(params); setSearchState(params); onSearch(params, 'reset'); changeVisible(false); }, [searchState, onSearch, changeVisible, analysis.outerKeys]); /** 累计筛选项 */ const searchCount = useMemo(() => { const keys = Object.keys(searchState); const hasKeys: string[] = []; keys.forEach(key => { if (!analysis.outerKeys.includes(key)) { const value = searchState[key]; if (value || value === 0) { if (checkType(value) === 'Array' && value.length > 0) { hasKeys.push(key); } else if (checkType(value) === 'Object' && Object.keys(value).length > 0) { hasKeys.push(key); } else if (checkType(value) !== 'Array' && checkType(value) !== 'Object') { hasKeys.push(key); } } } }); const singleKeys = Array.from( new Set(hasKeys.map(item => item.replace('Low', '').replace('High', ''))), ); return singleKeys.filter( key => !analysis.outerKeys.concat(['pageNum', 'pageSize']).includes(key), ).length; }, [searchState, analysis.outerKeys]); useEffect(() => { if (defaultValues) { setState(defaultValues); setSearchState(defaultValues); } }, [defaultValues]); /** 判断是否在组件内 */ const contains = useCallback((e: MouseEvent) => { let ownFlag = false; let dropdownFlag = false; if (searchFrameEl.current) { ownFlag = (searchFrameEl.current as any).contains(e.target); } const dropdownDom = document.querySelectorAll(`.${dropdownClassName}`); dropdownDom.forEach((element: any) => { if (element.contains(e.target)) { dropdownFlag = true; } }); if (!(ownFlag || dropdownFlag)) { setVisible(false); } }, []); useEffect(() => { window.addEventListener('click', contains); return () => { window.removeEventListener('click', contains); }; }, []); useImperativeHandle(ref, () => ({ clear: () => { setState({}); setSearchState({}); onSearch({}, 'reset'); }, resetState: (paramNameList: string[]) => { const paramObjNeedClear = Object.assign( {}, ...paramNameList.map(item => ({ [item]: undefined })), ); setState((pre: { [key: string]: any }) => ({ ...pre, ...paramObjNeedClear, })); setSearchState((pre: { [key: string]: any }) => ({ ...pre, ...paramObjNeedClear, })); }, })); return (
{renderNode.outerNode} {analysis.innerKeys.length > 0 && (
0, })} onClick={() => setVisible(!visible)} >
)} {searchCount > 0 && (
已筛选{searchCount}
)}
{renderNode.innerNode.length > 0 && (
{renderNode.innerNode}
)}
); }; export default React.forwardRef(XuiSearchFrame);