feat: 对接各个视频数据

This commit is contained in:
zhongyidian committed 2025-12-30 19:42:47 +08:00
1 parent 6103453e32
commit 6ab98c4764
339 files changed
+17316 -1531

No files matched your search

+349
View File
@@ -0,0 +1,349 @@
<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'
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
})
/** 他人实战作品列表 */
interface WorkItem {
id: number
image: string
title: string
stars: number
path: string
description: string
}
const worksList = ref<WorkItem[]>([
{
id: 1,
image: 'https://picsum.photos/400/300?random=1',
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
}
}
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(() => {
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>
</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>
+170
View File
@@ -0,0 +1,170 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { Overlay } from '@/components/Overlay'
import { StarRating } from '@/components/Rating'
const router = useRouter()
/** 笔记内容 */
const noteContent = ref<string>('')
/** 保存弹窗 */
const showModal = ref<boolean>(false)
/** 返回上一页 */
const handleBack = (): void => {
router.back()
}
const disabled = computed<boolean>(() => noteContent.value.trim() === '')
interface EvaluationItem {
/** 标题 */
title: string
/** 描述 */
description: string
/** 评分 */
score: number
}
/** 评估项目列表 */
const evaluationItems = ref<EvaluationItem[]>([
{
title: '画面氛围',
description: '画面具有明确情绪或空间气质,整体风格统一',
score: 4
},
{
title: '拍摄方法',
description: '拍摄方式选择合理,镜头语言服务于画面表达',
score: 2
},
{
title: '剪辑逻辑',
description: '剪辑节奏清晰,镜头衔接自然,有整体感',
score: 5
},
{
title: '创作意识',
description: '能体现主动构思,而非随拍拼接',
score: 0
},
{
title: '综合观感',
description: '作品具有“电影感”,完成度高',
score: 5
}
])
const handleSave = (): void => {
console.log('保存笔记')
handleBack()
}
</script>
<template>
<div
class="w-full h-full bg-no-repeat bg-cover px-8 pt-[400px]"
:style="{ backgroundImage: getBackgroundImage('film/practice/创作笔记底图-电影感') }"
>
<ImgButton
class="absolute top-0 right-0"
:image="IMAGE_PATHS.COMPONENTS.CLOSE"
@click="handleBack"
/>
<textarea
v-model="noteContent"
class="w-full h-[300px]"
name="myNotes"
placeholder="点击输入创作笔记"
></textarea>
<div class="absolute bottom-24 left-1/2 -translate-x-1/2 flex gap-60">
<ImgButton image="vlog/practice/保存" @click="handleSave" />
<ImgButton image="vlog/practice/自评" :disabled="disabled" @click="showModal = true" />
</div>
<!-- 保存图片弹窗 -->
<Overlay :visible="showModal" :clickable="false">
<div
class="w-[1795px] h-[987px] bg-no-repeat bg-cover p-10 pt-[100px]"
:style="{ backgroundImage: getBackgroundImage('film/practice/评分底板-电影感') }"
>
<ImgButton
class="absolute -top-8 -right-14 z-10"
:image="IMAGE_PATHS.COMPONENTS.CLOSE"
@click="showModal = false"
/>
<div class="relative h-full overflow-y-scroll px-8">
<div class="pl-10 space-y-10 mt-10">
<p class="glow-text">
评分时要关注5个维度哦~每个维度满分5分,能打1-5分,记得对照作品表现来打分呀~
</p>
<div
v-for="(item, index) in evaluationItems"
:key="index"
class="relative w-full h-[182px] bg-no-repeat bg-cover px-12 py-3 flex justify-between"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/评分条') }"
>
<div class="pr-8 space-y-10">
<h4 class="text-3xl font-bold text-white mb-3">
{{ `0${index + 1}` }} {{ item.title }}
</h4>
<p class="text-3xl text-gray-800 font-bold leading-relaxed">
{{ item.description }}
</p>
</div>
<StarRating class="absolute -bottom-5 right-10" :score="item.score" size="lg" />
</div>
<p class="glow-text">
己评价的实战作品,从封面入口就能直接看到具体评分!打了多少分一目了然,不用再点进详情页翻找啦~
</p>
<div class="w-full flex justify-center">
<ImgButton image="vlog/practice/保存" @click="showModal = false" />
</div>
</div>
</div>
</div>
</Overlay>
</div>
</template>
<style scoped>
textarea {
border: none;
outline: none;
resize: none;
font-size: 36px;
background-color: transparent;
}
textarea::placeholder {
color: #5d5b5b;
font-weight: 700;
}
.glow-text {
color: white;
font-size: 2rem;
font-weight: bold;
text-shadow:
/* 描边效果 - 多层阴影模拟描边 */
-1px -1px 0 #0066ff,
1px -1px 0 #0066ff,
-1px 1px 0 #0066ff,
1px 1px 0 #0066ff,
-2px -2px 0 #0066ff,
2px -2px 0 #0066ff,
-2px 2px 0 #0066ff,
2px 2px 0 #0066ff,
/* 发光效果 - 蓝色光晕 */ 0 0 10px #0066ff,
0 0 20px #0066ff,
0 0 30px #0066ff,
0 0 40px #0066ff,
0 0 50px #0066ff;
}
</style>
+129
View File
@@ -0,0 +1,129 @@
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { Video } from '@/components'
import { ImgButton } from '@/components/Button'
import { StarRating } from '@/components/Rating'
import { IMAGE_PATHS, getBackgroundImage } from '@/utils'
import { Overlay } from '@/components/Overlay'
import { ref } from 'vue'
const router = useRouter()
const props = defineProps({
path: {
type: String,
required: true
}
})
interface EvaluationItem {
/** 标题 */
title: string
/** 描述 */
description: string
/** 评分 */
score: number
}
/** 评估项目列表 */
const evaluationItems = ref<EvaluationItem[]>([
{
title: '主题与内容',
description: '主题明确聚焦"身边美好",内容真实、有感染力,能让观众感受到日常闪光',
score: 3
},
{
title: '拍摄技巧',
description: '运镜自然、光线运用合理、画面清晰饱和度、能辅助主题表达(至少1种运镜+1种光线)',
score: 4
},
{
title: '剪辑与包装',
description: '素材拼接流畅,调色/滤镜适配、字幕清晰、音乐不盖人声、整体风格统一',
score: 5
},
{
title: '创意与独特性',
description: '视角新颖、有个人观察,或用特殊细节/情节展现“美好”,避免平淡复刻',
score: 4
},
{
title: '综合感受',
description: '视角新颖、有个人观察,或用特殊细节/情节展现“美好”避免平淡复刻',
score: 5
}
])
/** 保存弹窗 */
const showModal = ref<boolean>(false)
/** 返回上一页 */
const handleBack = (): void => {
router.back()
}
</script>
<template>
<div class="relative h-full">
<ImgButton
class="absolute right-0 top-0 z-50"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleBack"
/>
<ImgButton
class="absolute right-0 bottom-10 z-50"
image="vlog/practice/评分"
@click="showModal = true"
/>
<Video
:src="decodeURIComponent(props.path)"
:volume="1"
:muted="false"
:controls="true"
:loop="false"
/>
<!-- 保存图片弹窗 -->
<Overlay :visible="showModal" :clickable="false">
<div
class="w-[1795px] h-[987px] bg-no-repeat bg-cover p-10 pt-[80px]"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/评分底板') }"
>
<ImgButton
class="absolute -top-8 -right-14 z-10"
:image="IMAGE_PATHS.COMPONENTS.CLOSE"
@click="showModal = false"
/>
<div class="relative h-full overflow-y-scroll px-8">
<div
class="w-full bg-no-repeat bg-cover bg-left py-44"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/滑动表内文字') }"
>
<div class="pl-10 space-y-16">
<div
v-for="(item, index) in evaluationItems"
:key="index"
class="relative w-full h-[182px] bg-no-repeat bg-cover px-12 py-3 flex justify-between"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/评分条') }"
>
<div class="pr-8 space-y-10">
<h4 class="text-3xl font-bold text-white mb-3">
{{ `0${index + 1}` }} {{ item.title }}
</h4>
<p class="text-3xl text-gray-800 font-bold leading-relaxed">
{{ item.description }}
</p>
</div>
<StarRating class="absolute -bottom-5 right-10" :score="item.score" size="lg" />
</div>
<div class="w-full flex justify-center">
<ImgButton image="vlog/practice/保存" @click="showModal = false" />
</div>
</div>
</div>
</div>
</div>
</Overlay>
</div>
</template>