Files
super_video/src/views/film/practice/index.vue
T

370 lines
11 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.
<script lang="ts" setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { StarRating } from '@/components/Rating'
import { useRouter } from 'vue-router'
import BackToTopIcon from '@/assets/svg/back-to-top.svg'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter()
const TOTAL_PAGES = 3
const PAGE_HEIGHT = 1080
const SCROLL_DURATION = 1000
const currentPage = ref<number>(0)
const worksScrollIndex = ref<number>(0)
const animatingToPage = ref<number>(0)
const containerRef = ref<HTMLElement | null>(null)
const isAnimating = ref<boolean>(false)
/** 是否为最后一页 */
const isLastPage = computed<boolean>(() => currentPage.value >= TOTAL_PAGES - 1)
/** 是否正在从第二页回到第一页 */
const isScrollingBackToFirst = computed<boolean>(() => {
return isAnimating.value && currentPage.value === 1 && animatingToPage.value === 0
})
/** 第一页坐着的人物是否显示 */
const showSittingPerson = computed<boolean>(() => {
// 当前在第一页,且不是正在回滚中
return currentPage.value === 0 && !isScrollingBackToFirst.value
})
const visible = ref<boolean>(false)
//每一页的引导词
const steps = ref([
'欢迎来到实战操作台! 这里的内容很丰富,滚动鼠标就能浏览到全部内容啦~',
'点击【我的创作笔记】,编写创作思路~',
'点击【查看】按钮,欣赏他人实战作品~'
])
const contentSteps = computed(() => {
return steps.value[currentPage.value]
})
/** 他人实战作品列表 */
interface WorkItem {
id: number
image: string
title: string
stars: number
path: string
description: string
}
const worksList = ref<WorkItem[]>([
{
id: 1,
image: getStaticImage('film/practice/学生案例'),
title: '学生案例',
stars: 4,
path: '/video/practice/film_anli1.mp4',
description:
'作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介'
}
])
/** 左滑动作品列表 */
const scrollWorksLeft = (): void => {
if (worksScrollIndex.value > 0) {
worksScrollIndex.value--
}
}
/** 右滑动作品列表 */
const scrollWorksRight = (): void => {
if (worksScrollIndex.value < worksList.value.length - 3) {
worksScrollIndex.value++
}
}
const handleClose = (): void => {
router.back()
}
/** 自定义平滑滚动动画 */
const smoothScrollTo = (toPage: number): void => {
const container = containerRef.value
if (!container || isAnimating.value) return
isAnimating.value = true
animatingToPage.value = toPage
const startTop = container.scrollTop
const targetTop = toPage * PAGE_HEIGHT
const distance = targetTop - startTop
const startTime = performance.now()
/** easeInOutQuart 缓动函数 - 更平滑 */
const easeInOutQuart = (t: number): number => {
return t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2
}
const animateScroll = (currentTime: number): void => {
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / SCROLL_DURATION, 1)
const easeProgress = easeInOutQuart(progress)
container.scrollTop = startTop + distance * easeProgress
if (progress < 1) {
requestAnimationFrame(animateScroll)
} else {
isAnimating.value = false
currentPage.value = toPage
visible.value = true
}
}
requestAnimationFrame(animateScroll)
}
/** 滚动到指定页 */
const scrollToPage = (page: number): void => {
const targetPage = Math.max(0, Math.min(page, TOTAL_PAGES - 1))
if (targetPage !== currentPage.value && !isAnimating.value) {
smoothScrollTo(targetPage)
}
}
/** 滚动到下一页 */
const scrollToNextPage = (): void => {
scrollToPage(currentPage.value + 1)
}
/** 滚动到上一页 */
const scrollToPrevPage = (): void => {
scrollToPage(currentPage.value - 1)
}
/** 回到顶部 */
const scrollToTop = (): void => {
scrollToPage(0)
}
/** 处理鼠标滚轮事件 */
const handleWheel = (event: WheelEvent): void => {
event.preventDefault()
if (isAnimating.value) return
if (event.deltaY > 0) {
scrollToNextPage()
} else if (event.deltaY < 0) {
scrollToPrevPage()
}
}
onMounted(() => {
visible.value = true
containerRef.value?.addEventListener('wheel', handleWheel, { passive: false })
})
onUnmounted(() => {
containerRef.value?.removeEventListener('wheel', handleWheel)
})
</script>
<template>
<div ref="containerRef" class="w-full h-full overflow-auto">
<div class="relative min-h-full overflow-hidden">
<!-- 关闭 -->
<ImgButton
class="absolute right-0 top-0 z-50"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleClose"
/>
<!-- 实战操作台 -->
<div
class="w-full h-[1080px] bg-cover bg-no-repeat relative"
:style="{ backgroundImage: getBackgroundImage('film/practice/实战操作台首页- 电影感') }"
>
<ImgButton
class="absolute left-40 bottom-28 z-10"
image="vlog/practice/查看任务要求"
@click="scrollToNextPage"
/>
<!-- img -->
<img
class="absolute right-0"
draggable="false"
:src="getStaticImage('vlog/practice/背景1')"
alt=""
/>
</div>
<!-- 任务要求 -->
<div
class="w-full h-[1080px] bg-cover bg-no-repeat relative overflow-hidden"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/背景2') }"
>
<ImgButton
class="absolute right-20 bottom-0 z-99"
image="vlog/practice/我的创作笔记"
path="/film/practice/my-notes"
/>
<div class="task-panel w-[757px] h-full ml-10 overflow-y-auto" @wheel.stop>
<img :src="getStaticImage('film/practice/滑动任务面板-电影感')" alt="" />
</div>
<img
class="absolute right-0 bottom-0 pointer-events-none"
:src="getStaticImage('vlog/practice/摄像机')"
alt=""
/>
<!-- 站着的人物 - 第二页固定在此(不在回滚动画中) -->
<Transition name="fade">
<img
v-show="currentPage === 1 && !isScrollingBackToFirst"
class="absolute right-8 bottom-0 max-h-[90%] pointer-events-none"
draggable="false"
:src="getStaticImage('vlog/practice/摄像机-人')"
alt=""
/>
</Transition>
</div>
<!-- 他人实战作品 -->
<div
class="w-full h-[1080px] bg-cover bg-no-repeat pt-44 px-16"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/他人实战作品') }"
>
<!-- 作品列表 -->
<div class="w-full overflow-hidden">
<div
class="flex gap-6 transition-transform duration-500"
:style="{ transform: `translateX(-${worksScrollIndex * 509}px)` }"
>
<div
v-for="work in worksList"
:key="work.id"
class="w-[503px] h-[722px] flex-shrink-0 rounded-[40px] overflow-hidden bg-[#FFF8F0] py-8 px-6"
>
<!-- 作品图片 -->
<div class="w-full h-[280px] rounded-2xl overflow-hidden">
<img class="w-full h-full object-cover" :src="work.image" :alt="work.title" />
</div>
<!-- 作品信息 -->
<div class="flex flex-col items-center pt-8 space-y-4">
<h3 class="text-5xl font-bold text-gray-800 mb-2">{{ work.title }}</h3>
<!-- 星星评分 -->
<StarRating :score="work.stars" size="lg" readonly />
<!-- 简介 -->
<p class="h-[110px] text-2xl text-gray-600 mb-3 line-clamp-3 leading-relaxed">
{{ work.description }}
</p>
<!-- 查看按钮 -->
<ImgButton
image="vlog/practice/查看"
:path="`/film/practice/video/${encodeURIComponent(work.path)}`"
/>
</div>
</div>
</div>
</div>
<!-- 指示按钮 -->
<div class="absolute bottom-5 left-1/2 -translate-x-1/2 flex gap-16">
<img
class="cursor-pointer hover:scale-110 transition-transform"
:class="{ 'opacity-50 cursor-not-allowed': worksScrollIndex === 0 }"
:src="getStaticImage('vlog/practice/滑动左')"
alt="向左"
@click="scrollWorksLeft"
/>
<img
class="cursor-pointer hover:scale-110 transition-transform rotate-180"
:class="{ 'opacity-50 cursor-not-allowed': worksScrollIndex >= worksList.length - 3 }"
:src="getStaticImage('vlog/practice/滑动左')"
alt="向右"
@click="scrollWorksRight"
/>
</div>
</div>
</div>
<!-- 下滑按钮 - 最后一页隐藏 -->
<ImgButton
v-show="!isLastPage"
class="absolute bottom-0 left-1/2 -translate-x-1/2"
:image="'vlog/practice/下滑'"
@click="scrollToNextPage"
/>
<!-- 回到顶部按钮 - 最后一页显示 -->
<img
v-show="isLastPage"
class="absolute bottom-10 right-10 cursor-pointer transition-all duration-300 hover:scale-105"
:src="BackToTopIcon"
@click="scrollToTop"
/>
<!-- 跨页人物动画 -->
<div v-show="showSittingPerson || isScrollingBackToFirst" class="character-container">
<!-- 坐着的人物 - 第一页 -->
<Transition name="fade">
<img
v-show="showSittingPerson"
class="character-img right-20 bottom-10"
draggable="false"
:src="getStaticImage('vlog/practice/背景1-人')"
alt=""
/>
</Transition>
<!-- 站着的人物 - 回滚动画中 -->
<Transition name="fade">
<img
v-show="isScrollingBackToFirst"
class="character-img right-8 bottom-0"
draggable="false"
:src="getStaticImage('vlog/practice/摄像机-人')"
alt=""
/>
</Transition>
</div>
<GuideSteps v-if="visible" position-offset-y="70%" @complete="visible = false">
<GuideStepsItem>{{ contentSteps }}</GuideStepsItem>
</GuideSteps>
</div>
</template>
<style scoped>
/* 隐藏滚动条但保持可滚动 - 兼容主流浏览器 */
.task-panel {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
.task-panel::-webkit-scrollbar {
display: none; /* Chrome/Safari/Opera */
}
/* 第一页人物动画容器 */
.character-container {
position: fixed;
right: 0;
top: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 20;
}
.character-img {
position: absolute;
max-height: 90%;
}
/* 淡入淡出动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 1s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>