hookehuyr

feat(学习资料): 添加学习资料全屏弹窗和文件展示功能

- 移除原有文件列表内联展示方式
- 新增学习资料入口卡片,点击可打开全屏弹窗
- 实现文件分类展示、在线查看PDF和下载功能
- 添加文件图标识别和类型显示功能
- 优化课程时长显示为"建议时长"
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
46 </van-swipe> 46 </van-swipe>
47 </div> 47 </div>
48 <!-- 文件列表展示区域 --> 48 <!-- 文件列表展示区域 -->
49 - <div v-if="course.course_type === 'file'" class="w-full relative bg-white rounded-lg shadow-sm"> 49 + <!-- <div v-if="course.course_type === 'file'" class="w-full relative bg-white rounded-lg shadow-sm">
50 <div class="p-4 space-y-3"> 50 <div class="p-4 space-y-3">
51 <div v-for="(item, index) in courseFile?.list" :key="index" 51 <div v-for="(item, index) in courseFile?.list" :key="index"
52 class="group hover:bg-gray-50 transition-colors rounded-lg p-3"> 52 class="group hover:bg-gray-50 transition-colors rounded-lg p-3">
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
64 </div> 64 </div>
65 </div> 65 </div>
66 </div> 66 </div>
67 - </div> 67 + </div> -->
68 68
69 <!-- 默认展示区 --> 69 <!-- 默认展示区 -->
70 <div v-if="!course.course_type" class="relative" style="border-bottom: 1px solid #e5e7eb;"> 70 <div v-if="!course.course_type" class="relative" style="border-bottom: 1px solid #e5e7eb;">
...@@ -95,6 +95,24 @@ ...@@ -95,6 +95,24 @@
95 <!-- <span class="text-gray-300">|</span> --> 95 <!-- <span class="text-gray-300">|</span> -->
96 <!-- <span>没有字段{{ course.studyCount || 0 }}次学习</span> --> 96 <!-- <span>没有字段{{ course.studyCount || 0 }}次学习</span> -->
97 </div> 97 </div>
98 +
99 + <!-- 学习资料入口 -->
100 + <div v-if="course.course_type === 'file' && courseFile?.list && courseFile.list.length > 0"
101 + class="bg-white rounded-lg p-4 mb-4 cursor-pointer hover:bg-gray-50 transition-colors mt-4"
102 + @click="showMaterialsPopup = true">
103 + <div class="flex items-center justify-between">
104 + <div class="flex items-center gap-3">
105 + <div class="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
106 + <van-icon name="notes" class="text-green-600" size="20" />
107 + </div>
108 + <div>
109 + <div class="text-base font-medium text-gray-900">学习资料</div>
110 + <div class="text-sm text-gray-500">共{{ courseFile.list.length }}个文件</div>
111 + </div>
112 + </div>
113 + <van-icon name="arrow" class="text-gray-400" />
114 + </div>
115 + </div>
98 </div> 116 </div>
99 117
100 <div class="h-2 bg-gray-100"></div> 118 <div class="h-2 bg-gray-100"></div>
...@@ -229,7 +247,7 @@ ...@@ -229,7 +247,7 @@
229 <span v-if="course_type_maps[lesson.course_type]" class="mx-2">|</span> 247 <span v-if="course_type_maps[lesson.course_type]" class="mx-2">|</span>
230 <span>开课时间: {{ lesson.schedule_time ? dayjs(lesson.schedule_time).format('YYYY-MM-DD') : '暂无' }}</span> 248 <span>开课时间: {{ lesson.schedule_time ? dayjs(lesson.schedule_time).format('YYYY-MM-DD') : '暂无' }}</span>
231 <span class="mx-2">|</span> 249 <span class="mx-2">|</span>
232 - <span>课程时长: {{ lesson.duration }} 分钟</span> 250 + <span v-if="lesson.duration">建议时长: {{ lesson.duration }} 分钟</span>
233 </div> 251 </div>
234 </div> 252 </div>
235 </div> 253 </div>
...@@ -359,6 +377,109 @@ ...@@ -359,6 +377,109 @@
359 </div> 377 </div>
360 </div> 378 </div>
361 </van-popup> 379 </van-popup>
380 +
381 + <!-- 学习资料全屏弹窗 -->
382 + <van-popup
383 + v-model:show="showMaterialsPopup"
384 + position="right"
385 + :style="{ width: '100%', height: '100%' }"
386 + :close-on-click-overlay="false"
387 + :lock-scroll="true"
388 + >
389 + <div class="flex flex-col h-full bg-gray-50">
390 + <!-- 头部导航栏 -->
391 + <div class="bg-white shadow-sm border-b border-gray-100">
392 + <div class="flex items-center justify-between px-4 py-3">
393 + <div class="flex items-center gap-3">
394 + <van-button
395 + @click="showMaterialsPopup = false"
396 + type="default"
397 + size="small"
398 + round
399 + class="w-8 h-8 p-0 bg-gray-100 border-0"
400 + >
401 + <van-icon name="arrow-left" size="16" class="text-gray-600" />
402 + </van-button>
403 + <div>
404 + <h2 class="text-lg font-medium text-gray-900">学习资料</h2>
405 + <p class="text-xs text-gray-500">
406 + 共 {{ courseFile?.list ? courseFile.list.length : 0 }} 个文件
407 + </p>
408 + </div>
409 + </div>
410 + <div class="px-2 py-1 bg-blue-50 rounded-full">
411 + <span class="text-blue-600 text-sm font-medium">
412 + {{ courseFile?.list ? courseFile.list.length : 0 }}
413 + </span>
414 + </div>
415 + </div>
416 + </div>
417 +
418 + <!-- 文件列表 -->
419 + <div class="flex-1 overflow-y-auto p-4 pb-safe">
420 + <div v-if="courseFile?.list && courseFile.list.length > 0" class="space-y-4">
421 + <FrostedGlass
422 + v-for="(file, index) in courseFile.list"
423 + :key="index"
424 + :bgOpacity="70"
425 + blurLevel="md"
426 + className="p-5 hover:bg-white/80 transition-all duration-300 hover:shadow-xl hover:scale-[1.02] transform"
427 + >
428 + <!-- 文件信息 -->
429 + <div class="flex items-start gap-4 mb-4">
430 + <div class="w-12 h-12 bg-gradient-to-br from-blue-50 to-indigo-100 rounded-xl flex items-center justify-center flex-shrink-0 shadow-sm">
431 + <van-icon
432 + :name="getFileIcon(file.title || file.name)"
433 + class="text-blue-600"
434 + :size="22"
435 + />
436 + </div>
437 + <div class="flex-1 min-w-0">
438 + <h3 class="text-base font-semibold text-gray-900 mb-2 line-clamp-2">{{ file.title || file.name }}</h3>
439 + <div class="flex items-center gap-4 text-sm text-gray-600">
440 + <div class="flex items-center gap-1">
441 + <van-icon name="label-o" size="12" style="margin-right: 0.25rem;"/>
442 + <span>{{ getFileType(file.title || file.name) }}</span>
443 + </div>
444 + </div>
445 + </div>
446 + </div>
447 +
448 + <!-- 操作按钮 -->
449 + <div class="flex gap-3" style="margin: 1rem;">
450 + <!-- PDF文件只显示在线查看按钮 -->
451 + <button
452 + v-if="file.url && file.url.toLowerCase().includes('.pdf')"
453 + @click="showPdf(file)"
454 + class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2"
455 + >
456 + <van-icon name="eye-o" size="16" />
457 + 在线查看
458 + </button>
459 + <!-- 非PDF文件只显示下载按钮 -->
460 + <button
461 + v-else
462 + @click="downloadFile(file)"
463 + class="btn-secondary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2"
464 + >
465 + <van-icon name="down" size="16" />
466 + 下载文件
467 + </button>
468 + </div>
469 + </FrostedGlass>
470 + </div>
471 +
472 + <!-- 空状态 -->
473 + <div v-else class="flex flex-col items-center justify-center py-16 px-4">
474 + <div class="w-16 h-16 sm:w-20 sm:h-20 bg-gray-100 rounded-full flex items-center justify-center mb-4">
475 + <van-icon name="folder-o" :size="28" class="text-gray-400" />
476 + </div>
477 + <p class="text-gray-500 text-base sm:text-lg mb-2 text-center">暂无学习资料</p>
478 + <p class="text-gray-400 text-sm text-center">请联系老师上传相关资料</p>
479 + </div>
480 + </div>
481 + </div>
482 + </van-popup>
362 </div> 483 </div>
363 </template> 484 </template>
364 485
...@@ -368,6 +489,7 @@ import { useRoute, useRouter } from 'vue-router'; ...@@ -368,6 +489,7 @@ import { useRoute, useRouter } from 'vue-router';
368 import { useTitle } from '@vueuse/core'; 489 import { useTitle } from '@vueuse/core';
369 import VideoPlayer from '@/components/ui/VideoPlayer.vue'; 490 import VideoPlayer from '@/components/ui/VideoPlayer.vue';
370 import AudioPlayer from '@/components/ui/AudioPlayer.vue'; 491 import AudioPlayer from '@/components/ui/AudioPlayer.vue';
492 +import FrostedGlass from '@/components/ui/FrostedGlass.vue';
371 import dayjs from 'dayjs'; 493 import dayjs from 'dayjs';
372 import { formatDate } from '@/utils/tools' 494 import { formatDate } from '@/utils/tools'
373 import axios from 'axios'; 495 import axios from 'axios';
...@@ -795,6 +917,9 @@ const downloadFailInfo = ref({ ...@@ -795,6 +917,9 @@ const downloadFailInfo = ref({
795 fileUrl: '' 917 fileUrl: ''
796 }); 918 });
797 919
920 +// 学习资料弹窗状态
921 +const showMaterialsPopup = ref(false);
922 +
798 /** 923 /**
799 * 复制文件URL到剪贴板 924 * 复制文件URL到剪贴板
800 * @param {string} url - 要复制的URL 925 * @param {string} url - 要复制的URL
...@@ -1212,6 +1337,103 @@ const closeCheckInDialog = () => { ...@@ -1212,6 +1337,103 @@ const closeCheckInDialog = () => {
1212 showTimeoutTaskList.value = false; 1337 showTimeoutTaskList.value = false;
1213 default_list.value = task_list.value; 1338 default_list.value = task_list.value;
1214 } 1339 }
1340 +
1341 +/**
1342 + * 格式化文件大小
1343 + * @param {number} size - 文件大小(字节)
1344 + * @returns {string} 格式化后的文件大小
1345 + */
1346 +const formatFileSize = (size) => {
1347 + if (!size) return '0 B';
1348 + const units = ['B', 'KB', 'MB', 'GB'];
1349 + let index = 0;
1350 + let fileSize = size;
1351 +
1352 + while (fileSize >= 1024 && index < units.length - 1) {
1353 + fileSize /= 1024;
1354 + index++;
1355 + }
1356 +
1357 + return `${fileSize.toFixed(1)} ${units[index]}`;
1358 +}
1359 +
1360 +/**
1361 + * 根据文件名获取文件图标
1362 + * @param {string} fileName - 文件名
1363 + * @returns {string} 图标名称
1364 + */
1365 +const getFileIcon = (fileName) => {
1366 + // 添加空值检查
1367 + if (!fileName || typeof fileName !== 'string') {
1368 + return 'description'; // 默认图标
1369 + }
1370 +
1371 + const extension = fileName.split('.').pop().toLowerCase();
1372 + const iconMap = {
1373 + 'pdf': 'description',
1374 + 'doc': 'description',
1375 + 'docx': 'description',
1376 + 'xls': 'description',
1377 + 'xlsx': 'description',
1378 + 'ppt': 'description',
1379 + 'pptx': 'description',
1380 + 'txt': 'description',
1381 + 'zip': 'bag-o',
1382 + 'rar': 'bag-o',
1383 + '7z': 'bag-o',
1384 + 'mp3': 'music-o',
1385 + 'aac': 'music-o',
1386 + 'wav': 'music-o',
1387 + 'ogg': 'music-o',
1388 + 'mp4': 'video-o',
1389 + 'avi': 'video-o',
1390 + 'mov': 'video-o',
1391 + 'jpg': 'photo-o',
1392 + 'jpeg': 'photo-o',
1393 + 'png': 'photo-o',
1394 + 'gif': 'photo-o'
1395 + };
1396 + return iconMap[extension] || 'description';
1397 +}
1398 +
1399 +/**
1400 + * 根据文件名获取文件类型描述
1401 + * @param {string} fileName - 文件名
1402 + * @returns {string} 文件类型描述
1403 + */
1404 +const getFileType = (fileName) => {
1405 + // 添加空值检查
1406 + if (!fileName || typeof fileName !== 'string') {
1407 + return '未知文件'; // 默认类型
1408 + }
1409 +
1410 + const extension = fileName.split('.').pop().toLowerCase();
1411 + const typeMap = {
1412 + 'pdf': 'PDF文档',
1413 + 'doc': 'Word文档',
1414 + 'docx': 'Word文档',
1415 + 'xls': 'Excel表格',
1416 + 'xlsx': 'Excel表格',
1417 + 'ppt': 'PPT演示',
1418 + 'pptx': 'PPT演示',
1419 + 'txt': '文本文件',
1420 + 'zip': '压缩文件',
1421 + 'rar': '压缩文件',
1422 + '7z': '压缩文件',
1423 + 'mp3': '音频文件',
1424 + 'aac': '音频文件',
1425 + 'wav': '音频文件',
1426 + 'ogg': '音频文件',
1427 + 'mp4': '视频文件',
1428 + 'avi': '视频文件',
1429 + 'mov': '视频文件',
1430 + 'jpg': '图片文件',
1431 + 'jpeg': '图片文件',
1432 + 'png': '图片文件',
1433 + 'gif': '图片文件'
1434 + };
1435 + return typeMap[extension] || '未知文件';
1436 +}
1215 </script> 1437 </script>
1216 1438
1217 <style lang="less" scoped> 1439 <style lang="less" scoped>
......