效果如下
实现
需求分析
- 上传图片
- 可对图片进行裁剪且可预览
- 图片可放大、缩小、旋转
- 可重新上传图片
设计知识点
需求实现
引入相关 Upload 组件
一开始样式相关的内容就直接 pass 了,最后会把所有代码贴上来,这里只放关键流程。本项目使用的是antdv,ElementUI 流程一样。
<a-upload
v-model:file-list="fileList"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
:before-upload="beforeUpload"
:custom-request="handleUpload"
>
...code
</a-upload>
这里有两个需要注意的点:
- 自定义
Upload上传方法handleUpload,用URL.createObjectURL方法将上传的文件转成img标签可以使用的链接。 - 使用第三方插件
vue-cropper执行图片裁剪的操作。
引入 vue-cropper 插件
安装
-
Vue2
-
npm install vue-cropper
-
-
Vue3
-
npm install vue-cropper@next
-
引入
import { VueCropper } from 'vue-cropper'
import 'vue-cropper/dist/index.css'
配置
<script setup>
const option = ref({
size: 1,
full: false,
outputType: 'png',
canMove: true,
fixedBox: true, // fixedBox 效果是固定截图框大小。若需要更改截图框大小将属性改为 false 即可
fixed: true,
original: false,
canMoveBox: true,
autoCrop: true,
autoCropWidth: 200,
autoCropHeight: 200,
centerBox: false,
high: true,
fixedNumber: [1, 1],
max: 2000,
})
</script>
<template>
<div class="cut">
<VueCropper
@real-time="realTime"
ref="cropper"
:img="imageUrl"
:output-size="option.size"
:output-type="option.outputType"
:info="true"
:full="option.full"
:fixed="option.fixed"
:fixed-number="option.fixedNumber"
:can-move="option.canMove"
:can-move-box="option.canMoveBox"
:fixed-box="option.fixedBox"
:original="option.original"
:auto-crop="option.autoCrop"
:auto-crop-width="option.autoCropWidth"
:auto-crop-height="option.autoCropHeight"
:center-box="option.centerBox"
:high="option.high"
mode="cover"
:max-img-size="option.max"
/>
</div>
</template>
这里一定要给VueCropper外面包一层,外面容器的宽高就是它的宽高。
预览
<script>
const realTime = data => {
previews.value = data
}
</script>
<template>
<div
class="show-preview"
:style="{
'width': previews?.w + 'px',
'height': previews?.h + 'px',
'overflow': 'hidden',
}"
>
<div :style="previews?.div">
<img :src="previews?.url" :style="previews?.img">
</div>
</div>
</template>
要注意的是这里previews的宽高都是在配置中设置的autoCropWidth和autoCropHeight。
到这里基本关键部分就差不多了,放大缩小旋转都可以在相关文档上查询。
还有一点最关键的,这个插件只能把图片转成Base64或Blob文件。所以如果要完成最后一步上传图片的话可能需要费点事,这里提供几个方向供大家参考:
-
将
Base64传给后端,让后端生产一个文件地址,我们保存后端返给我们的这个文件地址。 -
将
Base64再转回File文件,然后再调用上传接口,保存上传接口返回的文件地址。方法如下,需要注意该方法目前仅支持谷歌和火狐
function dataURLtoFile(dataurl, filename) {
var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
while(n--){
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, {type:mime});
}
const Base64Path = '...'
const file = dataURLtoFile(Base64Path, 'fileName')
完整代码
<script setup>
import { ref } from 'vue'
import { message } from 'ant-design-vue';
import { PlusOutlined, PlusCircleOutlined, MinusOutlined, UndoOutlined, RedoOutlined } from '@ant-design/icons-vue';
import { VueCropper } from 'vue-cropper'
import 'vue-cropper/dist/index.css'
const outerImage = ref(undefined)
const visible = ref(false)
const fileList = ref([]);
const imageUrl = ref('');
const previews = ref(null)
const cropper = ref()
const option = ref({
size: 1,
full: false,
outputType: 'png',
canMove: true,
fixedBox: true,
fixed: true,
original: false,
canMoveBox: true,
autoCrop: true,
// 只有自动截图开启 宽度高度才生效
autoCropWidth: 200,
autoCropHeight: 200,
centerBox: false,
high: true,
fixedNumber: [1, 1],
max: 2000,
})
const beforeUpload = file => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
if (!isJpgOrPng) {
message.error('You can only upload JPG file!');
}
const isLt2M = file.size / 1024 / 1024 < 6;
if (!isLt2M) {
message.error('Image must smaller than 6MB!');
}
return isJpgOrPng && isLt2M;
};
const handleUpload = file => {
console.log('[ file ] >', file)
imageUrl.value = URL.createObjectURL(file.file);
}
const realTime = (data) => {
previews.value = data
}
const handlePlus = () => {
cropper.value.changeScale(1)
}
const handleMinus = () => {
cropper.value.changeScale(-1)
}
const handleTurnLeft = () => {
cropper.value.rotateLeft()
}
const handleTurnRight = () => {
cropper.value.rotateRight()
}
const handleSubmit = () => {
cropper.value.getCropData((data) => {
outerImage.value = data
console.log('[ data ] >', data)
});
visible.value = false
}
</script>
<template>
<div class="avatar-wrapper" v-if="!outerImage" @click="visible = true">
<PlusOutlined />
</div>
<img class="avatar-wrapper" v-else :src="outerImage" alt="outerImage">
<a-modal
v-model:open="visible"
title="选择图片"
@ok="handleSubmit"
width="720px"
cancelText="取消"
okText="确定"
:maskClosable="false"
>
<div class="modal-content">
<a-upload
v-if="!imageUrl"
ref="upload"
v-model:file-list="fileList"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
:before-upload="beforeUpload"
:custom-request="handleUpload"
>
<div class="img-wrapper">
<PlusCircleOutlined
:style="{
fontSize: '20px',
marginBottom: '5px',
}"
/>
<span class="img-tips">选择图片</span>
</div>
</a-upload>
<div v-else class="cut">
<VueCropper
@real-time="realTime"
ref="cropper"
:img="imageUrl"
:output-size="option.size"
:output-type="option.outputType"
:info="true"
:full="option.full"
:fixed="option.fixed"
:fixed-number="option.fixedNumber"
:can-move="option.canMove"
:can-move-box="option.canMoveBox"
:fixed-box="option.fixedBox"
:original="option.original"
:auto-crop="option.autoCrop"
:auto-crop-width="option.autoCropWidth"
:auto-crop-height="option.autoCropHeight"
:center-box="option.centerBox"
:high="option.high"
mode="cover"
:max-img-size="option.max"
/>
</div>
<div class="content-right">
<div class="avatar default" v-if="!imageUrl">图片</div>
<div v-else>
<div
class="show-preview"
:style="{
'width': previews?.w + 'px',
'height': previews?.h + 'px',
'overflow': 'hidden',
}"
>
<div :style="previews?.div">
<img :src="previews?.url" :style="previews?.img">
</div>
</div>
</div>
<div class="upload-tips">
<div class="upload-tip">请按照以下建议上传图片以达到最佳展示效果:</div>
<div class="upload-tip">1、图片大小不超过6MB</div>
<div class="upload-tip">2、支持jpg、png、jpeg</div>
<div class="upload-tip">3、图片长宽比为1:1</div>
</div>
</div>
</div>
<div class="operate-wrapper" v-if="imageUrl">
<a-upload
ref="upload"
v-model:file-list="fileList"
class="avatar-uploader"
:show-upload-list="false"
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
:before-upload="beforeUpload"
:custom-request="handleUpload"
>
<a-button>重新上传</a-button>
</a-upload>
<div class="cropper-btns">
<PlusOutlined
@click="handlePlus"
:style="{
cursor: 'pointer',
fontSize: '16px',
marginRight: '15px'
}"
/>
<MinusOutlined
@click="handleMinus"
:style="{
cursor: 'pointer',
fontSize: '16px',
marginRight: '15px'
}"
/>
<UndoOutlined
@click="handleTurnLeft"
:style="{
cursor: 'pointer',
fontSize: '16px',
marginRight: '15px'
}"
/>
<RedoOutlined
@click="handleTurnRight"
:style="{
cursor: 'pointer',
fontSize: '16px',
marginRight: '10px'
}"
/>
</div>
</div>
</a-modal>
</template>
<style scoped lang="less">
.avatar-wrapper {
cursor: pointer;
width: 200px;
height: 200px;
line-height: 200px;
text-align: center;
border: 1px dashed #d9d9d9;
border-radius: 5px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.modal-content {
display: flex;
justify-content: space-between;
.img-wrapper {
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
border-radius: 10px;
background-color: #f4f4fb;
}
.content-right {
display: flex;
flex-direction: column;
justify-content: space-between;
.avatar {
border-radius: 50%;
display: inline-block;
width: 200px;
height: 200px;
font-size: 20px;
text-align: center;
line-height: 200px;
font-weight: 400;
background-color: #f4f4fb;
}
.upload-tips {
font-size: 12px;
width: 200px;
line-height: 18px;
}
}
:deep(.ant-upload.ant-upload-select.ant-upload-select-picture-card) {
width: 450px !important;
height: 400px !important;
}
}
.cut {
height: 400px;
width: 450px;
margin-bottom: 8px;
}
.show-preview {
border-radius: 50%;
display: inline-block;
font-weight: 700;
background-color: #f4f4fb;
}
.operate-wrapper {
width: 450px;
display: flex;
align-items: center;
justify-content: space-between;
}
</style>