@embedpdf-editor/vue3-chapter-viewer 0.2.1 → 0.3.1
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 +196 -373
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +180 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4472 -1683
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/vite.ts +2 -2
package/README.md
CHANGED
|
@@ -1,33 +1,18 @@
|
|
|
1
1
|
# @embedpdf-editor/vue3-chapter-viewer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Vue 3 版章节 PDF 阅读器。它把多份章节 PDF 组织成一本连续滚动的书,并内置划线/高亮、附注、段落书签、章节目录和标注导入导出能力。
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
> 组件样式以 **inline style** 为主,**不需要** Tailwind。
|
|
8
|
-
> 下文提供可直接复制到业务项目中的完整示例(不依赖仓库内 demo 工程)。
|
|
9
|
-
|
|
10
|
-
---
|
|
5
|
+
底层使用 `@embedpdf/core` 插件体系和 PDFium 引擎;Vue 3 包只负责提供 SFC 组件、composition hooks 和类型导出。
|
|
11
6
|
|
|
12
7
|
## 安装
|
|
13
8
|
|
|
14
9
|
```bash
|
|
15
10
|
pnpm add @embedpdf-editor/vue3-chapter-viewer
|
|
16
|
-
# 或 npm / yarn
|
|
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 项目(推荐)
|
|
13
|
+
## Vite
|
|
29
14
|
|
|
30
|
-
|
|
15
|
+
包内导出的是一段 Vite 配置,不是 Vite plugin。它用于补齐 `scheduler` alias 和 PDFium 引擎的预构建配置;`@embedpdf/engines` 已经是本包依赖,业务项目不需要单独安装。
|
|
31
16
|
|
|
32
17
|
```ts
|
|
33
18
|
// vite.config.ts
|
|
@@ -43,431 +28,269 @@ export default mergeConfig(
|
|
|
43
28
|
);
|
|
44
29
|
```
|
|
45
30
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
## 你需要准备的数据
|
|
49
|
-
|
|
50
|
-
阅读器**不内置**目录与 PDF 文件,全部由业务提供。
|
|
51
|
-
|
|
52
|
-
### 1. `ChapterManifest`(引擎用)
|
|
53
|
-
|
|
54
|
-
描述每一章对应的 PDF 与在「整本」中的页码区间:
|
|
55
|
-
|
|
56
|
-
```ts
|
|
57
|
-
import type { ChapterManifest } from '@embedpdf-editor/vue3-chapter-viewer';
|
|
58
|
-
|
|
59
|
-
const manifest: ChapterManifest = {
|
|
60
|
-
chapters: [
|
|
61
|
-
{
|
|
62
|
-
chapterId: '001_封面', // 唯一 ID,同时作为 documentId
|
|
63
|
-
title: '封面',
|
|
64
|
-
globalPageRange: [1, 1], // 在整本中的全局页(闭区间)
|
|
65
|
-
localPageRange: [0, 0], // 该 PDF 内 0-based 页(页数须与 global 一致)
|
|
66
|
-
source: { url: '/001_封面.pdf' }, // 或 buffer / load()
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
chapterId: '002_前言',
|
|
70
|
-
title: '前言',
|
|
71
|
-
globalPageRange: [2, 5],
|
|
72
|
-
localPageRange: [0, 3],
|
|
73
|
-
source: { url: '/002_前言.pdf' },
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
};
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
**`ChapterSource` 三种写法:**
|
|
80
|
-
|
|
81
|
-
| 写法 | 场景 |
|
|
82
|
-
|------|------|
|
|
83
|
-
| `{ url: string }` | 静态或可直链的 PDF |
|
|
84
|
-
| `{ buffer: ArrayBuffer }` | 已下载的二进制 |
|
|
85
|
-
| `{ load: () => Promise<{ url } \| { buffer }> }` | 单章自定义拉取(鉴权等) |
|
|
31
|
+
如果不用 `mergeConfig`,需要手动合并 `resolve.alias` 和 `optimizeDeps.include`,不要直接覆盖现有配置。
|
|
86
32
|
|
|
87
|
-
|
|
33
|
+
## 快速开始
|
|
88
34
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
### 2. `ChapterViewerCatalog`(带目录树时)
|
|
92
|
-
|
|
93
|
-
左侧章节目录由业务构造,与 manifest 对应:
|
|
94
|
-
|
|
95
|
-
```ts
|
|
96
|
-
import type { ChapterViewerCatalog } from '@embedpdf-editor/vue3-chapter-viewer';
|
|
97
|
-
|
|
98
|
-
const catalog: ChapterViewerCatalog = {
|
|
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
|
-
],
|
|
109
|
-
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`。
|
|
35
|
+
推荐只传 `options`。`editorOptions` 和 `features` 仍兼容,但已作为低层/旧写法保留。
|
|
122
36
|
|
|
123
37
|
```vue
|
|
124
38
|
<script setup lang="ts">
|
|
125
|
-
import { computed } from 'vue';
|
|
126
39
|
import {
|
|
127
|
-
usePdfiumEngine,
|
|
128
40
|
ChapterPdfViewer,
|
|
129
|
-
|
|
41
|
+
usePdfiumEngine,
|
|
130
42
|
type ChapterViewerOptions,
|
|
131
43
|
} from '@embedpdf-editor/vue3-chapter-viewer';
|
|
132
44
|
|
|
133
|
-
const props = defineProps<{ manifest: ChapterManifest }>();
|
|
134
45
|
const { engine, isLoading, error } = usePdfiumEngine();
|
|
135
46
|
|
|
136
|
-
const options
|
|
137
|
-
manifest:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
47
|
+
const options: ChapterViewerOptions = {
|
|
48
|
+
manifest: {
|
|
49
|
+
chapters: [
|
|
50
|
+
{
|
|
51
|
+
chapterId: 'ch-1',
|
|
52
|
+
title: '第一章',
|
|
53
|
+
globalPageRange: [1, 12],
|
|
54
|
+
localPageRange: [0, 11],
|
|
55
|
+
source: { url: '/pdfs/chapter-1.pdf' },
|
|
56
|
+
},
|
|
57
|
+
],
|
|
145
58
|
},
|
|
146
59
|
notes: {
|
|
147
|
-
loadNotes: () =>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
},
|
|
151
|
-
|
|
152
|
-
|
|
60
|
+
loadNotes: async () => [],
|
|
61
|
+
onCreateNote: async () => ({ noteId: crypto.randomUUID() }),
|
|
62
|
+
onUpdateNote: async () => {},
|
|
63
|
+
onDeleteNote: async () => {},
|
|
64
|
+
},
|
|
65
|
+
bookmarks: {
|
|
66
|
+
load: async () => [],
|
|
67
|
+
persist: async () => {},
|
|
68
|
+
onRequestRemove: async () => true,
|
|
153
69
|
},
|
|
154
70
|
features: {
|
|
155
|
-
|
|
71
|
+
markup: true,
|
|
72
|
+
bookmarks: true,
|
|
73
|
+
notes: true,
|
|
74
|
+
selectionToolbar: true,
|
|
75
|
+
zoom: { pageWidth: 720 },
|
|
156
76
|
},
|
|
157
|
-
}
|
|
77
|
+
};
|
|
158
78
|
</script>
|
|
159
79
|
|
|
160
80
|
<template>
|
|
161
|
-
<div
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
81
|
+
<div class="reader">
|
|
82
|
+
<div v-if="error">PDF engine failed: {{ error.message }}</div>
|
|
83
|
+
<div v-else-if="isLoading || !engine">Loading PDF engine...</div>
|
|
84
|
+
<ChapterPdfViewer
|
|
85
|
+
v-else
|
|
86
|
+
:engine="engine"
|
|
87
|
+
:options="options"
|
|
88
|
+
class-name="reader-fill"
|
|
89
|
+
viewport-class-name="reader-viewport"
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
169
92
|
</template>
|
|
170
|
-
```
|
|
171
93
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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;
|
|
94
|
+
<style scoped>
|
|
95
|
+
.reader {
|
|
96
|
+
height: 100vh;
|
|
97
|
+
min-height: 0;
|
|
191
98
|
}
|
|
192
99
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const noteId = await api.createNote({ ...pending.draft, content });
|
|
197
|
-
await pending.complete(noteId);
|
|
198
|
-
pendingCreate.value = null;
|
|
100
|
+
.reader-fill {
|
|
101
|
+
height: 100%;
|
|
102
|
+
min-height: 0;
|
|
199
103
|
}
|
|
200
|
-
</
|
|
201
|
-
|
|
202
|
-
<template>
|
|
203
|
-
<!-- YourNoteModal:业务自有组件 -->
|
|
204
|
-
<YourNoteModal
|
|
205
|
-
:open="!!pendingCreate"
|
|
206
|
-
:quoted-text="pendingCreate?.draft.selectedText"
|
|
207
|
-
@confirm="confirmCreate"
|
|
208
|
-
@cancel="pendingCreate = null"
|
|
209
|
-
/>
|
|
210
|
-
</template>
|
|
104
|
+
</style>
|
|
211
105
|
```
|
|
212
106
|
|
|
213
|
-
|
|
107
|
+
## `ChapterViewerOptions`
|
|
214
108
|
|
|
215
|
-
|
|
109
|
+
`options` 是三端渲染器的统一配置入口。
|
|
216
110
|
|
|
217
|
-
|
|
|
218
|
-
|
|
219
|
-
|
|
|
220
|
-
| `
|
|
221
|
-
| `
|
|
222
|
-
| `
|
|
111
|
+
| 字段 | 说明 |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `manifest` | 章节清单,描述每章 PDF 和全局页码关系 |
|
|
114
|
+
| `notes` | 附注加载、创建、更新、删除回调 |
|
|
115
|
+
| `bookmarks` | 段落书签加载、持久化、删除确认回调 |
|
|
116
|
+
| `chapterPdfLoader` | 可选:全局 PDF 预处理/拉取(见下文) |
|
|
117
|
+
| `overlapStrategy` | 可选:同一全局页多章节重叠时,用先/后出现的章节 PDF 渲染 |
|
|
118
|
+
| `features` | 可选:功能开关与样式配置,省略时使用默认能力 |
|
|
223
119
|
|
|
224
|
-
|
|
120
|
+
### Manifest
|
|
225
121
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
122
|
+
```ts
|
|
123
|
+
type ChapterManifest = {
|
|
124
|
+
chapters: Array<{
|
|
125
|
+
chapterId: string;
|
|
126
|
+
title: string;
|
|
127
|
+
globalPageRange: [number, number];
|
|
128
|
+
localPageRange: [number, number];
|
|
129
|
+
source?: { url: string } | { buffer: ArrayBuffer } | { load: () => Promise<{ url: string } | { buffer: ArrayBuffer }> };
|
|
130
|
+
encrypted?: boolean;
|
|
131
|
+
ownedGlobalPages?: number[];
|
|
132
|
+
}>;
|
|
133
|
+
totalGlobalPages?: number;
|
|
134
|
+
};
|
|
135
|
+
```
|
|
240
136
|
|
|
241
|
-
|
|
137
|
+
`globalPageRange` 和 `localPageRange` 都是闭区间,且页数必须一致。`chapterId` 同时作为内部 `documentId`,需要在整本书内唯一。
|
|
242
138
|
|
|
243
|
-
|
|
244
|
-
2. 用 `<EmbedPDF>` 挂载 `plugins`,在 scoped slot 里等 `pluginsReady === true` 再渲染子树。
|
|
245
|
-
3. 在 `EmbedPDF` 子树内用 `useCapability(ChapterManagerPlugin.id)` 对首章调用 `ensureChapterLoaded`。
|
|
246
|
-
4. 章节状态为 `loaded` 后再挂载 `<PdfChapterViewport :features="bundle.features" />`。
|
|
139
|
+
### 嵌套目录树与重叠页
|
|
247
140
|
|
|
248
|
-
|
|
141
|
+
侧栏目录支持**任意层级**(`ChapterTreePanel` + `ChapterTreeNode.children`)。引擎滚动用的 `manifest.chapters` 是**扁平列表**:父节点页范围可包含子节点,相邻/重叠全局页由 **owner 策略** 决定只渲染一个章节的 PDF,避免同页画两遍。
|
|
249
142
|
|
|
250
|
-
```
|
|
251
|
-
<script setup lang="ts">
|
|
252
|
-
import { computed } from 'vue';
|
|
143
|
+
```ts
|
|
253
144
|
import {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
createChapterViewerBundle,
|
|
257
|
-
type ChapterViewerCatalog,
|
|
145
|
+
buildChapterViewerCatalog,
|
|
146
|
+
overlapStrategyForSamePageOwner,
|
|
258
147
|
type ChapterViewerOptions,
|
|
259
148
|
} from '@embedpdf-editor/vue3-chapter-viewer';
|
|
260
|
-
import ChapterWorkspace from './ChapterWorkspace.vue';
|
|
261
149
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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>
|
|
150
|
+
// 从业务树生成 tree + manifest(深度优先 flatten,顺序影响重叠页归属)
|
|
151
|
+
const { tree, manifest } = buildChapterViewerCatalog([
|
|
152
|
+
{ chapterId: 'part-1', title: '第一篇', startPage: 1, endPage: 50, source: { url: '/p1.pdf' } },
|
|
153
|
+
{
|
|
154
|
+
chapterId: 'sec-1',
|
|
155
|
+
title: '1.1 节',
|
|
156
|
+
startPage: 5,
|
|
157
|
+
endPage: 20,
|
|
158
|
+
source: { url: '/s1.pdf' },
|
|
159
|
+
children: [/* ... */],
|
|
160
|
+
},
|
|
161
|
+
]);
|
|
283
162
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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>
|
|
163
|
+
const options: ChapterViewerOptions = {
|
|
164
|
+
manifest,
|
|
165
|
+
overlapStrategy: overlapStrategyForSamePageOwner('last'), // 重叠页用 manifest 中「后出现」的章节
|
|
166
|
+
// overlapStrategy: { kind: 'first-wins' }, // 默认:先出现的章节
|
|
167
|
+
notes: { /* ... */ },
|
|
168
|
+
bookmarks: { /* ... */ },
|
|
169
|
+
};
|
|
301
170
|
```
|
|
302
171
|
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
}>();
|
|
172
|
+
| `overlapStrategy` | 同一全局页多个章节条目时 |
|
|
173
|
+
| --- | --- |
|
|
174
|
+
| `{ kind: 'first-wins' }`(默认) | 使用 **manifest 中先出现** 的章节的 PDF |
|
|
175
|
+
| `{ kind: 'last-wins' }` | 使用 **后出现** 的章节(目录 flatten 后,重叠页常以「当前页最后一节」为准) |
|
|
176
|
+
| `{ kind: 'explicit' }` | 各章在 `ownedGlobalPages` 上声明归属页 |
|
|
177
|
+
| `{ kind: 'custom', resolve }` | 完全自定义 |
|
|
322
178
|
|
|
323
|
-
|
|
324
|
-
const chapterReady = ref(false);
|
|
179
|
+
简写:`overlapStrategyForSamePageOwner('first' | 'last')`。
|
|
325
180
|
|
|
326
|
-
|
|
327
|
-
ChapterManagerPlugin.id,
|
|
328
|
-
);
|
|
181
|
+
**注意**:仅作分组、没有独立 PDF 的父节点不要放进 flatten 后的 manifest;只保留需要加载 PDF 的节点。Demo:`examples/chapter-viewer-demo-vue3/src/demo/load-catalog.ts`。
|
|
329
182
|
|
|
330
|
-
|
|
331
|
-
[chapterManager, () => props.firstChapterId],
|
|
332
|
-
([mgr, chapterId], _prev, onCleanup) => {
|
|
333
|
-
if (!mgr || !chapterId) return;
|
|
183
|
+
### PDF 加载与预处理
|
|
334
184
|
|
|
335
|
-
|
|
336
|
-
chapterReady.value = false;
|
|
185
|
+
打开章节前,引擎按以下优先级解析 PDF(`ChapterManagerPlugin.resolvePdfPayload`):
|
|
337
186
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
});
|
|
187
|
+
1. `chapter.source.url` / `source.buffer` — 直接使用
|
|
188
|
+
2. `chapter.source.load()` — **按章**异步预处理,返回 `{ url }` 或 `{ buffer }`
|
|
189
|
+
3. `chapterPdfLoader.loadPdf(chapter)` — **全局**统一逻辑
|
|
342
190
|
|
|
343
|
-
|
|
344
|
-
if (!cancelled && status === 'loaded') chapterReady.value = true;
|
|
345
|
-
});
|
|
191
|
+
**按章 `source.load`(单章解密、鉴权 URL):**
|
|
346
192
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
193
|
+
```ts
|
|
194
|
+
{
|
|
195
|
+
chapterId: 'ch-1',
|
|
196
|
+
title: '第一章',
|
|
197
|
+
globalPageRange: [1, 10],
|
|
198
|
+
localPageRange: [0, 9],
|
|
199
|
+
source: {
|
|
200
|
+
load: async () => {
|
|
201
|
+
const bytes = await fetchDecryptedPdf('ch-1');
|
|
202
|
+
return { buffer: bytes };
|
|
203
|
+
// 或 return { url: URL.createObjectURL(blob) };
|
|
204
|
+
},
|
|
351
205
|
},
|
|
352
|
-
|
|
353
|
-
);
|
|
354
|
-
</script>
|
|
355
|
-
|
|
356
|
-
<template>
|
|
357
|
-
<div style="display: flex; flex: 1; min-height: 0; gap: 12px">
|
|
358
|
-
<ChapterTreePanel
|
|
359
|
-
:tree="tree"
|
|
360
|
-
:active-chapter-id="activeChapterId"
|
|
361
|
-
@active-chapter-change="activeChapterId = $event"
|
|
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>
|
|
206
|
+
}
|
|
371
207
|
```
|
|
372
208
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
---
|
|
376
|
-
|
|
377
|
-
## 笔记与书签回调
|
|
209
|
+
**全局 `chapterPdfLoader`(所有章走同一套 API):**
|
|
378
210
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
### 笔记 `options.notes`
|
|
382
|
-
|
|
383
|
-
| 回调 | 说明 |
|
|
384
|
-
|------|------|
|
|
385
|
-
| `loadNotes` | 初始化加载已有笔记 |
|
|
386
|
-
| `onRequestCreateNote` | **推荐**:宿主弹窗后 `complete(noteId)` |
|
|
387
|
-
| `onCreateNote` | 内置创建(与上一项二选一) |
|
|
388
|
-
| `onRequestEditNote` / `onUpdateNote` / `onDeleteNote` | 编辑、更新、删除 |
|
|
389
|
-
|
|
390
|
-
### 书签 `options.bookmarks`
|
|
211
|
+
```ts
|
|
212
|
+
import type { ChapterViewerOptions, IChapterPdfLoader } from '@embedpdf-editor/vue3-chapter-viewer';
|
|
391
213
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
214
|
+
const loader: IChapterPdfLoader = {
|
|
215
|
+
async loadPdf(chapter) {
|
|
216
|
+
const res = await api.getChapterPdf(chapter.chapterId);
|
|
217
|
+
return { url: res.signedUrl };
|
|
218
|
+
// 或 return { buffer: await res.arrayBuffer() };
|
|
219
|
+
},
|
|
220
|
+
};
|
|
397
221
|
|
|
398
|
-
|
|
222
|
+
const options: ChapterViewerOptions = {
|
|
223
|
+
manifest: { chapters: [/* 可省略各条 source */] },
|
|
224
|
+
chapterPdfLoader: loader,
|
|
225
|
+
notes: { /* ... */ },
|
|
226
|
+
bookmarks: { /* ... */ },
|
|
227
|
+
};
|
|
228
|
+
```
|
|
399
229
|
|
|
400
|
-
##
|
|
230
|
+
## Worker 与 WASM
|
|
401
231
|
|
|
402
|
-
|
|
232
|
+
`usePdfiumEngine()` 默认使用 `@embedpdf/engines` 内置的 PDFium CDN wasm 地址,并默认启用 worker:
|
|
403
233
|
|
|
404
|
-
|
|
234
|
+
```ts
|
|
235
|
+
usePdfiumEngine();
|
|
236
|
+
```
|
|
405
237
|
|
|
406
|
-
|
|
238
|
+
如果业务要求内网部署、固定版本或自定义 CDN,再传入自托管地址:
|
|
407
239
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
| `createChapterViewerBundle` | 生成 `{ plugins, features }`(自定义布局) |
|
|
412
|
-
| `PdfChapterViewport` | 仅视口(需在 `EmbedPDF` 内) |
|
|
413
|
-
| `ChapterTreePanel` | 章节目录树 |
|
|
414
|
-
| `EmbedPDF` / `useCapability` | 插件宿主与能力钩子 |
|
|
415
|
-
| `usePdfiumEngine` | PDFium 引擎(返回 ref) |
|
|
416
|
-
| `ChapterManagerPlugin` | 章节加载(`ensureChapterLoaded`) |
|
|
417
|
-
| `ChapterViewerOptions` | `options` 的类型 |
|
|
418
|
-
| `ChapterViewerConfig` | `options.features` 的类型 |
|
|
419
|
-
| `ChapterManifest` / `ChapterDescriptor` / `IChapterPdfLoader` | 数据与加载契约 |
|
|
420
|
-
| `ChapterViewerCatalog` / `ChapterTreeNode` | 目录树类型 |
|
|
421
|
-
| `applySelectionMarkup` | 编程式应用划线(高级) |
|
|
422
|
-
| `chapterViewerViteResolve` | Vite 配置辅助(`/vite` 子路径) |
|
|
240
|
+
```ts
|
|
241
|
+
usePdfiumEngine({ wasmUrl: '/assets/pdfium.wasm', worker: true });
|
|
242
|
+
```
|
|
423
243
|
|
|
424
|
-
|
|
244
|
+
使用 `worker: true` 时,部署环境需要允许 worker 加载 wasm。若只在 worker 模式失败,可先用 `worker: false` 定位资源或响应头问题。
|
|
425
245
|
|
|
426
|
-
|
|
246
|
+
## 进阶组合
|
|
427
247
|
|
|
428
|
-
|
|
248
|
+
内置 `ChapterPdfViewer` 已完成插件注册和章节视口渲染。需要插入自定义 shell、目录、工具栏或直接访问 registry 时,可以使用低层组合:
|
|
429
249
|
|
|
430
|
-
|
|
250
|
+
```vue
|
|
251
|
+
<script setup lang="ts">
|
|
252
|
+
import {
|
|
253
|
+
EmbedPDF,
|
|
254
|
+
PdfChapterViewport,
|
|
255
|
+
createChapterViewerBundle,
|
|
256
|
+
} from '@embedpdf-editor/vue3-chapter-viewer';
|
|
431
257
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
3. **首章加载**:自定义布局下,首章 `ensureChapterLoaded` 返回 `loaded` 后再挂载 `PdfChapterViewport`。
|
|
435
|
-
4. **笔记 UI**:`onRequestCreateNote` 需在宿主弹窗后调用 `complete(noteId)`。
|
|
436
|
-
5. **Vue ref**:`usePdfiumEngine()` 的 `engine` 在 script 中为 ref,传给 `ChapterPdfViewer` 时模板写 `:engine="engine"` 即可。
|
|
258
|
+
const { plugins, features } = createChapterViewerBundle(options);
|
|
259
|
+
</script>
|
|
437
260
|
|
|
438
|
-
|
|
261
|
+
<template>
|
|
262
|
+
<EmbedPDF :engine="engine" :plugins="plugins">
|
|
263
|
+
<template #default="{ pluginsReady }">
|
|
264
|
+
<PdfChapterViewport v-if="pluginsReady" :features="features" />
|
|
265
|
+
</template>
|
|
266
|
+
</EmbedPDF>
|
|
267
|
+
</template>
|
|
268
|
+
```
|
|
439
269
|
|
|
440
|
-
|
|
270
|
+
`createChapterViewerEditorOptions()` 只生成旧式 editor 配置,不含 `features`;新代码优先使用 `options` 或 `createChapterViewerBundle(options)`。
|
|
441
271
|
|
|
442
|
-
##
|
|
272
|
+
## 标注导入导出
|
|
443
273
|
|
|
444
|
-
|
|
445
|
-
|----|------|
|
|
446
|
-
| `@embedpdf-editor/react-chapter-viewer` | React 18+ |
|
|
447
|
-
| `@embedpdf-editor/chapter-snippet` | Vue 2.6+ Web Component / 无构建集成 |
|
|
274
|
+
包内直接导出章节标注 IO:
|
|
448
275
|
|
|
449
|
-
|
|
276
|
+
```ts
|
|
277
|
+
import {
|
|
278
|
+
exportAllChapterAnnotations,
|
|
279
|
+
importChapterAnnotationsArchive,
|
|
280
|
+
chapterAnnotationsArchiveToJson,
|
|
281
|
+
parseChapterAnnotationsArchiveJson,
|
|
282
|
+
useRegistry,
|
|
283
|
+
} from '@embedpdf-editor/vue3-chapter-viewer';
|
|
284
|
+
```
|
|
450
285
|
|
|
451
|
-
|
|
286
|
+
导出格式包含 `bookmarks`、`notes`、`markup`,版本常量为 `CHAPTER_ANNOTATIONS_ARCHIVE_VERSION`。导入时可选择 `mode: 'replace' | 'merge'`,并可传 `persistNotes` / `persistBookmarks` 把导入结果写回业务存储。
|
|
452
287
|
|
|
453
288
|
## 常见问题
|
|
454
289
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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` 后硬刷新浏览器。
|
|
468
|
-
|
|
469
|
-
**Q:笔记弹窗想完全自定义?**
|
|
470
|
-
使用 `onRequestCreateNote` + `complete(noteId)`,不要实现 `onCreateNote`。
|
|
471
|
-
|
|
472
|
-
**Q:`useCapability` 报错 “must be used inside EmbedPDF”?**
|
|
473
|
-
`ChapterWorkspace` 等子组件必须渲染在 `<EmbedPDF>` 的默认 slot 内,不能与它平级。
|
|
290
|
+
| 现象 | 处理 |
|
|
291
|
+
| --- | --- |
|
|
292
|
+
| 一直停在引擎加载 | 如果传了自定义 `wasmUrl`,检查地址是否 200、MIME/跨源头是否正确 |
|
|
293
|
+
| 只显示空白 | 确认外层容器有高度,且章节 `source.url` 可访问 |
|
|
294
|
+
| 页码或滚动错位 | 检查 `globalPageRange` 与 `localPageRange` 页数是否一致 |
|
|
295
|
+
| 划词笔记没有保存 | 实现 `notes.onCreateNote` 或自定义 `onRequestCreateNote` 流程 |
|
|
296
|
+
| 书签删除无效 | `bookmarks.onRequestRemove` 需要返回 `true` 才会删除 |
|