generated from corrad-software/corrad-af-2024
483 lines
19 KiB
Vue
483 lines
19 KiB
Vue
<script setup>
|
|
import { ref, computed } from 'vue';
|
|
|
|
const props = defineProps({
|
|
visible: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
currentPath: {
|
|
type: String,
|
|
default: '/'
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits(['close', 'upload', 'update:visible']);
|
|
|
|
// Local state
|
|
const isDragging = ref(false);
|
|
const uploadFiles = ref([]);
|
|
const isUploading = ref(false);
|
|
const uploadProgress = ref({});
|
|
const globalTags = ref(['Urgent', 'Confidential', 'Draft', 'Final', 'Review', 'Archive', 'Public']);
|
|
const selectedTags = ref({});
|
|
const customMetadata = ref({});
|
|
|
|
// Computed
|
|
const hasFiles = computed(() => uploadFiles.value.length > 0);
|
|
|
|
// Methods
|
|
const closeDialog = () => {
|
|
emit('update:visible', false);
|
|
emit('close');
|
|
};
|
|
|
|
const handleDrop = (event) => {
|
|
event.preventDefault();
|
|
isDragging.value = false;
|
|
|
|
const files = Array.from(event.dataTransfer.files);
|
|
addFiles(files);
|
|
};
|
|
|
|
const handleFileInput = (event) => {
|
|
const files = Array.from(event.target.files);
|
|
addFiles(files);
|
|
// Reset input
|
|
event.target.value = '';
|
|
};
|
|
|
|
const addFiles = (files) => {
|
|
files.forEach(file => {
|
|
const fileObj = {
|
|
id: generateId(),
|
|
file,
|
|
name: file.name,
|
|
size: formatFileSize(file.size),
|
|
type: file.type,
|
|
extension: getFileExtension(file.name),
|
|
progress: 0,
|
|
status: 'pending', // pending, uploading, completed, error
|
|
tags: [],
|
|
metadata: {}
|
|
};
|
|
|
|
uploadFiles.value.push(fileObj);
|
|
selectedTags.value[fileObj.id] = [];
|
|
customMetadata.value[fileObj.id] = {
|
|
title: '',
|
|
description: '',
|
|
category: '',
|
|
author: '',
|
|
department: ''
|
|
};
|
|
});
|
|
};
|
|
|
|
const removeFile = (fileId) => {
|
|
const index = uploadFiles.value.findIndex(f => f.id === fileId);
|
|
if (index !== -1) {
|
|
uploadFiles.value.splice(index, 1);
|
|
delete selectedTags.value[fileId];
|
|
delete customMetadata.value[fileId];
|
|
delete uploadProgress.value[fileId];
|
|
}
|
|
};
|
|
|
|
const addTag = (fileId, tag) => {
|
|
if (!selectedTags.value[fileId].includes(tag)) {
|
|
selectedTags.value[fileId].push(tag);
|
|
}
|
|
};
|
|
|
|
const removeTag = (fileId, tag) => {
|
|
const index = selectedTags.value[fileId].indexOf(tag);
|
|
if (index !== -1) {
|
|
selectedTags.value[fileId].splice(index, 1);
|
|
}
|
|
};
|
|
|
|
const addCustomTag = (fileId, tagInput) => {
|
|
const tag = tagInput.trim();
|
|
if (tag && !selectedTags.value[fileId].includes(tag)) {
|
|
selectedTags.value[fileId].push(tag);
|
|
}
|
|
};
|
|
|
|
const startUpload = async () => {
|
|
if (!hasFiles.value || isUploading.value) return;
|
|
|
|
isUploading.value = true;
|
|
|
|
for (const fileObj of uploadFiles.value) {
|
|
fileObj.status = 'uploading';
|
|
|
|
// Simulate upload progress
|
|
for (let progress = 0; progress <= 100; progress += 10) {
|
|
fileObj.progress = progress;
|
|
uploadProgress.value[fileObj.id] = progress;
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
}
|
|
|
|
// Prepare file data with metadata and tags
|
|
const fileData = {
|
|
file: fileObj.file,
|
|
name: fileObj.name,
|
|
path: props.currentPath,
|
|
tags: selectedTags.value[fileObj.id],
|
|
metadata: customMetadata.value[fileObj.id]
|
|
};
|
|
|
|
try {
|
|
// Emit upload event for parent to handle
|
|
emit('upload', fileData);
|
|
fileObj.status = 'completed';
|
|
} catch (error) {
|
|
fileObj.status = 'error';
|
|
console.error('Upload failed:', error);
|
|
}
|
|
}
|
|
|
|
isUploading.value = false;
|
|
|
|
// Close dialog after successful upload
|
|
setTimeout(() => {
|
|
closeDialog();
|
|
clearFiles();
|
|
}, 1000);
|
|
};
|
|
|
|
const clearFiles = () => {
|
|
uploadFiles.value = [];
|
|
selectedTags.value = {};
|
|
customMetadata.value = {};
|
|
uploadProgress.value = {};
|
|
};
|
|
|
|
// Utility functions
|
|
const generateId = () => {
|
|
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
|
};
|
|
|
|
const formatFileSize = (bytes) => {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
};
|
|
|
|
const getFileExtension = (filename) => {
|
|
return filename.split('.').pop().toLowerCase();
|
|
};
|
|
|
|
const getFileIcon = (extension) => {
|
|
const iconMap = {
|
|
pdf: 'text-red-500',
|
|
doc: 'text-blue-500',
|
|
docx: 'text-blue-500',
|
|
xls: 'text-green-500',
|
|
xlsx: 'text-green-500',
|
|
ppt: 'text-orange-500',
|
|
pptx: 'text-orange-500',
|
|
jpg: 'text-purple-500',
|
|
jpeg: 'text-purple-500',
|
|
png: 'text-purple-500',
|
|
gif: 'text-purple-500',
|
|
txt: 'text-gray-500'
|
|
};
|
|
return iconMap[extension] || 'text-gray-500';
|
|
};
|
|
|
|
const getSvgIcon = (iconType, size = 24) => {
|
|
const icons = {
|
|
upload: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>`,
|
|
file: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>`,
|
|
close: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`,
|
|
tag: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></svg>`,
|
|
trash: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>`,
|
|
plus: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`
|
|
};
|
|
return icons[iconType] || icons.file;
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="visible" class="upload-dialog fixed inset-0 z-50 bg-black bg-opacity-50 flex items-center justify-center">
|
|
<div class="dialog-container bg-white dark:bg-gray-900 rounded-lg shadow-2xl max-w-4xl w-full mx-4 max-h-[90vh] flex flex-col">
|
|
<!-- Header -->
|
|
<div class="dialog-header border-b border-gray-200 dark:border-gray-700 p-4">
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-xl font-semibold">Upload Documents</h2>
|
|
<button @click="closeDialog" class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">
|
|
<span v-html="getSvgIcon('close', 20)"></span>
|
|
</button>
|
|
</div>
|
|
<p class="text-sm text-gray-500 mt-1">Upload to: {{ currentPath }}</p>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="dialog-content flex-1 p-4 overflow-y-auto">
|
|
<!-- Drop Zone -->
|
|
<div
|
|
v-if="!hasFiles"
|
|
@drop="handleDrop"
|
|
@dragover.prevent
|
|
@dragenter="isDragging = true"
|
|
@dragleave="isDragging = false"
|
|
class="drop-zone border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-8 text-center"
|
|
:class="{ 'border-blue-500 bg-blue-50 dark:bg-blue-900/20': isDragging }"
|
|
>
|
|
<span v-html="getSvgIcon('upload', 48)" class="text-gray-400 mb-4 block mx-auto"></span>
|
|
<h3 class="text-lg font-semibold mb-2">Drop files here to upload</h3>
|
|
<p class="text-gray-500 mb-4">or</p>
|
|
<label for="file-input" class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 cursor-pointer">
|
|
Choose Files
|
|
</label>
|
|
<input
|
|
id="file-input"
|
|
type="file"
|
|
multiple
|
|
@change="handleFileInput"
|
|
class="hidden"
|
|
/>
|
|
<p class="text-xs text-gray-500 mt-2">Supports all document types, images, and more</p>
|
|
</div>
|
|
|
|
<!-- File List -->
|
|
<div v-else class="file-list space-y-4">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold">Files to Upload ({{ uploadFiles.length }})</h3>
|
|
<label for="add-more-files" class="inline-flex items-center px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md text-sm hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer">
|
|
<span v-html="getSvgIcon('plus', 16)" class="mr-1"></span>
|
|
Add More
|
|
</label>
|
|
<input
|
|
id="add-more-files"
|
|
type="file"
|
|
multiple
|
|
@change="handleFileInput"
|
|
class="hidden"
|
|
/>
|
|
</div>
|
|
|
|
<div v-for="fileObj in uploadFiles" :key="fileObj.id" class="file-item bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
|
|
<div class="flex items-start space-x-3">
|
|
<!-- File Icon -->
|
|
<div class="flex-shrink-0">
|
|
<span v-html="getSvgIcon('file', 24)" :class="getFileIcon(fileObj.extension)"></span>
|
|
</div>
|
|
|
|
<!-- File Info and Metadata -->
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<div class="flex-1">
|
|
<h4 class="text-sm font-medium truncate">{{ fileObj.name }}</h4>
|
|
<p class="text-xs text-gray-500">{{ fileObj.size }} • {{ fileObj.extension.toUpperCase() }}</p>
|
|
</div>
|
|
<button @click="removeFile(fileObj.id)" class="ml-2 p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded">
|
|
<span v-html="getSvgIcon('trash', 16)" class="text-red-500"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Progress Bar -->
|
|
<div v-if="fileObj.status === 'uploading'" class="mb-3">
|
|
<div class="flex items-center justify-between mb-1">
|
|
<span class="text-xs text-gray-500">Uploading...</span>
|
|
<span class="text-xs text-gray-500">{{ fileObj.progress }}%</span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
|
<div
|
|
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
|
:style="{ width: `${fileObj.progress}%` }"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status -->
|
|
<div v-if="fileObj.status === 'completed'" class="mb-3">
|
|
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
✓ Uploaded
|
|
</span>
|
|
</div>
|
|
|
|
<div v-if="fileObj.status === 'error'" class="mb-3">
|
|
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
|
✗ Error
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Metadata Form -->
|
|
<div v-if="fileObj.status === 'pending'" class="space-y-3">
|
|
<!-- Basic Metadata -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Title</label>
|
|
<input
|
|
v-model="customMetadata[fileObj.id].title"
|
|
type="text"
|
|
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
|
|
:placeholder="fileObj.name"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Category</label>
|
|
<select
|
|
v-model="customMetadata[fileObj.id].category"
|
|
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
|
|
>
|
|
<option value="">Select category</option>
|
|
<option value="contract">Contract</option>
|
|
<option value="report">Report</option>
|
|
<option value="invoice">Invoice</option>
|
|
<option value="specification">Specification</option>
|
|
<option value="manual">Manual</option>
|
|
<option value="other">Other</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Author</label>
|
|
<input
|
|
v-model="customMetadata[fileObj.id].author"
|
|
type="text"
|
|
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
|
|
placeholder="Document author"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Department</label>
|
|
<input
|
|
v-model="customMetadata[fileObj.id].department"
|
|
type="text"
|
|
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
|
|
placeholder="Responsible department"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Description</label>
|
|
<textarea
|
|
v-model="customMetadata[fileObj.id].description"
|
|
rows="2"
|
|
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
|
|
placeholder="Brief description of the document"
|
|
></textarea>
|
|
</div>
|
|
|
|
<!-- Tags -->
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Tags</label>
|
|
<div class="flex flex-wrap gap-1 mb-2">
|
|
<button
|
|
v-for="tag in globalTags"
|
|
:key="tag"
|
|
@click="addTag(fileObj.id, tag)"
|
|
class="inline-flex items-center px-2 py-1 rounded-full text-xs border"
|
|
:class="selectedTags[fileObj.id]?.includes(tag)
|
|
? 'bg-blue-100 text-blue-800 border-blue-200'
|
|
: 'bg-gray-100 text-gray-800 border-gray-200 hover:bg-gray-200'"
|
|
>
|
|
<span v-html="getSvgIcon('tag', 12)" class="mr-1"></span>
|
|
{{ tag }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Selected Tags -->
|
|
<div v-if="selectedTags[fileObj.id]?.length" class="flex flex-wrap gap-1 mb-2">
|
|
<span
|
|
v-for="tag in selectedTags[fileObj.id]"
|
|
:key="tag"
|
|
class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800"
|
|
>
|
|
{{ tag }}
|
|
<button @click="removeTag(fileObj.id, tag)" class="ml-1 hover:text-blue-600">
|
|
<span v-html="getSvgIcon('close', 12)"></span>
|
|
</button>
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Custom Tag Input -->
|
|
<div class="flex gap-2">
|
|
<input
|
|
@keyup.enter="addCustomTag(fileObj.id, $event.target.value); $event.target.value = ''"
|
|
type="text"
|
|
class="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
|
|
placeholder="Add custom tag and press Enter"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div v-if="hasFiles" class="dialog-footer border-t border-gray-200 dark:border-gray-700 p-4">
|
|
<div class="flex items-center justify-between">
|
|
<button @click="clearFiles" class="text-sm text-gray-500 hover:text-gray-700">
|
|
Clear All
|
|
</button>
|
|
<div class="flex space-x-2">
|
|
<rs-button variant="secondary" @click="closeDialog">Cancel</rs-button>
|
|
<rs-button
|
|
@click="startUpload"
|
|
:disabled="isUploading"
|
|
class="inline-flex items-center"
|
|
>
|
|
<span v-if="isUploading" class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></span>
|
|
<span v-html="getSvgIcon('upload', 16)" class="mr-2" v-else></span>
|
|
{{ isUploading ? 'Uploading...' : 'Upload Files' }}
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.upload-dialog {
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
|
|
.dialog-container {
|
|
max-height: 90vh;
|
|
}
|
|
|
|
.dialog-content {
|
|
min-height: 200px;
|
|
}
|
|
|
|
.drop-zone {
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.drop-zone:hover {
|
|
border-color: #3B82F6;
|
|
background-color: rgba(59, 130, 246, 0.05);
|
|
}
|
|
|
|
/* Custom scrollbar */
|
|
.dialog-content::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
.dialog-content::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.dialog-content::-webkit-scrollbar-thumb {
|
|
background-color: rgba(156, 163, 175, 0.5);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.dialog-content::-webkit-scrollbar-thumb:hover {
|
|
background-color: rgba(156, 163, 175, 0.7);
|
|
}
|
|
</style> |