更新记录

1.0.0(2026-09-08)

  • 首次发布
  • API 型 UTS 插件(vue / nvue 页面通用),基于 TBS 文档 SDK 公网版,Dialog 弹窗预览方式
  • 提供 initEngine / openDocumentByDialog / closeDocument / isEngineLoaded / canOpenFileExt / setLicenseKey 六个 API
  • 支持 doc、docx、rtf、ppt、pptx、xls、xlsx、xlsm、csv、pdf、txt、epub、chm、ofd 格式本地文档
  • 注意:与组件插件 zy-tbsfile 含同一 TBS AAR,不可同时打进同一自定义基座

平台兼容性

uni-app(4.72)

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

uni-app x(4.72)

Chrome Safari Android iOS 鸿蒙 微信小程序
× × × × × ×

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

基于腾讯浏览服务(TBS)文档 SDK 公网版封装的 API 型 UTS 插件(Dialog 弹窗预览方式)。

与组件插件 zy-tbsfile(component-uts,仅 nvue 页面可用)不同,本插件通过 import 调用,vue 页面与 nvue 页面均可使用。打开文档后由 SDK 弹出默认全屏 Dialog(带默认标题栏),单击翻页、滑动、缩放、页码显示均由 SDK 默认 UI 处理。

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. 请务必确保用户同意隐私政策后再调用 initEngine
  3. 真机运行需使用自定义基座(三方 SDK 不进标准基座)。
  4. 与 zy-tbsfile 组件插件互斥:两个插件包含同一个 TBS AAR,不可同时打进同一个自定义基座(会导致 Duplicate class 打包失败),请按需选用其一。

快速上手

import { initEngine, openDocumentByDialog, closeDocument, isEngineLoaded } from '@/uni_modules/zy-tbsfile-api'

// 1. 初始化引擎(App 启动后仅需调用一次,需在用户同意隐私政策后)
initEngine({
  license: 'your licenseKey',
  success: (res) => {
    console.log('引擎初始化成功')
  },
  fail: (res) => {
    console.log('初始化失败: ' + res.code + ' ' + res.message)
  }
})

// 2. 打开网络文档:TBS 仅支持本地路径,先下载
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)
      openDocumentByDialog({
        filePath: localPath,            // 本地绝对路径
        // fileExt: 'pdf',              // 可不传,自动从路径提取
        // enableLongPressMenu: true,   // 长按复制(PDF/DOCX/XLSX/TXT)
        // gotoLastPos: true,           // 跳转上次浏览位置
        success: (res) => {
          console.log('文档已打开: ' + res.typeDes)
        },
        fail: (res) => {
          console.log('打开失败: ' + res.code + ' ' + res.message)
        },
        onClose: () => {
          console.log('文档已关闭(用户关闭弹窗时也会触发)')
        }
      })
    }
  }
})

// 3. 打开本地文档(如沙盒目录文件)
openDocumentByDialog({ filePath: '/storage/emulated/0/Android/data/<包名>/files/tbsfile/test.txt' })

// 4. 主动关闭(用户关闭弹窗时 SDK 自动释放,通常无需调用)
closeDocument({ success: () => {} })

API 说明

方法 参数 返回 说明
setLicenseKey license: string void 设置 LicenseKey(initEngine 的 license 非空时会自动调用,一般无需单独调用)
initEngine options: InitEngineOptions void 异步初始化引擎,success/fail 回调
isEngineLoaded boolean 查询引擎是否初始化成功
canOpenFileExt ext: string boolean 判断格式是否支持
openDocumentByDialog options: OpenDocumentOptions void Dialog 弹窗方式打开文档
closeDocument options: CloseDocumentOptions void 关闭文档并释放资源

InitEngineOptions

属性 类型 必填 说明
license string LicenseKey
success (res: TbsInitResult) => void 初始化成功
fail (res: TbsFailResult) => void 初始化失败(res.code / res.message)

OpenDocumentOptions

属性 类型 必填 说明
filePath string 本地文件绝对路径(网络文档会报 9020004,需先下载)
fileExt string 文件后缀名,不传时自动从路径提取
tempPath string 临时目录路径,默认沙盒 getExternalFilesDir("tbsfile_temp")
enableLongPressMenu boolean 长按菜单复制(PDF/DOCX/XLSX/TXT),默认 false
pptPageMode boolean PPT 翻页模式(默认滑动模式),默认 false
gotoLastPos boolean 自动跳转上次浏览位置,默认 false
contentBgColor string 文档视图背景颜色,如 "#ffffff"
topBarBgColor string 顶栏颜色
topBarHeight number 顶栏高度(px)
success (res: TbsOpenResult) => void 文档打开成功(res.typeId / res.typeDes)
fail (res: TbsFailResult) => void 打开失败(res.code / res.message)
onClose () => void 文档关闭(用户关闭弹窗时也会触发)

错误码

code 说明
9020001 引擎未初始化,请先调用 initEngine
9020002 引擎初始化失败
9020003 TBS 不支持的文件格式
9020004 文件不存在(注意:传网络地址也会报此码,请先下载到本地)
9020005 参数错误
9020006 打开文档失败
9020007 获取 Activity 失败

示例代码

<template>
    <view class="page">
        <!-- 引擎初始化 -->
        <view class="section">
            <view class="section-title">
                <text>引擎初始化</text>
                <text class="badge" :class="engineReady ? 'badge-ok' : 'badge-wait'">{{ engineReady ? '已初始化' : '未初始化' }}</text>
            </view>
            <view class="form-item">
                <text class="form-label">LicenseKey</text>
                <input class="text-input" v-model="license" placeholder="请输入 TBS LicenseKey" />
            </view>
            <view class="btn-row">
                <button class="btn btn-primary" @click="handleInitEngine">初始化引擎</button>
                <button class="btn btn-plain" @click="handleCheckEngine">查询状态</button>
            </view>
        </view>

        <!-- 打开文档 -->
        <view class="section">
            <view class="section-title"><text>打开文档(Dialog 弹窗方式)</text></view>
            <view class="form-item">
                <text class="form-label">网络文档URL</text>
                <input class="text-input" v-model="netUrl" placeholder="https 链接,需带扩展名" />
            </view>
            <view class="btn-row">
                <button class="btn btn-plain" @click="handleDownload">下载到本地</button>
            </view>
            <view class="form-item">
                <text class="form-label">本地绝对路径</text>
                <input class="text-input" v-model="filePath" placeholder="/storage/.../test.pdf" />
            </view>
            <view class="opt-row">
                <text class="opt-label">长按复制</text>
                <switch :checked="enableLongPressMenu" @change="(e) => enableLongPressMenu = e.detail.value" color="#4f6ef7" />
            </view>
            <view class="opt-row">
                <text class="opt-label">跳转上次位置</text>
                <switch :checked="gotoLastPos" @change="(e) => gotoLastPos = e.detail.value" color="#4f6ef7" />
            </view>
            <view class="btn-row">
                <button class="btn btn-primary" @click="handleOpen">打开文档</button>
                <button class="btn btn-warn" @click="handleClose">关闭文档</button>
            </view>
            <view class="tip">提示:TBS 仅支持本地路径,网络文档请先下载;用户关闭弹窗后 onClose 会触发</view>
        </view>

        <!-- 日志 -->
        <view class="section log-section">
            <view class="section-title">
                <text>日志</text>
                <text class="clear-btn" @click="logs = []">清空</text>
            </view>
            <scroll-view class="log-list" scroll-y>
                <view class="log-item" v-for="(item, index) in logs" :key="index">
                    <text>{{ item }}</text>
                </view>
                <view v-if="logs.length === 0" class="log-empty"><text>暂无日志</text></view>
            </scroll-view>
        </view>
    </view>
</template>

<script>
    import {
        initEngine,
        openDocumentByDialog,
        closeDocument,
        isEngineLoaded,
        canOpenFileExt
    } from '@/uni_modules/zy-tbsfile-api'

    export default {
        data() {
            return {
                license: 'PooBEXT8UKSBQUN57PcdywzP+6OO3cRQ3qASkQffYtOSaM5hAjR3eLKfp4ck4wnT',
                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',
                filePath: '',
                enableLongPressMenu: false,
                gotoLastPos: false,
                engineReady: false,
                logs: []
            }
        },

        onUnload() {
            // 页面卸载时关闭文档并释放资源
            if (this.engineReady) {
                closeDocument({})
            }
        },

        methods: {
            addLog(msg) {
                const now = new Date()
                const pad = (n) => (n < 10 ? '0' + n : '' + n)
                const time = pad(now.getHours()) + ':' + pad(now.getMinutes()) + ':' + pad(now.getSeconds())
                this.logs.unshift('[' + time + '] ' + msg)
                if (this.logs.length > 100) {
                    this.logs.pop()
                }
            },

            handleInitEngine() {
                if (!this.license) {
                    uni.showToast({ title: '请输入 LicenseKey', icon: 'none' })
                    return
                }
                this.addLog('开始初始化引擎...')
                initEngine({
                    license: this.license,
                    success: (res) => {
                        this.engineReady = true
                        this.addLog('引擎初始化成功: ' + res.message)
                        uni.showToast({ title: '初始化成功', icon: 'success' })
                    },
                    fail: (res) => {
                        this.engineReady = false
                        this.addLog('初始化失败: code=' + res.code + ' ' + res.message)
                        uni.showToast({ title: '初始化失败', icon: 'none' })
                    }
                })
            },

            handleCheckEngine() {
                const loaded = isEngineLoaded()
                this.engineReady = loaded
                this.addLog('isEngineLoaded: ' + loaded)
            },

            // 下载网络文档到沙盒临时目录,成功后自动填充本地路径
            // TBS 仅支持本地路径;tempFilePath 保留扩展名,便于插件提取 fileExt
            handleDownload() {
                if (!this.netUrl) {
                    uni.showToast({ title: '请输入网络文档URL', icon: 'none' })
                    return
                }
                this.addLog('开始下载...')
                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)
                        } else {
                            this.addLog('下载失败: HTTP ' + res.statusCode)
                        }
                    },
                    fail: (err) => {
                        uni.hideLoading()
                        this.addLog('下载失败: ' + JSON.stringify(err))
                    }
                })
            },

            handleOpen() {
                if (!this.engineReady) {
                    uni.showToast({ title: '请先初始化引擎', icon: 'none' })
                    return
                }
                if (!this.filePath) {
                    uni.showToast({ title: '请先填写文件路径', icon: 'none' })
                    return
                }
                // 从路径提取后缀并检查格式支持
                const idx = this.filePath.lastIndexOf('.')
                const ext = idx >= 0 ? this.filePath.substring(idx + 1).toLowerCase() : ''
                if (ext && !canOpenFileExt(ext)) {
                    this.addLog('不支持格式: ' + ext)
                    uni.showToast({ title: '不支持 ' + ext + ' 格式', icon: 'none' })
                    return
                }
                this.addLog('打开文档: ' + this.filePath)
                openDocumentByDialog({
                    filePath: this.filePath,
                    enableLongPressMenu: this.enableLongPressMenu,
                    gotoLastPos: this.gotoLastPos,
                    success: (res) => {
                        this.addLog('文档已打开: typeId=' + res.typeId + ' ' + res.typeDes)
                    },
                    fail: (res) => {
                        this.addLog('打开失败: code=' + res.code + ' ' + res.message)
                        uni.showToast({ title: '打开失败: ' + res.code, icon: 'none' })
                    },
                    onClose: () => {
                        this.addLog('文档已关闭 (onClose)')
                    }
                })
            },

            handleClose() {
                closeDocument({
                    success: (res) => {
                        this.addLog('closeDocument: ' + res.message)
                    }
                })
            }
        }
    }
</script>

<style>
    .page {
        min-height: 100vh;
        background-color: #f5f6fa;
        padding: 24rpx;
    }

    .section {
        background-color: #ffffff;
        border-radius: 16rpx;
        padding: 30rpx;
        margin-bottom: 24rpx;
    }

    .section-title {
        display: flex;
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
        font-size: 30rpx;
        font-weight: bold;
        color: #333333;
        margin-bottom: 24rpx;
    }

    .badge {
        font-size: 22rpx;
        font-weight: normal;
        padding: 4rpx 16rpx;
        border-radius: 20rpx;
    }

    .badge-ok {
        background-color: #e6f7e9;
        color: #34a853;
    }

    .badge-wait {
        background-color: #fff4e5;
        color: #f57c00;
    }

    .form-item {
        margin-bottom: 20rpx;
    }

    .form-label {
        font-size: 26rpx;
        color: #666666;
        margin-bottom: 10rpx;
    }

    .text-input {
        height: 72rpx;
        background-color: #f5f6fa;
        border-radius: 10rpx;
        padding: 0 20rpx;
        font-size: 26rpx;
    }

    .btn-row {
        display: flex;
        flex-direction: row;
        margin-top: 10rpx;
    }

    .btn {
        flex: 1;
        font-size: 28rpx;
        height: 80rpx;
        line-height: 80rpx;
        border-radius: 10rpx;
        margin: 0 10rpx;
    }

    .btn-primary {
        background-color: #4f6ef7;
        color: #ffffff;
    }

    .btn-plain {
        background-color: #eef1ff;
        color: #4f6ef7;
    }

    .btn-warn {
        background-color: #fff1f0;
        color: #e54545;
    }

    .opt-row {
        display: flex;
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
        padding: 12rpx 0;
    }

    .opt-label {
        font-size: 26rpx;
        color: #333333;
    }

    .tip {
        font-size: 22rpx;
        color: #999999;
        margin-top: 16rpx;
        line-height: 1.6;
    }

    .log-section {
        /* 占满剩余高度 */
        flex: 1;
    }

    .log-list {
        height: 400rpx;
    }

    .log-item {
        font-size: 22rpx;
        color: #555555;
        padding: 8rpx 0;
        border-bottom: 1rpx solid #f0f0f0;
        word-break: break-all;
    }

    .log-empty {
        font-size: 24rpx;
        color: #bbbbbb;
        text-align: center;
        padding: 40rpx 0;
    }

    .clear-btn {
        font-size: 24rpx;
        color: #4f6ef7;
        font-weight: normal;
    }
</style>

注意事项

  1. TBS 引擎同一时刻仅支持一个文档,重复调用 openDocumentByDialog 会先自动关闭已打开文档。
  2. 单击/滑动/缩放、页码显示等由 SDK 默认弹窗 UI 自行处理,插件不透传手势事件(如需内嵌页面布局和页码跳转,请使用组件插件 zy-tbsfile 的 Layout 方式,但两者不可同时打包)。
  3. 混淆配置(离线打包场景):
    -dontwarn com.tencent.tbs.reader.**
    -keep class com.tencent.tbs.reader.** { *; }
  4. 隐私合规: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. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。