【超级详细】Vue3项目上传文件到七牛云的详细笔记

概述

继上一篇笔记介绍如何绑定七牛云的域名之后,这篇笔记主要介绍了如何在Vue3项目中实现文件上传至七牛云的功能。我们将使用Cropper.js来处理图像裁剪,并通过自定义组件和API调用来完成整个流程。


这里直接给出关键部分js代码,上传之前要先获取一个上传凭证,这里是由后端接口生成的具体的实现代码在后文有体现,这里实现的功能逻辑为:将上传的图片转化为base64数据之后调用uploadToQiniu 方法。

uploadToQiniu 方法实现为:

  • 将base64的数据解码出数据类型或直接使用默认类型
  • 调用父组件的props.nickname设置文件名称这一步可以自定义实现
  • 调用useFileApi().upload() 接口获取上传凭证以及空间域名用于拼接图片访问url
  • 将base64数据转为二进制数据
  • 动态导入qiniu-js (PS:这里用动态导入的原因是直接导入会导致父组件加载子组件的时候出现问题)
  • 调用Qiniu.upload方法上传文件
  • 跟踪上传进度这里用一个message弹框直接显示
const uploadToQiniu = async (base64Image) => {// 提取Base64编码中的图片类型const base64Data = base64Image.split(',')[0];const typeMatch = base64Data.match(/data:(.+);base64,/);const type = typeMatch ? typeMatch[1] : 'jpg';const nickname = (props.nickname || '匿名用户').replace(/[\\/:*?"<>|]/g, '').trim().substring(0, 50);const fileName = `${nickname}-avatar-${Date.now()}.${ type }`;const result = await useFileApi().upload(fileName);if (!result || !result.data) {ElMessage.error('未能获取上传凭证');return;}const { token, domain } = result.data;// 将base64转换为二进制数据const base64 = base64Image.split(',')[1];const byteCharacters = atob(base64);const byteNumbers = new Array(byteCharacters.length);for (let i = 0; i < byteCharacters.length; i++) {byteNumbers[i] = byteCharacters.charCodeAt(i);}const byteArray = new Uint8Array(byteNumbers);const file = new File([byteArray], fileName, { type: type });// 动态导入qiniu-jsconst Qiniu = await import("qiniu-js");const config = {useCdnDomain: true,region: 'z2',domain: domain,     //配置好的七牛云域名chunkSize: 100,     //每个分片的大小,单位mb,默认值3forceDirect: true   //直传还是断点续传方式,true为直传};const putExtra = {fname: '',params: {},mimeType: type,};let uploadMessage = null; // 用于跟踪进度消息return new Promise((resolve, reject) => {const observable = Qiniu.upload(file, fileName, token, putExtra, config);const observer = {next(res) {if (uploadMessage === null) { // 确保只显示一次进度消息uploadMessage = ElMessage({message: '上传中...',duration: 0,type: 'success',onClose: () => {uploadMessage = null;}});}// 更新消息内容以显示进度,这里假设res.total === 100表示100%if (res.total === 100) {uploadMessage.close();}},error(err) {if (uploadMessage) {uploadMessage.close(); // 关闭进度消息}ElMessage.error('上传失败: ' + err.message);reject(err);},complete(res) {if (res.key) {if (uploadMessage) {uploadMessage.close();}resolve(`http://${domain}/${res.key}`);} else {if (uploadMessage) {uploadMessage.close();}reject(new Error('上传成功,但未获取到文件key或域名'));}},};observable.subscribe(observer);});
};

下面是一个完整的前后端部分代码

技术栈

  • 前端框架: Vue3
  • 状态管理: Pinia
  • UI库: Element Plus
  • 图像裁剪工具: Cropper.js
  • 后端语言: Python (FastAPI)
  • 存储服务: 七牛云

实现步骤

1. 安装依赖

首先,确保你已经安装了必要的npm包:

npm install cropperjs element-plus pinia qiniu-js

2. 创建用户信息组件

创建一个名为Personal.vue的组件,用于展示和编辑用户信息。

前端代码比较多这里这展示关键部分js代码 源码在文末

<!--  设置头像组件 通过updateAvatar传递到父组件 通过nickname传递到子组件  -->
<SeePictures ref="SeePicturesRef"@updateAvatar="updateAvatar":nickname="state.personalForm.nickname"
></SeePictures>const SeePictures = defineAsyncComponent(() => import("/@/components/seePictures/index.vue"))
const SeePicturesRef = ref();// 打开裁剪弹窗
const onCropperDialogOpen = () => {nextTick(() => {SeePicturesRef.value.openDialog(state.personalForm.avatar);});
};
const save = async () => {// 保存用户信息await useUserApi().saveOrUpdate(state.personalForm)await getUserInfo()ElMessage.success("更新成功!╰(*°▽°*)╯😍")if (state.showEditPage){state.showEditPage = !state.showEditPage}
}
const updateAvatar = async (img) => {state.personalForm.avatar = img// 跟新session中用户信息await userStores.updateAvatar(img)save()
}

3. 创建图片查看与裁剪组件

创建一个名为SeePictures.vue的组件,用于显示和裁剪图片。

前端代码比较多这里这展示关键部分js代码 源码在文末

const onSubmit = async () => {state.cropperImgBase64 = state.cropper.getCroppedCanvas().toDataURL('image/jpeg');try {const qiniuResult = await uploadToQiniu(state.cropperImgBase64);if (qiniuResult) {emit("updateAvatar", qiniuResult);closeDialog();}} catch (error) {console.error('上传到七牛云失败:', error);}// emit("updateAvatar", state.cropperImgBase64)// closeDialog();
};// 上传到七牛云
const uploadToQiniu = async (base64Image) => {// 提取Base64编码中的图片类型const base64Data = base64Image.split(',')[0];const typeMatch = base64Data.match(/data:(.+);base64,/);const type = typeMatch ? typeMatch[1] : 'jpg';const nickname = (props.nickname || '匿名用户').replace(/[\\/:*?"<>|]/g, '').trim().substring(0, 50);const fileName = `${nickname}-avatar-${Date.now()}.${ type }`;const result = await useFileApi().upload(fileName);if (!result || !result.data) {ElMessage.error('未能获取上传凭证');return;}const { token, domain } = result.data;// 将base64转换为二进制数据const base64 = base64Image.split(',')[1];const byteCharacters = atob(base64);const byteNumbers = new Array(byteCharacters.length);for (let i = 0; i < byteCharacters.length; i++) {byteNumbers[i] = byteCharacters.charCodeAt(i);}const byteArray = new Uint8Array(byteNumbers);const file = new File([byteArray], fileName, { type: type });// 动态导入qiniu-jsconst Qiniu = await import("qiniu-js");const config = {useCdnDomain: true,region: 'z2',domain: domain,     //配置好的七牛云域名chunkSize: 100,     //每个分片的大小,单位mb,默认值3forceDirect: true   //直传还是断点续传方式,true为直传};const putExtra = {fname: '',params: {},mimeType: type,};let uploadMessage = null; // 用于跟踪进度消息return new Promise((resolve, reject) => {const observable = Qiniu.upload(file, fileName, token, putExtra, config);const observer = {next(res) {if (uploadMessage === null) { // 确保只显示一次进度消息uploadMessage = ElMessage({message: '上传中...',duration: 0,type: 'success',onClose: () => {uploadMessage = null;}});}// 更新消息内容以显示进度,这里假设res.total === 100表示100%if (res.total === 100) {uploadMessage.close();}},error(err) {if (uploadMessage) {uploadMessage.close(); // 关闭进度消息}ElMessage.error('上传失败: ' + err.message);reject(err);},complete(res) {if (res.key) {if (uploadMessage) {uploadMessage.close();}resolve(`http://${domain}/${res.key}`);} else {if (uploadMessage) {uploadMessage.close();}reject(new Error('上传成功,但未获取到文件key或域名'));}},};observable.subscribe(observer);});
};

4. 前端接口代码

在你的API模块中添加上传文件的方法。

import request from '/@/utils/request';/*** 文件接口*/
export function useFileApi() {return {upload: (data) => {return request({url: '/file/qiniu/token',method: 'POST',data,});}};
}

5. 后端接口代码

编写FastAPI后端接口以获取七牛云上传凭证并处理文件上传。

@router.post('/qiniu/token', description="获取文件上传凭证")
async def upload(file_name: str = Body(...)):result = await FileService.upload(file_name)return partner_success(result)
class FileService:@staticmethodasync def upload(file_name: str) -> dict:""" 生成七牛云上传凭证 """try:logger.info(f'生成七牛云上传凭证....')result = upload_file(file_name)logger.info(f"七牛云上传凭证生成成功--> {result['token'][:12] + '*'*20}")return resultexcept Exception as e:logger.error(f'七牛云上传凭证生成失败: {e}')raise ParameterError(CodeEnum.FILE_UPLOAD_FAILED)
# 上传文件类
class UploadQiNiu:# 仅支持单文件上传def __init__(self, config):self.access_key = config['access_key']self.secret_key = config['secret_key']self.bucket_name = config['bucket_name']self.domain = config['domain']self._q = Auth(self.access_key, self.secret_key)self._bucket = BucketManager(self._q)def get_qiniu_upload_token(self, save_file_name):""" 获取七牛云上传凭证 """return self._q.upload_token(self.bucket_name, save_file_name)
""" 七牛云配置 """
qiniu_config = {'access_key': config.access_key,    # access_key'secret_key': config.secret_key,    # secret_key'bucket_name': config.bucket_name,  # 存储空间名称'domain': config.domain             # 空间域名
}# 获取七牛云上传凭证
def upload_file(save_file_name: str) -> dict:try:upload_qiniu = UploadQiNiu(qiniu_config)token = upload_qiniu.get_qiniu_upload_token(save_file_name)return {'token': token,'domain': qiniu_config['domain']}except Exception as e:logger.warning(f'获取七牛云上传凭证失败: {e}')raise ParameterError(CodeEnum.FILE_UPLOAD_FAILED)
Personal.vue 组件代码
<template><div><!-- 设置头像组件 --><SeePictures ref="SeePicturesRef" @updateAvatar="updateAvatar" :nickname="state.personalForm.nickname"></SeePictures><!-- 其他表单项 --><el-form label-width="100px"><el-form-item label="用户名"><el-input v-model="state.personalForm.username" disabled></el-input></el-form-item><el-form-item label="昵称"><el-input v-model="state.personalForm.nickname"></el-input></el-form-item><el-form-item label="邮箱"><el-input v-model="state.personalForm.email"></el-input></el-form-item><el-form-item label="标签"><el-tag v-for="(tag, index) in state.personalForm.tags" :key="index" closable @close="removeTag(tag)">{{ tag }}</el-tag><el-input v-if="state.editTag" ref="UserTagInputRef" v-model="state.tagValue" size="small" style="width: 100px;"@keyup.enter.native="addTag" @blur="addTag"></el-input><el-button v-else class="button-new-tag ml-1" size="small" @click="showEditTag">+ New Tag</el-button></el-form-item><el-form-item><el-button type="primary" @click="save">保存</el-button></el-form-item></el-form></div>
</template><script setup name="personal">
import { defineAsyncComponent, nextTick, onMounted, reactive, ref } from 'vue';
import { useUserInfo } from "/@/stores/userInfo";
import { useUserApi } from "/@/api/useSystemApi/user";
import { ElMessage } from "element-plus";
import { storeToRefs } from "pinia";const SeePictures = defineAsyncComponent(() => import("/@/components/seePictures/index.vue"));
const SeePicturesRef = ref();
const UserTagInputRef = ref();// 用户信息
const userStores = useUserInfo();
const { userInfos } = storeToRefs(userStores);// 定义变量内容
const state = reactive({personalForm: {username: '',nickname: '',avatar: '',email: '',tags: '',},editTag: false,tagValue: "",cropperImg: '',
});const getUserInfo = async () => {let { data } = await useUserApi().getUserInfoByToken();if (!data.avatar) {data.avatar = "";}state.personalForm = data;
};// 打开裁剪弹窗
const onCropperDialogOpen = () => {nextTick(() => {SeePicturesRef.value.openDialog(state.personalForm.avatar);});
};// tags
const showEditTag = () => {state.editTag = true;nextTick(() => {UserTagInputRef.value?.input.focus();});
};const removeTag = (tag) => {state.personalForm.tags.splice(state.personalForm.tags.indexOf(tag), 1);
};const addTag = () => {if (state.editTag && state.tagValue) {if (!state.personalForm.tags) state.personalForm.tags = [];state.personalForm.tags.push(state.tagValue);}state.editTag = false;state.tagValue = '';
};const save = async () => {// 保存用户信息await useUserApi().saveOrUpdate(state.personalForm);await getUserInfo();ElMessage.success("更新成功!╰(*°▽°*)╯😍");
};const updateAvatar = async (img) => {state.personalForm.avatar = img;// 更新session中用户信息await userStores.updateAvatar(img);save();
};onMounted(() => {getUserInfo();
});
</script><style scoped>
.button-new-tag {margin-left: 10px;height: 32px;line-height: 30px;padding-top: 0;padding-bottom: 0;
}
</style>
<template><div><el-dialog title="裁剪图片" v-model="dialogVisible" width="80%"><div class="cropper-content"><img id="image" :src="imageUrl" alt="Source Image" /></div><template #footer><span class="dialog-footer"><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" @click="cropImage">确定</el-button></span></template></el-dialog></div>
</template><script setup>
import { ref, watch } from 'vue';
import Cropper from 'cropperjs';
import 'cropperjs/dist/cropper.min.css';const props = defineProps({nickname: String,
});const emit = defineEmits(['updateAvatar']);let imageUrl = ref('');
let dialogVisible = ref(false);
let cropper = null;watch(dialogVisible, (val) => {if (!val) {cropper.destroy();}
});const openDialog = (url) => {imageUrl.value = url;dialogVisible.value = true;nextTick(() => {cropper = new Cropper(document.getElementById('image'), {aspectRatio: 1 / 1,viewMode: 1,autoCropArea: 1,cropBoxResizable: false,});});
};const cropImage = () => {cropper.getCroppedCanvas().toBlob(async (blob) => {const formData = new FormData();formData.append('file', blob, `${props.nickname}_avatar.png`);try {const response = await fetch('/file/qiniu/upload', {method: 'POST',body: formData,});const result = await response.json();emit('updateAvatar', result.url);} catch (error) {console.error(error);}}, 'image/png');
};
</script><style scoped>
.cropper-content {max-height: 50vh;overflow-y: auto;
}#image {display: block;width: 100%;
}
</style>

总结

通过以上步骤,我们实现了从Vue3前端发起请求、获取七牛云上传凭证、上传文件到七牛云的过程。整个过程包括了用户信息展示、图片裁剪、文件上传等功能,适用于大多数需要文件上传功能的场景。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/64750.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Sqoop的使用

每个人的生活都是一个世界&#xff0c;即使最平凡的人也要为他那个世界的存在而战斗。 ——《平凡的世界》 目录 一、sqoop简介 1.1 导入流程 1.2 导出流程 二、使用sqoop 2.1 sqoop的常用参数 2.2 连接参数列表 2.3 操作hive表参数 2.4 其它参数 三、sqoop应用 - 导入…

FFmpeg 4.3 音视频-多路H265监控录放C++开发二十一.4,SDP协议分析

SDP在4566 中有详细描述。 SDP 全称是 Session Description Protocol&#xff0c; 翻译过来就是描述会话的协议。 主要用于两个会话实体之间的媒体协商。 什么叫会话呢&#xff0c;比如一次网络电话、一次电话会议、一次视频聊天&#xff0c;这些都可以称之为一次会话。 那为什…

智简未来创新与简化的AI之路

附上链接地址&#xff1a;https://aint.top 在这个数字化迅速发展的时代&#xff0c;人工智能&#xff08;AI&#xff09;不仅仅是技术的前沿&#xff0c;它正在成为每个行业创新的核心推动力。作为一家专注于AI技术应用与创新的公司&#xff0c;智简未来旨在通过智能化的工具…

[极客大挑战 2019]HardSQL 1

看了大佬的wp&#xff0c;没用字典爆破&#xff0c;手动试出来的&#xff0c;屏蔽了常用的关键字&#xff0c;例如&#xff1a;order select union and 最搞的是&#xff0c;空格也有&#xff0c;这个空格后面让我看了好久&#xff0c;该在哪里加括号。 先传入1’ 1试试&#…

【Pytorch实用教程】深入了解 torchvision.models.resnet18 新旧版本的区别

深入了解 torchvision.models.resnet18 新旧版本的区别 在深度学习模型开发中,PyTorch 和 torchvision 一直是我们不可或缺的工具。近期,torchvision 对其模型加载 API 进行了更新,将旧版的 pretrained 参数替换为新的 weights 参数。本文将介绍这一变化的背景、具体区别,…

Elasticsearch名词解释

文章目录 1.什么是Elasticsearch?2.什么是elastic stack(ELK)?3.什么是Lucene?4.什么是文档(document)&#xff1f;5.什么是词条(term)&#xff1f;6.什么是正向索引&#xff1f;7.什么是倒排索引&#xff1f;8.ES中的索引(index)9.映射(Mapping)10.DSL11.elastcisearch与my…

网络渗透测试实验三:SQL注入

1.实验目的和要求 实验目的:了解SQL注入的基本原理;掌握PHP脚本访问MySQL数据库的基本方法;掌握程序设计中避免出现SQL注入漏洞的基本方法;掌握网站配置。 系统环境:Kali Linux 2、Windows Server 网络环境:交换网络结构 实验工具: SqlMAP;DVWA 2.实验步骤 实验目…

SQL-Server链接服务器访问Oracle数据

SQL Server 链接服务器访问 Oracle 离线安装 .NET Framework 3.5 方法一&#xff1a;使用 NetFx3.cab 文件 下载 NetFx3.cab 文件&#xff0c;并将其放置在 Windows 10 系统盘的 C:Windows 文件夹中。 以管理员身份运行命令提示符&#xff0c;输入以下命令并回车&#xff1a; …

【R语言】校准曲线,绘制原理

①获取predict的结果&#xff0c;“prob.Case”这一列就是预测风险概率&#xff0c;“truth”列为实际发生结局的分组 ②将prob.Case进行分桶&#xff08;简单理解为分组&#xff0c;一般分10组)&#xff0c;常见的分桶方式有两种&#xff1a;一是将prob.Case从大到小排序后&a…

QTDemo:串口调试工具

项目简介 本项目通过QT框架设计一款可以在Windows、Linux等平台的跨平台串口助手&#xff0c;串口功能能够满足基本的调试需求。 本项目采用的版本为&#xff1a;QT5.14 visual studio 2022 进行开发。 项目源码&#xff1a;https://github.com/say-Hai/MyCOMDemo 项目页面&am…

基于SpringBoot和OAuth2,实现通过Github授权登录应用

基于SpringBoot和OAuth2&#xff0c;实现通过Github授权登录应用 文章目录 基于SpringBoot和OAuth2&#xff0c;实现通过Github授权登录应用0. 引言1. 创建Github应用2. 创建SpringBoot测试项目2.1 初始化项目2.2 设置配置文件信息2.3 创建Controller层2.4 创建Html页面 3. 启动…

CMS漏洞靶场攻略

DeDeCMS 环境搭建 傻瓜式安装 漏洞一&#xff1a;通过文件管理器上传WebShel 步骤⼀:访问目标靶场其思路为 dedecms 后台可以直接上传任意文件&#xff0c;可以通过⽂件管理器上传php文件获取webshell 登陆网站后台 步骤二&#xff1a;登陆到后台点击 【核心】 --》 【文件式…

0xc0000020错误代码怎么处理,Windows11、10坏图像错误0xc0000020的修复办法

“0xc0000020”是一种 Windows 应用程序错误代码&#xff0c;通常表明某些文件缺失或损坏。这可能是由于系统文件损坏、应用程序安装或卸载问题、恶意软件感染、有问题的 Windows 更新等原因导致的。 比如&#xff0c;当运行软件时&#xff0c;可能会出现类似“C:\xx\xxx.dll …

LabVIEW 中 NI Vision 模块的IMAQ Create VI

IMAQ Create VI 是 LabVIEW 中 NI Vision 模块&#xff08;NI Vision Development Module&#xff09;的一个常用 VI&#xff0c;用于创建一个图像变量。该图像变量可以存储和操作图像数据&#xff0c;是图像处理任务的基础。 ​ 通过以上操作&#xff0c;IMAQ Create VI 是构建…

HTML5 标签输入框(Tag Input)详解

HTML5 标签输入框&#xff08;Tag Input&#xff09;详解 标签输入框&#xff08;Tag Input&#xff09;是一种用户界面元素&#xff0c;允许用户输入多个标签或关键词&#xff0c;通常用于表单、搜索框或内容分类等场景。以下是实现标签输入框的详细讲解。 1. 任务概述 标…

使用位操作符实现加减乘除!

欢迎拜访&#xff1a;雾里看山-CSDN博客 本篇主题&#xff1a;使用位操作符实现加减乘除 发布时间&#xff1a;2025.1.1 隶属专栏&#xff1a;C语言 目录 位操作实现加法运算&#xff08;&#xff09;原理代码示例 位操作实现减法运算&#xff08;-&#xff09;原理代码示例 位…

[Spring] Spring AOP

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏: &#x1f9ca; Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 &#x1f355; Collection与…

Java-数据结构-时间和空间复杂度

一、什么是时间和空间复杂度&#xff1f; &#x1f4da; 那么在了解时间复杂度和空间复杂度之前&#xff0c;我们先要知道为何有这两者的概念&#xff1a; 首先我们要先了解"算法"&#xff0c;在之前我们学习过关于"一维前缀和与差分"&#xff0c;"…

商汤C++开发面试题及参考答案

C++11 有哪些新特性? C++11 带来了众多令人瞩目的新特性,极大地丰富和增强了这门编程语言的功能与表现力。 首先是类型推导方面,引入了auto关键字。通过auto,编译器能够自动根据初始化表达式来推导出变量的类型,这在处理复杂的模板类型或者较长的类型声明时非常方便,能让…

Cesium 实战 27 - 三维视频融合(视频投影)

Cesium 实战 27 - 三维视频融合(视频投影) 核心代码完整代码在线示例在 Cesium 中有几种展示视频的方式,比如墙体使用视频材质,还有地面多边形使用视频材质,都可以实现视频功能。 但是随着摄像头和无人机的流行,需要视频和场景深度融合,简单的实现方式则不能满足需求。…