diff --git a/.vscode/settings.json b/.vscode/settings.json index ab5838fec3..f569dfa98b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "typescript.preferences.importModuleSpecifier": "relative", + "js/ts.preferences.importModuleSpecifier": "relative", "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], "[html]": { "editor.formatOnSave": true, @@ -7,7 +7,7 @@ }, "[typescriptreact]": { "editor.formatOnSave": true, - "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" + "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[javascriptreact]": { "editor.formatOnSave": true, diff --git a/packages/components/cascader/Cascader.tsx b/packages/components/cascader/Cascader.tsx index 6c80fff423..e7b66733db 100644 --- a/packages/components/cascader/Cascader.tsx +++ b/packages/components/cascader/Cascader.tsx @@ -236,8 +236,17 @@ const Cascader: React.FC = (originalProps) => { {props.panelTopContent && parseTNode(props.panelTopContent)} + {...pick(props, [ + 'trigger', + 'onChange', + 'empty', + 'loading', + 'loadingText', + 'option', + 'columnHeader', + 'columnFooter', + ])} + /> {props.panelBottomContent && parseTNode(props.panelBottomContent)} } diff --git a/packages/components/cascader/_example/column-slot.tsx b/packages/components/cascader/_example/column-slot.tsx new file mode 100644 index 0000000000..d00bcd1482 --- /dev/null +++ b/packages/components/cascader/_example/column-slot.tsx @@ -0,0 +1,228 @@ +import React from 'react'; +import { Cascader, Input } from 'tdesign-react'; + +const options = [ + { + label: '北京市', + value: '1', + children: [ + { + label: '东城区', + value: '1.1', + children: [ + { label: '安定门街道', value: '1.1.1' }, + { label: '建国门街道', value: '1.1.2' }, + { label: '朝阳门街道', value: '1.1.3' }, + { label: '东直门街道', value: '1.1.4' }, + { label: '和平里街道', value: '1.1.5' }, + { label: '北新桥街道', value: '1.1.6' }, + { label: '交道口街道', value: '1.1.7' }, + { label: '景山街道', value: '1.1.8' }, + ], + }, + { + label: '西城区', + value: '1.2', + children: [ + { label: '西长安街街道', value: '1.2.1' }, + { label: '新街口街道', value: '1.2.2' }, + { label: '月坛街道', value: '1.2.3' }, + { label: '展览路街道', value: '1.2.4' }, + { label: '德胜街道', value: '1.2.5' }, + { label: '金融街街道', value: '1.2.6' }, + ], + }, + { + label: '朝阳区', + value: '1.3', + children: [ + { label: '三里屯街道', value: '1.3.1' }, + { label: '望京街道', value: '1.3.2' }, + { label: '呼家楼街道', value: '1.3.3' }, + { label: '双井街道', value: '1.3.4' }, + { label: '建外街道', value: '1.3.5' }, + ], + }, + { + label: '海淀区', + value: '1.4', + children: [ + { label: '中关村街道', value: '1.4.1' }, + { label: '海淀街道', value: '1.4.2' }, + { label: '清河街道', value: '1.4.3' }, + { label: '上地街道', value: '1.4.4' }, + ], + }, + { label: '丰台区', value: '1.5' }, + { label: '石景山区', value: '1.6' }, + { label: '通州区', value: '1.7' }, + { label: '顺义区', value: '1.8' }, + { label: '大兴区', value: '1.9' }, + { label: '昌平区', value: '1.10' }, + ], + }, + { + label: '上海市', + value: '2', + children: [ + { + label: '黄浦区', + value: '2.1', + children: [ + { label: '南京东路街道', value: '2.1.1' }, + { label: '外滩街道', value: '2.1.2' }, + { label: '豫园街道', value: '2.1.3' }, + { label: '老西门街道', value: '2.1.4' }, + ], + }, + { + label: '徐汇区', + value: '2.2', + children: [ + { label: '徐家汇街道', value: '2.2.1' }, + { label: '天平路街道', value: '2.2.2' }, + { label: '漕河泾街道', value: '2.2.3' }, + ], + }, + { label: '长宁区', value: '2.3' }, + { label: '静安区', value: '2.4' }, + { label: '普陀区', value: '2.5' }, + { label: '虹口区', value: '2.6' }, + { label: '杨浦区', value: '2.7' }, + { label: '浦东新区', value: '2.8' }, + { label: '闵行区', value: '2.9' }, + ], + }, + { + label: '广东省', + value: '3', + children: [ + { + label: '广州市', + value: '3.1', + children: [ + { label: '天河区', value: '3.1.1' }, + { label: '越秀区', value: '3.1.2' }, + { label: '海珠区', value: '3.1.3' }, + { label: '荔湾区', value: '3.1.4' }, + { label: '白云区', value: '3.1.5' }, + { label: '番禺区', value: '3.1.6' }, + ], + }, + { + label: '深圳市', + value: '3.2', + children: [ + { label: '南山区', value: '3.2.1' }, + { label: '福田区', value: '3.2.2' }, + { label: '罗湖区', value: '3.2.3' }, + { label: '宝安区', value: '3.2.4' }, + { label: '龙岗区', value: '3.2.5' }, + ], + }, + { label: '珠海市', value: '3.3' }, + { label: '佛山市', value: '3.4' }, + { label: '东莞市', value: '3.5' }, + { label: '中山市', value: '3.6' }, + ], + }, + { + label: '浙江省', + value: '4', + children: [ + { label: '杭州市', value: '4.1' }, + { label: '宁波市', value: '4.2' }, + { label: '温州市', value: '4.3' }, + { label: '嘉兴市', value: '4.4' }, + ], + }, + { + label: '江苏省', + value: '5', + children: [ + { label: '南京市', value: '5.1' }, + { label: '苏州市', value: '5.2' }, + { label: '无锡市', value: '5.3' }, + ], + }, + { + label: '四川省', + value: '6', + children: [ + { label: '成都市', value: '6.1' }, + { label: '绵阳市', value: '6.2' }, + ], + }, + { + label: '湖北省', + value: '7', + children: [ + { label: '武汉市', value: '7.1' }, + { label: '宜昌市', value: '7.2' }, + ], + }, + { + label: '福建省', + value: '8', + children: [ + { label: '福州市', value: '8.1' }, + { label: '厦门市', value: '8.2' }, + ], + }, + { + label: '山东省', + value: '9', + children: [ + { label: '济南市', value: '9.1' }, + { label: '青岛市', value: '9.2' }, + ], + }, + { + label: '河南省', + value: '10', + children: [ + { label: '郑州市', value: '10.1' }, + { label: '洛阳市', value: '10.2' }, + ], + }, +]; + +const panelFooterStyle = { + padding: '4px 8px', + fontSize: '12px', + color: 'var(--td-text-color-placeholder)', + textAlign: 'center', +} as React.CSSProperties; + +export default function Example() { + const [value, setValue] = React.useState(''); + const [searchValues, setSearchValues] = React.useState>({}); + + return ( + { + setValue(val); + }} + columnHeader={({ panelIndex, onFilter }) => ( + { + onFilter(val); + setSearchValues((prev) => ({ + ...prev, + [panelIndex]: val, + })); + }} + placeholder={`搜索第(${panelIndex + 1})级`} + /> + )} + columnFooter={({ filteredOptions, options: columnOptions }) => ( +
+ {filteredOptions.length} / {columnOptions.length} 项 +
+ )} + >
+ ); +} diff --git a/packages/components/cascader/cascader.en-US.md b/packages/components/cascader/cascader.en-US.md index e59b0547d6..707e5020a0 100644 --- a/packages/components/cascader/cascader.en-US.md +++ b/packages/components/cascader/cascader.en-US.md @@ -14,6 +14,8 @@ checkProps | Object | - | Typescript: `CheckboxProps`,[Checkbox API Documents] checkStrictly | Boolean | false | \- | N clearable | Boolean | false | \- | N collapsedItems | TElement | - | Typescript: `TNode<{ value: CascaderOption[]; collapsedSelectedItems: CascaderOption[]; count: number; onClose: (context: { index: number, e?: MouseEvent }) => void }>`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N +columnFooter | TNode | - | Custom content at the bottom of each column. `panelIndex` indicates the current column index, `options` is the original option list, `filteredOptions` is the filtered option list (same as `options` when no filter is applied), `onFilter` is used to filter the current column options (built-in case-insensitive matching for strings; pass a filter function for custom matching). When the built-in search (filterable) has input, the panel switches to flat mode and `onFilter` becomes a noop。Typescript: `TNode<{ panelIndex: number; options: TreeOptionData[]; filteredOptions: TreeOptionData[]; onFilter: (filter: string \| ((node: TreeOptionData, panelIndex: number) => boolean)) => void }>`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N +columnHeader | TNode | - | Custom content at the top of each column. `panelIndex` indicates the current column index, `options` is the original option list, `filteredOptions` is the filtered option list (same as `options` when no filter is applied), `onFilter` is used to filter the current column options (built-in case-insensitive matching for strings; pass a filter function for custom matching). When the built-in search (filterable) has input, the panel switches to flat mode and `onFilter` becomes a noop。Typescript: `TNode<{ panelIndex: number; options: TreeOptionData[]; filteredOptions: TreeOptionData[]; onFilter: (filter: string \| ((node: TreeOptionData, panelIndex: number) => boolean)) => void }>`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N disabled | Boolean | undefined | \- | N empty | TNode | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N filter | Function | - | Typescript: `(filterWords: string, node: TreeNodeModel) => boolean \| Promise` | N diff --git a/packages/components/cascader/cascader.md b/packages/components/cascader/cascader.md index 4448764a25..a00b7f99a4 100644 --- a/packages/components/cascader/cascader.md +++ b/packages/components/cascader/cascader.md @@ -14,6 +14,8 @@ checkProps | Object | - | 参考 checkbox 组件 API。TS 类型:`CheckboxProp checkStrictly | Boolean | false | 父子节点选中状态不再关联,可各自选中或取消 | N clearable | Boolean | false | 是否支持清空选项 | N collapsedItems | TElement | - | 多选情况下,用于设置折叠项内容,默认为 `+N`。如果需要悬浮就显示其他内容,可以使用 collapsedItems 自定义。`value` 表示当前存在的所有标签,`collapsedSelectedItems` 表示折叠的标签,`count` 表示折叠的数量,`onClose` 表示移除标签的事件回调。TS 类型:`TNode<{ value: CascaderOption[]; collapsedSelectedItems: CascaderOption[]; count: number; onClose: (context: { index: number, e?: MouseEvent }) => void }>`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N +columnFooter | TNode | - | 每一列的底部自定义内容。`panelIndex` 表示当前列索引,`options` 表示当前列原始选项,`filteredOptions` 表示当前列过滤后的选项(未过滤时与 options 相同),`onFilter` 用于过滤当前列选项(传入字符串时内置大小写不敏感匹配;如需自定义匹配逻辑请传入过滤函数)。当内置搜索(filterable)有输入时,面板切换为扁平模式,`onFilter` 为空操作。TS 类型:`TNode<{ panelIndex: number; options: TreeOptionData[]; filteredOptions: TreeOptionData[]; onFilter: (filter: string \| ((node: TreeOptionData, panelIndex: number) => boolean)) => void }>`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N +columnHeader | TNode | - | 每一列的顶部自定义内容。`panelIndex` 表示当前列索引,`options` 表示当前列原始选项,`filteredOptions` 表示当前列过滤后的选项(未过滤时与 options 相同),`onFilter` 用于过滤当前列选项(传入字符串时内置大小写不敏感匹配;如需自定义匹配逻辑请传入过滤函数)。当内置搜索(filterable)有输入时,面板切换为扁平模式,`onFilter` 为空操作。。TS 类型:`TNode<{ panelIndex: number; options: TreeOptionData[]; filteredOptions: TreeOptionData[]; onFilter: (filter: string \| ((node: TreeOptionData, panelIndex: number) => boolean)) => void }>`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N disabled | Boolean | undefined | 是否禁用组件 | N empty | TNode | - | 无匹配选项时的内容,默认全局配置为 '暂无数据'。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N filter | Function | - | 自定义过滤方法,用于对现有数据进行搜索过滤,判断是否过滤某一项数据。TS 类型:`(filterWords: string, node: TreeNodeModel) => boolean \| Promise` | N diff --git a/packages/components/cascader/components/Panel.tsx b/packages/components/cascader/components/Panel.tsx index 5b973b526d..29cc3cca77 100644 --- a/packages/components/cascader/components/Panel.tsx +++ b/packages/components/cascader/components/Panel.tsx @@ -1,48 +1,146 @@ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import classNames from 'classnames'; +import noop from '../../_util/noop'; import parseTNode from '../../_util/parseTNode'; import useConfig from '../../hooks/useConfig'; import { useLocaleReceiver } from '../../locale/LocalReceiver'; -import { expendClickEffect, valueChangeEffect } from '../core/effect'; -import { getPanels } from '../core/helper'; +import { expandClickEffect, valueChangeEffect } from '../core/effect'; +import { FILTER_INACTIVE_LEVEL, filterOptions, getPanels, isFilterActive, isFilterLevelActive } from '../core/helper'; import Item from './Item'; import type { StyledProps } from '../../common'; -import type { CascaderContextType, TreeNode } from '../interface'; +import type { CascaderContextType, FilterState, FilterValue, TreeNode } from '../interface'; import type { TdCascaderProps } from '../type'; export interface CascaderPanelProps extends StyledProps, - Pick { + Pick< + TdCascaderProps, + 'trigger' | 'empty' | 'onChange' | 'loading' | 'loadingText' | 'option' | 'columnHeader' | 'columnFooter' + > { cascaderContext: CascaderContextType; } const Panel = (props: CascaderPanelProps) => { - const { cascaderContext, option } = props; + const { cascaderContext, option, columnHeader, columnFooter } = props; + const { classPrefix } = useConfig(); + const [global] = useLocaleReceiver('cascader'); + const [filterState, setFilterState] = useState(null); + const filterFnsRef = useRef void> | null>(null); + // console.log('filterState', filterState); + if (!filterFnsRef.current) { + filterFnsRef.current = new Map void>(); + } + + const COMPONENT_NAME = `${classPrefix}-cascader`; const panels = useMemo(() => getPanels(cascaderContext.treeNodes), [cascaderContext.treeNodes]); - const handleExpand = (node: TreeNode, trigger: 'hover' | 'click') => { - const { trigger: propsTrigger, cascaderContext } = props; - expendClickEffect(propsTrigger, trigger, node, cascaderContext); + const hasActiveFilter = useMemo(() => filterState && hasAnyActiveFilter(filterState.filters), [filterState]); + + const getFilteredNodes = (nodes: TreeNode[], index: number): TreeNode[] => { + const state = filterState; + if (!state) return nodes; + const filter = state.filters[index]; + if (!filter) return nodes; + return filterOptions(nodes, filter, index); }; - const { classPrefix } = useConfig(); - const [global] = useLocaleReceiver('cascader'); - const COMPONENT_NAME = `${classPrefix}-cascader`; + function hasAnyActiveFilter(filters: Record): boolean { + return Object.values(filters).some((f) => isFilterActive(f)); + } + + const clearExpiredFilters = (filters: Record, maxLevel: number): Record => + Object.fromEntries(Object.entries(filters).filter(([panelIndex]) => Number(panelIndex) <= maxLevel)); + + const calculateCascadeMaxLevel = (panelIndex: number, filteredNodes: TreeNode[], currentMaxLevel: number): number => { + if (filteredNodes.length === 0) { + return panelIndex; + } + return Math.max(panelIndex, currentMaxLevel); + }; + + const handleFilter = (index: number, filter: FilterValue) => { + const prev = filterState; + + let filters: Record = { ...prev?.filters }; + if (isFilterActive(filter)) { + filters[index] = filter; + } else { + delete filters[index]; + } + + let maxLevel = prev?.maxLevel ?? FILTER_INACTIVE_LEVEL; + + if (isFilterActive(filter)) { + const currentNodes = panels[index] || []; + const filteredNodes = filterOptions(currentNodes, filter, index); + maxLevel = calculateCascadeMaxLevel(index, filteredNodes, maxLevel); + } else if (!hasAnyActiveFilter(filters)) { + maxLevel = FILTER_INACTIVE_LEVEL; + } + + if (maxLevel < (prev?.maxLevel ?? FILTER_INACTIVE_LEVEL)) { + filters = clearExpiredFilters(filters, maxLevel); + } + // console.log('handleFilter', index, filters, maxLevel); + setFilterState({ filters, maxLevel }); + }; + + const shouldShowPanel = (index: number): boolean => { + const state = filterState; + if (!hasActiveFilter || !state || !isFilterLevelActive(state.maxLevel)) { + return true; + } + return index <= state.maxLevel; + }; + + const handleExpand = (node: TreeNode, trigger: 'hover' | 'click', level: number) => { + const state = filterState; + + const { children } = node; + if ( + state && + isFilterLevelActive(state.maxLevel) && + props.trigger === trigger && + Array.isArray(children) && + children.length + ) { + const childLevel = level + 1; + if (childLevel > state.maxLevel) { + const cleanedFilters = clearExpiredFilters(state.filters, childLevel); + setFilterState({ filters: cleanedFilters, maxLevel: childLevel }); + } + } + + expandClickEffect(props.trigger, trigger, node, cascaderContext); + }; + + // 参照 vue-next 的写法 + const getOnFilterCallback = (index: number) => { + let callback = filterFnsRef.current.get(index); + if (!callback) { + callback = (filter: FilterValue) => handleFilter(index, filter); + filterFnsRef.current.set(index, callback); + } + return callback; + }; + + // 不使用map 存callback的写法 + // const getOnFilterCallback = (index: number) => (filter: FilterValue) => handleFilter(index, filter); const renderItem = (node: TreeNode, index: number) => ( { - handleExpand(node, 'click'); + handleExpand(node, 'click', index); }} onMouseEnter={() => { - handleExpand(node, 'hover'); + handleExpand(node, 'hover', index); }} onChange={() => { valueChangeEffect(node, cascaderContext); @@ -50,28 +148,66 @@ const Panel = (props: CascaderPanelProps) => { /> ); - const renderList = (treeNodes: TreeNode[], isFilter = false, segment = true, key = '1') => ( -
    - {treeNodes.map((node: TreeNode, index: number) => renderItem(node, index))} -
- ); + const renderList = (treeNodes: TreeNode[], segment = true, index = 0) => { + const displayNodes = hasActiveFilter ? getFilteredNodes(treeNodes, index) : treeNodes; + + const columnParams = { + panelIndex: index, + options: treeNodes.map((node) => node.data), + filteredOptions: displayNodes.map((node) => node.data), + onFilter: getOnFilterCallback(index), + }; + + return ( +
    + {parseTNode(columnHeader, columnParams)} + {displayNodes.map((node: TreeNode) => renderItem(node, index))} + {parseTNode(columnFooter, columnParams)} +
+ ); + }; + + const renderFilteredList = (treeNodes: TreeNode[]) => { + const columnParams = { + panelIndex: 0, + options: treeNodes.map((node) => node.data), + filteredOptions: treeNodes.map((node) => node.data), + onFilter: noop, + }; + + return ( +
    + {parseTNode(columnHeader, columnParams)} + {treeNodes.map((node: TreeNode) => renderItem(node, 0))} + {parseTNode(columnFooter, columnParams)} +
+ ); + }; const renderPanels = () => { - const { inputVal, treeNodes } = props.cascaderContext; - return inputVal - ? renderList(treeNodes, true) - : panels.map((treeNodes, index: number) => - renderList(treeNodes, false, index !== panels.length - 1, `${COMPONENT_NAME}__menu${index}`), - ); + const { inputVal, treeNodes } = cascaderContext; + if (inputVal) return renderFilteredList(treeNodes); + + const result = []; + const len = panels.length; + for (let i = 0; i < len; i++) { + if (shouldShowPanel(i)) { + result.push(renderList(panels[i], i !== len - 1, i)); + } + } + return result; }; - let content; + let content: React.ReactNode; + if (props.loading) { content =
{props.loadingText ?? global.loadingText}
; } else { @@ -81,6 +217,25 @@ const Panel = (props: CascaderPanelProps) => {
{props.empty ?? global.empty}
); } + + useEffect(() => { + if (!panels.length) return; + const filterFns = filterFnsRef.current; + const maxIndex = panels.length - 1; + for (const [index] of filterFns) { + if (index > maxIndex) { + filterFns.delete(index); + } + } + }, [panels]); + + useEffect( + () => () => { + filterFnsRef.current.clear(); + }, + [], + ); + return (
checkOptionMatchKeyword(node, keyword)); + } + return nodes.filter((node) => filter(node.data, panelIndex)); +} diff --git a/packages/components/cascader/interface.ts b/packages/components/cascader/interface.ts index ee13e287cc..74e1425b09 100644 --- a/packages/components/cascader/interface.ts +++ b/packages/components/cascader/interface.ts @@ -1,7 +1,8 @@ import type TreeNode from '@tdesign/common-js/tree-v1/tree-node'; import type TreeStore from '@tdesign/common-js/tree-v1/tree-store'; -import type { TreeNodeModel, TreeNodeValue } from '@tdesign/common-js/tree-v1/types'; +import type { TreeNodeModel, TreeNodeValue, TypeTreeNodeData } from '@tdesign/common-js/tree-v1/types'; import type { TdSelectInputProps } from '../select-input/type'; +import type { TreeOptionData } from './interface'; import type { CascaderChangeSource, CascaderValue, TdCascaderProps } from './type'; export * from './type'; @@ -43,3 +44,12 @@ export { TreeNode } from '@tdesign/common-js/tree-v1/tree-node'; export type { TreeNodeValue } from '@tdesign/common-js/tree-v1/types'; export const EVENT_NAME_WITH_KEBAB = ['remove', 'blur', 'focus']; + +export type CascaderOption = TreeOptionData | TypeTreeNodeData; + +export interface FilterState { + filters: Record boolean)>; + maxLevel: number; +} + +export type FilterValue = string | ((node: CascaderOption, panelIndex: number) => boolean); diff --git a/packages/components/cascader/type.ts b/packages/components/cascader/type.ts index fc03b04c8b..a8cfb2ac5e 100644 --- a/packages/components/cascader/type.ts +++ b/packages/components/cascader/type.ts @@ -48,6 +48,24 @@ export interface TdCascaderProps void; }>; + /** + * 每一列的底部自定义内容。`panelIndex` 表示当前列索引,`options` 表示当前列原始选项,`filteredOptions` 表示当前列过滤后的选项(未过滤时与 options 相同),`onFilter` 用于过滤当前列选项(传入字符串时内置大小写不敏感匹配;如需自定义匹配逻辑请传入过滤函数)。当内置搜索(filterable)有输入时,面板切换为扁平模式,`onFilter` 为空操作 + */ + columnFooter?: TNode<{ + panelIndex: number; + options: TreeOptionData[]; + filteredOptions: TreeOptionData[]; + onFilter: (filter: string | ((node: TreeOptionData, panelIndex: number) => boolean)) => void; + }>; + /** + * 每一列的顶部自定义内容。`panelIndex` 表示当前列索引,`options` 表示当前列原始选项,`filteredOptions` 表示当前列过滤后的选项(未过滤时与 options 相同),`onFilter` 用于过滤当前列选项(传入字符串时内置大小写不敏感匹配;如需自定义匹配逻辑请传入过滤函数)。当内置搜索(filterable)有输入时,面板切换为扁平模式,`onFilter` 为空操作。 + */ + columnHeader?: TNode<{ + panelIndex: number; + options: TreeOptionData[]; + filteredOptions: TreeOptionData[]; + onFilter: (filter: string | ((node: TreeOptionData, panelIndex: number) => boolean)) => void; + }>; /** * 是否禁用组件 */