@embedpdf-editor/vue3-chapter-viewer 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -395
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +110 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4225 -1466
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,473 +1,299 @@
|
|
|
1
1
|
# @embedpdf-editor/vue3-chapter-viewer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Vue 3 章节 PDF 阅读器:多章 PDF 拼成一条连续滚动流,支持划词划线、附注、段落书签、工具栏与标注 JSON 导入导出。
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
> 组件样式以 **inline style** 为主,**不需要** Tailwind。
|
|
8
|
-
> 下文提供可直接复制到业务项目中的完整示例(不依赖仓库内 demo 工程)。
|
|
9
|
-
|
|
10
|
-
---
|
|
5
|
+
底层引擎:`@embedpdf-editor/editor-engine`(Preact 渲染岛 + `@embedpdf/core` 插件体系)。
|
|
11
6
|
|
|
12
7
|
## 安装
|
|
13
8
|
|
|
14
9
|
```bash
|
|
15
|
-
pnpm add @embedpdf-editor/vue3-chapter-viewer
|
|
16
|
-
# 或 npm / yarn
|
|
10
|
+
pnpm add @embedpdf-editor/vue3-chapter-viewer @embedpdf/engines
|
|
17
11
|
```
|
|
18
12
|
|
|
19
|
-
|
|
20
|
-
|------|------|
|
|
21
|
-
| `vue` | **peerDependencies**(建议 Vue 3.3+) |
|
|
22
|
-
| 本包 `dependencies` | 已包含 `@embedpdf/engines`、`scheduler`、章节插件与 `editor-engine` |
|
|
23
|
-
|
|
24
|
-
在业务入口注册本包组件前,请确保已 `import { createApp } from 'vue'` 并正常挂载应用(与常规 Vue 3 项目一致)。
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Vite 项目(推荐)
|
|
29
|
-
|
|
30
|
-
若 dev 时出现 `scheduler` 解析失败,可合并本包提供的 Vite 片段(将 `scheduler` 指到本包依赖目录并加入预构建):
|
|
13
|
+
## Vite 与 WASM
|
|
31
14
|
|
|
32
15
|
```ts
|
|
33
16
|
// vite.config.ts
|
|
34
|
-
import { defineConfig
|
|
17
|
+
import { defineConfig } from 'vite';
|
|
35
18
|
import vue from '@vitejs/plugin-vue';
|
|
36
|
-
import {
|
|
37
|
-
|
|
38
|
-
export default mergeConfig(
|
|
39
|
-
defineConfig({
|
|
40
|
-
plugins: [vue()],
|
|
41
|
-
}),
|
|
42
|
-
chapterViewerViteResolve(),
|
|
43
|
-
);
|
|
44
|
-
```
|
|
19
|
+
import { chapterViewerVitePlugin } from '@embedpdf-editor/vue3-chapter-viewer/vite';
|
|
45
20
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
21
|
+
export default defineConfig({
|
|
22
|
+
plugins: [vue(), chapterViewerVitePlugin()],
|
|
23
|
+
});
|
|
24
|
+
```
|
|
49
25
|
|
|
50
|
-
|
|
26
|
+
将 `pdfium.wasm` 放到 `public/`(或由 `chapterViewerVitePlugin` 自动复制)。`usePdfiumEngine({ wasmUrl: '/pdfium.wasm' })` 的地址须与静态资源路径一致。
|
|
51
27
|
|
|
52
|
-
|
|
28
|
+
## 快速上手
|
|
53
29
|
|
|
54
|
-
|
|
30
|
+
```vue
|
|
31
|
+
<script setup lang="ts">
|
|
32
|
+
import { ref } from 'vue';
|
|
33
|
+
import {
|
|
34
|
+
ChapterPdfViewer,
|
|
35
|
+
usePdfiumEngine,
|
|
36
|
+
createChapterViewerEditorOptions,
|
|
37
|
+
DEFAULT_CHAPTER_VIEWER_FEATURES,
|
|
38
|
+
CallbackPasswordProvider,
|
|
39
|
+
} from '@embedpdf-editor/vue3-chapter-viewer';
|
|
40
|
+
import type { ChapterManifest, NoteAnchor } from '@embedpdf-editor/vue3-chapter-viewer';
|
|
55
41
|
|
|
56
|
-
|
|
57
|
-
import type { ChapterManifest } from '@embedpdf-editor/vue3-chapter-viewer';
|
|
42
|
+
const { engine, isLoading } = usePdfiumEngine({ wasmUrl: '/pdfium.wasm' });
|
|
58
43
|
|
|
59
44
|
const manifest: ChapterManifest = {
|
|
60
45
|
chapters: [
|
|
61
46
|
{
|
|
62
|
-
chapterId: '
|
|
63
|
-
title: '
|
|
64
|
-
globalPageRange: [1,
|
|
65
|
-
localPageRange: [0,
|
|
66
|
-
source: { url: '/
|
|
67
|
-
|
|
68
|
-
{
|
|
69
|
-
chapterId: '002_前言',
|
|
70
|
-
title: '前言',
|
|
71
|
-
globalPageRange: [2, 5],
|
|
72
|
-
localPageRange: [0, 3],
|
|
73
|
-
source: { url: '/002_前言.pdf' },
|
|
47
|
+
chapterId: 'ch-1',
|
|
48
|
+
title: '第一章',
|
|
49
|
+
globalPageRange: [1, 10],
|
|
50
|
+
localPageRange: [0, 9],
|
|
51
|
+
source: { url: '/pdfs/ch1.pdf' },
|
|
52
|
+
encrypted: true, // 可选:业务标记该章为加密 PDF
|
|
74
53
|
},
|
|
75
54
|
],
|
|
76
55
|
};
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
**`ChapterSource` 三种写法:**
|
|
80
|
-
|
|
81
|
-
| 写法 | 场景 |
|
|
82
|
-
|------|------|
|
|
83
|
-
| `{ url: string }` | 静态或可直链的 PDF |
|
|
84
|
-
| `{ buffer: ArrayBuffer }` | 已下载的二进制 |
|
|
85
|
-
| `{ load: () => Promise<{ url } \| { buffer }> }` | 单章自定义拉取(鉴权等) |
|
|
86
|
-
|
|
87
|
-
若 manifest 里**未写** `source`,需传入全局加载器 `chapterPdfLoader`(实现 `IChapterPdfLoader.loadPdf(chapter)`)。
|
|
88
|
-
|
|
89
|
-
相邻章节的 `globalPageRange` **允许重叠**(例如上下章各含一页过渡页);默认按 `first-wins` 解析归属。
|
|
90
|
-
|
|
91
|
-
### 2. `ChapterViewerCatalog`(带目录树时)
|
|
92
|
-
|
|
93
|
-
左侧章节目录由业务构造,与 manifest 对应:
|
|
94
|
-
|
|
95
|
-
```ts
|
|
96
|
-
import type { ChapterViewerCatalog } from '@embedpdf-editor/vue3-chapter-viewer';
|
|
97
56
|
|
|
98
|
-
const
|
|
99
|
-
tree: [
|
|
100
|
-
{ id: '001_封面', title: '封面', startPage: 1, endPage: 1 },
|
|
101
|
-
{
|
|
102
|
-
id: '002_前言',
|
|
103
|
-
title: '前言',
|
|
104
|
-
startPage: 2,
|
|
105
|
-
endPage: 5,
|
|
106
|
-
children: [/* 可选子节点 */],
|
|
107
|
-
},
|
|
108
|
-
],
|
|
57
|
+
const viewerProps = createChapterViewerEditorOptions({
|
|
109
58
|
manifest,
|
|
110
|
-
};
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
`tree` 仅用于 UI;真正加载 PDF 以 `manifest.chapters` 为准。
|
|
114
|
-
|
|
115
|
-
---
|
|
116
|
-
|
|
117
|
-
## 快速开始:`<ChapterPdfViewer />`
|
|
118
|
-
|
|
119
|
-
适合「单容器铺满、manifest 已就绪」的页面。推荐只传 **`options`**(不必再拆 `editorOptions` + `features`,也不必包一层 `callbacks`)。
|
|
120
|
-
|
|
121
|
-
`usePdfiumEngine()` 返回的是 **ref**,模板里会自动解包;在 `<script setup>` 中请使用 `engine.value`。
|
|
122
|
-
|
|
123
|
-
```vue
|
|
124
|
-
<script setup lang="ts">
|
|
125
|
-
import { computed } from 'vue';
|
|
126
|
-
import {
|
|
127
|
-
usePdfiumEngine,
|
|
128
|
-
ChapterPdfViewer,
|
|
129
|
-
type ChapterManifest,
|
|
130
|
-
type ChapterViewerOptions,
|
|
131
|
-
} from '@embedpdf-editor/vue3-chapter-viewer';
|
|
132
|
-
|
|
133
|
-
const props = defineProps<{ manifest: ChapterManifest }>();
|
|
134
|
-
const { engine, isLoading, error } = usePdfiumEngine();
|
|
135
|
-
|
|
136
|
-
const options = computed<ChapterViewerOptions>(() => ({
|
|
137
|
-
manifest: props.manifest,
|
|
138
|
-
bookmarks: {
|
|
139
|
-
load: () => fetchBookmarks(),
|
|
140
|
-
persist: (list) => saveBookmarks(list),
|
|
141
|
-
onRequestRemove: async (b) => {
|
|
142
|
-
await api.deleteBookmark(b.id);
|
|
143
|
-
return true;
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
59
|
notes: {
|
|
147
|
-
loadNotes: () =>
|
|
148
|
-
|
|
149
|
-
openCreateModal(draft).then((noteId) => complete(noteId));
|
|
150
|
-
},
|
|
151
|
-
onRequestEditNote: (noteId) => openEditModal(noteId),
|
|
152
|
-
onDeleteNote: (id) => api.deleteNote(id),
|
|
60
|
+
loadNotes: async () => [] as NoteAnchor[],
|
|
61
|
+
onCreateNote: async (draft) => ({ noteId: crypto.randomUUID() }),
|
|
153
62
|
},
|
|
154
|
-
|
|
155
|
-
|
|
63
|
+
bookmarks: {
|
|
64
|
+
load: async () => [],
|
|
65
|
+
persist: async () => {},
|
|
156
66
|
},
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
<template>
|
|
161
|
-
<div v-if="error" style="padding: 16px">引擎失败:{{ error.message }}</div>
|
|
162
|
-
<div v-else-if="isLoading || !engine" style="padding: 16px">正在加载 PDFium…</div>
|
|
163
|
-
<ChapterPdfViewer
|
|
164
|
-
v-else
|
|
165
|
-
:engine="engine"
|
|
166
|
-
:options="options"
|
|
167
|
-
style="height: 100vh; width: 100%"
|
|
168
|
-
/>
|
|
169
|
-
</template>
|
|
170
|
-
```
|
|
67
|
+
features: DEFAULT_CHAPTER_VIEWER_FEATURES,
|
|
68
|
+
});
|
|
171
69
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
阅读器不会自带 Modal,需在宿主里监听 `onRequestCreateNote` / `onRequestEditNote`,弹窗确认后调用 `complete(noteId)` 或 `onUpdateNote`:
|
|
175
|
-
|
|
176
|
-
```vue
|
|
177
|
-
<script setup lang="ts">
|
|
178
|
-
import { ref, shallowRef } from 'vue';
|
|
179
|
-
import type { NoteDraft } from '@embedpdf-editor/vue3-chapter-viewer';
|
|
180
|
-
|
|
181
|
-
const pendingCreate = shallowRef<{
|
|
182
|
-
draft: NoteDraft;
|
|
183
|
-
complete: (noteId: string) => void | Promise<void>;
|
|
184
|
-
} | null>(null);
|
|
185
|
-
|
|
186
|
-
function onRequestCreateNote(payload: {
|
|
187
|
-
draft: NoteDraft;
|
|
188
|
-
complete: (noteId: string) => void | Promise<void>;
|
|
189
|
-
}) {
|
|
190
|
-
pendingCreate.value = payload;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function confirmCreate(content: string) {
|
|
194
|
-
const pending = pendingCreate.value;
|
|
195
|
-
if (!pending) return;
|
|
196
|
-
const noteId = await api.createNote({ ...pending.draft, content });
|
|
197
|
-
await pending.complete(noteId);
|
|
198
|
-
pendingCreate.value = null;
|
|
199
|
-
}
|
|
70
|
+
// 加密章:在 createPdfChapterEditor / 低层 options 中传入 passwordProvider(见下文)
|
|
200
71
|
</script>
|
|
201
72
|
|
|
202
73
|
<template>
|
|
203
|
-
|
|
204
|
-
<YourNoteModal
|
|
205
|
-
:open="!!pendingCreate"
|
|
206
|
-
:quoted-text="pendingCreate?.draft.selectedText"
|
|
207
|
-
@confirm="confirmCreate"
|
|
208
|
-
@cancel="pendingCreate = null"
|
|
209
|
-
/>
|
|
74
|
+
<ChapterPdfViewer v-if="engine && !isLoading" :engine="engine" :viewer-props="viewerProps" />
|
|
210
75
|
</template>
|
|
211
76
|
```
|
|
212
77
|
|
|
213
|
-
|
|
78
|
+
完整交互见 monorepo 示例:`examples/chapter-viewer-demo-vue3`。
|
|
214
79
|
|
|
215
|
-
|
|
80
|
+
## Manifest(章节清单)
|
|
216
81
|
|
|
217
|
-
|
|
|
82
|
+
| 字段 | 说明 |
|
|
218
83
|
|------|------|
|
|
219
|
-
|
|
|
220
|
-
| `
|
|
221
|
-
| `
|
|
222
|
-
| `
|
|
223
|
-
|
|
224
|
-
|
|
84
|
+
| `chapterId` | 业务唯一 ID,同时作为 `documentManager` 的 `documentId` |
|
|
85
|
+
| `title` | 展示标题 |
|
|
86
|
+
| `globalPageRange` | 该章在「整本」中的全局页闭区间;相邻章可重叠 |
|
|
87
|
+
| `localPageRange` | 该章 PDF 内 0-based 页闭区间;页数须与 global 区间一致 |
|
|
88
|
+
| `source` | `{ url }` / `{ buffer }` / `{ load: () => Promise<...> }`;可省略并在 `chapterPdfLoader` 统一加载 |
|
|
89
|
+
| `encrypted` | **可选提示位**:标记该章可能需密码;真正触发密码流程的是 PDFium 打开时报 `Password` 错误 |
|
|
90
|
+
| `ownedGlobalPages` | 仅 `overlapStrategy: 'explicit'` 时使用 |
|
|
225
91
|
|
|
226
|
-
|
|
227
|
-
- 鼠标移到文本行末 → 显示「添加书签」;已加书签点击 → 删除确认(走 `onRequestRemove`)
|
|
228
|
-
- 笔记区域悬停 → 编辑 / 删除
|
|
229
|
-
- **缩放**:`features.zoom.enabled !== false` 时,在 PDF 滚动区域内 **`Ctrl/Cmd + 滚轮`** 或 **双指捏合**(触控板)缩放;缩放写入 core 的 `document.scale`,PDF 与书签/笔记 overlay 同步变化
|
|
92
|
+
## 加密 PDF(密码 / 解密)
|
|
230
93
|
|
|
231
|
-
|
|
94
|
+
章节 PDF 若带打开密码,引擎在 `documentManager` 打开失败且错误码为 **Password** 时,会向 `passwordProvider` 索取密码;用户放弃则该章进入 **`password-required`** 状态。
|
|
232
95
|
|
|
233
|
-
|
|
96
|
+
### 配置方式
|
|
234
97
|
|
|
235
|
-
|
|
98
|
+
`createChapterViewerEditorOptions` 默认使用「始终返回 `null`」的 `CallbackPasswordProvider`(即不弹窗、直接失败)。业务须在 **`createPdfChapterEditor`** 或展开 `viewerProps` 底层 options 时传入自定义 provider:
|
|
236
99
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
`createChapterViewerBundle` → `EmbedPDF` → `ChapterTreePanel` + `PdfChapterViewport`
|
|
240
|
-
|
|
241
|
-
**步骤简述:**
|
|
242
|
-
|
|
243
|
-
1. 用 `createChapterViewerBundle(options)` 得到 `{ plugins, features }`(`options` 与上一节相同)。
|
|
244
|
-
2. 用 `<EmbedPDF>` 挂载 `plugins`,在 scoped slot 里等 `pluginsReady === true` 再渲染子树。
|
|
245
|
-
3. 在 `EmbedPDF` 子树内用 `useCapability(ChapterManagerPlugin.id)` 对首章调用 `ensureChapterLoaded`。
|
|
246
|
-
4. 章节状态为 `loaded` 后再挂载 `<PdfChapterViewport :features="bundle.features" />`。
|
|
247
|
-
|
|
248
|
-
**父页面(可复制):**
|
|
249
|
-
|
|
250
|
-
```vue
|
|
251
|
-
<script setup lang="ts">
|
|
252
|
-
import { computed } from 'vue';
|
|
100
|
+
```ts
|
|
253
101
|
import {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
createChapterViewerBundle,
|
|
257
|
-
type ChapterViewerCatalog,
|
|
258
|
-
type ChapterViewerOptions,
|
|
102
|
+
createChapterViewerEditorOptions,
|
|
103
|
+
CallbackPasswordProvider,
|
|
259
104
|
} from '@embedpdf-editor/vue3-chapter-viewer';
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const props = defineProps<{
|
|
263
|
-
catalog: ChapterViewerCatalog;
|
|
264
|
-
notes: ChapterViewerOptions['notes'];
|
|
265
|
-
bookmarks: ChapterViewerOptions['bookmarks'];
|
|
266
|
-
}>();
|
|
267
|
-
|
|
268
|
-
const { engine, isLoading, error } = usePdfiumEngine();
|
|
269
|
-
|
|
270
|
-
const bundle = computed(() =>
|
|
271
|
-
createChapterViewerBundle({
|
|
272
|
-
manifest: props.catalog.manifest,
|
|
273
|
-
notes: props.notes,
|
|
274
|
-
bookmarks: props.bookmarks,
|
|
275
|
-
features: { zoom: { pageWidth: 800 } },
|
|
276
|
-
}),
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
const firstChapterId = computed(
|
|
280
|
-
() => props.catalog.manifest.chapters[0]?.chapterId ?? '',
|
|
281
|
-
);
|
|
282
|
-
</script>
|
|
283
|
-
|
|
284
|
-
<template>
|
|
285
|
-
<div v-if="error">引擎失败:{{ error.message }}</div>
|
|
286
|
-
<div v-else-if="isLoading || !engine">正在加载 PDFium…</div>
|
|
287
|
-
<div v-else style="height: 100vh; display: flex; flex-direction: column">
|
|
288
|
-
<EmbedPDF :engine="engine" :plugins="bundle.plugins">
|
|
289
|
-
<template #default="{ pluginsReady }">
|
|
290
|
-
<ChapterWorkspace
|
|
291
|
-
v-if="pluginsReady"
|
|
292
|
-
:tree="catalog.tree"
|
|
293
|
-
:first-chapter-id="firstChapterId"
|
|
294
|
-
:features="bundle.features"
|
|
295
|
-
/>
|
|
296
|
-
<div v-else>正在初始化插件…</div>
|
|
297
|
-
</template>
|
|
298
|
-
</EmbedPDF>
|
|
299
|
-
</div>
|
|
300
|
-
</template>
|
|
301
|
-
```
|
|
105
|
+
// StaticPasswordProvider / UiPromptPasswordProvider 从 @embedpdf-editor/chapter-core 引入
|
|
302
106
|
|
|
303
|
-
|
|
107
|
+
const passwordProvider = new CallbackPasswordProvider(async (chapter, attempt) => {
|
|
108
|
+
// attempt: 0 为首次,递增表示重试次数(可做「错三次锁定」)
|
|
109
|
+
const pwd = await myAskPasswordUi(chapter.title, attempt);
|
|
110
|
+
return pwd; // 返回 null 表示用户取消 → 章节 status: password-required
|
|
111
|
+
});
|
|
304
112
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
useCapability,
|
|
310
|
-
ChapterManagerPlugin,
|
|
311
|
-
ChapterTreePanel,
|
|
312
|
-
PdfChapterViewport,
|
|
313
|
-
type ChapterViewerFeaturesConfig,
|
|
314
|
-
type ChapterTreeNode,
|
|
315
|
-
} from '@embedpdf-editor/vue3-chapter-viewer';
|
|
316
|
-
|
|
317
|
-
const props = defineProps<{
|
|
318
|
-
tree: ChapterTreeNode[];
|
|
319
|
-
firstChapterId: string;
|
|
320
|
-
features: ChapterViewerFeaturesConfig;
|
|
321
|
-
}>();
|
|
113
|
+
const viewerProps = createChapterViewerEditorOptions({ manifest, notes, bookmarks });
|
|
114
|
+
// viewerProps 内部会 merge 到 createPdfChapterEditor;若需覆盖 passwordProvider,
|
|
115
|
+
// 请使用 createPdfChapterEditor({ ...opts, passwordProvider }) + 自行组装 EmbedPDF。
|
|
116
|
+
```
|
|
322
117
|
|
|
323
|
-
|
|
324
|
-
const chapterReady = ref(false);
|
|
118
|
+
### 内置 Provider(`@embedpdf-editor/chapter-core`)
|
|
325
119
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
)
|
|
120
|
+
| 类 | 用途 |
|
|
121
|
+
|----|------|
|
|
122
|
+
| `CallbackPasswordProvider` | 异步回调 `(chapter, attempt) => string \| null`,最常用 |
|
|
123
|
+
| `StaticPasswordProvider` | 构造时传入 `Record<chapterId, password>`,适合 SSO 一次解锁多章 |
|
|
124
|
+
| `UiPromptPasswordProvider` | `onPrompt(chapter, attempt)` 通知 UI,UI 调 `submit(chapterId, password \| null)` 完成 Promise |
|
|
125
|
+
|
|
126
|
+
`getCachedPassword?(chapterId)` 可选:同步返回已缓存密码,避免重复弹窗。
|
|
127
|
+
|
|
128
|
+
### 章节加载状态
|
|
129
|
+
|
|
130
|
+
`ChapterLoadStatus` 包含:`idle` | `loading` | `loaded` | `error` | **`password-required`**。密码提交成功后 `ChapterManagerPlugin` 会带密码重试打开。
|
|
131
|
+
|
|
132
|
+
## 标注导入 / 导出(JSON 格式)
|
|
133
|
+
|
|
134
|
+
类型定义见 `editor-engine` → `chapter-annotations-io/types.ts`。当前归档版本常量:`CHAPTER_ANNOTATIONS_ARCHIVE_VERSION === 1`。
|
|
135
|
+
|
|
136
|
+
### 全书归档 `ChapterAnnotationsArchive`
|
|
137
|
+
|
|
138
|
+
```jsonc
|
|
139
|
+
{
|
|
140
|
+
// 固定为 1;未来格式升级时解析器可据此分支
|
|
141
|
+
"version": 1,
|
|
142
|
+
// ISO 8601 导出时间(UTC 字符串)
|
|
143
|
+
"exportedAt": "2026-06-01T12:00:00.000Z",
|
|
144
|
+
// key = chapterId;value 不含重复的 chapterId 字段
|
|
145
|
+
"chapters": {
|
|
146
|
+
"ch-1": {
|
|
147
|
+
// ---------- 段落书签(plugin-paragraph-bookmark)----------
|
|
148
|
+
"bookmarks": [
|
|
149
|
+
{
|
|
150
|
+
"id": "bm-uuid",
|
|
151
|
+
"label": "用户可见标题",
|
|
152
|
+
"metadata": { "任意业务字段": true },
|
|
153
|
+
"anchor": {
|
|
154
|
+
"chapterId": "ch-1",
|
|
155
|
+
"localPageIndex": 0,
|
|
156
|
+
"globalPageIndex": 1,
|
|
157
|
+
"globalPageNumber": 1,
|
|
158
|
+
"rectPdfCoord": { "origin": { "x": 0, "y": 0 }, "size": { "width": 100, "height": 20 } },
|
|
159
|
+
"rectsPdfCoord": [],
|
|
160
|
+
"markerAnchor": { "x": 100, "y": 20 }
|
|
161
|
+
},
|
|
162
|
+
"createdAt": 1717243200000
|
|
163
|
+
}
|
|
164
|
+
],
|
|
165
|
+
// ---------- 附注(plugin-note)----------
|
|
166
|
+
"notes": [
|
|
167
|
+
{
|
|
168
|
+
"noteId": "note-uuid",
|
|
169
|
+
"chapterId": "ch-1",
|
|
170
|
+
"globalPageIndex": 1,
|
|
171
|
+
"globalPageNumber": 1,
|
|
172
|
+
"localPageIndex": 0,
|
|
173
|
+
"rectsPdfCoord": [{ "origin": { "x": 0, "y": 0 }, "size": { "width": 200, "height": 16 } }],
|
|
174
|
+
"endAnchor": { "x": 200, "y": 16 },
|
|
175
|
+
"selectedText": "被选中的原文",
|
|
176
|
+
"content": "用户笔记正文"
|
|
177
|
+
}
|
|
178
|
+
],
|
|
179
|
+
// ---------- 划线 / 高亮 / 图章等(@embedpdf/plugin-annotation)----------
|
|
180
|
+
"markup": [
|
|
181
|
+
{
|
|
182
|
+
// 与 AnnotationTransferItem.annotation 同结构(类型、页码、几何、颜色等)
|
|
183
|
+
"annotation": {
|
|
184
|
+
"id": "ann-uuid",
|
|
185
|
+
"type": "highlight",
|
|
186
|
+
"pageIndex": 0,
|
|
187
|
+
"rect": { "origin": { "x": 0, "y": 0 }, "size": { "width": 100, "height": 12 } }
|
|
188
|
+
},
|
|
189
|
+
// 图章等二进制附件:导出时 ArrayBuffer → base64
|
|
190
|
+
"ctx": {
|
|
191
|
+
"dataBase64": "iVBORw0KGgo...",
|
|
192
|
+
"mimeType": "image/png"
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
329
200
|
|
|
330
|
-
|
|
331
|
-
[chapterManager, () => props.firstChapterId],
|
|
332
|
-
([mgr, chapterId], _prev, onCleanup) => {
|
|
333
|
-
if (!mgr || !chapterId) return;
|
|
201
|
+
### 单章快照 `ChapterAnnotationsSnapshot`
|
|
334
202
|
|
|
335
|
-
|
|
336
|
-
chapterReady.value = false;
|
|
203
|
+
与归档中某一章对象相同,但**顶层多一个** `chapterId`:
|
|
337
204
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
205
|
+
```jsonc
|
|
206
|
+
{
|
|
207
|
+
"chapterId": "ch-1",
|
|
208
|
+
"bookmarks": [],
|
|
209
|
+
"notes": [],
|
|
210
|
+
"markup": []
|
|
211
|
+
}
|
|
212
|
+
```
|
|
342
213
|
|
|
343
|
-
|
|
344
|
-
if (!cancelled && status === 'loaded') chapterReady.value = true;
|
|
345
|
-
});
|
|
214
|
+
`bookmarks` / `notes` / `markup` 均可省略;导出时可按 `ExportChapterAnnotationsOptions` 关闭某一类。
|
|
346
215
|
|
|
347
|
-
|
|
348
|
-
cancelled = true;
|
|
349
|
-
unsub();
|
|
350
|
-
});
|
|
351
|
-
},
|
|
352
|
-
{ immediate: true },
|
|
353
|
-
);
|
|
354
|
-
</script>
|
|
216
|
+
### 导入选项 `ImportChapterAnnotationsOptions`
|
|
355
217
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
<div style="flex: 1; min-width: 0; min-height: 0; position: relative">
|
|
364
|
-
<div v-if="chapterReady" style="position: absolute; inset: 0">
|
|
365
|
-
<PdfChapterViewport :features="features" />
|
|
366
|
-
</div>
|
|
367
|
-
<div v-else style="padding: 16px">正在加载章节 PDF…</div>
|
|
368
|
-
</div>
|
|
369
|
-
</div>
|
|
370
|
-
</template>
|
|
371
|
-
```
|
|
218
|
+
| 选项 | 默认 | 说明 |
|
|
219
|
+
|------|------|------|
|
|
220
|
+
| `mode` | `replace` | `replace`:先清空该章对应数据再写入;`merge`:与现有合并(书签按 anchor 去重) |
|
|
221
|
+
| `ensureChapterLoaded` | `true` | 导入划线前确保该章 PDF 已打开 |
|
|
222
|
+
| `bookmarks` / `notes` / `markup` | `true` | 控制是否处理对应字段 |
|
|
223
|
+
| `persistNotes` | — | 导入完成后回调,参数为**全书**附注列表,便于写 localStorage / 后端 |
|
|
224
|
+
| `persistBookmarks` | — | 同上,段落书签列表 |
|
|
372
225
|
|
|
373
|
-
|
|
226
|
+
### API(均需 `PluginRegistry`,Vue 3 用 `useRegistry()`)
|
|
374
227
|
|
|
375
|
-
|
|
228
|
+
```ts
|
|
229
|
+
import { useRegistry } from '@embedpdf-editor/vue3-chapter-viewer';
|
|
230
|
+
import {
|
|
231
|
+
exportChapterAnnotations,
|
|
232
|
+
exportAllChapterAnnotations,
|
|
233
|
+
importChapterAnnotations,
|
|
234
|
+
importChapterAnnotationsArchive,
|
|
235
|
+
chapterAnnotationsArchiveToJson,
|
|
236
|
+
parseChapterAnnotationsArchiveJson,
|
|
237
|
+
chapterAnnotationsSnapshotToJson,
|
|
238
|
+
parseChapterAnnotationsSnapshotJson,
|
|
239
|
+
downloadChapterAnnotationsArchive,
|
|
240
|
+
downloadChapterAnnotationsSnapshot,
|
|
241
|
+
} from '@embedpdf-editor/vue3-chapter-viewer';
|
|
376
242
|
|
|
377
|
-
|
|
243
|
+
const { registry } = useRegistry();
|
|
378
244
|
|
|
379
|
-
|
|
245
|
+
const archive = await exportAllChapterAnnotations(registry.value!);
|
|
246
|
+
const json = chapterAnnotationsArchiveToJson(archive);
|
|
380
247
|
|
|
381
|
-
|
|
248
|
+
await importChapterAnnotationsArchive(registry.value!, parseChapterAnnotationsArchiveJson(json), {
|
|
249
|
+
mode: 'replace',
|
|
250
|
+
persistNotes: (all) => myStore.saveNotes(all),
|
|
251
|
+
persistBookmarks: (all) => myStore.saveBookmarks(all),
|
|
252
|
+
});
|
|
253
|
+
```
|
|
382
254
|
|
|
383
|
-
|
|
|
255
|
+
| 函数 | 说明 |
|
|
384
256
|
|------|------|
|
|
385
|
-
| `
|
|
386
|
-
| `
|
|
387
|
-
| `
|
|
388
|
-
| `
|
|
389
|
-
|
|
390
|
-
|
|
257
|
+
| `exportChapterAnnotations(registry, chapterId, options?)` | 导出单章快照 |
|
|
258
|
+
| `exportAllChapterAnnotations(registry, options?)` | 导出全书归档 |
|
|
259
|
+
| `importChapterAnnotations(registry, snapshot, options?)` | 导入单章 |
|
|
260
|
+
| `importChapterAnnotationsArchive(registry, archive, options?)` | 导入全书 |
|
|
261
|
+
| `chapterAnnotationsArchiveToJson` / `parseChapterAnnotationsArchiveJson` | 序列化 / 反序列化 |
|
|
262
|
+
| `chapterAnnotationsSnapshotToJson` / `parseChapterAnnotationsSnapshotJson` | 单章版 |
|
|
263
|
+
| `downloadChapterAnnotationsArchive` / `downloadChapterAnnotationsSnapshot` | 触发浏览器下载 `.json` |
|
|
391
264
|
|
|
392
|
-
|
|
393
|
-
|------|------|
|
|
394
|
-
| `load` | 加载已有书签 |
|
|
395
|
-
| `persist` | 增删改后持久化 |
|
|
396
|
-
| `onRequestRemove` | 删除确认,返回 `true` 后从渲染层移除 |
|
|
265
|
+
Demo 参考:`examples/chapter-viewer-demo-vue3/src/components/AnnotationsDemoBar.vue`。
|
|
397
266
|
|
|
398
|
-
|
|
267
|
+
## 功能开关 `features`
|
|
399
268
|
|
|
400
|
-
|
|
269
|
+
通过 `createChapterViewerEditorOptions({ features })` 或 `DEFAULT_CHAPTER_VIEWER_FEATURES` 控制划线样式、笔记、书签、选区卡片、缩放等。详见 `@embedpdf-editor/chapter-viewer` 的 `ChapterViewerFeaturesConfig`。
|
|
401
270
|
|
|
402
|
-
|
|
271
|
+
## 笔记与书签
|
|
403
272
|
|
|
404
|
-
|
|
273
|
+
- **附注**:`notes.loadNotes`、`onCreateNote` 或 `onRequestCreateNote`(自定义弹窗)、`onUpdateNote`、`onDeleteNote` 等,类型 `NoteCallbacks`。
|
|
274
|
+
- **书签**:`bookmarks.load`、`persist`、`onRequestRemove`,类型 `ParagraphBookmarkCallbacks`。
|
|
405
275
|
|
|
406
276
|
## 主要导出
|
|
407
277
|
|
|
408
|
-
| 导出 |
|
|
278
|
+
| 导出 | 说明 |
|
|
409
279
|
|------|------|
|
|
410
|
-
| `ChapterPdfViewer` |
|
|
411
|
-
| `
|
|
412
|
-
| `
|
|
413
|
-
| `
|
|
414
|
-
| `
|
|
415
|
-
| `
|
|
416
|
-
|
|
|
417
|
-
| `ChapterViewerOptions` | `options` 的类型 |
|
|
418
|
-
| `ChapterViewerConfig` | `options.features` 的类型 |
|
|
419
|
-
| `ChapterManifest` / `ChapterDescriptor` / `IChapterPdfLoader` | 数据与加载契约 |
|
|
420
|
-
| `ChapterViewerCatalog` / `ChapterTreeNode` | 目录树类型 |
|
|
421
|
-
| `applySelectionMarkup` | 编程式应用划线(高级) |
|
|
422
|
-
| `chapterViewerViteResolve` | Vite 配置辅助(`/vite` 子路径) |
|
|
280
|
+
| `ChapterPdfViewer` | 开箱即用阅读器壳 |
|
|
281
|
+
| `PdfChapterViewport` / `ChapterTreePanel` | 自定义布局 |
|
|
282
|
+
| `usePdfiumEngine` | 加载 PDFium WASM |
|
|
283
|
+
| `useRegistry` / `EmbedPDF` / `useCapability` | 插件注册表与能力 |
|
|
284
|
+
| `createChapterViewerEditorOptions` | 推荐配置入口 |
|
|
285
|
+
| `CallbackPasswordProvider` | 密码回调(更多 Provider 见 `@embedpdf-editor/chapter-core`) |
|
|
286
|
+
| 标注 IO 函数与类型 | 见上表 |
|
|
423
287
|
|
|
424
|
-
|
|
288
|
+
## 与其它包
|
|
425
289
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
## 接入检查清单
|
|
429
|
-
|
|
430
|
-
在业务项目中接入时,请确认:
|
|
431
|
-
|
|
432
|
-
1. **静态资源**:`manifest` 里每章 `source.url` 可被浏览器访问(或实现 `chapterPdfLoader` / `load()`)。
|
|
433
|
-
2. **目录数据**:若使用 `ChapterTreePanel`,自行从后端组装 `ChapterViewerCatalog`(`tree` + `manifest` 字段一致)。
|
|
434
|
-
3. **首章加载**:自定义布局下,首章 `ensureChapterLoaded` 返回 `loaded` 后再挂载 `PdfChapterViewport`。
|
|
435
|
-
4. **笔记 UI**:`onRequestCreateNote` 需在宿主弹窗后调用 `complete(noteId)`。
|
|
436
|
-
5. **Vue ref**:`usePdfiumEngine()` 的 `engine` 在 script 中为 ref,传给 `ChapterPdfViewer` 时模板写 `:engine="engine"` 即可。
|
|
437
|
-
|
|
438
|
-
本包随 npm 发布,**不包含**可运行的示例站点或 mock 数据;请以上文代码片段为模板,在业务仓库中创建页面并放置自有 PDF。
|
|
439
|
-
|
|
440
|
-
---
|
|
441
|
-
|
|
442
|
-
## 其他框架
|
|
443
|
-
|
|
444
|
-
| 包 | 说明 |
|
|
290
|
+
| 包 | 场景 |
|
|
445
291
|
|----|------|
|
|
446
|
-
| `@embedpdf-editor/react-chapter-viewer` | React 18
|
|
447
|
-
| `@embedpdf-editor/chapter-
|
|
448
|
-
|
|
449
|
-
能力与 React 版视口内核对齐;API 命名一致,仅组合方式随框架变化。
|
|
450
|
-
|
|
451
|
-
---
|
|
452
|
-
|
|
453
|
-
## 常见问题
|
|
454
|
-
|
|
455
|
-
**Q:页面一直「正在加载」?**
|
|
456
|
-
确认 `manifest` 中每章 `source.url` 可访问,或 `chapterPdfLoader` / `ensureChapterLoaded` 返回 `loaded`。首章未加载完成时不要提前挂载 `PdfChapterViewport`。
|
|
457
|
-
|
|
458
|
-
**Q:需要用户安装 `@embedpdf/engines` 吗?**
|
|
459
|
-
不需要,已作为本包依赖。只需 `vue`。
|
|
460
|
-
|
|
461
|
-
**Q:如何关闭缩放?**
|
|
462
|
-
`features: { zoom: false }`,或 `zoom: { enabled: false }`。
|
|
463
|
-
|
|
464
|
-
**Q:`zoom.enabled: true` 但缩放手势没反应?**
|
|
465
|
-
1. 确认使用 `<ChapterPdfViewer />` 或 `<PdfChapterViewport :features="..." />`(缩放逻辑在视口内)。
|
|
466
|
-
2. 在 PDF 区域使用 **Ctrl/Cmd + 滚轮**,不要用普通滚轮。
|
|
467
|
-
3. 升级 `@embedpdf-editor/vue3-chapter-viewer` 后硬刷新浏览器。
|
|
292
|
+
| `@embedpdf-editor/react-chapter-viewer` | React 18,API 与本包对称 |
|
|
293
|
+
| `@embedpdf-editor/vue2-chapter-viewer` | Vue 2.6,内部为 `chapter-snippet` Web Component |
|
|
468
294
|
|
|
469
|
-
|
|
470
|
-
使用 `onRequestCreateNote` + `complete(noteId)`,不要实现 `onCreateNote`。
|
|
295
|
+
## 故障排查
|
|
471
296
|
|
|
472
|
-
|
|
473
|
-
`
|
|
297
|
+
- **一直 Loading**:确认 `manifest` 已 `setManifest`、章节 `source` / `chapterPdfLoader` 可访问、WASM 与 COOP/COEP 已配置。
|
|
298
|
+
- **加密章无法打开**:检查 `passwordProvider` 是否返回非空密码;取消时为 `password-required`。
|
|
299
|
+
- **导出 markup 为空**:该章须已 `loaded`,且 `ensureChapterLoaded` 为 true(默认)。
|