pdfjsLib预览本地PDF文件跳转到浏览器预览,代码复制即可使用。pdfLocalPath根据实际情况可换成后端返回数据

5 阅读3分钟

该代码纯记录

    <view class="m-vue-pdf">
        <!-- 加载中 -->
        <view v-if="!loaded && !showPicture" class="loading">
            <uni-load-more type="loading" color="#007AFF"></uni-load-more>
        </view>

        <!-- PDF渲染容器(Vue响应式渲染,无手动DOM操作) -->
        <view v-show="loaded && !showPicture" class="pdf-wrap">
            <scroll-view class="pdf-scroll" scroll-y="true" style="height: 100vh;">
                <view class="pdf-container">
                    <!-- 核心:用Vue循环渲染PDF图片,完全避开DOM操作 -->
                    <image v-for="(img, index) in pdfImages" :key="index" :src="img" class="pdf-page" mode="widthFix">
                    </image>
                </view>
            </scroll-view>
        </view>

    </view>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf'
import pdfjsWorker from 'pdfjs-dist/legacy/build/pdf.worker.mjs?url'

// 配置PDF.js
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker
let pdfInstance = null

// 响应式数据(核心:新增pdfImages数组存储Base64图片)
const deptId = ref('')
const riskCode = ref('')
const loaded = ref(false)
const flag = ref('')
const showPicture = ref(false)
const pdfLocalPath = ref('')
const pdfImages = ref([]) // 存储PDF各页的Base64图片

// 静态资源
const staticAssets = {
    pdfs: {
        hospital: '/static/hospital.pdf',
        policy: '/static/policy.pdf'
    }
}

onMounted(async () => {
    try {
        // 1. 获取路由参数
        const pages = getCurrentPages()
        const currentPage = pages[pages.length - 1]
        const query = currentPage.options || {}

        riskCode.value = query.riskCode
        flag.value = query.flag
        deptId.value = query.deptId

        if (!riskCode.value) {
            uni.setNavigationBarTitle({ title: '隐私政策' })
        }

        // 2. 确定PDF路径
        if (riskCode.value && deptId.value) {
            pdfLocalPath.value = `/static/${deptId.value}${riskCode.value}.pdf`
        } else if (flag.value === 'hospital') {
            pdfLocalPath.value = staticAssets.pdfs.hospital
        } else {
            pdfLocalPath.value = staticAssets.pdfs.policy
        }

        // 3. 处理手动引导页/渲染PDF
        if (flag.value !== 'manual') {
            const fullPdfPath = getFullPdfPath(pdfLocalPath.value)
            // 直接渲染PDF(无需等待DOM,因为用Vue响应式渲染)
            await renderPdfToImages(fullPdfPath)
            loaded.value = true
        } else {
            showPicture.value = true
        }
    } catch (error) {
        console.error('初始化失败:', error)
        uni.showToast({ title: '页面加载失败', icon: 'none' })
    }
})

onUnmounted(() => {
    // 销毁PDF实例,释放内存
    if (pdfInstance) {
        pdfInstance.destroy()
        pdfInstance = null
    }
    // 清空图片数组,释放内存
    pdfImages.value = []
})

/**
 * 获取适配H5+App的完整PDF路径
 * @param {string} relativePath PDF相对路径
 * @returns {string} 完整可访问路径
 */
const getFullPdfPath = (relativePath) => {
    // #ifdef H5
    // H5环境:拼接当前域名,避免file://协议
    const baseUrl = window.location.origin
    return `${baseUrl}${relativePath}`
    // #endif

    // #ifdef APP-PLUS
    // App环境:转换为本地文件系统路径
    return plus.io.convertLocalFileSystemURL(`_www${relativePath}`)
    // #endif

    // 默认返回相对路径
    return relativePath
}

/**
 * 核心重构:PDF转Base64图片数组(无任何DOM操作)
 * @param {string} pdfPath PDF完整路径
 */
const renderPdfToImages = async (pdfPath) => {
    try {
        // 清空历史图片
        pdfImages.value = []

        // 1. 加载PDF
        const loadingTask = pdfjsLib.getDocument({
            url: pdfPath,
            cMapUrl: 'https://cdn.jsdelivr.net/npm/pdfjs-dist/2.16.105/cmaps/',
            cMapPacked: true,
            disableWorker: true, // App端禁用Worker
            verbosity: pdfjsLib.VerbosityLevel.ERRORS
        })

        pdfInstance = await loadingTask.promise
        const numPages = pdfInstance.numPages
        const systemInfo = uni.getSystemInfoSync()
        const targetWidth = systemInfo.screenWidth - 20 // 留边距

        // 2. 逐页渲染为Base64图片
        for (let i = 1; i <= numPages; i++) {
            const page = await pdfInstance.getPage(i)
            const viewport = page.getViewport({ scale: 1 })
            const scale = targetWidth / viewport.width // 按屏幕宽度缩放
            const scaledViewport = page.getViewport({ scale })

            // 创建canvas(仅内存中使用,不插入DOM)
            const canvas = document.createElement('canvas')
            canvas.width = scaledViewport.width * systemInfo.pixelRatio
            canvas.height = scaledViewport.height * systemInfo.pixelRatio
            const ctx = canvas.getContext('2d')

            // 白色背景
            ctx.fillStyle = '#FFFFFF'
            ctx.fillRect(0, 0, canvas.width, canvas.height)

            // 适配设备像素比
            ctx.scale(systemInfo.pixelRatio, systemInfo.pixelRatio)
            await page.render({ canvasContext: ctx, viewport: scaledViewport }).promise

            // 转为Base64并添加到数组(核心:不操作DOM,只存数据)
            const imgBase64 = canvas.toDataURL('image/png')
            pdfImages.value.push(imgBase64)

            // 释放canvas内存
            canvas.remove()
        }

    } catch (error) {
        console.error('PDF转图片失败:', error)
        // 兜底:如果PDF加载失败,提示用户并尝试web-view(仅作为备选)
        uni.showModal({
            title: '提示',
            content: 'PDF预览失败,是否尝试其他方式打开?',
            success: (res) => {
                if (res.confirm) {
                    uni.openDocument({
                        filePath: pdfLocalPath.value,
                        showMenu: false,
                        success: () => { },
                        fail: () => {
                            uni.showToast({ title: '打开失败', icon: 'none' })
                        }
                    })
                }
            }
        })
        throw error
    }
}

</script>

<style scoped lang="scss">
// 全局背景改为纯白色
.m-vue-pdf {
    width: 100%;
    min-height: 100vh;
    background-color: #FFFFFF !important;
    box-sizing: border-box;

    // 全局隐藏滚动条(兼容所有平台)
    ::-webkit-scrollbar {
        display: none !important;
        width: 0 !important;
        height: 0 !important;
    }

    -ms-overflow-style: none !important; // IE/Edge
    scrollbar-width: none !important; // Firefox
}

.loading {
    padding: 40px 0;
    text-align: center;
    background-color: #FFFFFF;
}

.pdf-wrap {
    width: 100%;
    height: 100vh;
    box-sizing: border-box;
    background-color: #FFFFFF;
    overflow: hidden;
}

// 核心:隐藏scroll-view的滚动条
.pdf-scroll {
    width: 100%;
    height: 100%;
    -webkit-overflow-scrolling: touch;
    box-sizing: border-box;
    padding: 10px;
    background-color: #FFFFFF;

    ::-webkit-scrollbar {
        display: none !important;
        width: 0 !important;
    }

    -ms-overflow-style: none !important;
    scrollbar-width: none !important;
    overflow-x: hidden !important;
    overflow-y: auto !important;
}

.pdf-container {
    width: 100%;
    box-sizing: border-box;
    background-color: #FFFFFF;
}

// PDF每页图片样式(替代手动创建的img样式)
.pdf-page {
    width: 100%;
    height: auto;
    display: block;
    margin: 0 auto 10px;
    background-color: #FFFFFF;
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}

// 手动引导页样式不变
.manual {
    padding: 0 20px;
    box-sizing: border-box;
    background-color: #FFFFFF;
    overflow: hidden;

    .video-title {
        font-weight: 600;
        font-size: 14px;
        color: red;
        line-height: 22px;
        text-indent: 2em;
        text-align: justify;
        margin: 10px 0;
        display: block;
    }

    .video {
        width: 100%;
        height: auto;
        background-color: #000;
        margin: 10px 0;
        border-radius: 8px;
    }

    .title {
        height: 50px;
        line-height: 50px;
        font-weight: 600;
        font-size: 18px;
        color: #333;
        text-align: center;
    }

    .content {
        font-size: 16px;
        color: #333;
        text-align: justify;
        text-indent: 2em;
        line-height: 25px;
        display: block;
        margin: 5px 0;
    }

    .imgs {
        margin: 0 auto;
        text-align: center;

        .img {
            width: 90%;
            margin: 20px 0;
            border: 1px solid #eee;
            border-radius: 8px;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
        }
    }
}
</style>