Generated
+5681
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const alertIcons = {
|
|||||||
info: Info
|
info: Info
|
||||||
}
|
}
|
||||||
|
|
||||||
let intervalId: number
|
let intervalId: ReturnType<typeof setInterval>
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
intervalId = setInterval(() => {
|
intervalId = setInterval(() => {
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user