EDMS/components/dms/preview/DocumentInfoModal.vue
2025-06-05 14:57:08 +08:00

437 lines
15 KiB
Vue

<template>
<div
v-if="visible"
class="fixed inset-0 z-50 bg-black bg-opacity-50 flex items-center justify-center"
@click="closeModal"
>
<div
class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[80vh] overflow-hidden"
@click.stop
>
<!-- Header -->
<div class="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100">
Document Information
</h2>
<button
@click="closeModal"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<Icon name="mdi:close" class="w-6 h-6" />
</button>
</div>
<!-- Content -->
<div class="p-6 overflow-y-auto max-h-[60vh]">
<!-- Document Details -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Basic Information -->
<div class="space-y-4">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Basic Information</h3>
<div class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Name</label>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ document.name }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Type</label>
<div class="mt-1 flex items-center space-x-2">
<Icon
:name="getFileTypeIcon(document.name)"
:class="['w-4 h-4', getFileTypeColor(document.name)]"
/>
<span class="text-sm text-gray-900 dark:text-gray-100">
{{ getFileTypeName(document.name) }}
</span>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Size</label>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ formatFileSize(document.size) }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Last Modified</label>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ formatDate(document.lastModified) }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Owner</label>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ document.owner || 'Unknown' }}</p>
</div>
</div>
</div>
<!-- Access & Security -->
<div class="space-y-4">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Access & Security</h3>
<div class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Access Level</label>
<div class="mt-1">
<span :class="getAccessLevelBadge(document.accessLevel).class"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium">
{{ getAccessLevelBadge(document.accessLevel).text }}
</span>
</div>
</div>
<div v-if="document.permissions">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Permissions</label>
<div class="mt-1 flex flex-wrap gap-1">
<span
v-for="permission in document.permissions"
:key="permission"
class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400"
>
{{ permission }}
</span>
</div>
</div>
<div v-if="document.encryption">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Encryption</label>
<div class="mt-1 flex items-center space-x-2">
<Icon name="mdi:shield-check" class="w-4 h-4 text-green-500" />
<span class="text-sm text-gray-900 dark:text-gray-100">{{ document.encryption }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Tags Section -->
<div class="mt-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">Tags</h3>
<div class="space-y-3">
<!-- Existing Tags -->
<div v-if="documentTags.length > 0" class="flex flex-wrap gap-2">
<span
v-for="tag in documentTags"
:key="tag"
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300"
>
{{ tag }}
<button
@click="removeTag(tag)"
class="ml-2 text-gray-500 hover:text-red-500"
>
<Icon name="mdi:close" class="w-3 h-3" />
</button>
</span>
</div>
<!-- Add New Tag -->
<div class="flex items-center space-x-2">
<input
v-model="newTag"
@keydown.enter="addTag"
type="text"
placeholder="Add a tag..."
class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100
focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
<button
@click="addTag"
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm"
>
Add
</button>
</div>
</div>
</div>
<!-- Annotations Summary -->
<div v-if="annotations.length > 0" class="mt-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">Annotations</h3>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<div class="grid grid-cols-3 gap-4 text-center">
<div>
<div class="text-2xl font-bold text-yellow-600 dark:text-yellow-400">
{{ getAnnotationCount('highlight') }}
</div>
<div class="text-sm text-gray-600 dark:text-gray-400">Highlights</div>
</div>
<div>
<div class="text-2xl font-bold text-blue-600 dark:text-blue-400">
{{ getAnnotationCount('comment') }}
</div>
<div class="text-sm text-gray-600 dark:text-gray-400">Comments</div>
</div>
<div>
<div class="text-2xl font-bold text-green-600 dark:text-green-400">
{{ getAnnotationCount('draw') }}
</div>
<div class="text-sm text-gray-600 dark:text-gray-400">Drawings</div>
</div>
</div>
</div>
</div>
<!-- Version History (if available) -->
<div v-if="document.versions" class="mt-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">Version History</h3>
<div class="space-y-2">
<div
v-for="version in document.versions.slice(0, 5)"
:key="version.id"
class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"
>
<div class="flex items-center space-x-3">
<Icon name="mdi:history" class="w-4 h-4 text-gray-500" />
<div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
Version {{ version.number }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ formatDate(version.timestamp) }} by {{ version.author }}
</div>
</div>
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ formatFileSize(version.size) }}
</div>
</div>
</div>
</div>
<!-- File Location -->
<div class="mt-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">Location</h3>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
<div class="flex items-center space-x-2 text-sm text-gray-700 dark:text-gray-300">
<Icon name="mdi:folder" class="w-4 h-4" />
<span>{{ document.path || '/Documents/' + document.name }}</span>
</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="flex items-center justify-end space-x-3 p-6 border-t border-gray-200 dark:border-gray-700">
<button
@click="closeModal"
class="px-4 py-2 text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100"
>
Close
</button>
<button
@click="downloadDocument"
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
Download
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
const props = defineProps({
visible: {
type: Boolean,
default: false
},
document: {
type: Object,
required: true
},
annotations: {
type: Array,
default: () => []
}
});
const emit = defineEmits([
'update:visible',
'tag-added',
'tag-removed'
]);
// State
const newTag = ref('');
const documentTags = ref([]);
// Computed
const isVisible = computed({
get() {
return props.visible;
},
set(value) {
emit('update:visible', value);
}
});
// Methods
const closeModal = () => {
isVisible.value = false;
};
const getFileTypeIcon = (fileName) => {
if (!fileName) return 'mdi:file-document';
const extension = fileName.split('.').pop()?.toLowerCase();
const iconMap = {
pdf: 'mdi:file-pdf-box',
doc: 'mdi:file-word-box',
docx: 'mdi:file-word-box',
xls: 'mdi:file-excel-box',
xlsx: 'mdi:file-excel-box',
ppt: 'mdi:file-powerpoint-box',
pptx: 'mdi:file-powerpoint-box',
txt: 'mdi:file-document-outline',
md: 'mdi:language-markdown',
jpg: 'mdi:file-image',
jpeg: 'mdi:file-image',
png: 'mdi:file-image',
gif: 'mdi:file-image',
default: 'mdi:file-document'
};
return iconMap[extension] || iconMap.default;
};
const getFileTypeColor = (fileName) => {
if (!fileName) return 'text-gray-400';
const extension = fileName.split('.').pop()?.toLowerCase();
const colorMap = {
pdf: 'text-red-400',
doc: 'text-blue-400',
docx: 'text-blue-400',
xls: 'text-green-400',
xlsx: 'text-green-400',
ppt: 'text-orange-400',
pptx: 'text-orange-400',
txt: 'text-gray-400',
md: 'text-purple-400',
jpg: 'text-purple-400',
jpeg: 'text-purple-400',
png: 'text-purple-400',
gif: 'text-purple-400',
default: 'text-gray-400'
};
return colorMap[extension] || colorMap.default;
};
const getFileTypeName = (fileName) => {
if (!fileName) return 'Unknown';
const extension = fileName.split('.').pop()?.toLowerCase();
const nameMap = {
pdf: 'PDF Document',
doc: 'Word Document',
docx: 'Word Document',
xls: 'Excel Spreadsheet',
xlsx: 'Excel Spreadsheet',
ppt: 'PowerPoint Presentation',
pptx: 'PowerPoint Presentation',
txt: 'Text File',
md: 'Markdown Document',
jpg: 'JPEG Image',
jpeg: 'JPEG Image',
png: 'PNG Image',
gif: 'GIF Image',
default: 'Document'
};
return nameMap[extension] || nameMap.default;
};
const getAccessLevelBadge = (accessLevel) => {
switch (accessLevel) {
case 'public':
return { class: 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400', text: 'Public' };
case 'department':
return { class: 'bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400', text: 'Department' };
case 'private':
return { class: 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400', text: 'Private' };
case 'restricted':
return { class: 'bg-orange-100 text-orange-800 dark:bg-orange-900/20 dark:text-orange-400', text: 'Restricted' };
default:
return { class: 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400', text: 'Unknown' };
}
};
const formatFileSize = (size) => {
if (typeof size === 'string') return size;
if (!size) return '0 B';
const units = ['B', 'KB', 'MB', 'GB'];
let index = 0;
while (size >= 1024 && index < units.length - 1) {
size /= 1024;
index++;
}
return `${size.toFixed(1)} ${units[index]}`;
};
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
};
const getAnnotationCount = (type) => {
return props.annotations.filter(annotation => annotation.type === type).length;
};
const addTag = () => {
if (newTag.value.trim() && !documentTags.value.includes(newTag.value.trim())) {
const tag = newTag.value.trim();
documentTags.value.push(tag);
emit('tag-added', tag);
newTag.value = '';
}
};
const removeTag = (tag) => {
const index = documentTags.value.indexOf(tag);
if (index > -1) {
documentTags.value.splice(index, 1);
emit('tag-removed', tag);
}
};
const downloadDocument = () => {
// Create download link
const link = document.createElement('a');
link.href = `/api/documents/${props.document.id}/download`;
link.download = props.document.name;
link.click();
};
// Watch for document changes
watch(() => props.document, (newDocument) => {
if (newDocument) {
documentTags.value = [...(newDocument.tags || [])];
}
}, { immediate: true });
</script>
<style scoped>
/* Modal animations */
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s ease;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
.modal-content-enter-active,
.modal-content-leave-active {
transition: transform 0.3s ease;
}
.modal-content-enter-from,
.modal-content-leave-to {
transform: scale(0.95) translateY(-20px);
}
</style>