hookehuyr

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

- 移除原有文件列表内联展示方式
- 新增学习资料入口卡片,点击可打开全屏弹窗
- 实现文件分类展示、在线查看PDF和下载功能
- 添加文件图标识别和类型显示功能
- 优化课程时长显示为"建议时长"
......@@ -46,7 +46,7 @@
</van-swipe>
</div>
<!-- 文件列表展示区域 -->
<div v-if="course.course_type === 'file'" class="w-full relative bg-white rounded-lg shadow-sm">
<!-- <div v-if="course.course_type === 'file'" class="w-full relative bg-white rounded-lg shadow-sm">
<div class="p-4 space-y-3">
<div v-for="(item, index) in courseFile?.list" :key="index"
class="group hover:bg-gray-50 transition-colors rounded-lg p-3">
......@@ -64,7 +64,7 @@
</div>
</div>
</div>
</div>
</div> -->
<!-- 默认展示区 -->
<div v-if="!course.course_type" class="relative" style="border-bottom: 1px solid #e5e7eb;">
......@@ -95,6 +95,24 @@
<!-- <span class="text-gray-300">|</span> -->
<!-- <span>没有字段{{ course.studyCount || 0 }}次学习</span> -->
</div>
<!-- 学习资料入口 -->
<div v-if="course.course_type === 'file' && courseFile?.list && courseFile.list.length > 0"
class="bg-white rounded-lg p-4 mb-4 cursor-pointer hover:bg-gray-50 transition-colors mt-4"
@click="showMaterialsPopup = true">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<van-icon name="notes" class="text-green-600" size="20" />
</div>
<div>
<div class="text-base font-medium text-gray-900">学习资料</div>
<div class="text-sm text-gray-500">共{{ courseFile.list.length }}个文件</div>
</div>
</div>
<van-icon name="arrow" class="text-gray-400" />
</div>
</div>
</div>
<div class="h-2 bg-gray-100"></div>
......@@ -229,7 +247,7 @@
<span v-if="course_type_maps[lesson.course_type]" class="mx-2">|</span>
<span>开课时间: {{ lesson.schedule_time ? dayjs(lesson.schedule_time).format('YYYY-MM-DD') : '暂无' }}</span>
<span class="mx-2">|</span>
<span>课程时长: {{ lesson.duration }} 分钟</span>
<span v-if="lesson.duration">建议时长: {{ lesson.duration }} 分钟</span>
</div>
</div>
</div>
......@@ -359,6 +377,109 @@
</div>
</div>
</van-popup>
<!-- 学习资料全屏弹窗 -->
<van-popup
v-model:show="showMaterialsPopup"
position="right"
:style="{ width: '100%', height: '100%' }"
:close-on-click-overlay="false"
:lock-scroll="true"
>
<div class="flex flex-col h-full bg-gray-50">
<!-- 头部导航栏 -->
<div class="bg-white shadow-sm border-b border-gray-100">
<div class="flex items-center justify-between px-4 py-3">
<div class="flex items-center gap-3">
<van-button
@click="showMaterialsPopup = false"
type="default"
size="small"
round
class="w-8 h-8 p-0 bg-gray-100 border-0"
>
<van-icon name="arrow-left" size="16" class="text-gray-600" />
</van-button>
<div>
<h2 class="text-lg font-medium text-gray-900">学习资料</h2>
<p class="text-xs text-gray-500">
共 {{ courseFile?.list ? courseFile.list.length : 0 }} 个文件
</p>
</div>
</div>
<div class="px-2 py-1 bg-blue-50 rounded-full">
<span class="text-blue-600 text-sm font-medium">
{{ courseFile?.list ? courseFile.list.length : 0 }}
</span>
</div>
</div>
</div>
<!-- 文件列表 -->
<div class="flex-1 overflow-y-auto p-4 pb-safe">
<div v-if="courseFile?.list && courseFile.list.length > 0" class="space-y-4">
<FrostedGlass
v-for="(file, index) in courseFile.list"
:key="index"
:bgOpacity="70"
blurLevel="md"
className="p-5 hover:bg-white/80 transition-all duration-300 hover:shadow-xl hover:scale-[1.02] transform"
>
<!-- 文件信息 -->
<div class="flex items-start gap-4 mb-4">
<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">
<van-icon
:name="getFileIcon(file.title || file.name)"
class="text-blue-600"
:size="22"
/>
</div>
<div class="flex-1 min-w-0">
<h3 class="text-base font-semibold text-gray-900 mb-2 line-clamp-2">{{ file.title || file.name }}</h3>
<div class="flex items-center gap-4 text-sm text-gray-600">
<div class="flex items-center gap-1">
<van-icon name="label-o" size="12" style="margin-right: 0.25rem;"/>
<span>{{ getFileType(file.title || file.name) }}</span>
</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="flex gap-3" style="margin: 1rem;">
<!-- PDF文件只显示在线查看按钮 -->
<button
v-if="file.url && file.url.toLowerCase().includes('.pdf')"
@click="showPdf(file)"
class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2"
>
<van-icon name="eye-o" size="16" />
在线查看
</button>
<!-- 非PDF文件只显示下载按钮 -->
<button
v-else
@click="downloadFile(file)"
class="btn-secondary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2"
>
<van-icon name="down" size="16" />
下载文件
</button>
</div>
</FrostedGlass>
</div>
<!-- 空状态 -->
<div v-else class="flex flex-col items-center justify-center py-16 px-4">
<div class="w-16 h-16 sm:w-20 sm:h-20 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<van-icon name="folder-o" :size="28" class="text-gray-400" />
</div>
<p class="text-gray-500 text-base sm:text-lg mb-2 text-center">暂无学习资料</p>
<p class="text-gray-400 text-sm text-center">请联系老师上传相关资料</p>
</div>
</div>
</div>
</van-popup>
</div>
</template>
......@@ -368,6 +489,7 @@ import { useRoute, useRouter } from 'vue-router';
import { useTitle } from '@vueuse/core';
import VideoPlayer from '@/components/ui/VideoPlayer.vue';
import AudioPlayer from '@/components/ui/AudioPlayer.vue';
import FrostedGlass from '@/components/ui/FrostedGlass.vue';
import dayjs from 'dayjs';
import { formatDate } from '@/utils/tools'
import axios from 'axios';
......@@ -795,6 +917,9 @@ const downloadFailInfo = ref({
fileUrl: ''
});
// 学习资料弹窗状态
const showMaterialsPopup = ref(false);
/**
* 复制文件URL到剪贴板
* @param {string} url - 要复制的URL
......@@ -1212,6 +1337,103 @@ const closeCheckInDialog = () => {
showTimeoutTaskList.value = false;
default_list.value = task_list.value;
}
/**
* 格式化文件大小
* @param {number} size - 文件大小(字节)
* @returns {string} 格式化后的文件大小
*/
const formatFileSize = (size) => {
if (!size) return '0 B';
const units = ['B', 'KB', 'MB', 'GB'];
let index = 0;
let fileSize = size;
while (fileSize >= 1024 && index < units.length - 1) {
fileSize /= 1024;
index++;
}
return `${fileSize.toFixed(1)} ${units[index]}`;
}
/**
* 根据文件名获取文件图标
* @param {string} fileName - 文件名
* @returns {string} 图标名称
*/
const getFileIcon = (fileName) => {
// 添加空值检查
if (!fileName || typeof fileName !== 'string') {
return 'description'; // 默认图标
}
const extension = fileName.split('.').pop().toLowerCase();
const iconMap = {
'pdf': 'description',
'doc': 'description',
'docx': 'description',
'xls': 'description',
'xlsx': 'description',
'ppt': 'description',
'pptx': 'description',
'txt': 'description',
'zip': 'bag-o',
'rar': 'bag-o',
'7z': 'bag-o',
'mp3': 'music-o',
'aac': 'music-o',
'wav': 'music-o',
'ogg': 'music-o',
'mp4': 'video-o',
'avi': 'video-o',
'mov': 'video-o',
'jpg': 'photo-o',
'jpeg': 'photo-o',
'png': 'photo-o',
'gif': 'photo-o'
};
return iconMap[extension] || 'description';
}
/**
* 根据文件名获取文件类型描述
* @param {string} fileName - 文件名
* @returns {string} 文件类型描述
*/
const getFileType = (fileName) => {
// 添加空值检查
if (!fileName || typeof fileName !== 'string') {
return '未知文件'; // 默认类型
}
const extension = fileName.split('.').pop().toLowerCase();
const typeMap = {
'pdf': 'PDF文档',
'doc': 'Word文档',
'docx': 'Word文档',
'xls': 'Excel表格',
'xlsx': 'Excel表格',
'ppt': 'PPT演示',
'pptx': 'PPT演示',
'txt': '文本文件',
'zip': '压缩文件',
'rar': '压缩文件',
'7z': '压缩文件',
'mp3': '音频文件',
'aac': '音频文件',
'wav': '音频文件',
'ogg': '音频文件',
'mp4': '视频文件',
'avi': '视频文件',
'mov': '视频文件',
'jpg': '图片文件',
'jpeg': '图片文件',
'png': '图片文件',
'gif': '图片文件'
};
return typeMap[extension] || '未知文件';
}
</script>
<style lang="less" scoped>
......