更新记录
1.0.0(2026-09-22)
修复已知bug
平台兼容性
uni-app(4.31)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | √ | - | - | - | √ | 5.0 | 12 | - |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| - | - | - | - | - | - | - | - | - | - | - | - |
文档预览(Android TBS / iOS QuickLook)
| 平台 | 预览实现 | 需要 LicenseKey | 内核 |
|---|---|---|---|
| Android | 腾讯浏览服务文档 SDK(新版) | 需要 | 已内置在 aar 中,无需下载 |
| iOS | 系统 QuickLook(QLPreviewController) |
不需要 | 系统自带 |
两种使用形态
| 形态 | 说明 | 适用页面 |
|---|---|---|
| 全屏预览 | 全屏展示,自带标题栏与关闭按钮 | vue / nvue 均可 |
嵌入式预览 <tbs-doc-view> |
宽高完全由页面决定,可做半屏、卡片、弹窗 | 只能 nvue |
在线文档(http/https)两端都不支持直接打开:Android 的
filePath只接受本地文件,iOS 的previewItemURL官方要求必须是file://URL。在线文档请先下载到本地再预览,见 5.2 在线文件。
一、环境要求
- HBuilderX
^3.6.18 - 支持平台:App-Android、App-iOS(H5 / 小程序 / uni-app x 不支持)
- 必须运行在「自定义调试基座」或正式包上:插件含 UTS 原生代码,标准基座无法运行
<tbs-doc-view>组件只能在 nvue 页面中使用(uni-app 下只有 nvue 页面能承载原生扩展组件)
二、安装
把 uni_modules/lj-office-preview 目录放进项目即可(插件市场导入或手动拷贝),无需修改 pages.json。
插件自带 AndroidManifest.xml,已声明所需权限(INTERNET、MANAGE_EXTERNAL_STORAGE)。
装好后请重新制作自定义基座(Android 与 iOS 都要),否则原生代码不生效。
三、快速开始(三步)
第 1 步:申请 LicenseKey(仅 Android 需要)
腾讯云控制台路径:
浏览服务 TBS → 基础版 → 立即体验 → 实名认证 → 产品控制台 → 概览页 → 领取免费量包(75000 次)→ 复制 licensekey
⚠️ 不要把 key 写进 uni_modules/ 目录,原因有两个:
uni_modules/lj-office-preview下的文件在插件更新时会被整体覆盖,你的 key 会丢;- 该 key 绑定你的腾讯云账号并有调用配额,不应随插件分发。
正确做法是放在自己的项目里:
// 你自己的配置文件,例如 utils/tbs-license.js
export const TBS_LICENSE_KEY = '你的 licensekey'
第 2 步:App 启动时初始化一次
// App.vue
import { initReaderOnce } from '@/uni_modules/lj-office-preview'
// #ifdef APP
import { TBS_LICENSE_KEY } from '@/utils/tbs-license.js'
// #endif
export default {
onLaunch() {
// #ifdef APP
// Android:设置 key 并初始化引擎;iOS:空实现,不传也不会报错
initReaderOnce(TBS_LICENSE_KEY)
// #endif
}
}
启动时调用一次即可。之后
openDocument*系列与<tbs-doc-view>组件都可以不再传 key。
第 3 步:打开文档
import { openDocumentByPath } from '@/uni_modules/lj-office-preview'
// path:本地文件绝对路径(在线文档请先下载,见 5.2)
const err = openDocumentByPath(path) // 空字符串 = 成功,否则为 "错误码:描述"
if (err) {
uni.showToast({ title: err, icon: 'none' })
}
四、拿到本地文件路径的两种方式
4.1 让用户选择本地文件
plus.io.chooseFile({
title: '选择文档',
filetypes: ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf', 'txt'],
multiple: false
}, (e) => {
const files = (e && e.files) ? e.files : []
if (files.length <= 0) return
// ⚠️ 不同版本返回的可能字符串、也可能是 File 对象,必须兼容
const first = files[0]
const p = typeof first === 'string' ? first : (first && first.path ? first.path : '')
// p 可能是 file:// 或 _doc 相对路径,交给插件前建议转成绝对路径
openDocumentByPath(toNativePath(p))
}, (err) => {
console.log('选择失败', err)
})
这里的
toNativePath是把file://前缀去掉、把_doc/xxx转成绝对路径的小工具,示例见 5.2。
4.2 下载在线文件
见下一节。
五、全屏预览
5.1 本地文件
import { openDocumentByPath, canOpenFile, getFileExtension } from '@/uni_modules/lj-office-preview'
function openLocal(path) {
const ext = getFileExtension(path) // 'pdf'
if (!canOpenFile(ext)) {
uni.showToast({ title: '不支持的格式:' + ext, icon: 'none' })
return
}
const err = openDocumentByPath(path)
if (err) {
uni.showToast({ title: err, icon: 'none' })
}
}
5.2 在线文件(先下载再预览)
两端都只能预览本地文件,所以在线文档需要先下载到本地。下面是可直接使用的最小实现:
import { toNativePath } from './path-helper' // 见本节末尾
/** 从路径/网址里取后缀(自动去掉 ?query 和 #hash) */
export function extOf(source) {
if (!source) return ''
let p = String(source)
const cut = p.search(/[?#]/)
if (cut >= 0) p = p.substring(0, cut)
const slash = p.lastIndexOf('/')
const dot = p.lastIndexOf('.')
if (dot < 0 || dot < slash) return ''
return p.substring(dot + 1).toLowerCase()
}
/** 是否是在线地址 */
export function isRemoteSource(source) {
return /^https?:\/\//i.test(String(source || ''))
}
/**
* 统一入口:在线地址自动下载,本地路径直接返回
* @returns {Promise<String>} 可直接交给插件的本地绝对路径
*/
export function resolveDocSource(source) {
if (!source) return Promise.reject(new Error('文档来源为空'))
if (isRemoteSource(source)) return downloadToLocal(source)
return Promise.resolve(toNativePath(source))
}
/** 下载到本地,并保证路径带上正确后缀 */
export function downloadToLocal(url) {
return new Promise((resolve, reject) => {
const wantExt = extOf(url)
uni.showLoading({ title: '下载中...', mask: true })
uni.downloadFile({
url,
// 需要鉴权的接口可以在这里加 header: { Authorization: '...' }
success: (res) => {
if (res.statusCode !== 200) {
uni.hideLoading()
reject(new Error('下载失败,状态码 ' + res.statusCode))
return
}
const temp = toNativePath(res.tempFilePath)
// 临时文件后缀正确就直接用
if (!wantExt || extOf(temp) === wantExt) {
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (s) => {
uni.hideLoading()
resolve(toNativePath(s.savedFilePath))
},
fail: () => {
uni.hideLoading()
resolve(temp)
}
})
return
}
// 后缀丢了(iOS 的 saveFile 会返回无后缀路径)→ 落盘后补后缀
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (s) => {
ensureLocalExt(toNativePath(s.savedFilePath), wantExt).then((p) => {
uni.hideLoading()
resolve(p)
})
},
fail: () => {
uni.hideLoading()
resolve(temp)
}
})
},
fail: (e) => {
uni.hideLoading()
reject(e)
}
})
})
}
/** 保证本地路径带正确后缀(TBS / QuickLook 都靠后缀判断文件类型) */
export function ensureLocalExt(path, ext) {
return new Promise((resolve) => {
const want = String(ext || '').toLowerCase()
if (!path || !want || extOf(path) === want) {
resolve(path)
return
}
const name = 'doc_' + Date.now() + '.' + want
try {
plus.io.resolveLocalFileSystemURL(path, (entry) => {
plus.io.resolveLocalFileSystemURL('_doc/', (dir) => {
entry.moveTo(dir, name, () => resolve(toNativePath('_doc/' + name)), () => resolve(path))
}, () => resolve(path))
}, () => resolve(path))
} catch (e) {
resolve(path)
}
})
}
/** _doc / file:// 等相对路径 → 本地绝对路径 */
export function toNativePath(path) {
if (path && typeof path === 'object') {
path = path.path || path.fullPath || ''
}
if (!path || typeof path !== 'string') return ''
let p = path
if (p.indexOf('file://') === 0) p = p.substring(7)
if (/^_/.test(p)) {
const abs = plus.io.convertLocalFileSystemURL(p)
return abs.indexOf('file://') === 0 ? abs.substring(7) : abs
}
return p
}
用法(在线、本地都走同一个入口):
import { resolveDocSource } from '@/utils/doc.js'
import { openDocumentByPath } from '@/uni_modules/lj-office-preview'
function preview(source) { // source 可以是 https 地址,也可以是本地路径
resolveDocSource(source)
.then((localPath) => {
const err = openDocumentByPath(localPath)
if (err) uni.showToast({ title: err, icon: 'none' })
})
.catch((e) => {
uni.showToast({ title: '失败:' + (e && e.message ? e.message : e), icon: 'none' })
})
}
preview('https://example.com/报告.pdf')
preview('/var/mobile/.../文件.pdf')
几个必须注意的点
- 文件名后缀不能少:TBS 与 QuickLook 都靠文件名后缀判断类型。iOS 的
uni.saveFile会返回无后缀路径(如_doc/uniapp_save/XXXX),必须补上后缀,否则报"不支持的文件格式"。 - URL 带 query 也能取对后缀:
.../file.pdf?token=abc要先把?token=abc去掉再取.pdf。 - 需要鉴权的地址:
uni.downloadFile支持header,可以放 token / cookie,但不能把它直接交给插件(插件只吃本地路径)。 - 文件名含中文/空格:正常支持,不要自己做 URL 编码,交给
uni.downloadFile处理。 - 大文件:预览前需等下载完成,建议自行加进度提示(
uni.downloadFile的onProgressUpdate)。
5.3 三种调用形态(都在两端可用)
import { openDocumentByPath, openDocumentFlat, openDocument } from '@/uni_modules/lj-office-preview'
// ① 单字符串参数(推荐,iOS 兼容性最好)
const err = openDocumentByPath(path) // '' = 成功
// ② 多字符串参数
const r1 = openDocumentFlat(path, 'pdf', '文档预览', '') // { code, message }
// ③ 对象参数(建议仅在 Android 使用)
const r2 = openDocument({ filePath: path, fileExt: 'pdf', title: '文档预览', licenseKey: '' })
iOS 请优先用
openDocumentByPath。iOS 的原生桥接对"对象参数 / 回调参数 / 多参数"不够稳定,出问题时表现为method call failed(函数体一行都不执行)。因此插件对 iOS 只暴露标量参数入口。
重要:原生调用一定要包 try/catch
try {
openDocumentByPath(path)
} catch (e) {
// 桥接异常不捕获会直接终止 App(不是崩溃,是未捕获的 JS 异常)
uni.showToast({ title: '打开失败:' + e.message, icon: 'none' })
}
六、嵌入式预览(半屏 / 卡片 / 弹窗)
只能在 nvue 页面使用。 vue 页面里写 <tbs-doc-view> 不会渲染(官方限制:uni-app 下只有 nvue 页面能放置原生扩展组件)。
6.1 nvue 页面完整示例
<template>
<div class="wrap">
<!-- 上半屏:文档区。原生组件必须给具体尺寸,flex 撑不开 -->
<div class="doc-area" :style="{ height: docHeight + 'px' }">
<tbs-doc-view
ref="docView"
:path="docPath"
:fileext="docExt"
:token="openToken"
:licensekey="licenseKey"
:viewwidth="docWidth"
:viewheight="docHeight"
@docstate="onDocState"
></tbs-doc-view>
</div>
<!-- 下半屏:业务区 -->
<div class="biz-area">
<text>{{ stateText }}</text>
<button text="重新加载" @tap="onReopen"></button>
<button text="关闭" @tap="onClose"></button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
docPath: '',
docExt: '',
licenseKey: '',
openToken: 0,
docWidth: 0,
docHeight: 0,
stateText: '准备中'
}
},
onLoad() {
// 原生组件在 nvue 里靠 flex 拿不到高度,必须算出具体 px
const info = uni.getSystemInfoSync()
const winW = info.windowWidth > 0 ? info.windowWidth : 375
const winH = info.windowHeight > 0 ? info.windowHeight : 667
const bizH = Math.round(420 * winW / 750) // 业务区 420rpx
this.docWidth = winW
this.docHeight = winH - bizH
// 从上一页拿路径
const app = getApp()
const g = (app && app.globalData) ? app.globalData : {}
this.docPath = g.docPath || ''
this.docExt = g.docExt || ''
this.licenseKey = g.docLicenseKey || ''
},
methods: {
onDocState(e) {
// 载荷是 Map:e.get('state') / e.get('message')
let state = ''
let message = ''
if (e && typeof e.get === 'function') {
state = e.get('state')
message = e.get('message')
} else if (e) {
state = e.state
message = e.message
}
this.stateText = (state || '') + ' ' + (message || '')
},
onReopen() {
this.openToken = this.openToken + 1 // 改 token 即触发重新打开
},
onClose() {
const ref = this.$refs.docView
const target = Array.isArray(ref) ? ref[0] : ref // nvue 下 ref 可能是数组
if (target) target.close()
}
}
}
</script>
<style>
.wrap { flex: 1; flex-direction: column; background-color: #ffffff; }
.doc-area { background-color: #f0f2f5; }
.biz-area { height: 420rpx; padding: 24rpx; background-color: #ffffff; }
</style>
6.2 属性
Android 与 iOS 支持的属性不同,请注意区分:
| 属性 | 平台 | 类型 | 说明 |
|---|---|---|---|
path |
两端 | String | 本地文件绝对路径(在线请先下载) |
fileext |
Android | String | 文件后缀;不传时自动从 path 截取 |
fileExt |
iOS | String | 同上(iOS 端属性名是驼峰 fileExt) |
token |
Android | Number | 自增计数器,改变它的值即触发打开(nvue 下直接调方法有时不生效) |
licensekey |
Android | String | LicenseKey;已在 App.vue 初始化过则可不传(prop 名必须全小写) |
viewwidth |
Android | Number | 嵌入区宽度,单位 CSS px |
viewheight |
Android | Number | 嵌入区高度,单位 CSS px |
iOS 端没有
token/licensekey/viewwidth/viewheight:iOS 用系统 QuickLook,不需要 key,尺寸通过style指定(见 6.4),触发打开靠改path或调用openFile()。组件标签是
tbs-doc-view(历史命名),与插件 IDlj-office-preview不同名,属正常情况。
6.3 事件与方法
| 类型 | 名称 | 说明 |
|---|---|---|
| 事件 | docstate |
载荷为 Map,键 state / message |
| 方法 | openFile(path) |
手动打开(需通过 ref 调用) |
| 方法 | close() |
关闭并释放 |
state 的实际取值:
| 值 | 含义 |
|---|---|
loading |
正在加载(已验证路径与格式,交给原生渲染中) |
error |
失败,message 是原因(文件不存在 / 格式不支持 / 引擎未初始化) |
closed |
已关闭 |
没有"加载完成"事件:原生 SDK 不回调渲染完成,只能通过
loading之后没有error来判断。
ref 取实例(nvue 下可能是数组):
const ref = this.$refs.docView
const target = Array.isArray(ref) ? ref[0] : ref
if (target && typeof target.openFile === 'function') {
target.openFile(this.docPath)
}
6.4 尺寸与弹窗
尺寸必须显式指定,原生组件在 nvue 里靠 flex: 1 拿不到高度,会按错误尺寸渲染导致错位。
| 平台 | 怎么指定 |
|---|---|
| Android | 传 :viewwidth / :viewheight(CSS px,插件内部会换算物理像素);也可用 style 设置组件占位 |
| iOS | 用 style 设置宽高(如 style="width:750rpx;height:600rpx") |
Android 端的兜底行为(建议永远显式传值):
viewwidth≤ 0 → 用屏宽;viewheight≤ 0 → 用屏高 − 420rpx;- 组件尚未完成布局时,先用「整屏宽 × 半屏高」占位,布局完成后自动纠正。
放进弹窗(必须在 nvue 页面里自己做弹窗):
<view class="mask" v-if="show">
<view class="card" :style="{ width: popW + 'px', height: popH + 'px' }">
<tbs-doc-view
:path="docPath"
:viewwidth="popW"
:viewheight="popH"
:token="openToken"
@docstate="onDocState"
></tbs-doc-view>
</view>
</view>
不能用 vue 页面的
uni-popup之类组件包它 —— 那是 webview 弹窗,里面的原生组件不会渲染。Android 端文档视图显示在组件所在位置的最上层:盖在组件上方的遮罩不会遮挡文档本身;弹窗做位移动画时,文档区域要等布局稳定后才对齐,快速拖动可能有短暂错位。
七、API 一览
两端导出完全一致(iOS 侧部分接口是空实现,保证跨端代码无需条件编译)。
| API | 返回 | 说明 |
|---|---|---|
initReaderOnce(licenseKey?) |
void | Android 设置 key 并初始化引擎(建议 App.vue onLaunch 调用一次);iOS 空实现 |
isReaderReady() |
boolean | 引擎是否就绪(iOS 恒 true) |
utsBuildTag() |
string | 插件版本探针,用于确认安装包里的原生插件是否为最新 |
canOpenFile(ext) |
boolean | 后缀是否受支持 |
getFileExtension(path, ext?) |
string | 取文件后缀(统一转小写) |
fileExists(path) |
boolean | 本地文件是否存在 |
getTempDir() |
string | Android 的 SDK 临时目录(iOS 返回空串) |
getTbsVersion() |
number | SDK 版本号(iOS 恒 0) |
getTbsStatus() |
TbsStatus | 引擎状态 { progress, state, message, version } |
startCoreDownload() |
boolean | 新版 SDK 内核已内置,无需下载;保留接口做跨端一致 |
hasAllFilesAccess() |
boolean | 是否已获得"所有文件访问权限"(iOS 恒 true) |
requestAllFilesAccess() |
void | 申请"所有文件访问权限"(Android 11+ 跳系统设置页) |
openDocumentByPath(filePath) |
string | 推荐:全屏打开,返回空串 = 成功 |
openDocumentFlat(filePath, fileExt, title, licenseKey) |
TbsDocResult | 多字符串参数版全屏打开 |
openDocument(options) |
TbsDocResult | 对象参数版全屏打开(建议仅 Android 使用) |
closeDocument() |
void | 关闭全屏预览并释放资源 |
八、支持的文件格式
doc docx rtf ppt pptx xls xlsx xlsm csv pdf txt epub chm
ofd需要在 Android 端额外引入一组依赖(kotlinx-coroutines、commons-io、dom4j、bcpkix、zip4j、pdfbox-android 等)并配置混淆规则,本插件默认未打包。
九、错误码与常见问题
错误码
| code | 含义 | 处理 |
|---|---|---|
0 |
成功 | — |
-1 |
不支持的文件格式 | 检查后缀,或文件是否没有后缀(见 5.2 第 1 点) |
-2 |
文件不存在或不可读 | 确认路径是本地绝对路径、文件已下载完成 |
-3 |
获取 Activity 失败 | 极少数时序问题,稍后重试 |
-4 |
文件无读取权限 | 见第十一节(Android 11+ 路径问题) |
-100 |
拿不到 App 上下文 | 确认在 App 环境调用 |
-101 |
LicenseKey 为空 | 见第三节 |
报 method call failed
说明当前安装包里的原生插件是旧版本。UTS 原生代码改动的产物不能通过 wgt / 热更更新,必须重新制作自定义调试基座或重新整包。
自检方式:调用 utsBuildTag(),能返回字符串即说明原生插件是新的。本版本期望值:
| 平台 | 期望返回 |
|---|---|
| Android | lj-preview-android-1 |
| iOS | lj-preview-ios-1 |
约定:每次改动 UTS 原生代码后把末尾数字往上加,便于一眼确认基座是否为最新。
点开就退出应用(不是崩溃)
多半是未捕获的 JS 异常。iOS 桥接失败时抛的异常如果没人接,运行时会直接终止 App。任何原生调用都要包 try/catch(见 5.3)。
嵌入式区域一片空白
- 页面是不是 nvue?
- Android 是否传了有效的
viewwidth/viewheight?iOS 是否用style设了宽高? - 路径是否已经下载完成、且带正确后缀?
- 看
@docstate是否报error。
打开后一两秒闪退(Android)
已修复。原因是 SDK 在主线程操作视图,而调用发生在 JS 线程。插件内部已统一把"关旧会话 + 开新会话"放到主线程执行,请勿绕过插件直接调用 TBS SDK。
全屏关闭后再打开一直停在"预览中"
已修复。SDK 是全局单例,关闭时必须真正释放会话。插件已在文档界面关闭回调里做了释放,全屏与嵌入式来回切换也已处理。
十、打开速度与体验说明
- 全屏预览:Android 走 SDK 自带 Dialog(自带标题栏与关闭按钮);iOS 用
present弹出 QuickLook。 - 首次打开:Android 首次初始化引擎会稍慢(毫秒级到百毫秒级,取决于设备);iOS 无初始化成本。
- 在线文档:需要先下载,等待时间取决于文件大小与网速,请自行加进度提示。
- 大体积文档:建议先下载到应用私有目录再预览,避免每次重复下载。
十一、Android 文件路径与权限
Android 11+ 无法直接读取公共目录(微信 / QQ 下载目录、Downloads 等),直读会报 EACCES。请通过系统文件选择器等方式取得路径,让系统授予临时读取权限后再交给插件。
- 插件内部会自动把读不到的文件复制到应用私有目录再打开(同一文件会复用已复制的副本);
- 需要读取其他应用目录时,可先引导用户授予"所有文件访问权限":
import { hasAllFilesAccess, requestAllFilesAccess } from '@/uni_modules/lj-office-preview'
if (!hasAllFilesAccess()) {
requestAllFilesAccess() // 跳系统设置页
}
- iOS 无此限制。
十二、更新日志
v1.0.0(首个版本)
Android
- 腾讯浏览服务文档 SDK(新版,内核已内置,无需下载),支持全屏预览与嵌入式预览
- Android 11+ 公共目录文件自动复制到应用私有目录后打开
- 修复「打开预览一两秒后闪退」
- 修复「全屏与嵌入式来回切换时闪退」
- 修复全屏预览关闭后再次打开会一直停留在"预览中"
iOS
- 系统 QuickLook,支持全屏预览与 nvue 组件嵌入
- 修复一点开就退出应用(
QLPreviewItem必须返回NSURL,URL不遵守该协议) - 新增单参数入口
openDocumentByPath,规避桥接对对象/回调参数的兼容问题
通用
- 统一 16 个跨端 API,页面侧无需区分平台
- 插件 ID 为
lj-office-preview;组件标签tbs-doc-view、自检串lj-preview-android-1/lj-preview-ios-1为独立命名,不受插件 ID 影响

收藏人数:
购买普通授权版(
试用
赞赏(0)
下载 6
赞赏 0
下载 12630694
赞赏 1950
赞赏
京公网安备:11010802035340号