CRIME

导航

Django从零搭建个人博客 | 使用jQuery插件Cropper实现上传图片的裁剪

原文章地址: EOSONES博客

在本博客的用户个人资料中允许用户上传头像的过程中进行裁剪上传,对于不熟悉前端的人来说有很多优秀的前端图片裁剪插件可以使我们快速完成功能,本文使用的是功能全面的jQuery插件Cropper,结合本文进行简单的应用介绍。查看全部参数设置推荐阅读Cropper的基本使用

安装配置

1、下载地址

  1. 官方示例:官方示例
  2. jQuery:GitHub项目地址
  3. JS:GitHub项目地址

2、基本使用

首先要引入必要的js和css文件,上线可使用min压缩版

<link href="/Myblog/static/css/cropper.css" rel="stylesheet">
<script src="/Myblog/static/js/cropper.js"></script>

可以将图片或canvas直接包裹到一个块级元素中

<!-- Wrap the image or canvas with a block element -->
<div class="container">
    <img src="picture.jpg">
</div>

在JS中调用该图片剪裁插件,即可在图片上出现裁剪框

<script>
$('.container > img')').cropper({
        aspectRatio: 1 / 1,  #长宽比
        viewMode:1,   #视图模式,可以使用0,1,2,3,具体查看demo示例
        crop: function (e) {
            // Output the result data for cropping image.
        }
    });
</script>

需要注意的是剪裁区域的尺寸继承自图片的父容器(包裹容器),所以要确保包裹图片的是一个可见的块级元素。输出的剪裁数据基于原始的图片尺寸,可以使用这些数据直接剪裁图片。

应用项目

首先,根据需求,我们需要的是点击头像即可打开图片,选中图片后出现在模态框中的图片即可进行裁剪,确认提交后通过Ajax传给后端保存到服务器,将地址保存到用户表的avatar字段。

1、隐藏上传文件按钮

 /*首先画个圆形*/
.circle {
    width: 100px;
    height: 100px;
    border-radius: 50%;
    -moz-border-radius: 50%;
    -webkit-border-radius: 50%;
    /*水平居中*/
    margin: 25px auto 25px auto;
    /*作用于子标签*/
    overflow: hidden;
    position: relative;
    text-align: center;
}
 /*上传文件样式*/
.uploadhead {
    position: absolute;
    bottom: 0;
    width: 100px;
    height: 35px;
    background-color: #000;
    opacity: 0.7;
    color: #fff;
    font-size: 14px;
    line-height: 30px;
    display: none;
}
/*经过头像显示上传文字*/
.circle:hover .uploadhead {
    display: block;
    color: #fff;
}
/*隐藏Input上传按钮*/
.uploadhead input {
    position: absolute;
    top: 0;
    opacity: 0;
}
...
<div class="circle">
    <img id="avatar" src="{{user.avatar.url}}">
    <a href="javascript:" class="uploadhead">
       <input type="file" name="file" onchange="preview(this)">上传头像
    </a>
</div>

2、触发事件

在这之前我们先引入需要用到的Bootstrap模态框,注意提前引入Bootstrap框架的相关文件。

<!-- 裁剪图片Modal -->
<div class="modal fade" id="changeModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                <h4 class="modal-title" id="myModalLabel">请选择合适的区域作为头像</h4>
            </div>
            <div class="modal-body">
                <div class="img-container">
                    <img id="uploadPreview" src="">
                </div>
                <div id="error_text" style="display:none"></div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
                <button id='sendPhoto' type="button" class="btn btn-primary">上传头像</button>
            </div>
        </div>
    </div>
</div>

注意我们在上传头像中的给Input标签绑定的是onchange()事件,每次选择图片后才会触发该事件,之后才开模态对话框。在preview函数中,首先我们判断文件格式,之后打开model显示错误信息,或者初始化cropper的相关设置,最后在model关闭的回调函数进行清空cropper及相关设置。

function preview(obj) {
    var _alertMsg = document.getElementById('error_text');
    var _myModalLabel = document.getElementById("myModalLabel");

    //此事件在模态框被隐藏(并且同时在 CSS 过渡效果完成)之后被触发。
    $('#changeModal').modal('show').on('hidden.bs.modal', function(e) { 
        //关闭模态对话框后清空file input的值
        $(obj).val('');
        //隐藏错误提示
        _alertMsg.style.display = 'none';
        //清空图片信息
        document.getElementById("uploadPreview").src = '';
        $("#uploadPreview").cropper('reset').cropper('replace', '');
        //摧毁cropper
        $("#uploadPreview").cropper("destroy");
        //解绑changeModal的所有事件
        $('#changeModal').off('shown.bs.modal');
        $('#changeModal').off('hidden.bs.modal');
    });

    var val = obj.value;
    //设定可上传的格式
    var upLoadType = '.jpg,.gif,.bmp,.png';
    //从字符串中抽出最后一次出现.之后的字符,并且转换成小写
    var fileExt = val.substr(val.lastIndexOf(".")).toLowerCase();
    //查找后缀名是否符合条件,如果符合返回>=0,如果不符合则返回负数;
    var result = upLoadType.indexOf(fileExt);
    //如果只有一个文件则只需要访问这个FileList对象中的第一个元素.
    var oFile = obj.files[0];
    //文件不存在直接返回或者不符合格式
    if (obj.files.length === 0 || result < 0) {
        _alertMsg.innerHTML = "请输入正确格式:" + upLoadType;
        _alertMsg.style.display = 'inline-block';
        _myModalLabel.innerHTML = "上传出现错误";
        //隐藏图片容器
        document.getElementById("uploadPreview").parentElement.style.display = 'none';
        return;
    };
    if (oFile.size / 1024 > 100) {
        _alertMsg.innerHTML = "请上传100k内的文件";
        _alertMsg.style.display = 'inline-block';
        _myModalLabel.innerHTML = "上传出现错误";
        //隐藏图片容器
        document.getElementById("uploadPreview").parentElement.style.display = 'none';
        return;
    };

    //model显示并在CSS过渡完成回调
    $('#changeModal').on('shown.bs.modal', function() {
        //转为基于file API的Blob对象
        var blobURL;
        //URL对象是硬盘(SD卡等)指向文件的一个路径
        var URL = window.URL || window.webkitURL;
        //获得一个http格式的url路径
        blobURL = URL.createObjectURL(oFile);
        document.getElementById("uploadPreview").parentElement.style.display = 'block';
        document.getElementById("uploadPreview").src = blobURL;

        //绑定cropper插件
        $("#uploadPreview").cropper({
            aspectRatio: 1, //1比1
            viewMode: 3,
            zoomOnWheel: false, //禁止缩放原图
            zoomOnTouch: false, //禁止缩放原图
            ready: function(data) {
                // Output the result data for cropping image.
                // And then
            }
        });
        //重置cropper设置并替换生成的cropper图片url
        $("#uploadPreview").cropper('reset').cropper('replace', blobURL);
        _myModalLabel.innerHTML = "请选择合适的区域作为头像";
    });
};

最后,裁剪图片后,通过model中的确认上传按钮中绑定的点击事件用Ajax将数据发送后台。

$('#sendPhoto').on('click', function() {
    var username = document.getElementById('username').innerHTML.trim();
    // cropper可以得到两种裁剪后图片的数据(即blob和dataURL),dataURL过于长,此处用toBlob
    var photo = $("#uploadPreview").cropper('getCroppedCanvas', {
        width: 100,
        height: 100,
    }).toBlob(function(blob) {
        //因为上传的是文件不是string类型,因此用到H5的FormData方法
        //组装formdata
        var fd = new FormData();
        fd.append('username', username);
        //fd.append("fileName", "avatar"); fileName为自定义,名字随机生成或者写死,看需求
        fd.append("avatar", blob); //fileData为自定义,blob包含图片的各种信息
        fd.append("key","avatar");
        //ajax上传,ajax的形式随意,JQ的写法也没有问题
        //需要注意的是服务端需要设定,允许跨域请求。数据接收的方式和<input type="file"/> 上传的文件没有区别
        $.ajax({
            url: '/accounts/profile/update/',
            type: 'post',
            data: fd,
            processData: false, //不设置Content-Type请求头
            contentType: false, //不处理发送的数据
            success: function(data) {
                var avaterurl = JSON.parse(data).url;
                $("#avatar").attr("src", avaterurl);
                $('#changeModal').modal('hide');
            },error: function() { console.log("保存失败"); }
        });
    });
});

3、视图函数

首先配置更新用户信息的url

#Myaccount/urls.py

from django.conf.urls import re_path
from . import views

app_name = "Myaccount"

urlpatterns = [
    re_path(r'^profile/$', views.profile, name='profile'),
    re_path(r'^profile/update/$', views.profile_update, name='profile_update'),
]

视图函数处理用户通过Ajax提交的个人网站与用户头像信息

#Myaccount/views.py

from django.shortcuts import render
from django.http import HttpResponse

from Myaccount import models

#auth中用户权限有关的类。auth可以设置每个用户的权限。
from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import csrf_exempt

# 使用login_required装饰器,用户只有登录了才能访问其用户资料
@login_required
#个人信息
def profile(request):
    # AUTH_USER_MODEL 类型的对象,表示当前登录的用户。
    user = request.user
    return render(request, 'account/profile.html', {'user': user})

import os
import json
import base64
from django.shortcuts import get_object_or_404
@csrf_exempt #取消当前函数防跨站请求伪造功能,即便settings中设置了全局中间件。
def profile_update(request):
    #request.is_ajax(): #判断请求头中是否含有X-Requested-With的值
    if request.is_ajax():
       key=request.POST.get('key')#request.POST.get('')不存在默认为空,request.POST[]不存在报错
      username=request.POST['username']
      user_profile=get_object_or_404(models.User,username=username)

      if key=='link':
        link=request.POST['link']
        models.User.objects.filter(username=username).update(link=link)
        link=models.User.objects.filter(username=username).first().link
        linkJson={'link':link}
        return HttpResponse(json.dumps(linkJson))

      elif key=='avatar':
        upload_image=request.FILES.get('avatar')
        image_name=user_profile.save_avatar(upload_image)
        user_profile.avatar=os.path.join('avatar',user_profile.username,image_name)
        user_profile.save()
        url=user_profile.avatar.url
        dataJson={'url':url}
        return HttpResponse(json.dumps(dataJson))

其中,由于未知原因部署后无法用Form、ModelForm等直接保存上传的图片文件,所以此处在User model中写了一个手动储存方法参考。

from django.db import models
#from django.contrib.auth.models import User
#AbstractUser类可自由定制需要的model
from django.contrib.auth.models import AbstractUser  
#用pillow、django-imagekit模块设置图片,可以处理图片,生成指定大小的缩略图
from imagekit.models import ProcessedImageField
from imagekit.processors import ResizeToFill

#扩展Django自带的User模型字
# 继承 AbstractUser ,django 自带用户类,可扩展用户个人信息,AbstractUser 模块下有:password,username、first_name、last_name、email、last_login,is_superuser,is_staff,is_active,date_joined
class User(AbstractUser):
    #nickname = models.CharField(max_length=30, blank=True, null=True, verbose_name='昵称')
    # 扩展用户个人网站字段
    link = models.URLField('个人网址', blank=True, help_text='提示:网址必须填写以http开头的完整形式')
    # 扩展用户头像字段,upload_to后必须是相对路径,上传路径已设置为media,保存的是图片地址,前端user.avatar.url获取
    avatar = ProcessedImageField(upload_to='avatar',default='avatar/default.png',verbose_name='头像',
                                processors=[ResizeToFill(100, 100)], # 处理后的图像大小
                                format='JPEG', # 处理后的图片格式
                                options={'quality': 95} # 处理后的图片质量
                                )

     #定义手动保存图(IIS下User.save()保存失败)
    def save_avatar(self,upload_image):
        import os
        import uuid
        from django.conf import settings
        #创建与用户名的文件夹
        upload_path=os.path.join(settings.MEDIA_ROOT,'avatar',self.username)
        if not upload_path:
          try:
            os.makedirs(new_path)
          except:
            pass
        # 生成一个随机字符串
        uuid_str_name = uuid.uuid4().hex+'.jpg'
        #保存
        with open(os.path.join(upload_path,uuid_str_name), 'wb+') as file:
        for chunk in upload_image.chunks():
            file.write(chunk)
        return uuid_str_name

    #显示用户的邮箱是否验证过,并提醒他们去验证邮箱
    def account_verified(self):
        if self.user.is_authenticated: #django的auth系统功能,只能利用django自己的登陆方法才能判断用户是否登录
          result = EmailAddress.objects.filter(email=self.user.email)
        if len(result):
          return result[0].verified
        return False

    # 定义网站管理后台表名
    class Meta:
        verbose_name = '用户信息' 
        verbose_name_plural = verbose_name #指定模型的复数形式是什么,如果不指定Django会自动在模型名称后加一个’s’
        ordering = ['-id']
        #admin后台显示名字关联到此表的字段的后天显示名字
    def __str__(self):
        return self.username

至此基本完成了用户头像的上传交互,代码中有许多不足望大佬指出,一起交流学习。

posted on 2019-06-15 09:23  CRIME  阅读(1014)  评论(0编辑  收藏  举报