【超級詳細】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.pswp.cn/web/64750.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/64750.shtml
英文地址,請注明出處:http://en.pswp.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 中有幾種展示視頻的方式,比如墻體使用視頻材質,還有地面多邊形使用視頻材質,都可以實現視頻功能。 但是隨著攝像頭和無人機的流行,需要視頻和場景深度融合,簡單的實現方式則不能滿足需求。…