效果:
选择图片,调整大小位置,上传图片,图片会被存到服务器并返回图片名称,前端再更新本地头像地址。
目录结构
为了方便测试,vue在本地目录测试,laravel接口放在服务器上
Vue前端
1、/src/views/test.vue组件的html部分:
<template>
//左边
<input
type="file" //输入类型为文件
accept="image/*" //仅可选图片类型文件
ref="avatarInput" //ref相当于id
@change="changeImage" //选中图片后调用changeImage函数
style="display:none" //隐藏input组件,因为太丑
>
<img
:src="my_avatar" //我的头像
class="avatar" //样式为圆形
/>
//右边
<div style="display:flex;flex-direction:column">
<div class="big_avatar" ref="imageWrapper"> //裁剪框
<img
:class="moveimg_class"
:src="preview_avatar"
:style="{'top':top,'left':left}"
@mousedown="movestart"
@mouseenter="moveenter"
@mouseleave="moveleave"
ref="move_img">
</div>
<div class="long"> //滑动条
<img @mousedown="potstart" class="pot" />
</div>
<div class="avatar_button" @click="upLoad">选择图片</div>
<div class="avatar_button" @click="draw">上传头像</div>
</div>
</template>
2、/src/test.vue组件的js部分:
<script>
import axios from 'axios';
import html2canvas from 'html2canvas';
export default {
name:'test',
data(){
return{
cursor:'', //鼠标进入裁剪框会变图标
my_avatar:'http://lanyue.ink/api/default_avatar.png', //左边头像
preview_avatar:'http://lanyue.ink/api/default_avatar.png', //裁剪框里的头像
ava_width:0, //头像上传前的宽度
top:'0px', //裁剪框内头像的top
left:'0px', //裁剪框内头像的left
ava_left:0, //头像在每次缩放后的left
moveimg_class:'move_img3', //裁剪框内图片的class
}
},
mounted(){
this.$refs.move_img.onmousedown = function(e){ //取消img自带的拖拽效果
e.preventDefault();
};
},
methods:{
upLoad:function(){ //上传头像,触发input点击事件
this.$refs.avatarInput.dispatchEvent(new MouseEvent("click"));
},
changeImage() {
let files = this.$refs.avatarInput.files;
let that = this;
this.top = '0px';
this.left = '0px';
if (/\.(jpe?g|png|gif)$/i.test(files[0].name)) {
let reader = new FileReader();
reader.readAsDataURL(files[0]);
reader.onload = function() {
that.preview_avatar = this.result; //裁剪框内的图片换成刚刚选择的图片
let img = new Image(); //新建Image是为了获取图片原本的高度宽度
img.src = this.result; //如果图片高>=宽,则让图片的宽抵满裁剪框
img.onload = function () { //如果图片高<宽,则让图片的高抵满裁剪框
if(this.height>=this.width){
that.moveimg_class = 'move_img1';
}else{
that.moveimg_class = 'move_img2';
}
}
}
}
},
draw:function(){
let that = this
let opts = {
logging: true, // 启用日志记录以进行调试 (发现加上对去白边有帮助)
allowTaint: true, // 否允许跨源图像污染画布
backgroundColor: null, // 解决生成的图片有白边
useCORS: true // 如果截图的内容里有图片,解决文件跨域问题
}
window.pageYOffset = 0; //绘制div前必须保证滚动条是在最顶端,否则图片会错位
document.documentElement.scrollTop = 0
document.body.scrollTop = 0
html2canvas(that.$refs.imageWrapper, opts).then((canvas) => { //绘制裁剪框div
let url = canvas.toDataURL('image/png')
that.my_avatar = url;
axios.post('/api/upload_avatar',{data:url},{ //上传图片的axios请求
headers:{'Content-Type':'application/json;charset=UTF-8'}
}).then(res=>{
this.my_avatar = 'http://lanyue.ink/api/'+res.data;
});
})
},
moveenter:function(){ //鼠标移入裁剪框
this.cursor = "cursor: url('http://lanyue.ink/api/icon.png'),auto;";
},
moveleave:function(){ //鼠标移出裁剪框
this.cursor = "cursor: ;";
},
movestart(e){ //拖动裁剪框中的图片
let op = e.target;
let that = this;
let disX = e.clientX - op.offsetLeft;
let disY = e.clientY - op.offsetTop;
document.onmousemove = (e)=>{
//用鼠标的位置减去鼠标相对元素的位置,得到元素的位置
let left = e.clientX - disX;
let top = e.clientY - disY;
if(top <= 0 && top >= (256-op.height))
that.top = top + 'px';
if(left <=0 && left >= (256-op.width))
that.left = left + 'px';
};
document.onmouseup = () => {
document.onmousemove = null;
document.onmouseup = null;
};
},
potstart(e){ //拖动滑动条,调整图片大小
let op = e.target;
let disX = e.clientX - op.offsetLeft;
let that = this;
//获取原始宽
if(!this.ava_width)
this.ava_width = that.$refs.move_img.width;
let str1 = that.top.slice(0,-2)
let img_top = parseInt(str1);
let str2 = that.left.slice(0,-2)
let img_left = parseInt(str2);
let record_letf = 0;
document.onmousemove = (e) =>{
let left = e.clientX - disX;
if(left >= 5 && left <= 221){
//滑动条移动
op.style.left = left+'px';
//裁剪框内图片的宽增加
that.$refs.move_img.style.width = (left+this.ava_width) +'px';
//中心缩放
let a=(left-that.ava_left)/2;
that.top = (img_top-a)+'px';
that.left = (img_left-a)+'px';
record_letf = left;
}
};
document.onmouseup = () => {
that.ava_left = record_letf; //记录每次拖完滑动条的left
document.onmousemove = null;
document.onmouseup = null;
};
}
}
}
</script>
3、 /src/test.vue组件的css部分:
<style scoped>
.avatar{
width: 128px;
height: 128px;
border-radius: 50%;
}
.big_avatar{
position: relative;
width: 256px;
height: 256px;
background-color: rgb(228, 51, 51);
overflow: hidden;
}
//竖直
.move_img1{
position: absolute;
min-width: 256px;
}
//横向
.move_img2{
position: absolute;
min-height: 256px;
}
.move_img3{
width: 256px;
height: 256px;
}
.avatar_button{
width: 256px;
height: 40px;
line-height: 40px;
background-color: rgba(207, 207, 207, 0.5);
margin-top: 20px;
}
.long{
margin-top: 20px;
width: 256px;
height: 40px;
border-radius: 20px;
background-color: #fff;
position: relative;
.pot{
width: 30px;
height: 30px;
background-color: rgba(0, 0, 0, 0.5);
position: absolute;
border-radius: 100%;
top:5px;
left: 5px;
}
}
</style>
4、在vue.config.js中设置跨域访问(如果没有就新建这个文件,并且要重新npm run serve)
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://***.com',
ws: true,
changeOrigin: true,
pathRewrite: {
'^/api': '/api/api' //我们要把接口放到routes/api.php上,所以将前缀/api转为/api/api
//要是放到routes/web.php上,需要进行csrf_token的验证
}
}
}
}
}
Laravel后端
1、/routes/api.php
Route::post('/upload_avatar','UserController@upload_avatar');
2、App/Http/Controllers/UserController.php
public function upload_avatar(Request $request){
//获取base64编码
$img = $request->data;
//用逗号分割base64编码
$explode = explode(',',$img);
//对base64解码
$decoded = base64_decode($explode[1]);
//判断图片的后缀
if(strpos($explode[0],'jpeg')){
$extension = 'jpg';
}else if(strpos($explode[0],'png')){
$extension = 'png';
}
//生成随机字符串
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$str = '';
$length = 15;
for($i = 0; $i < $length; $i++)
{
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
//生成文件名
$filename = $str.'.'.$extension;
//图片存放的public目录
$path = public_path().'/'.$filename;
//将图片放到public目录的方法
file_put_contents($path,$decoded);
//返回前端文件名
return $filename;
}