forked from zhongyidian/super_video
完善新手导航和三大创作模块的视频完成缓存、未完成提示与学习进度计算,确保进度按实际学习资源完成情况更新。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
275 lines
7.8 KiB
Vue
275 lines
7.8 KiB
Vue
<template>
|
||
<Transition
|
||
name="step-navigation"
|
||
appear
|
||
>
|
||
<!-- 抽屉遮罩层 -->
|
||
<div
|
||
v-if="visible"
|
||
class="fixed inset-0 z-50 bg-black/30 backdrop-blur-md"
|
||
>
|
||
<ImgButton
|
||
class="absolute right-0 top-0 z-[10]"
|
||
image="components/close"
|
||
@click="handleClose"
|
||
/>
|
||
|
||
<!-- 抽屉内容 -->
|
||
<div
|
||
class="step-navigation-drawer relative flex h-full w-full justify-end"
|
||
@click="handleClose"
|
||
>
|
||
<!-- 右侧步骤面板 -->
|
||
<div
|
||
class="relative flex h-full w-[648px] flex-col py-[100px]"
|
||
:class="'steps-panel-bg'"
|
||
@click.stop
|
||
>
|
||
<!-- 顶部分页箭头(上一页) -->
|
||
<div
|
||
class="relative z-10 flex items-center justify-center pointer-events-none"
|
||
>
|
||
<button
|
||
class="pointer-events-auto flex h-20 w-20 items-center justify-center border-none bg-transparent transition-all duration-200 disabled:cursor-not-allowed disabled:opacity-40 hover:enabled:scale-110"
|
||
@click="goPrevPage"
|
||
:disabled="!canPrevPage"
|
||
>
|
||
<svg
|
||
width="64"
|
||
height="64"
|
||
viewBox="0 0 40 40"
|
||
fill="none"
|
||
>
|
||
<path
|
||
d="M5 30L35 30L20 6Z"
|
||
fill="white"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 步骤列表(分页,每页最多5条) -->
|
||
<div class="scrollbar-hide z-10 h-[800px] flex-1 px-6 py-4">
|
||
<div class="h-auto space-y-4">
|
||
<div
|
||
v-for="(step, index) in pagedSteps"
|
||
:key="step.id"
|
||
class="step-item-bg relative flex h-[122px] w-[831px] cursor-pointer items-center px-8 py-4 transition-all duration-300 hover:-translate-x-[23%]"
|
||
:class="{
|
||
'step-item-active-bg h-[230px] w-[1034px] pl-[180px] -translate-x-[23%]':
|
||
currentPageStartIndex + index === currentStepIndex,
|
||
}"
|
||
@click="handleStepClick(index)"
|
||
>
|
||
<!-- 步骤标签 -->
|
||
<div
|
||
class="mr-0 min-w-[5rem] rounded-full px-16 py-4 text-center text-2xl font-semibold shadow-md"
|
||
:class="getTagColor(step.label)"
|
||
>
|
||
{{ step.label }}
|
||
</div>
|
||
|
||
<!-- 步骤内容 -->
|
||
<div
|
||
class="mr-4 flex-1 text-3xl font-bold leading-[1.5] flex items-center"
|
||
>
|
||
<img
|
||
class="pointer-events-none h-12 w-12 object-contain transition-all duration-300"
|
||
:src="
|
||
completedSteps?.[currentPageStartIndex + index]
|
||
? completedIcon
|
||
: incompleteIcon
|
||
"
|
||
alt=""
|
||
/>
|
||
{{ step.content }}
|
||
</div>
|
||
|
||
<!-- 步骤数字(全局序号) -->
|
||
<div
|
||
class="absolute right-[15%] z-[1] flex items-center justify-center text-6xl font-semibold text-white/30 transition-all duration-300"
|
||
:class="{
|
||
'mb-20 text-[150px] text-white drop-shadow-[5px_10px_5px_rgba(0,0,0,0.1)]':
|
||
currentPageStartIndex + index === currentStepIndex,
|
||
}"
|
||
>
|
||
{{ currentPageStartIndex + index + 1 }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部分页箭头(下一页) -->
|
||
<div class="relative z-10 flex flex-col items-center justify-center">
|
||
<button
|
||
class="flex h-20 w-20 items-center justify-center border-none bg-transparent transition-all duration-200 disabled:cursor-not-allowed disabled:opacity-40 hover:enabled:scale-110"
|
||
@click="goNextPage"
|
||
:disabled="!canNextPage"
|
||
>
|
||
<svg
|
||
width="64"
|
||
height="64"
|
||
viewBox="0 0 40 40"
|
||
fill="none"
|
||
>
|
||
<path
|
||
d="M5 10L35 10L20 34Z"
|
||
fill="white"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ImgButton } from '@/components'
|
||
import { ref, computed, watch } from 'vue'
|
||
import { getTagColor } from '@/utils'
|
||
import completedIcon from '@/assets/images/midterm/完成.png'
|
||
import incompleteIcon from '@/assets/images/midterm/未完成.png'
|
||
|
||
export interface StepItem {
|
||
id: string
|
||
label: string
|
||
content: string
|
||
type?: 'shooting' | 'planning' | 'post' | 'other'
|
||
}
|
||
|
||
interface Props {
|
||
/** 是否显示抽屉 */
|
||
visible: boolean
|
||
/** 背景图片路径 */
|
||
backgroundImage: string
|
||
/** 步骤列表 */
|
||
steps: StepItem[]
|
||
/** 当前步骤索引 */
|
||
currentStepIndex?: number
|
||
/** 步骤完成状态 */
|
||
completedSteps?: boolean[]
|
||
/** 是否显示关闭按钮 */
|
||
showClose?: boolean
|
||
}
|
||
|
||
interface Emits {
|
||
(e: 'step-click', index: number, step: StepItem): void
|
||
(e: 'close'): void
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
currentStepIndex: 0,
|
||
showClose: true,
|
||
})
|
||
|
||
const emit = defineEmits<Emits>()
|
||
|
||
// 分页:每页最多 5 条
|
||
const pageSize = 5
|
||
const currentPage = ref(0)
|
||
|
||
const totalPages = computed(() =>
|
||
Math.max(1, Math.ceil((props.steps?.length ?? 0) / pageSize)),
|
||
)
|
||
const currentPageStartIndex = computed(() => currentPage.value * pageSize)
|
||
const pagedSteps = computed(() =>
|
||
props.steps.slice(
|
||
currentPageStartIndex.value,
|
||
currentPageStartIndex.value + pageSize,
|
||
),
|
||
)
|
||
|
||
const canPrevPage = computed(() => currentPage.value > 0)
|
||
const canNextPage = computed(() => currentPage.value < totalPages.value - 1)
|
||
|
||
const goPrevPage = () => {
|
||
if (!canPrevPage.value) return
|
||
currentPage.value -= 1
|
||
}
|
||
|
||
const goNextPage = () => {
|
||
if (!canNextPage.value) return
|
||
currentPage.value += 1
|
||
}
|
||
|
||
// 处理步骤点击
|
||
const handleStepClick = (index: number) => {
|
||
const globalIndex = currentPageStartIndex.value + index
|
||
emit('step-click', globalIndex, props.steps[globalIndex])
|
||
}
|
||
|
||
// 处理关闭
|
||
const handleClose = () => {
|
||
emit('close')
|
||
}
|
||
|
||
// 当外部当前步骤改变时,自动定位到对应页
|
||
watch(
|
||
() => props.currentStepIndex,
|
||
(newVal) => {
|
||
const idx = newVal ?? 0
|
||
const targetPage = Math.floor(idx / pageSize)
|
||
if (targetPage !== currentPage.value) {
|
||
currentPage.value = Math.min(
|
||
Math.max(targetPage, 0),
|
||
totalPages.value - 1,
|
||
)
|
||
}
|
||
},
|
||
{ immediate: true },
|
||
)
|
||
</script>
|
||
|
||
<style scoped>
|
||
.step-navigation-enter-active,
|
||
.step-navigation-leave-active {
|
||
transition: opacity 300ms ease;
|
||
}
|
||
|
||
.step-navigation-enter-from,
|
||
.step-navigation-leave-to {
|
||
opacity: 0;
|
||
}
|
||
|
||
.step-navigation-enter-active .step-navigation-drawer,
|
||
.step-navigation-leave-active .step-navigation-drawer {
|
||
transition: transform 300ms ease-out;
|
||
}
|
||
|
||
.step-navigation-enter-from .step-navigation-drawer,
|
||
.step-navigation-leave-to .step-navigation-drawer {
|
||
transform: translateX(100%);
|
||
}
|
||
|
||
.step-navigation-enter-to .step-navigation-drawer,
|
||
.step-navigation-leave-from .step-navigation-drawer {
|
||
transform: translateX(0);
|
||
}
|
||
|
||
.steps-panel-bg {
|
||
background-image: url('@/assets/images/vlog/专业剖析侧边.png');
|
||
background-repeat: no-repeat;
|
||
background-position: center center;
|
||
background-size: cover;
|
||
}
|
||
|
||
.step-item-bg {
|
||
background-image: url('@/assets/images/vlog/侧边-底板未选中.png');
|
||
}
|
||
|
||
.step-item-active-bg {
|
||
background-image: url('@/assets/images/vlog/侧边-选中底板.png');
|
||
}
|
||
|
||
.scrollbar-hide {
|
||
scrollbar-width: none;
|
||
-ms-overflow-style: none;
|
||
}
|
||
|
||
.scrollbar-hide::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
</style>
|