跳到内容
  • 福昕首页
  • 开发中心
  • SDK文档资料
  • 插件商店
  • 福昕首页
  • 开发中心
  • SDK文档资料
  • 插件商店
申请试用
  • 企业自动化
    • Compressor
  • 福昕CloudAPI
  • 福昕PDF SDK 软件开发工具包
    • 福昕PDF SDK(ActiveX)
    • 福昕PDF SDK(桌面/服务器)
    • 福昕PDF SDK(Plug-in)
    • 福昕 PDF SDK(安卓)
    • 福昕PDF SDK(iOS)
    • 福昕PDF SDK(Web)
  • 福昕管理控制台
    • 公有云
    • 私有云
    • 通用情况
  • 福昕阅读器
    • RMS插件
  • 福昕高级编辑器
    • AI助手
    • Mac版本
      • 常规问题
    • Windows版本
      • 表单
      • ECM集成
      • 互联PDF
      • 企业管理指南
      • 保护
      • 内容编辑
      • 创建PDF
      • 压缩
      • 图章
      • 安装与卸载
      • 常见问题
      • 打印
      • 注释/评论
      • 福昕插件
      • 翻译助手
      • 翻译助手教程
      • 试用与激活
      • 转换
      • 页面管理
    • 教育用户
      • 论文查重
      • 操作指南
    • 网页版
      • Foxit eSign
      • 电子签章
    • 订阅
    • 资源
  • 福昕高级编辑器Linux版本
  • 福船图纸管理系统
  • 福昕PDF SDK 软件开发工具包 > 福昕PDF SDK(Web)
  • 标签:
  • Fragments,getFile接口,UIExtension,工具栏,文件流,新增按钮,自定义按钮

如何在「编辑」菜单工具栏中添加自定义按钮

  • 福昕知识库
  • 2026-11-06

在基于 Web View 示例做二次开发时,经常需要把自有功能(例如「PDF转Word」「上传到业务系统」)做成按钮,放到阅读器顶部工具栏里,让用户能像使用原生功能一样直接调用。

本文给出在「编辑」标签页(Edit)中添加自定义按钮的完整做法,包含按钮定义、回调绑定、启用状态联动,以及点击后如何取到当前文档的 PDF 文件流交给后端处理。

适用版本与验证环境

本文的组件名与接口均基于 WebSDK 11.1.0 实测。10.x 的组件体系大体一致,「编辑」标签页对应组件已存在,但 Fragment 与 Component 接口的完备程度需要按实际版本回归确认;9.x 及更早版本使用另一套 UI 定制机制,本文方案不适用。

需要先确认客户现场的构建版本,可读取 lib/package.json 中的 version 字段,或解包 jr-engine/gsdk/gsdk.wasm 查版本号。

一、先明确一个易踩的误区

部分资料会提到 customizeToolbar、customizeMenu、addButton 这类接口。这些接口在 11.1.0 的 lib/UIExtension.full.js 与 lib/index.d.ts 中并不存在,全文检索无任何命中。

11.1.0 实际提供两套机制:一是 Fragment 配置,即通过 appearance.getDefaultFragments() 或构造时传入的 fragments 选项,以 { target, action, template, config } 描述要插入的组件;二是 Component API,即通过 pdfui.getComponentByName(name) 取到组件后再调用 append、insert 等方法动态操作。

添加按钮推荐使用 Fragment 配置,它在 UI 构建之前生效,与原生按钮同批次渲染,样式和折叠行为完全一致。

二、找到「编辑」标签页的插入点

Fragment 的 target 建议写组件名称(component-name),获取组件名的方法:在浏览器中打开阅读器,右键点击目标位置的组件并选择「审查」,在对应标签中查看 component-name 属性的值;如果目标是容器组件,则右键点击它内部的子组件,读取子组件所在标签上的值。

「编辑」标签页相关的组件名如下:

用途 组件名
「编辑」标签按钮 edit-tab
「编辑」页按钮组容器(本文插入点) edit-toolbar-group-list
「编辑」页原生分组 adv-edit-tab-group-mode、edit-tab-group-editor、comment-tab-group-media、edit-tab-group-hand

其它标签页同理,分别是 home-toolbar-group-list、comment-toolbar-group-list、organize-toolbar-group-list、protect-toolbar-group-list。这些名称在 SDK 内部也以常量形式存在,位于 UIExtension.UIConsts.COMPONENT_NAMES 下,例如 EDIT_TOOLBAR_GROUP_LIST 的值就是 edit-toolbar-group-list。使用组件名比使用选择器路径更稳健,因为它不依赖 DOM 的嵌套结构。

三、插入按钮

在自定义 Appearance 中重写 getDefaultFragments(),向「编辑」页的按钮组容器追加一个自定义分组,组内放一个 ribbon 按钮。

var CustomAppearance = UIExtension.appearances.adaptive.extend({
    getDefaultFragments: function () {
        return [{
            target: 'edit-toolbar-group-list',
            action: UIExtension.UIConsts.FRAGMENT_ACTION.APPEND,
            template:
                '<group name="ext-pdf2word-group" retain-count="1">' +
                '  <ribbon-button name="ext-pdf-to-word" ' +
                '                 icon-class="ext-pdf2word-icon" ' +
                '                 text="ext:buttons.pdfToWord" ' +
                '                 @tooltip tooltip-title="ext:buttons.pdfToWord">' +
                '  </ribbon-button>' +
                '</group>',
            config: {
                target: 'ext-pdf-to-word',
                callback: PdfToWordController
            }
        }];
    }
});

几个要点需要说明。action 取 FRAGMENT_ACTION.APPEND,表示追加到目标容器的末尾;如果希望插到某个原生分组之前或之后,可改用 BEFORE 或 AFTER。template 中嵌套一层 group 可以让自定义按钮与原生按钮处于同一层级,从而保持相同的折叠与响应式行为;retain-count 控制该分组在窄屏下优先保留的按钮数量。icon-class 只负责挂载类名,图标样式需要自行提供 CSS,不会自动出现图标。text 走国际化资源,格式为「命名空间:键名」。

四、编写按钮的回调

回调用 Controller 子类实现,通过 fragments[].config.callback 绑定。框架在按钮被点击时调用 handle();mounted() 是处理生命周期逻辑(例如根据文档是否打开来启用或禁用按钮)的合适位置。

class PdfToWordController extends UIExtension.Controller {
    mounted() {
        super.mounted();
        var self = this;
        var pdfui = this.getPDFUI();
        var OPEN_FILE_SUCCESS = 'open-file-success';
        var WILL_CLOSE_DOCUMENT = 'will-close-document';

        pdfui.addViewerEventListener(OPEN_FILE_SUCCESS, function () {
            self.component.enable();
        });
        pdfui.addViewerEventListener(WILL_CLOSE_DOCUMENT, function () {
            self.component.disable();
        });
        self.component.disable();
    }

    handle() {
        var self = this;
        var pdfui = this.getPDFUI();
        return pdfui.getPDFViewer().then(function (viewer) {
            var doc = viewer.getCurrentPDFDoc();
            if (!doc) {
                alert('请先打开一个 PDF 文件。');
                return;
            }
            return self._exportAndSend(doc);
        });
    }

    _exportAndSend(doc) {
        var FLAGS = { normal: 0, incremental: 0x0001, XRefStream: 0x0008 };
        var flags = doc.hasSignature() ? FLAGS.incremental : FLAGS.XRefStream;

        return doc.getFile({ flags: flags }).then(function (file) {
            var fd = new FormData();
            fd.append('file', file, (file && file.name) || 'document.pdf');

            // 此处对接后端转换服务,详见下一节
            window.__lastPdfBlob = file;
            return file;
        }).catch(function (err) {
            console.error('获取文件流失败', err);
            alert('获取文件流失败:' + (err && (err.message || err)));
            throw err;
        });
    }
}

关于事件名,需要关闭文档时可用的是 will-close-document,ViewerEvents 里没有 close-file-success(即 closeFileSuccess),这一点容易误写导致禁用逻辑失效。

五、取当前文档的 PDF 文件流

很多自定义按钮(导出、上传、送后端转换)都需要拿到当前文档的完整字节流。WebSDK 提供 doc.getFile(options) 接口,返回一个 Promise,解析结果是 File 对象。

doc.getFile({ flags: flags }).then(function (file) {
    // file 是 File(Blob 的子类),可直接用于上传
});

这里有一个非常容易写错的地方:返回值已经是 File,不要再包一层 new Blob([file])。把 File 当作数组元素传入,它会被当成普通对象转成字符串,最终得到一个内容为 [object File] 的极小 Blob,后端收到的文件必然无法解析。

flags 是保存掩码,对应 lib/index.d.ts 中的 Saving_Flag 枚举,常用取值如下:

取值 名称 含义
0 normal 不做优化
0x0001 incremental 增量保存,保留原有签名
0x0002 noOriginal 不保留原始对象
0x0008 XRefStream 交叉引用流,标准导出方式(推荐)
0x0010 removeRedundantObjects 清理冗余对象
0x1000 linearized 线性化,适合快速网页浏览

选择策略可以直接沿用 SDK 内置「保存」按钮的逻辑:文档带签名时用 0x0001 增量保存,避免破坏签名;否则用 0x0008。需要注意,UIExtension.PDFDoc 在运行时并不存在,这个枚举无法从全局对象取到,实际使用时直接写上述字面量即可。

如果文档较大、需要展示进度,可以传第二个参数:

doc.getFile({ flags: flags }, {
    progressHandler: function (p) {
        console.log(p.current + ' / ' + p.total, p.status);
    }
});

六、把文件流交给后端

需要特别说明:WebSDK 本身不提供 PDF 转 Word 能力。格式互转属于福昕服务端SDK 或 Conversion SDK 的能力,可以在服务端部署并通过 HTTP 接口提供服务。

前端拿到 File 后,用 FormData 上传即可:

doc.getFile({ flags: flags }).then(function (file) {
    var fd = new FormData();
    fd.append('file', file, file.name || 'document.pdf');
    // 如需附带业务参数
    fd.append('targetFormat', 'docx');

    return fetch('/api/pdf2word', {
        method: 'POST',
        body: fd,                 // 不要手动设置 Content-Type
        credentials: 'include'
    });
}).then(function (r) {
    if (!r.ok) throw new Error('转换服务返回 ' + r.status);
    return r.blob();
}).then(function (docxBlob) {
    var url = URL.createObjectURL(docxBlob);
    var a = document.createElement('a');
    a.href = url;
    a.download = 'document.docx';
    a.click();
    URL.revokeObjectURL(url);
});

使用 FormData 时不要手动设置 Content-Type,浏览器会自动附加 multipart/form-data 所需的 boundary,手动设置反而会导致后端解析失败。

七、几个容易卡住的问题

以下是实测中反复出现、且现象容易被误判的问题,按排查优先级列出。

其一,授权参数必须完整传入。viewerOptions.jr 中需要同时提供 licenseSN 与 licenseKey,只传 preloadJrWorker 会导致引擎层授权失败。此时页面上看不出异常,但打开 PDF 的调用会永久挂起,既不 resolve 也不 reject,也不触发任何事件,现象很像「SDK 卡死」。实际原因是引擎初始化报错(error: 2,即 license is invalid)。这类问题优先检查授权。

其二,enginePath 的解析基准是 worker 脚本所在目录,而非当前页面。若照抄示例中的相对路径,常见的表现是 404。正确的写法是相对于 libPath 的引擎目录,例如 jr-engine/gsdk。

其三,addons 的两种写法互斥。数组形式传入 addon 类与字符串形式传入 allInOne.js 路径不能混在同一个数组里,混写会导致 SDK 去请求 allInOne.js/addon.info.json 而返回 404,整个 UI 无法启动。若需要同时使用,将自定义 addon 展开合并,例如 [ExtAddon].concat(window.UIXAddons || [])。

其四,自定义 addon 若走默认加载器,addon.info.json 中必须包含 i18n.ns 字段,否则会抛出 Cannot read properties of undefined (reading 'ns')。更省事的做法是在 addon 中自行实现 static getLoader(),返回 { loadI18N, loadAddonInfo },国际化资源直接由 getI18NResources() 提供,无需额外的 addon.info.json。

八、验证方法与结论

在受控环境中验证时,建议同时准备一个未做任何改动的官方示例作为对照组,以区分「自定义方案的问题」与「环境本身的问题」。

本例的验证方式是在 headless 浏览器中对自定义按钮做三项断言:组件级命中(按钮组件存在且父组件为自定义分组)、DOM 级归属(按钮节点位于「编辑」标签页面板内)、交互级回调(模拟点击后回调被真实调用)。三项断言均通过,页面无 4xx 请求,无未捕获异常。

需要说明的是,本次验证环境中连官方原始示例也无法打开 PDF 文档,属于环境性限制,与自定义按钮方案无关。因此「打开文档后取文件流」这一段未能在该环境中端到端跑通,其结论依据是接口的类型签名以及 SDK 内部两处同款调用点(内置保存按钮、清除签名后重载文档)。建议在真实浏览器环境中复跑一次,确认取到的对象类型、文件名与字节数符合预期。

九、接入步骤小结

第一步,在工程中加入自定义 Appearance,重写 getDefaultFragments() 并返回本文第三节的 Fragment 配置,或将其追加到既有的 fragments 数组中。第二步,将 target 改为自己工程中实际要插入的标签页组件名。第三步,把 text 换成自己的国际化键,并补充按钮图标的 CSS。第四步,在 handle() 中取到文件流后,改为调用自家的转换接口或 Conversion SDK 服务。第五步,确认 viewerOptions.jr 已同时传入 licenseSN 与 licenseKey,避免出现「页面能出、一开文档就卡住且无报错」的情况。

相关内容

如何统计 PDF 每页的图形对象数量(含按类型细分)

扫描件又歪又灰?福昕黑白去底+纠偏矫正教程来啦!

PDF如何转为PDF/A?

无文件时进入 PDF 操作页:previewFile 必填 content 的三种应对方案

UIExtension 功能按钮隐藏与禁用最佳实践

播放多媒体条目时不使用QuickTime播放器

如何自动检测PDF文档中的URL并创建链接

如何在AI助手中接入自己的模型

如何在AI助手中接入技能

如何通过权限控制有选择的运行JS脚本

推荐内容

如何统计 PDF 每页的图形对象数量(含按类型细分)

扫描件又歪又灰?福昕黑白去底+纠偏矫正教程来啦!

如何在「编辑」菜单工具栏中添加自定义按钮

PDF如何转为PDF/A?

无文件时进入 PDF 操作页:previewFile 必填 content 的三种应对方案

UIExtension 功能按钮隐藏与禁用最佳实践

播放多媒体条目时不使用QuickTime播放器

如何自动检测PDF文档中的URL并创建链接

如何在AI助手中接入自己的模型

如何在AI助手中接入技能

产品
  • 应用行业
  • 白皮书
开发支持
  • 开发中心
  • SDK文档资料

销售咨询:010-50951668

客服电话:0591-38509808

销售咨询
微信公众号

©2026 福建福昕软件开发股份有限公司 版权所有

隐私策略