@embedpdf-editor/vue3-chapter-viewer
v1.2.20
Published
Vue 3 版章节 PDF 阅读器。它把多份章节 PDF 组织成一本连续滚动的书,并内置划线/高亮、附注、段落书签、章节目录和标注导入导出能力。
Readme
@embedpdf-editor/vue3-chapter-viewer
Vue 3 版章节 PDF 阅读器。它把多份章节 PDF 组织成一本连续滚动的书,并内置划线/高亮、附注、段落书签、章节目录和标注导入导出能力。
底层使用 @embedpdf/core 插件体系和 PDFium 引擎;Vue 3 包只负责提供 SFC 组件、composition hooks 和类型导出。
安装
pnpm add @embedpdf-editor/vue3-chapter-viewerVite
包内导出的是一段 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)。
章节级全量持久化用 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? } 或 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 | enabled(默认 true)、min 0.5、max 3、initial 1、pageWidth;实际上限由 [data-chapter-scroll-viewport] 宽度决定,resize 时自动更新 |
| scrollViewport | background(默认 #f1f5f9),滚动视口背景 |
| page | background(默认 #ffffff),单页 PDF 画布背景;运行时 ref.setPdfPageBackground(color) |
| selectionToolbar | enabled、selectionBackground(划词底色)、hiddenBuiltinActions(含 copy)、renderCopyIcon、extraActions |
| pageOverlays | enabled、defaultBoxStyle(任意 CSS)、renderContent、onClick(ctx.payload) |
默认划线 offsetY:squiggly 为 4。内置工具条顺序:copy → 划线类 → 扩展 → note。
pageOverlays 详见 13-page-overlays.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 |
| 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 | — | 每页叠加层 |
| #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 定位资源或响应头问题。
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。
页内矩形叠加层 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。
进阶组合
内置 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 | 导入导出子集,默认 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 |
