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-09 09:53:06 +08:00
1 parent 6f8252472f
commit 1f8723aabd
122 files changed
+2595 -1325

No files matched your search

+140 -53
View File
@@ -1,8 +1,13 @@
<script lang="ts" setup>
defineOptions({
name: 'Action'
name: 'Action',
})
import { getBackgroundImage, getPublicPath, IMAGE_PATHS, getStaticImage } from '@/utils'
import {
getBackgroundImage,
getPublicPath,
IMAGE_PATHS,
getStaticImage,
} from '@/utils'
import { ImgButton } from '@/components/Button'
import { ModuleSwitcher } from './components/ModuleSwitcher'
import StudyProgressPopup from './components/StudyProgressPopup.vue'
@@ -13,7 +18,7 @@ import {
useFilmStore,
useVlogStore,
useInterviewStore,
useBasicElementsStore
useBasicElementsStore,
} from '@/stores'
import { GuideSteps } from '@/components/Guide'
import type { GuideStep } from '@/components/Guide/types'
@@ -46,66 +51,67 @@ const homeGuideSteps: GuideStep[] = [
content: '点击新手导航按钮,可以展开/收起新手导航面板!',
positionOffsetY: '50%',
highlightSelector: '.newUserGuide',
audioSrc: getPublicPath('audio/py_0_005.MP3')
audioSrc: getPublicPath('audio/py_0_005.MP3'),
},
{
id: 'home-guide-locked-modules',
content: '新手导航模块全部完成后才能解锁三大进阶模块哦~',
highlightSelector: '.newUserGuide',
positionOffsetY: '50%',
audioSrc: getPublicPath('audio/py_0_006.MP3')
audioSrc: getPublicPath('audio/py_0_006.MP3'),
},
{
id: 'home-guide-module-switcher',
content: '舞台划分为三大创作板块!点击 Vlog、访谈、电影的对应区域,右侧屏幕就会切换到对应板块!',
content:
'舞台划分为三大创作板块!点击 Vlog、访谈、电影的对应区域,右侧屏幕就会切换到对应板块!',
highlightSelector: '.module-switcher-guide',
positionOffsetY: '85%',
audioSrc: getPublicPath('audio/py_0_007.MP3')
audioSrc: getPublicPath('audio/py_0_007.MP3'),
},
{
id: 'home-guide-tv',
content: '再点击右侧屏幕就会开启对应的板块学习哦!',
highlightSelector: '.tvGuide',
positionOffsetY: '80%',
audioSrc: getPublicPath('audio/py_0_008.MP3')
audioSrc: getPublicPath('audio/py_0_008.MP3'),
},
{
id: 'home-guide-skill',
content: '点击技能树,可以一键回顾新手导航和三大创作板块所有的知识点哦!',
highlightSelector: '.skill-tree',
positionOffsetY: '50%',
audioSrc: getPublicPath('audio/py_0_009.MP3')
}
audioSrc: getPublicPath('audio/py_0_009.MP3'),
},
]
const unlockedHomeGuideSteps: GuideStep[] = [
{
id: 'home-guide-welcome',
content: '欢迎来到新手村!在这里,我会带着你解锁短视频创作的核心基本功!',
audioSrc: getPublicPath('audio/py_1_001.MP3')
audioSrc: getPublicPath('audio/py_1_001.MP3'),
},
{
id: 'home-guide-map',
content: '我们会读懂构图、用好光线、灵活运镜,了解基础拍摄技巧和剪映软件的基本操作。',
audioSrc: getPublicPath('audio/py_1_002.MP3')
content:
'我们会读懂构图、用好光线、灵活运镜,了解基础拍摄技巧和剪映软件的基本操作。',
audioSrc: getPublicPath('audio/py_1_002.MP3'),
},
{
id: 'home-guide-start-node',
content: '选择任意节点开始学习~',
highlightSelector: '.guide-content',
positionOffsetY: '20%',
audioSrc: getPublicPath('audio/py_1_003.MP3')
}
audioSrc: getPublicPath('audio/py_1_003.MP3'),
},
]
const elementGuideSteps: GuideStep[] = [
{
id: 'home-guide-start-element',
content: '选择任意要素开始学习~',
highlightSelector: '.btn',
positionOffsetY: '20%',
audioSrc: getPublicPath('audio/py_1_004.MP3')
}
audioSrc: getPublicPath('audio/py_1_004.MP3'),
},
]
const handleHomeGuideComplete = () => {
@@ -137,17 +143,21 @@ const isImageLoading = ref(false)
const imageMap = {
vlog: IMAGE_PATHS.ACTION.VIDEO1,
film: IMAGE_PATHS.ACTION.VIDEO2,
interview: IMAGE_PATHS.ACTION.VIDEO3
interview: IMAGE_PATHS.ACTION.VIDEO3,
}
//获取新手导航引导状态
const isGuideNewUser = computed(() => basicElements.isGuideNewUser)
const hasFinishedActionGuide = computed(() => basicElements.guideShown[actionGuideKey] === true)
const hasFinishedActionGuide = computed(
() => basicElements.guideShown[actionGuideKey] === true,
)
// 预加载所有图片
const preloadImages = () => {
const imageUrls = Object.values(imageMap).map(path => getBackgroundImage(path))
imageUrls.forEach(url => {
const imageUrls = Object.values(imageMap).map((path) =>
getBackgroundImage(path),
)
imageUrls.forEach((url) => {
const img = new Image()
img.src = url
})
@@ -156,7 +166,11 @@ const preloadImages = () => {
// 事件处理函数
const handleOpenGuide = () => {
guideShow.value = true
if (activeActionGuide.value !== 'home' && isGuideNewUser.value && !hasFinishedActionGuide.value) {
if (
activeActionGuide.value !== 'home' &&
isGuideNewUser.value &&
!hasFinishedActionGuide.value
) {
activeActionGuide.value = 'unlocked'
}
}
@@ -185,7 +199,9 @@ const handleClick = () => {
// console.log(selectedModule.value.charAt(0).toUpperCase() + selectedModule.value.slice(1))
router.push({
//首字母大写
name: selectedModule.value.charAt(0).toUpperCase() + selectedModule.value.slice(1)
name:
selectedModule.value.charAt(0).toUpperCase() +
selectedModule.value.slice(1),
})
}
@@ -204,7 +220,7 @@ const handleSkill = () => {
if (isSkillDisabled.value) return
router.push({
name: 'Skill'
name: 'Skill',
})
}
@@ -227,10 +243,25 @@ const nodes: GuideNode[] = [
y: 30,
hasChildren: true,
children: [
{ id: 1, label: '构图', icon: 'home/action/新手导航构图', routeName: 'Composition' },
{ id: 2, label: '光', icon: 'home/action/新手导航光', routeName: 'Light' },
{ id: 3, label: '运镜', icon: 'home/action/新手导航运镜', routeName: 'CameraMovement' }
]
{
id: 1,
label: '构图',
icon: 'home/action/新手导航构图',
routeName: 'Composition',
},
{
id: 2,
label: '光',
icon: 'home/action/新手导航光',
routeName: 'Light',
},
{
id: 3,
label: '运镜',
icon: 'home/action/新手导航运镜',
routeName: 'CameraMovement',
},
],
},
{
id: 2,
@@ -239,7 +270,7 @@ const nodes: GuideNode[] = [
x: 50,
y: 60,
hasChildren: false,
routeName: 'BasicShootingSkills'
routeName: 'BasicShootingSkills',
},
{
id: 3,
@@ -248,8 +279,8 @@ const nodes: GuideNode[] = [
x: 80,
y: 30,
hasChildren: false,
routeName: 'ShortVideoEditingSkills'
}
routeName: 'ShortVideoEditingSkills',
},
]
// 显示子节点的节点ID
@@ -326,7 +357,10 @@ onMounted(() => {
<!-- 学习进度 -->
<div class="absolute top-0 left-0">
<ImgButton image="home/action/学习进度入口" @click="handleOpenStudyProgress" />
<ImgButton
image="home/action/学习进度入口"
@click="handleOpenStudyProgress"
/>
</div>
<StudyProgressPopup v-model="studyProgressVisible" />
@@ -344,11 +378,18 @@ onMounted(() => {
</div>
<!-- 选择模块 -->
<div :class="['absolute left-0 top-[-140px] inset-0 z-[8] pointer-events-none']">
<div
:class="[
'absolute left-0 top-[-140px] inset-0 z-[8] pointer-events-none',
]"
>
<div
class="module-switcher-guide absolute w-[1380px] h-[600px] top-[460px] left-[calc(50%-690px)] pointer-events-none"
></div>
<ModuleSwitcher v-model="selectedModule" @change="handleModuleChange" />
<ModuleSwitcher
v-model="selectedModule"
@change="handleModuleChange"
/>
</div>
<!-- 电视机 -->
@@ -372,14 +413,16 @@ onMounted(() => {
v-if="isGuideNewUser && !hasFinishedActionGuide"
:class="[
'absolute inset-0 z-10',
activeActionGuide === 'home' ? 'bg-black/0' : 'bg-black/50'
activeActionGuide === 'home' ? 'bg-black/0' : 'bg-black/50',
]"
></div>
<!-- 新手指导 -->
<div
class="absolute bottom-0 left-0 w-full h-[497px] bg-cover bg-center bg-no-repeat z-20 pointer-events-none"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.ACTION.FOOTER_BTN) }"
:style="{
backgroundImage: getBackgroundImage(IMAGE_PATHS.ACTION.FOOTER_BTN),
}"
v-if="!guideShow"
>
<div
@@ -390,7 +433,9 @@ onMounted(() => {
<div
class="absolute bottom-0 left-0 w-full h-[911px] bg-cover bg-center bg-no-repeat z-40 flex flex-col items-center justify-center pt-40"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.ACTION.FOOTER_BG) }"
:style="{
backgroundImage: getBackgroundImage(IMAGE_PATHS.ACTION.FOOTER_BG),
}"
v-else
>
<ImgButton
@@ -401,13 +446,20 @@ onMounted(() => {
<!-- 新手导航内容 -->
<div class="relative w-[880px] h-[400px] mt-20">
<div class="w-full h-full relative guide-content" @click="handleClickOutside">
<div
class="w-full h-full relative guide-content"
@click="handleClickOutside"
>
<!-- 标题区域 -->
<div class="text-center mt-10 mb-8">
<h2 class="text-5xl font-bold mb-3 text-[#7bfffd] tracking-wider drop-shadow-lg">
<h2
class="text-5xl font-bold mb-3 text-[#7bfffd] tracking-wider drop-shadow-lg"
>
新手导航
</h2>
<p class="text-4xl text-white font-bold drop-shadow-md">点击节点即可开始学习</p>
<p class="text-4xl text-white font-bold drop-shadow-md">
点击节点即可开始学习
</p>
</div>
<!-- 节点图表 -->
@@ -492,24 +544,37 @@ onMounted(() => {
</div>
<!-- 子节点图标(当点击三大基础要素时显示) -->
<div
v-if="node.hasChildren && showChildrenNodeId === node.id"
class="absolute -top-40 left-1/2 transform -translate-x-1/2 flex z-20 btn"
>
<Transition name="child-node-pop">
<div
v-for="childNode in node.children"
:key="childNode.id"
class="flex flex-col items-center cursor-pointer"
@click.stop="handleChildNodeClick(childNode, $event)"
v-if="node.hasChildren && showChildrenNodeId === node.id"
class="child-node-buttons btn absolute -top-48 left-1/2 z-20 flex -translate-x-1/2 flex-col items-center"
>
<!-- 图标 -->
<div
class="w-36 flex items-center justify-center transition-all duration-300 hover:scale-110"
class="relative flex rounded-[30px] bg-[rgba(8,40,58,0.82)] px-3 py-2 shadow-[0_0_0_1px_rgba(123,255,253,0.28),0_0_20px_rgba(123,255,253,0.28)] after:absolute after:left-1/2 after:top-[calc(100%-1px)] after:h-0 after:w-0 after:-translate-x-1/2 after:border-x-[12px] after:border-t-[14px] after:border-x-transparent after:border-t-[rgba(8,40,58,0.82)]"
>
<img :src="getStaticImage(childNode.icon)" :alt="childNode.label" />
<div
v-for="childNode in node.children"
:key="childNode.id"
class="group/child relative z-10 flex flex-col items-center cursor-pointer"
@click.stop="handleChildNodeClick(childNode, $event)"
>
<!-- 图标 -->
<div
class="relative w-36 flex items-center justify-center transition-all duration-300 hover:scale-110"
>
<div
class="absolute inset-x-5 inset-y-4 rounded-full bg-[#7bfffd]/12 blur-xl opacity-60 transition-opacity duration-300 group-hover/child:opacity-100"
></div>
<img
class="relative z-10 drop-shadow-[0_0_10px_rgba(123,255,253,0.45)] transition-all duration-300 group-hover/child:brightness-125 group-hover/child:drop-shadow-[0_0_18px_rgba(123,255,253,0.8)]"
:src="getStaticImage(childNode.icon)"
:alt="childNode.label"
/>
</div>
</div>
</div>
</div>
</div>
</Transition>
</div>
</div>
</div>
@@ -517,3 +582,25 @@ onMounted(() => {
</div>
</div>
</template>
<style scoped>
.child-node-pop-enter-active,
.child-node-pop-leave-active {
transition:
opacity 0.22s ease,
transform 0.22s cubic-bezier(0.2, 0.9, 0.2, 1.1);
transform-origin: bottom center;
}
.child-node-pop-enter-from,
.child-node-pop-leave-to {
opacity: 0;
transform: translateX(-50%) translateY(10px) scale(0.86);
}
.child-node-pop-enter-to,
.child-node-pop-leave-from {
opacity: 1;
transform: translateX(-50%) translateY(0) scale(1);
}
</style>
+82 -20
View File
@@ -1,30 +1,47 @@
import { defineComponent, ref } from 'vue'
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import { computed, defineComponent, ref } from 'vue'
import {
getBackgroundImage,
getPublicPath,
getStaticImage,
IMAGE_PATHS,
} from '@/utils'
import { ImgButton } from '@/components/Button'
import { GuideSteps, ImagePreview } from '@/components'
import { GuideSteps, ImagePreview, Video } from '@/components'
import { useScale } from '@/composables/useScale'
import { useBasicElementsStore } from '@/stores/modules/basic-elements'
const CourseFrameGuide = () => (
<div class="flex items-center justify-start">
点击
<img class="w-20" src={getStaticImage(IMAGE_PATHS.BUTTONS.COURSE_FRAME)} alt="" />
<img
class="w-40"
src={getStaticImage(IMAGE_PATHS.BUTTONS.COURSE_FRAME)}
alt=""
/>
查看课程结构,快速了解每个模块的学习内容。
</div>
)
const OperateGuide = () => (
<div class="flex items-center justify-start">
<img class="w-20 inline" src={getStaticImage(IMAGE_PATHS.BUTTONS.OPERATE_GUIDE)} alt="" />
包含课程介绍和操作指南,观看指引视频,轻松上手课程。(资源建设中...)
<img
class="w-40 inline"
src={getStaticImage(IMAGE_PATHS.BUTTONS.OPERATE_GUIDE)}
alt=""
/>
包含课程介绍和操作指南,点击即可查看!
</div>
)
const StartFilmingGuide = () => (
<div class="flex items-center justify-start">
点击
<img class="w-20" src={getStaticImage(IMAGE_PATHS.BUTTONS.START_FILMING)} alt="" />
正式进入课程,开启你的短视频拍摄之旅。
<img
class="w-40"
src={getStaticImage(IMAGE_PATHS.BUTTONS.START_FILMING)}
alt=""
/>
,一起我们的短视频学习之旅吧!
</div>
)
@@ -38,7 +55,7 @@ export default defineComponent({
content: '欢迎来到《短视频达人养成记》网络课程的学习!',
highlightSelector: null,
audioSrc: getPublicPath('audio/py_0_001.MP3'),
positionOffsetY: '80%'
positionOffsetY: '80%',
},
{
id: 'step-2',
@@ -46,7 +63,7 @@ export default defineComponent({
content: CourseFrameGuide,
highlightSelector: '.kecheng-wrapper',
audioSrc: getPublicPath('audio/py_0_002.MP3'),
positionOffsetY: '40%'
positionOffsetY: '40%',
},
{
id: 'step-3',
@@ -54,7 +71,7 @@ export default defineComponent({
content: OperateGuide,
highlightSelector: '.zhiying-wrapper',
audioSrc: getPublicPath('audio/py_0_003.MP3'),
positionOffsetY: '40%'
positionOffsetY: '40%',
},
{
id: 'step-4',
@@ -62,15 +79,27 @@ export default defineComponent({
content: StartFilmingGuide,
highlightSelector: '.start-filming-wrapper',
audioSrc: getPublicPath('audio/py_0_004.MP3'),
positionOffsetY: '40%'
}
positionOffsetY: '40%',
},
])
const basicElementsStore = useBasicElementsStore()
const guideKey = 'home-intro'
const showGuide = ref(!basicElementsStore.guideShown[guideKey])
const isFirstVisit = !basicElementsStore.guideShown[guideKey]
const showGuide = ref(isFirstVisit)
const { scale, offsetX, offsetY } = useScale()
if (showGuide.value) {
// 首次进入的视频播放序列
const introVideos = computed(() => [
getPublicPath('video/campus life.mp4'),
getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
])
const introVideoIndex = ref(0)
const showIntroVideo = ref(isFirstVisit)
const currentIntroSrc = computed(
() => introVideos.value[introVideoIndex.value] ?? '',
)
if (isFirstVisit) {
basicElementsStore.markGuideShown(guideKey)
}
@@ -78,11 +107,24 @@ export default defineComponent({
showGuide.value = false
}
const handleIntroVideoEnded = () => {
if (introVideoIndex.value < introVideos.value.length - 1) {
introVideoIndex.value++
} else {
showIntroVideo.value = false
// 引导步骤在视频播放结束后出现
}
}
const openMindMap = () => {
ImagePreview({
images: [getStaticImage('maps/dt')],
maskClosable: true,
getScaleMetrics: () => ({ scale: scale.value, offsetX: offsetX.value, offsetY: offsetY.value })
getScaleMetrics: () => ({
scale: scale.value,
offsetX: offsetX.value,
offsetY: offsetY.value,
}),
})
}
@@ -94,19 +136,39 @@ export default defineComponent({
/>
<div class="absolute right-[420px] bottom-[200px] start-filming-wrapper">
<ImgButton image="home/action-btn" path={{ name: 'Action' }} />
<ImgButton
image="home/action-btn"
path={{ name: 'Action' }}
/>
</div>
<div class="absolute right-20 bottom-36 kecheng-wrapper">
<ImgButton image={IMAGE_PATHS.BUTTONS.COURSE_FRAME} onClick={openMindMap} />
<ImgButton
image={IMAGE_PATHS.BUTTONS.COURSE_FRAME}
onClick={openMindMap}
/>
</div>
<div class="absolute right-20 bottom-10 zhiying-wrapper">
<ImgButton image={IMAGE_PATHS.BUTTONS.OPERATE_GUIDE} />
</div>
{showIntroVideo.value && (
<div class="absolute inset-0 z-[10000] bg-black flex items-center justify-center">
<Video
src={currentIntroSrc.value}
autoplay={true}
muted={false}
controls={true}
volume={1}
fit="contain"
onEnded={handleIntroVideoEnded}
/>
</div>
)}
<GuideSteps
visible={showGuide.value}
visible={showGuide.value && !showIntroVideo.value}
steps={guideSteps.value}
highlightPadding={12}
highlightRadius={16}
@@ -116,5 +178,5 @@ export default defineComponent({
/>
</div>
)
}
},
})