feat(pdf): 添加pdf-vue3依赖并实现新版PDF查看组件
添加pdf-vue3依赖包用于PDF文件预览 创建PdfViewer组件替换原有PdfPreview组件 优化PDF加载性能并添加进度显示和错误处理 在StudyDetailPage中使用新组件并添加文件大小显示
Showing
6 changed files
with
475 additions
and
5 deletions
| ... | @@ -35,6 +35,7 @@ | ... | @@ -35,6 +35,7 @@ |
| 35 | "browser-md5-file": "^1.1.1", | 35 | "browser-md5-file": "^1.1.1", |
| 36 | "dayjs": "^1.11.13", | 36 | "dayjs": "^1.11.13", |
| 37 | "lodash": "^4.17.21", | 37 | "lodash": "^4.17.21", |
| 38 | + "pdf-vue3": "^1.0.12", | ||
| 38 | "swiper": "^11.2.6", | 39 | "swiper": "^11.2.6", |
| 39 | "uuid": "^11.1.0", | 40 | "uuid": "^11.1.0", |
| 40 | "vant": "^4.9.19", | 41 | "vant": "^4.9.19", | ... | ... |
| ... | @@ -18,6 +18,7 @@ declare module 'vue' { | ... | @@ -18,6 +18,7 @@ declare module 'vue' { |
| 18 | CourseCard: typeof import('./components/ui/CourseCard.vue')['default'] | 18 | CourseCard: typeof import('./components/ui/CourseCard.vue')['default'] |
| 19 | CourseImageCard: typeof import('./components/ui/CourseImageCard.vue')['default'] | 19 | CourseImageCard: typeof import('./components/ui/CourseImageCard.vue')['default'] |
| 20 | CourseList: typeof import('./components/courses/CourseList.vue')['default'] | 20 | CourseList: typeof import('./components/courses/CourseList.vue')['default'] |
| 21 | + FilePreviewPopup: typeof import('./components/ui/FilePreviewPopup.vue')['default'] | ||
| 21 | FormPage: typeof import('./components/infoEntry/formPage.vue')['default'] | 22 | FormPage: typeof import('./components/infoEntry/formPage.vue')['default'] |
| 22 | FrostedGlass: typeof import('./components/ui/FrostedGlass.vue')['default'] | 23 | FrostedGlass: typeof import('./components/ui/FrostedGlass.vue')['default'] |
| 23 | GradientHeader: typeof import('./components/ui/GradientHeader.vue')['default'] | 24 | GradientHeader: typeof import('./components/ui/GradientHeader.vue')['default'] |
| ... | @@ -25,6 +26,7 @@ declare module 'vue' { | ... | @@ -25,6 +26,7 @@ declare module 'vue' { |
| 25 | MenuItem: typeof import('./components/ui/MenuItem.vue')['default'] | 26 | MenuItem: typeof import('./components/ui/MenuItem.vue')['default'] |
| 26 | OfficeViewer: typeof import('./components/ui/OfficeViewer.vue')['default'] | 27 | OfficeViewer: typeof import('./components/ui/OfficeViewer.vue')['default'] |
| 27 | PdfPreview: typeof import('./components/ui/PdfPreview.vue')['default'] | 28 | PdfPreview: typeof import('./components/ui/PdfPreview.vue')['default'] |
| 29 | + PdfViewer: typeof import('./components/ui/PdfViewer.vue')['default'] | ||
| 28 | ReviewPopup: typeof import('./components/courses/ReviewPopup.vue')['default'] | 30 | ReviewPopup: typeof import('./components/courses/ReviewPopup.vue')['default'] |
| 29 | RouterLink: typeof import('vue-router')['RouterLink'] | 31 | RouterLink: typeof import('vue-router')['RouterLink'] |
| 30 | RouterView: typeof import('vue-router')['RouterView'] | 32 | RouterView: typeof import('vue-router')['RouterView'] | ... | ... |
| ... | @@ -79,12 +79,29 @@ const initPdfViewer = () => { | ... | @@ -79,12 +79,29 @@ const initPdfViewer = () => { |
| 79 | // customPdfOption是 pdfjs getDocument 函数中一些配置参数 具体可参考 https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib.html#~DocumentInitParameters | 79 | // customPdfOption是 pdfjs getDocument 函数中一些配置参数 具体可参考 https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib.html#~DocumentInitParameters |
| 80 | cMapPacked: true, //指定 CMap 是否是二进制打包的 | 80 | cMapPacked: true, //指定 CMap 是否是二进制打包的 |
| 81 | cMapUrl: "https://cdn.jsdelivr.net/npm/pdfjs-dist@2.2.228/cmaps/", //预定义 Adobe CMaps 所在的 URL。可解决字体加载错误 | 81 | cMapUrl: "https://cdn.jsdelivr.net/npm/pdfjs-dist@2.2.228/cmaps/", //预定义 Adobe CMaps 所在的 URL。可解决字体加载错误 |
| 82 | + // 内存优化配置 | ||
| 83 | + disableAutoFetch: true, // 启用自动获取,提升加载速度 | ||
| 84 | + disableStream: true, // 启用流式加载 | ||
| 85 | + disableRange: true, // 启用范围请求,支持分片下载 | ||
| 86 | + // maxImageSize: 1 * 1024 * 1024, // 限制图片最大尺寸为1MB | ||
| 87 | + isEvalSupported: false, // 禁用eval,提升安全性和性能 | ||
| 88 | + // 启用懒加载,减少内存占用 | ||
| 89 | + enableXfa: true, // 禁用XFA表单支持,减少内存占用 | ||
| 90 | + // 添加更多性能优化配置 | ||
| 91 | + useOnlyCssZoom: true, // 仅使用CSS缩放,减少重新渲染 | ||
| 92 | + verbosity: 0, // 减少日志输出,提升性能 | ||
| 93 | + // 微信浏览器优化配置 | ||
| 94 | + useWorkerFetch: true, // 在微信浏览器中禁用Worker fetch,避免兼容性问题 | ||
| 95 | + disableFontFace: true, // 保持字体渲染,但可能需要根据情况调整 | ||
| 96 | + // 内存管理配置 | ||
| 97 | + pdfBug: true, // 禁用调试模式,减少内存占用 | ||
| 98 | + stopAtErrors: true, // 不要在错误时停止,让错误回调处理 | ||
| 82 | }, | 99 | }, |
| 83 | // 禁用右键菜单和文本选择 | 100 | // 禁用右键菜单和文本选择 |
| 84 | selectConfig: undefined, // 禁用文本选择功能 | 101 | selectConfig: undefined, // 禁用文本选择功能 |
| 85 | } | 102 | } |
| 86 | }); | 103 | }); |
| 87 | - | 104 | + |
| 88 | // 添加额外的事件监听器来防止长按保存 | 105 | // 添加额外的事件监听器来防止长按保存 |
| 89 | const pdfContainer = document.querySelector("#pdf-container"); | 106 | const pdfContainer = document.querySelector("#pdf-container"); |
| 90 | if (pdfContainer) { | 107 | if (pdfContainer) { |
| ... | @@ -93,13 +110,13 @@ const initPdfViewer = () => { | ... | @@ -93,13 +110,13 @@ const initPdfViewer = () => { |
| 93 | e.preventDefault(); | 110 | e.preventDefault(); |
| 94 | return false; | 111 | return false; |
| 95 | }); | 112 | }); |
| 96 | - | 113 | + |
| 97 | // 防止长按选择 | 114 | // 防止长按选择 |
| 98 | pdfContainer.addEventListener('selectstart', (e) => { | 115 | pdfContainer.addEventListener('selectstart', (e) => { |
| 99 | e.preventDefault(); | 116 | e.preventDefault(); |
| 100 | return false; | 117 | return false; |
| 101 | }); | 118 | }); |
| 102 | - | 119 | + |
| 103 | // 防止拖拽 | 120 | // 防止拖拽 |
| 104 | pdfContainer.addEventListener('dragstart', (e) => { | 121 | pdfContainer.addEventListener('dragstart', (e) => { |
| 105 | e.preventDefault(); | 122 | e.preventDefault(); | ... | ... |
src/components/ui/PdfViewer.vue
0 → 100644
| 1 | +<!-- | ||
| 2 | + * @Date: 2025-01-21 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-10-21 14:33:07 | ||
| 5 | + * @FilePath: /mlaj/src/components/ui/PdfViewer.vue | ||
| 6 | + * @Description: PDF预览组件 - 使用pdf-vue3库 | ||
| 7 | +--> | ||
| 8 | +<template> | ||
| 9 | + <van-popup v-if="show" :show="show" @update:show="emit('update:show', $event)" position="right" | ||
| 10 | + :style="{ height: '100%', width: '100%' }"> | ||
| 11 | + <div class="pdf-viewer-container"> | ||
| 12 | + <!-- PDF内容区域 --> | ||
| 13 | + <div class="pdf-content" :class="{ 'pdf-no-select': preventSave }"> | ||
| 14 | + <PDF v-if="url && show && !loadingError" :src="url" :showProgress="true" :progressColor="'#1989fa'" | ||
| 15 | + :showPageTooltip="true" :showBackToTopBtn="true" :scrollThreshold="300" :pdfWidth="'100%'" | ||
| 16 | + :rowGap="8" :page="1" :cMapUrl="'https://unpkg.com/pdfjs-dist@3.7.107/cmaps/'" | ||
| 17 | + :withCredentials="false" :useSystemFonts="true" :stopAtErrors="false" :disableFontFace="false" | ||
| 18 | + :disableRange="false" :disableStream="false" :disableAutoFetch="false" @onProgress="handleProgress" | ||
| 19 | + @onComplete="handleComplete" @onScroll="handleScroll" @onPageChange="handlePageChange" | ||
| 20 | + @onPdfInit="handlePdfInit" @onError="handlePdfError" /> | ||
| 21 | + | ||
| 22 | + <!-- 错误状态显示 --> | ||
| 23 | + <div v-if="loadingError" class="error-container"> | ||
| 24 | + <div class="error-content"> | ||
| 25 | + <van-icon name="warning-o" size="48" color="#ff4444" /> | ||
| 26 | + <div class="error-title">加载失败</div> | ||
| 27 | + <div class="error-message">{{ errorMessage }}</div> | ||
| 28 | + <van-button type="primary" size="small" @click="retryLoad" class="retry-btn"> | ||
| 29 | + 重新加载 | ||
| 30 | + </van-button> | ||
| 31 | + </div> | ||
| 32 | + </div> | ||
| 33 | + </div> | ||
| 34 | + | ||
| 35 | + <!-- 关闭按钮 --> | ||
| 36 | + <van-button class="close-btn" type="default" icon="cross" round @click="emit('update:show', false)" /> | ||
| 37 | + | ||
| 38 | + <!-- 加载遮罩 --> | ||
| 39 | + <van-overlay :show="loading && !loadingError"> | ||
| 40 | + <div class="wrapper" @click.stop> | ||
| 41 | + <div class="loading-content"> | ||
| 42 | + <!-- 进度环形图 --> | ||
| 43 | + <div class="progress-circle"> | ||
| 44 | + <div class="progress-percent">{{ loadingProgress }}%</div> | ||
| 45 | + </div> | ||
| 46 | + | ||
| 47 | + <!-- 加载文字 --> | ||
| 48 | + <div class="loading-text"> | ||
| 49 | + <div class="loading-title">正在加载PDF文档</div> | ||
| 50 | + <div class="loading-subtitle"> | ||
| 51 | + {{ loadingProgress < 10 ? '正在连接服务器...' : loadingProgress < 50 ? '正在下载文件...' : | ||
| 52 | + loadingProgress < 90 ? '正在解析文档...' : '即将完成...' }} </div> | ||
| 53 | + </div> | ||
| 54 | + </div> | ||
| 55 | + </div> | ||
| 56 | + </van-overlay> | ||
| 57 | + </div> | ||
| 58 | + </van-popup> | ||
| 59 | +</template> | ||
| 60 | + | ||
| 61 | +<script setup> | ||
| 62 | +import { ref, watch, nextTick } from 'vue'; | ||
| 63 | +import PDF from "pdf-vue3"; | ||
| 64 | + | ||
| 65 | +/** | ||
| 66 | + * 组件属性定义 | ||
| 67 | + */ | ||
| 68 | +const props = defineProps({ | ||
| 69 | + // 是否显示弹窗 | ||
| 70 | + show: { | ||
| 71 | + type: Boolean, | ||
| 72 | + default: false | ||
| 73 | + }, | ||
| 74 | + // PDF文件URL | ||
| 75 | + url: { | ||
| 76 | + type: String, | ||
| 77 | + default: '' | ||
| 78 | + }, | ||
| 79 | + // PDF标题 | ||
| 80 | + title: { | ||
| 81 | + type: String, | ||
| 82 | + default: '' | ||
| 83 | + }, | ||
| 84 | + // 是否防止保存(禁用右键、长按等) | ||
| 85 | + preventSave: { | ||
| 86 | + type: Boolean, | ||
| 87 | + default: true | ||
| 88 | + } | ||
| 89 | +}); | ||
| 90 | + | ||
| 91 | +/** | ||
| 92 | + * 事件定义 | ||
| 93 | + */ | ||
| 94 | +const emit = defineEmits(['update:show', 'onLoad', 'onProgress', 'onComplete']); | ||
| 95 | + | ||
| 96 | +// 响应式数据 | ||
| 97 | +const loading = ref(true); | ||
| 98 | +const loadingProgress = ref(0); | ||
| 99 | +const loadingError = ref(false); | ||
| 100 | +const errorMessage = ref(''); | ||
| 101 | + | ||
| 102 | +/** | ||
| 103 | + * 监听show属性变化,控制PDF加载 | ||
| 104 | + */ | ||
| 105 | +watch(() => props.show, (newVal) => { | ||
| 106 | + if (newVal && props.url) { | ||
| 107 | + // 重置所有状态 | ||
| 108 | + loading.value = true; | ||
| 109 | + loadingError.value = false; | ||
| 110 | + loadingProgress.value = 0; | ||
| 111 | + errorMessage.value = ''; | ||
| 112 | + } else if (!newVal) { | ||
| 113 | + // 弹窗关闭时重置状态 | ||
| 114 | + loading.value = true; | ||
| 115 | + loadingError.value = false; | ||
| 116 | + loadingProgress.value = 0; | ||
| 117 | + errorMessage.value = ''; | ||
| 118 | + } | ||
| 119 | +}); | ||
| 120 | + | ||
| 121 | +/** | ||
| 122 | + * 监听URL变化,重新加载PDF | ||
| 123 | + */ | ||
| 124 | +watch(() => props.url, (newUrl) => { | ||
| 125 | + if (newUrl && props.show) { | ||
| 126 | + // URL变化时重置状态 | ||
| 127 | + loading.value = true; | ||
| 128 | + loadingError.value = false; | ||
| 129 | + loadingProgress.value = 0; | ||
| 130 | + errorMessage.value = ''; | ||
| 131 | + } | ||
| 132 | +}); | ||
| 133 | + | ||
| 134 | +/** | ||
| 135 | + * 处理PDF下载进度 | ||
| 136 | + * @param {number} loadRatio - 加载进度 0-100 | ||
| 137 | + */ | ||
| 138 | +const handleProgress = (loadRatio) => { | ||
| 139 | + loadingProgress.value = Math.round(loadRatio); | ||
| 140 | + emit('onProgress', loadRatio); | ||
| 141 | +}; | ||
| 142 | + | ||
| 143 | +/** | ||
| 144 | + * 处理PDF下载完成 | ||
| 145 | + */ | ||
| 146 | +const handleComplete = () => { | ||
| 147 | + loading.value = false; | ||
| 148 | + loadingError.value = false; | ||
| 149 | + loadingProgress.value = 100; | ||
| 150 | + emit('onLoad', false); | ||
| 151 | + emit('onComplete'); | ||
| 152 | +}; | ||
| 153 | + | ||
| 154 | +/** | ||
| 155 | + * 处理PDF滚动事件 | ||
| 156 | + * @param {number} scrollOffset - 滚动偏移量 | ||
| 157 | + */ | ||
| 158 | +const handleScroll = (scrollOffset) => { | ||
| 159 | + // 可以在这里处理滚动事件 | ||
| 160 | +}; | ||
| 161 | + | ||
| 162 | +/** | ||
| 163 | + * 处理页面变化事件 | ||
| 164 | + * @param {number} page - 当前页码 | ||
| 165 | + */ | ||
| 166 | +const handlePageChange = (page) => { | ||
| 167 | + // 可以在这里处理页面变化事件 | ||
| 168 | +}; | ||
| 169 | + | ||
| 170 | +/** | ||
| 171 | + * 处理PDF初始化完成 | ||
| 172 | + * @param {Object} pdf - PDF文档对象 | ||
| 173 | + */ | ||
| 174 | +const handlePdfInit = (pdf) => { | ||
| 175 | + // PDF初始化完成后的处理 | ||
| 176 | + loadingError.value = false; | ||
| 177 | + nextTick(() => { | ||
| 178 | + if (props.preventSave) { | ||
| 179 | + // 添加防止保存的事件监听器 | ||
| 180 | + addPreventSaveListeners(); | ||
| 181 | + } | ||
| 182 | + }); | ||
| 183 | +}; | ||
| 184 | + | ||
| 185 | +/** | ||
| 186 | + * 处理PDF加载错误 | ||
| 187 | + * @param {Error} error - 错误对象 | ||
| 188 | + */ | ||
| 189 | +const handlePdfError = (error) => { | ||
| 190 | + console.error('PDF加载失败:', error); | ||
| 191 | + loading.value = false; | ||
| 192 | + loadingError.value = true; | ||
| 193 | + loadingProgress.value = 0; | ||
| 194 | + | ||
| 195 | + // 根据错误类型设置不同的错误信息 | ||
| 196 | + if (error.name === 'NetworkError' || error.message.includes('network')) { | ||
| 197 | + errorMessage.value = '网络连接失败,请检查网络后重试'; | ||
| 198 | + } else if (error.name === 'InvalidPDFException' || error.message.includes('Invalid PDF')) { | ||
| 199 | + errorMessage.value = 'PDF文件格式错误或已损坏'; | ||
| 200 | + } else if (error.message.includes('404') || error.message.includes('Not Found')) { | ||
| 201 | + errorMessage.value = '文件不存在或已被删除'; | ||
| 202 | + } else if (error.message.includes('403') || error.message.includes('Forbidden')) { | ||
| 203 | + errorMessage.value = '没有权限访问此文件'; | ||
| 204 | + } else { | ||
| 205 | + errorMessage.value = '文件加载失败,请重试'; | ||
| 206 | + } | ||
| 207 | +}; | ||
| 208 | + | ||
| 209 | +/** | ||
| 210 | + * 重试加载PDF | ||
| 211 | + */ | ||
| 212 | +const retryLoad = () => { | ||
| 213 | + loadingError.value = false; | ||
| 214 | + loading.value = true; | ||
| 215 | + loadingProgress.value = 0; | ||
| 216 | + errorMessage.value = ''; | ||
| 217 | +}; | ||
| 218 | + | ||
| 219 | +/** | ||
| 220 | + * 添加防止保存的事件监听器 | ||
| 221 | + */ | ||
| 222 | +const addPreventSaveListeners = () => { | ||
| 223 | + const pdfContainer = document.querySelector('.pdf-viewer-container .pdf-content'); | ||
| 224 | + if (pdfContainer) { | ||
| 225 | + // 防止上下文菜单(右键菜单) | ||
| 226 | + pdfContainer.addEventListener('contextmenu', (e) => { | ||
| 227 | + e.preventDefault(); | ||
| 228 | + return false; | ||
| 229 | + }); | ||
| 230 | + | ||
| 231 | + // 防止长按选择 | ||
| 232 | + pdfContainer.addEventListener('selectstart', (e) => { | ||
| 233 | + e.preventDefault(); | ||
| 234 | + return false; | ||
| 235 | + }); | ||
| 236 | + | ||
| 237 | + // 防止拖拽 | ||
| 238 | + pdfContainer.addEventListener('dragstart', (e) => { | ||
| 239 | + e.preventDefault(); | ||
| 240 | + return false; | ||
| 241 | + }); | ||
| 242 | + | ||
| 243 | + // 防止触摸回调(移动端长按) | ||
| 244 | + pdfContainer.addEventListener('touchstart', (e) => { | ||
| 245 | + if (e.touches.length > 1) { | ||
| 246 | + e.preventDefault(); | ||
| 247 | + } | ||
| 248 | + }); | ||
| 249 | + | ||
| 250 | + // 防止双指缩放等手势 | ||
| 251 | + pdfContainer.addEventListener('gesturestart', (e) => { | ||
| 252 | + e.preventDefault(); | ||
| 253 | + }); | ||
| 254 | + } | ||
| 255 | +}; | ||
| 256 | +</script> | ||
| 257 | + | ||
| 258 | +<style scoped> | ||
| 259 | +.pdf-viewer-container { | ||
| 260 | + width: 100%; | ||
| 261 | + height: 100%; | ||
| 262 | + position: relative; | ||
| 263 | + background-color: #f5f5f5; | ||
| 264 | +} | ||
| 265 | + | ||
| 266 | +.pdf-content { | ||
| 267 | + width: 100%; | ||
| 268 | + height: 100%; | ||
| 269 | + overflow: auto; | ||
| 270 | +} | ||
| 271 | + | ||
| 272 | +/* 防止PDF内容被选择和保存的样式 */ | ||
| 273 | +.pdf-no-select { | ||
| 274 | + -webkit-user-select: none; | ||
| 275 | + /* Safari */ | ||
| 276 | + -moz-user-select: none; | ||
| 277 | + /* Firefox */ | ||
| 278 | + -ms-user-select: none; | ||
| 279 | + /* IE10+/Edge */ | ||
| 280 | + user-select: none; | ||
| 281 | + /* Standard */ | ||
| 282 | + -webkit-touch-callout: none; | ||
| 283 | + /* iOS Safari */ | ||
| 284 | + -webkit-tap-highlight-color: transparent; | ||
| 285 | + /* 移除点击高亮 */ | ||
| 286 | + pointer-events: auto; | ||
| 287 | + /* 保持可点击 */ | ||
| 288 | +} | ||
| 289 | + | ||
| 290 | +/* 深度选择器,防止PDF内部元素被保存 */ | ||
| 291 | +:deep(.pdf-no-select img) { | ||
| 292 | + -webkit-user-select: none; | ||
| 293 | + -moz-user-select: none; | ||
| 294 | + -ms-user-select: none; | ||
| 295 | + user-select: none; | ||
| 296 | + -webkit-touch-callout: none; | ||
| 297 | + -webkit-tap-highlight-color: transparent; | ||
| 298 | + pointer-events: none; | ||
| 299 | + /* 禁用图片的所有交互 */ | ||
| 300 | + -webkit-user-drag: none; | ||
| 301 | + /* 禁止拖拽 */ | ||
| 302 | + -khtml-user-drag: none; | ||
| 303 | + -moz-user-drag: none; | ||
| 304 | + -o-user-drag: none; | ||
| 305 | + user-drag: none; | ||
| 306 | +} | ||
| 307 | + | ||
| 308 | +/* 防止Canvas元素被保存 */ | ||
| 309 | +:deep(.pdf-no-select canvas) { | ||
| 310 | + -webkit-user-select: none; | ||
| 311 | + -moz-user-select: none; | ||
| 312 | + -ms-user-select: none; | ||
| 313 | + user-select: none; | ||
| 314 | + -webkit-touch-callout: none; | ||
| 315 | + -webkit-tap-highlight-color: transparent; | ||
| 316 | + pointer-events: none; | ||
| 317 | + -webkit-user-drag: none; | ||
| 318 | + -khtml-user-drag: none; | ||
| 319 | + -moz-user-drag: none; | ||
| 320 | + -o-user-drag: none; | ||
| 321 | + user-drag: none; | ||
| 322 | +} | ||
| 323 | + | ||
| 324 | +/* 关闭按钮样式 */ | ||
| 325 | +.close-btn { | ||
| 326 | + position: fixed; | ||
| 327 | + right: 20px; | ||
| 328 | + top: 20px; | ||
| 329 | + z-index: 100; | ||
| 330 | + width: 40px; | ||
| 331 | + height: 40px; | ||
| 332 | + padding: 0; | ||
| 333 | + border-radius: 50%; | ||
| 334 | + background: rgba(0, 0, 0, 0.6); | ||
| 335 | + color: #fff; | ||
| 336 | +} | ||
| 337 | + | ||
| 338 | +/* 加载遮罩样式 */ | ||
| 339 | +.wrapper { | ||
| 340 | + display: flex; | ||
| 341 | + align-items: center; | ||
| 342 | + justify-content: center; | ||
| 343 | + height: 100%; | ||
| 344 | + background: rgba(0, 0, 0, 0.7); | ||
| 345 | + backdrop-filter: blur(4px); | ||
| 346 | +} | ||
| 347 | + | ||
| 348 | +/* 加载内容样式 */ | ||
| 349 | +.loading-content { | ||
| 350 | + display: flex; | ||
| 351 | + flex-direction: column; | ||
| 352 | + align-items: center; | ||
| 353 | + justify-content: center; | ||
| 354 | + text-align: center; | ||
| 355 | + color: #fff; | ||
| 356 | +} | ||
| 357 | + | ||
| 358 | +.progress-circle { | ||
| 359 | + width: 80px; | ||
| 360 | + height: 80px; | ||
| 361 | + background-color: white; | ||
| 362 | + border-radius: 50%; | ||
| 363 | + display: flex; | ||
| 364 | + align-items: center; | ||
| 365 | + justify-content: center; | ||
| 366 | + margin: 0 auto 20px; | ||
| 367 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 368 | +} | ||
| 369 | + | ||
| 370 | +.progress-percent { | ||
| 371 | + font-size: 18px; | ||
| 372 | + font-weight: bold; | ||
| 373 | + color: #1989fa; | ||
| 374 | +} | ||
| 375 | + | ||
| 376 | +.loading-text { | ||
| 377 | + max-width: 200px; | ||
| 378 | +} | ||
| 379 | + | ||
| 380 | +.loading-title { | ||
| 381 | + font-size: 16px; | ||
| 382 | + font-weight: 500; | ||
| 383 | + margin-bottom: 8px; | ||
| 384 | + color: #fff; | ||
| 385 | +} | ||
| 386 | + | ||
| 387 | +.loading-subtitle { | ||
| 388 | + font-size: 14px; | ||
| 389 | + color: rgba(255, 255, 255, 0.8); | ||
| 390 | + line-height: 1.4; | ||
| 391 | +} | ||
| 392 | + | ||
| 393 | +/* 错误状态样式 */ | ||
| 394 | +.error-container { | ||
| 395 | + display: flex; | ||
| 396 | + align-items: center; | ||
| 397 | + justify-content: center; | ||
| 398 | + height: 100%; | ||
| 399 | + background: #f8f9fa; | ||
| 400 | +} | ||
| 401 | + | ||
| 402 | +.error-content { | ||
| 403 | + display: flex; | ||
| 404 | + flex-direction: column; | ||
| 405 | + align-items: center; | ||
| 406 | + justify-content: center; | ||
| 407 | + text-align: center; | ||
| 408 | + padding: 40px 20px; | ||
| 409 | + max-width: 300px; | ||
| 410 | +} | ||
| 411 | + | ||
| 412 | +.error-title { | ||
| 413 | + font-size: 18px; | ||
| 414 | + font-weight: 600; | ||
| 415 | + color: #333; | ||
| 416 | + margin: 16px 0 8px 0; | ||
| 417 | +} | ||
| 418 | + | ||
| 419 | +.error-message { | ||
| 420 | + font-size: 14px; | ||
| 421 | + color: #666; | ||
| 422 | + line-height: 1.5; | ||
| 423 | + margin-bottom: 20px; | ||
| 424 | +} | ||
| 425 | + | ||
| 426 | +.retry-btn { | ||
| 427 | + min-width: 100px; | ||
| 428 | +} | ||
| 429 | +</style> |
| ... | @@ -261,7 +261,7 @@ | ... | @@ -261,7 +261,7 @@ |
| 261 | </van-popup> | 261 | </van-popup> |
| 262 | 262 | ||
| 263 | <!-- PDF预览 --> | 263 | <!-- PDF预览 --> |
| 264 | - <PdfPreview v-model:show="pdfShow" :url="pdfUrl" :title="pdfTitle" @onLoad="onPdfLoad" /> | 264 | + <PdfViewer v-model:show="pdfShow" :url="pdfUrl" :title="pdfTitle" @onLoad="onPdfLoad" @onProgress="onPdfProgress" @onComplete="onPdfComplete" /> |
| 265 | 265 | ||
| 266 | <!-- Office 文档预览弹窗 --> | 266 | <!-- Office 文档预览弹窗 --> |
| 267 | <van-popup | 267 | <van-popup |
| ... | @@ -536,6 +536,7 @@ | ... | @@ -536,6 +536,7 @@ |
| 536 | <div class="flex items-center gap-1"> | 536 | <div class="flex items-center gap-1"> |
| 537 | <van-icon name="label-o" size="12" style="margin-right: 0.25rem;"/> | 537 | <van-icon name="label-o" size="12" style="margin-right: 0.25rem;"/> |
| 538 | <span>{{ getFileType(file.title || file.name) }}</span> | 538 | <span>{{ getFileType(file.title || file.name) }}</span> |
| 539 | + <span class="ml-2">{{ file.size ? (file.size / 1024 / 1024).toFixed(2) + 'MB' : '' }}</span> | ||
| 539 | </div> | 540 | </div> |
| 540 | <!-- 复制地址按钮 --> | 541 | <!-- 复制地址按钮 --> |
| 541 | <!-- <button | 542 | <!-- <button |
| ... | @@ -656,7 +657,7 @@ import { formatDate, wxInfo } from '@/utils/tools' | ... | @@ -656,7 +657,7 @@ import { formatDate, wxInfo } from '@/utils/tools' |
| 656 | import axios from 'axios'; | 657 | import axios from 'axios'; |
| 657 | import { v4 as uuidv4 } from "uuid"; | 658 | import { v4 as uuidv4 } from "uuid"; |
| 658 | import { useIntersectionObserver } from '@vueuse/core'; | 659 | import { useIntersectionObserver } from '@vueuse/core'; |
| 659 | -import PdfPreview from '@/components/ui/PdfPreview.vue'; | 660 | +import PdfViewer from '@/components/ui/PdfViewer.vue'; |
| 660 | import { showToast } from 'vant'; | 661 | import { showToast } from 'vant'; |
| 661 | 662 | ||
| 662 | // 导入接口 | 663 | // 导入接口 |
| ... | @@ -986,6 +987,21 @@ const onPdfLoad = (load) => { | ... | @@ -986,6 +987,21 @@ const onPdfLoad = (load) => { |
| 986 | }; | 987 | }; |
| 987 | 988 | ||
| 988 | /** | 989 | /** |
| 990 | + * PDF下载进度回调 | ||
| 991 | + * @param {number} progress - 下载进度 0-100 | ||
| 992 | + */ | ||
| 993 | +const onPdfProgress = (progress) => { | ||
| 994 | + // console.log('PDF下载进度:', progress); | ||
| 995 | +}; | ||
| 996 | + | ||
| 997 | +/** | ||
| 998 | + * PDF下载完成回调 | ||
| 999 | + */ | ||
| 1000 | +const onPdfComplete = () => { | ||
| 1001 | + // console.log('PDF下载完成'); | ||
| 1002 | +}; | ||
| 1003 | + | ||
| 1004 | +/** | ||
| 989 | * Office 文档渲染完成回调 | 1005 | * Office 文档渲染完成回调 |
| 990 | */ | 1006 | */ |
| 991 | const onOfficeRendered = () => { | 1007 | const onOfficeRendered = () => { | ... | ... |
| ... | @@ -2124,6 +2124,11 @@ pathe@^2.0.1, pathe@^2.0.2, pathe@^2.0.3: | ... | @@ -2124,6 +2124,11 @@ pathe@^2.0.1, pathe@^2.0.2, pathe@^2.0.3: |
| 2124 | resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" | 2124 | resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" |
| 2125 | integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== | 2125 | integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== |
| 2126 | 2126 | ||
| 2127 | +pdf-vue3@^1.0.12: | ||
| 2128 | + version "1.0.12" | ||
| 2129 | + resolved "https://registry.npmjs.org/pdf-vue3/-/pdf-vue3-1.0.12.tgz#8b2c4346cd75a7d3307aa6117c591c047549f4ed" | ||
| 2130 | + integrity sha512-7SMTx1RfRwdc+2WPniDzqM8MxJLqTNNzdyV0SeQTxeRLJGndb5Wv/fz5afO13oBSIvvaqcbZ/S3gF+XjqkSb9g== | ||
| 2131 | + | ||
| 2127 | pdfjs-dist@3.4.120: | 2132 | pdfjs-dist@3.4.120: |
| 2128 | version "3.4.120" | 2133 | version "3.4.120" |
| 2129 | resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-3.4.120.tgz#6f4222117157498f179c95dc4569fad6336a8fdd" | 2134 | resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-3.4.120.tgz#6f4222117157498f179c95dc4569fad6336a8fdd" | ... | ... |
-
Please register or login to post a comment