fix: 🐛 头像上传,文件上传问题

This commit is contained in:
H0nGzA1
2023-04-11 18:40:20 +08:00
parent 68cd42246d
commit 0ccf2e3725
12 changed files with 348 additions and 86 deletions

View File

@@ -1,6 +1,9 @@
from rest_framework import serializers from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.parsers import FileUploadParser
from dvadmin.system.models import FileList from dvadmin.system.models import FileList
from dvadmin.utils.json_response import SuccessResponse
from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet from dvadmin.utils.viewset import CustomModelViewSet
@@ -16,6 +19,7 @@ class FileSerializer(CustomModelSerializer):
fields = "__all__" fields = "__all__"
def create(self, validated_data): def create(self, validated_data):
print(self.context['request'])
validated_data['name'] = str(self.initial_data.get('file')) validated_data['name'] = str(self.initial_data.get('file'))
validated_data['url'] = self.initial_data.get('file') validated_data['url'] = self.initial_data.get('file')
return super().create(validated_data) return super().create(validated_data)
@@ -34,3 +38,8 @@ class FileViewSet(CustomModelViewSet):
serializer_class = FileSerializer serializer_class = FileSerializer
filter_fields = ['name', ] filter_fields = ['name', ]
permission_classes = [] permission_classes = []
@action(detail=False, methods=['post'])
def test_post_file(self, request):
return SuccessResponse(msg='test_is_ok')

View File

@@ -117,7 +117,6 @@ class MenuViewSet(CustomModelViewSet):
"""前端拖拽菜单之后重写parent""" """前端拖拽菜单之后重写parent"""
menu_id = request.data['menu_id'] menu_id = request.data['menu_id']
parent_id = request.data['parent_id'] parent_id = request.data['parent_id']
print(parent_id)
menu = Menu.objects.get(id=menu_id) menu = Menu.objects.get(id=menu_id)
menu.parent_id = parent_id menu.parent_id = parent_id
menu.save() menu.save()

View File

@@ -85,7 +85,7 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
def get_is_read(self, instance): def get_is_read(self, instance):
user_id = self.request.user.id user_id = self.request.user.id
message_center_id = instance.id message_center_id = instance.id
queryset = MessageCenterTargetUser.objects.filter(messagecenter__id=message_center_id,users_id=user_id).first() queryset = MessageCenterTargetUser.objects.filter(messagecenter__id=message_center_id, users_id=user_id).first()
if queryset: if queryset:
return queryset.is_read return queryset.is_read
return False return False
@@ -95,12 +95,12 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
fields = "__all__" fields = "__all__"
read_only_fields = ["id"] read_only_fields = ["id"]
def websocket_push(user_id, message): def websocket_push(user_id, message):
""" """
主动推送消息 主动推送消息
""" """
username = "user_"+str(user_id) username = "user_" + str(user_id)
print(103,message)
channel_layer = get_channel_layer() channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)( async_to_sync(channel_layer.group_send)(
username, username,
@@ -110,6 +110,7 @@ def websocket_push(user_id, message):
} }
) )
class MessageCenterCreateSerializer(CustomModelSerializer): class MessageCenterCreateSerializer(CustomModelSerializer):
""" """
消息中心-新增-序列化器 消息中心-新增-序列化器
@@ -122,10 +123,10 @@ class MessageCenterCreateSerializer(CustomModelSerializer):
# 在保存之前,根据目标类型,把目标用户查询出来并保存 # 在保存之前,根据目标类型,把目标用户查询出来并保存
users = initial_data.get('target_user', []) users = initial_data.get('target_user', [])
if target_type in [1]: # 按角色 if target_type in [1]: # 按角色
target_role = initial_data.get('target_role',[]) target_role = initial_data.get('target_role', [])
users = Users.objects.filter(role__id__in=target_role).values_list('id', flat=True) users = Users.objects.filter(role__id__in=target_role).values_list('id', flat=True)
if target_type in [2]: # 按部门 if target_type in [2]: # 按部门
target_dept = initial_data.get('target_dept',[]) target_dept = initial_data.get('target_dept', [])
users = Users.objects.filter(dept__id__in=target_dept).values_list('id', flat=True) users = Users.objects.filter(dept__id__in=target_dept).values_list('id', flat=True)
if target_type in [3]: # 系统通知 if target_type in [3]: # 系统通知
users = Users.objects.values_list('id', flat=True) users = Users.objects.values_list('id', flat=True)
@@ -195,7 +196,6 @@ class MessageCenterViewSet(CustomModelViewSet):
self_user_id = self.request.user.id self_user_id = self.request.user.id
# queryset = MessageCenterTargetUser.objects.filter(users__id=self_user_id).order_by('-create_datetime') # queryset = MessageCenterTargetUser.objects.filter(users__id=self_user_id).order_by('-create_datetime')
queryset = MessageCenter.objects.filter(target_user__id=self_user_id) queryset = MessageCenter.objects.filter(target_user__id=self_user_id)
print(queryset)
# queryset = self.filter_queryset(queryset) # queryset = self.filter_queryset(queryset)
page = self.paginate_queryset(queryset) page = self.paginate_queryset(queryset)
if page is not None: if page is not None:

View File

@@ -45,6 +45,7 @@
"ts-md5": "^1.3.1", "ts-md5": "^1.3.1",
"vue": "^3.2.45", "vue": "^3.2.45",
"vue-clipboard3": "^2.0.0", "vue-clipboard3": "^2.0.0",
"vue-cropper": "^1.0.8",
"vue-grid-layout": "^3.0.0-beta1", "vue-grid-layout": "^3.0.0-beta1",
"vue-i18n": "^9.2.2", "vue-i18n": "^9.2.2",
"vue-router": "^4.1.6", "vue-router": "^4.1.6",

View File

@@ -0,0 +1,171 @@
<template>
<div class="user-info-head" @click="editCropper()">
<el-avatar :size="100" :src="options.img" />
<el-dialog :title="title" v-model="open" width="600px" append-to-body @opened="modalOpened" @close="closeDialog">
<el-row>
<el-col class="flex justify-center">
<vue-cropper
ref="cropper"
:img="options.img"
:info="true"
:autoCrop="options.autoCrop"
:autoCropWidth="options.autoCropWidth"
:autoCropHeight="options.autoCropHeight"
:fixedBox="options.fixedBox"
:outputType="options.outputType"
@realTime="realTime"
:centerBox="true"
v-if="visible"
class="cropper"
/>
</el-col>
</el-row>
<br />
<el-row class="flex justify-center">
<el-col :lg="2" :md="2">
<el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload">
<el-button type="success">
选择
<el-icon class="el-icon--right"><Plus /></el-icon>
</el-button>
</el-upload>
</el-col>
<el-col :lg="{ span: 1, offset: 2 }" :md="2">
<el-button icon="RefreshLeft" @click="rotateLeft()"></el-button>
</el-col>
<el-col :lg="{ span: 1, offset: 2 }" :md="2">
<el-button icon="RefreshRight" @click="rotateRight()"></el-button>
</el-col>
<el-col :lg="{ span: 2, offset: 2 }" :md="2">
<el-button type="primary" @click="uploadImg()">更新头像</el-button>
</el-col>
</el-row>
</el-dialog>
</div>
</template>
<script setup>
import 'vue-cropper/dist/index.css';
import { VueCropper } from 'vue-cropper';
import { useUserInfo } from '/@/stores/userInfo';
import { getCurrentInstance, nextTick, reactive, ref } from 'vue';
import { base64ToFile } from '/@/utils/tools';
const userStore = useUserInfo();
const { proxy } = getCurrentInstance();
const open = ref(false);
const visible = ref(false);
const title = ref('修改头像');
const emit = defineEmits(['uploadImg']);
//图片裁剪数据
const options = reactive({
img: userStore.userInfos.avatar, // 裁剪图片的地址
autoCrop: true, // 是否默认生成截图框
autoCropWidth: 200, // 默认生成截图框宽度
autoCropHeight: 200, // 默认生成截图框高度
fixedBox: true, // 固定截图框大小 不允许改变
outputType: 'png', // 默认生成截图为PNG格式
});
/** 编辑头像 */
function editCropper() {
open.value = true;
}
/** 打开弹出层结束时的回调 */
function modalOpened() {
nextTick(() => {
visible.value = true;
});
}
/** 覆盖默认上传行为 */
function requestUpload() {}
/** 向左旋转 */
function rotateLeft() {
proxy.$refs.cropper.rotateLeft();
}
/** 向右旋转 */
function rotateRight() {
proxy.$refs.cropper.rotateRight();
}
/** 图片缩放 */
function changeScale(num) {
num = num || 1;
proxy.$refs.cropper.changeScale(num);
}
/** 上传预处理 */
function beforeUpload(file) {
if (file.type.indexOf('image/') == -1) {
proxy.$modal.msgError('文件格式错误,请上传图片类型,如JPGPNG后缀的文件。');
} else {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
options.img = reader.result;
};
}
}
/** 上传图片 */
function uploadImg() {
// 获取截图的 base64 数据
proxy.$refs.cropper.getCropData((data) => {
let img = new Image();
img.src = data;
img.onload = async () => {
let _data = compress(img);
const imgFile = base64ToFile(_data, 'testname');
emit('uploadImg', imgFile);
};
});
}
// 压缩图片
function compress(img) {
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
// let initSize = img.src.length;
let width = img.width;
let height = img.height;
canvas.width = width;
canvas.height = height;
// 铺底色
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, width, height);
// 进行压缩
let ndata = canvas.toDataURL('image/jpeg', 0.8);
return ndata;
}
/** 关闭窗口 */
function closeDialog() {
options.img = userStore.userInfos.avatar;
options.visible = false;
}
</script>
<style lang="scss" scoped>
.user-info-head {
position: relative;
display: inline-block;
height: 120px;
}
.user-info-head:hover:after {
content: '修改头像';
position: absolute;
text-align: center;
left: 0;
right: 0;
top: 0;
bottom: 0;
color: #000000;
font-size: 20px;
font-style: normal;
cursor: pointer;
line-height: 110px;
}
.cropper {
height: 400px;
width: 400px;
}
</style>

View File

@@ -5,11 +5,17 @@
// 用户信息 // 用户信息
export interface UserInfosState { export interface UserInfosState {
authBtnList: string[]; avatar: string;
photo: string;
roles: string[];
time: number;
userName: string; userName: string;
name: string;
email: string;
mobile: string;
gender: string;
dept_info: {
dept_id: number;
dept_name: string;
};
role_info: any[];
} }
export interface UserInfosStates { export interface UserInfosStates {
userInfos: UserInfosState; userInfos: UserInfosState;

View File

@@ -11,11 +11,22 @@ import { request } from '../utils/service';
export const useUserInfo = defineStore('userInfo', { export const useUserInfo = defineStore('userInfo', {
state: (): UserInfosStates => ({ state: (): UserInfosStates => ({
userInfos: { userInfos: {
avatar: '',
userName: '', userName: '',
photo: '', name: '',
time: 0, email: '',
roles: [], mobile: '',
authBtnList: [], gender: '',
dept_info: {
dept_id: 0,
dept_name: '',
},
role_info: [
{
id: 0,
name: '',
},
],
}, },
}), }),
actions: { actions: {
@@ -26,17 +37,22 @@ export const useUserInfo = defineStore('userInfo', {
} else { } else {
let userInfos: any = await this.getApiUserInfo(); let userInfos: any = await this.getApiUserInfo();
this.userInfos.userName = userInfos.data.name; this.userInfos.userName = userInfos.data.name;
this.userInfos.photo = userInfos.data.avatar || 'https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500' this.userInfos.avatar =
this.userInfos.time = new Date().getTime() userInfos.data.avatar || 'https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500';
this.userInfos.roles = ['admin'] this.userInfos.name = userInfos.data.name;
Session.set('userInfo', this.userInfos) this.userInfos.email = userInfos.data.email;
this.userInfos.mobile = userInfos.data.mobile;
this.userInfos.gender = userInfos.data.gender;
this.userInfos.dept_info = userInfos.data.dept_info;
this.userInfos.role_info = userInfos.data.role_info;
Session.set('userInfo', this.userInfos);
} }
}, },
async getApiUserInfo() { async getApiUserInfo() {
return request({ return request({
url: '/api/system/user/user_info/', url: '/api/system/user/user_info/',
method: 'get', method: 'get',
}) });
} },
}, },
}); });

View File

@@ -3,9 +3,9 @@
* @param {String} jsonString 需要解析的 json 字符串 * @param {String} jsonString 需要解析的 json 字符串
* @param {String} defaultValue 默认值 * @param {String} defaultValue 默认值
*/ */
import { uiContext } from "@fast-crud/fast-crud"; import { uiContext } from '@fast-crud/fast-crud';
export function parse(jsonString = "{}", defaultValue = {}) { export function parse(jsonString = '{}', defaultValue = {}) {
let result = defaultValue; let result = defaultValue;
try { try {
result = JSON.parse(jsonString); result = JSON.parse(jsonString);
@@ -21,7 +21,7 @@ export function parse(jsonString = "{}", defaultValue = {}) {
* @param {String} msg 状态信息 * @param {String} msg 状态信息
* @param {Number} code 状态码 * @param {Number} code 状态码
*/ */
export function response(data = {}, msg = "", code = 0) { export function response(data = {}, msg = '', code = 0) {
return [200, { code, msg, data }]; return [200, { code, msg, data }];
} }
@@ -30,7 +30,7 @@ export function response(data = {}, msg = "", code = 0) {
* @param {Any} data 返回值 * @param {Any} data 返回值
* @param {String} msg 状态信息 * @param {String} msg 状态信息
*/ */
export function responseSuccess(data = {}, msg = "成功") { export function responseSuccess(data = {}, msg = '成功') {
return response(data, msg); return response(data, msg);
} }
@@ -40,7 +40,7 @@ export function responseSuccess(data = {}, msg = "成功") {
* @param {String} msg 状态信息 * @param {String} msg 状态信息
* @param {Number} code 状态码 * @param {Number} code 状态码
*/ */
export function responseError(data = {}, msg = "请求失败", code = 500) { export function responseError(data = {}, msg = '请求失败', code = 500) {
return response(data, msg, code); return response(data, msg, code);
} }
@@ -48,22 +48,49 @@ export function responseError(data = {}, msg = "请求失败", code = 500) {
* @description 记录和显示错误 * @description 记录和显示错误
* @param {Error} error 错误对象 * @param {Error} error 错误对象
*/ */
export function errorLog(error:any,notification=true) { export function errorLog(error: any, notification = true) {
// 打印到控制台 // 打印到控制台
console.error(error); console.error(error);
// 显示提示 // 显示提示
if(notification){ if (notification) {
uiContext.get().notification.error({ message: error.message }); uiContext.get().notification.error({ message: error.message });
} }
} }
/** /**
* @description 创建一个错误 * @description 创建一个错误
* @param {String} msg 错误信息 * @param {String} msg 错误信息
*/ */
export function errorCreate(msg:any,notification=true) { export function errorCreate(msg: any, notification = true) {
const error = new Error(msg); const error = new Error(msg);
errorLog(error,notification); errorLog(error, notification);
// throw error; // throw error;
} }
export function base64ToFile(base64: any, fileName: string) {
// 将base64按照 , 进行分割 将前缀 与后续内容分隔开
let data = base64.split(',');
// 利用正则表达式 从前缀中获取图片的类型信息image/png、image/jpeg、image/webp等
let type = data[0].match(/:(.*?);/)[1];
// 从图片的类型信息中 获取具体的文件格式后缀png、jpeg、webp
let suffix = type.split('/')[1];
// 使用atob()对base64数据进行解码 结果是一个文件数据流 以字符串的格式输出
const bstr = window.atob(data[1]);
// 获取解码结果字符串的长度
let n = bstr.length;
// 根据解码结果字符串的长度创建一个等长的整形数字数组
// 但在创建时 所有元素初始值都为 0
const u8arr = new Uint8Array(n);
// 将整形数组的每个元素填充为解码结果字符串对应位置字符的UTF-16 编码单元
while (n--) {
// charCodeAt():获取给定索引处字符对应的 UTF-16 代码单元
u8arr[n] = bstr.charCodeAt(n);
}
// 利用构造函数创建File文件对象
// new File(bits, name, options)
const file = new File([u8arr], `${fileName}.${suffix}`, {
type: type,
});
// 将File文件对象返回给方法的调用者
return file;
}

View File

@@ -89,8 +89,8 @@ export default defineComponent({
const state = reactive({ const state = reactive({
isShowPassword: false, isShowPassword: false,
ruleForm: { ruleForm: {
username: 'superadmin', username: '',
password: 'admin123456', password: '',
captcha: '', captcha: '',
captchaKey: '', captchaKey: '',
captchaImgBase: '', captchaImgBase: '',

View File

@@ -1,15 +1,14 @@
import { request } from '/@/utils/service'; import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud'; import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
import {apiPrefix} from "/@/views/system/messageCenter/api"; import { apiPrefix } from '/@/views/system/messageCenter/api';
export function GetUserInfo(query: PageQuery) { export function GetUserInfo(query: PageQuery) {
return request({ return request({
url: '/api/system/user/user_info/', url: '/api/system/user/user_info/',
method: 'get', method: 'get',
params: query params: query,
}); });
} }
/** /**
* 更新用户信息 * 更新用户信息
* @param data * @param data
@@ -18,23 +17,22 @@ export function updateUserInfo(data: AddReq) {
return request({ return request({
url: '/api/system/user/update_user_info/', url: '/api/system/user/update_user_info/',
method: 'put', method: 'put',
data: data data: data,
}) });
} }
/** /**
* 获取自己接收的消息 * 获取自己接收的消息
* @param query * @param query
* @returns {*} * @returns {*}
* @constructor * @constructor
*/ */
export function GetSelfReceive (query:PageQuery) { export function GetSelfReceive(query: PageQuery) {
return request({ return request({
url: '/api/system/message_center/get_self_receive/', url: '/api/system/message_center/get_self_receive/',
method: 'get', method: 'get',
params: query params: query,
}) });
} }
/*** /***
@@ -45,6 +43,21 @@ export function UpdatePassword(data: EditReq) {
return request({ return request({
url: '/api/system/user/change_password/', url: '/api/system/user/change_password/',
method: 'put', method: 'put',
data: data data: data,
}) });
}
/***
* 上传头像
* @param data
*/
export function uploadAvatar(data: AddReq) {
return request({
url: 'api/system/file/',
method: 'post',
data: data,
headers: {
'Content-Type': 'multipart/form-data',
},
});
} }

View File

@@ -6,10 +6,9 @@
<el-card shadow="hover" header="个人信息"> <el-card shadow="hover" header="个人信息">
<div class="personal-user"> <div class="personal-user">
<div class="personal-user-left"> <div class="personal-user-left">
<el-upload class="h100 personal-user-left-upload" :action="uploadAvatar.action" :headers="uploadAvatar.headers" multiple :limit="1"> <!-- <el-avatar :size="100" v-if="state.personalForm.avatar" :src="state.personalForm.avatar" /> -->
<img v-if="state.personalForm.avatar" :src="state.personalForm.avatar" /> <!-- <el-avatar :size="100" v-else src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" /> -->
<img v-else src="https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500" /> <avatarSelector @uploadImg="uploadImg"></avatarSelector>
</el-upload>
</div> </div>
<div class="personal-user-right"> <div class="personal-user-right">
<el-row> <el-row>
@@ -174,13 +173,18 @@
</template> </template>
<script setup lang="ts" name="personal"> <script setup lang="ts" name="personal">
import { reactive, computed, onMounted, ref } from 'vue'; import { reactive, computed, onMounted, ref, defineAsyncComponent } from 'vue';
import { formatAxis } from '/@/utils/formatTime'; import { formatAxis } from '/@/utils/formatTime';
import * as api from './api'; import * as api from './api';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { getBaseURL } from '/@/utils/baseUrl'; import { getBaseURL } from '/@/utils/baseUrl';
import { Session } from '/@/utils/storage'; import { Session } from '/@/utils/storage';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useUserInfo } from '/@/stores/userInfo';
const userStore = useUserInfo();
// 头像裁剪组件
const avatarSelector = defineAsyncComponent(() => import('/@/components/avatarSelector/index.vue'));
// 当前时间提示语 // 当前时间提示语
const currentTime = computed(() => { const currentTime = computed(() => {
@@ -339,6 +343,17 @@ const settingPassword = () => {
} }
}); });
}; };
const uploadImg = (data: any) => {
console.log(data);
let formdata = new FormData();
formdata.append('key', 'test_file');
formdata.append('file', data);
api.uploadAvatar(formdata).then((res: any) => {
// userStore.setUserInfos()
console.log(res);
});
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -5045,6 +5045,11 @@ vue-clipboard3@^2.0.0:
dependencies: dependencies:
clipboard "^2.0.6" clipboard "^2.0.6"
vue-cropper@^1.0.8:
version "1.0.8"
resolved "https://registry.npmjs.org/vue-cropper/-/vue-cropper-1.0.8.tgz#05853bb7702557d05a4784a8d0cd072b57dd8e4f"
integrity sha512-EX9XoT5a/PQ62J6KDZz8hhaFi9ME1k2yBZlRfYCm8iySzTcjw0nDBq8Y65HtyHaH2jJwUKgYfD6mdFCE0GhUzA==
vue-cropperjs@^5.0.0: vue-cropperjs@^5.0.0:
version "5.0.0" version "5.0.0"
resolved "https://registry.npmjs.org/vue-cropperjs/-/vue-cropperjs-5.0.0.tgz#7f8cbc460737af3831b4ded634c95905198e329e" resolved "https://registry.npmjs.org/vue-cropperjs/-/vue-cropperjs-5.0.0.tgz#7f8cbc460737af3831b4ded634c95905198e329e"