fix: 🐛 头像上传,文件上传问题
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
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.utils.json_response import SuccessResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
|
||||
@@ -16,6 +19,7 @@ class FileSerializer(CustomModelSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
def create(self, validated_data):
|
||||
print(self.context['request'])
|
||||
validated_data['name'] = str(self.initial_data.get('file'))
|
||||
validated_data['url'] = self.initial_data.get('file')
|
||||
return super().create(validated_data)
|
||||
@@ -34,3 +38,8 @@ class FileViewSet(CustomModelViewSet):
|
||||
serializer_class = FileSerializer
|
||||
filter_fields = ['name', ]
|
||||
permission_classes = []
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def test_post_file(self, request):
|
||||
|
||||
return SuccessResponse(msg='test_is_ok')
|
||||
|
||||
@@ -117,7 +117,6 @@ class MenuViewSet(CustomModelViewSet):
|
||||
"""前端拖拽菜单之后重写parent"""
|
||||
menu_id = request.data['menu_id']
|
||||
parent_id = request.data['parent_id']
|
||||
print(parent_id)
|
||||
menu = Menu.objects.get(id=menu_id)
|
||||
menu.parent_id = parent_id
|
||||
menu.save()
|
||||
|
||||
@@ -95,12 +95,12 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
|
||||
fields = "__all__"
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
def websocket_push(user_id, message):
|
||||
"""
|
||||
主动推送消息
|
||||
"""
|
||||
username = "user_" + str(user_id)
|
||||
print(103,message)
|
||||
channel_layer = get_channel_layer()
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
username,
|
||||
@@ -110,6 +110,7 @@ def websocket_push(user_id, message):
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class MessageCenterCreateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
消息中心-新增-序列化器
|
||||
@@ -195,7 +196,6 @@ class MessageCenterViewSet(CustomModelViewSet):
|
||||
self_user_id = self.request.user.id
|
||||
# queryset = MessageCenterTargetUser.objects.filter(users__id=self_user_id).order_by('-create_datetime')
|
||||
queryset = MessageCenter.objects.filter(target_user__id=self_user_id)
|
||||
print(queryset)
|
||||
# queryset = self.filter_queryset(queryset)
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"ts-md5": "^1.3.1",
|
||||
"vue": "^3.2.45",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"vue-cropper": "^1.0.8",
|
||||
"vue-grid-layout": "^3.0.0-beta1",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.1.6",
|
||||
|
||||
171
web/src/components/avatarSelector/index.vue
Normal file
171
web/src/components/avatarSelector/index.vue
Normal 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('文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。');
|
||||
} 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>
|
||||
@@ -5,11 +5,17 @@
|
||||
|
||||
// 用户信息
|
||||
export interface UserInfosState {
|
||||
authBtnList: string[];
|
||||
photo: string;
|
||||
roles: string[];
|
||||
time: number;
|
||||
avatar: 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 {
|
||||
userInfos: UserInfosState;
|
||||
|
||||
@@ -11,11 +11,22 @@ import { request } from '../utils/service';
|
||||
export const useUserInfo = defineStore('userInfo', {
|
||||
state: (): UserInfosStates => ({
|
||||
userInfos: {
|
||||
avatar: '',
|
||||
userName: '',
|
||||
photo: '',
|
||||
time: 0,
|
||||
roles: [],
|
||||
authBtnList: [],
|
||||
name: '',
|
||||
email: '',
|
||||
mobile: '',
|
||||
gender: '',
|
||||
dept_info: {
|
||||
dept_id: 0,
|
||||
dept_name: '',
|
||||
},
|
||||
role_info: [
|
||||
{
|
||||
id: 0,
|
||||
name: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
actions: {
|
||||
@@ -26,17 +37,22 @@ export const useUserInfo = defineStore('userInfo', {
|
||||
} else {
|
||||
let userInfos: any = await this.getApiUserInfo();
|
||||
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.time = new Date().getTime()
|
||||
this.userInfos.roles = ['admin']
|
||||
Session.set('userInfo', this.userInfos)
|
||||
this.userInfos.avatar =
|
||||
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.name = userInfos.data.name;
|
||||
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() {
|
||||
return request({
|
||||
url: '/api/system/user/user_info/',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* @param {String} jsonString 需要解析的 json 字符串
|
||||
* @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;
|
||||
try {
|
||||
result = JSON.parse(jsonString);
|
||||
@@ -21,7 +21,7 @@ export function parse(jsonString = "{}", defaultValue = {}) {
|
||||
* @param {String} msg 状态信息
|
||||
* @param {Number} code 状态码
|
||||
*/
|
||||
export function response(data = {}, msg = "", code = 0) {
|
||||
export function response(data = {}, msg = '', code = 0) {
|
||||
return [200, { code, msg, data }];
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function response(data = {}, msg = "", code = 0) {
|
||||
* @param {Any} data 返回值
|
||||
* @param {String} msg 状态信息
|
||||
*/
|
||||
export function responseSuccess(data = {}, msg = "成功") {
|
||||
export function responseSuccess(data = {}, msg = '成功') {
|
||||
return response(data, msg);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export function responseSuccess(data = {}, msg = "成功") {
|
||||
* @param {String} msg 状态信息
|
||||
* @param {Number} code 状态码
|
||||
*/
|
||||
export function responseError(data = {}, msg = "请求失败", code = 500) {
|
||||
export function responseError(data = {}, msg = '请求失败', code = 500) {
|
||||
return response(data, msg, code);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,6 @@ export function errorLog(error:any,notification=true) {
|
||||
if (notification) {
|
||||
uiContext.get().notification.error({ message: error.message });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,3 +66,31 @@ export function errorCreate(msg:any,notification=true) {
|
||||
errorLog(error, notification);
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -89,8 +89,8 @@ export default defineComponent({
|
||||
const state = reactive({
|
||||
isShowPassword: false,
|
||||
ruleForm: {
|
||||
username: 'superadmin',
|
||||
password: 'admin123456',
|
||||
username: '',
|
||||
password: '',
|
||||
captcha: '',
|
||||
captchaKey: '',
|
||||
captchaImgBase: '',
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { request } from '/@/utils/service';
|
||||
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) {
|
||||
return request({
|
||||
url: '/api/system/user/user_info/',
|
||||
method: 'get',
|
||||
params: query
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
* @param data
|
||||
@@ -18,11 +17,10 @@ export function updateUserInfo(data: AddReq) {
|
||||
return request({
|
||||
url: '/api/system/user/update_user_info/',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取自己接收的消息
|
||||
* @param query
|
||||
@@ -33,8 +31,8 @@ export function GetSelfReceive (query:PageQuery) {
|
||||
return request({
|
||||
url: '/api/system/message_center/get_self_receive/',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/***
|
||||
@@ -45,6 +43,21 @@ export function UpdatePassword(data: EditReq) {
|
||||
return request({
|
||||
url: '/api/system/user/change_password/',
|
||||
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',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,10 +6,9 @@
|
||||
<el-card shadow="hover" header="个人信息">
|
||||
<div class="personal-user">
|
||||
<div class="personal-user-left">
|
||||
<el-upload class="h100 personal-user-left-upload" :action="uploadAvatar.action" :headers="uploadAvatar.headers" multiple :limit="1">
|
||||
<img v-if="state.personalForm.avatar" :src="state.personalForm.avatar" />
|
||||
<img v-else src="https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500" />
|
||||
</el-upload>
|
||||
<!-- <el-avatar :size="100" v-if="state.personalForm.avatar" :src="state.personalForm.avatar" /> -->
|
||||
<!-- <el-avatar :size="100" v-else src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" /> -->
|
||||
<avatarSelector @uploadImg="uploadImg"></avatarSelector>
|
||||
</div>
|
||||
<div class="personal-user-right">
|
||||
<el-row>
|
||||
@@ -174,13 +173,18 @@
|
||||
</template>
|
||||
|
||||
<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 * as api from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getBaseURL } from '/@/utils/baseUrl';
|
||||
import { Session } from '/@/utils/storage';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useUserInfo } from '/@/stores/userInfo';
|
||||
const userStore = useUserInfo();
|
||||
|
||||
// 头像裁剪组件
|
||||
const avatarSelector = defineAsyncComponent(() => import('/@/components/avatarSelector/index.vue'));
|
||||
|
||||
// 当前时间提示语
|
||||
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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -5045,6 +5045,11 @@ vue-clipboard3@^2.0.0:
|
||||
dependencies:
|
||||
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:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.npmjs.org/vue-cropperjs/-/vue-cropperjs-5.0.0.tgz#7f8cbc460737af3831b4ded634c95905198e329e"
|
||||
|
||||
Reference in New Issue
Block a user