@embedpdf-editor/chapter-snippet
v1.4.5
Published
框架无关的章节 PDF 阅读器运行时。它把 Preact 阅读器封装成 Web Component。
Downloads
833
Readme
@embedpdf-editor/chapter-snippet
框架无关的章节 PDF 阅读器运行时。它把 Preact 阅读器封装成 Web Component。
Vue 2 请使用本包(@embedpdf-editor/chapter-snippet),不要用已移除的 @embedpdf-editor/vue2-chapter-viewer。宿主为 Vue 2 / 传统页面 / 微前端,或任何不想引入 React/Vue3 渲染器的场景。
能力与 React/Vue3 渲染器保持一致:多章连续滚动、划线/高亮、附注、段落书签、章节目录和标注导入导出。
安装
pnpm add @embedpdf-editor/chapter-snippetVite
chapterSnippetViteResolve() 用于 Vite + Vue2/snippet 场景:
// vite.config.ts
import { defineConfig } from 'vite';
import { chapterSnippetViteResolve } from '@embedpdf-editor/chapter-snippet/vite';
export default defineConfig({
...chapterSnippetViteResolve(),
});它会:
| 配置 | 作用 |
| --- | --- |
| server.headers | 开发环境添加 COOP/COEP,满足 worker/wasm 常见要求 |
| optimizeDeps.exclude | 避免 Vite 二次预构建 snippet 和 PDFium 引擎 |
Vue CLI / Webpack
无需修改 vue.config.js。 安装后直接:
import ChapterEmbedPDF from '@embedpdf-editor/chapter-snippet';发包构建会对 dist/embedpdf-chapter.js 做语法降级(去掉 ?? 等 ES2020 语法),Webpack / Vue CLI 默认可 parse。默认 wasmUrl 指向 jsDelivr 上的 pdfium.wasm,无需复制到 public/。
离线或内网部署时,可传 wasmUrl(init 顶层,与 options 同级):
// 相对路径:自行把 dist/pdfium.wasm 放到静态目录
wasmUrl: '/pdfium.wasm',
// 自有 OSS / CDN(完整 HTTPS 地址,示例)
wasmUrl:
'https://hep-editor.oss-cn-beijing.aliyuncs.com/public/editor-public/js/pdfium.wasm',详见 docs/get-started/01-installation.md。
@embedpdf-editor/chapter-snippet/webpack 为可选辅助(仅 monorepo Vue 2.6 解析、或自定义 devServer COOP/COEP),普通用户不必使用。
完整示例见 examples/chapter-viewer-demo-vue2。
快速开始
不需要把 pdfium.wasm 手动放到宿主项目的 public/。默认会从 @embedpdf-editor/chapter-snippet/dist/pdfium.wasm 加载。只有当你要走 CDN 或自定义静态域名时,才需要传 wasmUrl。
import ChapterEmbedPDF, {
CHAPTER_SNIPPET_EVENTS,
} from '@embedpdf-editor/chapter-snippet';
const viewer = ChapterEmbedPDF.init({
type: 'container',
target: document.getElementById('reader'),
worker: true,
options: {
manifest: {
chapters: [
{
chapterId: 'ch-1',
title: '第一章',
globalPageRange: [1, 12],
localPageRange: [0, 11],
source: { url: '/pdfs/chapter-1.pdf' },
},
],
},
notes: {
loadNotes: async () => [],
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 },
},
},
});
viewer?.addEventListener(CHAPTER_SNIPPET_EVENTS.noteRequestCreate, (event) => {
const { record, complete } = event.detail;
openNoteDialog(record, async (content) => {
const noteId = 'note-' + Date.now();
const nodeId = await saveNote(content, record);
await complete({ noteId, content, nodeId });
});
});
viewer?.addEventListener(CHAPTER_SNIPPET_EVENTS.ready, (event) => {
console.log('registry ready', event.detail.registry);
});容器需要稳定高度:
#reader {
height: 100vh;
min-height: 0;
}配置
| 字段 | 默认 | 说明 |
| --- | --- | --- |
| type | 必填 | 目前使用 'container' |
| target | 必填 | 挂载目标元素 |
| options | 必填 | 推荐的章节阅读器配置,和 React/Vue3 的 options 语义一致 |
| wasmUrl | dist/pdfium.wasm | 自定义 PDFium wasm 地址 |
| fontFallback | 默认 jsDelivr | 自定义 PDFium 缺字 fallback 字体地址;可换成业务 CDN/OSS |
| worker | true | 是否启用 PDFium worker |
| fallbackToDirectEngine | true | 首章 worker 加载超时后自动降级到 direct engine |
| workerOpenTimeoutMs | 8000 | 首章加载超时时间 |
| features | - | 与 options.features 深度合并;顶层同名字段优先。勿放进 createChapterViewerEditorOptions() |
| className / viewportClassName | - | 传给内部阅读器容器 |
旧版本的 editorInput 仍可使用,但新代码应改为 options,并把 features 写在 options.features 或顶层 features。
字体 fallback CDN
PDF 缺少内嵌字体时,PDFium 会按需请求 fallback 字体。默认使用 jsDelivr;如果字体加载慢,可锁定版本或改成业务 CDN/OSS:
import ChapterEmbedPDF, {
FontCharset,
createCdnFontConfig,
} from '@embedpdf-editor/chapter-snippet';
ChapterEmbedPDF.init({
type: 'container',
target: document.getElementById('viewer'),
options,
// 锁定 @embedpdf/fonts-* 版本,避免 latest 解析抖动
fontFallback: createCdnFontConfig('1.0.0'),
});
ChapterEmbedPDF.init({
type: 'container',
target: document.getElementById('viewer'),
options,
// 完全自定义简体中文字体地址
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。
PDF 三步加载(Vue 2 推荐)
| 步骤 | 说明 |
| --- | --- |
| 1 | manifest.chapters:仅页码;segmentPageThreshold 写在章节上 |
| 2 | chapterPdfLoader.loadChapterUrls:按章 getOneChap,每章只调一次 |
| 3 | chapterPdfLoader.openPdf(可选):解密等;省略则直接打开 ctx.url |
options: {
manifest: {
chapters: [
{
chapterId: item._id,
title: item.title,
globalPageRange: [item.startPage, item.endPage],
localPageRange: [0, item.endPage - item.startPage],
segmentPageThreshold: item.page,
},
],
},
chapterPdfLoader: {
async loadChapterUrls(chapter) {
const res = await getOneChap(chapter.chapterId);
if (!res.success) throw new Error(res.message);
const raw = res.data.resourceUrl;
return Array.isArray(raw) ? raw : raw ? [raw] : [];
},
async openPdf(ctx) {
const decrypted = await decryptPdf(ctx.url);
return { buffer: decrypted }; // ArrayBuffer / Uint8Array / DataView 都支持
},
},
notes: { /* ... */ },
bookmarks: { /* ... */ },
},勿在 loader 外再请求章节详情;勿把 segmentPageThreshold 放进 source。
03-manifest.md · 12-segmented-pdf-and-per-chapter-storage.md
按章持久化
options.notes / options.bookmarks 回调里的 chapterId、localPageIndex 与单 URL 章相同。划线备份用 exportChapterAnnotations,JSON 键为 chapters[chapterId](导出 markup 时会拉全部分段)。
ChapterViewerOptions(与 React / Vue3 一致)
| 字段 | 说明 |
| --- | --- |
| manifest | 章节目录,见 React README Manifest |
| notes | NoteCallbacks,见 React README |
| bookmarks | ParagraphBookmarkCallbacks,见 React README |
| chapterPdfLoader / overlapStrategy | 自定义 PDF 加载与重叠页策略 |
| features | 功能开关与 UI 定制,见下 |
加密 PDF 可在底层 createPdfChapterEditor 选项中设置 passwordProvider(如 CallbackPasswordProvider from @embedpdf-editor/chapter-core)。
features 配置
与 React / Vue3 同一套 ChapterViewerConfig(@embedpdf-editor/chapter-viewer)。完整字段表见 React README:features 配置。
| 模块 | 要点 |
| --- | --- |
| markup | styles 四类划线;annotationMenu 默认选中后在下方显示「删除」;onClick 配置后不弹菜单;squiggly.offsetY 默认 4 |
| bookmarks | marker / hover 自定义图标;marker.onClick 配置后不弹删除浮窗 |
| notes | marker.renderIcon、renderMenuActions、iconSize、offsetX/offsetY、highlightColor、highlightStyle(任意 CSS)、alwaysVisible;onClick(高亮区与图标同一事件)、selectedOutline |
| zoom | pageWidth、fitParentWidth(勿与 initial 同配)、clampMaxToViewport(默认 true)、pinchExponent(默认 1)、min / max / enabled;滚轮 / 捏合锚点缩放;移动端推荐 fitParentWidth + clampMaxToViewport: false + max: 8 |
| scrollViewport | background(默认 #f1f5f9),[data-chapter-scroll-viewport] 背景 |
| page | background(默认 #ffffff),单页 PDF 画布背景 |
| selectionToolbar | 两行浮窗、色盘 markupColors、onCopySuccess(复制成功回调,配置后不显示默认「已复制」提示)、dismissToolbarOnAction({ copy?, note? } 默认 true)、dismissToolbarOn(click / scroll,默认 ['click'])、extraActions[].dismissToolbarOnAction(默认 true);或监听 DOM 事件 selectionExtraAction |
| selectionHandles | enabled(默认 true)、color(默认 #2196f3)、size(圆球直径 px,默认 16);移动端长按选词后的可拖动选区手柄,仅触摸设备生效 |
| pageOverlays | 页内矩形框;defaultBoxStyle(任意 CSS)、renderContent、onClick(ctx.payload);数据经 importChapterAnnotations 的 pageOverlays 注入 |
| boxSelect | 用户自框选(PC,默认 enabled:false);defaultBoxStyle、menuAlign(浮窗左/中/右对齐)、renderBox(框内 UI,禁缩放)、renderMenu(浮窗容器);开关 / 增删 / 滚动经 chapter-viewer-ready 拿 registry 后调 box-select capability。见 用户自框选 |
| markup | 除 styles 外可配 onCreated、onRequestDelete(Promise<boolean>,true 才真删)、onDeleted |
| bookmarks(options) | 除 load / persist 外可配 onAdded(新增书签,含 position) |
移动端交互开箱即用,无需配置:单指滑动只滚动 PDF,绝不触发划词(严格);长按约 450ms 选中所在词并弹出浮窗。双指捏合以两指中心为锚点缩放(pinchExponent: 1 默认 1:1)。仅阅读模式生效,与桌面 Ctrl+滚轮互不影响(Shadow DOM 内已自动注入所需样式)。Vue 2 / Web Component 端仍通过 selectionToolbar 配置和 selectionExtraAction 事件扩展浮窗。
移动端 / PC 分端 zoom(options.features 或顶层 features):
const isMobile = window.matchMedia('(max-width: 768px)').matches;
features: {
zoom: isMobile
? { fitParentWidth: true, clampMaxToViewport: false, max: 8, pinchExponent: 1, enabled: true }
: { pageWidth: 800, max: 3, enabled: true },
},详版见 React README:features.zoom。
原生风格选区手柄:长按选词后,选区首/尾各出现一个可拖动的圆球手柄,拖动即可扩大 / 收缩选中内容(接近 iOS / Android 原生),仅触摸设备显示。通过 features.selectionHandles 配置(默认开启):enabled、color(默认 #2196f3)、size(圆球直径 px,默认 16)。手柄挂在文档 body 上、根节点带 class embedpdf-chapter-selection-handle(首/尾附加 --start / --end),可用全局 CSS 覆写图标。完善支持单页选区内拖动;跨页 / 旋转页为已知限制。
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 },
},
annotationMenu: {
enabled: true,
renderMenu: ({ onDelete }) => {
const btn = document.createElement('button');
btn.textContent = '移除';
btn.onclick = onDelete;
return btn;
},
},
},
notes: {
marker: {
renderIcon: () => /* DOM */,
renderMenuActions: ({ onEdit, onDelete }) => /* DOM */,
// 默认 false:仅 hover 高亮时显示图标。
// true:图标常驻显示,hover 感知区仅覆盖图标本身。
alwaysVisible: true,
},
},
selectionToolbar: {
selectionBackground: 'rgba(255, 193, 7, 0.35)',
// 复制默认开启,浮窗最左侧;隐藏:hiddenBuiltinActions: ['copy']
renderCopyIcon: () => {
const span = document.createElement('span');
span.textContent = '📋';
span.setAttribute('aria-hidden', 'true');
return span;
},
markupColors: ['#93c5fd', '#fde047'],
extraActions: [
{
id: 'translate',
label: '翻译',
onClick: ({ selectedText, chapterId, localPageIndex }) => {
console.log('翻译', selectedText, chapterId, localPageIndex);
},
},
],
// 或统一回调:onExtraAction: (ctx) => { ... }
},
markup: {
onCreated: ({ kind, strokeColor, position }) => {
console.log('新划线', kind, strokeColor, position);
},
},
},划词后点复制会将选中文本写入剪贴板。扩展按钮回调与 DOM 事件 detail 均含 selectedText。程序化:
import { copyTextToClipboard } from '@embedpdf-editor/chapter-snippet';
await copyTextToClipboard('文本');renderMenu / renderIcon / renderCopyIcon 在 snippet(Preact)内执行,请返回 DOM 或 Preact 节点;Vue 2 宿主不要用 h() 直接塞进 Shadow DOM。
pageOverlays(矩形叠加层)
features: {
pageOverlays: {
defaultBoxStyle: {
border: '2px dashed #ef4444',
cursor: 'pointer',
},
renderContent: ({ payload }) => {
const span = document.createElement('span');
span.textContent = payload ?? 'QR';
return span;
},
onClick: ({ overlay, payload }) => {
console.log(overlay.overlayId, payload);
},
},
},通过 importChapterAnnotations(registry, { chapterId, pageOverlays: [...] }) 注入数据;后端 QR 比例坐标转换见 13-page-overlays.md(qrTargetToPageOverlayRecord 由 React/Vue3 包导出,snippet 可手写 rectPdfCoord 或自行实现相同公式)。
用户自框选 boxSelect
用户拖拽画矩形框,框可点击选中、拖动、缩放,选中即弹自定义浮窗。PC 端能力,默认关闭。renderBox / renderMenu 是渲染函数(Preact 节点或 DOM),须通过 JS el.config(或 init({...}))传入,不能走 HTML 属性。
import { boxSelectBackendToRecord } from '@embedpdf-editor/chapter-snippet';
el.config = {
manifest,
features: {
boxSelect: {
enabled: false, // 默认关,运行时用 registry 开
menuAlign: 'center', // left | center | right
renderMenu: ({ info, getInfo, getRecord, applySave, payload, getPayload, deleteSelf, dismissMenu, menuWrapperProps, menuBarStyle }) => {
const wrap = document.createElement('div');
Object.assign(wrap.style, menuWrapperProps.style);
const bar = document.createElement('div');
Object.assign(bar.style, menuBarStyle, { display: 'flex', gap: '8px' });
const save = document.createElement('button'); save.textContent = '保存'; save.onclick = async () => {
const current = getRecord!();
const infoNow = getInfo?.() ?? info;
const payloadNow = getPayload?.() ?? payload;
const isCreate = current.id.startsWith('bs_');
const savedId = isCreate ? `custom-${infoNow.chapterId}-p${infoNow.pageIndex}` : current.id;
await saveToBackend(getInfo?.() ?? info, payloadNow, isCreate);
applySave?.({ id: savedId, payload: payloadNow });
};
const del = document.createElement('button'); del.textContent = '删除'; del.onclick = deleteSelf;
bar.append(save, del);
wrap.append(bar);
return wrap;
},
},
},
};命令式 API 经 chapter-viewer-ready 拿 registry 后调 box-select capability:
el.addEventListener('chapter-viewer-ready', (event) => {
const registry = event.detail.registry;
const box = registry.getPlugin('box-select').provides();
box.setEnabled(true); // 开启框选(或 box.toggle())
box.onEnd(({ record, info }) => saveToBackend(info)); // 建框/改框完成
box.replaceForChapter('ch-1', [ // 按需绘制后端数据
boxSelectBackendToRecord({
target: { id: 'b1', pageIndex: 1, x: 0.1, y: 0.2, width: 0.3, height: 0.15, pageWidth: 531.39, pageHeight: 746.85 },
chapterId: 'ch-1',
}),
]);
box.removeById('b1'); // 删除
});info: BoxSelectRectInfo 含 1-based pageIndex + 归一化 x/y/width/height + 页宽高,可直接回后端。滚动到指定框可用 React/Vue3 的 scrollToBoxSelectRecord(snippet 亦 re-export),或经 chapter-scroll capability 的 scrollToGlobalPdfPoint。
事件
snippet 通过 DOM CustomEvent 抛出动作。常量见 CHAPTER_SNIPPET_EVENTS。
若在 options 里已写 onClick / onCreated / onAdded 等回调,仍会派发同名 DOM 事件(便于 Vue 2 只绑 @xxx)。
| 常量 | 事件名 | detail(关键字段) |
| --- | --- | --- |
| noteRequestCreate | chapter-note-request-create | record(含 selectedText、position)、complete({ noteId, content?, nodeId? }) |
| noteRequestEdit | chapter-note-request-edit | record、nodeId? |
| selectionExtraAction | chapter-selection-extra-action | actionId、selectedText、chapterId、rectsPdfCoord |
| bookmarkAdded | chapter-bookmark-added | bookmark、position |
| markupCreated | chapter-markup-created | kind、annotationId、strokeColor、position、record |
| boxSelectStart | chapter-box-select-start | { chapterId, localPageIndex, pageIndex, startPdfPoint } |
| boxSelectChange | chapter-box-select-change | { record, info }(拖拽/缩放实时) |
| boxSelectEnd | chapter-box-select-end | { record, info }(建框完成或拖拽结束,可落库) |
| setPdfPageBackground | chapter-set-pdf-page-background | background(CSS 颜色,派发以更新页背景) |
| pdfPageBackgroundChange | chapter-pdf-page-background-change | background(更新后通知) |
| ready | chapter-viewer-ready | registry(导出标注用) |
el.addEventListener('chapter-selection-extra-action', (e) => {
const { actionId, selectedText, chapterId } = e.detail;
if (actionId === 'translate') translate(selectedText, chapterId);
});
el.addEventListener('chapter-markup-created', (e) => {
const { strokeColor, position, record } = e.detail;
saveMarkup({ strokeColor, position, record });
});
// Vue 2:框选完成(落库 info 即可,含 1-based pageIndex + 归一化坐标)
el.addEventListener('chapter-box-select-end', (e) => {
saveBoxSelect(e.detail.info);
});
// 运行时更新 PDF 页背景色
el.dispatchEvent(
new CustomEvent('chapter-set-pdf-page-background', {
detail: { background: '#1e293b' },
bubbles: true,
composed: true,
}),
);
el.addEventListener('chapter-pdf-page-background-change', (e) => {
console.log('page background', e.detail.background);
});markup record 顶层额外带 localPageIndex、globalPageIndex、globalPageNumber。宿主若持有 ChapterPdfViewer ref 或兼容的滚动对象,可直接调用 scrollToMarkupRecord(viewer, markupRecord)、scrollToNoteRecord(viewer, noteRecord)、scrollToBookmarkRecord(viewer, bookmarkRecord)、scrollToPageOverlayRecord(viewer, overlayRecord)、scrollToBackendPageOverlayItem(viewer, backendItem) 回到对应位置(内部会先滚到页、等 PDF 真实尺寸加载后再精确定位 Y,与 QR overlay 一致)。
外部列表删除标记后,需要同步移除渲染器内存里的对象。snippet 没有 ChapterPdfViewer ref,可在 chapter-viewer-ready 里保存 registry 后调用插件 capability:
import { removeChapterMarkupAnnotation } from '@embedpdf-editor/chapter-snippet';
let registry;
el.addEventListener('chapter-viewer-ready', (event) => {
registry = event.detail.registry;
});
async function deleteNote(noteId) {
await api.deleteNote(noteId);
registry?.getPlugin('note')?.provides()?.removeNote(noteId);
}
async function deleteBookmark(bookmarkId) {
await api.deleteBookmark(bookmarkId);
registry?.getPlugin('paragraph-bookmark')?.provides()?.purgeBookmark(bookmarkId);
}
async function deleteMarkup(record) {
await api.deleteMarkup(record.nodeId ?? record.annotation.id);
if (registry) await removeChapterMarkupAnnotation(registry, record);
}removeChapterMarkupAnnotation(registry, record) 会把 chapterId + localPageIndex 解析成实际 documentId + pageIndex,分段 PDF 也可直接使用。
附注创建:宿主弹窗 → 保存 → 调用 complete({ noteId, content?, nodeId? }) 注册锚点(也可事先写 record.nodeId)。划线 onCreated、书签 onAdded 同样可写 record.nodeId 或 return { nodeId }。编辑/删除 DOM 事件与回调均带 nodeId。
完整回调表:11-events-callbacks-and-component-api.md
Vue 2 用法
Vue 2 组件只需要在 mounted 时初始化,在 beforeDestroy 时销毁:
import ChapterEmbedPDF from '@embedpdf-editor/chapter-snippet';
export default {
props: {
options: { type: Object, required: true },
},
mounted() {
this.viewer = ChapterEmbedPDF.init({
type: 'container',
target: this.$refs.host,
options: this.options,
worker: true,
});
},
beforeDestroy() {
this.viewer?.destroy?.();
},
};完整示例见 examples/chapter-viewer-demo-vue2。
标注导入导出
包内直接导出章节标注 IO:
import {
exportAllChapterAnnotations,
importChapterAnnotationsArchive,
chapterAnnotationsArchiveToJson,
parseChapterAnnotationsArchiveJson,
} from '@embedpdf-editor/chapter-snippet';在 snippet 中可通过 chapter-viewer-ready 事件拿到 registry,再调用这些 API。导出格式包含 bookmarks、notes、markup、pageOverlays、boxSelects,版本常量为 CHAPTER_ANNOTATIONS_ARCHIVE_VERSION。
| 选项 | 说明 |
| --- | --- |
| mode | replace 清空后导入;merge 合并 |
| ensureChapterLoaded | 默认 true;含 markup 的分段章会加载 全部段 再合并页码 |
| bookmarks / notes / markup / pageOverlays / boxSelects | 默认 true,可关闭某一类 |
| persistNotes / persistBookmarks | 导入后写回业务存储 |
示例见 examples/chapter-viewer-demo-vue2/src/components/AnnotationsDemoBar.vue。详见 10-annotations-io.md。
教程索引
| 主题 | 文档 |
| --- | --- |
| 目录 | docs/get-started/README.md |
| wasmUrl(含 OSS 示例) | 01-installation.md |
| 划词复制 | 07-selection-toolbar.md |
| 事件常量 | 11-events-callbacks-and-component-api.md |
| 分段 + 按章存储 | 12-segmented-pdf-and-per-chapter-storage.md |
| 页内矩形叠加层 | 13-page-overlays.md |
与 React / Vue3 渲染器的区别
| 项 | React / Vue3 | chapter-snippet(本包,含 Vue 2) |
| --- | --- | --- |
| 运行时 | 框架内 editor-engine 组件 | Shadow DOM Web Component + Preact |
| 初始化 | <ChapterPdfViewer options={...} /> | ChapterEmbedPDF.init({ type: 'container', target, options }) |
| 动态 manifest | 改 options.manifest(自动 setManifest) | 改 init 的 options.manifest(Preact 内同步) |
| Registry | onInitialized / useRegistry() | chapter-viewer-ready → detail.registry |
| Vite | chapterViewerViteResolve() | chapterSnippetViteResolve() |
许可
- 本包 JavaScript / 类型定义:MIT(见
LICENSE、NOTICE) dist/pdfium.wasm:来自 PDFium,适用 Apache License 2.0(见LICENSE.pdfium、NOTICE.pdfium、THIRD-PARTY-NOTICES.md)- 再分发本包或随产品打包 wasm 时,须保留上述许可文件
常见问题
| 现象 | 处理 |
| --- | --- |
| 卡在“正在加载 PDFium” | 检查 dist/pdfium.wasm 是否随包发布并能被浏览器访问;自定义部署时传 wasmUrl |
| 卡在“正在加载 xxx.pdf” | 优先确认章节 PDF URL 是否 200;如只在 worker 模式出现,可保留默认 fallback 或临时设 worker: false 定位环境问题 |
| 只显示空白 | 确认宿主元素有高度,且没有被父容器 overflow/flex 布局压到 0 |
| 划词后没有笔记弹窗 | 监听 chapter-note-request-create,保存后调用 detail.complete({ noteId, ... }) |
| Vue/Vite 开发环境异常预构建 | 使用 chapterSnippetViteResolve(),确保 snippet 和 PDFium 引擎没有被 optimizeDeps 预构建 |
| 分段章存了 #sN | 业务层只用 chapterId;引擎段 ID 勿写入库 |
