224 lines
5.2 KiB
Vue
224 lines
5.2 KiB
Vue
<template>
|
|
<v-dialog
|
|
v-model="isOpen"
|
|
max-width="90vw"
|
|
max-height="90vh"
|
|
scrollable
|
|
>
|
|
<v-card v-if="file">
|
|
<v-card-title class="d-flex align-center justify-space-between">
|
|
<div class="d-flex align-center">
|
|
<v-icon class="mr-2">{{ file.icon }}</v-icon>
|
|
<span>{{ file.displayName }}</span>
|
|
</div>
|
|
<v-btn
|
|
icon
|
|
variant="text"
|
|
@click="closeModal"
|
|
>
|
|
<v-icon>mdi-close</v-icon>
|
|
</v-btn>
|
|
</v-card-title>
|
|
|
|
<v-card-text class="pa-0">
|
|
<!-- Loading State -->
|
|
<div v-if="loading" class="text-center pa-10">
|
|
<v-progress-circular
|
|
indeterminate
|
|
color="primary"
|
|
size="64"
|
|
/>
|
|
<p class="mt-4">Loading preview...</p>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div v-else-if="error" class="text-center pa-10">
|
|
<v-icon size="64" color="error">mdi-alert-circle</v-icon>
|
|
<p class="mt-4">{{ error }}</p>
|
|
</div>
|
|
|
|
<!-- Image Preview -->
|
|
<div v-else-if="isImage" class="image-preview-container">
|
|
<img
|
|
:src="previewUrl"
|
|
:alt="file.displayName"
|
|
class="image-preview"
|
|
@load="loading = false"
|
|
@error="handlePreviewError"
|
|
/>
|
|
</div>
|
|
|
|
<!-- PDF Preview -->
|
|
<div v-else-if="isPdf" class="pdf-preview-container">
|
|
<iframe
|
|
:src="previewUrl"
|
|
width="100%"
|
|
height="100%"
|
|
frameborder="0"
|
|
@load="loading = false"
|
|
@error="handlePreviewError"
|
|
/>
|
|
</div>
|
|
</v-card-text>
|
|
|
|
<v-card-actions>
|
|
<v-spacer />
|
|
<v-btn
|
|
color="primary"
|
|
variant="outlined"
|
|
@click="downloadFile"
|
|
prepend-icon="mdi-download"
|
|
>
|
|
Download
|
|
</v-btn>
|
|
<v-btn
|
|
color="primary"
|
|
variant="flat"
|
|
@click="closeModal"
|
|
>
|
|
Close
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, watch } from 'vue';
|
|
|
|
interface FileItem {
|
|
name: string;
|
|
size: number;
|
|
sizeFormatted: string;
|
|
lastModified: string;
|
|
extension: string;
|
|
icon: string;
|
|
displayName: string;
|
|
isFolder: boolean;
|
|
}
|
|
|
|
interface Props {
|
|
modelValue: boolean;
|
|
file: FileItem | null;
|
|
}
|
|
|
|
interface Emits {
|
|
(e: 'update:modelValue', value: boolean): void;
|
|
}
|
|
|
|
const props = defineProps<Props>();
|
|
const emit = defineEmits<Emits>();
|
|
|
|
const loading = ref(false);
|
|
const error = ref('');
|
|
const previewUrl = ref('');
|
|
|
|
// Computed property for v-model binding
|
|
const isOpen = computed({
|
|
get: () => props.modelValue,
|
|
set: (value: boolean) => emit('update:modelValue', value),
|
|
});
|
|
|
|
// Check if file is an image
|
|
const isImage = computed(() => {
|
|
if (!props.file) return false;
|
|
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'];
|
|
return imageExtensions.includes(props.file.extension.toLowerCase());
|
|
});
|
|
|
|
// Check if file is a PDF
|
|
const isPdf = computed(() => {
|
|
if (!props.file) return false;
|
|
return props.file.extension.toLowerCase() === 'pdf';
|
|
});
|
|
|
|
// Watch for file changes and load preview
|
|
watch(() => props.file, async (newFile) => {
|
|
if (newFile && props.modelValue) {
|
|
await loadPreview();
|
|
}
|
|
});
|
|
|
|
// Watch for dialog open and load preview
|
|
watch(() => props.modelValue, async (isOpen) => {
|
|
if (isOpen && props.file) {
|
|
await loadPreview();
|
|
}
|
|
});
|
|
|
|
// Load preview URL
|
|
const loadPreview = async () => {
|
|
if (!props.file) return;
|
|
|
|
loading.value = true;
|
|
error.value = '';
|
|
previewUrl.value = '';
|
|
|
|
try {
|
|
// For images and PDFs, use the proxy endpoint to avoid CORS issues
|
|
if (isImage.value || isPdf.value) {
|
|
// Use the proxy endpoint that serves the file directly
|
|
previewUrl.value = `/api/files/proxy-preview?fileName=${encodeURIComponent(props.file.name)}`;
|
|
// The loading state will be handled by the image/iframe onload event
|
|
} else {
|
|
throw new Error('File type does not support preview');
|
|
}
|
|
} catch (err: any) {
|
|
error.value = err.data?.statusMessage || err.message || 'Failed to load preview';
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
// Handle preview load error
|
|
const handlePreviewError = () => {
|
|
error.value = 'Failed to load preview';
|
|
loading.value = false;
|
|
};
|
|
|
|
// Download file
|
|
const downloadFile = async () => {
|
|
if (!props.file) return;
|
|
|
|
try {
|
|
const response = await $fetch('/api/files/download', {
|
|
params: { fileName: props.file.name },
|
|
});
|
|
|
|
window.open(response.url, '_blank');
|
|
} catch (err) {
|
|
console.error('Failed to download file:', err);
|
|
}
|
|
};
|
|
|
|
// Close modal
|
|
const closeModal = () => {
|
|
isOpen.value = false;
|
|
previewUrl.value = '';
|
|
error.value = '';
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.image-preview-container {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
min-height: 400px;
|
|
max-height: 70vh;
|
|
overflow: auto;
|
|
background-color: #f5f5f5;
|
|
}
|
|
|
|
.image-preview {
|
|
max-width: 100%;
|
|
max-height: 70vh;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.pdf-preview-container {
|
|
width: 100%;
|
|
height: 70vh;
|
|
overflow: hidden;
|
|
}
|
|
</style>
|