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鏈接
介紹性博文

另外,雖然頭像上傳的功能到這裏確實是已經實現了,但這只是初步實現,我們可能還需要進一步驗證上傳的頭像的尺寸、大小等,關於這些只好留待以後研究了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章