feat: 完善课程学习与技能树体验

完善课程资源、学习进度与技能树解锁体验,统一视频播放与引导相关交互。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
zhongyidianandClaude Opus 4.7 committed 2026-05-24 10:30:18 +08:00
1 parent 6f289959be
commit 3e258b87d9
314 files changed
+7020 -3386

No files matched your search

+6 -37
View File
@@ -69,7 +69,8 @@
</template>
<script setup lang="ts">
import { ref, computed, useSlots, onMounted, onUnmounted, reactive } from 'vue'
import { ref, computed, useSlots, onMounted, onUnmounted } from 'vue'
import { useScale } from '@/composables/useScale'
import type { GuideStep } from './types'
import SpotlightOverlay from './SpotlightOverlay.vue'
import GuideDragon from './GuideDragon.vue'
@@ -101,6 +102,7 @@ interface Props {
interface Emits {
(e: 'complete'): void
(e: 'skip'): void
(e: 'close'): void
(e: 'step-change', step: GuideStep | null, index: number): void
}
@@ -122,25 +124,13 @@ const emit = defineEmits<Emits>()
const slots = useSlots()
const internalShow = ref(true)
const currentStepIndex = ref(0)
const { scaleStyle: alignStyle } = useScale()
// 合并外部 visible 和内部 show 状态
// 只有当外部 visible 为 true 且内部 internalShow 也为 true 时才显示
const show = computed(() => {
return props.visible && internalShow.value
})
const alignStyle = reactive<{
left: string
top: string
transform: string
width: string
height: string
}>({
left: '0px',
top: '0px',
transform: 'scale(1)',
width: '1920px',
height: '1080px',
})
const normalizeSelector = (selector?: string | null) => {
if (!selector) return null
const trimmed = selector.trim()
@@ -244,12 +234,14 @@ const handleNext = () => {
}
const handleSkip = () => {
emit('skip')
handleComplete()
}
const handleComplete = () => {
internalShow.value = false
emit('complete')
emit('close')
}
const goToStep = (index: number) => {
@@ -335,34 +327,11 @@ onMounted(() => {
if (props.autoStart && props.autoPlayInterval > 0) {
startAutoPlay()
}
// 同步到 .app-content 的缩放与偏移,使引导面板与页面对齐
const syncAlign = () => {
const el = document.querySelector('.app-content') as HTMLElement | null
if (!el) return
const style = window.getComputedStyle(el)
const transform = style.transform
let scale = 1
if (transform && transform !== 'none') {
const match = transform.match(/matrix\(([^)]+)\)/)
if (match) {
const parts = match[1].split(',').map((s) => parseFloat(s.trim()))
if (!Number.isNaN(parts[0])) scale = parts[0]
}
}
alignStyle.transform = `scale(${scale})`
alignStyle.left = style.left || '0px'
alignStyle.top = style.top || '0px'
alignStyle.width = style.width || '1920px'
alignStyle.height = style.height || '1080px'
}
syncAlign()
window.addEventListener('resize', syncAlign)
})
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown)
stopAutoPlay()
window.removeEventListener('resize', () => {})
})
defineExpose({