React 实现图片裁剪/上传/预览/下载/删除(主要是图片裁剪)
Posted 张志翔ۤ
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React 实现图片裁剪/上传/预览/下载/删除(主要是图片裁剪)相关的知识,希望对你有一定的参考价值。
最近工作中需要封装一个图片裁剪组件,也是封装了两天时间才完成,特此记录便于日后查阅。
先看一下最终效果,图示如下:
裁剪之后的图片可以预览,下载,删除,裁剪需要用到NodeJs的 jimp 组件,安装一下:
$ npm install --save jimp
组件代码,如下所示:
1、UploadCutImage.jsx
import {message, Modal, Upload} from 'antd';
import React, {useEffect, useState} from 'react';
import {DeleteOutlined, DownloadOutlined, LoadingOutlined, UploadOutlined,} from '@ant-design/icons';
import {uploadFile} from './service';
import CutImg from "@/components/CutImg/CutImg";
import Jimp from 'jimp/es';
function beforeUpload(file) {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
if (!isJpgOrPng) {
message.error('只能上传JPG/PNG文件!');
}
const isLt2M = file.size / 1024 / 1024 < 10;
if (!isLt2M) {
message.error('上传的文件不能超过10MB!');
}
return isJpgOrPng && isLt2M;
}
const UploadCutImage = (props) => {
const {onChange, value, disable} = props; // 有默认传来的 chang事件,和 value值
const [loading, setLoading] = useState(false);
const [fileList, setFileList] = useState([]);
const [imageUrl, setImageUrl] = useState();
const [previewImage, setPreviewImage] = useState(null);
const [previewVisible, setPreviewVisible] = useState(false);
const [cutImgVisible, setCutImgVisible] = useState(false);
const [originImgUrl, setOriginImgUrl] = useState();
const [cutImgUrl, setCutImgUrl] = useState();
// console.log(value);
useEffect(() => {
if (value !== null && value !== undefined && value.length > 0) {
setImageUrl(value);
setFileList([
{
uid: '-1',
status: 'done',
url: value,
},
]);
} else {
setImageUrl(null);
setFileList([]);
}
}, [value]);
const uploadButton = (
<div>
{loading ? <LoadingOutlined/> : <UploadOutlined/>}
<div style={{marginTop: 8}}>上传</div>
</div>
);
const handleDownload = (info) => {
window.location.href = imageUrl;
};
const handleRemove = (info) => {
//setFileList([]);
setOriginImgUrl(null);
setCutImgUrl(null);
setImageUrl(null);
onChange('');
return true;
};
const handlePreview = (info) => {
setPreviewImage(imageUrl);
setPreviewVisible(true);
};
const handleCancelPreview = () => {
setPreviewVisible(false);
};
const handleChange = ({file, fileList}) => {
if (file.status == 'removed') {
setFileList([]);
}
};
const doImgUpload = async (options) => {
const {file} = options;
const imgItem = {
uid: '1', // 注意,这个uid一定不能少,否则上传失败
name: file['name'],
status: 'uploading',
url: '',
percent: 99, // 注意不要写100。100表示上传完成
};
setFileList([imgItem]);
//原始图片上传
let formData = new FormData();
formData.append('file', file);
await uploadFile(formData)
.then(r => {
// 弹出裁剪层,把url传过去
const url = r['data']['url'];
setOriginImgUrl(url);
setCutImgVisible(true);
})
.catch((e) => {
console.log('smyhvae 图片上传失败:' + JSON.stringify(e || ''));
message.error('图片上传失败,请重试');
});
};
return (
<>
<CutImg
image={originImgUrl}
visible={cutImgVisible}
onOK={async (data) => {
const wh = data['originSize']['width'] < data['originSize']['height'] ? data['originSize']['width'] : data['originSize']['height'];
const x = -(data['position']['x'] * (data['originSize']['width'] / data['imgSize']['width'])), y = data['position']['y'], w = wh, h = wh;
// 裁剪图片&上传
await Jimp.read(data['img'],
(err, image) => {
if (err) throw err;
//裁剪图片
image.crop(x, y, w, h).getBase64Async(Jimp.MIME_PNG).then((base64Url) => {
let bytes = window.atob(base64Url.split(',')[1]);
let array = [];
for(let i = 0; i < bytes.length; i++){
array.push(bytes.charCodeAt(i));
}
let blob = new Blob([new Uint8Array(array)], {type: 'image/png'});
let formData = new FormData();
const filename = Date.now() + '.png';
formData.append('file', blob, filename);
//上传图片
uploadFile(formData)
.then(r => {
// 关闭裁剪层
const url = r['data']['url'];
setCutImgUrl(url);
setCutImgVisible(false);
const imgItem = {
uid: '1', // 注意,这个uid一定不能少,否则上传失败
name: filename,
status: 'done',
url: url, // url 是展示在页面上的绝对链接
imgUrl: url, // imgUrl 是存到 db 里的相对链接
};
onChange && onChange(url);
setImageUrl(url);
setFileList([imgItem]);
})
.catch((e) => {
console.log('图片上传失败:' + JSON.stringify(e || ''));
message.error('图片上传失败,请重试');
});
});
})
}}
onClose={() => {
setFileList([]);
setCutImgVisible(false);
}}
/>
<Upload
accept="image/jpeg,image/png"
multiple={false}
name="file"
fileList={fileList}
listType="picture-card"
showUploadList={{
showDownloadIcon: true,
downloadIcon: <DownloadOutlined style={{color: 'x00000'}}/>,
showRemoveIcon: !disable,
removeIcon: <DeleteOutlined/>,
}}
action="2"
customRequest={doImgUpload}
beforeUpload={beforeUpload}
onChange={handleChange}
onDownload={handleDownload}
onRemove={handleRemove}
onPreview={handlePreview}
disabled={disable}
>
{imageUrl ? null : uploadButton}
</Upload>
<div>
<Modal visible={previewVisible} footer={null} onCancel={handleCancelPreview}>
<img alt="picture" style={{width: '100%', height: '100%'}} src={previewImage}/>
</Modal>
</div>
</>
);
};
export default UploadCutImage;
上述代码用到了 <CutImg/> ,这个就是我们的核心实现类了。
2、CutEffect.jsx
import React, { Component } from "react";
import { Modal, Slider, Button } from "antd";
import Draggable from "react-simple-draggable";
import styles from "./CutEffect.less";
const Success = Modal.success;
const Error = Modal.error;
class CutEffect extends Component {
constructor() {
super();
this.viewNode = React.createRef();
this.state = {
objStyles: { width: "auto", height: "auto" }, //首次渲染图片的显示样式
controled: {}, //可拖拽的范围
origin: { width: 0, height: 0 }, //第一次渲染图片后的图片的宽高
computed: { computedWidth: 0, computedHeight: 0 }, //第一次渲染图片后的图片的宽高
initPosition: { x: 0, y: 0 }, //初始拖拽的位置
value: 0, //滑动条的值
preRatio: 1, //前一次的放大缩小的 比例
draggedPosition: {
x: 0,
y: 0
}, //拖拽后的位置,仅传给后端
title: "修改成功!" //弹窗title
};
}
handleDrag(e) {
// console.log(this.state);
const { childNode } = this;
childNode.style.top = e.y + "px";
childNode.style.left = e.x + "px";
this.setState({
draggedPosition: e
});
}
setImageData(ratio) {
const {
viewNode: { current }
} = this;
const { preRatio } = this.state;
const { computedWidth, computedHeight } = this.state.computed;
let width = computedWidth * ratio;
let height = computedHeight * ratio;
this.setState(
() => {
return {
objStyles: { width, height }
};
},
() => {
let x, y;
x = -((Math.abs(current.parentNode.offsetLeft) / preRatio) * ratio + 80 * (ratio - preRatio));
y = -((Math.abs(current.parentNode.offsetTop) / preRatio) * ratio + 80 * (ratio - preRatio));
if (x > 0) {
x = 0;
}
if (y > 0) {
y = 0;
}
if (preRatio > ratio) {
//特殊位置处理 left
if (current.offsetWidth - Math.abs(current.parentNode.offsetLeft) < 250) {
x = -(current.offsetWidth - 250);
}
//特殊位置处理 top
if (current.offsetHeight - Math.abs(current.parentNode.offsetTop) < 250) {
y = -(current.offsetHeight - 250);
}
}
let initPosition = {
x: x,
y: y
};
this.setState(() => {
return {
controled: {
top: -(height - 250),
left: -(width - 250),
bottom: 0,
right: 0
},
initPosition,
draggedPosition:initPosition,
preRatio: ratio
};
});
}
);
}
handleChange(val) {
const ratio = 1 + val;
this.setState(() => {
return {
value: val
};
});
this.setImageData(ratio);
}
judgeProperty(target) {
//原始图片的尺寸
const imgObj = {
width: target.naturalWidth,
height: target.naturalHeight
};
//设置原始图片尺寸
this.setState({
origin: {width: target.naturalWidth, height: target.naturalHeight}
});
if (imgObj.width > imgObj.height) {
this.setState(() => {
return {
objStyles: { width: "auto", height: "100%" },
controled: {
top: 0,
left: -((250 / imgObj.height) * imgObj.width - 250),
bottom: 0,
right: 0
}
};
});
} else if (imgObj.width < imgObj.height) {
this.setState(() => {
return {
objStyles: { width: "100%", height: "auto" },
controled: {
top: -((250 / imgObj.width) * imgObj.height - 250),
left: 0,
bottom: 0,
right: 0
}
};
});
} else if (imgObj.width == imgObj.height) {
this.setState(() => ({
objStyles: { width: "100%", height: "auto" },
controled: { top: 0, left: 0, bottom: 0, right: 0 }
}));
}
}
getComputedImgProprety(target) {
const { width, height } = window.getComputedStyle(target);
this.setState(() => ({
computed: {
computedWidth: parseFloat(width).toFixed(2),
computedHeight: parseFloat(height).toFixed(2)
}
}));
}
async postData() {
const { draggedPosition, objStyles, computed, origin } = this.state;
const { onOK } = this.props;
let data = {
position: draggedPosition,
originSize: {
width: origin['width'],
height: origin['height'],
},
imgSize: {
width: objStyles.width == "100%" || objStyles.width == "auto" ? parseInt(computed.computedWidth) : objStyles.width,
height: objStyles.height == "100%" || objStyles.height == "auto" ? parseInt(computed.computedHeight) : objStyles.height
},
img: this.props.imgUrl
};
onOK && onOK(data)
this.props.closePost();
}
componentDidMount() {
setTimeout(() => {
this.initImage();
}, 0);
}
initImage() {
const { visible, imgUrl } = this.props;
const { childNode } = this;
childNode.onload = () => {
this.judgeProperty(childNode);
this.getComputedImgProprety(childNode);
};
}
reInitImage(target) {
if (target.isUpdate) {
this.setState(
{
objStyles: { width: "auto", height: "auto" },
controled: {},
initPosition: { x: 0, y: 0 },
value: 0,
computed: { computedWidth: 0, computedHeight: 0 },
preRatio: 1,
draggedPosition: {
x: 0,
y: 0
}
},
() => {
target.closeUpdate();
}
);
} else {
const { childNode } = this;
Promise.resolve().then(() => {
this.judgeProperty.call(this, childNode);
this.getComputedImgProprety.call(this, childNode);
});
}
}
componentWillReceiveProps(pre, next) {
if (pre.isPost) {
this.postData();
return;
}
//初始化状态
if (pre.isCloseToUpdate && pre.isPost == this.props.isPost) {
this.reInitImage(pre);
}
}
successRender() {
const { title } = this.state;
const modal = Success({
title,
centered: true,
key: 1,
okText: "确定",
onOk: () => {
this.props.closeCutting();
modal.destroy();
}
});
setTimeout(() => {
modal.destroy();
this.props.closeCutting();
}, 3000);
}
errorRender() {
const modal = Error({
title: "未知错误!请稍后重试!",
centered: true,
key: 2,
okText: "确定",
onOk: () => {
this.props.closeCutting();
modal.destroy();
}
});
setTimeout(() => {
modal.destroy();
this.props.closeCutting();
}, 3000);
}
render() {
// console.log(this.props);
const { objStyles, controled, initPosition, value ,draggedPosition} = this.state;
const { handleDrag } = this;
const { imgUrl } = this.props;
return (
<div className={styles.model_wrap_sin}>
<div className={styles.model_wrap_cox}>
<div className={styles.model_wrap_cox_cen}>
<div className={styles.model_wrap_cox_ast}>
<div className={styles.model_wrap_liz}>
<div className={styles.model_wrap_mas} />
<div className={styles.model_wrap_lig}>
<Draggable OnDragging={handleDrag.bind(this)} controled={controled} initPosition={initPosition}>
<img
src={imgUrl}
ref={this.viewNode}
onDragStart={e => e.preventDefault()}
style={{
width: objStyles.width,
height: objStyles.height
}}
/>
</Draggable>
</div>
<div className={styles.model_wrap_ligt}>
<img
src={imgUrl}
ref={ref => (this.childNode = ref)}
style={{
position: "relative",
top: draggedPosition.y,
left: draggedPosition.x,
width: objStyles.width,
height: objStyles.height
}}
data-ccc={initPosition.x}
/>
</div>
</div>
</div>
</div>
</div>
{/*<Slider*/}
{/* value={value}*/}
{/* tipFormatter={null}*/}
{/* disabled={false}*/}
{/* onChange={this.handleChange.bind(this)}*/}
{/* max={1}*/}
{/* min={0}*/}
{/* step={0.1}*/}
{/* style={{ width: "250px", margin: "15px auto" }}*/}
{/*/>*/}
</div>
);
}
}
export default CutEffect;
3、CutEffect.less
.model_wrap_sin {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.model_wrap_cox {
width: 100%;
max-width: 500px;
height: 300px;
.model_wrap_cox_cen {
width: 300px;
height: 300px;
margin: 0 auto;
background-color: rgba(255, 255, 255, 0.6);
.model_wrap_cox_ast {
background-color: #f7f8fa;
display: flex;
align-items: center;
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
.model_wrap_liz {
width: 250px;
height: 250px;
margin: 0 auto;
overflow: visible;
.model_wrap_mas {
width: 300px;
height: 300px;
background-color: rgba(255, 255, 255, 0.6);
position: absolute;
top: 0;
left: 0;
z-index: 2;
}
.model_wrap_lig {
display: flex;
width: 250px;
height: 250px;
position: absolute;
overflow: hidden;
z-index: 3;
}
.model_wrap_ligt {
width: 250px;
height: 250px;
z-index: 1;
position: relative;
top: 0;
left: 0;
overflow: visible;
img {
margin: 0;
padding: 0;
}
}
}
}
}
}
}
4、CutImg.jsx
import React, { Component } from "react";
import { Col, Modal, Button, Slider } from "antd";
import CutEffect from "./CutEffect";
import styles from "./CutImg.less";
class CutImg extends React.Component {
constructor(props) {
super(props);
this.handleOk = this.handleOk.bind(this);
this.state = {
isPost: false, //是否允许子组件提交表单
isUpdate: false //是否初始化裁剪组件状态
};
}
handleCancel() {
this.props.onClose();
this.openUpdate();
}
handleOk() {
this.setState(() => ({
isPost: true
}));
}
closePost() {
this.setState(() => ({
isPost: false
}));
}
openUpdate() {
this.setState(() => ({
isUpdate: true
}));
}
closeUpdate() {
this.setState(() => ({
isUpdate: false
}));
}
render() {
const { image, visible, isCloseToUpdate, onOK } = this.props;
const { isPost, isUpdate } = this.state;
return (
<Col xs={24} md={12} className={styles.model_wrap} style={{ ...this.props.style }}>
<Modal
title='拖动图片调整显示区域'
wrapClassName={styles.model_wrap_modal}
visible={visible}
onOk={this.handleOk}
width={500}
onCancel={this.handleCancel.bind(this)}
footer={[
<Button key='back' onClick={this.handleCancel.bind(this)}>
取消
</Button>,
<Button key='submit' type='primary' onClick={this.handleOk}>
确定
</Button>
]}
>
<CutEffect
imgUrl={image}
visible={visible}
isPost={isPost}
isUpdate={isUpdate}
isCloseToUpdate={isCloseToUpdate}
closePost={this.closePost.bind(this)}
closeUpdate={this.closeUpdate.bind(this)}
openUpdate={this.openUpdate.bind(this)}
closeCutting={this.handleCancel.bind(this)}
onOK={onOK}
/>
</Modal>
</Col>
);
}
}
export default CutImg;
5、CutImg.less
.model_wrap {
width: 100%;
max-width: 500px;
margin: 0 auto;
}
.model_wrap_modal {
width: 100%;
max-width: 500px;
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
margin-top: -100px;
// transform: translate(0, -50%);
}
6、service.js
import request from 'umi-request';
export async function uploadFile(formData) {
return await request("/v1.0/sys/admin/files/upload", {
method: 'post',
headers: {},
requestType: 'form',
data: formData
})
}
我是把 CutImg 放到了全局组件的文件夹下面,你们也可以这么干,图示如下:
6、调用的代码
用的时候直接通过下面这种方式引入,就可以了,代码如下:
<UploadCutImage disable={false}/>
到此 React 实现图片裁剪介绍完成。
以上是关于React 实现图片裁剪/上传/预览/下载/删除(主要是图片裁剪)的主要内容,如果未能解决你的问题,请参考以下文章
HTML5 MUI 手机预览图片,裁剪上传base64,保存数据库
jQuery插件ImgAreaSelect 实例讲解一(头像上传预览和裁剪功能)