feat: support multi-files

This commit is contained in:
复语
2026-05-08 16:43:00 +08:00
parent eaef56f4f9
commit b7c6dc6f6d
4 changed files with 5755 additions and 10 deletions
+5681
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -17,6 +17,7 @@
"axios": "^1.13.2",
"dompurify": "^3.2.6",
"file-saver": "^2.0.5",
"jszip": "3.10.1",
"lru-cache": "^11.1.0",
"lucide-vue-next": "^0.535.0",
"marked": "^16.1.1",
@@ -51,7 +52,6 @@
"tailwindcss": "^3.4.17",
"typescript": "~5.8.3",
"vite": "^7.3.0",
"vite-plugin-vue-devtools": "^8.0.0",
"vue-tsc": "^3.0.4"
}
}
+25 -4
View File
@@ -18,6 +18,7 @@
@change="handleFileUpload"
:accept="acceptedTypes"
:disabled="isUploading"
multiple
/>
<div class="absolute inset-0 w-full h-full" v-if="progress > 0">
<BorderProgressBar :progress="progress" />
@@ -38,7 +39,14 @@
: 'text-gray-600 group-hover:text-indigo-600'
]"
>
<span class="block truncate">
<span v-if="selectedFiles && selectedFiles.length > 1" class="block">
<span
v-for="(f, i) in selectedFiles"
:key="i"
class="block truncate"
>{{ f.name }}</span>
</span>
<span v-else class="block truncate">
{{ displayText }}
</span>
</p>
@@ -87,6 +95,7 @@ type UploadStatusType = 'idle' | 'uploading' | 'success' | 'error' | 'initializi
interface Props {
selectedFile?: File | null
selectedFiles?: File[]
progress?: number
placeholder?: string
description?: string
@@ -109,12 +118,14 @@ interface Props {
interface Emits {
fileSelected: [file: File]
filesSelected: [files: File[]]
fileDrop: [event: DragEvent]
retry: []
}
const props = withDefaults(defineProps<Props>(), {
selectedFile: null,
selectedFiles: () => [],
progress: 0,
placeholder: '',
description: '',
@@ -146,6 +157,12 @@ const hasError = computed(() => props.uploadStatus === 'error')
const isSuccess = computed(() => props.uploadStatus === 'success')
const displayText = computed(() => {
if (props.selectedFiles && props.selectedFiles.length === 1) {
return props.selectedFiles[0].name
}
if (props.selectedFiles && props.selectedFiles.length > 1) {
return `已选择 ${props.selectedFiles.length} 个文件`
}
if (props.selectedFile) {
return props.selectedFile.name
}
@@ -229,9 +246,13 @@ const triggerFileUpload = () => {
const handleFileUpload = (event: Event) => {
const target = event.target as HTMLInputElement
const file = target.files?.[0]
if (file) {
emit('fileSelected', file)
const files = target.files
if (files && files.length > 0) {
if (files.length === 1) {
emit('fileSelected', files[0])
} else {
emit('filesSelected', Array.from(files))
}
}
// 重置 input 值,允许选择同名文件
target.value = ''
+48 -5
View File
@@ -23,9 +23,11 @@
<div v-if="sendType === 'file'" key="file" class="grid grid-cols-1 gap-8">
<FileUploadArea
:selected-file="selectedFile"
:selected-files="selectedFiles"
:progress="uploadProgress"
:description="`支持各种常见格式,最大${getStorageUnit(config.uploadSize)}`"
@file-selected="handleFileSelected"
@files-selected="handleFilesSelected"
@file-drop="handleFileDrop"
/>
</div>
@@ -522,6 +524,7 @@ import {
} from 'lucide-vue-next'
import { useRouter } from 'vue-router'
import QRCode from 'qrcode.vue'
import JSZip from 'jszip'
import { useFileDataStore } from '@/stores/fileData'
import { useAlertStore } from '@/stores/alertStore'
import api from '@/utils/api'
@@ -561,6 +564,7 @@ const fileDataStore = useFileDataStore()
const sendType = ref('file')
const selectedFile = ref<File | null>(null)
const selectedFiles = ref<File[]>([])
const textContent = ref('')
const expirationMethod = ref(config.expireStyle?.[0] || 'day')
@@ -586,17 +590,47 @@ const fileHash = ref('')
const handleFileSelected = async (file: File) => {
selectedFile.value = file
selectedFiles.value = []
if (!checkOpenUpload()) return
if (!checkFileSize(file)) return
fileHash.value = await calculateFileHash(file)
}
const handleFilesSelected = async (files: File[]) => {
if (!checkOpenUpload()) return
selectedFiles.value = files
selectedFile.value = null
fileHash.value = ''
}
const packFilesAsZip = async (files: File[]): Promise<File> => {
const zip = new JSZip()
for (const file of files) {
zip.file(file.name, file)
}
const blob = await zip.generateAsync({
type: 'blob',
compression: 'DEFLATE',
compressionOptions: { level: 6 }
})
const zipName = `files_${Date.now()}.zip`
return new File([blob], zipName, { type: 'application/zip' })
}
const handleFileDrop = async (event: DragEvent) => {
if (event.dataTransfer?.files && event.dataTransfer.files.length > 0) {
const file = event.dataTransfer.files[0]
if (!event.dataTransfer?.files || event.dataTransfer.files.length === 0) return
const files = Array.from(event.dataTransfer.files)
if (files.length === 1) {
const file = files[0]
selectedFile.value = file
selectedFiles.value = []
if (!checkUpload()) return
fileHash.value = await calculateFileHash(file)
} else {
if (!checkOpenUpload()) return
selectedFiles.value = files
selectedFile.value = null
fileHash.value = ''
}
}
@@ -1006,7 +1040,7 @@ const handleSubmit = async () => {
isSubmitting.value = true
try {
if (sendType.value === 'file' && !selectedFile.value) {
if (sendType.value === 'file' && !selectedFile.value && selectedFiles.value.length === 0) {
alertStore.showAlert(t('send.messages.selectFile'), 'error')
return
}
@@ -1029,10 +1063,18 @@ const handleSubmit = async () => {
let response: ApiResponse
if (sendType.value === 'file') {
// 多文件时先打包成 zip
let fileToUpload = selectedFile.value!
if (selectedFiles.value.length > 0) {
alertStore.showAlert('正在打包文件...', 'success')
fileToUpload = await packFilesAsZip(selectedFiles.value)
if (!checkFileSize(fileToUpload)) return
fileHash.value = await calculateFileHash(fileToUpload)
}
if (config.enableChunk) {
response = await handleChunkUpload(selectedFile.value!)
response = await handleChunkUpload(fileToUpload)
} else {
response = await handlePresignedUpload(selectedFile.value!)
response = await handlePresignedUpload(fileToUpload)
}
} else {
// 文本上传保持不变
@@ -1071,6 +1113,7 @@ const handleSubmit = async () => {
alertStore.showAlert(t('send.messages.sendSuccess', { code: retrieveCode }), 'success')
// 重置表单 - 只重置文件和文本内容,保留过期信息
selectedFile.value = null
selectedFiles.value = []
textContent.value = ''
uploadProgress.value = 0
resetPresignUpload()