2025-10-13 17:59:51 +08:00

1019 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="resources-page">
<!-- 横幅图区域 -->
<div class="banner-section">
<div class="banner-container">
<img src="/images/Featured_resources/精选资源轮播.png" alt="精选资源横幅" class="banner-image" />
</div>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<div class="container">
<!-- 精选视频区域 -->
<section class="featured-videos">
<h2 class="section-title">精选视频</h2>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<div class="loading-spinner"></div>
<p>正在加载精选资源...</p>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-container">
<p class="error-message">{{ error }}</p>
<button @click="fetchFeaturedResources" class="retry-button">重试</button>
</div>
<!-- 精选视频网格 -->
<div v-else class="featured-grid">
<div v-for="video in featuredVideos" :key="video.id" class="featured-card" @click="handleVideoClick(video)">
<div class="card-image">
<img
:src="video.thumbnailUrl || video.image"
:alt="video.name || video.title"
class="video-thumbnail"
/>
<div v-if="video.type === 0 && video.duration" class="duration-badge">
<img src="/images/Featured_resources/duration.png" alt="时长" class="duration-icon">
{{ formatDuration(video.duration) }}
</div>
<div v-if="video.type === 0" class="play-button">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M8 5V19L19 12L8 5Z" fill="white" />
</svg>
</div>
</div>
<div class="card-content">
<h3 class="card-title">{{ video.name || video.title }}</h3>
</div>
</div>
</div>
</section>
<!-- 全部视频区域 -->
<section class="all-videos">
<h2 class="section-title">全部视频</h2>
<!-- 筛选标签 -->
<div class="filter-tabs">
<button v-for="tab in videoTabs" :key="tab.id"
:class="['filter-tab', { active: activeVideoTab === tab.id }]" @click="activeVideoTab = tab.id">
{{ tab.name }}
</button>
</div>
<!-- 视频网格 -->
<div class="video-grid">
<div v-for="video in allVideos" :key="video.id" class="video-card" @click="handleVideoClick(video)">
<div class="card-image">
<img :src="video.image" :alt="video.title" class="video-thumbnail" />
<div class="duration-badge">
<img src="/images/Featured_resources/duration.png" alt="时长" class="duration-icon">
42:52
</div>
<div class="play-button">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<path d="M8 5V19L19 12L8 5Z" fill="white" />
</svg>
</div>
</div>
<div class="card-content">
<h3 class="card-title">{{ video.title }}</h3>
</div>
</div>
</div>
<div class="load-more">
<button class="load-more-btn">查看更多</button>
</div>
</section>
<!-- 全部图片区域 -->
<section class="all-images">
<h2 class="section-title">全部图片</h2>
<!-- 筛选标签 -->
<div class="filter-tabs">
<button v-for="tab in imageTabs" :key="tab.id"
:class="['filter-tab', { active: activeImageTab === tab.id }]" @click="activeImageTab = tab.id">
{{ tab.name }}
</button>
</div>
<!-- 图片网格 -->
<div class="image-grid">
<div v-for="image in allImages" :key="image.id" class="image-card">
<div class="card-image">
<img :src="image.image" :alt="image.title" class="image-thumbnail" />
</div>
<div class="card-content">
<h3 class="card-title">{{ image.title }}</h3>
</div>
</div>
</div>
<div class="load-more">
<button class="load-more-btn">查看更多</button>
</div>
</section>
</div>
</div>
<!-- 视频播放弹窗 -->
<div v-if="showVideoModal" class="video-modal-overlay" @click="closeVideoModal">
<div class="video-modal" @click.stop>
<div class="video-modal-header">
<h3 class="video-modal-title">{{ currentVideo?.name || currentVideo?.title || '视频播放' }}</h3>
<button class="close-btn" @click="closeVideoModal">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
</button>
</div>
<div class="video-modal-body">
<DPlayerVideo ref="videoPlayerRef" :video-url="currentVideoUrl" :placeholder-image="currentVideo?.thumbnailUrl || currentVideo?.image"
:placeholder-text="'点击播放视频'" :title="currentVideo?.name || currentVideo?.title || '视频播放'" @play="handleVideoPlay"
@pause="handleVideoPause" @ended="handleVideoEnded" @error="handleVideoError" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, onMounted } from 'vue'
import DPlayerVideo from '@/components/course/DPlayerVideo.vue'
import { ResourceApi, type FeaturedResource } from '@/api/modules/resource'
// 视频播放弹窗相关状态
const showVideoModal = ref(false)
const currentVideo = ref<any>(null)
const currentVideoUrl = ref('')
const videoPlayerRef = ref<InstanceType<typeof DPlayerVideo>>()
// 视频源配置
const VIDEO_CONFIG = {
// 本地视频(当前使用)
LOCAL: '/video/first.mp4',
// HLS流服务器准备好后使用
HLS: 'http://110.42.96.65:55513/learn/index.m3u8'
}
// 精选视频数据
const featuredVideos = ref<FeaturedResource[]>([])
const loading = ref(false)
const error = ref('')
// 视频筛选标签
const videoTabs = ref([
{ id: 'all', name: '全部' },
{ id: 'educational', name: '中小学教育资源' },
{ id: 'training', name: '师资培训' },
{ id: 'technology', name: '技术资源' },
{ id: 'management', name: '管理资源' }
])
const activeVideoTab = ref('all')
// 全部视频数据
const allVideos = ref([
{
id: 1,
title: '北京工业大学内部资源之一',
image: '/images/Featured_resources/全部视频1.png',
videoUrl: VIDEO_CONFIG.LOCAL
},
{
id: 2,
title: '北京工业大学内部资源之一',
image: '/images/Featured_resources/全部视频2.png',
videoUrl: VIDEO_CONFIG.LOCAL
},
{
id: 3,
title: '西安工业大学内部资源之一',
image: '/images/Featured_resources/全部视频3.png',
videoUrl: VIDEO_CONFIG.LOCAL
},
{
id: 4,
title: '北京工业大学内部资源之一',
image: '/images/Featured_resources/全部视频4.png',
videoUrl: VIDEO_CONFIG.LOCAL
},
{
id: 5,
title: '中国工业大学内部资源之一',
image: '/images/Featured_resources/全部视频5.png',
videoUrl: VIDEO_CONFIG.LOCAL
},
{
id: 6,
title: '西安工业大学内部资源之一',
image: '/images/Featured_resources/全部视频6.png',
videoUrl: VIDEO_CONFIG.LOCAL
},
{
id: 7,
title: '西安工业大学内部资源之一',
image: '/images/Featured_resources/全部视频7.png',
videoUrl: VIDEO_CONFIG.LOCAL
},
{
id: 8,
title: '内蒙古工业大学内部资源之一',
image: '/images/Featured_resources/全部视频8.png',
videoUrl: VIDEO_CONFIG.LOCAL
}
])
// 图片筛选标签
const imageTabs = ref([
{ id: 'all', name: '全部' },
{ id: 'educational', name: '中小学教育资源' },
{ id: 'training', name: '师资培训' },
{ id: 'technology', name: '技术资源' },
{ id: 'management', name: '管理资源' }
])
const activeImageTab = ref('all')
// 全部图片数据
const allImages = ref([
{
id: 1,
title: '中国工业大学内部资源之一',
image: '/images/Featured_resources/全部图片1.png'
},
{
id: 2,
title: '西安工业大学内部资源之一',
image: '/images/Featured_resources/全部图片2.png'
},
{
id: 3,
title: '西安工业大学内部资源之一',
image: '/images/Featured_resources/全部图片3.png'
},
{
id: 4,
title: '内蒙古工业大学内部资源之一',
image: '/images/Featured_resources/全部图片4.png'
},
{
id: 5,
title: '北京工业大学内部资源之一',
image: '/images/Featured_resources/全部图片5.png'
},
{
id: 6,
title: '北京工业大学内部资源之一',
image: '/images/Featured_resources/全部图片6.png'
},
{
id: 7,
title: '西安工业大学内部资源之一',
image: '/images/Featured_resources/全部图片7.png'
},
{
id: 8,
title: '内蒙古工业大学内部资源之一',
image: '/images/Featured_resources/全部图片8.png'
}
])
// 获取精选资源数据
const fetchFeaturedResources = async () => {
try {
loading.value = true
error.value = ''
const response = await ResourceApi.getFeaturedResources()
if (response.code === 200 && response.data?.result) {
featuredVideos.value = response.data.result
console.log('✅ 精选资源加载成功:', featuredVideos.value)
} else {
throw new Error(response.message || '获取精选资源失败')
}
} catch (err: any) {
error.value = err.message || '获取精选资源失败'
console.error('❌ 获取精选资源失败:', err)
} finally {
loading.value = false
}
}
// 格式化时长显示
const formatDuration = (seconds: number): string => {
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const secs = seconds % 60
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
} else {
return `${minutes}:${secs.toString().padStart(2, '0')}`
}
}
// 解析视频URL支持多清晰度
const parseVideoUrls = (fileUrl: string): string[] => {
if (!fileUrl) return []
return fileUrl.split(',').map(url => url.trim()).filter(url => url)
}
// 获取最佳视频URL
const getBestVideoUrl = (fileUrl: string): string => {
console.log('🔍 解析视频URL:', fileUrl)
const urls = parseVideoUrls(fileUrl)
console.log('🔍 解析后的URL数组:', urls)
if (urls.length === 0) {
console.warn('⚠️ 没有有效的视频URL使用本地视频作为备用')
return VIDEO_CONFIG.LOCAL
}
// 优先选择720p如果没有则选择第一个
const preferredUrl = urls.find(url => url.includes('720p')) || urls[0]
console.log('✅ 选择的视频URL:', preferredUrl)
return preferredUrl
}
// 视频播放相关方法
const handleVideoClick = async (video: FeaturedResource) => {
// 只有视频类型才能播放
if (video.type !== 0) {
console.log('这是图片资源,不能播放')
return
}
console.log('🎬 点击视频:', video.name)
console.log('🔍 视频数据:', {
id: video.id,
name: video.name,
type: video.type,
fileUrl: video.fileUrl,
thumbnailUrl: video.thumbnailUrl
})
currentVideo.value = video
// 获取最佳视频URL
const videoUrl = getBestVideoUrl(video.fileUrl)
currentVideoUrl.value = videoUrl
console.log('🎯 最终使用的视频URL:', currentVideoUrl.value)
showVideoModal.value = true
// 等待弹窗显示后初始化播放器
await nextTick()
if (videoPlayerRef.value) {
console.log('🔧 初始化播放器URL:', currentVideoUrl.value)
await videoPlayerRef.value.initializePlayer(currentVideoUrl.value)
}
}
const closeVideoModal = () => {
showVideoModal.value = false
currentVideo.value = null
currentVideoUrl.value = ''
// 销毁播放器实例
if (videoPlayerRef.value) {
videoPlayerRef.value.destroy()
}
}
// 图片加载处理
const handleImageError = (event: Event) => {
const img = event.target as HTMLImageElement
console.error('❌ 图片加载失败:', img.src)
// 设置默认图片
img.src = '/images/Featured_resources/default-thumbnail.png'
}
const handleImageLoad = (event: Event) => {
const img = event.target as HTMLImageElement
console.log('✅ 图片加载成功:', img.src)
}
// 页面初始化
onMounted(() => {
fetchFeaturedResources()
})
// DPlayer 事件处理方法
const handleVideoPlay = () => {
console.log('视频开始播放')
}
const handleVideoPause = () => {
console.log('视频暂停')
}
const handleVideoEnded = () => {
console.log('视频播放结束')
}
const handleVideoError = (error: any) => {
console.error('视频播放错误:', error)
// 可以在这里处理错误,比如自动切换到本地视频
if (currentVideoUrl.value !== VIDEO_CONFIG.LOCAL) {
currentVideoUrl.value = VIDEO_CONFIG.LOCAL
}
}
</script>
<style scoped>
.resources-page {
min-height: 100vh;
background: #F5F8FB;
}
/* 横幅图区域 */
.banner-section {
position: relative;
width: 100%;
overflow: hidden;
}
.banner-container {
position: relative;
height: auto;
}
.banner-image {
width: 100%;
height: auto;
display: block;
object-fit: cover;
}
/* 主要内容区域 */
.main-content {
padding: 0 0 40px 0;
}
.container {
margin: 0 auto;
/* padding: 0 20px; */
}
.section-title {
font-size: 36px;
color: #333;
margin: 0 0 30px 0;
text-align: center;
}
/* 精选视频区域 */
.featured-videos {
margin-bottom: 80px;
}
/* 加载和错误状态 */
.loading-container,
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #0088D1;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
color: #ff4757;
font-size: 16px;
margin-bottom: 16px;
}
.retry-button {
background: #0088D1;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.retry-button:hover {
background: #0066A1;
}
.featured-grid {
margin: 0 auto;
width: 1420px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
.featured-card {
background: white;
border-radius: 5px;
overflow: hidden;
transition: all 0.3s;
cursor: pointer;
}
.featured-card:hover {
transform: translateY(-2px);
}
.card-image {
position: relative;
height: 245px;
background: #f5f5f5;
overflow: hidden;
}
.video-thumbnail {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.image-thumbnail {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.image-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #e0e0e0 0%, #f0f0f0 100%);
display: flex;
align-items: center;
justify-content: center;
}
.image-placeholder::after {
content: '图片占位';
color: #999;
font-size: 14px;
}
.play-button {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 48px;
height: 48px;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
z-index: 1;
}
.duration-badge {
position: absolute;
top: 14px;
left: 14px;
background: #9CAEB8;
color: white;
font-size: 16px;
padding: 2px 8px;
border-radius: 6px;
z-index: 1;
display: flex;
align-items: center;
gap: 4px;
}
.duration-icon {
width: 16px;
height: 16px;
object-fit: contain;
}
.play-button:hover {
background: rgba(0, 0, 0, 0.8);
transform: translate(-50%, -50%) scale(1.1);
}
.card-content {
padding: 20px 15px;
}
.card-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin: 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* 筛选标签 */
.filter-tabs {
display: flex;
gap: 20px;
margin: 0 auto 30px auto;
justify-content: center;
align-items: center;
width: fit-content;
}
.filter-tab {
padding: 8px 16px;
border: none;
background: #F2F2F2;
color: #878787;
font-size: 18px;
cursor: pointer;
border-radius: 4px;
transition: all 0.3s;
white-space: nowrap;
font-weight: 400;
border-radius: 30px;
}
.filter-tab:hover {
color: #4A90E2;
}
.filter-tab.active {
background: #EEF9FF;
color: #0088D1;
font-weight: 500;
}
/* 全部视频区域 */
.all-videos {
width: 100vw;
padding: 40px 0;
background-color: #fff;
margin-bottom: 80px;
}
.video-grid {
width: 1420px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 40px;
}
.video-card {
background: white;
border-radius: 5px;
overflow: hidden;
transition: all 0.3s;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.video-card .card-image {
height: 245px;
}
.video-card .play-button {
width: 40px;
height: 40px;
}
/* 全部图片区域 */
.all-images {
margin-bottom: 40px;
}
.image-grid {
margin: auto;
width: 1420px;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 40px;
}
.image-card {
background: white;
border-radius: 5px;
overflow: hidden;
transition: all 0.3s;
cursor: pointer;
}
.image-card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.image-card .card-image {
height: 240px;
}
/* 查看更多按钮 */
.load-more {
text-align: center;
}
.load-more-btn {
padding: 12px 32px;
background: none;
border: none;
color: #292C2E;
border-radius: 6px;
cursor: pointer;
font-size: 18px;
transition: all 0.3s;
}
.load-more-btn:hover {
background: #f8f9fa;
border-color: #4A90E2;
color: #4A90E2;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.container {
padding: 0 16px;
}
.featured-grid {
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.video-grid,
.image-grid {
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.banner-title {
font-size: 28px;
}
}
@media (max-width: 768px) {
.banner-section {
height: 300px;
}
.banner-title {
font-size: 24px;
}
.main-content {
padding: 40px 0;
}
.featured-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.video-grid,
.image-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.section-title {
font-size: 20px;
margin-bottom: 20px;
text-align: center;
}
.filter-tabs {
overflow-x: auto;
gap: 16px;
justify-content: flex-start;
padding: 0 20px;
}
.filter-tab {
padding: 6px 16px;
font-size: 13px;
flex-shrink: 0;
}
.featured-videos,
.all-videos {
margin-bottom: 60px;
}
.loading-container,
.error-container {
padding: 40px 20px;
}
}
@media (max-width: 480px) {
.banner-image {
max-height: 250px;
object-fit: cover;
}
.container {
padding: 0 12px;
}
.video-grid,
.image-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.card-image {
height: 160px;
}
.video-card .card-image,
.image-card .card-image {
height: 120px;
}
.section-title {
font-size: 18px;
text-align: center;
}
.filter-tabs {
margin-bottom: 20px;
gap: 12px;
}
.filter-tab {
padding: 6px 12px;
font-size: 12px;
}
}
/* 视频播放弹窗样式 */
.video-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 20px;
}
.video-modal {
background: white;
border-radius: 12px;
overflow: hidden;
max-width: 90vw;
max-height: 90vh;
width: 1000px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
animation: modalFadeIn 0.3s ease-out;
}
@keyframes modalFadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.video-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 1px solid #e5e7eb;
background: #f9fafb;
}
.video-modal-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
line-height: 1.4;
}
.close-btn {
background: none;
border: none;
padding: 8px;
border-radius: 6px;
cursor: pointer;
color: #666;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
background: #e5e7eb;
color: #333;
}
.video-modal-body {
padding: 0;
background: #000;
position: relative;
height: 60vh;
min-height: 400px;
}
/* 播放按钮显示样式 */
.play-button {
opacity: 0;
transition: opacity 0.3s ease;
}
.featured-card:hover .play-button,
.video-card:hover .play-button {
opacity: 1;
}
.featured-card .play-button {
width: 48px;
height: 48px;
}
.video-card .play-button {
width: 40px;
height: 40px;
}
/* 响应式设计 - 弹窗 */
@media (max-width: 768px) {
.video-modal {
width: 95vw;
max-width: none;
}
.video-modal-header {
padding: 16px 20px;
}
.video-modal-title {
font-size: 16px;
}
.video-modal-body {
height: 50vh;
min-height: 300px;
}
}
@media (max-width: 480px) {
.video-modal-overlay {
padding: 10px;
}
.video-modal {
width: 100%;
}
.video-modal-header {
padding: 12px 16px;
}
.video-modal-title {
font-size: 14px;
}
.video-modal-body {
height: 40vh;
min-height: 250px;
}
}
</style>