使用场景
Cropper 用于裁切图片,支持自定义裁切框样式,可通过拖动调整裁切框位置,被裁切图片位置;可缩放,旋转被裁切图片。
代码演示
如何引入
import { Cropper } from '@fibrous/semi-ui-solid';
基本用法
通过 src 设置被裁切的图片; 可通过 shape 设置裁切框形状,默认为方形。


代码
import { Show, createSignal } from 'solid-js';
import { Button, Cropper, Radio, RadioGroup, type CropperElement, type CropperProps, type RadioChangeEvent } from '@fibrous/semi-ui-solid';
const containerStyle = {
width: '550px',
height: '300px',
margin: '20px',
};
export default function CropperDemo() {
let cropper: CropperElement | undefined;
const [shape, setShape] = createSignal<CropperProps['shape']>('rect');
const [cropperUrl, setCropperUrl] = createSignal('');
const onButtonClick = () => {
const canvas = cropper?.getCropperCanvas();
if (canvas) setCropperUrl(canvas.toDataURL());
};
const onShapeChange = (event: RadioChangeEvent) => {
setShape(event.target.value);
};
return (
<>
<RadioGroup aria-label="裁切框形状" onChange={onShapeChange} value={shape()}>
<Radio value="rect">rect</Radio>
<Radio value="round">round</Radio>
<Radio value="roundRect">roundRect</Radio>
</RadioGroup>
<Cropper ref={(node) => (cropper = node)} src="/assets/other/image.png" style={containerStyle} shape={shape()} />
<Button onClick={onButtonClick}>裁切</Button>
<br />
<br />
<Show when={cropperUrl()}>
<img src={cropperUrl()} alt="裁切结果" style={{ height: '400px' }} />
</Show>
</>
);
}自定义裁切框比例
可通过 defaultAspectRatio 初始的裁切框比例(默认为 1)。可通过 aspectRatio 设置固定的裁切框比例。
设置 defaultAspectRatio仅对初始的裁切框比例生效, 拖动时,裁切框比例会随着拖动而变化。
设置 aspectRatio 时,裁切框比例固定,拖动时将裁切框将以此比例变化。图片加载后改变 aspectRatio,裁切框会按新比例重排:保持高度,宽度随比例变化,超出容器时缩到容器内,中心尽量保持不动;aspectRatio 变为 undefined 时不重排。


代码
import { Show, createSignal } from 'solid-js';
import { Button, Cropper, type CropperElement } from '@fibrous/semi-ui-solid';
const containerStyle = {
width: '550px',
height: '300px',
margin: '20px',
};
export default function CropperDemo() {
let cropper: CropperElement | undefined;
const [cropperUrl, setCropperUrl] = createSignal('');
const onButtonClick = () => {
const canvas = cropper?.getCropperCanvas();
if (canvas) setCropperUrl(canvas.toDataURL());
};
return (
<>
<Cropper aspectRatio={3 / 4} ref={(node) => (cropper = node)} src="/assets/other/image.png" style={containerStyle} />
<Button onClick={onButtonClick}>裁切</Button>
<br />
<br />
<Show when={cropperUrl()}>
<img src={cropperUrl()} alt="裁切结果" style={{ height: '400px' }} />
</Show>
</>
);
}受控旋转/缩放图片
通过 rotate 和 zoom 控制图片旋转和缩放,可通过 onZoomChange 拿到最新的 zoom 值。rotate 为单向绑定,组件不会改变它。滚轮缩放时会调用 onZoomChange,父组件需在回调中更新 zoom,否则缩放会被还原为受控的值。


代码
import { Show, createSignal } from 'solid-js';
import { Button, Cropper, Slider, type CropperElement } from '@fibrous/semi-ui-solid';
const containerStyle = {
width: '550px',
height: '300px',
margin: '20px',
};
const actionStyle = {
'margin-top': '20px',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
width: 'fit-content',
};
export default function CropperDemo() {
let cropper: CropperElement | undefined;
const [rotate, setRotate] = createSignal(0);
const [zoom, setZoom] = createSignal(1);
const [cropperUrl, setCropperUrl] = createSignal('');
const onSliderChange = (value?: number | number[]) => {
if (typeof value === 'number') setRotate(value);
};
const onZoomSliderChange = (value?: number | number[]) => {
if (typeof value === 'number') setZoom(value);
};
const onButtonClick = () => {
const canvas = cropper?.getCropperCanvas();
if (canvas) setCropperUrl(canvas.toDataURL());
};
return (
<div id="cropper-container">
<Cropper ref={(node) => (cropper = node)} src="/assets/other/image.png" style={containerStyle} rotate={rotate()} zoom={zoom()} onZoomChange={(value) => setZoom(value)} />
<div style={actionStyle}>
<span id="cropper-rotate-zoom-rotate">旋转</span>
<Slider aria-labelledby="cropper-rotate-zoom-rotate" style={{ width: '500px' }} value={rotate()} step={1} min={-360} max={360} onChange={onSliderChange} />
</div>
<div style={actionStyle}>
<span id="cropper-rotate-zoom-zoom">缩放</span>
<Slider aria-labelledby="cropper-rotate-zoom-zoom" style={{ width: '500px' }} value={zoom()} step={0.1} min={0.1} max={3} onChange={onZoomSliderChange} />
</div>
<br />
<Button onClick={onButtonClick}>裁切</Button>
<br />
<br />
<Show when={cropperUrl()}>
<img src={cropperUrl()} alt="裁切结果" style={{ height: '400px' }} />
</Show>
</div>
);
}裁切框设置
可通过 cropperBoxStyle, cropperBoxCls 自定义裁切框样式。可通过 showResizeBox 设置是否展示裁切框边角的调整块。


代码
import { Show, createSignal } from 'solid-js';
import { Button, Cropper, type CropperElement } from '@fibrous/semi-ui-solid';
const containerStyle = {
width: '550px',
height: '300px',
margin: '20px',
};
export default function CropperDemo() {
let cropper: CropperElement | undefined;
const [cropperUrl, setCropperUrl] = createSignal('');
const onButtonClick = () => {
const canvas = cropper?.getCropperCanvas();
if (canvas) setCropperUrl(canvas.toDataURL());
};
return (
<>
<strong>showResizeBox = false,并修改边框颜色</strong>
<Cropper ref={(node) => (cropper = node)} src="/assets/other/image.png" style={containerStyle} cropperBoxStyle={{ 'outline-color': 'var(--semi-color-bg-0)' }} showResizeBox={false} />
<Button onClick={onButtonClick}>裁切</Button>
<br />
<br />
<Show when={cropperUrl()}>
<img src={cropperUrl()} alt="裁切结果" style={{ height: '400px' }} />
</Show>
</>
);
}实时预览裁切效果
通过 preview 指定预览容器,实时预览裁切效果。preview 是返回容器 DOM 的函数,容器尚未挂载时可返回 null。


代码
import { Show, createSignal } from 'solid-js';
import { Button, Cropper, Slider, type CropperElement } from '@fibrous/semi-ui-solid';
const containerStyle = {
width: '550px',
height: '300px',
margin: '20px',
};
const actionStyle = {
'margin-top': '20px',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
width: 'fit-content',
};
export default function CropperDemo() {
let cropper: CropperElement | undefined;
let previewWrapper: HTMLDivElement | undefined;
const [rotate, setRotate] = createSignal(0);
const [zoom, setZoom] = createSignal(1);
const [cropperUrl, setCropperUrl] = createSignal('');
const onSliderChange = (value?: number | number[]) => {
if (typeof value === 'number') setRotate(value);
};
const onZoomSliderChange = (value?: number | number[]) => {
if (typeof value === 'number') setZoom(value);
};
const onButtonClick = () => {
const canvas = cropper?.getCropperCanvas();
if (canvas) setCropperUrl(canvas.toDataURL());
};
return (
<div>
<Cropper ref={(node) => (cropper = node)} rotate={rotate()} zoom={zoom()} onZoomChange={(value) => setZoom(value)} preview={() => previewWrapper ?? null} src="/assets/root-web-sites/abstract.jpg" style={containerStyle} />
<div style={actionStyle}>
<span id="cropper-preview-rotate">旋转</span>
<Slider aria-labelledby="cropper-preview-rotate" style={{ width: '500px' }} value={rotate()} step={1} min={-360} max={360} onChange={onSliderChange} />
</div>
<div style={actionStyle}>
<span id="cropper-preview-zoom">缩放</span>
<Slider aria-labelledby="cropper-preview-zoom" style={{ width: '500px' }} value={zoom()} step={0.1} min={0.1} max={3} onChange={onZoomSliderChange} />
</div>
<br />
<div style={{ display: 'flex' }}>
<div style={{ width: '50%', 'flex-grow': '1' }}>
<strong>实时预览</strong>
<div ref={(node) => (previewWrapper = node)} style={{ height: '300px', 'margin-top': '8px' }} />
</div>
<div style={{ width: '50%', 'flex-grow': '1', 'padding-left': '10px' }}>
<Button onClick={onButtonClick}>裁切</Button>
<br />
<br />
<Show when={cropperUrl()}>
<img src={cropperUrl()} alt="裁切结果" style={{ width: '90%' }} />
</Show>
</div>
</div>
</div>
);
}API
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| aspectRatio | 固定的裁切框宽高比;加载后改变时,裁切框按新比例重排。 | number | - |
| cropperBoxCls | 裁切框类名。 | string | - |
| cropperBoxStyle | 裁切框样式,与组件计算的宽高和位移合并,宽高与位移以组件为准。 | JSX.CSSProperties | - |
| defaultAspectRatio | 裁切框的初始宽高比,拖动时会随之变化。 | number | 1 |
| fill | 裁切结果中非图片部分的填充色。 | string | "rgba(0, 0, 0, 0)" |
| imgProps | 传给被裁切图片 img 标签的属性。src 与布局样式由组件决定,class 与组件自身的类名合并,style 位于组件布局之下(宽高与位移以组件为准),onLoad 在组件处理完成后调用;alt 默认为空字符串,crossorigin 默认为 anonymous,均可覆盖。 | CropperImgProps | - |
| maxZoom | 最大缩放倍数。 | number | 3 |
| minZoom | 最小缩放倍数。 | number | 0.1 |
| onZoomChange | 滚轮缩放时触发,参数为新的缩放比例。 | Type(zoom: number) => void | - |
| preview | 返回预览容器 DOM 的函数,裁切效果会实时镜像到该容器;可返回 null,表示容器尚未挂载。 | Type() => HTMLElement | null | - |
| ref | 根 div 元素,其上附加 getCropperCanvas 方法。 | TypeRef<CropperElement> | - |
| rotate | 图片旋转角度,单向绑定,组件不会改变它。 | number | - |
| shape | 裁切框形状。 | 'rect' | 'round' | 'roundRect' | "rect" |
| showResizeBox | 是否展示裁切框边角的调整块。 | boolean | true |
| src | 被裁切图片的地址。 | string | - |
| zoom | 受控的缩放比例,叠加在图片适应裁切区域的初始缩放之上。 | number | - |
| zoomStep | 滚轮缩放的步长。 | number | 0.1 |
Methods
| 方法 | 说明 | 类型 |
|---|---|---|
| getCropperCanvas | 将裁切框内的区域按图片自身的分辨率绘制到新的 canvas 上。 | Type() => HTMLCanvasElement |
class、style、ref 与原生属性和事件(如 id、data-*、aria-*、onKeyDown)落在根 <div> 上;ref 拿到的就是根元素,其上附加了 getCropperCanvas 方法。
Accessibility
- 根元素传入
aria-label或aria-labelledby且未传role时,自动带role="group";传入的role优先 - 裁切框是可聚焦的命名分组(
role="group",名称与角色描述取自语言包,并通过aria-describedby指向操作提示),图片加载完成后才可聚焦。被裁切的img是装饰性的,alt默认为空字符串,可经imgProps覆盖 - 只有主按钮的鼠标按下,以及 touch、pen 触点,才开始拖动裁切框、调整块和图片;鼠标中键、右键按下不会开始拖动
键盘和焦点
- 裁切框是组件内唯一的 Tab 停靠点,图片加载完成后才可聚焦
- 焦点在裁切框上时,方向键每次移动裁切框 10px,不会移出容器
- Shift + 方向键每次移动裁切框右下角 10px 来调整大小,限制在容器内;设置了
aspectRatio时保持该比例(横向的变化换算为高度)。仅在showResizeBox为 true(有调整块)时可用,为 false 时 Shift + 方向键不起作用 - 缩放与旋转没有内置的键盘操作,需要使用方提供可用键盘操作的控件(如示例中的 Slider)
设计变量
| Sass 变量 | 值 | 用法 |
|---|---|---|
| $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 |