feat(office): 添加Office文档预览功能组件
添加OfficeViewer组件支持docx/excel/pptx文件预览 在StudyDetailPage中集成Office文档预览功能 添加相关依赖包@vue-office/docx/excel/pptx和vue-demi
Showing
6 changed files
with
461 additions
and
11 deletions
| ... | @@ -29,6 +29,9 @@ | ... | @@ -29,6 +29,9 @@ |
| 29 | "@vant/touch-emulator": "^1.4.0", | 29 | "@vant/touch-emulator": "^1.4.0", |
| 30 | "@vant/use": "^1.6.0", | 30 | "@vant/use": "^1.6.0", |
| 31 | "@videojs-player/vue": "^1.0.0", | 31 | "@videojs-player/vue": "^1.0.0", |
| 32 | + "@vue-office/docx": "^1.6.3", | ||
| 33 | + "@vue-office/excel": "^1.7.14", | ||
| 34 | + "@vue-office/pptx": "^1.0.1", | ||
| 32 | "browser-md5-file": "^1.1.1", | 35 | "browser-md5-file": "^1.1.1", |
| 33 | "dayjs": "^1.11.13", | 36 | "dayjs": "^1.11.13", |
| 34 | "lodash": "^4.17.21", | 37 | "lodash": "^4.17.21", |
| ... | @@ -38,6 +41,7 @@ | ... | @@ -38,6 +41,7 @@ |
| 38 | "vconsole": "^3.15.1", | 41 | "vconsole": "^3.15.1", |
| 39 | "video.js": "^7.21.7", | 42 | "video.js": "^7.21.7", |
| 40 | "vue": "^3.5.13", | 43 | "vue": "^3.5.13", |
| 44 | + "vue-demi": "0.14.6", | ||
| 41 | "vue-router": "^4.5.0", | 45 | "vue-router": "^4.5.0", |
| 42 | "weixin-js-sdk": "^1.6.5" | 46 | "weixin-js-sdk": "^1.6.5" |
| 43 | }, | 47 | }, | ... | ... |
| ... | @@ -13,13 +13,16 @@ declare module 'vue' { | ... | @@ -13,13 +13,16 @@ declare module 'vue' { |
| 13 | AudioPlayer: typeof import('./components/ui/AudioPlayer.vue')['default'] | 13 | AudioPlayer: typeof import('./components/ui/AudioPlayer.vue')['default'] |
| 14 | BottomNav: typeof import('./components/layout/BottomNav.vue')['default'] | 14 | BottomNav: typeof import('./components/layout/BottomNav.vue')['default'] |
| 15 | CheckInDialog: typeof import('./components/ui/CheckInDialog.vue')['default'] | 15 | CheckInDialog: typeof import('./components/ui/CheckInDialog.vue')['default'] |
| 16 | + CollapsibleCalendar: typeof import('./components/ui/CollapsibleCalendar.vue')['default'] | ||
| 16 | ConfirmDialog: typeof import('./components/ui/ConfirmDialog.vue')['default'] | 17 | ConfirmDialog: typeof import('./components/ui/ConfirmDialog.vue')['default'] |
| 17 | CourseCard: typeof import('./components/ui/CourseCard.vue')['default'] | 18 | CourseCard: typeof import('./components/ui/CourseCard.vue')['default'] |
| 18 | CourseList: typeof import('./components/courses/CourseList.vue')['default'] | 19 | CourseList: typeof import('./components/courses/CourseList.vue')['default'] |
| 20 | + FormPage: typeof import('./components/infoEntry/formPage.vue')['default'] | ||
| 19 | FrostedGlass: typeof import('./components/ui/FrostedGlass.vue')['default'] | 21 | FrostedGlass: typeof import('./components/ui/FrostedGlass.vue')['default'] |
| 20 | GradientHeader: typeof import('./components/ui/GradientHeader.vue')['default'] | 22 | GradientHeader: typeof import('./components/ui/GradientHeader.vue')['default'] |
| 21 | LiveStreamCard: typeof import('./components/ui/LiveStreamCard.vue')['default'] | 23 | LiveStreamCard: typeof import('./components/ui/LiveStreamCard.vue')['default'] |
| 22 | MenuItem: typeof import('./components/ui/MenuItem.vue')['default'] | 24 | MenuItem: typeof import('./components/ui/MenuItem.vue')['default'] |
| 25 | + OfficeViewer: typeof import('./components/ui/OfficeViewer.vue')['default'] | ||
| 23 | PdfPreview: typeof import('./components/ui/PdfPreview.vue')['default'] | 26 | PdfPreview: typeof import('./components/ui/PdfPreview.vue')['default'] |
| 24 | ReviewPopup: typeof import('./components/courses/ReviewPopup.vue')['default'] | 27 | ReviewPopup: typeof import('./components/courses/ReviewPopup.vue')['default'] |
| 25 | RouterLink: typeof import('vue-router')['RouterLink'] | 28 | RouterLink: typeof import('vue-router')['RouterLink'] | ... | ... |
src/components/ui/OfficeViewer.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="office-viewer"> | ||
| 3 | + <!-- 错误状态 --> | ||
| 4 | + <div v-if="error" class="error-container"> | ||
| 5 | + <van-icon name="warning-o" size="24" color="#ff6b6b" /> | ||
| 6 | + <span class="error-text">{{ error }}</span> | ||
| 7 | + <van-button | ||
| 8 | + type="primary" | ||
| 9 | + size="small" | ||
| 10 | + @click="retry" | ||
| 11 | + class="retry-btn" | ||
| 12 | + > | ||
| 13 | + 重试 | ||
| 14 | + </van-button> | ||
| 15 | + </div> | ||
| 16 | + | ||
| 17 | + <!-- 文档预览 --> | ||
| 18 | + <div v-else class="document-container"> | ||
| 19 | + <!-- DOCX 文档预览 --> | ||
| 20 | + <vue-office-docx | ||
| 21 | + v-if="fileType === 'docx'" | ||
| 22 | + :src="src" | ||
| 23 | + :style="{ height: containerHeight }" | ||
| 24 | + @rendered="onRendered" | ||
| 25 | + @error="onError" | ||
| 26 | + /> | ||
| 27 | + | ||
| 28 | + <!-- Excel 文档预览 --> | ||
| 29 | + <vue-office-excel | ||
| 30 | + v-else-if="fileType === 'excel'" | ||
| 31 | + :src="src" | ||
| 32 | + :style="{ height: containerHeight }" | ||
| 33 | + @rendered="onRendered" | ||
| 34 | + @error="onError" | ||
| 35 | + /> | ||
| 36 | + | ||
| 37 | + <!-- PPTX 文档预览 --> | ||
| 38 | + <vue-office-pptx | ||
| 39 | + v-else-if="fileType === 'pptx'" | ||
| 40 | + :src="src" | ||
| 41 | + :style="{ height: containerHeight }" | ||
| 42 | + @rendered="onRendered" | ||
| 43 | + @error="onError" | ||
| 44 | + /> | ||
| 45 | + | ||
| 46 | + <!-- 不支持的文件类型 --> | ||
| 47 | + <div v-else class="unsupported-container"> | ||
| 48 | + <van-icon name="warning-o" size="24" color="#ff6b6b" /> | ||
| 49 | + <span class="unsupported-text">不支持的文件类型: {{ fileType }}</span> | ||
| 50 | + </div> | ||
| 51 | + </div> | ||
| 52 | + </div> | ||
| 53 | +</template> | ||
| 54 | + | ||
| 55 | +<script setup> | ||
| 56 | +import { ref, computed, onMounted, watch } from 'vue' | ||
| 57 | +import VueOfficeDocx from '@vue-office/docx' | ||
| 58 | +import VueOfficeExcel from '@vue-office/excel' | ||
| 59 | +import VueOfficePptx from '@vue-office/pptx' | ||
| 60 | +import '@vue-office/docx/lib/index.css' | ||
| 61 | +import '@vue-office/excel/lib/index.css' | ||
| 62 | + | ||
| 63 | +// Props 定义 | ||
| 64 | +const props = defineProps({ | ||
| 65 | + // 文件 URL 或 ArrayBuffer 数据 | ||
| 66 | + src: { | ||
| 67 | + type: [String, ArrayBuffer], | ||
| 68 | + required: true | ||
| 69 | + }, | ||
| 70 | + // 文件类型 | ||
| 71 | + fileType: { | ||
| 72 | + type: String, | ||
| 73 | + required: true, | ||
| 74 | + validator: (value) => ['docx', 'excel', 'pptx'].includes(value) | ||
| 75 | + }, | ||
| 76 | + // 容器高度 | ||
| 77 | + height: { | ||
| 78 | + type: String, | ||
| 79 | + default: '70vh' | ||
| 80 | + } | ||
| 81 | +}) | ||
| 82 | + | ||
| 83 | +// Emits 定义 | ||
| 84 | +const emit = defineEmits(['rendered', 'error', 'retry']) | ||
| 85 | + | ||
| 86 | +// 响应式数据 | ||
| 87 | +const error = ref('') | ||
| 88 | + | ||
| 89 | +// 计算属性 | ||
| 90 | +const containerHeight = computed(() => props.height) | ||
| 91 | + | ||
| 92 | +/** | ||
| 93 | + * 文档渲染完成回调 | ||
| 94 | + */ | ||
| 95 | +const onRendered = () => { | ||
| 96 | + console.log('Office document rendered successfully') | ||
| 97 | + error.value = '' | ||
| 98 | + emit('rendered') | ||
| 99 | +} | ||
| 100 | + | ||
| 101 | +/** | ||
| 102 | + * 文档渲染错误回调 | ||
| 103 | + * @param {Error} err - 错误对象 | ||
| 104 | + */ | ||
| 105 | +const onError = (err) => { | ||
| 106 | + console.error('Office document render error:', err) | ||
| 107 | + error.value = '文档加载失败,请检查文件格式或网络连接' | ||
| 108 | + emit('error', err) | ||
| 109 | +} | ||
| 110 | + | ||
| 111 | +/** | ||
| 112 | + * 重试加载文档 | ||
| 113 | + */ | ||
| 114 | +const retry = () => { | ||
| 115 | + console.log('Retrying to load office document') | ||
| 116 | + error.value = '' | ||
| 117 | + emit('retry') | ||
| 118 | +} | ||
| 119 | + | ||
| 120 | +/** | ||
| 121 | + * 监听 src 变化,重新加载文档 | ||
| 122 | + */ | ||
| 123 | +watch(() => props.src, (newSrc) => { | ||
| 124 | + console.log('Office document src changed:', newSrc) | ||
| 125 | + if (newSrc) { | ||
| 126 | + error.value = '' | ||
| 127 | + | ||
| 128 | + // 验证 URL 是否有效 | ||
| 129 | + if (typeof newSrc === 'string') { | ||
| 130 | + // 检查是否是有效的 URL | ||
| 131 | + try { | ||
| 132 | + new URL(newSrc) | ||
| 133 | + console.log('Valid URL detected:', newSrc) | ||
| 134 | + } catch (e) { | ||
| 135 | + // 可能是相对路径,检查是否以 http 开头 | ||
| 136 | + if (!newSrc.startsWith('http') && !newSrc.startsWith('/')) { | ||
| 137 | + console.warn('Invalid URL format:', newSrc) | ||
| 138 | + error.value = '文档地址格式不正确' | ||
| 139 | + return | ||
| 140 | + } | ||
| 141 | + } | ||
| 142 | + } | ||
| 143 | + } | ||
| 144 | +}, { immediate: true }) | ||
| 145 | + | ||
| 146 | +/** | ||
| 147 | + * 监听 fileType 变化 | ||
| 148 | + */ | ||
| 149 | +watch(() => props.fileType, (newType) => { | ||
| 150 | + console.log('Office document fileType changed:', newType) | ||
| 151 | +}) | ||
| 152 | + | ||
| 153 | +// 组件挂载时的初始化 | ||
| 154 | +onMounted(() => { | ||
| 155 | + console.log('OfficeViewer 组件已挂载') | ||
| 156 | +}) | ||
| 157 | +</script> | ||
| 158 | + | ||
| 159 | +<style lang="less" scoped> | ||
| 160 | +.office-viewer { | ||
| 161 | + width: 100%; | ||
| 162 | + height: 100%; | ||
| 163 | + background: #fff; | ||
| 164 | + border-radius: 8px; | ||
| 165 | + overflow: hidden; | ||
| 166 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 167 | + | ||
| 168 | + .error-container { | ||
| 169 | + display: flex; | ||
| 170 | + flex-direction: column; | ||
| 171 | + align-items: center; | ||
| 172 | + justify-content: center; | ||
| 173 | + height: 200px; | ||
| 174 | + gap: 12px; | ||
| 175 | + padding: 20px; | ||
| 176 | + | ||
| 177 | + .error-text { | ||
| 178 | + font-size: 14px; | ||
| 179 | + color: #666; | ||
| 180 | + text-align: center; | ||
| 181 | + line-height: 1.5; | ||
| 182 | + } | ||
| 183 | + | ||
| 184 | + .retry-btn { | ||
| 185 | + margin-top: 8px; | ||
| 186 | + } | ||
| 187 | + } | ||
| 188 | + | ||
| 189 | + .unsupported-container { | ||
| 190 | + display: flex; | ||
| 191 | + flex-direction: column; | ||
| 192 | + align-items: center; | ||
| 193 | + justify-content: center; | ||
| 194 | + height: 200px; | ||
| 195 | + gap: 12px; | ||
| 196 | + padding: 20px; | ||
| 197 | + | ||
| 198 | + .unsupported-text { | ||
| 199 | + font-size: 14px; | ||
| 200 | + color: #666; | ||
| 201 | + text-align: center; | ||
| 202 | + line-height: 1.5; | ||
| 203 | + } | ||
| 204 | + } | ||
| 205 | + | ||
| 206 | + .document-container { | ||
| 207 | + width: 100%; | ||
| 208 | + height: 100%; | ||
| 209 | + overflow: auto; | ||
| 210 | + | ||
| 211 | + // 覆盖 vue-office 默认样式 | ||
| 212 | + :deep(.vue-office-docx), | ||
| 213 | + :deep(.vue-office-excel), | ||
| 214 | + :deep(.vue-office-pptx) { | ||
| 215 | + border: none; | ||
| 216 | + border-radius: 8px; | ||
| 217 | + min-height: 100%; | ||
| 218 | + } | ||
| 219 | + | ||
| 220 | + // Excel 表格样式优化 | ||
| 221 | + :deep(.vue-office-excel) { | ||
| 222 | + .luckysheet-cell-main { | ||
| 223 | + font-size: 12px; | ||
| 224 | + } | ||
| 225 | + } | ||
| 226 | + | ||
| 227 | + // DOCX 文档样式优化 | ||
| 228 | + :deep(.vue-office-docx) { | ||
| 229 | + .docx-wrapper { | ||
| 230 | + padding: 16px; | ||
| 231 | + min-height: 100%; | ||
| 232 | + background: #fff; | ||
| 233 | + } | ||
| 234 | + } | ||
| 235 | + | ||
| 236 | + // PPTX 演示文稿样式优化 | ||
| 237 | + :deep(.vue-office-pptx) { | ||
| 238 | + .pptx-wrapper { | ||
| 239 | + background: #f5f5f5; | ||
| 240 | + min-height: 100%; | ||
| 241 | + } | ||
| 242 | + } | ||
| 243 | + } | ||
| 244 | +} | ||
| 245 | + | ||
| 246 | +// 移动端适配 | ||
| 247 | +@media (max-width: 768px) { | ||
| 248 | + .office-viewer { | ||
| 249 | + border-radius: 0; | ||
| 250 | + box-shadow: none; | ||
| 251 | + | ||
| 252 | + .document-container { | ||
| 253 | + // 移动端确保滚动流畅 | ||
| 254 | + -webkit-overflow-scrolling: touch; | ||
| 255 | + | ||
| 256 | + :deep(.vue-office-docx) { | ||
| 257 | + .docx-wrapper { | ||
| 258 | + padding: 12px; | ||
| 259 | + font-size: 14px; | ||
| 260 | + line-height: 1.6; | ||
| 261 | + } | ||
| 262 | + } | ||
| 263 | + | ||
| 264 | + :deep(.vue-office-excel) { | ||
| 265 | + .luckysheet-cell-main { | ||
| 266 | + font-size: 11px; | ||
| 267 | + } | ||
| 268 | + } | ||
| 269 | + | ||
| 270 | + :deep(.vue-office-pptx) { | ||
| 271 | + .pptx-wrapper { | ||
| 272 | + padding: 8px; | ||
| 273 | + } | ||
| 274 | + } | ||
| 275 | + } | ||
| 276 | + } | ||
| 277 | +} | ||
| 278 | +</style> |
| ... | @@ -122,10 +122,10 @@ const handleMounted = (payload) => { | ... | @@ -122,10 +122,10 @@ const handleMounted = (payload) => { |
| 122 | const errorCode = player.value.error(); | 122 | const errorCode = player.value.error(); |
| 123 | if (errorCode) { | 123 | if (errorCode) { |
| 124 | console.error('错误代码:', errorCode.code, '错误信息:', errorCode.message); | 124 | console.error('错误代码:', errorCode.code, '错误信息:', errorCode.message); |
| 125 | - | 125 | + |
| 126 | // 显示用户友好的错误信息 | 126 | // 显示用户友好的错误信息 |
| 127 | showErrorOverlay.value = true; | 127 | showErrorOverlay.value = true; |
| 128 | - | 128 | + |
| 129 | // 根据错误类型进行处理 | 129 | // 根据错误类型进行处理 |
| 130 | switch (errorCode.code) { | 130 | switch (errorCode.code) { |
| 131 | case 4: // MEDIA_ERR_SRC_NOT_SUPPORTED | 131 | case 4: // MEDIA_ERR_SRC_NOT_SUPPORTED |
| ... | @@ -231,10 +231,10 @@ const retryLoad = () => { | ... | @@ -231,10 +231,10 @@ const retryLoad = () => { |
| 231 | errorMessage.value = '重试次数已达上限,请稍后再试'; | 231 | errorMessage.value = '重试次数已达上限,请稍后再试'; |
| 232 | return; | 232 | return; |
| 233 | } | 233 | } |
| 234 | - | 234 | + |
| 235 | retryCount.value++; | 235 | retryCount.value++; |
| 236 | showErrorOverlay.value = false; | 236 | showErrorOverlay.value = false; |
| 237 | - | 237 | + |
| 238 | if (player.value && !player.value.isDisposed()) { | 238 | if (player.value && !player.value.isDisposed()) { |
| 239 | console.log(`第${retryCount.value}次重试加载视频`); | 239 | console.log(`第${retryCount.value}次重试加载视频`); |
| 240 | player.value.load(); | 240 | player.value.load(); |
| ... | @@ -431,7 +431,7 @@ defineExpose({ | ... | @@ -431,7 +431,7 @@ defineExpose({ |
| 431 | visibility: visible !important; | 431 | visibility: visible !important; |
| 432 | margin-right: 8px; | 432 | margin-right: 8px; |
| 433 | } | 433 | } |
| 434 | - | 434 | + |
| 435 | :deep(.vjs-playback-rate .vjs-playback-rate-value) { | 435 | :deep(.vjs-playback-rate .vjs-playback-rate-value) { |
| 436 | font-size: 1.3em; | 436 | font-size: 1.3em; |
| 437 | padding: 0 6px; | 437 | padding: 0 6px; |
| ... | @@ -441,14 +441,14 @@ defineExpose({ | ... | @@ -441,14 +441,14 @@ defineExpose({ |
| 441 | width: auto; | 441 | width: auto; |
| 442 | margin: 0; | 442 | margin: 0; |
| 443 | } | 443 | } |
| 444 | - | 444 | + |
| 445 | :deep(.vjs-playback-rate .vjs-menu) { | 445 | :deep(.vjs-playback-rate .vjs-menu) { |
| 446 | min-width: 100px; | 446 | min-width: 100px; |
| 447 | max-height: 180px; | 447 | max-height: 180px; |
| 448 | bottom: 120%; | 448 | bottom: 120%; |
| 449 | right: -10px; | 449 | right: -10px; |
| 450 | } | 450 | } |
| 451 | - | 451 | + |
| 452 | :deep(.vjs-playback-rate .vjs-menu-item) { | 452 | :deep(.vjs-playback-rate .vjs-menu-item) { |
| 453 | padding: 12px 16px; | 453 | padding: 12px 16px; |
| 454 | font-size: 16px; | 454 | font-size: 16px; |
| ... | @@ -464,7 +464,7 @@ defineExpose({ | ... | @@ -464,7 +464,7 @@ defineExpose({ |
| 464 | :deep(.vjs-playback-rate) { | 464 | :deep(.vjs-playback-rate) { |
| 465 | margin-right: 6px; | 465 | margin-right: 6px; |
| 466 | } | 466 | } |
| 467 | - | 467 | + |
| 468 | :deep(.vjs-playback-rate .vjs-playback-rate-value) { | 468 | :deep(.vjs-playback-rate .vjs-playback-rate-value) { |
| 469 | font-size: 1.2em; | 469 | font-size: 1.2em; |
| 470 | padding: 0 4px; | 470 | padding: 0 4px; |
| ... | @@ -474,12 +474,12 @@ defineExpose({ | ... | @@ -474,12 +474,12 @@ defineExpose({ |
| 474 | width: auto; | 474 | width: auto; |
| 475 | margin: 0; | 475 | margin: 0; |
| 476 | } | 476 | } |
| 477 | - | 477 | + |
| 478 | :deep(.vjs-playback-rate .vjs-menu) { | 478 | :deep(.vjs-playback-rate .vjs-menu) { |
| 479 | min-width: 90px; | 479 | min-width: 90px; |
| 480 | right: -5px; | 480 | right: -5px; |
| 481 | } | 481 | } |
| 482 | - | 482 | + |
| 483 | :deep(.vjs-playback-rate .vjs-menu-item) { | 483 | :deep(.vjs-playback-rate .vjs-menu-item) { |
| 484 | padding: 10px 12px; | 484 | padding: 10px 12px; |
| 485 | font-size: 15px; | 485 | font-size: 15px; | ... | ... |
| ... | @@ -265,6 +265,32 @@ | ... | @@ -265,6 +265,32 @@ |
| 265 | <!-- PDF预览 --> | 265 | <!-- PDF预览 --> |
| 266 | <PdfPreview v-model:show="pdfShow" :url="pdfUrl" :title="pdfTitle" @onLoad="onPdfLoad" /> | 266 | <PdfPreview v-model:show="pdfShow" :url="pdfUrl" :title="pdfTitle" @onLoad="onPdfLoad" /> |
| 267 | 267 | ||
| 268 | + <!-- Office 文档预览弹窗 --> | ||
| 269 | + <van-popup | ||
| 270 | + v-model:show="officeShow" | ||
| 271 | + position="center" | ||
| 272 | + round | ||
| 273 | + closeable | ||
| 274 | + :style="{ height: '80%', width: '90%' }" | ||
| 275 | + > | ||
| 276 | + <div class="h-full flex flex-col"> | ||
| 277 | + <div class="p-4 border-b border-gray-200"> | ||
| 278 | + <h3 class="text-lg font-medium text-center truncate">{{ officeTitle }}</h3> | ||
| 279 | + </div> | ||
| 280 | + <div class="flex-1 overflow-auto"> | ||
| 281 | + <OfficeViewer | ||
| 282 | + v-if="officeShow && officeUrl && officeFileType" | ||
| 283 | + :src="officeUrl" | ||
| 284 | + :file-type="officeFileType" | ||
| 285 | + height="100%" | ||
| 286 | + @rendered="onOfficeRendered" | ||
| 287 | + @error="onOfficeError" | ||
| 288 | + @retry="onOfficeRetry" | ||
| 289 | + /> | ||
| 290 | + </div> | ||
| 291 | + </div> | ||
| 292 | + </van-popup> | ||
| 293 | + | ||
| 268 | <!-- 音频播放器弹窗 --> | 294 | <!-- 音频播放器弹窗 --> |
| 269 | <van-popup | 295 | <van-popup |
| 270 | v-model:show="audioShow" | 296 | v-model:show="audioShow" |
| ... | @@ -542,9 +568,18 @@ | ... | @@ -542,9 +568,18 @@ |
| 542 | 568 | ||
| 543 | <!-- 移动端:根据文件类型显示不同的预览按钮 --> | 569 | <!-- 移动端:根据文件类型显示不同的预览按钮 --> |
| 544 | <template v-else> | 570 | <template v-else> |
| 571 | + <!-- Office 文档显示预览按钮 --> | ||
| 572 | + <button | ||
| 573 | + v-if="isOfficeFile(file.url)" | ||
| 574 | + @click="showOfficeDocument(file)" | ||
| 575 | + class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2" | ||
| 576 | + > | ||
| 577 | + <van-icon name="description" size="16" /> | ||
| 578 | + 文档预览 | ||
| 579 | + </button> | ||
| 545 | <!-- PDF文件显示在线查看按钮 --> | 580 | <!-- PDF文件显示在线查看按钮 --> |
| 546 | <button | 581 | <button |
| 547 | - v-if="file.url && file.url.toLowerCase().includes('.pdf')" | 582 | + v-else-if="file.url && file.url.toLowerCase().includes('.pdf')" |
| 548 | @click="showPdf(file)" | 583 | @click="showPdf(file)" |
| 549 | class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2" | 584 | class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2" |
| 550 | > | 585 | > |
| ... | @@ -613,6 +648,7 @@ import { useTitle } from '@vueuse/core'; | ... | @@ -613,6 +648,7 @@ import { useTitle } from '@vueuse/core'; |
| 613 | import VideoPlayer from '@/components/ui/VideoPlayer.vue'; | 648 | import VideoPlayer from '@/components/ui/VideoPlayer.vue'; |
| 614 | import AudioPlayer from '@/components/ui/AudioPlayer.vue'; | 649 | import AudioPlayer from '@/components/ui/AudioPlayer.vue'; |
| 615 | import FrostedGlass from '@/components/ui/FrostedGlass.vue'; | 650 | import FrostedGlass from '@/components/ui/FrostedGlass.vue'; |
| 651 | +import OfficeViewer from '@/components/ui/OfficeViewer.vue'; | ||
| 616 | import dayjs from 'dayjs'; | 652 | import dayjs from 'dayjs'; |
| 617 | import { formatDate, wxInfo } from '@/utils/tools' | 653 | import { formatDate, wxInfo } from '@/utils/tools' |
| 618 | import axios from 'axios'; | 654 | import axios from 'axios'; |
| ... | @@ -817,6 +853,12 @@ const pdfShow = ref(false); | ... | @@ -817,6 +853,12 @@ const pdfShow = ref(false); |
| 817 | const pdfTitle = ref(''); | 853 | const pdfTitle = ref(''); |
| 818 | const pdfUrl = ref(''); | 854 | const pdfUrl = ref(''); |
| 819 | 855 | ||
| 856 | +// Office 文档预览相关 | ||
| 857 | +const officeShow = ref(false); | ||
| 858 | +const officeTitle = ref(''); | ||
| 859 | +const officeUrl = ref(''); | ||
| 860 | +const officeFileType = ref(''); | ||
| 861 | + | ||
| 820 | // 音频播放器相关 | 862 | // 音频播放器相关 |
| 821 | const audioShow = ref(false); | 863 | const audioShow = ref(false); |
| 822 | const audioTitle = ref(''); | 864 | const audioTitle = ref(''); |
| ... | @@ -842,6 +884,46 @@ const showPdf = ({ title, url, meta_id }) => { | ... | @@ -842,6 +884,46 @@ const showPdf = ({ title, url, meta_id }) => { |
| 842 | }; | 884 | }; |
| 843 | 885 | ||
| 844 | /** | 886 | /** |
| 887 | + * 显示 Office 文档预览 | ||
| 888 | + * @param {Object} file - 文件对象,包含title、url、meta_id | ||
| 889 | + */ | ||
| 890 | +const showOfficeDocument = ({ title, url, meta_id }) => { | ||
| 891 | + console.log('showOfficeDocument called with:', { title, url, meta_id }); | ||
| 892 | + | ||
| 893 | + // 清理 URL 中的反引号和多余空格 | ||
| 894 | + const cleanUrl = url.replace(/`/g, '').trim(); | ||
| 895 | + | ||
| 896 | + officeTitle.value = title; | ||
| 897 | + officeUrl.value = cleanUrl; | ||
| 898 | + officeFileType.value = getOfficeFileType(cleanUrl); | ||
| 899 | + | ||
| 900 | + console.log('Office document props set:', { | ||
| 901 | + title: officeTitle.value, | ||
| 902 | + url: officeUrl.value, | ||
| 903 | + fileType: officeFileType.value | ||
| 904 | + }); | ||
| 905 | + | ||
| 906 | + // 验证 URL 格式 | ||
| 907 | + try { | ||
| 908 | + new URL(cleanUrl); | ||
| 909 | + console.log('URL validation passed:', cleanUrl); | ||
| 910 | + } catch (error) { | ||
| 911 | + console.error('Invalid URL format:', cleanUrl, error); | ||
| 912 | + showToast('文档链接格式不正确'); | ||
| 913 | + return; | ||
| 914 | + } | ||
| 915 | + | ||
| 916 | + officeShow.value = true; | ||
| 917 | + | ||
| 918 | + // 新增记录 | ||
| 919 | + let paramsObj = { | ||
| 920 | + schedule_id: courseId.value, | ||
| 921 | + meta_id | ||
| 922 | + } | ||
| 923 | + addRecord(paramsObj); | ||
| 924 | +}; | ||
| 925 | + | ||
| 926 | +/** | ||
| 845 | * 显示音频播放器 | 927 | * 显示音频播放器 |
| 846 | * @param {Object} file - 文件对象,包含title、url、meta_id | 928 | * @param {Object} file - 文件对象,包含title、url、meta_id |
| 847 | */ | 929 | */ |
| ... | @@ -901,6 +983,31 @@ const onPdfLoad = (load) => { | ... | @@ -901,6 +983,31 @@ const onPdfLoad = (load) => { |
| 901 | // console.warn('pdf加载状态', load); | 983 | // console.warn('pdf加载状态', load); |
| 902 | }; | 984 | }; |
| 903 | 985 | ||
| 986 | +/** | ||
| 987 | + * Office 文档渲染完成回调 | ||
| 988 | + */ | ||
| 989 | +const onOfficeRendered = () => { | ||
| 990 | + console.log('Office 文档渲染完成'); | ||
| 991 | + showToast('文档加载完成'); | ||
| 992 | +}; | ||
| 993 | + | ||
| 994 | +/** | ||
| 995 | + * Office 文档渲染错误回调 | ||
| 996 | + * @param {Error} error - 错误对象 | ||
| 997 | + */ | ||
| 998 | +const onOfficeError = (error) => { | ||
| 999 | + console.error('Office 文档渲染失败:', error); | ||
| 1000 | + showToast('文档加载失败,请重试'); | ||
| 1001 | +}; | ||
| 1002 | + | ||
| 1003 | +/** | ||
| 1004 | + * Office 文档重试回调 | ||
| 1005 | + */ | ||
| 1006 | +const onOfficeRetry = () => { | ||
| 1007 | + console.log('重试加载 Office 文档'); | ||
| 1008 | + // 可以在这里添加重新获取文档的逻辑 | ||
| 1009 | +}; | ||
| 1010 | + | ||
| 904 | const courseId = computed(() => { | 1011 | const courseId = computed(() => { |
| 905 | return route.params.id || ''; | 1012 | return route.params.id || ''; |
| 906 | }); | 1013 | }); |
| ... | @@ -1396,6 +1503,44 @@ const isImageFile = (fileName) => { | ... | @@ -1396,6 +1503,44 @@ const isImageFile = (fileName) => { |
| 1396 | } | 1503 | } |
| 1397 | 1504 | ||
| 1398 | /** | 1505 | /** |
| 1506 | + * 判断文件是否为 Office 文档 | ||
| 1507 | + * @param {string} fileName - 文件名 | ||
| 1508 | + * @returns {boolean} 是否为 Office 文档 | ||
| 1509 | + */ | ||
| 1510 | +const isOfficeFile = (fileName) => { | ||
| 1511 | + if (!fileName || typeof fileName !== 'string') { | ||
| 1512 | + return false; | ||
| 1513 | + } | ||
| 1514 | + | ||
| 1515 | + const extension = fileName.split('.').pop().toLowerCase(); | ||
| 1516 | + const officeTypes = ['docx', 'xlsx', 'xls', 'pptx']; | ||
| 1517 | + return officeTypes.includes(extension); | ||
| 1518 | +} | ||
| 1519 | + | ||
| 1520 | +/** | ||
| 1521 | + * 获取 Office 文档类型 | ||
| 1522 | + * @param {string} fileName - 文件名 | ||
| 1523 | + * @returns {string} 文档类型 (docx, excel, pptx) | ||
| 1524 | + */ | ||
| 1525 | +const getOfficeFileType = (fileName) => { | ||
| 1526 | + if (!fileName || typeof fileName !== 'string') { | ||
| 1527 | + return ''; | ||
| 1528 | + } | ||
| 1529 | + | ||
| 1530 | + const extension = fileName.split('.').pop().toLowerCase(); | ||
| 1531 | + | ||
| 1532 | + if (extension === 'docx') { | ||
| 1533 | + return 'docx'; | ||
| 1534 | + } else if (extension === 'xlsx' || extension === 'xls') { | ||
| 1535 | + return 'excel'; | ||
| 1536 | + } else if (extension === 'pptx') { | ||
| 1537 | + return 'pptx'; | ||
| 1538 | + } | ||
| 1539 | + | ||
| 1540 | + return ''; | ||
| 1541 | +} | ||
| 1542 | + | ||
| 1543 | +/** | ||
| 1399 | * 音频播放事件 | 1544 | * 音频播放事件 |
| 1400 | * @param audio 音频对象 | 1545 | * @param audio 音频对象 |
| 1401 | */ | 1546 | */ | ... | ... |
| ... | @@ -713,6 +713,21 @@ | ... | @@ -713,6 +713,21 @@ |
| 713 | resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz#71a8fc82d4d2e425af304c35bf389506f674d89b" | 713 | resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz#71a8fc82d4d2e425af304c35bf389506f674d89b" |
| 714 | integrity sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg== | 714 | integrity sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg== |
| 715 | 715 | ||
| 716 | +"@vue-office/docx@^1.6.3": | ||
| 717 | + version "1.6.3" | ||
| 718 | + resolved "https://registry.yarnpkg.com/@vue-office/docx/-/docx-1.6.3.tgz#0c183b13553c029fea702eb8b8b4260a7e1f0961" | ||
| 719 | + integrity sha512-Cs+3CAaRBOWOiW4XAhTwwxJ0dy8cPIf6DqfNvYcD3YACiLwO4kuawLF2IAXxyijhbuOeoFsfvoVbOc16A/4bZA== | ||
| 720 | + | ||
| 721 | +"@vue-office/excel@^1.7.14": | ||
| 722 | + version "1.7.14" | ||
| 723 | + resolved "https://registry.yarnpkg.com/@vue-office/excel/-/excel-1.7.14.tgz#6e1446abae9690a09eed6f5e8c09bb923b3d5b10" | ||
| 724 | + integrity sha512-pVUgt+emDQUnW7q22CfnQ+jl43mM/7IFwYzOg7lwOwPEbiVB4K4qEQf+y/bc4xGXz75w1/e3Kz3G6wAafmFBFg== | ||
| 725 | + | ||
| 726 | +"@vue-office/pptx@^1.0.1": | ||
| 727 | + version "1.0.1" | ||
| 728 | + resolved "https://registry.yarnpkg.com/@vue-office/pptx/-/pptx-1.0.1.tgz#f34d5a7aa78cd534c5724540dbe53f0539e94b3e" | ||
| 729 | + integrity sha512-+V7Kctzl6f6+Yk4NaD/wQGRIkqLWcowe0jEhPexWQb8Oilbzt1OyhWRWcMsxNDTdrgm6aMLP+0/tmw27cxddMg== | ||
| 730 | + | ||
| 716 | "@vue/babel-helper-vue-transform-on@1.4.0": | 731 | "@vue/babel-helper-vue-transform-on@1.4.0": |
| 717 | version "1.4.0" | 732 | version "1.4.0" |
| 718 | resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.4.0.tgz#616020488692a9c42a613280d62ed1b727045d95" | 733 | resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.4.0.tgz#616020488692a9c42a613280d62ed1b727045d95" |
| ... | @@ -2855,6 +2870,11 @@ vite@^6.2.0: | ... | @@ -2855,6 +2870,11 @@ vite@^6.2.0: |
| 2855 | optionalDependencies: | 2870 | optionalDependencies: |
| 2856 | fsevents "~2.3.3" | 2871 | fsevents "~2.3.3" |
| 2857 | 2872 | ||
| 2873 | +vue-demi@0.14.6: | ||
| 2874 | + version "0.14.6" | ||
| 2875 | + resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.6.tgz#dc706582851dc1cdc17a0054f4fec2eb6df74c92" | ||
| 2876 | + integrity sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w== | ||
| 2877 | + | ||
| 2858 | vue-router@^4.5.0: | 2878 | vue-router@^4.5.0: |
| 2859 | version "4.5.1" | 2879 | version "4.5.1" |
| 2860 | resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.5.1.tgz#47bffe2d3a5479d2886a9a244547a853aa0abf69" | 2880 | resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.5.1.tgz#47bffe2d3a5479d2886a9a244547a853aa0abf69" | ... | ... |
-
Please register or login to post a comment