|
@@ -121,28 +121,57 @@
|
|
|
<div v-if="currentHighlight" class="highlight-detail">
|
|
<div v-if="currentHighlight" class="highlight-detail">
|
|
|
<!-- 大图展示 -->
|
|
<!-- 大图展示 -->
|
|
|
<div class="highlight-image">
|
|
<div class="highlight-image">
|
|
|
|
|
+ <!-- 图片展示 -->
|
|
|
<img
|
|
<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"
|
|
class="detail-image"
|
|
|
loading="lazy"
|
|
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>
|
|
|
<!-- 缩略图小图集 -->
|
|
<!-- 缩略图小图集 -->
|
|
|
<div class="thumbnail-gallery">
|
|
<div class="thumbnail-gallery">
|
|
|
<div
|
|
<div
|
|
|
- v-for="(image, index) in currentHighlight.images"
|
|
|
|
|
|
|
+ v-for="(item, index) in currentHighlight.media"
|
|
|
:key="index"
|
|
:key="index"
|
|
|
class="thumbnail-item"
|
|
class="thumbnail-item"
|
|
|
:class="{ 'active': activeImageIndex === index }"
|
|
:class="{ 'active': activeImageIndex === index }"
|
|
|
@click="handleImageClick(index)"
|
|
@click="handleImageClick(index)"
|
|
|
>
|
|
>
|
|
|
|
|
+ <!-- 图片缩略图 -->
|
|
|
<img
|
|
<img
|
|
|
- :src="image"
|
|
|
|
|
- :alt="`${currentHighlight.title} - 图${index + 1}`"
|
|
|
|
|
|
|
+ v-if="item.type === 'image'"
|
|
|
|
|
+ :src="item.src"
|
|
|
|
|
+ :alt="item.alt"
|
|
|
class="thumbnail-image"
|
|
class="thumbnail-image"
|
|
|
loading="lazy"
|
|
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>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -473,9 +502,12 @@ const viewerVideoPlaying = ref(false)
|
|
|
|
|
|
|
|
// 技术亮点选中状态
|
|
// 技术亮点选中状态
|
|
|
const activeHighlight = ref(0)
|
|
const activeHighlight = ref(0)
|
|
|
-// 当前图集的图片索引
|
|
|
|
|
|
|
+// 活动的图片索引
|
|
|
const activeImageIndex = ref(0)
|
|
const activeImageIndex = ref(0)
|
|
|
|
|
|
|
|
|
|
+// 视频封面缓存
|
|
|
|
|
+const videoPosters = ref({})
|
|
|
|
|
+
|
|
|
// 当前选中的技术亮点
|
|
// 当前选中的技术亮点
|
|
|
const currentHighlight = computed(() => {
|
|
const currentHighlight = computed(() => {
|
|
|
if (activeHighlight.value !== null) {
|
|
if (activeHighlight.value !== null) {
|
|
@@ -485,10 +517,63 @@ const currentHighlight = computed(() => {
|
|
|
return null
|
|
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) => {
|
|
const handleHighlightClick = (index) => {
|
|
|
activeHighlight.value = index
|
|
activeHighlight.value = index
|
|
|
activeImageIndex.value = 0 // 切换分类时重置图片索引
|
|
activeImageIndex.value = 0 // 切换分类时重置图片索引
|
|
|
|
|
+ // 预加载当前技术亮点的视频封面
|
|
|
|
|
+ preloadVideoPosters()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 处理图片点击
|
|
// 处理图片点击
|
|
@@ -679,12 +764,36 @@ const router = useRouter()
|
|
|
// 当前项目数据
|
|
// 当前项目数据
|
|
|
const currentProject = ref(allProjects[0])
|
|
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(() => {
|
|
onMounted(() => {
|
|
|
const projectId = route.params.projectId || 'tai-pu-river'
|
|
const projectId = route.params.projectId || 'tai-pu-river'
|
|
|
const project = getProjectById(projectId)
|
|
const project = getProjectById(projectId)
|
|
|
if (project) {
|
|
if (project) {
|
|
|
currentProject.value = project
|
|
currentProject.value = project
|
|
|
|
|
+ // 预加载所有技术亮点中的视频封面
|
|
|
|
|
+ preloadAllVideoPosters()
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -1227,6 +1336,49 @@ const goToHome = () => {
|
|
|
transform: scale(1.05);
|
|
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) {
|
|
@media (max-width: 768px) {
|
|
|
.highlights-container {
|
|
.highlights-container {
|