模版部分:
鼠标悬浮到头像的部分就出现下拉框显示可以修改头像,
el-upload是隐藏的,可能只是为了实现on-change函数和before-upload函数吧
这块做的确实有点马虎了。
<div class="r-content">
<el-dropdown>
<span class="el-dropdown-link">
<img :src="getImageUrl" class="avatar">
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleUpdateAvatar">修改头像</el-dropdown-item>
<el-dropdown-item @click="handleLoginOut">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-upload
class="avatar-uploader"
action="#"
:show-file-list="true"
:on-change="handleAvatarChange"
:before-upload="beforeAvatarUpload"
style="display: none;"
ref="uploadRef"
>
<!-- <el-button ref="btn" size="large" type="primary">选取文件</el-button> -->
</el-upload>
</div>
import { ref, computed,nextTick } from 'vue'
import { useAllDataStore } from '../stores'
import { useRouter } from 'vue-router'
// import { ElMessage,ElUpload } from 'element-plus'
import { handleAvatarChange } from '@/services/editService.js'
const store = useAllDataStore()
import defaultAvatar from '@/assets/images/user-default.png'
const getImageUrl = computed(() => {
return store.state.avatar||defaultAvatar;
})
// 监听状态变化(调试用)
watch(
() => store.state.avatar,
(newVal, oldVal) => {
console.log('头像更新了:', newVal, oldVal)
},
{ immediate: true } // 立即执行一次
)
const uploadRef = ref(null)
function handleUpdateAvatar() {
console.log('handleUpdateAvatar')
uploadRef.value.$el.querySelector('input').click()
}
// 上传前的校验,比如限制文件类型、大小等
function beforeAvatarUpload(file) {
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png';
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isJPG) {
ElMessage.error('上传头像只能是 JPG/PNG 格式!');
}
if (!isLt2M) {
ElMessage.error('上传头像大小不能超过 2MB!');
}
return isJPG && isLt2M;
}
//跨组件之间的传值
const router = useRouter()
const handleLoginOut = () => {
store.clean();
router.push('/login')
}
前端发送请求部分:
export const handleAvatarChange=async (file)=> {
try {
// 创建 FormData 对象,用于上传文件
const formData = new FormData();
formData.append('file', file.raw);
// 调用后端接口上传头像,这里的接口地址根据实际后端定义填写
const res = await axios.post(`${API_URL}/updateAvatar`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
'Authorization':`Bearer ${localStorage.getItem('token')}`,
},
withCredentials: true
});
if (res.data.code === 200) { // 假设后端返回 code 为 200 表示成功
ElMessage.success('头像修改成功');
// 更新 store 中的头像地址
store.updateImg(res.data.data.avatar);
}
} catch (error) {
// console.error('上传头像出错!:', error);
ElMessage.error('网络异常,头像修改失败!');
}
}
后端处理请求部分:
import { Router } from 'express';
import multer from 'multer';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs/promises';
import pool from '../config/db.js';
import jwt from 'jsonwebtoken';
import { JWT_SECRET } from '../config/config.js';
import bodyParser from 'body-parser';
const router = Router();
// 1. 直接通过 import.meta.url 计算上传目录路径(不使用 __dirname)
const currentFileUrl = new URL(import.meta.url); //当前文件完整的地址
const currentDirPath = path.dirname(fileURLToPath(currentFileUrl)); // 当前文件所在目录routes的地址
const uploadDir = path.join(currentDirPath, '../public/avatars'); // 拼接上传目录路径
// console.log('uploadDir:',uploadDir);
// 初始化上传目录
try {
await fs.access(uploadDir);
} catch {
await fs.mkdir(uploadDir, { recursive: true });
}
// 2. 配置 multer 存储规则
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const safeName = file.originalname.replace(/[^a-zA-Z0-9_.-]/g, '');
const uniqueName = `${Date.now()}-${safeName}`;
cb(null, uniqueName);
//生成唯一文件名
}
});
// 3. 创建 multer 实例
const upload = multer({
limits: { fileSize: 2 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('只允许上传图片文件!'), false);
}
},
storage: storage
});
// 4.添加认证中间件
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
console.log(req.headers);
if (!token) {
return res.status(401).json({
code: 401,
message: '未提供Token认证信息!'
});
}
try {
// 获取发送请求方的token信息,验证发送人 同时在post请求中顺利的修改请求人的数据库字段
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded;
console.log('req.user:',req.user);
next();
} catch (error) {
res.status(401).json({
code: 401,
message: '这是无效或者过期的Token!'
});
}
}
// 5. 处理头像上传请求
router.post('/updateAvatar',authenticate, upload.single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
code: 400,
message: '文件为空,请选择要上传的头像!'
});
}
console.log('req.file.filename:',req.file.filename);
// 生成图片访问 URL
const avatarUrl = `http://localhost:3000/avatars/${req.file.filename}`;
// 验证登录态
if (!req.user?.id) {
return res.status(401).json({
code: 401,
message: '未登录,无法修改头像'
});
}
const userId = req.user.id;
// 更新数据库
const [results] = await pool.query(
'UPDATE users SET avatarUrl = ? WHERE id = ?',
[avatarUrl, userId]
);
if (results.affectedRows === 0) {
return res.status(404).json({
code: 404,
message: '用户不存在,更新失败'
});
}
res.status(200).json({
code: 200,
message: '头像修改成功',
data: { avatar: avatarUrl }
});
} catch (error) {
console.error('头像上传失败:', error);
res.status(500).json({
code: 500,
message: '服务器错误,上传失败'
});
}
});
因为有文件处理中间件这些的吧所以确实麻烦了些,还有认证中间件,可以自行删减,注意头像要上传原图片,然后对应的格式就是multipart/form-data了 这块content-type我这个项目里