panels-origin/web-panel/components/DocumentPreviewDialog.vue
2026-08-21 16:55:01 -05:00

146 lines
3.8 KiB
Vue

<template>
<Dialog
v-model:visible="visible"
modal
:header="title"
class="document-preview-dialog"
:style="{ width: 'min(920px, 96vw)' }"
:content-style="{ padding: '0' }"
@hide="onHide"
>
<div class="document-preview-body">
<div v-if="loading" class="document-preview-state">
<ProgressSpinner stroke-width="4" />
<span class="muted">Cargando vista previa…</span>
</div>
<div v-else-if="error" class="document-preview-state">
<i class="pi pi-exclamation-triangle" />
<span>{{ error }}</span>
</div>
<img v-else-if="kind === 'image' && url" :src="url" :alt="title" class="document-preview-image" />
<iframe v-else-if="kind === 'pdf' && url" :src="url" class="document-preview-frame" title="Vista previa PDF" />
<div v-else class="document-preview-state">
<i class="pi pi-file" />
<span>Este archivo no tiene vista previa en el panel.</span>
<Button label="Descargar" icon="pi pi-download" outlined size="small" @click="downloadFile" />
</div>
</div>
<template #footer>
<Button label="Descargar" icon="pi pi-download" text :disabled="!url" @click="downloadFile" />
<Button label="Cerrar" severity="secondary" @click="visible = false" />
</template>
</Dialog>
</template>
<script setup lang="ts">
const props = defineProps<{
modelValue: boolean;
path: string;
name: string;
mime?: string | null;
}>();
const emit = defineEmits<{ "update:modelValue": [value: boolean] }>();
const { api } = useApi();
const visible = computed({
get: () => props.modelValue,
set: (value) => emit("update:modelValue", value),
});
const title = computed(() => props.name || "Documento");
const loading = ref(false);
const error = ref("");
const url = ref("");
const blobRef = ref<Blob | null>(null);
const kind = computed<"image" | "pdf" | "other">(() => {
const mime = (props.mime || blobRef.value?.type || "").toLowerCase();
const name = props.name.toLowerCase();
if (mime.startsWith("image/") || /\.(jpe?g|png|gif|webp|bmp)$/i.test(name)) return "image";
if (mime.includes("pdf") || name.endsWith(".pdf")) return "pdf";
return "other";
});
async function loadPreview() {
revoke();
error.value = "";
blobRef.value = null;
if (!props.path) return;
loading.value = true;
try {
const blob = await api<Blob>(props.path);
blobRef.value = blob;
url.value = URL.createObjectURL(blob);
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : "No se pudo abrir el documento";
} finally {
loading.value = false;
}
}
function downloadFile() {
if (!url.value) return;
const a = document.createElement("a");
a.href = url.value;
a.download = props.name || "documento";
a.rel = "noopener";
document.body.appendChild(a);
a.click();
a.remove();
}
function revoke() {
if (url.value) URL.revokeObjectURL(url.value);
url.value = "";
}
function onHide() {
revoke();
blobRef.value = null;
error.value = "";
}
watch(
() => [props.modelValue, props.path] as const,
([open]) => {
if (open) loadPreview();
else onHide();
},
);
</script>
<style scoped>
.document-preview-body {
min-height: 360px;
max-height: min(78vh, 760px);
display: flex;
align-items: center;
justify-content: center;
background: #f4f6f8;
}
.document-preview-state {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 28px 20px;
color: #5d6e84;
text-align: center;
}
.document-preview-state .pi {
font-size: 28px;
color: #8a96a8;
}
.document-preview-image {
display: block;
max-width: 100%;
max-height: min(78vh, 760px);
object-fit: contain;
background: #fff;
}
.document-preview-frame {
width: 100%;
height: min(78vh, 760px);
border: 0;
background: #fff;
}
</style>