209 lines
5.1 KiB
Vue
209 lines
5.1 KiB
Vue
<template>
|
|
<div>
|
|
<!-- Drop Zone -->
|
|
<div
|
|
class="drop-zone pa-8 text-center rounded-lg"
|
|
:class="{ 'drop-zone-active': isDragging }"
|
|
@drop="handleDrop"
|
|
@dragover.prevent="isDragging = true"
|
|
@dragleave.prevent="isDragging = false"
|
|
>
|
|
<v-icon size="64" color="primary" class="mb-4">
|
|
mdi-cloud-upload-outline
|
|
</v-icon>
|
|
<h3 class="text-h6 mb-2">Drag and drop files here</h3>
|
|
<p class="text-body-2 text-grey mb-4">or</p>
|
|
<v-btn
|
|
color="primary"
|
|
@click="openFileDialog"
|
|
prepend-icon="mdi-folder-open"
|
|
>
|
|
Browse Files
|
|
</v-btn>
|
|
<input
|
|
ref="fileInput"
|
|
type="file"
|
|
multiple
|
|
hidden
|
|
@change="handleFileSelect"
|
|
/>
|
|
<p class="text-caption text-grey mt-4">
|
|
Maximum file size: 50MB
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Selected Files -->
|
|
<v-list v-if="selectedFiles.length > 0" class="mt-4">
|
|
<v-list-subheader>Selected Files ({{ selectedFiles.length }})</v-list-subheader>
|
|
<v-list-item
|
|
v-for="(file, index) in selectedFiles"
|
|
:key="index"
|
|
:title="file.name"
|
|
:subtitle="formatFileSize(file.size)"
|
|
>
|
|
<template v-slot:prepend>
|
|
<v-icon>{{ getFileIcon(file.name) }}</v-icon>
|
|
</template>
|
|
<template v-slot:append>
|
|
<v-btn
|
|
icon
|
|
variant="text"
|
|
size="small"
|
|
@click="removeFile(index)"
|
|
>
|
|
<v-icon>mdi-close</v-icon>
|
|
</v-btn>
|
|
</template>
|
|
</v-list-item>
|
|
</v-list>
|
|
|
|
<!-- Upload Progress -->
|
|
<v-progress-linear
|
|
v-if="uploading && uploadProgress > 0"
|
|
:model-value="uploadProgress"
|
|
color="primary"
|
|
height="8"
|
|
class="mt-4"
|
|
/>
|
|
|
|
<!-- Actions -->
|
|
<v-card-actions v-if="selectedFiles.length > 0" class="mt-4">
|
|
<v-spacer />
|
|
<v-btn @click="clearFiles">Clear All</v-btn>
|
|
<v-btn
|
|
color="primary"
|
|
variant="flat"
|
|
@click="uploadFiles"
|
|
:loading="uploading"
|
|
:disabled="selectedFiles.length === 0"
|
|
>
|
|
Upload {{ selectedFiles.length }} File{{ selectedFiles.length > 1 ? 's' : '' }}
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref } from 'vue';
|
|
|
|
interface Props {
|
|
uploading: boolean;
|
|
currentPath?: string;
|
|
}
|
|
|
|
interface Emits {
|
|
(e: 'upload', files: File[]): void;
|
|
(e: 'close'): void;
|
|
}
|
|
|
|
const props = defineProps<Props>();
|
|
const emit = defineEmits<Emits>();
|
|
|
|
const selectedFiles = ref<File[]>([]);
|
|
const isDragging = ref(false);
|
|
const uploadProgress = ref(0);
|
|
const fileInput = ref<HTMLInputElement>();
|
|
|
|
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
|
|
|
|
// Handle drag and drop
|
|
const handleDrop = (e: DragEvent) => {
|
|
e.preventDefault();
|
|
isDragging.value = false;
|
|
|
|
const files = Array.from(e.dataTransfer?.files || []);
|
|
addFiles(files);
|
|
};
|
|
|
|
// Handle file selection
|
|
const handleFileSelect = (e: Event) => {
|
|
const input = e.target as HTMLInputElement;
|
|
const files = Array.from(input.files || []);
|
|
addFiles(files);
|
|
|
|
// Reset input value to allow selecting same file again
|
|
input.value = '';
|
|
};
|
|
|
|
// Add files to selection with validation
|
|
const addFiles = (files: File[]) => {
|
|
const validFiles = files.filter(file => {
|
|
if (file.size > MAX_FILE_SIZE) {
|
|
alert(`File "${file.name}" exceeds 50MB limit`);
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
selectedFiles.value = [...selectedFiles.value, ...validFiles];
|
|
};
|
|
|
|
// Remove file from selection
|
|
const removeFile = (index: number) => {
|
|
selectedFiles.value.splice(index, 1);
|
|
};
|
|
|
|
// Clear all files
|
|
const clearFiles = () => {
|
|
selectedFiles.value = [];
|
|
uploadProgress.value = 0;
|
|
};
|
|
|
|
// Upload files
|
|
const uploadFiles = () => {
|
|
if (selectedFiles.value.length === 0) return;
|
|
emit('upload', selectedFiles.value);
|
|
};
|
|
|
|
// Open file dialog
|
|
const openFileDialog = () => {
|
|
if (fileInput.value) {
|
|
fileInput.value.click();
|
|
}
|
|
};
|
|
|
|
// Helpers
|
|
const formatFileSize = (bytes: number): string => {
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
if (bytes === 0) return '0 Bytes';
|
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
|
};
|
|
|
|
const getFileIcon = (filename: string): string => {
|
|
const ext = filename.split('.').pop()?.toLowerCase() || '';
|
|
const iconMap: Record<string, string> = {
|
|
pdf: 'mdi-file-pdf-box',
|
|
doc: 'mdi-file-document',
|
|
docx: 'mdi-file-document',
|
|
xls: 'mdi-file-excel',
|
|
xlsx: 'mdi-file-excel',
|
|
jpg: 'mdi-file-image',
|
|
jpeg: 'mdi-file-image',
|
|
png: 'mdi-file-image',
|
|
gif: 'mdi-file-image',
|
|
svg: 'mdi-file-image',
|
|
zip: 'mdi-folder-zip',
|
|
rar: 'mdi-folder-zip',
|
|
txt: 'mdi-file-document-outline',
|
|
csv: 'mdi-file-delimited',
|
|
mp4: 'mdi-file-video',
|
|
mp3: 'mdi-file-music',
|
|
};
|
|
return iconMap[ext] || 'mdi-file';
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.drop-zone {
|
|
border: 2px dashed #ccc;
|
|
transition: all 0.3s;
|
|
background-color: #fafafa;
|
|
}
|
|
|
|
.drop-zone-active {
|
|
border-color: #1976d2;
|
|
background-color: rgba(25, 118, 210, 0.05);
|
|
}
|
|
</style>
|