@embedpdf-editor/react-chapter-viewer 0.1.0 → 0.2.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 CHANGED
@@ -1,90 +1,395 @@
1
1
  # @embedpdf-editor/react-chapter-viewer
2
2
 
3
- 章节 PDF 阅读器 React 统一入口:划线、笔记、书签、选区浮窗、可选缩放。
3
+ 面向 React 的**章节 PDF 阅读器**统一入口:多章 PDF 纵向拼接滚动、划词高亮/划线、段落书签、选区笔记,以及可选缩放。
4
+
5
+ 业务侧只需安装本包与 `react` / `react-dom`;PDFium(`@embedpdf/engines`)、`scheduler` 及阅读器依赖的 `@embedpdf/*` 插件由本包 **dependencies** 带入,**不必**在业务 `package.json` 里逐个声明 `@embedpdf/engines` 等。
6
+
7
+ > 组件样式以 **inline style** 为主,**不需要** Tailwind。
8
+ > 完整可运行示例见 monorepo:`examples/chapter-viewer-demo-react`。
9
+
10
+ ---
4
11
 
5
12
  ## 安装
6
13
 
7
14
  ```bash
8
- pnpm add @embedpdf-editor/react-chapter-viewer react react-dom
15
+ pnpm add @embedpdf-editor/react-chapter-viewer
16
+ # 或 npm / yarn
9
17
  ```
10
18
 
11
- `@embedpdf/engines`(PDFium WASM)、`scheduler`、`@embedpdf/*` 插件与 editor 能力由本包作为依赖一并安装,无需在业务项目里逐个声明。
19
+ | 依赖 | 说明 |
20
+ |------|------|
21
+ | `react` `react-dom` | **peerDependencies**(建议 React 18+) |
22
+ | 本包 `dependencies` | 已包含 `@embedpdf/engines`、`scheduler`、章节插件与 `editor-engine` |
23
+
24
+ ---
12
25
 
13
- 目录与 PDF 均由业务提供:`ChapterManifest` 中每章的 `source.url` 写 PDF 地址;若需统一鉴权下载,可实现 `IChapterPdfLoader` 并传入 `chapterPdfLoader`(或单章 `source.load`)。
26
+ ## Vite 项目(推荐)
14
27
 
15
- ### Vite
28
+ dev 时出现 `scheduler` 解析失败,可合并本包提供的 Vite 片段(将 `scheduler` 指到本包依赖目录并加入预构建):
16
29
 
17
30
  ```ts
31
+ // vite.config.ts
18
32
  import { defineConfig, mergeConfig } from 'vite';
33
+ import react from '@vitejs/plugin-react';
19
34
  import { chapterViewerViteResolve } from '@embedpdf-editor/react-chapter-viewer/vite';
20
35
 
21
- export default mergeConfig(defineConfig({ /* ... */ }), chapterViewerViteResolve());
36
+ export default mergeConfig(
37
+ defineConfig({
38
+ plugins: [react()],
39
+ }),
40
+ chapterViewerViteResolve(),
41
+ );
42
+ ```
43
+
44
+ ---
45
+
46
+ ## 你需要准备的数据
47
+
48
+ 阅读器**不内置**目录与 PDF 文件,全部由业务提供。
49
+
50
+ ### 1. `ChapterManifest`(引擎用)
51
+
52
+ 描述每一章对应的 PDF 与在「整本」中的页码区间:
53
+
54
+ ```ts
55
+ import type { ChapterManifest } from '@embedpdf-editor/react-chapter-viewer';
56
+
57
+ const manifest: ChapterManifest = {
58
+ chapters: [
59
+ {
60
+ chapterId: '001_封面', // 唯一 ID,同时作为 documentId
61
+ title: '封面',
62
+ globalPageRange: [1, 1], // 在整本中的全局页(闭区间)
63
+ localPageRange: [0, 0], // 该 PDF 内 0-based 页(页数须与 global 一致)
64
+ source: { url: '/001_封面.pdf' }, // 或 buffer / load()
65
+ },
66
+ {
67
+ chapterId: '002_前言',
68
+ title: '前言',
69
+ globalPageRange: [2, 5],
70
+ localPageRange: [0, 3],
71
+ source: { url: '/002_前言.pdf' },
72
+ },
73
+ ],
74
+ };
75
+ ```
76
+
77
+ **`ChapterSource` 三种写法:**
78
+
79
+ | 写法 | 场景 |
80
+ |------|------|
81
+ | `{ url: string }` | 静态或可直链的 PDF |
82
+ | `{ buffer: ArrayBuffer }` | 已下载的二进制 |
83
+ | `{ load: () => Promise<{ url } \| { buffer }> }` | 单章自定义拉取(鉴权等) |
84
+
85
+ 若 manifest 里**未写** `source`,需传入全局加载器 `chapterPdfLoader`(实现 `IChapterPdfLoader.loadPdf(chapter)`)。
86
+
87
+ 相邻章节的 `globalPageRange` **允许重叠**(例如上下章各含一页过渡页);默认按 `first-wins` 解析归属。
88
+
89
+ ### 2. `ChapterViewerCatalog`(带目录树时)
90
+
91
+ 左侧章节目录由业务构造,与 manifest 对应:
92
+
93
+ ```ts
94
+ import type { ChapterViewerCatalog } from '@embedpdf-editor/react-chapter-viewer';
95
+
96
+ const catalog: ChapterViewerCatalog = {
97
+ tree: [
98
+ { id: '001_封面', title: '封面', startPage: 1, endPage: 1 },
99
+ {
100
+ id: '002_前言',
101
+ title: '前言',
102
+ startPage: 2,
103
+ endPage: 5,
104
+ children: [/* 可选子节点 */],
105
+ },
106
+ ],
107
+ manifest,
108
+ };
22
109
  ```
23
110
 
24
- ## 快速开始
111
+ `tree` 仅用于 UI;真正加载 PDF 以 `manifest.chapters` 为准。
112
+
113
+ ---
114
+
115
+ ## 快速开始:`<ChapterPdfViewer />`
116
+
117
+ 适合「单容器铺满、manifest 已就绪」的页面。
25
118
 
26
119
  ```tsx
27
120
  import {
28
121
  usePdfiumEngine,
29
122
  ChapterPdfViewer,
30
- type ChapterViewerFeaturesConfig,
123
+ createChapterViewerEditorOptions,
124
+ DEFAULT_CHAPTER_VIEWER_FEATURES,
125
+ } from '@embedpdf-editor/react-chapter-viewer';
126
+
127
+ export function Reader({ manifest }: { manifest: ChapterManifest }) {
128
+ const { engine, isLoading, error } = usePdfiumEngine();
129
+
130
+ if (error) return <div>引擎失败:{error.message}</div>;
131
+ if (isLoading || !engine) return <div>正在加载 PDFium…</div>;
132
+
133
+ const editorOptions = createChapterViewerEditorOptions({
134
+ manifest,
135
+ bookmarks: {
136
+ callbacks: {
137
+ load: () => fetchBookmarks(),
138
+ persist: (list) => saveBookmarks(list),
139
+ onRequestRemove: async (b) => {
140
+ await api.deleteBookmark(b.id);
141
+ return true;
142
+ },
143
+ },
144
+ },
145
+ notes: {
146
+ callbacks: {
147
+ loadNotes: () => fetchNotes(),
148
+ onRequestCreateNote: ({ draft, complete }) => {
149
+ // 打开你的弹窗,用户确认后:
150
+ openCreateModal(draft).then((noteId) => complete(noteId));
151
+ },
152
+ onRequestEditNote: (noteId) => openEditModal(noteId),
153
+ onDeleteNote: (id) => api.deleteNote(id),
154
+ },
155
+ },
156
+ });
157
+
158
+ return (
159
+ <div style={{ height: '100vh' }}>
160
+ <ChapterPdfViewer
161
+ engine={engine}
162
+ editorOptions={editorOptions}
163
+ features={{
164
+ ...DEFAULT_CHAPTER_VIEWER_FEATURES,
165
+ zoom: { enabled: true, pageWidth: 800 },
166
+ }}
167
+ onExtraSelectionAction={(actionId) => {
168
+ if (actionId === 'translate') {
169
+ /* 选区浮窗扩展按钮 */
170
+ }
171
+ }}
172
+ />
173
+ </div>
174
+ );
175
+ }
176
+ ```
177
+
178
+ **交互说明(默认开启时):**
179
+
180
+ - 划词 → 浮窗:高亮、下划线、波浪线、删除线、笔记
181
+ - 鼠标移到文本行末 → 显示「添加书签」;已加书签点击 → 删除确认(走 `onRequestRemove`)
182
+ - 笔记区域悬停 → 编辑 / 删除
183
+ - `Ctrl/Cmd + 滚轮`、双指捏合 → 缩放(写入文档 `scale`,非单纯 CSS 放大)
184
+
185
+ ---
186
+
187
+ ## 进阶:自定义布局(目录 + 首章预加载)
188
+
189
+ 官方 React Demo 使用 **`EmbedPDF` + `PdfChapterViewport` + `ChapterTreePanel`**,便于:
190
+
191
+ - 左侧树切换章节(`ChapterManagerPlugin.ensureChapterLoaded`)
192
+ - 首章加载完成后再挂载视口,避免空白
193
+
194
+ 核心步骤:
195
+
196
+ ```tsx
197
+ import {
198
+ usePdfiumEngine,
199
+ EmbedPDF,
200
+ useCapability,
201
+ ChapterManagerPlugin,
202
+ ChapterTreePanel,
203
+ PdfChapterViewport,
204
+ createChapterViewerEditor,
205
+ createChapterViewerEditorOptions,
206
+ DEFAULT_CHAPTER_VIEWER_FEATURES,
31
207
  } from '@embedpdf-editor/react-chapter-viewer';
32
208
 
209
+ function App({ catalog }: { catalog: ChapterViewerCatalog }) {
210
+ const { engine } = usePdfiumEngine();
211
+ const editorOptions = useMemo(
212
+ () => createChapterViewerEditorOptions({ manifest: catalog.manifest, /* bookmarks, notes */ }),
213
+ [catalog],
214
+ );
215
+ const plugins = useMemo(
216
+ () =>
217
+ createChapterViewerEditor({
218
+ ...editorOptions,
219
+ features: DEFAULT_CHAPTER_VIEWER_FEATURES,
220
+ }).plugins,
221
+ [editorOptions],
222
+ );
223
+
224
+ if (!engine) return null;
225
+
226
+ return (
227
+ <EmbedPDF engine={engine} plugins={plugins}>
228
+ {({ pluginsReady }) =>
229
+ pluginsReady ? (
230
+ <Layout catalog={catalog} />
231
+ ) : (
232
+ <div>正在初始化插件…</div>
233
+ )
234
+ }
235
+ </EmbedPDF>
236
+ );
237
+ }
238
+
239
+ function Layout({ catalog }: { catalog: ChapterViewerCatalog }) {
240
+ const [activeId, setActiveId] = useState(catalog.manifest.chapters[0]?.chapterId ?? '');
241
+ const { provides: chapterManager } = useCapability(ChapterManagerPlugin.id);
242
+ const [ready, setReady] = useState(false);
243
+
244
+ useEffect(() => {
245
+ if (!chapterManager || !activeId) return;
246
+ void chapterManager.ensureChapterLoaded(activeId).then((s) => setReady(s === 'loaded'));
247
+ }, [chapterManager, activeId]);
248
+
249
+ return (
250
+ <div style={{ display: 'flex', height: '100%' }}>
251
+ <ChapterTreePanel
252
+ tree={catalog.tree}
253
+ activeChapterId={activeId}
254
+ onActiveChapterChange={setActiveId}
255
+ />
256
+ <div style={{ flex: 1, minWidth: 0 }}>
257
+ {ready ? (
258
+ <PdfChapterViewport features={DEFAULT_CHAPTER_VIEWER_FEATURES} />
259
+ ) : (
260
+ <div>正在加载章节 PDF…</div>
261
+ )}
262
+ </div>
263
+ </div>
264
+ );
265
+ }
266
+ ```
267
+
268
+ 参考实现:`examples/chapter-viewer-demo-react/src/App.tsx`。
269
+
270
+ ---
271
+
272
+ ## 笔记与书签回调
273
+
274
+ ### 笔记 `editorOptions.notes.callbacks`
275
+
276
+ | 回调 | 说明 |
277
+ |------|------|
278
+ | `loadNotes` | 初始化加载已有笔记 |
279
+ | `onRequestCreateNote` | **推荐**:只发事件,宿主弹窗后 `complete(noteId)` |
280
+ | `onCreateNote` | 内置流程:直接 `Promise<{ noteId }>`(与上一项二选一) |
281
+ | `onRequestEditNote` | 自定义编辑,传出 `noteId` |
282
+ | `onUpdateNote` / `onDeleteNote` | 更新、删除 |
283
+
284
+ ### 书签 `editorOptions.bookmarks.callbacks`
285
+
286
+ | 回调 | 说明 |
287
+ |------|------|
288
+ | `load` | 加载已有书签 |
289
+ | `persist` | 增删改后持久化整表 |
290
+ | `onRequestRemove` | 用户点删除 → 业务删库 → 返回 `true` 后渲染器移除 |
291
+ | `onRemoveSuccess` | 移除后的收尾通知 |
292
+
293
+ ---
294
+
295
+ ## 功能配置 `features`
296
+
297
+ 通过 `ChapterPdfViewer` 的 `features`,或 `createChapterViewerEditor({ features })` 传入。默认见 `DEFAULT_CHAPTER_VIEWER_FEATURES`。
298
+
299
+ ```ts
300
+ import type { ChapterViewerFeaturesConfig } from '@embedpdf-editor/react-chapter-viewer';
301
+
33
302
  const features: ChapterViewerFeaturesConfig = {
34
303
  markup: {
35
304
  enabled: true,
36
305
  styles: {
37
- underline: { color: '#dc2626', offsetY: 3, thickness: 1.5 },
306
+ underline: { color: '#dc2626', thickness: 1.5, offsetY: 3 },
307
+ highlight: { color: '#facc15', opacity: 0.35 },
38
308
  },
39
309
  },
40
- zoom: { enabled: false },
310
+ bookmarks: { enabled: true },
311
+ notes: { enabled: true },
41
312
  selectionToolbar: {
313
+ enabled: true,
314
+ hiddenBuiltinActions: ['squiggly'],
42
315
  extraActions: [{ id: 'translate', label: '翻译', order: 10 }],
43
316
  },
317
+ zoom: {
318
+ enabled: true,
319
+ min: 0.5,
320
+ max: 3,
321
+ initial: 1,
322
+ pageWidth: 800, // 按 CSS 宽度适配首页 PDF
323
+ },
44
324
  };
325
+ ```
45
326
 
46
- export function App({ manifest, tree }) {
47
- const { engine } = usePdfiumEngine();
48
- if (!engine) return null;
327
+ `onExtraSelectionAction` / `buildSelectionMenu` 可处理 `extraActions` 或完全自定义选区菜单。
49
328
 
50
- return (
51
- <ChapterPdfViewer
52
- engine={engine}
53
- editorOptions={{
54
- // manifest.chapters[].source.url 由业务提供 PDF 地址
55
- manifest,
56
- notes: {
57
- callbacks: {
58
- onRequestCreateNote: ({ draft, complete }) => {
59
- openYourModal(draft).then((id) => complete(id));
60
- },
61
- onRequestEditNote: (noteId) => openYourEditor(noteId),
62
- loadNotes: () => api.loadNotes(),
63
- onDeleteNote: (id) => api.deleteNote(id),
64
- },
65
- },
66
- bookmarks: { callbacks: { load: () => api.loadBookmarks(), persist: api.save } },
67
- }}
68
- features={features}
69
- onExtraSelectionAction={(id) => {
70
- if (id === 'translate') { /* ... */ }
71
- }}
72
- />
73
- );
74
- }
75
- ```
329
+ ---
330
+
331
+ ## 主要导出
332
+
333
+ | 导出 | 用途 |
334
+ |------|------|
335
+ | `ChapterPdfViewer` | 一站式阅读器(引擎 + 插件 + 视口 + 缩放) |
336
+ | `PdfChapterViewport` | 仅 PDF 滚动视口(需在 `EmbedPDF` 内) |
337
+ | `ChapterTreePanel` | 章节目录树(需在 `EmbedPDF` 内) |
338
+ | `usePdfiumEngine` | 创建 PDFium `engine` |
339
+ | `EmbedPDF` / `useCapability` | 插件宿主与能力访问 |
340
+ | `createChapterViewerEditorOptions` | 生成 `editorOptions`(manifest、书签、笔记) |
341
+ | `createChapterViewerEditor` | 生成 `plugins` 数组 |
342
+ | `DEFAULT_CHAPTER_VIEWER_FEATURES` | 默认功能开关 |
343
+ | `ChapterManifest` / `ChapterDescriptor` / `IChapterPdfLoader` | 数据与加载契约 |
344
+ | `ChapterViewerCatalog` / `ChapterTreeNode` | 目录树类型 |
345
+ | `applySelectionMarkup` | 编程式应用划线(高级) |
346
+ | `chapterViewerViteResolve` | Vite 配置辅助(`/vite` 子路径) |
76
347
 
77
- ## Vue3 / Vue2
348
+ 类型详见 `dist/index.d.ts`。
78
349
 
79
- - `@embedpdf-editor/vue3-chapter-viewer`
80
- - `@embedpdf-editor/vue2-chapter-viewer`
350
+ ---
81
351
 
82
- 视口内核仍是 React 实现的 `ChapterPdfViewer`,Vue 组件只在 DOM 节点上通过 `react-dom/client` 挂载,因此 **还需安装 `react`、`react-dom`**,且全应用只能有一份 React(勿重复打包进阅读器 dist)。
352
+ ## 本地运行官方 Demo
83
353
 
84
354
  ```bash
85
- pnpm add @embedpdf-editor/vue3-chapter-viewer vue react react-dom
355
+ # monorepo 根目录
356
+ pnpm install
357
+ pnpm --filter @embedpdf-editor/react-chapter-viewer build
358
+ pnpm --filter @embedpdf-editor/example-chapter-viewer-react dev
86
359
  ```
87
360
 
88
- Vite 使用 `@embedpdf-editor/vue3-chapter-viewer/vite` 导出同样的 `chapterViewerViteResolve()`。
361
+ 将章节 PDF `mock.json` 放在 demo 的 `public/` 下(`title` 字段对应 `/{title}.pdf`)。Demo 的 `loadDemoCatalog` 仅作示例,生产环境请自行请求后端并组装 `ChapterViewerCatalog`。
362
+
363
+ 构建静态站点:
364
+
365
+ ```bash
366
+ pnpm --filter @embedpdf-editor/example-chapter-viewer-react build
367
+ pnpm --filter @embedpdf-editor/example-chapter-viewer-react preview
368
+ ```
369
+
370
+ ---
371
+
372
+ ## 其他框架
373
+
374
+ | 包 | 说明 |
375
+ |----|------|
376
+ | `@embedpdf-editor/vue3-chapter-viewer` | Vue 3 组件封装 |
377
+ | `@embedpdf-editor/vue2-chapter-viewer` | Vue 2.6+(或 `@embedpdf-editor/chapter-snippet` Web Component) |
378
+
379
+ Vue 3 包同样只需安装对应包 + `vue`;视口内核与 React 版能力对齐。
380
+
381
+ ---
382
+
383
+ ## 常见问题
384
+
385
+ **Q:页面一直「正在加载」?**
386
+ 确认 `manifest` 中每章 `source.url` 可访问,或 `chapterPdfLoader` / `ensureChapterLoaded` 返回 `loaded`。首章未加载完成时不要提前挂载 `PdfChapterViewport`。
387
+
388
+ **Q:需要用户安装 `@embedpdf/engines` 吗?**
389
+ 不需要,已作为本包依赖。只需 `react` / `react-dom`。
390
+
391
+ **Q:如何关闭缩放?**
392
+ `features={{ ...DEFAULT_CHAPTER_VIEWER_FEATURES, zoom: { enabled: false } }}`。
89
393
 
90
- 使用 `<ChapterPdfViewer :viewer-props="..." />`。
394
+ **Q:笔记弹窗想完全自定义?**
395
+ 使用 `onRequestCreateNote` + `complete(noteId)`,不要实现 `onCreateNote`。