vue3-components-plus 3.0.23 → 3.0.30

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.
@@ -56,9 +56,6 @@ function openDialog(data = {}) {
56
56
  width: '800px', // 宽度, 整个弹出框的高度,非内容高度
57
57
  height: '450px', // 高度, 不配置则默认为内容高度
58
58
  dialogPadding: [10, 20], // 弹窗内padding
59
- // 弹窗绝对定位
60
- x: 250 + openIndex.value * 20,
61
- y: 100 + openIndex.value * 20,
62
59
  // 设置函数时,则有放大和还原按钮,且按返回的对象设置弹出框。(会关闭拖动功能)
63
60
  // maxSize: function () {
64
61
  // return { width: '100%', height: '800px', x: 0, y: 100 }
@@ -68,6 +65,16 @@ function openDialog(data = {}) {
68
65
  showFooter: true, // 默认显示底部按钮
69
66
  immediately: false, // true立即取消弹出框, false异步请求后取消弹出框,默认false
70
67
  draggable: true, // 是否可拖拽,默认false
68
+ // 最大化方法
69
+ maxSize: () => ({
70
+ width: '100%',
71
+ height: '100%',
72
+ x: 0,
73
+ y: 0
74
+ }),
75
+ // 弹窗绝对定位
76
+ x: 'calc(50% - 400px)',
77
+ y: 'calc(50% - 225px)',
71
78
  // 底部确认按钮回调事件
72
79
  confirm: async (closeFn: any, componentRef: any, footerLoading: any) => {
73
80
  // 2.componentRef可以调用内部函数,前提需要defineExpose
@@ -0,0 +1,234 @@
1
+ <template>
2
+ <div class="demo-container">
3
+ <div class="control-panel">
4
+ <h3>PDF 文档预览演示</h3>
5
+
6
+ <!-- 本地PDF文件 -->
7
+ <div class="local-section">
8
+ <h4>本地PDF文件</h4>
9
+ <span class="file-name">驿云通 (11).pdf</span>
10
+ <button @click="loadLocalPdf">加载本地文件</button>
11
+ <button @click="printPdf" :disabled="!currentUrl">打印</button>
12
+ </div>
13
+
14
+ <!-- 文件上传方式 -->
15
+ <div class="upload-section">
16
+ <h4>方式一:文件上传</h4>
17
+ <input
18
+ type="file"
19
+ @change="importPdf(($event.target as any)?.files?.[0])"
20
+ accept=".pdf"
21
+ />
22
+ <button @click="clearFile" :disabled="!file">清除文件</button>
23
+ </div>
24
+
25
+ <!-- URL方式 -->
26
+ <div class="url-section">
27
+ <h4>方式二:URL地址</h4>
28
+ <input
29
+ v-model="pdfUrl"
30
+ type="text"
31
+ placeholder="请输入PDF文档的URL地址"
32
+ class="url-input"
33
+ />
34
+ <button @click="loadFromUrl" :disabled="!pdfUrl.trim()">加载URL</button>
35
+ <button @click="clearUrl" :disabled="!pdfUrl">清除URL</button>
36
+ </div>
37
+
38
+ <!-- 搜索功能 -->
39
+ <div class="search-section">
40
+ <h4>文档搜索</h4>
41
+ <input
42
+ v-model="searchKeyword"
43
+ type="text"
44
+ placeholder="输入要搜索的关键字"
45
+ class="search-input"
46
+ />
47
+ <button @click="clickPdf" :disabled="!searchKeyword.trim()">搜索</button>
48
+ </div>
49
+ </div>
50
+
51
+ <!-- PDF组件 -->
52
+ <div class="pdf-container">
53
+ <NsPdf
54
+ v-if="counts"
55
+ ref="pdfRef"
56
+ :url="currentUrl"
57
+ :hasTool="true">
58
+ </NsPdf>
59
+ </div>
60
+ </div>
61
+ </template>
62
+
63
+ <script setup lang="ts">
64
+ import { ref, nextTick } from 'vue'
65
+ import localPdfUrl from '@/assets/驿云通 (11).pdf'
66
+
67
+ const counts = ref(true)
68
+ const file = ref()
69
+ const pdfRef = ref()
70
+ const pdfUrl = ref('')
71
+ const currentUrl = ref('')
72
+ const searchKeyword = ref('')
73
+
74
+ function loadLocalPdf() {
75
+ pdfUrl.value = ''
76
+ file.value = null
77
+ currentUrl.value = localPdfUrl
78
+ }
79
+
80
+ function printPdf() {
81
+ if (!currentUrl.value) return
82
+ window.print()
83
+ }
84
+
85
+ function importPdf(f: any) {
86
+ pdfUrl.value = ''
87
+ file.value = f
88
+
89
+ if (f && f.name.endsWith('.pdf')) {
90
+ const fileUrl = URL.createObjectURL(f)
91
+ currentUrl.value = fileUrl
92
+ } else if (f) {
93
+ alert('请选择PDF文件')
94
+ clearFile()
95
+ }
96
+ }
97
+
98
+ function loadFromUrl() {
99
+ if (pdfUrl.value.trim()) {
100
+ clearFile()
101
+ currentUrl.value = pdfUrl.value.trim()
102
+ }
103
+ }
104
+
105
+ function clearFile() {
106
+ file.value = null
107
+ if (currentUrl.value.startsWith('blob:')) {
108
+ URL.revokeObjectURL(currentUrl.value)
109
+ }
110
+ currentUrl.value = ''
111
+ }
112
+
113
+ function clearUrl() {
114
+ pdfUrl.value = ''
115
+ currentUrl.value = ''
116
+ }
117
+
118
+ function clickPdf() {
119
+ if (searchKeyword.value.trim() && pdfRef.value) {
120
+ pdfRef.value.search(searchKeyword.value.trim())
121
+ }
122
+ }
123
+
124
+ function reloadComponent() {
125
+ counts.value = false
126
+ nextTick(() => {
127
+ counts.value = true
128
+ })
129
+ }
130
+ </script>
131
+
132
+ <style scoped lang="scss">
133
+ .demo-container {
134
+ padding: 20px;
135
+ max-width: 1400px;
136
+ margin: 0 auto;
137
+ }
138
+
139
+ .control-panel {
140
+ background: #f5f5f5;
141
+ padding: 20px;
142
+ border-radius: 8px;
143
+ margin-bottom: 20px;
144
+
145
+ h3 {
146
+ margin: 0 0 20px 0;
147
+ color: #333;
148
+ }
149
+
150
+ h4 {
151
+ margin: 15px 0 10px 0;
152
+ color: #666;
153
+ font-size: 14px;
154
+ }
155
+ }
156
+
157
+ .local-section,
158
+ .upload-section,
159
+ .url-section,
160
+ .search-section {
161
+ margin-bottom: 20px;
162
+ padding: 15px;
163
+ background: white;
164
+ border-radius: 6px;
165
+ border: 1px solid #e0e0e0;
166
+
167
+ input[type='file'] {
168
+ margin-right: 10px;
169
+ }
170
+
171
+ .url-input,
172
+ .search-input {
173
+ width: 300px;
174
+ padding: 8px 12px;
175
+ border: 1px solid #ddd;
176
+ border-radius: 4px;
177
+ margin-right: 10px;
178
+ font-size: 14px;
179
+
180
+ &:focus {
181
+ outline: none;
182
+ border-color: #409eff;
183
+ }
184
+ }
185
+
186
+ button {
187
+ padding: 8px 16px;
188
+ background: #409eff;
189
+ color: white;
190
+ border: none;
191
+ border-radius: 4px;
192
+ cursor: pointer;
193
+ margin-right: 10px;
194
+ font-size: 14px;
195
+
196
+ &:hover:not(:disabled) {
197
+ background: #337ecc;
198
+ }
199
+
200
+ &:disabled {
201
+ background: #c0c4cc;
202
+ cursor: not-allowed;
203
+ }
204
+ }
205
+ }
206
+
207
+ .pdf-container {
208
+ height: 600px;
209
+ border: 1px solid #e0e0e0;
210
+ border-radius: 8px;
211
+ overflow: hidden;
212
+ margin-bottom: 20px;
213
+ }
214
+
215
+ @media (max-width: 768px) {
216
+ .url-input,
217
+ .search-input {
218
+ width: 100% !important;
219
+ margin-bottom: 10px;
220
+ }
221
+ }
222
+
223
+ @media print {
224
+ .control-panel {
225
+ display: none !important;
226
+ }
227
+
228
+ .pdf-container {
229
+ height: auto !important;
230
+ border: none !important;
231
+ overflow: visible !important;
232
+ }
233
+ }
234
+ </style>
@@ -3,6 +3,14 @@
3
3
  <div class="control-panel">
4
4
  <h3>PDF 文档预览演示</h3>
5
5
 
6
+ <!-- 本地PDF文件 -->
7
+ <div class="local-section">
8
+ <h4>本地PDF文件</h4>
9
+ <span class="file-name">驿云通 (11).pdf</span>
10
+ <button @click="loadLocalPdf">加载本地文件</button>
11
+ <button @click="printPdf" :disabled="!currentUrl">打印</button>
12
+ </div>
13
+
6
14
  <!-- 文件上传方式 -->
7
15
  <div class="upload-section">
8
16
  <h4>方式一:文件上传</h4>
@@ -54,23 +62,31 @@
54
62
 
55
63
  <script setup lang="ts">
56
64
  import { ref, nextTick } from 'vue'
65
+ import localPdfUrl from '@/assets/驿云通 (11).pdf'
57
66
 
58
67
  const counts = ref(true)
59
68
  const file = ref()
60
69
  const pdfRef = ref()
61
- const pdfUrl = ref(
62
- 'https://501351981.github.io/vue-office/examples/dist/static/test-files/test.pdf',
63
- )
64
- const currentUrl = ref(pdfUrl.value)
70
+ const pdfUrl = ref('')
71
+ const currentUrl = ref('')
65
72
  const searchKeyword = ref('')
66
73
 
74
+ function loadLocalPdf() {
75
+ pdfUrl.value = ''
76
+ file.value = null
77
+ currentUrl.value = localPdfUrl
78
+ }
79
+
80
+ function printPdf() {
81
+ if (!currentUrl.value) return
82
+ window.print()
83
+ }
84
+
67
85
  function importPdf(f: any) {
68
- // 清除URL,使用文件上传
69
86
  pdfUrl.value = ''
70
87
  file.value = f
71
88
 
72
89
  if (f && f.name.endsWith('.pdf')) {
73
- // 创建文件URL
74
90
  const fileUrl = URL.createObjectURL(f)
75
91
  currentUrl.value = fileUrl
76
92
  } else if (f) {
@@ -81,7 +97,6 @@ function importPdf(f: any) {
81
97
 
82
98
  function loadFromUrl() {
83
99
  if (pdfUrl.value.trim()) {
84
- // 清除文件,使用URL
85
100
  clearFile()
86
101
  currentUrl.value = pdfUrl.value.trim()
87
102
  }
@@ -106,7 +121,6 @@ function clickPdf() {
106
121
  }
107
122
  }
108
123
 
109
- // 重新加载组件
110
124
  function reloadComponent() {
111
125
  counts.value = false
112
126
  nextTick(() => {
@@ -140,6 +154,7 @@ function reloadComponent() {
140
154
  }
141
155
  }
142
156
 
157
+ .local-section,
143
158
  .upload-section,
144
159
  .url-section,
145
160
  .search-section {
@@ -204,4 +219,16 @@ function reloadComponent() {
204
219
  margin-bottom: 10px;
205
220
  }
206
221
  }
222
+
223
+ @media print {
224
+ .control-panel {
225
+ display: none !important;
226
+ }
227
+
228
+ .pdf-container {
229
+ height: auto !important;
230
+ border: none !important;
231
+ overflow: visible !important;
232
+ }
233
+ }
207
234
  </style>
@@ -0,0 +1,369 @@
1
+ <template>
2
+ <div class="pdf-reader-page">
3
+ <div class="toolbar">
4
+ <div class="file-info">
5
+ <strong>{{ fileName }}</strong>
6
+ <span>{{ statusText }}</span>
7
+ </div>
8
+ <button type="button" :disabled="isPrinting || !pdfData || !totalPages" @click="printPdf">
9
+ {{ isPrinting ? 'Loading...' : 'Print' }}
10
+ </button>
11
+ </div>
12
+
13
+ <div ref="pdfWrapperRef" class="pdf-wrapper">
14
+ <PDF
15
+ v-if="pdfData"
16
+ :key="viewerKey"
17
+ :src="pdfData"
18
+ pdf-width="100%"
19
+ :row-gap="12"
20
+ :show-progress="true"
21
+ :show-page-tooltip="true"
22
+ :show-back-to-top-btn="false"
23
+ @onPdfInit="handlePdfInit"
24
+ @onProgress="handleProgress"
25
+ @onComplete="handleDownloadComplete"
26
+ />
27
+ <div v-else class="loading-state">
28
+ {{ statusText }}
29
+ </div>
30
+ </div>
31
+ </div>
32
+ </template>
33
+
34
+ <script setup lang="ts">
35
+ import { computed, nextTick, onBeforeMount, onMounted, ref } from 'vue'
36
+ import PDF from 'pdf-vue3'
37
+
38
+ const fileName = 'yiyuntong-11.pdf'
39
+ const pdfWrapperRef = ref<HTMLElement | null>(null)
40
+ const pdfData = ref<Uint8Array | null>(null)
41
+ const totalPages = ref(0)
42
+ const loadProgress = ref(0)
43
+ const isDownloadComplete = ref(false)
44
+ const isPrinting = ref(false)
45
+ const loadError = ref('')
46
+ const viewerKey = ref(0)
47
+ const pdfAssetUrl = new URL('../assets/驿云通 (11).pdf', import.meta.url).href
48
+
49
+ const statusText = computed(() => {
50
+ if (loadError.value) {
51
+ return loadError.value
52
+ }
53
+
54
+ if (!pdfData.value) {
55
+ return `Reading PDF ${Math.floor(loadProgress.value)}%`
56
+ }
57
+
58
+ if (!totalPages.value) {
59
+ return `Loading ${Math.floor(loadProgress.value)}%`
60
+ }
61
+
62
+ if (isPrinting.value) {
63
+ return 'Waiting for all pages to render'
64
+ }
65
+
66
+ return `${totalPages.value} pages`
67
+ })
68
+
69
+ onBeforeMount(() => {
70
+ resetLegacyPdfWorker()
71
+ })
72
+
73
+ onMounted(() => {
74
+ loadLocalPdf()
75
+ })
76
+
77
+ async function loadLocalPdf() {
78
+ loadError.value = ''
79
+ loadProgress.value = 10
80
+ totalPages.value = 0
81
+ isDownloadComplete.value = false
82
+ pdfData.value = null
83
+
84
+ try {
85
+ resetLegacyPdfWorker()
86
+
87
+ const response = await fetch(pdfAssetUrl)
88
+ if (!response.ok) {
89
+ throw new Error(`HTTP ${response.status}`)
90
+ }
91
+
92
+ loadProgress.value = 60
93
+ const buffer = await response.arrayBuffer()
94
+
95
+ resetLegacyPdfWorker()
96
+ viewerKey.value += 1
97
+ pdfData.value = new Uint8Array(buffer)
98
+ loadProgress.value = 100
99
+ } catch (error) {
100
+ console.error(error)
101
+ loadError.value = 'PDF file read failed'
102
+ }
103
+ }
104
+
105
+ function resetLegacyPdfWorker() {
106
+ delete (window as Window & { pdfjsWorker?: unknown }).pdfjsWorker
107
+ }
108
+
109
+ function handlePdfInit(pdf: { numPages?: number }) {
110
+ totalPages.value = pdf.numPages ?? 0
111
+ }
112
+
113
+ function handleProgress(progress: number) {
114
+ loadProgress.value = progress
115
+ }
116
+
117
+ function handleDownloadComplete() {
118
+ isDownloadComplete.value = true
119
+ loadProgress.value = 100
120
+ }
121
+
122
+ async function printPdf() {
123
+ if (isPrinting.value) return
124
+
125
+ isPrinting.value = true
126
+
127
+ try {
128
+ await waitForAllPagesRendered()
129
+ window.print()
130
+ } catch (error) {
131
+ console.error(error)
132
+ window.alert('PDF page loading timed out, please try again later')
133
+ } finally {
134
+ isPrinting.value = false
135
+ }
136
+ }
137
+
138
+ async function waitForAllPagesRendered(timeout = 30000) {
139
+ const startTime = Date.now()
140
+
141
+ while (Date.now() - startTime < timeout) {
142
+ await nextTick()
143
+ await waitFrame()
144
+
145
+ if (isDownloadComplete.value && totalPages.value > 0 && areAllCanvasesPainted()) {
146
+ await waitForStableLayout()
147
+ return
148
+ }
149
+
150
+ await sleep(100)
151
+ }
152
+
153
+ throw new Error('PDF render timeout')
154
+ }
155
+
156
+ async function waitForStableLayout() {
157
+ let previousHeight = -1
158
+
159
+ for (let index = 0; index < 5; index += 1) {
160
+ await waitFrame()
161
+ await sleep(80)
162
+
163
+ const currentHeight = getCanvasContainerHeight()
164
+ if (currentHeight > 0 && currentHeight === previousHeight) {
165
+ await waitFrame()
166
+ await waitFrame()
167
+ return
168
+ }
169
+
170
+ previousHeight = currentHeight
171
+ }
172
+ }
173
+
174
+ function areAllCanvasesPainted() {
175
+ const canvases = getCanvases()
176
+
177
+ return (
178
+ canvases.length >= totalPages.value &&
179
+ canvases.every((canvas) => canvas.width > 0 && canvas.height > 0 && isCanvasPainted(canvas))
180
+ )
181
+ }
182
+
183
+ function getCanvases() {
184
+ return Array.from(
185
+ pdfWrapperRef.value?.querySelectorAll<HTMLCanvasElement>(
186
+ '.pdf-vue3-canvas-container canvas',
187
+ ) ?? [],
188
+ )
189
+ }
190
+
191
+ function getCanvasContainerHeight() {
192
+ return pdfWrapperRef.value?.querySelector('.pdf-vue3-canvas-container')?.scrollHeight ?? 0
193
+ }
194
+
195
+ function isCanvasPainted(canvas: HTMLCanvasElement) {
196
+ const context = canvas.getContext('2d')
197
+ if (!context) return false
198
+
199
+ const points = [
200
+ [0.5, 0.5],
201
+ [0.25, 0.25],
202
+ [0.75, 0.25],
203
+ [0.25, 0.75],
204
+ [0.75, 0.75],
205
+ ]
206
+
207
+ try {
208
+ return points.some(([xRatio, yRatio]) => {
209
+ const x = Math.max(0, Math.min(canvas.width - 1, Math.floor(canvas.width * xRatio)))
210
+ const y = Math.max(0, Math.min(canvas.height - 1, Math.floor(canvas.height * yRatio)))
211
+ return context.getImageData(x, y, 1, 1).data[3] > 0
212
+ })
213
+ } catch {
214
+ return canvas.width > 0 && canvas.height > 0
215
+ }
216
+ }
217
+
218
+ function waitFrame() {
219
+ return new Promise<void>((resolve) => {
220
+ requestAnimationFrame(() => resolve())
221
+ })
222
+ }
223
+
224
+ function sleep(duration: number) {
225
+ return new Promise<void>((resolve) => {
226
+ window.setTimeout(resolve, duration)
227
+ })
228
+ }
229
+ </script>
230
+
231
+ <style scoped lang="scss">
232
+ .pdf-reader-page {
233
+ display: flex;
234
+ flex-direction: column;
235
+ height: 100%;
236
+ min-height: 720px;
237
+ background: #f3f5f8;
238
+ }
239
+
240
+ .toolbar {
241
+ flex: none;
242
+ display: flex;
243
+ align-items: center;
244
+ justify-content: space-between;
245
+ gap: 16px;
246
+ padding: 14px 18px;
247
+ border-bottom: 1px solid #d8dee8;
248
+ background: #ffffff;
249
+
250
+ .file-info {
251
+ display: flex;
252
+ flex-direction: column;
253
+ gap: 4px;
254
+ min-width: 0;
255
+ color: #1f2937;
256
+
257
+ strong,
258
+ span {
259
+ overflow: hidden;
260
+ text-overflow: ellipsis;
261
+ white-space: nowrap;
262
+ }
263
+
264
+ span {
265
+ color: #64748b;
266
+ font-size: 13px;
267
+ }
268
+ }
269
+
270
+ button {
271
+ flex: none;
272
+ min-width: 88px;
273
+ height: 36px;
274
+ padding: 0 16px;
275
+ border: 1px solid #2563eb;
276
+ border-radius: 4px;
277
+ background: #2563eb;
278
+ color: #ffffff;
279
+ cursor: pointer;
280
+
281
+ &:disabled {
282
+ border-color: #b8c2d6;
283
+ background: #b8c2d6;
284
+ cursor: not-allowed;
285
+ }
286
+ }
287
+ }
288
+
289
+ .pdf-wrapper {
290
+ flex: 1;
291
+ min-height: 0;
292
+ }
293
+
294
+ .loading-state {
295
+ display: flex;
296
+ align-items: center;
297
+ justify-content: center;
298
+ height: 100%;
299
+ min-height: 360px;
300
+ color: #475569;
301
+ font-size: 14px;
302
+ }
303
+
304
+ :deep(.pdf-vue3-scroller) {
305
+ background: #e9edf3;
306
+ }
307
+
308
+ :deep(.pdf-vue3-canvas-container) {
309
+ padding: 18px 0;
310
+ }
311
+
312
+ @media (max-width: 768px) {
313
+ .pdf-reader-page {
314
+ min-height: 100vh;
315
+ }
316
+
317
+ .toolbar {
318
+ align-items: stretch;
319
+ flex-direction: column;
320
+
321
+ button {
322
+ width: 100%;
323
+ }
324
+ }
325
+ }
326
+
327
+ @media print {
328
+ .pdf-reader-page {
329
+ display: block;
330
+ height: auto;
331
+ min-height: auto;
332
+ background: #ffffff;
333
+ }
334
+
335
+ .toolbar {
336
+ display: none !important;
337
+ }
338
+
339
+ .pdf-wrapper,
340
+ :deep(.pdf-vue3-main),
341
+ :deep(.pdf-vue3-container),
342
+ :deep(.pdf-vue3-scroller) {
343
+ height: auto !important;
344
+ max-height: none !important;
345
+ overflow: visible !important;
346
+ background: #ffffff !important;
347
+ }
348
+
349
+ :deep(.pdf-vue3-canvas-container) {
350
+ width: 100% !important;
351
+ padding: 0 !important;
352
+ }
353
+
354
+ :deep(.pdf-vue3-canvas-container canvas) {
355
+ width: 100% !important;
356
+ height: auto !important;
357
+ margin: 0 auto !important;
358
+ box-shadow: none !important;
359
+ break-after: page;
360
+ page-break-after: always;
361
+ }
362
+
363
+ :deep(.pdf-vue3-progress),
364
+ :deep(.pdf-vue3-pageTooltip),
365
+ :deep(.pdf-vue3-backToTopBtn) {
366
+ display: none !important;
367
+ }
368
+ }
369
+ </style>