說實話,給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_URL和MEDIA_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鏈接
介紹性博文
另外,雖然頭像上傳的功能到這裏確實是已經實現了,但這只是初步實現,我們可能還需要進一步驗證上傳的頭像的尺寸、大小等,關於這些只好留待以後研究了。