Generated
+5681
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ const alertIcons = {
|
||||
info: Info
|
||||
}
|
||||
|
||||
let intervalId: number
|
||||
let intervalId: ReturnType<typeof setInterval>
|
||||
|
||||
onMounted(() => {
|
||||
intervalId = setInterval(() => {
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
+1
-1
@@ -114,7 +114,7 @@ export function debounce<T extends (...args: unknown[]) => unknown>(
|
||||
func: T,
|
||||
wait: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: number | null = null
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
if (timeout) {
|
||||
|
||||
@@ -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 {
|
||||
// 文本上传保持不变
|
||||
@@ -1058,6 +1100,8 @@ const handleSubmit = async () => {
|
||||
size:
|
||||
sendType.value === 'text'
|
||||
? `${(textContent.value.length / 1024).toFixed(2)} KB`
|
||||
: 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:
|
||||
expirationMethod.value === 'forever'
|
||||
@@ -1071,6 +1115,7 @@ const handleSubmit = async () => {
|
||||
alertStore.showAlert(t('send.messages.sendSuccess', { code: retrieveCode }), 'success')
|
||||
// 重置表单 - 只重置文件和文本内容,保留过期信息
|
||||
selectedFile.value = null
|
||||
selectedFiles.value = []
|
||||
textContent.value = ''
|
||||
uploadProgress.value = 0
|
||||
resetPresignUpload()
|
||||
|
||||
Reference in New Issue
Block a user