generated from corrad-software/corrad-af-2024
883 lines
40 KiB
Vue
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> |