@embedpdf-editor/vue3-chapter-viewer

Vue 3 版章节 PDF 阅读器。它把多份章节 PDF 组织成一本连续滚动的书,并内置划线/高亮、附注、段落书签、章节目录和标注导入导出能力。
23,883
Install
npm install @embedpdf-editor/vue3-chapter-viewer
Latest Version:1.4.8
Vue:3
License:MIT
Last Updated:Oct 19, 2026
Links: npm
Maintainer: xiaoliuing

@embedpdf-editor/vue3-chapter-viewer

Vue 3 版章节 PDF 阅读器。它把多份章节 PDF 组织成一本连续滚动的书,并内置划线/高亮、附注、段落书签、章节目录和标注导入导出能力。

底层使用 @embedpdf/core 插件体系和 PDFium 引擎;Vue 3 包只负责提供 SFC 组件、composition hooks 和类型导出。

安装

pnpm add @embedpdf-editor/vue3-chapter-viewer

Vite

包内导出的是一段 Vite 配置,不是 Vite plugin。它用于补齐 scheduler alias 和 PDFium 引擎的预构建配置;@embedpdf/engines 已经是本包依赖,业务项目不需要单独安装。

// vite.config.ts
import { defineConfig, mergeConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { chapterViewerViteResolve } from '@embedpdf-editor/vue3-chapter-viewer/vite';

export default mergeConfig(
  defineConfig({
    plugins: [vue()],
  }),
  chapterViewerViteResolve(),
);

如果不用 mergeConfig,需要手动合并 resolve.alias 和 optimizeDeps.include,不要直接覆盖现有配置。

快速开始

推荐只传 options。editorOptions 和 features 仍兼容,但已作为低层/旧写法保留。

<script setup lang="ts">
import {
  ChapterPdfViewer,
  usePdfiumEngine,
  type ChapterViewerOptions,
} from '@embedpdf-editor/vue3-chapter-viewer';

const { engine, isLoading, error } = usePdfiumEngine();

const options: ChapterViewerOptions = {
  manifest: {
    chapters: [
      {
        chapterId: 'ch-1',
        title: '第一章',
        globalPageRange: [1, 12],
        localPageRange: [0, 11],
        source: { url: '/pdfs/chapter-1.pdf' },
      },
    ],
  },
  notes: {
    loadNotes: async () => [],
    onCreateNote: async () => ({ noteId: crypto.randomUUID() }),
    onUpdateNote: async () => {},
    // 两阶段删除:业务先确认外部数据,返回 true 才让插件擦 PDF 笔记
    onRequestDeleteNote: async () => true,
  },
  bookmarks: {
    load: async () => [],
    persist: async () => {},
    onRequestRemove: async () => true,
  },
  features: {
    markup: true,
    bookmarks: true,
    notes: true,
    selectionToolbar: true,
    zoom: { pageWidth: 720 },
  },
};
</script>

<template>
  <div class="reader">
    <div v-if="error">PDF engine failed: {{ error.message }}</div>
    <div v-else-if="isLoading || !engine">Loading PDF engine...</div>
    <ChapterPdfViewer
      v-else
      :engine="engine"
      :options="options"
      class-name="reader-fill"
      viewport-class-name="reader-viewport"
    />
  </div>
</template>

<style scoped>
.reader {
  height: 100vh;
  min-height: 0;
}

.reader-fill {
  height: 100%;
  min-height: 0;
}
</style>

配置总览

层级 说明
ChapterViewerOptions ChapterPdfViewer 的 options:manifest、notes、bookmarks、features 等
ChapterViewerConfig options.features:划线样式、图标、缩放、选区工具栏
ChapterPdfViewer props class-name、build-selection-menu、@extra-selection-action 等

省略 features 时默认全部开启。字段说明与 React 包 README 一致,下文为 Vue 3 用法示例。


ChapterViewerOptions

字段 必填 说明
manifest 是 ChapterManifest(可响应式更新,见 ChapterPdfViewer 一节)
notes 是 NoteCallbacks(须 onCreateNote 或 onRequestCreateNote)
bookmarks 是 ParagraphBookmarkCallbacks(删除须 onRequestRemove 返回 true)
chapterPdfLoader 否 全局 IChapterPdfLoader
overlapStrategy 否 默认 { kind: 'first-wins' }
features 否 见 features 配置

Manifest

type ChapterManifest = {
  chapters: Array<{
    chapterId: string;
    title: string;
    globalPageRange: [number, number];
    localPageRange: [number, number];
    source?:
      | { url: string }
      | { buffer: ArrayBuffer | Uint8Array | DataView }
      | {
          load: () => Promise<
            { url: string } | { buffer: ArrayBuffer | Uint8Array | DataView }
          >;
        }
      | { urls: string[]; segmentPageThreshold: number }; // legacy
    segmentPageThreshold?: number;
    encrypted?: boolean;
    ownedGlobalPages?: number[];
  }>;
  totalGlobalPages?: number;
};

globalPageRange 和 localPageRange 都是闭区间,且页数必须一致。chapterId 在整本书内唯一;单 URL 章时 documentId === chapterId,分段章内部为 chapterId#s0、#s1…(业务存储仍只用 chapterId)。

字段 说明
chapterId / title 唯一 ID 与标题
globalPageRange / localPageRange 全局与 PDF 内闭区间,页数须一致
source url | buffer | load(),见下文
encrypted 可选标记;密码流见 加密 PDF
ownedGlobalPages overlapStrategy: explicit 时使用

notes / bookmarks 回调

NoteCallbacks: loadNotes、onCreateNote | onRequestCreateNote(二选一)、onRequestEditNote、onUpdateNote、onRequestDeleteNote(Promise<boolean>,true 才真删)、onDeleteSuccess。创建时直接给 record(含 position、selectedText,可 JSON 直存);complete({ noteId, content?, nodeId? })。编辑/删除回调为 { record, nodeId? }。

ParagraphBookmarkCallbacks: load、onAdded({ bookmark, record, position })、persist、onRequestRemove({ record, nodeId? },返回 true 才删除)、onRemoveSuccess。

nodeId:业务库主键,不参与渲染。创建时可写入 record.nodeId 或回调返回值,删除回调会带上:

  • 笔记:complete({ noteId, nodeId }) 或事先写 record.nodeId;onCreateNote 也可返回 { noteId, nodeId? }
  • 划线/高亮:onCreated 内写 record.nodeId = … 或 return { nodeId }(支持 async)
  • 书签:onAdded 内写 record.nodeId = … 或 return { nodeId }(支持 async)
  • 导入时写在 NoteAnchor / ParagraphBookmark / markup[].nodeId 上;仍可用 setMarkupAnnotationNodeId 手动登记

features.markup 单条事件:

  • onCreated:划词后 { kind, record, position, selectedText, ... }
  • onRequestDelete / onDeleted:{ record, nodeId? };返回 true 才真删

record 里的 markup 现在额外带 localPageIndex、globalPageIndex、globalPageNumber,便于业务直接落库和外部定位。外部滚动可直接用:scrollToMarkupRecord(viewerRef.value, markupRecord)、scrollToNoteRecord(viewerRef.value, noteRecord)、scrollToBookmarkRecord(viewerRef.value, bookmarkRecord)、scrollToPageOverlayRecord(viewerRef.value, overlayRecord)、scrollToBackendPageOverlayItem(viewerRef.value, backendItem)(内部两步精确定位,与 QR overlay 一致)。

章节级全量持久化用 subscribeChapterMarkupChanges(registry, listener)(每次变更后推该章 markup 快照),与上面单条事件互补。

事件全集:11-events-callbacks-and-component-api.md

嵌套目录树与重叠页

侧栏目录支持任意层级(ChapterTreePanel + ChapterTreeNode.children)。引擎滚动用的 manifest.chapters 是扁平列表:父节点页范围可包含子节点,相邻/重叠全局页由 owner 策略 决定只渲染一个章节的 PDF,避免同页画两遍。

import {
  buildChapterViewerCatalog,
  overlapStrategyForSamePageOwner,
  type ChapterViewerOptions,
} from '@embedpdf-editor/vue3-chapter-viewer';

// 从业务树生成 tree + manifest(深度优先 flatten,顺序影响重叠页归属)
const { tree, manifest } = buildChapterViewerCatalog([
  { chapterId: 'part-1', title: '第一篇', startPage: 1, endPage: 50, source: { url: '/p1.pdf' } },
  {
    chapterId: 'sec-1',
    title: '1.1 节',
    startPage: 5,
    endPage: 20,
    source: { url: '/s1.pdf' },
    children: [/* ... */],
  },
]);

const options: ChapterViewerOptions = {
  manifest,
  overlapStrategy: overlapStrategyForSamePageOwner('last'), // 重叠页用 manifest 中「后出现」的章节
  // overlapStrategy: { kind: 'first-wins' }, // 默认:先出现的章节
  notes: { /* ... */ },
  bookmarks: { /* ... */ },
};
overlapStrategy 同一全局页多个章节条目时
{ kind: 'first-wins' }(默认) 使用 manifest 中先出现 的章节的 PDF
{ kind: 'last-wins' } 使用 后出现 的章节(目录 flatten 后,重叠页常以「当前页最后一节」为准)
{ kind: 'explicit' } 各章在 ownedGlobalPages 上声明归属页
{ kind: 'custom', resolve } 完全自定义

简写:overlapStrategyForSamePageOwner('first' | 'last')。

注意:仅作分组、没有独立 PDF 的父节点不要放进 flatten 后的 manifest;只保留需要加载 PDF 的节点。Demo:examples/chapter-viewer-demo-vue3/src/demo/load-catalog.ts。

PDF 加载与预处理

与 React 包一致,推荐 三步 chapterPdfLoader:loadChapterUrls → 可选 openPdf。
loadChapterUrls 每章只请求一次(引擎缓存);PDF 按段各打开一次。

import type { ChapterViewerOptions, ChapterPdfLoadContext } from '@embedpdf-editor/vue3-chapter-viewer';

const options: ChapterViewerOptions = {
  manifest: {
    chapters: [
      {
        chapterId: 'ch-1',
        title: '第一章',
        globalPageRange: [1, 13],
        localPageRange: [0, 12],
        segmentPageThreshold: 5,
      },
    ],
  },
  chapterPdfLoader: {
    async loadChapterUrls(chapter) {
      const res = await getOneChap(chapter.chapterId);
      const raw = res.data.resourceUrl;
      return Array.isArray(raw) ? raw : raw ? [raw] : [];
    },
    async openPdf(ctx: ChapterPdfLoadContext) {
      return { url: ctx.url };
    },
  },
  notes: { /* ... */ },
  bookmarks: { /* ... */ },
};

静态单文件仍可用 source.url / load()。兼容 loadPdf(chapter, segmentIndex)。

03-manifest.md · 12-segmented-pdf-and-per-chapter-storage.md

按章持久化(笔记 / 书签)

NoteAnchor / ParagraphBookmark.anchor 的 chapterId、localPageIndex 与单 URL 章相同。全书一次加载用 loadNotes / bookmarks.load;按章按需见下节。划线备份用 exportChapterAnnotations(归档键为 chapterId)。

按章按需加载标注(笔记 / 书签 / 划线)

配置 options.annotations.loadChapterAnnotations 后,滚动到焦点章会自动拉取并 importChapterAnnotations;也可通过 ref.loadChapterAnnotations(chapterId) 手动触发(ref 仍只传章节 ID)。回调入参为 { chapterId, chapter },chapter 为 manifest 中的 ChapterDescriptor。

const options: ChapterViewerOptions = {
  manifest: { chapters: [...] },
  // 勿再配置 notes.loadNotes / bookmarks.load 一次拉全书
  notes: { onCreateNote: async () => ({ noteId: crypto.randomUUID() }), /* ... */ },
  bookmarks: { persist: async () => {}, onRequestRemove: async () => true },
  annotations: {
    async loadChapterAnnotations({ chapterId, chapter }) {
      const res = await fetch(`/api/chapters/${chapterId}/annotations`).then((r) => r.json());
      // chapter.title / chapter.globalPageRange / chapter.segmentPageThreshold …
      return res; // { notes?, bookmarks?, markup?, pageOverlays?, boxSelects? } 或 null
    },
    importOptions: { mode: 'replace' },
    autoLoadOnActiveChapter: true, // 默认 true
  },
};

事件:@chapter-annotations-loading、@chapter-annotations-loaded、@chapter-annotations-error。

外部删除后同步渲染器

如果业务侧在外部列表里删除了笔记 / 书签 / 划线高亮,删除接口成功后再调用 ChapterPdfViewer ref,把渲染器内存里的对应标记移除。这里不会再触发 onRequestDeleteNote、onRequestRemove 或 features.markup.onRequestDelete,避免二次请求后端。

<script setup lang="ts">
import { useTemplateRef } from 'vue';
import {
  ChapterPdfViewer,
  type ChapterPdfViewerExpose,
  type SerializableAnnotationTransferItem,
} from '@embedpdf-editor/vue3-chapter-viewer';

const viewerRef = useTemplateRef<ChapterPdfViewerExpose>('viewer');

async function deleteNote(noteId: string) {
  await api.deleteNote(noteId);
  viewerRef.value?.removeNote(noteId);
}

async function deleteBookmark(bookmarkId: string) {
  await api.deleteBookmark(bookmarkId);
  viewerRef.value?.removeBookmark(bookmarkId);
}

async function deleteMarkup(record: SerializableAnnotationTransferItem) {
  await api.deleteMarkup(record.nodeId ?? record.annotation.id);
  await viewerRef.value?.removeMarkupAnnotation(record);
}
</script>

<template>
  <ChapterPdfViewer ref="viewer" :engine="engine" :options="options" />
</template>

加密 PDF 与 passwordProvider

import { CallbackPasswordProvider } from '@embedpdf-editor/vue3-chapter-viewer';

// 在 createPdfChapterEditor / 自定义 bundle 中传入
const passwordProvider = new CallbackPasswordProvider(async (chapter, attempt) =>
  askPassword(chapter.chapterId, attempt),
);

features 配置

ChapterViewerConfig:每项可 true | false | 对象。

模块 主要字段
markup enabled、styles(color/thickness/offsetY/opacity)、annotationMenu(enabled、renderMenu)、onClick(与 annotationMenu 互斥)
bookmarks enabled、marker(renderIcon、iconSize、onClick)、hover(renderAddIcon、iconSize)
notes enabled、marker(renderIcon、renderMenuActions、iconSize、offsetX/offsetY、highlightColor、highlightStyle、alwaysVisible、onClick、selectedOutline)
zoom 见下方 features.zoom
scrollViewport background(默认 #f1f5f9),滚动视口背景
page background(默认 #ffffff),单页 PDF 画布背景;运行时 ref.setPdfPageBackground(color)
selectionToolbar enabled、selectionBackground(划词底色)、hiddenBuiltinActions(含 copy)、renderCopyIcon、onCopySuccess(复制成功回调,配置后不显示默认「已复制」提示)、dismissToolbarOnAction({ copy?, note? },默认 true)、dismissToolbarOn(click / scroll,默认 ['click'])、extraActions
selectionHandles enabled(默认 true)、color(默认 #2196f3)、size(圆球直径 px,默认 16);移动端长按选词后的可拖动选区手柄,仅触摸设备生效
pageOverlays enabled、defaultBoxStyle(任意 CSS)、renderContent、onClick(ctx.payload)
boxSelect enabled(默认 false,PC)、defaultBoxStyle、renderBox(框内 UI,禁缩放)、renderMenu(浮窗容器);见下方用户自框选

默认划线 offsetY:squiggly 为 4。内置工具条顺序:copy → 划线类 → 扩展 → note。

pageOverlays 详见 13-page-overlays.md。

features.zoom

字段 默认 说明
enabled true Ctrl+滚轮 / 双指捏合
min / max 0.5 / 3 scale 范围
initial 1 初始 scale(与 fitParentWidth / pageWidth 互斥)
pageWidth — 版心 CSS 宽度
fitParentWidth false 初始占满滚动视口;勿与 initial 同配
clampMaxToViewport true false 时可放大超出视口并横向滑动
pinchExponent 1 捏合灵敏度(默认 1:1)

锚点缩放:滚轮 / 捏合以指针或两指中心为锚点,画面不漂移。

<script setup lang="ts">
const isMobile =
  typeof window !== 'undefined' && window.matchMedia('(max-width: 768px)').matches;

const zoom = isMobile
  ? { fitParentWidth: true, clampMaxToViewport: false, max: 8, pinchExponent: 1, enabled: true }
  : { pageWidth: 800, max: 3, enabled: true };
</script>

<template>
  <ChapterPdfViewer :engine="engine" :options="{ manifest, features: { zoom } }" />
</template>

详版字段与优先级见 React README 与 08-zoom-scroll-viewport.md。


ChapterPdfViewer 组件

Prop / 事件 默认 说明
engine / options — 必填;options.manifest 可响应式更新(内部 setManifest)
class-name / viewport-class-name — 布局
@initialized / :on-initialized — 插件就绪,拿到 registry
@active-chapter-change — 滚动焦点变化:{ chapterId, globalPageIndex, globalPageNumber, localPageIndex }
@chapter-annotations-loading / loaded / error — 按需标注加载生命周期(须配置 options.annotations)
@pdf-page-background-change — PDF 页背景色变更:{ background }
ref(ChapterPdfViewerExpose) — scrollToChapter、loadChapterAnnotations(chapterId)、isChapterAnnotationsLoaded、setPdfPageBackground、getPdfPageBackground;框选:setBoxSelectEnabled/toggleBoxSelect/isBoxSelectEnabled/setBoxSelects/mergeBoxSelects/listBoxSelects/scrollToBoxSelect/scrollToBoxSelectRecord/removeBoxSelect/setBoxSelectClickSize
@box-select-start / @box-select-change / @box-select-end — 框选事件(features.boxSelect);payload { record, info }
build-selection-menu — 包装划词菜单
annotation-selection-menu — 点击已有划线标注的菜单
redaction-selection-menu — Redaction 层菜单
show-note-markers true 笔记角标
show-bookmark-markers true 书签角标
show-redaction-layer false 脱敏层
@extra-selection-action — 扩展工具条
render-page-overlay — 每页叠加层
render-selection-toolbar — 完全自定义划词浮窗内容;阅读器提供定位容器、JSON payload 与 actions
#prepend — 视口上方插槽
#default — 视口内插槽
#loading — 插件未 ready 时

动态换章

<script setup lang="ts">
const options = ref<ChapterViewerOptions>({
  manifest: { chapters: [] },
  notes: { /* ... */ },
  bookmarks: { /* ... */ },
});

async function reload() {
  options.value.manifest.chapters = await fetchChapters();
}
</script>

<template>
  <ChapterPdfViewer :engine="engine" :options="options" @initialized="onReady" />
</template>

保持 options 为 ref 即可;不要用 computed(() => createChapterViewerBundle(options)) 随 manifest 重建 plugins。裸 EmbedPDF 时用 useSyncChapterManifest。


Worker 与 WASM

usePdfiumEngine() 默认使用 @embedpdf/engines 内置的 PDFium CDN wasm 地址,并默认启用 worker:

usePdfiumEngine();

如果业务要求内网部署、固定版本或自定义 CDN,再传入自托管地址(完整 URL 或站点相对路径):

usePdfiumEngine({ wasmUrl: '/assets/pdfium.wasm', worker: true });

// 自有 OSS / CDN(示例)
usePdfiumEngine({
  wasmUrl:
    'https://hep-editor.oss-cn-beijing.aliyuncs.com/public/editor-public/js/pdfium.wasm',
  worker: true,
});

详见 docs/get-started/01-installation.md。

使用 worker: true 时,部署环境需要允许 worker 加载 wasm。若只在 worker 模式失败,可先用 worker: false 定位资源或响应头问题。

字体 fallback CDN

PDF 缺少内嵌字体时,PDFium 会按需请求 fallback 字体。默认使用 jsDelivr;如果字体加载慢,可锁定版本或改成业务 CDN/OSS:

import {
  FontCharset,
  createCdnFontConfig,
  usePdfiumEngine,
} from '@embedpdf-editor/vue3-chapter-viewer';

// 锁定 @embedpdf/fonts-* 版本,避免 latest 解析抖动
usePdfiumEngine({ fontFallback: createCdnFontConfig('1.0.0') });

// 完全自定义简体中文字体地址
usePdfiumEngine({
  fontFallback: {
    fonts: {
      [FontCharset.GB2312]: [
        {
          url: 'https://cdn.jsdelivr.net/npm/@embedpdf/fonts-sc@latest/fonts/NotoSansHans-Regular.otf',
          weight: 400,
        },
      ],
    },
  },
});

内置字体 URL 完整清单、FontCharset 对应关系,以及“下载后上传到自有服务器再替换 URL”的完整配置见 docs/get-started/01-installation.md。

features 配置示例

完整字段表见 React README 的 features 配置。

划线颜色、粗细与位置

features: {
  markup: {
    styles: {
      highlight: { color: '#fef08a', opacity: 0.45 },
      underline: { color: '#dc2626', thickness: 1.5, offsetY: 2.5 },
      squiggly: { color: '#dc2626', thickness: 1.5, offsetY: 4 },
      strikeout: { color: '#64748b', offsetY: 0 },
    },
  },
},

offsetY 为 PDF 点、正值向下;波浪线默认 4,减轻盖住字形。markup: false 关闭划线。

点击已有划线:删除

选中高亮/下划线/波浪线/删除线后,默认在标注下方显示「删除」。可自定义:

import { h } from 'vue';

features: {
  markup: {
    annotationMenu: {
      renderMenu: ({ onDelete }) =>
        h('button', { type: 'button', onClick: onDelete }, '移除'),
    },
  },
},

PdfChapterViewport 上传 annotation-selection-menu 时宿主优先,未渲染时再回退默认删除菜单。

笔记、书签图标

import { h } from 'vue';

features: {
  notes: {
    marker: {
      renderIcon: () => h('img', { src: '/icons/note.svg', width: 20, height: 20 }),
      iconSize: 22,
      // 始终显示图标(默认 false:仅 hover 高亮时显示);
      // true 时 hover 感知区只覆盖图标本身,不影响该段划词。
      alwaysVisible: true,
      renderMenuActions: ({ onEdit, onDelete }) =>
        h('div', [
          h('button', { onClick: onEdit }, '编辑'),
          h('button', { onClick: onDelete }, '删除'),
        ]),
    },
  },
  bookmarks: {
    marker: { renderIcon: () => h('img', { src: '/icons/bookmark.svg' }) },
    hover: { renderAddIcon: () => h('span', '+') },
  },
},

选区工具栏

复制(默认开启,浮窗最左侧)
import { h } from 'vue';

features: {
  selectionToolbar: {
    selectionBackground: 'rgba(255, 193, 7, 0.35)',
    // hiddenBuiltinActions: ['copy'],
    renderCopyIcon: () =>
      h('span', { 'aria-hidden': true, style: { fontSize: '18px' } }, '📋'),
  },
},
import { copyTextToClipboard } from '@embedpdf-editor/vue3-chapter-viewer';

await copyTextToClipboard('文本');
扩展按钮(含划选文本)
features: {
  selectionToolbar: {
    markupColors: ['#93c5fd', '#fde047', '#fca5a5'],
    extraActions: [
      {
        id: 'translate',
        label: '翻译',
        onClick: ({ selectedText, chapterId, localPageIndex }) => {
          void openTranslate(selectedText, chapterId, localPageIndex);
        },
      },
    ],
  },
},

也可在 ChapterPdfViewer 上用 @extra-selection-action。回调含 selectedText。
更多字段见 React 包 features.selectionToolbar、07-selection-toolbar。

完全自定义浮窗内容

移动端交互开箱即用,无需配置:

  • 单指滑动 = 滚动:任何单指拖动都只滚动 PDF,绝不触发划词(严格)。
  • 长按 = 选词:手指按住某处文字停留约 450ms(其间不移动)即选中所在词并弹出浮窗。
  • 原生风格选区手柄:长按选词后,选区首/尾各出现一个可拖动的圆球手柄,拖动即可扩大 / 收缩选中内容(接近 iOS / Android 原生)。仅触摸设备显示。
  • 仅在阅读模式生效;进入标注 / 绘制等模式时不拦截,双指缩放与桌面鼠标行为均不受影响。

选区手柄可通过 features.selectionHandles 配置(默认开启):

features: {
  selectionHandles: {
    // enabled: false,   // 关闭手柄
    color: '#2196f3',    // 圆球 + 竖线颜色(默认与选区高亮同色)
    size: 16,            // 圆球直径 px
  },
}

自定义图标:手柄根节点带 class embedpdf-chapter-selection-handle(首/尾分别再附加 --start / --end),可用全局 CSS 覆写外观。完善支持单页选区内拖动;跨页 / 旋转页为已知限制。

需要完全自定义浮窗时传 render-selection-toolbar:阅读器仍负责浮窗定位,业务只拿 payload 和 actions 渲染内容。返回 null / undefined 时会自动回退内置浮窗(ctx.defaultMenu())。

<script setup lang="ts">
import { h } from 'vue';
import type { ChapterSelectionToolbarRenderer } from '@embedpdf-editor/vue3-chapter-viewer';

const renderSelectionToolbar: ChapterSelectionToolbarRenderer = ({ payload, actions }) =>
  h(
    'div',
    {
      style: {
        display: 'flex',
        gap: '8px',
        padding: '10px 12px',
        borderRadius: '12px',
        background: '#111827',
        color: '#fff',
      },
    },
    payload.actions.map((action) =>
      h(
        'button',
        {
          type: 'button',
          style: { color: 'inherit' },
          onClick: () => actions.run(action.id),
        },
        action.label,
      ),
    ),
  );
</script>

<template>
  <ChapterPdfViewer
    :engine="engine"
    :options="options"
    :render-selection-toolbar="renderSelectionToolbar"
  />
</template>

页内矩形叠加层 pageOverlays

按 PDF 坐标绘制矩形框,适用于后端 QR 热点、自定义浮层。数据可通过 importChapterAnnotations 的 pageOverlays 字段注入。

import { h } from 'vue';
import {
  importChapterAnnotations,
  qrTargetToPageOverlayRecord,
} from '@embedpdf-editor/vue3-chapter-viewer';

features: {
  pageOverlays: {
    defaultBoxStyle: {
      border: '2px dashed #ef4444',
      boxShadow: '0 0 0 1px rgba(239, 68, 68, 0.3)',
      cursor: 'pointer',
    },
    renderContent: ({ payload }) =>
      h('span', { style: { fontSize: '10px' } }, payload ?? 'QR'),
    onClick: ({ overlay, payload }) => {
      console.log('[点击矩形框]', overlay.overlayId, payload);
    },
  },
},
  • onClick / renderContent 参数为 PageOverlayRenderContext,含 payload(content.kind === 'qr' 或 metadata.payload)
  • defaultBoxStyle 支持任意 CSS,不限于边框简写
  • 后端 0~1 比例坐标用 qrTargetToPageOverlayRecord 转换

完整说明:13-page-overlays.md。Demo:examples/chapter-viewer-demo-vue3。

用户自框选 boxSelect(PC)

用户拖拽画矩形框,框可点击选中、拖动、缩放;选中即弹自定义浮窗。默认关闭,一页可多个框,数据落后端、按需绘制。坐标模型与 pageOverlays 同构。

字段 说明
enabled 初始开关,默认 false;运行时用 ref
defaultBoxStyle 框默认样式(复用 PageOverlayBoxStyle);单条 record.boxStyle 可覆盖
renderBox (ctx) => VNode,替换框内 UI;提供后该框禁用缩放,仍可拖动
renderMenu (ctx) => VNode;含 applySave(patch) 保存后回写自定义 id、getInfo/getRecord 等
menuAlign 'left' | 'center' | 'right',浮窗相对框底水平对齐,默认 'left'
clickSize 点击建框固定尺寸:框选模式下点击(未拖拽)以点击点为中心画该尺寸的框。归一化 { width, height, pageWidth?, pageHeight? },可直接传后端二维码识别结果;带 pageWidth/pageHeight 时换算为固定 pt,否则相对当前页。已有选中框时点击空白只收起菜单。运行时用 ref.setBoxSelectClickSize(size | null)
dragCreate 是否允许拖动拉出矩形,默认 true。设为 false 后拖动不出现选区,只保留点击建框。已有框仍可拖动、缩放
clickPad 默认 0.15。点击建出的框尺寸不变;info.capture 向四边外扩,识别/截图用 capture(x/y/width/height 仍是框本身)。clickSize.pad 优先。0 时截取区域与框重合。拖拽建框不外扩

renderMenu 的 restoreSize?.() 把框恢复到初始尺寸(保持当前中心点,触发 @box-select-end):初始尺寸为建框时尺寸(record.initialSize),后端加载的框回退到 clickSize;两者都没有时为 undefined,可据此隐藏「恢复」按钮。

<script setup lang="ts">
import { h, useTemplateRef } from 'vue';
import type { ChapterPdfViewerExpose, BoxSelectMenuContext } from '@embedpdf-editor/vue3-chapter-viewer';

const viewer = useTemplateRef<ChapterPdfViewerExpose>('viewer');

const features = {
    boxSelect: {
      enabled: false,
      menuAlign: 'center', // left | center | right
      // 点击即画二维码大小的框(后端识别结果直接传入)
      clickSize: { width: 0.0809, height: 0.0574, pageWidth: 530.56, pageHeight: 747.18 },
      renderMenu: ({ info, getInfo, deleteSelf, dismissMenu, restoreSize, menuWrapperProps, menuBarStyle }: BoxSelectMenuContext) =>
      h('div', menuWrapperProps, [
        h('div', { style: { ...menuBarStyle, display: 'flex', gap: '8px' } }, [
          h('button', { onClick: () => saveToBackend(getInfo?.() ?? info) }, '保存'),
          restoreSize ? h('button', { onClick: restoreSize }, '恢复') : null,
          h('button', { onClick: deleteSelf }, '删除'),
          h('button', { onClick: dismissMenu }, '关闭'),
        ]),
      ]),
  },
};

function toggle() { viewer.value?.toggleBoxSelect(); }
function draw() {
  viewer.value?.setBoxSelects('ch-1', [
    { id: 'b1', pageIndex: 1, x: 0.1, y: 0.2, width: 0.3, height: 0.15, pageWidth: 531.39, pageHeight: 746.85 },
  ]);
}
</script>

<template>
  <ChapterPdfViewer
    ref="viewer"
    :engine="engine"
    :options="{ manifest, features }"
    @box-select-end="({ info }) => console.log('新框/改框', info)"
  />
</template>
  • ref 方法:setBoxSelectEnabled / toggleBoxSelect / isBoxSelectEnabled / setBoxSelects / mergeBoxSelects / listBoxSelects / scrollToBoxSelect / scrollToBoxSelectRecord / removeBoxSelect / setBoxSelectClickSize(异步拿到二维码位置后设置点击建框尺寸;查看器未就绪时会缓存、就绪后自动生效。features 只在挂载时读取,异步数据请用此方法)
  • 事件:@box-select-start / @box-select-change / @box-select-end,payload { record, info }
  • info: BoxSelectRectInfo 含 1-based pageIndex + 归一化 x/y/width/height + 页宽高,可直接回后端

进阶组合

内置 ChapterPdfViewer 已完成插件注册和章节视口渲染。需要插入自定义 shell、目录、工具栏或直接访问 registry 时,可以使用低层组合:

<!-- 动态 manifest:优先用 ChapterPdfViewer :options="options" -->

<script setup lang="ts">
import {
  EmbedPDF,
  PdfChapterViewport,
  createChapterViewerBundle,
} from '@embedpdf-editor/vue3-chapter-viewer';

const { plugins, features } = createChapterViewerBundle(options);
</script>

<template>
  <EmbedPDF :engine="engine" :plugins="plugins" @initialized="onReady">
    <template #default="{ pluginsReady }">
      <PdfChapterViewport v-if="pluginsReady" :features="features" />
    </template>
  </EmbedPDF>
</template>

裸 EmbedPDF 且需动态换章时,在子组件内调用 useSyncChapterManifest(() => options.manifest)(不可在 EmbedPDF 外调用)。

createChapterViewerEditorOptions() 只生成旧式 editor 配置,不含 features;新代码优先 ChapterPdfViewer + 响应式 options。

标注导入导出

import {
  exportChapterAnnotations,
  exportAllChapterAnnotations,
  importChapterAnnotationsArchive,
  CHAPTER_ANNOTATIONS_ARCHIVE_VERSION,
  useRegistry,
} from '@embedpdf-editor/vue3-chapter-viewer';
选项 说明
mode replace | merge
bookmarks / notes / markup / pageOverlays / boxSelects 导入导出子集,默认 true
ensureChapterLoaded 默认 true;导出含 markup 的分段章会加载 全部段
persistNotes / persistBookmarks 导入后写回存储

归档按 chapterId 分桶。详见 10-annotations-io.md、12-segmented-pdf-and-per-chapter-storage.md、React README 标注章节。

教程索引

docs/get-started/README.md(含 wasmUrl、划词复制、事件全集 11、分段 12、矩形叠加层 13)

常见问题

现象 处理
一直停在引擎加载 如果传了自定义 wasmUrl,检查地址是否 200、MIME/跨源头是否正确
只显示空白 确认外层容器有高度,且章节 source.url 可访问
页码或滚动错位 检查 globalPageRange 与 localPageRange 页数是否一致
划词笔记没有保存 实现 notes.onCreateNote 或自定义 onRequestCreateNote 流程
书签删除无效 bookmarks.onRequestRemove 需要返回 true 才会删除
分段章业务 ID 混乱 对外只用 chapterId + localPageIndex,勿用 chapterId#sN

Version History

Version Released Vue License
1.4.8 ^3.3.0 MIT
1.4.7 ^3.3.0 MIT
1.4.6 ^3.3.0 MIT
1.4.5 ^3.3.0 MIT
1.4.4 ^3.3.0 MIT
Pre-releases (59)
Version Released Vue License
1.4.5-beta.7 ^3.3.0 MIT
1.4.5-beta.6 ^3.3.0 MIT
1.4.5-beta.5 ^3.3.0 MIT
1.4.5-beta.4 ^3.3.0 MIT
1.4.5-beta.3 ^3.3.0 MIT