Browse Source

feat: Tree/TreeSelect support keyMaps API (#1858)

* feat: Tree/TreeSelect support fieldNames API

* fix: Set the fieldNames parameter of the calcCheckedKeys function to be optional to prevent type errors

* chore: update yarn.lock file

* fix: use keyMaps as a param to customize the key name of TreeData

* fix: Modify API type name and code optimization

* fix: Code optimization to reduce unnecessary execution

* fix: fix wrong change in powerQuery.js

* docs: add right version for keyMaps API

* feat: pick icon in renderTreeNode

* fix: Fix the key acquisition error problem in tree
YyumeiZhang 1 year ago
parent
commit
68be8082ac

+ 1 - 0
content/input/treeselect/index-en-US.md

@@ -1423,6 +1423,7 @@ function Demo() {
 | expandAction             | Expand logic, one of false, 'click', 'doubleClick'. Default is set to false, which means item will not be expanded on clicking except on expand icon    | boolean \| string   | false | 1.4.0        |
 | expandAll | Set whether to expand all nodes by default. If the data (`treeData`) changes, the default expansion will still be affected by this api | boolean | false | 1.30.0 |
 | expandedKeys        | (Controlled)Keys of expanded nodes. Direct child nodes will be displayed.  | string[]                    | -       | 0.32.0 |
+| keyMaps | Customize the key, label, and value fields in the node | object |  - | 2.47.0 |
 | filterTreeNode           | Toggle whether searchable or pass in a function to customize search behavior, data parameter provided since v2.28.0 | boolean\|(inputValue: string, treeNodeString: TreeNodeString, data?: TreeNodeData) => boolean | false       | -       |
 | getPopupContainer        | Container to render pop-up, you need to set 'position: relative`  This will change the DOM tree position, but not the view's rendering position.                                                    | function():HTMLElement                                            | -           | -       |
 | insetLabel               | Prefix alias,used mainly in Form                                                   | ReactNode                                                         | -           | 0.28.0  |

+ 1 - 0
content/input/treeselect/index.md

@@ -1404,6 +1404,7 @@ function Demo() {
 | expandAction             | 展开逻辑,可选 false, 'click', 'doubleClick'。默认值为 false,即仅当点击展开按钮时才会展开                                                                            | boolean \| string   | false | 1.4.0      |
 | expandAll | 设置是否默认展开所有节点,若后续数据(`treeData`)发生改变,默认的展开情况也是会受到这个 api 影响的                                                                                  | boolean | false | 1.30.0 |
 | expandedKeys | (受控)展开的节点,默认展开节点显示其直接子级                                                                                                                    | string[] | - | 0.32.0 |
+| keyMaps | 自定义节点中 key、label、value 的字段 | object |  - | 2.47.0 |
 | filterTreeNode | 是否根据输入项进行筛选,默认用 `treeNodeFilterProp` 的值作为要筛选的 `TreeNodeData` 的属性值, data 参数自 v2.28.0 开始提供                                                   | boolean\| <ApiType detail='(inputValue: string, treeNodeString: string, data?: TreeNodeData) => boolean'>Function</ApiType> | false | - |
 | getPopupContainer  | 指定父级 DOM,弹层将会渲染至该 DOM 中,自定义需要设置 `position: relative` 这会改变浮层 DOM 树位置,但不会改变视图渲染位置。                                                                                       | function():HTMLElement | - | - |
 | insetLabel | 前缀标签别名,主要用于 Form                                                                                                                           | ReactNode | - |0.28.0 |

+ 1 - 0
content/navigation/tree/index-en-US.md

@@ -2212,6 +2212,7 @@ import { IconFixedStroked, IconSectionStroked, IconAbsoluteStroked, IconInnerSec
 | expandAction             | Expand logic, one of false, 'click', 'doubleClick'. Default is set to false, which means item will not be expanded on clicking except on expand icon    | boolean \| string   | false | 0.35.0       |
 | expandAll | Set whether to expand all nodes by default. If the subsequent data (`treeData`/`treeDataSimpleJson`) changes, the default expansion will also be affected by this api | boolean | false | 1.30.0 |
 | expandedKeys        | (Controlled)Keys of expanded nodes. Direct child nodes will be displayed.  | string[]                    | -       | - |
+| keyMaps | Customize the key, label, and value fields in the node | object |  - | 2.47.0 |
 | filterTreeNode      | Toggle whether searchable or pass in a function to customize search behavior, data parameter provided since v2.28.0 | boolean \| ((inputValue: string, treeNodeString: string, data?: TreeNodeData) => boolean)  | false   | - |
 | hideDraggingNode | Toggle whether to hide dragImg of dragging node | boolean | false | 1.8.0 | 
 | icon       | Icon | ReactNode         | -       | - |

+ 1 - 0
content/navigation/tree/index.md

@@ -2227,6 +2227,7 @@ import { IconFixedStroked, IconSectionStroked, IconAbsoluteStroked, IconInnerSec
 | expandAction             | 展开逻辑,可选 false, 'click', 'doubleClick'。默认值为 false,即仅当点击展开按钮时才会展开  | boolean \| string   | false | 0.35.0       |
 | expandAll | 设置是否默认展开所有节点,若后续数据(`treeData`/`treeDataSimpleJson`)发生改变,默认展开情况也是会受到这个 api 影响的 | boolean | false | 1.30.0 |
 | expandedKeys | (受控)展开的节点,默认展开节点显示其直接子级 | string[] | - | - |
+| keyMaps | 自定义节点中 key、label、value 的字段 | object |  - | 2.47.0 |
 | filterTreeNode | 是否根据输入项进行筛选,默认用 `treeNodeFilterProp` 的值作为要筛选的 `TreeNodeData` 的属性值,  data 参数自 v2.28.0 开始提供 | boolean \| ((inputValue: string, treeNodeString: string, data?: TreeNodeData) => boolean) | false | - |
 | hideDraggingNode | 是否隐藏正在拖拽的节点的 dragImg | boolean | false | 1.8.0 | 
 | icon | 自定义图标 | ReactNode | - | - |

+ 14 - 7
packages/semi-foundation/tree/foundation.ts

@@ -22,6 +22,8 @@ import {
     calcDropActualPosition
 } from './treeUtil';
 
+export { KeyMapProps } from './treeUtil';
+
 export interface BasicTreeNodeProps {
     [x: string]: any;
     expanded?: boolean;
@@ -44,7 +46,7 @@ export interface BasicTreeNodeProps {
 
 export interface BasicTreeNodeData {
     [x: string]: any;
-    key: string;
+    key?: string;
     value?: number | string;
     label?: any;
     icon?: any;
@@ -425,7 +427,7 @@ export default class TreeFoundation extends BaseFoundation<TreeAdapter, BasicTre
 
     notifyMultipleChange(key: string[], e: any) {
         const { keyEntities } = this.getStates();
-        const { leafOnly, checkRelation } = this.getProps();
+        const { leafOnly, checkRelation, keyMaps } = this.getProps();
         let value;
         let keyList = [];
         if (checkRelation === 'related') {
@@ -436,13 +438,14 @@ export default class TreeFoundation extends BaseFoundation<TreeAdapter, BasicTre
         if (this.getProp('onChangeWithObject')) {
             value = keyList.map((itemKey: string) => keyEntities[itemKey].data);
         } else {
-            value = getValueOrKey(keyList.map((itemKey: string) => keyEntities[itemKey].data));
+            value = getValueOrKey(keyList.map((itemKey: string) => keyEntities[itemKey].data), keyMaps);
         }
         this._adapter.notifyChange(value);
     }
 
     notifyChange(key: string[] | string, e: any) {
         const isMultiple = this._isMultiple();
+        const { keyMaps } = this.getProps();
         const { keyEntities } = this.getStates();
         if (this.getProp('treeDataSimpleJson')) {
             this.notifyJsonChange(key, e);
@@ -454,7 +457,7 @@ export default class TreeFoundation extends BaseFoundation<TreeAdapter, BasicTre
                 value = get(keyEntities, key).data;
             } else {
                 const { data } = get(keyEntities, key);
-                value = getValueOrKey(data);
+                value = getValueOrKey(data, keyMaps);
             }
             this._adapter.notifyChange(value);
         }
@@ -464,7 +467,8 @@ export default class TreeFoundation extends BaseFoundation<TreeAdapter, BasicTre
         // Input is a controlled component, so the value value needs to be updated
         this._adapter.updateInputValue(sugInput);
         const { expandedKeys, selectedKeys, keyEntities, treeData } = this.getStates();
-        const { showFilteredOnly, filterTreeNode, treeNodeFilterProp } = this.getProps();
+        const { showFilteredOnly, filterTreeNode, treeNodeFilterProp, keyMaps } = this.getProps();
+        const realFilterProp = treeNodeFilterProp !== 'label' ? treeNodeFilterProp : get(keyMaps, 'label', 'label');
         let filteredOptsKeys: string[] = [];
         let expandedOptsKeys: string[] = [];
         let flattenNodes: BasicFlattenNode[] = [];
@@ -473,10 +477,10 @@ export default class TreeFoundation extends BaseFoundation<TreeAdapter, BasicTre
         if (!sugInput) {
             expandedOptsKeys = findAncestorKeys(selectedKeys, keyEntities);
             expandedOptsKeys.forEach(item => expandedKeys.add(item));
-            flattenNodes = flattenTreeData(treeData, expandedKeys);
+            flattenNodes = flattenTreeData(treeData, expandedKeys, keyMaps);
         } else {
             filteredOptsKeys = Object.values(keyEntities)
-                .filter((item: BasicKeyEntity) => filter(sugInput, item.data, filterTreeNode, treeNodeFilterProp))
+                .filter((item: BasicKeyEntity) => filter(sugInput, item.data, filterTreeNode, realFilterProp))
                 .map((item: BasicKeyEntity) => item.key);
             expandedOptsKeys = findAncestorKeys(filteredOptsKeys, keyEntities, false);
             const shownChildKeys = findDescendantKeys(filteredOptsKeys, keyEntities, true);
@@ -484,6 +488,7 @@ export default class TreeFoundation extends BaseFoundation<TreeAdapter, BasicTre
             flattenNodes = flattenTreeData(
                 treeData,
                 new Set(expandedOptsKeys),
+                keyMaps,
                 showFilteredOnly && filteredShownKeys
             );
         }
@@ -627,6 +632,7 @@ export default class TreeFoundation extends BaseFoundation<TreeAdapter, BasicTre
 
     setExpandedStatus(treeNode: BasicTreeNodeProps) {
         const { inputValue, treeData, filteredShownKeys, keyEntities } = this.getStates();
+        const { keyMaps } = this.getProps();
         const isSearching = Boolean(inputValue);
         const showFilteredOnly = this._showFilteredOnly();
         const expandedStateKey = isSearching ? 'filteredExpandedKeys' : 'expandedKeys';
@@ -646,6 +652,7 @@ export default class TreeFoundation extends BaseFoundation<TreeAdapter, BasicTre
             const flattenNodes = flattenTreeData(
                 treeData,
                 expandedKeys,
+                keyMaps,
                 isSearching && showFilteredOnly && filteredShownKeys
             );
             const motionKeys = this._isAnimated() ? getMotionKeys(eventKey, expandedKeys, keyEntities) : [];

+ 47 - 18
packages/semi-foundation/tree/treeUtil.ts

@@ -3,7 +3,7 @@
  * https://github.com/react-component/tree/blob/master/src/util.tsx
  */
 
-import { difference, uniq, max, isObject, isNull, isUndefined, isEmpty, pick, get } from 'lodash';
+import { difference, uniq, max, isObject, isNull, isUndefined, isEmpty, pick, get, omit } from 'lodash';
 
 export interface KeyEntities {
     [x: string]: any
@@ -20,6 +20,16 @@ export interface NodeData {
     children?: any
 }
 
+export interface KeyMapProps {
+    key?: string;
+    label?: string;
+    value?: string;
+    disabled?: string;
+    children?: string;
+    isLeaf?: string;
+    icon?: string
+}
+
 const DRAG_OFFSET = 0.45;
 
 function getPosition(level: any, index: any) {
@@ -37,17 +47,28 @@ function isValid(val: any) {
  * @param filteredShownKeys
  * need expanded keys, provides `true` means all expanded
  */
-export function flattenTreeData(treeNodeList: any[], expandedKeys: Set<string>, filteredShownKeys: boolean | Set<any> = false) {
+export function flattenTreeData(treeNodeList: any[], expandedKeys: Set<string>, keyMaps: KeyMapProps, filteredShownKeys: boolean | Set<any> = false) {
     const flattenList: any[] = [];
     const filterSearch = Boolean(filteredShownKeys);
+    const realKeyName = get(keyMaps, 'key', 'key');
+    const realChildrenName = get(keyMaps, 'children', 'children');
     function flatten(list: any[], parent: any = null) {
         return list.map((treeNode, index) => {
             const pos = getPosition(parent ? parent.pos : '0', index);
-            const mergedKey = treeNode.key;
+            const mergedKey = treeNode[realKeyName];
+
+            const otherData = {};
+            if (keyMaps) {
+                Object.entries(omit(keyMaps, 'children')).forEach(([key, value]) => {
+                    const result = treeNode[value as string];
+                    !isUndefined(result) && (otherData[key] = result);
+                });
+            }
 
             // Add FlattenDataNode into list
             const flattenNode: any = {
                 ...pick(treeNode, ['key', 'label', 'value', 'icon', 'disabled', 'isLeaf']),
+                ...otherData,
                 parent,
                 pos,
                 children: null,
@@ -61,7 +82,7 @@ export function flattenTreeData(treeNodeList: any[], expandedKeys: Set<string>,
 
             // Loop treeNode children
             if (expandedKeys.has(mergedKey) && (!filterSearch || (!isBooleanFilteredShownKeys && filteredShownKeys.has(mergedKey)))) {
-                flattenNode.children = flatten(treeNode.children || [], flattenNode);
+                flattenNode.children = flatten(treeNode[realChildrenName] || [], flattenNode);
             } else {
                 flattenNode.children = [];
             }
@@ -100,17 +121,20 @@ export function convertJsonToData(treeJson: TreeDataSimpleJson) {
 /**
  * Traverse all the data by `treeData`.
  */
-export function traverseDataNodes(treeNodes: any[], callback: (data: any) => void) {
+export function traverseDataNodes(treeNodes: any[], callback: (data: any) => void, keyMaps: KeyMapProps) {
+    const realKeyName = get(keyMaps, 'key', 'key');
+    const realChildrenName = get(keyMaps, 'children', 'children');
     const processNode = (node: any, ind?: number, parent?: any) => {
-        const children = node ? node.children : treeNodes;
+        const children = node ? node[realChildrenName] : treeNodes;
         const pos = node ? getPosition(parent.pos, ind) : '0';
         // Process node if is not root
         if (node) {
+            const nodeKey = get(node, realKeyName, null);
             const data = {
                 data: { ...node },
                 ind,
                 pos,
-                key: node.key !== null ? node.key : pos,
+                key: nodeKey !== null ? nodeKey : pos,
                 parentPos: parent.node ? parent.pos : null,
                 level: Number(parent.level) + 1,
             };
@@ -132,7 +156,7 @@ export function traverseDataNodes(treeNodes: any[], callback: (data: any) => voi
 }
 
 /* Convert data to entities map */
-export function convertDataToEntities(dataNodes: any[]) {
+export function convertDataToEntities(dataNodes: any[], keyMaps?: KeyMapProps) {
     const posEntities = {};
     const keyEntities = {};
     const valueEntities = {};
@@ -141,11 +165,12 @@ export function convertDataToEntities(dataNodes: any[]) {
         keyEntities,
         valueEntities,
     };
+    const realValueName = get(keyMaps, 'value', 'value');
 
     traverseDataNodes(dataNodes, (data: any) => {
         const { pos, key, parentPos } = data;
         const entity = { ...data };
-        const value = get(entity, 'data.value', null);
+        const value = get(entity, `data.${realValueName}`, null);
 
         if (value !== null) {
             valueEntities[value] = key;
@@ -160,7 +185,7 @@ export function convertDataToEntities(dataNodes: any[]) {
             entity.parent.children = entity.parent.children || [];
             entity.parent.children.push(entity);
         }
-    });
+    }, keyMaps);
 
     return wrapper;
 }
@@ -566,6 +591,7 @@ export function filterTreeData(info: any) {
         filterTreeNode,
         filterProps,
         prevExpandedKeys,
+        keyMaps
     } = info;
 
     let filteredOptsKeys = [];
@@ -579,7 +605,7 @@ export function filterTreeData(info: any) {
     }
     const shownChildKeys = findDescendantKeys(filteredOptsKeys, keyEntities, true);
     const filteredShownKeys = new Set([...shownChildKeys, ...expandedOptsKeys]);
-    const flattenNodes = flattenTreeData(treeData, new Set(expandedOptsKeys), showFilteredOnly && filteredShownKeys);
+    const flattenNodes = flattenTreeData(treeData, new Set(expandedOptsKeys), keyMaps, showFilteredOnly && filteredShownKeys);
 
     return {
         flattenNodes,
@@ -590,17 +616,19 @@ export function filterTreeData(info: any) {
 }
 
 // return data.value if data.value exist else fall back to key
-export function getValueOrKey(data: any) {
+export function getValueOrKey(data: any, keyMaps?: KeyMapProps) {
+    const valueName = get(keyMaps, 'value', 'value');
+    const keyName = get(keyMaps, 'key', 'key');
     if (Array.isArray(data)) {
-        return data.map(item => get(item, 'value', item.key));
+        return data.map(item => get(item, valueName, item[keyName]));
     }
-    return get(data, 'value', data.key);
+    return get(data, valueName, data[keyName]);
 }
 
 /* Convert value to string */
-export function normalizeValue(value: any, withObject: boolean) {
+export function normalizeValue(value: any, withObject: boolean, keyMaps?: KeyMapProps) {
     if (withObject && isValid(value)) {
-        return getValueOrKey(value);
+        return getValueOrKey(value, keyMaps);
     } else {
         return value;
     }
@@ -611,8 +639,9 @@ export function updateKeys(keySet: Set<string> | string[], keyEntities: KeyEntit
     return keyArr.filter(key => key in keyEntities);
 }
 
-export function calcDisabledKeys(keyEntities: KeyEntities) {
-    const disabledKeys = Object.keys(keyEntities).filter(key => keyEntities[key].data.disabled);
+export function calcDisabledKeys(keyEntities: KeyEntities, keyMaps?: KeyMapProps) {
+    const disabledName = get(keyMaps, 'disabled', 'disabled');
+    const disabledKeys = Object.keys(keyEntities).filter(key => keyEntities[key].data[disabledName]);
     const { checkedKeys } = calcCheckedKeys(disabledKeys, keyEntities);
     return checkedKeys;
 }

+ 35 - 26
packages/semi-foundation/treeSelect/foundation.ts

@@ -271,7 +271,9 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
     }
 
     findDataForValue(findValue: string) {
-        const { value, defaultValue } = this.getProps();
+        const { value, defaultValue, keyMaps } = this.getProps();
+        const realValueName = get(keyMaps, 'value', 'value');
+        const realKeyName = get(keyMaps, 'key', 'key');
         let valueArr = [];
         if (value) {
             valueArr = Array.isArray(value) ? value : [value];
@@ -279,15 +281,17 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
             valueArr = Array.isArray(defaultValue) ? defaultValue : [defaultValue];
         }
         return valueArr.find(item => {
-            return item.value === findValue || item.key === findValue;
+            return item[realValueName] === findValue || item[realKeyName] === findValue;
         });
     }
 
     constructDataForValue(value: string) {
-        const { treeNodeLabelProp } = this.getProps();
+        const { treeNodeLabelProp, keyMaps } = this.getProps();
+        const keyName = get(keyMaps, 'key', 'key');
+        const labelName = get(keyMaps, 'label', treeNodeLabelProp);
         return {
-            key: value,
-            [treeNodeLabelProp]: value
+            [keyName]: value,
+            [labelName]: value
         };    
     }
 
@@ -401,29 +405,30 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
 
     _notifyMultipleChange(key: string[], e: any) {
         const { keyEntities } = this.getStates();
-        const { leafOnly, checkRelation } = this.getProps();
+        const { leafOnly, checkRelation, keyMaps } = this.getProps();
         let keyList = [];
         if (checkRelation === 'related') {
             keyList = normalizeKeyList(key, keyEntities, leafOnly, true);
         } else if (checkRelation === 'unRelated') {
             keyList = key as string[];
         }
-        const nodes = keyList.map(key => (keyEntities[key] && keyEntities[key].data.key === key) ? keyEntities[key].data : this.getDataForKeyNotInKeyEntities(key));
+        const nodes = keyList.map(key => (keyEntities[key] && keyEntities[key].key === key) ? keyEntities[key].data : this.getDataForKeyNotInKeyEntities(key));
         if (this.getProp('onChangeWithObject')) {
             this._adapter.notifyChangeWithObject(nodes, e);
         } else {
-            const value = getValueOrKey(nodes);
+            const value = getValueOrKey(nodes, keyMaps);
             this._adapter.notifyChange(value, nodes, e);
         }
     }
 
     _notifyChange(key: any, e: any) {
         const { keyEntities } = this.getStates();
+        const { keyMaps } = this.getProps();
         if (this._isMultiple() && Array.isArray(key)) {
             this._notifyMultipleChange(key, e);
         } else {
             const nodes = isUndefined(key) ? key : keyEntities[key].data;
-            const value = isUndefined(key) ? key : getValueOrKey(nodes);
+            const value = isUndefined(key) ? key : getValueOrKey(nodes, keyMaps);
             if (this.getProp('onChangeWithObject')) {
                 this._adapter.notifyChangeWithObject(nodes, e);
             } else {
@@ -552,10 +557,11 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
     }
 
     removeTag(eventKey: BasicTreeNodeData['key']) {
-        const { disableStrictly, checkRelation } = this.getProps();
+        const { disableStrictly, checkRelation, keyMaps } = this.getProps();
         const { keyEntities, disabledKeys, realCheckedKeys } = this.getStates();
-        const item = (keyEntities[eventKey] && keyEntities[eventKey].data.key === eventKey) ? keyEntities[eventKey].data : this.getDataForKeyNotInKeyEntities(eventKey);
-        if (item.disabled || (disableStrictly && disabledKeys.has(eventKey))) {
+        const item = (keyEntities[eventKey] && keyEntities[eventKey].key === eventKey) ? keyEntities[eventKey].data : this.getDataForKeyNotInKeyEntities(eventKey);
+        const disabledName = get(keyMaps, 'disabled', 'disabled');
+        if (item[disabledName] || (disableStrictly && disabledKeys.has(eventKey))) {
             return;
         }
         if (checkRelation === 'unRelated') {
@@ -583,11 +589,12 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
 
     clearInput() {
         const { flattenNodes, expandedKeys, selectedKeys, keyEntities, treeData } = this.getStates();
+        const { keyMaps } = this.getProps();
         const newExpandedKeys: Set<string> = new Set(expandedKeys);
         const isExpandControlled = this._isExpandControlled();
         const expandedOptsKeys = findAncestorKeys(selectedKeys, keyEntities);
         expandedOptsKeys.forEach(item => newExpandedKeys.add(item));
-        const newFlattenNodes = flattenTreeData(treeData, newExpandedKeys);
+        const newFlattenNodes = flattenTreeData(treeData, newExpandedKeys, keyMaps);
 
         this._adapter.updateState({
             expandedKeys: isExpandControlled ? expandedKeys : newExpandedKeys,
@@ -604,7 +611,8 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
         // Input is used as controlled component
         this._adapter.updateInputValue(sugInput);
         const { flattenNodes, expandedKeys, selectedKeys, keyEntities, treeData } = this.getStates();
-        const { showFilteredOnly, filterTreeNode, treeNodeFilterProp } = this.getProps();
+        const { showFilteredOnly, filterTreeNode, treeNodeFilterProp, keyMaps } = this.getProps();
+        const realFilterProp = treeNodeFilterProp !== 'label' ? treeNodeFilterProp : get(keyMaps, 'label', 'label');
         const newExpandedKeys: Set<string> = new Set(expandedKeys);
         let filteredOptsKeys: string[] = [];
         let expandedOptsKeys = [];
@@ -613,18 +621,18 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
         if (!sugInput) {
             expandedOptsKeys = findAncestorKeys(selectedKeys, keyEntities);
             expandedOptsKeys.forEach(item => newExpandedKeys.add(item));
-            newFlattenNodes = flattenTreeData(treeData, newExpandedKeys);
+            newFlattenNodes = flattenTreeData(treeData, newExpandedKeys, keyMaps);
         } else {
             filteredOptsKeys = Object.values(keyEntities)
                 .filter((item: BasicKeyEntity) => {
                     const { data } = item;
-                    return filter(sugInput, data, filterTreeNode, treeNodeFilterProp);
+                    return filter(sugInput, data, filterTreeNode, realFilterProp);
                 })
                 .map((item: BasicKeyEntity) => item.key);
             expandedOptsKeys = findAncestorKeys(filteredOptsKeys, keyEntities, false);
             const shownChildKeys = findDescendantKeys(filteredOptsKeys, keyEntities, true);
             filteredShownKeys = new Set([...shownChildKeys, ...expandedOptsKeys]);
-            newFlattenNodes = flattenTreeData(treeData, new Set(expandedOptsKeys), showFilteredOnly && filteredShownKeys);
+            newFlattenNodes = flattenTreeData(treeData, new Set(expandedOptsKeys), keyMaps, showFilteredOnly && filteredShownKeys);
         }
         const newFilteredExpandedKeys = new Set(expandedOptsKeys);
         this._adapter.notifySearch(sugInput, Array.from(newFilteredExpandedKeys));
@@ -733,7 +741,7 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
         const nonDisabled = descendantKeys.filter(key => !disabledKeys.has(key));
         const newCheckedKeys = targetStatus ?
             [...nonDisabled, ...checkedKeys] :
-            difference(normalizeKeyList([...checkedKeys], keyEntities, true), nonDisabled);
+            difference(normalizeKeyList([...checkedKeys], keyEntities, true, true), nonDisabled);
         return calcCheckedKeys(newCheckedKeys, keyEntities);
     }
 
@@ -752,7 +760,7 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
         return !allChecked;
     }
     handleNodeExpandInSearch(e: any, treeNode: BasicTreeNodeProps) {
-        const { treeData, filteredShownKeys, keyEntities } = this.getStates();
+        const { treeData, filteredShownKeys, keyEntities, keyMaps } = this.getStates();
         const showFilteredOnly = this._showFilteredOnly();
         // clone otherwise will be modified unexpectedly
         const { filteredExpandedKeys } = this.getCopyFromState('filteredExpandedKeys');
@@ -772,7 +780,7 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
 
         if (!this._isExpandControlled()) {
             // debugger;
-            const flattenNodes = flattenTreeData(treeData, filteredExpandedKeys, showFilteredOnly && filteredShownKeys);
+            const flattenNodes = flattenTreeData(treeData, filteredExpandedKeys, keyMaps, showFilteredOnly && filteredShownKeys);
             const motionKeys = this._isAnimated() ? getMotionKeys(eventKey, filteredExpandedKeys, keyEntities) : [];
             const newState = {
                 filteredExpandedKeys,
@@ -792,7 +800,7 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
 
     handleNodeExpand(e: any, treeNode: BasicTreeNodeProps) {
         // debugger;
-        const { loadData } = this.getProps();
+        const { loadData, keyMaps } = this.getProps();
         const { inputValue, keyEntities } = this.getStates();
         const isSearching = Boolean(inputValue);
         if (!loadData && (!treeNode.children || !treeNode.children.length)) {
@@ -820,7 +828,7 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
 
         if (!isExpandControlled) {
             // debugger;
-            const flattenNodes = flattenTreeData(treeData, expandedKeys);
+            const flattenNodes = flattenTreeData(treeData, expandedKeys, keyMaps);
             const motionKeys = this._isAnimated() ? getMotionKeys(eventKey, expandedKeys, keyEntities) : [];
             const newState = {
                 expandedKeys,
@@ -841,17 +849,18 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
      * The selected items that need to be displayed in the search box when obtaining a single selection
      */
     getRenderTextInSingle() {
-        const { renderSelectedItem: propRenderSelectedItem, treeNodeLabelProp } = this.getProps();
+        const { renderSelectedItem: propRenderSelectedItem, treeNodeLabelProp, keyMaps } = this.getProps();
         const { selectedKeys, keyEntities } = this.getStates();
+        const realLabelName = get(keyMaps, 'label', treeNodeLabelProp);
         const renderSelectedItem = isFunction(propRenderSelectedItem) ?
             propRenderSelectedItem :
-            (item: BasicTreeNodeData) => get(item, treeNodeLabelProp, null);
+            (item: BasicTreeNodeData) => get(item, realLabelName, null);
         let item;
         if (selectedKeys.length) {
             const key = selectedKeys[0];
-            item = (keyEntities[key] && keyEntities[key].data.key === key) ? keyEntities[key].data : this.getDataForKeyNotInKeyEntities(key);
+            item = (keyEntities[key] && keyEntities[key].key === key) ? keyEntities[key].data : this.getDataForKeyNotInKeyEntities(key);
         }
-        const renderText = item && treeNodeLabelProp in item ? renderSelectedItem(item) : null;
+        const renderText = item ? renderSelectedItem(item) : null;
         return renderText;
     }
 

+ 123 - 0
packages/semi-ui/tree/__test__/tree.test.js

@@ -135,6 +135,76 @@ const treeData3 = [
     }
 ];
 
+const specialTreeData = [
+    {
+      label1: '亚洲',
+      value1: 'Yazhou',
+      key1: 'yazhou',
+      children1: [
+        {
+          label1: '中国',
+          value1: 'Zhongguo',
+          key1: 'zhongguo',
+          disabled1: true,
+          children1: [
+            {
+              label1: '北京',
+              value1: 'Beijing',
+              key1: 'beijing',
+            },
+            {
+              label1: '上海',
+              value1: 'Shanghai',
+              key1: 'shanghai',
+            },
+          ],
+        },
+        {
+          label1: '日本',
+          value1: 'Riben',
+          key1: 'riben',
+          children1: [
+            {
+              label1: '东京',
+              value1: 'Dongjing',
+              key1: 'dongjing',
+            },
+            {
+              label1: '大阪',
+              value1: 'Daban',
+              key1: 'daban',
+            },
+          ],
+        },
+      ],
+    },
+    {
+      label1: '北美洲',
+      value1: 'Beimeizhou',
+      key1: 'beimeizhou',
+      children1: [
+        {
+          label1: '美国',
+          value1: 'Meiguo',
+          key1: 'meiguo',
+        },
+        {
+          label1: '加拿大',
+          value1: 'Jianada',
+          key1: 'jianada',
+        },
+      ],
+    },
+];
+
+const defaultKeyMaps = {
+    value: 'value1',
+    key: 'key1',
+    label: 'label1',
+    children: 'children1',
+    disabled: 'disabled1'
+}
+
 const dragNodeData = {"label":"亚洲","value":"Yazhou","key":"yazhou","children":[{"label":"中国","value":"Zhongguo","key":"zhongguo","children":[{"label":"北京","value":"Beijing","key":"beijing"},{"label":"上海","value":"Shanghai","key":"shanghai"}]},{"label":"日本","value":"Riben","key":"riben","children":[{"label":"东京","value":"Dongjing","key":"dongjing"},{"label":"大阪","value":"Daban","key":"daban"}]}],"expanded":false,"pos":"0-0"}
 const dropNodeData = {"label":"北美洲","value":"Beimeizhou","key":"beimeizhou","children":[{"label":"美国","value":"Meiguo","key":"meiguo"},{"label":"加拿大","value":"Jianada","key":"jianada"}],"expanded":false,"pos":"0-1"}
 
@@ -971,4 +1041,57 @@ describe('Tree', () => {
         expect(spyOnDragEnd.calledOnce).toBe(true);
         expect(spyOnDragEnd.calledWithMatch({node: dragNodeData})).toEqual(true);
     });
+
+    it('keyMaps', () => {
+        let spyOnChange = sinon.spy(() => { });
+        let tree = getTree({
+            treeData: specialTreeData,
+            defaultValue: 'Beijing',
+            onChange: spyOnChange,
+            defaultExpandAll: true,
+            motion: false,
+            keyMaps: defaultKeyMaps
+        });
+        let disabledNode = tree.find(`.${BASE_CLASS_PREFIX}-tree-option-disabled`).at(0);
+        expect(disabledNode.find(`.${BASE_CLASS_PREFIX}-tree-option-label-text`).instance().textContent).toEqual('中国');
+        let selectedNode = tree.find(`.${BASE_CLASS_PREFIX}-tree-option-selected`);
+        expect(selectedNode.instance().textContent).toEqual('北京');
+        let nodeShanghai = tree.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-3`).at(1);
+        // select beijing
+        nodeShanghai.simulate('click');
+        // onSelect & onChange
+        expect(spyOnChange.calledOnce).toBe(true);
+        expect(spyOnChange.calledWithMatch("Shanghai")).toEqual(true);
+    });
+
+    it('keyMaps + onChangeWithObject', () => {
+        let spyOnChange = sinon.spy(() => { });
+        let tree = getTree({
+            treeData: specialTreeData,
+            defaultValue: {
+                label1: '北京',
+                value1: 'Beijing',
+                key1: 'beijing',
+            },
+            onChangeWithObject: true,
+            onChange: spyOnChange,
+            defaultExpandAll: true,
+            keyMaps: defaultKeyMaps
+        });
+        let disabledNode = tree.find(`.${BASE_CLASS_PREFIX}-tree-option-disabled`).at(0);
+        expect(disabledNode.find(`.${BASE_CLASS_PREFIX}-tree-option-label-text`).instance().textContent).toEqual('中国');
+        let selectedNode = tree.find(`.${BASE_CLASS_PREFIX}-tree-option-selected`);
+        expect(selectedNode.instance().textContent).toEqual('北京');
+        let nodeShanghai = tree.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-3`).at(1);
+        // select beijing
+        nodeShanghai.simulate('click');
+        // onSelect & onChange
+        expect(spyOnChange.calledOnce).toBe(true);
+        expect(spyOnChange.calledWithMatch({
+            label1: '上海',
+            value1: 'Shanghai',
+            key1: 'shanghai',
+        })).toEqual(true);
+    });
+
 })

+ 123 - 0
packages/semi-ui/tree/__test__/treeMultiple.test.js

@@ -124,6 +124,75 @@ const treeDataWithDisabled = [
     }
 ];
 
+const specialTreeData = [
+    {
+      label1: '亚洲',
+      value1: 'Yazhou',
+      key1: 'yazhou',
+      children1: [
+        {
+          label1: '中国',
+          value1: 'Zhongguo',
+          key1: 'zhongguo',
+          disabled1: true,
+          children1: [
+            {
+              label1: '北京',
+              value1: 'Beijing',
+              key1: 'beijing',
+            },
+            {
+              label1: '上海',
+              value1: 'Shanghai',
+              key1: 'shanghai',
+            },
+          ],
+        },
+        {
+          label1: '日本',
+          value1: 'Riben',
+          key1: 'riben',
+          children1: [
+            {
+              label1: '东京',
+              value1: 'Dongjing',
+              key1: 'dongjing',
+            },
+            {
+              label1: '大阪',
+              value1: 'Daban',
+              key1: 'daban',
+            },
+          ],
+        },
+      ],
+    },
+    {
+      label1: '北美洲',
+      value1: 'Beimeizhou',
+      key1: 'beimeizhou',
+      children1: [
+        {
+          label1: '美国',
+          value1: 'Meiguo',
+          key1: 'meiguo',
+        },
+        {
+          label1: '加拿大',
+          value1: 'Jianada',
+          key1: 'jianada',
+        },
+      ],
+    },
+];
+
+const defaultKeyMaps = {
+    value: 'value1',
+    key: 'key1',
+    label: 'label1',
+    children: 'children1',
+    disabled: 'disabled1'
+}
 
 function getTree(props, haveDisabled = false) {
     if (haveDisabled) {
@@ -684,4 +753,58 @@ describe('Tree', () => {
         expect(spyOnChange.notCalled).toBe(true);
         expect(tree.find(`.${BASE_CLASS_PREFIX}-tree-option-selected`).exists()).toEqual(false);
     });
+
+    it('keyMaps', () => {
+        let spyOnChange = sinon.spy(() => { });
+        let tree = getTree({
+            treeData: specialTreeData,
+            defaultValue: 'Beijing',
+            onChange: spyOnChange,
+            defaultExpandAll: true,
+            motion: false,
+            keyMaps: defaultKeyMaps
+        });
+        let disabledNode = tree.find(`.${BASE_CLASS_PREFIX}-tree-option-disabled`).at(0);
+        expect(disabledNode.find(`.${BASE_CLASS_PREFIX}-tree-option-label-text`).instance().textContent).toEqual('中国');
+        let selectedNode = tree.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-3`).at(0);
+        expect(selectedNode.find(`.${BASE_CLASS_PREFIX}-checkbox-inner-checked`).exists()).toEqual(true);
+        expect(selectedNode.find(`.${BASE_CLASS_PREFIX}-tree-option-label-text`).instance().textContent).toEqual('北京');
+        let nodeShanghai = tree.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-3`).at(1);
+        // select beijing
+        nodeShanghai.simulate('click');
+        // onSelect & onChange
+        expect(spyOnChange.calledOnce).toBe(true);
+        expect(spyOnChange.calledWithMatch(['Zhongguo'])).toEqual(true);
+        const nodeZhongguo = tree.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-2`).at(0);
+        expect(nodeZhongguo.find(`.${BASE_CLASS_PREFIX}-checkbox-inner-checked`).exists()).toEqual(true);
+    });
+
+    it('keyMaps + onChangeWithObject', () => {
+        let spyOnChange = sinon.spy(() => { });
+        let tree = getTree({
+            treeData: specialTreeData,
+            defaultValue: {
+                label1: '北京',
+                value1: 'Beijing',
+                key1: 'beijing',
+            },
+            onChangeWithObject: true,
+            onChange: spyOnChange,
+            defaultExpandAll: true,
+            keyMaps: defaultKeyMaps
+        });
+        let disabledNode = tree.find(`.${BASE_CLASS_PREFIX}-tree-option-disabled`).at(0);
+        expect(disabledNode.find(`.${BASE_CLASS_PREFIX}-tree-option-label-text`).instance().textContent).toEqual('中国');
+        let selectedNode = tree.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-3`).at(0);
+        expect(selectedNode.find(`.${BASE_CLASS_PREFIX}-checkbox-inner-checked`).exists()).toEqual(true);
+        expect(selectedNode.find(`.${BASE_CLASS_PREFIX}-tree-option-label-text`).instance().textContent).toEqual('北京');
+        let nodeShanghai = tree.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-3`).at(1);
+        // select beijing
+        nodeShanghai.simulate('click');
+        // onSelect & onChange
+        expect(spyOnChange.calledOnce).toBe(true);
+        expect(spyOnChange.calledWithMatch([(specialTreeData[0].children1)[0]])).toEqual(true);
+        const nodeZhongguo = tree.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-2`).at(0);
+        expect(nodeZhongguo.find(`.${BASE_CLASS_PREFIX}-checkbox-inner-checked`).exists()).toEqual(true);
+    });
 })

+ 171 - 1
packages/semi-ui/tree/_story/tree.stories.jsx

@@ -1,4 +1,4 @@
-import React, { useRef, useState, useCallback, useMemo } from 'react';
+import React, { useRef, useState, useCallback, useMemo, useEffect } from 'react';
 import { cloneDeep, difference, isEqual } from 'lodash';
 import { IconEdit, IconMapPin, IconMore } from '@douyinfe/semi-icons';
 import Tree from '../index';
@@ -77,6 +77,68 @@ const treeData1 = [
   },
 ];
 
+const specialTreeData = [
+  {
+    label1: '亚洲',
+    value1: 'Yazhou',
+    key1: 'yazhou',
+    children1: [
+      {
+        label1: '中国',
+        value1: 'Zhongguo',
+        key1: 'zhongguo',
+        disabled1: true,
+        children1: [
+          {
+            label1: '北京',
+            value1: 'Beijing',
+            key1: 'beijing',
+          },
+          {
+            label1: '上海',
+            value1: 'Shanghai',
+            key1: 'shanghai',
+          },
+        ],
+      },
+      {
+        label1: '日本',
+        value1: 'Riben',
+        key1: 'riben',
+        children1: [
+          {
+            label1: '东京',
+            value1: 'Dongjing',
+            key1: 'dongjing',
+          },
+          {
+            label1: '大阪',
+            value1: 'Daban',
+            key1: 'daban',
+          },
+        ],
+      },
+    ],
+  },
+  {
+    label1: '北美洲',
+    value1: 'Beimeizhou',
+    key1: 'beimeizhou',
+    children1: [
+      {
+        label1: '美国',
+        value1: 'Meiguo',
+        key1: 'meiguo',
+      },
+      {
+        label1: '加拿大',
+        value1: 'Jianada',
+        key1: 'jianada',
+      },
+    ],
+  },
+];
+
 const treeDataWithoutValue = [
   {
     label: '亚洲',
@@ -2820,3 +2882,111 @@ export const ChangeTreeData = () => {
     />
   </>
 }
+
+export const KeyMaps = () => {
+  const [withObject, setWithObject] = useState(false);
+  const [value1, setValue1] = useState(undefined);
+  const [value2, setValue2] = useState([]);
+  const [expandKeys, setExpandedKeys] = useState(undefined);
+
+  const onSingleChange = useCallback((value) => {
+    console.log('onSingleChange', value);
+    setValue1(value);
+  }, []);
+
+  const onMultipleChange = useCallback((value) => {
+    console.log('onMultipleChange', value);
+    setValue2(value);
+  }, []);
+
+  const normalChange = useCallback((value) => {
+    console.log('onChange', value);
+  }, []);
+
+  const normalExpanded = useCallback((expandedKeys, {expanded, node}) => {
+    console.log('onExpanded', expandedKeys, expanded, cloneDeep(node));
+  }, []);
+  
+  const normalSearch = useCallback((input, filteredExpandedKeys) => {
+    console.log('onSearch', input, filteredExpandedKeys);
+  }, []);
+
+  const keyMaps = useMemo(() => {
+    return {
+      value: 'value1',
+      key: 'key1',
+      label: 'label1',
+      children: 'children1',
+      disabled: 'disabled1'
+    };
+  }, []);
+
+  const switchChange = useCallback((checked) => {
+    setWithObject(checked);
+    setValue1(undefined);
+    setValue2(undefined);
+  }, []);
+
+  const regularTreeProps = useMemo(() => ({
+    keyMaps: keyMaps,
+    treeData: specialTreeData,
+    style: { width: 300 },
+    onChange: normalChange,
+    onExpand: normalExpanded,
+    onChangeWithObject: withObject,
+  }), [withObject]);
+
+  const defaultSelectedObj = {
+    label1: '北京',
+    value1: 'Beijing',
+    key1: 'beijing',
+  };
+
+  return (
+    <>
+      <span>onChangeWithObject</span><Switch checked={withObject} onChange={switchChange} />
+      <div key={String(withObject)} style={{ marginTop: 10 }}>
+        <span>Single select</span>
+        <Tree
+          {...regularTreeProps}
+          defaultValue={withObject ? [defaultSelectedObj] : 'Beijing'}
+        />
+        <span>Single select, onSearch, filterTreeNode</span>
+        <Tree
+          {...regularTreeProps}
+          filterTreeNode
+          onSearch={normalSearch}
+        />
+        <span>Single select, controlled</span>
+        <Tree
+          {...regularTreeProps}
+          value={value1}
+          onChange={onSingleChange}
+        />
+        <span>Multiple select</span>
+        <Tree
+          {...regularTreeProps}
+          multiple
+          defaultValue={withObject ? [defaultSelectedObj] : ['Beijing']}
+        />
+        <span>Multiple select, controlled, disableStrictly</span>
+        <Tree
+          {...regularTreeProps}
+          multiple
+          value={value2}
+          disableStrictly
+          onChange={onMultipleChange}
+        />
+        <span>Multiple, 展开受控</span>
+        <Tree
+          {...regularTreeProps}
+          multiple
+          expandedKeys={expandKeys}
+          onExpand={(expandedKeys, {expanded, node}) => {
+            setExpandedKeys(expandedKeys);
+          }}
+        />
+      </div>
+    </> 
+  );
+}

+ 23 - 15
packages/semi-ui/tree/index.tsx

@@ -19,7 +19,7 @@ import {
 } from '@douyinfe/semi-foundation/tree/treeUtil';
 import { cssClasses, strings } from '@douyinfe/semi-foundation/tree/constants';
 import BaseComponent from '../_base/baseComponent';
-import { isEmpty, isEqual, get, isFunction } from 'lodash';
+import { isEmpty, isEqual, get, isFunction, pick, isUndefined } from 'lodash';
 import { cloneDeep } from './treeUtil';
 import Input from '../input/index';
 import { FixedSizeList as VirtualList } from 'react-window';
@@ -82,12 +82,13 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
         style: PropTypes.object,
         treeData: PropTypes.arrayOf(
             PropTypes.shape({
-                key: PropTypes.string.isRequired,
+                key: PropTypes.string,
                 value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
                 label: PropTypes.any,
                 isLeaf: PropTypes.bool,
             })
         ),
+        keyMaps: PropTypes.object,
         treeDataSimpleJson: PropTypes.object,
         treeNodeFilterProp: PropTypes.string,
         value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.object]),
@@ -190,6 +191,7 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
 
     static getDerivedStateFromProps(props: TreeProps, prevState: TreeState) {
         const { prevProps } = prevState;
+        const { keyMaps } = props;
         let treeData;
         let keyEntities = prevState.keyEntities || {};
         let valueEntities = prevState.cachedKeyValuePairs || {};
@@ -220,7 +222,7 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
         if (needUpdateTreeData || (props.draggable && needUpdateData())) {
             treeData = props.treeData;
             newState.treeData = treeData;
-            const entitiesMap = convertDataToEntities(treeData);
+            const entitiesMap = convertDataToEntities(treeData, keyMaps);
             newState.keyEntities = {
                 ...entitiesMap.keyEntities,
             };
@@ -231,7 +233,7 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
             // Convert treeDataSimpleJson to treeData
             treeData = convertJsonToData(props.treeDataSimpleJson);
             newState.treeData = treeData;
-            const entitiesMap = convertDataToEntities(treeData);
+            const entitiesMap = convertDataToEntities(treeData, keyMaps);
             newState.keyEntities = {
                 ...entitiesMap.keyEntities,
             };
@@ -314,7 +316,8 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
             if (treeData || newState.expandedKeys) {
                 const flattenNodes = flattenTreeData(
                     treeData || prevState.treeData,
-                    newState.expandedKeys || prevState.expandedKeys
+                    newState.expandedKeys || prevState.expandedKeys,
+                    keyMaps
                 );
                 newState.flattenNodes = flattenNodes;
             }
@@ -331,6 +334,7 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
                     showFilteredOnly: props.showFilteredOnly,
                     keyEntities: newState.keyEntities,
                     prevExpandedKeys: [...prevState.filteredExpandedKeys],
+                    keyMaps: keyMaps
                 });
                 newState.flattenNodes = filteredState.flattenNodes;
                 newState.motionKeys = new Set([]);
@@ -374,6 +378,7 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
                 newState.flattenNodes = flattenTreeData(
                     treeData || prevState.treeData,
                     newState.filteredExpandedKeys || prevState.filteredExpandedKeys,
+                    keyMaps,
                     props.showFilteredOnly && prevState.filteredShownKeys
                 );
             }
@@ -387,13 +392,13 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
             if (needUpdate('value')) {
                 newState.selectedKeys = findKeysForValues(
                     // In both cases whether withObject is turned on, the value is standardized to string
-                    normalizeValue(props.value, withObject),
+                    normalizeValue(props.value, withObject, keyMaps),
                     valueEntities,
                     isMultiple
                 );
             } else if (!prevProps && props.defaultValue) {
                 newState.selectedKeys = findKeysForValues(
-                    normalizeValue(props.defaultValue, withObject),
+                    normalizeValue(props.defaultValue, withObject, keyMaps),
                     valueEntities,
                     isMultiple
                 );
@@ -401,7 +406,7 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
                 // If `treeData` changed, we also need check it
                 if (props.value) {
                     newState.selectedKeys = findKeysForValues(
-                        normalizeValue(props.value, withObject) || '',
+                        normalizeValue(props.value, withObject, keyMaps) || '',
                         valueEntities,
                         isMultiple
                     );
@@ -412,13 +417,13 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
             // Get the selected node during multiple selection
             if (needUpdate('value')) {
                 checkedKeyValues = findKeysForValues(
-                    normalizeValue(props.value, withObject),
+                    normalizeValue(props.value, withObject, keyMaps),
                     valueEntities,
                     isMultiple
                 );
             } else if (!prevProps && props.defaultValue) {
                 checkedKeyValues = findKeysForValues(
-                    normalizeValue(props.defaultValue, withObject),
+                    normalizeValue(props.defaultValue, withObject, keyMaps),
                     valueEntities,
                     isMultiple
                 );
@@ -426,7 +431,7 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
                 // If `treeData` changed, we also need check it
                 if (props.value) {
                     checkedKeyValues = findKeysForValues(
-                        normalizeValue(props.value, withObject) || [],
+                        normalizeValue(props.value, withObject, keyMaps) || [],
                         valueEntities,
                         isMultiple
                     );
@@ -454,7 +459,7 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
 
         // update disableStrictly
         if (treeData && props.disableStrictly && props.checkRelation === 'related') {
-            newState.disabledKeys = calcDisabledKeys(keyEntities);
+            newState.disabledKeys = calcDisabledKeys(keyEntities, keyMaps);
         }
 
         return newState;
@@ -651,13 +656,16 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
     };
 
     renderTreeNode = (treeNode: FlattenNode, ind?: number, style?: React.CSSProperties) => {
-        const { data } = treeNode;
-        const { key } = data;
+        const { data, key } = treeNode;
         const treeNodeProps = this.foundation.getTreeNodeProps(key);
         if (!treeNodeProps) {
             return null;
         }
-        return <TreeNode {...treeNodeProps} {...data} key={key} data={data} style={isEmpty(style) ? {} : style} />;
+        const { keyMaps } = this.props;
+        const props: any = pick(treeNode, ['key', 'label', 'disabled', 'isLeaf', 'icon']);
+        const children = data[get(keyMaps, 'children', 'children')];
+        !isUndefined(children) && (props.children = children);
+        return <TreeNode {...treeNodeProps} {...data} {...props} data={data} style={isEmpty(style) ? {} : style} />;
     };
 
     itemKey = (index: number, data: KeyEntity) => {

+ 2 - 0
packages/semi-ui/tree/interface.ts

@@ -11,6 +11,7 @@ import {
     BasicFlattenNode,
     BasicTreeNodeData,
     BasicOnDragProps,
+    KeyMapProps,
 } from '@douyinfe/semi-foundation/tree/foundation';
 
 /* Tree */
@@ -67,6 +68,7 @@ export interface TreeProps extends BasicTreeProps {
     treeData?: TreeNodeData[];
     value?: Value;
     icon?: ReactNode;
+    keyMaps?: KeyMapProps;
     loadData?: (treeNode?: TreeNodeData) => Promise<void>;
     onChange?: (value?: Value) => void;
     onDoubleClick?: (e: MouseEvent, node: TreeNodeData) => void;

+ 1 - 3
packages/semi-ui/tree/nodeList.tsx

@@ -5,9 +5,7 @@ import { FlattenNode, NodeListProps, NodeListState, TransitionNodes } from './in
 import NodeCollapsible from './nodeCollapsible';
 
 const getTreeNodeKey = (treeNode: FlattenNode) => {
-    const { data } = treeNode;
-    const { key } = data;
-    return key;
+    return treeNode.key;
 };
 
 export default class NodeList extends PureComponent<NodeListProps, NodeListState> {

+ 124 - 0
packages/semi-ui/treeSelect/__test__/treeMultiple.test.js

@@ -169,6 +169,76 @@ const treeDataWithoutValue = [
     },
 ];
 
+const specialTreeData = [
+    {
+      label1: '亚洲',
+      value1: 'Yazhou',
+      key1: 'yazhou',
+      children1: [
+        {
+          label1: '中国',
+          value1: 'Zhongguo',
+          key1: 'zhongguo',
+          disabled1: true,
+          children1: [
+            {
+              label1: '北京',
+              value1: 'Beijing',
+              key1: 'beijing',
+            },
+            {
+              label1: '上海',
+              value1: 'Shanghai',
+              key1: 'shanghai',
+            },
+          ],
+        },
+        {
+          label1: '日本',
+          value1: 'Riben',
+          key1: 'riben',
+          children1: [
+            {
+              label1: '东京',
+              value1: 'Dongjing',
+              key1: 'dongjing',
+            },
+            {
+              label1: '大阪',
+              value1: 'Daban',
+              key1: 'daban',
+            },
+          ],
+        },
+      ],
+    },
+    {
+      label1: '北美洲',
+      value1: 'Beimeizhou',
+      key1: 'beimeizhou',
+      children1: [
+        {
+          label1: '美国',
+          value1: 'Meiguo',
+          key1: 'meiguo',
+        },
+        {
+          label1: '加拿大',
+          value1: 'Jianada',
+          key1: 'jianada',
+        },
+      ],
+    },
+];
+
+const defaultKeyMaps = {
+    value: 'value1',
+    key: 'key1',
+    label: 'label1',
+    children: 'children1',
+    disabled: 'disabled1'
+};
+
 let commonProps = {
     motion: false,
     motionExpand: false,
@@ -1005,4 +1075,58 @@ describe('TreeSelect', () => {
             { label: '北京', key: 'beijing' }
         ])).toEqual(true);
     });
+
+    it('keyMaps', () => {
+        let spyOnChange = sinon.spy(() => { });
+        let treeSelect = getTreeSelect({
+            treeData: specialTreeData,
+            defaultValue: 'Beijing',
+            onChange: spyOnChange,
+            defaultExpandAll: true,
+            motion: false,
+            keyMaps: defaultKeyMaps
+        });
+        let disabledNode = treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-option-disabled`).at(0);
+        expect(disabledNode.find(`.${BASE_CLASS_PREFIX}-tree-option-label-text`).instance().textContent).toEqual('中国');
+        let selectedNode = treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-3`).at(0);
+        expect(selectedNode.find(`.${BASE_CLASS_PREFIX}-checkbox-inner-checked`).exists()).toEqual(true);
+        expect(selectedNode.find(`.${BASE_CLASS_PREFIX}-tree-option-label-text`).instance().textContent).toEqual('北京');
+        let nodeShanghai = treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-3`).at(1);
+        nodeShanghai.simulate('click');
+        // onSelect & onChange
+        expect(spyOnChange.calledOnce).toBe(true);
+        expect(spyOnChange.calledWithMatch(["Zhongguo"])).toEqual(true);
+        const nodeZhongguo = treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-2`).at(0);
+        expect(nodeZhongguo.find(`.${BASE_CLASS_PREFIX}-checkbox-inner-checked`).exists()).toEqual(true);
+        let selectContentNode = treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-select-selection`).at(0);
+        expect(selectContentNode.find(`.${BASE_CLASS_PREFIX}-tag-content`).instance().textContent).toEqual('中国');
+    });
+
+    it('keyMaps + onChangeWithObject', () => {
+        let spyOnChange = sinon.spy(() => { });
+        let treeSelect = getTreeSelect({
+            treeData: specialTreeData,
+            defaultValue: {
+                label1: '北京',
+                value1: 'Beijing',
+                key1: 'beijing',
+            },
+            onChangeWithObject: true,
+            onChange: spyOnChange,
+            defaultExpandAll: true,
+            keyMaps: defaultKeyMaps
+        });
+        let disabledNode = treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-option-disabled`).at(0);
+        expect(disabledNode.find(`.${BASE_CLASS_PREFIX}-tree-option-label-text`).instance().textContent).toEqual('中国');
+        let selectedNode = treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-3`).at(0);
+        expect(selectedNode.find(`.${BASE_CLASS_PREFIX}-checkbox-inner-checked`).exists()).toEqual(true);
+        expect(selectedNode.find(`.${BASE_CLASS_PREFIX}-tree-option-label-text`).instance().textContent).toEqual('北京');
+        let nodeShanghai = treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-3`).at(1);
+        nodeShanghai.simulate('click');
+        // onSelect & onChange
+        expect(spyOnChange.calledOnce).toBe(true);
+        expect(spyOnChange.calledWithMatch([(specialTreeData[0].children1)[0]])).toEqual(true);
+        let selectContentNode = treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-select-selection`).at(0);
+        expect(selectContentNode.find(`.${BASE_CLASS_PREFIX}-tag-content`).instance().textContent).toEqual('中国');
+    });
 })

+ 126 - 0
packages/semi-ui/treeSelect/__test__/treeSelect.test.js

@@ -117,6 +117,76 @@ const treeData3 = [
     }
 ];
 
+const specialTreeData = [
+    {
+      label1: '亚洲',
+      value1: 'Yazhou',
+      key1: 'yazhou',
+      children1: [
+        {
+          label1: '中国',
+          value1: 'Zhongguo',
+          key1: 'zhongguo',
+          disabled1: true,
+          children1: [
+            {
+              label1: '北京',
+              value1: 'Beijing',
+              key1: 'beijing',
+            },
+            {
+              label1: '上海',
+              value1: 'Shanghai',
+              key1: 'shanghai',
+            },
+          ],
+        },
+        {
+          label1: '日本',
+          value1: 'Riben',
+          key1: 'riben',
+          children1: [
+            {
+              label1: '东京',
+              value1: 'Dongjing',
+              key1: 'dongjing',
+            },
+            {
+              label1: '大阪',
+              value1: 'Daban',
+              key1: 'daban',
+            },
+          ],
+        },
+      ],
+    },
+    {
+      label1: '北美洲',
+      value1: 'Beimeizhou',
+      key1: 'beimeizhou',
+      children1: [
+        {
+          label1: '美国',
+          value1: 'Meiguo',
+          key1: 'meiguo',
+        },
+        {
+          label1: '加拿大',
+          value1: 'Jianada',
+          key1: 'jianada',
+        },
+      ],
+    },
+];
+
+const defaultKeyMaps = {
+    value: 'value1',
+    key: 'key1',
+    label: 'label1',
+    children: 'children1',
+    disabled: 'disabled1'
+}
+
 const treeChildrenWithoutValue = [
     {
         label: '北京',
@@ -1076,4 +1146,60 @@ describe('TreeSelect', () => {
         expect(spyOnChange.calledWithMatch({ label: '北京', key: 'beijing' })).toEqual(true);
         treeSelect.unmount(); 
     })
+
+    it('keyMaps', () => {
+        let spyOnChange = sinon.spy(() => { });
+        let treeSelect = getTreeSelect({
+            treeData: specialTreeData,
+            defaultValue: 'Beijing',
+            onChange: spyOnChange,
+            defaultExpandAll: true,
+            motion: false,
+            keyMaps: defaultKeyMaps
+        });
+        let disabledNode = treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-option-disabled`).at(0);
+        expect(disabledNode.find(`.${BASE_CLASS_PREFIX}-tree-option-label-text`).instance().textContent).toEqual('中国');
+        let selectedNode = treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-option-selected`);
+        expect(selectedNode.instance().textContent).toEqual('北京');
+        let nodeShanghai = treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-3`).at(1);
+        // select beijing
+        nodeShanghai.simulate('click');
+        // onSelect & onChange
+        expect(spyOnChange.calledOnce).toBe(true);
+        expect(spyOnChange.calledWithMatch("Shanghai")).toEqual(true);
+        let selectContentNode = treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-select-selection-content`)
+        expect(selectContentNode.instance().textContent).toEqual('上海');
+    });
+
+    it('keyMaps + onChangeWithObject', () => {
+        let spyOnChange = sinon.spy(() => { });
+        let treeSelect = getTreeSelect({
+            treeData: specialTreeData,
+            defaultValue: {
+                label1: '北京',
+                value1: 'Beijing',
+                key1: 'beijing',
+            },
+            onChangeWithObject: true,
+            onChange: spyOnChange,
+            defaultExpandAll: true,
+            keyMaps: defaultKeyMaps
+        });
+        let disabledNode = treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-option-disabled`).at(0);
+        expect(disabledNode.find(`.${BASE_CLASS_PREFIX}-tree-option-label-text`).instance().textContent).toEqual('中国');
+        let selectedNode = treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-option-selected`);
+        expect(selectedNode.instance().textContent).toEqual('北京');
+        let nodeShanghai = treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-3`).at(1);
+        // select beijing
+        nodeShanghai.simulate('click');
+        // onSelect & onChange
+        expect(spyOnChange.calledOnce).toBe(true);
+        expect(spyOnChange.calledWithMatch({
+            label1: '上海',
+            value1: 'Shanghai',
+            key1: 'shanghai',
+        })).toEqual(true);
+        let selectContentNode = treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-select-selection-content`)
+        expect(selectContentNode.instance().textContent).toEqual('上海');
+    });
 })

+ 182 - 3
packages/semi-ui/treeSelect/_story/treeSelect.stories.jsx

@@ -1,7 +1,7 @@
-import React, { useState, useMemo, useRef, useCallback } from 'react';
-import { Icon, Input, Button, Form, Popover, Tag, Typography, CheckboxGroup, TagInput } from '../../index';
+import React, { useState, useMemo, useRef, useCallback, useEffect } from 'react';
+import { Icon, Input, Button, Form, Popover, Tag, Typography, CheckboxGroup, TagInput, Switch } from '../../index';
 import TreeSelect from '../index';
-import { flattenDeep } from 'lodash';
+import { flattenDeep, cloneDeep } from 'lodash';
 import CustomTrigger from './CustomTrigger';
 import { IconCreditCard, IconChevronDown, IconClose } from '@douyinfe/semi-icons';
 import { setFocusToPreviousMenuItem } from '@douyinfe/semi-foundation/utils/a11y';
@@ -219,6 +219,68 @@ const treeDataWithoutValue = [
   },
 ];
 
+const specialTreeData = [
+  {
+    label1: '亚洲',
+    // value1: 'Yazhou',
+    key1: 'yazhou',
+    children1: [
+      {
+        label1: '中国',
+        // value1: 'Zhongguo',
+        key1: 'zhongguo',
+        disabled1: true,
+        children1: [
+          {
+            label1: '北京',
+            // value1: 'Beijing',
+            key1: 'beijing',
+          },
+          {
+            label1: '上海',
+            // value1: 'Shanghai',
+            key1: 'shanghai',
+          },
+        ],
+      },
+      {
+        label1: '日本',
+        // value1: 'Riben',
+        key1: 'riben',
+        children1: [
+          {
+            label1: '东京',
+            // value1: 'Dongjing',
+            key1: 'dongjing',
+          },
+          {
+            label1: '大阪',
+            // value1: 'Daban',
+            key1: 'daban',
+          },
+        ],
+      },
+    ],
+  },
+  {
+    label1: '北美洲',
+    // value1: 'Beimeizhou',
+    key1: 'beimeizhou',
+    children1: [
+      {
+        label1: '美国',
+        // value1: 'Meiguo',
+        key1: 'meiguo',
+      },
+      {
+        label1: '加拿大',
+        // value1: 'Jianada',
+        key1: 'jianada',
+      },
+    ],
+  },
+];
+
 export const TreeSelectWrapper = () => (
   <div>
     <div>github issue 750 修改测试用例</div>
@@ -2474,3 +2536,120 @@ export const ChangeTreeData = () => {
     />
   </>
 }
+
+export const KeyMaps = () => {
+  const [withObject, setWithObject] = useState(false);
+  const [value1, setValue1] = useState(undefined);
+  const [value2, setValue2] = useState(undefined);
+  const [expandKeys, setExpandedKeys] = useState(["yazhou", 'zhongguo']);
+  
+  const switchChange = useCallback((checked) => {
+    setWithObject(checked);
+    setValue1(undefined);
+    setValue2(undefined);
+  }, []);
+
+  const onSingleChange = useCallback((value) => {
+    console.log('onSingleChange', value);
+    setValue1(value);
+  }, []);
+
+  const onMultipleChange = useCallback((value) => {
+    console.log('onMultipleChange', value);
+    setValue2(value);
+  }, []);
+
+  const normalChange = useCallback((value) => {
+    console.log('onChange', value);
+  }, []);
+
+  const normalExpand = useCallback((expandedKeys, {expanded, node}) => {
+    console.log('onExpanded', expandedKeys, expanded, cloneDeep(node));
+  }, []);
+
+  const keyMaps = useMemo(() => {
+    return {
+      // value: 'value1',
+      key: 'key1',
+      label: 'label1',
+      children: 'children1',
+      disabled: 'disabled1'
+    };
+  }, []);
+
+  const regularTreeProps = useMemo(() => ({
+    keyMaps: keyMaps,
+    treeData: specialTreeData,
+    style: { width: 300 },
+    dropdownStyle: { maxHeight: 400, overflow: 'auto' },
+    onChange: normalChange,
+    onExpand: normalExpand,
+    onChangeWithObject: withObject,
+  }), [withObject]);
+
+  const defaultSelectedObj = {
+    label1: '北京',
+    // value1: 'Beijing',
+    key1: 'beijing',
+  };
+
+  return (
+    <>
+      <span>onChangeWithObject</span><Switch checked={withObject} onChange={switchChange} />
+      <div key={String(withObject)} style={{ marginTop: 10 }}>
+        <div> Single select</div>
+        <TreeSelect
+          {...regularTreeProps}
+          defaultValue={withObject ? defaultSelectedObj : 'beijing'}
+        />
+        <div> Single select, onSearch, filterTreeNode, treeNodeFilterProp</div>
+        <TreeSelect
+          {...regularTreeProps}
+          filterTreeNode={(inputValue, treeNodeString, data)=> {
+            console.log("filterTreeNode", inputValue, treeNodeString, data);
+            return treeNodeString.includes(inputValue);
+          }}
+          treeNodeFilterProp={"key1"}
+          onSearch={(input, filteredExpandedKeys) => {
+            console.log('onSearch', input, filteredExpandedKeys);
+          }}
+        />
+        <div>Single select, controlled</div>
+        <TreeSelect  
+          {...regularTreeProps}
+          value={value1}
+          onChange={onSingleChange}
+        />
+        <div> Multiple select</div>
+        <TreeSelect
+          {...regularTreeProps}
+          multiple
+          defaultValue={withObject ? [defaultSelectedObj] : ['beijing']}
+        />
+        <div> Multiple select, controlled</div>
+        <TreeSelect
+          {...regularTreeProps}
+          value={value2}
+          multiple
+          onChange={onMultipleChange}
+        />
+        <div> Multiple select, disableStrictly</div>
+        <TreeSelect
+          {...regularTreeProps}
+          multiple
+          disableStrictly
+        />
+        <div> Multiple, 展开受控</div>
+        <TreeSelect
+          {...regularTreeProps}
+          multiple
+          expandedKeys={expandKeys}
+          onExpand={(expandedKeys, {expanded, node}) => {
+            console.log('onExpanded', expandedKeys, expanded, cloneDeep(node));
+            setExpandedKeys(expandedKeys);
+          }}
+        />
+      </div>
+    </> 
+  );
+}

+ 37 - 26
packages/semi-ui/treeSelect/index.tsx

@@ -2,7 +2,7 @@ import React, { Fragment } from 'react';
 import ReactDOM from 'react-dom';
 import cls from 'classnames';
 import PropTypes from 'prop-types';
-import { isEqual, isString, isEmpty, noop, get, isFunction, isUndefined, isNull } from 'lodash';
+import { isEqual, isString, isEmpty, noop, get, isFunction, isUndefined, isNull, pick } from 'lodash';
 import TreeSelectFoundation, {
     Size,
     BasicTriggerRenderProps,
@@ -93,7 +93,8 @@ export type OverrideCommonProps =
     | 'style'
     | 'treeData'
     | 'value'
-    | 'onExpand';
+    | 'onExpand'
+    | 'keyMaps';
 
 /**
 * Type definition description:
@@ -216,10 +217,11 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
         showFilteredOnly: PropTypes.bool,
         motionExpand: PropTypes.bool,
         emptyContent: PropTypes.node,
+        keyMaps: PropTypes.object,
         leafOnly: PropTypes.bool,
         treeData: PropTypes.arrayOf(
             PropTypes.shape({
-                key: PropTypes.string.isRequired,
+                key: PropTypes.string,
                 value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
                 label: PropTypes.any,
             })
@@ -354,6 +356,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
 
     static getDerivedStateFromProps(props: TreeSelectProps, prevState: TreeSelectState) {
         const { prevProps, rePosKey } = prevState;
+        const { keyMaps } = props;
         const needUpdate = (name: string) => (
             (!prevProps && name in props) ||
             (prevProps && !isEqual(prevProps[name], props[name]))
@@ -373,7 +376,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
         if (needUpdateTreeData) {
             treeData = props.treeData;
             newState.treeData = treeData;
-            const entitiesMap = convertDataToEntities(treeData);
+            const entitiesMap = convertDataToEntities(treeData, keyMaps);
             newState.keyEntities = {
                 ...entitiesMap.keyEntities,
             };
@@ -417,14 +420,14 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
             newState.expandedKeys = calcExpandedKeys(props.defaultExpandedKeys, keyEntities);
         } else if (!prevProps && props.defaultValue) {
             newState.expandedKeys = calcExpandedKeysForValues(
-                normalizeValue(props.defaultValue, withObject),
+                normalizeValue(props.defaultValue, withObject, keyMaps),
                 keyEntities,
                 props.multiple,
                 valueEntities
             );
         } else if (!prevProps && props.value) {
             newState.expandedKeys = calcExpandedKeysForValues(
-                normalizeValue(props.value, withObject),
+                normalizeValue(props.value, withObject, keyMaps),
                 keyEntities,
                 props.multiple,
                 valueEntities
@@ -434,7 +437,8 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
         if (treeData || needUpdateExpandedKeys) {
             const flattenNodes = flattenTreeData(
                 treeData || prevState.treeData,
-                newState.expandedKeys || prevState.expandedKeys
+                newState.expandedKeys || prevState.expandedKeys,
+                keyMaps,
             );
             newState.flattenNodes = flattenNodes;
         }
@@ -444,13 +448,13 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
         if (!isMultiple) {
             if (needUpdate('value')) {
                 newState.selectedKeys = findKeysForValues(
-                    normalizeValue(props.value, withObject),
+                    normalizeValue(props.value, withObject, keyMaps),
                     valueEntities,
                     isMultiple
                 );
             } else if (!prevProps && props.defaultValue) {
                 newState.selectedKeys = findKeysForValues(
-                    normalizeValue(props.defaultValue, withObject),
+                    normalizeValue(props.defaultValue, withObject, keyMaps),
                     valueEntities,
                     isMultiple
                 );
@@ -458,7 +462,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
                 // If `treeData` changed, we also need check it
                 if (props.value) {
                     newState.selectedKeys = findKeysForValues(
-                        normalizeValue(props.value, withObject) || '',
+                        normalizeValue(props.value, withObject, keyMaps) || '',
                         valueEntities,
                         isMultiple
                     );
@@ -472,13 +476,13 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
 
             if (needUpdate('value')) {
                 checkedKeyValues = findKeysForValues(
-                    normalizeValue(props.value, withObject),
+                    normalizeValue(props.value, withObject, keyMaps),
                     valueEntities,
                     isMultiple
                 );
             } else if (!prevProps && props.defaultValue) {
                 checkedKeyValues = findKeysForValues(
-                    normalizeValue(props.defaultValue, withObject),
+                    normalizeValue(props.defaultValue, withObject, keyMaps),
                     valueEntities,
                     isMultiple
                 );
@@ -486,7 +490,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
                 // If `treeData` changed, we also need check it
                 if (props.value) {
                     checkedKeyValues = findKeysForValues(
-                        normalizeValue(props.value, withObject) || [],
+                        normalizeValue(props.value, withObject, keyMaps) || [],
                         valueEntities,
                         isMultiple
                     );
@@ -519,7 +523,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
 
         // ================ disableStrictly =================
         if (treeData && props.disableStrictly && props.checkRelation === 'related') {
-            newState.disabledKeys = calcDisabledKeys(keyEntities);
+            newState.disabledKeys = calcDisabledKeys(keyEntities, keyMaps);
         }
 
         return newState;
@@ -768,13 +772,15 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
             disableStrictly,
             size,
             checkRelation,
-            renderSelectedItem: propRenderSelectedItem
+            renderSelectedItem: propRenderSelectedItem,
+            keyMaps
         } = this.props;
+        const realLabelName = get(keyMaps, 'label', treeNodeLabelProp);
         const renderSelectedItem = isFunction(propRenderSelectedItem) ?
             propRenderSelectedItem :
             (item: TreeNodeData) => ({
                 isRenderInTag: true,
-                content: get(item, treeNodeLabelProp, null)
+                content: get(item, realLabelName, null)
             });
         let renderKeys = [];
         if (checkRelation === 'related') {
@@ -784,7 +790,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
         }
         const tagList: Array<React.ReactNode> = [];
         renderKeys.forEach((key: TreeNodeData['key'], index) => {
-            const item = (keyEntities[key] && keyEntities[key].data.key === key) ? keyEntities[key].data : this.getDataForKeyNotInKeyEntities(key);
+            const item = (keyEntities[key] && keyEntities[key].key === key) ? keyEntities[key].data : this.getDataForKeyNotInKeyEntities(key);
             const onClose = (tagContent: any, e: React.MouseEvent) => {
                 if (e && typeof e.preventDefault === 'function') {
                     // make sure that tag will not hidden immediately in controlled mode
@@ -792,7 +798,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
                 }
                 this.removeTag(key);
             };
-            const { content, isRenderInTag } = (item && treeNodeLabelProp in item) ?
+            const { content, isRenderInTag } = item ?
                 (renderSelectedItem as RenderSelectedItemInMultiple)(item, { index, onClose }) :
                 ({} as any);
             if (isNull(content) || isUndefined(content)) {
@@ -1076,11 +1082,13 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
             disabled,
             disableStrictly,
             renderSelectedItem: propRenderSelectedItem,
-            treeNodeLabelProp
+            treeNodeLabelProp,
+            keyMaps
         } = this.props;
+        const realLabelName = get(keyMaps, 'label', treeNodeLabelProp);
         const keyList = normalizeKeyList([key], keyEntities, leafOnly, true);
-        const nodes = keyList.map(i => (keyEntities[key] && keyEntities[key].data.key === key) ? keyEntities[key].data : this.getDataForKeyNotInKeyEntities(key));
-        const value = getValueOrKey(nodes);
+        const nodes = keyList.map(i => (keyEntities[key] && keyEntities[key].key === key) ? keyEntities[key].data : this.getDataForKeyNotInKeyEntities(key));
+        const value = getValueOrKey(nodes, keyMaps);
         const tagCls = cls(`${prefixcls}-selection-tag`, {
             [`${prefixcls}-selection-tag-disabled`]: disabled,
         });
@@ -1104,10 +1112,10 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
         const renderSelectedItem = isFunction(propRenderSelectedItem) ? propRenderSelectedItem :
             (selectedItem: TreeNodeData) => ({
                 isRenderInTag: true,
-                content: get(selectedItem, treeNodeLabelProp, null)
+                content: get(selectedItem, realLabelName, null)
             });
         if (isFunction(renderSelectedItem)) {
-            const { content, isRenderInTag } = item && treeNodeLabelProp in item ?
+            const { content, isRenderInTag } = item ?
                 (renderSelectedItem as RenderSelectedItemInMultiple)(item, { index: idx, onClose }) :
                 ({} as any);
             if (isRenderInTag) {
@@ -1305,13 +1313,16 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
     }
 
     renderTreeNode = (treeNode: FlattenNode, ind: number, style: React.CSSProperties) => {
-        const { data } = treeNode;
-        const { key }: { key: string } = data;
+        const { data, key } = treeNode;
         const treeNodeProps = this.foundation.getTreeNodeProps(key);
         if (!treeNodeProps) {
             return null;
         }
-        return <TreeNode {...treeNodeProps} {...data} key={key} data={data} style={style} />;
+        const props: any = pick(treeNode, ['key', 'label', 'disabled', 'isLeaf', 'icon']);
+        const { keyMaps } = this.props;
+        const children = data[get(keyMaps, 'children', 'children')];
+        !isUndefined(children) && (props.children = children);
+        return <TreeNode {...treeNodeProps} {...data} {...props} data={data} style={style} />;
     };
 
     itemKey = (index: number, data: Record<string, any>) => {

+ 86 - 0
yarn.lock

@@ -1519,11 +1519,25 @@
     "@douyinfe/semi-animation-styled" "2.23.2"
     classnames "^2.2.6"
 
+"@douyinfe/[email protected]":
+  version "2.44.0"
+  resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-react/-/semi-animation-react-2.44.0.tgz#3548c4c200a1e18b1113cba5cb8c0d5b4d77f797"
+  integrity sha512-Zo4lpmrWI3cqCyzv9TeytBA3Cu8vrZ2HxqmVJax7PuGB6N13eDOtzOKg6j58MqfB4ClKjqvUBqapXNT3z/Q93g==
+  dependencies:
+    "@douyinfe/semi-animation" "2.44.0"
+    "@douyinfe/semi-animation-styled" "2.44.0"
+    classnames "^2.2.6"
+
 "@douyinfe/[email protected]":
   version "2.23.2"
   resolved "https://registry.npmjs.org/@douyinfe/semi-animation-styled/-/semi-animation-styled-2.23.2.tgz#f18bc074515441c297cc636ed98521e249d093c9"
   integrity sha512-cKaA1yGHPF76Rx7EZDZicj+1oX1su2wnqb/UGFOTquAwqWmkTfgQ+EKxCd/N704WH+RtmGf4xbrJKpBvvcEdSQ==
 
+"@douyinfe/[email protected]":
+  version "2.44.0"
+  resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-styled/-/semi-animation-styled-2.44.0.tgz#1223b79bc06f6a3871badf633ef39a9c74f61fce"
+  integrity sha512-QSAxxpkamyxR0d81Ep2fDrdgHON9GB0ffarjt7YWJ2VD6oH269ArLtdW/UNCrusJagcO5xkfLNJC0UURExNi7A==
+
 "@douyinfe/[email protected]":
   version "2.12.0"
   resolved "https://registry.npmjs.org/@douyinfe/semi-animation/-/semi-animation-2.12.0.tgz#51fe52d3911c2591a80a6e9fe96e6809c1511f13"
@@ -1539,6 +1553,13 @@
   dependencies:
     bezier-easing "^2.1.0"
 
+"@douyinfe/[email protected]":
+  version "2.44.0"
+  resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation/-/semi-animation-2.44.0.tgz#11171f7d7e543c7c3e0ee1196a3deaa1deff2bd2"
+  integrity sha512-PZPljqV5/yPBdnPtGKBfdDJ0xHLMsC5VK/5tKKXeNp9BT23P5/GOmkvvuQ5on8tPJo/yqOpbg0tVVLorPOx6Hw==
+  dependencies:
+    bezier-easing "^2.1.0"
+
 "@douyinfe/[email protected]":
   version "2.33.1"
   resolved "https://registry.npmjs.org/@douyinfe/semi-foundation/-/semi-foundation-2.33.1.tgz#1dfe6233e35a4ed768cb580b0c9a677d1c34ffba"
@@ -1553,6 +1574,20 @@
     memoize-one "^5.2.1"
     scroll-into-view-if-needed "^2.2.24"
 
+"@douyinfe/[email protected]":
+  version "2.44.0"
+  resolved "https://registry.yarnpkg.com/@douyinfe/semi-foundation/-/semi-foundation-2.44.0.tgz#7de5f89a003fc0fdd704aefcce2d13bd0e1c9fc5"
+  integrity sha512-U96IGnhQH32NuRwTGtwAbtr+eqQkwBJSu56dKbOrxV4fAzSeDvDSB2lVxwOeF53eX/j7c60S2Kohs7E/7oB3fQ==
+  dependencies:
+    "@douyinfe/semi-animation" "2.44.0"
+    async-validator "^3.5.0"
+    classnames "^2.2.6"
+    date-fns "^2.29.3"
+    date-fns-tz "^1.3.8"
+    lodash "^4.17.21"
+    memoize-one "^5.2.1"
+    scroll-into-view-if-needed "^2.2.24"
+
 "@douyinfe/[email protected]", "@douyinfe/semi-icons@latest":
   version "2.33.1"
   resolved "https://registry.yarnpkg.com/@douyinfe/semi-icons/-/semi-icons-2.33.1.tgz#8e2871d9bc0ab7e12df74e3c71802d53d69b7425"
@@ -1560,11 +1595,23 @@
   dependencies:
     classnames "^2.2.6"
 
+"@douyinfe/[email protected]", "@douyinfe/semi-icons@^2.0.0":
+  version "2.44.0"
+  resolved "https://registry.yarnpkg.com/@douyinfe/semi-icons/-/semi-icons-2.44.0.tgz#5832d5ff0c88d2584f773734f9b40810f07d295e"
+  integrity sha512-hWFGNLjGbZl4HSUcnFzcTfxSOGcQmqSJdRYDDiaMB9E8JaqEJO9SA0YRtDNkQ9lUJL3acSts/Oe2oNWUVUkW+Q==
+  dependencies:
+    classnames "^2.2.6"
+
 "@douyinfe/[email protected]":
   version "2.33.1"
   resolved "https://registry.npmjs.org/@douyinfe/semi-illustrations/-/semi-illustrations-2.33.1.tgz#530ab851f4dc32a52221c4067c778c800b9b55d7"
   integrity sha512-tTTUN8QwnQiF++sk4VBNzfkG87aYZ4iUeqk2ys8/ymVUmCZQ7y46ys020GO1MfPHRR47OMFPI82FVcH1WQtE3g==
 
+"@douyinfe/[email protected]":
+  version "2.44.0"
+  resolved "https://registry.yarnpkg.com/@douyinfe/semi-illustrations/-/semi-illustrations-2.44.0.tgz#92ed6fdeb073f2fb9a52f50324b7055c88f3c6be"
+  integrity sha512-mQaBYGoDr+HncJEUKr0qCuPzfdzU06i8on9SrJ23cHFBmXCqzwebm+PkeY3448fNdE9srZP1W4Ca+q/hXGw04Q==
+
 "@douyinfe/[email protected]":
   version "2.23.2"
   resolved "https://registry.npmjs.org/@douyinfe/semi-scss-compile/-/semi-scss-compile-2.23.2.tgz#30884bb194ee9ae1e81877985e5663c3297c1ced"
@@ -1638,6 +1685,40 @@
   dependencies:
     glob "^7.1.6"
 
+"@douyinfe/[email protected]":
+  version "2.44.0"
+  resolved "https://registry.yarnpkg.com/@douyinfe/semi-theme-default/-/semi-theme-default-2.44.0.tgz#5281af7454c01c76742db794c813f84341a7e24d"
+  integrity sha512-YKi7mgFgj9wkL5VFE1cZx/4/4Dfxbf6XNx2fWIbeUcYDESHvOxbmpB13D8fRdF/0lruz0yWBhU7irPT26OhaMg==
+  dependencies:
+    glob "^7.1.6"
+
+"@douyinfe/semi-ui@^2.0.0":
+  version "2.44.0"
+  resolved "https://registry.yarnpkg.com/@douyinfe/semi-ui/-/semi-ui-2.44.0.tgz#3aaefef64dd00f7cfb06fe25db945790a9335140"
+  integrity sha512-SYtx/YTfOHEraMeAEONRLeiCMbZZD6pyNnwLIU8dXIXd71sZpKpPl5pvQegxgFzE16A73zk4SslC5CguVxct2w==
+  dependencies:
+    "@dnd-kit/core" "^6.0.8"
+    "@dnd-kit/sortable" "^7.0.2"
+    "@dnd-kit/utilities" "^3.2.1"
+    "@douyinfe/semi-animation" "2.44.0"
+    "@douyinfe/semi-animation-react" "2.44.0"
+    "@douyinfe/semi-foundation" "2.44.0"
+    "@douyinfe/semi-icons" "2.44.0"
+    "@douyinfe/semi-illustrations" "2.44.0"
+    "@douyinfe/semi-theme-default" "2.44.0"
+    async-validator "^3.5.0"
+    classnames "^2.2.6"
+    copy-text-to-clipboard "^2.1.1"
+    date-fns "^2.29.3"
+    date-fns-tz "^1.3.8"
+    lodash "^4.17.21"
+    prop-types "^15.7.2"
+    react-resizable "^3.0.5"
+    react-window "^1.8.2"
+    resize-observer-polyfill "^1.5.1"
+    scroll-into-view-if-needed "^2.2.24"
+    utility-types "^3.10.0"
+
 "@douyinfe/semi-ui@latest":
   version "2.33.1"
   resolved "https://registry.yarnpkg.com/@douyinfe/semi-ui/-/semi-ui-2.33.1.tgz#3234ca96eb3560b8299bc9750fbe59446522d9bb"
@@ -11205,6 +11286,11 @@ eslint-plugin-react@^7.20.6, eslint-plugin-react@^7.24.0:
     semver "^6.3.0"
     string.prototype.matchall "^4.0.8"
 
+eslint-plugin-semi-design@^2.33.0:
+  version "2.44.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-semi-design/-/eslint-plugin-semi-design-2.44.0.tgz#3e1909d9adf72b3dc3dd44c127a4f78fd3775fc6"
+  integrity sha512-Lq9h2mQ7l5dY0r+OJ/wPkuI3DkBAMbnf94C/O+SP1kIE1reuyzWUzoMs3xEOL5E9XWnOfK5aAdRRsv6+Jqk9QA==
+
 eslint-rule-composer@^0.3.0:
   version "0.3.0"
   resolved "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9"