diff --git a/packages/pro/search/demo/RemoteSearch.md b/packages/pro/search/demo/RemoteSearch.md new file mode 100644 index 000000000..83d21373e --- /dev/null +++ b/packages/pro/search/demo/RemoteSearch.md @@ -0,0 +1,14 @@ +--- +order: 42 +title: + zh: 服务端搜索 + en: Server-side searching +--- + +## zh + +`'select'`,`'treeSelect'` 类型搜索项支持服务端搜索。 + +## en + +Server-side searching is supported under field type of `'select'`, `'treeSelect'`. diff --git a/packages/pro/search/demo/RemoteSearch.vue b/packages/pro/search/demo/RemoteSearch.vue new file mode 100644 index 000000000..e905e699c --- /dev/null +++ b/packages/pro/search/demo/RemoteSearch.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/packages/pro/search/docs/Api.zh.md b/packages/pro/search/docs/Api.zh.md index 2ce87c783..5802f4c0d 100644 --- a/packages/pro/search/docs/Api.zh.md +++ b/packages/pro/search/docs/Api.zh.md @@ -99,6 +99,7 @@ SelectSearchFieldConfig | `showSelectAll` | 是否支持全选 | `boolean` | `true` | - | - | | `virtual` | 是否支持虚拟滚动 | `boolean` | `false` | - | 默认不支持 | | `overlayItemWidth` | 选项宽度 | `number` | - | - | - | +| `onSearch` | 搜索回调函数 | `(searchValue: string) => void | ((searchValue: string) => void)[]` | - | - | 在触发搜索值改变时执行 | > 注:使用 `Ctrl + Enter` 在多选下切换面板中选项选中状态 @@ -140,6 +141,7 @@ TreeSelectSearchFieldConfig | `onDrop` | `drop` 触发时调用 | `(options: TreeDragDropOptions) => void | ((options: TreeDragDropOptions) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | | `onExpand` | 点击展开图标时触发 | `(expanded: boolean, node: TreeSelectPanelData) => void | ((expanded: boolean, node: TreeSelectPanelData) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | | `onSelect` | 选中状态发生变化时触发 | `(selected: boolean, node: TreeSelectPanelData) => void | ((selected: boolean, node: TreeSelectPanelData) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | +| `onSearch` | 搜索回调函数 | `(searchValue: string) => void | ((searchValue: string) => void)[]` | - | - | 在触发搜索值改变时执行 | | `onLoaded` | 子节点加载完毕时触发 | `(loadedKeys: any[], node: TreeSelectPanelData) => void | ((loadedKeys: any[], node: TreeSelectPanelData) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | ```typescript diff --git a/packages/pro/search/src/composables/useSearchItem.ts b/packages/pro/search/src/composables/useSearchItem.ts index 8e36c9702..0977f2483 100644 --- a/packages/pro/search/src/composables/useSearchItem.ts +++ b/packages/pro/search/src/composables/useSearchItem.ts @@ -43,6 +43,7 @@ export function useSearchItems( key: searchState.key, optionKey: searchState.fieldKey, error: searchItemErrors.value?.find(error => error.index === searchState.index), + searchField, segments: searchState.segmentValues .map(segmentValue => { if (segmentValue.name === 'name') { diff --git a/packages/pro/search/src/composables/useSegmentStates.ts b/packages/pro/search/src/composables/useSegmentStates.ts index 8f071599f..a6444737c 100644 --- a/packages/pro/search/src/composables/useSegmentStates.ts +++ b/packages/pro/search/src/composables/useSegmentStates.ts @@ -7,17 +7,17 @@ import type { ProSearchContext } from '../token' -import { type Ref, ref, watch } from 'vue' +import { type ComputedRef, type Ref, ref, watch } from 'vue' import { callEmit } from '@idux/cdk/utils' -import { SearchState, tempSearchStateKey } from '../composables/useSearchStates' +import { type SegmentValue, tempSearchStateKey } from '../composables/useSearchStates' import { type ProSearchProps, type SearchItemProps, Segment, searchDataTypes } from '../types' type SegmentStates = Record export interface SegmentStatesContext { segmentStates: Ref - initSegmentStates: () => void + initSegmentStates: (force?: boolean) => void handleSegmentInput: (name: string, input: string) => void handleSegmentChange: (name: string, value: unknown) => void handleSegmentConfirm: (name: string, confirmItem?: boolean) => void @@ -28,6 +28,7 @@ export function useSegmentStates( props: SearchItemProps, proSearchProps: ProSearchProps, proSearchContext: ProSearchContext, + isActive: ComputedRef, ): SegmentStatesContext { const { getSearchStateByKey, @@ -44,9 +45,7 @@ export function useSegmentStates( } = proSearchContext const segmentStates = ref({}) - const _genInitSegmentState = (segment: Segment, searchState: SearchState, index: number) => { - const name = segment.name - const segmentValue = searchState?.segmentValues.find(value => value.name === name) + const _genInitSegmentState = (segment: Segment, segmentValue: SegmentValue | undefined, index: number) => { return { input: segment.format(segmentValue?.value) ?? '', value: segmentValue?.value, @@ -55,9 +54,19 @@ export function useSegmentStates( } // reset temp segment states - const initSegmentStates = () => { + const initSegmentStates = (force = false) => { + const searchState = getSearchStateByKey(props.searchItem!.key)! + segmentStates.value = props.searchItem!.segments.reduce((states, segment, index) => { - states[segment.name] = _genInitSegmentState(segment, getSearchStateByKey(props.searchItem!.key)!, index) + const currentSegmentState = segmentStates.value[segment.name] + const segmentValue = searchState?.segmentValues.find(value => value.name === segment.name) + + if (currentSegmentState && !force) { + states[segment.name] = { ...currentSegmentState, index } + } else { + states[segment.name] = _genInitSegmentState(segment, segmentValue, index) + } + return states }, {} as SegmentStates) } @@ -69,14 +78,29 @@ export function useSegmentStates( return } + const searchState = getSearchStateByKey(props.searchItem!.key)! const segment = props.searchItem!.segments[segmentState.index] - segmentStates.value[name] = _genInitSegmentState( - segment, - getSearchStateByKey(props.searchItem!.key)!, - segmentState.index, - ) + const segmentValue = searchState?.segmentValues.find(value => value.name === segment.name) + segmentStates.value[name] = _genInitSegmentState(segment, segmentValue, segmentState.index) } - watch(() => props.searchItem?.segments, initSegmentStates, { immediate: true }) + + watch( + () => props.searchItem?.segments, + (segments, oldSegments) => { + if ( + segments?.length !== oldSegments?.length || + segments?.some(segment => !oldSegments?.find(oldSegment => oldSegment.name === segment.name)) + ) { + initSegmentStates() + } + }, + { immediate: true }, + ) + watch([isActive, () => props.searchItem?.searchField], ([active, searchField]) => { + if (!active || !searchField) { + initSegmentStates(true) + } + }) const setSegmentValue = (name: string, value: unknown) => { if (!segmentStates.value[name]) { diff --git a/packages/pro/search/src/panel/SelectPanel.tsx b/packages/pro/search/src/panel/SelectPanel.tsx index d5c986730..78cfd6758 100644 --- a/packages/pro/search/src/panel/SelectPanel.tsx +++ b/packages/pro/search/src/panel/SelectPanel.tsx @@ -13,17 +13,19 @@ import { inject, onBeforeUnmount, onMounted, + onUnmounted, ref, watch, } from 'vue' -import { type VKey, useState } from '@idux/cdk/utils' +import { type VKey, callEmit, useState } from '@idux/cdk/utils' import { IxButton } from '@idux/components/button' import { IxCheckbox } from '@idux/components/checkbox' import { IxSelectPanel, type SelectData, type SelectPanelInstance } from '@idux/components/select' import { proSearchContext } from '../token' -import { type ProSearchSelectPanelProps, proSearchSelectPanelProps } from '../types' +import { type ProSearchSelectPanelProps, type SelectPanelData, proSearchSelectPanelProps } from '../types' +import { filterDataSource, matchRule } from '../utils/selectData' export default defineComponent({ props: proSearchSelectPanelProps, @@ -31,6 +33,19 @@ export default defineComponent({ const { locale, mergedPrefixCls } = inject(proSearchContext)! const [activeValue, setActiveValue] = useState(undefined) const partiallySelected = computed(() => props.value && props.value.length > 0 && !props.allSelected) + const filteredDataSource = computed(() => { + const { dataSource, searchValue, searchFn } = props + if (!searchValue) { + return dataSource + } + + const trimedSearchValue = searchValue?.trim() ?? '' + const mergedSearchFn = searchFn + ? (option: SelectPanelData) => searchFn(option, trimedSearchValue) + : (option: SelectPanelData) => matchRule(option.label, trimedSearchValue) + + return filterDataSource(dataSource ?? [], mergedSearchFn) + }) watch( () => props.value, @@ -38,7 +53,19 @@ export default defineComponent({ const key = value?.[value.length - 1] key && setActiveValue(key) }, + { immediate: true }, ) + watch( + () => props.searchValue, + searchValue => { + callEmit(props.onSearch, searchValue ?? '') + }, + ) + onUnmounted(() => { + if (props.searchValue) { + callEmit(props.onSearch, '') + } + }) const panelRef = ref() const changeSelected = (key: VKey) => { @@ -48,27 +75,30 @@ export default defineComponent({ const isSelected = targetIndex > -1 if (!multiple) { - props.onChange?.([key]) + callEmit(props.onChange, [key]) return } if (isSelected) { - props.onChange?.(currValue.filter((_, index) => targetIndex !== index)) + callEmit( + props.onChange, + currValue.filter((_, index) => targetIndex !== index), + ) return } - props.onChange?.([...currValue, key]) + callEmit(props.onChange, [...currValue, key]) } const handleOptionClick = (option: SelectData) => { changeSelected(option.key!) } const handleConfirm = () => { - props.onConfirm?.() + callEmit(props.onConfirm) } const handleCancel = () => { - props.onCancel?.() + callEmit(props.onCancel) } const handleSelectAllClick = () => { - props.onSelectAllClick?.() + callEmit(props.onSelectAllClick) } const handleKeyDown = useOnKeyDown(props, panelRef, activeValue, changeSelected, handleConfirm) @@ -120,7 +150,7 @@ export default defineComponent({ const prefixCls = `${mergedPrefixCls.value}-select-panel` const panelProps = { activeValue: activeValue.value, - dataSource: props.dataSource, + dataSource: filteredDataSource.value, multiple: props.multiple, getKey: 'key', labelKey: 'label', diff --git a/packages/pro/search/src/panel/TreeSelectPanel.tsx b/packages/pro/search/src/panel/TreeSelectPanel.tsx index 4e7c2e1ca..39dbb481f 100644 --- a/packages/pro/search/src/panel/TreeSelectPanel.tsx +++ b/packages/pro/search/src/panel/TreeSelectPanel.tsx @@ -5,7 +5,7 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { type ComputedRef, computed, defineComponent, inject } from 'vue' +import { type ComputedRef, computed, defineComponent, inject, onUnmounted, watch } from 'vue' import { isFunction } from 'lodash-es' @@ -29,6 +29,18 @@ export default defineComponent({ return props.cascaderStrategy }) + watch( + () => props.searchValue, + searchValue => { + callEmit(props.onSearch, searchValue ?? '') + }, + ) + onUnmounted(() => { + if (props.searchValue) { + callEmit(props.onSearch, '') + } + }) + const { expandedKeys, setExpandedKeys } = useExpandedKeys(props) const changeSelected = (keys: VKey[]) => { diff --git a/packages/pro/search/src/searchItem/SearchItem.tsx b/packages/pro/search/src/searchItem/SearchItem.tsx index d7fde2f5a..b82790752 100644 --- a/packages/pro/search/src/searchItem/SearchItem.tsx +++ b/packages/pro/search/src/searchItem/SearchItem.tsx @@ -5,7 +5,7 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { computed, defineComponent, inject, provide, watch } from 'vue' +import { computed, defineComponent, inject, provide } from 'vue' import SearchItemTag from './SearchItemTag' import Segment from './Segment' @@ -20,9 +20,13 @@ export default defineComponent({ setup(props) { const context = inject(proSearchContext)! const { props: proSearchProps, mergedPrefixCls, activeSegment } = context - const segmentStateContext = useSegmentStates(props, proSearchProps, context) + + const isActive = computed(() => activeSegment.value?.itemKey === props.searchItem!.key) + const itemVisible = computed(() => isActive.value && !proSearchProps.disabled) + + const segmentStateContext = useSegmentStates(props, proSearchProps, context, isActive) const segmentOverlayUpdateContext = useSegmentOverlayUpdate() - const { segmentStates, initSegmentStates } = segmentStateContext + const { segmentStates } = segmentStateContext provide(searchItemContext, { ...segmentStateContext, @@ -36,20 +40,12 @@ export default defineComponent({ const segmentState = segmentStates.value[segment.name] return { ...segment, - input: segmentState.input, - value: segmentState.value, + input: segmentState?.input, + value: segmentState?.value, } }) }) - const isActive = computed(() => activeSegment.value?.itemKey === props.searchItem!.key) - const itemVisible = computed(() => isActive.value && !proSearchProps.disabled) - watch(isActive, active => { - if (!active) { - initSegmentStates() - } - }) - return () => ( <> {!itemVisible.value && props.searchItem?.key !== tempSearchStateKey && ( diff --git a/packages/pro/search/src/segments/CreateNameSegment.tsx b/packages/pro/search/src/segments/CreateNameSegment.tsx index 722770cf9..44efa41d2 100644 --- a/packages/pro/search/src/segments/CreateNameSegment.tsx +++ b/packages/pro/search/src/segments/CreateNameSegment.tsx @@ -10,7 +10,7 @@ import type { PanelRenderContext, SearchField, Segment } from '../types' import { type VKey, convertArray } from '@idux/cdk/utils' import SelectPanel from '../panel/SelectPanel' -import { filterSelectDataSourceByInput } from '../utils/selectData' +import { filterDataSource, matchRule } from '../utils/selectData' export const defaultNameSegmentEndSymbol = ':' @@ -26,7 +26,7 @@ export function createNameSegment( setValue(value[0]) ok() } - const filteredDataSource = filterSelectDataSourceByInput(names, getRawInput(input)) + const filteredDataSource = filterDataSource(names, nameOption => matchRule(nameOption.label, getRawInput(input))) if (!filteredDataSource?.length) { return } diff --git a/packages/pro/search/src/segments/CreateSelectSegment.tsx b/packages/pro/search/src/segments/CreateSelectSegment.tsx index be214ebbb..fc0378ec1 100644 --- a/packages/pro/search/src/segments/CreateSelectSegment.tsx +++ b/packages/pro/search/src/segments/CreateSelectSegment.tsx @@ -12,12 +12,7 @@ import { isNil, toString } from 'lodash-es' import { type VKey, convertArray } from '@idux/cdk/utils' import SelectPanel from '../panel/SelectPanel' -import { - filterDataSource, - filterSelectDataSourceByInput, - findDataSourceItem, - getSelectDataSourceKeys, -} from '../utils/selectData' +import { filterDataSource, getSelectDataSourceKeys } from '../utils/selectData' const defaultSeparator = '|' @@ -26,7 +21,7 @@ export function createSelectSegment( searchField: SelectSearchField, ): Segment { const { - fieldConfig: { dataSource, separator, searchable, showSelectAll, searchFn, multiple, virtual }, + fieldConfig: { dataSource, separator, searchable, showSelectAll, searchFn, multiple, virtual, onSearch }, defaultValue, inputClassName, onPanelVisibleChange, @@ -36,17 +31,8 @@ export function createSelectSegment( const { input, value, setValue, ok, cancel, setOnKeyDown } = context const panelValue = convertArray(value) const keys = getSelectDataSourceKeys(dataSource) - const selectableKeys = getSelectDataSourceKeys(filterDataSource(dataSource, option => !option.disabled)) - const lastInputPart = input - .trim() - .split(separator ?? defaultSeparator) - .pop() - ?.trim() - - const panelDataSource = - searchable && !findDataSourceItem(dataSource, item => item.label === lastInputPart) - ? filterSelectDataSourceByInput(dataSource, lastInputPart, searchFn) - : dataSource + const inputParts = input.trim().split(separator ?? defaultSeparator) + const lastInputPart = inputParts.length > panelValue.length ? inputParts.pop()?.trim() : '' const handleChange = (value: VKey[]) => { if (!multiple) { @@ -57,6 +43,7 @@ export function createSelectSegment( } } const handleSelectAll = () => { + const selectableKeys = getSelectDataSourceKeys(filterDataSource(dataSource, option => !option.disabled)) setValue(selectableKeys.length !== panelValue.length ? selectableKeys : undefined) } @@ -64,15 +51,18 @@ export function createSelectSegment( 0 && keys.length <= panelValue.length} - dataSource={panelDataSource} + dataSource={dataSource} multiple={multiple} virtual={virtual} setOnKeyDown={setOnKeyDown} showSelectAll={showSelectAll} + searchValue={searchable ? lastInputPart : ''} + searchFn={searchFn} onChange={handleChange} onSelectAllClick={handleSelectAll} onConfirm={ok} onCancel={cancel} + onSearch={onSearch} /> ) } diff --git a/packages/pro/search/src/segments/CreateTreeSelectSegment.tsx b/packages/pro/search/src/segments/CreateTreeSelectSegment.tsx index 1937da734..93445d27d 100644 --- a/packages/pro/search/src/segments/CreateTreeSelectSegment.tsx +++ b/packages/pro/search/src/segments/CreateTreeSelectSegment.tsx @@ -41,6 +41,7 @@ export function createTreeSelectSegment( onDrop, onExpand, onSelect, + onSearch, onLoaded, }, defaultValue, @@ -58,11 +59,8 @@ export function createTreeSelectSegment( const panelRenderer = (context: PanelRenderContext) => { const { input, value, setValue, ok, cancel } = context const panelValue = convertArray(value) - const lastInputPart = input - .trim() - .split(separator ?? defaultSeparator) - .pop() - ?.trim() + const inputParts = input.trim().split(separator ?? defaultSeparator) + const lastInputPart = inputParts.length > panelValue.length ? inputParts.pop()?.trim() : '' const handleChange = (value: VKey[]) => { if (!multiple) { @@ -76,7 +74,7 @@ export function createTreeSelectSegment( return ( boolean>, setOnKeyDown: Function as PropType<(onKeyDown: ((evt: KeyboardEvent) => boolean) | undefined) => void>, + virtual: { type: Boolean, default: false }, - onChange: Function as PropType<(value: VKey[]) => void>, - onSelectAllClick: Function as PropType<() => void>, - onConfirm: Function as PropType<() => void>, - onCancel: Function as PropType<() => void>, + onChange: [Function, Array] as PropType void>>, + onConfirm: [Function, Array] as PropType void>>, + onCancel: [Function, Array] as PropType void>>, + onSearch: [Function, Array] as PropType void>>, + onSelectAllClick: [Function, Array] as PropType void>>, } as const export type ProSearchSelectPanelProps = ExtractInnerPropTypes export const proSearchTreeSelectPanelProps = { value: { type: Array as PropType, default: undefined }, - searchValue: { type: String, default: undefined }, dataSource: { type: Array as PropType, default: undefined }, multiple: { type: Boolean, default: false }, checkable: { type: Boolean, default: false }, @@ -56,6 +58,7 @@ export const proSearchTreeSelectPanelProps = { }, leafLineIcon: { type: String, default: undefined }, showLine: { type: Boolean, default: undefined }, + searchValue: { type: String, default: undefined }, searchFn: Function as PropType<(node: TreeSelectPanelData, searchValue?: string) => boolean>, virtual: { type: Boolean, default: false }, @@ -72,6 +75,7 @@ export const proSearchTreeSelectPanelProps = { onDrop: [Function, Array] as PropType) => void>>, onExpand: [Function, Array] as PropType void>>, onSelect: [Function, Array] as PropType void>>, + onSearch: [Function, Array] as PropType void>>, onLoaded: [Function, Array] as PropType void>>, } as const export type ProSearchTreeSelectPanelProps = ExtractInnerPropTypes diff --git a/packages/pro/search/src/types/searchFields.ts b/packages/pro/search/src/types/searchFields.ts index 7ee7ece66..1ab9a13be 100644 --- a/packages/pro/search/src/types/searchFields.ts +++ b/packages/pro/search/src/types/searchFields.ts @@ -39,7 +39,8 @@ export interface SelectSearchField extends SearchFieldBase { separator?: string showSelectAll?: boolean virtual?: boolean - searchFn?: (data: SelectPanelData, searchText: string) => boolean + searchFn?: (data: SelectPanelData, searchText?: string) => boolean + onSearch?: MaybeArray<(searchValue: string) => void> overlayItemWidth?: number } } @@ -68,6 +69,7 @@ export interface TreeSelectSearchField extends SearchFieldBase { onDrop?: MaybeArray<(options: TreeDragDropOptions) => void> onExpand?: MaybeArray<(expanded: boolean, node: TreeSelectPanelData) => void> onSelect?: MaybeArray<(selected: boolean, node: TreeSelectPanelData) => void> + onSearch?: MaybeArray<(searchValue: string) => void> onLoaded?: MaybeArray<(loadedKeys: any[], node: TreeSelectPanelData) => void> } } diff --git a/packages/pro/search/src/types/searchItem.ts b/packages/pro/search/src/types/searchItem.ts index 9738d8008..f91d51086 100644 --- a/packages/pro/search/src/types/searchItem.ts +++ b/packages/pro/search/src/types/searchItem.ts @@ -5,6 +5,7 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ +import type { SearchField } from './searchFields' import type { SearchValue } from './searchValue' import type { Segment } from './segment' import type { ExtractInnerPropTypes, VKey } from '@idux/cdk/utils' @@ -26,6 +27,7 @@ export interface SearchItem { key: VKey optionKey?: VKey error?: SearchItemError + searchField: SearchField segments: Segment[] } diff --git a/packages/pro/search/src/utils/selectData.ts b/packages/pro/search/src/utils/selectData.ts index 4beb68f98..24e3f91f0 100644 --- a/packages/pro/search/src/utils/selectData.ts +++ b/packages/pro/search/src/utils/selectData.ts @@ -8,7 +8,7 @@ import type { SelectPanelData } from '../types' import type { VKey } from '@idux/cdk/utils' -import { isNil } from 'lodash-es' +import { isNil, toString } from 'lodash-es' export function getSelectDataSourceKeys(dataSource: SelectPanelData[]): VKey[] { const keys = [] @@ -66,22 +66,6 @@ export function filterDataSource( return filteredData } -export function filterSelectDataSourceByInput( - dataSource: SelectPanelData[], - input: string | undefined, - searchFn?: (data: SelectPanelData, searchText: string) => boolean, -): SelectPanelData[] { - if (!input) { - return dataSource - } - - const filterFn = searchFn - ? (option: SelectPanelData) => searchFn(option, input.trim()) - : (option: SelectPanelData) => matchRule(option.label, input.trim()) - - return filterDataSource(dataSource, filterFn) -} - -function matchRule(srcString: string | number | undefined, targetString: string): boolean { - return !isNil(srcString) && String(srcString).toLowerCase().includes(targetString.toLowerCase()) +export function matchRule(srcString: string | number | undefined, targetString: string): boolean { + return !isNil(srcString) && toString(srcString).toLowerCase().includes(targetString.toLowerCase()) }