当前位置: 首页 > news >正文

上传头像到腾讯云对象存储-前端基于antdv

到腾讯云官网开通对象存储、获取 api 密钥,等一系列步骤略过

腾讯云官网文档:https://cloud.tencent.com/document/product/436/65935#0a5a6b09-0777-4d51-a090-95565985fe2c

1. 后端

1.1. 引入依赖

pom.xml

<!-- 腾讯云对象存储 -->
<dependency><groupId>com.qcloud</groupId><artifactId>cos_api</artifactId><version>5.6.227</version>
</dependency>

1.2. 初始化对象存储客户端

CosClientConfig

package com.yu.cloudpicturebackend.config;import com.qcloud.cos.COSClient;import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicCOSCredentials;
import com.qcloud.cos.auth.COSCredentials;
import com.qcloud.cos.http.HttpProtocol;
import com.qcloud.cos.region.Region;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
@ConfigurationProperties(prefix = "cos.client")
@Data
public class CosClientConfig {/*** 域名*/private String host;/*** secretId*/private String secretId;/*** 密钥 (注意不要泄露)*/private String secretKey;/*** 桶名*/private String bucket;/*** 区域*/private String region;@Beanpublic COSClient cosClient() {// 1 初始化用户身份信息(secretId, secretKey)。COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);// 2 设置 bucket 的地域, COS 地域的简称请参见 https://cloud.tencent.com/document/product/436/6224ClientConfig clientConfig = new ClientConfig(new Region(region));// 这里建议设置使用 https 协议// 从 5.6.54 版本开始,默认使用了 httpsclientConfig.setHttpProtocol(HttpProtocol.https);// 3 生成 cos 客户端。return new COSClient(cred, clientConfig);}}

1.3. 对象存储配置

application-local.yml

# 对象存储配置
cos:client:host: xxxxxxxxxxxxxxxxxxxxxx.cos.ap-nanjing.myqcloud.comsecretId: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxsecretKey: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxregion: ap-nanjingbucket: xxxxxxxxxxxxxxxx

主配置文件中启用本地配置:

application.yml

server:port: 8123spring:profiles:active: local

1.4. 对象存储管理器

CosManager

package com.yu.cloudpicturebackend.manager;import com.qcloud.cos.COSClient;
import com.qcloud.cos.exception.CosClientException;
import com.qcloud.cos.exception.CosServiceException;
import com.qcloud.cos.model.PutObjectRequest;
import com.qcloud.cos.model.PutObjectResult;
import com.yu.cloudpicturebackend.config.CosClientConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.io.File;/*** 对象存储管理器-封装通用的对象存储方法*/
@Component
@Slf4j
public class CosManager {@Resourceprivate CosClientConfig cosClientConfig;@Resourceprivate COSClient cosClient;// 将本地文件上传到 COSpublic PutObjectResult putObject(String key, File file) {PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key, file);return cosClient.putObject(putObjectRequest);}}

1.5. 上传图片测试接口

/*** 测试文件上传** @param multipartFile* @return*/
@PostMapping("/test/upload")
public BaseResponse<String> testUploadFile(@RequestPart("file") MultipartFile multipartFile) {String filename = multipartFile.getOriginalFilename();String filePath = String.format("/test/%s", filename);File file = null;try {// 创建空的临时文件file = File.createTempFile(filePath, null);// 将上传文件内容传输到临时文件multipartFile.transferTo(file);// 上传到COS(可能是腾讯云对象存储)cosManager.putObject(filePath, file);// 返回可访问地址return ResultUtils.success(filePath);} catch (Exception e) {log.error("file upload error,filePath" + filePath, e);throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传失败");} finally {if (file != null) {// 删除临时文件boolean delete = file.delete();if (!delete) {log.error("file delete error,filePath={}", filePath);}}}
}

1.6. 业务代码

FilePictureUpload

package com.yu.cloudpicturebackend.manager.upload;import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.RandomUtil;
import com.yu.cloudpicturebackend.config.CosClientConfig;
import com.yu.cloudpicturebackend.exception.BusinessException;
import com.yu.cloudpicturebackend.exception.ErrorCode;
import com.yu.cloudpicturebackend.exception.ThrowUtils;
import com.yu.cloudpicturebackend.manager.CosManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;import javax.annotation.Resource;
import java.io.File;
import java.util.Arrays;
import java.util.Date;
import java.util.List;/*** 图片上传服务*/
@Service
@Slf4j
public class FilePictureUpload {@Resourceprivate CosClientConfig cosClientConfig;@Resourceprivate CosManager cosManager;/*** 上传图片** @param multipartFile    文件* @param uploadPathPrefix 上传路径前缀* @return*/public String uploadAvatar(MultipartFile multipartFile, String uploadPathPrefix) {// 校验文件validatePicture(multipartFile);// 源文件名String uuid = RandomUtil.randomString(12);String originalFilename = multipartFile.getOriginalFilename();// 上传到对象存储的文件名-文件格式:日期_uuid.文件后缀String uploadFileName = String.format("%s_%s.%s", DateUtil.formatDate(new Date()),uuid, FileUtil.getSuffix(originalFilename));// 拼接文件上传路径,而不是使用原始文件名,可以增强安全性String uploadPath = String.format("/%s/%s", uploadPathPrefix, uploadFileName);// 上传文件File file = null;try {// 创建空的临时文件file = File.createTempFile(uploadPath, null);// 将上传文件内容传输到临时文件multipartFile.transferTo(file);// 上传到 coscosManager.putObject(uploadPath, file);// 封装返回结果return cosClientConfig.getHost() + uploadPath;} catch (Exception e) {log.error("图片上传到对象存储失败", e);throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传失败");} finally {// 清理临时文件deleteTempFile(file);}}// 校验图片文件private void validatePicture(MultipartFile multipartFile) {ThrowUtils.throwIf(multipartFile == null, ErrorCode.PARAMS_ERROR, "文件不能为空");// 校验图片文件大小final long ONE_M = 1024 * 1024;ThrowUtils.throwIf(multipartFile.getSize() > 2 * ONE_M, ErrorCode.PARAMS_ERROR, "文件大小不能超过 2MB");// 校验文件后缀String suffix = FileUtil.getSuffix(multipartFile.getOriginalFilename());final List<String> ALLOW_FILE_FORMAT = Arrays.asList("png", "jpg", "jpeg", "gif", "webp");ThrowUtils.throwIf(!ALLOW_FILE_FORMAT.contains(suffix), ErrorCode.PARAMS_ERROR, "文件类型错误");}// 清理临时文件public void deleteTempFile(File file) {if (file == null) {return;}boolean isDelete = file.delete();if (!isDelete) {log.error("临时文件删除失败,文件路径: {}", file.getAbsolutePath());}}}

UserController

/*** @author lianyu* @date 2025-09-28 19:35:01*/
@RestController
@RequestMapping("/user")
@Api(tags = "用户接口")
@Slf4j
public class UserController {@Resourceprivate CosManager cosManager;@Resourceprivate FilePictureUpload filePictureUpload;/*** 上传头像** @param multipartFile 文件* @return*/@PostMapping("/avatar/upload")@ApiOperation("上传用户头像")public BaseResponse<Boolean> uploadAvatar(@RequestParam Long id,@RequestPart("file") MultipartFile multipartFile) {ThrowUtils.throwIf(!saTokenUtils.isLogin(), NOT_LOGIN_ERROR);ThrowUtils.throwIf(id == null, PARAMS_ERROR, "用户 id 不能为空");LoginUserVO loginUser = saTokenUtils.getLoginUser();String userRole = loginUser.getUserRole();// 不是管理员,并且修改的用户头像不是自己的,则无权限if (!userRole.equals(UserRoleEnum.ADMIN.getValue()) && !id.equals(loginUser.getId())) {throw new BusinessException(NO_AUTH_ERROR);}String avatarUrl = filePictureUpload.uploadAvatar(multipartFile, "user");UpdateUserRequest updateUserRequest = new UpdateUserRequest();updateUserRequest.setId(id);updateUserRequest.setUserAvatar(avatarUrl);UpdateWrapper<User> updateWrapper = userService.getUpdateWrapper(updateUserRequest);boolean isUpdated = userService.update(updateWrapper);ThrowUtils.throwIf(!isUpdated, OPERATION_ERROR);return ResultUtils.success(true);}
}

2. 前端

2.1. UserManagePage.vue - 父页面

<template><div class="user-manage-page"><a-table:columns="columns":data-source="dataList":pagination="pagination":loading="loading":scroll="{ x: 1400, y: 460 }"@change="doTableChange"><template #bodyCell="{ column, record }"><template v-if="column.dataIndex === 'userAvatar'"><div class="avatar-box"><div class="avatar-mask" @click="handleUploadAvatarModal(record.id, record.userAvatar)"></div><a-avatar:size="52":src="record.userAvatar ? record.userAvatar : '/src/assets/avatar.png'"></a-avatar></div></template></template></a-table><UploadAvatarModalref="uploadAvatarModalRef":onSuccess="async () => {await fetchData()}"/></div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref, type UnwrapRef } from 'vue'
import {updateUserUsingPost
} from '@/api/yonghujiekou.ts'
import { message } from 'ant-design-vue'const columns = [{title: 'id',dataIndex: 'id',width: '170px',},{title: '用户名',dataIndex: 'userName',width: '140px',},{title: '邮箱',dataIndex: 'email',width: '140px',},{title: '头像',dataIndex: 'userAvatar',width: '90px',},{title: '角色',dataIndex: 'userRole',width: '150px',},{title: '手机号',dataIndex: 'mobile',width: '150px',},{title: '创建时间',dataIndex: 'createTime',},{title: '编辑时间',dataIndex: 'editTime',},{title: '操作',key: 'action',width: '180px',},
]const dataList = ref<API.UserVO[]>([])
const cancel = (key: string) => {delete editableData[key]
}
const uploadAvatarModalRef = ref()// 打开更换头像对话框
const handleUploadAvatarModal = (id: string, avatarUrl: string) => {if (uploadAvatarModalRef.value) {uploadAvatarModalRef.value.openModal(id, avatarUrl)}
}const loading = ref<boolean>(false)import UploadAvatarModal from '@/components/UploadAvatarModal.vue'// 获取数据
const fetchData = async () => {loading.value = trueconst res = await listUserVoUsingPost({...searchParams,})if (res.data.code == 0 && res.data.data) {dataList.value = res.data.data.records ?? []total.value = res.data.data.total ?? 0loading.value = false} else {message.error(res.data.message)}
}onMounted(async () => {await fetchData()
})
</script><style scoped>
.user-manage-page {box-sizing: border-box;.avatar-box {width: 52px;height: 52px;position: relative;.avatar-mask {position: absolute;width: 52px;height: 52px;border-radius: 50%;z-index: 1;text-align: center;line-height: 52px;color: #fff;background-color: rgba(128, 128, 128, 0);transition: all 0.5s ease;}.avatar-mask:hover {cursor: pointer;background-color: rgba(128, 128, 128, 0.8);box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);&:before {content: '更换';}}}
}
</style>

2.2. UploadAvatarModal.vue - 图片预览 + 手动上传

实现图片预览,两种方式

  • 使用 base64 预览图片
  • 使用 Object URL 预览图片

对比:

特性

Base64

Object URL

性能

较差,需要编码整个文件

较好,直接引用文件

内存占用

较高,字符串形式存储

较低,引用形式

使用难度

简单

简单

内存管理

自动回收

需要手动释放

兼容性

很好

很好

2.2.1. base64 预览图片

<template><div><a-uploadv-model:file-list="fileList"name="avatar"list-type="picture-card"class="avatar-uploader":show-upload-list="true":before-upload="beforeUpload"@preview="handlePreview"><div v-if="fileList.length === 0"><loading-outlined v-if="loading"></loading-outlined><plus-outlined v-else></plus-outlined><div class="ant-upload-text">Upload</div></div></a-upload><a-modal:open="previewVisible":title="previewTitle":footer="null"@cancel="handleCancelPreview"><img alt="image" style="width: 100%" :src="previewImage" /></a-modal></div>
</template>
<script setup lang="ts">
import { ref } from 'vue'const fileList = ref([])
const loading = ref<boolean>(false)const previewVisible = ref(false)
const previewImage = ref('')
const previewTitle = ref('')const props = defineProps<Props>()// 这个函数就是将文件转换为 Base64 用于预览
function getBase64(file: File) {return new Promise((resolve, reject) => {const reader = new FileReader()reader.readAsDataURL(file)reader.onload = () => resolve(reader.result)reader.onerror = (error) => reject(error)})
}// 在预览时使用
const handlePreview = async (file: UploadProps['fileList'][number]) => {if (!file.url && !file.preview) {file.preview = (await getBase64(file.originFileObj)) as string}previewImage.value = file.url || file.preview || ''previewVisible.value = truepreviewTitle.value = file.name || file.url.substring(file.url.lastIndexOf('/') + 1)
}const handleCancel = () => {open.value = falsefileList.value = []
}const openModal = (id: string) => {formState.value.id = idopen.value = true
}defineExpose({ openModal })
</script><style scoped></style>

2.2.2. Object URL 预览图片

<template><div><a-uploadv-model:file-list="fileList"name="avatar"list-type="picture-card"class="avatar-uploader":show-upload-list="true":before-upload="beforeUpload"@remove="handleRemove"@preview="handlePreview"><div v-if="fileList.length === 0"><loading-outlined v-if="loading"></loading-outlined><plus-outlined v-else></plus-outlined><div class="ant-upload-text">Upload</div></div></a-upload><a-modal:open="previewVisible":title="previewTitle":footer="null"@cancel="handleCancelPreview"><img alt="image" style="width: 100%" :src="previewImage" /></a-modal></div>
</template>
<script setup lang="ts">
import { ref } from 'vue'const fileList = ref([])
const loading = ref<boolean>(false)const previewVisible = ref(false)
const previewImage = ref('')
const previewTitle = ref('')// 在预览时使用
const handlePreview = async (file: UploadProps['fileList'][number]) => {console.log('文件对象:', file) // 先打印看看文件结构let previewUrl = file.url || file.previewif (!previewUrl && file.originFileObj) {// 如果有 originFileObj,使用它,创建对象URL - 性能更好,不占用内存previewUrl = URL.createObjectURL(file.originFileObj)} else if (!previewUrl && file instanceof File) {// 如果 file 本身就是 File 对象previewUrl = URL.createObjectURL(file)} else if (!previewUrl && file) {// 尝试直接使用 filetry {previewUrl = URL.createObjectURL(file)} catch (error) {console.error('创建预览URL失败:', error)}}previewImage.value = file.url || previewUrl || ''previewVisible.value = truepreviewTitle.value = file.name || file.url.substring(file.url.lastIndexOf('/') + 1)
}const handleCancel = () => {open.value = falsefileList.value = []
}// 记得在组件卸载时释放URL
const handleCancelPreview = () => {// 释放对象URL,避免内存泄漏if (previewImage.value.startsWith('blob:')) {URL.revokeObjectURL(previewImage.value)}previewVisible.value = false
}const openModal = (id: string) => {formState.value.id = idopen.value = true
}defineExpose({ openModal })
</script><style scoped></style>

2.2.3. 子组件的头像回显

父页面中已经传给子组件对应的用户头像,子组件中将头像地址设置到文件列表内中即可回显

<script setup lang="ts">
// ... 其他代码保持不变const openModal = (id: string, avatar: string) => {formState.value.id = idoriginalAvatar.value = avatar// 将原头像添加到文件列表中显示if (avatar) {fileList.value = [{uid: '-1', // 使用负值避免冲突name: '原头像',status: 'done',url: avatar,thumbUrl: avatar}]} else {fileList.value = []}open.value = true
}// 修改 beforeUpload 方法,保留原头像的显示
const beforeUpload = (file: UploadProps['fileList'][number]) => {const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp', 'image/gif']const isAllowed = allowedTypes.some((type) => file.type.includes(type))if (!isAllowed) {message.error('图片格式错误!')return false}const isLt2M = file.size / 1024 / 1024 < 2if (!isLt2M) {message.error('图片大小不能超过2MB')return false}// 添加上传的文件,保留原头像信息fileList.value = [...fileList.value.filter(item => item.uid === '-1'), // 保留原头像file // 添加新文件]return false
}
</script>

2.2.4. 手动上传

antdv 的 upload 组件支持手动上传,需要在 beforeUpload 中返回 false,阻止自动上传

// 上传文件前的逻辑
const beforeUpload = (file: UploadProps['fileList'][number]) => {const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp', 'image/gif']const isAllowed = allowedTypes.some((type) => file.type.includes(type))if (!isAllowed) {message.error('图片格式错误!')return false}const isLt2M = file.size / 1024 / 1024 < 2if (!isLt2M) {message.error('图片大小不能超过2MB')return false}// 手动添加文件到列表fileList.value = [file] // 如果是单文件上传,只保留一个文件return false // 返回 false 阻止自动上传
}

2.2.5. 调用上传接口

接口(由 openapi 自动生成)

/** 上传用户头像 POST /api/user/avatar/upload */
export async function uploadAvatarUsingPost(// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)params: API.uploadAvatarUsingPOSTParams,body: {},file?: File,options?: { [key: string]: any }
) {const formData = new FormData()if (file) {formData.append('file', file)}Object.keys(body).forEach((ele) => {const item = (body as any)[ele]if (item !== undefined && item !== null) {if (typeof item === 'object' && !(item instanceof File)) {if (item instanceof Array) {item.forEach((f) => formData.append(ele, f || ''))} else {formData.append(ele, new Blob([JSON.stringify(item)], { type: 'application/json' }))}} else {formData.append(ele, item)}}})return request<API.BaseResponseBoolean_>('/api/user/avatar/upload', {method: 'POST',params: {...params,},data: formData,requestType: 'form',...(options || {}),})
}

方案1:直接传递源文件上传文件

// 上传文件
const handleUpload = async () => {if (fileList.value.length === 0) {message.error('请选择图片')return}const file = fileList.value[0]// 调试信息console.log('上传文件信息:', {file: file,originFileObj: file.originFileObj,isFile: file instanceof File,id: formState.value.id})loading.value = truetry {// 方式1:直接传递文件对象(推荐)const res = await uploadAvatarUsingPost({ id: formState.value.id }, // 第一个参数:URL参数{}, // 第二个参数:body参数file.originFileObj || file // 第三个参数:文件对象)if (res.data.code === 0) {message.success('更换成功')handleCancel()props.onSuccess?.()} else {message.error(res.data.message || '上传失败')}} catch (error: any) {console.error('上传错误详情:', error)message.error(error.response?.data?.message || '上传失败,请重试')} finally {loading.value = false}
}

方案2:使用 FormData 上传文件

// 上传文件 - FormData方式
const handleUpload = async () => {if (fileList.value.length === 0) {message.error('请选择图片')return}const file = fileList.value[0]const fileObj = file.originFileObj || file// 创建 FormDataconst formData = new FormData()formData.append('file', fileObj)// 如果有其他参数,也添加到 FormDataif (formState.value.id) {formData.append('id', formState.value.id)}loading.value = truetry {const res = await uploadAvatarUsingPost({}, // URL参数为空formData, // 第二个参数:FormData// 不传第三个文件参数,因为文件已经在 FormData 里了)if (res.data.code === 0) {message.success('更换成功')handleCancel()props.onSuccess?.()} else {message.error(res.data.message || '上传失败')}} catch (error: any) {console.error('上传错误详情:', error)message.error(error.response?.data?.message || '上传失败,请重试')} finally {loading.value = false}
}

2.2.6. 完整代码

<template><div class="reset-password-modal"><a-modaltitle="更换头像":open="open"@ok="handleUpload"@cancel="handleCancel"ok-text="确定"cancel-text="取消":ok-button-props="{ disabled: fileList.length === 0 }"style="width: 200px; min-width: 300px":bodyStyle="{ textAlign: 'center' }"><a-uploadv-model:file-list="fileList"name="avatar"list-type="picture-card"class="avatar-uploader":show-upload-list="true":before-upload="beforeUpload"@remove="handleRemove"@preview="handlePreview"><div v-if="fileList.length === 0"><loading-outlined v-if="loading"></loading-outlined><plus-outlined v-else></plus-outlined><div class="ant-upload-text">Upload</div></div></a-upload></a-modal><a-modal:open="previewVisible":title="previewTitle":footer="null"@cancel="handleCancelPreview"><img alt="image" style="width: 100%" :src="previewImage" /></a-modal></div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { message } from 'ant-design-vue'
import { uploadAvatarUsingPost } from '@/api/yonghujiekou.ts'
import { PlusOutlined, LoadingOutlined } from '@ant-design/icons-vue'import type { UploadProps } from 'ant-design-vue'
import { useLoginUserStore } from '@/stores/loginUserStore.ts'const open = ref(false)interface Props {onSuccess?: () => void
}const formState = ref<API.ResetPasswordRequest>({password: '',
})const fileList = ref([])
const loading = ref<boolean>(false)const previewVisible = ref(false)
const previewImage = ref('')
const previewTitle = ref('')const props = defineProps<Props>()const handleRemove: UploadProps['onRemove'] = (file) => {const index = fileList.value.indexOf(file)const newFileList = fileList.value.slice()newFileList.splice(index, 1)fileList.value = newFileList
}// 这个函数就是将文件转换为 Base64 用于预览
// function getBase64(file: File) {
//   return new Promise((resolve, reject) => {
//     const reader = new FileReader()
//     reader.readAsDataURL(file)
//     reader.onload = () => resolve(reader.result)
//     reader.onerror = (error) => reject(error)
//   })
// }// 在预览时使用
const handlePreview = async (file: UploadProps['fileList'][number]) => {// if (!file.url && !file.preview) {//   file.preview = (await getBase64(file.originFileObj)) as string// }console.log('文件对象:', file) // 先打印看看文件结构let previewUrl = file.url || file.previewif (!previewUrl && file.originFileObj) {// 如果有 originFileObj,使用它,创建对象URL - 性能更好,不占用内存previewUrl = URL.createObjectURL(file.originFileObj)} else if (!previewUrl && file instanceof File) {// 如果 file 本身就是 File 对象previewUrl = URL.createObjectURL(file)} else if (!previewUrl && file) {// 尝试直接使用 filetry {previewUrl = URL.createObjectURL(file)} catch (error) {console.error('创建预览URL失败:', error)}}previewImage.value = file.url || previewUrl || ''previewVisible.value = truepreviewTitle.value = file.name || file.url.substring(file.url.lastIndexOf('/') + 1)
}// 上传文件前的逻辑
const beforeUpload = (file: UploadProps['fileList'][number]) => {const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp', 'image/gif']const isAllowed = allowedTypes.some((type) => file.type.includes(type))if (!isAllowed) {message.error('图片格式错误!')return false}const isLt2M = file.size / 1024 / 1024 < 2if (!isLt2M) {message.error('图片大小不能超过2MB')return false}// 手动添加文件到列表fileList.value = [...fileList.value.filter((item) => item.uid === '-1'), // 保留原头像file,] // 如果是单文件上传,只保留一个文件return false // 返回 false 阻止自动上传
}const loginUserStore = useLoginUserStore()// 上传文件
const handleUpload = async () => {if (fileList.value.length === 0) {message.error('请选择图片')return}const file = fileList.value[0]try {// 方式1:直接传递文件对象(推荐)const res = await uploadAvatarUsingPost({ id: formState.value.id }, // 第一个参数:URL参数{}, // 第二个参数:body参数file.originFileObj || file, // 第三个参数:文件对象)if (res.data.code === 0 && res.data.data) {message.success('更换成功')// 重新获取用户登录信息,保持前端展示用户信息数据最新await loginUserStore.fetchLoginUser()handleCancel()props.onSuccess?.()} else {message.error(res.data.message || '上传失败')}} catch (error: any) {console.error('上传错误详情:', error)message.error(error.response?.data?.message || '上传失败,请重试')} finally {loading.value = false}
}const handleCancel = () => {open.value = falsefileList.value = []
}// 记得在组件卸载时释放URL
const handleCancelPreview = () => {// 释放对象URL,避免内存泄漏if (previewImage.value.startsWith('blob:')) {URL.revokeObjectURL(previewImage.value)}previewVisible.value = false
}const openModal = (id: string, avatarUrl) => {formState.value.id = id// 将原头像添加到文件列表中显示if (avatarUrl) {fileList.value = [{uid: '-1', // 使用负值避免冲突name: '原头像',status: 'done',url: avatarUrl,thumbUrl: avatarUrl,},]} else {fileList.value = []}open.value = true
}defineExpose({ openModal })
</script><style scoped>
.avatar-uploader > .ant-upload {width: 128px;height: 128px;
}.ant-upload-select-picture-card i {font-size: 32px;color: #999;
}.ant-upload-select-picture-card .ant-upload-text {margin-top: 8px;color: #666;
}
</style>

http://www.dtcms.com/a/469369.html

相关文章:

  • 百度智能建站系统深圳网站公司招聘信息
  • STM32单片机:基本定时器应用:PWM 生成(STM32L4xx)
  • 驱动开发-Linux启动
  • 【力扣】hot100系列(三)链表(二)(多解法+时间复杂度分析)
  • 初学者小白复盘14之——指针(3)
  • word和wps下分别设置签名或图片背景透明色的方法
  • 适合户外探险、物流、应急、工业,五款三防智能手机深度解析
  • Java 在 Word 文档中插入图片
  • Python 处理 Word 文档中的批注(添加、删除)
  • 做一个什么网站好软件推广联盟
  • 480元做网站昆明网
  • 使用 openpyxl 生成 excel 折线图
  • Java-idea编辑器中Jar方式打包启动
  • vim 编辑中,临时挂起编辑器进程,返回到终端命令行
  • 基于 Reactor 模式的 HTTP 协议扩展实现
  • 2025 FastExcel在Java的Maven项目的导出和导入,简单易上手,以下为完整示例
  • 做的好点的外贸网站有哪些网站建设实训指导书
  • 【Linux】Centos 8 默认OpenSSH 升级OpenSSH9.8【升级其他OpenSSH版本通用】
  • 【Nginx开荒攻略】深度解析基于域名的虚拟主机配置:从域名解析到实战部署
  • 互联网网站样式坪山建设网站建站
  • 全链路智能运维中的业务影响度评估与资源动态优化机制
  • 微信小程序学习(五)
  • Jmeter接口的负载测试概念
  • Linux-CentOS 7 上安装 MySQL 8.0.43(保姆级教程)
  • 视频分辨率4K,比特率50000kbps,电脑播放时卡顿的原因
  • 使用aspx做电影网站网站建设专用术语
  • Linux内核网络优化:两个网络调优解决方案
  • day7_vite 啊哈哈啊哈哈哈哈哈
  • 化妆品产品的自建网站哟哪些能想到的域名都被注册了
  • 网络协议的零拷贝 和 操作系统的零拷贝异同