From 4bf719ddcea66b12500d2df891c09ba0af8621f1 Mon Sep 17 00:00:00 2001 From: saller Date: Mon, 16 Jan 2023 14:34:29 +0800 Subject: [PATCH] feat(cdk:utils, pro:search): add tree utils, add pro search `'treeSelect'` field (#1391) * fix(pro:search): clicking input container should focus temp item when certain item is active, temp item should be focused when clicking elsewhere within input container * feat(pro:search): add `treeSelect` type search field --- packages/pro/search/demo/Basic.vue | 41 ++++ packages/pro/search/docs/Api.zh.md | 46 ++++- .../src/composables/useActiveSegment.ts | 2 +- .../search/src/composables/useFocusedState.ts | 26 ++- .../search/src/composables/useSearchItem.ts | 3 + .../pro/search/src/panel/TreeSelectPanel.tsx | 175 ++++++++++++++++++ .../pro/search/src/searchItem/SearchItem.tsx | 33 ++-- .../pro/search/src/searchItem/Segment.tsx | 17 +- .../src/segments/CreateTreeSelectSegment.tsx | 160 ++++++++++++++++ packages/pro/search/src/types/panels.ts | 50 ++++- packages/pro/search/src/types/searchFields.ts | 43 ++++- packages/pro/search/style/index.less | 19 ++ packages/pro/search/style/themes/default.ts | 1 + .../search/style/themes/default.variable.less | 4 + packages/pro/search/style/themes/seer.ts | 1 + 15 files changed, 579 insertions(+), 42 deletions(-) create mode 100644 packages/pro/search/src/panel/TreeSelectPanel.tsx create mode 100644 packages/pro/search/src/segments/CreateTreeSelectSegment.tsx diff --git a/packages/pro/search/demo/Basic.vue b/packages/pro/search/demo/Basic.vue index b66e131ea..3c5b71494 100644 --- a/packages/pro/search/demo/Basic.vue +++ b/packages/pro/search/demo/Basic.vue @@ -94,6 +94,47 @@ const searchFields: SearchField[] = [ ], }, }, + { + type: 'treeSelect', + label: 'Tree Data', + key: 'tree_data', + fieldConfig: { + multiple: true, + searchable: true, + checkable: true, + cascaderStrategy: 'all', + dataSource: [ + { + label: 'Node 0', + key: '0', + children: [ + { + label: 'Node 0-0', + key: '0-0', + children: [ + { + label: 'Node 0-0-0', + key: '0-0-0', + }, + { + label: 'Node 0-0-1', + key: '0-0-1', + }, + ], + }, + { + label: 'Node 0-1', + key: '0-1', + children: [ + { label: 'Node 0-1-0', key: '0-1-0' }, + { label: 'Node 0-1-1', key: '0-1-1' }, + ], + }, + ], + }, + ], + }, + }, { type: 'datePicker', label: 'Date', diff --git a/packages/pro/search/docs/Api.zh.md b/packages/pro/search/docs/Api.zh.md index a3d140554..2ce87c783 100644 --- a/packages/pro/search/docs/Api.zh.md +++ b/packages/pro/search/docs/Api.zh.md @@ -93,7 +93,7 @@ SelectSearchFieldConfig | --- | --- | --- | --- | --- | --- | | `dataSource` | 类型 | `SelectPanelData[]` | - | - | 继承自`SelectData`,但`key`和`label`为必填,不支持可配,详情参考[Select](/components/select/zh) | | `multiple` | 是否为多选 | `boolean` | - | - | 默认为单选 | -| `searchable` | 是否支持筛选 | `boolean` | false | - | 默认不支持 | +| `searchable` | 是否支持筛选 | `boolean` | `false` | - | 默认不支持 | | `searchFn` | 搜索函数 | `(data: SelectPanelData, searchText: string) => boolean` | - | - | 默认模糊匹配 | | `separator` | 多选分隔符 | `string` | `'|'` | - | - | | `showSelectAll` | 是否支持全选 | `boolean` | `true` | - | - | @@ -106,6 +106,49 @@ SelectSearchFieldConfig type SelectPanelData = Required> & SelectData ``` +#### TreeSelectSearchField + +树选择类型 + +| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | +| --- | --- | --- | --- | --- | --- | +| `type` | 类型 | `'treeSelect'` | - | - | 固定为 `'select'` | +| `fieldConfig` | 配置 | `TreeSelectSearchFieldConfig` | - | - | - | + +TreeSelectSearchFieldConfig + +| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | +| --- | --- | --- | --- | --- | --- | +| `dataSource` | 类型 | `TreeSelectPanelData[]` | - | - | 继承自`TreeSelectNode`,但`key`和`label`为必填,不支持可配,且`childrenKey`固定为`'children'`,详情参考[Tree](/components/tree/zh) | +| `multiple` | 是否为多选 | `boolean` | - | - | 默认为单选 | +| `checkable` | 是否可勾选 | `boolean` | - | - | 默认不可勾选 | +| `cascaderStrategy` | 级联策略 | `CascaderStrategy` | - | - | 详情参考[Tree](/components/tree/zh) | +| `draggable` | 是否可拖拽 | `boolean` | - | - | 详情参考[Tree](/components/tree/zh) | +| `draggableIcon` | 拖拽图标 | `string` | - | - | 详情参考[Tree](/components/tree/zh) | +| `showLine` | 是否展示连线 | `boolean` | - | - | 详情参考[Tree](/components/tree/zh) | +| `searchable` | 是否支持筛选 | `boolean` | false | - | 默认不支持 | +| `searchFn` | 搜索函数 | `(node: TreeSelectPanelData, searchValue?: string) => boolean` | - | - | 默认模糊匹配 | +| `separator` | 多选分隔符 | `string` | `'|'` | - | - | +| `virtual` | 是否支持虚拟滚动 | `boolean` | `false` | - | 默认不支持 | + +| `onCheck` | 勾选回调函数 | `(checked: boolean, node: TreeSelectPanelData) => void | ((checked: boolean, node: TreeSelectPanelData) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | +| `onDragstart` | `dragstart` 触发时调用 | `(options: TreeDragDropOptions) => void | ((options: TreeDragDropOptions) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | +| `onDragend` | `dragend` 触发时调用 | `(options: TreeDragDropOptions) => void | ((options: TreeDragDropOptions) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | +| `onDragenter` | `dragenter` 触发时调用 | `(options: TreeDragDropOptions) => void | ((options: TreeDragDropOptions) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | +| `onDragleave` | `dragleave` 触发时调用 | `(options: TreeDragDropOptions) => void | ((options: TreeDragDropOptions) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | +| `onDragover` | `dragover` 触发时调用 | `(options: TreeDragDropOptions) => void | ((options: TreeDragDropOptions) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | +| `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) | +| `onLoaded` | 子节点加载完毕时触发 | `(loadedKeys: any[], node: TreeSelectPanelData) => void | ((loadedKeys: any[], node: TreeSelectPanelData) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | + +```typescript +type TreeSelectPanelData = TreeSelectNode & + Required> & { + children?: TreeSelectPanelData[] + } +``` + #### DatePickerSearchField 日期选择类型 @@ -169,5 +212,6 @@ interface PanelRenderContext { ok: () => void // 确认 cancel: () => void // 取消 setValue: (value: V) => void // 设置搜索值 + setOnKeyDown: (onKeyDown: ((evt: KeyboardEvent) => boolean) | undefined) => void // 设置 `keydown` 回调函数 } ``` diff --git a/packages/pro/search/src/composables/useActiveSegment.ts b/packages/pro/search/src/composables/useActiveSegment.ts index 6af3da8fb..1d7bea6d7 100644 --- a/packages/pro/search/src/composables/useActiveSegment.ts +++ b/packages/pro/search/src/composables/useActiveSegment.ts @@ -102,7 +102,7 @@ export function useActiveSegment( } else { setActiveSegment({ itemKey: tempSearchStateKey, - name: 'name', + name: activeSegment.value?.itemKey === tempSearchStateKey ? activeSegment.value.name : 'name', overlayOpened, }) } diff --git a/packages/pro/search/src/composables/useFocusedState.ts b/packages/pro/search/src/composables/useFocusedState.ts index 845feed4b..4354590b1 100644 --- a/packages/pro/search/src/composables/useFocusedState.ts +++ b/packages/pro/search/src/composables/useFocusedState.ts @@ -6,7 +6,6 @@ */ import type { ActiveSegmentContext } from './useActiveSegment' -import type { SearchStateContext } from './useSearchStates' import type { ProSearchProps } from '../types' import type { ɵOverlayProps } from '@idux/components/_private/overlay' @@ -17,6 +16,8 @@ import { isFunction, isString } from 'lodash-es' import { useSharedFocusMonitor } from '@idux/cdk/a11y' import { MaybeElementRef, callEmit, useState } from '@idux/cdk/utils' +import { type SearchStateContext, tempSearchStateKey } from './useSearchStates' + export interface FocusEventContext { focused: ComputedRef focus: (options?: FocusOptions) => void @@ -34,7 +35,7 @@ export function useFocusedState( const { activeSegment, setInactive, setTempActive } = activeSegmentContext const [focused, setFocused] = useState(false) - const { handleFocus, handleBlur } = useFocusHandlers(props, focused, setFocused, setInactive, initTempSearchState) + const { handleFocus, handleBlur } = useFocusHandlers(props, focused, setFocused, setInactive) watch([activeSegment, searchStates], ([segment]) => { if (!segment && focused.value) { @@ -47,10 +48,19 @@ export function useFocusedState( return } - handleFocus(evt, () => { - if (evt.target === elementRef.value) { - setTempActive(true) - } + if (evt.target === elementRef.value) { + setTempActive(activeSegment.value?.itemKey !== tempSearchStateKey) + } + + handleFocus(evt) + } + const _handleBlur = (evt: FocusEvent) => { + if (props.disabled) { + return + } + + handleBlur(evt, () => { + initTempSearchState() }) } @@ -62,7 +72,7 @@ export function useFocusedState( setFocused(false) } - registerHandlers(elementRef, () => getContainerEl(commonOverlayProps.value), _handleFocus, handleBlur) + registerHandlers(elementRef, () => getContainerEl(commonOverlayProps.value), _handleFocus, _handleBlur) return { focused, focus, blur } } @@ -83,7 +93,6 @@ function useFocusHandlers( focused: ComputedRef, setFocused: (focused: boolean) => void, setInactive: (blur?: boolean) => void, - initTempSearchState: () => void, ): { handleFocus: (evt: FocusEvent, cb?: () => void) => void handleBlur: (evt: FocusEvent, cb?: () => void) => void @@ -112,7 +121,6 @@ function useFocusHandlers( return } - initTempSearchState() cb?.() setInactive(true) diff --git a/packages/pro/search/src/composables/useSearchItem.ts b/packages/pro/search/src/composables/useSearchItem.ts index 21211c5b4..8e36c9702 100644 --- a/packages/pro/search/src/composables/useSearchItem.ts +++ b/packages/pro/search/src/composables/useSearchItem.ts @@ -16,6 +16,7 @@ import { createDateRangePickerSegment } from '../segments/CreateDateRangePickerS import { createNameSegment } from '../segments/CreateNameSegment' import { createOperatorSegment } from '../segments/CreateOperatorSegment' import { createSelectSegment } from '../segments/CreateSelectSegment' +import { createTreeSelectSegment } from '../segments/CreateTreeSelectSegment' import { createCustomSegment } from '../segments/createCustomSegment' import { createInputSegment } from '../segments/createInputSegment' @@ -69,6 +70,8 @@ function createSearchItemContentSegment( switch (searchField.type) { case 'select': return createSelectSegment(prefixCls, searchField) + case 'treeSelect': + return createTreeSelectSegment(prefixCls, searchField) case 'input': return createInputSegment(prefixCls, searchField) case 'datePicker': diff --git a/packages/pro/search/src/panel/TreeSelectPanel.tsx b/packages/pro/search/src/panel/TreeSelectPanel.tsx new file mode 100644 index 000000000..4e7c2e1ca --- /dev/null +++ b/packages/pro/search/src/panel/TreeSelectPanel.tsx @@ -0,0 +1,175 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import { type ComputedRef, computed, defineComponent, inject } from 'vue' + +import { isFunction } from 'lodash-es' + +import { NoopFunction, type VKey, callEmit, traverseTree, useState } from '@idux/cdk/utils' +import { IxButton } from '@idux/components/button' +import { IxTree, type TreeProps } from '@idux/components/tree' + +import { proSearchContext } from '../token' +import { type ProSearchTreeSelectPanelProps, type TreeSelectPanelData, proSearchTreeSelectPanelProps } from '../types' + +export default defineComponent({ + props: proSearchTreeSelectPanelProps, + setup(props) { + const { mergedPrefixCls, locale } = inject(proSearchContext)! + + const mergedCheckable = computed(() => props.multiple && props.checkable) + const mergedCascaderStrategy = computed(() => { + if (!mergedCheckable.value) { + return 'off' + } + return props.cascaderStrategy + }) + + const { expandedKeys, setExpandedKeys } = useExpandedKeys(props) + + const changeSelected = (keys: VKey[]) => { + if (!props.multiple && !keys.length) { + return + } + + props.onChange?.(keys) + } + + const handleConfirm = () => { + props.onConfirm?.() + } + const handleCancel = () => { + props.onCancel?.() + } + const handleCheck = (checked: boolean, node: TreeSelectPanelData) => { + const { onCheck } = props + callEmit(onCheck, checked, node) + } + const handleSelect = (selected: boolean, node: TreeSelectPanelData) => { + const { onSelect } = props + + if (!props.multiple && props.value?.[0] !== node.key) { + callEmit(onSelect, selected, node) + } + } + const handleExpand = (expanded: boolean, node: TreeSelectPanelData) => { + const { onExpand } = props + callEmit(onExpand, expanded, node) + } + const onLoaded = async (loadedKeys: VKey[], node: TreeSelectPanelData) => { + callEmit(props.onLoaded, loadedKeys, node) + } + + const renderFooter = (prefixCls: string) => { + if (!props.multiple) { + return + } + + return ( +
+ + {locale.ok} + + + {locale.cancel} + +
+ ) + } + + return () => { + const { + dataSource, + draggable, + draggableIcon, + expandIcon, + multiple, + leafLineIcon, + virtual, + showLine, + searchValue, + onDragstart, + onDragend, + onDragenter, + onDragleave, + onDragover, + onDrop, + droppable, + loadChildren, + searchFn, + } = props + + const treeProps = { + blocked: true, + checkOnClick: true, + checkedKeys: props.value, + labelKey: 'label', + checkable: mergedCheckable.value, + cascaderStrategy: mergedCascaderStrategy.value, + childrenKey: 'children', + dataSource, + draggable, + draggableIcon, + droppable, + expandedKeys: expandedKeys.value, + expandIcon: expandIcon, + getKey: 'key', + autoHeight: true, + loadChildren, + leafLineIcon, + virtual, + selectable: multiple ? 'multiple' : true, + selectedKeys: props.value, + searchValue, + searchFn: isFunction(searchFn) ? searchFn : undefined, + showLine: showLine, + onCheck: handleCheck, + onDragstart, + onDragend, + onDragenter, + onDragleave, + onDragover, + onDrop, + onExpand: handleExpand, + onSelect: handleSelect, + onLoaded, + onCheckedChange: changeSelected, + onSelectedChange: !mergedCheckable.value ? changeSelected : NoopFunction, + onExpandedChange: setExpandedKeys, + } as TreeProps + + const prefixCls = `${mergedPrefixCls.value}-tree-select-panel` + + return ( +
evt.preventDefault()}> + + {renderFooter(prefixCls)} +
+ ) + } + }, +}) + +function useExpandedKeys(props: ProSearchTreeSelectPanelProps): { + expandedKeys: ComputedRef + setExpandedKeys: (keys: VKey[]) => void +} { + const initialExpandedKeySet = new Set() + props.dataSource && + traverseTree(props.dataSource, 'children', (item, parents) => { + if (props.value?.includes(item.key)) { + parents.forEach(parent => initialExpandedKeySet.add(parent.key)) + } + }) + + const [expandedKeys, setExpandedKeys] = useState([...initialExpandedKeySet]) + + return { + expandedKeys, + setExpandedKeys, + } +} diff --git a/packages/pro/search/src/searchItem/SearchItem.tsx b/packages/pro/search/src/searchItem/SearchItem.tsx index 98acecba1..d7fde2f5a 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, normalizeClass, provide, watch } from 'vue' +import { computed, defineComponent, inject, provide, watch } from 'vue' import SearchItemTag from './SearchItemTag' import Segment from './Segment' @@ -20,8 +20,6 @@ export default defineComponent({ setup(props) { const context = inject(proSearchContext)! const { props: proSearchProps, mergedPrefixCls, activeSegment } = context - - const prefixCls = computed(() => `${mergedPrefixCls.value}-search-item`) const segmentStateContext = useSegmentStates(props, proSearchProps, context) const segmentOverlayUpdateContext = useSegmentOverlayUpdate() const { segmentStates, initSegmentStates } = segmentStateContext @@ -45,32 +43,27 @@ export default defineComponent({ }) const isActive = computed(() => activeSegment.value?.itemKey === props.searchItem!.key) + const itemVisible = computed(() => isActive.value && !proSearchProps.disabled) watch(isActive, active => { if (!active) { initSegmentStates() } }) - const classes = computed(() => { - return normalizeClass({ - [prefixCls.value]: true, - [`${prefixCls.value}-invalid`]: !!props.searchItem?.error, - }) - }) - - return () => { - if (!isActive.value || proSearchProps.disabled) { - return props.searchItem?.key !== tempSearchStateKey ? ( + return () => ( + <> + {!itemVisible.value && props.searchItem?.key !== tempSearchStateKey && ( - ) : undefined - } - - return ( - evt.preventDefault()}> + )} + evt.preventDefault()} + > {segmentRenderDatas.value.map(segment => ( ))} - ) - } + + ) }, }) diff --git a/packages/pro/search/src/searchItem/Segment.tsx b/packages/pro/search/src/searchItem/Segment.tsx index 74f4d9982..90ff1e500 100644 --- a/packages/pro/search/src/searchItem/Segment.tsx +++ b/packages/pro/search/src/searchItem/Segment.tsx @@ -8,7 +8,6 @@ import { type ComputedRef, type Ref, - type WatchStopHandle, computed, defineComponent, inject, @@ -89,14 +88,23 @@ export default defineComponent({ }) }) - let stopActiveSegmentWatch: WatchStopHandle onMounted(() => { - stopActiveSegmentWatch = watch( + watch( + activeSegment, + () => { + nextTick(() => { + if (isActive.value) { + segmentInputRef.value?.focus() + } + }) + }, + { immediate: true }, + ) + watch( isActive, active => { nextTick(() => { if (active) { - segmentInputRef.value?.focus() if (!props.value && props.segment.defaultValue) { handleSegmentChange(props.segment.name, props.segment.defaultValue) handleSegmentConfirm(props.segment.name, false) @@ -128,7 +136,6 @@ export default defineComponent({ registerOverlayUpdate(updateOverlay) }) onBeforeUnmount(() => { - stopActiveSegmentWatch?.() unregisterOverlayUpdate(updateOverlay) }) diff --git a/packages/pro/search/src/segments/CreateTreeSelectSegment.tsx b/packages/pro/search/src/segments/CreateTreeSelectSegment.tsx new file mode 100644 index 000000000..1937da734 --- /dev/null +++ b/packages/pro/search/src/segments/CreateTreeSelectSegment.tsx @@ -0,0 +1,160 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { PanelRenderContext, Segment, TreeSelectPanelData, TreeSelectSearchField } from '../types' + +import { isNil, toString } from 'lodash-es' + +import { type VKey, convertArray, traverseTree } from '@idux/cdk/utils' + +import TreeSelectPanel from '../panel/TreeSelectPanel' + +const defaultSeparator = '|' + +export function createTreeSelectSegment( + prefixCls: string, + searchField: TreeSelectSearchField, +): Segment { + const { + fieldConfig: { + dataSource, + checkable, + cascaderStrategy, + draggable, + draggableIcon, + separator, + showLine, + searchable, + searchFn, + multiple, + virtual, + onCheck, + onDragstart, + onDragend, + onDragenter, + onDragleave, + onDragover, + onDrop, + onExpand, + onSelect, + onLoaded, + }, + defaultValue, + inputClassName, + onPanelVisibleChange, + } = searchField + + const nodeKeyMap = new Map() + const nodeLabelMap = new Map() + traverseTree(dataSource, 'children', item => { + nodeKeyMap.set(item.key, item) + nodeLabelMap.set(toString(item.label).trim(), [...(nodeLabelMap.get(item.label) ?? []), item]) + }) + + 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 handleChange = (value: VKey[]) => { + if (!multiple) { + setValue(value[0]) + ok() + } else { + setValue(value.length > 0 ? value : undefined) + } + } + + return ( + + ) + } + + return { + name: searchField.type, + inputClassName: [inputClassName, `${prefixCls}-tree-select-segment-input`], + placeholder: searchField.placeholder, + defaultValue, + parse: input => parseInput(input, searchField, nodeLabelMap), + format: value => formatValue(value, searchField, nodeKeyMap), + panelRenderer, + onVisibleChange: onPanelVisibleChange, + } +} + +function parseInput( + input: string, + searchField: TreeSelectSearchField, + nodeLabelMap: Map, +): VKey | VKey[] | undefined { + const { separator, multiple } = searchField.fieldConfig + const trimedInput = input.trim() + + const keys = getKeyByLabels(nodeLabelMap, trimedInput.split(separator ?? defaultSeparator)) + + return multiple ? (keys.length > 0 ? keys : undefined) : keys[0] +} + +function formatValue( + value: VKey | VKey[] | undefined, + searchField: TreeSelectSearchField, + nodeKeyMap: Map, +): string { + const { separator } = searchField.fieldConfig + if (isNil(value)) { + return '' + } + + return getLabelByKeys(nodeKeyMap, convertArray(value)).join(` ${separator ?? defaultSeparator} `) +} + +function getLabelByKeys(nodeKeyMap: Map, keys: VKey[]): (string | number)[] { + if (keys.length <= 0) { + return [] + } + + return keys.map(key => nodeKeyMap.get(key)?.label).filter(Boolean) as (string | number)[] +} + +function getKeyByLabels(nodeLabelMap: Map, labels: string[]): VKey[] { + if (labels.length <= 0) { + return [] + } + + const trimedLabels = labels.map(label => label.trim()) + + return trimedLabels.map(label => nodeLabelMap.get(label)?.[0].key).filter(Boolean) as VKey[] +} diff --git a/packages/pro/search/src/types/panels.ts b/packages/pro/search/src/types/panels.ts index 1b21c8d1d..0df680aea 100644 --- a/packages/pro/search/src/types/panels.ts +++ b/packages/pro/search/src/types/panels.ts @@ -5,12 +5,21 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { ExtractInnerPropTypes, VKey } from '@idux/cdk/utils' +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { ExtractInnerPropTypes, MaybeArray, VKey } from '@idux/cdk/utils' +import type { CascaderStrategy } from '@idux/components/cascader' import type { DatePanelProps, DateRangePanelProps } from '@idux/components/date-picker' import type { SelectData } from '@idux/components/select' +import type { TreeDragDropOptions, TreeDroppable } from '@idux/components/tree' +import type { TreeSelectNode } from '@idux/components/tree-select' import type { PropType } from 'vue' export type SelectPanelData = Required> & SelectData +export type TreeSelectPanelData = TreeSelectNode & + Required> & { + children?: TreeSelectPanelData[] + } export const proSearchSelectPanelProps = { value: { type: Array as PropType, default: undefined }, @@ -28,6 +37,45 @@ export const proSearchSelectPanelProps = { } 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 }, + expandedKeys: { type: Array as PropType, default: undefined }, + cascaderStrategy: { type: String as PropType, default: 'off' }, + draggable: { type: Boolean, default: false }, + draggableIcon: { type: String, default: undefined }, + droppable: { type: Function as PropType, default: undefined }, + + expandIcon: { type: [String, Array] as PropType, default: undefined }, + loadChildren: { + type: Function as PropType<(node: TreeSelectPanelData) => Promise>, + default: undefined, + }, + leafLineIcon: { type: String, default: undefined }, + showLine: { type: Boolean, default: undefined }, + searchFn: Function as PropType<(node: TreeSelectPanelData, searchValue?: string) => boolean>, + virtual: { type: Boolean, default: false }, + + onChange: Function as PropType<(value: VKey[]) => void>, + onConfirm: Function as PropType<() => void>, + onCancel: Function as PropType<() => void>, + + onCheck: [Function, Array] as PropType void>>, + onDragstart: [Function, Array] as PropType) => void>>, + onDragend: [Function, Array] as PropType) => void>>, + onDragenter: [Function, Array] as PropType) => void>>, + onDragleave: [Function, Array] as PropType) => void>>, + onDragover: [Function, Array] as PropType) => void>>, + onDrop: [Function, Array] as PropType) => void>>, + onExpand: [Function, Array] as PropType void>>, + onSelect: [Function, Array] as PropType void>>, + onLoaded: [Function, Array] as PropType void>>, +} as const +export type ProSearchTreeSelectPanelProps = ExtractInnerPropTypes + export const proSearchDatePanelProps = { panelType: { type: String as PropType<'datePicker' | 'dateRangePicker'>, diff --git a/packages/pro/search/src/types/searchFields.ts b/packages/pro/search/src/types/searchFields.ts index 67e34918a..7ee7ece66 100644 --- a/packages/pro/search/src/types/searchFields.ts +++ b/packages/pro/search/src/types/searchFields.ts @@ -5,12 +5,16 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { SelectPanelData } from './panels' +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { SelectPanelData, TreeSelectPanelData } from './panels' import type { SearchItemError } from './searchItem' import type { SearchValue } from './searchValue' import type { InputFormater, InputParser, PanelRenderContext } from './segment' -import type { VKey } from '@idux/cdk/utils' +import type { MaybeArray, VKey } from '@idux/cdk/utils' +import type { CascaderStrategy } from '@idux/components/cascader' import type { DatePanelProps, DateRangePanelProps } from '@idux/components/date-picker' +import type { TreeDragDropOptions } from '@idux/components/tree' import type { VNodeChild } from 'vue' interface SearchFieldBase { @@ -26,9 +30,6 @@ interface SearchFieldBase { onPanelVisibleChange?: (visible: boolean) => void } -export const searchDataTypes = ['select', 'input', 'datePicker', 'dateRangePicker', 'custom'] as const -export type SearchDataTypes = (typeof searchDataTypes)[number] - export interface SelectSearchField extends SearchFieldBase { type: 'select' fieldConfig: { @@ -43,6 +44,34 @@ export interface SelectSearchField extends SearchFieldBase { } } +export interface TreeSelectSearchField extends SearchFieldBase { + type: 'treeSelect' + fieldConfig: { + dataSource: TreeSelectPanelData[] + multiple?: boolean + checkable?: boolean + cascaderStrategy: CascaderStrategy + draggable?: boolean + draggableIcon?: string + showLine?: boolean + searchable?: boolean + separator?: string + virtual?: boolean + searchFn?: (node: TreeSelectPanelData, searchValue?: string) => boolean + + onCheck?: MaybeArray<(checked: boolean, node: TreeSelectPanelData) => void> + onDragstart?: MaybeArray<(options: TreeDragDropOptions) => void> + onDragend?: MaybeArray<(options: TreeDragDropOptions) => void> + onDragenter?: MaybeArray<(options: TreeDragDropOptions) => void> + onDragleave?: MaybeArray<(options: TreeDragDropOptions) => void> + onDragover?: MaybeArray<(options: TreeDragDropOptions) => void> + onDrop?: MaybeArray<(options: TreeDragDropOptions) => void> + onExpand?: MaybeArray<(expanded: boolean, node: TreeSelectPanelData) => void> + onSelect?: MaybeArray<(selected: boolean, node: TreeSelectPanelData) => void> + onLoaded?: MaybeArray<(loadedKeys: any[], node: TreeSelectPanelData) => void> + } +} + export interface InputSearchField extends SearchFieldBase { type: 'input' fieldConfig: { @@ -84,7 +113,11 @@ export interface CustomSearchField extends SearchFieldBase { export type SearchField = | SelectSearchField + | TreeSelectSearchField | InputSearchField | DatePickerSearchField | DateRangePickerSearchField | CustomSearchField + +export const searchDataTypes = ['select', 'treeSelect', 'input', 'datePicker', 'dateRangePicker', 'custom'] as const +export type SearchDataTypes = (typeof searchDataTypes)[number] diff --git a/packages/pro/search/style/index.less b/packages/pro/search/style/index.less index 96849660e..34d4d0e89 100644 --- a/packages/pro/search/style/index.less +++ b/packages/pro/search/style/index.less @@ -224,6 +224,17 @@ .panel-footer(); } } + &-tree-select-panel { + &-body { + padding: @spacing-sm 0; + .@{tree-node-prefix} { + padding: 0 @spacing-sm; + } + } + &-footer { + .panel-footer(); + } + } &-date-picker-panel { &-body { padding: @pro-search-date-picker-panel-body-padding; @@ -242,6 +253,10 @@ &-select-panel { min-width: @pro-search-select-panel-min-width; } + &-tree-select-panel { + min-width: @pro-search-tree-select-panel-min-width; + max-width: @pro-search-tree-select-panel-max-width; + } &-name-segment-input { min-width: @pro-search-name-segment-input-min-width; @@ -259,6 +274,10 @@ min-width: @pro-search-select-segment-input-min-width; text-align: @pro-search-select-segment-input-text-align; } + &-tree-select-segment-input { + min-width: @pro-search-tree-select-segment-input-min-width; + text-align: @pro-search-tree-select-segment-input-text-align; + } &-date-picker-segment-input { min-width: @pro-search-date-picker-segment-input-min-width; text-align: @pro-search-date-picker-segment-input-text-align; diff --git a/packages/pro/search/style/themes/default.ts b/packages/pro/search/style/themes/default.ts index 92d0b6e82..256574045 100644 --- a/packages/pro/search/style/themes/default.ts +++ b/packages/pro/search/style/themes/default.ts @@ -1,6 +1,7 @@ // style dependencies import '@idux/components/icon/style/themes/default' import '@idux/components/select/style/themes/default' +import '@idux/components/tree/style/themes/default' import '@idux/components/tooltip/style/themes/default' import '@idux/components/date-picker/style/themes/default' diff --git a/packages/pro/search/style/themes/default.variable.less b/packages/pro/search/style/themes/default.variable.less index c2199bc6a..9e47ee390 100644 --- a/packages/pro/search/style/themes/default.variable.less +++ b/packages/pro/search/style/themes/default.variable.less @@ -75,6 +75,8 @@ @pro-search-name-segment-panel-min-width: 100px; @pro-search-operator-segment-panel-min-width: 20px; @pro-search-select-panel-min-width: 100px; +@pro-search-tree-select-panel-min-width: 200px; +@pro-search-tree-select-panel-max-width: 400px; @pro-search-name-segment-input-min-width: 60px; @pro-search-name-segment-input-text-align: start; @@ -84,6 +86,8 @@ @pro-search-input-segment-input-text-align: start; @pro-search-select-segment-input-min-width: 100px; @pro-search-select-segment-input-text-align: start; +@pro-search-tree-select-segment-input-min-width: 200px; +@pro-search-tree-select-segment-input-text-align: start; @pro-search-date-picker-segment-input-min-width: 100px; @pro-search-date-picker-segment-input-text-align: start; @pro-search-date-range-picker-segment-input-min-width: 100px; diff --git a/packages/pro/search/style/themes/seer.ts b/packages/pro/search/style/themes/seer.ts index ccd120ce4..98cf8f8a1 100644 --- a/packages/pro/search/style/themes/seer.ts +++ b/packages/pro/search/style/themes/seer.ts @@ -2,6 +2,7 @@ import '@idux/components/icon/style/themes/seer' import '@idux/components/select/style/themes/seer' +import '@idux/components/tree/style/themes/seer' import '@idux/components/tooltip/style/themes/seer' import '@idux/components/date-picker/style/themes/seer'