Browse Source

Merge branch 'release'

林艳 6 months ago
parent
commit
57be7cced1

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

@@ -271,6 +271,12 @@ Methods bound to the component instance can be called via `ref` to achieve certa
 | ---------- | ---------------------- |
 | getValue() | Get current value      |
 | format()   | Format current content |
+| search(searchText: string, caseSensitive?: boolean, wholeWord?: boolean, regex?: boolean) | Search for text with optional parameters |
+| getSearchResults() | Get current search results |
+| prevSearch(step?: number) | Navigate to previous search result, with optional step size |
+| nextSearch(step?: number) | Navigate to next search result, with optional step size |
+| replace(replaceText: string) | Replace current search match |
+| replaceAll(replaceText: string) | Replace all search matches |
 
 ### Performance
 

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

@@ -261,12 +261,19 @@ render(CustomRenderJsonComponent);
 
 ## Methods
 
-绑定在组件实例上的方法,可以通过 ref 调用实现某些特殊交互
+可以通过 `ref` 调用组件实例上绑定的方法,实现某些特殊交互
 
 | 名称    | 描述     |
 |---------|--------|
 | getValue()  | 获取当前值 |
-| format() | 格式化 |
+| format() | 格式化当前内容 |
+| search(searchText: string, caseSensitive?: boolean, wholeWord?: boolean, regex?: boolean) | 搜索文本,可选参数控制大小写敏感、全词匹配和正则表达式 |
+| getSearchResults() | 获取当前搜索结果 |
+| prevSearch(step?: number) | 导航到上一个搜索结果,可选步长参数 |
+| nextSearch(step?: number) | 导航到下一个搜索结果,可选步长参数 |
+| replace(replaceText: string) | 替换当前搜索匹配项 |
+| replaceAll(replaceText: string) | 替换所有搜索匹配项 |
+
 
 
 ### Performance 

+ 126 - 26
content/show/cropper/index-en-US.md

@@ -27,6 +27,7 @@ Use `sr` to set the cropped image; use `shape` to set the shape of the cropping
 
 ```jsx live=true dir=column noInline=true
 import { Cropper, Button, RadioGroup, Radio } from '@douyinfe/semi-ui';
+import React, { useState, useRef, useCallback } from 'react';
 
 const containerStyle = {
   width: 550,
@@ -36,13 +37,12 @@ const containerStyle = {
 
 function Demo() {
     const ref = useRef(null);
-  const [shape, setShape] = useState('rect');
+    const [shape, setShape] = useState('rect');
+    const [cropperUrl, setCropperUrl] = useState('');
 
     const onButtonClick = useCallback(() => {
-        const value = ref.current.getCropperCanvas();
-        const previewContainer = document.getElementById('previewContainer');
-        previewContainer.innerHTML = '';
-        previewContainer.appendChild(value);
+        const canvas = ref.current.getCropperCanvas();
+        setCropperUrl(canvas.toDataURL());
     }, []);
 
     const onShapeChange = useCallback((e) => {
@@ -62,7 +62,8 @@ function Demo() {
             shape={shape}
         />
         <Button onClick={onButtonClick}>Get Cropped Image</Button>
-        <div id='previewContainer'/>
+         <br/><br/>
+        {cropperUrl && <img src={cropperUrl} style={{height: 400}}/>}
     </>;
 }
 
@@ -79,6 +80,7 @@ When setting `aspectRatio`, the crop box ratio is fixed, and the crop box will c
 
 ```jsx live=true dir=column noInline=true
 import { Cropper, Button, RadioGroup, Radio } from '@douyinfe/semi-ui';
+import React, { useState, useRef, useCallback } from 'react';
 
 const containerStyle = {
   width: 550,
@@ -88,13 +90,12 @@ const containerStyle = {
 
 function Demo() {
     const ref = useRef(null);
-    const shape = useState('rect');
+    const [cropperUrl, setCropperUrl] = useState('');
 
     const onButtonClick = useCallback(() => {
-        const value = ref.current.getCropperCanvas();
-        const previewContainer = document.getElementById('previewContainer-aspect');
-        previewContainer.innerHTML = '';
-        previewContainer.appendChild(value);
+      const canvas = ref.current.getCropperCanvas();
+      const url = canvas.toDataURL();
+      setCropperUrl(url);
     }, []);
 
     return <>
@@ -105,7 +106,8 @@ function Demo() {
             style={containerStyle}
         />
         <Button onClick={onButtonClick}>Get Cropped Image</Button>
-        <div id='previewContainer-aspect' />
+         <br /><br />
+        {cropperUrl && <img src={cropperUrl} style={{height: 400}}/>}
     </>;
 }
 
@@ -118,6 +120,7 @@ Control image rotation and zoom through `rotate` and `zoom`, and get the latest
 
 ```jsx live=true dir=column noInline=true
 import { Cropper, Button, Slider } from '@douyinfe/semi-ui';
+import React, { useState, useRef, useCallback } from 'react';
 
 const containerStyle = {
   width: 550,
@@ -137,6 +140,7 @@ function Demo() {
   const [rotate, setRotate] = useState(0);
   const [zoom, setZoom] = useState(1);
   const ref = useRef();
+  const [cropperUrl, setCropperUrl] = useState('');
 
   const onZoomChange = useCallback((value) => {
     setZoom(value);
@@ -147,10 +151,8 @@ function Demo() {
   }, []);
 
   const onButtonClick = useCallback(() => {
-    const value = ref.current.getCropperCanvas();
-    const previewContainer = document.getElementById('previewContainer-control');
-    previewContainer.innerHTML = '';
-    previewContainer.appendChild(value);
+    const canvas = ref.current.getCropperCanvas();
+    setCropperUrl(canvas.toDataURL());
   }, []);
 
   return (
@@ -187,11 +189,8 @@ function Demo() {
            </div>
            <br />
            <Button onClick={onButtonClick}>Get Cropped Image</Button>
-           <br />
-           <div >
-            <div id='previewContainer-control'
-            />
-          </div>
+           <br /><br />
+          {cropperUrl && <img src={cropperUrl} style={{height: 400}}/>}
       </div>
   );
 };
@@ -205,6 +204,7 @@ The crop box style can be customized through `cropperBoxStyle`, `cropperBoxClass
 
 ```jsx live=true dir=column noInline=true
 import { Cropper, Button, Switch } from '@douyinfe/semi-ui';
+import React, { useState, useRef, useCallback } from 'react';
 
 const containerStyle = {
   width: 550,
@@ -221,12 +221,11 @@ const centerStyle = {
 
 function Demo() {
     const ref = useRef(null);
+    const [cropperUrl, setCropperUrl] = useState('');
 
     const onButtonClick = useCallback(() => {
-        const value = ref.current.getCropperCanvas();
-        const previewContainer = document.getElementById('previewContainer-cropperBox');
-        previewContainer.innerHTML = '';
-        previewContainer.appendChild(value);
+        const canvas = ref.current.getCropperCanvas();
+        setCropperUrl(canvas.toDataURL());
     }, []);
 
     return <>
@@ -239,13 +238,113 @@ function Demo() {
             showResizeBox={false}
         />
         <Button onClick={onButtonClick}>Get Cropped Image</Button>
-        <div id='previewContainer-cropperBox'/>
+        <br /><br />
+        {cropperUrl && <img src={cropperUrl} style={{height: 400}}/>}
     </>;
 }
 
 render(<Demo />)
 ```
 
+### 实时预览裁切效果
+
+通过 `preview` 指定预览容器,实时预览裁切效果。
+
+```jsx live=true dir=column noInline=true
+import { Cropper, Button, RadioGroup, Radio } from '@douyinfe/semi-ui';
+import React, { useState, useRef, useCallback } from 'react';
+
+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 [cropperData, setCropperUrl ] = useState('');
+  const ref = useRef();
+
+  const onZoomChange = useCallback((value) => {
+    setZoom(value);
+  })
+
+  const onSliderChange = useCallback((value) => {
+    setRotate(value);
+  }, []);
+
+  const onButtonClick = useCallback(() => {
+    const canvas = ref.current.getCropperCanvas();
+    const url = canvas.toDataURL();
+    setCropperUrl(url);
+  }, []);
+
+  const preview = useCallback(() => {
+    const previewContainer = document.getElementById('previewWrapper');
+    return previewContainer;
+  }, []);
+
+  return (
+      <div >
+           <Cropper 
+              ref={ref} 
+              src={"https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg"}
+              style={containerStyle}
+              rotate={rotate}
+              zoom={zoom}
+              onZoomChange={onZoomChange}
+              preview={preview}
+           />
+           <div style={actionStyle} >
+            <span>旋转</span>
+            <Slider
+              style={{ width: 500}}
+              value={rotate}
+              step={1}
+              min={-360}
+              max={360}
+              onChange={onSliderChange}
+            />
+           </div>
+           <div style={actionStyle} >
+            <span>缩放</span>
+            <Slider
+              style={{ width: 500}}
+              value={zoom}
+              step={0.1}
+              min={0.1}
+              max={3}
+              onChange={onZoomChange}
+            />
+           </div>
+           <br />
+           <div style={{ display: 'flex', }}>
+              <div style={{ width: '50%', flexGrow: 1}}>
+                <strong>实时预览</strong>
+                <div id='previewWrapper' style={{height: 300, marginTop: 8}}/>
+              </div>
+              <div style={{width: '50%', flexGrow: 1, paddingLeft: 10 }}>
+                <Button onClick={onButtonClick}>裁切</Button>
+                <br /><br />
+                <img src={cropperData} style={{ width: '90%'}} />
+              </div>
+           </div>
+      </div>
+  );
+};
+
+render(<Demo />)
+```
+
 ### API
 
 | PROPERTIES | INSTRUCTIONS | TYPE | DEFAULT |
@@ -260,6 +359,7 @@ render(<Demo />)
 | maxZoom | Maximum zoom factor | number | 3 |
 | minZoom | Minimum zoom factor | number | 0.1 |
 | onZoomChange | Callback during zoom transformation | (zoom: number) => void | - |
+| preview | The container of the preview image | () => HTMLElement | - |
 | rotate | rotation angle | number | - |
 | shape | Crop box shape | 'rect' \| 'round' \| 'roundRect' | 'rect' |
 | src | The address of the cropped image | string | - |

+ 127 - 30
content/show/cropper/index.md

@@ -30,6 +30,7 @@ import { Cropper } from '@douyinfe/semi-ui';
 
 ```jsx live=true dir=column noInline=true
 import { Cropper, Button, RadioGroup, Radio } from '@douyinfe/semi-ui';
+import React, { useState, useRef, useCallback } from 'react';
 
 const containerStyle = {
   width: 550,
@@ -39,13 +40,12 @@ const containerStyle = {
 
 function Demo() {
     const ref = useRef(null);
-  const [shape, setShape] = useState('rect');
+    const [shape, setShape] = useState('rect');
+    const [cropperUrl, setCropperUrl] = useState('');
 
     const onButtonClick = useCallback(() => {
-        const value = ref.current.getCropperCanvas();
-        const previewContainer = document.getElementById('previewContainer');
-        previewContainer.innerHTML = '';
-        previewContainer.appendChild(value);
+        const canvas = ref.current.getCropperCanvas();
+        setCropperUrl(canvas.toDataURL());
     }, []);
 
     const onShapeChange = useCallback((e) => {
@@ -65,7 +65,8 @@ function Demo() {
             shape={shape}
         />
         <Button onClick={onButtonClick}>裁切</Button>
-        <div id='previewContainer'/>
+        <br/><br/>
+        {cropperUrl && <img src={cropperUrl} style={{height: 400}}/>}
     </>;
 }
 
@@ -82,6 +83,7 @@ render(<Demo />)
 
 ```jsx live=true dir=column noInline=true
 import { Cropper, Button, RadioGroup, Radio } from '@douyinfe/semi-ui';
+import React, { useState, useRef, useCallback } from 'react';
 
 const containerStyle = {
   width: 550,
@@ -91,13 +93,11 @@ const containerStyle = {
 
 function Demo() {
     const ref = useRef(null);
-    const shape = useState('rect');
+    const [cropperUrl, setCropperUrl] = useState('');
 
     const onButtonClick = useCallback(() => {
-        const value = ref.current.getCropperCanvas();
-        const previewContainer = document.getElementById('previewContainer-aspect');
-        previewContainer.innerHTML = '';
-        previewContainer.appendChild(value);
+        const canvas = ref.current.getCropperCanvas();
+        setCropperUrl(canvas.toDataURL());
     }, []);
 
     return <>
@@ -108,7 +108,8 @@ function Demo() {
             style={containerStyle}
         />
         <Button onClick={onButtonClick}>裁切</Button>
-        <div id='previewContainer-aspect' />
+        <br /><br />
+        {cropperUrl && <img src={cropperUrl} style={{height: 400}}/>}
     </>;
 }
 
@@ -121,6 +122,7 @@ render(<Demo />)
 
 ```jsx live=true dir=column noInline=true
 import { Cropper, Button, Slider } from '@douyinfe/semi-ui';
+import React, { useState, useRef, useCallback } from 'react';
 
 const containerStyle = {
   width: 550,
@@ -140,6 +142,7 @@ function Demo() {
   const [rotate, setRotate] = useState(0);
   const [zoom, setZoom] = useState(1);
   const ref = useRef();
+  const [cropperUrl, setCropperUrl] = useState('');
 
   const onZoomChange = useCallback((value) => {
     setZoom(value);
@@ -150,10 +153,8 @@ function Demo() {
   }, []);
 
   const onButtonClick = useCallback(() => {
-    const value = ref.current.getCropperCanvas();
-    const previewContainer = document.getElementById('previewContainer-control');
-    previewContainer.innerHTML = '';
-    previewContainer.appendChild(value);
+    const canvas = ref.current.getCropperCanvas();
+    setCropperUrl(canvas.toDataURL());
   }, []);
 
   return (
@@ -167,7 +168,7 @@ function Demo() {
               onZoomChange={onZoomChange}
            />
            <div style={actionStyle} >
-            <span>Rotate</span>
+            <span>旋转</span>
             <Slider
               style={{ width: 500}}
               value={rotate}
@@ -178,7 +179,7 @@ function Demo() {
             />
            </div>
            <div style={actionStyle} >
-            <span>Zoom</span>
+            <span>缩放</span>
             <Slider
               style={{ width: 500}}
               value={zoom}
@@ -190,13 +191,8 @@ function Demo() {
            </div>
            <br />
            <Button onClick={onButtonClick}>裁切</Button>
-           <br />
-           <div 
-            // style={{ background: 'pink' }} 
-           >
-            <div id='previewContainer-control'
-            />
-          </div>
+          <br /><br />
+          {cropperUrl && <img src={cropperUrl} style={{height: 400}}/>}
       </div>
   );
 };
@@ -210,6 +206,7 @@ render(<Demo />)
 
 ```jsx live=true dir=column noInline=true
 import { Cropper, Button, Switch } from '@douyinfe/semi-ui';
+import React, { useState, useRef, useCallback } from 'react';
 
 const containerStyle = {
   width: 550,
@@ -226,12 +223,11 @@ const centerStyle = {
 
 function Demo() {
     const ref = useRef(null);
+    const [cropperUrl, setCropperUrl] = useState('');
 
     const onButtonClick = useCallback(() => {
-        const value = ref.current.getCropperCanvas();
-        const previewContainer = document.getElementById('previewContainer-cropperBox');
-        previewContainer.innerHTML = '';
-        previewContainer.appendChild(value);
+        const canvas = ref.current.getCropperCanvas();
+        setCropperUrl(canvas.toDataURL());
     }, []);
 
     return <>
@@ -244,13 +240,113 @@ function Demo() {
             showResizeBox={false}
         />
         <Button onClick={onButtonClick}>裁切</Button>
-        <div id='previewContainer-cropperBox'/>
+        <br /><br />
+        {cropperUrl && <img src={cropperUrl} style={{height: 400}}/>}
     </>;
 }
 
 render(<Demo />)
 ```
 
+### 实时预览裁切效果
+
+通过 `preview` 指定预览容器,实时预览裁切效果。
+
+```jsx live=true dir=column noInline=true
+import { Cropper, Button, RadioGroup, Radio } from '@douyinfe/semi-ui';
+import React, { useState, useRef, useCallback } from 'react';
+
+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 [cropperUrl, setCropperUrl ] = useState('');
+  const ref = useRef();
+
+  const onZoomChange = useCallback((value) => {
+    setZoom(value);
+  })
+
+  const onSliderChange = useCallback((value) => {
+    setRotate(value);
+  }, []);
+
+  const onButtonClick = useCallback(() => {
+    const canvas = ref.current.getCropperCanvas();
+    const url = canvas.toDataURL();
+    setCropperUrl(url);
+  }, []);
+
+  const preview = useCallback(() => {
+    const previewContainer = document.getElementById('previewWrapper');
+    return previewContainer;
+  }, []);
+
+  return (
+      <div >
+           <Cropper 
+              ref={ref} 
+              src={"https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg"}
+              style={containerStyle}
+              rotate={rotate}
+              zoom={zoom}
+              onZoomChange={onZoomChange}
+              preview={preview}
+           />
+           <div style={actionStyle} >
+            <span>旋转</span>
+            <Slider
+              style={{ width: 500}}
+              value={rotate}
+              step={1}
+              min={-360}
+              max={360}
+              onChange={onSliderChange}
+            />
+           </div>
+           <div style={actionStyle} >
+            <span>缩放</span>
+            <Slider
+              style={{ width: 500}}
+              value={zoom}
+              step={0.1}
+              min={0.1}
+              max={3}
+              onChange={onZoomChange}
+            />
+           </div>
+           <br />
+           <div style={{ display: 'flex', }}>
+              <div style={{ width: '50%', flexGrow: 1}}>
+                <strong>实时预览</strong>
+                <div id='previewWrapper' style={{height: 300, marginTop: 8}}/>
+              </div>
+              <div style={{width: '50%', flexGrow: 1, paddingLeft: 10 }}>
+                <Button onClick={onButtonClick}>裁切</Button>
+                <br /><br />
+                <img src={cropperUrl} style={{ width: '90%'}} />
+              </div>
+           </div>
+      </div>
+  );
+};
+
+render(<Demo />)
+```
+
 ### API
 
 | 属性 | 说明 | 类型 | 默认值 |
@@ -265,6 +361,7 @@ render(<Demo />)
 | maxZoom | 最大缩放倍数 | number | 3 |
 | minZoom | 最小缩放倍数 | number | 0.1 |
 | onZoomChange | 缩放回调 | (zoom: number) => void | - |
+| preview | 指定预览容器 | () => HTMLElement | - |
 | rotate | 旋转角度 | number | - |
 | shape | 裁切框形状 | 'rect' \| 'round' \| 'roundRect' | 'rect' |
 | src | 图片地址 | string | - |

+ 3 - 0
content/start/changelog/index-en-US.md

@@ -16,6 +16,9 @@ Version:Major.Minor.Patch (follow the **Semver** specification)
 
 ---
 
+#### 🎉 2.78.0-beta.0 (2025-04-01)
+- 【Feat】
+    - Cropper adds preview API to support real-time preview of cropping effects  [#2783](https://github.com/DouyinFE/semi-design/issues/2783)
 #### 🎉 2.77.0 (2025-03-25)
 - 【Fix】
     - Fixed the issue of click-through when the UserGuide is in the bubble mode [#2764](https://github.com/DouyinFE/semi-design/pull/2764)

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

@@ -13,7 +13,9 @@ Semi 版本号遵循 **Semver** 规范(主版本号 - 次版本号 - 修订版
 -   修订版本号(patch):仅会进行 bugfix,发布时间不限
 -   不同版本间的详细关系,可查阅 [FAQ](/zh-CN/start/faq)
 
-
+#### 🎉 2.78.0-beta.0 (2025-04-01)
+- 【Feat】
+    - Cropper 增加 preview API 用于支持实时预览裁切效果  [#2783](https://github.com/DouyinFE/semi-design/issues/2783)
 #### 🎉 2.77.0 (2025-03-25)
 - 【Fix】
     - 修复 UserGuide 在气泡模式下点击穿透的问题 [#2764](https://github.com/DouyinFE/semi-design/pull/2764)

+ 1 - 1
lerna.json

@@ -1,5 +1,5 @@
 {
     "useWorkspaces": true,
     "npmClient": "yarn",
-    "version": "2.77.1"
+    "version": "2.78.0-beta.0"
 }

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

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-animation-react",
-    "version": "2.77.1",
+    "version": "2.78.0-beta.0",
     "description": "motion library for semi-ui-react",
     "keywords": [
         "motion",
@@ -25,8 +25,8 @@
         "prepublishOnly": "npm run build:lib"
     },
     "dependencies": {
-        "@douyinfe/semi-animation": "2.77.1",
-        "@douyinfe/semi-animation-styled": "2.77.1",
+        "@douyinfe/semi-animation": "2.78.0-beta.0",
+        "@douyinfe/semi-animation-styled": "2.78.0-beta.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.77.1",
+    "version": "2.78.0-beta.0",
     "description": "semi styled animation",
     "keywords": [
         "semi",

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

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-animation",
-    "version": "2.77.1",
+    "version": "2.78.0-beta.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.77.1",
+    "version": "2.78.0-beta.0",
     "description": "semi ui eslint plugin",
     "keywords": [
         "semi",

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

@@ -60,6 +60,12 @@ export default class CropperFoundation <P = Record<string, any>, S = Record<stri
     rangeX: [number, number];
     rangeY: [number, number];
     initial: boolean;
+    previewImg: HTMLImageElement;
+    previewContainer: HTMLElement;
+    previewContainerInitSize: {
+        width: number;
+        height: number
+    };
     
     constructor(adapter: CropperAdapter<P, S>) {
         super({ ...adapter });
@@ -74,6 +80,8 @@ export default class CropperFoundation <P = Record<string, any>, S = Record<stri
         this.rangeX = null;
         this.rangeY = null;
         this.initial = false;
+        this.previewImg = null;
+        this.previewContainer = null;
     }
 
     init() {
@@ -88,6 +96,7 @@ export default class CropperFoundation <P = Record<string, any>, S = Record<stri
     destroy() {
         this.unBindMoveEvent();
         this.unBindResizeEvent();
+        this.removePreview();
     }
 
     getImgDataWhenResize = (ratio: number) => {
@@ -228,6 +237,80 @@ export default class CropperFoundation <P = Record<string, any>, S = Record<stri
             cropperBox: newCropperBoxState,
             loaded: true,
         } as any);
+
+        this.renderPreview();
+    }
+
+    renderPreview = () => {
+        const { preview, src } = this.getProps();
+        const previewNode = preview?.();
+        if (!previewNode) {
+            return;
+        }
+        const img = document.createElement('img');
+        this.previewImg = img;
+        this.previewContainer = previewNode;
+        img.src = src;
+        previewNode.appendChild(img);
+        this.previewContainer.style.overflow = 'hidden';
+        // 记录预览容器初始宽高
+        const { width: previewWidth, height: previewHeight } = previewNode.getBoundingClientRect();
+        this.previewContainerInitSize = {
+            width: previewWidth,
+            height: previewHeight,
+        };
+    }
+
+    updatePreview = (props: {
+        width: number;
+        height: number;
+        translateX: number;
+        translateY: number;
+        rotate: number
+    }) => {
+        if (!this.previewImg) {
+            return;
+        }
+        const { cropperBox } = this.getStates();
+        let zoom = 1;
+        const { width: containerWidth, height: containerHeight } = this.previewContainerInitSize;
+        let previewWidth = containerWidth;
+        let previewHeight = containerHeight;
+        if (previewWidth < previewHeight) {
+            zoom = containerWidth / cropperBox.width;
+            let tempHeight = zoom * cropperBox.height;
+            if (tempHeight > containerHeight) {
+                zoom = containerHeight / cropperBox.height;
+                previewWidth = zoom * cropperBox.width;
+            } else {
+                previewHeight = tempHeight;
+            }
+        } else {
+            zoom = containerHeight / cropperBox.height;
+            let tempWidth = zoom * cropperBox.width;
+            if (tempWidth > containerWidth) {
+                zoom = containerWidth / cropperBox.width;
+                previewHeight = zoom * cropperBox.height;
+            } else {
+                previewWidth = tempWidth;
+            }
+        }
+        const { width, height, translateX, translateY, rotate } = props;
+        // Set the image style
+        this.previewImg.style.width = `${width * zoom}px`;
+        this.previewImg.style.height = `${height * zoom}px`;
+        this.previewImg.style.transform = `translate(${translateX * zoom}px, ${translateY * zoom}px) rotate(${rotate}deg)`;
+        this.previewImg.style.transformOrigin = 'center';
+        // set preview container size
+        this.previewContainer.style.width = `${previewWidth}px`;
+        this.previewContainer.style.height = `${previewHeight}px`;
+    }
+
+    removePreview = () => {
+        if (this.previewImg && this.previewContainer) {
+            this.previewContainer.removeChild(this.previewImg);
+            this.previewImg = null;
+        }
     }
 
     handleWheel = (e: any) => {

+ 29 - 9
packages/semi-foundation/jsonViewer/foundation.ts

@@ -1,4 +1,3 @@
-
 import { JsonViewer, JsonViewerOptions, CustomRenderRule } from '@douyinfe/semi-json-viewer-core';
 import BaseFoundation, { DefaultAdapter } from '../base/foundation';
 
@@ -37,18 +36,35 @@ class JsonViewerFoundation extends BaseFoundation<JsonViewerAdapter> {
 
     }
 
-    search(searchText: string) {
-        const state = this.getState('searchOptions');
-        const { caseSensitive, wholeWord, regex } = state;
-        this.jsonViewer?.getSearchWidget().search(searchText, caseSensitive, wholeWord, regex);
+    search(searchText: string, caseSensitive?: boolean, wholeWord?: boolean, regex?: boolean) {
+        let options;
+        if (caseSensitive !== undefined || wholeWord !== undefined || regex !== undefined) {
+            options = {
+                caseSensitive: caseSensitive ?? false,
+                wholeWord: wholeWord ?? false,
+                regex: regex ?? false
+            };
+        } else {
+            options = this.getState('searchOptions');
+        }
+        const { caseSensitive: cs, wholeWord: ww, regex: rx } = options;
+        this.jsonViewer?.getSearchWidget().search(searchText, cs, ww, rx);
     }
 
-    prevSearch() {
-        this.jsonViewer?.getSearchWidget().navigateResults(-1);
+    prevSearch(step?: number) {
+        if (step === undefined) {
+            this.jsonViewer?.getSearchWidget().navigateResults(-1);
+        } else {
+            this.jsonViewer?.getSearchWidget().navigateResults(-step);
+        }
     }
 
-    nextSearch() {
-        this.jsonViewer?.getSearchWidget().navigateResults(1);
+    nextSearch(step?: number) {
+        if (step === undefined) {
+            this.jsonViewer?.getSearchWidget().navigateResults(1);
+        } else {
+            this.jsonViewer?.getSearchWidget().navigateResults(step);
+        }
     }
 
     replace(replaceText: string) {
@@ -72,6 +88,10 @@ class JsonViewerFoundation extends BaseFoundation<JsonViewerAdapter> {
     showSearchBar() {
         this._adapter.showSearchBar();
     }
+
+    getSearchResults() {
+        return this.jsonViewer?.getSearchWidget().searchResults;
+    }
 }
 
 export default JsonViewerFoundation;

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

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

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

@@ -1,6 +1,6 @@
 {
   "name": "@douyinfe/semi-icons-lab",
-  "version": "2.77.1",
+  "version": "2.78.0-beta.0",
   "description": "semi icons lab",
   "keywords": [
     "semi",

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

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-icons",
-    "version": "2.77.1",
+    "version": "2.78.0-beta.0",
     "description": "semi icons",
     "keywords": [
         "semi",

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

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-illustrations",
-    "version": "2.77.1",
+    "version": "2.78.0-beta.0",
     "description": "semi illustrations",
     "keywords": [
         "semi",

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

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-json-viewer-core",
-    "version": "2.77.1",
+    "version": "2.78.0-beta.0",
     "description": "",
     "main": "lib/index.js",
     "module": "lib/index.js",

+ 0 - 6
packages/semi-json-viewer-core/src/view/search/searchWidget.ts

@@ -8,9 +8,6 @@ import { IModelContentChangeEvent } from '../../common/emitterEvents';
  */
 export class SearchWidget {
     private _view: View;
-    private _searchInput: HTMLInputElement;
-    private _replaceInput: HTMLInputElement;
-    private _container: HTMLElement;
     private _jsonModel: JSONModel;
     //TODO: 修改searchResults存储数据结构
     public searchResults: FindMatch[] | null = null;
@@ -18,8 +15,6 @@ export class SearchWidget {
     public matchCase: boolean = false;
     public wordSeparators: string | null = null;
     public isRegex: boolean = false;
-    private _searchDiv: HTMLElement;
-    private _replaceDiv: HTMLElement;
 
     constructor(view: View, jsonModel: JSONModel) {
         this._view = view;
@@ -70,7 +65,6 @@ export class SearchWidget {
             rangeLength: endOffset - startOffset,
         };
         this.searchResults.splice(this._currentResultIndex, 1);
-
         this._jsonModel.applyOperation(op);
     }
 

+ 2 - 2
packages/semi-next/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-next",
-    "version": "2.77.1",
+    "version": "2.78.0-beta.0",
     "description": "Plugin that support Semi Design in Next.js",
     "author": "伍浩威 <[email protected]>",
     "homepage": "",
@@ -22,7 +22,7 @@
         "typescript": "^4"
     },
     "dependencies": {
-        "@douyinfe/semi-webpack-plugin": "2.77.1"
+        "@douyinfe/semi-webpack-plugin": "2.78.0-beta.0"
     },
     "gitHead": "eb34a4f25f002bb4cbcfa51f3df93bed868c831a"
 }

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

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-rspack-plugin",
-    "version": "2.77.1",
+    "version": "2.78.0-beta.0",
     "description": "",
     "homepage": "",
     "license": "MIT",

+ 1 - 1
packages/semi-scss-compile/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-scss-compile",
-    "version": "2.77.1",
+    "version": "2.78.0-beta.0",
     "description": "compile semi scss to css",
     "author": "[email protected]",
     "license": "MIT",

+ 1 - 1
packages/semi-theme-default/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-theme-default",
-    "version": "2.77.1",
+    "version": "2.78.0-beta.0",
     "description": "semi-theme-default",
     "keywords": [
         "semi-theme",

+ 75 - 0
packages/semi-ui/cropper/_story/cropper.stories.jsx

@@ -317,4 +317,79 @@ export const NoResizeBox = () => {
   );
 }
 
+export const RealTimePreview  = () => {
+  const [rotate, setRotate] = useState(0);
+  const [zoom, setZoom] = useState(1);
+  const [cropperUrl, setCropperUrl ] = useState('');
+  const ref = useRef();
+
+  const onZoomChange = useCallback((value) => {
+    setZoom(value);
+  })
+
+  const onSliderChange = useCallback((value) => {
+    setRotate(value);
+  }, []);
+
+  const onButtonClick = useCallback(() => {
+    const canvas = ref.current.getCropperCanvas();
+    const url = canvas.toDataURL();
+    setCropperUrl(url);
+  }, []);
+
+  const preview = useCallback(() => {
+    const previewContainer = document.getElementById('previewContainer');
+    return previewContainer;
+  }, []);
+
+  return (
+      <div id='cropper-container'>
+           <Cropper 
+              ref={ref} 
+              src={"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/9130eh7pltbfnuhog/16.jpeg'}
+              style={containerStyle}
+              rotate={rotate}
+              zoom={zoom}
+              onZoomChange={onZoomChange}
+              preview={preview}
+           />
+           <div style={actionStyle} >
+            <span>Rotate</span>
+            <Slider
+              style={{ width: 720}}
+              value={rotate}
+              step={1}
+              min={-360}
+              max={360}
+              onChange={onSliderChange}
+            />
+           </div>
+           <div style={actionStyle} >
+            <span>Zoom</span>
+            <Slider
+              style={{ width: 720}}
+              value={zoom}
+              step={0.1}
+              min={0.1}
+              max={3}
+              onChange={onZoomChange}
+            />
+           </div>
+           <br />
+           <div style={{ display: 'flex', columnGap: 10}}>
+              <div style={{width: '50%', flexGrow: 1 }}>
+                <Button onClick={onButtonClick}>Cropper</Button>
+                <br /><br />
+                <img src={cropperUrl} style={{ width: '100%'}} />
+              </div>
+              <div style={{width: '50%', flexGrow: 1 }}>
+                <span style={{marginBottom: 4}}>Preview</span>
+                <div id='previewContainer' style={{height: 300, marginTop: 8}}/>
+              </div>
+           </div>
+      </div>
+  );
+};
+
 

+ 12 - 3
packages/semi-ui/cropper/index.tsx

@@ -36,9 +36,10 @@ interface CropperProps {
     cropperBoxCls?: string;
     /* The fill color of the non-picture parts in the cut result */
     fill?: string;
-    maxZoom: number;
-    minZoom: number;
-    zoomStep: number
+    maxZoom?: number;
+    minZoom?: number;
+    zoomStep?: number;
+    preview?: () => HTMLElement
 }
 
 interface CropperState {
@@ -219,6 +220,14 @@ class Cropper extends BaseComponent<CropperProps, CropperState> {
         const cropperImgX = imgX - cropperBoxX;
         const cropperImgY = imgY - cropperBoxY;
 
+        this.foundation.updatePreview({
+            width: imgData.width,
+            height: imgData.height,
+            translateX: cropperImgX, 
+            translateY: cropperImgY,
+            rotate: rotate,
+        });
+
         return (<ResizeObserver 
             onResize={this.foundation.handleResize} 
             observerProperty={ObserverProperty.Width}

+ 31 - 1
packages/semi-ui/jsonViewer/index.tsx

@@ -24,6 +24,7 @@ import { createPortal } from 'react-dom';
 import { isEqual } from "lodash";
 import LocaleConsumer from '../locale/localeConsumer';
 import { Locale } from '../locale/interface';
+
 const prefixCls = cssClasses.PREFIX;
 
 export type { JsonViewerOptions };
@@ -45,7 +46,7 @@ export interface JsonViewerState {
     customRenderMap: Map<HTMLElement, React.ReactNode>
 }
 
-interface SearchOptions {
+export interface SearchOptions {
     caseSensitive: boolean;
     wholeWord: boolean;
     regex: boolean
@@ -127,6 +128,11 @@ class JsonViewerCom extends BaseComponent<JsonViewerProps, JsonViewerState> {
             },
             showSearchBar: () => {
                 this.setState({ showSearchBar: !this.state.showSearchBar });
+                this.setState({ searchOptions: {
+                    caseSensitive: false,
+                    wholeWord: false,
+                    regex: false,
+                } });
             },
         };
     }
@@ -139,6 +145,30 @@ class JsonViewerCom extends BaseComponent<JsonViewerProps, JsonViewerState> {
         this.foundation.jsonViewer.format();
     }
 
+    search(searchText: string, caseSensitive?: boolean, wholeWord?: boolean, regex?: boolean) {
+        this.foundation.search(searchText, caseSensitive, wholeWord, regex);
+    }
+
+    getSearchResults() {
+        return this.foundation.getSearchResults();
+    }
+
+    prevSearch(step?: number) {
+        this.foundation.prevSearch(step);
+    }
+
+    nextSearch(step?: number) {
+        this.foundation.nextSearch(step);
+    }
+
+    replace(replaceText: string) {
+        this.foundation.replace(replaceText);
+    }
+
+    replaceAll(replaceText: string) {
+        this.foundation.replaceAll(replaceText);
+    }
+
     getStyle() {
         const { width, height } = this.props;
         return {

+ 7 - 7
packages/semi-ui/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-ui",
-    "version": "2.77.1",
+    "version": "2.78.0-beta.0",
     "description": "A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.",
     "main": "lib/cjs/index.js",
     "module": "lib/es/index.js",
@@ -20,12 +20,12 @@
         "@dnd-kit/core": "^6.0.8",
         "@dnd-kit/sortable": "^7.0.2",
         "@dnd-kit/utilities": "^3.2.1",
-        "@douyinfe/semi-animation": "2.77.1",
-        "@douyinfe/semi-animation-react": "2.77.1",
-        "@douyinfe/semi-foundation": "2.77.1",
-        "@douyinfe/semi-icons": "2.77.1",
-        "@douyinfe/semi-illustrations": "2.77.1",
-        "@douyinfe/semi-theme-default": "2.77.1",
+        "@douyinfe/semi-animation": "2.78.0-beta.0",
+        "@douyinfe/semi-animation-react": "2.78.0-beta.0",
+        "@douyinfe/semi-foundation": "2.78.0-beta.0",
+        "@douyinfe/semi-icons": "2.78.0-beta.0",
+        "@douyinfe/semi-illustrations": "2.78.0-beta.0",
+        "@douyinfe/semi-theme-default": "2.78.0-beta.0",
         "async-validator": "^3.5.0",
         "classnames": "^2.2.6",
         "copy-text-to-clipboard": "^2.1.1",

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

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-webpack-plugin",
-    "version": "2.77.1",
+    "version": "2.78.0-beta.0",
     "description": "",
     "author": "伍浩威 <[email protected]>",
     "homepage": "",

File diff suppressed because it is too large
+ 197 - 197
sitemap.xml


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