generated from corrad-software/corrad-af-2024
836 lines
22 KiB
Vue
836 lines
22 KiB
Vue
<template>
|
|
<div class="pdf-viewer w-full h-full bg-gray-100 dark:bg-gray-900 relative overflow-hidden">
|
|
<!-- PDF Canvas Container -->
|
|
<div
|
|
ref="canvasContainer"
|
|
class="pdf-canvas-container w-full h-full overflow-auto scroll-smooth"
|
|
@wheel="handleWheel"
|
|
@mousedown="handleMouseDown"
|
|
@mousemove="handleMouseMove"
|
|
@mouseup="handleMouseUp"
|
|
>
|
|
<div class="flex flex-col items-center py-8 space-y-4">
|
|
<!-- Page Canvas -->
|
|
<div
|
|
v-for="pageNum in totalPages"
|
|
:key="pageNum"
|
|
:ref="el => setPageRef(el, pageNum)"
|
|
class="page-container bg-white shadow-lg relative"
|
|
:style="{
|
|
transform: `scale(${zoom / 100})`,
|
|
transformOrigin: 'top center'
|
|
}"
|
|
>
|
|
<canvas
|
|
:id="`pdf-page-${pageNum}`"
|
|
class="pdf-page border border-gray-300"
|
|
@contextmenu.prevent
|
|
></canvas>
|
|
|
|
<!-- Annotation Layer -->
|
|
<div
|
|
class="annotation-layer absolute inset-0 pointer-events-none"
|
|
:style="{ transform: `scale(${zoom / 100})` }"
|
|
>
|
|
<!-- Highlights -->
|
|
<div
|
|
v-for="highlight in getPageAnnotations(pageNum, 'highlight')"
|
|
:key="highlight.id"
|
|
class="absolute pointer-events-auto cursor-pointer"
|
|
:style="{
|
|
left: highlight.x + 'px',
|
|
top: highlight.y + 'px',
|
|
width: highlight.width + 'px',
|
|
height: highlight.height + 'px',
|
|
backgroundColor: highlight.color + '40',
|
|
border: `2px solid ${highlight.color}`
|
|
}"
|
|
@click="editAnnotation(highlight)"
|
|
@contextmenu.prevent="deleteAnnotation(highlight.id)"
|
|
:title="`${highlight.text} - ${highlight.author}`"
|
|
></div>
|
|
|
|
<!-- Comments -->
|
|
<div
|
|
v-for="comment in getPageAnnotations(pageNum, 'comment')"
|
|
:key="comment.id"
|
|
class="absolute pointer-events-auto"
|
|
:style="{
|
|
left: comment.x + 'px',
|
|
top: comment.y + 'px'
|
|
}"
|
|
>
|
|
<button
|
|
@click="showCommentDialog(comment)"
|
|
class="w-6 h-6 bg-blue-500 text-white rounded-full flex items-center justify-center text-xs font-bold shadow-lg hover:bg-blue-600 transition-colors"
|
|
:title="comment.text"
|
|
>
|
|
<Icon name="mdi:comment" class="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Drawings -->
|
|
<svg
|
|
v-for="drawing in getPageAnnotations(pageNum, 'draw')"
|
|
:key="drawing.id"
|
|
class="absolute pointer-events-auto"
|
|
:style="{
|
|
left: '0px',
|
|
top: '0px',
|
|
width: '100%',
|
|
height: '100%'
|
|
}"
|
|
@contextmenu.prevent="deleteAnnotation(drawing.id)"
|
|
>
|
|
<path
|
|
:d="drawing.path"
|
|
:stroke="drawing.color"
|
|
:stroke-width="drawing.strokeWidth || 2"
|
|
fill="none"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
|
|
<!-- Page Number -->
|
|
<div class="absolute bottom-2 right-2 bg-black bg-opacity-50 text-white px-2 py-1 rounded text-xs">
|
|
{{ pageNum }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Drawing Canvas Overlay (for annotation mode) -->
|
|
<canvas
|
|
v-if="annotationMode === 'draw'"
|
|
ref="drawingCanvas"
|
|
class="absolute inset-0 cursor-crosshair pointer-events-auto"
|
|
@mousedown="startDrawing"
|
|
@mousemove="draw"
|
|
@mouseup="endDrawing"
|
|
@mouseleave="endDrawing"
|
|
></canvas>
|
|
|
|
<!-- Loading Overlay -->
|
|
<div
|
|
v-if="isLoading"
|
|
class="absolute inset-0 bg-white bg-opacity-90 flex items-center justify-center z-10"
|
|
>
|
|
<div class="text-center">
|
|
<Icon name="mdi:loading" class="w-8 h-8 animate-spin text-blue-600 mx-auto mb-2" />
|
|
<p class="text-gray-600">Loading PDF...</p>
|
|
<div class="w-48 bg-gray-200 rounded-full h-2 mt-2">
|
|
<div
|
|
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
|
:style="{ width: `${loadingProgress}%` }"
|
|
></div>
|
|
</div>
|
|
<p class="text-xs text-gray-500 mt-1">Page {{ currentLoadingPage }} of {{ totalPages }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Comment Dialog -->
|
|
<div
|
|
v-if="showingComment"
|
|
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
|
@click="closeCommentDialog"
|
|
>
|
|
<div
|
|
class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4"
|
|
@click.stop
|
|
>
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Comment</h3>
|
|
<button
|
|
@click="closeCommentDialog"
|
|
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
>
|
|
<Icon name="mdi:close" class="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Comment Text
|
|
</label>
|
|
<textarea
|
|
v-model="commentText"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md
|
|
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100
|
|
focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
rows="3"
|
|
placeholder="Enter your comment..."
|
|
></textarea>
|
|
</div>
|
|
|
|
<div class="flex justify-end space-x-2">
|
|
<button
|
|
@click="closeCommentDialog"
|
|
class="px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
@click="saveComment"
|
|
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
|
>
|
|
{{ editingComment ? 'Update' : 'Save' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Page Navigation Thumbnail Strip -->
|
|
<div
|
|
v-if="totalPages > 1 && showThumbnails"
|
|
class="absolute left-4 top-1/2 transform -translate-y-1/2 bg-white dark:bg-gray-800 rounded-lg shadow-lg p-2 max-h-80 overflow-y-auto"
|
|
>
|
|
<div class="space-y-2">
|
|
<div
|
|
v-for="pageNum in totalPages"
|
|
:key="`thumb-${pageNum}`"
|
|
@click="scrollToPage(pageNum)"
|
|
:class="[
|
|
'thumbnail-page cursor-pointer border-2 transition-all',
|
|
currentVisiblePage === pageNum
|
|
? 'border-blue-500 shadow-md'
|
|
: 'border-gray-300 dark:border-gray-600 hover:border-blue-300'
|
|
]"
|
|
>
|
|
<canvas
|
|
:id="`pdf-thumbnail-${pageNum}`"
|
|
class="thumbnail-canvas"
|
|
:width="60"
|
|
:height="80"
|
|
></canvas>
|
|
<div class="text-xs text-center py-1">{{ pageNum }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Zoom Level Indicator -->
|
|
<div
|
|
v-if="showZoomIndicator"
|
|
class="absolute top-4 left-1/2 transform -translate-x-1/2 bg-black bg-opacity-70 text-white px-3 py-1 rounded-full text-sm"
|
|
>
|
|
{{ Math.round(zoom) }}%
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
|
|
|
|
// PDF.js imports (in real implementation)
|
|
// import * as pdfjsLib from 'pdfjs-dist';
|
|
|
|
const props = defineProps({
|
|
document: {
|
|
type: Object,
|
|
required: true
|
|
},
|
|
mode: {
|
|
type: String,
|
|
default: 'view' // 'view' | 'edit'
|
|
},
|
|
annotations: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
zoom: {
|
|
type: Number,
|
|
default: 100
|
|
},
|
|
annotationMode: {
|
|
type: String,
|
|
default: null // 'highlight' | 'comment' | 'draw' | null
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits([
|
|
'annotation-added',
|
|
'annotation-updated',
|
|
'annotation-deleted',
|
|
'page-changed',
|
|
'zoom-changed'
|
|
]);
|
|
|
|
// State
|
|
const isLoading = ref(true);
|
|
const loadingProgress = ref(0);
|
|
const currentLoadingPage = ref(0);
|
|
const totalPages = ref(1);
|
|
const currentVisiblePage = ref(1);
|
|
const zoom = ref(props.zoom);
|
|
const showThumbnails = ref(false);
|
|
const showZoomIndicator = ref(false);
|
|
|
|
// PDF.js state
|
|
const pdfDocument = ref(null);
|
|
const pageRefs = ref(new Map());
|
|
const renderedPages = ref(new Set());
|
|
|
|
// Annotation state
|
|
const isAnnotating = ref(false);
|
|
const currentAnnotation = ref(null);
|
|
const showingComment = ref(false);
|
|
const commentText = ref('');
|
|
const editingComment = ref(null);
|
|
|
|
// Drawing state
|
|
const isDrawing = ref(false);
|
|
const drawingPath = ref('');
|
|
const drawingStartPoint = ref(null);
|
|
|
|
// Container refs
|
|
const canvasContainer = ref(null);
|
|
const drawingCanvas = ref(null);
|
|
|
|
// Computed
|
|
const annotationMode = computed(() => props.annotationMode);
|
|
|
|
// Methods
|
|
const setPageRef = (el, pageNum) => {
|
|
if (el) {
|
|
pageRefs.value.set(pageNum, el);
|
|
}
|
|
};
|
|
|
|
const loadPDF = async () => {
|
|
isLoading.value = true;
|
|
loadingProgress.value = 0;
|
|
|
|
try {
|
|
// Mock PDF loading - in real implementation, use PDF.js
|
|
// const loadingTask = pdfjsLib.getDocument(props.document.url);
|
|
// pdfDocument.value = await loadingTask.promise;
|
|
|
|
// Mock implementation
|
|
await simulatePDFLoading();
|
|
|
|
// Render initial pages
|
|
await renderVisiblePages();
|
|
await generateThumbnails();
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load PDF:', error);
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const simulatePDFLoading = async () => {
|
|
// Simulate PDF loading with progress
|
|
totalPages.value = Math.floor(Math.random() * 10) + 5; // 5-15 pages
|
|
|
|
for (let i = 1; i <= totalPages.value; i++) {
|
|
currentLoadingPage.value = i;
|
|
loadingProgress.value = (i / totalPages.value) * 100;
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
}
|
|
};
|
|
|
|
const renderVisiblePages = async () => {
|
|
// Get visible pages based on scroll position
|
|
const container = canvasContainer.value;
|
|
if (!container) return;
|
|
|
|
const containerRect = container.getBoundingClientRect();
|
|
const containerTop = container.scrollTop;
|
|
const containerBottom = containerTop + containerRect.height;
|
|
|
|
// Find visible pages
|
|
for (let pageNum = 1; pageNum <= totalPages.value; pageNum++) {
|
|
const pageElement = pageRefs.value.get(pageNum);
|
|
if (!pageElement) continue;
|
|
|
|
const pageRect = pageElement.getBoundingClientRect();
|
|
const pageTop = pageElement.offsetTop;
|
|
const pageBottom = pageTop + pageRect.height;
|
|
|
|
// Check if page is visible with some buffer
|
|
const buffer = 200;
|
|
if (pageBottom >= containerTop - buffer && pageTop <= containerBottom + buffer) {
|
|
if (!renderedPages.value.has(pageNum)) {
|
|
await renderPage(pageNum);
|
|
renderedPages.value.add(pageNum);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update current visible page
|
|
updateCurrentVisiblePage();
|
|
};
|
|
|
|
const renderPage = async (pageNum) => {
|
|
const canvas = document.getElementById(`pdf-page-${pageNum}`);
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Mock rendering - in real implementation, use PDF.js page.render()
|
|
canvas.width = 600;
|
|
canvas.height = 800;
|
|
|
|
// Fill with white background
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Add some mock content
|
|
ctx.fillStyle = '#333333';
|
|
ctx.font = '16px Arial';
|
|
ctx.fillText(`Page ${pageNum} Content`, 50, 50);
|
|
|
|
// Add some mock text lines
|
|
for (let i = 0; i < 20; i++) {
|
|
ctx.fillText(`This is line ${i + 1} of page ${pageNum}`, 50, 100 + i * 25);
|
|
}
|
|
};
|
|
|
|
const generateThumbnails = async () => {
|
|
for (let pageNum = 1; pageNum <= totalPages.value; pageNum++) {
|
|
const canvas = document.getElementById(`pdf-thumbnail-${pageNum}`);
|
|
if (!canvas) continue;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Mock thumbnail generation
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.fillRect(0, 0, 60, 80);
|
|
ctx.fillStyle = '#cccccc';
|
|
ctx.fillRect(5, 5, 50, 70);
|
|
ctx.fillStyle = '#333333';
|
|
ctx.font = '8px Arial';
|
|
ctx.fillText(`P${pageNum}`, 25, 40);
|
|
}
|
|
};
|
|
|
|
const updateCurrentVisiblePage = () => {
|
|
const container = canvasContainer.value;
|
|
if (!container) return;
|
|
|
|
const containerTop = container.scrollTop;
|
|
const containerCenter = containerTop + container.clientHeight / 2;
|
|
|
|
let closestPage = 1;
|
|
let closestDistance = Infinity;
|
|
|
|
for (let pageNum = 1; pageNum <= totalPages.value; pageNum++) {
|
|
const pageElement = pageRefs.value.get(pageNum);
|
|
if (!pageElement) continue;
|
|
|
|
const pageTop = pageElement.offsetTop;
|
|
const pageCenter = pageTop + pageElement.clientHeight / 2;
|
|
const distance = Math.abs(pageCenter - containerCenter);
|
|
|
|
if (distance < closestDistance) {
|
|
closestDistance = distance;
|
|
closestPage = pageNum;
|
|
}
|
|
}
|
|
|
|
if (closestPage !== currentVisiblePage.value) {
|
|
currentVisiblePage.value = closestPage;
|
|
emit('page-changed', closestPage);
|
|
}
|
|
};
|
|
|
|
const scrollToPage = (pageNum) => {
|
|
const pageElement = pageRefs.value.get(pageNum);
|
|
if (!pageElement || !canvasContainer.value) return;
|
|
|
|
const container = canvasContainer.value;
|
|
const pageTop = pageElement.offsetTop;
|
|
const containerHeight = container.clientHeight;
|
|
const pageHeight = pageElement.clientHeight;
|
|
|
|
// Center the page in the viewport
|
|
const scrollTop = pageTop - (containerHeight - pageHeight) / 2;
|
|
|
|
container.scrollTo({
|
|
top: Math.max(0, scrollTop),
|
|
behavior: 'smooth'
|
|
});
|
|
};
|
|
|
|
// Annotation methods
|
|
const getPageAnnotations = (pageNum, type) => {
|
|
return props.annotations.filter(annotation =>
|
|
annotation.page === pageNum && annotation.type === type
|
|
);
|
|
};
|
|
|
|
const handleMouseDown = (event) => {
|
|
if (!annotationMode.value) return;
|
|
|
|
const rect = event.target.getBoundingClientRect();
|
|
const x = event.clientX - rect.left;
|
|
const y = event.clientY - rect.top;
|
|
|
|
if (annotationMode.value === 'highlight') {
|
|
startHighlight(x, y, event.target);
|
|
} else if (annotationMode.value === 'comment') {
|
|
addComment(x, y, event.target);
|
|
}
|
|
};
|
|
|
|
const handleMouseMove = (event) => {
|
|
if (!isAnnotating.value || annotationMode.value !== 'highlight') return;
|
|
|
|
const rect = event.target.getBoundingClientRect();
|
|
const x = event.clientX - rect.left;
|
|
const y = event.clientY - rect.top;
|
|
|
|
updateHighlight(x, y);
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
if (isAnnotating.value && annotationMode.value === 'highlight') {
|
|
finishHighlight();
|
|
}
|
|
};
|
|
|
|
const startHighlight = (x, y, canvas) => {
|
|
const pageNum = getPageNumberFromCanvas(canvas);
|
|
if (!pageNum) return;
|
|
|
|
isAnnotating.value = true;
|
|
currentAnnotation.value = {
|
|
type: 'highlight',
|
|
page: pageNum,
|
|
x: x,
|
|
y: y,
|
|
width: 0,
|
|
height: 20,
|
|
color: '#ffff00',
|
|
text: 'Highlighted text',
|
|
author: 'Current User',
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
};
|
|
|
|
const updateHighlight = (x, y) => {
|
|
if (!currentAnnotation.value) return;
|
|
|
|
currentAnnotation.value.width = x - currentAnnotation.value.x;
|
|
currentAnnotation.value.height = Math.max(20, y - currentAnnotation.value.y);
|
|
};
|
|
|
|
const finishHighlight = () => {
|
|
if (!currentAnnotation.value) return;
|
|
|
|
const annotation = {
|
|
...currentAnnotation.value,
|
|
id: Date.now().toString()
|
|
};
|
|
|
|
emit('annotation-added', annotation);
|
|
|
|
isAnnotating.value = false;
|
|
currentAnnotation.value = null;
|
|
};
|
|
|
|
const addComment = (x, y, canvas) => {
|
|
const pageNum = getPageNumberFromCanvas(canvas);
|
|
if (!pageNum) return;
|
|
|
|
currentAnnotation.value = {
|
|
type: 'comment',
|
|
page: pageNum,
|
|
x: x,
|
|
y: y,
|
|
text: '',
|
|
author: 'Current User',
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
commentText.value = '';
|
|
editingComment.value = null;
|
|
showingComment.value = true;
|
|
};
|
|
|
|
const showCommentDialog = (comment) => {
|
|
editingComment.value = comment;
|
|
commentText.value = comment.text;
|
|
showingComment.value = true;
|
|
};
|
|
|
|
const closeCommentDialog = () => {
|
|
showingComment.value = false;
|
|
commentText.value = '';
|
|
editingComment.value = null;
|
|
currentAnnotation.value = null;
|
|
};
|
|
|
|
const saveComment = () => {
|
|
if (editingComment.value) {
|
|
// Update existing comment
|
|
const updatedComment = {
|
|
...editingComment.value,
|
|
text: commentText.value,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
emit('annotation-updated', updatedComment);
|
|
} else if (currentAnnotation.value) {
|
|
// Create new comment
|
|
const annotation = {
|
|
...currentAnnotation.value,
|
|
id: Date.now().toString(),
|
|
text: commentText.value
|
|
};
|
|
emit('annotation-added', annotation);
|
|
}
|
|
|
|
closeCommentDialog();
|
|
};
|
|
|
|
const editAnnotation = (annotation) => {
|
|
if (annotation.type === 'comment') {
|
|
showCommentDialog(annotation);
|
|
}
|
|
};
|
|
|
|
const deleteAnnotation = (annotationId) => {
|
|
if (confirm('Delete this annotation?')) {
|
|
emit('annotation-deleted', annotationId);
|
|
}
|
|
};
|
|
|
|
// Drawing methods
|
|
const startDrawing = (event) => {
|
|
if (annotationMode.value !== 'draw') return;
|
|
|
|
isDrawing.value = true;
|
|
const rect = drawingCanvas.value.getBoundingClientRect();
|
|
const x = event.clientX - rect.left;
|
|
const y = event.clientY - rect.top;
|
|
|
|
drawingStartPoint.value = { x, y };
|
|
drawingPath.value = `M ${x} ${y}`;
|
|
};
|
|
|
|
const draw = (event) => {
|
|
if (!isDrawing.value || annotationMode.value !== 'draw') return;
|
|
|
|
const rect = drawingCanvas.value.getBoundingClientRect();
|
|
const x = event.clientX - rect.left;
|
|
const y = event.clientY - rect.top;
|
|
|
|
drawingPath.value += ` L ${x} ${y}`;
|
|
|
|
// Draw on canvas for immediate feedback
|
|
const ctx = drawingCanvas.value.getContext('2d');
|
|
ctx.lineTo(x, y);
|
|
ctx.stroke();
|
|
};
|
|
|
|
const endDrawing = () => {
|
|
if (!isDrawing.value) return;
|
|
|
|
isDrawing.value = false;
|
|
|
|
// Create annotation from drawing
|
|
const annotation = {
|
|
id: Date.now().toString(),
|
|
type: 'draw',
|
|
page: currentVisiblePage.value,
|
|
path: drawingPath.value,
|
|
color: '#ff0000',
|
|
strokeWidth: 2,
|
|
author: 'Current User',
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
emit('annotation-added', annotation);
|
|
|
|
// Clear drawing canvas
|
|
const ctx = drawingCanvas.value.getContext('2d');
|
|
ctx.clearRect(0, 0, drawingCanvas.value.width, drawingCanvas.value.height);
|
|
|
|
drawingPath.value = '';
|
|
drawingStartPoint.value = null;
|
|
};
|
|
|
|
// Utility methods
|
|
const getPageNumberFromCanvas = (canvas) => {
|
|
const id = canvas.id;
|
|
const match = id.match(/pdf-page-(\d+)/);
|
|
return match ? parseInt(match[1]) : null;
|
|
};
|
|
|
|
const handleWheel = (event) => {
|
|
if (event.ctrlKey || event.metaKey) {
|
|
// Zoom with Ctrl + wheel
|
|
event.preventDefault();
|
|
const delta = event.deltaY > 0 ? -10 : 10;
|
|
const newZoom = Math.max(25, Math.min(300, zoom.value + delta));
|
|
zoom.value = newZoom;
|
|
emit('zoom-changed', newZoom);
|
|
|
|
// Show zoom indicator
|
|
showZoomIndicator.value = true;
|
|
setTimeout(() => {
|
|
showZoomIndicator.value = false;
|
|
}, 1000);
|
|
} else {
|
|
// Normal scroll - update visible pages
|
|
nextTick(() => {
|
|
renderVisiblePages();
|
|
});
|
|
}
|
|
};
|
|
|
|
// Watchers
|
|
watch(() => props.zoom, (newZoom) => {
|
|
zoom.value = newZoom;
|
|
});
|
|
|
|
watch(() => props.document, () => {
|
|
if (props.document) {
|
|
loadPDF();
|
|
}
|
|
});
|
|
|
|
watch(annotationMode, (newMode) => {
|
|
if (newMode === 'draw' && drawingCanvas.value) {
|
|
// Setup drawing canvas
|
|
const container = canvasContainer.value;
|
|
const canvas = drawingCanvas.value;
|
|
canvas.width = container.scrollWidth;
|
|
canvas.height = container.scrollHeight;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.strokeStyle = '#ff0000';
|
|
ctx.lineWidth = 2;
|
|
ctx.lineCap = 'round';
|
|
ctx.lineJoin = 'round';
|
|
}
|
|
});
|
|
|
|
// Lifecycle
|
|
onMounted(() => {
|
|
if (props.document) {
|
|
loadPDF();
|
|
}
|
|
|
|
// Setup scroll listener for lazy loading
|
|
if (canvasContainer.value) {
|
|
canvasContainer.value.addEventListener('scroll', () => {
|
|
nextTick(() => {
|
|
renderVisiblePages();
|
|
});
|
|
}, { passive: true });
|
|
}
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
// Cleanup
|
|
if (canvasContainer.value) {
|
|
canvasContainer.value.removeEventListener('scroll', renderVisiblePages);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.pdf-viewer {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
}
|
|
|
|
.pdf-canvas-container {
|
|
scroll-behavior: smooth;
|
|
}
|
|
|
|
.pdf-canvas-container::-webkit-scrollbar {
|
|
width: 12px;
|
|
height: 12px;
|
|
}
|
|
|
|
.pdf-canvas-container::-webkit-scrollbar-track {
|
|
background: #f1f1f1;
|
|
}
|
|
|
|
.pdf-canvas-container::-webkit-scrollbar-thumb {
|
|
background: #c1c1c1;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.pdf-canvas-container::-webkit-scrollbar-thumb:hover {
|
|
background: #a8a8a8;
|
|
}
|
|
|
|
.page-container {
|
|
transition: transform 0.3s ease;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.pdf-page {
|
|
display: block;
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
.annotation-layer {
|
|
pointer-events: none;
|
|
}
|
|
|
|
.annotation-layer > * {
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.thumbnail-page {
|
|
width: 60px;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.thumbnail-canvas {
|
|
display: block;
|
|
width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
/* Selection styles for text selection in annotation mode */
|
|
.pdf-viewer ::selection {
|
|
background: rgba(255, 255, 0, 0.3);
|
|
}
|
|
|
|
.pdf-viewer ::-moz-selection {
|
|
background: rgba(255, 255, 0, 0.3);
|
|
}
|
|
|
|
/* Annotation cursor styles */
|
|
.annotation-mode-highlight {
|
|
cursor: text;
|
|
}
|
|
|
|
.annotation-mode-comment {
|
|
cursor: crosshair;
|
|
}
|
|
|
|
.annotation-mode-draw {
|
|
cursor: crosshair;
|
|
}
|
|
|
|
/* Smooth animations */
|
|
.page-container,
|
|
.thumbnail-page {
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.annotation-layer > div {
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
|
|
/* Loading animation */
|
|
@keyframes spin {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.animate-spin {
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
</style> |