优化PptxRendererPreview.vue

This commit is contained in:
jingrow 2026-05-31 03:41:31 +08:00
parent c36357547f
commit e7d64e2842

View File

@ -3,13 +3,13 @@
<!-- 渲染区 -->
<div class="pptx-renderer-body" ref="containerRef" />
<!-- 导航栏 -->
<div v-if="!loading && !hasError" class="pptx-nav">
<button class="nav-btn" :disabled="currentSlide <= 0" @click="goPrev" :title="t('Previous slide')">
<!-- 导航栏始终渲染避免 body 高度跳变 -->
<div class="pptx-nav">
<button class="nav-btn" :disabled="!viewer || currentSlide <= 0" @click="goPrev" :title="t('Previous slide')">
<Icon icon="tabler:chevron-left" :size="18" />
</button>
<span class="nav-indicator">{{ currentSlide + 1 }} / {{ totalSlides }}</span>
<button class="nav-btn" :disabled="currentSlide >= totalSlides - 1" @click="goNext" :title="t('Next slide')">
<span class="nav-indicator">{{ viewer ? (currentSlide + 1) + ' / ' + totalSlides : '—' }}</span>
<button class="nav-btn" :disabled="!viewer || currentSlide >= totalSlides - 1" @click="goNext" :title="t('Next slide')">
<Icon icon="tabler:chevron-right" :size="18" />
</button>
</div>
@ -50,6 +50,8 @@ const currentSlide = ref(0)
const totalSlides = ref(0)
let viewer: PptxViewer | null = null
let resizeObserver: ResizeObserver | null = null
let resizeTimer: ReturnType<typeof setTimeout> | null = null
function resolveUrl(url: string): string {
if (!url) return ''
@ -57,6 +59,44 @@ function resolveUrl(url: string): string {
return window.location.origin + url
}
/**
* fitMode:'contain' 自动按宽度缩放宽度变化时自动重渲染
* 但不考虑高度约束当高度溢出时 setZoom 缩小使宽高都适应
* zoom=100 表示库默认按宽度 fit<100 表示需进一步缩小以适应高度
*/
function calcFitZoom(): number {
if (!viewer || !containerRef.value) return 100
const cW = containerRef.value.clientWidth
const cH = containerRef.value.clientHeight
const sW = viewer.slideWidth
const sH = viewer.slideHeight
if (!cW || !cH || !sW || !sH) return 100
// contain scale = cW / sW fit
// scale
const scaleByW = cW / sW
const scaleByH = cH / sH
if (scaleByH < scaleByW) {
// fit
return Math.round((scaleByH / scaleByW) * 100)
}
return 100
}
/** 容器尺寸变化时重算 zoom库已自动按宽度 fit只需修正高度溢出 */
async function handleResize() {
if (!viewer) return
const zoom = calcFitZoom()
if (zoom !== viewer.zoomPercent) {
await viewer.setZoom(zoom)
}
}
function scheduleResize() {
if (resizeTimer) clearTimeout(resizeTimer)
resizeTimer = setTimeout(() => handleResize(), 200)
}
function goPrev() {
if (!viewer || currentSlide.value <= 0) return
viewer.goToSlide(currentSlide.value - 1)
@ -111,7 +151,8 @@ async function loadPresentation(fileUrl: string) {
if (!containerRef.value) return
// slide
// fitMode:'contain' fit
// calcFitZoom + setZoom
viewer = await PptxViewer.open(buffer, containerRef.value, {
renderMode: 'slide',
zipLimits: RECOMMENDED_ZIP_LIMITS,
@ -124,6 +165,12 @@ async function loadPresentation(fileUrl: string) {
totalSlides.value = viewer.slideCount
currentSlide.value = viewer.currentSlideIndex
//
const zoom = calcFitZoom()
if (zoom < 100) {
await viewer.setZoom(zoom)
}
loading.value = false
emit('loaded')
} catch (e) {
@ -139,12 +186,20 @@ watch(() => props.fileUrl, (newUrl) => {
}, { immediate: true })
onMounted(() => {
//
const el = containerRef.value?.closest('.pptx-renderer-preview') as HTMLElement
el?.focus()
// zoom
if (containerRef.value) {
resizeObserver = new ResizeObserver(() => scheduleResize())
resizeObserver.observe(containerRef.value)
}
})
onBeforeUnmount(() => {
if (resizeTimer) clearTimeout(resizeTimer)
resizeObserver?.disconnect()
resizeObserver = null
viewer?.destroy()
viewer = null
})
@ -168,6 +223,9 @@ onBeforeUnmount(() => {
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
/* ===== 导航栏 ===== */