Django 绝对可以成功的通过cropper.js实现的头像上传功能


说实话,给Django项目加上可以上传头像的功能真的是花了我不少时间,差不多前后折腾了两天,期间也在网上找了很多很多,很多很多的例子,但是几乎无一例外在我这里都行不通,要么是点击提交按钮无反应,要么是报一些奇奇怪怪让我难以解决的错误,要么是大神写的代码实在是看不明白,连复制粘贴都胆战心惊,更不要说二次改造了。嘛,人菜就不得不瞎折腾,所幸,最后居然侥幸成功了,所以特地心情澎湃的写篇博客,记录一下!

准备

首先说一下用到的一些工具和开发环境:

  • Django3
  • cropper.js插件

主体其实就这两个,关于怎么使用接下来我会详细说明。

接下来说一下头像上传功能的具体实现思路:

#1 一开始我是打算把用户上传的头像的base64编码通过ajax发送到后端,然后在后端接受数据并处理,网上的很多这方面的博客也是这种方法,但我一用不知道为什么就错,网上的博客写的没毛病,但是我自己对ajax和json等实在是不擅长,所以一旦报错,解决错误的能力几乎为0,所以接下来我介绍的方法并没有用到ajax,但是想通过ajax实现的可以移步其它博客。

#2 后来想想其实保存用户的头像很简单,就按照Django处理表单的一般思路,上传表单,验证数据,然后save()一下不就行了么。

效果图:
在这里插入图片描述
在这里插入图片描述


一、models.py

在编写models.py之前,需要先安装imagekit模块,该模块可以用于服务端处理图片。
通过以下方式安装:

pip install django-imagekit

没安装pillow的,顺便把pillow也装上。

from django.db import models
from django.contrib.auth.models import AbstractUser
import os
import uuid
from imagekit.models import ProcessedImageField

# 生成放置 avartar 的文件夹
def user_directory_path(instance, filename):
    ext = filename.split('.')[-1]
    filename = '{}.{}'.format(uuid.uuid4().hex[:10], ext)
    # 这里的id是User表的id
    return os.path.join('user', str(instance.id), "avatar", filename)
   
class Profile(AbstractUser):
	nickname = models.CharField('昵称',max_length=32,null=True, blank=True)
    avatar = ProcessedImageField(verbose_name='头像', upload_to=user_directory_path, 					       
                                default='avatar/default.png'
                               	format='JPEG',  # 处理后的图片格式
                               	options={'quality': 100}  # 处理后的图片质量
                               )
     ......

我项目里的Profile字段很多,这里就不再一一列出,只给出最重要的avatar字段。

需要注意的有以下几点:

  • 首先,我这里的Profile是继承自AbstractUser,所以在属性名上可能与使用Model的有些出入。
  • 安装完了imagekit后记得settings.py中注册。
  • 不要忘了配置MEDIA_URLMEDIA_ROOT参数。

二、urls.py

app_name = 'profile'

urlpatterns = [
    path('login/',views.user_login,name='login'),
    path('register/',views.user_register,name='register'),
    path('detail/<int:id>/',views.user_detail,name='detail'),
    path('detail-setting/<int:id>/',views.user_detail_setting,name='detail-setting'),
    path('logout/',views.user_logout,name='logout'),
    path('delete/<int:id>/',views.user_delete,name='delete'),
    path('pic-upload/',views.pic_upload,name='pic_upload'), #重点在这一行
]

三、forms.py

from PIL import Image
from django import forms
from django.core.files import File
from .models import Profile

class PhotoForm(forms.ModelForm):
    x = forms.FloatField(widget=forms.HiddenInput())
    y = forms.FloatField(widget=forms.HiddenInput())
    width = forms.FloatField(widget=forms.HiddenInput())
    height = forms.FloatField(widget=forms.HiddenInput())

    class Meta:
        model = Profile
        fields = ('avatar', 'x', 'y', 'width', 'height', )

    def save(self, commit=True, id=None):
        if Profile.objects.get(id=id):
            user = Profile.objects.get(id=id)
            user.avatar = super(PhotoForm, self).save(commit=False).avatar
        else:
            user = super(PhotoForm, self).save(commit=False)
            user.id = id
        user.save()

        x = self.cleaned_data.get('x')
        y = self.cleaned_data.get('y')
        w = self.cleaned_data.get('width')
        h = self.cleaned_data.get('height')

        image = Image.open(user.avatar)
        cropped_image = image.crop((x, y, w+x, h+y))
        resized_image = cropped_image.resize((200, 200), Image.ANTIALIAS)
        resized_image.save(user.avatar.path)

        return user

为了裁剪图像,我们还需要额外获得四条信息: x 坐标,y 坐标,裁剪框的高度和宽度,因此这里除了avatar字段外,又多定义了这四个字段。

对代码不理解的可以直接粘贴先拿来用,这里只需要把代码中有关Profile的部分换成你的模型名就好了。(当然,你的头像字段名也是avatar的话,如果不是那也要换)


四、views.py

from django.shortcuts import render,get_object_or_404,redirect
from .forms import PhotoForm
from .models import Profile
from django.contrib.auth.decorators import login_required

@login_required(login_url='/profile/login/')
def pic_upload(request):
    user = request.user
    if request.method == 'POST':
        form = PhotoForm(request.POST,request.FILES)
        if form.is_valid():
            id = user.id
            form.save(id=id)
            return redirect('profile:detail-setting',id=user.id)

views.py中的代码其实异常的很简单。


五、模板文件

这里其实才是最麻烦的地方,首先让我们在模板中引入我们需要用到的一些东西:

upload_img.html:

{% extends 'base.html' %}
{% load static %}
{% block title %}修改头像{% endblock title %}
{% block css %}
    <link href="https://cdn.bootcdn.net/ajax/libs/cropper/4.1.0/cropper.min.css" rel="stylesheet">
    <link rel="stylesheet" href="{% static '/css/ImgCropping.css' %}" />
{% endblock css %}

{% block content %}
	...
{% endblock content %}

{% block script %}
	<script src="https://cdn.bootcdn.net/ajax/libs/cropper/4.1.0/cropper.min.js"></script>
{% endblock script %}

base.html:

<!DOCTYPE html>
{% load static %}
<html lang="zh-cn">
<head>
	<meta charset="utf-8" />
	<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>{% block title %}{% endblock title %}</title>
	<link href="https://cdn.bootcss.com/normalize/8.0.1/normalize.min.css" rel="stylesheet">
	<link href="https://cdn.bootcss.com/twitter-bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet">
	{% block css %}{% endblock css %}
</head>
    
<body>
	<div class="wrapper">
        {% include 'header.html' %}
        {% block content %}{% endblock content %}
    </div>
    <div class="footer">
        {% include 'footer.html' %}
    </div>

	<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
    <script src="https://cdn.bootcss.com/popper.js/1.16.1/umd/popper.min.js"></script>
    <script src="https://cdn.bootcss.com/twitter-bootstrap/4.4.1/js/bootstrap.min.js"></script>
    {% block script %}{% endblock script %}
    
</body>
</html>

在布局上我用的是bootstrap,包括cropper.js在内全部是cdn引入,版本在cdn链接上已经注明,想把这些东西下载到本地的请选择正确的版本。

需要注意的是这里使用了一个基于cropper.js的插件,插件下载地址如下,完全免费:
插件下载地址
其实这个插件也是基于cropper.js的,只是设定了一些样式,因此这里其实你只需要在你的项目里引用该插件中的ImgCropping.css即可,其它的全部按我上面写的用cdn引入就可以。

因为不想再去费事研究cropper.js了,所以就用别人写好的模板了。

接下来是主体的html部分:

{% block content %}
<div class="user">
     <div class="user-head">
         <div class="user-img">
             <img id="finalImg"  src="{{ user.avatar.url }}" width="100%">
         </div>
     </div>
     <div class="user-title">
         <h2>李狗蛋</h2>
         <p><span class="text-mute"></span></p>
         <button id="replaceImg" class="change-user-pic">更换头像</button>
     </div>
     <div class="user-btns">
         <a href="#" class="user-btn"><span>问问题</span></a>
         <a href="create-note.html" class="user-btn"><span>记笔记</span></a>
     </div>
     <ul class="user-users">
         <li>
             <p>关注</p>
             <span>26</span>
         </li>
         <li>
             <p>粉丝</p>
             <span>888888</span>
         </li>
     </ul>
 </div>

<!---头像裁剪弹出框--->
<div style="display: none" class="tailoring-container">
    <div class="black-cloth" onclick="closeTailor(this)"></div>
    <div class="tailoring-content">
        <form id="formUpload" class="avatar-form" action="{% url 'profile:pic_upload' %}" enctype="multipart/form-data" method="post">
            {% csrf_token %}
            <div class="tailoring-content-one">
                <label title="上传图片" for="chooseImg" class="l-btn choose-btn">
                    <input type="file" name="avatar" id="chooseImg" accept=".jpg, .jpeg, .png, .bmp" class="hidden" onchange="selectImg(this)"/>
                    选择图片
                </label>
                <div class="close-tailoring"  onclick="closeTailor(this)">×</div>
            </div>
            <div class="tailoring-content-two">
                <div class="tailoring-box-parcel">
                    <img id="tailoringImg">
                </div>
                <div class="preview-box-parcel">
                    <p>图片预览:</p>
                    <div class="square previewImg"></div>
                    <div class="circular previewImg"></div>
                </div>
            </div>
            <div class="tailoring-content-three">
                <button type="button" class="l-btn cropper-reset-btn">复位</button>
                <button type="button" class="l-btn cropper-rotate-btn">旋转</button>
                <button type="button" class="l-btn cropper-scaleX-btn">换向</button>
                <button  type="button" class="l-btn js-zoom-in">放大</button>
                <button type="button" class="l-btn js-zoom-out">放小</button>
                <button class="l-btn sureCut js-crop-and-upload" id="sureCut" type="button">确定</button>
            </div>
            <div>
                <input type="hidden" name="x" id="id_x"/>
                <input type="hidden" name="y" id="id_y"/>
                <input type="hidden" name="width" id="id_width"/>
                <input type="hidden" name="height" id="id_height"/>
            </div>
        </form>
    </div>
</div>
{% endblock content %}

这里的css样式我就不贴了,并不是很难,需要的可以留言。

然后在{% block script %}{% endblock script %}中写入如下js代码:

{% block script %}
	<script src="https://cdn.bootcdn.net/ajax/libs/cropper/4.1.0/cropper.min.js"></script>

	<script type="text/javascript">
        //弹出框水平垂直居中
        (window.onresize = function () {
            var win_height = $(window).height();
            var win_width = $(window).width();
            if (win_width <= 768){
                $(".tailoring-content").css({
                    "top": (win_height - $(".tailoring-content").outerHeight())/2,
                    "left": 0
                });
            }else{
                $(".tailoring-content").css({
                    "top": (win_height - $(".tailoring-content").outerHeight())/2,
                    "left": (win_width - $(".tailoring-content").outerWidth())/2
                });
            }
        })();

        //弹出图片裁剪框
        $("#replaceImg").on("click",function () {
            $(".tailoring-container").toggle();
        });
        //图像上传
        function selectImg(file) {
            if (!file.files || !file.files[0]){
                return;
            }
            var reader = new FileReader();
            reader.onload = function (evt) {
                var replaceSrc = evt.target.result;
                //更换cropper的图片
                $('#tailoringImg').cropper('replace', replaceSrc,false);//默认false,适应高度,不失真
            }
            reader.readAsDataURL(file.files[0]);
        }
        //cropper图片裁剪
        $('#tailoringImg').cropper({
            aspectRatio: 1/1,//默认比例
            preview: '.previewImg',//预览视图
            guides: false,  //裁剪框的虚线(九宫格)
            autoCropArea: 0.5,  //0-1之间的数值,定义自动剪裁区域的大小,默认0.8
            movable: false, //是否允许移动图片
            dragCrop: true,  //是否允许移除当前的剪裁框,并通过拖动来新建一个剪裁框区域
            movable: true,  //是否允许移动剪裁框
            resizable: true,  //是否允许改变裁剪框的大小
            zoomable: true,  //是否允许缩放图片大小
            mouseWheelZoom: false,  //是否允许通过鼠标滚轮来缩放图片
            touchDragZoom: true,  //是否允许通过触摸移动来缩放图片
            rotatable: true,  //是否允许旋转图片
            crop: function(e) {
                // 输出结果数据裁剪图像。
            }
        });
        //旋转
        $(".cropper-rotate-btn").on("click",function () {
            $('#tailoringImg').cropper("rotate", 45);
        });
        //复位
        $(".cropper-reset-btn").on("click",function () {
            $('#tailoringImg').cropper("reset");
        });
        //换向
        var flagX = true;
        $(".cropper-scaleX-btn").on("click",function () {
            if(flagX){
                $('#tailoringImg').cropper("scaleX", -1);
                flagX = false;
            }else{
                $('#tailoringImg').cropper("scaleX", 1);
                flagX = true;
            }
            flagX != flagX;
        });
        //放大
        $(".js-zoom-in").on("click",function () {
            $('#tailoringImg').cropper("zoom", 0.1);
        });
        //放小
        $(".js-zoom-out").on("click",function () {
            $('#tailoringImg').cropper("zoom", -0.1);
        });

        //裁剪后的处理
        $("#sureCut").on("click",function () {
            var cropData = $("#tailoringImg").cropper("getData");
            $("#id_x").val(cropData["x"]);
            $("#id_y").val(cropData["y"]);
            $("#id_height").val(cropData["height"]);
            $("#id_width").val(cropData["width"]);
            $("#formUpload").submit();
        });
        //关闭裁剪框
        function closeTailor() {
            $(".tailoring-container").toggle();
        }
    </script>
{% endblock script %}

OK!至此功能基本上已经可以运行了。

关于最后的js代码,其实可以添加更多的设置,具体想了解的可以参看cropper.js官方文档,链接如下:
官网链接
github链接
介绍性博文

另外,虽然头像上传的功能到这里确实是已经实现了,但这只是初步实现,我们可能还需要进一步验证上传的头像的尺寸、大小等,关于这些只好留待以后研究了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值