generated from corrad-software/corrad-af-2024
941 lines
26 KiB
Vue
941 lines
26 KiB
Vue
<template>
|
|
<div
|
|
v-if="visible"
|
|
class="fixed inset-0 z-50 bg-black bg-opacity-90 flex items-center justify-center"
|
|
@keydown.esc="closePreview"
|
|
tabindex="-1"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
:aria-label="`Preview of ${document?.name}`"
|
|
>
|
|
<!-- Loading State -->
|
|
<div v-if="isLoading" class="flex flex-col items-center text-white">
|
|
<Icon name="mdi:loading" class="w-12 h-12 animate-spin mb-4" />
|
|
<p class="text-lg">Loading document...</p>
|
|
<div class="w-64 bg-gray-700 rounded-full h-2 mt-4">
|
|
<div
|
|
class="bg-blue-500 h-2 rounded-full transition-all duration-300"
|
|
:style="{ width: `${loadingProgress}%` }"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div v-else-if="hasError" class="text-center text-white max-w-md">
|
|
<Icon name="mdi:alert-circle" class="w-16 h-16 mx-auto mb-4 text-red-400" />
|
|
<h3 class="text-xl font-semibold mb-2">Unable to Preview Document</h3>
|
|
<p class="text-gray-300 mb-6">{{ errorMessage }}</p>
|
|
<div class="flex gap-3 justify-center">
|
|
<button
|
|
@click="downloadDocument"
|
|
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg flex items-center gap-2"
|
|
>
|
|
<Icon name="mdi:download" class="w-4 h-4" />
|
|
Download Instead
|
|
</button>
|
|
<button
|
|
@click="closePreview"
|
|
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 rounded-lg"
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Preview Content -->
|
|
<div v-else class="w-full h-full flex flex-col relative">
|
|
<!-- Header Bar -->
|
|
<div class="bg-gray-900 text-white p-4 flex items-center justify-between border-b border-gray-700">
|
|
<div class="flex items-center space-x-4">
|
|
<Icon
|
|
:name="getFileTypeIcon(document.name)"
|
|
:class="['w-6 h-6', getFileTypeColor(document.name)]"
|
|
/>
|
|
<div>
|
|
<h2 class="text-lg font-semibold truncate max-w-md">{{ document.name }}</h2>
|
|
<p class="text-sm text-gray-400">
|
|
{{ formatFileSize(document.size) }} • Modified {{ formatDate(document.lastModified) }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-2">
|
|
<!-- Mode Toggle -->
|
|
<div class="inline-flex rounded-lg border border-gray-600 p-1 bg-gray-800">
|
|
<button
|
|
@click="previewMode = 'view'"
|
|
:class="[
|
|
'px-3 py-1.5 text-sm font-medium rounded-md transition-all',
|
|
previewMode === 'view'
|
|
? 'bg-blue-600 text-white'
|
|
: 'text-gray-300 hover:text-white hover:bg-gray-700'
|
|
]"
|
|
>
|
|
<Icon name="mdi:eye" class="w-4 h-4 mr-1" />
|
|
View
|
|
</button>
|
|
<button
|
|
v-if="canEdit"
|
|
@click="previewMode = 'edit'"
|
|
:class="[
|
|
'px-3 py-1.5 text-sm font-medium rounded-md transition-all',
|
|
previewMode === 'edit'
|
|
? 'bg-blue-600 text-white'
|
|
: 'text-gray-300 hover:text-white hover:bg-gray-700'
|
|
]"
|
|
>
|
|
<Icon name="mdi:pencil" class="w-4 h-4 mr-1" />
|
|
Edit
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Close Button -->
|
|
<button
|
|
@click="closePreview"
|
|
class="p-2 hover:bg-gray-800 rounded-lg transition-colors"
|
|
aria-label="Close preview"
|
|
>
|
|
<Icon name="mdi:close" class="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content Area -->
|
|
<div class="flex-1 relative overflow-hidden">
|
|
<!-- PDF Viewer -->
|
|
<PDFViewer
|
|
v-if="fileType === 'pdf'"
|
|
:document="document"
|
|
:mode="previewMode"
|
|
:annotations="annotations"
|
|
@annotation-added="handleAnnotationAdded"
|
|
@annotation-updated="handleAnnotationUpdated"
|
|
@annotation-deleted="handleAnnotationDeleted"
|
|
@page-changed="handlePageChanged"
|
|
class="w-full h-full"
|
|
/>
|
|
|
|
<!-- Document Viewer (DOCX, DOC) -->
|
|
<DocumentViewer
|
|
v-else-if="['docx', 'doc'].includes(fileType)"
|
|
:document="document"
|
|
:mode="previewMode"
|
|
:content="documentContent"
|
|
@content-changed="handleContentChanged"
|
|
@save-requested="handleSaveDocument"
|
|
class="w-full h-full"
|
|
/>
|
|
|
|
<!-- Spreadsheet Viewer (XLSX, XLS) -->
|
|
<SpreadsheetViewer
|
|
v-else-if="['xlsx', 'xls'].includes(fileType)"
|
|
:document="document"
|
|
:mode="previewMode"
|
|
:data="spreadsheetData"
|
|
@data-changed="handleDataChanged"
|
|
@save-requested="handleSaveDocument"
|
|
class="w-full h-full"
|
|
/>
|
|
|
|
<!-- Image Viewer -->
|
|
<ImageViewer
|
|
v-else-if="['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileType)"
|
|
:document="document"
|
|
:zoom="imageZoom"
|
|
@zoom-changed="imageZoom = $event"
|
|
class="w-full h-full"
|
|
/>
|
|
|
|
<!-- Text Viewer -->
|
|
<TextViewer
|
|
v-else-if="['txt', 'md', 'json', 'xml', 'csv'].includes(fileType)"
|
|
:document="document"
|
|
:mode="previewMode"
|
|
:content="textContent"
|
|
@content-changed="handleContentChanged"
|
|
@save-requested="handleSaveDocument"
|
|
class="w-full h-full"
|
|
/>
|
|
|
|
<!-- Unsupported File Type -->
|
|
<div v-else class="flex items-center justify-center h-full text-white">
|
|
<div class="text-center">
|
|
<Icon name="mdi:file-question" class="w-24 h-24 mx-auto mb-4 text-gray-400" />
|
|
<h3 class="text-xl font-semibold mb-2">Preview Not Available</h3>
|
|
<p class="text-gray-400 mb-6">
|
|
This file type ({{ fileType?.toUpperCase() }}) cannot be previewed.
|
|
</p>
|
|
<button
|
|
@click="downloadDocument"
|
|
class="px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg flex items-center gap-2 mx-auto"
|
|
>
|
|
<Icon name="mdi:download" class="w-5 h-5" />
|
|
Download File
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Floating Toolbar -->
|
|
<div
|
|
v-if="!isLoading && !hasError && fileType"
|
|
class="absolute bottom-6 left-1/2 transform -translate-x-1/2 bg-gray-800 rounded-full px-4 py-2 shadow-lg border border-gray-600 flex items-center space-x-1"
|
|
>
|
|
<!-- Zoom Controls (for applicable file types) -->
|
|
<template v-if="['pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileType)">
|
|
<button
|
|
@click="zoomOut"
|
|
:disabled="currentZoom <= 25"
|
|
class="p-2 hover:bg-gray-700 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
|
title="Zoom Out"
|
|
>
|
|
<Icon name="mdi:magnify-minus" class="w-4 h-4 text-white" />
|
|
</button>
|
|
|
|
<div class="px-3 py-1 text-sm text-white min-w-16 text-center">
|
|
{{ currentZoom }}%
|
|
</div>
|
|
|
|
<button
|
|
@click="zoomIn"
|
|
:disabled="currentZoom >= 300"
|
|
class="p-2 hover:bg-gray-700 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
|
title="Zoom In"
|
|
>
|
|
<Icon name="mdi:magnify-plus" class="w-4 h-4 text-white" />
|
|
</button>
|
|
|
|
<div class="w-px h-6 bg-gray-600 mx-2"></div>
|
|
</template>
|
|
|
|
<!-- Page Navigation (for PDFs) -->
|
|
<template v-if="fileType === 'pdf' && totalPages > 1">
|
|
<button
|
|
@click="previousPage"
|
|
:disabled="currentPage <= 1"
|
|
class="p-2 hover:bg-gray-700 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
|
title="Previous Page"
|
|
>
|
|
<Icon name="mdi:chevron-left" class="w-4 h-4 text-white" />
|
|
</button>
|
|
|
|
<div class="px-3 py-1 text-sm text-white">
|
|
{{ currentPage }} / {{ totalPages }}
|
|
</div>
|
|
|
|
<button
|
|
@click="nextPage"
|
|
:disabled="currentPage >= totalPages"
|
|
class="p-2 hover:bg-gray-700 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
|
title="Next Page"
|
|
>
|
|
<Icon name="mdi:chevron-right" class="w-4 h-4 text-white" />
|
|
</button>
|
|
|
|
<div class="w-px h-6 bg-gray-600 mx-2"></div>
|
|
</template>
|
|
|
|
<!-- Annotation Tools (for PDFs) -->
|
|
<template v-if="fileType === 'pdf'">
|
|
<button
|
|
@click="toggleAnnotationMode('highlight')"
|
|
:class="[
|
|
'p-2 rounded-lg transition-colors',
|
|
annotationMode === 'highlight'
|
|
? 'bg-yellow-600 text-white'
|
|
: 'hover:bg-gray-700 text-white'
|
|
]"
|
|
title="Highlight Text"
|
|
>
|
|
<Icon name="mdi:marker" class="w-4 h-4" />
|
|
</button>
|
|
|
|
<button
|
|
@click="toggleAnnotationMode('comment')"
|
|
:class="[
|
|
'p-2 rounded-lg transition-colors',
|
|
annotationMode === 'comment'
|
|
? 'bg-blue-600 text-white'
|
|
: 'hover:bg-gray-700 text-white'
|
|
]"
|
|
title="Add Comment"
|
|
>
|
|
<Icon name="mdi:comment" class="w-4 h-4" />
|
|
</button>
|
|
|
|
<button
|
|
@click="toggleAnnotationMode('draw')"
|
|
:class="[
|
|
'p-2 rounded-lg transition-colors',
|
|
annotationMode === 'draw'
|
|
? 'bg-green-600 text-white'
|
|
: 'hover:bg-gray-700 text-white'
|
|
]"
|
|
title="Draw"
|
|
>
|
|
<Icon name="mdi:draw" class="w-4 h-4" />
|
|
</button>
|
|
|
|
<div class="w-px h-6 bg-gray-600 mx-2"></div>
|
|
</template>
|
|
|
|
<!-- Main Actions -->
|
|
<button
|
|
@click="toggleFullscreen"
|
|
class="p-2 hover:bg-gray-700 rounded-lg"
|
|
:title="isFullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'"
|
|
>
|
|
<Icon
|
|
:name="isFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen'"
|
|
class="w-4 h-4 text-white"
|
|
/>
|
|
</button>
|
|
|
|
<button
|
|
@click="printDocument"
|
|
class="p-2 hover:bg-gray-700 rounded-lg"
|
|
title="Print"
|
|
>
|
|
<Icon name="mdi:printer" class="w-4 h-4 text-white" />
|
|
</button>
|
|
|
|
<button
|
|
@click="downloadDocument"
|
|
class="p-2 hover:bg-gray-700 rounded-lg"
|
|
title="Download"
|
|
>
|
|
<Icon name="mdi:download" class="w-4 h-4 text-white" />
|
|
</button>
|
|
|
|
<button
|
|
v-if="canShare"
|
|
@click="shareDocument"
|
|
class="p-2 hover:bg-gray-700 rounded-lg"
|
|
title="Share"
|
|
>
|
|
<Icon name="mdi:share" class="w-4 h-4 text-white" />
|
|
</button>
|
|
|
|
<button
|
|
@click="showDocumentInfo = true"
|
|
class="p-2 hover:bg-gray-700 rounded-lg"
|
|
title="Document Info"
|
|
>
|
|
<Icon name="mdi:information" class="w-4 h-4 text-white" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Save Indicator -->
|
|
<div
|
|
v-if="isSaving"
|
|
class="absolute top-4 right-4 bg-blue-600 text-white px-4 py-2 rounded-lg shadow-lg flex items-center space-x-2"
|
|
>
|
|
<Icon name="mdi:loading" class="w-4 h-4 animate-spin" />
|
|
<span>Saving...</span>
|
|
</div>
|
|
|
|
<!-- Auto-save Indicator -->
|
|
<div
|
|
v-if="showAutoSaveIndicator"
|
|
class="absolute top-4 right-4 bg-green-600 text-white px-4 py-2 rounded-lg shadow-lg flex items-center space-x-2 transition-opacity duration-500"
|
|
:class="{ 'opacity-0': !showAutoSaveIndicator }"
|
|
>
|
|
<Icon name="mdi:check" class="w-4 h-4" />
|
|
<span>Auto-saved</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Document Info Modal -->
|
|
<DocumentInfoModal
|
|
v-model:visible="showDocumentInfo"
|
|
:document="document"
|
|
:annotations="annotations"
|
|
@tag-added="handleTagAdded"
|
|
@tag-removed="handleTagRemoved"
|
|
/>
|
|
|
|
<!-- Share Modal -->
|
|
<ShareDocumentModal
|
|
v-model:visible="showShareModal"
|
|
:document="document"
|
|
@shared="handleDocumentShared"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
|
|
|
|
// Component imports
|
|
import PDFViewer from './PDFViewer.vue';
|
|
import DocumentViewer from './DocumentViewer.vue';
|
|
import SpreadsheetViewer from './SpreadsheetViewer.vue';
|
|
import ImageViewer from './ImageViewer.vue';
|
|
import TextViewer from './TextViewer.vue';
|
|
import DocumentInfoModal from './DocumentInfoModal.vue';
|
|
import ShareDocumentModal from './ShareDocumentModal.vue';
|
|
|
|
// Props
|
|
const props = defineProps({
|
|
visible: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
document: {
|
|
type: Object,
|
|
default: null
|
|
},
|
|
initialMode: {
|
|
type: String,
|
|
default: 'view' // 'view' | 'edit'
|
|
}
|
|
});
|
|
|
|
// Emits
|
|
const emit = defineEmits([
|
|
'close',
|
|
'document-updated',
|
|
'annotation-added',
|
|
'annotation-updated',
|
|
'annotation-deleted',
|
|
'document-shared',
|
|
'tag-added',
|
|
'tag-removed'
|
|
]);
|
|
|
|
// State
|
|
const isLoading = ref(false);
|
|
const hasError = ref(false);
|
|
const errorMessage = ref('');
|
|
const loadingProgress = ref(0);
|
|
|
|
const previewMode = ref(props.initialMode);
|
|
const currentPage = ref(1);
|
|
const totalPages = ref(1);
|
|
const currentZoom = ref(100);
|
|
const imageZoom = ref(100);
|
|
const isFullscreen = ref(false);
|
|
const annotationMode = ref(null); // 'highlight' | 'comment' | 'draw' | null
|
|
|
|
const isSaving = ref(false);
|
|
const showAutoSaveIndicator = ref(false);
|
|
const showDocumentInfo = ref(false);
|
|
const showShareModal = ref(false);
|
|
|
|
// Content state
|
|
const documentContent = ref('');
|
|
const spreadsheetData = ref([]);
|
|
const textContent = ref('');
|
|
const annotations = ref([]);
|
|
|
|
// Computed
|
|
const fileType = computed(() => {
|
|
if (!props.document?.name) return null;
|
|
return props.document.name.split('.').pop()?.toLowerCase();
|
|
});
|
|
|
|
const canEdit = computed(() => {
|
|
// Check user permissions and file type
|
|
const editableTypes = ['docx', 'doc', 'xlsx', 'xls', 'txt', 'md', 'csv'];
|
|
return editableTypes.includes(fileType.value) && hasEditPermission();
|
|
});
|
|
|
|
const canShare = computed(() => {
|
|
return hasSharePermission();
|
|
});
|
|
|
|
// Methods
|
|
const closePreview = () => {
|
|
if (previewMode.value === 'edit' && hasUnsavedChanges()) {
|
|
if (confirm('You have unsaved changes. Are you sure you want to close?')) {
|
|
emit('close');
|
|
}
|
|
} else {
|
|
emit('close');
|
|
}
|
|
};
|
|
|
|
const loadDocument = async () => {
|
|
if (!props.document) return;
|
|
|
|
isLoading.value = true;
|
|
hasError.value = false;
|
|
loadingProgress.value = 0;
|
|
|
|
try {
|
|
// Simulate loading progress
|
|
const progressInterval = setInterval(() => {
|
|
loadingProgress.value = Math.min(loadingProgress.value + 10, 90);
|
|
}, 100);
|
|
|
|
// Load document content based on type
|
|
switch (fileType.value) {
|
|
case 'pdf':
|
|
await loadPDFDocument();
|
|
break;
|
|
case 'docx':
|
|
case 'doc':
|
|
await loadWordDocument();
|
|
break;
|
|
case 'xlsx':
|
|
case 'xls':
|
|
await loadExcelDocument();
|
|
break;
|
|
case 'txt':
|
|
case 'md':
|
|
case 'csv':
|
|
await loadTextDocument();
|
|
break;
|
|
default:
|
|
// For unsupported types, just show the preview with download option
|
|
break;
|
|
}
|
|
|
|
clearInterval(progressInterval);
|
|
loadingProgress.value = 100;
|
|
|
|
// Small delay to show 100% before hiding
|
|
setTimeout(() => {
|
|
isLoading.value = false;
|
|
}, 200);
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load document:', error);
|
|
hasError.value = true;
|
|
errorMessage.value = error.message || 'Failed to load document';
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const loadPDFDocument = async () => {
|
|
// Mock PDF loading - in real implementation, use PDF.js
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
totalPages.value = Math.floor(Math.random() * 20) + 1;
|
|
currentPage.value = 1;
|
|
|
|
// Load existing annotations
|
|
annotations.value = await fetchDocumentAnnotations(props.document.id);
|
|
};
|
|
|
|
const loadWordDocument = async () => {
|
|
// Mock Word document loading - in real implementation, use Office.js or similar
|
|
await new Promise(resolve => setTimeout(resolve, 800));
|
|
documentContent.value = `<h1>${props.document.name}</h1><p>Sample document content...</p>`;
|
|
};
|
|
|
|
const loadExcelDocument = async () => {
|
|
// Mock Excel loading - in real implementation, use appropriate library
|
|
await new Promise(resolve => setTimeout(resolve, 600));
|
|
spreadsheetData.value = [
|
|
['Name', 'Age', 'City'],
|
|
['John Doe', 30, 'New York'],
|
|
['Jane Smith', 25, 'London']
|
|
];
|
|
};
|
|
|
|
const loadTextDocument = async () => {
|
|
// Mock text loading
|
|
await new Promise(resolve => setTimeout(resolve, 400));
|
|
textContent.value = `This is the content of ${props.document.name}`;
|
|
};
|
|
|
|
const fetchDocumentAnnotations = async (documentId) => {
|
|
// Mock annotation fetching
|
|
return [
|
|
{
|
|
id: '1',
|
|
type: 'highlight',
|
|
page: 1,
|
|
text: 'Important section',
|
|
color: 'yellow',
|
|
author: 'Current User',
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
];
|
|
};
|
|
|
|
// File type detection
|
|
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;
|
|
};
|
|
|
|
// Utility functions
|
|
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();
|
|
};
|
|
|
|
// Zoom controls
|
|
const zoomIn = () => {
|
|
currentZoom.value = Math.min(currentZoom.value + 25, 300);
|
|
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileType.value)) {
|
|
imageZoom.value = currentZoom.value;
|
|
}
|
|
};
|
|
|
|
const zoomOut = () => {
|
|
currentZoom.value = Math.max(currentZoom.value - 25, 25);
|
|
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileType.value)) {
|
|
imageZoom.value = currentZoom.value;
|
|
}
|
|
};
|
|
|
|
// Page navigation
|
|
const nextPage = () => {
|
|
if (currentPage.value < totalPages.value) {
|
|
currentPage.value++;
|
|
}
|
|
};
|
|
|
|
const previousPage = () => {
|
|
if (currentPage.value > 1) {
|
|
currentPage.value--;
|
|
}
|
|
};
|
|
|
|
// Annotation tools
|
|
const toggleAnnotationMode = (mode) => {
|
|
annotationMode.value = annotationMode.value === mode ? null : mode;
|
|
};
|
|
|
|
// Fullscreen
|
|
const toggleFullscreen = () => {
|
|
if (!document.fullscreenElement) {
|
|
document.documentElement.requestFullscreen();
|
|
isFullscreen.value = true;
|
|
} else {
|
|
document.exitFullscreen();
|
|
isFullscreen.value = false;
|
|
}
|
|
};
|
|
|
|
// Actions
|
|
const printDocument = () => {
|
|
window.print();
|
|
};
|
|
|
|
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();
|
|
};
|
|
|
|
const shareDocument = () => {
|
|
showShareModal.value = true;
|
|
};
|
|
|
|
// Permission checks
|
|
const hasEditPermission = () => {
|
|
// Mock permission check
|
|
return props.document?.permissions?.includes('edit') ?? true;
|
|
};
|
|
|
|
const hasSharePermission = () => {
|
|
// Mock permission check
|
|
return props.document?.permissions?.includes('share') ?? true;
|
|
};
|
|
|
|
const hasUnsavedChanges = () => {
|
|
// Check if there are unsaved changes
|
|
return false; // Implement based on content state
|
|
};
|
|
|
|
// Auto-save functionality
|
|
let autoSaveTimeout = null;
|
|
const autoSave = () => {
|
|
if (previewMode.value === 'edit') {
|
|
clearTimeout(autoSaveTimeout);
|
|
autoSaveTimeout = setTimeout(async () => {
|
|
await saveDocument(true);
|
|
showAutoSaveIndicator.value = true;
|
|
setTimeout(() => {
|
|
showAutoSaveIndicator.value = false;
|
|
}, 2000);
|
|
}, 2000);
|
|
}
|
|
};
|
|
|
|
const saveDocument = async (isAutoSave = false) => {
|
|
if (!isAutoSave) {
|
|
isSaving.value = true;
|
|
}
|
|
|
|
try {
|
|
// Save document content based on type
|
|
const payload = {
|
|
id: props.document.id,
|
|
content: getDocumentContent(),
|
|
annotations: annotations.value
|
|
};
|
|
|
|
// Mock API call
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
emit('document-updated', payload);
|
|
} catch (error) {
|
|
console.error('Failed to save document:', error);
|
|
// Show error notification
|
|
} finally {
|
|
if (!isAutoSave) {
|
|
isSaving.value = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
const getDocumentContent = () => {
|
|
switch (fileType.value) {
|
|
case 'docx':
|
|
case 'doc':
|
|
return documentContent.value;
|
|
case 'xlsx':
|
|
case 'xls':
|
|
return spreadsheetData.value;
|
|
case 'txt':
|
|
case 'md':
|
|
case 'csv':
|
|
return textContent.value;
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// Event handlers
|
|
const handlePageChanged = (page) => {
|
|
currentPage.value = page;
|
|
};
|
|
|
|
const handleContentChanged = (content) => {
|
|
if (fileType.value === 'docx' || fileType.value === 'doc') {
|
|
documentContent.value = content;
|
|
} else {
|
|
textContent.value = content;
|
|
}
|
|
autoSave();
|
|
};
|
|
|
|
const handleDataChanged = (data) => {
|
|
spreadsheetData.value = data;
|
|
autoSave();
|
|
};
|
|
|
|
const handleSaveDocument = () => {
|
|
saveDocument();
|
|
};
|
|
|
|
const handleAnnotationAdded = (annotation) => {
|
|
annotations.value.push(annotation);
|
|
emit('annotation-added', annotation);
|
|
autoSave();
|
|
};
|
|
|
|
const handleAnnotationUpdated = (annotation) => {
|
|
const index = annotations.value.findIndex(a => a.id === annotation.id);
|
|
if (index !== -1) {
|
|
annotations.value[index] = annotation;
|
|
emit('annotation-updated', annotation);
|
|
autoSave();
|
|
}
|
|
};
|
|
|
|
const handleAnnotationDeleted = (annotationId) => {
|
|
annotations.value = annotations.value.filter(a => a.id !== annotationId);
|
|
emit('annotation-deleted', annotationId);
|
|
autoSave();
|
|
};
|
|
|
|
const handleDocumentShared = (shareData) => {
|
|
emit('document-shared', shareData);
|
|
showShareModal.value = false;
|
|
};
|
|
|
|
const handleTagAdded = (tag) => {
|
|
emit('tag-added', tag);
|
|
};
|
|
|
|
const handleTagRemoved = (tag) => {
|
|
emit('tag-removed', tag);
|
|
};
|
|
|
|
// Keyboard shortcuts
|
|
const handleKeydown = (event) => {
|
|
if (event.key === 'Escape') {
|
|
closePreview();
|
|
} else if (event.ctrlKey || event.metaKey) {
|
|
switch (event.key) {
|
|
case 's':
|
|
event.preventDefault();
|
|
if (previewMode.value === 'edit') {
|
|
saveDocument();
|
|
}
|
|
break;
|
|
case 'p':
|
|
event.preventDefault();
|
|
printDocument();
|
|
break;
|
|
case 'd':
|
|
event.preventDefault();
|
|
downloadDocument();
|
|
break;
|
|
case '+':
|
|
case '=':
|
|
event.preventDefault();
|
|
zoomIn();
|
|
break;
|
|
case '-':
|
|
event.preventDefault();
|
|
zoomOut();
|
|
break;
|
|
}
|
|
} else if (fileType.value === 'pdf') {
|
|
switch (event.key) {
|
|
case 'ArrowLeft':
|
|
event.preventDefault();
|
|
previousPage();
|
|
break;
|
|
case 'ArrowRight':
|
|
event.preventDefault();
|
|
nextPage();
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Watchers
|
|
watch(() => props.visible, (newValue) => {
|
|
if (newValue && props.document) {
|
|
loadDocument();
|
|
currentZoom.value = 100;
|
|
imageZoom.value = 100;
|
|
currentPage.value = 1;
|
|
previewMode.value = props.initialMode;
|
|
annotationMode.value = null;
|
|
}
|
|
});
|
|
|
|
watch(() => props.document, (newDoc) => {
|
|
if (newDoc && props.visible) {
|
|
loadDocument();
|
|
}
|
|
});
|
|
|
|
// Lifecycle
|
|
onMounted(() => {
|
|
document.addEventListener('keydown', handleKeydown);
|
|
|
|
// Listen for fullscreen changes
|
|
document.addEventListener('fullscreenchange', () => {
|
|
isFullscreen.value = !!document.fullscreenElement;
|
|
});
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
document.removeEventListener('keydown', handleKeydown);
|
|
clearTimeout(autoSaveTimeout);
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Custom scrollbars for preview content */
|
|
:deep(.preview-content) {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: #4B5563 #1F2937;
|
|
}
|
|
|
|
:deep(.preview-content::-webkit-scrollbar) {
|
|
width: 8px;
|
|
height: 8px;
|
|
}
|
|
|
|
:deep(.preview-content::-webkit-scrollbar-track) {
|
|
background: #1F2937;
|
|
}
|
|
|
|
:deep(.preview-content::-webkit-scrollbar-thumb) {
|
|
background: #4B5563;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
:deep(.preview-content::-webkit-scrollbar-thumb:hover) {
|
|
background: #6B7280;
|
|
}
|
|
|
|
/* Prevent text selection on toolbar */
|
|
.floating-toolbar {
|
|
user-select: none;
|
|
}
|
|
|
|
/* Smooth transitions */
|
|
.transition-opacity {
|
|
transition-property: opacity;
|
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
transition-duration: 500ms;
|
|
}
|
|
|
|
/* Loading animation */
|
|
@keyframes pulse {
|
|
0%, 100% {
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
opacity: 0.5;
|
|
}
|
|
}
|
|
|
|
.animate-pulse {
|
|
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
}
|
|
</style> |