feat: 完善模块引导与学习进度体验

统一引导提示的持久化控制,补充学习进度入口与模块资源,避免用户返回页面时重复触发提示。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
zhongyidianandClaude Opus 4.7 committed 2026-05-08 09:44:59 +08:00
1 parent fc9633f371
commit 6f8252472f
114 files changed
+2758 -1798

No files matched your search

@@ -0,0 +1,137 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { getBackgroundImage } from '@/utils'
import { useBasicElementsStore, useFilmStore, useInterviewStore, useVlogStore } from '@/stores'
import { AnchoredProgressBar, ImgButton, type ProgressAnchor } from '@/components'
import { studyModuleAnchors, type StudyProgressModuleKey } from './studyProgressConfig'
import { getProductionModuleProgress } from './studyProgressUtils'
defineOptions({
name: 'StudyProgressPopup'
})
type StudyProgressItem = {
key: StudyProgressModuleKey
progress: number
anchors: ProgressAnchor[]
}
const visible = defineModel<boolean>({ default: false })
const basicElements = useBasicElementsStore()
const vlog = useVlogStore()
const interview = useInterviewStore()
const film = useFilmStore()
const rowPositions = {
basic: { height: 168 },
vlog: { height: 148 },
interview: { height: 146 },
film: { height: 146 }
} as const
const labelTextShadow =
'0 0 5px rgba(255,255,255,0.95), 0 0 10px rgba(54,111,255,0.75), 0 3px 0 rgba(0, 48, 151, 0.75)'
const totalTextShadow = '4px 5px 0 rgba(40, 130, 253, 0.75)'
const childTextShadow = '0 0 6px rgba(54,250,255,0.9), 0 2px 0 rgba(0, 68, 151, 0.7)'
const basicProgress = computed(() =>
basicElements.isBasicElementsUnlock ? 100 : basicElements.basicElementsUnlockProgress
)
const totalProgress = computed(() => {
const total =
basicProgress.value + vlog.progressPercent + interview.progressPercent + film.progressPercent
return Math.round(total / 4)
})
const progressItems = computed<StudyProgressItem[]>(() => [
{
key: 'basic',
progress: basicProgress.value,
anchors: studyModuleAnchors.basic
},
{
key: 'vlog',
progress: getProductionModuleProgress('vlog', vlog.viewed),
anchors: studyModuleAnchors.vlog
},
{
key: 'interview',
progress: getProductionModuleProgress('interview', interview.viewed),
anchors: studyModuleAnchors.interview
},
{
key: 'film',
progress: getProductionModuleProgress('film', film.viewed),
anchors: studyModuleAnchors.film
}
])
const handleClose = () => {
visible.value = false
}
</script>
<template>
<Transition
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="opacity-0 scale-95 blur-[8px]"
enter-to-class="opacity-100 scale-100 blur-none"
leave-active-class="transition-all duration-300 ease-in"
leave-from-class="opacity-100 scale-100 blur-none"
leave-to-class="opacity-0 scale-95 blur-[8px]"
>
<div
v-if="visible"
class="fixed inset-0 z-[100] bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage('home/stuy/学习进度底图') }"
>
<ImgButton
class="absolute right-0 top-0 z-10"
@click="handleClose"
:image="'components/close'"
/>
<div
class="absolute left-1/2 top-[14%] flex -translate-x-1/2 items-end text-white font-[1000] tracking-widest pr-10"
:style="{ textShadow: totalTextShadow }"
>
<span class="text-[clamp(36px,4.5vw,72px)] leading-none">总进度</span>
<span class="text-[clamp(36px,4.5vw,72px)] leading-none">{{ totalProgress }}%</span>
</div>
<div class="w-[1580px] mx-auto mt-[215px] flex flex-col pr-16 pl-64 gap-2">
<AnchoredProgressBar
v-for="item in progressItems"
:key="item.key"
class="relative w-full"
:style="{ height: `${rowPositions[item.key].height}px` }"
:progress="item.progress"
:anchors="item.anchors"
node-image="home/stuy/节点圆圈"
:label-class="
item.key === 'basic'
? 'text-[clamp(24px,2.2vw,42px)] text-white font-[1000] tracking-wider'
: 'text-[clamp(24px,2.1vw,40px)] text-white font-[1000] tracking-wider'
"
child-label-class="text-[clamp(20px,1.75vw,34px)] text-[#43f3f2] font-[1000] tracking-wider"
percent-class="text-[clamp(22px,2vw,38px)] text-white font-[1000] italic"
label-top="2%"
child-label-top="34%"
bar-top="58%"
bar-height="18%"
marker-top="50%"
marker-height="34%"
marker-width="1%"
child-marker-top="53%"
child-marker-height="28%"
child-marker-width="0.3%"
node-top="41%"
node-height="52%"
percent-top="78%"
:label-style="{ textShadow: labelTextShadow }"
:child-label-style="{ textShadow: childTextShadow }"
:percent-style="{ textShadow: labelTextShadow }"
/>
</div>
</div>
</Transition>
</template>
@@ -0,0 +1,31 @@
import type { ProgressAnchor } from '@/components'
export type StudyProgressModuleKey = 'basic' | 'vlog' | 'interview' | 'film'
const productionAnchors: ProgressAnchor[] = [
{ id: 'task', label: '任务取景框', value: 0 },
{
id: 'zoom',
label: '制作变焦镜',
value: 20,
children: [
{ id: 'prepare', label: '前期准备', value: 20 },
{ id: 'shooting', label: '中期拍摄', value: 42 },
{ id: 'editing', label: '后期剪辑', value: 64 }
]
},
{ id: 'practice', label: '实战操作台', value: 85 },
{ id: 'done', label: '', value: 100 }
]
export const studyModuleAnchors: Record<StudyProgressModuleKey, ProgressAnchor[]> = {
basic: [
{ id: 'three-elements', label: '三大基础要素', value: 0 },
{ id: 'shooting-skills', label: '基础拍摄技巧', value: 33.33 },
{ id: 'editing-skills', label: '短视频剪辑技巧', value: 66.66 },
{ id: 'done', label: '', value: 100 }
],
vlog: productionAnchors,
interview: productionAnchors,
film: productionAnchors
}
@@ -0,0 +1,41 @@
import { moduleStepMap } from '@/config/moduleStepMap'
type ProductionModuleKey = 'vlog' | 'interview' | 'film'
type StepKey = 1 | 2 | 3
const countViewedRoutes = (routes: readonly string[], viewed: Record<string, boolean>) => {
if (routes.length === 0) return 0
return routes.filter(routeName => viewed[routeName]).length
}
const getStepProgress = (routes: readonly string[], viewed: Record<string, boolean>) => {
if (routes.length === 0) return 0
return countViewedRoutes(routes, viewed) / routes.length
}
const interpolate = (start: number, end: number, progress: number) =>
start + (end - start) * progress
export const getProductionModuleProgress = (
moduleKey: ProductionModuleKey,
viewed: Record<string, boolean>
) => {
const stepRoutes = moduleStepMap[moduleKey]
const hasAnyViewed = Object.values(stepRoutes).some(routes =>
routes.some(routeName => viewed[routeName])
)
if (!hasAnyViewed) return 0
const stepProgress = (step: StepKey) => getStepProgress(stepRoutes[step], viewed)
const step1Progress = stepProgress(1)
const step2Progress = stepProgress(2)
const step3Progress = stepProgress(3)
if (step1Progress < 1) return Math.round(interpolate(20, 42, step1Progress))
if (step2Progress < 1) return Math.round(interpolate(42, 64, step2Progress))
if (step3Progress < 1) return Math.round(interpolate(64, 85, step3Progress))
return 100
}