feature: Digitization Workstation UI
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="h-full flex flex-col bg-gray-900 rounded-lg overflow-hidden">
|
||||
<!-- Toolbar -->
|
||||
<div class="flex flex-wrap items-center gap-2 p-4 bg-gray-800 text-white text-sm">
|
||||
<button @click="zoomIn" class="px-2 py-1 hover:bg-gray-700 rounded" title="Zoom in">
|
||||
+
|
||||
</button>
|
||||
<button @click="zoomOut" class="px-2 py-1 hover:bg-gray-700 rounded" title="Zoom out">
|
||||
-
|
||||
</button>
|
||||
<button @click="resetZoom" class="px-2 py-1 hover:bg-gray-700 rounded" title="Reset">
|
||||
1:1
|
||||
</button>
|
||||
<button @click="rotateCw" class="px-2 py-1 hover:bg-gray-700 rounded" title="Rotate 90">
|
||||
Rotate
|
||||
</button>
|
||||
<span class="ml-auto text-gray-400 text-xs">{{ Math.round(scale * 100) }}%</span>
|
||||
</div>
|
||||
|
||||
<!-- Document area -->
|
||||
<div
|
||||
ref="viewerContainer"
|
||||
class="flex-1 overflow-auto cursor-grab active:cursor-grabbing"
|
||||
@mousedown="startPan"
|
||||
@mousemove="pan"
|
||||
@mouseup="stopPan"
|
||||
@mouseleave="stopPan"
|
||||
@wheel.prevent="onWheel"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
transform: `translate(${panX}px, ${panY}px) scale(${scale}) rotate(${rotation}deg)`,
|
||||
transformOrigin: 'top left',
|
||||
transition: isPanning ? 'none' : 'transform 0.2s',
|
||||
}"
|
||||
>
|
||||
<!-- PDF rendering via iframe for simplicity; production would use pdf.js -->
|
||||
<iframe
|
||||
v-if="isPdf"
|
||||
:src="url"
|
||||
class="w-[800px] h-[1100px] bg-white"
|
||||
frameborder="0"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
:src="url"
|
||||
class="max-w-none"
|
||||
draggable="false"
|
||||
@load="onImageLoad"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
url: string
|
||||
}>()
|
||||
|
||||
const isPdf = computed(() => {
|
||||
const lower = props.url.toLowerCase()
|
||||
return lower.includes('.pdf') || lower.includes('application/pdf')
|
||||
})
|
||||
|
||||
const scale = ref(1)
|
||||
const rotation = ref(0)
|
||||
const panX = ref(0)
|
||||
const panY = ref(0)
|
||||
const isPanning = ref(false)
|
||||
const lastX = ref(0)
|
||||
const lastY = ref(0)
|
||||
|
||||
function zoomIn() { scale.value = Math.min(scale.value + 0.25, 5) }
|
||||
function zoomOut() { scale.value = Math.max(scale.value - 0.25, 0.25) }
|
||||
function resetZoom() {
|
||||
scale.value = 1
|
||||
panX.value = 0
|
||||
panY.value = 0
|
||||
rotation.value = 0
|
||||
}
|
||||
function rotateCw() { rotation.value = (rotation.value + 90) % 360 }
|
||||
|
||||
function onWheel(e: WheelEvent) {
|
||||
if (e.deltaY < 0) zoomIn()
|
||||
else zoomOut()
|
||||
}
|
||||
|
||||
function startPan(e: MouseEvent) {
|
||||
isPanning.value = true
|
||||
lastX.value = e.clientX
|
||||
lastY.value = e.clientY
|
||||
}
|
||||
|
||||
function pan(e: MouseEvent) {
|
||||
if (!isPanning.value) return
|
||||
panX.value += e.clientX - lastX.value
|
||||
panY.value += e.clientY - lastY.value
|
||||
lastX.value = e.clientX
|
||||
lastY.value = e.clientY
|
||||
}
|
||||
|
||||
function stopPan() { isPanning.value = false }
|
||||
|
||||
function onImageLoad() {
|
||||
// Reset view when a new image loads
|
||||
scale.value = 1
|
||||
panX.value = 0
|
||||
panY.value = 0
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user