概要
一个老项目维护新功能,提出了对图片的各种处理,由于某些原因不能使用Antd的Image组件(但是实际上使用了Antd其他组件,客户总会提出奇怪的要求),于是自己实现了一个常见的处理图片功能。
本文将介绍如何实现一个完整的图片上传预览系统,包括上传、预览、移动、缩放和旋转等功能。
我们实现的功能包括:
- 图片上传和预览
- 鼠标拖动移动图片位置
- 滚轮缩放图片大小
- 按钮控制图片旋转
- 滑块控制精确缩放比例。
完整代码跑出来的样子,可以根据自己的需求修改样式和自定义展示逻辑
核心代码
图片上传组件
使用Ant Design的Upload组件实现图片上传功能:
<Upload
name="avatar"
listType="picture-card"
className="avatar-uploader"
showUploadList={false}
onChange={handleChange}
beforeUpload={(file) => {
const isJpgOrPng =
file.type === "image/jpeg" || file.type === "image/png";
if (!isJpgOrPng) {
message.error("只能上传 JPG/PNG 文件!");
}
return false; // 返回false阻止自动上传
}}
accept="image/jpeg,image/png"
>
{imgUrl ? (
<img
src={imgUrl}
alt="avatar"
style={{
width: "auto",
height: "auto",
maxWidth: "100%",
maxHeight: 300,
objectFit: "contain",
}}
/>
) : (
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload</div>
</div>
)}
</Upload>
图片移动功能
const handleMouseDown = (e: React.MouseEvent) => {
setIsDragging(true);
setDragStartPos({
x: e.clientX - moveConfig.moveX,
y: e.clientY - moveConfig.moveY,
});
};
const handleMouseMove = (e: React.MouseEvent) => {
if (!isDragging) return;
setMoveConfig({
moveX: e.clientX - dragStartPos.x,
moveY: e.clientY - dragStartPos.y,
});
};
const handleMouseUp = () => {
setIsDragging(false);
};
图片缩放功能
useEffect(() => {
if (imgDivRef.current) {
const handleWheel = (e: WheelEvent) => {
e.preventDefault();
e.stopImmediatePropagation();
const delta = e.deltaY > 0 ? -0.1 : 0.1;
setScale((prev) => Math.max(0.1, Math.min(3, prev + delta)));
};
imgDivRef.current.addEventListener("wheel", handleWheel, {
passive: false,
});
return () => {
imgDivRef.current?.removeEventListener("wheel", handleWheel);
};
}
}, [imgUrl]);
图片旋转功能
const handleRotate = (angle: number) => {
setRotate((prevRotate) => prevRotate + angle);
};
// 在渲染中使用
<Button icon={<RotateLeftOutlined />} onClick={() => handleRotate(-90)} />
<Button icon={<RotateRightOutlined />} onClick={() => handleRotate(90)} />
技术要点分析
- 事件处理优化 :
- 使用 passive: false 确保能阻止默认滚动行为
- 通过 stopImmediatePropagation 阻止事件冒泡
- 在useEffect中正确添加和移除事件监听器
- 性能考虑 :
- 使用transform代替直接修改位置属性,利用GPU加速
- 限制缩放范围(0.1-3倍)防止极端情况
- 用户体验 :
- 用户体验 :
- 拖动时显示grabbing光标提升交互感
- 限制文件类型为JPG/PNG
- 提供多种控制方式(按钮+滑块)
完整代码
import React, { useState, useRef, useEffect } from "react";
import { Upload, Button, Slider, message, Space } from "antd";
import {
PlusOutlined,
RotateLeftOutlined,
RotateRightOutlined,
ZoomInOutlined,
ZoomOutOutlined,
} from "@ant-design/icons";
import type { RcFile, UploadProps } from "antd/es/upload";
interface MoveConfig {
moveX: number;
moveY: number;
}
const PictureUpload: React.FC = () => {
const [imgUrl, setImgUrl] = useState<string>("");
const [scale, setScale] = useState<number>(1);
const [rotate, setRotate] = useState<number>(0);
const [moveConfig, setMoveConfig] = useState<MoveConfig>({
moveX: 0,
moveY: 0,
});
const [isDragging, setIsDragging] = useState(false);
const [dragStartPos, setDragStartPos] = useState({ x: 0, y: 0 });
const imgRef = useRef<HTMLImageElement>(null);
const imgDivRef = useRef<HTMLDivElement>(null);
const getBase64 = (img: RcFile, callback: (url: string) => void) => {
const reader = new FileReader();
reader.addEventListener("load", () => callback(reader.result as string));
reader.readAsDataURL(img);
};
const handleChange: UploadProps["onChange"] = (info) => {
const file = info.file as RcFile;
console.log("Uploaded file:", info, file); // 打印上传的文件对象
if (file) {
getBase64(file, (url) => {
setImgUrl(url);
});
}
};
const handleMouseDown = (e: React.MouseEvent) => {
setIsDragging(true);
setDragStartPos({
x: e.clientX - moveConfig.moveX,
y: e.clientY - moveConfig.moveY,
});
};
const handleMouseMove = (e: React.MouseEvent) => {
if (!isDragging) return;
setMoveConfig({
moveX: e.clientX - dragStartPos.x,
moveY: e.clientY - dragStartPos.y,
});
};
const handleMouseUp = () => {
setIsDragging(false);
};
const handleRotate = (angle: number) => {
setRotate((prevRotate) => prevRotate + angle);
};
const handleScale = (value: number | Function) => {
// 如果直接传入值(来自Slider)
if (typeof value === "number") {
setScale(Math.max(0.1, Math.min(3, value))); // 限制在0.1-3范围内
}
// 如果来自按钮点击(增量)
else if (typeof value === "function") {
setScale((prev) => {
const newScale = value(prev);
return Math.max(0.1, Math.min(3, newScale)); // 限制在0.1-3范围内
});
}
};
useEffect(() => {
if (imgDivRef.current) {
const handleWheel = (e: WheelEvent) => {
console.log("wheel event:", e);
e.preventDefault();
e.stopImmediatePropagation();
const delta = e.deltaY > 0 ? -0.1 : 0.1;
setScale((prev) => Math.max(0.1, Math.min(3, prev + delta)));
return false;
};
// 添加非passive事件监听
imgDivRef.current.addEventListener("wheel", handleWheel, {
passive: false,
});
return () => {
imgDivRef.current.removeEventListener("wheel", handleWheel);
};
}
}, [imgUrl]); // 依赖imgUrl,确保每次imgUrl变化时重新添加事件
// 修改Upload组件配置
return (
<div style={{ padding: 24 }}>
<Upload
name="avatar"
listType="picture-card"
className="avatar-uploader"
showUploadList={false}
onChange={handleChange}
beforeUpload={(file) => {
const isJpgOrPng =
file.type === "image/jpeg" || file.type === "image/png";
if (!isJpgOrPng) {
message.error("只能上传 JPG/PNG 文件!");
}
return false; // 返回false阻止自动上传
}}
accept="image/jpeg,image/png"
>
{imgUrl ? (
<img
src={imgUrl}
alt="avatar"
style={{
width: "auto",
height: "auto",
maxWidth: "100%",
maxHeight: 300,
objectFit: "contain",
}}
/>
) : (
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload</div>
</div>
)}
</Upload>
{imgUrl && (
<div>
{/* 图片编辑区域 */}
<div
ref={imgDivRef}
style={{
position: "relative",
overflow: "hidden",
width: "100%",
height: 660,
border: "1px dashed #ccc",
marginTop: 16,
}}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
<img
ref={imgRef}
src={imgUrl}
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: `translate(calc(-50% + ${moveConfig.moveX}px), calc(-50% + ${moveConfig.moveY}px)) scale(${scale}) rotate(${rotate}deg)`,
cursor: isDragging ? "grabbing" : "grab",
transformOrigin: "center center",
maxWidth: "none",
maxHeight: "none",
width: "auto",
height: "auto",
}}
draggable={false}
onLoad={(e) => {
const img = e.target as HTMLImageElement;
if (imgRef.current) {
imgRef.current.width = img.naturalWidth;
imgRef.current.height = img.naturalHeight;
}
}}
/>
</div>
{/* 控制按钮区域 */}
<div style={{ marginTop: 16 }}>
<Space.Compact>
<Button
icon={<RotateLeftOutlined />}
onClick={() => handleRotate(-90)}
/>
<Button
icon={<RotateRightOutlined />}
onClick={() => handleRotate(90)}
/>
<Button
icon={<ZoomInOutlined />}
onClick={() => handleScale(scale + 0.1)}
/>
<Button
icon={<ZoomOutOutlined />}
onClick={() => handleScale(scale - 0.1)}
/>
</Space.Compact>
{/* 缩放控制 */}
<div style={{ marginTop: 16 }}>
<h4>缩放: {scale.toFixed(1)}x</h4>
<Slider
min={0.1}
max={3}
step={0.1}
value={scale}
onChange={handleScale}
/>
</div>
</div>
</div>
)}
</div>
);
};
export default PictureUpload;
实现效果
最终实现的图片编辑器具有以下特点:
- 简洁的上传界面
- 直观的拖拽移动体验
- 平滑的滚轮缩放效果
- 精确的旋转控制
- 实时的缩放比例显示
小结
这个实现可以轻松集成到各种Web应用中,为用户提供完善的图片编辑体验。通过进一步扩展,还可以添加更多功能如裁剪、滤镜等,打造更强大的图片编辑器。