@agilehub/vue-pdf-flipbook 1.1.0 → 1.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 CHANGED
@@ -6,12 +6,44 @@
6
6
  * [The Online Flipbook Maker](https://www.paperturn.com/)
7
7
  * [turnjs](https://www.turnjs.cn/)
8
8
 
9
- ## 安装与使用
9
+ ## 目录
10
+
11
+ - [安装](#安装)
12
+ - [在业务页面使用](#在业务页面使用)
13
+ - [外部容器:放大、缩小与全屏](#外部容器放大缩小与全屏)
14
+ - [Props](#props)、[事件](#事件)、[实例方法](#实例方法)
15
+ - [自定义插槽](#外部功能接入)
16
+ - [常见接入问题](#常见接入问题)
17
+ - [开发](#开发)
18
+
19
+ ## 安装
20
+
21
+ 适用于 Vue 3.4+ 项目,Node.js 要求为 22.15+。以下命令任选一个,在调用方项目根目录执行:
22
+
23
+ ```bash
24
+ pnpm add @agilehub/vue-pdf-flipbook
25
+ ```
10
26
 
11
27
  ```bash
12
28
  npm install @agilehub/vue-pdf-flipbook
13
29
  ```
14
30
 
31
+ ```bash
32
+ yarn add @agilehub/vue-pdf-flipbook
33
+ ```
34
+
35
+ 包名为 `@agilehub/vue-pdf-flipbook`,安装、组件导入和类型导入都使用这个完整名称。Vue 是 peer dependency,由业务项目提供;PDF.js 和 Worker 已由组件提供,普通接入不需要额外安装 `pdfjs-dist` 或复制 Worker。
36
+
37
+ 在应用入口或使用组件的页面引入一次样式:
38
+
39
+ ```ts
40
+ import '@agilehub/vue-pdf-flipbook/style.css'
41
+ ```
42
+
43
+ ## 在业务页面使用
44
+
45
+ 下面可保存为业务项目中的 `PdfReader.vue`。它接收 PDF 地址,提供翻页、缩略图开关、错误提示与重试。PDF 地址必须支持 HTTP Range / 206;跨域时还需要 CORS,具体要求见[大文件按需预览](#大文件按需预览)。
46
+
15
47
  ```vue
16
48
  <script setup lang="ts">
17
49
  import { ref } from 'vue'
@@ -19,28 +51,228 @@ import { VuePdfFlipbook } from '@agilehub/vue-pdf-flipbook'
19
51
  import type { PdfFlipbookExpose, PdfFlipbookState } from '@agilehub/vue-pdf-flipbook'
20
52
  import '@agilehub/vue-pdf-flipbook/style.css'
21
53
 
54
+ const props = defineProps<{ url: string; fileSize?: number }>()
22
55
  const reader = ref<PdfFlipbookExpose>()
23
56
  const state = ref<PdfFlipbookState>()
57
+ const errorMessage = ref('')
58
+
59
+ function onError(error: unknown) {
60
+ errorMessage.value = error instanceof Error ? error.message : String(error)
61
+ }
62
+
63
+ function onStateChange(value: PdfFlipbookState) {
64
+ state.value = value
65
+ if (value.loading) errorMessage.value = ''
66
+ }
24
67
  </script>
25
68
 
26
69
  <template>
27
- <VuePdfFlipbook
28
- ref="reader"
29
- url="https://example.com/catalog.pdf"
30
- :height="720"
31
- @state-change="state = $event"
32
- />
33
- <p v-if="state?.loading">加载中 {{ state.progress }}%</p>
34
- <p v-if="state?.error">{{ state.error }}</p>
35
- <button :disabled="!state?.canPrevious" @click="reader?.previous()">上一页</button>
36
- <span>{{ state?.page }} / {{ state?.pages }}</span>
37
- <button :disabled="!state?.canNext" @click="reader?.next()">下一页</button>
70
+ <section class="vpf-example-page">
71
+ <nav class="vpf-example-toolbar" aria-label="阅读工具栏">
72
+ <button :disabled="!state?.canPrevious || state?.pageLoading" @click="reader?.previous()">上一页</button>
73
+ <span>{{ state?.page ?? 1 }} / {{ state?.pages ?? 0 }}</span>
74
+ <button :disabled="!state?.canNext || state?.pageLoading" @click="reader?.next()">下一页</button>
75
+ <button :disabled="!state?.pages || state?.loading" @click="state?.thumbnailsVisible ? reader?.hideThumbnails() : reader?.showThumbnails()">缩略图</button>
76
+ </nav>
77
+ <p v-if="errorMessage" role="alert">
78
+ {{ errorMessage }} <button @click="reader?.reload()">重试</button>
79
+ </p>
80
+ <div class="vpf-example-reader">
81
+ <VuePdfFlipbook
82
+ ref="reader" :url="props.url" :file-size="props.fileSize"
83
+ height="100%" loading-text="文档加载中,请稍候…"
84
+ @state-change="onStateChange" @error="onError"
85
+ />
86
+ </div>
87
+ </section>
38
88
  </template>
89
+
90
+ <style scoped>
91
+ .vpf-example-page { display: flex; flex-direction: column; width: 100%; height: 80dvh; min-width: 0; min-height: 0; overflow: hidden; }
92
+ .vpf-example-toolbar { display: flex; flex: none; flex-wrap: wrap; align-items: center; gap: 8px; padding: 8px; }
93
+ .vpf-example-reader { flex: 1; min-width: 0; min-height: 0; overflow: hidden; }
94
+ </style>
95
+ ```
96
+
97
+ 父页面使用这个业务封装,例如在同目录的 `App.vue` 中:
98
+
99
+ ```vue
100
+ <script setup lang="ts">
101
+ import PdfReader from './PdfReader.vue'
102
+ </script>
103
+
104
+ <template>
105
+ <!-- 将 public/catalog.pdf 替换成你的文件,并确保服务端支持 Range / 206。 -->
106
+ <PdfReader url="/catalog.pdf" />
107
+ </template>
108
+ ```
109
+
110
+ 也支持全局注册,在业务项目的 `main.ts` 中使用默认导出:
111
+
112
+ ```ts
113
+ import { createApp } from 'vue'
114
+ import App from './App.vue'
115
+ import PdfFlipbook from '@agilehub/vue-pdf-flipbook'
116
+ import '@agilehub/vue-pdf-flipbook/style.css'
117
+
118
+ createApp(App).use(PdfFlipbook).mount('#app')
39
119
  ```
40
120
 
41
- 也支持 `app.use(PdfFlipbook)` 全局注册,`PdfFlipbook` 为默认导出。
121
+ 注册后可直接使用 `<VuePdfFlipbook />`。需要调用实例方法时,仍应导入 `PdfFlipbookExpose` 为组件 ref 标注类型。
122
+
123
+ ## 外部容器:放大、缩小与全屏
124
+
125
+ 组件根据父容器的实际尺寸适配书页。它没有 `zoom` 属性或 `zoomIn()` / `zoomOut()` 方法;业务页面通过调整包裹组件的内容层尺寸实现缩放。
126
+
127
+ 容器分为三层:
128
+
129
+ | 层级 | 职责 | 关键样式 |
130
+ | --- | --- | --- |
131
+ | 页面容器 | 确定阅读区域总高度,容纳工具栏和阅读区 | 明确的 `height`、纵向 flex、`overflow: hidden` |
132
+ | 滚动视口 | 高度不随倍率增长,放大后允许滚动查看 | `flex: 1; min-width: 0; min-height: 0; overflow: auto` |
133
+ | 缩放内容层 | 宽和高同时乘以倍率,组件占满该层 | `flex: none`,动态 `width` 和 `height`,组件 `height="100%"` |
134
+
135
+ 只修改宽度时,书页可能仍受原高度约束,看起来没有变大;使用 `transform: scale()` 也不会改变正常布局尺寸。下面采用宽高同步变化,让组件的 ResizeObserver 和翻页引擎按实际新尺寸重新适配。
136
+
137
+ ### 完整示例:缩放、鼠标拖动平移、恢复与全屏
138
+
139
+ 此示例可以作为另一个业务组件 `ZoomPdfReader.vue`,通过 `<ZoomPdfReader :url="pdfUrl" />` 使用。工具栏放在滚动视口之外,保持原尺寸。默认倍率下支持书页拖拽翻页;放大后鼠标拖动用于平移,翻页使用外部工具栏。
140
+
141
+ ```vue
142
+ <script setup lang="ts">
143
+ import { nextTick, onBeforeUnmount, ref, watch } from 'vue'
144
+ import { VuePdfFlipbook } from '@agilehub/vue-pdf-flipbook'
145
+ import type { PdfFlipbookExpose, PdfFlipbookState } from '@agilehub/vue-pdf-flipbook'
146
+ import '@agilehub/vue-pdf-flipbook/style.css'
147
+
148
+ const props = defineProps<{ url: string; fileSize?: number }>()
149
+ const reader = ref<PdfFlipbookExpose>()
150
+ const shell = ref<HTMLElement>()
151
+ const scroll = ref<HTMLElement>()
152
+ const state = ref<PdfFlipbookState>()
153
+ const zoom = ref(1)
154
+ const dragging = ref(false)
155
+ const errorMessage = ref('')
156
+ let pan: { id: number; x: number; y: number; left: number; top: number } | undefined
157
+
158
+ function changeZoom(value: number) {
159
+ zoom.value = Math.max(0.5, Math.min(2.5, Math.round(value * 100) / 100))
160
+ }
161
+
162
+ function stopPan() {
163
+ const id = pan?.id
164
+ pan = undefined
165
+ dragging.value = false
166
+ if (id !== undefined && scroll.value?.hasPointerCapture(id)) scroll.value.releasePointerCapture(id)
167
+ }
168
+
169
+ function startPan(event: PointerEvent) {
170
+ const element = scroll.value
171
+ if (!element || zoom.value <= 1 || event.pointerType !== 'mouse' || event.button !== 0 || !event.isPrimary) return
172
+ // 自定义控件可加 data-no-pan,避免被平移逻辑接管。
173
+ if ((event.target as Element).closest('button, input, a, [data-no-pan]')) return
174
+ event.preventDefault()
175
+ event.stopPropagation()
176
+ pan = { id: event.pointerId, x: event.clientX, y: event.clientY, left: element.scrollLeft, top: element.scrollTop }
177
+ dragging.value = true
178
+ element.setPointerCapture(event.pointerId)
179
+ }
180
+
181
+ function movePan(event: PointerEvent) {
182
+ if (!pan || !scroll.value || event.pointerId !== pan.id) return
183
+ if (!(event.buttons & 1)) {
184
+ stopPan()
185
+ return
186
+ }
187
+ event.preventDefault()
188
+ scroll.value.scrollLeft = pan.left - (event.clientX - pan.x)
189
+ scroll.value.scrollTop = pan.top - (event.clientY - pan.y)
190
+ }
191
+
192
+ function endPan(event: PointerEvent) {
193
+ if (event.pointerId === pan?.id) stopPan()
194
+ }
195
+
196
+ function onError(error: unknown) {
197
+ errorMessage.value = error instanceof Error ? error.message : String(error)
198
+ }
199
+
200
+ function onStateChange(value: PdfFlipbookState) {
201
+ state.value = value
202
+ if (value.loading || value.pageLoading) stopPan()
203
+ if (value.loading) {
204
+ errorMessage.value = ''
205
+ zoom.value = 1
206
+ }
207
+ }
208
+
209
+ async function toggleFullscreen() {
210
+ try {
211
+ if (document.fullscreenElement === shell.value) await document.exitFullscreen()
212
+ else await shell.value?.requestFullscreen()
213
+ } catch (error) { onError(error) }
214
+ }
215
+
216
+ watch(zoom, async (value) => {
217
+ stopPan()
218
+ await nextTick()
219
+ if (value <= 1) scroll.value?.scrollTo(0, 0)
220
+ })
221
+ onBeforeUnmount(stopPan)
222
+ </script>
223
+
224
+ <template>
225
+ <section ref="shell" class="vpf-zoom-page">
226
+ <nav class="vpf-zoom-toolbar" aria-label="阅读工具栏">
227
+ <button :disabled="!state?.canPrevious || state?.pageLoading" @click="reader?.previous()">上一页</button>
228
+ <span>{{ state?.page ?? 1 }} / {{ state?.pages ?? 0 }}</span>
229
+ <button :disabled="!state?.canNext || state?.pageLoading" @click="reader?.next()">下一页</button>
230
+ <button :disabled="zoom <= 0.5" @click="changeZoom(zoom - 0.25)">缩小</button>
231
+ <button @click="changeZoom(1)">{{ Math.round(zoom * 100) }}% · 恢复</button>
232
+ <button :disabled="zoom >= 2.5" @click="changeZoom(zoom + 0.25)">放大</button>
233
+ <button @click="toggleFullscreen">切换全屏</button>
234
+ </nav>
235
+ <p v-if="errorMessage" role="alert">{{ errorMessage }} <button @click="reader?.reload()">重试</button></p>
236
+ <div
237
+ ref="scroll" class="vpf-zoom-scroll"
238
+ :class="{ 'is-zoomed': zoom > 1, 'is-dragging': dragging }"
239
+ @pointerdown.capture="startPan" @pointermove="movePan"
240
+ @pointerup="endPan" @pointercancel="endPan" @lostpointercapture="endPan"
241
+ >
242
+ <div class="vpf-zoom-content" :style="{ width: `${zoom * 100}%`, height: `${zoom * 100}%` }">
243
+ <VuePdfFlipbook
244
+ ref="reader" :url="props.url" :file-size="props.fileSize" height="100%"
245
+ @state-change="onStateChange" @error="onError"
246
+ />
247
+ </div>
248
+ </div>
249
+ </section>
250
+ </template>
251
+
252
+ <style scoped>
253
+ .vpf-zoom-page { display: flex; flex-direction: column; width: 100%; height: 80dvh; min-width: 0; min-height: 0; overflow: hidden; background: #edf0eb; }
254
+ .vpf-zoom-toolbar { display: flex; flex: none; flex-wrap: wrap; align-items: center; gap: 8px; padding: 8px; }
255
+ .vpf-zoom-scroll { display: flex; flex: 1; min-width: 0; min-height: 0; overflow: hidden; }
256
+ .vpf-zoom-content { flex: none; margin: auto; }
257
+ .vpf-zoom-scroll.is-zoomed { overflow: auto; cursor: grab; user-select: none; }
258
+ .vpf-zoom-scroll.is-dragging { cursor: grabbing; }
259
+ /* 放大时把鼠标操作交给外层平移,避免同时触发书页折角或拖拽翻页。 */
260
+ .vpf-zoom-scroll.is-zoomed :deep(.vpf-book-stage),
261
+ .vpf-zoom-scroll.is-zoomed :deep(.vpf-book-stage *) { pointer-events: none; }
262
+ .vpf-zoom-page:fullscreen { width: 100%; height: 100%; }
263
+ </style>
264
+ ```
42
265
 
43
- 发布包名为 `@agilehub/vue-pdf-flipbook`,安装和导入必须使用相同名称。
266
+ 上述 `pointer-events` 规则也会禁用书页上的内置按钮和底部缩略图,因此放大时使用外部工具栏,以及放在缩放内容层之外的自定义侧栏。若只需要滚轮/滚动条浏览、希望继续使用书页拖拽翻页,可去掉指针事件处理、`is-dragging` 和该 `pointer-events` 规则。
267
+
268
+ ### 容器布局注意事项
269
+
270
+ - 示例采用 `80dvh`。在后台管理页面可改成 `calc(100dvh - 64px)`;只有父级高度已经确定时才使用 `height: 100%`。全页应用也可在全局设置 `html, body, #app { height: 100%; margin: 0; }`。
271
+ - flex 或 grid 布局中,阅读区及中间容器都需要 `min-width: 0; min-height: 0`,否则内容可能撑开父级,使整个页面产生滚动条。
272
+ - 缩放层需要 `flex: none`,否则 flex 自动收缩可能抵消放大。组件继续使用 `height="100%"`,不要同时写死为 `720px`。
273
+ - 固定侧栏、工具栏放在缩放层之外;横向覆盖式缩略图可放在外层定位容器中,使其保持业务指定的尺寸。
274
+ - 放大改变布局尺寸,正文 Canvas 仍遵守组件的分辨率上限;很高倍率下可能变模糊。事件返回的缩略图用于预览,不应作为大图阅读源。
275
+ - 全屏目标应包含阅读区、工具栏和 Teleport 的缩略图目标。Teleport 到全屏元素之外的内容不会跟随显示。
44
276
 
45
277
  ## Props
46
278
 
@@ -50,12 +282,28 @@ const state = ref<PdfFlipbookState>()
50
282
  | `fileSize` | `number` | 自动读取 `Content-Range` | PDF 原始字节数;跨域响应未暴露 `Content-Range` 时必须由文件元数据接口提供,变化时重新加载 |
51
283
  | `initialPage` | `number` | `1` | 每次加载文档的初始页码,从 1 开始 |
52
284
  | `initialMode` | `'single'` / `'double'` | 根据 PDF 首页比例选择 | 横向页面默认单栏,纵向页面默认双栏;显式传入时优先使用指定模式,后续可用 `setMode()` 切换 |
285
+ | `flipAnimation` | `{ single?: boolean; double?: boolean }` | 两种模式均开启 | 按实际单栏、双栏布局分别控制翻页动画;关闭时翻页直接定位,属性变化无需重新加载 PDF |
53
286
  | `height` | `string` / `number` | `100%` | 默认填满父容器高度;数字单位为 px |
54
287
  | `background` | `string` | `'transparent'` | 背景色 |
55
288
  | `workerSrc` | `string` | 内置 Worker | 自定义 PDF.js Worker URL |
289
+ | `loadingText` | `string` | `'PDF 加载中…'` | 首次加载和重新加载时遮罩中的标题文字,支持动态更新 |
290
+ | `showPreviousButton` | `boolean` | `true` | 显示上一页按钮或其自定义插槽,仍遵守首尾页和动画显示规则 |
291
+ | `showNextButton` | `boolean` | `true` | 显示下一页按钮或其自定义插槽,仍遵守首尾页和动画显示规则 |
56
292
 
57
293
  双页模式在空间不足时自动显示单页。`visiblePages` 反映实际显示页,而 `mode` 表示选择的模式。
58
294
 
295
+ 例如单栏直接切页、双栏保留翻书动画:
296
+
297
+ ```vue
298
+ <VuePdfFlipbook :url="pdfUrl" :flip-animation="{ single: false, double: true }" />
299
+ ```
300
+
301
+ 两项都不传时均播放动画;可只传其中一项。按钮、缩略图、`goToPage()` 以及书页鼠标操作使用实际布局对应的设置,关闭动画后仍会等待目标页渲染完成并发送原有页码事件。窄屏下双栏会退化为单栏,此时采用 `single` 的设置。
302
+
303
+ 关闭动画时,引擎只在页面或容器尺寸变化时重绘,停止空闲时的持续帧循环和动画 Canvas 克隆监听,以减少 CPU 与 DOM 更新开销。阅读模式、缩放适配、缩略图预加载范围及事件保持一致;重新开启动画无需重载 PDF。若在翻页中关闭动画,会先完成当前折页,再进入按需绘制。
304
+
305
+ 可通过 `<VuePdfFlipbook :url="pdfUrl" loading-text="文档加载中,请稍候…" />` 自定义加载提示。加载错误仍显示错误标题和具体错误信息,不使用此文案。
306
+
59
307
  双页模式两侧显示纸张层叠边缘,厚度按当前页组之前和之后的页数分配:前半本右厚左薄,后半本左厚右薄,翻页或跳页后平滑变化。每侧最多 16px,短文档按页数减薄,单页模式隐藏。布局在双页状态下每侧预留 18px,防止页叠被裁切;封面及封底的页叠随闭合位置移动。
60
308
 
61
309
  双页模式采用书本封面布局:第 1 页单独居中显示,向后翻动展开为第 2–3 页,之后依次为第 4–5 页、第 6–7 页。返回第 1 页(上一页、缩略图或 `goToPage(1)`)会合上封面。开合使用翻页引擎动画,纸张尺寸保持一致;封面不显示中缝阴影。偶数总页数的最后一页作为单独封底显示。单页模式与窄屏下仍逐页阅读。`visiblePages` 分组相应变为 `[1]`、`[2, 3]`、`[4, 5]`,`page` 在双页内页表示该组左页。
@@ -76,6 +324,27 @@ const state = ref<PdfFlipbookState>()
76
324
 
77
325
  鼠标移入阅读区时,左侧显示“上一页”、右侧显示“下一页”按钮。双页模式首页只显示“下一页”,显示最后一页的页面组只显示“上一页”;封面打开或合上的动画过程中同时隐藏两个按钮,动画结束后再按当前页面恢复,避免按钮先于书本开合换位。单页模式在首尾保留对应禁用按钮。按钮复用现有翻页与预加载流程,目标页加载期间禁用相应操作。键盘聚焦时也会显示按钮;无悬停能力的触屏设备上保持显示。缩略图列表不占用按钮的定位区域。
78
326
 
327
+ 调用方可设置 `:show-previous-button="false"` 或 `:show-next-button="false"` 分别隐藏内置按钮,两者默认均为 `true`,属性变化会立即更新显示。隐藏按钮只影响界面;`previous()`、`next()`、缩略图和翻页手势仍可使用。上述首页、末页及动画期间的隐藏规则继续生效。
328
+
329
+ 使用 `#previous-button` 和 `#next-button` 可分别替换整个按钮。插槽提供 `disabled` 和 `navigate()`;外部按钮应绑定禁用状态,并在点击时调用 `navigate()`。插槽仍受对应的 `showPreviousButton` / `showNextButton` 属性及首尾页、封面动画期间的显示规则控制。外部负责按钮的布局与样式;可以沿用默认的 `vpf-page-nav`、`vpf-page-nav--previous` / `vpf-page-nav--next` 类,也可以使用自己的类。
330
+
331
+ 下面的模板片段放入前面的业务页面中。按钮内容可换成业务图标或 UI 组件。建议使用原生 `button`;普通 `div` 的 `disabled` 属性不会提供原生禁用和键盘操作,需要自行实现。若使用自己的 CSS 类,请设置定位,否则组件不会自动为插槽内容补上按钮样式。
332
+
333
+ ```vue
334
+ <VuePdfFlipbook :url="pdfUrl">
335
+ <template #previous-button="{ disabled, navigate }">
336
+ <button type="button" class="vpf-page-nav vpf-page-nav--previous" :disabled="disabled" @click.stop="navigate">
337
+ 返回
338
+ </button>
339
+ </template>
340
+ <template #next-button="{ disabled, navigate }">
341
+ <button type="button" class="vpf-page-nav vpf-page-nav--next" :disabled="disabled" @click.stop="navigate">
342
+ 继续
343
+ </button>
344
+ </template>
345
+ </VuePdfFlipbook>
346
+ ```
347
+
79
348
  ## 事件
80
349
 
81
350
  | 事件 | 参数 | 说明 |
@@ -83,12 +352,28 @@ const state = ref<PdfFlipbookState>()
83
352
  | `loaded` | `{ pages: number }` | 文档加载与翻页引擎初始化完成,页面 Canvas 可能仍在渲染 |
84
353
  | `error` | `unknown` | 加载或页面渲染错误,外部负责提示 |
85
354
  | `progress` | `number` | 下载百分比,服务端未提供总长度时可能为 0 |
86
- | `page-change` | `number` | 当前页码变化 |
355
+ | `page-change` | `(page: number, thumbnailUrl: string \| null)` | 当前页码及该页的 PNG Data URL 缩略图;生成失败时图片为 `null` |
87
356
  | `mode-change` | `ReaderMode` | 调用 `setMode` 改变模式 |
88
357
  | `state-change` | `PdfFlipbookState` | 加载、页码、布局等状态变化 |
89
358
 
90
359
  `PdfFlipbookState` 包含 `page`、`pages`、`mode`、`visiblePages`、`loading`、`progress`、`error`(文档加载错误文本)、`canPrevious`、`canNext`。状态为快照,修改它不会修改组件。
91
360
 
361
+ `page-change` 保持首个参数为页码,已有只接收页码的监听器无需修改。第二个参数可直接绑定到 `<img :src="thumbnailUrl">`;组件从已渲染的正文 Canvas 等比缩小为 PNG Data URL,竖向页的宽高上限为 160 × 220 像素,横向页为 220 × 160 像素,不会拉伸原始比例。生成过程不依赖缩略图列表是否显示,也适用于自定义缩略图插槽。首次加载时会等当前页 Canvas 绘制完成后再发送此事件;`loaded` 仍在文档和翻页引擎初始化后立即发送。双栏模式的页码和图片均对应当前页组的左页。
362
+
363
+ ```vue
364
+ <VuePdfFlipbook :url="pdfUrl" @page-change="onPageChange" />
365
+ ```
366
+
367
+ ```ts
368
+ import { ref } from 'vue'
369
+
370
+ const preview = ref<string | null>(null)
371
+ function onPageChange(page: number, thumbnailUrl: string | null) {
372
+ console.log('当前页码', page)
373
+ preview.value = thumbnailUrl
374
+ }
375
+ ```
376
+
92
377
  另外,`pageLoading` 表示跳页前正在下载或渲染目标及动画必需页面,`targetPage` 表示目标页码(准备及动画期间有效,空闲时为 `null`)。外部可监听 `state-change` 展示 `正在加载第 {{ state.targetPage }} 页`。它与首次打开文档的 `loading` 独立;页面准备完成后、动画开始前自动结束,失败、切换模式、重新加载或取消跳页时也会清除。已准备好的页面不会显示加载状态。demo 在阅读区显示遮罩和转圈提示,等待期间仍可通过进度条选择其他目标页。
93
378
 
94
379
  ## 实例方法
@@ -103,6 +388,7 @@ const state = ref<PdfFlipbookState>()
103
388
  | `reload()` | 重新加载当前 URL,返回 `Promise<void>`,错误通过事件报告 |
104
389
  | `getState()` | 获取当前状态快照 |
105
390
  | `getDocument()` | 获取 `PDFDocumentProxy`,未加载时为 `undefined` |
391
+ | `showThumbnails()` / `hideThumbnails()` | 更新缩略图显示状态;完整列表插槽须使用 `visible` 控制自身显示 |
106
392
 
107
393
  文档由组件管理,外部不要调用 `destroy()` 或 `cleanup()`。URL 变化、重新加载或组件卸载后应丢弃旧引用;Vue 中使用 `shallowRef` 保存代理对象。
108
394
 
@@ -147,7 +433,9 @@ function onError(error: unknown) {
147
433
 
148
434
  连续请求相同或重叠的目标窗口会保留已完成的渲染记录。书页点击、拖拽也会检查动画必需页面:未准备好时保留当前页,通过 `pageLoading` 提示外部,准备完成后自动翻页;已准备好时保留原有拖拽交互。尚未绘制的 Canvas 隐藏,避免显示黑色底图。
149
435
 
150
- 组件使用自定义 `PDFDataRangeTransport`:第一次请求就带 `Range: bytes=0-65535`,之后只请求 PDF.js 所需的字节段,关闭流式下载和自动预取。请求按 64 KiB 对齐,PDF.js 可能合并相邻段。每段校验 HTTP `206`、长度以及浏览器可见的 `Content-Range`;收到 `200` 时立即取消响应并报告错误,不再回退到整文件下载。
436
+ 组件使用自定义 `PDFDataRangeTransport`:第一次请求就带 `Range: bytes=0-262143`,之后只请求 PDF.js 所需的字节段,关闭流式下载和自动预取。请求按 256 KiB 对齐,PDF.js 可能合并相邻段,减少大图、字体跨多个小段时的网络往返。每段校验 HTTP `206`、长度以及浏览器可见的 `Content-Range`;收到 `200` 时立即取消响应并报告错误,不再回退到整文件下载。
437
+
438
+ 首段下载与 PDF.js/Worker 模块加载并行进行。分段请求遵守服务器的 HTTP 缓存策略,组件不再强制 `no-store`;实际缓存命中仍取决于浏览器、服务器响应头及开发者工具是否启用“停用缓存”。Network 中的多条 PDF 请求对应字节段,不是一条请求对应一页。排查慢请求时分别检查排队、等待首字节(TTFB)和下载耗时:较长的 TTFB 常需检查 CDN 命中及源站响应;图像较大的 PDF 还受下载带宽影响。
151
439
 
152
440
  跨域服务器必须允许 CORS 和 Range 请求。建议 OSS/CDN 的 `Access-Control-Expose-Headers` 包含 `Content-Range, Accept-Ranges, Content-Length, Content-Encoding, ETag`,PDF 的 `Content-Type` 设置为 `application/pdf`。组件优先从 `Content-Range` 获取总长度,不再依赖浏览器是否能读到 `Accept-Ranges`。
153
441
 
@@ -157,7 +445,7 @@ function onError(error: unknown) {
157
445
  <VuePdfFlipbook :url="file.url" :file-size="file.size" />
158
446
  ```
159
447
 
160
- `file.size` 应来自上传文件的 `File.size` 或后端文件元数据。不能使用可能被 gzip/br 压缩的 HEAD `Content-Length`。未暴露 `Content-Range` 且未提供 `fileSize` 时会明确报错;不会猜测长度或下载全文。demo 已为当前示例 URL 配置实测原始大小,换用其他 URL 时需暴露响应头或提供该文件自己的大小。`blob:` / `data:` URL 和不支持 Range 的服务器不适用于此分段传输模式。
448
+ `file.size` 应来自上传文件的 `File.size` 或后端文件元数据。不能使用可能被 gzip/br 压缩的 HEAD `Content-Length`。未暴露 `Content-Range` 且未提供 `fileSize` 时会明确报错;不会猜测长度或下载全文。换用其他 URL 时需暴露响应头或提供该文件自己的大小。`blob:` / `data:` URL 和不支持 Range 的服务器不适用于此分段传输模式。
161
449
 
162
450
  PDF 的页与字节段不是一一对应:解析索引、字体及共享资源可能读取窗口之外的字节。因此这里限制的是页面渲染窗口,并尽量按需请求文件字节,不保证只下载这几页的数据,也不保证 PDF.js 已下载的数据缓存固定大小。`loaded` 表示文档可用,并不要求下载进度达到 100%。
163
451
 
@@ -165,6 +453,17 @@ PDF 的页与字节段不是一一对应:解析索引、字体及共享资源
165
453
 
166
454
  ## 外部功能接入
167
455
 
456
+ ### 插槽一览
457
+
458
+ | 插槽 | 参数 | 由调用方负责的内容 |
459
+ | --- | --- | --- |
460
+ | `previous-button` | `{ disabled, navigate }` | 上一页按钮的标签、样式与点击绑定 |
461
+ | `next-button` | `{ disabled, navigate }` | 下一页按钮的标签、样式与点击绑定 |
462
+ | `thumbnail` | `{ page, pdf, isActive, shouldRender }` | 单项缩略图内容;外层点击和布局仍由组件处理 |
463
+ | `thumbnails` | `{ pdf, items, visible, pageAspectRatio, select, hide, reportError }` | 整个缩略图列表的布局、位置、尺寸和交互 |
464
+
465
+ 只提供需要替换的插槽即可。翻页按钮示例见 [Props](#props) 下方;缩略图示例见本节。加载标题使用 `loadingText` 属性,目前没有 `loading` 或 `toolbar` 插槽。
466
+
168
467
  ### 缩略图扩展:按需选择一个插槽
169
468
 
170
469
  默认不传插槽时使用内置底部缩略图,包含拖动滚动、点击跳页和封面动画同步。`showThumbnails()` / `hideThumbnails()` 控制显示开关,`state-change` 的 `thumbnailsVisible` 反映该状态。
@@ -190,7 +489,9 @@ PDF 的页与字节段不是一一对应:解析索引、字体及共享资源
190
489
 
191
490
  插槽在加载期间也可用,便于自定义空状态。请通过 `pdf && visible` 控制预览挂载,并仅在 `item.shouldRender` 时创建 `PdfCanvasPage`,防止缩略图抢占正文资源或触发全书渲染。更换 PDF 时插槽子树会重新挂载,释放旧预览。自定义列表负责自己的滚动、拖动、键盘可访问性和封面动画行为;组件不会强制干预这些交互。
192
491
 
193
- 下面的例子不使用额外缩略图属性。单项宽高、边框、额外按钮都由业务模板决定:
492
+ ### 横向覆盖阅读区的缩略图
493
+
494
+ 下面是可独立使用的业务组件示例。单项宽高、边框、额外按钮都由业务模板决定:
194
495
 
195
496
  ```vue
196
497
  <script setup lang="ts">
@@ -236,7 +537,7 @@ const reader = ref<PdfFlipbookExpose>()
236
537
  </template>
237
538
 
238
539
  <style scoped>
239
- .reader-wrap { height: 600px; }
540
+ .reader-wrap { position: relative; height: 600px; min-width: 0; min-height: 0; }
240
541
  .my-thumbnails { position: absolute; z-index: 20; left: 10%; right: 10%; bottom: 12px; background: #ffffffdd; }
241
542
  .my-thumbnail-list { display: flex; gap: 8px; overflow: auto; max-height: 180px; }
242
543
  .my-thumbnail { flex: 0 0 auto; width: 96px; }
@@ -245,28 +546,155 @@ const reader = ref<PdfFlipbookExpose>()
245
546
  </style>
246
547
  ```
247
548
 
248
- 这会横向覆盖阅读区域。需要外部两列侧栏时,在页面布局中放置固定存在的目标容器(例如 `<aside id="pdf-sidebar" />`),用 `<Teleport v-if="pdf && visible" to="#pdf-sidebar">` 包裹插槽里的 `section`。将 `.my-thumbnails` 改为普通布局,再将列表 CSS 改为:
549
+ 缩略图相对于 `.reader-wrap` 定位;`position: relative` 用于建立定位参照。若与缩放示例组合,固定尺寸的覆盖列表应 Teleport 到缩放层之外的定位容器;直接放在缩放内容层里会跟随该层的位置变化。
550
+
551
+ ### 外部两列侧栏与点击事件
552
+
553
+ 下面示例可保存为 `SidebarPdfReader.vue`。它使用元素 ref 作为 Teleport 目标,多份阅读器不会共享同一个固定 ID。先挂载侧栏容器,再挂载阅读器;侧栏用 `v-show` 显隐,避免 Teleport 目标被移除。
554
+
555
+ ```vue
556
+ <script setup lang="ts">
557
+ import { ref } from 'vue'
558
+ import { VuePdfFlipbook, PdfCanvasPage } from '@agilehub/vue-pdf-flipbook'
559
+ import type { PdfFlipbookExpose, PdfFlipbookState, PdfThumbnailsSlotProps } from '@agilehub/vue-pdf-flipbook'
560
+ import '@agilehub/vue-pdf-flipbook/style.css'
561
+
562
+ const props = defineProps<{ url: string; fileSize?: number }>()
563
+ const emit = defineEmits<{ 'thumbnail-click': [page: number] }>()
564
+ const reader = ref<PdfFlipbookExpose>()
565
+ const state = ref<PdfFlipbookState>()
566
+ const thumbnailTarget = ref<HTMLElement | null>(null)
567
+ const errorMessage = ref('')
568
+
569
+ function onThumbnailClick(page: number, select: PdfThumbnailsSlotProps['select']) {
570
+ emit('thumbnail-click', page) // 这是业务封装发出的事件,page 从 1 开始。
571
+ void select(page)
572
+ }
573
+
574
+ function onError(error: unknown) {
575
+ errorMessage.value = error instanceof Error ? error.message : String(error)
576
+ }
577
+ </script>
578
+
579
+ <template>
580
+ <section class="vpf-sidebar-page">
581
+ <nav class="vpf-sidebar-toolbar">
582
+ <button :disabled="!state?.pages || state?.loading" @click="state?.thumbnailsVisible ? reader?.hideThumbnails() : reader?.showThumbnails()">切换缩略图</button>
583
+ <span>{{ state?.page ?? 1 }} / {{ state?.pages ?? 0 }}</span>
584
+ </nav>
585
+ <p v-if="errorMessage" role="alert">{{ errorMessage }}</p>
586
+ <div class="vpf-sidebar-layout" :class="{ 'is-sidebar-open': state?.thumbnailsVisible }">
587
+ <aside ref="thumbnailTarget" v-show="state?.thumbnailsVisible" class="vpf-sidebar-target" aria-label="缩略图侧栏" />
588
+ <main class="vpf-sidebar-main">
589
+ <!-- 如需缩放,把前述滚动视口和缩放内容层放在 main 内,aside 保留在外侧。 -->
590
+ <VuePdfFlipbook
591
+ v-if="thumbnailTarget" ref="reader" :url="props.url" :file-size="props.fileSize"
592
+ height="100%" @state-change="state = $event" @error="onError"
593
+ >
594
+ <template #thumbnails="{ pdf, items, visible, select, reportError, pageAspectRatio }">
595
+ <Teleport v-if="pdf && visible && thumbnailTarget" :to="thumbnailTarget">
596
+ <nav class="vpf-sidebar-grid" aria-label="PDF 缩略图">
597
+ <button
598
+ v-for="item in items" :key="item.page" type="button"
599
+ class="vpf-sidebar-item" :aria-label="`跳转到第 ${item.page} 页`"
600
+ :aria-current="item.isActive ? 'page' : undefined"
601
+ @click.stop="onThumbnailClick(item.page, select)"
602
+ >
603
+ <PdfCanvasPage
604
+ v-if="item.shouldRender" :pdf="pdf" :page-number="item.page"
605
+ :render-scale="0.22" @error="reportError"
606
+ />
607
+ <div v-else :style="{ aspectRatio: pageAspectRatio }" />
608
+ <span>第 {{ item.page }} 页</span>
609
+ </button>
610
+ </nav>
611
+ </Teleport>
612
+ </template>
613
+ </VuePdfFlipbook>
614
+ </main>
615
+ </div>
616
+ </section>
617
+ </template>
618
+
619
+ <style scoped>
620
+ .vpf-sidebar-page { display: flex; flex-direction: column; height: 80dvh; min-width: 0; min-height: 0; overflow: hidden; }
621
+ .vpf-sidebar-toolbar { display: flex; flex: none; gap: 8px; padding: 8px; }
622
+ .vpf-sidebar-layout { display: grid; grid-template-columns: minmax(0, 1fr) 0; grid-template-rows: minmax(0, 1fr); flex: 1; min-width: 0; min-height: 0; }
623
+ .vpf-sidebar-layout.is-sidebar-open { grid-template-columns: minmax(0, 1fr) min(240px, 40%); }
624
+ .vpf-sidebar-main { display: flex; grid-column: 1; grid-row: 1; min-width: 0; min-height: 0; overflow: hidden; }
625
+ .vpf-sidebar-target { grid-column: 2; grid-row: 1; min-width: 0; min-height: 0; overflow: auto; padding: 8px; background: #edf0eb; box-sizing: border-box; }
626
+ .vpf-sidebar-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
627
+ .vpf-sidebar-item { min-width: 0; width: 100%; padding: 4px; border: 2px solid transparent; background: white; cursor: pointer; }
628
+ .vpf-sidebar-item[aria-current='page'] { border-color: #36835c; }
629
+ </style>
630
+ ```
631
+
632
+ 父页面可以监听业务封装的 `@thumbnail-click="onThumbnailClick"`,获取点击的确切页码。原始 `VuePdfFlipbook` 没有 `thumbnail-click` 事件;在完整列表插槽中,点击由自己的元素处理。`page-change` 表示实际阅读页变化,双栏时它可能是所点击页所在页组的左页;重复点击当前可见页仍可触发业务点击事件,但不一定发生翻页。
633
+
634
+ `item.isActive` 用于阅读页同步高亮,`item.shouldRender` 用于控制 Canvas 是否创建。单项宽高直接通过自己的 CSS 决定,图片容器应保持比例;`pageAspectRatio` 是 PDF 首页比例,混合尺寸文档中仅适合用作未渲染项的占位参考。`select(page)` 完成表示请求已启动、取消或被忽略,不代表翻页动画已经结束。
249
635
 
250
- ```css
251
- .my-thumbnail-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; overflow: auto; max-height: 100%; }
252
- .my-thumbnail { width: auto; min-width: 0; }
636
+ ### 只替换单项内容
637
+
638
+ 若保留组件内置列表和点击跳页,只需使用 `#thumbnail`。以下是替换内容的模板片段,`PdfCanvasPage` 需要从包入口导入:
639
+
640
+ ```vue
641
+ <VuePdfFlipbook ref="reader" :url="pdfUrl">
642
+ <template #thumbnail="{ page, pdf, isActive, shouldRender }">
643
+ <div :class="{ selected: isActive }" @click="console.log('点击缩略图', page)">
644
+ <PdfCanvasPage v-if="shouldRender" :pdf="pdf" :page-number="page" :render-scale="0.22" @error="onError" />
645
+ <span>第 {{ page }} 页</span>
646
+ </div>
647
+ </template>
648
+ </VuePdfFlipbook>
253
649
  ```
254
650
 
255
- 侧栏容器的宽高、展开与收起由业务页面控制。挂载目标需在 Teleport 启用时存在。自定义宽高可以直接写在 `.my-thumbnail`、内层预览或绑定的 `style` 上,不再受组件的 `86px` 宽度限制。
651
+ 单项插槽外层仍有组件提供的可点击 `div`。不要在上例的点击监听中使用 `.stop`,否则事件无法传到该外层并自动跳页。需要完全控制外层宽高、键盘交互和跳页行为时,使用前述 `#thumbnails`。
256
652
 
257
653
  `#thumbnail` 的参数仍为 `PdfThumbnailSlotProps`:`page`、`pdf`、`isActive`、`shouldRender`。已有 `thumbnailTarget`、`thumbnailLayout`、`thumbnailColumns`、`thumbnailItemStyle` 保留兼容并标记为 deprecated;新代码优先迁移到 `#thumbnails`,该插槽会忽略这些旧配置。原有调用不需要立即修改。
258
654
 
259
655
  - 工具栏、进度滑块、键盘快捷键:调用翻页方法并监听状态事件。
260
656
  - 加载提示、错误提示、重试:监听状态或 `error`,调用 `reload()`。
261
- - 缩略图:调用 `showThumbnails()` / `hideThumbnails()`,或通过 `thumbnail` 插槽自定义单项内容,见下方示例。
657
+ - 缩略图:调用 `showThumbnails()` / `hideThumbnails()`,结合本节的单项或完整列表插槽。
262
658
  - 全屏:在外部容器上调用浏览器 Fullscreen API,组件自动响应容器尺寸变化。
263
- - 缩放和平移:由外部包装容器实现变换和滚动,不属于翻页组件 API。
659
+ - 缩放和平移:由外部包装容器改变尺寸并处理滚动,完整代码见[外部容器示例](#外部容器放大缩小与全屏)。
264
660
 
265
661
  `src/demo/App.vue` 展示外部控件接入:放大后按住鼠标左键拖动可平移阅读区域,松开即停止;恢复 100% 时重置滚动位置并恢复书页拖拽翻页。放大时仍可通过外部按钮翻页。平移逻辑位于 demo,核心组件不会自动处理键盘事件或创建弹层。
266
662
 
663
+ ## 常见接入问题
664
+
665
+ ### 安装后提示找不到组件入口或类型
666
+
667
+ 确认安装和导入的都是 `@agilehub/vue-pdf-flipbook`,并且保留完整安装包中的 `dist/`。`VuePdfFlipbook` 使用具名导入,类型使用 `import type`。不要从包内的 `src/` 或 demo 路径导入。`useZoomPan`、`useFullscreen` 是仓库示例代码,不是 npm 包的公共导出;可以采用本 README 的独立实现。
668
+
669
+ ### 页面空白或出现整页滚动条
670
+
671
+ 检查阅读区父级是否有确定高度、flex/grid 子项是否设置 `min-width: 0; min-height: 0`,以及样式入口是否引入。缩放示例在 100% 时外层隐藏溢出,放大时仅阅读视口开启滚动。PDF 请求失败则根据 `error` 事件检查 URL、CORS 和 HTTP 206 支持。
672
+
673
+ ### 点击放大,书页尺寸没有变化
674
+
675
+ 同时改变缩放内容层的 `width` 和 `height`,保持 `flex: none`,并让组件使用 `height="100%"`。仅改变宽度、倍率变量或调用已移除的 `zoomIn()` 不会按上述容器方案放大。需要“适应页面”时把倍率恢复为 1。
676
+
677
+ ### 自定义缩略图不显示或选中状态不更新
678
+
679
+ 先调用 `showThumbnails()`,完整列表插槽中用 `pdf && visible` 控制挂载;Teleport 的目标必须已存在。选中样式绑定 `item.isActive`,预览 Canvas 绑定 `item.shouldRender`。业务 CSS 必须实际定义边框或背景高亮。仅替换单项时使用 `#thumbnail`,完全控制位置和尺寸时使用 `#thumbnails`。
680
+
681
+ ### 如何指定外部 Worker
682
+
683
+ 默认无需配置。必须使用外部文件时,将与当前包依赖版本完全一致的 `pdf.worker.min.mjs` 部署为可访问的静态资源,然后指定其地址:
684
+
685
+ ```vue
686
+ <VuePdfFlipbook :url="pdfUrl" worker-src="/pdf.worker.min.mjs" />
687
+ ```
688
+
689
+ 当前源码依赖 PDF.js 6.2.108,不能配用其他版本的 Worker。部署在子路径时请传入对应子路径下的实际 URL;跨域 Worker 还需要符合业务站点的跨域和 CSP 配置。
690
+
691
+ ### 翻页返回的图片如何显示
692
+
693
+ 监听 `@page-change="onPageChange"`,处理函数的两个参数分别是页码和 `string | null` 图片 URL。将第二个参数保存到 ref 后使用 `<img v-if="preview" :src="preview" style="max-width: 220px; height: auto" />`。这是 PNG Data URL,无需调用 `URL.revokeObjectURL()`;若业务累计保存每页图片,应自行限制缓存数量。
694
+
267
695
  ## 从内置阅读器迁移
268
696
 
269
- 已移除 `showToolbar`、`showThumbnails`、`minZoom`、`maxZoom`、`zoomStep`,以及 `zoomIn()`、`zoomOut()`、`resetZoom()`、`toggleFullscreen()` 和 `zoom-change`。请将对应逻辑迁移到业务组件。默认背景改为透明;组件不再附加页码和骨架屏。阅读区现提供左右翻页按钮。
697
+ 已移除属性 `showToolbar`、`showThumbnails`、`minZoom`、`maxZoom`、`zoomStep`,以及方法 `zoomIn()`、`zoomOut()`、`resetZoom()`、`toggleFullscreen()` 和事件 `zoom-change`。缩略图仍通过实例方法 `showThumbnails()` / `hideThumbnails()` 控制。请将工具栏、缩放与全屏逻辑迁移到业务组件。默认背景为透明,页码工具栏由业务实现;阅读区提供左右翻页按钮和首次加载遮罩。
270
698
 
271
699
  ## 开发
272
700
 
@@ -1,6 +1,8 @@
1
- import type { PdfFlipbookProps, PdfThumbnailSlotProps, PdfThumbnailsSlotProps } from './types';
1
+ import type { PdfFlipbookProps, PdfPageNavigationSlotProps, PdfThumbnailSlotProps, PdfThumbnailsSlotProps } from './types';
2
2
  /** thumbnail 是旧版单项插槽;thumbnails 接管整个列表、位置及交互,优先级更高。 */
3
3
  type __VLS_Slots = {
4
+ 'previous-button'?: (props: PdfPageNavigationSlotProps) => unknown;
5
+ 'next-button'?: (props: PdfPageNavigationSlotProps) => unknown;
4
6
  thumbnail?: (props: PdfThumbnailSlotProps) => unknown;
5
7
  thumbnails?: (props: PdfThumbnailsSlotProps) => unknown;
6
8
  };
@@ -10,7 +12,7 @@ declare const __VLS_base: import("vue").DefineComponent<PdfFlipbookProps, import
10
12
  loaded: (payload: {
11
13
  pages: number;
12
14
  }) => any;
13
- "page-change": (page: number) => any;
15
+ "page-change": (page: number, thumbnailUrl: string | null) => any;
14
16
  "mode-change": (mode: import("./types").ReaderMode) => any;
15
17
  "state-change": (state: import("./types").PdfFlipbookState) => any;
16
18
  }, string, import("vue").PublicProps, Readonly<PdfFlipbookProps> & Readonly<{
@@ -19,7 +21,7 @@ declare const __VLS_base: import("vue").DefineComponent<PdfFlipbookProps, import
19
21
  onLoaded?: ((payload: {
20
22
  pages: number;
21
23
  }) => any) | undefined;
22
- "onPage-change"?: ((page: number) => any) | undefined;
24
+ "onPage-change"?: ((page: number, thumbnailUrl: string | null) => any) | undefined;
23
25
  "onMode-change"?: ((mode: import("./types").ReaderMode) => any) | undefined;
24
26
  "onState-change"?: ((state: import("./types").PdfFlipbookState) => any) | undefined;
25
27
  }>, {
@@ -27,6 +29,9 @@ declare const __VLS_base: import("vue").DefineComponent<PdfFlipbookProps, import
27
29
  height: string | number;
28
30
  background: string;
29
31
  workerSrc: string;
32
+ loadingText: string;
33
+ showPreviousButton: boolean;
34
+ showNextButton: boolean;
30
35
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
31
36
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
32
37
  declare const _default: typeof __VLS_export;