Browse Source

Merge branch 'main' of github.com:DouyinFE/semi-design

point.halo 8 months ago
parent
commit
e14f6c69c9
100 changed files with 3379 additions and 471 deletions
  1. 1 0
      .gitignore
  2. 20 2
      .storybook/base/base.js
  3. 1 1
      content/feedback/banner/index-en-US.md
  4. 1 1
      content/feedback/banner/index.md
  5. 1 1
      content/feedback/notification/index-en-US.md
  6. 1 1
      content/feedback/notification/index.md
  7. 1 1
      content/feedback/popconfirm/index-en-US.md
  8. 1 1
      content/feedback/popconfirm/index.md
  9. 1 1
      content/feedback/progress/index-en-US.md
  10. 1 1
      content/feedback/progress/index.md
  11. 1 1
      content/feedback/skeleton/index-en-US.md
  12. 1 1
      content/feedback/skeleton/index.md
  13. 1 1
      content/feedback/spin/index-en-US.md
  14. 1 1
      content/feedback/spin/index.md
  15. 1 1
      content/feedback/toast/index-en-US.md
  16. 1 1
      content/feedback/toast/index.md
  17. 2 1
      content/input/treeselect/index-en-US.md
  18. 2 1
      content/input/treeselect/index.md
  19. 1 0
      content/input/upload/index-en-US.md
  20. 1 0
      content/input/upload/index.md
  21. 85 1
      content/navigation/tree/index-en-US.md
  22. 88 1
      content/navigation/tree/index.md
  23. 2 0
      content/order.js
  24. 1 1
      content/other/configprovider/index-en-US.md
  25. 1 1
      content/other/configprovider/index.md
  26. 1 1
      content/other/locale/index-en-US.md
  27. 1 1
      content/other/locale/index.md
  28. 171 0
      content/plus/audioPlayer/index-en-US.md
  29. 178 0
      content/plus/audioPlayer/index.md
  30. 4 3
      content/plus/chat/index-en-US.md
  31. 2 1
      content/plus/chat/index.md
  32. 1 1
      content/plus/dragMove/index-en-US.md
  33. 1 1
      content/plus/dragMove/index.md
  34. 2 0
      content/plus/jsonviewer/index-en-US.md
  35. 2 0
      content/plus/jsonviewer/index.md
  36. 1 1
      content/show/chart/index-en-US.md
  37. 1 1
      content/show/chart/index.md
  38. 281 0
      content/show/cropper/index-en-US.md
  39. 286 0
      content/show/cropper/index.md
  40. 1 0
      content/show/dropdown/index-en-US.md
  41. 1 0
      content/show/dropdown/index.md
  42. 104 133
      content/show/list/index-en-US.md
  43. 101 131
      content/show/list/index.md
  44. 1 1
      content/show/modal/index-en-US.md
  45. 1 1
      content/show/modal/index.md
  46. 1 1
      content/show/overflowlist/index-en-US.md
  47. 1 1
      content/show/overflowlist/index.md
  48. 1 1
      content/show/popover/index-en-US.md
  49. 1 1
      content/show/popover/index.md
  50. 1 1
      content/show/scrolllist/index-en-US.md
  51. 1 1
      content/show/scrolllist/index.md
  52. 1 1
      content/show/sidesheet/index-en-US.md
  53. 1 1
      content/show/sidesheet/index.md
  54. 1 1
      content/show/table/index-en-US.md
  55. 1 1
      content/show/table/index.md
  56. 1 1
      content/show/tag/index-en-US.md
  57. 1 1
      content/show/tag/index.md
  58. 1 1
      content/show/timeline/index-en-US.md
  59. 1 1
      content/show/timeline/index.md
  60. 1 1
      content/show/tooltip/index-en-US.md
  61. 1 1
      content/show/tooltip/index.md
  62. 49 1
      content/start/changelog/index-en-US.md
  63. 50 1
      content/start/changelog/index.md
  64. 3 1
      content/start/overview/index-en-US.md
  65. 6 2
      content/start/overview/index.md
  66. 164 0
      cypress/e2e/jsonViewer.spec.js
  67. 18 3
      gatsby-node.js
  68. 1 1
      lerna.json
  69. 1 0
      package.json
  70. 3 3
      packages/semi-animation-react/package.json
  71. 1 1
      packages/semi-animation-styled/package.json
  72. 1 1
      packages/semi-animation/package.json
  73. 1 1
      packages/semi-eslint-plugin/package.json
  74. 217 0
      packages/semi-foundation/audioPlayer/audioPlayer.scss
  75. 7 0
      packages/semi-foundation/audioPlayer/constants.ts
  76. 103 0
      packages/semi-foundation/audioPlayer/foundation.ts
  77. 55 0
      packages/semi-foundation/audioPlayer/variables.scss
  78. 8 0
      packages/semi-foundation/button/iconButton.scss
  79. 26 0
      packages/semi-foundation/cropper/constants.ts
  80. 116 0
      packages/semi-foundation/cropper/cropper.scss
  81. 821 0
      packages/semi-foundation/cropper/foundation.ts
  82. 12 0
      packages/semi-foundation/cropper/utils.ts
  83. 6 0
      packages/semi-foundation/cropper/variables.scss
  84. 12 0
      packages/semi-foundation/dragMove/foundation.ts
  85. 6 0
      packages/semi-foundation/jsonViewer/foundation.ts
  86. 8 3
      packages/semi-foundation/jsonViewer/jsonViewer.scss
  87. 3 3
      packages/semi-foundation/package.json
  88. 2 2
      packages/semi-foundation/select/foundation.ts
  89. 1 1
      packages/semi-foundation/steps/bacisSteps.scss
  90. 3 0
      packages/semi-foundation/tree/treeUtil.ts
  91. 1 1
      packages/semi-icons-lab/package.json
  92. 1 1
      packages/semi-icons/package.json
  93. 1 1
      packages/semi-illustrations/package.json
  94. 54 54
      packages/semi-json-viewer-core/package.json
  95. 5 0
      packages/semi-json-viewer-core/script/compileLib.js
  96. 2 1
      packages/semi-json-viewer-core/src/common/emitterEvents.ts
  97. 45 0
      packages/semi-json-viewer-core/src/common/strings.ts
  98. 19 0
      packages/semi-json-viewer-core/src/common/utils.ts
  99. 1 0
      packages/semi-json-viewer-core/src/json-viewer/jsonViewer.ts
  100. 171 75
      packages/semi-json-viewer-core/src/model/foldingModel.ts

+ 1 - 0
.gitignore

@@ -19,6 +19,7 @@ dist
 build
 _site
 packages/**/lib
+packages/**/workerLib
 packages/semi-theme-default/css/semi.css
 packages/semi-theme-default/semi.scss
 public

+ 20 - 2
.storybook/base/base.js

@@ -3,7 +3,7 @@ const path = require('path');
 const _ = require('lodash');
 const chalk = require('chalk').default;
 const utils = require('./utils');
-
+const fs = require('fs');
 let AnalyzePlugin = null
 if(process.env.__ENABLE_ANALYZE__ === 'true') {
     AnalyzePlugin = require("@ies/semi-page-analyze-inject/src/AnalyzePlugin")
@@ -79,6 +79,23 @@ module.exports = {
                 },
             ]
         })
+        rules.push({
+            test: /jsonWorkerManager\.ts$/,
+            use: [
+                {
+                    loader: 'webpack-replace-loader',
+                    options: {
+                        search: '%WORKER_RAW%',
+                        replace: () => {
+                            const workFilePath = resolve('packages/semi-json-viewer-core/workerLib/worker.js');
+                            const result = fs.readFileSync(workFilePath, 'utf-8');
+                            const encodedResult = encodeURIComponent(result);
+                            return encodedResult;
+                        }
+                    }
+                }
+            ]
+        });
         config.module.rules = rules;
         config.resolve.extensions.push('.js', '.jsx', '.ts', '.tsx');
         config.resolve.symlinks = false;
@@ -92,7 +109,8 @@ module.exports = {
             '@douyinfe/semi-illustrations': resolve('packages/semi-illustrations/src'),
             '@douyinfe/semi-animation': resolve('packages/semi-animation'),
             '@douyinfe/semi-animation-react': resolve('packages/semi-animation-react'),
-            '@douyinfe/semi-animation-styled': resolve('packages/semi-animation-styled')
+            '@douyinfe/semi-animation-styled': resolve('packages/semi-animation-styled'),
+            '@douyinfe/semi-json-viewer-core': resolve('packages/semi-json-viewer-core/src'),
         };
         config.devtool = 'source-map';
         // config.output.publicPath = "/storybook/"

+ 1 - 1
content/feedback/banner/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 80
+order: 81
 category: Feedback
 title:  Banner
 subTitle: Banner

+ 1 - 1
content/feedback/banner/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 80
+order: 81
 category: 反馈类
 title:  Banner 通知横幅
 icon: doc-banner

+ 1 - 1
content/feedback/notification/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 81
+order: 82
 category: Feedback
 title:  Notification
 subTitle: Notification

+ 1 - 1
content/feedback/notification/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 81
+order: 82
 category: 反馈类
 title: Notification 通知
 icon: doc-notification

+ 1 - 1
content/feedback/popconfirm/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 82
+order: 83
 category: Feedback
 title:  Popconfirm
 subTitle: Popconfirm

+ 1 - 1
content/feedback/popconfirm/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 82
+order: 83
 category: 反馈类
 title:  Popconfirm 气泡确认框
 icon: doc-popconfirm

+ 1 - 1
content/feedback/progress/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 83
+order: 84
 category: Feedback
 title: Progress
 subTitle: Progress

+ 1 - 1
content/feedback/progress/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 83
+order: 84
 category: 反馈类
 title: Progress 进度条
 icon: doc-progress

+ 1 - 1
content/feedback/skeleton/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 84
+order: 85
 category: Feedback
 title: Skeleton
 subTitle: Skeleton

+ 1 - 1
content/feedback/skeleton/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 84
+order: 85
 category: 反馈类
 title: Skeleton 骨架屏
 icon: doc-skeleton

+ 1 - 1
content/feedback/spin/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 85
+order: 86
 category: Feedback
 title: Spin
 subTitle: Spin

+ 1 - 1
content/feedback/spin/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 85
+order: 86
 category: 反馈类
 title: Spin 加载器
 icon: doc-spin

+ 1 - 1
content/feedback/toast/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 86
+order: 87
 category: Feedback
 title: Toast
 subTitle: Toast

+ 1 - 1
content/feedback/toast/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 86
+order: 87
 category: 反馈类
 title: Toast 提示
 icon: doc-toast

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

@@ -1408,7 +1408,7 @@ function Demo() {
 | arrowIcon|Customize the right drop-down arrow Icon, when the showClear switch is turned on and there is currently a selected value, hover will give priority to the clear icon| ReactNode | | - |
 |autoAdjustOverflow|Whether the pop-up layer automatically adjusts the direction when it is obscured (only vertical direction is supported for the time being, and the inserted parent is body)|boolean | true| - |
 | autoExpandParent | Toggle whether to expand parent nodes automatically | boolean | false | - |
-| autoMergeValue | Sets the automerge value. Specifically, when enabled, when a parent node is selected, value will include that node and its children. (Works if leafOnly is false)| boolean | true | 2.61.0 | 
+| autoMergeValue | Sets the automerge value. Specifically, when enabled, when a parent node is selected, the value will not include the descendants of the node. (Works if leafOnly is false)| boolean | true | 2.61.0 | 
 | borderless        | borderless mode  >=2.33.0                                                                                                                                                                     | boolean                         |           |
 | checkRelation | In multiple, the relationship between the checked states of the nodes, optional: 'related'、'unRelated' | string | 'related' | 2.5.0 |
 | className                | Class name                                                                          | string                                                            | -           | -       |
@@ -1429,6 +1429,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 | - |
 | 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 |- |
 | expandedKeys        | (Controlled)Keys of expanded nodes. Direct child nodes will be displayed.  | string[]                    | -       | - |
+| expandIcon | Custom expand icon, [example](/en-US/navigation/tree#Custom%20expansion%20icon) | ReactNode \| (props: expandProps)=>ReactNode | - | 2.75.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\| <ApiType detail='(inputValue: string, treeNodeString: string, data?: TreeNodeData) => boolean'>Function</ApiType> | 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                                            | -           | -       |

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

@@ -1391,7 +1391,7 @@ function Demo() {
 | arrowIcon | 自定义右侧下拉箭头Icon,当showClear开关打开且当前有选中值时,hover会优先显示clear icon                                                            |  ReactNode |       | 
 | autoAdjustOverflow| 浮层被遮挡时是否自动调整方向(暂时仅支持竖直方向,且插入的父级为 body)                                                                    | boolean | true| 
 | autoExpandParent | 是否自动展开父节点                                                                                                                  | boolean | false | 
-| autoMergeValue | 设置自动合并 value。具体而言是,开启后,当某个父节点被选中时,value 将包括该节点以及该子孙节点。(在leafOnly为false的情况下生效)。v2.61.0 后提供     | boolean | true |
+| autoMergeValue | 设置自动合并 value。具体而言是,开启后,当某个父节点被选中时,value 将不包括该节点的子孙节点。(在leafOnly为false的情况下生效)。v2.61.0 后提供     | boolean | true |
 | borderless        | 无边框模式,v2.33.0后提供                                                                                                          | boolean | false |
 | checkRelation | 多选时,节点之间选中状态的关系,可选:'related'、'unRelated'。v2.5.0后提供                                                                  | string | 'related' |
 | className | 选择框的 `className` 属性                                                                                                                 | string | - | - |
@@ -1412,6 +1412,7 @@ function Demo() {
 | expandAction | 展开逻辑,可选 false, 'click', 'doubleClick'。默认值为 false,即仅当点击展开按钮时才会展开                                                        | boolean \| string | false |
 | expandAll | 设置是否默认展开所有节点,若后续数据(`treeData`)发生改变,默认的展开情况也是会受到这个 api 影响的                                                         | boolean | false |
 | expandedKeys | (受控)展开的节点,默认展开节点显示其直接子级                                                                                                   | string[] | - |
+| expandIcon | 自定义展开图标,使用[示例](/zh-CN/navigation/tree#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%B1%95%E5%BC%80%20Icon) | ReactNode \| (props: expandProps)=>ReactNode | - | 2.75.0 |
 | keyMaps | 自定义节点中 key、label、value 的字段。v2.47.0后提供                                                                                                | object |  - |
 | 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 | - |

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

@@ -1396,6 +1396,7 @@ import { IconUpload } from '@douyinfe/semi-icons';
 |promptPosition | The position of the prompt text. When the listType is list, the reference object is the children element; when the listType is picture, the reference object is the picture list. Optional values ​​`left`, `right`, `bottom`<br/> (In picture wall mode, promptPosition is only supported after v1.3.0) | string |'right' | |
 |renderFileItem | Custom rendering of fileCard | (renderProps: RenderFileItemProps) => ReactNode | | 1.0.0 |
 |renderFileOperation | Custom list item operation area | (renderProps: RenderFileItemProps)=>ReactNode | | 2.5.0 |
+|renderPicClose| Customize the photo wall close button, only valid in photo wall mode| ({className: string, remove: (e: MouseEvent) => void})=>ReactNode | | 2.75.0 |
 |renderPicInfo| Custom photo wall information, only valid in photo wall mode| (renderProps: RenderFileItemProps)=>ReactNode | | 2.2.0 |
 |renderPicPreviewIcon| The preview icon displayed when customizing the photo wall hover, only valid in photo wall mode | (renderProps: RenderFileItemProps)=>ReactNode | | 2.5.0 |
 |renderThumbnail| Custom picture wall thumb, only valid in photo wall mode| (renderProps: RenderFileItemProps)=>ReactNode | | 2.2.0 |

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

@@ -1405,6 +1405,7 @@ import { IconUpload } from '@douyinfe/semi-icons';
 |promptPosition | 提示文本的位置,当 listType 为 list 时,参照物为 children 元素;当 listType 为 picture 时,参照物为图片列表。可选值 `left`、`right`、`bottom`<br/>(图片墙模式下,v1.3.0 后才支持使用 promptPosition) | string | 'right' |  |
 |renderFileItem | fileCard 的自定义渲染 | (renderProps: RenderFileItemProps) => ReactNode |  |  |
 |renderFileOperation | 自定义列表项操作区 | (renderProps: RenderFileItemProps)=>ReactNode | | 2.5.0 |
+|renderPicClose| 自定义照片墙 close 按钮,只在照片墙模式下有效| ({className: string, remove: (e: MouseEvent) => void})=>ReactNode | | 2.75.0 |
 |renderPicInfo| 自定义照片墙信息,只在照片墙模式下有效| (renderProps: RenderFileItemProps)=>ReactNode | | 2.2.0 |
 |renderPicPreviewIcon| 自定义照片墙hover时展示的预览图标,只在照片墙模式下有效 | (renderProps: RenderFileItemProps)=>ReactNode | | 2.5.0 |
 |renderThumbnail| 自定义图片墙缩略图,只在照片墙模式下有效| (renderProps: RenderFileItemProps)=>ReactNode | | 2.2.0 |

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

@@ -1213,6 +1213,89 @@ class Demo extends React.Component {
 }
 ```
 
+### Custom expansion icon
+
+The expansion Icon can be customized through `expandIcon`. Supports passing in ReactNode or functions. `expandIcon` is supported since 2.75.0.
+
+```ts
+expandIcon: ReactNode | ((props: {
+    onClick: (e: MouseEvent) => void;
+    className: string;
+    expanded: boolean;
+}))
+```
+
+Examples are as follows:
+
+```jsx live=true 
+() => {
+   const treeData = [
+            {
+                label: 'Asia',
+                key: 'Asia',
+                children: [
+                    {
+                        label: 'China',
+                        key: 'China',
+                        children: [
+                            {
+                                label: 'Beijing',
+                                key: 'Beijing',
+                            },
+                            {
+                                label: 'Shanghai',
+                                key: 'Shanghai',
+                            },
+                        ],
+                    },
+                ],
+            },
+            {
+                label: 'North America',
+                value: 'North America',
+            }
+        ];
+    const expandIconFunc = useCallback((props) => {
+        const { expanded, onClick, className } = props;
+        if (expanded) {
+        return <IconMinus size="small" className={className} onClick={onClick}/>
+        } else {
+        return <IconPlus size="small" className={className} onClick={onClick}/>
+        }
+    });
+    const style = {
+        width: 260,
+        height: 200,
+        border: '1px solid var(--semi-color-border)'
+    };
+
+  return (
+    <>
+      <p>ReactNode type</p>
+      <Tree
+        style={{ width: 300}}
+        expandIcon={<IconChevronDown size="small" className='testCls'/>}
+        multiple
+        defaultExpandedKeys={['Asia']}
+        treeData={treeData}
+        style={style}
+      />
+      <br />
+      <p>Function type</p>
+      <Tree
+        style={{ width: 300}}
+        multiple
+        expandIcon={expandIconFunc}
+        defaultExpandedKeys={['Asia']}
+        treeData={treeData}
+        style={style}
+      />
+    </>
+  );
+}
+```
+
+
 ### Tree with line
 
 Set the line between nodes through `showLine`, the default is false, supported starting from 2.50.0
@@ -2271,7 +2354,7 @@ import { IconFixedStroked, IconSectionStroked, IconAbsoluteStroked, IconInnerSec
 | ------------------- | --------------------- | ------------------------------------------------- | ------- | ------ |
 | autoExpandParent | Toggle whether to expand parent node automatically | boolean | false | 0.34.0 |
 | autoExpandWhenDragEnter | Toggle whether allow autoExpand when drag enter node | boolean | true | 1.8.0 | 
-| autoMergeValue | Sets the automerge value. Specifically, when enabled, when a parent node is selected, value will include that node and its children. (Works if leafOnly is false)| boolean | true | 2.61.0 | 
+| autoMergeValue | Sets the automerge value. Specifically, when enabled, when a parent node is selected, the value will not include the descendants of the node. (Works if leafOnly is false)| boolean | true | 2.61.0 | 
 | blockNode           | Toggle whether to display node as row     | boolean                     | true    | - |
 | checkRelation | In multiple, the relationship between the checked states of the nodes, optional: 'related'、'unRelated' | string | 'related' | 2.5.0 |
 | className           | Class name| string                      | -       | - |
@@ -2286,6 +2369,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[]                    | -       | - |
+| expandIcon | Custom expand icon | ReactNode \| (props: expandProps)=>ReactNode | - | 2.75.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: string, data?: TreeNodeData) => boolean)  | false   | - |
 | hideDraggingNode | Toggle whether to hide dragImg of dragging node | boolean | false | 1.8.0 | 

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

@@ -1241,6 +1241,92 @@ class Demo extends React.Component {
 }
 ```
 
+### 自定义展开 Icon
+
+可以通过 `expandIcon` 自定义展开 Icon。 支持传入 ReactNode 或者函数。`expandIcon` 自 2.75.0 开始支持。
+
+```ts
+expandIcon: ReactNode | ((props: {
+    onClick: (e: MouseEvent) => void;
+    className: string;
+    expanded: boolean;
+}))
+```
+
+示例如下:
+
+```jsx live=true 
+() => {
+    const treeData = [
+        {
+            label: '亚洲',
+            key: 'yazhou',
+            children: [
+                {
+                    label: '中国',
+                    key: 'zhongguo',
+                    children: [
+                        {
+                            label: '北京',
+                            key: 'beijing',
+                        },
+                        {
+                            label: '上海',
+                            key: 'shanghai',
+                        },
+                    ],
+                },
+                {
+                    label: '日本',
+                    key: 'riben',
+                },
+            ],
+        },
+        {
+            label: '北美洲',
+            key: 'beimeizhou',
+        },
+    ];
+    const expandIconFunc = useCallback((props) => {
+        const { expanded, onClick, className } = props;
+        if (expanded) {
+        return <IconMinus size="small" className={className} onClick={onClick}/>
+        } else {
+        return <IconPlus size="small" className={className} onClick={onClick}/>
+        }
+    });
+    const style = {
+        width: 260,
+        height: 200,
+        border: '1px solid var(--semi-color-border)'
+    };
+
+  return (
+    <>
+      <p>expandIcon 是  ReactNode</p>
+      <Tree
+        style={{ width: 300}}
+        expandIcon={<IconChevronDown size="small" className='testCls'/>}
+        multiple
+        defaultExpandedKeys={['yazhou']}
+        treeData={treeData}
+        style={style}
+      />
+      <br />
+      <p>expandIcon 是函数 </p>
+      <Tree
+        style={{ width: 300}}
+        multiple
+        expandIcon={expandIconFunc}
+        defaultExpandedKeys={['yazhou']}
+        treeData={treeData}
+        style={style}
+      />
+    </>
+  );
+}
+```
+
 ### 连接线
 
 通过 `showLine` 设置节点之间的连接线,默认为 false,从 2.50.0 开始支持
@@ -2286,7 +2372,7 @@ import { IconFixedStroked, IconSectionStroked, IconAbsoluteStroked, IconInnerSec
 |-------------   | ----------- | -------------- | -------------- | --------|
 | autoExpandParent | 是否自动展开父节点,默认为 false,当组件初次挂载时为 true | boolean | false | 0.34.0 |
 | autoExpandWhenDragEnter | 是否允许拖拽到节点上时自动展开改节点 | boolean | true | 1.8.0 | 
-| autoMergeValue | 设置自动合并 value。具体而言是,开启后,当某个父节点被选中时,value 将包括该节点以及该子孙节点。(在leafOnly为false的情况下生效)| boolean | true | 2.61.0 | 
+| autoMergeValue | 设置自动合并 value。具体而言是,开启后,当某个父节点被选中时,value 将不包括该节点的子孙节点。(在leafOnly为false的情况下生效)| boolean | true | 2.61.0 | 
 | blockNode | 行显示节点 | boolean | true | - |
 | checkRelation | 多选时,节点之间选中状态的关系,可选:'related'、'unRelated' | string | 'related' | 2.5.0 |
 | className | 类名 | string | - | - |
@@ -2301,6 +2387,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[] | - | - |
+| expandIcon | 自定义展开图标 | ReactNode \| (props: expandProps)=>ReactNode | - | 2.75.0 |
 | 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 | 

+ 2 - 0
content/order.js

@@ -67,6 +67,7 @@ const order = [
     'empty',
     'highlight',
     'image',
+    'cropper',
     'list',
     'modal',
     'overflowlist',
@@ -88,6 +89,7 @@ const order = [
     'configprovider',
     'locale',
     'jsonviewer',
+    'audioPlayer',
 ];
 let { exec } = require('child_process');
 let fs = require('fs');

+ 1 - 1
content/other/configprovider/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 87
+order: 88
 category: Other
 title: ConfigProvider
 icon: doc-configprovider

+ 1 - 1
content/other/configprovider/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 87
+order: 88
 category: 其他
 title:  ConfigProvider 全局配置
 icon: doc-configprovider

+ 1 - 1
content/other/locale/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 88
+order: 89
 category: Other
 title: LocaleProvider
 subTitle: LocaleProvider

+ 1 - 1
content/other/locale/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 88
+order: 89
 category: 其他
 title:  LocaleProvider 多语言
 icon: doc-i18n

+ 171 - 0
content/plus/audioPlayer/index-en-US.md

@@ -0,0 +1,171 @@
+---
+localeCode: en-US
+order: 91
+category: Plus
+title: AudioPlayer
+icon: doc-audioplayer
+width: 60%
+brief: Used to play audio
+showNew: true
+---
+
+## Demos
+
+### How to import
+
+```jsx import
+import { AudioPlayer } from '@douyinfe/semi-ui';
+```
+
+### Basic Usage
+
+Basic usage by passing audio URL through `audioUrl`.  
+audioUrl supports string, string array, object, and object array. See [AudioPlayer](#AudioPlayer) for detailed parameters.
+
+```jsx live=true noInline=true dir="column"
+import React from 'react';
+import { AudioPlayer } from '@douyinfe/semi-ui';
+
+function Demo() {
+    const audioUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3';
+    const audioUrlArr = [
+        'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3',
+        'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3',
+    ];
+    const audioUrlObj = {
+        src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3',
+        title: 'Audio Title',
+        cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg',
+    };
+    const audioUrlArrObj = [
+        {
+            src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3',
+            title: 'Audio Title 1',
+            cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg',
+        },
+        {
+            src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3',
+            title: 'Audio Title 2',
+            cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg',
+        },
+    ];
+  
+    return (
+        <div style={{ width: '100%' }}>
+            <div style={{ marginTop: 10 }}>
+                <AudioPlayer
+                    autoPlay={false}
+                    audioUrl={audioUrl}
+                />
+            </div>
+            <div style={{ marginTop: 10 }}>
+                <AudioPlayer
+                    autoPlay={false}
+                    audioUrl={audioUrlObj}
+                />
+            </div>
+            <div style={{ marginTop: 10 }}>
+                <AudioPlayer
+                    autoPlay={false}
+                    audioUrl={audioUrlArr}
+                />
+            </div>
+            <div style={{ marginTop: 10 }}>
+                <AudioPlayer
+                    autoPlay={false}
+                    audioUrl={audioUrlArrObj}
+                />
+            </div>
+        </div>
+    );
+}
+
+render(Demo);
+```
+
+### Hide Toolbar
+
+Set showToolbar to false to hide the toolbar
+
+```jsx live=true noInline=true dir="column"
+import React from 'react';
+import { AudioPlayer } from '@douyinfe/semi-ui';
+
+function Demo() {
+    const audioUrlObj = {
+        src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3',
+        title: 'Audio Title'
+    };
+  
+    return (
+        <div style={{ width: '100%' }}>
+            <AudioPlayer
+                autoPlay={false}
+                audioUrl={audioUrlObj}
+                showToolbar={false}
+            />
+        </div>
+    );
+}
+
+render(Demo);
+```
+
+### Theme
+
+Set the audio player theme through `theme`, supports `light` and `dark`, default is `dark`
+
+```jsx live=true noInline=true dir="column"
+import React from 'react';
+import { AudioPlayer } from '@douyinfe/semi-ui';
+
+function Demo() {
+    const audioUrlArrObj = [
+        {
+            src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3',
+            title: 'Audio Title 1',
+            cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg',
+        },
+        {
+            src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3',
+            title: 'Audio Title 2',
+            cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg',
+        },
+    ];
+  
+    return (
+        <div style={{ width: '100%' }}>
+            <AudioPlayer
+                audioUrl={audioUrlArrObj}
+                theme="light"
+            />
+        </div>
+    );
+}
+
+render(Demo);
+```
+
+## API Reference
+
+### AudioPlayer
+
+| Property | Description | Type | Default |
+|----------|-------------|------|---------|
+| audioUrl | Audio address | string | string[] | AudioInfo | AudioInfo[] | - |
+| autoPlay | Auto play | boolean | false |
+| theme | Theme, optional values: `dark` and `light` | string | "dark" |
+| showToolbar | Whether to display the toolbar | boolean | true |
+| skipDuration | Skip time | number | 10 |
+| className | Class name | string | - |
+| style | Inline style | object | - |
+
+### AudioInfo
+
+| Property | Description | Type | Default |
+|----------|-------------|------|---------|
+| src | Audio address | string | - |
+| title | Audio title | string | - |
+| cover | Cover image | string | - |
+
+

+ 178 - 0
content/plus/audioPlayer/index.md

@@ -0,0 +1,178 @@
+---
+localeCode: zh-CN
+order: 91
+category: Plus
+title: AudioPlayer 音频播放器
+icon: doc-audioplayer
+width: 60%
+brief: 用于播放音频
+showNew: true
+---
+
+## 代码演示
+
+### 如何引入
+
+```jsx import
+import { AudioPlayer } from '@douyinfe/semi-ui';
+```
+
+
+### 基本用法
+
+基本使用,通过`audioUrl`传入音频地址  
+audioUrl 可以传入字符串,字符串数组,对象,对象数组, 具体参数参考 [AudioPlayer](#AudioPlayer)
+
+```jsx live=true noInline=true dir="column"
+import React from 'react';
+import { AudioPlayer } from '@douyinfe/semi-ui';
+
+function Demo() {
+    const audioUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3';
+    const audioUrlArr = [
+        'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3',
+        'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3',
+    ];
+    const audioUrlObj = {
+        src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3',
+        title: '音频标题',
+        cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg',
+    };
+    const audioUrlArrObj = [
+        {
+            src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3',
+            title: '音频标题1',
+            cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg',
+        },
+        {
+            src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3',
+            title: '音频标题2',
+            cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg',
+        },
+    ];
+  
+    return (
+        <div style={{ width: '100%' }}>
+            <div style={{ marginTop: 10 }}>
+                <AudioPlayer
+                    autoPlay={false}
+                    audioUrl={audioUrl}
+                />
+            </div>
+            <div style={{ marginTop: 10 }}>
+                <AudioPlayer
+                    autoPlay={false}
+                    audioUrl={audioUrlObj}
+                />
+            </div>
+            <div style={{ marginTop: 10 }}>
+                <AudioPlayer
+                    autoPlay={false}
+                    audioUrl={audioUrlArr}
+                />
+            </div>
+            <div style={{ marginTop: 10 }}>
+                <AudioPlayer
+                    autoPlay={false}
+                    audioUrl={audioUrlArrObj}
+                />
+            </div>
+        </div>
+    );
+}
+
+render(Demo);
+
+```
+
+
+### 隐藏工具栏
+
+showToolbar 设置为false,则隐藏工具栏
+
+
+```jsx live=true noInline=true dir="column"
+import React from 'react';
+import { AudioPlayer } from '@douyinfe/semi-ui';
+
+function Demo() {
+    const audioUrlObj = {
+        src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3',
+        title: '音频标题'
+    };
+  
+    return (
+        <div style={{ width: '100%' }}>
+            <AudioPlayer
+                autoPlay={false}
+                audioUrl={audioUrlObj}
+                showToolbar={false}
+            />
+        </div>
+    );
+}
+
+render(Demo);
+
+```
+
+### 主题
+
+通过 `theme` 设置音频播放器主题,支持 `light` 和 `dark`,默认 `dark`
+
+
+```jsx live=true noInline=true dir="column"
+import React from 'react';
+import { AudioPlayer } from '@douyinfe/semi-ui';
+
+function Demo() {
+    const audioUrlArrObj = [
+        {
+            src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3',
+            title: '音频标题1',
+            cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg',
+        },
+        {
+            src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3',
+            title: '音频标题2',
+            cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg',
+        },
+    ];
+  
+    return (
+        <div style={{ width: '100%' }}>
+            <AudioPlayer
+                audioUrl={audioUrlArrObj}
+                theme="light"
+            />
+        </div>
+    );
+}
+
+render(Demo);
+
+```
+
+## API 参考
+
+### AudioPlayer
+
+| 属性                | 说明                                             | 类型                              | 默认值    |
+|-------------------|------------------------------------------------|---------------------------------|--------------|
+| audioUrl             | 音频地址                                    | string | string[] | AudioInfo | AudioInfo[]                                 | -  |
+| autoPlay            | 自动播放                                     | boolean                                  | false  |
+| theme             | 主题,可选值:`dark` 和 `light`                  | string        |                         "dark"  |
+| showToolbar       | 是否显示工具栏                           | boolean                                  | true      |
+| skipDuration       | 跳转时间                                     | number                                  | 10   |
+| className         | 类名                           | string                                  | -   |
+| style             | 内联样式                           | object                                  | -   |
+
+### AudioInfo
+
+| 属性                | 说明                                          | 类型                              | 默认值    |
+|-------------------|------------------------------------------------|---------------------------------|-----------|
+| src               | 音频地址                                    | string                          | -  |
+| title             | 音频标题                                    | string                          | -  |
+| cover             | 封面图片                                    | string                          | -  |
+
+

+ 4 - 3
content/plus/chat/index-en-US.md

@@ -1617,8 +1617,9 @@ render(DefaultChat);
 | inputBoxStyle | Input box style | CSSProperties | - |
 | inputBoxCls | Input box className | string | - |
 | sendHotKey | Keyboard shortcut for sending content, supports `enter` \| `shift+enter`. The former will send the message in the input box when you press enter alone. When the shift and enter keys are pressed at the same time, it will only wrap the line and not send it. The latter is the opposite | string | `enter` |
+| markdownRenderProps | This parameter will be passed to the MarkdownRender component used for dialog rendering. For details, see [MarkdownRenderProps](/en-US/plus/markdownrender#API)| MarkdownRenderProps |-|
 | mode | Conversation mode, support `bubble` \| `noBubble` \| `userBubble`  | string | `bubble` |
-| roleConfig | Role information configuration, see[RoleConfig](#RoleConfig) | RoleConfig | - |
+| roleConfig | Role information configuration, see [RoleConfig](#RoleConfig) | RoleConfig | - |
 | renderDivider | Custom render divider, supported since v2.67.0 | (message?: Message) => ReactNode | - |
 | renderHintBox | Custom rendering prompt information | (props: {content: string; index: number,onHintClick: () => void}) => React.ReactNode| - |
 | onChatsChange | Triggered when the conversation list changes | (chats: Message[]) => void | - |
@@ -1637,8 +1638,8 @@ render(DefaultChat);
 | showClearContext | Whether to display the clear context button| boolean | false |
 | showStopGenerate | Whether to display the stop generation button| boolean | false |
 | topSlot | top slot for chat | React.ReactNode | - |
-| uploadProps | Upload component properties, refer to details [Upload](/zh-CN/input/upload#API%20%E5%8F%82%E8%80%83) | UploadProps | - |
-| uploadTipProps | Upload component prompt attribute, refer to details [Tooltip](/zh-CN/show/tooltip#API%20%E5%8F%82%E8%80%83) | TooltipProps | - |
+| uploadProps | Upload component properties, refer to details [Upload](/en-US/input/upload#API%20%E5%8F%82%E8%80%83) | UploadProps | - |
+| uploadTipProps | Upload component prompt attribute, refer to details [Tooltip](/en-US/show/tooltip#API%20%E5%8F%82%E8%80%83) | TooltipProps | - |
 
 
 #### RoleConfig

+ 2 - 1
content/plus/chat/index.md

@@ -1620,8 +1620,9 @@ render(DefaultChat);
 | inputBoxStyle | 输入框样式 | CSSProperties | - |
 | inputBoxCls | 输入框类名 | string | - |
 | sendHotKey | 发送输入内容的键盘快捷键,支持 `enter` \| `shift+enter`。前者在单独按下 enter 将发送输入框中的消息, shift 和 enter 按键同时按下时,仅换行,不发送。后者相反 | string | `enter` |
+| markdownRenderProps | 该参数将透传给对话框渲染所用的 MarkdownRender 组件,详见 [MarkdownRenderProps](/zh-CN/plus/markdownrender#API)| MarkdownRenderProps |-|
 | mode | 对话模式,支持 `bubble` \| `noBubble` \| `userBubble`  | string | `bubble` |
-| roleConfig | 角色信息配置,具体见[RoleConfig](#RoleConfig) | RoleConfig | - |
+| roleConfig | 角色信息配置,具体见 [RoleConfig](#RoleConfig) | RoleConfig | - |
 | renderDivider | 自定义渲染分割线, 自 v2.67.0 支持 | (message?: Message) => ReactNode | - |
 | renderHintBox | 自定义渲染提示信息 | (props: {content: string; index: number,onHintClick: () => void}) => React.ReactNode| - |
 | onChatsChange | 对话列表变化时触发 | (chats: Message[]) => void | - |

+ 1 - 1
content/plus/dragMove/index-en-US.md

@@ -3,7 +3,7 @@ localeCode: en-US
 order: 26
 category: Plus
 title:  DragMove
-icon: doc-configprovider
+icon: doc-dragmove
 dir: column
 brief: Set elements to change their position by dragging
 showNew: true

+ 1 - 1
content/plus/dragMove/index.md

@@ -3,7 +3,7 @@ localeCode: zh-CN
 order: 26
 category: Plus
 title:  DragMove 拖拽移动
-icon: doc-configprovider
+icon: doc-dragmove
 dir: column
 brief: 可通过拖拽改变位置
 showNew: true

+ 2 - 0
content/plus/jsonviewer/index-en-US.md

@@ -161,6 +161,7 @@ render(FormatJsonComponent);
 | width | Width of wrapper DOM | number | - |
 | className | className of wrapper DOM | string | - |
 | style | InlineStyle of wrapper DOM | object | - |
+| showSearch | Whether to show search icon | boolean | true |
 | options | Formatting configuration | JsonViewerOptions | - |
 | onChange | Callback for content change | (value: string) => void | - |
 
@@ -170,6 +171,7 @@ render(FormatJsonComponent);
 | ------------- | --------------------------------------- | ----------------- | ------- |
 | lineHeight    | Height of each line of content, unit:px | number            | 20      |
 | autoWrap      | Whether to wrap lines automatically.    | boolean           | true    |
+| readOnly      | Whether to be read-only.    | boolean           | false    |
 | formatOptions | Content format setting                  | FormattingOptions | -       |
 
 ### FormattingOptions

+ 2 - 0
content/plus/jsonviewer/index.md

@@ -158,6 +158,7 @@ render(FormatJsonComponent);
 | width             | 宽度                                     | number                                  | -  |
 | className         | 类名                           | string                                  | -   |
 | style             | 内联样式                           | object                                  | -   |
+| showSearch        | 是否显示搜索Icon                           | boolean                                  | true   |
 | options           | 格式化配置                                | JsonViewerOptions                       | -   |
 | onChange          | 内容变化回调                           | (value: string) => void                  | -   |
 
@@ -167,6 +168,7 @@ render(FormatJsonComponent);
 |-------------------|------------------------------------------------|---------------------------------|-----------|
 | lineHeight        | 行高                                    | number                          | 20  |
 | autoWrap        | 是否自动换行                             | boolean                            | true  |
+| readOnly        | 是否只读                             | boolean                            | false  |
 | formatOptions     | 格式化配置                               | FormattingOptions                |  -  |
 
 ### FormattingOptions

+ 1 - 1
content/show/chart/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 79
+order: 80
 category: Show
 title: Data Visualization
 icon: doc-vchart

+ 1 - 1
content/show/chart/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 79
+order: 80
 category: 展示类
 title:  Data Visualization 数据可视化
 icon: doc-vchart

+ 281 - 0
content/show/cropper/index-en-US.md

@@ -0,0 +1,281 @@
+---
+localeCode: en-US
+order: 69
+category: Plus
+title:  Cropper
+icon: doc-cropper
+dir: column
+brief: Freely crop pictures
+showNew: true
+---
+
+## When to use
+
+Cropper is used to crop pictures. It supports custom cropping box styles. The positions of the cropping box, cropped image can be adjusted by dragging. It can zoom and rotate the cropped pictures.
+
+## Demos
+
+Cropper is supported starting from version v2.73.0.
+
+```jsx
+import { Cropper } from '@douyinfe/semi-ui';
+```
+
+### Basic usage
+
+Use `sr` to set the cropped image; use `shape` to set the shape of the cropping box, which defaults to square.
+
+```jsx live=true dir=column noInline=true
+import { Cropper, Button, RadioGroup, Radio } from '@douyinfe/semi-ui';
+
+const containerStyle = {
+  width: 550,
+  height: 300,
+  margin: 20,
+}
+
+function Demo() {
+    const ref = useRef(null);
+  const [shape, setShape] = useState('rect');
+
+    const onButtonClick = useCallback(() => {
+        const value = ref.current.getCropperCanvas();
+        const previewContainer = document.getElementById('previewContainer');
+        previewContainer.innerHTML = '';
+        previewContainer.appendChild(value);
+    }, []);
+
+    const onShapeChange = useCallback((e) => {
+        setShape(e.target.value);
+    }, []);
+
+    return <>
+        <RadioGroup onChange={onShapeChange} value={shape}>
+            <Radio value={'rect'}>rect</Radio>
+            <Radio value={'round'}>round</Radio>
+            <Radio value={'roundRect'}>roundRect</Radio>
+        </RadioGroup>
+        <Cropper
+            ref={ref} 
+            src={'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/image.png'}
+            style={containerStyle}
+            shape={shape}
+        />
+        <Button onClick={onButtonClick}>Get Cropped Image</Button>
+        <div id='previewContainer'/>
+    </>;
+}
+
+render(<Demo />)
+```
+
+### Customize crop box ratio
+
+The initial crop box ratio can be passed through `defaultAspectRatio` (default is 1). A fixed crop box ratio can be set via `aspectRatio`.
+
+Setting `defaultAspectRatio` only takes effect on the initial crop box ratio. When dragging, the crop box ratio will change with dragging.
+
+When setting `aspectRatio`, the crop box ratio is fixed, and the crop box will change according to this ratio when dragging.
+
+```jsx live=true dir=column noInline=true
+import { Cropper, Button, RadioGroup, Radio } from '@douyinfe/semi-ui';
+
+const containerStyle = {
+  width: 550,
+  height: 300,
+  margin: 20,
+}
+
+function Demo() {
+    const ref = useRef(null);
+    const shape = useState('rect');
+
+    const onButtonClick = useCallback(() => {
+        const value = ref.current.getCropperCanvas();
+        const previewContainer = document.getElementById('previewContainer-aspect');
+        previewContainer.innerHTML = '';
+        previewContainer.appendChild(value);
+    }, []);
+
+    return <>
+        <Cropper
+            aspectRatio={3/4}
+            ref={ref} 
+            src={'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/image.png'}
+            style={containerStyle}
+        />
+        <Button onClick={onButtonClick}>Get Cropped Image</Button>
+        <div id='previewContainer-aspect' />
+    </>;
+}
+
+render(<Demo />)
+```
+
+### Controlled rotation/zooming of images
+
+Control image rotation and zoom through `rotate` and `zoom`, and get the latest `zoom` value through `onZoomChange`
+
+```jsx live=true dir=column noInline=true
+import { Cropper, Button, Slider } from '@douyinfe/semi-ui';
+
+const containerStyle = {
+  width: 550,
+  height: 300,
+  margin: 20,
+}
+
+const actionStyle = {
+  marginTop: 20,
+  display: 'flex',
+  alignItems: 'center',
+  justifyContent: 'center',
+  width: 'fit-content'
+}
+
+function Demo() {
+  const [rotate, setRotate] = useState(0);
+  const [zoom, setZoom] = useState(1);
+  const ref = useRef();
+
+  const onZoomChange = useCallback((value) => {
+    setZoom(value);
+  })
+
+  const onSliderChange = useCallback((value) => {
+    setRotate(value);
+  }, []);
+
+  const onButtonClick = useCallback(() => {
+    const value = ref.current.getCropperCanvas();
+    const previewContainer = document.getElementById('previewContainer-control');
+    previewContainer.innerHTML = '';
+    previewContainer.appendChild(value);
+  }, []);
+
+  return (
+      <div id='cropper-container'>
+           <Cropper 
+              ref={ref} 
+              src={'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/image.png'}
+              style={containerStyle}
+              rotate={rotate}
+              zoom={zoom}
+              onZoomChange={onZoomChange}
+           />
+           <div style={actionStyle} >
+            <span>Rotate</span>
+            <Slider
+              style={{ width: 500}}
+              value={rotate}
+              step={1}
+              min={-360}
+              max={360}
+              onChange={onSliderChange}
+            />
+           </div>
+           <div style={actionStyle} >
+            <span>Zoom</span>
+            <Slider
+              style={{ width: 500}}
+              value={zoom}
+              step={0.1}
+              min={0.1}
+              max={3}
+              onChange={onZoomChange}
+            />
+           </div>
+           <br />
+           <Button onClick={onButtonClick}>Get Cropped Image</Button>
+           <br />
+           <div >
+            <div id='previewContainer-control'
+            />
+          </div>
+      </div>
+  );
+};
+
+render(<Demo />)
+```
+
+### Crop box settings
+
+The crop box style can be customized through `cropperBoxStyle`, `cropperBoxClassName`. You can use `showResizeBox` to set whether to display the adjustment blocks at the corners of the crop box.
+
+```jsx live=true dir=column noInline=true
+import { Cropper, Button, Switch } from '@douyinfe/semi-ui';
+
+const containerStyle = {
+  width: 550,
+  height: 300,
+  margin: 20,
+}
+
+const centerStyle = {
+    display: 'flex', 
+    alignItems: 'center', 
+    justifyContent: 'center',
+    width: 'fit-content'
+}
+
+function Demo() {
+    const ref = useRef(null);
+
+    const onButtonClick = useCallback(() => {
+        const value = ref.current.getCropperCanvas();
+        const previewContainer = document.getElementById('previewContainer-cropperBox');
+        previewContainer.innerHTML = '';
+        previewContainer.appendChild(value);
+    }, []);
+
+    return <>
+        <strong>showResizeBox = false,and change the outline color of cropper box</strong>
+        <Cropper
+            ref={ref} 
+            src={'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/image.png'}
+            style={containerStyle}
+            cropperBoxStyle={{ outlineColor: 'var(--semi-color-bg-0)'}}
+            showResizeBox={false}
+        />
+        <Button onClick={onButtonClick}>Get Cropped Image</Button>
+        <div id='previewContainer-cropperBox'/>
+    </>;
+}
+
+render(<Demo />)
+```
+
+### API
+
+| PROPERTIES | INSTRUCTIONS | TYPE | DEFAULT |
+|-----|------|-----|------|
+| aspectRatio | Crop box width to height ratio | number | - |
+| className | className | string | - |
+| cropperBoxClassName | The class name passed to the crop box | string | - |
+| cropperBoxStyle | The style passed to the crop box | CSSProperties | - |
+| defaultAspectRatio | Initial crop box ratio | number | 1 |
+| imgProps | Attributes passed through to the img tag | object | - |
+| fill | The fill color of the non-picture parts in the cropped result | string | 'rgba(0, 0, 0, 0)'  |
+| maxZoom | Maximum zoom factor | number | 3 |
+| minZoom | Minimum zoom factor | number | 0.1 |
+| onZoomChange | Callback during zoom transformation | (zoom: number) => void | - |
+| rotate | rotation angle | number | - |
+| shape | Crop box shape | 'rect' \| 'round' \| 'roundRect' | 'rect' |
+| src | The address of the cropped image | string | - |
+| showResizeBox | Whether to display the adjustment block of the cropping box | boolean | true |
+| style | Style  | CSSProperties | - |
+| zoom | Zoom value | number | - |
+| zoomStep | Zoom step size | number | 0.1 |
+
+### Methods
+
+Methods bound to component instances can be called through ref to achieve certain special interactions
+
+| Name    | Description  |
+|---------|--------------|
+| getCropperCanvas  | Get the canvas of the cropped image |
+
+## Design Token
+
+<DesignToken/>

+ 286 - 0
content/show/cropper/index.md

@@ -0,0 +1,286 @@
+---
+localeCode: zh-CN
+order: 69
+category: Plus
+title:  Cropper 图片裁切
+icon: doc-cropper
+dir: column
+brief: 通过设定裁切框的宽高比例,自由裁切图片
+showNew: true
+---
+
+## 使用场景
+
+Cropper 用于裁切图片,支持自定义裁切框样式,可通过拖动调整裁切框位置,被裁切图片位置;可缩放,旋转被裁切图片。
+
+
+## 代码演示
+
+### 如何引入
+
+Cropper 从 v2.73.0 开始支持
+
+```jsx
+import { Cropper } from '@douyinfe/semi-ui';
+```
+
+### 基本用法
+
+通过 `sr` 设置被裁切的图片; 可通过 `shape` 设置裁切框形状,默认为方形。
+
+```jsx live=true dir=column noInline=true
+import { Cropper, Button, RadioGroup, Radio } from '@douyinfe/semi-ui';
+
+const containerStyle = {
+  width: 550,
+  height: 300,
+  margin: 20,
+}
+
+function Demo() {
+    const ref = useRef(null);
+  const [shape, setShape] = useState('rect');
+
+    const onButtonClick = useCallback(() => {
+        const value = ref.current.getCropperCanvas();
+        const previewContainer = document.getElementById('previewContainer');
+        previewContainer.innerHTML = '';
+        previewContainer.appendChild(value);
+    }, []);
+
+    const onShapeChange = useCallback((e) => {
+        setShape(e.target.value);
+    }, []);
+
+    return <>
+        <RadioGroup onChange={onShapeChange} value={shape}>
+            <Radio value={'rect'}>rect</Radio>
+            <Radio value={'round'}>round</Radio>
+            <Radio value={'roundRect'}>roundRect</Radio>
+        </RadioGroup>
+        <Cropper
+            ref={ref} 
+            src={'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/image.png'}
+            style={containerStyle}
+            shape={shape}
+        />
+        <Button onClick={onButtonClick}>裁切</Button>
+        <div id='previewContainer'/>
+    </>;
+}
+
+render(<Demo />)
+```
+
+### 自定义裁切框比例
+
+可通过 `defaultAspectRatio` 初始的裁切框比例(默认为 1)。可通过 `aspectRatio` 设置固定的裁切框比例。
+
+设置 `defaultAspectRatio`仅对初始的裁切框比例生效, 拖动时,裁切框比例会随着拖动而变化。
+
+设置 `aspectRatio` 时,裁切框比例固定,拖动时将裁切框将以此比例变化。
+
+```jsx live=true dir=column noInline=true
+import { Cropper, Button, RadioGroup, Radio } from '@douyinfe/semi-ui';
+
+const containerStyle = {
+  width: 550,
+  height: 300,
+  margin: 20,
+}
+
+function Demo() {
+    const ref = useRef(null);
+    const shape = useState('rect');
+
+    const onButtonClick = useCallback(() => {
+        const value = ref.current.getCropperCanvas();
+        const previewContainer = document.getElementById('previewContainer-aspect');
+        previewContainer.innerHTML = '';
+        previewContainer.appendChild(value);
+    }, []);
+
+    return <>
+        <Cropper
+            aspectRatio={3/4}
+            ref={ref} 
+            src={'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/image.png'}
+            style={containerStyle}
+        />
+        <Button onClick={onButtonClick}>裁切</Button>
+        <div id='previewContainer-aspect' />
+    </>;
+}
+
+render(<Demo />)
+```
+
+### 受控旋转/缩放图片
+
+通过 `rotate` 和 `zoom` 控制图片旋转和缩放, 可通过 `onZoomChange` 拿到最新的 `zoom` 值。
+
+```jsx live=true dir=column noInline=true
+import { Cropper, Button, Slider } from '@douyinfe/semi-ui';
+
+const containerStyle = {
+  width: 550,
+  height: 300,
+  margin: 20,
+}
+
+const actionStyle = {
+  marginTop: 20,
+  display: 'flex',
+  alignItems: 'center',
+  justifyContent: 'center',
+  width: 'fit-content'
+}
+
+function Demo() {
+  const [rotate, setRotate] = useState(0);
+  const [zoom, setZoom] = useState(1);
+  const ref = useRef();
+
+  const onZoomChange = useCallback((value) => {
+    setZoom(value);
+  })
+
+  const onSliderChange = useCallback((value) => {
+    setRotate(value);
+  }, []);
+
+  const onButtonClick = useCallback(() => {
+    const value = ref.current.getCropperCanvas();
+    const previewContainer = document.getElementById('previewContainer-control');
+    previewContainer.innerHTML = '';
+    previewContainer.appendChild(value);
+  }, []);
+
+  return (
+      <div id='cropper-container'>
+           <Cropper 
+              ref={ref} 
+              src={'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/image.png'}
+              style={containerStyle}
+              rotate={rotate}
+              zoom={zoom}
+              onZoomChange={onZoomChange}
+           />
+           <div style={actionStyle} >
+            <span>Rotate</span>
+            <Slider
+              style={{ width: 500}}
+              value={rotate}
+              step={1}
+              min={-360}
+              max={360}
+              onChange={onSliderChange}
+            />
+           </div>
+           <div style={actionStyle} >
+            <span>Zoom</span>
+            <Slider
+              style={{ width: 500}}
+              value={zoom}
+              step={0.1}
+              min={0.1}
+              max={3}
+              onChange={onZoomChange}
+            />
+           </div>
+           <br />
+           <Button onClick={onButtonClick}>裁切</Button>
+           <br />
+           <div 
+            // style={{ background: 'pink' }} 
+           >
+            <div id='previewContainer-control'
+            />
+          </div>
+      </div>
+  );
+};
+
+render(<Demo />)
+```
+
+### 裁切框设置
+
+可通过 `cropperBoxStyle`, `cropperBoxClassName` 自定义裁切框样式。可通过 `showResizeBox` 设置是否展示裁切框边角的调整块。
+
+```jsx live=true dir=column noInline=true
+import { Cropper, Button, Switch } from '@douyinfe/semi-ui';
+
+const containerStyle = {
+  width: 550,
+  height: 300,
+  margin: 20,
+}
+
+const centerStyle = {
+    display: 'flex', 
+    alignItems: 'center', 
+    justifyContent: 'center',
+    width: 'fit-content'
+}
+
+function Demo() {
+    const ref = useRef(null);
+
+    const onButtonClick = useCallback(() => {
+        const value = ref.current.getCropperCanvas();
+        const previewContainer = document.getElementById('previewContainer-cropperBox');
+        previewContainer.innerHTML = '';
+        previewContainer.appendChild(value);
+    }, []);
+
+    return <>
+        <strong>showResizeBox = false,并修改边框颜色</strong>
+        <Cropper
+            ref={ref} 
+            src={'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/image.png'}
+            style={containerStyle}
+            cropperBoxStyle={{ outlineColor: 'var(--semi-color-bg-0)'}}
+            showResizeBox={false}
+        />
+        <Button onClick={onButtonClick}>裁切</Button>
+        <div id='previewContainer-cropperBox'/>
+    </>;
+}
+
+render(<Demo />)
+```
+
+### API
+
+| 属性 | 说明 | 类型 | 默认值 |
+|-----|------|-----|------|
+| aspectRatio | 裁切框比例 | number | - |
+| className | 类名 | string | - |
+| cropperBoxClassName | 裁切框类名 | string | - |
+| cropperBoxStyle | 裁切框样式 | CSSProperties | - |
+| defaultAspectRatio | 初始裁切框比例 | number | 1 |
+| imgProps | 透传给 img 标签的属性 | object | - |
+| fill | 裁切结果中非图片部分的填充色 | string | 'rgba(0, 0, 0, 0)'  |
+| maxZoom | 最大缩放倍数 | number | 3 |
+| minZoom | 最小缩放倍数 | number | 0.1 |
+| onZoomChange | 缩放回调 | (zoom: number) => void | - |
+| rotate | 旋转角度 | number | - |
+| shape | 裁切框形状 | 'rect' \| 'round' \| 'roundRect' | 'rect' |
+| src | 图片地址 | string | - |
+| showResizeBox | 是否展示调整块 | boolean | true |
+| style | 样式  | CSSProperties | - |
+| zoom | 缩放比例 | number | - |
+| zoomStep | 缩放步长 | number | 0.1 |
+
+### Methods
+
+绑定在组件实例上的方法,可以通过 ref 调用实现某些特殊交互
+
+| Name    | Description  |
+|---------|--------------|
+| getCropperCanvas  | 获取裁剪图片的 canvas |
+
+## 设计变量
+
+<DesignToken/>

+ 1 - 0
content/show/dropdown/index-en-US.md

@@ -322,6 +322,7 @@ function DropdownEvents() {
 | children | Child elements wrapped by the drop layer                                                                                                                                                                                                      | ReactNode |  |  |
 | clickToHide | Whether to close the drop-down layer automatically when clicking on the drop-down layer                                                                                                                                                       | boolean |  | **0.24.0** |
 | contentClassName | Drop-down menu root element class name                                                                                                                                                                                                        | string |  |  |
+| disableFocusListener | When trigger is `hover`, does not respond to the keyboard focus popup event, see details at [issue#977](https://github.com/DouyinFE/semi-design/issues/977)                                                                                   | boolean | true | **2.17.0** |
 | keepDOM | Whether to keep the internal component DOM from being destroyed when closing | boolean | false | **2.31.0** |
 | getPopupContainer | Specifies the parent DOM, and the bullet layer will be rendered to the DOM, you need to set 'position: relative`  This will change the DOM tree position, but not the view's rendering position.                                                                                                                              | function():HTMLElement | () => document.body |
 | margin | Popup layer calculates the size of the safe area when the current direction overflows, used in scenes covered by fixed elements, more detail refer to [issue#549](https://github.com/DouyinFE/semi-design/issues/549), same as Tooltip margin | object\|number |  | 2.25.0 |

+ 1 - 0
content/show/dropdown/index.md

@@ -346,6 +346,7 @@ function DropdownEvents() {
 | children | 触发弹出层的 Trigger 元素 | ReactNode |  |  |
 | clickToHide | 在弹出层内点击时是否自动关闭弹出层 | boolean |  | |
 | contentClassName | 下拉菜单根元素类名 | string |  |  |
+| disableFocusListener | trigger为`hover`时,不响应键盘聚焦弹出浮层事件,详见[issue#977](https://github.com/DouyinFE/semi-design/issues/977) | boolean | false | **2.17.0** |
 | getPopupContainer | 指定父级 DOM,弹层将会渲染至该 DOM 中,自定义需要设置 `position: relative` 这会改变浮层 DOM 树位置,但不会改变视图渲染位置。 | function():HTMLElement | () => document.body |  |
 | keepDOM | 关闭时是否保留内部组件 DOM 不销毁 | boolean | false | **2.31.0** |
 | margin | 弹出层计算溢出时的增加的冗余值,详见[issue#549](https://github.com/DouyinFE/semi-design/issues/549),作用同 Tooltip margin | number\|object |  | **2.25.0** |

+ 104 - 133
content/show/list/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 69
+order: 70
 category: Show
 title: List
 subTitle: List
@@ -755,165 +755,136 @@ render(VirtualizedScroll);
 ```
 
 ### Drag Sort
+You can integrate [dnd-kit](https://github.com/clauderic/dnd-kit/tree/master) to implement drag and drop sort.
 
-You can integrate [react-dnd](https://github.com/react-dnd/react-dnd) to implement drag and drop sort.
-
-```jsx live=true dir="column" noInline=true hideInDSM
-import React from 'react';
+```jsx live=true dir="column" hideInDSM
+import React, { useState } from 'react';
 import { List, Avatar } from '@douyinfe/semi-ui';
-import { DndProvider, DragSource, DropTarget } from 'react-dnd';
-import HTML5Backend from 'react-dnd-html5-backend';
-import ReactDOM from 'react-dom';
-
-class DraggableItem extends React.Component {
-    render() {
-        const { component, draggingItem, index, connectDragSource, connectDropTarget } = this.props;
-        const opacity = draggingItem && draggingItem.index === index ? 0.3 : 1;
-        const style = {
-            border: '1px solid var(--semi-color-border)',
-            marginBottom: 12,
-            backgroundColor: 'var(--semi-color-bg-2)',
-            cursor: 'move',
-        };
-
-        return connectDragSource(
-            connectDropTarget(
-                <div ref={node => (this.node = node)} style={{ ...style, opacity }}>
-                    {component}
-                </div>
-            )
-        );
-    }
-}
+import { DndContext, PointerSensor, MouseSensor, useSensors, useSensor } from '@dnd-kit/core';
+import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
+import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
+import { CSS as cssDndKit } from '@dnd-kit/utilities';
+import classNames from 'classnames';
 
-const cardSource = {
-    beginDrag(props) {
-        return {
-            id: props.id,
-            index: props.index,
-        };
-    },
-};
-
-const cardTarget = {
-    hover(props, monitor, component) {
-        const dragIndex = monitor.getItem().index;
-        const hoverIndex = props.index;
+() => {
+    const data = [
+        {
+            id: 1,  // 添加唯一id
+            title: 'Semi Design Title 1',
+            color: 'red',
+        },
+        {
+            id: 2,
+            title: 'Semi Design Title 2',
+            color: 'grey',
+        },
+        {
+            id: 3,
+            title: 'Semi Design Title 3',
+            color: 'light-green',
+        },
+        {
+            id: 4,
+            title: 'Semi Design Title 4',
+            color: 'light-blue',
+        },
+        {
+            id: 5,
+            title: 'Semi Design Title 5',
+            color: 'pink',
+        },
+    ];
+    const [listItems, setListItems] = useState(data);
 
-        if (dragIndex === hoverIndex) {
-            return;
-        }
-        const hoverBoundingRect = ReactDOM.findDOMNode(component).getBoundingClientRect();
-        const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
-        const clientOffset = monitor.getClientOffset();
-        const hoverClientY = clientOffset.y - hoverBoundingRect.top;
-        if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
-            return;
-        }
+    const sensors = useSensors(
+        useSensor(MouseSensor, {
+            activationConstraint: { distance: 1 },
+        })
+    );
 
-        if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
-            return;
+    const handleDragEnd = event => {
+        const { active, over } = event;
+        if (active.id !== over.id) {
+            setListItems((items) => {
+                const oldIndex = items.findIndex(item => item.id === active.id);
+                const newIndex = items.findIndex(item => item.id === over.id);
+                return arrayMove(items, oldIndex, newIndex);
+            });
         }
-
-        monitor.getItem().index = hoverIndex;
-        props.moveItem(dragIndex, hoverIndex);
-    },
-};
-
-function collectDragSource(connect, monitor) {
-    return {
-        connectDragSource: connect.dragSource(),
-        draggingItem: monitor.getItem(),
     };
-}
 
-function collectDropTarget(connect) {
-    return {
-        connectDropTarget: connect.dropTarget(),
-    };
-}
+    const ListItem = (props) => {
+        const { attributes, listeners, setNodeRef, transform, transition, isDragging, isOver } = useSortable({
+            id: props['id'],
+        });
 
-DraggableItem = DragSource('item', cardSource, collectDragSource)(DraggableItem);
-DraggableItem = DropTarget('item', cardTarget, collectDropTarget)(DraggableItem);
+        const styles = {
+            ...props.style,
+            transform: cssDndKit.Transform.toString(transform),
+            transition,
+            border: '1px solid var(--semi-color-border)',
+            marginBottom: 12,
+            cursor: 'grabbing',
+            ...(isDragging ? { zIndex: 999, position: 'relative', backgroundColor: 'var(--semi-color-bg-0)' } : {}),
 
-class DraggableList extends React.Component {
-    constructor() {
-        const listItems = [
-            {
-                title: 'Semi Design Title 1',
-                color: 'red',
-            },
-            {
-                title: 'Semi Design Title 2',
-                color: 'grey',
-            },
-            {
-                title: 'Semi Design Title 3',
-                color: 'light-green',
-            },
-            {
-                title: 'Semi Design Title 4',
-                color: 'light-blue',
-            },
-            {
-                title: 'Semi Design Title 5',
-                color: 'pink',
-            },
-        ];
-        super();
-        this.state = {
-            data: listItems,
         };
-        this.moveItem = this.moveItem.bind(this);
-        this.renderDraggable = this.renderDraggable.bind(this);
-    }
 
-    moveItem(dragIndex, hoverIndex) {
-        const { data } = this.state;
-        let temp = data[dragIndex];
-        data[dragIndex] = data[hoverIndex];
-        data[hoverIndex] = temp;
-        this.setState(
+        
+        const itemCls = classNames(
             {
-                ...this.state,
-                data
+                ['isDragging']: isDragging,
+                ['isOver']: isOver,
             }
         );
-    }
 
-    renderDraggable(item, id) {
-        const content = (
-            <List.Item
+        return (
+            <div
+                ref={setNodeRef} 
+                style={styles} 
+                className={itemCls}
+                {...listeners} 
+                {...attributes}
+            >
+                <List.Item {...props} ></List.Item>
+            </div>
+        );
+    };
+
+    const RenderDraggable = (item, id) => {
+        return (
+            <ListItem
+                id={id}
+                {...item}
                 header={<Avatar color={item.color}>SE</Avatar>}
                 main={
                     <div>
                         <span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{item.title}</span>
                         <p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0' }}>
-                            Create a consistent, good-looking, easy-to-use, and efficient user experience with a
-                            user-centric, content-first, and human-friendly design system
+                            Semi Design
+                            设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的
+                            Web 应用。
                         </p>
                     </div>
                 }
             />
         );
-        return (
-            <DraggableItem key={item.title} index={id} id={item.title} component={content} moveItem={this.moveItem} />
-        );
-    }
-
-    render() {
-        const { data } = this.state;
-        return (
-            <div style={{ padding: 12, border: '1px solid var(--semi-color-border)', margin: 12 }}>
-                <DndProvider backend={HTML5Backend}>
-                    <List dataSource={data} renderItem={this.renderDraggable} />
-                </DndProvider>
-            </div>
-        );
-    }
-}
+    };
 
-render(DraggableList);
+    return (
+        <div style={{ padding: 12, border: '1px solid var(--semi-color-border)', margin: 12 }}>
+            <DndContext
+                autoScroll={true}
+                sensors={sensors}
+                modifiers={[restrictToVerticalAxis]}
+                onDragEnd={handleDragEnd}
+            >
+                <SortableContext items={listItems.map(data => data.id)} strategy={verticalListSortingStrategy}>
+                    <List dataSource={listItems} renderItem={RenderDraggable} />
+                </SortableContext>
+            </DndContext>
+        </div>
+    );
+};
 ```
 
 ### With Pagination

+ 101 - 131
content/show/list/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 69
+order: 70
 category: 展示类
 title: List 列表
 icon: doc-list
@@ -757,135 +757,106 @@ render(VirtualizedScroll);
 ```
 
 ### 拖拽排序
+使用 [dnd-kit](https://github.com/clauderic/dnd-kit/tree/master) 可轻松实现拖拽排序。
 
-可以通过集成 [react-dnd](https://github.com/react-dnd/react-dnd) 来实现拖拽排序。
-
-```jsx live=true dir="column" noInline=true hideInDSM
-import React from 'react';
+```jsx live=true dir="column" hideInDSM
+import React, { useState } from 'react';
 import { List, Avatar } from '@douyinfe/semi-ui';
-import { DndProvider, DragSource, DropTarget } from 'react-dnd';
-import HTML5Backend from 'react-dnd-html5-backend';
-import ReactDOM from 'react-dom';
-
-class DraggableItem extends React.Component {
-    render() {
-        const { component, draggingItem, index, connectDragSource, connectDropTarget } = this.props;
-        const opacity = draggingItem && draggingItem.index === index ? 0.3 : 1;
-        const style = {
-            border: '1px solid var(--semi-color-border)',
-            marginBottom: 12,
-            backgroundColor: 'var(--semi-color-bg-2)',
-            cursor: 'move',
-        };
-
-        return connectDragSource(
-            connectDropTarget(
-                <div ref={node => (this.node = node)} style={{ ...style, opacity }}>
-                    {component}
-                </div>
-            )
-        );
-    }
-}
+import { DndContext, PointerSensor, MouseSensor, useSensors, useSensor } from '@dnd-kit/core';
+import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
+import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
+import { CSS as cssDndKit } from '@dnd-kit/utilities';
+import classNames from 'classnames';
 
-const cardSource = {
-    beginDrag(props) {
-        return {
-            id: props.id,
-            index: props.index,
-        };
-    },
-};
-
-const cardTarget = {
-    hover(props, monitor, component) {
-        const dragIndex = monitor.getItem().index;
-        const hoverIndex = props.index;
+() => {
+    const data = [
+        {
+            id: 1,  // 添加唯一id
+            title: 'Semi Design Title 1',
+            color: 'red',
+        },
+        {
+            id: 2,
+            title: 'Semi Design Title 2',
+            color: 'grey',
+        },
+        {
+            id: 3,
+            title: 'Semi Design Title 3',
+            color: 'light-green',
+        },
+        {
+            id: 4,
+            title: 'Semi Design Title 4',
+            color: 'light-blue',
+        },
+        {
+            id: 5,
+            title: 'Semi Design Title 5',
+            color: 'pink',
+        },
+    ];
+    const [listItems, setListItems] = useState(data);
 
-        if (dragIndex === hoverIndex) {
-            return;
-        }
-        const hoverBoundingRect = ReactDOM.findDOMNode(component).getBoundingClientRect();
-        const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
-        const clientOffset = monitor.getClientOffset();
-        const hoverClientY = clientOffset.y - hoverBoundingRect.top;
-        if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
-            return;
-        }
+    const sensors = useSensors(
+        useSensor(MouseSensor, {
+            activationConstraint: { distance: 1 },
+        })
+    );
 
-        if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
-            return;
+    const handleDragEnd = event => {
+        const { active, over } = event;
+        if (active.id !== over.id) {
+            setListItems((items) => {
+                const oldIndex = items.findIndex(item => item.id === active.id);
+                const newIndex = items.findIndex(item => item.id === over.id);
+                return arrayMove(items, oldIndex, newIndex);
+            });
         }
-
-        monitor.getItem().index = hoverIndex;
-        props.moveItem(dragIndex, hoverIndex);
-    },
-};
-
-function collectDragSource(connect, monitor) {
-    return {
-        connectDragSource: connect.dragSource(),
-        draggingItem: monitor.getItem(),
     };
-}
 
-function collectDropTarget(connect) {
-    return {
-        connectDropTarget: connect.dropTarget(),
-    };
-}
+    const ListItem = (props) => {
+        const { attributes, listeners, setNodeRef, transform, transition, isDragging, isOver } = useSortable({
+            id: props['id'],
+        });
 
-DraggableItem = DragSource('item', cardSource, collectDragSource)(DraggableItem);
-DraggableItem = DropTarget('item', cardTarget, collectDropTarget)(DraggableItem);
+        const styles = {
+            ...props.style,
+            transform: cssDndKit.Transform.toString(transform),
+            transition,
+            border: '1px solid var(--semi-color-border)',
+            marginBottom: 12,
+            cursor: 'grabbing',
+            ...(isDragging ? { zIndex: 999, position: 'relative', backgroundColor: 'var(--semi-color-bg-0)' } : {}),
 
-class DraggableList extends React.Component {
-    constructor() {
-        const listItems = [
-            {
-                title: 'Semi Design Title 1',
-                color: 'red',
-            },
-            {
-                title: 'Semi Design Title 2',
-                color: 'grey',
-            },
-            {
-                title: 'Semi Design Title 3',
-                color: 'light-green',
-            },
-            {
-                title: 'Semi Design Title 4',
-                color: 'light-blue',
-            },
-            {
-                title: 'Semi Design Title 5',
-                color: 'pink',
-            },
-        ];
-        super();
-        this.state = {
-            data: listItems,
         };
-        this.moveItem = this.moveItem.bind(this);
-        this.renderDraggable = this.renderDraggable.bind(this);
-    }
 
-    moveItem(dragIndex, hoverIndex) {
-        const { data } = this.state;
-        let temp = data[dragIndex];
-        data[dragIndex] = data[hoverIndex];
-        data[hoverIndex] = temp;
-        this.setState(
+        
+        const itemCls = classNames(
             {
-                ...this.state,
-                data
+                ['isDragging']: isDragging,
+                ['isOver']: isOver,
             }
         );
-    }
 
-    renderDraggable(item, id) {
-        const content = (
-            <List.Item
+        return (
+            <div
+                ref={setNodeRef} 
+                style={styles} 
+                className={itemCls}
+                {...listeners} 
+                {...attributes}
+            >
+                <List.Item {...props} ></List.Item>
+            </div>
+        );
+    };
+
+    const RenderDraggable = (item, id) => {
+        return (
+            <ListItem
+                id={id}
+                {...item}
                 header={<Avatar color={item.color}>SE</Avatar>}
                 main={
                     <div>
@@ -899,24 +870,23 @@ class DraggableList extends React.Component {
                 }
             />
         );
-        return (
-            <DraggableItem key={item.title} index={id} id={item.title} component={content} moveItem={this.moveItem} />
-        );
-    }
-
-    render() {
-        const { data } = this.state;
-        return (
-            <div style={{ padding: 12, border: '1px solid var(--semi-color-border)', margin: 12 }}>
-                <DndProvider backend={HTML5Backend}>
-                    <List dataSource={data} renderItem={this.renderDraggable} />
-                </DndProvider>
-            </div>
-        );
-    }
-}
+    };
 
-render(DraggableList);
+    return (
+        <div style={{ padding: 12, border: '1px solid var(--semi-color-border)', margin: 12 }}>
+            <DndContext
+                autoScroll={true}
+                sensors={sensors}
+                modifiers={[restrictToVerticalAxis]}
+                onDragEnd={handleDragEnd}
+            >
+                <SortableContext items={listItems.map(data => data.id)} strategy={verticalListSortingStrategy}>
+                    <List dataSource={listItems} renderItem={RenderDraggable} />
+                </SortableContext>
+            </DndContext>
+        </div>
+    );
+};
 ```
 
 

+ 1 - 1
content/show/modal/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 70
+order: 71
 category: Show
 title:  Modal
 subTitle: Modal

+ 1 - 1
content/show/modal/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 70
+order: 71
 category: 展示类
 title: Modal 模态对话框
 icon: doc-modal

+ 1 - 1
content/show/overflowlist/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 71
+order: 72
 category: Show
 title: OverflowList
 subTitle: OverflowList

+ 1 - 1
content/show/overflowlist/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 71
+order: 72
 category: 展示类
 title: OverflowList 折叠列表
 icon: doc-overflowList

+ 1 - 1
content/show/popover/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 72
+order: 73
 category: Show
 title: Popover
 subTitle: Popover

+ 1 - 1
content/show/popover/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 72
+order: 73
 category: 展示类
 title: Popover 气泡卡片
 icon: doc-popover

+ 1 - 1
content/show/scrolllist/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 73
+order: 74
 category: Show
 title:  ScrollList
 subTitle: ScrollList

+ 1 - 1
content/show/scrolllist/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 73
+order: 74
 category: 展示类
 title: ScrollList 滚动列表
 icon: doc-scrolllist

+ 1 - 1
content/show/sidesheet/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 74
+order: 75
 category: Show
 title: SideSheet
 subTitle: SideSheet

+ 1 - 1
content/show/sidesheet/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 74
+order: 75
 category: 展示类
 title: SideSheet 滑动侧边栏
 icon: doc-sidesheet

+ 1 - 1
content/show/table/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 75
+order: 76
 category: Show
 title: Table
 subTitle: Table

+ 1 - 1
content/show/table/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 75
+order: 76
 category: 展示类
 title: Table 表格
 icon: doc-table

+ 1 - 1
content/show/tag/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 76
+order: 77
 category: Show
 title: Tag
 subTitle: Tag

+ 1 - 1
content/show/tag/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 76
+order: 77
 category: 展示类
 title: Tag 标签
 icon: doc-tag

+ 1 - 1
content/show/timeline/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 77
+order: 78
 category: Show
 title:  Timeline
 subTitle: Timeline

+ 1 - 1
content/show/timeline/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 77
+order: 78
 category: 展示类
 title: Timeline 时间轴
 icon: doc-timeline

+ 1 - 1
content/show/tooltip/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 78
+order: 79
 category: Show
 title: Tooltip
 subTitle: Tooltip

+ 1 - 1
content/show/tooltip/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 78
+order: 79
 category: 展示类
 title: Tooltip 工具提示
 icon: doc-tooltip

+ 49 - 1
content/start/changelog/index-en-US.md

@@ -16,6 +16,55 @@ Version:Major.Minor.Patch (follow the **Semver** specification)
 
 ---
 
+#### 🎉 2.74.0 (2025-02-07)
+- 【Fix】
+    - Fix the issue where the List component's dataSource is empty and it is covered by the Spin component. [@LonelySnowman](https://github.com/LonelySnowman)
+    - Fix the TypeError when closing the panel of the TreeSelect component when search is enabled and treeData is undefined
+    - fixed the issue that the Steps icon and title of type basic were not centered with the line  [#2688 ](https://github.com/DouyinFE/semi-design/issues/2688)
+    - Fixed the problem that after the single-select and searchable Select loses focus when the panel is open, it cannot be focused again by clicking the trigger.
+    - Fixed the white edge issue of the AudioPlayer speed pop-up box style [@anjiazhuyouxing](https://github.com/anjiazhuyouxing)
+    - The internal ref usage of the AudioPlay component is modified to be compatible with other frameworks [@rashagu](https://github.com/rashagu)
+
+#### 🎉 2.74.0-beta.0 (2025-01-20)
+- 【Feat】
+  - The Chat component supports the markdownRenderProps API, which is used to set the MarkdownRender component for message rendering.  [#2640 ](https://github.com/DouyinFE/semi-design/issues/2640)
+- 【Fix】
+  - fixed the issue that the lowercase z input in JsonViewer is invalid [@anjiazhuyouxing](https://github.com/anjiazhuyouxing)
+
+#### 🎉 2.73.0 (2025-01-13)
+- 【Fix】
+    - Fix the problem that JsonViewer is not configured with default parameters [@anjiazhuyouxing](https://github.com/anjiazhuyouxing)
+    - Fix JsonViewer the judgment condition for whether to re-init. [@rashagu](https://github.com/rashagu)
+
+#### 🎉 2.73.0-beta.0 (2025-01-07)
+- 【New Component】
+    - Added audio player component [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2650](https://github.com/DouyinFE/semi-design/pull/2650)
+    - Added Cropper component [#2642](https://github.com/DouyinFE/semi-design/pull/2642)
+- 【Feat】
+    - Added read-only mode to JsonViewer [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2658](https://github.com/DouyinFE/semi-design/pull/2658)
+    - JsonViewer supports hidden search icon [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2658](https://github.com/DouyinFE/semi-design/pull/2658)
+    - JsonViewer adds Json format error information prompt function [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2638](https://github.com/DouyinFE/semi-design/pull/2638)
+    - JsonViewer Core package hot update problem [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2638](https://github.com/DouyinFE/semi-design/pull/2638)
+- 【Fix】
+    - Fix the cursor problem when clicking on the non-content area of ​​JsonViewer [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2658](https://github.com/DouyinFE/semi-design/pull/2658)
+    - Fix JsonViewer cursor problem after automatic indentation [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2658](https://github.com/DouyinFE/semi-design/pull/2658)
+    - Fix the missing content problem after JsonViewer folding [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2658](https://github.com/DouyinFE/semi-design/pull/2658)
+    - Fix the Chinese input method input problem in the JsonViewer search box [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2651](https://github.com/DouyinFE/semi-design/pull/2651)
+    - Fix the JsonViewer Undo&Redo text model out of sync problem [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2638](https://github.com/DouyinFE/semi-design/pull/2638)
+    - After setting the handler in DragMove, the child elements of DragMove can still be dragged [#2661](https://github.com/DouyinFE/semi-design/issues/2661) [#2662](https://github.com/DouyinFE/semi-design/pull/2662)
+    - Fixed the display problem of Loading when there is no Spin component in the project of Button [#2664](https://github.com/DouyinFE/semi-design/pull/2664)
+- 【Chore】
+    - JsonViewer refactors the underlying data structure of the folding model [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2658](https://github.com/DouyinFE/semi-design/pull/2658)
+    - Add JsonViewer E2E test [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2626](https://github.com/DouyinFE/semi-design/pull/2626)
+
+#### 🎉 2.72.2 (2025-01-06)
+- 【Fix】
+    - Fix the problem that Cascader's placeHolder and searchPlaceholder cannot be updated dynamically
+
+#### 🎉 2.72.1 (2025-01-02)
+- 【Fix】
+    - Fixed the problem of Typography’s JS omitting calculation error when display is none [#2656](https://github.com/DouyinFE/semi-design/pull/2656)
+
 #### 🎉 2.72.0 (2024-12-20)
 - 【Fix】
   - Fix the problem of JsonViewer using Chinese input method incorrectly [#2616](https://github.com/DouyinFE/semi-design/pull/2616)
@@ -33,7 +82,6 @@ Version:Major.Minor.Patch (follow the **Semver** specification)
 - 【Fix】
     - Fixed the problem of incorrect onChange callback result in Tree component treeDataSimpleJson mode  [#2508 ](https://github.com/DouyinFE/semi-design/issues/2508)
     - fixed the issue that the display of disabled subNavItem in vertical Navigation does not meet expectations when it is collapsed
-    - Set the max-width of the img node of the image preview to none to avoid enlargement display errors when using tailwind at the same time.[#2624](https://github.com/DouyinFE/semi-design/pull/2624)
 
 #### 🎉 2.71.2 (2024-12-13)
 - 【Fix】

+ 50 - 1
content/start/changelog/index.md

@@ -13,6 +13,56 @@ Semi 版本号遵循 **Semver** 规范(主版本号-次版本号-修订版本
 -   修订版本号(patch):仅会进行 bugfix,发布时间不限
 -   不同版本间的详细关系,可查阅 [FAQ](/zh-CN/start/faq)
 
+
+#### 🎉 2.74.0 (2025-02-07)
+- 【Fix】
+    - 修复 List 组件 dataSource 为空时被 Spin 组件遮挡问题 [@LonelySnowman](https://github.com/LonelySnowman) [#2693](https://github.com/DouyinFE/semi-design/pull/2693)
+    - 修复 TreeSelect 在开启搜索并且 treeData 为 undefined 时,关闭面板时候的 TypeError [#2694](https://github.com/DouyinFE/semi-design/pull/2694)
+    - 修复类型为 basic 的 Steps icon 和 title 未与 line 居中对齐问题  [#2688](https://github.com/DouyinFE/semi-design/issues/2688) [#2689](https://github.com/DouyinFE/semi-design/pull/2689)
+    - 修复单选,可搜索的 Select 在面板打开状态下失去焦点后,无法再次通过点击 trigger 聚焦问题 [#2668](https://github.com/DouyinFE/semi-design/pull/2668)
+    - 修复 AudioPlayer倍速弹出层样式白边问题 [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2685](https://github.com/DouyinFE/semi-design/pull/2685)
+    - AudioPlay 组件内部 ref 使用修改,兼容其他框架[@rashagu](https://github.com/rashagu) [#2673](https://github.com/DouyinFE/semi-design/pull/2673)
+
+#### 🎉 2.74.0-beta.0 (2025-01-20)
+- 【Feat】
+  - Chat 组件支持 markdownRenderProps API,用于设置对话渲染的 MarkdownRender 组件  [#2640 ](https://github.com/DouyinFE/semi-design/issues/2640) [#2679](https://github.com/DouyinFE/semi-design/pull/2679)
+- 【Fix】
+  - 修复JsonViewer输入小写z无效的问题 [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2680](https://github.com/DouyinFE/semi-design/pull/2680)
+
+#### 🎉 2.73.0 (2025-01-13)
+- 【Fix】
+    - 修复 JsonViewer 未配置默认参数问题 [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2670](https://github.com/DouyinFE/semi-design/pull/2670)
+    - 修复 JsonViewer 是否重新init的判断条件 [@rashagu](https://github.com/rashagu) [#2667](https://github.com/DouyinFE/semi-design/pull/2667)
+
+#### 🎉 2.73.0-beta.0 (2025-01-07)
+- 【New Component】
+    - 新增 AudioPlayer 音频播放器组件 [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2650](https://github.com/DouyinFE/semi-design/pull/2650)
+    - 新增 Cropper 图片裁切组件 [#2642](https://github.com/DouyinFE/semi-design/pull/2642)
+- 【Feat】
+    - JsonViewer 新增只读模式 [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2658](https://github.com/DouyinFE/semi-design/pull/2658)
+    - JsonViewer 支持隐藏搜索Icon [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2658](https://github.com/DouyinFE/semi-design/pull/2658)
+    - JsonViewer 新增Json格式错误信息提示功能 [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2638](https://github.com/DouyinFE/semi-design/pull/2638)
+    - JsonViewer Core 包热更新问题 [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2638](https://github.com/DouyinFE/semi-design/pull/2638)
+- 【Fix】
+    - 修复 JsonViewer 点击非内容区域下光标问题 [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2658](https://github.com/DouyinFE/semi-design/pull/2658)
+    - 修复 JsonViewer 自动缩进后光标问题 [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2658](https://github.com/DouyinFE/semi-design/pull/2658)
+    - 修复 JsonViewer 折叠后复制内容缺失问题 [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2658](https://github.com/DouyinFE/semi-design/pull/2658)
+    - 修复 JsonViewer 搜索框中文输入法输入问题 [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2651](https://github.com/DouyinFE/semi-design/pull/2651)
+    - 修复 JsonViewer Undo&Redo 文本模型不同步问题 [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2638](https://github.com/DouyinFE/semi-design/pull/2638)
+    - 修复 DragMove 中设置 handler 后,DragMove 的子元素仍然可以被拖动问题  [#2661 ](https://github.com/DouyinFE/semi-design/issues/2661) [#2662](https://github.com/DouyinFE/semi-design/pull/2662)
+    - 修复 Button 在项目内不存在 Spin 组件时 Loading 的显示问题 [#2664](https://github.com/DouyinFE/semi-design/pull/2664)
+- 【Chore】
+    - JsonViewer 重构折叠模型底层数据结构 [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2658](https://github.com/DouyinFE/semi-design/pull/2658)
+    - 新增 JsonViewer E2E 测试  [@anjiazhuyouxing](https://github.com/anjiazhuyouxing) [#2626](https://github.com/DouyinFE/semi-design/pull/2626)
+
+#### 🎉 2.72.2 (2025-01-06)
+- 【Fix】
+    - 修复 Cascader 的 placeHolder,searchPlaceholder 无法动态更新问题 [#2663](https://github.com/DouyinFE/semi-design/pull/2663)
+
+#### 🎉 2.72.1 (2025-01-02)
+- 【Fix】
+    - 修复在 display 为 none 时,Typography 的JS 省略计算错误问题 [#2656](https://github.com/DouyinFE/semi-design/pull/2656)
+
 #### 🎉 2.72.0 (2024-12-20)
 - 【Fix】
   - 修复 JsonViewer使用中文输入法错误的问题 [#2616](https://github.com/DouyinFE/semi-design/pull/2616)
@@ -30,7 +80,6 @@ Semi 版本号遵循 **Semver** 规范(主版本号-次版本号-修订版本
 - 【Fix】
   - 修复 Tree 组件 treeDataSimpleJson 模式下,onChange 回调结果错误问题  [#2508 ](https://github.com/DouyinFE/semi-design/issues/2508) [#2601](https://github.com/DouyinFE/semi-design/pull/2601)
   - 修复竖向 Navigation 在收起状态下 disabled subNavItem 展示不符合预期问题 [#2637](https://github.com/DouyinFE/semi-design/pull/2637)
-  - 设置图片预览的 img 节点的 max-width 为none,避免同时使用 tailwind 时放大显示错误问题[#2624](https://github.com/DouyinFE/semi-design/pull/2624)_
 
 
 #### 🎉 2.71.2 (2024-12-13)

+ 3 - 1
content/start/overview/index-en-US.md

@@ -24,7 +24,8 @@ CodeHighlight,
 Markdown,
 Lottie,
 Chat,
-HotKeys
+HotKeys,
+DragMove
 ```
 
 
@@ -79,6 +80,7 @@ Descriptions,
 Dropdown,
 Empty,
 Image,
+Cropper,
 List,
 Modal,
 OverflowList,

+ 6 - 2
content/start/overview/index.md

@@ -24,8 +24,11 @@ Typography 版式
 CodeHighlight 代码高亮,
 Markdown 渲染器,
 Lottie 动画,
-Chat 聊天,
-HotKeys 快捷键
+Chat 对话,
+HotKeys 快捷键,
+DragMove 拖拽移动,
+JsonViewer Json编辑器,
+AudioPlayer 音频播放器
 ```
 
 ## 输入类
@@ -80,6 +83,7 @@ Dropdown 下拉框,
 Empty 空状态,
 Highlight 高亮文本,
 Image 图片,
+Cropper 图片裁切,
 List 列表,
 Modal 模态对话框,
 OverflowList 折叠列表,

+ 164 - 0
cypress/e2e/jsonViewer.spec.js

@@ -0,0 +1,164 @@
+
+function typeTextAtPosition(lineNumber, column, text) {
+    let rightArrow = '{rightArrow}';
+    let leftArrow = '{leftArrow}';
+    for (let i = 0; i < column - 1; i++) {
+        rightArrow += '{rightArrow}';
+    }
+    if (lineNumber === 1) {
+        cy.get('.lines-content').children().eq(0).type(`${leftArrow}${rightArrow}${text}`);
+    } else {
+        cy.get('.lines-content').children().eq(lineNumber - 2).type(`${rightArrow}${text}`);
+    }
+}
+
+function undo(times) {
+    let z = '{meta+z}';
+
+    for (let i = 0; i < times - 1; i++) {
+        z += '{meta+z}';
+    }
+    cy.get('.lines-content').type(z);
+}
+
+function redo(times) {
+    let z = '{meta+shift+z}';
+
+    for (let i = 0; i < times - 1; i++) {
+        z += '{meta+shift+z}';
+    }
+    cy.get('.lines-content').type(z);
+}
+
+describe('jsonViewer', () => {
+    it('scroll to bottom', () => {
+        cy.visit('http://127.0.0.1:6006/iframe.html?path=/story/jsonviewer--default-json-viewer');
+        cy.get('.json-viewer-container').scrollTo('bottom');
+        cy.get('.lines-content').children().last().should('have.attr', 'data-line-number', '36');
+    });
+
+    it('scroll to top', () => {
+        cy.visit('http://127.0.0.1:6006/iframe.html?path=/story/jsonviewer--default-json-viewer');
+        cy.get('.json-viewer-container').scrollTo('top');
+        cy.get('.lines-content').children().first().should('have.attr', 'data-line-number', '1');
+    });
+
+    it('fold', () => {
+        cy.visit('http://127.0.0.1:6006/iframe.html?path=/story/jsonviewer--default-json-viewer');
+        cy.get('.line-scroll-container').trigger('mouseover', { which: 1 });
+        cy.get('.semi-json-viewer-line-number[data-line-number="1"]').children().should('have.length', 2);
+        cy.get('.semi-json-viewer-line-number[data-line-number="1"]').children().eq(1).click();
+        cy.get('.lines-content').children().should('have.length', 2);
+        cy.get('.lines-content').children().last().should('have.attr', 'data-line-number', '36');
+
+        cy.get('.line-scroll-container').trigger('mouseover', { which: 1 });
+        cy.get('.semi-json-viewer-line-number[data-line-number="1"]').children().should('have.length', 2);
+        cy.get('.semi-json-viewer-line-number[data-line-number="1"]').children().eq(1).click();
+        cy.get('.lines-content').children().should('have.length', 21);
+
+        cy.get('.line-scroll-container').trigger('mouseover', { which: 1 });
+        cy.get('.semi-json-viewer-line-number[data-line-number="13"]').children().should('have.length', 2);
+        cy.get('.semi-json-viewer-line-number[data-line-number="13"]').children().eq(1).click();
+        cy.get('.lines-content').children().should('have.length', 15);
+
+        cy.get('.line-scroll-container').trigger('mouseover', { which: 1 });
+        cy.get('.semi-json-viewer-line-number[data-line-number="13"]').children().should('have.length', 2);
+        cy.get('.semi-json-viewer-line-number[data-line-number="13"]').children().eq(1).click();
+        cy.get('.lines-content').children().should('have.length', 21);
+    });
+
+    it('edit', () => {
+        cy.visit('http://127.0.0.1:6006/iframe.html?path=/story/jsonviewer--default-json-viewer');
+        //insert
+        typeTextAtPosition(1, 1, '{enter}');
+        cy.get('.lines-content').children().eq(1).children().should('have.length', 1);
+        typeTextAtPosition(2, 2, `"`);
+        typeTextAtPosition(2, 3, `key`);
+        typeTextAtPosition(2, 7, `:`);
+        typeTextAtPosition(2, 8, `1`);
+        typeTextAtPosition(2, 9, `,`);
+        cy.get('.lines-content').children().eq(1).children().should('have.length', 5);
+
+
+        // undo redo
+        undo(1);
+        cy.get('.lines-content').children().eq(1).children().should('have.length', 4);
+        redo(1);
+        cy.get('.lines-content').children().eq(1).children().should('have.length', 5);
+        undo(8);
+        cy.get('.lines-content').children().eq(1).children().should('have.length', 6);
+
+        //del
+        typeTextAtPosition(2, 1, `{backspace}`);
+        cy.get('.lines-content').children().eq(0).children().should('have.length', 7);
+        undo(1);
+        cy.get('.lines-content').children().eq(0).children().should('have.length', 1);
+        cy.get('.lines-content').children().eq(1).children().should('have.length', 6);
+
+        // cut
+        // typeTextAtPosition(2, 1, `{meta+x}`);
+        // cy.get('.lines-content').children().eq(1).children().should('have.length', 0);
+        // cy.get('.lines-content').type('{meta+z}');
+        // cy.get('.lines-content').children().eq(1).children().should('have.length', 6);
+
+        //complete
+        typeTextAtPosition(14, 4, '{enter}');
+        cy.get('.lines-content').children().eq(14).children().should('have.length', 1);
+        typeTextAtPosition(15, 4, `c`);
+        cy.get('.semi-json-viewer-complete-suggestions-container').children().should('have.length', 2);
+        cy.get('.lines-content').type('{enter}');
+        cy.get('.semi-json-viewer-complete-container').should('have.css', 'display', 'none');
+        cy.get('.lines-content').children().eq(14).children().should('have.length', 2);
+        typeTextAtPosition(15, 11, `:`);
+        cy.get('.semi-json-viewer-complete-container').should('have.css', 'display', 'block');
+        cy.get('.semi-json-viewer-complete-suggestions-container').children().should('have.length', 2);
+        cy.get('.lines-content').type('{enter}');
+        cy.get('.semi-json-viewer-complete-container').should('have.css', 'display', 'none');
+        typeTextAtPosition(15, 19, `,{enter}`);
+        cy.get('.lines-content').children().eq(14).children().should('have.length', 5);
+        typeTextAtPosition(16, 4, `a`);
+        cy.get('.semi-json-viewer-complete-suggestions-container').children().should('have.length', 2);
+        typeTextAtPosition(16, 5, `{rightArrow}`);
+        cy.get('.semi-json-viewer-complete-container').should('have.css', 'display', 'none');
+        typeTextAtPosition(16, 5, `g`);
+        cy.get('.semi-json-viewer-complete-suggestions-container').children().should('have.length', 2);
+        cy.get('.lines-content').type('{enter}');
+        cy.get('.semi-json-viewer-complete-container').should('have.css', 'display', 'none');
+        typeTextAtPosition(16, 9, `:`);
+        cy.get('.lines-content').type('{enter}');
+        cy.get('.semi-json-viewer-complete-container').should('have.css', 'display', 'none');
+        cy.get('.lines-content').children().eq(15).children().should('have.length', 4);
+
+        //search
+        cy.get('.semi-json-viewer-search-bar-trigger').click();
+        cy.get('.semi-json-viewer-search-bar').children().eq(0).type('a');
+        cy.get('.semi-json-viewer-search-result').should('have.length.at.least', 1);
+        cy.get('.semi-icon.semi-icon-default.semi-icon-whole_word').click();
+        cy.get('.semi-json-viewer-search-result').should('have.length', 0);
+        cy.get('.semi-icon.semi-icon-default.semi-icon-whole_word').click();
+        cy.get('.semi-json-viewer-search-bar').children().eq(0).clear();
+        cy.get('.semi-json-viewer-search-result').should('have.length', 0);
+        const str = '\\d+';
+        cy.get('.semi-json-viewer-search-bar').children().eq(0).type(str);
+        cy.get('.semi-icon.semi-icon-default.semi-icon-reg_exp').click();
+        cy.get('.semi-json-viewer-search-result').should('have.length.at.least', 1);
+        cy.get('.semi-icon.semi-icon-default.semi-icon-reg_exp').click();
+        cy.get('.semi-json-viewer-search-bar').children().eq(0).clear();
+
+        //replace
+        cy.scrollTo('right');
+        cy.get('.semi-json-viewer-search-bar').children().eq(0).type('a');
+        cy.get('.semi-json-viewer-replace-bar').children().eq(0).type('x');
+        cy.get('.semi-json-viewer-search-result').then(($el) => {
+            let length = $el.length;
+            cy.get('.semi-json-viewer-replace-bar').children().eq(1).click();
+            cy.get('.semi-json-viewer-search-result').should('have.length', length - 1);
+            cy.get('.semi-json-viewer-replace-bar').children().eq(2).click();
+            cy.get('.semi-json-viewer-search-result').should('have.length', 0);
+        });
+
+
+    });
+
+
+});

+ 18 - 3
gatsby-node.js

@@ -112,7 +112,7 @@ exports.onCreateWebpackConfig = ({ stage, rules, loaders, plugins, actions }) =>
                 'semi-site-header': process.env.SEMI_SITE_HEADER || '@douyinfe/semi-site-header',
                 'semi-site-banner': process.env.SEMI_SITE_BANNER || '@douyinfe/semi-site-banner',
                 'univers-webview': process.env.SEMI_SITE_UNIVERS_WEBVIEW || resolve('packages/semi-ui'),
-                '@douyinfe/semi-json-viewer-core': resolve('packages/semi-json-viewer-core'),
+                '@douyinfe/semi-json-viewer-core': resolve('packages/semi-json-viewer-core/src'),
                 '@douyinfe/semi-ui': resolve('packages/semi-ui'),
                 '@douyinfe/semi-foundation': resolve('packages/semi-foundation'),
                 '@douyinfe/semi-icons': resolve('packages/semi-icons/src/'),
@@ -170,16 +170,31 @@ exports.onCreateWebpackConfig = ({ stage, rules, loaders, plugins, actions }) =>
                         },
                     },
                 },
+                {
+                    test: /jsonWorkerManager\.ts$/,
+                    use: [{
+                        loader: 'webpack-replace-loader',
+                        options: {
+                            search: '%WORKER_RAW%',
+                            replace: () => {
+                                const workFilePath = resolve('packages/semi-json-viewer-core/workerLib/worker.js');
+                                const result = fs.readFileSync(workFilePath, 'utf-8');
+                                const encodedResult = encodeURIComponent(result);
+                                return encodedResult;
+                            }
+                        }
+                    }],
+                },
                 {
                     test: [/\.tsx?$/],
                     include: [path.resolve(__dirname, 'src')],
-                    use: {
+                    use: [{
                         loader: 'esbuild-loader',
                         options: {
                             loader: 'tsx', // Remove this if you're not using JSX
                             target: 'esnext' // Syntax to compile to (see options below for possible values)
                         },
-                    },
+                    }],
                 },
                 {
                     test: /\.mjs$/,

+ 1 - 1
lerna.json

@@ -1,5 +1,5 @@
 {
     "useWorkspaces": true,
     "npmClient": "yarn",
-    "version": "2.72.0"
+    "version": "2.74.0"
 }

+ 1 - 0
package.json

@@ -217,6 +217,7 @@
         "webpack": "^5.77.0",
         "webpack-cli": "^5.1.4",
         "webpack-dev-server": "^3.11.2",
+        "webpack-replace-loader": "^5.0.1",
         "webpackbar": "^5.0.0-3",
         "worker-loader": "^3.0.8"
     },

+ 3 - 3
packages/semi-animation-react/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-animation-react",
-    "version": "2.72.0",
+    "version": "2.74.0",
     "description": "motion library for semi-ui-react",
     "keywords": [
         "motion",
@@ -25,8 +25,8 @@
         "prepublishOnly": "npm run build:lib"
     },
     "dependencies": {
-        "@douyinfe/semi-animation": "2.72.0",
-        "@douyinfe/semi-animation-styled": "2.72.0",
+        "@douyinfe/semi-animation": "2.74.0",
+        "@douyinfe/semi-animation-styled": "2.74.0",
         "classnames": "^2.2.6"
     },
     "devDependencies": {

+ 1 - 1
packages/semi-animation-styled/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-animation-styled",
-    "version": "2.72.0",
+    "version": "2.74.0",
     "description": "semi styled animation",
     "keywords": [
         "semi",

+ 1 - 1
packages/semi-animation/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-animation",
-    "version": "2.72.0",
+    "version": "2.74.0",
     "description": "animation base library for semi-ui",
     "keywords": [
         "animation",

+ 1 - 1
packages/semi-eslint-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "eslint-plugin-semi-design",
-    "version": "2.72.0",
+    "version": "2.74.0",
     "description": "semi ui eslint plugin",
     "keywords": [
         "semi",

+ 217 - 0
packages/semi-foundation/audioPlayer/audioPlayer.scss

@@ -0,0 +1,217 @@
+@import './variables.scss';
+
+$module: #{$prefix}-audio-player;
+
+.#{$module} {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: $gap-audio-player-large;
+    max-width: $width-audio-player-max;
+    height: $height-audio-player;
+    background: $color-audio-player-background;
+    &-control {
+        display: flex;
+        align-items: center;
+        gap: $gap-audio-player-medium;
+    }
+
+    &-control-button-icon {
+        color: $color-audio-player-control-icon;
+    }
+
+    &-control-button-play {
+        background: $color-audio-player-control-icon !important;
+        color: $color-audio-player-control-icon-play !important;
+    }
+
+    &-control-button-play-disabled {
+        background: $color-audio-player-disabled-bg !important;
+        color: $color-audio-player-disabled-text !important;
+    }
+
+    &-slider-container {
+        width: $width-audio-player-slider;
+        height: 100%;
+    }
+
+    &-info-container {
+        display: flex;
+        align-items: center;
+        gap: $gap-audio-player-medium;
+    }
+
+    &-info {
+        display: flex;
+        flex-direction: column;
+        gap: $gap-audio-player-small;
+    }
+
+    &-info-title {
+        font-size: $font-size-audio-player-text;
+        color: $color-audio-player-font-color;
+        font-weight: 600;
+        display: flex;
+        align-items: center;
+    }
+    &-info-time {
+        width: 100%;
+        height: $height-audio-player-time;
+        font-size: $font-size-audio-player-text;
+        color: $color-audio-player-font-color;
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        gap: $gap-audio-player-small;
+        user-select: none;
+    }
+    &-control-speed {
+        width: $width-audio-player-speed;
+        height: $height-audio-player-speed;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: $gap-audio-player-small;
+        background: $color-audio-player-font-color-speed;
+        border-radius: $border-radius-audio-player-speed;
+        font-size: $font-size-audio-player-small;
+        line-height: $line-height-audio-player-small;
+        color: var(--semi-color-default);
+        font-weight: 600;
+        user-select: none;
+    }
+
+    &-control-speed-menu {
+        background: $color-audio-player-font-color-speed;
+        width: $width-audio-player-speed-menu;
+    }
+
+    &-control-speed-menu-item {
+        color: $color-audio-player-text-default;
+    }
+
+    &-control-speed-menu-item:hover {
+        background: var(--semi-color-tertiary-active) !important;
+    }
+
+    &-control-volume {
+        width: $width-audio-player-volume;
+        height: $height-audio-player-volume;
+        background: $color-audio-player-font-color-speed;
+        border-radius: $border-radius-audio-player-volume;
+        padding: 4px 0;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        flex-direction: column;
+        gap: $gap-audio-player-small * 2;
+    }
+
+    &-control-volume-title {
+        font-size: $font-size-audio-player-small;
+        line-height: $line-height-audio-player-small;
+        color: $color-audio-player-text-default;
+        font-weight: 600;
+        user-select: none;
+    }
+
+    &-error {
+        display: flex;
+        align-items: center;
+        gap: $gap-audio-player-small;
+        margin-left: 4px;
+        color: var(--semi-color-danger);
+    }
+
+    &-light {
+        background: $color-audio-player-background-light;
+        border: 1px solid var(--semi-color-border);
+
+        .#{$module}-control-button-icon {
+            color: $color-audio-player-control-icon-light;
+        }
+
+        .#{$module}-control-button-play {
+            background: $color-audio-player-control-icon-light !important;
+            color: $color-audio-player-control-icon-play-light !important;
+        }
+
+        .#{$module}-control-button-play-disabled {
+            background: $color-audio-player-light-disabled-bg !important;
+            color: $color-audio-player-light-disabled-text !important;
+        }
+
+        .#{$module}-info-title,
+        .#{$module}-info-time {
+            color: $color-audio-player-font-color-light;
+        }
+
+        .#{$module}-control-speed-menu-item,
+        .#{$module}-control-volume-title {
+            color: $color-audio-player-light-text;
+        }
+
+        .#{$module}-control-speed-menu-item:hover {
+            background: $color-audio-player-light-hover-bg !important;
+        }
+    }
+}
+
+.#{$module}-slider {
+    background: $color-audio-player-slider-bg;
+    border-radius: $border-radius-audio-player-slider;
+    position: relative;
+    
+    &-light {
+        background: $color-audio-player-slider-bg-light;
+    }
+
+    &-wrapper {
+        position: relative;
+        cursor: pointer;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        &-vertical {
+            width: 100%;
+        }
+        &-horizontal {
+            height: 100%;
+        }
+    }
+
+    &-vertical {
+        width: $width-audio-player-slider-bar;
+        height: 100%;
+    }
+
+    &-horizontal {
+        width: 100%;
+        height: $width-audio-player-slider-bar;
+    }
+
+    &-progress {
+        position: absolute;
+        background: $color-audio-player-slider-progress;
+        border-radius: $border-radius-audio-player-slider;
+    }
+
+    &-progress-vertical {
+        bottom: 0;
+    }
+
+    &-progress-horizontal {
+        left: 0;
+    }
+
+    &-dot {
+        position: absolute;
+        width: $size-audio-player-slider-dot;
+        height: $size-audio-player-slider-dot;
+        background: $color-audio-player-slider-dot-bg;
+        border: 1px solid var(--semi-color-primary);
+        box-shadow: 0px 0px 4px 0px var(--semi-color-shadow);
+        border-radius: 50%;
+        transition: opacity 0.2s;
+    }
+}

+ 7 - 0
packages/semi-foundation/audioPlayer/constants.ts

@@ -0,0 +1,7 @@
+import { BASE_CLASS_PREFIX } from '../base/constants';
+
+const cssClasses = {
+    PREFIX: `${BASE_CLASS_PREFIX}-audio-player`,
+};
+
+export { cssClasses };

+ 103 - 0
packages/semi-foundation/audioPlayer/foundation.ts

@@ -0,0 +1,103 @@
+
+
+import BaseFoundation, { DefaultAdapter } from '../base/foundation';
+
+export interface AudioPlayerAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
+    init: () => void;
+    resetAudioState: () => void;
+    handleStatusClick: () => void;
+    handleTimeUpdate: () => void;
+    handleTrackChange: (direction: 'next' | 'prev') => void;
+    getAudioRef: () => HTMLAudioElement;
+    handleTimeChange: (value: number) => void;
+    handleSpeedChange: (value: { label: string; value: number }) => void;
+    handleSeek: (direction: number) => void;
+    handleRefresh: () => void;
+    handleVolumeChange: (value: number) => void;
+    destroy: () => void
+}
+
+class AudioPlayerFoundation extends BaseFoundation<AudioPlayerAdapter> {
+    constructor(adapter: AudioPlayerAdapter) {
+        super({ ...AudioPlayerFoundation, ...adapter });
+    }
+
+    initAudioState() {
+        const audioElement = this.getAudioRef();
+        const props = this.getProps();
+
+        this.setState({
+            totalTime: audioElement?.duration || 0,
+            isPlaying: props.autoPlay,
+            volume: audioElement?.volume * 100 || 100,
+            currentRate: { label: '1.0x', value: audioElement?.playbackRate || 1 },
+        });
+    }
+
+    endHandler() {
+        const props = this.getProps();
+        if (Array.isArray(props.audioUrl)) {
+            this.handleTrackChange('next');
+        } else {
+            this.setState({
+                isPlaying: false,
+            });
+        }
+    }
+
+    errorHandler() {
+        this.setState({
+            error: true,
+        });
+    }
+
+    init() {
+        this._adapter.init();
+    }
+
+    destroy() {
+        this._adapter.destroy();
+    }
+
+    resetAudioState() {
+        this._adapter.resetAudioState();
+    }
+
+    handleStatusClick() {
+        this._adapter.handleStatusClick();
+    }
+
+    handleTimeUpdate() {
+        this._adapter.handleTimeUpdate();
+    }
+
+    handleTrackChange(direction: 'next' | 'prev') {
+        this._adapter.handleTrackChange(direction);
+    }
+
+    getAudioRef() {
+        return this._adapter.getAudioRef();
+    }
+
+    handleTimeChange(value: number) {
+        this._adapter.handleTimeChange(value);
+    }
+
+    handleSpeedChange(value: { label: string; value: number }) {
+        this._adapter.handleSpeedChange(value);
+    }
+
+    handleSeek(direction: number) {
+        this._adapter.handleSeek(direction);
+    }
+
+    handleRefresh() {
+        this._adapter.handleRefresh();
+    }
+
+    handleVolumeChange(value: number) {
+        this._adapter.handleVolumeChange(value);
+    }
+}
+
+export default AudioPlayerFoundation;

+ 55 - 0
packages/semi-foundation/audioPlayer/variables.scss

@@ -0,0 +1,55 @@
+$color-audio-player-background: rgba(var(--semi-grey-9), .8);
+$color-audio-player-control-icon: var(--semi-color-bg-0);
+$color-audio-player-control-icon-play: var(--semi-color-text-0);
+$color-audio-player-font-color: var(--semi-color-bg-0);
+$color-audio-player-font-color-speed: rgba(var(--semi-grey-8), 1);
+
+
+$color-audio-player-background-light: var(--semi-color-bg-0);
+$color-audio-player-control-icon-light: rgba(var(--semi-grey-9), 1);
+$color-audio-player-control-icon-play-light: var(--semi-color-bg-0);
+$color-audio-player-font-color-light: rgba(var(--semi-grey-9), 1);
+
+$font-size-audio-player-text: 14px;
+
+$gap-audio-player-small: 4px;
+$gap-audio-player-medium: 16px;
+$gap-audio-player-large: 24px;
+
+// Size variables
+$width-audio-player-max: 1440px;
+$height-audio-player: 78px;
+$width-audio-player-slider: 323px;
+$width-audio-player-speed: 40px;
+$height-audio-player-speed: 24px;
+$width-audio-player-speed-menu: 65px;
+$width-audio-player-volume: 43px;
+$height-audio-player-volume: 164px;
+$height-audio-player-time: 22px;
+
+// Border radius
+$border-radius-audio-player-speed: 3px;
+$border-radius-audio-player-volume: 4px;
+$border-radius-audio-player-slider: 9999px;
+
+// Font sizes
+$font-size-audio-player-small: 12px;
+$line-height-audio-player-small: 16px;
+
+// Slider dimensions
+$width-audio-player-slider-bar: 4px;
+$size-audio-player-slider-dot: 16px;
+
+// Colors
+$color-audio-player-disabled-bg: rgba(var(--semi-grey-0), .35);
+$color-audio-player-slider-bg: rgba(var(--semi-grey-5), 1);
+$color-audio-player-slider-bg-light: rgba(var(--semi-grey-2), 1);
+$color-audio-player-slider-progress: rgba(var(--semi-blue-4), 1);
+$color-audio-player-slider-dot-bg: rgba(var(--semi-white), 1);
+
+$color-audio-player-disabled-text: var(--semi-color-grey-7);
+$color-audio-player-text-default: var(--semi-color-default);
+$color-audio-player-light-disabled-bg: var(--semi-color-disabled-text);
+$color-audio-player-light-disabled-text: rgba(var(--semi-white), 1);
+$color-audio-player-light-text: rgba(var(--semi-grey-9), 1);
+$color-audio-player-light-hover-bg: rgba(var(--semi-grey-1), 1);

+ 8 - 0
packages/semi-foundation/button/iconButton.scss

@@ -4,6 +4,14 @@
 $module: #{$prefix}-button;
 
 .#{$module} {
+    @keyframes #{$prefix}-animation-rotate {
+        from {
+            transform: rotate(0);
+        }
+        to {
+            transform: rotate(360deg);
+        }
+    }
     &.#{$module}-with-icon {
         display: inline-flex;
         align-items: center;

+ 26 - 0
packages/semi-foundation/cropper/constants.ts

@@ -0,0 +1,26 @@
+import { BASE_CLASS_PREFIX } from '../base/constants';
+
+const moduleName = `${BASE_CLASS_PREFIX}-cropper`;
+
+const cssClasses = {
+    PREFIX: `${moduleName}`,
+    IMG: `${moduleName}-img`,
+    IMG_WRAPPER: `${moduleName}-img-wrapper`,
+    CROPPER_BOX: `${moduleName}-box`,
+    CROPPER_VIEW_BOX: `${moduleName}-view-box`,
+    CROPPER_VIEW_BOX_ROUND: `${moduleName}-view-box-round`,
+    CROPPER_IMG: `${moduleName}-view-img`,
+    MASK: `${moduleName}-mask`,
+    CORNER: `${moduleName}-box-corner`,
+};
+
+const strings = {
+    shape: ['rect', 'round', 'roundRect'],
+    corner: ['tl', 'tm', 'tr',
+        'ml', 'mr',
+        'bl', 'bm', 'br'],
+    roundCorner: ['tm', 'ml', 'mr', 'bm'],
+
+};
+
+export { cssClasses, strings };

+ 116 - 0
packages/semi-foundation/cropper/cropper.scss

@@ -0,0 +1,116 @@
+@import "./variables.scss";
+$module: #{$prefix}-cropper;
+
+$half_corner_width: calc($width-cropper_box_corner / 2);
+
+.#{$module} {
+    position: relative;
+
+    &-img {
+      position: absolute;
+      user-select: none;
+    }
+
+    &-img-wrapper {
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      overflow: hidden;
+    }
+
+    &-mask {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      background: $color-cropper_mask-bg;
+      cursor: move;;
+    }
+
+    &-box {
+      position: absolute;
+      outline: $width-cropper_box-outline solid $color-cropper_box-outline;
+
+      &-corner {
+        position: absolute;
+        background: $color-cropper_box_corner-bg;
+        width: $width-cropper_box_corner;
+        height: $width-cropper_box_corner;
+        z-index: 1;
+
+        &-tl {
+          top: -$half_corner_width;
+          left: -$half_corner_width;
+          cursor: nwse-resize;
+        }
+
+        &-tr {
+          top: -$half_corner_width;
+          right: -$half_corner_width;
+          cursor: nesw-resize;
+        }
+
+        &-tm {
+          top: -$half_corner_width;
+          left: 50%;
+          margin-left: -$half_corner_width;
+          cursor: ns-resize;
+        }
+
+        &-ml {
+          top: 50%;
+          left: -$half_corner_width;
+          margin-top: -$half_corner_width;
+          cursor: ew-resize;
+        }
+
+        &-mr {
+          top: 50%;
+          right: -$half_corner_width;
+          margin-top: -$half_corner_width;
+          cursor: ew-resize;
+        }
+
+        &-bl {
+          bottom: -$half_corner_width;
+          left: -$half_corner_width;
+          cursor: nesw-resize;
+        }
+
+        &-bm {
+          bottom: -$half_corner_width;
+          left: 50%;
+          margin-left: -$half_corner_width; 
+          cursor: ns-resize;
+        }
+
+        &-br {
+          bottom: -$half_corner_width;
+          right: -$half_corner_width;
+          cursor: nwse-resize
+        }
+      }
+    }
+
+    &-view-box {
+      position: absolute;
+      overflow: hidden;
+      cursor: move;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+
+      &-round {
+        border-radius: 50%;
+      }
+    }
+
+    &-view-img {
+      // position: absolute;
+      user-select: none;
+    }
+}

+ 821 - 0
packages/semi-foundation/cropper/foundation.ts

@@ -0,0 +1,821 @@
+import BaseFoundation, { DefaultAdapter } from "../base/foundation";
+import { getMiddle, getAspectHW } from "./utils";
+
+export interface CropperAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
+    getContainer: () => HTMLElement;
+    notifyZoomChange: (zoom: number) => void;
+    getImg: () => HTMLImageElement
+}
+
+interface Point {
+    x: number;
+    y: number
+}
+
+export interface ImageData {
+    originalWidth: number;
+    originalHeight: number;
+    scale: number
+}
+
+export interface ImageDataState {
+    width: number;
+    height: number;
+    centerPoint: Point
+}
+
+export interface CropperBox {
+    width: number;
+    height: number;
+    centerPoint: Point
+}
+
+export interface ContainerData {
+    width: number;
+    height: number
+}
+
+export interface CropperBoxBorder {
+    borderTop: number;
+    borderLeft: number
+}
+
+export default class CropperFoundation <P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<CropperAdapter<P, S>, P, S> {
+    imgData: ImageData;
+    containerData: ContainerData;
+    boxMoveDir: string;
+    cropperBoxMoveStart: Point;
+    imgMoveStart: Point;
+    moveRange: {
+        xMax: number;
+        xMin: number;
+        yMax: number;
+        yMin: number
+    };
+    boxMoveParam: {
+        paramX: number;
+        paramY: number
+    }
+    cropperBox: CropperBoxBorder;
+    rangeX: [number, number];
+    rangeY: [number, number];
+    initial: boolean;
+    
+    constructor(adapter: CropperAdapter<P, S>) {
+        super({ ...adapter });
+
+        this.containerData = {} as ContainerData;
+        this.imgData = {} as ImageData;
+        this.boxMoveDir = '';
+        this.boxMoveParam = {
+            paramX: 0,
+            paramY: 0,
+        };
+        this.rangeX = null;
+        this.rangeY = null;
+        this.initial = false;
+    }
+
+    init() {
+        // 获取容器的宽高
+        // get cropping Container 's width & height
+        const container = this._adapter.getContainer();
+        this.containerData.width = container.clientWidth;
+        this.containerData.height = container.clientHeight;
+        this.cropperBoxMoveStart = null;
+    }
+
+    destroy() {
+        this.unBindMoveEvent();
+        this.unBindResizeEvent();
+    }
+
+    getImgDataWhenResize = (ratio: number) => {
+        const { imgData } = this.getStates();
+        const newImgData = {
+            width: imgData.width * ratio,
+            height: imgData.height * ratio,
+            centerPoint: {
+                x: imgData.centerPoint.x * ratio,
+                y: imgData.centerPoint.y * ratio,
+            }
+        };
+        this.imgData.scale *= ratio;
+        return newImgData;
+    }
+
+    getCropperBoxWhenResize = (ratio: number, newContainerData: ContainerData) => {
+        const { cropperBox } = this.getStates();
+        const { aspectRatio } = this.getProps();
+        const tempCropperBox = {
+            width: cropperBox.width * ratio,
+            height: cropperBox.height * ratio,
+            centerPoint: {
+                x: cropperBox.centerPoint.x * ratio,
+                y: cropperBox.centerPoint.y * ratio,
+            }
+        };
+        let xMin = tempCropperBox.centerPoint.x - tempCropperBox.width / 2;
+        let xMax = tempCropperBox.centerPoint.x + tempCropperBox.width / 2;
+        let yMin = tempCropperBox.centerPoint.y - tempCropperBox.height / 2;
+        let yMax = tempCropperBox.centerPoint.y + tempCropperBox.height / 2;
+        if (aspectRatio) {
+            if (xMax > newContainerData.width) {
+                xMax = newContainerData.width;
+                xMin = tempCropperBox.width > newContainerData.width ? 
+                    0 : newContainerData.width - tempCropperBox.width;
+                tempCropperBox.width = xMax - xMin;
+                tempCropperBox.height = tempCropperBox.width / aspectRatio;
+                yMax = yMin + tempCropperBox.height;
+            }
+            if (yMax > newContainerData.height) {
+                yMax = newContainerData.height;
+                yMin = tempCropperBox.height > newContainerData.height ? 
+                    0 : newContainerData.height - tempCropperBox.height;
+                tempCropperBox.height = yMax - yMin;
+                tempCropperBox.width = tempCropperBox.height * aspectRatio;
+                xMax = xMin + tempCropperBox.width;
+            }
+        } else {
+            if (xMax > newContainerData.width) {
+                xMax = newContainerData.width;
+                xMin = tempCropperBox.width > newContainerData.width ? 
+                    0 : newContainerData.width - tempCropperBox.width;
+            }
+            if (yMax > newContainerData.height) {
+                yMax = newContainerData.height;
+                yMin = tempCropperBox.height > newContainerData.height ? 
+                    0 : newContainerData.height - tempCropperBox.height;
+            }
+        }
+        return {
+            width: xMax - xMin,
+            height: yMax - yMin,
+            centerPoint: {
+                x: (xMax + xMin) / 2,
+                y: (yMax + yMin) / 2,
+            }
+        };
+    }
+
+
+    handleResize = () => {
+        const { loaded } = this.getStates();
+        if (!this.initial) {
+            this.initial = true;
+            return;
+        }
+        if (!loaded) {
+            return;
+        }
+        const container = this._adapter.getContainer();
+        const newContainerData = {
+            width: container.clientWidth,
+            height: container.clientHeight,
+        };
+        const ratio = newContainerData.width / this.containerData.width;
+        const newImgData = this.getImgDataWhenResize(ratio);
+        const newCropperBox = this.getCropperBoxWhenResize(ratio, newContainerData);
+       
+        this.containerData = newContainerData;
+        this.setState({
+            imgData: newImgData,
+            cropperBox: newCropperBox,
+        } as any);
+    }
+
+    handleImageLoad = (e: any) => {
+        /**
+         * 1. 图片加载完成后,获得图片的原始大小
+         * 2. 计算图片的缩放比例,中心点位置
+         */
+        const { naturalWidth, naturalHeight } = e.target;
+        const { width: containerWidth, height: containerHeight } = this.containerData;
+        this.imgData.originalWidth = naturalWidth;
+        this.imgData.originalHeight = naturalHeight;
+        let scale = 1;
+        const newImgDataState = {} as ImageDataState;
+        /* 计算图片加载后的初始显示尺寸 */
+        if (naturalWidth / containerWidth > naturalHeight / containerHeight) {
+            scale = containerWidth / naturalWidth;
+            newImgDataState.width = containerWidth;
+            newImgDataState.height = naturalHeight * scale; 
+        } else {
+            scale = containerHeight / naturalHeight;
+            newImgDataState.width = naturalWidth * scale;
+            newImgDataState.height = containerHeight;
+        }
+        this.imgData.scale = scale;
+        newImgDataState.centerPoint = {} as Point;
+        newImgDataState.centerPoint.x = containerWidth / 2;
+        newImgDataState.centerPoint.y = containerHeight / 2;
+        /* 计算裁切框大小 */
+        const newCropperBoxState = {} as CropperBox;
+        const { defaultAspectRatio, aspectRatio } = this.getProps();
+        const calcAspect = aspectRatio || defaultAspectRatio;
+        if (containerWidth / containerHeight > calcAspect) {
+            newCropperBoxState.width = containerHeight * calcAspect;
+            newCropperBoxState.height = containerHeight;
+        } else {
+            newCropperBoxState.width = containerWidth;
+            newCropperBoxState.height = containerWidth / calcAspect;
+        }
+        newCropperBoxState.centerPoint = {} as Point;
+        newCropperBoxState.centerPoint.x = containerWidth / 2;
+        newCropperBoxState.centerPoint.y = containerHeight / 2;
+        this.setState({
+            imgData: newImgDataState,
+            cropperBox: newCropperBoxState,
+            loaded: true,
+        } as any);
+    }
+
+    handleWheel = (e: any) => {
+        // 防止双手缩放导致页面被放大
+        e.preventDefault();
+        const { imgData, zoom: currZoom } = this.getStates();
+        const { maxZoom, minZoom, zoomStep } = this.getProps();
+
+        let _zoom: number;
+        if (e.deltaY < 0) {
+            /* zoom in */
+            if (currZoom + zoomStep <= maxZoom) {
+                _zoom = Number((currZoom + zoomStep).toFixed(2));
+            }
+        } else if (e.deltaY > 0) {
+            /* zoom out */
+            if (currZoom - zoomStep >= minZoom) {
+                _zoom = Number((currZoom - zoomStep).toFixed(2));
+            }
+        }
+        if (_zoom === undefined) {
+            return;
+        }
+        const boundingRect = e.currentTarget.getBoundingClientRect();
+        const offsetX = e.clientX - boundingRect.left;
+        const offsetY = e.clientY - boundingRect.top;
+        const scaleCenter = {
+            x: offsetX,
+            y: - offsetY,
+        };
+        
+        // 计算新的中心点位置
+        const currentPoint = { ...imgData.centerPoint } as Point;
+        currentPoint.y = - currentPoint.y;
+
+        const newCenterPoint = {
+            x: (currentPoint.x - scaleCenter.x) / currZoom * _zoom + scaleCenter.x,
+            y: - [(currentPoint.y - scaleCenter.y) / currZoom * _zoom + scaleCenter.y],
+        };
+
+        const newWidth = imgData.width / currZoom * _zoom;
+        const newHeight = imgData.height / currZoom * _zoom;
+       
+        const newImgDataState = {
+            width: newWidth,
+            height: newHeight,
+            centerPoint: newCenterPoint
+        };
+        this.setState({
+            imgData: newImgDataState,
+            zoom: _zoom
+        } as any);
+
+        this._adapter.notifyZoomChange(_zoom);
+    }
+
+    getMoveParamByDir(dir: string) {
+        let paramX = 0, paramY = 0;
+        switch (dir) {
+            case 'tl':
+                paramX = -1; paramY = -1; break;
+            case 'tm':
+                paramY = -1; break;
+            case 'tr':
+                paramX = 1; paramY = -1; break;
+            case 'ml':
+                paramX = -1; break;
+            case 'mr':
+                paramX = 1; break;
+            case 'bl':
+                paramX = -1; paramY = 1; break;
+            case 'bm':
+                paramY = 1; break;
+            case 'br':
+                paramX = 1; paramY = 1; break;
+            default:
+                break; 
+        }
+        return {
+            paramX,
+            paramY
+        };
+    }
+
+    getRangeForAspectChange = () => {
+        const { cropperBox } = this.getStates();
+        const { aspectRatio } = this.getProps();
+        const { width: containerWidth, height: containerHeight } = this.containerData;
+        // 可能的最大宽高
+        let height: number, width: number;
+        // 裁剪框当前的位置
+        const xMin = cropperBox.centerPoint.x - cropperBox.width / 2;
+        const xMax = cropperBox.centerPoint.x + cropperBox.width / 2;
+        const yMin = cropperBox.centerPoint.y - cropperBox.height / 2;
+        const yMax = cropperBox.centerPoint.y + cropperBox.height / 2;
+        switch (this.boxMoveDir) {
+            case 'tl':
+                height = yMax;
+                width = xMax;
+                [width, height] = getAspectHW(width, height, aspectRatio);
+                this.rangeX = [xMax - width, xMax];
+                this.rangeY = [yMax - height, yMax];
+                break;
+            case 'tm':
+                height = yMax;
+                const leftHalfWidth = cropperBox.centerPoint.x;
+                const rightHalfWidth = containerWidth - cropperBox.centerPoint.x;
+                width = 2 * (leftHalfWidth < rightHalfWidth ? leftHalfWidth : rightHalfWidth);
+                [width, height] = getAspectHW(width, height, aspectRatio);
+                this.rangeX = [
+                    cropperBox.centerPoint.x - width / 2, 
+                    cropperBox.centerPoint.x + width / 2
+                ];
+                this.rangeY = [yMax - height, yMax];
+                break;
+            case 'tr':
+                height = yMax;
+                width = containerWidth - xMin;
+                [width, height] = getAspectHW(width, height, aspectRatio);
+                this.rangeX = [xMin, xMin + width];
+                this.rangeY = [yMax - height, yMax];
+                break;
+            case 'ml':
+                width = xMax;
+                const topHalfHeight = cropperBox.centerPoint.y;
+                const bottomHalfHeight = containerHeight - cropperBox.centerPoint.y;
+                height = 2 * (topHalfHeight < bottomHalfHeight ? topHalfHeight : bottomHalfHeight);
+                [width, height] = getAspectHW(width, height, aspectRatio);
+                this.rangeX = [xMax - width, xMax];
+                this.rangeY = [
+                    cropperBox.centerPoint.y - height / 2, 
+                    cropperBox.centerPoint.y + height / 2
+                ];
+                break;
+            case 'mr':
+                width = containerWidth - xMin;
+                const topHalfHeight2 = cropperBox.centerPoint.y;
+                const bottomHalfHeight2 = containerHeight - cropperBox.centerPoint.y;
+                height = 2 * (topHalfHeight2 < bottomHalfHeight2 ? topHalfHeight2 : bottomHalfHeight2);
+                [width, height] = getAspectHW(width, height, aspectRatio);
+                this.rangeX = [xMin, xMin + width];
+                this.rangeY = [
+                    cropperBox.centerPoint.y - height / 2,
+                    cropperBox.centerPoint.y + height / 2
+                ];
+                break;
+            case 'bl':
+                height = containerHeight - yMin;
+                width = xMax;
+                [width, height] = getAspectHW(width, height, aspectRatio);
+                this.rangeX = [xMax - width, xMax];
+                this.rangeY = [yMin, yMin + height];
+                break;
+            case 'bm':
+                height = containerHeight - yMin;
+                const leftHalfWidth2 = cropperBox.centerPoint.x;
+                const rightHalfWidth2 = containerWidth - cropperBox.centerPoint.x;
+                width = 2 * (leftHalfWidth2 < rightHalfWidth2 ? leftHalfWidth2 : rightHalfWidth2);
+                [width, height] = getAspectHW(width, height, aspectRatio); 
+                this.rangeX = [
+                    cropperBox.centerPoint.x - width / 2,
+                    cropperBox.centerPoint.x + width / 2,
+                ];
+                this.rangeY = [yMin, yMin + height]; 
+                break;
+            case 'br':
+                height = containerHeight - yMin;
+                width = containerWidth - xMin;
+                [width, height] = getAspectHW(width, height, aspectRatio);
+                this.rangeX = [xMin, xMin + width];
+                this.rangeY = [yMin, yMin + height];
+                break;
+            default:
+                break;
+        }
+    }
+
+    handleCornerMouseDown = (e: any) => {
+        const currentTarget = e.currentTarget;
+        if (!currentTarget) {
+            return;
+        }
+        e.preventDefault();
+        const dir = currentTarget.dataset.dir;
+        this.boxMoveDir = dir;
+        this.boxMoveParam = this.getMoveParamByDir(dir);
+        this.bindResizeEvent();
+        const { aspectRatio } = this.getProps();
+        if (aspectRatio) {
+            this.getRangeForAspectChange();
+        } else {
+            this.rangeX = [0, this.containerData.width];
+            this.rangeY = [0, this.containerData.height];
+        }
+        
+    }
+
+    bindResizeEvent = () => {
+        const { aspectRatio } = this.getProps();
+        document.addEventListener('mousemove', aspectRatio ? this.handleCornerAspectMouseMove : this.handleCornerMouseMove);
+        document.addEventListener('mouseup', this.handleCornerMouseUp);
+    }
+
+    unBindResizeEvent = () => {
+        const { aspectRatio } = this.getProps();
+        document.removeEventListener('mousemove', aspectRatio ? this.handleCornerAspectMouseMove : this.handleCornerMouseMove);
+        document.removeEventListener('mouseup', this.handleCornerMouseUp);
+    }
+
+    viewIMGDragStart = (e: any) => {
+        e.preventDefault();
+    }
+
+    handleCornerAspectMouseMove = (e: any) => {
+        e.preventDefault();
+        const { clientX, clientY } = e;
+        const { cropperBox } = this.getStates();
+        const { aspectRatio } = this.getProps();
+        const boundingRect = this._adapter.getContainer().getBoundingClientRect();
+        const newCropperBoxPos = {
+            width: cropperBox.width,
+            height: cropperBox.height,
+            centerPoint: { ...cropperBox.centerPoint }
+        };
+        let offsetX: number, offsetY: number;
+        if (['ml', 'mr'].includes(this.boxMoveDir)) {
+            offsetX = getMiddle(clientX - boundingRect.left, this.rangeX);
+        } else {
+            offsetY = getMiddle(clientY - boundingRect.top, this.rangeY);
+        }
+        switch (this.boxMoveDir) {
+            case 'tl':
+                newCropperBoxPos.height = this.rangeY[1] - offsetY;
+                newCropperBoxPos.width = newCropperBoxPos.height * aspectRatio;
+                newCropperBoxPos.centerPoint = {
+                    x: this.rangeX[1] - newCropperBoxPos.width / 2,
+                    y: this.rangeY[1] - newCropperBoxPos.height / 2,
+                };
+                break;
+            case 'tm':
+                newCropperBoxPos.height = this.rangeY[1] - offsetY;
+                newCropperBoxPos.width = newCropperBoxPos.height * aspectRatio;
+                newCropperBoxPos.centerPoint = {
+                    x: cropperBox.centerPoint.x,
+                    y: this.rangeY[1] - newCropperBoxPos.height / 2,
+                };
+                break;
+            case 'tr':
+                newCropperBoxPos.height = this.rangeY[1] - offsetY;
+                newCropperBoxPos.width = newCropperBoxPos.height * aspectRatio;
+                newCropperBoxPos.centerPoint = {
+                    x: this.rangeX[0] + newCropperBoxPos.width / 2,
+                    y: this.rangeY[1] - newCropperBoxPos.height / 2,
+                };
+                break;
+            case 'ml':
+                newCropperBoxPos.width = this.rangeX[1] - offsetX;
+                newCropperBoxPos.height = newCropperBoxPos.width / aspectRatio;
+                newCropperBoxPos.centerPoint = {
+                    x: this.rangeX[1] - newCropperBoxPos.width / 2,
+                    y: cropperBox.centerPoint.y,
+                };
+                break;
+            case 'mr':
+                newCropperBoxPos.width = offsetX - this.rangeX[0];
+                newCropperBoxPos.height = newCropperBoxPos.width / aspectRatio;
+                newCropperBoxPos.centerPoint = {
+                    x: this.rangeX[0] + newCropperBoxPos.width / 2,
+                    y: cropperBox.centerPoint.y,
+                };
+                break;
+            case 'bl':
+                newCropperBoxPos.height = offsetY - this.rangeY[0];
+                newCropperBoxPos.width = newCropperBoxPos.height * aspectRatio;
+                newCropperBoxPos.centerPoint = {
+                    x: this.rangeX[1] - newCropperBoxPos.width / 2,
+                    y: this.rangeY[0] + newCropperBoxPos.height / 2,
+                };
+                break;
+            case 'bm':
+                newCropperBoxPos.height = offsetY - this.rangeY[0];
+                newCropperBoxPos.width = newCropperBoxPos.height * aspectRatio;
+                newCropperBoxPos.centerPoint = {
+                    x: cropperBox.centerPoint.x,
+                    y: this.rangeY[0] + newCropperBoxPos.height / 2,
+                };
+                break;
+            case 'br':
+                newCropperBoxPos.height = offsetY - this.rangeY[0];
+                newCropperBoxPos.width = newCropperBoxPos.height * aspectRatio;
+                newCropperBoxPos.centerPoint = {
+                    x: this.rangeX[0] + newCropperBoxPos.width / 2,
+                    y: this.rangeY[0] + newCropperBoxPos.height / 2,
+                };
+                break;
+            default:
+                break;
+        }
+        if (newCropperBoxPos.height === 0 && newCropperBoxPos.width === 0) {
+            this.changeDir();
+            this.getRangeForAspectChange();
+        } 
+        this.setState({
+            cropperBox: newCropperBoxPos
+        } as any);
+    }
+
+    changeDir = () => {
+        if (this.boxMoveDir.includes('t')) {
+            this.boxMoveDir = this.boxMoveDir.replace('t', 'b');
+        } else if (this.boxMoveDir.includes('b')) {
+            this.boxMoveDir = this.boxMoveDir.replace('b', 't');
+        }
+        if (this.boxMoveDir.includes('l')) {
+            this.boxMoveDir = this.boxMoveDir.replace('l', 'r');
+        } else if (this.boxMoveDir.includes('r')) {
+            this.boxMoveDir = this.boxMoveDir.replace('r', 'l');
+        }
+    }
+
+    handleCornerMouseMove = (e: any) => {
+        e.preventDefault();
+        const { clientX, clientY } = e;
+        const { cropperBox } = this.getStates();
+        const boundingRect = this._adapter.getContainer().getBoundingClientRect();
+        let offsetX = getMiddle(clientX - boundingRect.left, this.rangeX);
+        let offsetY = getMiddle(clientY - boundingRect.top, this.rangeY);
+        const newCropperBoxPos = {
+            width: cropperBox.width,
+            height: cropperBox.height,
+            centerPoint: {
+                x: cropperBox.centerPoint.x,
+                y: cropperBox.centerPoint.y
+            }
+        };
+        const { paramX, paramY } = this.boxMoveParam;
+        let x: number, y: number;
+        if (paramX) {
+            x = cropperBox.centerPoint.x + paramX * cropperBox.width / 2;
+            newCropperBoxPos.width = cropperBox.width + paramX * (offsetX - x);
+            if (newCropperBoxPos.width < 0) {
+                newCropperBoxPos.width = - newCropperBoxPos.width;
+                this.boxMoveParam.paramX = -paramX;
+            }
+            newCropperBoxPos.centerPoint.x = offsetX - paramX * newCropperBoxPos.width / 2;
+        }
+        if (paramY) {
+            y = cropperBox.centerPoint.y + paramY * cropperBox.height / 2;
+            newCropperBoxPos.height = cropperBox.height + paramY * (offsetY - y);
+            if (newCropperBoxPos.height < 0) {
+                newCropperBoxPos.height = -newCropperBoxPos.height;
+                this.boxMoveParam.paramY = -paramY;
+            }
+            newCropperBoxPos.centerPoint.y = offsetY - paramY * newCropperBoxPos.height / 2;
+        }
+        
+        this.setState({
+            cropperBox: newCropperBoxPos
+        } as any);
+    }
+
+    handleCornerMouseUp = (e: any) => {
+        this.boxMoveParam = { paramX: 0, paramY: 0 };
+        this.unBindResizeEvent();
+    }
+
+    handleCropperBoxMouseDown = (e: any) => {
+        const target = e.target;
+        const { cropperBox } = this.getStates();
+        const container = this._adapter.getContainer();
+        const boundingRect = container.getBoundingClientRect();
+        if (target.dataset.dir) {
+            // 如果鼠标是落在了corner上,那么不做任何操作
+            return;
+        }
+        // 移动裁切框
+        this.cropperBoxMoveStart = {
+            x: e.clientX,
+            y: e.clientY
+        };
+        this.bindMoveEvent();
+        // 计算 cropperBox 中心点移动范围
+        this.moveRange = {
+            xMin: cropperBox.width / 2,
+            xMax: boundingRect.width - cropperBox.width / 2,
+            yMin: cropperBox.height / 2,
+            yMax: boundingRect.height - cropperBox.height / 2,
+        };
+    }
+
+    bindMoveEvent = () => {
+        document.addEventListener('mousemove', this.handleCropperBoxMouseMove);
+        document.addEventListener('mouseup', this.handleCropperBoxMouseUp);
+    }
+
+    unBindMoveEvent = () => {
+        document.removeEventListener('mousemove', this.handleCropperBoxMouseMove);
+        document.removeEventListener('mouseup', this.handleCropperBoxMouseUp);
+    }
+
+    handleCropperBoxMouseMove = (e: any) => {
+        if (!this.cropperBoxMoveStart) {
+            return;
+        }
+        const { clientX, clientY } = e;
+        const { cropperBox } = this.getStates();
+        const offsetX = clientX - this.cropperBoxMoveStart.x;
+        const offsetY = clientY - this.cropperBoxMoveStart.y;
+        const newCenterPointX = getMiddle(cropperBox.centerPoint.x + offsetX, [this.moveRange.xMin, this.moveRange.xMax]);
+        const newCenterPointY = getMiddle(cropperBox.centerPoint.y + offsetY, [this.moveRange.yMin, this.moveRange.yMax]);
+        const newCropperBoxPos = {
+            width: cropperBox.width,
+            height: cropperBox.height,
+            centerPoint: {
+                x: newCenterPointX,
+                y: newCenterPointY
+            }
+        };
+        this.cropperBoxMoveStart = {
+            x: clientX,
+            y: clientY
+        };
+        this.setState({
+            cropperBox: newCropperBoxPos
+        } as any);
+    }
+
+    handleCropperBoxMouseUp = (e: any) => {
+        if (!this.cropperBoxMoveStart) {
+            return;
+        }
+        this.cropperBoxMoveStart = null;
+        this.unBindMoveEvent();
+    }
+
+    handleMaskMouseDown = (e: any) => {
+        if (e.currentTarget !== e.target) {
+            return;
+        }
+        this.bindImgMoveEvent();
+        // 记录开始移动的位置
+        this.imgMoveStart = {
+            x: e.clientX,
+            y: e.clientY
+        };
+    }
+
+    bindImgMoveEvent = () => {
+        document.addEventListener('mousemove', this.handleImgMove);
+        document.addEventListener('mouseup', this.handleImgMoveUp);
+    }
+
+    unBindImgMoveEvent = () => {
+        document.removeEventListener('mousemove', this.handleImgMove);
+        document.removeEventListener('mouseup', this.handleImgMoveUp);
+    }
+
+    handleImgMove = (e: any) => {
+        if (!this.imgMoveStart) {
+            return;
+        }
+        const { clientX, clientY } = e;
+        const { imgData } = this.getStates();
+        const offsetX = clientX - this.imgMoveStart.x;
+        const offsetY = clientY - this.imgMoveStart.y;
+        const newCenterPointX = imgData.centerPoint.x + offsetX;
+        const newCenterPointY = imgData.centerPoint.y + offsetY;
+        const newImgData = {
+            width: imgData.width,
+            height: imgData.height,
+            centerPoint: {
+                x: newCenterPointX,
+                y: newCenterPointY
+            }
+        };
+        this.imgMoveStart = {
+            x: clientX,
+            y: clientY
+        };
+        this.setState({
+            imgData: newImgData
+        } as any);
+    }
+
+    handleImgMoveUp = (e: any) => {
+        if (!this.imgMoveStart) {
+            return;
+        }
+        this.imgMoveStart = null;
+        this.unBindImgMoveEvent();
+    }
+
+    getCropperCanvas = () => {
+        const { cropperBox, imgData, rotate, zoom } = this.getStates();
+        const { fill } = this.getProps();
+        const canvas = document.createElement('canvas');
+        const ctx = canvas.getContext('2d');
+        const img = this._adapter.getImg();
+
+        // 计算包含旋转后的图片的矩形容器的宽高
+        const angle = rotate * Math.PI / 180;
+        const sine = Math.abs(Math.sin(angle));
+        const cosine = Math.abs(Math.cos(angle));
+        const imgWidth = this.imgData.originalWidth;
+        const imgHeight = this.imgData.originalHeight;
+        const containerWidth = imgWidth * cosine + imgHeight * sine;
+        const containerHeight = imgHeight * cosine + imgWidth * sine;
+
+        // 判断裁切区域和外接矩形是否存在交集,如果不存在,则直接返回空白图片
+        // 计算需要裁剪的区域实际大小和位置
+        const cropperContainerWidth = containerWidth * zoom * this.imgData.scale;
+        const cropperContainerHeight = containerHeight * zoom * this.imgData.scale;
+        const cropperContainerTop = imgData.centerPoint.y - cropperContainerHeight / 2;
+        const cropperContainerLeft = imgData.centerPoint.x - cropperContainerWidth / 2;
+        const cropperBoxLeft = cropperBox.centerPoint.x - cropperBox.width / 2;
+        const cropperBoxTop = cropperBox.centerPoint.y - cropperBox.height / 2;
+        const realZoom = zoom * this.imgData.scale;
+        
+        const relativeCropLeft = (cropperBoxLeft - cropperContainerLeft) / realZoom;
+        const relativeCropTop = (cropperBoxTop - cropperContainerTop) / realZoom;
+        const relativeWidth = cropperBox.width / realZoom;
+        const relativeHeight = cropperBox.height / realZoom;
+        const relativeCropRight = relativeCropLeft + relativeWidth;
+        const relativeCropBottom = relativeCropTop + relativeHeight;
+
+        if (relativeCropRight < 0 || relativeCropBottom < 0 || relativeCropLeft > containerWidth || relativeCropTop > containerHeight) {
+            // 没有交集,直接返回空白图片
+            const emptyCanvas = document.createElement('canvas');
+            const ctx = emptyCanvas.getContext('2d');
+            emptyCanvas.width = relativeWidth;
+            emptyCanvas.height = relativeHeight;
+            ctx.fillStyle = fill;
+            ctx.fillRect(0, 0, relativeWidth, relativeHeight);
+            return emptyCanvas;
+        }
+
+        canvas.width = containerWidth;
+        canvas.height = containerHeight;
+        ctx.fillStyle = fill;
+        ctx.fillRect(0, 0, containerWidth, containerHeight);
+
+        const halfWidth = containerWidth / 2;
+        const halfHeight = containerHeight / 2;
+        ctx.translate(halfWidth, halfHeight);
+        ctx.rotate(rotate * Math.PI / 180);
+        ctx.translate(-halfWidth, -halfHeight);
+
+        const imgX = (containerWidth - imgWidth) / 2;
+        const imgY = (containerHeight - imgHeight) / 2;
+        ctx.drawImage(img, 0, 0, imgWidth, imgHeight, imgX, imgY, imgWidth, imgHeight);
+
+        const canvas2 = document.createElement('canvas');
+        const ctx2 = canvas2.getContext('2d');
+        // 为了避免裁剪时候,超出被裁切的画布的部分颜色不正常,需要将裁切区域限制在画布范围内。
+        // 相对位置会在后续进行修正
+        let realLeft = relativeCropLeft;
+        let realTop = relativeCropTop;
+        let realWidth = relativeWidth;
+        let realHeight = relativeHeight;
+       
+        if (relativeCropLeft < 0) {
+            realLeft = 0;
+        }
+        if (relativeCropTop < 0) {
+            realTop = 0;
+        }
+        if (relativeCropRight > containerWidth) {
+            realWidth = containerWidth - realLeft;
+        } else if (relativeCropLeft < 0) {
+            realWidth = relativeCropRight;
+        }
+
+        if (relativeCropBottom > containerHeight) {
+            realHeight = containerHeight - realTop;
+        } else if (relativeCropTop < 0) {
+            realHeight = relativeCropBottom;
+        }
+
+        const imgDataResult = ctx.getImageData(realLeft, realTop, realWidth, realHeight);
+        canvas2.width = relativeWidth;
+        canvas2.height = relativeHeight;
+        ctx2.fillStyle = fill;
+        ctx2.fillRect(0, 0, relativeWidth, relativeHeight);
+        ctx2.putImageData(
+            imgDataResult, 
+            relativeCropLeft < 0 ? - relativeCropLeft : 0,  
+            relativeCropTop < 0 ? - relativeCropTop : 0,
+        );
+        return canvas2;
+    }
+}

+ 12 - 0
packages/semi-foundation/cropper/utils.ts

@@ -0,0 +1,12 @@
+export function getMiddle(value: number, [min, max]) {
+    return Math.min(Math.max(value, min), max);
+}
+
+export function getAspectHW(width: number, height: number, aspect: number) {
+    if (width / height > aspect) {
+        width = height * aspect;
+    } else {
+        height = width / aspect;
+    }
+    return [width, height];
+}

+ 6 - 0
packages/semi-foundation/cropper/variables.scss

@@ -0,0 +1,6 @@
+$color-cropper_mask-bg: var(--semi-color-overlay-bg); // 裁切框遮罩背景颜色
+$color-cropper_box-outline: var(--semi-color-primary); // 裁切框边框颜色
+$color-cropper_box_corner-bg: var(--semi-color-primary); // 裁切框调整块背景色
+
+$width-cropper_box-outline: 1px; // 裁切框边框宽度
+$width-cropper_box_corner: 10px; // 裁切框调整块宽高

+ 12 - 0
packages/semi-foundation/dragMove/foundation.ts

@@ -46,9 +46,21 @@ export default class DragMoveFoundation<P = Record<string, any>, S = Record<stri
         this.element = element;
         this.element.style.position = 'absolute';
         this.handler.style.cursor = 'move';
+        this._registerStartEvent();
+    }
+
+    _registerStartEvent = () => {
+        this.handler.addEventListener('mousedown', this.onMouseDown);
+        this.handler.addEventListener('touchstart', this.onTouchStart);
+    }
+
+    _unRegisterStartEvent = () => {
+        this.handler.removeEventListener('mousedown', this.onMouseDown);
+        this.handler.removeEventListener('touchstart', this.onTouchStart);
     }
 
     destroy() {
+        this._unRegisterStartEvent();
         this._unRegisterEvent();
     }
 

+ 6 - 0
packages/semi-foundation/jsonViewer/foundation.ts

@@ -53,10 +53,16 @@ class JsonViewerFoundation extends BaseFoundation<JsonViewerAdapter> {
     }
 
     replace(replaceText: string) {
+        if (this.getProps().options.readOnly) {
+            return;
+        }
         this.jsonViewer?.getSearchWidget().replace(replaceText);
     }
 
     replaceAll(replaceText: string) {
+        if (this.getProps().options.readOnly) {
+            return;
+        }
         this.jsonViewer?.getSearchWidget().replaceAll(replaceText);
     }
 

+ 8 - 3
packages/semi-foundation/jsonViewer/jsonViewer.scss

@@ -68,8 +68,8 @@ $module: #{$prefix}-json-viewer;
         font-variation-settings: normal;
         letter-spacing: 0px;
         color: #237893;
-        word-wrap: break-word;
-        white-space: pre-wrap;
+        word-break: break-all !important;
+        white-space: pre-wrap !important;
     }
 
     &-line-number {
@@ -82,6 +82,7 @@ $module: #{$prefix}-json-viewer;
         color: $color-json-viewer-line-number;
         text-align: center;
         width: 50px;
+        user-select: none;
     }
 
     &-content-container {
@@ -196,5 +197,9 @@ $module: #{$prefix}-json-viewer;
         cursor: pointer;
     }
 
-
+    &-error {
+        text-decoration: underline wavy var(--semi-color-danger);
+        text-decoration-thickness: 1px;
+        text-underline-position: under;
+    }
 }

+ 3 - 3
packages/semi-foundation/package.json

@@ -1,14 +1,14 @@
 {
     "name": "@douyinfe/semi-foundation",
-    "version": "2.72.0",
+    "version": "2.74.0",
     "description": "",
     "scripts": {
         "build:lib": "node ./scripts/compileLib.js",
         "prepublishOnly": "npm run build:lib"
     },
     "dependencies": {
-        "@douyinfe/semi-animation": "2.72.0",
-        "@douyinfe/semi-json-viewer-core": "2.72.0",
+        "@douyinfe/semi-animation": "2.74.0",
+        "@douyinfe/semi-json-viewer-core": "2.74.0",
         "@mdx-js/mdx": "^3.0.1",
         "async-validator": "^3.5.0",
         "classnames": "^2.2.6",

+ 2 - 2
packages/semi-foundation/select/foundation.ts

@@ -1113,10 +1113,10 @@ export default class SelectFoundation extends BaseFoundation<SelectAdapter> {
 
     handleInputBlur(e: any) {
         const { filter, autoFocus } = this.getProps();
-        const { showInput } = this.getStates();
+        const { showInput, isOpen } = this.getStates();
         const isMultiple = this._isMultiple();
         if (filter && !isMultiple ) {
-            if (showInput || autoFocus) {
+            if ((showInput || autoFocus) && !isOpen) {
                 this.toggle2SearchInput(false);
             }
         }

+ 1 - 1
packages/semi-foundation/steps/bacisSteps.scss

@@ -233,7 +233,7 @@ $basicType: #{$module}-basic;
             color: $color-steps_main-text-default;
             vertical-align: top;
             padding-right: $spacing-steps_basic_item_title-paddingRight;
-            padding-bottom: $spacing-steps_basic_item_title-paddingBottom;
+            margin-bottom: $spacing-steps_basic_item_title-paddingBottom;
             transition: color $transition_duration-steps_item_title-text $transition_function-steps_item_title-text $transition_delay-steps_item_title-text; //step文字color的transition变化
        
         }

+ 3 - 0
packages/semi-foundation/tree/treeUtil.ts

@@ -53,6 +53,9 @@ export function flattenTreeData(treeNodeList: any[], expandedKeys: Set<string>,
     const filterSearch = Boolean(filteredShownKeys);
     const realKeyName = get(keyMaps, 'key', 'key');
     const realChildrenName = get(keyMaps, 'children', 'children');
+    if (isUndefined(treeNodeList)) {
+        return [];
+    }
     function flatten(list: any[], parent: any = null) {
         return list.map((treeNode, index) => {
             const pos = getPosition(parent ? parent.pos : '0', index);

+ 1 - 1
packages/semi-icons-lab/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@douyinfe/semi-icons-lab",
-  "version": "2.72.0",
+  "version": "2.74.0",
   "description": "semi icons lab",
   "keywords": [
     "semi",

+ 1 - 1
packages/semi-icons/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-icons",
-    "version": "2.72.0",
+    "version": "2.74.0",
     "description": "semi icons",
     "keywords": [
         "semi",

+ 1 - 1
packages/semi-illustrations/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-illustrations",
-    "version": "2.72.0",
+    "version": "2.74.0",
     "description": "semi illustrations",
     "keywords": [
         "semi",

+ 54 - 54
packages/semi-json-viewer-core/package.json

@@ -1,56 +1,56 @@
 {
-  "name": "@douyinfe/semi-json-viewer-core",
-  "version": "2.72.0",
-  "description": "",
-  "main": "lib/index.js",
-  "module": "lib/index.js",
-  "typings": "src/index.ts",
-  "scripts": {
-    "build:lib": "node ./script/compileLib.js",
-    "prepublishOnly": "npm run build:lib"
-  },
-  "files": [
-    "dist/*",
-    "lib/*"
-  ],
-  "dependencies": {
-    "jsonc-parser": "^3.3.1"
-  },
-  "devDependencies": {
-    "esbuild": "^0.24.0"
-  },
-  "sideEffects": [
-    "*.scss",
-    "*.css",
-    "lib/es/index.js",
-    "./index.ts"
-  ],
-  "keywords": [
-    "bytedance douyin design system",
-    "semi design to any design",
-    "a11y react component library",
-    "design to code",
-    "code to design",
-    "3000+ design token",
-    "dark mode",
-    "semi design",
-    "design ops",
-    "modern design system",
-    "figma ui kit"
-  ],
-  "homepage": "https://semi.design",
-  "bugs": {
-    "url": "https://github.com/DouyinFE/semi-design/issues"
-  },
-  "repository": {
-    "type": "git",
-    "url": "https://github.com/DouyinFE/semi-design"
-  },
-  "_unpkg": true,
-  "unpkgFiles": [
-    "dist/css",
-    "dist/umd/*.js"
-  ],
-  "author": "",
-  "license": "MIT"
+    "name": "@douyinfe/semi-json-viewer-core",
+    "version": "2.74.0",
+    "description": "",
+    "main": "lib/index.js",
+    "module": "lib/index.js",
+    "typings": "src/index.ts",
+    "scripts": {
+        "build:lib": "node ./script/compileLib.js",
+        "prepublishOnly": "npm run build:lib"
+    },
+    "files": [
+        "dist/*",
+        "lib/*"
+    ],
+    "dependencies": {
+        "jsonc-parser": "^3.3.1"
+    },
+    "devDependencies": {
+        "esbuild": "^0.24.0"
+    },
+    "sideEffects": [
+        "*.scss",
+        "*.css",
+        "lib/es/index.js",
+        "./index.ts"
+    ],
+    "keywords": [
+        "bytedance douyin design system",
+        "semi design to any design",
+        "a11y react component library",
+        "design to code",
+        "code to design",
+        "3000+ design token",
+        "dark mode",
+        "semi design",
+        "design ops",
+        "modern design system",
+        "figma ui kit"
+    ],
+    "homepage": "https://semi.design",
+    "bugs": {
+        "url": "https://github.com/DouyinFE/semi-design/issues"
+    },
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/DouyinFE/semi-design"
+    },
+    "_unpkg": true,
+    "unpkgFiles": [
+        "dist/css",
+        "dist/umd/*.js"
+    ],
+    "author": "",
+    "license": "MIT"
 }

+ 5 - 0
packages/semi-json-viewer-core/script/compileLib.js

@@ -40,10 +40,15 @@ const compile = async ()=>{
     const finalRaw = mainRaw.replaceAll("%WORKER_RAW%", encodeURIComponent(workerRaw));
 
     const saveDir = path.join(__dirname, "..", "lib");
+    const workerSaveDir = path.join(__dirname, "..", "workerLib");
 
     if (!fs.existsSync(saveDir)) {
         fs.mkdirSync(saveDir);
     }
+    if (!fs.existsSync(workerSaveDir)) {
+        fs.mkdirSync(workerSaveDir);
+    }
+    fs.writeFileSync(path.join(workerSaveDir, "worker.js"), workerRaw, 'utf8');
     fs.writeFileSync(path.join(saveDir, "index.js"), finalRaw, 'utf8');
 };
 

+ 2 - 1
packages/semi-json-viewer-core/src/common/emitterEvents.ts

@@ -6,7 +6,8 @@ export interface GlobalEvents {
     contentChanged: IModelContentChangeEvent | IModelContentChangeEvent[];
     problemsChanged: IProblemsChangedEvent;
     hoverNode: IHoverNodeEvent;
-    renderHoverNode: IRenderHoverNodeEvent
+    renderHoverNode: IRenderHoverNodeEvent;
+    forceRender: undefined
 }
 
 interface IRange {

+ 45 - 0
packages/semi-json-viewer-core/src/common/strings.ts

@@ -138,3 +138,48 @@ export function firstNonWhitespaceIndex(str: string): number {
     }
     return -1;
 }
+
+export const enum StringEOL {
+    Unknown = 0,
+    Invalid = 3,
+    LF = 1,
+    CRLF = 2,
+}
+
+export function countEOL(text: string): [number, number, number, StringEOL] {
+    let eolCount = 0;
+    let firstLineLength = 0;
+    let lastLineStart = 0;
+    let eol: StringEOL = StringEOL.Unknown;
+    for (let i = 0, len = text.length; i < len; i++) {
+        const chr = text.charCodeAt(i);
+
+        if (chr === CharCode.CarriageReturn) {
+            if (eolCount === 0) {
+                firstLineLength = i;
+            }
+            eolCount++;
+            if (i + 1 < len && text.charCodeAt(i + 1) === CharCode.LineFeed) {
+                // \r\n... case
+                eol |= StringEOL.CRLF;
+                i++; // skip \n
+            } else {
+                // \r... case
+                eol |= StringEOL.Invalid;
+            }
+            lastLineStart = i + 1;
+        } else if (chr === CharCode.LineFeed) {
+            // \n... case
+            eol |= StringEOL.LF;
+            if (eolCount === 0) {
+                firstLineLength = i;
+            }
+            eolCount++;
+            lastLineStart = i + 1;
+        }
+    }
+    if (eolCount === 0) {
+        firstLineLength = text.length;
+    }
+    return [eolCount, firstLineLength, text.length - lastLineStart, eol];
+}

+ 19 - 0
packages/semi-json-viewer-core/src/common/utils.ts

@@ -4,4 +4,23 @@ export function isObject(val: any): val is Record<string, any> {
 
 export function isNumber(val: any): val is number {
     return typeof val === 'number';
+}
+
+export function findFirstIdxMonotonousOrArrLen<T>(
+    array: readonly T[],
+    predicate: (item: T) => boolean,
+    startIdx = 0,
+    endIdxEx = array.length
+): number {
+    let i = startIdx;
+    let j = endIdxEx;
+    while (i < j) {
+        const k = Math.floor((i + j) / 2);
+        if (predicate(array[k])) {
+            j = k;
+        } else {
+            i = k + 1;
+        }
+    }
+    return i;
 }

+ 1 - 0
packages/semi-json-viewer-core/src/json-viewer/jsonViewer.ts

@@ -12,6 +12,7 @@ import { setCurrentNameSpaceId } from '../common/nameSpace';
 export interface JsonViewerOptions {
     lineHeight?: number;
     autoWrap?: boolean;
+    readOnly?: boolean;
     formatOptions?: FormattingOptions;
     completionOptions?: CompletionOptions
 }

+ 171 - 75
packages/semi-json-viewer-core/src/model/foldingModel.ts

@@ -1,108 +1,146 @@
+/** based on https://github.com/microsoft/vscode with modifications for custom requirements */
 import { JSONModel } from './jsonModel';
-import { getFoldingRanges, FoldingRange } from '../service/jsonService';
 import { Emitter, getEmitter } from '../common/emitter';
 import { getJsonWorkerManager, JsonWorkerManager } from '../worker/jsonWorkerManager';
 import { GlobalEvents } from '../common/emitterEvents';
+import { FoldingRegion, FoldingRegions, FoldRange, FoldSource } from './foldingRange';
+import { HiddenRangeModel } from './hiddenRangeModel';
 
 /**
  * 折叠模型,管理JSON的折叠范围
  */
-//TODO 修改range数据结构
 export class FoldingModel {
     private _jsonModel: JSONModel;
-    private _foldingRanges: FoldingRange[] = [];
-    private _collapsedRanges: Map<number, number> = new Map(); // startLine -> endLine
+    private _regions: FoldingRegions | null = null;
+    private _hiddenRangeModel: HiddenRangeModel;
     private _jsonWorkerManager: JsonWorkerManager = getJsonWorkerManager();
     private emitter: Emitter<GlobalEvents> = getEmitter();
+
+    get regions() {
+        return this._regions;
+    }
     constructor(jsonModel: JSONModel) {
         this._jsonModel = jsonModel;
+        // this.emitter.on('problemsChanged', e => {
+        //     this.updateFoldingRanges();
+        // });
         this.updateFoldingRanges();
+        this.emitter.on('contentChanged', e => {
+            this._hiddenRangeModel.notifyChangeModelContent(e);
+        });
+
         this.emitter.on('problemsChanged', e => {
-            this.updateFoldingRanges();
+            this._jsonWorkerManager.foldRange().then(ranges => {
+                const newRegions = FoldingRegions.fromFoldRanges(ranges);
+                this.update(newRegions);
+                this._hiddenRangeModel.updateHiddenRanges();
+            });
         });
     }
 
     public updateFoldingRanges(): void {
         this._jsonWorkerManager.foldRange().then(ranges => {
-            this._foldingRanges = ranges;
-            this.updateCollapsedRanges();
+            this._regions = FoldingRegions.fromFoldRanges(ranges);
+            this._hiddenRangeModel = new HiddenRangeModel(this);
         });
     }
 
-    private updateCollapsedRanges(): void {
-        const newCollapsedRanges = new Map<number, number>();
+    public update(newRegions: FoldingRegions, blockedLineNumers: number[] = []): void {
+        const foldedOrManualRanges = this._currentFoldedOrManualRanges(blockedLineNumers, newRegions);
+        const newRanges = FoldingRegions.sanitizeAndMerge(
+            newRegions,
+            foldedOrManualRanges,
+            this._jsonModel.getLineCount()
+        );
+        this._regions = FoldingRegions.fromFoldRanges(newRanges);
+    }
 
-        for (const [startLine, endLine] of this._collapsedRanges) {
-            const range = this._foldingRanges.find(r => r.startLine === startLine);
-            if (range) {
-                newCollapsedRanges.set(startLine, range.endLine);
+    private _currentFoldedOrManualRanges(blockedLineNumers: number[] = [], newRegions?: FoldingRegions): FoldRange[] {
+        const isBlocked = (startLineNumber: number, endLineNumber: number) => {
+            if (newRegions) {
+                const index = newRegions.findRange(startLineNumber);
+                if (index === -1) return true;
+                
+                const region = newRegions.toRegion(index);
+                if (!region || region.endLineNumber !== endLineNumber) return true;
+            }
+            
+            for (const blockedLineNumber of blockedLineNumers) {
+                if (startLineNumber < blockedLineNumber && blockedLineNumber <= endLineNumber) {
+                    return true;
+                }
+            }
+            return false;
+        };
+
+        const foldedRanges: FoldRange[] = [];
+        for (let i = 0; i < this._regions.length; i++) {
+            if (this._regions.isCollapsed(i)) {
+                const startLineNumber = this._regions.getStartLineNumber(i);
+                const endLineNumber = this._regions.getEndLineNumber(i);
+                
+                if (!isBlocked(startLineNumber, endLineNumber)) {
+                    foldedRanges.push({
+                        startLineNumber,
+                        endLineNumber,
+                        isCollapsed: true,
+                        source: FoldSource.provider,
+                        type: this._regions.getType(i),
+                    });
+                }
             }
         }
-
-        this._collapsedRanges = newCollapsedRanges;
+        return foldedRanges;
     }
 
-    public getFoldingRanges(): FoldingRange[] {
-        return this._foldingRanges;
+    public toggleCollapseState(toggledRegions: FoldingRegion[]) {
+        if (!toggledRegions.length) {
+            return;
+        }
+        toggledRegions = toggledRegions.sort((r1, r2) => r1.regionIndex - r2.regionIndex);
+        for (const region of toggledRegions) {
+            const index = region.regionIndex;
+            const newCollapsed = !this._regions.isCollapsed(index);
+            this._regions.setCollapsed(index, newCollapsed);
+        }
+        this._hiddenRangeModel.updateHiddenRanges();
     }
 
     public toggleFoldingRange(startLine: number): void {
-        if (this._collapsedRanges.has(startLine)) {
-            this._collapsedRanges.delete(startLine);
-        } else {
-            const range = this._foldingRanges.find(r => r.startLine === startLine);
-            if (range) {
-                this._collapsedRanges.set(startLine, range.endLine);
-            }
-        }
+        toggleCollapseState(this, 1, [startLine]);
     }
 
     public isCollapsed(lineNumber: number): boolean {
-        return this._collapsedRanges.has(lineNumber);
+        if (!this._regions) return false;
+        const index = this._regions.findRange(lineNumber);
+        const region = this._regions.toRegion(index);
+        return region && region.isCollapsed;
     }
 
     public isLineCollapsed(lineNumber: number): boolean {
-        if (this._collapsedRanges.has(lineNumber)) {
-            return false;
-        }
-        for (const [startLine, endLine] of this._collapsedRanges) {
-            if (lineNumber > startLine && lineNumber <= endLine) {
-                return true;
-            }
-        }
-        return false;
+        if (!this._regions) return false;
+        return this._hiddenRangeModel.isHiddenLine(lineNumber);
     }
 
-    public getVisibleLineNumber(actualLineNumber: number): number {
-        let visibleLine = actualLineNumber;
-        for (const [startLine, endLine] of this._collapsedRanges) {
-            if (startLine < actualLineNumber) {
-                if (endLine < actualLineNumber) {
-                    visibleLine -= endLine - startLine;
-                } else if (actualLineNumber > startLine) {
-                    return -1;
-                }
-            } else {
-                break;
-            }
-        }
-        return visibleLine;
+    public isFoldable(lineNumber: number): boolean {
+        const index = this._regions.findRange(lineNumber);
+        const region = this._regions.toRegion(index);
+        return region && region.startLineNumber === lineNumber;
     }
 
-    public getNextVisibleLine(actualLineNumber: number): number {
-        for (const [startLine, endLine] of this._collapsedRanges) {
-            if (actualLineNumber >= startLine && actualLineNumber <= endLine) {
-                return actualLineNumber === startLine ? startLine + 1 : endLine + 1;
-            }
-        }
-        return actualLineNumber + 1;
+    public getVisibleLineCount(): number {
+        if (!this._regions || !this._hiddenRangeModel) return this._jsonModel.getLineCount();
+        return this._jsonModel.getLineCount() - this._hiddenRangeModel.getHiddenLineCount();
     }
-
+    
     public getActualLineNumber(visibleLineNumber: number): number {
+        if (!this._regions || !this._hiddenRangeModel) return visibleLineNumber;
+        
         let actualLine = visibleLineNumber;
-        for (const [startLine, endLine] of this._collapsedRanges) {
-            if (startLine < actualLine) {
-                actualLine += endLine - startLine;
+        const hiddenRanges = this._hiddenRangeModel.hiddenRanges;
+        for (const range of hiddenRanges) {
+            if (range.startLineNumber <= actualLine) {
+                actualLine += (range.endLineNumber - range.startLineNumber + 1);
             } else {
                 break;
             }
@@ -110,29 +148,87 @@ export class FoldingModel {
         return actualLine;
     }
 
-    public isFoldable(lineNumber: number): boolean {
-        return this._foldingRanges.some(range => range.startLine === lineNumber);
+    public getNextVisibleLine(actualLineNumber: number): number {
+        if (!this._regions || !this._hiddenRangeModel) return actualLineNumber + 1;
+        let nextLine = actualLineNumber + 1;
+        const hiddenRanges = this._hiddenRangeModel.hiddenRanges;
+        const containingRange = this._hiddenRangeModel.findRange(nextLine, hiddenRanges);
+        if (containingRange) {
+            return containingRange.endLineNumber + 1;
+        }
+        return nextLine;
     }
 
-    public expandLine(lineNumber: number): void {
-        for (const [startLine, endLine] of this._collapsedRanges) {
-            if (lineNumber > startLine && lineNumber <= endLine) {
-                this._collapsedRanges.delete(startLine);
+    getRegionAtLine(lineNumber: number): FoldingRegion | null {
+        if (this._regions) {
+            const index = this._regions.findRange(lineNumber);
+            if (index >= 0) {
+                return this._regions.toRegion(index);
             }
         }
+        return null;
     }
 
-    public getVisibleLineCount(): number {
-        let visibleCount = 0;
-        let lineNumber = 1;
-
-        while (lineNumber <= this._jsonModel.getLineCount()) {
-            if (!this.isLineCollapsed(lineNumber)) {
-                visibleCount++;
+    getRegionsInside(region: FoldingRegion | null, filter?: RegionFilter | RegionFilterWithLevel): FoldingRegion[] {
+        const result: FoldingRegion[] = [];
+        const index = region ? region.regionIndex + 1 : 0;
+        const endLineNumber = region ? region.endLineNumber : Number.MAX_VALUE;
+
+        if (filter && filter.length === 2) {
+            const levelStack: FoldingRegion[] = [];
+            for (let i = index, len = this._regions.length; i < len; i++) {
+                const current = this._regions.toRegion(i);
+                if (this._regions.getStartLineNumber(i) < endLineNumber) {
+                    while (levelStack.length > 0 && !current.containedBy(levelStack[levelStack.length - 1])) {
+                        levelStack.pop();
+                    }
+                    levelStack.push(current);
+                    if (filter(current, levelStack.length)) {
+                        result.push(current);
+                    }
+                } else {
+                    break;
+                }
+            }
+        } else {
+            for (let i = index, len = this._regions.length; i < len; i++) {
+                const current = this._regions.toRegion(i);
+                if (this._regions.getStartLineNumber(i) < endLineNumber) {
+                    if (!filter || (filter as RegionFilter)(current)) {
+                        result.push(current);
+                    }
+                } else {
+                    break;
+                }
             }
-            lineNumber = this.getNextVisibleLine(lineNumber);
         }
+        return result;
+    }
+}
 
-        return visibleCount;
+type RegionFilter = (r: FoldingRegion) => boolean;
+type RegionFilterWithLevel = (r: FoldingRegion, level: number) => boolean;
+
+/**
+ * Collapse or expand the regions at the given locations
+ * @param levels The number of levels. Use 1 to only impact the regions at the location, use Number.MAX_VALUE for all levels.
+ * @param lineNumbers the location of the regions to collapse or expand, or if not set, all regions in the model.
+ */
+export function toggleCollapseState(foldingModel: FoldingModel, levels: number, lineNumbers: number[]) {
+    const toToggle: FoldingRegion[] = [];
+    for (const lineNumber of lineNumbers) {
+        const region = foldingModel.getRegionAtLine(lineNumber);
+        if (region) {
+            const doCollapse = !region.isCollapsed;
+            toToggle.push(region);
+            if (levels > 1) {
+                const regionsInside = foldingModel.getRegionsInside(
+                    region,
+                    (r, level: number) => r.isCollapsed !== doCollapse && level < levels
+                );
+                toToggle.push(...regionsInside);
+            }
+        }
     }
+    foldingModel.toggleCollapseState(toToggle);
 }

Some files were not shown because too many files changed in this diff