EmbedPDF Vue 版 完整正文文档 全网首发

4 阅读11分钟

EmbedPDF Vue 版 完整正文文档 全网首发

来源:www.embedpdf.com/docs/vue(最后更新 2026-04-15)


一、架构总览

EmbedPDF 提供 两种使用方式

Drop-in ViewerHeadless Composables
定位开箱即用的完整 PDF 查看器无 UI 的逻辑 + 渲染原语
包含工具栏、侧边栏、缩略图Composables + 无样式组件
定制程度主题色、功能开关100% 自定义
包体积较大(含完整 UI)最小(Tree-shakeable)
上手时间分钟级小时级
适用场景标准文档预览自定义应用、编辑器、设计系统集成

选型指南:

使用场景推荐
需要几分钟内上线查看器Drop-in Viewer
需要完全匹配应用设计系统Headless Composables
简单文档预览Drop-in Viewer
构建文档编辑器或注释工具Headless Composables
最小包体积Headless Composables
不想写任何 UI 代码Drop-in Viewer

技术引擎(两种方式共享):

  • PDFium via WebAssembly(Chrome 同款渲染引擎)
  • 虚拟化渲染(仅渲染可见页,1000+ 页流畅)
  • TypeScript 全类型定义
  • Tree-shakeable

二、Drop-in Viewer

2.1 安装与基本使用

# npm
npm install @embedpdf/vue-pdf-viewer

# pnpm
pnpm add @embedpdf/vue-pdf-viewer

# yarn
yarn add @embedpdf/vue-pdf-viewer

# bun
bun add @embedpdf/vue-pdf-viewer

基本使用:

<!-- App.vue -->
<script setup lang="ts">
import { PDFViewer } from '@embedpdf/vue-pdf-viewer';
</script>

<template>
  <div style="height: 100vh;">
    <PDFViewer
      :config="{
        src: 'https://snippet.embedpdf.com/ebook.pdf',
        theme: { preference: 'light' }
      }"
      :style="{ width: '100%', height: '100%' }"
    />
  </div>
</template>

2.2 Nuxt 3 适配

需要 <ClientOnly> 包裹或动态导入(因为使用了 Canvas/WebAssembly):

<!-- pages/viewer.vue -->
<script setup lang="ts">
import { PDFViewer } from '@embedpdf/vue-pdf-viewer';
</script>

<template>
  <div style="height: 100vh;">
    <ClientOnly>
      <PDFViewer
        :config="{
          src: 'https://snippet.embedpdf.com/ebook.pdf'
        }"
        :style="{ width: '100%', height: '100%' }"
      />
    </ClientOnly>
  </div>
</template>

动态导入方式:

<!-- pages/viewer.vue -->
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';

const PDFViewer = defineAsyncComponent(() =>
  import('@embedpdf/vue-pdf-viewer').then((m) => m.PDFViewer)
);
</script>

<template>
  <div style="height: 100vh;">
    <ClientOnly>
      <PDFViewer
        :config="{
          src: 'https://snippet.embedpdf.com/ebook.pdf'
        }"
        :style="{ width: '100%', height: '100%' }"
      />
    </ClientOnly>
  </div>
</template>

2.3 Config 配置项

选项类型说明
srcstringPDF 文档 URL 或路径
themeobject主题配置
tabBar'always' | 'multiple' | 'never'标签栏显示策略,默认 'multiple'
disabledCategoriesstring[]禁用的功能分类
i18nobject国际化配置
annotationsobject注释默认配置(作者、工具等)
panobject手型工具配置
zoomobject缩放级别和限制
spreadobject双页展开布局
scrollobject滚动方向和逻辑
documentManagerobject文档加载选项
permissionsobject权限与安全配置
exportobject导出配置

2.4 生命周期事件

事件说明
@init容器初始化完成,可注册图标、获取 container
@ready插件注册完成,可获取 PluginRegistry 操作所有插件

三、主题系统(Theme)

3.1 模式设置

<script setup lang="ts">
import { PDFViewer } from '@embedpdf/vue-pdf-viewer';
</script>

<template>
  <PDFViewer
    :config="{
      src: 'https://snippet.embedpdf.com/ebook.pdf',
      theme: { preference: 'dark' }  // 'light' | 'dark' | 'system'
    }"
  />
</template>

3.2 颜色覆盖(深合并,只改需要改的)

<!-- 自定义品牌色示例 -->
<script setup lang="ts">
import { PDFViewer } from '@embedpdf/vue-pdf-viewer';
</script>

<template>
  <PDFViewer
    :config="{
      src: '/document.pdf',
      theme: {
        preference: 'system',
        light: {
          accent: {
            primary: '#9333ea',
            primaryHover: '#7e22ce',
            primaryActive: '#6b21a8',
            primaryLight: '#f3e8ff',
            primaryForeground: '#fff'
          }
        },
        dark: {
          accent: {
            primary: '#a855f7',
            primaryHover: '#9333ea',
            primaryActive: '#7e22ce',
            primaryLight: '#581c87',
            primaryForeground: '#fff'
          }
        }
      }
    }"
  />
</template>

3.3 主题结构

Accent(品牌色):

accent: {
  primary: string;         // 主品牌色
  primaryHover: string;    // 悬停态
  primaryActive: string;   // 按下态
  primaryLight: string;    // 淡色(选中背景等)
  primaryForeground: string; // 主色上的文字色
}

Background(背景层):

background: {
  app: string;       // 文档区主背景
  surface: string;   // 工具栏、侧边栏、面板
  surfaceAlt: string; // 次要工具栏
  elevated: string;  // 下拉菜单、弹窗
  overlay: string;   // 模态遮罩(通常为半透明 rgba)
  input: string;     // 输入框、复选框
}

Foreground(文字色):

foreground: {
  primary: string;    // 标题、正文
  secondary: string;  // 标签、次要文字
  muted: string;      // 占位符、弱化文字
  disabled: string;   // 禁用态元素
  onAccent: string;   // 品牌色上的文字
}

Interactive(交互态):

interactive: {
  hover: string;    // 标准按钮悬停背景
  active: string;   // 点击背景
  selected: string; // 选中项背景(如当前工具)
  focus: string;    // 焦点环颜色
}

Border(边框):

border: {
  default: string;  // 标准输入框、分割线
  subtle: string;   // 淡化分割线
  strong: string;   // 激活输入框、强调
}

Semantic States(语义状态):

state: {
  error: string;
  errorLight: string;
  warning: string;
  warningLight: string;
  success: string;
  successLight: string;
  info: string;
  infoLight: string;
}

3.4 与应用主题同步

<!-- ThemeSync.vue -->
<script setup lang="ts">
import { ref, watch } from 'vue';
import { PDFViewer, type EmbedPdfContainer } from '@embedpdf/vue-pdf-viewer';

interface Props {
  theme: 'light' | 'dark';
}

const props = defineProps<Props>();

const container = ref<EmbedPdfContainer | null>(null);

const handleInit = (c: EmbedPdfContainer) => {
  container.value = c;
};

watch(
  () => props.theme,
  (preference) => {
    container.value?.setTheme({ preference });
  }
);
</script>

<template>
  <PDFViewer
    @init="handleInit"
    :config="{
      src: 'https://snippet.embedpdf.com/ebook.pdf',
      theme: { preference: theme }
    }"
  />
</template>

四、UI 自定义

4.1 禁用功能(disabledCategories)

层级禁用——禁用父级会连带禁用所有子级:

<script setup lang="ts">
import { PDFViewer } from '@embedpdf/vue-pdf-viewer';
</script>

<template>
  <PDFViewer
    :config="{
      src: 'https://snippet.embedpdf.com/ebook.pdf',
      disabledCategories: ['annotation', 'print', 'export']
    }"
  />
</template>

可用分类:

可禁用的子分类
Zoomzoom, zoom-in, zoom-out, zoom-fit-page, zoom-fit-width, zoom-marquee, zoom-level
Annotationannotation, annotation-markup, annotation-highlight, annotation-underline, annotation-strikeout, annotation-squiggly, annotation-ink, annotation-text, annotation-stamp
Formform, form-textfield, form-checkbox, form-radio, form-select, form-listbox, form-fill-mode
Shapesannotation-shape, annotation-rectangle, annotation-circle, annotation-line, annotation-arrow, annotation-polygon, annotation-polyline
Redactionredaction, redaction-area, redaction-text, redaction-apply, redaction-clear
Documentdocument, document-open, document-close, document-print, document-capture, document-export, document-fullscreen, document-protect
Pagepage, spread, rotate, scroll, navigation
Panelpanel, panel-sidebar, panel-search, panel-comment
Toolstools, pan, pointer, capture
Selectionselection, selection-copy
Historyhistory, history-undo, history-redo
Insertinsert, insert-rubber-stamp, insert-signature, insert-image

4.2 高级自定义(访问 Viewer Registry)

<!-- CustomViewer.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import {
  PDFViewer,
  type EmbedPdfContainer,
  type PluginRegistry,
  type CommandsPlugin,
  type UIPlugin,
} from '@embedpdf/vue-pdf-viewer';

const container = ref<EmbedPdfContainer | null>(null);

const handleInit = (c: EmbedPdfContainer) => {
  container.value = c;
};

const handleReady = (registry: PluginRegistry) => {
  const commands = registry.getPlugin<CommandsPlugin>('commands')?.provides();
  const ui = registry.getPlugin<UIPlugin>('ui')?.provides();

  if (!commands || !ui) return;

  // 注册自定义命令
  commands.registerCommand({
    id: 'custom.hello',
    label: 'Say Hello',
    icon: 'smiley',
    action: () => alert('Hello from my custom button!')
  });

  // 添加到工具栏(见下文)
};
</script>

<template>
  <PDFViewer
    @init="handleInit"
    @ready="handleReady"
    :config="{ src: '/doc.pdf' }"
  />
</template>

4.3 注册自定义图标

<script setup lang="ts">
const handleInit = (c: EmbedPdfContainer) => {
  container.value = c;

  // 在 init 阶段注册图标
  container.value.registerIcons({
    smiley: {
      viewBox: '0 0 24 24',
      paths: [
        { d: 'M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0', stroke: 'currentColor', fill: 'none' },
        { d: 'M9 10l.01 0', stroke: 'currentColor', fill: 'none' },
        { d: 'M15 10l.01 0', stroke: 'currentColor', fill: 'none' },
        { d: 'M9.5 15a3.5 3.5 0 0 0 5 0', stroke: 'currentColor', fill: 'none' }
      ]
    }
  });
};
</script>

4.4 修改 Toolbar

const schema = ui.getSchema();
const toolbar = schema.toolbars['main-toolbar'];

// 克隆并修改 items
const items = structuredClone(toolbar.items);
const rightGroup = items.find(item => item.id === 'right-group');

if (rightGroup) {
  // 添加自定义按钮
  rightGroup.items.push({
    type: 'command-button',
    id: 'smiley-button',
    commandId: 'custom.hello',
    variant: 'icon'
  });
}

// 应用修改
ui.mergeSchema({
  toolbars: { 'main-toolbar': { ...toolbar, items } }
});

五、安全与权限

5.1 核心概念

  • 加密(Encryption):AES-256/RC4,是真正的密码学保护。分用户密码(打开文档)和所有者密码(完全访问)。解决"谁能访问"。
  • 权限标志(Permission Flags):不是安全机制!只要用户能解密查看内容,就能截图、打印、另存。权限标志只是给合规查看器的"礼貌请求",无法从技术上强制执行。

5.2 配置

<script setup lang="ts">
import { PDFViewer } from '@embedpdf/vue-pdf-viewer';
</script>

<template>
  <PDFViewer
    :config="{
      src: '/document.pdf',
      permissions: {
        // 忽略 PDF 内部权限标志
        enforceDocumentPermissions: false,

        // 或覆盖特定标志
        overrides: {
          print: false,        // 禁止打印
          copyContents: true   // 允许复制(即使 PDF 禁止)
        }
      }
    }"
  />
</template>

5.3 权限解析优先级

  1. overrides 中的显式覆盖 → 最高优先
  2. enforceDocumentPermissions: false → 忽略 PDF 标志,未覆盖的默认允许
  3. PDF 内嵌标志 → 默认行为

5.4 支持的权限名称

名称说明
print打印
printHighQuality高质量打印
modifyContents修改页面内容
copyContents选择和复制文本/图片
modifyAnnotations添加、编辑或删除注释
fillForms填写交互式表单字段
extractForAccessibility为屏幕阅读器提取内容
assembleDocument插入、旋转或删除页面

5.5 UI 自动适配

  • print 被拒 → 打印按钮禁用/隐藏
  • copyContents 被拒 → 禁用文本选择
  • modifyAnnotations 被拒 → 注释工具禁用

六、插件详解

6.1 注释(Annotation)

配置:

<template>
  <PDFViewer
    :config="{
      annotation: {
        annotationAuthor: 'John Doe',
      }
    }"
  />
</template>

设置活动工具:

<script setup lang="ts">
import { ref } from 'vue';
import { PDFViewer, type PluginRegistry, type AnnotationPlugin } from '@embedpdf/vue-pdf-viewer';

const registry = ref<PluginRegistry | null>(null);

const handleReady = (r: PluginRegistry) => {
  registry.value = r;
};

const activateHighlighter = () => {
  const annotationPlugin = registry.value?.getPlugin<AnnotationPlugin>('annotation')?.provides();
  annotationPlugin?.setActiveTool('highlight');
};

const activatePen = () => {
  const annotationPlugin = registry.value?.getPlugin<AnnotationPlugin>('annotation')?.provides();
  annotationPlugin?.setActiveTool('ink');
};

const deactivateTool = () => {
  const annotationPlugin = registry.value?.getPlugin<AnnotationPlugin>('annotation')?.provides();
  annotationPlugin?.setActiveTool(null);
};
</script>

<template>
  <PDFViewer @ready="handleReady" :config="{ src: '/doc.pdf' }" />
</template>

工具 ID 列表:

工具ID说明
高亮'highlight'文本高亮
下划线'underline'文本下划线
墨迹/画笔'ink'自由手绘
矩形'square'方形/矩形
圆形'circle'圆形/椭圆
自由文本'freeText'自由文本框
便签'text'便签注释

监听注释事件:

<script setup lang="ts">
import { PDFViewer, type PluginRegistry } from '@embedpdf/vue-pdf-viewer';

const handleReady = (registry: PluginRegistry) => {
  const annotationPlugin = registry.getPlugin('annotation')?.provides();

  // 监听创建、更新和删除
  annotationPlugin?.onAnnotationEvent((event) => {
    const { type, annotation, pageIndex } = event;

    if (type === 'create') {
      console.log('Created annotation:', annotation.id);
    } else if (type === 'delete') {
      console.log('Deleted annotation:', annotation.id);
    }
  });

  // 监听工具变化
  annotationPlugin?.onActiveToolChange(({ tool }) => {
    console.log('Active tool:', tool ? tool.name : 'Selection');
  });
};
</script>

<template>
  <PDFViewer @ready="handleReady" :config="{ src: '/doc.pdf' }" />
</template>

导出注释:

<script setup lang="ts">
import { ref } from 'vue';
import {
  PDFViewer,
  type PluginRegistry,
  type AnnotationPlugin,
  type AnnotationTransferItem,
} from '@embedpdf/vue-pdf-viewer';

const exported = ref<AnnotationTransferItem[] | null>(null);

const handleReady = (registry: PluginRegistry) => {
  const api = registry.getPlugin<AnnotationPlugin>('annotation')?.provides();

  api?.exportAnnotations().wait(
    (items) => {
      exported.value = items;
      console.log(`Exported ${items.length} annotations`);
    },
    (error) => console.error('Export failed', error),
  );
};
</script>

导入注释:

<script setup lang="ts">
api?.importAnnotations(exported.value);
</script>

对于印章(stamp),ctx.data 字段接受包含 PNG、JPEG 或 PDF 数据的原始 ArrayBuffer,引擎通过 magic bytes 自动检测格式。

添加自定义印章工具:

<script setup lang="ts">
import {
  PDFViewer,
  type PluginRegistry,
  type AnnotationPlugin,
  type AnnotationTool,
  PdfAnnotationSubtype,
  type PdfStampAnnoObject,
} from '@embedpdf/vue-pdf-viewer';

const handleReady = (registry: PluginRegistry) => {
  const api = registry.getPlugin<AnnotationPlugin>('annotation')?.provides();

  api?.addTool<AnnotationTool<PdfStampAnnoObject>>({
    id: 'stampCheckmark',
    name: 'Checkmark',
    interaction: { exclusive: true, cursor: 'crosshair' },
    matchScore: () => 0,
    defaults: {
      type: PdfAnnotationSubtype.STAMP,
      imageSrc: '/circle-checkmark.png',
      imageSize: { width: 30, height: 30 },
    },
    behavior: {
      showGhost: true,
      deactivateToolAfterCreate: true,
      selectAfterCreate: true,
    },
  });
};
</script>

<template>
  <PDFViewer @ready="handleReady" :config="{ src: '/doc.pdf' }" />
</template>

6.2 表单(Form)

加载含 AcroForm 字段的 PDF 自动可交互。默认锁定 form 分类,即填即用;切换到表单编辑模式可编辑控件。

配置:

<template>
  <PDFViewer
    :config="{
      src: '/form.pdf',
      export: {
        defaultFileName: 'filled-form.pdf'
      }
    }"
  />
</template>

访问表单插件:

<script setup lang="ts">
import { ref } from 'vue';
import { PDFViewer, type PluginRegistry, type FormPlugin } from '@embedpdf/vue-pdf-viewer';

const registry = ref<PluginRegistry | null>(null);

const handleReady = (r: PluginRegistry) => {
  registry.value = r;
};

const formScope = () => {
  const formPlugin = registry.value?.getPlugin<FormPlugin>('form')?.provides();
  return formPlugin?.forDocument('form-doc');
};
</script>

<template>
  <PDFViewer @ready="handleReady" :config="{ src: '/form.pdf' }" />
</template>

监听字段变化:

<script setup lang="ts">
const scope = formScope();

scope?.onFormReady((fields) => {
  console.log('Fields discovered:', fields.length);
  console.log('Initial values:', scope.getFormValues());
});

scope?.onFieldValueChange((event) => {
  console.log('Changed widget:', event.annotationId);
  console.log('Updated values:', scope.getFormValues());
});
</script>

程序化设置值:

<script setup lang="ts">
const fillForm = async () => {
  const scope = formScope();

  await scope?.setFormValues({
    First_Name: 'Jane',
    Last_Name: 'Doe',
    Email_Address: 'jane.doe@example.com',
  }).toPromise();
};
</script>

Radio 按钮需要 PDF 中定义的精确导出值。Checkbox 传 "Off" 清除,其他任意字符串会归一化为控件的选中导出值。

保存填写后的 PDF:

<script setup lang="ts">
const saveFilledPdf = async () => {
  const exportPlugin = registry.value?.getPlugin('export')?.provides();
  const pdfBytes = await exportPlugin
    ?.forDocument('form-doc')
    .saveAsCopy()
    .toPromise();
};
</script>

6.3 文档管理(Document Manager)

配置:

<template>
  <PDFViewer
    :config="{
      documentManager: {
        initialDocuments: [
          {
            url: 'https://snippet.embedpdf.com/ebook.pdf',
            autoActivate: true,
            documentId: 'ebook-embedpdf',
          },
          {
            url: 'https://snippet.embedpdf.com/ebook.pdf',
            autoActivate: false
          }
        ],
        maxDocuments: 10
      },
      tabBar: 'multiple'
    }"
  />
</template>

从 URL 加载:

<script setup lang="ts">
import { ref } from 'vue';
import { PDFViewer, type PluginRegistry } from '@embedpdf/vue-pdf-viewer';

const registry = ref<PluginRegistry | null>(null);

const handleReady = (r: PluginRegistry) => {
  registry.value = r;
};

const openRemotePdf = () => {
  const docManager = registry.value?.getPlugin('document-manager')?.provides();

  docManager?.openDocumentUrl({
    url: 'https://snippet.embedpdf.com/ebook.pdf',
    documentId: 'invoice-123',
    autoActivate: true
  });
};
</script>

<template>
  <PDFViewer @ready="handleReady" :config="{ src: '/doc.pdf' }" />
</template>

从 Buffer 加载(本地文件):

<script setup lang="ts">
import { ref } from 'vue';
import { PDFViewer, type PluginRegistry } from '@embedpdf/vue-pdf-viewer';

const registry = ref<PluginRegistry | null>(null);

const handleReady = (r: PluginRegistry) => {
  registry.value = r;
};

const handleFileSelect = async (event: Event) => {
  const target = event.target as HTMLInputElement;
  const file = target.files?.[0];
  if (!file) return;

  const buffer = await file.arrayBuffer();

  const docManager = registry.value?.getPlugin('document-manager')?.provides();

  docManager?.openDocumentBuffer({
    buffer: buffer,
    name: file.name,
    autoActivate: true
  });
};
</script>

<template>
  <input type="file" accept=".pdf" @change="handleFileSelect" />
  <PDFViewer @ready="handleReady" :config="{ src: '/doc.pdf' }" />
</template>

原生文件选择器:

<script setup lang="ts">
const openFilePicker = () => {
  const docManager = registry.value?.getPlugin('document-manager')?.provides();
  docManager?.openFileDialog();
};
</script>

管理活动文档:

<script setup lang="ts">
const switchDocument = () => {
  const docManager = registry.value?.getPlugin('document-manager')?.provides();
  docManager?.setActiveDocument('invoice-123');
};

const closeDocument = () => {
  const docManager = registry.value?.getPlugin('document-manager')?.provides();
  docManager?.closeDocument('invoice-123');
};

const closeAll = () => {
  const docManager = registry.value?.getPlugin('document-manager')?.provides();
  docManager?.closeAllDocuments();
};
</script>

事件:

事件Payload说明
onDocumentOpenedDocumentState文档加载成功
onDocumentCloseddocumentId文档关闭
onActiveDocumentChanged{ previous, current }切换标签页
onDocumentError{ documentId, error }加载失败
<script setup lang="ts">
import { PDFViewer, type PluginRegistry } from '@embedpdf/vue-pdf-viewer';

const handleReady = (registry: PluginRegistry) => {
  const docManager = registry.getPlugin('document-manager')?.provides();

  docManager?.onDocumentOpened((doc) => {
    console.log(`Opened: ${doc.name} (${doc.id})`);
  });
};
</script>

<template>
  <PDFViewer @ready="handleReady" :config="{ src: '/doc.pdf' }" />
</template>

6.4 缩放(Zoom)

配置:

<script setup lang="ts">
import { PDFViewer, ZoomMode } from '@embedpdf/vue-pdf-viewer';
</script>

<template>
  <PDFViewer
    :config="{
      zoom: {
        defaultZoomLevel: ZoomMode.FitPage,
        minZoom: 0.5,
        maxZoom: 3.0
      }
    }"
  />
</template>

缩放模式:

模式说明
ZoomMode.Automatic自动寻找最佳适配
ZoomMode.FitPage整页适配视口
ZoomMode.FitWidth页面宽度适配视口宽度

编程控制:

<script setup lang="ts">
import { ref } from 'vue';
import { PDFViewer, ZoomMode, type PluginRegistry, type ZoomPlugin } from '@embedpdf/vue-pdf-viewer';

const registry = ref<PluginRegistry | null>(null);

const handleReady = (r: PluginRegistry) => {
  registry.value = r;
};

const zoomIn = () => {
  const zoomPlugin = registry.value?.getPlugin<ZoomPlugin>('zoom')?.provides();
  const docZoom = zoomPlugin?.forDocument('my-document-id');
  docZoom?.zoomIn();
};

const zoomOut = () => {
  const zoomPlugin = registry.value?.getPlugin<ZoomPlugin>('zoom')?.provides();
  const docZoom = zoomPlugin?.forDocument('my-document-id');
  docZoom?.zoomOut();
};

const fitWidth = () => {
  const zoomPlugin = registry.value?.getPlugin<ZoomPlugin>('zoom')?.provides();
  const docZoom = zoomPlugin?.forDocument('my-document-id');
  docZoom?.requestZoom(ZoomMode.FitWidth);
};
</script>

<template>
  <PDFViewer @ready="handleReady" :config="{ src: '/doc.pdf' }" />
</template>

监听缩放变化:

<script setup lang="ts">
import { PDFViewer, type PluginRegistry } from '@embedpdf/vue-pdf-viewer';

const handleReady = (registry: PluginRegistry) => {
  const zoomPlugin = registry.getPlugin('zoom')?.provides();
  const docZoom = zoomPlugin?.forDocument('my-document-id');

  docZoom?.onStateChange((state) => {
    console.log('Current Zoom:', state.currentZoomLevel);
  });
};
</script>

<template>
  <PDFViewer @ready="handleReady" :config="{ src: '/doc.pdf' }" />
</template>

6.5 滚动与导航(Scroll)

配置:

<script setup lang="ts">
import { PDFViewer, ScrollStrategy } from '@embedpdf/vue-pdf-viewer';
</script>

<template>
  <PDFViewer
    :config="{
      documentManager: {
        initialDocuments: [{
          url: 'https://snippet.embedpdf.com/ebook.pdf',
          documentId: 'my-doc'
        }]
      },
      scroll: {
        defaultStrategy: ScrollStrategy.Vertical,
        defaultPageGap: 20
      }
    }"
  />
</template>

编程导航:

<script setup lang="ts">
import { ref } from 'vue';
import { PDFViewer, type PluginRegistry } from '@embedpdf/vue-pdf-viewer';

const registry = ref<PluginRegistry | null>(null);

const handleReady = (r: PluginRegistry) => {
  registry.value = r;
};

const nextPage = () => {
  const scroll = registry.value?.getPlugin('scroll')?.provides();
  scroll?.scrollToNextPage();
};

const goToPage = (pageNumber: number) => {
  const scroll = registry.value?.getPlugin('scroll')?.provides();
  scroll?.scrollToPage({ pageNumber });
};
</script>

<template>
  <PDFViewer @ready="handleReady" :config="{ src: '/doc.pdf' }" />
</template>

指定文档导航:

<script setup lang="ts">
const goToPage10 = () => {
  const scroll = registry.value?.getPlugin('scroll')?.provides();
  const docScroll = scroll?.forDocument('my-doc');
  docScroll?.scrollToPage({ pageNumber: 10 });
};
</script>

加载后跳页:

<script setup lang="ts">
import { PDFViewer, type PluginRegistry } from '@embedpdf/vue-pdf-viewer';

const handleReady = (registry: PluginRegistry) => {
  const scroll = registry.getPlugin('scroll')?.provides();

  scroll?.onLayoutReady((event) => {
    if (event.documentId === 'my-doc' && event.isInitial) {
      scroll.forDocument('my-doc').scrollToPage({
        pageNumber: 3,
        behavior: 'instant' // 初始加载瞬间跳转
      });
    }
  });
};
</script>

<template>
  <PDFViewer @ready="handleReady" :config="{ src: '/doc.pdf' }" />
</template>

监听页面变化:

<script setup lang="ts">
import { PDFViewer, type PluginRegistry } from '@embedpdf/vue-pdf-viewer';

const handleReady = (registry: PluginRegistry) => {
  const scroll = registry.getPlugin('scroll')?.provides();

  scroll?.onPageChange((event) => {
    console.log(`Doc: ${event.documentId}`);
    console.log(`Current Page: ${event.pageNumber}`);
    console.log(`Total Pages: ${event.totalPages}`);
  });
};
</script>

<template>
  <PDFViewer @ready="handleReady" :config="{ src: '/doc.pdf' }" />
</template>

6.6 手型工具(Pan)

配置:

<template>
  <PDFViewer
    :config="{
      pan: {
        defaultMode: 'mobile'  // 'mobile' | 'always' | 'never'
      }
    }"
  />
</template>

模式说明:

模式说明
'mobile'默认。仅触屏设备默认启用手型工具,桌面端为文本选择模式
'always'始终默认手型工具
'never'始终为文本选择或其他模式

编程控制:

<script setup lang="ts">
import { ref } from 'vue';
import { PDFViewer, type PluginRegistry, type PanPlugin } from '@embedpdf/vue-pdf-viewer';

const registry = ref<PluginRegistry | null>(null);

const handleReady = (r: PluginRegistry) => {
  registry.value = r;
};

const togglePan = () => {
  const panPlugin = registry.value?.getPlugin<PanPlugin>('pan')?.provides();
  const docPan = panPlugin?.forDocument('my-doc-id');
  docPan?.togglePan();
};

const enablePan = () => {
  const panPlugin = registry.value?.getPlugin<PanPlugin>('pan')?.provides();
  const docPan = panPlugin?.forDocument('my-doc-id');
  docPan?.enablePan();
};

const disablePan = () => {
  const panPlugin = registry.value?.getPlugin<PanPlugin>('pan')?.provides();
  const docPan = panPlugin?.forDocument('my-doc-id');
  docPan?.disablePan();
};
</script>

<template>
  <PDFViewer @ready="handleReady" :config="{ src: '/doc.pdf' }" />
</template>

检查状态:

<script setup lang="ts">
const isPanning = () => {
  const panPlugin = registry.value?.getPlugin('pan')?.provides();
  const docPan = panPlugin?.forDocument('my-doc-id');
  return docPan?.isPanMode();
};
</script>

监听变化:

<script setup lang="ts">
import { ref } from 'vue';
import { PDFViewer, type PluginRegistry } from '@embedpdf/vue-pdf-viewer';

const toolState = ref<'hand' | 'cursor'>('cursor');

const handleReady = (registry: PluginRegistry) => {
  const panPlugin = registry.getPlugin('pan')?.provides();

  // 订阅全局变化
  panPlugin?.onPanModeChange(({ documentId, isPanMode }) => {
    console.log(`Document ${documentId} pan mode is now: ${isPanMode}`);
  });

  // 或订阅特定文档
  const docPan = panPlugin?.forDocument('my-doc');
  docPan?.onPanModeChange((isPanMode) => {
    toolState.value = isPanMode ? 'hand' : 'cursor';
  });
};
</script>

<template>
  <PDFViewer @ready="handleReady" :config="{ src: '/doc.pdf' }" />
</template>

6.7 导出与保存(Export)

导出的是包含所有修改(注释、涂黑、表单数据)的新 PDF

配置:

<template>
  <PDFViewer
    :config="{
      export: {
        defaultFileName: 'exported-document.pdf'
      }
    }"
  />
</template>

触发下载:

<script setup lang="ts">
import { ref } from 'vue';
import { PDFViewer, type PluginRegistry } from '@embedpdf/vue-pdf-viewer';

const registry = ref<PluginRegistry | null>(null);

const handleReady = (r: PluginRegistry) => {
  registry.value = r;
};

const triggerDownload = () => {
  const exportPlugin = registry.value?.getPlugin('export')?.provides();

  // 下载活动文档
  exportPlugin?.download();

  // 下载指定文档
  // exportPlugin?.forDocument(documentId).download();
};
</script>

<template>
  <PDFViewer @ready="handleReady" :config="{ src: '/doc.pdf' }" />
</template>

保存到服务器:

<script setup lang="ts">
import { ref } from 'vue';
import { PDFViewer, type PluginRegistry } from '@embedpdf/vue-pdf-viewer';

const registry = ref<PluginRegistry | null>(null);

const handleReady = (r: PluginRegistry) => {
  registry.value = r;
};

const handleSave = async () => {
  const exportPlugin = registry.value?.getPlugin('export')?.provides();

  // 1. 获取 PDF 数据为 ArrayBuffer
  const arrayBuffer = await exportPlugin?.saveAsCopy().toPromise();
  if (!arrayBuffer) return;

  // 2. 转换为 Blob/File
  const blob = new Blob([arrayBuffer], { type: 'application/pdf' });
  const file = new File([blob], 'updated-doc.pdf');

  // 3. 上传到服务器
  const formData = new FormData();
  formData.append('file', file);
  formData.append('id', '12345');

  await fetch('/api/documents/save', {
    method: 'POST',
    body: formData
  });

  console.log('Document saved successfully!');
};
</script>

<template>
  <PDFViewer @ready="handleReady" :config="{ src: '/doc.pdf' }" />
</template>

6.8 国际化(i18n)

内置语言: en(默认)、nldefreszh-CNzh-TWjasvpt-BR

配置:

<template>
  <PDFViewer
    :config="{
      i18n: {
        defaultLocale: 'de',
        fallbackLocale: 'en'
      }
    }"
  />
</template>

运行时切换语言:

<script setup lang="ts">
import { ref } from 'vue';
import { PDFViewer, type PluginRegistry } from '@embedpdf/vue-pdf-viewer';

const registry = ref<PluginRegistry | null>(null);

const handleReady = (r: PluginRegistry) => {
  registry.value = r;
};

const changeLanguage = (locale: string) => {
  const i18n = registry.value?.getPlugin('i18n')?.provides();
  i18n?.setLocale(locale);
};
</script>

<template>
  <button @click="changeLanguage('de')">German</button>
  <button @click="changeLanguage('fr')">French</button>
  <button @click="changeLanguage('en')">English</button>

  <PDFViewer @ready="handleReady" :config="{ src: '/doc.pdf' }" />
</template>

监听语言变化:

<script setup lang="ts">
import { PDFViewer, type PluginRegistry } from '@embedpdf/vue-pdf-viewer';

const handleReady = (registry: PluginRegistry) => {
  const i18n = registry.getPlugin('i18n')?.provides();

  i18n?.onLocaleChange(({ previousLocale, currentLocale }) => {
    console.log(`Language changed from ${previousLocale} to ${currentLocale}`);
  });
};
</script>

<template>
  <PDFViewer @ready="handleReady" :config="{ src: '/doc.pdf' }" />
</template>

自定义翻译:

<script setup lang="ts">
import { PDFViewer } from '@embedpdf/vue-pdf-viewer';

const customSpanish = {
  code: 'es',
  name: 'Español',
  translations: {
    zoom: {
      in: 'Acercar',
      out: 'Alejar',
    },
    document: {
      open: 'Abrir Documento',
    }
  }
};
</script>

<template>
  <PDFViewer
    :config="{
      i18n: {
        defaultLocale: 'es',
        locales: [customSpanish]
      }
    }"
  />
</template>

七、Headless Composables

7.1 核心理念

  • 你控制 UI:只提供逻辑和渲染原语,零样式
  • Composable 设计:通过组合 <RenderLayer /><Scroller />useSearchuseSelection 等构建
  • 最小包体积:按需导入插件

7.2 安装

# npm
npm install @embedpdf/core @embedpdf/engines @embedpdf/plugin-document-manager @embedpdf/plugin-viewport @embedpdf/plugin-scroll @embedpdf/plugin-render

# pnpm
pnpm add @embedpdf/core @embedpdf/engines @embedpdf/plugin-document-manager @embedpdf/plugin-viewport @embedpdf/plugin-scroll @embedpdf/plugin-render

# yarn
yarn add @embedpdf/core @embedpdf/engines @embedpdf/plugin-document-manager @embedpdf/plugin-viewport @embedpdf/plugin-scroll @embedpdf/plugin-render

# bun
bun add @embedpdf/core @embedpdf/engines @embedpdf/plugin-document-manager @embedpdf/plugin-viewport @embedpdf/plugin-scroll @embedpdf/plugin-render

7.3 最小示例

<!-- PDFViewer.vue -->
<script setup lang="ts">
import { usePdfiumEngine } from '@embedpdf/engines/vue';
import { EmbedPDF } from '@embedpdf/core/vue';
import { createPluginRegistration } from '@embedpdf/core';

// 导入必要的插件
import { ViewportPluginPackage, Viewport } from '@embedpdf/plugin-viewport/vue';
import { ScrollPluginPackage, Scroller } from '@embedpdf/plugin-scroll/vue';
import {
  DocumentContent,
  DocumentManagerPluginPackage,
} from '@embedpdf/plugin-document-manager/vue';
import { RenderLayer, RenderPluginPackage } from '@embedpdf/plugin-render/vue';

// 1. 使用 Vue composable 初始化引擎
const { engine, isLoading } = usePdfiumEngine();

// 2. 注册所需的插件
const plugins = [
  createPluginRegistration(DocumentManagerPluginPackage, {
    initialDocuments: [{ url: 'https://snippet.embedpdf.com/ebook.pdf' }],
  }),
  createPluginRegistration(ViewportPluginPackage),
  createPluginRegistration(ScrollPluginPackage),
  createPluginRegistration(RenderPluginPackage),
];
</script>

<template>
  <div v-if="isLoading || !engine" class="loading-pane">
    Loading PDF Engine...
  </div>

  <!-- 3. 用 <EmbedPDF> provider 包裹你的 UI -->
  <div v-else style="height: 500px">
    <EmbedPDF :engine="engine" :plugins="plugins" v-slot="{ activeDocumentId }">
      <DocumentContent
        v-if="activeDocumentId"
        :document-id="activeDocumentId"
        v-slot="{ isLoaded }"
      >
        <Viewport
          v-if="isLoaded"
          :document-id="activeDocumentId"
          style="background-color: #f1f3f5"
        >
          <Scroller :document-id="activeDocumentId">
            <template #default="{ page }">
              <div :style="{ width: page.width + 'px', height: page.height + 'px' }">
                <!-- RenderLayer 负责绘制页面 -->
                <RenderLayer
                  :document-id="activeDocumentId"
                  :page-index="page.pageIndex"
                />
              </div>
            </template>
          </Scroller>
        </Viewport>
      </DocumentContent>
    </EmbedPDF>
  </div>
</template>

<style scoped>
.loading-pane {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
}
</style>

在主应用中使用:

<!-- App.vue -->
<script setup lang="ts">
import PDFViewer from './PDFViewer.vue';
</script>

<template>
  <PDFViewer />
</template>

7.4 下一步

了解更多插件功能,参考 Understanding Plugins


八、完整 API 参考

PDFViewerConfig.permissions

属性类型默认值说明
enforceDocumentPermissionsbooleantruefalse 时忽略 PDF 内部权限标志
overridesobjectundefined权限名称到布尔值的映射(true = 允许,false = 拒绝)

PDFViewerConfig 全量

属性类型默认值说明
srcstringPDF URL 或路径
themeobject主题配置(见主题系统)
tabBar'always' | 'multiple' | 'never''multiple'标签栏策略
disabledCategoriesstring[][]禁用功能分类
i18nobject国际化配置
annotationsobject注释配置
panobject手型工具配置
zoomobject缩放配置
spreadobject双页布局配置
scrollobject滚动配置
documentManagerobject文档管理配置
permissionsobject权限配置
exportobject导出配置

本内容由 Coze AI 生成,请遵循相关法律法规及《人工智能生成合成内容标识办法》使用与传播。