我需要把放大和缩小的功能变为用鼠标滚轮控制,并且图片放大时,不能只固定在左上角,我还需要可以用数据拖拽到放大的其他区域,以下就是相关vue代码
<template>
<Teleport to="body">
<div v-if="visible" class="pdf-mask" @mousedown="startDrag">
<div ref="modalRef" class="pdf-modal" :style="modalStyle">
<div class="pdf-header">
<span class="title">报关资料</span>
<div class="tools">
<el-button size="small" @click="zoomIn">放大</el-button>
<el-button size="small" @click="zoomOut">缩小</el-button>
<el-button size="small" @click="close">关闭</el-button>
</div>
</div>
<!-- 滚动容器 -->
<el-tabs v-model="activeTab" class="pdf-tabs">
<el-tab-pane v-for="tab in tabs" :key="tab.id" :label="tab.name" :name="tab.id">
<!-- 单个 PDF 容器 -->
<div class="pdf-body" ref="scrollRef" @wheel="onWheel" @mousedown="onPdfMouseDown">
<div :ref="(el) => setPdfContainer(tab.id, el)" />
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref, reactive, watch, nextTick } from 'vue'
import * as pdfjsLib from 'pdfjs-dist'
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?worker&url'
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker
const emit = defineEmits<{
(e: 'update:visible', v: boolean): void
(e: 'tabschange', id: string | number): void // 新增
}>()
const pdfRealSize = ref({ width: 0, height: 0 })
/* tabs 数据 */
const tabs = ref<Array<{ id: string | number, name: string, url: string }>>([])
const activeTab = ref<string | number>('')
async function openWithTabs(
list: Array<{ code: string; name: string; url: string }>
) {
tabs.value = list
.filter((i) => i.name !== 'all_files')
.map((i) => ({ id: i.code, name: i.name, url: i.url }))
const firstId = tabs.value[0]?.id
activeTab.value = firstId ?? ''
// 手动让弹窗显示
emit('update:visible', true)
// 等 DOM 更新完,立即渲染第一页
await nextTick()
if (firstId) {
const container = pdfContainerMap.get(firstId)
const tab = tabs.value.find((t) => t.id === firstId)
if (container && tab) {
await renderSinglePDF(tab.url, container)
}
}
// 弹框居中
const cw = window.innerWidth,
ch = window.innerHeight
modalStyle.left = (cw - 800) / 2 + 'px'
modalStyle.top = (ch - 600) / 2 + 'px'
}
const pdfContainerMap = new Map<string | number, HTMLDivElement>()
function setPdfContainer(id: string | number, el: any) {
if (el) pdfContainerMap.set(id, el as HTMLDivElement)
}
watch(activeTab, (id) => {
const tab = tabs.value.find(t => t.id === id)
if (!tab) return
renderSinglePDF(tab.url, pdfContainerMap.get(id)!)
})
/* ---------------- props & emits ---------------- */
const props = defineProps<{
visible: boolean
url?: string
highlight?: { x0: number; y0: number; x1: number; y1: number } | null
}>()
/* ---------------- 弹窗位置 ---------------- */
const modalRef = ref<HTMLDivElement>()
const pdfContainer = ref<HTMLDivElement>()
const scrollRef = ref<HTMLDivElement>() // 新增:滚动容器
const modalStyle = reactive({
left: '0px',
top: '0px',
width: '800px',
height: '600px',
cursor: 'default'
})
let dragStart: { x: number; y: number } | null = null
/* ---------------- 拖拽 ---------------- */
function startDrag(e: MouseEvent) {
const target = e.target as HTMLElement
if (!target.classList.contains('pdf-mask') && !target.closest('.pdf-header')) return
if (!modalRef.value) return
dragStart = { x: e.clientX, y: e.clientY }
modalStyle.cursor = 'grabbing'
document.addEventListener('mousemove', onDrag)
document.addEventListener('mouseup', stopDrag)
e.preventDefault()
}
function onDrag(e: MouseEvent) {
if (!dragStart) return
const dx = e.clientX - dragStart.x
const dy = e.clientY - dragStart.y
modalStyle.left = parseFloat(modalStyle.left) + dx + 'px'
modalStyle.top = parseFloat(modalStyle.top) + dy + 'px'
dragStart = { x: e.clientX, y: e.clientY }
}
function stopDrag() {
dragStart = null
modalStyle.cursor = 'default'
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
if (modalRef.value) {
modalStyle.width = modalRef.value.offsetWidth + 'px'
modalStyle.height = modalRef.value.offsetHeight + 'px'
}
}
/* ---------------- 缩放 ---------------- */
let scale = 1
// 1️⃣ 换掉全局 scale → 每个 tab 独立
const tabScale = new Map<string | number, number>()
const getScale = (id: string | number) => tabScale.get(id) ?? 1
const setScale = (id: string | number, v: number) =>
tabScale.set(id, Math.min(Math.max(v, 0.5), 3))
// 2️⃣ 放大 / 缩小
const zoomIn = () => { setScale(activeTab.value, getScale(activeTab.value) + 0.2); rerenderActiveTab() }
const zoomOut = () => { setScale(activeTab.value, getScale(activeTab.value) - 0.2); rerenderActiveTab() }
// 3️⃣ 重新渲染当前 tab
async function rerenderActiveTab() {
const id = activeTab.value
const tab = tabs.value.find(t => t.id === id)
const container = pdfContainerMap.get(id)
if (tab && container) await renderSinglePDF(tab.url, container)
}
// 4️⃣ renderSinglePDF 改用当前 tab 的 scale
let baseViewport: pdfjsLib.PageViewport | null = null
const tabBaseViewport = new Map<string | number, pdfjsLib.PageViewport>()
async function renderSinglePDF(url: string, container: HTMLDivElement) {
if (!container) return
container.innerHTML = ''
const pdf = await pdfjsLib.getDocument(url).promise
const page = await pdf.getPage(1)
baseViewport = page.getViewport({ scale: 1 }) // ← 保存原始 viewport
tabBaseViewport.set(activeTab.value, baseViewport)
const scale = getScale(activeTab.value) * (800 / baseViewport.width)
const vp = page.getViewport({ scale })
const canvas = document.createElement('canvas')
canvas.width = vp.width
canvas.height = vp.height
const ctx = canvas.getContext('2d')!
container.appendChild(canvas)
await page.render({
canvasContext: ctx,
viewport: vp,
canvas
}).promise;
// canvas.dataset.scale = String(scale) // ← 保存当前缩放比例
// 计算当前缩放后的真实尺寸
// 新增:更新滚动容器尺寸
canvas.dataset.scale = String(getScale(activeTab.value))
pdfRealSize.value = { width: vp.width, height: vp.height }
const scrollBox = container.parentElement as HTMLDivElement
if (scrollBox) {
scrollBox.style.width = vp.width + 'px'
scrollBox.style.height = vp.height + 'px'
}
}
/* ---------------- PDF 逻辑宽高 ---------------- */
const pdfLogicWidth = ref(0)
const pdfLogicHeight = ref(0)
/* ---------------- PDF 渲染 ---------------- */
async function renderPDF() {
if (!pdfContainer.value || !props.url) return
pdfContainer.value.innerHTML = ''
try {
const pdf = await pdfjsLib.getDocument(props.url).promise
const page = await pdf.getPage(1)
const baseViewport = page.getViewport({ scale: 1 })
pdfLogicWidth.value = baseViewport.width
pdfLogicHeight.value = baseViewport.height
const viewport = page.getViewport({ scale: (800 / pdfLogicWidth.value) * scale })
const canvas = document.createElement('canvas')
canvas.width = viewport.width
canvas.height = viewport.height
const ctx = canvas.getContext('2d')!
pdfContainer.value.appendChild(canvas)
await page.render({ canvasContext: ctx, viewport, canvas }).promise
if (props.highlight) drawHighlight(props.highlight)
} catch (err) {
console.error('Failed to load PDF:', err)
}
}
/* ---------------- 高亮 + 自动滚动 ---------------- */
function drawHighlight(rect: { x0: number; y0: number; x1: number; y1: number }) {
const id = activeTab.value
const container = pdfContainerMap.get(id)
const scrollContainer = container?.parentElement as HTMLDivElement
if (!container || !scrollContainer) return
// 清除旧高亮
container.querySelectorAll('.pdf-highlight').forEach(el => el.remove())
const canvas = container.querySelector('canvas')
if (!canvas) return
const canvasScale = parseFloat(canvas.dataset.scale || '1')
const baseViewport = tabBaseViewport.get(id)
if (!baseViewport) return
// 将 PDF 原始坐标 → canvas 渲染坐标
const sx = canvas.width / baseViewport.width
const sy = canvas.height / baseViewport.height
const hlLeft = rect.x0 * sx
const hlTop = rect.y0 * sy
const hlWidth = (rect.x1 - rect.x0) * sx
const hlHeight = (rect.y1 - rect.y0) * sy
const div = document.createElement('div')
div.className = 'pdf-highlight'
Object.assign(div.style, {
position: 'absolute',
left: hlLeft + 'px',
top: hlTop + 'px',
width: hlWidth + 'px',
height: hlHeight + 'px',
background: 'rgba(0,255,0,0.4)',
border: '2px dashed #ff4d4f',
pointerEvents: 'none',
zIndex: 9999
})
container.style.position = 'relative'
container.appendChild(div)
/* 自动滚动 */
nextTick(() => {
const contW = scrollContainer.clientWidth
const contH = scrollContainer.clientHeight
scrollContainer.scrollTo({
left: Math.max(0, hlLeft + hlWidth / 2 - contW / 2),
top: Math.max(0, hlTop + hlHeight / 2 - contH / 2),
behavior: 'smooth'
})
})
}
/* ---------------- 拖拽 PDF 内容 ---------------- */
let pdfDragging = false
let pdfDragStart = { x: 0, y: 0 }
let scrollStart = { left: 0, top: 0 }
function onPdfMouseDown(e: MouseEvent) {
const container = scrollRef.value
if (!container) return
// 只有在放大时才允许拖动
const scale = getScale(activeTab.value)
if (scale <= 1) return
pdfDragging = true
pdfDragStart = { x: e.clientX, y: e.clientY }
scrollStart = { left: container.scrollLeft, top: container.scrollTop }
document.addEventListener('mousemove', onPdfMouseMove)
document.addEventListener('mouseup', onPdfMouseUp)
e.preventDefault()
}
function onPdfMouseMove(e: MouseEvent) {
if (!pdfDragging) return
const dx = e.clientX - pdfDragStart.x
const dy = e.clientY - pdfDragStart.y
const container = scrollRef.value
if (container) {
container.scrollTo({
left: scrollStart.left - dx,
top: scrollStart.top - dy
})
}
}
function onPdfMouseUp() {
pdfDragging = false
document.removeEventListener('mousemove', onPdfMouseMove)
document.removeEventListener('mouseup', onPdfMouseUp)
}
/* ---------------- 阻止滚轮冒泡 ---------------- */
function onWheel(e: WheelEvent) {
e.stopPropagation()
}
/* ---------------- 监听 visible ---------------- */
watch(
() => props.visible,
async (v, ov) => {
if (!v) { scale = 1; return }
await nextTick()
if (ov === false) {
const cw = window.innerWidth
const ch = window.innerHeight
const mw = 800
const mh = 600
modalStyle.left = (cw - mw) / 2 + 'px'
modalStyle.top = (ch - mh) / 2 + 'px'
}
renderPDF()
},
{ immediate: true }
)
/* ---------------- 关闭 ---------------- */
function close() {
pdfRealSize.value = { width: 0, height: 0 }
emit('update:visible', false)
}
/* ---------------- 监听 highlight ---------------- */
watch(
() => props.highlight,
(newVal) => {
if (newVal) drawHighlight(newVal)
},
{ immediate: false }
)
watch(activeTab, (id) => {
const tab = tabs.value.find(t => t.id === id)
if (!tab) return
emit('tabschange', id)
renderSinglePDF(tab.url, pdfContainerMap.get(id)!)
})
function drawHighlightByCoords(rect: { x0: number; y0: number; x1: number; y1: number }) {
drawHighlight(rect)
}
onMounted(async () => {
await nextTick();
const scrollContainer = scrollRef.value;
if (scrollContainer) {
scrollContainer.addEventListener('mousedown', onPdfMouseDown);
}
});
defineExpose({ openWithTabs, drawHighlight: drawHighlightByCoords })
</script>
<style scoped>
.pdf-mask {
position: fixed;
inset: 0;
z-index: 9999;
pointer-events: none;
}
.pdf-modal {
position: absolute;
background: #fff;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
pointer-events: auto;
min-width: 400px;
min-height: 300px;
resize: both;
overflow: hidden;
}
.pdf-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #f5f5f5;
cursor: grab;
border-bottom: 1px solid #e4e4e4;
user-select: none;
}
.pdf-header:active {
cursor: grabbing;
}
/* .pdf-body {
flex: 1;
overflow: auto;
} */
.pdf-body {
flex: none;
/* 不拉伸 */
overflow: auto;
/* 出现滚动条 */
/* 宽高由 JS 动态设置,这里不再写死 */
}
.pdf-body canvas {
pointer-events: none;
}
.pdf-modal {
height: 700px;
}
</style>
最新发布