feat(frontend): Implement multi-file upload with progress and speed
This commit is contained in:
@@ -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)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 删除素材
|
// 删除素材
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,47 +249,69 @@ 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()
|
||||||
|
}
|
||||||
|
|
||||||
// 预览素材
|
// 预览素材
|
||||||
const handlePreview = async (material) => {
|
const handlePreview = async (material) => {
|
||||||
currentMaterial.value = null
|
currentMaterial.value = null
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user