后台开发过程中,需要实现一个图片上传功能,后台api接口我们使用的是lin-cms-koa.
接口封装
// 上传文件
export function uploadImg(file) {
const formData = new FormData();
formData.append('file', file); // 将文件添加到 FormData 对象中
return request({
url: '/cms/file/upload',
method: 'post',
data: formData, // 使用 FormData 作为数据
headers: {
// 'Content-Type' 不需要在这里设置,axios 会自动处理
}
});
}
单个图片上传
<template>
<div class="avatar-uploader" @click="handleUpload">
<img v-if="computedAvatar" :src="computedAvatar" class="avatar" />
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
<input
ref="fileInput"
type="file"
class="file-input"
@change="handleFileChange"
style="display: none;"
/>
</div>
</template>
<script setup>
import { defineProps, defineEmits, computed, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { apiBaseUrl } from '@/api/ipConfig.js'
import { uploadImg } from '@/api/api/cms.js'
const props = defineProps({
avatar: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:avatar'])
const fileInput = ref(null)
// 计算属性,用于生成完整的头像 URL
const computedAvatar = computed(() => {
return props.avatar ? apiBaseUrl + '/assets/' + props.avatar : ''
})
// 点击上传按钮时触发文件选择
const handleUpload = () => {
fileInput.value.value = null; // 清空文件输入框的值
fileInput.value.click()
}
// 文件选择后的处理函数
const handleFileChange = async (e) => {
const file = e.target.files[0]
if (!file) return; // 如果没有选择文件,直接返回
const isJPG = file.type === 'image/jpeg'
const isPNG = file.type === 'image/png'
const isLt2M = file.size / 1024 / 1024 < 2
if (!isJPG && !isPNG) {
ElMessage.error('头像只能是 JPG 或 PNG 格式!')
return
}
if (!isLt2M) {
ElMessage.error('头像大小不能超过 2MB!')
return
}
try {
const response = await uploadImg(file); // 直接传递文件对象
if (response && response.length > 0) {
const newAvatarPath = response[0].path; // 更新头像路径
emit('update:avatar', newAvatarPath); // 触发事件并传递新路径
ElMessage.success('头像上传成功');
fileInput.value.value = null; // 上传成功后清空文件输入框的值
}
} catch (error) {
ElMessage.error('上传失败,请重试');
}
}
</script>
<style scoped>
.avatar-uploader {
text-align: center;
width: 178px;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: nowrap;
flex-direction: row;
align-content: flex-start;
height: 178px;
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar {
width: 178px;
height: 178px;
display: block;
}
</style>
使用
<AvatarUploader v-model:avatar="form.avatar"></AvatarUploader>
多个图片上传
多个图片上传,上传后可以删除,返回的图片路径,逗号分割。
<template>
<div class="uploadImgList">
<div v-for="(image, index) in computedImages" :key="index" class="avatar">
<img :src="image" class="avatar"/>
<el-icon class="delete-icon" @click.stop="removeImage(index)">
<Delete/>
</el-icon>
</div>
<div class="avatar-uploader" @click="handleUpload">
<el-icon v-if="images.length < maxImages" class="avatar-uploader-icon">
<Plus/>
</el-icon>
<input
ref="fileInput"
type="file"
class="file-input"
@change="handleFileChange"
style="display: none;"
multiple
/>
</div>
</div>
</template>
<script setup>
import {defineProps, defineEmits, computed, ref} from 'vue'
import {ElMessage} from 'element-plus'
import {apiBaseUrl} from '@/api/ipConfig.js'
import {uploadImg} from '@/api/api/cms.js'
import {Plus, Delete} from '@element-plus/icons-vue' // 确保引入删除图标
const props = defineProps({
avatar: {
type: String,
default: ''
},
maxImages: {
type: Number,
default: 3 // 默认最多上传3张图片
}
})
const emit = defineEmits(['update:avatar'])
const fileInput = ref(null)
const images = ref(props.avatar ? props.avatar.split(',') : []) // 用于存储上传的图片路径
// 计算属性,用于生成完整的图片 URL
const computedImages = computed(() => {
return images.value.map(img => apiBaseUrl + '/assets/' + img)
})
// 点击上传按钮时触发文件选择
const handleUpload = () => {
fileInput.value.value = null; // 清空文件输入框的值
fileInput.value.click()
}
// 文件选择后的处理函数
const handleFileChange = async (e) => {
const files = e.target.files
if (!files.length) return; // 如果没有选择文件,直接返回
for (const file of files) {
const isJPG = file.type === 'image/jpeg'
const isPNG = file.type === 'image/png'
const isLt2M = file.size / 1024 / 1024 < 2
if (!isJPG && !isPNG) {
ElMessage.error('头像只能是 JPG 或 PNG 格式!')
return
}
if (!isLt2M) {
ElMessage.error('头像大小不能超过 2MB!')
return
}
try {
const response = await uploadImg(file); // 直接传递文件对象
if (response && response.length > 0) {
const newAvatarPath = response[0].path; // 更新头像路径
images.value.push(newAvatarPath); // 添加新图片路径
emit('update:avatar', images.value.join(',')); // 触发事件并传递新路径
ElMessage.success('头像上传成功');
}
} catch (error) {
ElMessage.error('上传失败,请重试');
}
}
}
// 移除图片
const removeImage = (index) => {
images.value.splice(index, 1); // 从数组中移除图片
emit('update:avatar', images.value.join(',')); // 更新父组件的图片路径
}
</script>
<style scoped>
.uploadImgList {
display: flex;
justify-content: flex-start;
align-items: flex-start;
flex-wrap: wrap;
flex-direction: row;
align-content: flex-start;
}
.avatar-uploader {
text-align: center;
width: 150px;
height: 150px;
align-items: center;
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: nowrap;
flex-direction: row;
align-content: flex-start;
}
.avatar {
width: 150px;
position: relative;
height: 150px;
display: block;
margin-right: 10px;
border: 1px solid #ddd;
}
.delete-icon {
position: absolute;
top: 0;
right: 0;
cursor: pointer;
color: red; /* 删除图标颜色 */
}
</style>
使用组件
<AvatarUploader v-model:avatar="form.img"></AvatarUploader>