feat(miaoPDF): 重构PDF查看器并添加拖拽和缩放功能

- 将margin缩放方案改为更精确的scale缩放方案
- 添加拖拽功能,支持长按后拖动PDF内容
- 重新设计顶部工具栏样式和布局
- 添加缩放比例显示和操作提示
This commit is contained in:
539943419 committed 2025-08-23 02:54:40 +08:00
1 parent 9ee472e5de
commit 79221d37d6
1 file changed
+214 -63
+214 -63
View File
@@ -2,48 +2,30 @@
<miao-message-provider ref="miaoMessageRef">
<div class="miao-container">
<div class="miao-container-topBar">
<div
class="miao-container-topBar-btn"
@click="() => handleZoom(10)"
title="快速缩小">
--
</div>
<div
class="miao-container-topBar-btn"
@click="() => handleZoom()"
title="缩小">
-
</div>
<div
class="miao-container-topBar-btn"
@click="() => handleResetMargin()"
title="还原">
还原缩放
</div>
<div
class="miao-container-topBar-btn"
@click="() => handleShrink()"
title="放大">
+
</div>
<div
class="miao-container-topBar-btn"
@click="() => handleShrink(10)"
title="快速放大">
++
<div class="topBar-main">
<div class="top-btn" @click="() => handleZoom(10)" title="快速缩小">--</div>
<div class="top-btn" @click="() => handleZoom()" title="缩小">-</div>
<div class="top-btn" @click="() => handleResetMargin()" title="重置">重置</div>
<div class="top-btn" @click="() => handleShrink()" title="放大">+</div>
<div class="top-btn" @click="() => handleShrink(10)" title="快速放大">++</div>
<div class="scale-indicator">{{ Math.round(scale * 100) }}%</div>
</div>
</div>
<NScrollbar>
<NScrollbar ref="scrollRef">
<div
class="miao-container-pdf"
:style="{
alignItems: pdfMargin < 0 ? 'baseline' : 'center'
}">
@pointerdown="onPointerDown"
@pointermove="onPointerMove"
@pointerup="onPointerUp"
@pointercancel="onPointerCancel"
:class="{ 'is-dragging': isDragging}"
:style="{'align-items': scale > 1 ? 'unset' : 'center' }">
<div
ref="pdfEl"
class="miao-container-pdf-content"
:style="{
width: `calc( 100% - ${pdfMargin}px)`
width: `${(scale * 100).toFixed(2)}%`,
}"></div>
</div>
</NScrollbar>
@@ -53,7 +35,7 @@
<script setup lang="ts">
import miaoMessageProvider from '@/components/miaoAlertTipProvider.vue'
import { onMounted, ref, shallowReactive } from 'vue'
import { onMounted, ref, shallowReactive, computed, onBeforeUnmount } from 'vue'
import PdfjsWorker from 'pdfjs-dist/build/pdf.worker.mjs?worker'
import { VirtualFile } from '@/class/VirtualDirectory'
import { NScrollbar } from 'naive-ui'
@@ -64,31 +46,148 @@ const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
const pdfEl = ref<HTMLDivElement>()
const miaoMessageRef = ref<InstanceType<typeof miaoMessageProvider>>()
// 通过margin来实现缩小功能
const pdfMargin = ref<number>(0)
// 新缩放方案:使用等比scale
const scale = ref<number>(1)
const minScale = 0.5
const maxScale = 4
const pdfPageCount = ref<number>(-1)
const scrollRef = ref<InstanceType<typeof NScrollbar> | null>(null)
const isDragging = ref(false)
let pressTimeoutId: number | null = null
let dragging = false
let startX = 0
let startY = 0
let startLeft = 0
let startTop = 0
const longPressDelay = 100
const canvasList = shallowReactive<HTMLCanvasElement[]>([])
// 更稳健的滚动容器解析逻辑,避免依赖 $el.querySelector
const scrollContainer = ref<HTMLElement | null>(null)
const findScrollableParent = (el: HTMLElement | null): HTMLElement | null => {
let cur: HTMLElement | null = el?.parentElement ?? null
while (cur) {
const style = window.getComputedStyle(cur)
const overflowY = style.overflowY
const overflowX = style.overflowX
const canScroll =
(overflowY === 'auto' || overflowY === 'scroll' || overflowX === 'auto' || overflowX === 'scroll') &&
(cur.scrollHeight > cur.clientHeight || cur.scrollWidth > cur.clientWidth)
if (canScroll) return cur
cur = cur.parentElement
}
return null
}
const resolveScrollContainer = () => {
const fromScrollbar = pdfEl.value?.closest('.n-scrollbar')?.querySelector('.n-scrollbar-container') as HTMLElement | null
scrollContainer.value = fromScrollbar ?? findScrollableParent(pdfEl.value ?? null)
}
const getScrollEl = (): HTMLElement | null => {
if (!scrollContainer.value) resolveScrollContainer()
return scrollContainer.value
}
const onPointerDown = (e: PointerEvent) => {
resolveScrollContainer()
const el = getScrollEl()
if (!el) return
startX = e.clientX
startY = e.clientY
startLeft = el.scrollLeft
startTop = el.scrollTop
dragging = false
;(e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId)
if (pressTimeoutId) window.clearTimeout(pressTimeoutId)
pressTimeoutId = window.setTimeout(() => {
dragging = true
isDragging.value = true
document.body.style.cursor = 'grabbing'
}, longPressDelay)
}
const onPointerMove = (e: PointerEvent) => {
const el = getScrollEl()
if (!el || !dragging) return
const dx = e.clientX - startX
const dy = e.clientY - startY
let newLeft = startLeft - dx
let newTop = startTop - dy
const maxLeft = Math.max(0, el.scrollWidth - el.clientWidth)
const maxTop = Math.max(0, el.scrollHeight - el.clientHeight)
newLeft = Math.max(0, Math.min(maxLeft, newLeft))
newTop = Math.max(0, Math.min(maxTop, newTop))
el.scrollLeft = newLeft
el.scrollTop = newTop
e.preventDefault()
}
const endDrag = (e: PointerEvent) => {
if (pressTimeoutId) {
window.clearTimeout(pressTimeoutId)
pressTimeoutId = null
}
if (dragging) {
isDragging.value = false
document.body.style.cursor = ''
}
dragging = false
try {
(e.currentTarget as HTMLElement).releasePointerCapture?.(e.pointerId)
} catch {}
}
const onPointerUp = endDrag
const onPointerCancel = endDrag
onBeforeUnmount(() => {
if (pressTimeoutId) window.clearTimeout(pressTimeoutId)
})
// 等比缩放并保持视窗中心不抖动
const applyZoom = (multiplier: number) => {
// const el = getScrollEl()
const oldScale = scale.value
let newScale = oldScale * multiplier
newScale = Math.max(minScale, Math.min(maxScale, newScale))
// const ratio = newScale / oldScale
// if (el) {
// const cx = el.clientWidth / 2
// const cy = el.clientHeight / 2
// // const targetLeft = (el.scrollLeft + cx) * ratio - cx
// // const targetTop = (el.scrollTop + cy) * ratio - cy
// scale.value = +newScale.toFixed(4)
// // requestAnimationFrame(() => {
// // const maxLeft = Math.max(0, el.scrollWidth - el.clientWidth)
// // const maxTop = Math.max(0, el.scrollHeight - el.clientHeight)
// // el.scrollLeft = Math.max(0, Math.min(maxLeft, targetLeft))
// // el.scrollTop = Math.max(0, Math.min(maxTop, targetTop))
// // })
// return
// }
scale.value = +newScale.toFixed(4)
}
const handleZoom = (index?: number) => {
const _index = index ?? 1
pdfMargin.value += 10 * _index
const factor = 1 + 0.1 * _index
applyZoom(1 / factor)
}
const handleShrink = (index?: number) => {
const _index = index ?? 1
// if (pdfMargin.value - 10 * _index <= 0) {
// return pdfMargin.value = 0
// }
pdfMargin.value -= 10 * _index
const factor = 1 + 0.1 * _index
applyZoom(factor)
}
const handleResetMargin = () => {
pdfMargin.value = 0
const el = getScrollEl()
scale.value = 1
}
const pdfPageCount = ref<number>(-1)
const canvasList = shallowReactive<HTMLCanvasElement[]>([])
onMounted(async () => {
// 初始解析一次滚动容器(等待一帧确保DOM就绪)
requestAnimationFrame(() => resolveScrollContainer())
const setLoadingMessage = miaoMessageRef.value?.alertTip('加载PDF插件中', {
type: 'info',
timeout: -1
@@ -134,6 +233,15 @@ onMounted(async () => {
<style scoped lang="scss">
.miao-container {
--bar-height: 44px;
--bar-bg: linear-gradient(90deg, #eef1f5 0%, #e3e7ec 100%);
--btn-bg: #ffffff;
--btn-hover: #f2f4f7;
--btn-active: #e6e9ef;
--btn-border: #d7dbe2;
--text: #1f2328;
--subtext: #6b7280;
width: 100%;
height: 100%;
background-color: rgb(201, 201, 201);
@@ -144,31 +252,57 @@ onMounted(async () => {
.miao-container-topBar {
box-sizing: border-box;
padding: 0 30px;
height: 30px;
padding: 0 12px;
height: var(--bar-height);
width: 100%;
background-color: rgb(170, 176, 182);
background: var(--bar-bg);
display: flex;
justify-content: space-around;
justify-content: center;
align-items: center;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
backdrop-filter: saturate(180%) blur(8px);
&-btn {
cursor: pointer;
user-select: none;
height: 80%;
padding: 0 5px;
.topBar-main {
display: flex;
align-items: center;
justify-content: center;
font-size: larger;
letter-spacing: 1px;
color: black;
border-radius: 5px;
&:hover {
background-color: #eaeaea42;
}
gap: 8px;
position: relative;
user-select: none;
}
.top-btn {
cursor: pointer;
user-select: none;
height: 30px;
min-width: 34px;
padding: 0 10px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
letter-spacing: 0.5px;
color: var(--text);
background-color: var(--btn-bg);
border: 1px solid var(--btn-border);
border-radius: 8px;
box-shadow: 0 1px 0 rgba(0,0,0,0.02);
transition: background-color .15s ease, transform .08s ease, box-shadow .15s ease;
&:hover { background-color: var(--btn-hover); }
&:active { background-color: var(--btn-active); transform: scale(0.98); }
}
.scale-indicator {
position: absolute;
right: -60px;
min-width: 54px;
text-align: center;
font-size: 13px;
color: var(--subtext);
padding: 0 8px;
}
.hint { color: var(--subtext); font-size: 12px; }
}
.miao-container-pdf {
@@ -176,9 +310,26 @@ onMounted(async () => {
width: 100%;
display: flex;
flex-direction: column;
align-items: center; // 居中以便缩小时居中显示
touch-action: none; // 以便自定义拖动
&.is-dragging { cursor: grabbing; }
.miao-container-pdf-content {
display: flex;
flex-direction: column;
transition: width .12s ease, padding-top .12s ease;
will-change: width, padding-top;
// 让Canvas随容器宽度等比缩放,保证显示完整
canvas {
width: 100%;
height: auto;
display: block;
margin: 0 auto 12px auto;
background: #fff;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
}
}