generated from corrad-software/corrad-af-2024
369 lines
17 KiB
Vue
369 lines
17 KiB
Vue
<script setup>
|
|
import { ref, computed, onMounted, watch } from 'vue';
|
|
|
|
const props = defineProps({
|
|
document: {
|
|
type: Object,
|
|
required: true
|
|
},
|
|
visible: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits(['close', 'download', 'print', 'edit']);
|
|
|
|
// Local state
|
|
const isLoading = ref(true);
|
|
const zoomLevel = ref(100);
|
|
const currentPage = ref(1);
|
|
const totalPages = ref(1);
|
|
const isFullscreen = ref(false);
|
|
const previewMode = ref('fit-width'); // fit-width, fit-page, actual-size
|
|
|
|
// Computed properties
|
|
const zoomOptions = computed(() => [
|
|
{ label: '50%', value: 50 },
|
|
{ label: '75%', value: 75 },
|
|
{ label: '100%', value: 100 },
|
|
{ label: '125%', value: 125 },
|
|
{ label: '150%', value: 150 },
|
|
{ label: '200%', value: 200 },
|
|
{ label: 'Fit Width', value: 'fit-width' },
|
|
{ label: 'Fit Page', value: 'fit-page' }
|
|
]);
|
|
|
|
const canPreview = computed(() => {
|
|
if (!props.document) return false;
|
|
const ext = props.document.extension?.toLowerCase();
|
|
return ['pdf', 'jpg', 'jpeg', 'png', 'gif', 'txt', 'doc', 'docx', 'xls', 'xlsx'].includes(ext);
|
|
});
|
|
|
|
const documentIcon = computed(() => {
|
|
if (!props.document) return 'file';
|
|
const ext = props.document.extension?.toLowerCase();
|
|
switch (ext) {
|
|
case 'pdf': return 'pdf';
|
|
case 'doc':
|
|
case 'docx': return 'doc';
|
|
case 'xls':
|
|
case 'xlsx': return 'excel';
|
|
case 'ppt':
|
|
case 'pptx': return 'powerpoint';
|
|
case 'jpg':
|
|
case 'jpeg':
|
|
case 'png':
|
|
case 'gif': return 'image';
|
|
case 'txt': return 'text';
|
|
default: return 'file';
|
|
}
|
|
});
|
|
|
|
// Methods
|
|
const closeViewer = () => {
|
|
emit('close');
|
|
};
|
|
|
|
const downloadDocument = () => {
|
|
emit('download', props.document);
|
|
};
|
|
|
|
const printDocument = () => {
|
|
emit('print', props.document);
|
|
};
|
|
|
|
const editDocument = () => {
|
|
emit('edit', props.document);
|
|
};
|
|
|
|
const toggleFullscreen = () => {
|
|
isFullscreen.value = !isFullscreen.value;
|
|
if (isFullscreen.value) {
|
|
document.documentElement.requestFullscreen?.();
|
|
} else {
|
|
document.exitFullscreen?.();
|
|
}
|
|
};
|
|
|
|
const zoomIn = () => {
|
|
if (zoomLevel.value < 400) {
|
|
zoomLevel.value += 25;
|
|
}
|
|
};
|
|
|
|
const zoomOut = () => {
|
|
if (zoomLevel.value > 25) {
|
|
zoomLevel.value -= 25;
|
|
}
|
|
};
|
|
|
|
const setZoom = (value) => {
|
|
if (typeof value === 'string') {
|
|
previewMode.value = value;
|
|
} else {
|
|
zoomLevel.value = value;
|
|
previewMode.value = 'custom';
|
|
}
|
|
};
|
|
|
|
const previousPage = () => {
|
|
if (currentPage.value > 1) {
|
|
currentPage.value--;
|
|
}
|
|
};
|
|
|
|
const nextPage = () => {
|
|
if (currentPage.value < totalPages.value) {
|
|
currentPage.value++;
|
|
}
|
|
};
|
|
|
|
const getSvgIcon = (iconType, size = 24) => {
|
|
const icons = {
|
|
pdf: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><path d="M16 13a2 2 0 0 1-2 2h-2v4"></path><path d="M12 17v-4h2a2 2 0 0 1 2 2z"></path></svg>`,
|
|
doc: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><line x1="10" y1="9" x2="8" y2="9"></line></svg>`,
|
|
excel: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><rect x="8" y="12" width="8" height="6"></rect><line x1="8" y1="16" x2="16" y2="16"></line><line x1="12" y1="12" x2="12" y2="18"></line></svg>`,
|
|
powerpoint: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><rect x="9" y="12" width="6" height="6"></rect><circle cx="12" cy="15" r="1"></circle></svg>`,
|
|
image: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>`,
|
|
text: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>`,
|
|
file: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>`,
|
|
close: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`,
|
|
download: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`,
|
|
print: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>`,
|
|
edit: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>`,
|
|
fullscreen: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path></svg>`,
|
|
zoomIn: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><path d="M21 21l-4.35-4.35"></path><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>`,
|
|
zoomOut: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><path d="M21 21l-4.35-4.35"></path><line x1="8" y1="11" x2="14" y2="11"></line></svg>`,
|
|
chevronLeft: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>`,
|
|
chevronRight: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>`
|
|
};
|
|
return icons[iconType] || icons.file;
|
|
};
|
|
|
|
// Simulate loading
|
|
watch(() => props.document, (newDoc) => {
|
|
if (newDoc) {
|
|
isLoading.value = true;
|
|
setTimeout(() => {
|
|
isLoading.value = false;
|
|
totalPages.value = Math.floor(Math.random() * 20) + 1; // Random page count for demo
|
|
}, 1000);
|
|
}
|
|
}, { immediate: true });
|
|
|
|
onMounted(() => {
|
|
// Set up document loading simulation
|
|
if (props.document) {
|
|
setTimeout(() => {
|
|
isLoading.value = false;
|
|
}, 1000);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="visible" class="document-viewer fixed inset-0 z-50 bg-black bg-opacity-75 flex items-center justify-center">
|
|
<div class="viewer-container bg-white dark:bg-gray-900 rounded-lg shadow-2xl max-w-7xl max-h-[95vh] w-full mx-4 flex flex-col">
|
|
<!-- Header with toolbar -->
|
|
<div class="viewer-header border-b border-gray-200 dark:border-gray-700 p-3">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center space-x-3">
|
|
<span v-html="getSvgIcon(documentIcon, 24)" class="text-blue-600"></span>
|
|
<div>
|
|
<h3 class="text-lg font-semibold">{{ document?.name }}</h3>
|
|
<p class="text-sm text-gray-500">{{ document?.extension?.toUpperCase() }} • {{ document?.size }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-2">
|
|
<!-- Zoom Controls -->
|
|
<div class="flex items-center space-x-1 border border-gray-300 dark:border-gray-600 rounded">
|
|
<button
|
|
@click="zoomOut"
|
|
class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-l"
|
|
:disabled="zoomLevel <= 25"
|
|
>
|
|
<span v-html="getSvgIcon('zoomOut', 16)"></span>
|
|
</button>
|
|
|
|
<select
|
|
v-model="zoomLevel"
|
|
@change="setZoom($event.target.value)"
|
|
class="px-2 py-1 text-sm border-0 focus:ring-0 bg-transparent"
|
|
>
|
|
<option v-for="option in zoomOptions" :key="option.value" :value="option.value">
|
|
{{ option.label }}
|
|
</option>
|
|
</select>
|
|
|
|
<button
|
|
@click="zoomIn"
|
|
class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-r"
|
|
:disabled="zoomLevel >= 400"
|
|
>
|
|
<span v-html="getSvgIcon('zoomIn', 16)"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Page Navigation (for PDFs) -->
|
|
<div v-if="document?.extension === 'pdf'" class="flex items-center space-x-1 border border-gray-300 dark:border-gray-600 rounded">
|
|
<button
|
|
@click="previousPage"
|
|
class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-l"
|
|
:disabled="currentPage <= 1"
|
|
>
|
|
<span v-html="getSvgIcon('chevronLeft', 16)"></span>
|
|
</button>
|
|
|
|
<span class="px-2 py-1 text-sm">{{ currentPage }} / {{ totalPages }}</span>
|
|
|
|
<button
|
|
@click="nextPage"
|
|
class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-r"
|
|
:disabled="currentPage >= totalPages"
|
|
>
|
|
<span v-html="getSvgIcon('chevronRight', 16)"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="flex items-center space-x-1">
|
|
<rs-button size="sm" variant="ghost" @click="downloadDocument">
|
|
<span v-html="getSvgIcon('download', 16)" class="mr-1"></span>
|
|
Download
|
|
</rs-button>
|
|
|
|
<rs-button size="sm" variant="ghost" @click="printDocument">
|
|
<span v-html="getSvgIcon('print', 16)" class="mr-1"></span>
|
|
Print
|
|
</rs-button>
|
|
|
|
<rs-button size="sm" variant="ghost" @click="editDocument">
|
|
<span v-html="getSvgIcon('edit', 16)" class="mr-1"></span>
|
|
Edit
|
|
</rs-button>
|
|
|
|
<rs-button size="sm" variant="ghost" @click="toggleFullscreen">
|
|
<span v-html="getSvgIcon('fullscreen', 16)"></span>
|
|
</rs-button>
|
|
</div>
|
|
|
|
<button @click="closeViewer" class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">
|
|
<span v-html="getSvgIcon('close', 20)"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content Area -->
|
|
<div class="viewer-content flex-1 p-4 overflow-auto bg-gray-50 dark:bg-gray-800">
|
|
<!-- Loading State -->
|
|
<div v-if="isLoading" class="flex items-center justify-center h-full">
|
|
<div class="flex flex-col items-center space-y-4">
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
|
<p class="text-gray-500">Loading document...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Document Preview -->
|
|
<div v-else-if="canPreview" class="document-preview flex justify-center">
|
|
<div class="preview-area bg-white shadow-lg rounded border max-w-full"
|
|
:style="{
|
|
transform: `scale(${zoomLevel / 100})`,
|
|
transformOrigin: 'top center'
|
|
}">
|
|
<!-- PDF Preview -->
|
|
<div v-if="document?.extension === 'pdf'" class="pdf-preview w-[210mm] h-[297mm] p-8 border">
|
|
<div class="flex items-center justify-center h-full text-gray-400">
|
|
<div class="text-center">
|
|
<span v-html="getSvgIcon('pdf', 48)" class="mb-4 block text-red-500"></span>
|
|
<p class="text-lg font-semibold">{{ document.name }}</p>
|
|
<p class="text-sm">PDF Document Preview</p>
|
|
<p class="text-xs mt-2">Page {{ currentPage }} of {{ totalPages }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Image Preview -->
|
|
<div v-else-if="['jpg', 'jpeg', 'png', 'gif'].includes(document?.extension?.toLowerCase())" class="image-preview">
|
|
<img
|
|
:src="`/api/documents/${document.id}/preview`"
|
|
:alt="document.name"
|
|
class="max-w-full h-auto rounded shadow"
|
|
@error="$event.target.style.display = 'none'"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Office Document Preview -->
|
|
<div v-else class="office-preview w-[210mm] h-[297mm] p-8 border bg-white">
|
|
<div class="flex items-center justify-center h-full text-gray-400">
|
|
<div class="text-center">
|
|
<span v-html="getSvgIcon(documentIcon, 48)" class="mb-4 block text-blue-500"></span>
|
|
<p class="text-lg font-semibold">{{ document.name }}</p>
|
|
<p class="text-sm">{{ document.extension?.toUpperCase() }} Document Preview</p>
|
|
<p class="text-xs mt-2">Preview not available for this file type</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- No Preview Available -->
|
|
<div v-else class="no-preview flex items-center justify-center h-full">
|
|
<div class="text-center text-gray-500">
|
|
<span v-html="getSvgIcon(documentIcon, 64)" class="mb-4 block mx-auto"></span>
|
|
<p class="text-lg font-semibold">Preview not available</p>
|
|
<p class="text-sm">This file type cannot be previewed</p>
|
|
<rs-button @click="downloadDocument" class="mt-4">
|
|
<span v-html="getSvgIcon('download', 16)" class="mr-2"></span>
|
|
Download to view
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.document-viewer {
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
|
|
.viewer-container {
|
|
height: 95vh;
|
|
}
|
|
|
|
.viewer-content {
|
|
min-height: 0;
|
|
}
|
|
|
|
.preview-area {
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
select:focus {
|
|
outline: none;
|
|
box-shadow: none;
|
|
}
|
|
|
|
/* Custom scrollbar */
|
|
.viewer-content::-webkit-scrollbar {
|
|
width: 8px;
|
|
height: 8px;
|
|
}
|
|
|
|
.viewer-content::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.viewer-content::-webkit-scrollbar-thumb {
|
|
background-color: rgba(156, 163, 175, 0.5);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.viewer-content::-webkit-scrollbar-thumb:hover {
|
|
background-color: rgba(156, 163, 175, 0.7);
|
|
}
|
|
</style> |