目录
一、简介
本篇文章主要用于介绍如何使用VUE+DRF实现用户头像上传
二、DRF
①Settings文件配置
在DRF框架下如果想要实现文件的上传,需要配置媒体文件的目录与路由,具体配置如下:
# zero_demo/settings.py
# 媒体文件目录
from pathlib import Path
import os
BASE_DIR = Path(__file__).resolve().parent.parent
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
# 上传文件目录
UPLOAD_PATH = 'upload/'
# 访问媒体文件的路由
MEDIA_URL = '/media/'
②Urls文件配置
settings文件最后一行代码配置了访问媒体文件的路由,这是不够的,还需要在urls文件中配置具体的路由,其配置如下:
# zero_demo/urls.py
from django.urls import re_path
from django.views.static import serve
from django.conf import settings
urlpatterns = [
re_path(r"^media/(?P<path>.*)$", serve, {'document_root': settings.MEDIA_ROOT}, name='media'),
]
③Models文件
配置好settings和urls文件之后,在数据库中创建一张用户表,用于存储与用户有关的所有信息,models文件代码如下:
class Admin(models.Model):
""" 用户表 """
user = models.CharField(verbose_name="用户名", max_length=32)
password = models.CharField(verbose_name="密码", max_length=32)
head = models.CharField(verbose_name="头像路径", max_length=128)
④Views文件
最后则是在Views文件中实现上传头像的逻辑,大致是思路是编写一个API接口提供给前端VUE界面发送POST请求,然后将上传的图片存储与media目录下,接着返回存储与media目录下的url给前端,以供前端通过url找到存储在media目录下的图片进行展示 ,具体代码实现如下:
序列化器
# apps/rbac/views.py
class AdminSerializer(serializers.ModelSerializer):
role_display = AdminRoleSerializer(many=True, source='roles', read_only=True)
# 使用自定义钩子方法,返回带前缀的url
head_url = serializers.SerializerMethodField()
class Meta:
model = models.Admin
fields = "__all__"
def get_head_url(self, obj):
# 调用request中的build_absolute_uri方法,返回带前缀的url
return self.context['request'].build_absolute_uri(obj.head)
View类
这里调用了@action方法,它的作用可以让在调用AdminView的url的后面增加新的url,例如/api/rbac/admin/这个url调用了AdminView,它的后面会多增加一个/api/rbac/admin/upload/的url
from rest_framework.decorators import action
class AdminView(ModelViewSet):
authentication_classes = []
permission_classes = []
queryset = models.Admin.objects.all()
serializer_class = AdminSerializer
def get_serializer_class(self):
if self.request.method == "POST":
return AdminCreateSerializer
return AdminSerializer
# detail=False的意思是生成的额外url不用带id
@action(detail=False, methods=['post'], url_path="upload")
def upload(self, request):
upload_object = request.FILES.get("file")
# upload_object.size # 文件大小
# upload_object.name # 文件名称
# upload_object.read() # 读取整个文件
# for chunk in upload_object.chunks(1024): # 每次读取1024字节的文件
# pass # 读取内容写入到本地
if upload_object.size > 1 * 1024 * 1024:
return Response({
"code": 444,
"msg": "文件太大"
})
upload_url = get_upload_filename(upload_object.name)
save_path = default_storage.save(upload_url, upload_object)
local_url = default_storage.url(save_path)
abs_url = request.build_absolute_uri(local_url)
context = {
'data': {
'url': local_url,
'abs_url': abs_url
}
}
# 返回本地路径
return Response(context)
上面的代码中调用了get_upload_filename方法
upload_url = get_upload_filename(upload_object.name)
其定义如下:
# apps/rbac/views.py
from datetime import datetime
from django.conf import settings
import os
from django.core.files.storage import default_storage
def get_upload_filename(file_name):
date_path = datetime.now().strftime('%Y/%m/%d')
upload_path = os.path.join(settings.UPLOAD_PATH, date_path)
file_path = os.path.join(upload_path, file_name)
return default_storage.get_available_name(file_path)
将Views文件配置完成后,后端也就算准备完成,接下来编写前端VUE代码
三、VUE
①router
首先在router注册一个用户编辑的路由
# router/index.js
const routes = [
{
path: '/authEdit',
name: 'AuthEdit',
component: () => import('../views/account/AuthEditView.vue')
},
]
②views
路由中调用了AuthEditView.vue,因此需要在编写该vue,即用户编辑的页面
<template>
<el-card class="box-card" shadow="never">
<template #header>
<div class="card-header" style="display: flex;justify-content: space-between;align-items: center;">
<div>
<span style="font-weight: bold;font-size: 18px;">个人信息编辑</span>
</div>
<router-link :to="{name:'ip156'}" style="text-decoration: none">
<el-button type="primary">返回</el-button>
</router-link>
</div>
</template>
<el-form :model="state.form" ref="formRef" label-width="140px">
<div style="padding: 0 10px;">
<div>
<h4>个人信息</h4>
<el-row :gutter="30">
<el-col :span="12">
<el-form-item style="margin-top: 24px;" :error="state.error.user" label="邮箱前缀">
<el-input v-model="state.form.user" placeholder="邮箱前缀"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item style="margin-top: 24px;" :error="state.error.password" label="账户密码">
<el-input v-model="state.form.password" placeholder="账户密码"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="30">
<el-col :span="12">
<el-form-item style="margin-top: 24px;" :error="state.error.head" label="个人头像">
<ul v-if="state.form.head" class="el-upload-list el-upload-list--picture-card"
style="width: 200px;height: 150px;">
<li class="el-upload-list__item is-success" style="width: 200px;height: 120px;">
<img class="el-upload-list__item-thumbnail" :src="state.form.head_url">
<span class="el-upload-list__item-actions">
<span class="el-upload-list__item-preview"
@click="state.dialogLicenceVisible=true">
<el-icon><ZoomIn/></el-icon>
</span>
<span class="el-upload-list__item-delete" @click="removeLicenceImage">
<el-icon><Delete/></el-icon>
</span>
</span>
</li>
</ul>
<el-upload
v-else
style="width: 200px;height: 150px;"
drag
:data="{type:'head'}"
:show-file-list="false"
:multiple="false"
:action="imageUploadUrl"
:before-upload="beforeImageUpload"
:on-success="uploadSuccessWrapper('head','head_url')">
<el-icon class="el-icon--upload">
<upload-filled/>
</el-icon>
<template #tip>
<div class="el-upload__tip"
style=" text-align: center;font-size: 8px;margin-top: 0;line-height: 25px;">
只能上传图片文件,且不超过2M
</div>
</template>
</el-upload>
</el-form-item>
</el-col>
</el-row>
</div>
<el-divider border-style="dotted"/>
<el-row justify="center" align="middle" style="height: 80px;">
<el-button type="primary" style="width: 200px;height: 40px;" @click="doSubmit">提交修改</el-button>
</el-row>
</div>
</el-form>
</el-card>
<el-dialog v-model="state.dialogLicenceVisible">
<img w-full :src="state.form.head_url" alt="Preview Image" style="width: 55%;height: 100%"/>
</el-dialog>
</template>
<script setup>
import {reactive, onMounted, getCurrentInstance} from 'vue'
import {UploadFilled, Delete, ZoomIn} from '@element-plus/icons-vue'
import {ElMessage} from 'element-plus'
import {useStore} from "vuex";
import {clearFormError} from "@/plugins/form";
const store = useStore()
const {proxy} = getCurrentInstance()
// 上传图片
const imageUploadUrl = "http://127.0.0.1:8000/api/rbac/admin/upload/"
const state = reactive({
dialogLicenceVisible: false,
form: {
user: "",
password: "",
head: "",
head_url: "",
},
// preview:{
// head_url: "",
// },
error: {
user: "",
password: "",
head: "",
},
})
function beforeImageUpload(file) {
const isPNG = file.type === 'image/png';
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isPNG) {
ElMessage.error("上传图片只能是图片格式!");
}
if (!isLt2M) {
ElMessage.error("上传图片大小不能超过 2MB!");
}
return isPNG && isLt2M;
}
function uploadSuccessWrapper(fieldName, preViewFieldName) {
return function (res) {
if (res.code === 0) {
// 1.图片地址+返回时添加
// 2.服务器支持访问静态图片
// {"code":0,"data":{"url":"/media/uploads/2022/04/01/2_nO3mqV7.jpeg","abs_url":".htt"}}
state.form[fieldName] = res.data.data.url;
state.form[preViewFieldName] = res.data.data.abs_url;
} else {
ElMessage.error('上传失败:' + res.msg);
}
}
}
function removeLicenceImage() {
state.form.head_url = ""
state.form.head = ""
}
onMounted(() => {
initAccount()
})
function initAccount() {
let id = store.state.id;
proxy.$axios.get(`/api/rbac/admin/${id}/`).then((res) => {
if (res.data.code === 0) {
state.form = res.data.data
} else {
ElMessage.error('数据获取失败')
}
})
}
function doSubmit() {
//1.表单校验
clearFormError(state.error)
//2.发送请求
let id = store.state.id;
proxy.$axios.patch(`/api/rbac/admin/${id}/`, state.form).then((res) => {
ElMessage.success("修改成功")
})
}
</script>
<style scoped>
.avatar-uploader .avatar {
width: 200px;
height: 120px;
display: block;
}
</style>
<style>
.el-upload-dragger {
padding: 20px;
}
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 200px;
height: 120px;
text-align: center;
}
</style>