Improved design for UI Layout and Structuring Hiearchy

This commit is contained in:
Aiman Fakhrullah Mantasan 2025-05-30 17:45:37 +08:00
parent b00a9f94c3
commit 9b14213beb
6 changed files with 1964 additions and 675 deletions

View File

@ -0,0 +1,398 @@
<script setup>
import { ref, computed, watch } from 'vue';
const props = defineProps({
visible: {
type: Boolean,
default: false
},
currentPath: {
type: String,
default: '/'
},
parentType: {
type: String,
default: 'root' // root, cabinet, drawer, folder
}
});
const emit = defineEmits(['close', 'create', 'update:visible']);
// Local state
const createType = ref('cabinet');
const itemName = ref('');
const itemDescription = ref('');
const itemCategory = ref('');
const itemDepartment = ref('');
const isPrivate = ref(false);
const selectedTags = ref([]);
const customTag = ref('');
const isCreating = ref(false);
// Available types based on parent
const availableTypes = computed(() => {
switch (props.parentType) {
case 'root':
return [{ value: 'cabinet', label: 'Cabinet', icon: 'cabinet' }];
case 'cabinet':
return [{ value: 'drawer', label: 'Drawer', icon: 'drawer' }];
case 'drawer':
return [{ value: 'folder', label: 'Folder', icon: 'folder' }];
case 'folder':
return [{ value: 'subfolder', label: 'Subfolder', icon: 'subfolder' }];
default:
return [];
}
});
// Global tags
const globalTags = ref([
'Important', 'Urgent', 'Confidential', 'Public', 'Archive',
'Financial', 'Legal', 'Technical', 'Administrative', 'Project'
]);
// Validation
const isValid = computed(() => {
return itemName.value.trim().length > 0;
});
const nameError = computed(() => {
if (!itemName.value.trim()) return 'Name is required';
if (itemName.value.length > 100) return 'Name must be less than 100 characters';
if (!/^[a-zA-Z0-9\s\-_()]+$/.test(itemName.value)) return 'Name contains invalid characters';
return null;
});
// Methods
const closeDialog = () => {
resetForm();
emit('update:visible', false);
emit('close');
};
const resetForm = () => {
itemName.value = '';
itemDescription.value = '';
itemCategory.value = '';
itemDepartment.value = '';
isPrivate.value = false;
selectedTags.value = [];
customTag.value = '';
createType.value = availableTypes.value[0]?.value || 'cabinet';
};
const addTag = (tag) => {
if (!selectedTags.value.includes(tag)) {
selectedTags.value.push(tag);
}
};
const removeTag = (tag) => {
const index = selectedTags.value.indexOf(tag);
if (index !== -1) {
selectedTags.value.splice(index, 1);
}
};
const addCustomTag = () => {
const tag = customTag.value.trim();
if (tag && !selectedTags.value.includes(tag)) {
selectedTags.value.push(tag);
customTag.value = '';
}
};
const createItem = async () => {
if (!isValid.value || isCreating.value) return;
isCreating.value = true;
const itemData = {
type: createType.value,
name: itemName.value.trim(),
description: itemDescription.value.trim(),
category: itemCategory.value,
department: itemDepartment.value,
isPrivate: isPrivate.value,
tags: selectedTags.value,
parentPath: props.currentPath,
parentType: props.parentType
};
try {
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
emit('create', itemData);
closeDialog();
} catch (error) {
console.error('Failed to create item:', error);
} finally {
isCreating.value = false;
}
};
// Set initial type when available types change
watch(() => availableTypes.value, (newTypes) => {
if (newTypes.length > 0) {
createType.value = newTypes[0].value;
}
}, { immediate: true });
const getSvgIcon = (iconType, size = 24) => {
const icons = {
cabinet: `<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"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/></svg>`,
drawer: `<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"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/></svg>`,
folder: `<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="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>`,
subfolder: `<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="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path><path d="M8 13h8"></path></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>`,
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>`,
lock: `<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"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>`,
unlock: `<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"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 9.9-1"></path></svg>`
};
return icons[iconType] || icons.folder;
};
</script>
<template>
<div v-if="visible" class="create-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-2xl 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">Create New Item</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">Create in: {{ currentPath }}</p>
</div>
<!-- Content -->
<div class="dialog-content flex-1 p-6 overflow-y-auto">
<form @submit.prevent="createItem" class="space-y-6">
<!-- Item Type Selection -->
<div v-if="availableTypes.length > 1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Type</label>
<div class="grid grid-cols-2 gap-3">
<button
v-for="type in availableTypes"
:key="type.value"
type="button"
@click="createType = type.value"
class="flex items-center space-x-3 p-3 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800"
:class="createType === type.value ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20' : ''"
>
<span v-html="getSvgIcon(type.icon, 24)" class="text-blue-600"></span>
<span class="font-medium">{{ type.label }}</span>
</button>
</div>
</div>
<div v-else-if="availableTypes.length === 1" class="flex items-center space-x-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<span v-html="getSvgIcon(availableTypes[0].icon, 24)" class="text-blue-600"></span>
<div>
<h3 class="font-medium">{{ availableTypes[0].label }}</h3>
<p class="text-sm text-gray-500">Creating a new {{ availableTypes[0].label.toLowerCase() }}</p>
</div>
</div>
<!-- Name Field -->
<div>
<label for="item-name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Name <span class="text-red-500">*</span>
</label>
<input
id="item-name"
v-model="itemName"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
:class="nameError ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''"
:placeholder="`Enter ${createType} name`"
required
/>
<p v-if="nameError" class="mt-1 text-sm text-red-600">{{ nameError }}</p>
</div>
<!-- Description Field -->
<div>
<label for="item-description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Description
</label>
<textarea
id="item-description"
v-model="itemDescription"
rows="3"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
:placeholder="`Brief description of this ${createType}`"
></textarea>
</div>
<!-- Metadata Fields -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="item-category" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Category
</label>
<select
id="item-category"
v-model="itemCategory"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
>
<option value="">Select category</option>
<option value="administrative">Administrative</option>
<option value="financial">Financial</option>
<option value="legal">Legal</option>
<option value="technical">Technical</option>
<option value="project">Project</option>
<option value="operational">Operational</option>
<option value="other">Other</option>
</select>
</div>
<div>
<label for="item-department" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Department
</label>
<input
id="item-department"
v-model="itemDepartment"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
placeholder="Responsible department"
/>
</div>
</div>
<!-- Privacy Setting -->
<div>
<label class="flex items-center space-x-3">
<input
v-model="isPrivate"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span class="flex items-center space-x-2">
<span v-html="getSvgIcon(isPrivate ? 'lock' : 'unlock', 16)"></span>
<span class="text-sm font-medium">Private {{ createType }}</span>
</span>
</label>
<p class="ml-6 text-xs text-gray-500 mt-1">
{{ isPrivate ? 'Only authorized users can access this item' : 'This item will be publicly accessible' }}
</p>
</div>
<!-- Tags -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Tags</label>
<!-- Predefined Tags -->
<div class="flex flex-wrap gap-2 mb-3">
<button
v-for="tag in globalTags"
:key="tag"
type="button"
@click="addTag(tag)"
class="inline-flex items-center px-2 py-1 rounded-full text-xs border"
:class="selectedTags.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.length" class="flex flex-wrap gap-2 mb-3">
<span
v-for="tag in selectedTags"
:key="tag"
class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800"
>
{{ tag }}
<button type="button" @click="removeTag(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
v-model="customTag"
@keyup.enter="addCustomTag"
type="text"
class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
placeholder="Add custom tag and press Enter"
/>
<button
type="button"
@click="addCustomTag"
class="px-3 py-2 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600"
>
<span v-html="getSvgIcon('plus', 16)"></span>
</button>
</div>
</div>
</form>
</div>
<!-- Footer -->
<div class="dialog-footer border-t border-gray-200 dark:border-gray-700 p-4">
<div class="flex justify-end space-x-3">
<rs-button variant="secondary" @click="closeDialog">Cancel</rs-button>
<rs-button
@click="createItem"
:disabled="!isValid || isCreating"
class="inline-flex items-center"
>
<span v-if="isCreating" class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></span>
<span v-html="getSvgIcon('plus', 16)" class="mr-2" v-else></span>
{{ isCreating ? 'Creating...' : `Create ${createType}` }}
</rs-button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.create-dialog {
backdrop-filter: blur(4px);
}
.dialog-container {
max-height: 90vh;
}
.dialog-content {
min-height: 200px;
}
/* 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);
}
/* Form validation styles */
input:invalid:not(:focus):not(:placeholder-shown) {
border-color: #ef4444;
}
input:valid:not(:focus):not(:placeholder-shown) {
border-color: #10b981;
}
</style>

View File

@ -0,0 +1,483 @@
<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>

View File

@ -1,6 +1,9 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import DMSDocumentViewer from '~/components/dms/viewers/DMSDocumentViewer.vue';
import DMSUploadDialog from '~/components/dms/dialogs/DMSUploadDialog.vue';
import DMSCreateNewDialog from '~/components/dms/dialogs/DMSCreateNewDialog.vue';
// Props for the explorer
const props = defineProps({
@ -23,6 +26,15 @@ const selectedItem = ref(null);
const viewMode = ref(props.viewMode);
const isLoading = ref(true);
const treeExpanded = ref({});
const sortBy = ref('name'); // name, modified, size, type
const sortOrder = ref('asc'); // asc, desc
const searchQuery = ref('');
// Dialog states
const showDocumentViewer = ref(false);
const showUploadDialog = ref(false);
const showCreateDialog = ref(false);
const currentDocument = ref(null);
// Mock data - replace with actual API calls
const allItems = ref([]);
@ -30,8 +42,50 @@ const router = useRouter();
// Computed properties
const currentItems = computed(() => {
// Filter items based on current path
return allItems.value.filter(item => item.parentPath === currentPath.value);
let items = allItems.value.filter(item => item.parentPath === currentPath.value);
// Apply search filter
if (searchQuery.value.trim()) {
const query = searchQuery.value.toLowerCase();
items = items.filter(item =>
item.name.toLowerCase().includes(query) ||
item.type.toLowerCase().includes(query) ||
(item.tags && item.tags.some(tag => tag.toLowerCase().includes(query)))
);
}
// Apply sorting
items.sort((a, b) => {
let aValue, bValue;
switch (sortBy.value) {
case 'name':
aValue = a.name.toLowerCase();
bValue = b.name.toLowerCase();
break;
case 'modified':
aValue = new Date(a.modified || 0);
bValue = new Date(b.modified || 0);
break;
case 'size':
aValue = parseFileSize(a.size || '0');
bValue = parseFileSize(b.size || '0');
break;
case 'type':
aValue = a.type;
bValue = b.type;
break;
default:
aValue = a.name.toLowerCase();
bValue = b.name.toLowerCase();
}
if (aValue < bValue) return sortOrder.value === 'asc' ? -1 : 1;
if (aValue > bValue) return sortOrder.value === 'asc' ? 1 : -1;
return 0;
});
return items;
});
const breadcrumbs = computed(() => {
@ -53,6 +107,15 @@ const breadcrumbs = computed(() => {
];
});
const parentType = computed(() => {
const pathParts = currentPath.value.split('/').filter(Boolean);
if (pathParts.length === 0) return 'root';
if (pathParts.length === 1) return 'cabinet';
if (pathParts.length === 2) return 'drawer';
if (pathParts.length === 3) return 'folder';
return 'subfolder';
});
// Methods
const loadItems = async () => {
isLoading.value = true;
@ -66,17 +129,138 @@ const loadItems = async () => {
// Mock data for development
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate API delay
// Example item structure
// Enhanced mock data with more metadata
allItems.value = [
{ id: 'cabinet1', name: 'Cabinet 1', type: 'cabinet', parentPath: '/' },
{ id: 'cabinet2', name: 'Cabinet 2', type: 'cabinet', parentPath: '/' },
{ id: 'drawer1', name: 'Drawer 1', type: 'drawer', parentPath: '/cabinet1' },
{ id: 'drawer2', name: 'Drawer 2', type: 'drawer', parentPath: '/cabinet1' },
{ id: 'folder1', name: 'Folder 1', type: 'folder', parentPath: '/cabinet1/drawer1' },
{ id: 'folder2', name: 'Folder 2', type: 'folder', parentPath: '/cabinet1/drawer1' },
{ id: 'subfolder1', name: 'Subfolder 1', type: 'subfolder', parentPath: '/cabinet1/drawer1/folder1' },
{ id: 'file1', name: 'Document 1.pdf', type: 'file', parentPath: '/cabinet1/drawer1/folder1/subfolder1', extension: 'pdf', size: '1.2 MB', modified: '2023-10-15' },
{ id: 'file2', name: 'Spreadsheet.xlsx', type: 'file', parentPath: '/cabinet1/drawer1/folder1/subfolder1', extension: 'xlsx', size: '0.5 MB', modified: '2023-10-16' }
{
id: 'cabinet1',
name: 'Administrative Cabinet',
type: 'cabinet',
parentPath: '/',
description: 'Contains administrative documents',
tags: ['Administrative', 'Public'],
created: '2023-01-15',
modified: '2023-10-15'
},
{
id: 'cabinet2',
name: 'Technical Cabinet',
type: 'cabinet',
parentPath: '/',
description: 'Technical specifications and manuals',
tags: ['Technical', 'Confidential'],
created: '2023-02-20',
modified: '2023-10-10'
},
{
id: 'drawer1',
name: 'Policy Documents',
type: 'drawer',
parentPath: '/cabinet1',
description: 'Company policies and procedures',
tags: ['Policy', 'Important'],
created: '2023-03-01',
modified: '2023-09-30'
},
{
id: 'drawer2',
name: 'Financial Records',
type: 'drawer',
parentPath: '/cabinet1',
description: 'Financial statements and reports',
tags: ['Financial', 'Confidential'],
created: '2023-03-15',
modified: '2023-10-01'
},
{
id: 'folder1',
name: 'HR Policies',
type: 'folder',
parentPath: '/cabinet1/drawer1',
description: 'Human resources policies',
tags: ['HR', 'Policy'],
created: '2023-04-01',
modified: '2023-08-15'
},
{
id: 'folder2',
name: 'IT Policies',
type: 'folder',
parentPath: '/cabinet1/drawer1',
description: 'Information technology policies',
tags: ['IT', 'Technical'],
created: '2023-04-10',
modified: '2023-09-01'
},
{
id: 'subfolder1',
name: 'Employee Handbook',
type: 'subfolder',
parentPath: '/cabinet1/drawer1/folder1',
description: 'Employee guidelines and procedures',
tags: ['Employee', 'Handbook'],
created: '2023-05-01',
modified: '2023-07-20'
},
{
id: 'file1',
name: 'Employee_Handbook_2023.pdf',
type: 'file',
parentPath: '/cabinet1/drawer1/folder1/subfolder1',
extension: 'pdf',
size: '2.4 MB',
modified: '2023-10-15',
created: '2023-05-15',
author: 'HR Department',
department: 'Human Resources',
tags: ['Employee', 'Handbook', '2023'],
description: 'Complete employee handbook for 2023',
metadata: {
title: 'Employee Handbook 2023',
subject: 'HR policies and procedures',
category: 'Policy',
confidentiality: 'Internal'
}
},
{
id: 'file2',
name: 'IT_Security_Policy.docx',
type: 'file',
parentPath: '/cabinet1/drawer1/folder2',
extension: 'docx',
size: '1.8 MB',
modified: '2023-09-20',
created: '2023-06-01',
author: 'IT Department',
department: 'Information Technology',
tags: ['Security', 'Policy', 'IT'],
description: 'Information technology security policies',
metadata: {
title: 'IT Security Policy',
subject: 'Cybersecurity guidelines',
category: 'Technical',
confidentiality: 'Confidential'
}
},
{
id: 'file3',
name: 'Financial_Report_Q3.xlsx',
type: 'file',
parentPath: '/cabinet1/drawer2',
extension: 'xlsx',
size: '3.2 MB',
modified: '2023-10-01',
created: '2023-09-30',
author: 'Finance Team',
department: 'Finance',
tags: ['Financial', 'Report', 'Q3'],
description: 'Third quarter financial report',
metadata: {
title: 'Q3 Financial Report',
subject: 'Quarterly financial analysis',
category: 'Financial',
confidentiality: 'Confidential'
}
}
];
} catch (error) {
console.error('Failed to load items:', error);
@ -86,14 +270,31 @@ const loadItems = async () => {
}
};
const parseFileSize = (sizeStr) => {
if (!sizeStr || typeof sizeStr !== 'string') return 0;
const match = sizeStr.match(/(\d+\.?\d*)\s*(KB|MB|GB|TB)?/i);
if (!match) return 0;
const size = parseFloat(match[1]);
const unit = (match[2] || 'B').toUpperCase();
const multipliers = { B: 1, KB: 1024, MB: 1024**2, GB: 1024**3, TB: 1024**4 };
return size * (multipliers[unit] || 1);
};
const selectItem = (item) => {
selectedItem.value = item;
emit('itemSelected', item);
if (item.type === 'file') {
currentDocument.value = item;
}
};
const navigateTo = (path) => {
currentPath.value = path;
selectedItem.value = null;
currentDocument.value = null;
emit('pathChanged', path);
loadItems();
};
@ -123,6 +324,39 @@ const changeViewMode = (mode) => {
emit('viewModeChanged', mode);
};
const setSortBy = (field) => {
if (sortBy.value === field) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
} else {
sortBy.value = field;
sortOrder.value = 'asc';
}
};
const openFileViewer = (document) => {
currentDocument.value = document;
showDocumentViewer.value = true;
};
const closeFileViewer = () => {
showDocumentViewer.value = false;
currentDocument.value = null;
};
const handleUpload = (fileData) => {
console.log('Uploading file:', fileData);
// Handle file upload logic here
// Add to allItems.value or refresh from API
loadItems();
};
const handleCreateNew = (itemData) => {
console.log('Creating new item:', itemData);
// Handle create new item logic here
// Add to allItems.value or refresh from API
loadItems();
};
// Format file size
const formatSize = (size) => {
return size;
@ -145,27 +379,28 @@ const getItemIcon = (item) => {
};
// Get SVG path based on icon type
const getSvgIcon = (iconType) => {
switch (iconType) {
case 'cabinet':
return `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/></svg>`;
case 'folder':
return `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>`;
case 'pdf':
return `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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><path d="M9 15h6"></path><path d="M9 11h6"></path></svg>`;
case 'doc':
return `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>`;
case 'sheet':
return `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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><rect x="8" y="12" width="8" height="6"></rect><line x1="8" y1="16" x2="16" y2="16"></line><line x1="11" y1="12" x2="11" y2="18"></line></svg>`;
default:
return `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>`;
}
};
// Render SVG from string
const renderSvgIcon = (iconType, size = 24) => {
const svgString = getSvgIcon(iconType);
return svgString.replace('width="24"', `width="${size}"`).replace('height="24"', `height="${size}"`);
const getSvgIcon = (iconType, size = 24) => {
const icons = {
cabinet: `<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"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/></svg>`,
folder: `<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="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>`,
pdf: `<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><path d="M9 15h6"></path><path d="M9 11h6"></path></svg>`,
doc: `<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><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>`,
sheet: `<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><rect x="8" y="12" width="8" height="6"></rect><line x1="8" y1="16" x2="16" y2="16"></line><line x1="11" y1="12" x2="11" y2="18"></line></svg>`,
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>`,
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>`,
search: `<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"><circle cx="11" cy="11" r="8"></circle><path d="M21 21l-4.35-4.35"></path></svg>`,
sort: `<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="3" y1="6" x2="15" y2="6"></line><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>`,
sortAsc: `<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="M3 6l3-3l3 3"></path><path d="M6 3v18"></path><path d="M13 6h8"></path><path d="M13 12h5"></path><path d="M13 18h3"></path></svg>`,
sortDesc: `<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="M3 18l3 3l3-3"></path><path d="M6 21V3"></path><path d="M13 6h8"></path><path d="M13 12h5"></path><path d="M13 18h3"></path></svg>`,
home: `<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="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>`,
back: `<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="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>`,
forward: `<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="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></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>`,
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>`,
chevronDown: `<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="M6 9l6 6 6-6"/></svg>`,
chevronRight: `<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="M9 18l6-6-6-6"/></svg>`
};
return icons[iconType] || icons.file;
};
// Lifecycle hooks
@ -176,114 +411,173 @@ onMounted(() => {
<template>
<div class="dms-explorer bg-gray-50 dark:bg-gray-900 h-full flex flex-col">
<!-- Toolbar -->
<div class="dms-toolbar p-2 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<div class="flex items-center space-x-2">
<button class="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
</button>
<button class="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
</button>
<div class="dms-breadcrumbs flex items-center space-x-1">
<template v-for="(crumb, index) in breadcrumbs" :key="index">
<span v-if="index > 0" class="text-gray-400">/</span>
<button
@click="navigateTo(crumb.path)"
class="text-primary hover:underline p-1 rounded"
:class="index === breadcrumbs.length - 1 ? 'font-medium' : ''"
>
{{ crumb.name }}
</button>
</template>
<!-- Enhanced Toolbar -->
<div class="dms-toolbar p-3 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<div class="flex items-center justify-between mb-3">
<!-- Navigation Controls -->
<div class="flex items-center space-x-2">
<button class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50" title="Back">
<span v-html="getSvgIcon('back', 18)"></span>
</button>
<button class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50" title="Forward">
<span v-html="getSvgIcon('forward', 18)"></span>
</button>
<button @click="navigateTo('/')" class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700" title="Home">
<span v-html="getSvgIcon('home', 18)"></span>
</button>
</div>
<!-- Action Buttons -->
<div class="flex items-center space-x-2">
<rs-button @click="showUploadDialog = true" size="sm">
<span v-html="getSvgIcon('upload', 16)" class="mr-2"></span>
Upload
</rs-button>
<rs-button @click="showCreateDialog = true" size="sm" variant="secondary">
<span v-html="getSvgIcon('plus', 16)" class="mr-2"></span>
Create New
</rs-button>
</div>
</div>
<div class="flex items-center space-x-2">
<button
@click="changeViewMode('list')"
class="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700"
:class="viewMode === 'list' ? 'bg-gray-200 dark:bg-gray-700' : ''"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>
</button>
<button
@click="changeViewMode('grid')"
class="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700"
:class="viewMode === 'grid' ? 'bg-gray-200 dark:bg-gray-700' : ''"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>
</button>
<button
@click="changeViewMode('details')"
class="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700"
:class="viewMode === 'details' ? 'bg-gray-200 dark:bg-gray-700' : ''"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="21" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="21" y1="18" x2="3" y2="18"></line></svg>
</button>
<!-- Breadcrumbs -->
<div class="flex items-center space-x-1 mb-3">
<template v-for="(crumb, index) in breadcrumbs" :key="index">
<span v-if="index > 0" class="text-gray-400">/</span>
<button
@click="navigateTo(crumb.path)"
class="text-primary hover:underline px-2 py-1 rounded text-sm"
:class="index === breadcrumbs.length - 1 ? 'font-medium bg-gray-100 dark:bg-gray-700' : ''"
>
{{ crumb.name }}
</button>
</template>
</div>
<!-- Search and View Controls -->
<div class="flex items-center justify-between">
<!-- Search -->
<div class="flex items-center space-x-3 flex-1">
<div class="relative flex-1 max-w-md">
<span v-html="getSvgIcon('search', 18)" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></span>
<input
v-model="searchQuery"
type="text"
placeholder="Search documents, folders, and tags..."
class="pl-10 pr-4 py-2 w-full border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
/>
</div>
<!-- Sort Controls -->
<div class="flex items-center space-x-1">
<span class="text-sm text-gray-500">Sort:</span>
<select
v-model="sortBy"
@change="setSortBy(sortBy)"
class="text-sm border border-gray-300 dark:border-gray-600 rounded px-2 py-1 dark:bg-gray-700"
>
<option value="name">Name</option>
<option value="modified">Modified</option>
<option value="size">Size</option>
<option value="type">Type</option>
</select>
<button @click="setSortBy(sortBy)" class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">
<span v-html="getSvgIcon(sortOrder === 'asc' ? 'sortAsc' : 'sortDesc', 16)"></span>
</button>
</div>
</div>
<!-- View Mode Controls -->
<div class="flex items-center space-x-1 ml-4">
<button
@click="changeViewMode('list')"
class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
:class="viewMode === 'list' ? 'bg-gray-200 dark:bg-gray-600' : ''"
title="List View"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>
</button>
<button
@click="changeViewMode('grid')"
class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
:class="viewMode === 'grid' ? 'bg-gray-200 dark:bg-gray-600' : ''"
title="Grid View"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>
</button>
<button
@click="changeViewMode('details')"
class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
:class="viewMode === 'details' ? 'bg-gray-200 dark:bg-gray-600' : ''"
title="Details View"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="21" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="21" y1="18" x2="3" y2="18"></line></svg>
</button>
</div>
</div>
</div>
<!-- Main content area -->
<div class="flex-1 flex overflow-hidden">
<!-- Tree navigation -->
<div class="dms-tree w-64 border-r border-gray-200 dark:border-gray-700 overflow-y-auto p-2">
<div class="dms-tree w-64 border-r border-gray-200 dark:border-gray-700 overflow-y-auto p-2 bg-white dark:bg-gray-800">
<div class="tree-item p-1">
<div
@click="navigateTo('/')"
class="flex items-center space-x-1 p-1 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700"
:class="currentPath === '/' ? 'bg-primary/20 text-primary' : ''"
class="flex items-center space-x-2 p-2 rounded cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"
:class="currentPath === '/' ? 'bg-blue-100 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300' : ''"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
<span>Home</span>
<span v-html="getSvgIcon('home', 18)"></span>
<span class="font-medium">Home</span>
</div>
</div>
<!-- Tree items, recursive component would be better in production -->
<div v-for="cabinet in allItems.filter(i => i.type === 'cabinet')" :key="cabinet.id" class="tree-item p-1">
<!-- Tree items -->
<div v-for="cabinet in allItems.filter(i => i.type === 'cabinet')" :key="cabinet.id" class="tree-item">
<div
@click="toggleTreeItem(cabinet.id)"
class="flex items-center space-x-1 p-1 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700"
class="flex items-center space-x-2 p-2 rounded cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"
:class="currentPath.includes(cabinet.id) ? 'bg-blue-50 dark:bg-blue-900/10' : ''"
>
<svg v-if="treeExpanded[cabinet.id]" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/></svg>
<span>{{ cabinet.name }}</span>
<span v-html="getSvgIcon(treeExpanded[cabinet.id] ? 'chevronDown' : 'chevronRight', 16)"></span>
<span v-html="getSvgIcon('cabinet', 18)" class="text-blue-600"></span>
<span class="text-sm truncate">{{ cabinet.name }}</span>
</div>
<div v-if="treeExpanded[cabinet.id]" class="pl-5">
<div v-if="treeExpanded[cabinet.id]" class="ml-6">
<div
v-for="drawer in allItems.filter(i => i.parentPath === `/${cabinet.id}`)"
:key="drawer.id"
class="tree-item"
>
<div
@click="toggleTreeItem(drawer.id)"
class="flex items-center space-x-1 p-1 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700"
@click="navigateTo(`/${cabinet.id}/${drawer.id}`)"
class="flex items-center space-x-2 p-1 rounded cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"
:class="currentPath === `/${cabinet.id}/${drawer.id}` ? 'bg-blue-100 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300' : ''"
>
<svg v-if="treeExpanded[drawer.id]" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
<span>{{ drawer.name }}</span>
<span v-html="getSvgIcon('folder', 16)"></span>
<span class="text-xs truncate">{{ drawer.name }}</span>
</div>
<!-- Further nesting would be handled with recursive components in production -->
</div>
</div>
</div>
</div>
<!-- Content area -->
<div class="dms-content flex-1 overflow-y-auto">
<div class="dms-content flex-1 overflow-y-auto bg-white dark:bg-gray-900">
<!-- Loading indicator -->
<div v-if="isLoading" class="flex justify-center items-center h-full">
<Loading />
<div class="flex flex-col items-center space-y-4">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<p class="text-gray-500">Loading documents...</p>
</div>
</div>
<!-- Empty state -->
<div v-else-if="currentItems.length === 0" class="flex flex-col justify-center items-center h-full text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
<p class="mt-2">This folder is empty</p>
<span v-html="getSvgIcon('folder', 64)" class="mb-4 text-gray-300"></span>
<p class="text-lg font-medium">{{ searchQuery ? 'No results found' : 'This folder is empty' }}</p>
<p class="text-sm">{{ searchQuery ? 'Try adjusting your search terms' : 'Upload documents or create new folders to get started' }}</p>
</div>
<!-- List view -->
@ -292,43 +586,99 @@ onMounted(() => {
v-for="item in currentItems"
:key="item.id"
@click="navigateToItem(item)"
class="flex items-center p-2 border-b border-gray-100 dark:border-gray-800 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
:class="selectedItem?.id === item.id ? 'bg-primary/10 dark:bg-primary/20' : ''"
@dblclick="item.type === 'file' ? openFileViewer(item) : null"
class="flex items-center p-3 border-b border-gray-100 dark:border-gray-800 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg mb-1"
:class="selectedItem?.id === item.id ? 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-700' : ''"
>
<span class="mr-3" v-html="renderSvgIcon(getItemIcon(item), 24)"></span>
<div class="flex-1">
<div class="font-medium">{{ item.name }}</div>
<div v-if="item.type === 'file'" class="text-xs text-gray-500">
{{ item.extension.toUpperCase() }} · {{ item.size }} · Modified {{ item.modified }}
<span class="mr-3 flex-shrink-0" v-html="getSvgIcon(getItemIcon(item), 24)"></span>
<div class="flex-1 min-w-0">
<div class="flex items-center space-x-2 mb-1">
<h4 class="font-medium truncate">{{ item.name }}</h4>
<div v-if="item.tags" class="flex space-x-1">
<span
v-for="tag in item.tags.slice(0, 2)"
:key="tag"
class="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"
>
<span v-html="getSvgIcon('tag', 10)" class="mr-1"></span>
{{ tag }}
</span>
<span v-if="item.tags.length > 2" class="text-xs text-gray-400">+{{ item.tags.length - 2 }}</span>
</div>
</div>
<div class="flex items-center space-x-4 text-xs text-gray-500">
<span class="capitalize">{{ item.type }}</span>
<span v-if="item.type === 'file'">{{ item.extension?.toUpperCase() }}</span>
<span v-if="item.size">{{ item.size }}</span>
<span v-if="item.modified">Modified {{ item.modified }}</span>
<span v-if="item.author">by {{ item.author }}</span>
</div>
<p v-if="item.description" class="text-xs text-gray-600 dark:text-gray-400 mt-1 truncate">{{ item.description }}</p>
</div>
</div>
</div>
<!-- Grid view -->
<div v-else-if="viewMode === 'grid'" class="p-4 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4">
<div v-else-if="viewMode === 'grid'" class="p-4 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4">
<div
v-for="item in currentItems"
:key="item.id"
@click="navigateToItem(item)"
class="flex flex-col items-center p-4 rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 text-center"
:class="selectedItem?.id === item.id ? 'bg-primary/10 dark:bg-primary/20' : ''"
@dblclick="item.type === 'file' ? openFileViewer(item) : null"
class="flex flex-col items-center p-4 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 text-center group"
:class="selectedItem?.id === item.id ? 'bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-200 dark:ring-blue-700' : ''"
>
<span class="mb-2" v-html="renderSvgIcon(getItemIcon(item), 48)"></span>
<div class="w-full truncate text-sm font-medium">{{ item.name }}</div>
<div v-if="item.type === 'file'" class="text-xs text-gray-500">{{ item.size }}</div>
<span class="mb-2 group-hover:scale-110 transition-transform" v-html="getSvgIcon(getItemIcon(item), 48)"></span>
<div class="w-full">
<h4 class="text-sm font-medium truncate mb-1">{{ item.name }}</h4>
<div v-if="item.type === 'file'" class="text-xs text-gray-500">
<div>{{ item.extension?.toUpperCase() }}</div>
<div>{{ item.size }}</div>
</div>
<div v-else class="text-xs text-gray-500 capitalize">{{ item.type }}</div>
<div v-if="item.tags" class="flex flex-wrap gap-1 mt-2 justify-center">
<span
v-for="tag in item.tags.slice(0, 1)"
:key="tag"
class="inline-block px-1 py-0.5 rounded text-xs bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"
>
{{ tag }}
</span>
</div>
</div>
</div>
</div>
<!-- Details view -->
<div v-else-if="viewMode === 'details'" class="p-4">
<div v-else-if="viewMode === 'details'" class="overflow-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800">
<thead class="bg-gray-50 dark:bg-gray-800 sticky top-0">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Size</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Modified</th>
<th @click="setSortBy('name')" scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
<div class="flex items-center space-x-1">
<span>Name</span>
<span v-if="sortBy === 'name'" v-html="getSvgIcon(sortOrder === 'asc' ? 'sortAsc' : 'sortDesc', 12)"></span>
</div>
</th>
<th @click="setSortBy('type')" scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
<div class="flex items-center space-x-1">
<span>Type</span>
<span v-if="sortBy === 'type'" v-html="getSvgIcon(sortOrder === 'asc' ? 'sortAsc' : 'sortDesc', 12)"></span>
</div>
</th>
<th @click="setSortBy('size')" scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
<div class="flex items-center space-x-1">
<span>Size</span>
<span v-if="sortBy === 'size'" v-html="getSvgIcon(sortOrder === 'asc' ? 'sortAsc' : 'sortDesc', 12)"></span>
</div>
</th>
<th @click="setSortBy('modified')" scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
<div class="flex items-center space-x-1">
<span>Modified</span>
<span v-if="sortBy === 'modified'" v-html="getSvgIcon(sortOrder === 'asc' ? 'sortAsc' : 'sortDesc', 12)"></span>
</div>
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Tags</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-800">
@ -336,23 +686,48 @@ onMounted(() => {
v-for="item in currentItems"
:key="item.id"
@click="navigateToItem(item)"
class="cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
:class="selectedItem?.id === item.id ? 'bg-primary/10 dark:bg-primary/20' : ''"
@dblclick="item.type === 'file' ? openFileViewer(item) : null"
class="cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800"
:class="selectedItem?.id === item.id ? 'bg-blue-50 dark:bg-blue-900/20' : ''"
>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<span class="mr-3" v-html="renderSvgIcon(getItemIcon(item), 24)"></span>
<div class="font-medium">{{ item.name }}</div>
<span class="mr-3" v-html="getSvgIcon(getItemIcon(item), 20)"></span>
<div>
<div class="font-medium">{{ item.name }}</div>
<div v-if="item.description" class="text-sm text-gray-500 truncate max-w-xs">{{ item.description }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 capitalize">
{{ item.type }}
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium capitalize"
:class="{
'bg-blue-100 text-blue-800': item.type === 'cabinet',
'bg-green-100 text-green-800': item.type === 'drawer',
'bg-yellow-100 text-yellow-800': item.type === 'folder',
'bg-orange-100 text-orange-800': item.type === 'subfolder',
'bg-purple-100 text-purple-800': item.type === 'file'
}">
{{ item.type }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ item.type === 'file' ? item.size : '—' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ item.type === 'file' ? item.modified : '—' }}
{{ item.modified || '—' }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div v-if="item.tags" class="flex flex-wrap gap-1">
<span
v-for="tag in item.tags.slice(0, 3)"
:key="tag"
class="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"
>
{{ tag }}
</span>
<span v-if="item.tags.length > 3" class="text-xs text-gray-400">+{{ item.tags.length - 3 }}</span>
</div>
</td>
</tr>
</tbody>
@ -360,62 +735,125 @@ onMounted(() => {
</div>
</div>
<!-- Details panel - shown when a file is selected -->
<div v-if="selectedItem && selectedItem.type === 'file'" class="dms-details w-72 border-l border-gray-200 dark:border-gray-700 overflow-y-auto p-4">
<div class="flex flex-col items-center mb-6">
<span class="mb-3" v-html="renderSvgIcon(getItemIcon(selectedItem), 64)"></span>
<h3 class="text-lg font-medium text-center">{{ selectedItem.name }}</h3>
<p class="text-gray-500 text-sm">{{ selectedItem.type }}</p>
</div>
<div class="border-t border-gray-200 dark:border-gray-700 pt-4">
<div class="mb-4">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Properties</h4>
<div class="text-sm">
<div class="flex justify-between py-1">
<span class="text-gray-500">Type</span>
<span>{{ selectedItem.extension.toUpperCase() }} File</span>
</div>
<div class="flex justify-between py-1">
<span class="text-gray-500">Size</span>
<span>{{ selectedItem.size }}</span>
</div>
<div class="flex justify-between py-1">
<span class="text-gray-500">Modified</span>
<span>{{ selectedItem.modified }}</span>
</div>
<!-- Windows-style Details panel -->
<div v-if="currentDocument" class="dms-details w-80 border-l border-gray-200 dark:border-gray-700 overflow-y-auto bg-white dark:bg-gray-800">
<div class="p-4">
<!-- Document Preview -->
<div class="text-center mb-6">
<div class="bg-gray-100 dark:bg-gray-700 rounded-lg p-8 mb-4">
<span v-html="getSvgIcon(getItemIcon(currentDocument), 64)" class="text-gray-400"></span>
</div>
<h3 class="text-lg font-semibold truncate mb-1">{{ currentDocument.name }}</h3>
<p class="text-sm text-gray-500">{{ currentDocument.extension?.toUpperCase() }} File</p>
</div>
<div class="mb-4">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Actions</h4>
<div class="flex flex-col gap-2">
<rs-button size="sm" block>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
Preview
</rs-button>
<rs-button size="sm" block>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
Download
</rs-button>
<rs-button size="sm" block>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
Print
</rs-button>
</div>
</div>
<div>
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Access Control</h4>
<p class="text-sm text-gray-500 mb-2">You have full access to this document</p>
<rs-button size="sm" block color="secondary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
Request More Access
<!-- Quick Actions -->
<div class="space-y-2 mb-6">
<rs-button @click="openFileViewer(currentDocument)" block size="sm">
Preview
</rs-button>
<rs-button variant="secondary" block size="sm">
Download
</rs-button>
<rs-button variant="secondary" block size="sm">
Share
</rs-button>
</div>
<!-- Properties -->
<div class="space-y-4">
<div>
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Properties</h4>
<dl class="space-y-2 text-sm">
<div class="flex justify-between">
<dt class="text-gray-500">Size</dt>
<dd class="font-medium">{{ currentDocument.size }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">Modified</dt>
<dd class="font-medium">{{ currentDocument.modified }}</dd>
</div>
<div v-if="currentDocument.author" class="flex justify-between">
<dt class="text-gray-500">Author</dt>
<dd class="font-medium">{{ currentDocument.author }}</dd>
</div>
<div v-if="currentDocument.department" class="flex justify-between">
<dt class="text-gray-500">Department</dt>
<dd class="font-medium">{{ currentDocument.department }}</dd>
</div>
</dl>
</div>
<!-- Description -->
<div v-if="currentDocument.description">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Description</h4>
<p class="text-sm text-gray-600 dark:text-gray-400">{{ currentDocument.description }}</p>
</div>
<!-- Tags -->
<div v-if="currentDocument.tags">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Tags</h4>
<div class="flex flex-wrap gap-1">
<span
v-for="tag in currentDocument.tags"
:key="tag"
class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
>
<span v-html="getSvgIcon('tag', 10)" class="mr-1"></span>
{{ tag }}
</span>
</div>
</div>
<!-- Metadata -->
<div v-if="currentDocument.metadata">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Metadata</h4>
<dl class="space-y-2 text-sm">
<div v-if="currentDocument.metadata.title" class="flex justify-between">
<dt class="text-gray-500">Title</dt>
<dd class="font-medium text-right">{{ currentDocument.metadata.title }}</dd>
</div>
<div v-if="currentDocument.metadata.subject" class="flex justify-between">
<dt class="text-gray-500">Subject</dt>
<dd class="font-medium text-right">{{ currentDocument.metadata.subject }}</dd>
</div>
<div v-if="currentDocument.metadata.category" class="flex justify-between">
<dt class="text-gray-500">Category</dt>
<dd class="font-medium text-right">{{ currentDocument.metadata.category }}</dd>
</div>
</dl>
</div>
</div>
</div>
</div>
</div>
<!-- Document Viewer Modal -->
<DMSDocumentViewer
:visible="showDocumentViewer"
:document="currentDocument"
@close="closeFileViewer"
@download="(doc) => console.log('Download:', doc)"
@print="(doc) => console.log('Print:', doc)"
@edit="(doc) => console.log('Edit:', doc)"
/>
<!-- Upload Dialog -->
<DMSUploadDialog
v-model:visible="showUploadDialog"
:current-path="currentPath"
@upload="handleUpload"
@close="showUploadDialog = false"
/>
<!-- Create New Dialog -->
<DMSCreateNewDialog
v-model:visible="showCreateDialog"
:current-path="currentPath"
:parent-type="parentType"
@create="handleCreateNew"
@close="showCreateDialog = false"
/>
</div>
</template>

View File

@ -1,78 +1,64 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { ref, computed, onMounted, watch } from 'vue';
const props = defineProps({
document: {
type: Object,
required: true
},
viewMode: {
type: String,
default: 'preview' // preview, fullscreen
visible: {
type: Boolean,
default: false
}
});
const emit = defineEmits(['close', 'download', 'print', 'requestAccess']);
const emit = defineEmits(['close', 'download', 'print', 'edit']);
// State
// Local state
const isLoading = ref(true);
const zoomLevel = ref(100); // percentage
const zoomLevel = ref(100);
const currentPage = ref(1);
const totalPages = ref(1);
const viewerReady = ref(false);
const isFullscreen = ref(false);
const previewMode = ref('fit-width'); // fit-width, fit-page, actual-size
// Computed properties
const documentType = computed(() => {
const extension = props.document.extension?.toLowerCase();
if (['pdf'].includes(extension)) return 'pdf';
if (['doc', 'docx'].includes(extension)) return 'word';
if (['xls', 'xlsx'].includes(extension)) return 'excel';
if (['ppt', 'pptx'].includes(extension)) return 'powerpoint';
if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension)) return 'image';
if (['txt', 'md', 'json', 'csv'].includes(extension)) return 'text';
return 'generic';
const zoomOptions = computed(() => [
{ label: '50%', value: 50 },
{ label: '75%', value: 75 },
{ label: '100%', value: 100 },
{ label: '125%', value: 125 },
{ label: '150%', value: 150 },
{ label: '200%', value: 200 },
{ label: 'Fit Width', value: 'fit-width' },
{ label: 'Fit Page', value: 'fit-page' }
]);
const canPreview = computed(() => {
if (!props.document) return false;
const ext = props.document.extension?.toLowerCase();
return ['pdf', 'jpg', 'jpeg', 'png', 'gif', 'txt', 'doc', 'docx', 'xls', 'xlsx'].includes(ext);
});
const documentUrl = computed(() => {
// In production, this would come from the document API
return props.document.url || `#`;
});
const documentTitle = computed(() => {
return props.document.name || 'Untitled Document';
});
// Get SVG icon based on document type
const getDocumentIcon = (type, size = 24) => {
let svg = '';
switch (type) {
case 'pdf':
svg = `<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><path d="M9 15h6"></path><path d="M9 11h6"></path></svg>`;
break;
case 'word':
svg = `<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><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>`;
break;
case 'excel':
svg = `<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><rect x="8" y="12" width="8" height="6"></rect><line x1="8" y1="16" x2="16" y2="16"></line><line x1="11" y1="12" x2="11" y2="18"></line></svg>`;
break;
case 'powerpoint':
svg = `<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><rect x="8" y="12" width="8" height="6"></rect></svg>`;
break;
case 'image':
svg = `<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"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>`;
break;
case 'text':
svg = `<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><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>`;
break;
default:
svg = `<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>`;
const documentIcon = computed(() => {
if (!props.document) return 'file';
const ext = props.document.extension?.toLowerCase();
switch (ext) {
case 'pdf': return 'pdf';
case 'doc':
case 'docx': return 'doc';
case 'xls':
case 'xlsx': return 'excel';
case 'ppt':
case 'pptx': return 'powerpoint';
case 'jpg':
case 'jpeg':
case 'png':
case 'gif': return 'image';
case 'txt': return 'text';
default: return 'file';
}
return svg;
};
});
// Methods
const closeViewer = () => {
@ -87,27 +73,43 @@ const printDocument = () => {
emit('print', props.document);
};
const requestAccess = () => {
emit('requestAccess', props.document);
const editDocument = () => {
emit('edit', props.document);
};
const zoomIn = () => {
zoomLevel.value += 10;
};
const zoomOut = () => {
if (zoomLevel.value > 50) {
zoomLevel.value -= 10;
const toggleFullscreen = () => {
isFullscreen.value = !isFullscreen.value;
if (isFullscreen.value) {
document.documentElement.requestFullscreen?.();
} else {
document.exitFullscreen?.();
}
};
const resetZoom = () => {
zoomLevel.value = 100;
const zoomIn = () => {
if (zoomLevel.value < 400) {
zoomLevel.value += 25;
}
};
const goToPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page;
const zoomOut = () => {
if (zoomLevel.value > 25) {
zoomLevel.value -= 25;
}
};
const setZoom = (value) => {
if (typeof value === 'string') {
previewMode.value = value;
} else {
zoomLevel.value = value;
previewMode.value = 'custom';
}
};
const previousPage = () => {
if (currentPage.value > 1) {
currentPage.value--;
}
};
@ -117,149 +119,251 @@ const nextPage = () => {
}
};
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--;
}
const getSvgIcon = (iconType, size = 24) => {
const icons = {
pdf: `<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><path d="M16 13a2 2 0 0 1-2 2h-2v4"></path><path d="M12 17v-4h2a2 2 0 0 1 2 2z"></path></svg>`,
doc: `<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><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><line x1="10" y1="9" x2="8" y2="9"></line></svg>`,
excel: `<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><rect x="8" y="12" width="8" height="6"></rect><line x1="8" y1="16" x2="16" y2="16"></line><line x1="12" y1="12" x2="12" y2="18"></line></svg>`,
powerpoint: `<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><rect x="9" y="12" width="6" height="6"></rect><circle cx="12" cy="15" r="1"></circle></svg>`,
image: `<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"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>`,
text: `<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><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></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>`,
download: `<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="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`,
print: `<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="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>`,
edit: `<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="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>`,
fullscreen: `<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="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path></svg>`,
zoomIn: `<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"><circle cx="11" cy="11" r="8"></circle><path d="M21 21l-4.35-4.35"></path><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>`,
zoomOut: `<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"><circle cx="11" cy="11" r="8"></circle><path d="M21 21l-4.35-4.35"></path><line x1="8" y1="11" x2="14" y2="11"></line></svg>`,
chevronLeft: `<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="15 18 9 12 15 6"></polyline></svg>`,
chevronRight: `<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="9 18 15 12 9 6"></polyline></svg>`
};
return icons[iconType] || icons.file;
};
// Mock viewer loading for demonstration
// Simulate loading
watch(() => props.document, (newDoc) => {
if (newDoc) {
isLoading.value = true;
setTimeout(() => {
isLoading.value = false;
totalPages.value = Math.floor(Math.random() * 20) + 1; // Random page count for demo
}, 1000);
}
}, { immediate: true });
onMounted(() => {
setTimeout(() => {
isLoading.value = false;
viewerReady.value = true;
totalPages.value = 5; // Mock total pages
}, 1000);
// Set up document loading simulation
if (props.document) {
setTimeout(() => {
isLoading.value = false;
}, 1000);
}
});
</script>
<template>
<div
class="dms-document-viewer bg-gray-900 text-white flex flex-col"
:class="viewMode === 'fullscreen' ? 'fixed inset-0 z-50' : 'h-full'"
>
<!-- Toolbar -->
<div class="viewer-toolbar p-2 flex items-center justify-between bg-gray-800">
<div class="flex items-center gap-3">
<button @click="closeViewer" class="p-2 rounded hover:bg-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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>
</button>
<h3 class="font-medium truncate max-w-md">{{ documentTitle }}</h3>
</div>
<div class="flex items-center gap-2">
<!-- Page navigation (for multi-page documents) -->
<div v-if="['pdf', 'word', 'powerpoint'].includes(documentType)" class="flex items-center gap-1">
<button @click="prevPage" class="p-2 rounded hover:bg-gray-700" :disabled="currentPage <= 1">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>
</button>
<span class="text-sm">
{{ currentPage }} / {{ totalPages }}
</span>
<button @click="nextPage" class="p-2 rounded hover:bg-gray-700" :disabled="currentPage >= totalPages">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
</button>
</div>
<!-- Zoom controls -->
<div class="flex items-center gap-1">
<button @click="zoomOut" class="p-2 rounded hover:bg-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
</button>
<span class="text-sm">{{ zoomLevel }}%</span>
<button @click="zoomIn" class="p-2 rounded hover:bg-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
</button>
<button @click="resetZoom" class="p-2 rounded hover:bg-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"></path><path d="M9 21H3v-6"></path><path d="M21 3l-7 7"></path><path d="M3 21l7-7"></path></svg>
</button>
</div>
<!-- Actions -->
<div class="flex items-center gap-1">
<button @click="downloadDocument" class="p-2 rounded hover:bg-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
</button>
<button @click="printDocument" class="p-2 rounded hover:bg-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
</button>
<button @click="requestAccess" class="p-2 rounded hover:bg-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
</button>
</div>
</div>
</div>
<!-- Document content area -->
<div class="document-content flex-1 overflow-auto flex justify-center items-start p-4">
<!-- Loading state -->
<div v-if="isLoading" class="flex flex-col items-center justify-center h-full w-full">
<Loading />
<p class="mt-4 text-gray-400">Loading document...</p>
</div>
<!-- PDF Viewer -->
<div v-else-if="documentType === 'pdf'" class="document-display">
<div class="bg-white p-4 shadow-lg" :style="{ transform: `scale(${zoomLevel/100})` }">
<div class="w-[816px] h-[1056px] flex justify-center items-center border border-gray-300">
<!-- This would be replaced with an actual PDF viewer like vue-pdf-embed or similar -->
<div class="text-gray-800 text-center">
<span v-html="getDocumentIcon('pdf', 64)" class="mx-auto mb-4 block"></span>
<p class="font-medium">PDF Viewer</p>
<p class="text-sm text-gray-500">Page {{ currentPage }} of {{ totalPages }}</p>
<p class="text-sm text-gray-500 mt-2">{{ props.document.name }}</p>
<div v-if="visible" class="document-viewer fixed inset-0 z-50 bg-black bg-opacity-75 flex items-center justify-center">
<div class="viewer-container bg-white dark:bg-gray-900 rounded-lg shadow-2xl max-w-7xl max-h-[95vh] w-full mx-4 flex flex-col">
<!-- Header with toolbar -->
<div class="viewer-header border-b border-gray-200 dark:border-gray-700 p-3">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<span v-html="getSvgIcon(documentIcon, 24)" class="text-blue-600"></span>
<div>
<h3 class="text-lg font-semibold">{{ document?.name }}</h3>
<p class="text-sm text-gray-500">{{ document?.extension?.toUpperCase() }} {{ document?.size }}</p>
</div>
</div>
<div class="flex items-center space-x-2">
<!-- Zoom Controls -->
<div class="flex items-center space-x-1 border border-gray-300 dark:border-gray-600 rounded">
<button
@click="zoomOut"
class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-l"
:disabled="zoomLevel <= 25"
>
<span v-html="getSvgIcon('zoomOut', 16)"></span>
</button>
<select
v-model="zoomLevel"
@change="setZoom($event.target.value)"
class="px-2 py-1 text-sm border-0 focus:ring-0 bg-transparent"
>
<option v-for="option in zoomOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
<button
@click="zoomIn"
class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-r"
:disabled="zoomLevel >= 400"
>
<span v-html="getSvgIcon('zoomIn', 16)"></span>
</button>
</div>
<!-- Page Navigation (for PDFs) -->
<div v-if="document?.extension === 'pdf'" class="flex items-center space-x-1 border border-gray-300 dark:border-gray-600 rounded">
<button
@click="previousPage"
class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-l"
:disabled="currentPage <= 1"
>
<span v-html="getSvgIcon('chevronLeft', 16)"></span>
</button>
<span class="px-2 py-1 text-sm">{{ currentPage }} / {{ totalPages }}</span>
<button
@click="nextPage"
class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-r"
:disabled="currentPage >= totalPages"
>
<span v-html="getSvgIcon('chevronRight', 16)"></span>
</button>
</div>
<!-- Action Buttons -->
<div class="flex items-center space-x-1">
<rs-button size="sm" variant="ghost" @click="downloadDocument">
<span v-html="getSvgIcon('download', 16)" class="mr-1"></span>
Download
</rs-button>
<rs-button size="sm" variant="ghost" @click="printDocument">
<span v-html="getSvgIcon('print', 16)" class="mr-1"></span>
Print
</rs-button>
<rs-button size="sm" variant="ghost" @click="editDocument">
<span v-html="getSvgIcon('edit', 16)" class="mr-1"></span>
Edit
</rs-button>
<rs-button size="sm" variant="ghost" @click="toggleFullscreen">
<span v-html="getSvgIcon('fullscreen', 16)"></span>
</rs-button>
</div>
<button @click="closeViewer" class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">
<span v-html="getSvgIcon('close', 20)"></span>
</button>
</div>
</div>
</div>
<!-- Image Viewer -->
<div v-else-if="documentType === 'image'" class="document-display">
<div :style="{ transform: `scale(${zoomLevel/100})` }">
<img src="https://via.placeholder.com/800x600" alt="Document preview" class="max-w-full shadow-lg" />
<!-- Content Area -->
<div class="viewer-content flex-1 p-4 overflow-auto bg-gray-50 dark:bg-gray-800">
<!-- Loading State -->
<div v-if="isLoading" class="flex items-center justify-center h-full">
<div class="flex flex-col items-center space-y-4">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<p class="text-gray-500">Loading document...</p>
</div>
</div>
</div>
<!-- Office Document Viewer -->
<div v-else-if="['word', 'excel', 'powerpoint'].includes(documentType)" class="document-display">
<div class="bg-white p-4 shadow-lg" :style="{ transform: `scale(${zoomLevel/100})` }">
<div class="w-[816px] h-[1056px] flex justify-center items-center border border-gray-300">
<!-- This would be replaced with an actual Office viewer -->
<div class="text-gray-800 text-center">
<span v-html="getDocumentIcon(documentType, 64)" class="mx-auto mb-4 block"></span>
<p class="font-medium">{{ documentType.charAt(0).toUpperCase() + documentType.slice(1) }} Viewer</p>
<p class="text-sm text-gray-500 mt-2">{{ props.document.name }}</p>
<!-- Document Preview -->
<div v-else-if="canPreview" class="document-preview flex justify-center">
<div class="preview-area bg-white shadow-lg rounded border max-w-full"
:style="{
transform: `scale(${zoomLevel / 100})`,
transformOrigin: 'top center'
}">
<!-- PDF Preview -->
<div v-if="document?.extension === 'pdf'" class="pdf-preview w-[210mm] h-[297mm] p-8 border">
<div class="flex items-center justify-center h-full text-gray-400">
<div class="text-center">
<span v-html="getSvgIcon('pdf', 48)" class="mb-4 block text-red-500"></span>
<p class="text-lg font-semibold">{{ document.name }}</p>
<p class="text-sm">PDF Document Preview</p>
<p class="text-xs mt-2">Page {{ currentPage }} of {{ totalPages }}</p>
</div>
</div>
</div>
<!-- Image Preview -->
<div v-else-if="['jpg', 'jpeg', 'png', 'gif'].includes(document?.extension?.toLowerCase())" class="image-preview">
<img
:src="`/api/documents/${document.id}/preview`"
:alt="document.name"
class="max-w-full h-auto rounded shadow"
@error="$event.target.style.display = 'none'"
/>
</div>
<!-- Office Document Preview -->
<div v-else class="office-preview w-[210mm] h-[297mm] p-8 border bg-white">
<div class="flex items-center justify-center h-full text-gray-400">
<div class="text-center">
<span v-html="getSvgIcon(documentIcon, 48)" class="mb-4 block text-blue-500"></span>
<p class="text-lg font-semibold">{{ document.name }}</p>
<p class="text-sm">{{ document.extension?.toUpperCase() }} Document Preview</p>
<p class="text-xs mt-2">Preview not available for this file type</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Text Viewer -->
<div v-else-if="documentType === 'text'" class="document-display">
<div class="bg-white p-6 shadow-lg text-gray-800 w-[816px]" :style="{ transform: `scale(${zoomLevel/100})` }">
<!-- This would be replaced with actual text content -->
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam auctor ligula nec nisl consectetur, nec tincidunt nisl tincidunt. Duis vitae urna euismod, volutpat nisl ac, malesuada nunc.</p>
<p>Sed vel lectus vel orci ultrices tincidunt. Nulla facilisi. Donec vitae nisi vel elit elementum tincidunt. Sed vel justo vel nisi volutpat tincidunt. Duis vitae urna euismod, volutpat nisl ac, malesuada nunc.</p>
<!-- No Preview Available -->
<div v-else class="no-preview flex items-center justify-center h-full">
<div class="text-center text-gray-500">
<span v-html="getSvgIcon(documentIcon, 64)" class="mb-4 block mx-auto"></span>
<p class="text-lg font-semibold">Preview not available</p>
<p class="text-sm">This file type cannot be previewed</p>
<rs-button @click="downloadDocument" class="mt-4">
<span v-html="getSvgIcon('download', 16)" class="mr-2"></span>
Download to view
</rs-button>
</div>
</div>
</div>
<!-- Generic/Unsupported File -->
<div v-else class="document-display flex flex-col items-center justify-center">
<span v-html="getDocumentIcon('generic', 96)" class="mb-4"></span>
<h3 class="text-lg font-medium mb-2">Preview not available</h3>
<p class="text-gray-400 mb-6">This file type cannot be previewed</p>
<rs-button @click="downloadDocument" color="primary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
Download File
</rs-button>
</div>
</div>
</div>
</template>
<style scoped>
.document-display {
.document-viewer {
backdrop-filter: blur(4px);
}
.viewer-container {
height: 95vh;
}
.viewer-content {
min-height: 0;
}
.preview-area {
transition: transform 0.2s ease;
transform-origin: center top;
}
select:focus {
outline: none;
box-shadow: none;
}
/* Custom scrollbar */
.viewer-content::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.viewer-content::-webkit-scrollbar-track {
background: transparent;
}
.viewer-content::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5);
border-radius: 4px;
}
.viewer-content::-webkit-scrollbar-thumb:hover {
background-color: rgba(156, 163, 175, 0.7);
}
</style>

View File

@ -0,0 +1,102 @@
import { ref } from 'vue';
export const useAsnafMockData = () => {
// Mock data for Asnaf (beneficiaries) module
const mockData = ref([
{
id: 1,
name: 'Ahmad bin Abdullah',
ic: '850123-01-1234',
phone: '019-1234567',
address: 'No. 123, Jalan Merdeka, 15000 Kota Bharu, Kelantan',
category: 'Fakir',
status: 'Active',
registrationDate: '2023-01-15',
lastUpdate: '2023-10-15'
},
{
id: 2,
name: 'Fatimah binti Hassan',
ic: '900615-05-5678',
phone: '013-9876543',
address: 'No. 456, Lorong Parit, 16150 Kubang Kerian, Kelantan',
category: 'Miskin',
status: 'Active',
registrationDate: '2023-02-20',
lastUpdate: '2023-09-30'
},
{
id: 3,
name: 'Omar bin Salleh',
ic: '751203-14-9012',
phone: '017-5554321',
address: 'No. 789, Kampung Tok Guru, 17070 Pasir Mas, Kelantan',
category: 'Gharimin',
status: 'Pending',
registrationDate: '2023-03-10',
lastUpdate: '2023-10-01'
}
]);
const categories = ref([
'Fakir',
'Miskin',
'Amil',
'Muallaf',
'Riqab',
'Gharimin',
'Fi Sabilillah',
'Ibn Sabil'
]);
const statuses = ref([
'Active',
'Pending',
'Suspended',
'Inactive'
]);
// Methods
const getAsnafById = (id) => {
return mockData.value.find(item => item.id === parseInt(id));
};
const getAsnafByCategory = (category) => {
return mockData.value.filter(item => item.category === category);
};
const updateAsnaf = (id, updatedData) => {
const index = mockData.value.findIndex(item => item.id === parseInt(id));
if (index !== -1) {
mockData.value[index] = { ...mockData.value[index], ...updatedData };
return mockData.value[index];
}
return null;
};
const addAsnaf = (newData) => {
const newId = Math.max(...mockData.value.map(item => item.id)) + 1;
const newAsnaf = { id: newId, ...newData };
mockData.value.push(newAsnaf);
return newAsnaf;
};
const deleteAsnaf = (id) => {
const index = mockData.value.findIndex(item => item.id === parseInt(id));
if (index !== -1) {
return mockData.value.splice(index, 1)[0];
}
return null;
};
return {
mockData,
categories,
statuses,
getAsnafById,
getAsnafByCategory,
updateAsnaf,
addAsnaf,
deleteAsnaf
};
};

View File

@ -2,6 +2,7 @@
import { ref, computed, onMounted } from 'vue';
import { useDmsStore } from '~/stores/dms';
import DMSNavigation from '~/components/dms/navigation/DMSNavigation.vue';
import DMSExplorer from '~/components/dms/explorer/DMSExplorer.vue';
// Define page metadata
definePageMeta({
@ -30,6 +31,8 @@ const searchQuery = ref('');
const isSearching = ref(false);
const currentPath = ref('JKR Cawangan Kota Bharu, Kelantan');
const viewMode = ref('explorer'); // explorer, cabinets, list
const selectedItem = ref(null);
const activeTab = ref('all');
// File selection state
const selectedFiles = ref([]);
@ -123,57 +126,41 @@ const formatFileSize = (size) => {
return `${size.toFixed(2)} ${units[i]}`;
};
// Mock data for the example
const mockFiles = [
{
id: 'file1',
name: 'Pembangunan_Sistem_IT_2021.pdf',
type: 'file',
extension: 'pdf',
size: '4MB',
modified: '2021-05-20',
status: 'locked',
info: {
title: 'Projek Pembangunan Sistem IT',
subject: 'Dokumen spesifikasi sistem',
state: 'Kelantan',
date: '2021-05-20',
user: 'Mohd Faizal bin Abdullah',
storeDate: '2021-05-25'
}
},
{
id: 'file2',
name: 'Projek_Jalan_Raya_Kota_Bharu.pdf',
type: 'file',
extension: 'pdf',
size: '5MB',
modified: '2021-06-15',
status: 'unlocked'
},
{
id: 'file3',
name: 'Anggaran_Kos_Projek_MRT3.xlsx',
type: 'file',
extension: 'xlsx',
size: '3MB',
modified: '2021-07-10',
status: 'locked'
},
{
id: 'file4',
name: 'EIA_Empangan_Nenggiri.pdf',
type: 'file',
extension: 'pdf',
size: '15MB',
modified: '2021-04-18',
status: 'locked'
}
// Document category tabs
const documentTabs = [
{ id: 'all', label: 'All Documents', icon: 'folder' },
{ id: 'public', label: 'Public', icon: 'unlock' },
{ id: 'private', label: 'Private', icon: 'lock' },
{ id: 'personal', label: 'Personal', icon: 'user' }
];
// Handle events from explorer
const handleItemSelected = (item) => {
selectedItem.value = item;
};
const handleViewModeChanged = (mode) => {
console.log('View mode changed to:', mode);
};
const handlePathChanged = (path) => {
currentPath.value = path;
};
// Get SVG icon
const getSvgIcon = (iconType, size = 16) => {
const icons = {
folder: `<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="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>`,
unlock: `<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"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 9.9-1"></path></svg>`,
lock: `<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"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>`,
user: `<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 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>`
};
return icons[iconType] || icons.folder;
};
// Lifecycle hooks
onMounted(() => {
// In a real app, we would load the initial data here
// Any initialization logic
});
</script>
@ -183,270 +170,34 @@ onMounted(() => {
<rs-card class="h-full">
<template #body>
<div class="explorer-layout h-full flex flex-col">
<!-- Address bar (Windows Explorer style) -->
<div class="address-bar border-b border-gray-200 dark:border-gray-700 p-2 flex items-center gap-2">
<button class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
</button>
<button class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</button>
<button class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
</button>
<div class="flex-1 flex items-center px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2 text-gray-500"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
<span>{{ currentPath }}</span>
</div>
<div class="relative">
<input
type="text"
placeholder="Search in current folder..."
class="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 w-56"
/>
<span class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</span>
<div class="dms-layout h-full flex flex-col">
<!-- Document Category Tabs -->
<div class="tabs-header border-b border-gray-200 dark:border-gray-700 p-4 bg-white dark:bg-gray-800">
<div class="flex items-center space-x-1">
<button
v-for="tab in documentTabs"
:key="tab.id"
@click="activeTab = tab.id"
class="flex items-center space-x-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors"
:class="activeTab === tab.id
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/20 dark:text-blue-300'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-700'"
>
<span v-html="getSvgIcon(tab.icon, 16)"></span>
<span>{{ tab.label }}</span>
</button>
</div>
</div>
<!-- View options toolbar -->
<div class="view-toolbar border-b border-gray-200 dark:border-gray-700 p-2 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="flex items-center bg-gray-100 dark:bg-gray-800 rounded-md overflow-hidden border border-gray-200 dark:border-gray-700">
<button
@click="changeViewMode('explorer')"
class="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center"
:class="viewMode === 'explorer' ? 'bg-white dark:bg-gray-700 shadow-sm' : ''"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>
<span class="text-sm">Explorer View</span>
</button>
<button
@click="changeViewMode('cabinets')"
class="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center"
:class="viewMode === 'cabinets' ? 'bg-white dark:bg-gray-700 shadow-sm' : ''"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>
<span class="text-sm">Cabinets View</span>
</button>
<button
@click="changeViewMode('list')"
class="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center"
:class="viewMode === 'list' ? 'bg-white dark:bg-gray-700 shadow-sm' : ''"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>
<span class="text-sm">List View</span>
</button>
</div>
</div>
<div class="flex items-center gap-3">
<rs-button size="sm" color="primary" class="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><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>
Upload
</rs-button>
<rs-button size="sm" color="secondary" class="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
Create New
</rs-button>
<div class="flex items-center border border-gray-200 dark:border-gray-700 rounded-md px-3 py-1 bg-white dark:bg-gray-800">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M11 5h10"></path><path d="M11 9h10"></path><path d="M11 13h10"></path><path d="M3 17h18"></path><path d="M3 5l4 4"></path><path d="M3 13l4-4"></path></svg>
<span class="text-sm">Sort</span>
</div>
</div>
</div>
<!-- Main content area -->
<div class="flex-1 flex overflow-hidden">
<!-- Left sidebar navigation -->
<DMSNavigation />
<!-- File explorer main content -->
<div class="flex-1 overflow-y-auto">
<!-- List view -->
<table class="min-w-full">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-6">
<input
type="checkbox"
class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
@click="selectAllFiles"
>
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Nama
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Jenis
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Saiz
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Modified Date
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
<tr
v-for="file in mockFiles"
:key="file.id"
@click="viewFile(file)"
class="hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
:class="{ 'bg-blue-50 dark:bg-blue-900/20': isFileSelected(file) }"
>
<td class="px-4 py-3 whitespace-nowrap">
<input
type="checkbox"
class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
:checked="isFileSelected(file)"
@click.stop="toggleFileSelection(file)"
>
</td>
<td class="px-4 py-3 whitespace-nowrap">
<div class="flex items-center">
<span class="text-gray-500 mr-2">
<svg v-if="file.extension === 'pdf'" xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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>
<path d="M9 15h6"></path>
<path d="M9 11h6"></path>
</svg>
<svg v-else-if="file.extension === 'xlsx'" xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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>
<rect x="8" y="12" width="8" height="6"></rect>
<line x1="8" y1="16" x2="16" y2="16"></line>
<line x1="11" y1="12" x2="11" y2="18"></line>
</svg>
</span>
<div class="flex items-center">
<span>{{ file.name }}</span>
<span v-if="file.status === 'locked'" class="ml-2 text-red-500">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
</span>
</div>
</div>
</td>
<td class="px-4 py-3 whitespace-nowrap">
<span class="uppercase">{{ file.extension }}</span>
</td>
<td class="px-4 py-3 whitespace-nowrap">
{{ file.size }}
</td>
<td class="px-4 py-3 whitespace-nowrap">
{{ file.modified }}
</td>
</tr>
</tbody>
</table>
</div>
<!-- Right sidebar for document details -->
<div v-if="currentDocument" class="w-72 border-l border-gray-200 dark:border-gray-700 overflow-y-auto bg-gray-50 dark:bg-gray-800">
<div class="p-4">
<div class="text-center mb-4 bg-white dark:bg-gray-700 p-8 rounded shadow-sm">
<div v-if="currentDocument.extension === 'pdf'" class="flex justify-center mb-4">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" 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>
<path d="M9 15h6"></path>
<path d="M9 11h6"></path>
</svg>
</div>
<div v-else-if="currentDocument.extension === 'xlsx'" class="flex justify-center mb-4">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#1D6F42" stroke-width="1" 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" fill="white"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<rect x="8" y="12" width="8" height="6" fill="#1D6F42"></rect>
<line x1="8" y1="16" x2="16" y2="16" stroke="white"></line>
<line x1="11" y1="12" x2="11" y2="18" stroke="white"></line>
</svg>
</div>
<h2 class="text-lg font-medium">{{ currentDocument.name }}</h2>
</div>
<div class="border-t border-gray-200 dark:border-gray-700 pt-4">
<h3 class="text-lg font-medium mb-3">Document Information</h3>
<div class="grid grid-cols-3 gap-2 mb-2">
<div class="col-span-1 text-gray-500 dark:text-gray-400">Title:</div>
<div class="col-span-2 text-right">{{ currentDocument.info?.title || 'N/A' }}</div>
</div>
<div class="grid grid-cols-3 gap-2 mb-2">
<div class="col-span-1 text-gray-500 dark:text-gray-400">Subject:</div>
<div class="col-span-2 text-right">{{ currentDocument.info?.subject || 'N/A' }}</div>
</div>
<div class="grid grid-cols-3 gap-2 mb-2">
<div class="col-span-1 text-gray-500 dark:text-gray-400">State:</div>
<div class="col-span-2 text-right">{{ currentDocument.info?.state || 'N/A' }}</div>
</div>
<div class="grid grid-cols-3 gap-2 mb-2">
<div class="col-span-1 text-gray-500 dark:text-gray-400">Date:</div>
<div class="col-span-2 text-right">{{ currentDocument.info?.date || currentDocument.modified }}</div>
</div>
</div>
<div class="border-t border-gray-200 dark:border-gray-700 pt-4 mt-4">
<h3 class="text-lg font-medium mb-3">Document Details</h3>
<div class="grid grid-cols-3 gap-2 mb-2">
<div class="col-span-1 text-gray-500 dark:text-gray-400">File Name:</div>
<div class="col-span-2 text-right">{{ currentDocument.name }}</div>
</div>
<div class="grid grid-cols-3 gap-2 mb-2">
<div class="col-span-1 text-gray-500 dark:text-gray-400">Type:</div>
<div class="col-span-2 text-right">{{ currentDocument.extension.toUpperCase() }}</div>
</div>
<div class="grid grid-cols-3 gap-2 mb-2">
<div class="col-span-1 text-gray-500 dark:text-gray-400">Size:</div>
<div class="col-span-2 text-right">{{ currentDocument.size }}</div>
</div>
<div v-if="currentDocument.info?.user" class="grid grid-cols-3 gap-2 mb-2">
<div class="col-span-1 text-gray-500 dark:text-gray-400">Author:</div>
<div class="col-span-2 text-right">{{ currentDocument.info.user }}</div>
</div>
<div v-if="currentDocument.info?.storeDate" class="grid grid-cols-3 gap-2 mb-2">
<div class="col-span-1 text-gray-500 dark:text-gray-400">Date modified:</div>
<div class="col-span-2 text-right">{{ currentDocument.info.storeDate }}</div>
</div>
</div>
<div class="flex gap-2 mt-6">
<rs-button size="sm" color="primary" block class="flex-1">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
Preview
</rs-button>
<rs-button size="sm" color="secondary" block class="flex-1">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
Download
</rs-button>
</div>
<rs-button v-if="currentDocument.status === 'locked'" size="sm" color="default" block class="mt-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
Request Access
</rs-button>
</div>
</div>
<!-- Explorer Component -->
<div class="explorer-container flex-1 overflow-hidden">
<DMSExplorer
:initial-path="'/'"
:view-mode="'list'"
@item-selected="handleItemSelected"
@view-mode-changed="handleViewModeChanged"
@path-changed="handlePathChanged"
/>
</div>
</div>
</template>
@ -456,10 +207,23 @@ onMounted(() => {
<style scoped>
.dms-page {
height: calc(100vh - 64px - 48px - 32px); /* Adjust based on your layout */
height: calc(100vh - 64px);
}
.explorer-layout {
height: calc(100vh - 200px); /* Adjust based on your layout */
.dms-layout {
height: 100%;
}
.explorer-container {
min-height: 0;
}
/* Ensure smooth transitions */
.tabs-header button {
transition: all 0.2s ease;
}
.tabs-header button:hover {
transform: translateY(-1px);
}
</style>