<template>
<Teleport to="body">
<div v-if="visible" class="pdf-mask" @mousedown="startDrag">
<div ref="modalRef" class="pdf-modal" :style="modalStyle">
<!-- PDF 头部:标题 + 操作按钮 -->
<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>
<!-- PDF 标签页 + 内容区域 -->
<el-tabs v-model="activeTab" class="pdf-tabs" @tab-click="handleTabClick">
<el-tab-pane
v-for="tab in tabs"
:key="tab.id"
:label="tab.name"
:name="tab.id.toString()"
>
<div
class="pdf-body"
:ref="(el) => el && scrollRefMap.set(tab.id, el as HTMLDivElement)"
@wheel="(e) => onWheel(e, tab.id)"
@mousedown="(e) => onPdfMouseDown(e, tab.id)"
>
<div :ref="(el) => el && setPdfContainer(tab.id, el as HTMLDivElement)" />
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref, reactive, watch, nextTick, onUnmounted } from 'vue'
import type { CSSProperties } from 'vue'
import * as pdfjsLib from 'pdfjs-dist'
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?worker&url'
// 修复类型导入
import type {
PDFDocumentLoadingTask,
PDFDocumentProxy,
PDFPageProxy,
RenderParameters
} from 'pdfjs-dist/types/src/display/api'
import type { PageViewport } from 'pdfjs-dist'
import { ElButton, ElTabs, ElTabPane } from 'element-plus'
import type { TabsPaneContext } from 'element-plus'
// PDF.js 初始化:设置 Worker 路径
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker
/* ---------------- Props & Emits 定义 ---------------- */
const props = defineProps<{
visible: boolean
url?: string
highlight?: { x0: number; y0: number; x1: number; y1: number } | null
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
(e: 'tabschange', tabId: string | number): void
}>()
/* ---------------- 状态管理 ---------------- */
const tabs = ref<Array<{ id: string | number; name: string; url: string }>>([])
const activeTab = ref<string>('')
const pdfContainerMap = new Map<string | number, HTMLDivElement>()
const scrollRefMap = new Map<string | number, HTMLDivElement>()
const tabScale = new Map<string | number, number>()
const tabBaseViewport = new Map<string | number, PageViewport>()
const pdfRealSize = ref({ width: 0, height: 0 })
const modalRef = ref<HTMLDivElement | null>(null)
let abortController: AbortController | null = null
let currentScrollContainer: HTMLDivElement | null = null
/* ---------------- 弹窗位置 & 样式 ---------------- */
const modalStyle = reactive<{
left: string
top: string
width: string
height: string
cursor: CSSProperties['cursor']
}>({
left: '0px',
top: '0px',
width: '800px',
height: '600px',
cursor: 'default'
})
let dragStart: { x: number; y: number } | null = null
/* ---------------- 标签页初始化 & 渲染 ---------------- */
async function openWithTabs(
list: Array<{ code: string; name: string; url: string }>
) {
tabs.value = []
pdfContainerMap.clear()
scrollRefMap.clear()
tabScale.clear()
tabBaseViewport.clear()
if (abortController) {
abortController.abort()
abortController = null
}
tabs.value = list
.filter(item => item.name !== 'all_files')
.map(item => ({ id: item.code, name: item.name, url: item.url }))
const firstTabId = tabs.value[0]?.id.toString() ?? ''
activeTab.value = firstTabId
emit('update:visible', true)
await nextTick()
const firstTab = tabs.value[0]
if (firstTab) {
const container = pdfContainerMap.get(firstTab.id)
if (container) {
await renderSinglePDF(firstTab.url, container, firstTab.id)
}
}
// 弹窗居中
const windowWidth = window.innerWidth
const windowHeight = window.innerHeight
modalStyle.left = `${(windowWidth - 800) / 2}px`
modalStyle.top = `${(windowHeight - 600) / 2}px`
}
function setPdfContainer(tabId: string | number, el: HTMLDivElement) {
pdfContainerMap.set(tabId, el)
}
async function renderSinglePDF(
pdfUrl: string,
container: HTMLDivElement,
currentTabId: string | number
) {
if (activeTab.value !== currentTabId.toString()) return
if (!(container instanceof HTMLDivElement)) return
if (abortController) {
abortController.abort()
}
abortController = new AbortController()
container.innerHTML = ''
const scrollContainer = scrollRefMap.get(currentTabId)
if (!scrollContainer) return
try {
// 修复类型:使用 PDFDocumentLoadingTask
const pdfLoadingTask: PDFDocumentLoadingTask = pdfjsLib.getDocument({
url: pdfUrl,
signal: abortController.signal
} as any) as PDFDocumentLoadingTask
const pdfDoc: PDFDocumentProxy = await pdfLoadingTask.promise
const pdfPage: PDFPageProxy = await pdfDoc.getPage(1)
if (activeTab.value !== currentTabId.toString()) return
const baseViewport = pdfPage.getViewport({ scale: 1 })
tabBaseViewport.set(currentTabId, baseViewport)
const currentScale = tabScale.get(currentTabId) ?? 1
const renderScale = currentScale * (800 / baseViewport.width)
const renderViewport = pdfPage.getViewport({ scale: renderScale })
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (!ctx) {
container.innerHTML = '<div class="pdf-error">Canvas 初始化失败</div>'
return
}
canvas.width = renderViewport.width
canvas.height = renderViewport.height
canvas.dataset.scale = String(currentScale)
container.appendChild(canvas)
// 修复渲染参数类型
const renderParams: RenderParameters = {
canvasContext: ctx,
viewport: renderViewport,
signal: abortController
} as any
await pdfPage.render(renderParams).promise
if (activeTab.value !== currentTabId.toString()) {
container.innerHTML = ''
return
}
scrollContainer.style.width = `${renderViewport.width}px`
scrollContainer.style.height = `${renderViewport.height}px`
pdfRealSize.value = {
width: renderViewport.width,
height: renderViewport.height
}
if (props.highlight) {
drawHighlight(props.highlight, currentTabId)
}
} catch (error) {
if ((error as Error).name !== 'AbortError') {
console.error(`PDF 加载失败:`, error)
container.innerHTML = '<div class="pdf-error">PDF 加载失败,请重试</div>'
}
}
}
/* ---------------- 标签页切换 & 缩放 ---------------- */
function handleTabClick(tab: TabsPaneContext) {
emit('tabschange', tab.paneName)
}
watch(activeTab, (newTabIdStr) => {
const newTabId = tabs.value.find(tab => tab.id.toString() === newTabIdStr)?.id
if (!newTabId) return
const targetTab = tabs.value.find(tab => tab.id === newTabId)
const targetContainer = pdfContainerMap.get(newTabId)
if (targetTab && targetContainer) {
renderSinglePDF(targetTab.url, targetContainer, newTabId)
}
}, { immediate: false })
const zoomIn = () => {
const currentTabIdStr = activeTab.value
const currentTabId = tabs.value.find(tab => tab.id.toString() === currentTabIdStr)?.id
if (!currentTabId) return
const newScale = Math.min((tabScale.get(currentTabId) ?? 1) + 0.2, 3)
tabScale.set(currentTabId, newScale)
rerenderActiveTab()
}
const zoomOut = () => {
const currentTabIdStr = activeTab.value
const currentTabId = tabs.value.find(tab => tab.id.toString() === currentTabIdStr)?.id
if (!currentTabId) return
const newScale = Math.max((tabScale.get(currentTabId) ?? 1) - 0.2, 0.5)
tabScale.set(currentTabId, newScale)
rerenderActiveTab()
}
async function rerenderActiveTab() {
const currentTabIdStr = activeTab.value
const currentTabId = tabs.value.find(tab => tab.id.toString() === currentTabIdStr)?.id
if (!currentTabId) return
const targetTab = tabs.value.find(tab => tab.id === currentTabId)
const targetContainer = pdfContainerMap.get(currentTabId)
if (targetTab && targetContainer) {
await renderSinglePDF(targetTab.url, targetContainer, currentTabId)
}
}
/* ---------------- 弹窗拖拽功能 ---------------- */
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)
}
/* ---------------- PDF 内容拖拽 ---------------- */
let isPdfDragging = false
let pdfDragStart = { x: 0, y: 0 }
let scrollStart = { left: 0, top: 0 }
function onPdfMouseDown(e: MouseEvent, tabId: string | number) {
const scrollContainer = scrollRefMap.get(tabId)
if (!scrollContainer) return
const currentScale = tabScale.get(tabId) ?? 1
if (currentScale <= 1) return
isPdfDragging = true
pdfDragStart = { x: e.clientX, y: e.clientY }
scrollStart = {
left: scrollContainer.scrollLeft,
top: scrollContainer.scrollTop
}
currentScrollContainer = scrollContainer
document.addEventListener('mousemove', onPdfMouseMove)
document.addEventListener('mouseup', onPdfMouseUp)
e.preventDefault()
}
function onPdfMouseMove(e: MouseEvent) {
if (!isPdfDragging || !currentScrollContainer) return
const dx = e.clientX - pdfDragStart.x
const dy = e.clientY - pdfDragStart.y
currentScrollContainer.scrollTo({
left: scrollStart.left - dx,
top: scrollStart.top - dy,
behavior: 'auto'
})
}
function onPdfMouseUp() {
isPdfDragging = false
currentScrollContainer = null
document.removeEventListener('mousemove', onPdfMouseMove)
document.removeEventListener('mouseup', onPdfMouseUp)
}
/* ---------------- 滚轮事件 ---------------- */
function onWheel(e: WheelEvent, tabId: string | number) {
e.stopPropagation()
const scrollContainer = scrollRefMap.get(tabId)
if (scrollContainer) {
// 考虑缩放比例的滚动速度
const scale = tabScale.get(tabId) ?? 1
scrollContainer.scrollLeft -= e.deltaX * scale
scrollContainer.scrollTop -= e.deltaY * scale
e.preventDefault()
}
}
/* ---------------- 高亮区域绘制 ---------------- */
function drawHighlight(
rect: { x0: number; y0: number; x1: number; y1: number },
tabId?: string | number
) {
const targetTabId = tabId ?? (
tabs.value.find(tab => tab.id.toString() === activeTab.value)?.id ?? ''
)
const pdfContainer = pdfContainerMap.get(targetTabId)
const scrollContainer = scrollRefMap.get(targetTabId)
if (!pdfContainer || !scrollContainer) return
pdfContainer.querySelectorAll('.pdf-highlight').forEach(el => el.remove())
const canvas = pdfContainer.querySelector('canvas')
const baseViewport = tabBaseViewport.get(targetTabId)
if (!canvas || !baseViewport) return
const currentScale = parseFloat(canvas.dataset.scale || '1')
const scaleX = canvas.width / baseViewport.width
const scaleY = canvas.height / baseViewport.height
// 修复:考虑容器偏移
const containerRect = pdfContainer.getBoundingClientRect()
const highlightStyle = {
left: `${rect.x0 * scaleX}px`,
top: `${rect.y0 * scaleY}px`,
width: `${(rect.x1 - rect.x0) * scaleX}px`,
height: `${(rect.y1 - rect.y0) * scaleY}px`
}
const highlightEl = document.createElement('div')
highlightEl.className = 'pdf-highlight'
Object.assign(highlightEl.style, {
...highlightStyle,
position: 'absolute',
background: 'rgba(0, 255, 0, 0.4)',
border: '2px dashed #ff4d4f',
pointerEvents: 'none',
zIndex: 9999
})
pdfContainer.style.position = 'relative'
pdfContainer.appendChild(highlightEl)
nextTick(() => {
const containerWidth = scrollContainer.clientWidth
const containerHeight = scrollContainer.clientHeight
scrollContainer.scrollTo({
left: Math.max(0, parseFloat(highlightStyle.left) - containerWidth/3),
top: Math.max(0, parseFloat(highlightStyle.top) - containerHeight/3),
behavior: 'smooth'
})
})
}
function drawHighlightByCoords(rect: { x0: number; y0: number; x1: number; y1: number }) {
drawHighlight(rect)
}
/* ---------------- 监听 Props 变化 ---------------- */
watch(
() => props.visible,
async (isVisible) => {
if (!isVisible) {
tabScale.clear()
if (abortController) {
abortController.abort()
abortController = null
}
currentScrollContainer = null
return
}
await nextTick()
const windowWidth = window.innerWidth
const windowHeight = window.innerHeight
modalStyle.left = `${(windowWidth - 800) / 2}px`
modalStyle.top = `${(windowHeight - 600) / 2}px`
},
{ immediate: true }
)
watch(
() => props.highlight,
(newHighlight) => {
if (newHighlight && activeTab.value) {
drawHighlight(newHighlight)
}
},
{ immediate: false, deep: true }
)
/* ---------------- 关闭弹窗 ---------------- */
function close() {
if (abortController) {
abortController.abort()
abortController = null
}
currentScrollContainer = null
pdfRealSize.value = { width: 0, height: 0 }
emit('update:visible', false)
}
/* ---------------- 组件卸载清理 ---------------- */
onUnmounted(() => {
if (abortController) {
abortController.abort()
abortController = null
}
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
document.removeEventListener('mousemove', onPdfMouseMove)
document.removeEventListener('mouseup', onPdfMouseUp)
pdfContainerMap.clear()
scrollRefMap.clear()
tabScale.clear()
tabBaseViewport.clear()
currentScrollContainer = null
})
/* ---------------- 暴露方法 ---------------- */
defineExpose({
openWithTabs,
drawHighlight: drawHighlightByCoords
})
</script>
<style scoped>
.pdf-mask {
position: fixed;
inset: 0;
z-index: 9999;
background: rgba(0, 0, 0, 0.3);
}
.pdf-modal {
position: absolute;
background: #ffffff;
border: 1px solid #e4e7ed;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
min-width: 400px;
min-height: 300px;
resize: both;
overflow: hidden;
}
.pdf-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #f5f7fa;
cursor: grab;
border-bottom: 1px solid #e4e7ed;
user-select: none;
}
.pdf-header:active {
cursor: grabbing;
}
.pdf-header .title {
font-size: 16px;
font-weight: 500;
color: #1f2937;
}
.pdf-header .tools {
display: flex;
gap: 8px;
}
.pdf-tabs {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.pdf-body {
flex: 1;
overflow: auto;
position: relative;
}
.pdf-body canvas {
pointer-events: none;
display: block;
}
.pdf-highlight {
box-sizing: border-box;
}
.pdf-tabs .el-tabs__content {
flex: 1;
overflow: hidden;
padding: 0 !important;
}
.pdf-tabs .el-tab-pane {
height: 100%;
display: flex;
flex-direction: column;
}
/* 错误提示样式 */
.pdf-error {
padding: 20px;
text-align: center;
color: #f56c6c;
}
</style>
这个代码的功能是可以的,但是有一个问题,就是我放大很多很多倍之后,就会只显示左上角,拖拽只能上下拖拽,右边的数据根本看不到,根本拖拽不了
最新发布