EDMS/components/dms/explorer/DMSExplorer.vue
2025-05-30 17:45:37 +08:00

883 lines
40 KiB
Vue

<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({
initialPath: {
type: String,
default: '/'
},
viewMode: {
type: String,
default: 'list', // list, grid, details
}
});
// Emits for events
const emit = defineEmits(['itemSelected', 'viewModeChanged', 'pathChanged']);
// Component state
const currentPath = ref(props.initialPath);
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([]);
const router = useRouter();
// Computed properties
const currentItems = computed(() => {
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(() => {
// Split the path and create breadcrumb items
if (currentPath.value === '/') return [{ name: 'Home', path: '/' }];
const paths = currentPath.value.split('/').filter(Boolean);
let breadcrumbPath = '';
return [
{ name: 'Home', path: '/' },
...paths.map(segment => {
breadcrumbPath += `/${segment}`;
return {
name: segment.charAt(0).toUpperCase() + segment.slice(1),
path: breadcrumbPath
};
})
];
});
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;
try {
// Replace with actual API call
// const { data } = await useFetch('/api/dms/items', {
// params: { path: currentPath.value }
// });
// allItems.value = data.value;
// Mock data for development
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate API delay
// Enhanced mock data with more metadata
allItems.value = [
{
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);
// Show error notification
} finally {
isLoading.value = false;
}
};
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();
};
const navigateToItem = (item) => {
if (item.type === 'file') {
selectItem(item);
return;
}
let newPath;
if (item.type === 'cabinet') newPath = `/${item.id}`;
else if (item.type === 'drawer') newPath = `/cabinet1/${item.id}`;
else if (item.type === 'folder') newPath = `/cabinet1/drawer1/${item.id}`;
else if (item.type === 'subfolder') newPath = `/cabinet1/drawer1/folder1/${item.id}`;
else newPath = currentPath.value;
navigateTo(newPath);
};
const toggleTreeItem = (path) => {
treeExpanded.value[path] = !treeExpanded.value[path];
};
const changeViewMode = (mode) => {
viewMode.value = 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;
};
// Get icon for item type
const getItemIcon = (item) => {
switch (item.type) {
case 'cabinet': return 'cabinet';
case 'drawer': return 'folder';
case 'folder': return 'folder';
case 'subfolder': return 'folder';
case 'file':
if (item.extension === 'pdf') return 'pdf';
if (['doc', 'docx'].includes(item.extension)) return 'doc';
if (['xls', 'xlsx'].includes(item.extension)) return 'sheet';
return 'file';
default: return 'folder';
}
};
// Get SVG path based on icon type
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
onMounted(() => {
loadItems();
});
</script>
<template>
<div class="dms-explorer bg-gray-50 dark:bg-gray-900 h-full flex flex-col">
<!-- 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>
<!-- 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 bg-white dark:bg-gray-800">
<div class="tree-item p-1">
<div
@click="navigateTo('/')"
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' : ''"
>
<span v-html="getSvgIcon('home', 18)"></span>
<span class="font-medium">Home</span>
</div>
</div>
<!-- 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-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' : ''"
>
<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="ml-6">
<div
v-for="drawer in allItems.filter(i => i.parentPath === `/${cabinet.id}`)"
:key="drawer.id"
class="tree-item"
>
<div
@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' : ''"
>
<span v-html="getSvgIcon('folder', 16)"></span>
<span class="text-xs truncate">{{ drawer.name }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Content area -->
<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">
<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">
<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 -->
<div v-else-if="viewMode === 'list'" class="p-4">
<div
v-for="item in currentItems"
:key="item.id"
@click="navigateToItem(item)"
@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 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-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)"
@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 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="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 sticky top-0">
<tr>
<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">
<tr
v-for="item in currentItems"
:key="item.id"
@click="navigateToItem(item)"
@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="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">
<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.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>
</table>
</div>
</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>
<!-- 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>
<style scoped>
.dms-explorer {
height: calc(100vh - 64px);
}
/* Ensure scrollbars look nice across browsers */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(156, 163, 175, 0.7);
}
</style>