Browse Source

Feat/add tree showLine api (#1973)

* feat: add showline

* feat: add showline

* chore: Complete showline demo

* fix: fixed treeSelect treeNode

* fix: 删除无用标记

* chore: update lock file

* style: Fix style error of draggable Tree in showLine state

* docs: Tree add showLine

* style: add padding-left for option

* feat: TreeSelect add showLine

---------

Co-authored-by: yeming <[email protected]>
Co-authored-by: zhangyumei.0319 <[email protected]>
小铭 1 year ago
parent
commit
71322e850c

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

@@ -1450,6 +1450,7 @@ function Demo() {
 | searchPosition | Set the position of the search box, one of: `dropdown`、`trigger` | string | `dropdown` | 1.29.0 |
 | showClear | When the value is not empty, whether the trigger displays the clear button | boolean | false |  |
 | showFilteredOnly | Toggle whether to displayed filtered result only in search mode | boolean | false | 0.32.0 |
+| showLine | The option in the options panel shows connecting lines | boolean | false | 2.50.0 |
 | showRestTagsPopover | When the number of tags exceeds maxTagCount and hover reaches +N, whether to display the remaining content through Popover | boolean | false | 2.22.0 |
 | showSearchClear | Toggle whether to support clear search box | boolean | true | 0.35.0 |
 | size                     | Size for input box,one of `large`,`small`,`default`                              | string                                                            | `default`   | -       |

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

@@ -1431,6 +1431,7 @@ function Demo() {
 | searchPosition | 设置搜索框的位置,可选: `dropdown`、`trigger`                                                                                                          | string | `dropdown` | 1.29.0 |
 | showClear | 当值不为空时,trigger 是否展示清除按钮                                                                                                                    | boolean | false |  |
 | showFilteredOnly | 搜索状态下是否只展示过滤后的结果                                                                                                                           | boolean | false | 0.32.0 |
+| showLine | 选项面板中选项显示连接线 | boolean | false | 2.50.0 |
 | showRestTagsPopover | 当超过 maxTagCount,hover 到 +N 时,是否通过 Popover 显示剩余内容                                                                                           | boolean | false | 2.22.0 |
 | showSearchClear | 是否显示搜索框的清除按钮                                                                                                                               | boolean | true | 0.35.0 |
 | size | 选择框大小,可选 `large`,`small`,`default`                                                                                                         | string | `default` | - |

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

@@ -1213,6 +1213,79 @@ class Demo extends React.Component {
 }
 ```
 
+### Tree with line
+
+Set the line between nodes through `showLine`, the default is false, supported starting from 2.50.0
+
+```jsx live=true hideInDSM
+import React, { useState, useCallback } from 'react';
+import { Tree, Switch } from '@douyinfe/semi-ui';
+
+() => {
+    const [show, setShow] = useState(true);
+    const onChange = useCallback((value) => {
+        setShow(value);
+    }, []);
+    const treeData = useMemo(() => {
+        return [
+            {
+                label: 'parent-0',
+                key: 'parent-0',
+                children: [
+                    {
+                        label: 'leaf-0-0',
+                        key: 'leaf-0-0',
+                        children: [
+                            {
+                                label: 'leaf-0-0-0',
+                                key: 'leaf-0-0-0',
+                            },
+                            {
+                                label: 'leaf-0-0-1',
+                                key: 'leaf-0-0-1',
+                            },
+                            {
+                                label: 'leaf-0-0-2',
+                                key: 'leaf-0-0-2',
+                            },
+                        ]
+                    },
+                    {
+                        label: 'leaf-0-1',
+                        key: 'leaf-0-1',
+                    }
+                ]
+            },
+            {
+                label: 'parent-1',
+                key: 'parent-1',
+            }
+        ];
+    }, []);
+
+    const style = {
+        width: 260,
+        height: 420,
+        border: '1px solid var(--semi-color-border)'
+    };
+
+    return (
+        <>
+            <div style={{ display: 'flex', alignItems: 'center', columnGap: 5, marginBottom: 5 }}>
+                <strong>showLine </strong>
+                <Switch checked={show} onChange={onChange} />
+            </div>
+            <Tree
+                showLine={show}
+                defaultExpandAll
+                treeData={treeData}
+                style={style}
+            />
+        </>
+    );
+};
+```
+
 ### Virtualized Tree
 If you need to render large sets of tree structured data, you could use virtualized tree. In virtualized mode, animation / motion is disabled for better performance. 
 
@@ -2232,6 +2305,7 @@ import { IconFixedStroked, IconSectionStroked, IconAbsoluteStroked, IconInnerSec
 | searchStyle         | Style for for search box           | CSSProperties                      | -       | - |
 | showClear   | Toggle whether to support clear input box | boolean                     | true   | 0.35.0|
 | showFilteredOnly | Toggle whether to displayed filtered result only in search mode | boolean | false | 0.32.0 |
+| showLine | show line between tree nodes | boolean | false | 2.50.0 |
 | style               | Inline style                       | CSSProperties                      | -       | - |
 | treeData            | Data for treeNodes                 | TreeNodeData[]            | \[]     | - |
 | treeDataSimpleJson  | Data for treeNodes in JSON format, return value in JSON format as well    | TreeDataSimpleJson                      | \{}     | - |

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

@@ -1241,6 +1241,79 @@ class Demo extends React.Component {
 }
 ```
 
+### 连接线
+
+通过 `showLine` 设置节点之间的连接线,默认为 false,从 2.50.0 开始支持
+
+```jsx live=true hideInDSM
+import React, { useState, useCallback } from 'react';
+import { Tree, Switch } from '@douyinfe/semi-ui';
+
+() => {
+    const [show, setShow] = useState(true);
+    const onChange = useCallback((value) => {
+        setShow(value);
+    }, []);
+    const treeData = useMemo(() => {
+        return [
+            {
+                label: 'parent-0',
+                key: 'parent-0',
+                children: [
+                    {
+                        label: 'leaf-0-0',
+                        key: 'leaf-0-0',
+                        children: [
+                            {
+                                label: 'leaf-0-0-0',
+                                key: 'leaf-0-0-0',
+                            },
+                            {
+                                label: 'leaf-0-0-1',
+                                key: 'leaf-0-0-1',
+                            },
+                            {
+                                label: 'leaf-0-0-2',
+                                key: 'leaf-0-0-2',
+                            },
+                        ]
+                    },
+                    {
+                        label: 'leaf-0-1',
+                        key: 'leaf-0-1',
+                    }
+                ]
+            },
+            {
+                label: 'parent-1',
+                key: 'parent-1',
+            }
+        ];
+    }, []);
+
+    const style = {
+        width: 260,
+        height: 420,
+        border: '1px solid var(--semi-color-border)'
+    };
+
+    return (
+        <>
+            <div style={{ display: 'flex', alignItems: 'center', columnGap: 5, marginBottom: 5 }}>
+                <strong>showLine </strong>
+                <Switch checked={show} onChange={onChange} />
+            </div>
+            <Tree
+                showLine={show}
+                defaultExpandAll
+                treeData={treeData}
+                style={style}
+            />
+        </>
+    );
+};
+```
+
 ### 虚拟化
 列表虚拟化,用于大量树节点的情况。开启后,动画效果将被关闭。
 
@@ -2247,6 +2320,7 @@ import { IconFixedStroked, IconSectionStroked, IconAbsoluteStroked, IconInnerSec
 | searchStyle | 搜索框的样式 | CSSProperties | - | - |
 | showClear | 支持清除搜索框 | boolean | true | 0.35.0 |
 | showFilteredOnly | 搜索状态下是否只展示过滤后的结果 | boolean | false | 0.32.0 |
+| showLine | 显示连接线 | boolean | false | 2.50.0 |
 | style | 样式  | CSSProperties | - | - |
 | treeData | treeNodes 数据,如果设置则不需要手动构造 TreeNode 节点(key值在整个树范围内唯一) | TreeNodeData[] | \[] | - |
 | treeDataSimpleJson | 简单 JSON 形式的 `TreeNodeData` 数据,如果设置则不需要手动构造 TreeNode 节点,返回值为包含选中节点的Json数据 | TreeDataSimpleJson | \{} | - |

+ 3 - 1
packages/semi-foundation/tree/foundation.ts

@@ -42,7 +42,8 @@ export interface BasicTreeNodeProps {
     directory?: boolean;
     selectedKey?: string;
     motionKey?: string[] | string;
-    eventKey?: string
+    eventKey?: string;
+    showLine?: boolean
 }
 
 export interface BasicTreeNodeData {
@@ -236,6 +237,7 @@ export interface BasicTreeProps {
     searchStyle?: any;
     showClear?: boolean;
     showFilteredOnly?: boolean;
+    showLine?: boolean;
     style?: any;
     treeData?: BasicTreeNodeData[];
     treeDataSimpleJson?: TreeDataSimpleJson;

+ 8 - 16
packages/semi-foundation/tree/rtl.scss

@@ -5,55 +5,47 @@ $module: #{$prefix}-tree;
     .#{$module} {
         direction: rtl;
     }
-    
+
     .#{$module}-wrapper {
         direction: rtl;
     }
-    
+
     .#{$module}-option-list {
         direction: rtl;
-    
+
         .#{$module}-option-expand-icon,
         .#{$module}-option-empty-icon {
             margin-right: 0;
             margin-left: $spacing-tree_icon-marginRight;
         }
-    
+
         .#{$module}-option {
             &-label {
                 & > .#{$prefix}-icon {
                     margin-right: 0;
                     margin-left: $spacing-tree_icon-marginRight;
                 }
-    
+
                 .#{$prefix}-checkbox {
                     margin-right: 0;
                     margin-left: $spacing-tree_icon-marginRight;
                 }
             }
-    
+
             &-collapsed {
                 .#{$module}-option-expand-icon {
                     transform: rotate(90deg);
                 }
             }
         }
-    
-        @for $i from 1 through 20 {
-            .#{$module}-option-level-#{$i} {
-                padding-left: 0;
-                padding-right: $spacing-tree_option_level-paddingLeft * ($i - 1) + $spacing-tree_option_level1-paddingLeft;
-            }
-        }
-    
+
         .#{$module}-option-label-empty {
             padding-left: auto;
             padding-right: 0;
         }
     }
-    
+
     .#{$module}-option-list-block {
         direction: rtl;
     }
 }
-

+ 107 - 15
packages/semi-foundation/tree/tree.scss

@@ -1,11 +1,9 @@
-@import "./animation.scss";
+@import './animation.scss';
 @import './variables.scss';
 
-
 $module: #{$prefix}-tree;
 
 .#{$module} {
-
     &-search-wrapper {
         padding: $spacing-tree_search_wrapper-paddingY $spacing-tree_search_wrapper-paddingX;
     }
@@ -34,6 +32,7 @@ $module: #{$prefix}-tree;
         box-sizing: border-box;
         padding-top: $spacing-tree_option-paddingTop;
         padding-bottom: $spacing-tree_option-paddingBottom;
+        padding-left: $spacing-tree_option_level1-paddingLeft;
     }
 
     li > .#{$module}-option-label {
@@ -55,8 +54,9 @@ $module: #{$prefix}-tree;
         display: flex;
         align-items: center;
         cursor: pointer;
-        transition: background-color $transition_duration-tree_option-bg $transition_function-tree_option-bg $transition_delay-tree_option-bg;
-        transform:scale($transform_scale-tree-option);
+        transition: background-color $transition_duration-tree_option-bg $transition_function-tree_option-bg
+            $transition_delay-tree_option-bg;
+        transform: scale($transform_scale-tree-option);
 
         @include font-size-regular;
         word-break: break-word;
@@ -163,7 +163,7 @@ $module: #{$prefix}-tree;
         &-draggable {
             box-sizing: border-box;
             border-left: $width-tree_option_draggable-border solid transparent;
-            margin-top: -$width-tree_option_draggable-border;
+            // margin-top: -$width-tree_option_draggable-border;
 
             .#{$module}-option-label {
                 border-top: $width-tree_option_draggable-border transparent solid;
@@ -175,7 +175,26 @@ $module: #{$prefix}-tree;
             }
 
             .#{$module}-option-drag-over-gap-bottom {
-                border-bottom: $width-tree_option_draggable-border $color-tree_option_draggable_insert-border-default solid;
+                border-bottom: $width-tree_option_draggable-border $color-tree_option_draggable_insert-border-default
+                    solid;
+            }
+
+            .#{$module}-option-indent {
+                .#{$module}-option-indent-unit:before {
+                    top: 0px;
+                    bottom: 0px;
+                }  
+            }
+
+            .#{$module}-option-switcher-leaf-line::before {
+                top: 0px;
+                bottom: 0px;
+            }
+
+            &.#{$module}-option-tree-node-last-leaf {
+                .#{$module}-option-switcher-leaf-line::before {
+                    height: 50%;
+                }
             }
         }
 
@@ -185,7 +204,8 @@ $module: #{$prefix}-tree;
             }
 
             &.#{$module}-option-fullLabel-drag-over-gap-bottom {
-                border-bottom: $width-tree_option_draggable-border $color-tree_option_draggable_insert-border-default solid;
+                border-bottom: $width-tree_option_draggable-border $color-tree_option_draggable_insert-border-default
+                    solid;
             }
         }
 
@@ -207,7 +227,85 @@ $module: #{$prefix}-tree;
                     left: -$width-tree_option_draggable-border;
                     bottom: 0;
                     right: -1px;
-                    border-top: $width-tree_option_draggable-border solid $color-tree_option_draggable_insert-border-default;
+                    border-top: $width-tree_option_draggable-border solid
+                        $color-tree_option_draggable_insert-border-default;
+                }
+            }
+        }
+        &-indent {
+            align-self: stretch;
+            white-space: nowrap;
+            user-select: none;
+            &-unit {
+                display: inline-block;
+                width: $spacing-tree_option_level-paddingLeft;
+            }
+        }
+
+        &-indent-show-line {
+            .#{$module}-option-indent-unit {
+                position: relative;
+                height: 100%;
+
+                &::before {
+                    position: absolute;
+                    top: -$spacing-tree_option-paddingTop;
+                    inset-inline-start: calc($width-tree_emptyIcon / 2);
+                    bottom: -$spacing-tree_option-paddingBottom;
+                    border-inline-end: $width-tree_option_line solid var(--semi-color-text-3);
+                    content: '';
+                }
+
+                &-end {
+                    &::before {
+                        display: none;
+                    }
+                }
+            }
+        }
+
+        &-switcher {
+            position: relative;
+            flex: none;
+            align-self: stretch;
+            width: $width-tree_emptyIcon;
+            margin: 0;
+            text-align: center;
+            cursor: pointer;
+            user-select: none;
+            margin-right: $spacing-tree_icon-marginRight;
+            &-leaf-line {
+                z-index: 1;
+                position: relative;
+                display: inline-block;
+                width: 100%;
+                height: 100%;
+
+                &::before {
+                    position: absolute;
+                    top: -$spacing-tree_option-paddingTop;
+                    inset-inline-start: calc($width-tree_emptyIcon / 2);
+                    bottom: -$spacing-tree_option-paddingBottom;
+                    border-inline-end: $width-tree_option_line solid var(--semi-color-text-3);
+                    content: '';
+                }
+
+                &::after {
+                    box-sizing: border-box;
+                    position: absolute;
+                    width: calc(($spacing-tree_option_level-paddingLeft / 2) * 0.8);
+                    height: 50%;
+                    border-bottom: $width-tree_option_line solid var(--semi-color-text-3);
+                    content: '';
+                    margin-inline-start: $width-tree_option_line;
+                }
+            }
+        }
+
+        &-tree-node-last-leaf {
+            .#{$module}-option-switcher-leaf-line {
+                &::before {
+                    height: calc(50% + $spacing-tree_option-paddingTop);
                 }
             }
         }
@@ -231,12 +329,6 @@ $module: #{$prefix}-tree;
         }
     }
 
-    @for $i from 1 through 20 {
-        .#{$module}-option-level-#{$i} {
-            padding-left: $spacing-tree_option_level-paddingLeft * ($i - 1) + $spacing-tree_option_level1-paddingLeft;
-        }
-    }
-
     .#{$module}-option-empty {
         &:hover,
         &:active {

+ 2 - 2
packages/semi-foundation/tree/treeUtil.ts

@@ -56,7 +56,6 @@ export function flattenTreeData(treeNodeList: any[], expandedKeys: Set<string>,
         return list.map((treeNode, index) => {
             const pos = getPosition(parent ? parent.pos : '0', index);
             const mergedKey = treeNode[realKeyName];
-
             const otherData = {};
             if (keyMaps) {
                 Object.entries(omit(keyMaps, 'children')).forEach(([key, value]) => {
@@ -74,6 +73,7 @@ export function flattenTreeData(treeNodeList: any[], expandedKeys: Set<string>,
                 children: null,
                 data: treeNode,
                 _innerDataTag: true,
+                isEnd: [...(parent ? parent.isEnd : []), index === list.length - 1],
             };
             const isBooleanFilteredShownKeys = typeof filteredShownKeys === 'boolean';
             if (!filterSearch || (!isBooleanFilteredShownKeys && filteredShownKeys.has(mergedKey))) {
@@ -340,7 +340,7 @@ export function calcCheckedKeys(values: any, keyEntities: KeyEntities) {
     let halfCheckedKeys = new Set([]);
     let visited: any[] = [];
 
-    const levelMap: {[key: number]: string[]} = getSortedKeyList(keyList, keyEntities);
+    const levelMap: { [key: number]: string[] } = getSortedKeyList(keyList, keyEntities);
 
     const calcCurrLevel = (node: any) => {
         const { key, parent, level } = node;

+ 3 - 2
packages/semi-foundation/tree/variables.scss

@@ -28,9 +28,10 @@ $spacing-tree_option-paddingTop: 4px; // 树选项顶部内边距
 $spacing-tree_option-paddingBottom: 4px; // 树选项底部内边距
 $spacing-tree_icon-marginRight: 8px; // 树选项图标右侧外边距
 $spacing-tree_label_withIcon-marginRight: 8px; // 树选项图标右侧外边距
-$spacing-tree_option_draggable-paddingY: 3px; // 可拖拽的树选项垂直内边距
+$spacing-tree_option_draggable-paddingY: 2px; // 可拖拽的树选项垂直内边距
 $spacing-tree_option_draggable-paddingX: 0; // 可拖拽的树选项水平内边距
 
 $width-tree_emptyIcon: $width-icon-small; // 树选项空图标宽度
 $width-tree_spinIcon: $width-icon-small; // 树选项加载 spin 宽度
-$width-tree_option_draggable-border: 2px; // 可拖拽的树标示线宽度
+$width-tree_option_draggable-border: 2px; // 可拖拽的树标示线宽度
+$width-tree_option_line: 1px; // showline展示线宽度

+ 495 - 403
packages/semi-ui/tree/_story/tree.stories.jsx

@@ -3,7 +3,7 @@ import { cloneDeep, difference, isEqual } from 'lodash';
 import { IconEdit, IconMapPin, IconMore } from '@douyinfe/semi-icons';
 import Tree from '../index';
 import AutoSizer from '../autoSizer';
-import { Button, ButtonGroup, Input, Popover, Toast, Space, Select, Switch, Typography} from '../../index';
+import { Button, ButtonGroup, Input, Popover, Toast, Space, Select, Switch, Typography } from '../../index';
 import BigTree from './BigData';
 import testData from './data';
 const TreeNode = Tree.TreeNode;
@@ -1872,8 +1872,8 @@ const MutipleHLTree = () => {
       backgroundColor: selected.has(key)
         ? 'rgba(var(--semi-blue-0), 1)'
         : selectedThroughParent.has(key)
-        ? 'rgba(var(--semi-blue-0), .5)'
-        : 'transparent',
+          ? 'rgba(var(--semi-blue-0), .5)'
+          : 'transparent',
     };
     return (
       <li className={className} role="treeitem" onClick={onClick} style={style}>
@@ -1981,6 +1981,7 @@ const DnDTree = () => {
   ];
 
   const [treeData, setTreeData] = useState(initialData);
+  const [showLine, setShowLine] = useState(false);
 
   // const [expandedKeys, setExpandedKeys] = useState(['zhongguo']);
 
@@ -2041,14 +2042,23 @@ const DnDTree = () => {
     setTreeData(data);
   }
 
+  const onSwitchChange = useCallback((value) => {
+    setShowLine(value);
+  }, []);
+
   return (
-    <Tree
-      treeData={treeData}
-      draggable
-      //expandedKeys={expandedKeys}
-      onDragEnter={onDragEnter}
-      onDrop={onDrop}
-    />
+    <>
+      <Switch onChange={onSwitchChange}/>
+      <Tree
+        treeData={treeData}
+        draggable
+        showLine={showLine}
+        //expandedKeys={expandedKeys}
+        onDragEnter={onDragEnter}
+        onDrop={onDrop}
+      />
+    </>
+    
   );
 };
 
@@ -2199,8 +2209,8 @@ export const RenderFullLabelWithDraggable = () => {
       backgroundColor: selected.has(key)
         ? 'rgba(var(--semi-blue-0), 1)'
         : selectedThroughParent.has(key)
-        ? 'rgba(var(--semi-blue-0), .5)'
-        : 'transparent',
+          ? 'rgba(var(--semi-blue-0), .5)'
+          : 'transparent',
     };
     return (
       <li className={className} role="treeitem" onClick={onClick} style={style}>
@@ -2236,62 +2246,62 @@ RenderFullLabelWithDraggable.story = {
 export const CheckRelationDemo = () => {
   const treeData = [
     {
-        label: 'Asia',
-        value: 'Asia',
-        key: '0',
-        children: [
+      label: 'Asia',
+      value: 'Asia',
+      key: '0',
+      children: [
+        {
+          label: 'China',
+          value: 'China',
+          key: '0-0',
+          children: [
             {
-                label: 'China',
-                value: 'China',
-                key: '0-0',
-                children: [
-                    {
-                        label: 'Beijing',
-                        value: 'Beijing',
-                        key: '0-0-0',
-                    },
-                    {
-                        label: 'Shanghai',
-                        value: 'Shanghai',
-                        key: '0-0-1',
-                    },
-                    {
-                        label: 'Chengdu',
-                        value: 'Chengdu',
-                        key: '0-0-2',
-                    },
-                ],
+              label: 'Beijing',
+              value: 'Beijing',
+              key: '0-0-0',
             },
             {
-                label: 'Japan',
-                value: 'Japan',
-                key: '0-1',
-                children: [
-                    {
-                        label: 'Osaka',
-                        value: 'Osaka',
-                        key: '0-1-0'
-                    }
-                ]
+              label: 'Shanghai',
+              value: 'Shanghai',
+              key: '0-0-1',
             },
-        ],
-    },
-    {
-        label: 'North America',
-        value: 'North America',
-        key: '1',
-        children: [
             {
-                label: 'United States',
-                value: 'United States',
-                key: '1-0'
+              label: 'Chengdu',
+              value: 'Chengdu',
+              key: '0-0-2',
             },
+          ],
+        },
+        {
+          label: 'Japan',
+          value: 'Japan',
+          key: '0-1',
+          children: [
             {
-                label: 'Canada',
-                value: 'Canada',
-                key: '1-1'
+              label: 'Osaka',
+              value: 'Osaka',
+              key: '0-1-0'
             }
-        ]
+          ]
+        },
+      ],
+    },
+    {
+      label: 'North America',
+      value: 'North America',
+      key: '1',
+      children: [
+        {
+          label: 'United States',
+          value: 'United States',
+          key: '1-0'
+        },
+        {
+          label: 'Canada',
+          value: 'Canada',
+          key: '1-1'
+        }
+      ]
     }
   ];
   const [value, setValue] = useState('China');
@@ -2396,7 +2406,7 @@ export const CheckRelationDemo = () => {
         checkRelation='unRelated'
         defaultExpandAll
         style={style}
-        onSelect={(value,status,node)=>console.log('select', value, status, node)}
+        onSelect={(value, status, node) => console.log('select', value, status, node)}
       />
     </>
   );
@@ -2404,240 +2414,250 @@ export const CheckRelationDemo = () => {
 
 export const ValueImpactExpansionWithDynamicTreeData = () => {
   const json = {
-      "Node0": { 
-          "Child Node0-0": '0-0', 
-          "Child Node0-1": '0-1', 
-      },
-      "Node1": { 
-          "Child Node1-0": '1-0', 
-          "Child Node1-1": '1-1', 
-      }
+    "Node0": {
+      "Child Node0-0": '0-0',
+      "Child Node0-1": '0-1',
+    },
+    "Node1": {
+      "Child Node1-0": '1-0',
+      "Child Node1-1": '1-1',
+    }
   }
   const json2 = {
-      "Updated Node0": { 
-          "Updated Child Node0-0": {
-              'Updated Child Node0-0-0':'0-0'
-          }, 
-          "Updated Child Node0-1": '0-1', 
+    "Updated Node0": {
+      "Updated Child Node0-0": {
+        'Updated Child Node0-0-0': '0-0'
       },
-      "Updated Node1": { 
-          "Updated Child Node1-0": '1-0', 
-          "Updated Child Node1-1": '1-1', 
-      }
+      "Updated Child Node0-1": '0-1',
+    },
+    "Updated Node1": {
+      "Updated Child Node1-0": '1-0',
+      "Updated Child Node1-1": '1-1',
+    }
   }
   const style = {
-      width: 260,
-      height: 420,
-      border: '1px solid var(--color-border)'
+    width: 260,
+    height: 420,
+    border: '1px solid var(--color-border)'
   }
   const [value, setValue] = useState('0-0')
   const [tree, setTree] = useState(json);
   const handleValueButtonClick = () => {
-      if (value === '0-0') {
-          setValue('1-0');
-      } else {
-          setValue('0-0');
-      }
+    if (value === '0-0') {
+      setValue('1-0');
+    } else {
+      setValue('0-0');
+    }
   }
   const handleTreeDataButtonClick = () => {
-      if(isEqual(tree, json)){
-          setTree(json2);
-      } else {
-          setTree(json);
-      }
+    if (isEqual(tree, json)) {
+      setTree(json2);
+    } else {
+      setTree(json);
+    }
   }
-  return (  
+  return (
     <>
       <div>value 受控时,当 treeData/treeDataSimpleJson 改变时,应该根据 value 自动展开</div>
-      <Tree 
-          value={value}
-          treeDataSimpleJson={tree} 
-          style={style}
-          onChange={v => setValue(v)}
+      <Tree
+        value={value}
+        treeDataSimpleJson={tree}
+        style={style}
+        onChange={v => setValue(v)}
       />
       <Space>
-          <Button onClick={handleValueButtonClick}>改变 value</Button>
-          <Button onClick={handleTreeDataButtonClick}>改变 TreeData</Button>    
+        <Button onClick={handleValueButtonClick}>改变 value</Button>
+        <Button onClick={handleTreeDataButtonClick}>改变 TreeData</Button>
       </Space>
     </>
   )
 }
 
 class DemoV extends React.Component {
-    constructor() {
-        super();
-        this.state = {
-            gData: [],
-            total: 0,
-            align: 'center',
-            scrollKey: '',
-            expandAll: false,
-        };
-        this.onGen = this.onGen.bind(this);
-        this.onScroll = this.onScroll.bind(this);
-        this.onInputChange = this.onInputChange.bind(this);
-        this.onInputBlur = this.onInputBlur.bind(this);
-        this.onSelectChange = this.onSelectChange.bind(this);
-        this.treeRef = React.createRef();
-    }
-
-    generateData(x = 5, y = 4, z = 3, gData = []) {
-        // x:每一级下的节点总数。y:每级节点里有y个节点、存在子节点。z:树的level层级数(0表示一级)
-        function _loop(_level, _preKey, _tns) {
-            const preKey = _preKey || '0';
-            const tns = _tns || gData;
-
-            const children = [];
-            for (let i = 0; i < x; i++) {
-                const key = `${preKey}-${i}`;
-                tns.push({ label: `${key}-标签`, key: `${key}-key`, value: `${key}-value` });
-                if (i < y) {
-                    children.push(key);
-                }
-            }
-            if (_level < 0) {
-                return tns;
-            }
-            const __level = _level - 1;
-            children.forEach((key, index) => {
-                tns[index].children = [];
-                return _loop(__level, key, tns[index].children);
-            });
+  constructor() {
+    super();
+    this.state = {
+      gData: [],
+      total: 0,
+      align: 'center',
+      scrollKey: '',
+      expandAll: false,
+      showLine: false,
+    };
+    this.onGen = this.onGen.bind(this);
+    this.onScroll = this.onScroll.bind(this);
+    this.onInputChange = this.onInputChange.bind(this);
+    this.onInputBlur = this.onInputBlur.bind(this);
+    this.onSelectChange = this.onSelectChange.bind(this);
+    this.treeRef = React.createRef();
+  }
 
-            return null;
+  generateData(x = 5, y = 4, z = 3, gData = []) {
+    // x:每一级下的节点总数。y:每级节点里有y个节点、存在子节点。z:树的level层级数(0表示一级)
+    function _loop(_level, _preKey, _tns) {
+      const preKey = _preKey || '0';
+      const tns = _tns || gData;
+
+      const children = [];
+      for (let i = 0; i < x; i++) {
+        const key = `${preKey}-${i}`;
+        tns.push({ label: `${key}-标签`, key: `${key}-key`, value: `${key}-value` });
+        if (i < y) {
+          children.push(key);
         }
-        _loop(z);
+      }
+      if (_level < 0) {
+        return tns;
+      }
+      const __level = _level - 1;
+      children.forEach((key, index) => {
+        tns[index].children = [];
+        return _loop(__level, key, tns[index].children);
+      });
 
-        function calcTotal(x, y, z) {
-            const rec = n => (n >= 0 ? x * y ** n-- + rec(n) : 0);
-            return rec(z + 1);
-        }
-        return { gData, total: calcTotal(x, y, z) };
+      return null;
     }
+    _loop(z);
 
-    onGen() {
-        const { gData, total } = this.generateData();
-        this.setState({
-            gData,
-            total
-        });
-    };
-
-    onScroll(scrollKey, align) {
-      this.treeRef?.current.scrollTo({ key: scrollKey, align});
+    function calcTotal(x, y, z) {
+      const rec = n => (n >= 0 ? x * y ** n-- + rec(n) : 0);
+      return rec(z + 1);
     }
+    return { gData, total: calcTotal(x, y, z) };
+  }
 
-    onInputChange(value) {
-      this.setState({
-        scrollKey: value,
-      })
-    }
+  onGen() {
+    const { gData, total } = this.generateData();
+    this.setState({
+      gData,
+      total
+    });
+  };
 
-    onInputBlur(e) {
-      const { value } = e.target;
-      this.onScroll(value, this.state.align);
-    }
+  onScroll(scrollKey, align) {
+    this.treeRef?.current.scrollTo({ key: scrollKey, align });
+  }
 
-    onSelectChange(align){
-      this.setState({
-        align: align,
-      })
-      this.onScroll(this.state.scrollKey, align);
-    }
+  onInputChange(value) {
+    this.setState({
+      scrollKey: value,
+    })
+  }
 
-    render() {
-        const style = {
-            width: 260,
-            border: '1px solid var(--semi-color-border)'
-        };
-        return (
-            <div style={{ padding: '0 20px' }}>
-                <Button onClick={this.onGen}>生成数据: </Button>
-                <span>共 {this.state.total} 个节点</span>
-                <br/>
-                <br/>
-                <div style={{ display: 'flex', alignItems: 'center', }}>
-                  <span>defaultExpandAll</span>
-                  <Switch onChange={(value) => {
-                      this.setState({
-                        expandAll: value,
-                      })
-                  }}/>
-                </div>
-                <br/>
-                <span>跳转的key:</span>
-                <Input
-                  placeholder={'格式:x-x-key'} 
-                  style={{ width: 180, marginRight: 20 }} 
-                  onChange={this.onInputChange}
-                  onBlur={this.onInputBlur}
-                ></Input>
-                <span>scroll align:</span>
-                <Select 
-                  defaultValue='center'
-                  style={{ width: 180 }} 
-                  optionList={['center', 'start', 'end', 'smart', 'auto'].map(item => ({
-                    value: item,
-                    label: item,
-                  }))}
-                  onChange={this.onSelectChange}
-                >
-                </Select>
-                <br />
-                <br />
-                {this.state.gData.length ? (
-                    <Tree
-                        key={`key-${this.state.expandAll}`}
-                        ref={this.treeRef}
-                        defaultExpandAll={this.state.expandAll}
-                        treeData={this.state.gData}
-                        filterTreeNode
-                        showFilteredOnly
-                        style={style}
-                        virtualize={{
-                            // if set height for tree, it will fill 100%
-                            height: 300,
-                            itemSize: 28,
-                        }}
-                    />
-                ) : null}
-            </div>
-        );
-    }
+  onInputBlur(e) {
+    const { value } = e.target;
+    this.onScroll(value, this.state.align);
+  }
+
+  onSelectChange(align) {
+    this.setState({
+      align: align,
+    })
+    this.onScroll(this.state.scrollKey, align);
+  }
+
+  render() {
+    const style = {
+      width: 260,
+      border: '1px solid var(--semi-color-border)'
+    };
+    return (
+      <div style={{ padding: '0 20px' }}>
+        <Button onClick={this.onGen}>生成数据: </Button>
+        <span>共 {this.state.total} 个节点</span>
+        <br />
+        <br />
+        <div style={{ display: 'flex', alignItems: 'center', }}>
+          <span>defaultExpandAll</span>
+          <Switch onChange={(value) => {
+            this.setState({
+              expandAll: value,
+            })
+          }} />
+        </div>
+        <div style={{ display: 'flex', alignItems: 'center', }}>
+          <span>showLine</span>
+          <Switch onChange={(value) => {
+            this.setState({
+              showLine: value,
+            })
+          }} />
+        </div>
+        <br />
+        <span>跳转的key:</span>
+        <Input
+          placeholder={'格式:x-x-key'}
+          style={{ width: 180, marginRight: 20 }}
+          onChange={this.onInputChange}
+          onBlur={this.onInputBlur}
+        ></Input>
+        <span>scroll align:</span>
+        <Select
+          defaultValue='center'
+          style={{ width: 180 }}
+          optionList={['center', 'start', 'end', 'smart', 'auto'].map(item => ({
+            value: item,
+            label: item,
+          }))}
+          onChange={this.onSelectChange}
+        >
+        </Select>
+        <br />
+        <br />
+        {this.state.gData.length ? (
+          <Tree
+            key={`key-${this.state.expandAll}`}
+            ref={this.treeRef}
+            defaultExpandAll={this.state.expandAll}
+            showLine={this.state.showLine}
+            treeData={this.state.gData}
+            filterTreeNode
+            showFilteredOnly
+            style={style}
+            virtualize={{
+              // if set height for tree, it will fill 100%
+              height: 300,
+              itemSize: 28,
+            }}
+          />
+        ) : null}
+      </div>
+    );
+  }
 }
 
 export const issue1124 = () => {
   const [v, setV] = useState(['1']);
-  const initialData = [ 
-      {
-          label: 'Expand to load',
-          value: '0',
-          key: '0',
-      },
-      {
-          label: 'Expand to load',
-          value: '1',
-          key: '1',
-      },
-      {
-          label: 'Leaf Node',
-          value: '2',
-          key: '2',
-          isLeaf: true,
-      },
+  const initialData = [
+    {
+      label: 'Expand to load',
+      value: '0',
+      key: '0',
+    },
+    {
+      label: 'Expand to load',
+      value: '1',
+      key: '1',
+    },
+    {
+      label: 'Leaf Node',
+      value: '2',
+      key: '2',
+      isLeaf: true,
+    },
   ];
   const [treeData, setTreeData] = useState(initialData);
 
   function updateTreeData(list, key, children) {
-      return list.map(node => {
-          if (node.key === key) {
-              return { ...node, children };
-          }
-          if (node.children) {
-              return { ...node, children: updateTreeData(node.children, key, children) };
-          }
-          return node;
-      });
+    return list.map(node => {
+      if (node.key === key) {
+        return { ...node, children };
+      }
+      if (node.children) {
+        return { ...node, children: updateTreeData(node.children, key, children) };
+      }
+      return node;
+    });
   }
 
   const onChange = (value) => {
@@ -2645,38 +2665,38 @@ export const issue1124 = () => {
   }
 
   function onLoadData({ key, children }) {
-      return new Promise(resolve => {
-          if (children) {
-              resolve();
-              return;
-          }
-          setTimeout(() => {
-              setTreeData(origin =>
-                  updateTreeData(origin, key, [
-                      {
-                          label: `Child Node${key}-0`,
-                          key: `${key}-0`,
-                          value: `${key}-0`,
-                      },
-                      {
-                          label: `Child Node${key}-1`,
-                          key: `${key}-1`,
-                          value: `${key}-1`,
-                      },
-                  ]),
-              );
-              resolve();
-          }, 1000);
-      });
+    return new Promise(resolve => {
+      if (children) {
+        resolve();
+        return;
+      }
+      setTimeout(() => {
+        setTreeData(origin =>
+          updateTreeData(origin, key, [
+            {
+              label: `Child Node${key}-0`,
+              key: `${key}-0`,
+              value: `${key}-0`,
+            },
+            {
+              label: `Child Node${key}-1`,
+              key: `${key}-1`,
+              value: `${key}-1`,
+            },
+          ]),
+        );
+        resolve();
+      }, 1000);
+    });
   }
   return (
-      <Tree 
-          onChange={onChange}
-          loadData={onLoadData}
-          treeData={[...treeData]}
-          value={v}
-          multiple
-      />
+    <Tree
+      onChange={onChange}
+      loadData={onLoadData}
+      treeData={[...treeData]}
+      value={v}
+      multiple
+    />
   );
 }
 
@@ -2692,126 +2712,126 @@ export const SearchableAndExpandedKeys = () => {
   const [expandedKeys3, setExpandedKeys3] = useState([]);
   const [expandedKeys4, setExpandedKeys4] = useState([]);
   return (
-      <>
-          <Title heading={6}>expandedKeys 受控</Title>
-          <Tree
-              style={{ width: 300, marginBottom: 30 }}
-              dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
-              treeData={treeData1}
-              expandedKeys={expandedKeys1}
-              defaultValue='beijing'
-              onExpand={v => {
-                  console.log('onExpand value: ', v);
-                  setExpandedKeys1(v);
-              }}
-          />
-          <Title heading={6}>expandedKeys 受控 + 开启搜索</Title>
-          <Tree
-              style={{ width: 300, marginBottom: 30 }}
-              dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
-              treeData={treeData1}
-              filterTreeNode
-              defaultValue='beijing'
-              expandedKeys={expandedKeys2}
-              onExpand={v => {
-                  console.log('onExpand value: ', v);
-                  setExpandedKeys2(v);
-              }}
-          />
-          <Title heading={6}>expandedKeys 受控 + 开启搜索 + 搜索时更新 expandedKeys</Title>
-          <Tree
-              style={{ width: 300, marginBottom: 30 }}
-              dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
-              treeData={treeData1}
-              filterTreeNode
-              expandedKeys={expandedKeys3}
-              defaultValue='beijing'
-              onExpand={v => {
-                  console.log('onExpand value: ', v);
-                  setExpandedKeys3(v)
-              }}
-              onSearch={(input, filterExpandedKeys) => {
-                  console.log('onExpand filterExpandedKeys: ', filterExpandedKeys);
-                  setExpandedKeys3(filterExpandedKeys);
-              }}
-          />
-          <Title heading={6}>expandedKeys 受控 + 开启搜索 + showFilteredOnly + 搜索时更新 expandedKeys</Title>
-          <Tree
-              style={{ width: 300, marginBottom: 30 }}
-              dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
-              treeData={treeData1}
-              filterTreeNode
-              showFilteredOnly
-              expandedKeys={expandedKeys4}
-              defaultValue='beijing'
-              onExpand={v => {
-                  console.log('onExpand value: ', v);
-                  setExpandedKeys4(v)
-              }}
-              onSearch={(input, filterExpandedKeys) => {
-                  console.log('onExpand filterExpandedKeys: ', filterExpandedKeys);
-                  setExpandedKeys4(filterExpandedKeys);
-              }}
-          />
-      </>
+    <>
+      <Title heading={6}>expandedKeys 受控</Title>
+      <Tree
+        style={{ width: 300, marginBottom: 30 }}
+        dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+        treeData={treeData1}
+        expandedKeys={expandedKeys1}
+        defaultValue='beijing'
+        onExpand={v => {
+          console.log('onExpand value: ', v);
+          setExpandedKeys1(v);
+        }}
+      />
+      <Title heading={6}>expandedKeys 受控 + 开启搜索</Title>
+      <Tree
+        style={{ width: 300, marginBottom: 30 }}
+        dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+        treeData={treeData1}
+        filterTreeNode
+        defaultValue='beijing'
+        expandedKeys={expandedKeys2}
+        onExpand={v => {
+          console.log('onExpand value: ', v);
+          setExpandedKeys2(v);
+        }}
+      />
+      <Title heading={6}>expandedKeys 受控 + 开启搜索 + 搜索时更新 expandedKeys</Title>
+      <Tree
+        style={{ width: 300, marginBottom: 30 }}
+        dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+        treeData={treeData1}
+        filterTreeNode
+        expandedKeys={expandedKeys3}
+        defaultValue='beijing'
+        onExpand={v => {
+          console.log('onExpand value: ', v);
+          setExpandedKeys3(v)
+        }}
+        onSearch={(input, filterExpandedKeys) => {
+          console.log('onExpand filterExpandedKeys: ', filterExpandedKeys);
+          setExpandedKeys3(filterExpandedKeys);
+        }}
+      />
+      <Title heading={6}>expandedKeys 受控 + 开启搜索 + showFilteredOnly + 搜索时更新 expandedKeys</Title>
+      <Tree
+        style={{ width: 300, marginBottom: 30 }}
+        dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+        treeData={treeData1}
+        filterTreeNode
+        showFilteredOnly
+        expandedKeys={expandedKeys4}
+        defaultValue='beijing'
+        onExpand={v => {
+          console.log('onExpand value: ', v);
+          setExpandedKeys4(v)
+        }}
+        onSearch={(input, filterExpandedKeys) => {
+          console.log('onExpand filterExpandedKeys: ', filterExpandedKeys);
+          setExpandedKeys4(filterExpandedKeys);
+        }}
+      />
+    </>
   )
 }
 
 export const UnRelatedAndAsyncLoad = () => {
   const initialData = [
-      {
-          label: 'Expand to load0',
-          value: '0',
-          key: '0',
-      },
-      {
-          label: 'Expand to load1',
-          value: '1',
-          key: '1',
-      },
-      {
-          label: 'Leaf Node',
-          value: '2',
-          key: '2',
-          isLeaf: true,
-      },
+    {
+      label: 'Expand to load0',
+      value: '0',
+      key: '0',
+    },
+    {
+      label: 'Expand to load1',
+      value: '1',
+      key: '1',
+    },
+    {
+      label: 'Leaf Node',
+      value: '2',
+      key: '2',
+      isLeaf: true,
+    },
   ];
   const [treeData, setTreeData] = useState(initialData);
 
   function updateTreeData(list, key, children) {
-      return list.map(node => {
-          if (node.key === key) {
-              return { ...node, children };
-          }
-          if (node.children) {
-              return { ...node, children: updateTreeData(node.children, key, children) };
-          }
-          return node;
-      });
+    return list.map(node => {
+      if (node.key === key) {
+        return { ...node, children };
+      }
+      if (node.children) {
+        return { ...node, children: updateTreeData(node.children, key, children) };
+      }
+      return node;
+    });
   }
 
   function onLoadData({ key, children }) {
-      return new Promise(resolve => {
-          if (children) {
-              resolve();
-              return;
-          }
-          setTimeout(() => {
-              setTreeData(origin =>
-                  updateTreeData(origin, key, [
-                      {
-                          label: `Child Node${key}-0`,
-                          key: `${key}-0`,
-                      },
-                      {
-                          label: `Child Node${key}-1`,
-                          key: `${key}-1`,
-                      },
-                  ]),
-              );
-              resolve();
-          }, 1000);
-      });
+    return new Promise(resolve => {
+      if (children) {
+        resolve();
+        return;
+      }
+      setTimeout(() => {
+        setTreeData(origin =>
+          updateTreeData(origin, key, [
+            {
+              label: `Child Node${key}-0`,
+              key: `${key}-0`,
+            },
+            {
+              label: `Child Node${key}-1`,
+              key: `${key}-1`,
+            },
+          ]),
+        );
+        resolve();
+      }, 1000);
+    });
   }
   return (
     <>
@@ -2861,7 +2881,7 @@ export const ChangeTreeData = () => {
     return constructLargeData();
   }, []);
 
-  const treeData2 =  useMemo(() => {
+  const treeData2 = useMemo(() => {
     return constructLargeData();
   }, []);
 
@@ -2873,12 +2893,12 @@ export const ChangeTreeData = () => {
 
   return <>
     <Button onClick={onButtonClick}>点击修改TreeData</Button>
-    <br/><br/>
+    <br /><br />
     <Tree
-        treeData={sign ? treeData1 : treeData2}
-        style={{ width: 300 }}
-        dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
-        placeholder="请选择"
+      treeData={sign ? treeData1 : treeData2}
+      style={{ width: 300 }}
+      dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+      placeholder="请选择"
     />
   </>
 }
@@ -2903,10 +2923,10 @@ export const KeyMaps = () => {
     console.log('onChange', value);
   }, []);
 
-  const normalExpanded = useCallback((expandedKeys, {expanded, node}) => {
+  const normalExpanded = useCallback((expandedKeys, { expanded, node }) => {
     console.log('onExpanded', expandedKeys, expanded, cloneDeep(node));
   }, []);
-  
+
   const normalSearch = useCallback((input, filteredExpandedKeys) => {
     console.log('onSearch', input, filteredExpandedKeys);
   }, []);
@@ -2982,11 +3002,83 @@ export const KeyMaps = () => {
           {...regularTreeProps}
           multiple
           expandedKeys={expandKeys}
-          onExpand={(expandedKeys, {expanded, node}) => {
+          onExpand={(expandedKeys, { expanded, node }) => {
             setExpandedKeys(expandedKeys);
           }}
         />
       </div>
-    </> 
+    </>
   );
 }
+
+
+export const ShowLine = () => {
+  const [showLine,setShowLine] = useState(true);
+  return (
+    <div>
+    <h2>showLine
+      <Switch checked={showLine} onChange={(checked)=>{setShowLine(checked)}}/>
+    </h2>
+    <h2>单选</h2>
+    <Tree
+      showLine={showLine}
+      treeData={treeDataWithoutValue}
+      value="meiguo"
+      defaultExpandAll
+      onChange={(...args) => console.log(args)}
+    />
+    <h2>多选</h2>
+    <Tree
+      showLine={showLine}
+      treeData={[
+        {
+          label: 'Asia',
+          value: 'Asia',
+          key: '0',
+          children: [
+            {
+              label: 'China',
+              value: 'China',
+              key: '0-0',
+              disabled: true,
+              children: [
+                {
+                  label: 'Beijing',
+                  value: 'Beijing',
+                  key: '0-0-0',
+                },
+                {
+                  label: 'Shanghai',
+                  value: 'Shanghai',
+                  key: '0-0-1',
+                  disabled: true,
+                },
+              ],
+            },
+            {
+              label: 'Japan',
+              value: 'Japan',
+              key: '0-1',
+              children: [
+                {
+                  label: 'Osaka',
+                  value: 'Osaka',
+                  key: '0-1-0',
+                },
+              ],
+            },
+          ],
+        },
+      ]}
+      defaultValue="Shanghai"
+      multiple
+      defaultExpandAll
+      disableStrictly
+    />
+  </div>
+  )
+}
+
+ShowLine.story = {
+  name: 'show line',
+};

+ 36 - 0
packages/semi-ui/tree/indent.tsx

@@ -0,0 +1,36 @@
+import * as React from 'react';
+import classNames from 'classnames';
+
+interface IndentProps {
+    prefixcls: string;
+    level: number;
+    isEnd: boolean[];
+    showLine: boolean
+}
+
+const Indent = ({ prefixcls, level, isEnd, showLine }: IndentProps) => {
+    const baseClassName = `${prefixcls}-indent-unit`;
+    const list: React.ReactElement[] = [];
+    for (let i = 0; i < level; i += 1) {
+        list.push(
+            <span
+                key={i}
+                className={classNames(baseClassName, {
+                    [`${baseClassName}-end`]: isEnd[i],
+                })}
+            />,
+        );
+    }
+
+    return (
+        <span aria-hidden="true" className={
+            classNames(`${prefixcls}-indent`, {
+                [`${prefixcls}-indent-show-line`]: showLine,
+            })
+        }>
+            {list}
+        </span>
+    );
+};
+
+export default React.memo(Indent);

+ 9 - 6
packages/semi-ui/tree/index.tsx

@@ -79,6 +79,7 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
         searchStyle: PropTypes.object,
         selectedKey: PropTypes.string,
         showFilteredOnly: PropTypes.bool,
+        showLine: PropTypes.bool,
         style: PropTypes.object,
         treeData: PropTypes.arrayOf(
             PropTypes.shape({
@@ -133,6 +134,7 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
         motion: true,
         leafOnly: false,
         showFilteredOnly: false,
+        showLine: false,
         expandAction: false,
         disableStrictly: false,
         draggable: false,
@@ -661,11 +663,11 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
         if (!treeNodeProps) {
             return null;
         }
-        const { keyMaps } = this.props;
-        const props: any = pick(treeNode, ['key', 'label', 'disabled', 'isLeaf', 'icon']);
+        const { keyMaps, showLine } = this.props;
+        const props: any = pick(treeNode, ['key', 'label', 'disabled', 'isLeaf', 'icon', 'isEnd']);
         const children = data[get(keyMaps, 'children', 'children')];
         !isUndefined(children) && (props.children = children);
-        return <TreeNode {...treeNodeProps} {...data} {...props} data={data} style={isEmpty(style) ? {} : style} />;
+        return <TreeNode {...treeNodeProps} {...data} {...props} showLine={showLine} data={data} style={isEmpty(style) ? {} : style} />;
     };
 
     itemKey = (index: number, data: KeyEntity) => {
@@ -729,7 +731,7 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
             filteredKeys,
             dragOverNodeKey,
             dropPosition,
-            checkedKeys, 
+            checkedKeys,
             realCheckedKeys,
         } = this.state;
 
@@ -743,6 +745,7 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
             directory,
             multiple,
             showFilteredOnly,
+            showLine,
             motion,
             expandAction,
             loadData,
@@ -807,10 +810,10 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
                 <div aria-label={this.props['aria-label']} className={wrapperCls} style={style} {...this.getDataAttr(rest)}>
                     {filterTreeNode ? this.renderInput() : null}
                     <div className={listCls} {...ariaAttr}>
-                        {noData ? this.renderEmpty() : (multiple ? 
+                        {noData ? this.renderEmpty() : (multiple ?
                             (<CheckboxGroup value={Array.from(checkRelation === 'related' ? checkedKeys : realCheckedKeys)}>
                                 {this.renderNodeList()}
-                            </CheckboxGroup>) : 
+                            </CheckboxGroup>) :
                             this.renderNodeList()
                         )}
                     </div>

+ 6 - 4
packages/semi-ui/tree/interface.ts

@@ -108,16 +108,17 @@ export interface TreeState extends BasicTreeInnerData {
 }
 
 /* TreeNode */
-export interface TreeNodeProps extends BasicTreeNodeProps{
+export interface TreeNodeProps extends BasicTreeNodeProps {
     children?: TreeNodeData[];
-    icon?: ReactNode
+    icon?: ReactNode;
+    isEnd?: boolean[]
 }
 export interface TreeNodeState {
     [x: string]: any
 }
 
 /* NodeList */
-export interface TreeNodeData extends BasicTreeNodeData{
+export interface TreeNodeData extends BasicTreeNodeData {
     label?: ReactNode;
     icon?: ReactNode;
     children?: TreeNodeData[]
@@ -126,7 +127,8 @@ export interface FlattenNode extends BasicFlattenNode {
     children?: FlattenNode[];
     data?: BasicTreeNodeData;
     label?: ReactNode;
-    parent?: null | FlattenNode
+    parent?: null | FlattenNode;
+    isEnd?: boolean[]
 }
 export interface NodeListProps {
     [x: string]: any;

+ 31 - 7
packages/semi-ui/tree/treeNode.tsx

@@ -10,6 +10,7 @@ import TreeContext, { TreeContextValue } from './treeContext';
 import Spin from '../spin';
 import { TreeNodeProps, TreeNodeState } from './interface';
 import { getHighLightTextHTML } from '../_utils/index';
+import Indent from './indent';
 
 const prefixcls = cssClasses.PREFIX_OPTION;
 
@@ -33,7 +34,9 @@ export default class TreeNode extends PureComponent<TreeNodeProps, TreeNodeState
         keyword: PropTypes.string,
         treeNodeFilterProp: PropTypes.string,
         selectedKey: PropTypes.string,
-        motionKey: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)])
+        motionKey: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
+        isEnd: PropTypes.arrayOf(PropTypes.bool),
+        showLine: PropTypes.bool
     };
 
     static defaultProps = {
@@ -200,7 +203,7 @@ export default class TreeNode extends PureComponent<TreeNodeProps, TreeNodeState
 
     renderArrow() {
         const showIcon = !this.isLeaf();
-        const { loading, expanded } = this.props;
+        const { loading, expanded, showLine } = this.props;
         if (loading) {
             return <Spin wrapperClassName={`${prefixcls}-spin-icon`} />;
         }
@@ -208,13 +211,16 @@ export default class TreeNode extends PureComponent<TreeNodeProps, TreeNodeState
             return (
                 <IconTreeTriangleDown
                     role='button'
-                    aria-label={`${expanded ? 'Expand' : 'Collapse'} the tree item`} 
+                    aria-label={`${expanded ? 'Expand' : 'Collapse'} the tree item`}
                     className={`${prefixcls}-expand-icon`}
                     size="small"
                     onClick={this.onExpand}
                 />
             );
         }
+        if (showLine) {
+            return this.renderSwitcher();
+        }
         return (
             <span className={`${prefixcls}-empty-icon`} />
         );
@@ -226,7 +232,7 @@ export default class TreeNode extends PureComponent<TreeNodeProps, TreeNodeState
         return (
             <div
                 role='none'
-                onClick={this.onCheck} 
+                onClick={this.onCheck}
                 onKeyPress={this.handleCheckEnterPress}
             >
                 <Checkbox
@@ -240,6 +246,18 @@ export default class TreeNode extends PureComponent<TreeNodeProps, TreeNodeState
         );
     }
 
+    // Switcher
+    renderSwitcher = () => {
+        if (this.isLeaf()) {
+            // if switcherIconDom is null, no render switcher span
+            return (<span className={cls(`${prefixcls}-switcher`)} >
+                <span className={`${prefixcls}-switcher-leaf-line`} />
+            </span>);
+
+        }
+        return null;
+    };
+
     renderIcon() {
         const {
             directory,
@@ -315,6 +333,8 @@ export default class TreeNode extends PureComponent<TreeNodeProps, TreeNodeState
             treeNodeFilterProp,
             display,
             style,
+            isEnd,
+            showLine,
             ...rest
         } = this.props;
         if (empty) {
@@ -328,6 +348,7 @@ export default class TreeNode extends PureComponent<TreeNodeProps, TreeNodeState
             dropPosition,
             labelEllipsis
         } = this.context;
+        const isEndNode = isEnd[isEnd.length - 1];
         const disabled = this.isDisabled();
         const dragOver = dragOverNodeKey === eventKey && dropPosition === 0;
         const dragOverGapTop = dragOverNodeKey === eventKey && dropPosition === -1;
@@ -346,6 +367,7 @@ export default class TreeNode extends PureComponent<TreeNodeProps, TreeNodeState
             // When draggable + renderFullLabel is turned on, the style of dragover
             [`${prefixcls}-fullLabel-drag-over-gap-top`]: !disabled && dragOverGapTop && renderFullLabel,
             [`${prefixcls}-fullLabel-drag-over-gap-bottom`]: !disabled && dragOverGapBottom && renderFullLabel,
+            [`${prefixcls}-tree-node-last-leaf`]: isEndNode,
         });
         const labelProps = {
             onClick: this.onClick,
@@ -406,18 +428,18 @@ export default class TreeNode extends PureComponent<TreeNodeProps, TreeNodeState
             [`${prefixcls}-drag-over-gap-bottom`]: !disabled && dragOverGapBottom,
         });
         const setsize = get(rest, ['data', 'children', 'length']);
-        const posinset = isString(rest.pos) ? Number(rest.pos.split('-')[level+1]) + 1 : 1;
+        const posinset = isString(rest.pos) ? Number(rest.pos.split('-')[level + 1]) + 1 : 1;
         return (
             <li
                 className={nodeCls}
                 role="treeitem"
-                aria-disabled={disabled} 
+                aria-disabled={disabled}
                 aria-checked={checked}
                 aria-selected={selected}
                 aria-setsize={setsize}
                 aria-posinset={posinset}
                 aria-expanded={expanded}
-                aria-level={level+1}
+                aria-level={level + 1}
                 data-key={eventKey}
                 onClick={this.onClick}
                 onKeyPress={this.handleliEnterPress}
@@ -427,10 +449,12 @@ export default class TreeNode extends PureComponent<TreeNodeProps, TreeNodeState
                 style={style}
                 {...dragProps}
             >
+                <Indent showLine={showLine} prefixcls={prefixcls} level={level} isEnd={isEnd} />
                 {this.renderArrow()}
                 <span
                     className={labelCls}
                 >
+
                     {multiple ? this.renderCheckbox() : null}
                     {this.renderIcon()}
                     <span className={`${prefixcls}-label-text`}>{this.renderRealLabel()}</span>

+ 6 - 3
packages/semi-ui/treeSelect/index.tsx

@@ -94,7 +94,8 @@ export type OverrideCommonProps =
     | 'treeData'
     | 'value'
     | 'onExpand'
-    | 'keyMaps';
+    | 'keyMaps'
+    | 'showLine';
 
 /**
 * Type definition description:
@@ -215,6 +216,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
         showSearchClear: PropTypes.bool,
         autoAdjustOverflow: PropTypes.bool,
         showFilteredOnly: PropTypes.bool,
+        showLine: PropTypes.bool,
         motionExpand: PropTypes.bool,
         emptyContent: PropTypes.node,
         keyMaps: PropTypes.object,
@@ -1315,14 +1317,15 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
     renderTreeNode = (treeNode: FlattenNode, ind: number, style: React.CSSProperties) => {
         const { data, key } = treeNode;
         const treeNodeProps = this.foundation.getTreeNodeProps(key);
+        const { showLine } = this.props;
         if (!treeNodeProps) {
             return null;
         }
-        const props: any = pick(treeNode, ['key', 'label', 'disabled', 'isLeaf', 'icon']);
+        const props: any = pick(treeNode, ['key', 'label', 'disabled', 'isLeaf', 'icon', 'isEnd']);
         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} />;
+        return <TreeNode {...treeNodeProps} {...data} {...props} data={data} style={style} showLine={showLine}/>;
     };
 
     itemKey = (index: number, data: Record<string, any>) => {

+ 0 - 86
yarn.lock

@@ -1519,25 +1519,11 @@
     "@douyinfe/semi-animation-styled" "2.23.2"
     classnames "^2.2.6"
 
-"@douyinfe/[email protected]":
-  version "2.49.1"
-  resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-react/-/semi-animation-react-2.49.1.tgz#cbffd1ba40f7b50aba7b191ddc5bdb323758a577"
-  integrity sha512-JTXgM3oRJpwhLDBlADf2/fClY73nE5/Ao701Z8bTOzH91PINbvgsD6cdZdZ4JRwd+XCSf6wXSGzaHen73r2gKg==
-  dependencies:
-    "@douyinfe/semi-animation" "2.49.1"
-    "@douyinfe/semi-animation-styled" "2.49.1"
-    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.49.1"
-  resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-styled/-/semi-animation-styled-2.49.1.tgz#34aed59a16d495ac7b5614438652352f17f20a81"
-  integrity sha512-cbGmEgibydvFCYvU8PqBrW2Jsft7j5T0iVZ/Yl95oPQpni8TevOuIzxAxiuSF5iLfeHKRNx0goIdRTzolH6kLA==
-
 "@douyinfe/[email protected]":
   version "2.12.0"
   resolved "https://registry.npmjs.org/@douyinfe/semi-animation/-/semi-animation-2.12.0.tgz#51fe52d3911c2591a80a6e9fe96e6809c1511f13"
@@ -1553,13 +1539,6 @@
   dependencies:
     bezier-easing "^2.1.0"
 
-"@douyinfe/[email protected]":
-  version "2.49.1"
-  resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation/-/semi-animation-2.49.1.tgz#24a9a5d881e6b7b71d05240efb05617c38e71655"
-  integrity sha512-sC/J7J5+Yd5y8CLWqD76wkjni9pjYOVBymvAZRrY7vd5kUQmOiNvFWK4ewEFxm4Pk2rdPRLQKGiPfkfHAej0dQ==
-  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"
@@ -1574,20 +1553,6 @@
     memoize-one "^5.2.1"
     scroll-into-view-if-needed "^2.2.24"
 
-"@douyinfe/[email protected]":
-  version "2.49.1"
-  resolved "https://registry.yarnpkg.com/@douyinfe/semi-foundation/-/semi-foundation-2.49.1.tgz#336ee534ec954072f8b79960666b55bfcf21aa99"
-  integrity sha512-pifLEWOhmv3/XYscmPPLMaUcvRaG+lZPVMD6uOXFhUrD+HUBs7yfIMLfXyJ6Af7Kcl5ecqOgO9NNTyRR+pnxuQ==
-  dependencies:
-    "@douyinfe/semi-animation" "2.49.1"
-    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"
@@ -1595,23 +1560,11 @@
   dependencies:
     classnames "^2.2.6"
 
-"@douyinfe/[email protected]", "@douyinfe/semi-icons@^2.0.0":
-  version "2.49.1"
-  resolved "https://registry.yarnpkg.com/@douyinfe/semi-icons/-/semi-icons-2.49.1.tgz#1a33ea0ae40961931b8cbedeac11ed5d87e245f5"
-  integrity sha512-d2YdR0Z3pqygWNdGB7M97cqi0dec+mWHcLL2C4Yd9I0VJCPWnNd88dWVPTNPU2GCx9hEA8qL9aFawUR24s5p1Q==
-  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.49.1"
-  resolved "https://registry.yarnpkg.com/@douyinfe/semi-illustrations/-/semi-illustrations-2.49.1.tgz#6099aabde44d59f481e2b67504f6b67f56da1782"
-  integrity sha512-zJ71DrsGNvTpQwv2itQe7SkMyBMmWvlTGYoAUD4mDVaxAVPlUGgl3Fc5zVG9BHXIizHrb3Fi5PVFC5g/dFXn0A==
-
 "@douyinfe/[email protected]":
   version "2.23.2"
   resolved "https://registry.npmjs.org/@douyinfe/semi-scss-compile/-/semi-scss-compile-2.23.2.tgz#30884bb194ee9ae1e81877985e5663c3297c1ced"
@@ -1685,40 +1638,6 @@
   dependencies:
     glob "^7.1.6"
 
-"@douyinfe/[email protected]":
-  version "2.49.1"
-  resolved "https://registry.yarnpkg.com/@douyinfe/semi-theme-default/-/semi-theme-default-2.49.1.tgz#cacb83f5df53beddafd696103b4f139ceb115c26"
-  integrity sha512-Wmb6W9rAjVzLemRHEdQAgO2m0002SBsc7edA6lb7tgoDNs2IofD/OrPmNyFV8PbDrVQxXlJrkNgpS3rzruk7+w==
-  dependencies:
-    glob "^7.1.6"
-
-"@douyinfe/semi-ui@^2.0.0":
-  version "2.49.1"
-  resolved "https://registry.yarnpkg.com/@douyinfe/semi-ui/-/semi-ui-2.49.1.tgz#f4151fe576da586892f1bf42c92c308c76465242"
-  integrity sha512-4oqyAyezrdKNpFSJWCQChcRQj8pApZfHuTTILoVO0SEMAOrygvsf2Zd0LzHnO6H9oTC7tV3eny0d4E4BYz6kLw==
-  dependencies:
-    "@dnd-kit/core" "^6.0.8"
-    "@dnd-kit/sortable" "^7.0.2"
-    "@dnd-kit/utilities" "^3.2.1"
-    "@douyinfe/semi-animation" "2.49.1"
-    "@douyinfe/semi-animation-react" "2.49.1"
-    "@douyinfe/semi-foundation" "2.49.1"
-    "@douyinfe/semi-icons" "2.49.1"
-    "@douyinfe/semi-illustrations" "2.49.1"
-    "@douyinfe/semi-theme-default" "2.49.1"
-    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"
@@ -11665,11 +11584,6 @@ 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.49.1"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-semi-design/-/eslint-plugin-semi-design-2.49.1.tgz#aefed7fe36c6d673cd4837ec30843089f880eb7f"
-  integrity sha512-55UMmBZIYGVC/NN++rWTp1xWXXEgWkL1HN7GzzW2sdxKViRMQXLpRJKtirWbUeYoKcKdBPf6RH/OHZlnc97PDg==
-
 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"