Merge pull request #10 from DuranceX/dev

支持批量上传文件
This commit is contained in:
Lan
2026-05-17 01:05:23 +08:00
committed by GitHub
6 changed files with 5760 additions and 13 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", "axios": "^1.13.2",
"dompurify": "^3.2.6", "dompurify": "^3.2.6",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jszip": "3.10.1",
"lru-cache": "^11.1.0", "lru-cache": "^11.1.0",
"lucide-vue-next": "^0.535.0", "lucide-vue-next": "^0.535.0",
"marked": "^16.1.1", "marked": "^16.1.1",
@@ -51,7 +52,6 @@
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"vite": "^7.3.0", "vite": "^7.3.0",
"vite-plugin-vue-devtools": "^8.0.0",
"vue-tsc": "^3.0.4" "vue-tsc": "^3.0.4"
} }
} }
+1 -1
View File
@@ -69,7 +69,7 @@ const alertIcons = {
info: Info info: Info
} }
let intervalId: number let intervalId: ReturnType<typeof setInterval>
onMounted(() => { onMounted(() => {
intervalId = setInterval(() => { intervalId = setInterval(() => {
+25 -4
View File
@@ -18,6 +18,7 @@
@change="handleFileUpload" @change="handleFileUpload"
:accept="acceptedTypes" :accept="acceptedTypes"
:disabled="isUploading" :disabled="isUploading"
multiple
/> />
<div class="absolute inset-0 w-full h-full" v-if="progress > 0"> <div class="absolute inset-0 w-full h-full" v-if="progress > 0">
<BorderProgressBar :progress="progress" /> <BorderProgressBar :progress="progress" />
@@ -38,7 +39,14 @@
: 'text-gray-600 group-hover:text-indigo-600' : '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 }} {{ displayText }}
</span> </span>
</p> </p>
@@ -87,6 +95,7 @@ type UploadStatusType = 'idle' | 'uploading' | 'success' | 'error' | 'initializi
interface Props { interface Props {
selectedFile?: File | null selectedFile?: File | null
selectedFiles?: File[]
progress?: number progress?: number
placeholder?: string placeholder?: string
description?: string description?: string
@@ -109,12 +118,14 @@ interface Props {
interface Emits { interface Emits {
fileSelected: [file: File] fileSelected: [file: File]
filesSelected: [files: File[]]
fileDrop: [event: DragEvent] fileDrop: [event: DragEvent]
retry: [] retry: []
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
selectedFile: null, selectedFile: null,
selectedFiles: () => [],
progress: 0, progress: 0,
placeholder: '', placeholder: '',
description: '', description: '',
@@ -146,6 +157,12 @@ const hasError = computed(() => props.uploadStatus === 'error')
const isSuccess = computed(() => props.uploadStatus === 'success') const isSuccess = computed(() => props.uploadStatus === 'success')
const displayText = computed(() => { 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) { if (props.selectedFile) {
return props.selectedFile.name return props.selectedFile.name
} }
@@ -229,9 +246,13 @@ const triggerFileUpload = () => {
const handleFileUpload = (event: Event) => { const handleFileUpload = (event: Event) => {
const target = event.target as HTMLInputElement const target = event.target as HTMLInputElement
const file = target.files?.[0] const files = target.files
if (file) { if (files && files.length > 0) {
emit('fileSelected', file) if (files.length === 1) {
emit('fileSelected', files[0])
} else {
emit('filesSelected', Array.from(files))
}
} }
// 重置 input 值,允许选择同名文件 // 重置 input 值,允许选择同名文件
target.value = '' target.value = ''
+1 -1
View File
@@ -114,7 +114,7 @@ export function debounce<T extends (...args: unknown[]) => unknown>(
func: T, func: T,
wait: number wait: number
): (...args: Parameters<T>) => void { ): (...args: Parameters<T>) => void {
let timeout: number | null = null let timeout: ReturnType<typeof setTimeout> | null = null
return (...args: Parameters<T>) => { return (...args: Parameters<T>) => {
if (timeout) { if (timeout) {
+51 -6
View File
@@ -23,9 +23,11 @@
<div v-if="sendType === 'file'" key="file" class="grid grid-cols-1 gap-8"> <div v-if="sendType === 'file'" key="file" class="grid grid-cols-1 gap-8">
<FileUploadArea <FileUploadArea
:selected-file="selectedFile" :selected-file="selectedFile"
:selected-files="selectedFiles"
:progress="uploadProgress" :progress="uploadProgress"
:description="`支持各种常见格式,最大${getStorageUnit(config.uploadSize)}`" :description="`支持各种常见格式,最大${getStorageUnit(config.uploadSize)}`"
@file-selected="handleFileSelected" @file-selected="handleFileSelected"
@files-selected="handleFilesSelected"
@file-drop="handleFileDrop" @file-drop="handleFileDrop"
/> />
</div> </div>
@@ -522,6 +524,7 @@ import {
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import QRCode from 'qrcode.vue' import QRCode from 'qrcode.vue'
import JSZip from 'jszip'
import { useFileDataStore } from '@/stores/fileData' import { useFileDataStore } from '@/stores/fileData'
import { useAlertStore } from '@/stores/alertStore' import { useAlertStore } from '@/stores/alertStore'
import api from '@/utils/api' import api from '@/utils/api'
@@ -561,6 +564,7 @@ const fileDataStore = useFileDataStore()
const sendType = ref('file') const sendType = ref('file')
const selectedFile = ref<File | null>(null) const selectedFile = ref<File | null>(null)
const selectedFiles = ref<File[]>([])
const textContent = ref('') const textContent = ref('')
const expirationMethod = ref(config.expireStyle?.[0] || 'day') const expirationMethod = ref(config.expireStyle?.[0] || 'day')
@@ -586,17 +590,47 @@ const fileHash = ref('')
const handleFileSelected = async (file: File) => { const handleFileSelected = async (file: File) => {
selectedFile.value = file selectedFile.value = file
selectedFiles.value = []
if (!checkOpenUpload()) return if (!checkOpenUpload()) return
if (!checkFileSize(file)) return if (!checkFileSize(file)) return
fileHash.value = await calculateFileHash(file) 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) => { const handleFileDrop = async (event: DragEvent) => {
if (event.dataTransfer?.files && event.dataTransfer.files.length > 0) { if (!event.dataTransfer?.files || event.dataTransfer.files.length === 0) return
const file = event.dataTransfer.files[0] const files = Array.from(event.dataTransfer.files)
if (files.length === 1) {
const file = files[0]
selectedFile.value = file selectedFile.value = file
selectedFiles.value = []
if (!checkUpload()) return if (!checkUpload()) return
fileHash.value = await calculateFileHash(file) 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 isSubmitting.value = true
try { try {
if (sendType.value === 'file' && !selectedFile.value) { if (sendType.value === 'file' && !selectedFile.value && selectedFiles.value.length === 0) {
alertStore.showAlert(t('send.messages.selectFile'), 'error') alertStore.showAlert(t('send.messages.selectFile'), 'error')
return return
} }
@@ -1029,10 +1063,18 @@ const handleSubmit = async () => {
let response: ApiResponse let response: ApiResponse
if (sendType.value === 'file') { 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) { if (config.enableChunk) {
response = await handleChunkUpload(selectedFile.value!) response = await handleChunkUpload(fileToUpload)
} else { } else {
response = await handlePresignedUpload(selectedFile.value!) response = await handlePresignedUpload(fileToUpload)
} }
} else { } else {
// 文本上传保持不变 // 文本上传保持不变
@@ -1058,7 +1100,9 @@ const handleSubmit = async () => {
size: size:
sendType.value === 'text' sendType.value === 'text'
? `${(textContent.value.length / 1024).toFixed(2)} KB` ? `${(textContent.value.length / 1024).toFixed(2)} KB`
: `${(selectedFile.value!.size / (1024 * 1024)).toFixed(1)} MB`, : selectedFiles.value.length > 0
? `${(selectedFiles.value.reduce((acc, f) => acc + f.size, 0) / (1024 * 1024)).toFixed(1)} MB`
: `${(selectedFile.value!.size / (1024 * 1024)).toFixed(1)} MB`,
expiration: expiration:
expirationMethod.value === 'forever' expirationMethod.value === 'forever'
? t('send.expiration.forever') ? t('send.expiration.forever')
@@ -1071,6 +1115,7 @@ const handleSubmit = async () => {
alertStore.showAlert(t('send.messages.sendSuccess', { code: retrieveCode }), 'success') alertStore.showAlert(t('send.messages.sendSuccess', { code: retrieveCode }), 'success')
// 重置表单 - 只重置文件和文本内容,保留过期信息 // 重置表单 - 只重置文件和文本内容,保留过期信息
selectedFile.value = null selectedFile.value = null
selectedFiles.value = []
textContent.value = '' textContent.value = ''
uploadProgress.value = 0 uploadProgress.value = 0
resetPresignUpload() resetPresignUpload()