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

收藏人数:
购买源码授权版(
试用
赞赏(0)
下载 39
赞赏 0
下载 12573880
赞赏 1949
赞赏
京公网安备:11010802035340号