Przeglądaj źródła

技术亮点可以放视频

WQQ 3 tygodni temu
rodzic
commit
14a9f5ede7

+ 21 - 4
RuoYi-Vue3/src/data/projects/taipuRiver.js

@@ -8,6 +8,7 @@ import siYuImage from '@/assets/images/四预.png'
 import historicalFloodImage from '@/assets/images/历史洪水.png'
 import waterResourcesImage from '@/assets/images/水资源模型调配.png'
 import taiPuRiverVideo from '@/assets/video/太浦河资料.mp4'
+import gongHuVideo from '@/assets/video/贡湖水文站视频.mp4'
 
 // 太浦河项目数据
 export const taiPuHeProject = {
@@ -117,25 +118,41 @@ export const taiPuHeProject = {
       title: '数据底板',
       description: '数据底板构建核心在于构建符合平原感潮河网双向水流、水位多变特点的全要素、高精度、动态化数字映射。',
       features: ['多源数据融合', '水利枢纽', '三维场景'],
-      images: [taiPuRiverImage2, gongHuImage, buZhangDiaoYanImage] // 图片集合
+      media: [
+        { type: 'image', src: taiPuRiverImage2, alt: '太浦河全景' },
+        { type: 'video', src: gongHuVideo, alt: '贡湖水文站视频' },
+        { type: 'image', src: buZhangDiaoYanImage, alt: '部长调研' }
+      ] // 媒体集合(支持图片和视频)
     },
     floodControl: {
       title: '四预一体化',
       description: '建成满足多目标统筹调度要求的“四预”应用,实现洪水的精准预测、预警、预演和预案,提升流域防洪能力。',
       features: ['洪水预测预警', '防洪调度预演', '应急预案管理', '实时风险评估'],
-      images: [siYuImage, fangXunYanLianImage, fangXunYanLian2023Image] // 图片集合
+      media: [
+        { type: 'image', src: siYuImage, alt: '四预一体化' },
+        { type: 'image', src: fangXunYanLianImage, alt: '防汛演练' },
+        { type: 'image', src: fangXunYanLian2023Image, alt: '2023防汛演练' }
+      ] // 媒体集合(支持图片和视频)
     },
     historicalTopics: {
       title: '历史专题',
       description: '系统梳理和分析流域历史水文数据、灾害事件和治理经验,为当前和未来的水资源管理提供参考。',
       features: ['历史数据挖掘', '灾害事件分析', '治理经验总结', '专题知识库'],
-      images: [historicalFloodImage, kangXianBaoGongImage, taiPuRiverImage2] // 图片集合
+      media: [
+        { type: 'image', src: historicalFloodImage, alt: '历史洪水' },
+        { type: 'image', src: kangXianBaoGongImage, alt: '抗咸保供' },
+        { type: 'image', src: taiPuRiverImage2, alt: '太浦河全景' }
+      ] // 媒体集合(支持图片和视频)
     },
     waterResources: {
       title: '水资源调配',
       description: '实现水资源的优化配置和调度,保障流域供水安全,提升水资源利用效率,支持经济社会可持续发展。',
       features: ['优化调度策略', '供需平衡分析', '调度方案评估', '实时监控调度'],
-      images: [waterResourcesImage, taiPuRiverImage2, gongHuImage] // 图片集合
+      media: [
+        { type: 'video', src: taiPuRiverVideo, alt: '太浦河水资源调配视频' },
+        { type: 'image', src: waterResourcesImage, alt: '水资源模型调配' },
+        { type: 'image', src: gongHuImage, alt: '贡湖水资源' }
+      ] // 媒体集合(支持图片和视频)
     }
   },
   achievements: [

+ 158 - 6
RuoYi-Vue3/src/views/front/ProjectCases.vue

@@ -121,28 +121,57 @@
               <div v-if="currentHighlight" class="highlight-detail">
                 <!-- 大图展示 -->
                 <div class="highlight-image">
+                  <!-- 图片展示 -->
                   <img 
-                    :src="currentHighlight.images[activeImageIndex]" 
-                    :alt="currentHighlight.title"
+                    v-if="currentHighlight.media[activeImageIndex].type === 'image'"
+                    :src="currentHighlight.media[activeImageIndex].src" 
+                    :alt="currentHighlight.media[activeImageIndex].alt"
                     class="detail-image"
                     loading="lazy"
                   >
+                  <!-- 视频展示 -->
+          <video
+            v-else
+            :src="currentHighlight.media[activeImageIndex].src"
+            :alt="currentHighlight.media[activeImageIndex].alt"
+            :poster="videoPosters[currentHighlight.media[activeImageIndex].src]"
+            class="detail-video"
+            controls
+            autoplay
+            muted
+            loop
+          ></video>
                 </div>
                 <!-- 缩略图小图集 -->
                 <div class="thumbnail-gallery">
                   <div 
-                    v-for="(image, index) in currentHighlight.images" 
+                    v-for="(item, index) in currentHighlight.media" 
                     :key="index"
                     class="thumbnail-item"
                     :class="{ 'active': activeImageIndex === index }"
                     @click="handleImageClick(index)"
                   >
+                    <!-- 图片缩略图 -->
                     <img 
-                      :src="image" 
-                      :alt="`${currentHighlight.title} - 图${index + 1}`"
+                      v-if="item.type === 'image'"
+                      :src="item.src" 
+                      :alt="item.alt"
                       class="thumbnail-image"
                       loading="lazy"
                     >
+                    <!-- 视频缩略图 -->
+                    <div v-else class="thumbnail-video-container">
+                      <img 
+                        :src="videoPosters[item.src] || item.src" 
+                        :alt="item.alt"
+                        class="thumbnail-image"
+                        loading="lazy"
+                        @error="handleThumbnailError($event, item)"
+                      >
+                      <div class="video-play-icon">
+                        <el-icon class="play-icon"><VideoPlay /></el-icon>
+                      </div>
+                    </div>
                   </div>
                 </div>
               </div>
@@ -473,9 +502,12 @@ const viewerVideoPlaying = ref(false)
 
 // 技术亮点选中状态
 const activeHighlight = ref(0)
-// 当前图集的图片索引
+// 活动的图片索引
 const activeImageIndex = ref(0)
 
+// 视频封面缓存
+const videoPosters = ref({})
+
 // 当前选中的技术亮点
 const currentHighlight = computed(() => {
   if (activeHighlight.value !== null) {
@@ -485,10 +517,63 @@ const currentHighlight = computed(() => {
   return null
 })
 
+// 提取视频第一帧作为封面
+const extractVideoPoster = (videoUrl) => {
+  return new Promise((resolve) => {
+    // 如果已经缓存了封面,直接返回
+    if (videoPosters.value[videoUrl]) {
+      resolve(videoPosters.value[videoUrl])
+      return
+    }
+
+    const video = document.createElement('video')
+    // 本地视频不需要设置crossOrigin
+    // video.crossOrigin = 'anonymous'
+    video.src = videoUrl
+    
+    video.addEventListener('loadeddata', () => {
+      const canvas = document.createElement('canvas')
+      canvas.width = video.videoWidth || 320
+      canvas.height = video.videoHeight || 240
+      
+      const ctx = canvas.getContext('2d')
+      ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
+      
+      const posterUrl = canvas.toDataURL('image/jpeg', 0.8)
+      // 缓存封面
+      videoPosters.value[videoUrl] = posterUrl
+      resolve(posterUrl)
+      
+      // 清理
+      video.remove()
+      canvas.remove()
+    })
+    
+    video.addEventListener('error', () => {
+      // 出错时返回null,使用默认封面
+      resolve(null)
+      video.remove()
+    })
+  })
+}
+
+// 预加载当前技术亮点的视频封面
+const preloadVideoPosters = () => {
+  if (!currentHighlight.value || !currentHighlight.value.media) return
+  
+  currentHighlight.value.media.forEach(item => {
+    if (item.type === 'video' && !videoPosters.value[item.src]) {
+      extractVideoPoster(item.src)
+    }
+  })
+}
+
 // 处理技术亮点点击
 const handleHighlightClick = (index) => {
   activeHighlight.value = index
   activeImageIndex.value = 0 // 切换分类时重置图片索引
+  // 预加载当前技术亮点的视频封面
+  preloadVideoPosters()
 }
 
 // 处理图片点击
@@ -679,12 +764,36 @@ const router = useRouter()
 // 当前项目数据
 const currentProject = ref(allProjects[0])
 
+// 处理视频缩略图加载错误
+const handleThumbnailError = (event, item) => {
+  // 加载失败时显示默认占位图(使用项目中已存在的图片)
+  event.target.src = require('@/assets/images/太浦河全景.png')
+}
+
+// 预加载所有技术亮点中的视频封面
+const preloadAllVideoPosters = () => {
+  if (!currentProject.value || !currentProject.value.technicalHighlights) return
+  
+  // 遍历所有技术亮点
+  Object.values(currentProject.value.technicalHighlights).forEach(highlight => {
+    if (highlight.media) {
+      highlight.media.forEach(item => {
+        if (item.type === 'video' && !videoPosters.value[item.src]) {
+          extractVideoPoster(item.src)
+        }
+      })
+    }
+  })
+}
+
 // 页面加载时获取项目数据
 onMounted(() => {
   const projectId = route.params.projectId || 'tai-pu-river'
   const project = getProjectById(projectId)
   if (project) {
     currentProject.value = project
+    // 预加载所有技术亮点中的视频封面
+    preloadAllVideoPosters()
   }
 })
 
@@ -1227,6 +1336,49 @@ const goToHome = () => {
   transform: scale(1.05);
 }
 
+/* 视频样式 */
+.detail-video {
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+  transition: transform 0.3s ease;
+}
+
+.highlight-image:hover .detail-video {
+  transform: scale(1.05);
+}
+
+/* 视频缩略图容器 */
+.thumbnail-video-container {
+  position: relative;
+  width: 100%;
+  height: 100%;
+}
+
+/* 视频播放图标 */
+.video-play-icon {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: rgba(0, 0, 0, 0.3);
+  opacity: 0;
+  transition: opacity 0.3s ease;
+}
+
+.thumbnail-item:hover .video-play-icon {
+  opacity: 1;
+}
+
+.play-icon {
+  font-size: 1.5rem;
+  color: white;
+}
+
 /* 响应式设计 */
 @media (max-width: 768px) {
   .highlights-container {