说实话,给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链接
介绍性博文
另外,虽然头像上传的功能到这里确实是已经实现了,但这只是初步实现,我们可能还需要进一步验证上传的头像的尺寸、大小等,关于这些只好留待以后研究了。