更新记录

1.0.0(2026-09-08)

  • 首次发布
  • 基于 TBS 文档 SDK 公网版(TbsFileSdk_base_universal_release.aar)封装
  • 支持 doc、docx、rtf、ppt、pptx、xls、xlsx、xlsm、csv、pdf、txt、epub、chm、ofd 格式
  • 支持页码跳转、上一页/下一页、页码监听、长按复制、PPT 翻页模式、跳转上次浏览位置、文档背景色、Dialog 顶栏自定义等参数

平台兼容性

uni-app(4.72)

Vue2 Vue2插件版本 Vue3 Vue3插件版本 Chrome Safari app-vue app-nvue app-nvue插件版本 Android Android插件版本 iOS 鸿蒙
1.0.0 1.0.0 × × × 1.0.0 8.0 1.0.0 × ×
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
× × × × × × × × × × × ×

zy-tbsfile 腾讯TBS文档预览插件

基于腾讯浏览服务(TBS)文档 SDK 公网版封装的 uni-app UTS 组件插件(uni-app 兼容模式组件),提供 Layout 内嵌预览方式(文档渲染到组件区域,宿主可自定义标题栏,支持页码跳转)。

如需 Dialog 全屏弹窗预览或在 vue 页面中使用,请使用 API 插件 zy-tbsfile-api(vue / nvue 页面均可调用)。注意:两个插件包含同一 TBS AAR,不可同时打进同一个自定义基座

SDK 接口文档:https://cloud.tencent.com/document/product/1645/83900

支持格式

doc、docx、rtf、ppt、pptx、xls、xlsx、xlsm、csv、pdf、txt、epub、chm、ofd

仅支持 Android 平台(TBS 文档 SDK 官方仅提供 Android SDK)。

使用前提

  1. 本插件需要配合腾讯云 TBS 文档 SDK LicenseKey 使用,购买方式参见 License 使用说明
  2. 请务必确保用户同意隐私政策后,先调用 setLicenseKey,再调用 initEngine
  3. 本插件为 uni-app 兼容模式组件,需在 nvue 页面中使用。

快速上手

页面中引入组件(easycom 自动注册):

<template>
  <view>
    <!-- Layout 方式:组件占据一定区域,文档渲染在该区域内 -->
    <zy-tbsfile ref="tbsfile" style="height:600rpx" @onEvent="onEvent" />
  </view>
</template>

初始化引擎(App 启动后仅需调用一次):

this.$refs['tbsfile'].initEngine('your licenseKey')
// 或分两步:
// this.$refs['tbsfile'].setLicenseKey('your licenseKey')
// this.$refs['tbsfile'].initEngine('')

打开网络文档(重要)

TBS SDK 仅支持本地文件路径filePath 传 https 网络地址会报 9020004(文件不存在)。服务器文档(如 COS/OSS)需先下载到本地再打开:

uni.downloadFile({
  url: 'https://xxx.cos.ap-beijing.myqcloud.com/test.pdf',
  success: (res) => {
    if (res.statusCode === 200) {
      // 转为本地绝对路径后打开
      const localPath = plus.io.convertLocalFileSystemURL(res.tempFilePath)
      this.$refs['tbsfile'].openDocument({ filePath: localPath })
    }
  }
})

Layout 方式(内嵌到页面区域,可自定义标题栏)

this.$refs['tbsfile'].openDocument({
  filePath: '/sdcard/Android/data/<包名>/files/tbsfile/test.pdf',
  gotoLastPos: true,
  enableLongPressMenu: true,
})

页码跳转

this.$refs['tbsfile'].nextPage()   // 下一页
this.$refs['tbsfile'].prevPage()   // 上一页
this.$refs['tbsfile'].gotoPage(2)  // 跳转到第 3 页(数组下标从 0 开始)

关闭文档

this.$refs['tbsfile'].closeDocument()

API 说明(通过 ref 调用)

方法 参数 说明
setLicenseKey license: string 设置 LicenseKey,需在用户同意隐私政策后、initEngine 前调用
initEngine license: string 异步初始化引擎,App 启动后仅需调用一次;license 可传空串(需提前 setLicenseKey)
isEngineLoaded 查询引擎是否初始化成功,返回 boolean
canOpenFileExt ext: string 判断格式是否支持,返回 boolean
openDocument options: TbsDocOptions Layout 方式打开文档,渲染到组件区域
closeDocument 关闭文档并释放资源
gotoPage pageIndex: number 跳转到指定页面(数组下标,0 为第一页),支持 PDF/DOCX/PPTX
nextPage / prevPage 下一页 / 上一页
getPageInfo 获取当前页码信息,返回 { curPage, pageCount }
copyAssetToDoc assetName: string 将插件 assets 中的文件复制到沙盒目录(测试用)

TbsDocOptions

属性 类型 必填 说明
filePath string 本地文件绝对路径(SDK 只支持本地文档,网络文档需先下载)
fileExt string 文件后缀名,如 "pdf",不传时自动从路径提取
tempPath string 临时目录路径(记录上次浏览位置等缓存),默认使用沙盒 getExternalFilesDir("tbsfile_temp")
enableLongPressMenu boolean 开启长按菜单复制(PDF/DOCX/XLSX/TXT),默认 false
pptPageMode boolean PPT 打开为翻页模式(默认滑动模式),默认 false
gotoLastPos boolean 自动跳转到上次浏览位置,默认 false
contentBgColor string 文档视图背景颜色,如 "#ffffff"(PDF/DOCX/PPTX)

事件说明(@onEvent)

onEvent(e) {
  // e.type 事件类型
  // e.code  业务码:0 成功,非 0 失败
  // e.msg   描述信息
  // e.data  附加数据
}
type 说明
initEngine 引擎初始化回调(code=0 成功)
openDocument 文档打开回调(code=0 打开成功;非 0 失败原因见 msg)
closeDocument 文档已关闭(closeDocument 调用后或组件销毁时触发)
rendered 文档引擎渲染成功,即将显示文档
pageChange 页码变化(data: cur_page 当前真实页码、page_count 总页码)
gotoPage 页面跳转反馈
readerEvent 其他阅读器事件透传(data.actionType 为 SDK 原生事件类型)
copyAssetFile assets 文件复制结果(data.path 为目标路径)

示例代码

<template>
    <view class="page">
        <scroll-view scroll-y class="scroll-content">
            <view class="status-bar">
                <view class="status-dot" :class="engineReady ? 'dot-online' : 'dot-offline'"></view>
                <text class="status-text">{{ statusText }}</text>
                <text class="page-indicator" v-if="pageCount > 0">{{ curPage }}/{{ pageCount }}页</text>
            </view>

            <view class="section">
                <text class="section-title">引擎初始化</text>
                <view class="input-row">
                    <input class="text-input" v-model="license" placeholder="请输入 TBS LicenseKey" />
                </view>
                <view class="btn-row">
                    <button class="btn" @click="initEngine">初始化引擎</button>
                </view>
                <text class="hint">需购买 TBS 文档 SDK LicenseKey,并确保用户已同意隐私政策</text>
            </view>

            <view class="section">
                <text class="section-title">测试文件</text>
                <view class="input-row">
                    <input class="text-input" v-model="filePath" placeholder="本地文件绝对路径" />
                </view>
                <view class="input-row">
                    <input class="text-input" v-model="netUrl" placeholder="网络文档URL(https, 需带扩展名)" />
                </view>
                <view class="btn-row">
                    <button class="btn" @click="createTestFile">生成测试TXT</button>
                    <button class="btn" @click="downloadNetFile">下载网络文档</button>
                </view>
                <text class="hint">TBS SDK 仅支持本地路径:网络文档会先下载到沙盒再自动填充路径</text>
            </view>

            <view class="section">
                <text class="section-title">Layout 方式(内嵌预览)</text>
                <view class="preview-wrap">
                    <zy-tbsfile ref="tbsfile" class="doc-view" @onEvent="onEvent" />
                </view>
                <view class="btn-row">
                    <button class="btn" @click="openInLayout">打开文档</button>
                    <button class="btn" :class="!docOpened ? 'btn-disabled' : ''" :disabled="!docOpened"
                        @click="prevPage">上一页</button>
                    <button class="btn" :class="!docOpened ? 'btn-disabled' : ''" :disabled="!docOpened"
                        @click="nextPage">下一页</button>
                    <button class="btn" :class="!docOpened ? 'btn-disabled' : ''" :disabled="!docOpened"
                        @click="gotoPage">跳页</button>
                    <button class="btn" :class="!docOpened ? 'btn-disabled' : ''" :disabled="!docOpened"
                        @click="closeDoc">关闭</button>
                </view>
                <view class="input-row" v-if="docOpened">
                    <input class="text-input" type="number" v-model="gotoPageNo" placeholder="页码下标(从0开始)" />
                </view>
            </view>

            <view class="section">
                <text class="section-title">事件日志</text>
                <view class="btn-row">
                    <button class="btn btn-xs" @click="clearLogs">清空日志</button>
                </view>
                <view class="log-item" v-for="(log, i) in logs" :key="i">
                    <text class="log-text">{{ log }}</text>
                </view>
                <text class="hint" v-if="!logs.length">暂无日志</text>
            </view>
        </scroll-view>
    </view>
</template>

<script>
    function unwrapUTS(obj) {
        if (!obj || typeof obj !== 'object') return obj
        if (obj.dynamicJSONFields) return unwrapUTS(obj.dynamicJSONFields)
        if (Array.isArray(obj)) return obj.map(unwrapUTS)
        let result = {}
        for (let key of Object.keys(obj)) {
            if (key === 'jSONArray') continue
            result[key] = unwrapUTS(obj[key])
        }
        return result
    }

    export default {
    data() {
        return {
            license: 'PooBEXT8UKSBQUN57PcdywzP+6OO3cRQ3qASkQffYtOSaM5hAjR3eLKfp4ck4wnT',
            filePath: '',
            netUrl: 'https://hg-zhjd-test-1307093093.cos.ap-beijing.myqcloud.com/centralBank/%E3%80%90Python%E5%BC%80%E5%8F%91%E5%B7%A5%E7%A8%8B%E5%B8%88_%E9%83%91%E5%B7%9E%208-10K%E3%80%91%E5%BE%90%E5%A8%81%2010%E5%B9%B4.pdf',
            gotoPageNo: '',
                engineReady: false,
                docOpened: false,
                curPage: 0,
                pageCount: 0,
                statusText: '等待初始化引擎...',
                logs: []
            }
        },
        methods: {
            addLog(msg) {
                const time = new Date().toTimeString().substring(0, 8)
                this.logs.unshift('[' + time + '] ' + msg)
                if (this.logs.length > 30) this.logs.pop()
            },

            onEvent(e) {
                const data = unwrapUTS(e.detail)
                if (!data) return
                console.log('tbsfile onEvent', data)
                this.addLog('[' + data.type + '] code=' + data.code + ' ' + (data.msg || ''))
                switch (data.type) {
                    case 'initEngine':
                        if (data.code == 0) {
                            this.engineReady = true
                            this.statusText = '引擎已初始化'
                        } else {
                            this.statusText = '初始化失败: ' + data.msg
                        }
                        break
                    case 'openDocument':
                        if (data.code == 0) {
                            this.docOpened = true
                            this.statusText = '文档已打开'
                        } else {
                            this.docOpened = false
                            this.statusText = '打开失败: ' + data.msg
                        }
                        break
                    case 'rendered':
                        this.statusText = '文档渲染完成'
                        break
                    case 'pageChange':
                        if (data.data) {
                            this.curPage = data.data.cur_page
                            this.pageCount = data.data.page_count
                            this.statusText = '第 ' + this.curPage + ' / ' + this.pageCount + ' 页'
                        }
                        break
                    case 'closeDocument':
                        this.docOpened = false
                        this.statusText = '文档已关闭'
                        break
                }
            },

            initEngine() {
                if (!this.license) {
                    uni.showToast({ title: '请输入 LicenseKey', icon: 'none' })
                    return
                }
                uni.setStorageSync('tbs_license', this.license)
                this.statusText = '正在初始化引擎...'
                console.log(this.$refs.tbsfile)
                try {
                    this.$refs.tbsfile.initEngine(this.license)
                } catch (e) {
                    this.statusText = '调用失败: ' + e.message
                }
            },

            createTestFile() {
                const content = 'TBS 文档预览测试文件\n\n'
                    + '这是由 zy-tbsfile 插件示例页面生成的测试文本文件。\n\n'
                    + '腾讯浏览服务(TBS)文档 SDK 支持打开以下格式:\n'
                    + 'doc、docx、rtf、ppt、pptx、xls、xlsx、xlsm、csv、pdf、txt、epub、chm、ofd\n\n'
                    + '本组件以 Layout 方式打开文档:文档渲染到组件指定区域,宿主可自定义标题栏;\n'
                    + 'Dialog 全屏弹窗方式请使用 zy-tbsfile-api 插件。\n\n'
                    + '如需测试 PDF/DOCX 等格式,可将文件放入应用沙盒目录后填入路径。'
                plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fs) => {
                    fs.root.getFile('tbsfile_test.txt', { create: true }, (entry) => {
                        entry.createWriter((writer) => {
                            writer.onwrite = () => {
                                const absPath = plus.io.convertLocalFileSystemURL(entry.fullPath)
                                this.filePath = absPath
                                this.addLog('测试文件已生成: ' + absPath)
                                uni.showToast({ title: '已生成测试TXT', icon: 'none' })
                            }
                            writer.onerror = (err) => {
                                this.addLog('写入文件失败: ' + JSON.stringify(err))
                            }
                            writer.write(content)
                        }, (err) => {
                            this.addLog('createWriter失败: ' + JSON.stringify(err))
                        })
                    }, (err) => {
                        this.addLog('getFile失败: ' + JSON.stringify(err))
                    })
            }, (err) => {
                    this.addLog('requestFileSystem失败: ' + JSON.stringify(err))
                })
            },

            // 下载网络文档到沙盒临时目录,成功后自动填充本地路径
            // TBS SDK 仅支持本地路径,网络文档必须先下载;tempFilePath 保留扩展名,便于插件提取 fileExt
            downloadNetFile() {
                if (!this.netUrl) {
                    uni.showToast({ title: '请输入网络文档URL', icon: 'none' })
                    return
                }
                this.statusText = '正在下载文档...'
                uni.showLoading({ title: '下载中...', mask: true })
                uni.downloadFile({
                    url: this.netUrl,
                    success: (res) => {
                        uni.hideLoading()
                        if (res.statusCode === 200) {
                            const abs = plus.io.convertLocalFileSystemURL(res.tempFilePath)
                            this.filePath = abs
                            this.addLog('下载完成: ' + abs)
                            this.statusText = '下载完成,可点击打开'
                        } else {
                            this.addLog('下载失败: statusCode=' + res.statusCode)
                            this.statusText = '下载失败: HTTP ' + res.statusCode
                        }
                    },
                    fail: (err) => {
                        uni.hideLoading()
                        this.addLog('下载失败: ' + JSON.stringify(err))
                        this.statusText = '下载失败'
                    }
                })
            },

            checkBeforeOpen() {
                if (!this.engineReady) {
                    uni.showToast({ title: '请先初始化引擎', icon: 'none' })
                    return false
                }
                if (!this.filePath) {
                    uni.showToast({ title: '请先填写文件路径', icon: 'none' })
                    return false
                }
                return true
            },

            openInLayout() {
                if (!this.checkBeforeOpen()) return
                this.statusText = '正在打开文档...'
                try {
                    this.$refs.tbsfile.openDocument({ filePath: this.filePath })
                } catch (e) {
                    this.statusText = '调用失败: ' + e.message
                }
            },

            prevPage() {
                try {
                    this.$refs.tbsfile.prevPage()
                } catch (e) {
                    this.addLog('上一页失败: ' + e.message)
                }
            },

            nextPage() {
                try {
                    this.$refs.tbsfile.nextPage()
                } catch (e) {
                    this.addLog('下一页失败: ' + e.message)
                }
            },

            gotoPage() {
                const idx = parseInt(this.gotoPageNo)
                if (isNaN(idx) || idx < 0) {
                    uni.showToast({ title: '请输入有效页码下标', icon: 'none' })
                    return
                }
                try {
                    this.$refs.tbsfile.gotoPage(idx)
                } catch (e) {
                    this.addLog('跳页失败: ' + e.message)
                }
            },

            closeDoc() {
                try {
                    this.$refs.tbsfile.closeDocument()
                } catch (e) {
                    this.addLog('关闭失败: ' + e.message)
                }
            },

            clearLogs() {
                this.logs = []
            }
        }
    }
</script>

<style>
    .page { flex: 1; background: #fff; }
    .scroll-content { flex: 1; }
    .status-bar { flex-direction: row; align-items: center; padding: 10rpx 24rpx; background: #f5f5f5; border-bottom: 1rpx solid #e0e0e0; }
    .status-dot { width: 10rpx; height: 10rpx; border-radius: 5rpx; margin-right: 10rpx; }
    .dot-online { background: #22c55e; }
    .dot-offline { background: #ccc; }
    .status-text { font-size: 24rpx; color: #666; flex: 1; }
    .page-indicator { font-size: 24rpx; color: #007aff; }
    .section { padding: 20rpx 24rpx; border-bottom: 1rpx solid #f0f0f0; }
    .section-title { font-size: 28rpx; color: #333; font-weight: 600; margin-bottom: 14rpx; }
    .btn-row { flex-direction: row; flex-wrap: wrap; align-items: center; }
    .btn { padding: 14rpx 28rpx; border-radius: 6rpx; font-size: 26rpx; color: #333; background: #fff; border: 1rpx solid #ddd; text-align: center; margin-right: 12rpx; margin-bottom: 10rpx; }
    .btn-xs { padding: 8rpx 20rpx; font-size: 22rpx; width: 140rpx; }
    .btn-disabled { opacity: 0.4; }
    .input-row { margin-bottom: 12rpx; }
    .text-input { font-size: 24rpx; padding: 12rpx 16rpx; border: 1rpx solid #ddd; border-radius: 6rpx; background: #fafafa; }
    .preview-wrap { width: 750rpx; height: 500px; background: #f0f0f0; margin-bottom: 14rpx; border: 1rpx solid #e0e0e0; }
    .doc-view { width: 750rpx; height: 500px; }
    .hint { font-size: 22rpx; color: #999; margin-top: 6rpx; }
    .log-item { padding: 8rpx 0; border-bottom: 1rpx dashed #eee; }
    .log-text { font-size: 22rpx; color: #666; }
</style>

注意事项

  1. 页面类型:本插件为 uni-app 兼容模式 UTS 组件,只能在 nvue 页面中使用。
  2. 单文档限制:TBS 引擎同一时刻仅支持打开一个文档,再次打开前请先 closeDocument(插件已自动处理:打开新文档前会关闭已打开文档)。
  3. 文件路径:SDK 只支持本地文档,服务器文档需先下载到本地;若文件在公共存储目录,需宿主自行申请存储权限;建议放在 App 沙盒目录(无需存储权限)。
  4. 资源释放:closeDocument 或组件销毁(NVBeforeUnload)时插件内部会自动调用 closeFileReader 释放 SDK 资源。
  5. 横竖屏:自定义 Layout 方式横竖屏切换时需主动调用 onSizeChanged 适配,插件在打开文档时按当前布局高度设置 set_content_view_height;如需横屏支持,请在宿主 AndroidManifest 的对应 activity 配置 android:configChanges="orientation|keyboardHidden|navigation|screenSize"
  6. 混淆配置:云打包基座已默认保留 UTS 插件类;如自行离线打包并启用混淆,需在 proguard-rules.pro 添加:
    -dontwarn com.tencent.tbs.reader.**
    -keep class com.tencent.tbs.reader.** { *; }
  7. 隐私合规:SDK 初始化会采集设备信息和应用信息用于加载文档引擎与授权鉴权,请确保用户同意隐私政策后再调用 initEngine。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

"android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE"

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件使用的腾讯浏览服务文档SDK会在初始化时采集设备信息(操作系统版本、CPU类型、屏幕宽高、屏幕方向、屏幕像素)和应用信息(包名、版本号)用于加载文档引擎和授权鉴权,详情可参考:https://rule.tencent.com/rule/preview/b01dde7c-2c5b-487a-9bdd-3275dc716a83

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。