import type { ISchema } from '@formily/json-schema'; import { createSchemaField } from '@formily/react'; import { ArrayItems, DatePicker, Form, FormGrid, FormItem, FormTab, Input, NumberPicker, PreviewText, Select, Space, TreeSelect, } from '@formily/antd'; import type { Field, FieldDataSource } from '@formily/core'; import { createForm, onFieldReact, onFieldValueChange } from '@formily/core'; import GroupNameControl from '@/components/SearchComponent/GroupNameControl'; import { DeleteOutlined, DoubleRightOutlined, ReloadOutlined, SaveOutlined, SearchOutlined, } from '@ant-design/icons'; import { Button, Card, Dropdown, Empty, Menu, Popconfirm, Popover, Typography } from 'antd'; import { useEffect, useMemo, useRef, useState } from 'react'; import type { ProColumns } from '@jetlinks/pro-table'; import type { EnumData } from '@/utils/typings'; import styles from './index.less'; import Service from '@/components/SearchComponent/service'; import _ from 'lodash'; import { useIntl } from '@@/plugin-locale/localeExports'; import classnames from 'classnames'; import { onlyMessage, randomString } from '@/utils/util'; import { useHistory, useLocation } from 'umi'; const ui2Server = (source: SearchTermsUI): SearchTermsServer => [ { terms: source.terms1 }, { terms: source.terms2, type: source.type }, ]; const server2Ui = (source: SearchTermsServer): SearchTermsUI => ({ terms1: source[0].terms, terms2: source[1]?.terms, type: source[0]?.type || 'and', }); interface Props { /** @name "搜索条件" */ field: ProColumns[]; onSearch: (params: { terms: SearchTermsServer }) => void; target?: string; /** * @name "固定查询参数" * eg: 1: {[{ column: 'test', value: 'admin' }]} * 2: {[ * { * terms: [{ column: 'parentId$isnull', value: '' }, { column: 'parentId$not', value: 'test', type: 'or' }], * }, * { * terms: [{ column: 'id$not', value: 'test', type: 'and' }], * }, * ]} * */ defaultParam?: SearchTermsServer | Term[]; /** * @name "搜索组件模式" * simple 限制只支持一组搜索条件,用于小弹窗搜索时使用 */ model?: 'simple' | 'advance'; enableSave?: boolean; initParam?: SearchTermsServer; } const termType = [ { label: '=', value: 'eq' }, { label: '!=', value: 'not' }, { label: '包含', value: 'like' }, { label: '不包含', value: 'nlike' }, { label: '>', value: 'gt' }, { label: '>=', value: 'gte' }, { label: '<', value: 'lt' }, { label: '<=', value: 'lte' }, { label: '属于', value: 'in' }, { label: '不属于', value: 'nin' }, ]; const service = new Service(); const SchemaField = createSchemaField({ components: { FormItem, FormTab, Input, Select, NumberPicker, FormGrid, ArrayItems, DatePicker, PreviewText, GroupNameControl, Space, TreeSelect, }, }); /** * 搜索字段排序 * @param field */ const sortField = (field: ProColumns[]) => { let _temp = false; field.forEach((item) => { if (item.index) { _temp = true; return; } }); if (!_temp) { // 如果没有index 就默认name字段最第一个 field.map((item) => { if (item.dataIndex === 'name') { item.index = 0; return item; } else { return item; } }); } // index排序 return _.sortBy(field, (i) => i.index); }; // 保存历史记录 // 过滤不参与搜索的列数据 ==> 字段排序 // 场景一:简单模式 // 默认搜索参数,根据Index 来判断,或者默认name为第一组条件 // 场景二:高级模式 // 默认六组搜索条件。根据字段index排序 // const nodeFor = >(props: Props, ref) => { // console.log(props,ref) // return (<>) // } // // export const node = forwardRef(nodeFor) as >(props: Props, ref?: React.Ref) => React.ReactElement const SearchComponent = >(props: Props) => { const { field, target, onSearch, defaultParam, enableSave = true, initParam, model } = props; const _history = useHistory(); const _location = useLocation(); /** * 过滤不参与搜索的数据 ? * TODO Refactor 依赖透明? */ const filterSearchTerm = (): ProColumns[] => field .filter((item) => item.dataIndex) .filter((item) => !item.hideInSearch) .filter((item) => !['index', 'option'].includes(item.dataIndex as string)); /** * 根据dataIndex 过滤不参与查询的参数 * * @param _field 查询的列 * @param excludes 过滤的字段名称 */ // const filterSearchTerm2 = (_field: ProColumns[] = [], excludes: string[] = []) => // _field.filter(item => item.dataIndex) // .filter(item => !item.hideInSearch) // .filter(item => !excludes.includes(item.dataIndex as string)) // 处理后的搜索条件 const processedField = sortField(filterSearchTerm()); const defaultTerms = (index: number) => ({ termType: 'like', column: (processedField[index]?.dataIndex as string) || null, type: 'or', } as Partial); const intl = useIntl(); const [expand, setExpand] = useState(true); const initForm = server2Ui(initParam || [{ terms: [defaultTerms(0)] }]); const [aliasVisible, setAliasVisible] = useState(false); const [initParams, setInitParams] = useState(initForm); const [history, setHistory] = useState([]); const [logVisible, setLogVisible] = useState(false); const uiParamRef = useRef(initParam); const form = useMemo( () => createForm({ validateFirst: true, initialValues: initParams, effects() { onFieldReact('*.*.column', async (typeFiled, f) => { // if ((typeFiled as Field).modified) { const isModified = (typeFiled as Field).modified; const _column = (typeFiled as Field).value; const _field = field.find((item) => item.dataIndex === _column); if (_column === 'id') { if (isModified) { f.setFieldState(typeFiled.query('.termType'), async (state) => { state.value = 'eq'; }); } f.setFieldState(typeFiled.query('.value'), async (state) => { state.componentType = 'Input'; state.componentProps = { allowClear: true }; }); } else { switch (_field?.valueType) { case 'select': let __option: { label: any; value: any }[] | FieldDataSource | undefined = []; f.setFieldState(typeFiled.query('.termType'), async (state) => { state.value = 'eq'; }); if (_field?.valueEnum) { __option = Object.values(_field?.valueEnum || {}).map((item) => ({ label: item.text, value: item.status, })); } else if (_field?.request) { __option = await _field.request(); } if (isModified) { f.setFieldState(typeFiled.query('.termType'), async (state) => { state.value = 'eq'; }); } f.setFieldState(typeFiled.query('.value'), async (state) => { console.log(state.value); state.componentType = 'Select'; state.dataSource = __option; state.componentProps = { allowClear: true, showSearch: true, filterOption: (input: string, option: any) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0, }; }); break; case 'treeSelect': let _option: { label: any; value: any }[] | FieldDataSource | undefined = []; if (_field?.valueEnum) { _option = Object.values(_field?.valueEnum || {}).map((item) => ({ label: item.text, value: item.status, })); } else if (_field?.request) { _option = await _field.request(); } if (isModified) { f.setFieldState(typeFiled.query('.termType'), (_state) => { _state.value = 'eq'; }); } f.setFieldState(typeFiled.query('.value'), (state) => { state.componentType = 'TreeSelect'; state.dataSource = _option; state.componentProps = { ..._field.fieldProps, allowClear: true, showSearch: true, treeNodeFilterProp: 'name', filterOption: (input: string, option: any) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0, }; }); break; case 'digit': f.setFieldState(typeFiled.query('.value'), async (state) => { state.componentType = 'NumberPicker'; state.componentProps = { allowClear: true }; }); if (isModified) { f.setFieldState(typeFiled.query('.termType'), async (state) => { state.value = 'eq'; }); } break; case 'dateTime': f.setFieldState(typeFiled.query('.value'), async (state) => { state.componentType = 'DatePicker'; state.componentProps = { showTime: true, allowClear: true }; }); f.setFieldState(typeFiled.query('.termType'), async (state) => { state.value = 'gte'; }); // console.log(isModified); if (isModified) { f.setFieldState(typeFiled.query('.termType'), async (state) => { state.value = 'gte'; }); } break; default: if (isModified) { f.setFieldState(typeFiled.query('.termType'), async (state) => { state.value = 'like'; }); } f.setFieldState(typeFiled.query('.value'), async (state) => { state.componentType = 'Input'; state.componentProps = { allowClear: true }; }); break; } } // } }); onFieldValueChange('*.*.column', (field1, form1) => { form1.setFieldState(field1.query('.value'), (state1) => { if (field1.modified) { state1.value = undefined; } }); }); }, }), [target, expand], ); const historyForm = createForm(); const queryHistory = async () => { const response = await service.history.query(`${target}-search`); if (response.status === 200) { setHistory(response.result); } }; const createGroup = (name: string): ISchema => ({ 'x-decorator': 'FormItem', 'x-decorator-props': { gridSpan: 4, }, 'x-component': 'ArrayItems', type: 'array', 'x-value': new Array(expand ? 1 : 3).fill({ termType: 'like' }), items: { type: 'object', 'x-component': 'FormGrid', 'x-component-props': { minColumns: 14, maxColumns: 14, columnGap: 24, // rowGap: 1, }, properties: { type: { 'x-decorator': 'FormItem', 'x-component': 'GroupNameControl', 'x-decorator-props': { gridSpan: 3, }, default: 'or', 'x-component-props': { name: name, }, 'x-visible': !expand, }, column: { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-decorator-props': { gridSpan: 3, }, 'x-component-props': { placeholder: '请选择', }, enum: filterSearchTerm().map((i) => ({ label: i.title, value: i.dataIndex } as EnumData)), }, termType: { type: 'enum', 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-decorator-props': { gridSpan: 3, }, default: 'like', enum: termType, }, value: { 'x-decorator-props': { gridSpan: 6, }, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, }); const schema: ISchema = { type: 'object', properties: { layout: { type: 'void', 'x-component': 'FormGrid', 'x-component-props': { minColumns: 9, maxColumns: 9, }, properties: { terms1: createGroup('第一组'), type: { 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-decorator-props': { gridSpan: 1, style: { display: 'flex', alignItems: 'center', marginTop: '-22px', padding: '0 30px', }, }, default: 'and', enum: [ { label: '并且', value: 'and' }, { label: '或者', value: 'or' }, ], }, terms2: createGroup('第二组'), }, }, }, }; const handleForm = (_expand?: boolean) => { const value = form.values; const __expand = _expand !== undefined ? _expand : expand; // 第一组条件值 const _terms1 = _.cloneDeep(value.terms1?.[0]); const uiParam = uiParamRef.current; // 判断一下条件。。是否展开。 if (__expand) { value.terms1 = [ uiParam?.[0]?.terms?.[0] || _terms1 || defaultTerms(0), uiParam?.[0]?.terms?.[1] || defaultTerms(1), uiParam?.[0]?.terms?.[2] || defaultTerms(2), ]; value.terms2 = [ uiParam?.[1]?.terms?.[0] || defaultTerms(3), uiParam?.[1]?.terms?.[1] || defaultTerms(4), uiParam?.[1]?.terms?.[2] || defaultTerms(5), ]; } else { value.terms1 = [uiParam?.[0]?.terms?.[0] || _terms1 || defaultTerms(0)]; value.terms2 = []; } setInitParams(value); }; const handleExpand = () => { handleForm(); setExpand(!expand); }; useEffect(() => { // 1、一组条件时的表单值 // 2、六组条件时的表单值 // 3、拥有默认条件时的表单值 // 合并初始化的值 //expand false 6组条件 true 1组条件 if (initParam && initParam[0].terms && initParam[0].terms.length > 1) { handleExpand(); } }, [initParam]); const simpleSchema: ISchema = { type: 'object', properties: { terms1: createGroup('第一组'), }, }; const handleHistory = (item: SearchHistory) => { const log = JSON.parse(item.content) as SearchTermsUI; setLogVisible(false); uiParamRef.current = ui2Server(log); const _expand = !!(log.terms1 && log.terms1.length > 1) || !!(log.terms2 && log.terms2.length > 1); if (_expand) { setExpand(false); } handleForm(_expand); }; const historyDom = ( {history.length > 0 ? ( history.map((item: SearchHistory) => (
handleHistory(item)} > {item.name} { const response = await service.history.remove(`${target}-search`, item.key); if (response.status === 200) { onlyMessage('操作成功'); const temp = history.filter((h: any) => h.key !== item.key); setHistory(temp); } }} >
)) ) : (
)}
); const formatValue = (value: SearchTermsUI): SearchTermsServer => { let _value = ui2Server(value); // 处理默认查询参数 if (defaultParam && defaultParam?.length > 0) { if ('terms' in defaultParam[0]) { _value = _value.concat(defaultParam as SearchTermsServer); } else if ('value' in defaultParam[0]) { _value = _value.concat([{ terms: defaultParam }]); } } return _value .filter((i) => i.terms && i.terms?.length > 0) .map((_term) => { _term.terms = _term.terms ?.filter((term) => term.value !== '') .map((item) => { if (item.termType === 'like' && item.value && item.value !== '') { item.value = `%${item.value}%`; return item; } if (item.termType === 'nlike' && item.value && item.value !== '') { item.value = `%${item.value}%`; return item; } return item; }); return _term; }); }; const handleSearchValue = ( data: SearchTermsServer, fields: ProColumns[], ): SearchTermsServer => { return data.map((item) => { item.terms?.forEach((termsItem) => { const _fieldItem = fields.find((fieldItem) => fieldItem.dataIndex === termsItem.column); if ( _fieldItem && _fieldItem.search && _fieldItem.search.transform && _.isFunction(_fieldItem.search.transform) ) { termsItem.value = _fieldItem.search.transform(termsItem.value, '', ''); } }); return item; }); }; const handleSearch = async (type: boolean = true) => { const value = form.values; const filterTerms = (data: Partial[] | undefined) => data && data.filter((item) => item.column != null).filter((item) => item.value !== undefined); const _terms = _.cloneDeep(value); _terms.terms1 = filterTerms(_terms.terms1); _terms.terms2 = filterTerms(_terms.terms2); const _temp = formatValue(_terms); if ( (_terms.terms1 && _terms.terms1.length > 1) || (_terms.terms2 && _terms.terms2.length > 1) ) { // 展开高级搜索 uiParamRef.current = ui2Server(value); setExpand(false); handleForm(true); } const params = new URLSearchParams(_location.search); params.delete('q'); params.delete('target'); if ( (value.terms1 && value.terms1.length && value.terms1?.some((item) => item.value)) || (value.terms2 && value.terms2.length && value.terms2?.some((item) => item.value)) ) { if (type) { params.append('q', JSON.stringify(value)); if (props.target) { params.append('target', props.target); } _history.push({ hash: _location.hash, search: '?' + params.toString(), }); // setUrl({ q: JSON.stringify(value), target: props.target }); } } else { _history.push({ hash: _location.hash, search: '?' + params.toString(), }); } const newTemp = handleSearchValue(_.cloneDeep(_temp), props.field); onSearch({ terms: newTemp }); }; const handleLocation = async (l: any, tar?: string) => { // 防止页面下多个TabsTabPane中的查询组件共享路由中的参数 const params = new URLSearchParams(l.search); const q = params.get('q'); const _target = params.get('target'); const value = await form.submit(); if (q && props.model !== 'simple' && value && !value.terms1?.[0].value && !value.terms2) { // 表单有值的情况下,不改变表单 if (_target && tar && _target === tar) { form.setInitialValues(JSON.parse(q)); handleSearch(false); return; } form.setInitialValues(JSON.parse(q)); handleSearch(false); } }; useEffect(() => { handleLocation(_location, props.target); }, [_location, props.target]); useEffect(() => { if (defaultParam) { handleSearch(!(props.model === 'simple')); } }, []); const handleSaveLog = async () => { const value = await form.submit(); const value2 = await historyForm.submit<{ alias: string }>(); const response = await service.history.save(`${target}-search`, { name: value2.alias, content: JSON.stringify(value), }); if (response.status === 200) { onlyMessage('保存成功!'); } else { onlyMessage('保存失败', 'error'); } setAliasVisible(!aliasVisible); }; const resetForm = async (type: boolean) => { const value = form.values; if (!expand) { value.terms1 = [defaultTerms(0), defaultTerms(1), defaultTerms(2)]; value.terms2 = [defaultTerms(3), defaultTerms(4), defaultTerms(5)]; } else { value.terms1 = [defaultTerms(0)]; value.terms2 = []; } setInitParams(value); await handleSearch(type); }; const SearchBtn = { simple: ( <> { // @ts-ignore } ), advance: ( } placement={'bottomLeft'} destroyPopupOnHide // @ts-ignore onClick={handleSearch} visible={logVisible} onVisibleChange={async (visible) => { setLogVisible(visible); if (visible) { await queryHistory(); } }} type="primary" overlay={historyDom} > 搜索 ), }; const SaveBtn = ( } visible={aliasVisible} onVisibleChange={setAliasVisible} title="搜索名称" trigger="click" > ); return (
{enableSave ? SearchBtn.advance : SearchBtn.simple} {enableSave && SaveBtn} {model !== 'simple' && (
)}
); }; export default SearchComponent;