feat(miaoPDF): 重构PDF查看器并添加拖拽和缩放功能
- 将margin缩放方案改为更精确的scale缩放方案 - 添加拖拽功能,支持长按后拖动PDF内容 - 重新设计顶部工具栏样式和布局 - 添加缩放比例显示和操作提示
This commit is contained in:
1 parent
9ee472e5de
commit
79221d37d6
1 file changed
+210
-59
+210
-59
@@ -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 {
|
||||
.topBar-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.top-btn {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
height: 80%;
|
||||
padding: 0 5px;
|
||||
display: flex;
|
||||
height: 30px;
|
||||
min-width: 34px;
|
||||
padding: 0 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: larger;
|
||||
letter-spacing: 1px;
|
||||
color: black;
|
||||
border-radius: 5px;
|
||||
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: #eaeaea42;
|
||||
&: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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user