Tout ce flux (visionneuse de donnees, fiches locataire et operation, tagging, autocompletion, panneau de differences) etait en theme clair, au milieu d'une application sombre : le contraste sautait a chaque passage d'une page a l'edition d'un document. Les surfaces, bordures, textes et etats colores suivent desormais la palette commune. Les champs natifs (date, nombre, cases a cocher) recoivent color-scheme: dark pour que le calendrier et les widgets systeme s'affichent en sombre eux aussi. Les fiches de metadonnees se repartissent sur 2 puis 3 colonnes selon la largeur disponible, au lieu d'une seule colonne. Le bouton « Annuler » du bandeau de tagging devient « Retour a l'edition », qui decrit son action reelle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
147 lines
4.3 KiB
Vue
147 lines
4.3 KiB
Vue
<template>
|
|
<div class="flex flex-col h-full bg-gray-950 overflow-hidden relative">
|
|
<!-- Header -->
|
|
<div class="flex-shrink-0 flex items-center px-3 py-2 bg-gray-900 border-b border-gray-700 text-white">
|
|
<span class="text-sm font-medium truncate">{{ fileName }}</span>
|
|
</div>
|
|
|
|
<!-- États -->
|
|
<div
|
|
v-if="loading"
|
|
class="flex-1 flex items-center justify-center text-gray-500 text-sm"
|
|
>
|
|
Chargement du PDF…
|
|
</div>
|
|
<div
|
|
v-else-if="error"
|
|
class="flex-1 flex items-center justify-center px-4 text-center text-red-400 text-sm"
|
|
>
|
|
{{ error }}
|
|
</div>
|
|
<!-- Rendu PDF.js : une <canvas> par page, empilées et scrollables.
|
|
Ne dépend pas du lecteur PDF natif du moteur (Qt WebEngine,
|
|
WebView2, navigateur) -> rendu identique partout. -->
|
|
<div
|
|
v-else-if="hasSource"
|
|
ref="container"
|
|
class="flex-1 overflow-auto custom-scrollbar p-2 bg-gray-950"
|
|
/>
|
|
<!-- Empty state -->
|
|
<div
|
|
v-else
|
|
class="flex-1 flex items-center justify-center text-gray-500 text-sm"
|
|
>
|
|
Aucun PDF sélectionné
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, watch, onUnmounted, nextTick } from 'vue'
|
|
// Build « legacy » (et non le build par défaut) : il embarque les polyfills
|
|
// nécessaires aux moteurs Chromium embarqués (Qt WebEngine, WebView2), plus
|
|
// anciens que la dernière version de Chrome. Le build moderne utilise
|
|
// `Map.prototype.getOrInsertComputed` (proposition JS récente) sans polyfill,
|
|
// ce qui casse le rendu dans la fenêtre bureau. Voir docs pdf.js « legacy ».
|
|
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs'
|
|
import PdfWorker from 'pdfjs-dist/legacy/build/pdf.worker.min.mjs?url'
|
|
|
|
// Worker bundlé localement par Vite (pas de CDN) -> fonctionne hors-ligne.
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = PdfWorker
|
|
|
|
const props = defineProps({
|
|
file: { type: File, default: null },
|
|
url: { type: String, default: null },
|
|
fileName: { type: String, default: 'document.pdf' }
|
|
})
|
|
|
|
const container = ref(null)
|
|
const loading = ref(false)
|
|
const error = ref(null)
|
|
const hasSource = ref(false)
|
|
|
|
let loadingTask = null
|
|
// Jeton de génération : invalide les rendus concurrents (changement de source).
|
|
let renderToken = 0
|
|
|
|
async function resolveSource() {
|
|
if (props.file) return { data: await props.file.arrayBuffer() }
|
|
if (props.url) return { url: props.url }
|
|
return null
|
|
}
|
|
|
|
async function destroyTask() {
|
|
if (loadingTask) {
|
|
try {
|
|
await loadingTask.destroy()
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
loadingTask = null
|
|
}
|
|
}
|
|
|
|
async function render() {
|
|
const token = ++renderToken
|
|
error.value = null
|
|
await destroyTask()
|
|
|
|
const source = await resolveSource()
|
|
hasSource.value = source !== null
|
|
if (!source) {
|
|
loading.value = false
|
|
return
|
|
}
|
|
|
|
loading.value = true
|
|
try {
|
|
loadingTask = pdfjsLib.getDocument(source)
|
|
const doc = await loadingTask.promise
|
|
if (token !== renderToken) return
|
|
|
|
// Le conteneur n'est monté qu'une fois loading=false.
|
|
loading.value = false
|
|
await nextTick()
|
|
const el = container.value
|
|
if (!el || token !== renderToken) return
|
|
el.innerHTML = ''
|
|
|
|
const targetWidth = Math.max(el.clientWidth - 16, 0)
|
|
const dpr = window.devicePixelRatio || 1
|
|
|
|
for (let n = 1; n <= doc.numPages; n++) {
|
|
if (token !== renderToken) return
|
|
const page = await doc.getPage(n)
|
|
const base = page.getViewport({ scale: 1 })
|
|
const scale = targetWidth > 0 ? targetWidth / base.width : 1
|
|
const viewport = page.getViewport({ scale })
|
|
|
|
const canvas = document.createElement('canvas')
|
|
canvas.width = Math.floor(viewport.width * dpr)
|
|
canvas.height = Math.floor(viewport.height * dpr)
|
|
canvas.style.width = '100%'
|
|
canvas.className = 'block mx-auto mb-2 bg-white shadow'
|
|
|
|
const ctx = canvas.getContext('2d')
|
|
ctx.scale(dpr, dpr)
|
|
await page.render({ canvasContext: ctx, viewport }).promise
|
|
if (token !== renderToken) return
|
|
el.appendChild(canvas)
|
|
}
|
|
} catch (e) {
|
|
console.error('[PdfPreview] render error:', e?.message, e)
|
|
if (token === renderToken) {
|
|
error.value = "Impossible d'afficher le PDF."
|
|
loading.value = false
|
|
}
|
|
}
|
|
}
|
|
|
|
watch([() => props.file, () => props.url], render, { immediate: true })
|
|
|
|
onUnmounted(() => {
|
|
renderToken++
|
|
destroyTask()
|
|
})
|
|
</script>
|