npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@embedpdf-editor/vue3-chapter-viewer

v1.4.5

Published

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

Downloads

288

Readme

@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.aliasoptimizeDeps.include,不要直接覆盖现有配置。

快速开始

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

<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 | ChapterPdfVieweroptionsmanifestnotesbookmarksfeatures 等 | | ChapterViewerConfig | options.features:划线样式、图标、缩放、选区工具栏 | | ChapterPdfViewer props | class-namebuild-selection-menu@extra-selection-action 等 |

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


ChapterViewerOptions

| 字段 | 必填 | 说明 | | --- | --- | --- | | manifest | 是 | ChapterManifest可响应式更新,见 ChapterPdfViewer 一节) | | notes | 是 | NoteCallbacks(须 onCreateNoteonRequestCreateNote) | | 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;
};

globalPageRangelocalPageRange 都是闭区间,且页数必须一致。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 loadNotesonCreateNote | onRequestCreateNote(二选一)、onRequestEditNoteonUpdateNoteonRequestDeleteNotePromise<boolean>,true 才真删)、onDeleteSuccess。创建时直接给 record(含 positionselectedText,可 JSON 直存);complete({ noteId, content?, nodeId? })。编辑/删除回调为 { record, nodeId? }

ParagraphBookmarkCallbacks loadonAdded{ bookmark, record, position })、persistonRequestRemove{ record, nodeId? },返回 true 才删除)、onRemoveSuccess

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

  • 笔记complete({ noteId, nodeId }) 或事先写 record.nodeIdonCreateNote 也可返回 { 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 现在额外带 localPageIndexglobalPageIndexglobalPageNumber,便于业务直接落库和外部定位。外部滚动可直接用: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 包一致,推荐 三步 chapterPdfLoaderloadChapterUrls → 可选 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.anchorchapterIdlocalPageIndex 与单 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,把渲染器内存里的对应标记移除。这里不会再触发 onRequestDeleteNoteonRequestRemovefeatures.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 | enabledstylescolor/thickness/offsetY/opacity)、annotationMenuenabledrenderMenu)、onClick(与 annotationMenu 互斥) | | bookmarks | enabledmarkerrenderIconiconSizeonClick)、hoverrenderAddIconiconSize) | | notes | enabledmarkerrenderIconrenderMenuActionsiconSizeoffsetX/offsetYhighlightColorhighlightStylealwaysVisibleonClickselectedOutline) | | zoom | 见下方 features.zoom | | scrollViewport | background(默认 #f1f5f9),滚动视口背景 | | page | background(默认 #ffffff),单页 PDF 画布背景;运行时 ref.setPdfPageBackground(color) | | selectionToolbar | enabledselectionBackground(划词底色)、hiddenBuiltinActions(含 copy)、renderCopyIcononCopySuccess(复制成功回调,配置后不显示默认「已复制」提示)、dismissToolbarOnAction{ copy?, note? },默认 true)、dismissToolbarOnclick / scroll,默认 ['click'])、extraActions | | selectionHandles | enabled(默认 true)、color(默认 #2196f3)、size(圆球直径 px,默认 16);移动端长按选词后的可拖动选区手柄,仅触摸设备生效 | | pageOverlays | enableddefaultBoxStyle(任意 CSS)、renderContentonClickctx.payload) | | boxSelect | enabled(默认 false,PC)、defaultBoxStylerenderBox(框内 UI,禁缩放)、renderMenu(浮窗容器);见下方用户自框选 |

默认划线 offsetYsquiggly4。内置工具条顺序: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 README08-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 } | | refChapterPdfViewerExpose) | — | scrollToChapterloadChapterAnnotations(chapterId)isChapterAnnotationsLoadedsetPdfPageBackgroundgetPdfPageBackground;框选:setBoxSelectEnabled/toggleBoxSelect/isBoxSelectEnabled/setBoxSelects/mergeBoxSelects/listBoxSelects/scrollToBoxSelect/scrollToBoxSelectRecord/removeBoxSelect | | @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>

保持 optionsref 即可;不要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.selectionToolbar07-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:阅读器仍负责浮窗定位,业务只拿 payloadactions 渲染内容。返回 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 热点、自定义浮层。数据可通过 importChapterAnnotationspageOverlays 字段注入。

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,含 payloadcontent.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' |

<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
      renderMenu: ({ info, getInfo, deleteSelf, dismissMenu, menuWrapperProps, menuBarStyle }: BoxSelectMenuContext) =>
      h('div', menuWrapperProps, [
        h('div', { style: { ...menuBarStyle, display: 'flex', gap: '8px' } }, [
          h('button', { onClick: () => saveToBackend(getInfo?.() ?? info) }, '保存'),
          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
  • 事件:@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.md12-segmented-pdf-and-per-chapter-storage.mdReact README 标注章节

教程索引

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

常见问题

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