feat(frontend): Implement multi-file upload with progress and speed

This commit is contained in:
Deroino
2025-11-13 05:47:31 +08:00
parent 0cdd088d97
commit 6b9fdb35d1
3 changed files with 127 additions and 49 deletions
+2 -2
View File
@@ -8,9 +8,9 @@ export const materialApi = {
}, },
// 上传素材 // 上传素材
uploadMaterial: (formData) => { uploadMaterial: (formData, onUploadProgress) => {
// 使用http.upload方法,它已经配置了正确的Content-Type // 使用http.upload方法,它已经配置了正确的Content-Type
return http.upload('/uploadSave', formData) return http.upload('/uploadSave', formData, onUploadProgress)
}, },
// 删除素材 // 删除素材
+3 -2
View File
@@ -87,11 +87,12 @@ export const http = {
return request.delete(url, { params }) return request.delete(url, { params })
}, },
upload(url, formData) { upload(url, formData, onUploadProgress) {
return request.post(url, formData, { return request.post(url, formData, {
headers: { headers: {
'Content-Type': 'multipart/form-data' 'Content-Type': 'multipart/form-data'
} },
onUploadProgress
}) })
} }
} }
+114 -37
View File
@@ -58,7 +58,8 @@
<el-form-item label="文件名称:"> <el-form-item label="文件名称:">
<el-input <el-input
v-model="customFilename" v-model="customFilename"
placeholder="选填" placeholder="选填 (仅单个文件时生效)"
:disabled="customFilenameDisabled"
clearable clearable
/> />
</el-form-item> </el-form-item>
@@ -66,10 +67,11 @@
<el-upload <el-upload
class="upload-demo" class="upload-demo"
drag drag
multiple
:auto-upload="false" :auto-upload="false"
:on-change="handleFileChange" :on-change="handleFileChange"
:on-remove="handleFileRemove"
:file-list="fileList" :file-list="fileList"
:limit="1"
> >
<el-icon class="el-icon--upload"><Upload /></el-icon> <el-icon class="el-icon--upload"><Upload /></el-icon>
<div class="el-upload__text"> <div class="el-upload__text">
@@ -77,11 +79,26 @@
</div> </div>
<template #tip> <template #tip>
<div class="el-upload__tip"> <div class="el-upload__tip">
支持视频、图片等格式文件,只能上传一个文件 支持视频、图片等格式文件,可一次选择多个文件
</div> </div>
</template> </template>
</el-upload> </el-upload>
</el-form-item> </el-form-item>
<el-form-item label="上传列表" v-if="fileList.length > 0">
<div class="upload-file-list">
<div v-for="file in fileList" :key="file.uid" class="upload-file-item">
<span class="file-name">{{ file.name }}</span>
<el-progress
:percentage="uploadProgress[file.uid]?.percentage || 0"
:text-inside="true"
:stroke-width="20"
style="width: 100%; margin-top: 5px;"
>
<span>{{ uploadProgress[file.uid]?.speed || '' }}</span>
</el-progress>
</div>
</div>
</el-form-item>
</el-form> </el-form>
</div> </div>
<template #footer> <template #footer>
@@ -123,7 +140,7 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, computed, onMounted } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import { Refresh, Upload } from '@element-plus/icons-vue' import { Refresh, Upload } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { materialApi } from '@/api/material' import { materialApi } from '@/api/material'
@@ -145,6 +162,17 @@ const currentMaterial = ref(null)
// 文件上传 // 文件上传
const fileList = ref([]) const fileList = ref([])
const customFilename = ref('') const customFilename = ref('')
const customFilenameDisabled = computed(() => fileList.value.length > 1)
const uploadProgress = ref({}); // { [uid]: { percentage: 0, speed: '' } }
watch(fileList, (newList) => {
if (newList.length <= 1) {
// If you want to clear the custom name when going back to single file, uncomment below
// customFilename.value = ''
}
});
// 获取素材列表 // 获取素材列表
const fetchMaterials = async () => { const fetchMaterials = async () => {
@@ -186,6 +214,7 @@ const handleUploadMaterial = () => {
// 清空变量 // 清空变量
fileList.value = [] fileList.value = []
customFilename.value = '' customFilename.value = ''
uploadProgress.value = {};
uploadDialogVisible.value = true uploadDialogVisible.value = true
} }
@@ -193,16 +222,24 @@ const handleUploadMaterial = () => {
const handleUploadDialogClose = () => { const handleUploadDialogClose = () => {
fileList.value = [] fileList.value = []
customFilename.value = '' customFilename.value = ''
uploadProgress.value = {};
} }
// 文件选择变更 // 文件选择变更
const handleFileChange = (file, uploadFileList) => { const handleFileChange = (file, uploadFileList) => {
// 只保留最新选择的文件 fileList.value = uploadFileList;
console.log('选择的文件:', file) const newProgress = {};
if (file.raw) { for (const f of uploadFileList) {
// 确保获取到原始文件对象 newProgress[f.uid] = { percentage: 0, speed: '' };
fileList.value = [file]
} }
uploadProgress.value = newProgress;
}
const handleFileRemove = (file, uploadFileList) => {
fileList.value = uploadFileList;
const newProgress = { ...uploadProgress.value };
delete newProgress[file.uid];
uploadProgress.value = newProgress;
} }
// 提交上传 // 提交上传
@@ -212,45 +249,67 @@ const submitUpload = async () => {
return return
} }
// 确保文件对象存在
const fileObj = fileList.value[0]
if (!fileObj || !fileObj.raw) {
ElMessage.warning('文件对象无效,请重新选择文件')
return
}
isUploading.value = true isUploading.value = true
for (const file of fileList.value) {
try { try {
// 使用FormData进行表单提交 // 确保文件对象存在
const formData = new FormData() if (!file || !file.raw) {
ElMessage.warning(`文件 ${file.name} 对象无效,已跳过`)
// 添加文件,确保使用正确的文件对象 continue
console.log('上传文件对象:', fileObj.raw)
formData.append('file', fileObj.raw)
// 如果用户输入了自定义文件名,则添加到表单中
if (customFilename.value.trim()) {
formData.append('filename', customFilename.value.trim())
console.log('自定义文件名:', customFilename.value.trim())
} }
const response = await materialApi.uploadMaterial(formData) const formData = new FormData()
formData.append('file', file.raw)
// 只有当只有一个文件时,自定义文件名才生效
if (fileList.value.length === 1 && customFilename.value.trim()) {
formData.append('filename', customFilename.value.trim())
}
let lastLoaded = 0;
let lastTime = Date.now();
const response = await materialApi.uploadMaterial(formData, (progressEvent) => {
const progressData = uploadProgress.value[file.uid];
if (!progressData) return;
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
progressData.percentage = progress;
const currentTime = Date.now();
const timeDiff = (currentTime - lastTime) / 1000; // in seconds
const loadedDiff = progressEvent.loaded - lastLoaded;
if (timeDiff > 0.5) { // Update speed every 0.5 seconds
const speed = loadedDiff / timeDiff; // bytes per second
if (speed > 1024 * 1024) {
progressData.speed = (speed / (1024 * 1024)).toFixed(2) + ' MB/s';
} else {
progressData.speed = (speed / 1024).toFixed(2) + ' KB/s';
}
lastLoaded = progressEvent.loaded;
lastTime = currentTime;
}
})
if (response.code === 200) { if (response.code === 200) {
ElMessage.success('上传成功') ElMessage.success(`文件 ${file.name} 上传成功`)
uploadDialogVisible.value = false const progressData = uploadProgress.value[file.uid];
// 上传成功后直接刷新素材列表 if(progressData) progressData.speed = '完成';
await fetchMaterials()
} else { } else {
ElMessage.error(response.msg || '上传失败') ElMessage.error(`文件 ${file.name} 上传失败: ${response.msg || '未知错误'}`)
} }
} catch (error) { } catch (error) {
console.error('上传素材出错:', error) console.error(`上传文件 ${file.name} 出错:`, error)
ElMessage.error('上传失败: ' + (error.message || '未知错误')) ElMessage.error(`文件 ${file.name} 上传失败: ${error.message || '未知错误'}`)
} finally {
isUploading.value = false
} }
}
isUploading.value = false
// Keep dialog open to show results
// uploadDialogVisible.value = false
await fetchMaterials()
} }
// 预览素材 // 预览素材
@@ -430,6 +489,24 @@ onMounted(() => {
justify-content: flex-end; justify-content: flex-end;
} }
.upload-file-list {
width: 100%;
}
.upload-file-item {
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
}
.upload-file-item .file-name {
font-size: 14px;
color: #606266;
margin-bottom: 5px;
display: block;
}
/* 覆盖Element Plus对话框样式 */ /* 覆盖Element Plus对话框样式 */
:deep(.el-dialog__body) { :deep(.el-dialog__body) {
padding: 20px 0; padding: 20px 0;