-
+
-
![导学小龙]()
+
@@ -74,8 +80,6 @@ import { getStaticImage, getBackgroundImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
interface Props {
- /** 是否显示跳过按钮 */
- showSkip?: boolean
/** 当前步骤 */
currentStep?: number
/** 总步骤数 */
@@ -86,28 +90,16 @@ interface Props {
interface Emits {
(e: 'next'): void
- (e: 'skip'): void
- (e: 'complete'): void
}
const props = withDefaults(defineProps
(), {
- showSkip: true,
currentStep: 1,
totalSteps: 1,
- positionOffsetY: '50%'
+ positionOffsetY: '50%',
})
const emit = defineEmits()
-// 处理跳过
-const handleSkip = () => {
- try {
- emit('skip')
- } catch (error) {
- console.error('GuideDragon handleSkip error:', error)
- }
-}
-
//继续
const handleContinue = () => {
emit('next')
@@ -122,7 +114,10 @@ const normalizeOffset = (value: number | string) => {
const positionStyle = computed(() => {
const y = normalizeOffset(props.positionOffsetY)
// 百分比:按中心定位;px:按像素直接定位
- if (typeof props.positionOffsetY === 'string' && props.positionOffsetY.includes('%')) {
+ if (
+ typeof props.positionOffsetY === 'string' &&
+ props.positionOffsetY.includes('%')
+ ) {
return { top: y, transform: 'translateY(-50%)' }
}
return { top: y, transform: 'translateY(0)' }
@@ -169,13 +164,6 @@ const positionStyle = computed(() => {
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
-.skip-button {
- @apply text-white text-sm underline hover:no-underline;
- position: absolute;
- right: 30px;
- top: 35px;
-}
-
.guide-content {
@apply flex items-start pl-6 pt-9 pb-0 h-full;
}
@@ -235,7 +223,11 @@ const positionStyle = computed(() => {
.dragon-glow {
@apply absolute inset-0 rounded-full;
- background: radial-gradient(circle, rgba(255, 255, 0, 0.2) 0%, transparent 70%);
+ background: radial-gradient(
+ circle,
+ rgba(255, 255, 0, 0.2) 0%,
+ transparent 70%
+ );
animation: glow 2s ease-in-out infinite alternate;
}
diff --git a/src/components/Guide/GuideSteps.vue b/src/components/Guide/GuideSteps.vue
index be36d6c..cf6ebcc 100644
--- a/src/components/Guide/GuideSteps.vue
+++ b/src/components/Guide/GuideSteps.vue
@@ -16,13 +16,10 @@
:style="alignStyle"
>
@@ -101,7 +98,6 @@ interface Props {
interface Emits {
(e: 'complete'): void
- (e: 'skip'): void
(e: 'close'): void
(e: 'step-change', step: GuideStep | null, index: number): void
}
@@ -233,11 +229,6 @@ const handleNext = () => {
}
}
-const handleSkip = () => {
- emit('skip')
- handleComplete()
-}
-
const handleComplete = () => {
internalShow.value = false
emit('complete')
@@ -288,10 +279,6 @@ const handleKeydown = (event: KeyboardEvent) => {
emit('step-change', currentStepData.value, currentStepIndex.value)
}
break
- case 'Escape':
- event.preventDefault()
- handleSkip()
- break
}
}
diff --git a/src/components/Guide/README.md b/src/components/Guide/README.md
index fa68c9f..80c488c 100644
--- a/src/components/Guide/README.md
+++ b/src/components/Guide/README.md
@@ -80,11 +80,8 @@ interface GuideStep {
```vue
欢迎使用短视频达人!
让我来为你介绍如何使用这个应用吧~
@@ -98,10 +95,6 @@ const handleNext = () => {
console.log('下一步')
}
-const handleSkip = () => {
- console.log('跳过')
-}
-
const handleComplete = () => {
console.log('完成')
}
@@ -112,7 +105,10 @@ const handleComplete = () => {
```vue
-
+
diff --git a/src/views/film/practice/video/index.vue b/src/views/film/practice/video/index.vue
index 115d610..6d01792 100644
--- a/src/views/film/practice/video/index.vue
+++ b/src/views/film/practice/video/index.vue
@@ -83,10 +83,18 @@ const handleSaveEvaluation = (items: PracticeRatingItem[]): void => {
store.setPracticeWorkRating(workKey.value, toScores(items))
}
+const watchKey = computed(() => {
+ const val = route.query.watchKey
+ return typeof val === 'string' ? val : undefined
+})
+
+const isWatched = computed(() =>
+ watchKey.value ? store.practiceVideosWatched[watchKey.value] === true : true,
+)
+
const handleVideoEnded = (): void => {
- const watchKey = route.query.watchKey
- if (typeof watchKey === 'string' && watchKey) {
- filmStore.markPracticeVideoWatched(watchKey)
+ if (watchKey.value) {
+ filmStore.markPracticeVideoWatched(watchKey.value)
}
}
@@ -118,6 +126,7 @@ const handleBack = (): void => {
:muted="false"
:controls="true"
:loop="false"
+ :seekable="isWatched"
@ended="handleVideoEnded"
/>
diff --git a/src/views/film/video/index.vue b/src/views/film/video/index.vue
index 3321f89..73f90a0 100644
--- a/src/views/film/video/index.vue
+++ b/src/views/film/video/index.vue
@@ -116,6 +116,13 @@ const hasUnfinishedLearningVideos = computed(
completedNavigationSteps.value.some((completed) => !completed),
)
+const currentVideoWatched = computed(
+ () =>
+ film.professionVideosWatched[
+ professionVideoKeys[currentVideoIndex.value]
+ ] === true,
+)
+
const markCurrentProfessionVideoWatched = () => {
const videoKey = professionVideoKeys[currentVideoIndex.value]
if (!videoKey) return
@@ -379,6 +386,7 @@ onMounted(() => {
:volume="1"
:controls="true"
:hideNativeControls="true"
+ :seekable="key === 'profession' ? currentVideoWatched : true"
@play="handlePlay"
@ended="onEnded"
>
@@ -439,7 +447,6 @@ onMounted(() => {
position-offset-y="85%"
@step-change="(_: any, index: number) => (guideStepIndex = index)"
@complete="handleProfessionGuideComplete"
- @skip="handleProfessionGuideComplete"
>
从刚才的案例可以看出,电影感画面自带一种独特的艺术魅力——或大气恢弘,扑朔迷离或充满故事感。
diff --git a/src/views/film/work/index.vue b/src/views/film/work/index.vue
index 2888635..9151b54 100644
--- a/src/views/film/work/index.vue
+++ b/src/views/film/work/index.vue
@@ -254,7 +254,6 @@ function openImages(urls: string[], startIndex = 0) {
:highlight-radius="20"
@step-change="handleStepChange"
@complete="handleGuideComplete"
- @skip="handleGuideComplete"
position-offset-y="85%"
>
@@ -274,7 +273,6 @@ function openImages(urls: string[], startIndex = 0) {
:highlight-padding="12"
:highlight-radius="20"
@complete="handleReturnGuideComplete"
- @skip="handleReturnGuideComplete"
position-offset-y="85%"
/>
diff --git a/src/views/home/components/StudyProgressPopup/CertificateDetailPopup.vue b/src/views/home/components/StudyProgressPopup/CertificateDetailPopup.vue
index f3249b4..b1206ba 100644
--- a/src/views/home/components/StudyProgressPopup/CertificateDetailPopup.vue
+++ b/src/views/home/components/StudyProgressPopup/CertificateDetailPopup.vue
@@ -1,4 +1,5 @@
-
+
{
@@ -58,22 +59,57 @@ const handleClose = () => {
{{ date }}
-
+
diff --git a/src/views/home/components/StudyProgressPopup/StudyProgressPopup.vue b/src/views/home/components/StudyProgressPopup/StudyProgressPopup.vue
index 2d17eea..9903f95 100644
--- a/src/views/home/components/StudyProgressPopup/StudyProgressPopup.vue
+++ b/src/views/home/components/StudyProgressPopup/StudyProgressPopup.vue
@@ -125,6 +125,22 @@ const totalProgress = computed(() =>
filmProgress.value,
]),
)
+const isRefreshingProgress = ref(false)
+
+const refreshStudyProgress = async () => {
+ if (isRefreshingProgress.value) return
+ isRefreshingProgress.value = true
+ try {
+ await Promise.allSettled([
+ basicElements.loadBasicElementsStore(),
+ vlog.loadVlogStore(),
+ interview.loadInterviewStore(),
+ film.loadFilmStore(),
+ ])
+ } finally {
+ isRefreshingProgress.value = false
+ }
+}
const CERTIFICATE_SHOWN_KEY = 'study-progress-certificate-shown'
const certificateVisible = ref(false)
@@ -140,10 +156,8 @@ const hasAllBadges = computed(() =>
const hasCertificate = computed(
() => basicElements.guideShown[CERTIFICATE_SHOWN_KEY] === true,
)
-const certificateSrc = computed(() =>
- getStaticImage(
- `home/stuy/通关证书-${hasCertificate.value ? '已获取' : '暂未获取'}`,
- ),
+const certificateSrc = computed(
+ () => `home/stuy/通关证书-${hasCertificate.value ? '已获取' : '暂未获取'}`,
)
const buildItem = (
@@ -222,6 +236,12 @@ const closeCertificate = () => {
}
}
+const openCertificate = () => {
+ if (!hasCertificate.value) return
+ basicElements.ensureCertificate()
+ certificateVisible.value = true
+}
+
watch([visible, hasAllBadges, hasCertificate], scheduleCertificatePopup)
onBeforeUnmount(clearCertificateTimer)
@@ -267,20 +287,39 @@ const certificateInfo = computed(() => basicElements.certificate)
@click="handleClose"
/>
-
-
-
总进度
-
{{ totalProgress }}%
+
+
+ 总进度
+ {{ totalProgress }}%
+
+
diff --git a/src/views/home/index.tsx b/src/views/home/index.tsx
index 402d923..7cb069d 100644
--- a/src/views/home/index.tsx
+++ b/src/views/home/index.tsx
@@ -127,10 +127,6 @@ export default defineComponent({
}
}
- const handleSkipIntro = () => {
- showIntroVideo.value = false
- }
-
// Loading 加载状态
const showLoading = ref(isFirstVisit)
const loadingProgress = ref(0)
@@ -251,15 +247,6 @@ export default defineComponent({
visible={showLoading.value}
progress={loadingProgress.value}
/>
- {introVideoIndex.value > 0 && !showLoading.value && (
-
})
- )}
)}
diff --git a/src/views/interview/after-editing/editing.vue b/src/views/interview/after-editing/editing.vue
index 6dc235b..4dce669 100644
--- a/src/views/interview/after-editing/editing.vue
+++ b/src/views/interview/after-editing/editing.vue
@@ -84,6 +84,13 @@ const hasUnfinishedLearningVideos = computed(() =>
completedNavigationSteps.value.some((completed) => !completed),
)
+const currentVideoWatched = computed(
+ () =>
+ interview.professionVideosWatched[
+ navigationSteps[currentVideoIndex.value]?.id
+ ] === true,
+)
+
const markCurrentVideoWatched = () => {
const videoKey = navigationSteps[currentVideoIndex.value]?.id
if (videoKey) interview.markEditingVideoWatched(videoKey)
@@ -162,6 +169,7 @@ onBeforeRouteLeave((to) => {
:volume="1"
:controls="true"
:hideNativeControls="true"
+ :seekable="currentVideoWatched"
@ended="playNextVideo"
>
diff --git a/src/views/interview/after-editing/export-publish/index.vue b/src/views/interview/after-editing/export-publish/index.vue
index 8e54dc2..9b06a90 100644
--- a/src/views/interview/after-editing/export-publish/index.vue
+++ b/src/views/interview/after-editing/export-publish/index.vue
@@ -1,5 +1,5 @@
@@ -47,6 +51,7 @@ const handleVideoEnded = () => {
:volume="1"
:controls="true"
:hideNativeControls="true"
+ :seekable="currentVideoWatched"
@ended="handleVideoEnded"
/>
{
v-if="showGuide"
position-offset-y="85%"
@complete="handleComplete"
- @skip="handleComplete"
>
现在一起来看看导出后的影片效果吧!
diff --git a/src/views/interview/after-editing/sound-effect/index.vue b/src/views/interview/after-editing/sound-effect/index.vue
index a7eda03..f8778ba 100644
--- a/src/views/interview/after-editing/sound-effect/index.vue
+++ b/src/views/interview/after-editing/sound-effect/index.vue
@@ -1,5 +1,5 @@
@@ -34,6 +38,7 @@ const handleEnded = () => {
:volume="1"
:controls="true"
:hideNativeControls="true"
+ :seekable="currentVideoWatched"
@ended="handleEnded"
/>
{
:steps="returnSteps"
position-offset-y="85%"
@complete="handleReturnGuideComplete"
- @skip="handleReturnGuideComplete"
/>
@@ -323,7 +322,6 @@ onMounted(() => {
:steps="stageGuideSteps"
position-offset-y="85%"
@complete="handleStageGuideComplete"
- @skip="handleStageGuideComplete"
/>
completedNavigationSteps.value.some((completed) => !completed),
)
+const currentVideoWatched = computed(
+ () =>
+ interview.professionVideosWatched[
+ onsiteVideoKeys[currentVideoIndex.value]
+ ] === true,
+)
+
const markCurrentOnsiteVideoWatched = () => {
const videoKey = onsiteVideoKeys[currentVideoIndex.value]
if (videoKey) interview.markOnsiteVideoWatched(videoKey)
@@ -198,6 +205,7 @@ onBeforeRouteLeave((to) => {
:volume="1"
:controls="true"
:hideNativeControls="true"
+ :seekable="currentVideoWatched"
@ended="handleEnded"
>
diff --git a/src/views/interview/practice/index.vue b/src/views/interview/practice/index.vue
index def35c7..ae2c54b 100644
--- a/src/views/interview/practice/index.vue
+++ b/src/views/interview/practice/index.vue
@@ -240,6 +240,41 @@ const handleWheel = (event: WheelEvent): void => {
}
}
+/** 触摸起点(移动端滑动翻页) */
+let touchStartY = 0
+let touchStartX = 0
+
+/** 触摸点是否落在可独立滚动的内部面板(如任务要求)中 */
+const isInPanelScroll = (target: EventTarget | null): boolean => {
+ const panel = (target as HTMLElement | null)?.closest('.task-panel')
+ return !!panel && panel.scrollHeight > panel.clientHeight
+}
+
+/** 记录触摸起点 */
+const handleTouchStart = (event: TouchEvent): void => {
+ touchStartY = event.touches[0].clientY
+ touchStartX = event.touches[0].clientX
+}
+
+/** 阻止容器原生滚动,改由整页翻页控制(内部面板除外) */
+const handleTouchMove = (event: TouchEvent): void => {
+ if (!isInPanelScroll(event.target)) event.preventDefault()
+}
+
+/** 根据滑动方向翻页 */
+const handleTouchEnd = (event: TouchEvent): void => {
+ if (isAnimating.value || isInPanelScroll(event.target)) return
+ const deltaY = touchStartY - event.changedTouches[0].clientY
+ const deltaX = touchStartX - event.changedTouches[0].clientX
+ // 仅响应以纵向为主、且超过阈值的滑动
+ if (Math.abs(deltaY) < 40 || Math.abs(deltaX) > Math.abs(deltaY)) return
+ if (deltaY > 0) {
+ scrollToNextPage()
+ } else {
+ scrollToPrevPage()
+ }
+}
+
const restoreScrollPage = (): void => {
const savedPage = interviewStore.practiceScrollPage
const targetPage = Math.max(0, Math.min(savedPage, TOTAL_PAGES - 1))
@@ -264,12 +299,20 @@ const saveScrollPage = (): void => {
onMounted(() => {
restoreScrollPage()
showPageGuide()
- containerRef.value?.addEventListener('wheel', handleWheel, { passive: false })
+ const container = containerRef.value
+ container?.addEventListener('wheel', handleWheel, { passive: false })
+ container?.addEventListener('touchstart', handleTouchStart, { passive: true })
+ container?.addEventListener('touchmove', handleTouchMove, { passive: false })
+ container?.addEventListener('touchend', handleTouchEnd, { passive: true })
})
onUnmounted(() => {
saveScrollPage()
- containerRef.value?.removeEventListener('wheel', handleWheel)
+ const container = containerRef.value
+ container?.removeEventListener('wheel', handleWheel)
+ container?.removeEventListener('touchstart', handleTouchStart)
+ container?.removeEventListener('touchmove', handleTouchMove)
+ container?.removeEventListener('touchend', handleTouchEnd)
})
diff --git a/src/views/interview/practice/video/index.vue b/src/views/interview/practice/video/index.vue
index cc445ef..f0c8e90 100644
--- a/src/views/interview/practice/video/index.vue
+++ b/src/views/interview/practice/video/index.vue
@@ -32,6 +32,11 @@ type PracticeRatingScores = Record
const workKey = computed(() => decodeURIComponent(props.path))
+const watchKey = computed(() => {
+ const key = route.query.watchKey
+ return typeof key === 'string' && key ? key : undefined
+})
+
const applyScores = (
items: PracticeRatingItem[],
scores: PracticeRatingScores,
@@ -124,6 +129,9 @@ const handleBack = (): void => {
:muted="false"
:controls="true"
:loop="false"
+ :seekable="
+ watchKey ? store.practiceVideosWatched[watchKey] === true : true
+ "
@ended="handleVideoEnded"
/>
diff --git a/src/views/interview/video/index.vue b/src/views/interview/video/index.vue
index 6e5cc63..770b0f0 100644
--- a/src/views/interview/video/index.vue
+++ b/src/views/interview/video/index.vue
@@ -255,6 +255,13 @@ const hasUnfinishedLearningVideos = computed(
completedNavigationSteps.value.some((completed) => !completed),
)
+const currentVideoWatched = computed(
+ () =>
+ interview.professionVideosWatched[
+ professionVideoKeys[currentVideoIndex.value]
+ ] === true,
+)
+
const markCurrentProfessionVideoWatched = () => {
const videoKey = professionVideoKeys[currentVideoIndex.value]
if (!videoKey) return
@@ -461,6 +468,7 @@ onBeforeRouteLeave((to) => {
:volume="1"
:controls="true"
:hideNativeControls="true"
+ :seekable="key === 'profession' ? currentVideoWatched : true"
@play="handlePlay"
@ended="onEnded"
>
@@ -501,7 +509,6 @@ onBeforeRouteLeave((to) => {
:steps="introGuideSteps"
position-offset-y="85%"
@complete="handleIntroGuideDone"
- @skip="handleIntroGuideDone"
/>
diff --git a/src/views/interview/work/index.vue b/src/views/interview/work/index.vue
index 02f3b0f..8ddc074 100644
--- a/src/views/interview/work/index.vue
+++ b/src/views/interview/work/index.vue
@@ -245,7 +245,6 @@ function openImages(urls: string[], startIndex = 0) {
:highlight-padding="12"
:highlight-radius="20"
@complete="handleGuideComplete"
- @skip="handleGuideComplete"
position-offset-y="85%"
/>
@@ -255,7 +254,6 @@ function openImages(urls: string[], startIndex = 0) {
:highlight-padding="12"
:highlight-radius="20"
@complete="handleReturnGuideComplete"
- @skip="handleReturnGuideComplete"
position-offset-y="85%"
/>
diff --git a/src/views/skill/index.vue b/src/views/skill/index.vue
index 7acea0b..091c302 100644
--- a/src/views/skill/index.vue
+++ b/src/views/skill/index.vue
@@ -704,7 +704,6 @@ const handleGuideSkip = () => {
:highlight-radius="16"
position-offset-y="85%"
@complete="handleGuideComplete"
- @skip="handleGuideSkip"
/>
diff --git a/src/views/vlog/after-editing/color.vue b/src/views/vlog/after-editing/color.vue
index 20248c2..c5de648 100644
--- a/src/views/vlog/after-editing/color.vue
+++ b/src/views/vlog/after-editing/color.vue
@@ -63,6 +63,10 @@ const handlePracticeGuideComplete = () => {
showPracticeGuide.value = false
}
+const currentVideoWatched = computed(
+ () => vlogStore.professionVideosWatched[COLOR_VIDEO_KEY] === true,
+)
+
const handleBack = () => {
router.back()
}
@@ -90,6 +94,7 @@ const handleBack = () => {
:volume="1"
:controls="true"
:hideNativeControls="true"
+ :seekable="currentVideoWatched"
@ended="handleEnded"
>
@@ -129,7 +134,6 @@ const handleBack = () => {
highlight-selector=".practice-button"
position-offset-y="80%"
@complete="handlePracticeGuideComplete"
- @skip="handlePracticeGuideComplete"
>
点击【调色实践】,一起给案例中的图片调个色吧!
diff --git a/src/views/vlog/after-editing/editing.vue b/src/views/vlog/after-editing/editing.vue
index 7aa4231..c60ae39 100644
--- a/src/views/vlog/after-editing/editing.vue
+++ b/src/views/vlog/after-editing/editing.vue
@@ -96,6 +96,13 @@ const hasUnfinishedLearningVideos = computed(() =>
completedNavigationSteps.value.some((completed) => !completed),
)
+const currentVideoWatched = computed(
+ () =>
+ vlogStore.professionVideosWatched[
+ navigationSteps.value[currentVideoIndex.value]?.id
+ ] === true,
+)
+
const markCurrentVideoWatched = () => {
const videoKey = navigationSteps.value[currentVideoIndex.value]?.id
if (videoKey) vlogStore.markEditingVideoWatched(videoKey)
@@ -180,6 +187,7 @@ onBeforeRouteLeave((to) => {
:volume="1"
:controls="true"
:hideNativeControls="true"
+ :seekable="currentVideoWatched"
@ended="handleEnded"
>
@@ -244,7 +252,6 @@ onBeforeRouteLeave((to) => {
v-if="showEndingGuide"
position-offset-y="85%"
@complete="handleEndingGuideComplete"
- @skip="handleEndingGuideComplete"
>
当内容结构完成后,我们还可以通过调色与包装,让画面更加统一、更具观感。
diff --git a/src/views/vlog/after-editing/export-publish/index.vue b/src/views/vlog/after-editing/export-publish/index.vue
index aa14c5e..9e4c421 100644
--- a/src/views/vlog/after-editing/export-publish/index.vue
+++ b/src/views/vlog/after-editing/export-publish/index.vue
@@ -1,5 +1,5 @@
@@ -50,6 +54,7 @@ const handleVideoEnded = () => {
:volume="1"
:controls="true"
:hideNativeControls="true"
+ :seekable="currentVideoWatched"
@ended="handleVideoEnded"
/>
diff --git a/src/views/vlog/index.vue b/src/views/vlog/index.vue
index c7313cf..d140651 100644
--- a/src/views/vlog/index.vue
+++ b/src/views/vlog/index.vue
@@ -348,7 +348,6 @@ onMounted(() => {
:steps="returnSteps"
position-offset-y="85%"
@complete="handleReturnGuideComplete"
- @skip="handleReturnGuideComplete"
/>
@@ -357,7 +356,6 @@ onMounted(() => {
:steps="stageGuideSteps"
position-offset-y="85%"
@complete="handleStageGuideComplete"
- @skip="handleStageGuideComplete"
/>
completedNavigationSteps.value.some((completed) => !completed),
)
+const currentVideoWatched = computed(
+ () =>
+ vlogStore.professionVideosWatched[
+ navigationSteps.value[currentVideoIndex.value]?.id
+ ] === true,
+)
+
const markCurrentVideoWatched = () => {
const videoKey = navigationSteps.value[currentVideoIndex.value]?.id
if (videoKey) vlogStore.markOnsiteVideoWatched(videoKey)
@@ -211,6 +218,7 @@ onBeforeRouteLeave((to) => {
:volume="1"
:controls="true"
:hideNativeControls="true"
+ :seekable="currentVideoWatched"
:anchors="[]"
@ended="handleEnded"
>
diff --git a/src/views/vlog/practice/index.vue b/src/views/vlog/practice/index.vue
index bf8fc65..d3b0c86 100644
--- a/src/views/vlog/practice/index.vue
+++ b/src/views/vlog/practice/index.vue
@@ -241,6 +241,41 @@ const handleWheel = (event: WheelEvent): void => {
}
}
+/** 触摸起点(移动端滑动翻页) */
+let touchStartY = 0
+let touchStartX = 0
+
+/** 触摸点是否落在可独立滚动的内部面板(如任务要求)中 */
+const isInPanelScroll = (target: EventTarget | null): boolean => {
+ const panel = (target as HTMLElement | null)?.closest('.task-panel')
+ return !!panel && panel.scrollHeight > panel.clientHeight
+}
+
+/** 记录触摸起点 */
+const handleTouchStart = (event: TouchEvent): void => {
+ touchStartY = event.touches[0].clientY
+ touchStartX = event.touches[0].clientX
+}
+
+/** 阻止容器原生滚动,改由整页翻页控制(内部面板除外) */
+const handleTouchMove = (event: TouchEvent): void => {
+ if (!isInPanelScroll(event.target)) event.preventDefault()
+}
+
+/** 根据滑动方向翻页 */
+const handleTouchEnd = (event: TouchEvent): void => {
+ if (isAnimating.value || isInPanelScroll(event.target)) return
+ const deltaY = touchStartY - event.changedTouches[0].clientY
+ const deltaX = touchStartX - event.changedTouches[0].clientX
+ // 仅响应以纵向为主、且超过阈值的滑动
+ if (Math.abs(deltaY) < 40 || Math.abs(deltaX) > Math.abs(deltaY)) return
+ if (deltaY > 0) {
+ scrollToNextPage()
+ } else {
+ scrollToPrevPage()
+ }
+}
+
const restoreScrollPage = (): void => {
const savedPage = vlogStore.practiceScrollPage
const targetPage = Math.max(0, Math.min(savedPage, TOTAL_PAGES - 1))
@@ -265,12 +300,20 @@ const saveScrollPage = (): void => {
onMounted(() => {
restoreScrollPage()
showPageGuide()
- containerRef.value?.addEventListener('wheel', handleWheel, { passive: false })
+ const container = containerRef.value
+ container?.addEventListener('wheel', handleWheel, { passive: false })
+ container?.addEventListener('touchstart', handleTouchStart, { passive: true })
+ container?.addEventListener('touchmove', handleTouchMove, { passive: false })
+ container?.addEventListener('touchend', handleTouchEnd, { passive: true })
})
onUnmounted(() => {
saveScrollPage()
- containerRef.value?.removeEventListener('wheel', handleWheel)
+ const container = containerRef.value
+ container?.removeEventListener('wheel', handleWheel)
+ container?.removeEventListener('touchstart', handleTouchStart)
+ container?.removeEventListener('touchmove', handleTouchMove)
+ container?.removeEventListener('touchend', handleTouchEnd)
})
diff --git a/src/views/vlog/practice/video/index.vue b/src/views/vlog/practice/video/index.vue
index 20893a1..1520ba9 100644
--- a/src/views/vlog/practice/video/index.vue
+++ b/src/views/vlog/practice/video/index.vue
@@ -32,6 +32,11 @@ type PracticeRatingScores = Record
const workKey = computed(() => decodeURIComponent(props.path))
+const watchKey = computed(() => {
+ const key = route.query.watchKey
+ return typeof key === 'string' && key ? key : undefined
+})
+
const applyScores = (
items: PracticeRatingItem[],
scores: PracticeRatingScores,
@@ -125,6 +130,9 @@ const handleBack = (): void => {
:muted="false"
:controls="true"
:loop="false"
+ :seekable="
+ watchKey ? store.practiceVideosWatched[watchKey] === true : true
+ "
@ended="handleVideoEnded"
/>
diff --git a/src/views/vlog/video/index.vue b/src/views/vlog/video/index.vue
index 5b9099f..3ea2db9 100644
--- a/src/views/vlog/video/index.vue
+++ b/src/views/vlog/video/index.vue
@@ -160,6 +160,13 @@ const hasUnfinishedLearningVideos = computed(
completedNavigationSteps.value.some((completed) => !completed),
)
+const currentVideoWatched = computed(
+ () =>
+ vlog.professionVideosWatched[
+ professionVideoKeys[currentVideoIndex.value]
+ ] === true,
+)
+
const markCurrentProfessionVideoWatched = () => {
const videoKey = professionVideoKeys[currentVideoIndex.value]
if (!videoKey) return
@@ -350,6 +357,7 @@ onBeforeRouteLeave((to) => {
:volume="1"
:controls="true"
:hideNativeControls="true"
+ :seekable="key === 'profession' ? currentVideoWatched : true"
:anchors="[]"
@ended="onEnded"
>
@@ -426,7 +434,6 @@ onBeforeRouteLeave((to) => {
:steps="introGuideSteps"
position-offset-y="85%"
@complete="handleIntroGuideDone"
- @skip="handleIntroGuideDone"
/>
diff --git a/src/views/vlog/work/index.vue b/src/views/vlog/work/index.vue
index c27dcb1..a4c30ac 100644
--- a/src/views/vlog/work/index.vue
+++ b/src/views/vlog/work/index.vue
@@ -249,7 +249,6 @@ function openImages(urls: string[], startIndex = 0) {
:highlight-padding="12"
:highlight-radius="20"
@complete="handleGuideComplete"
- @skip="handleGuideComplete"
position-offset-y="85%"
/>
@@ -259,7 +258,6 @@ function openImages(urls: string[], startIndex = 0) {
:highlight-padding="12"
:highlight-radius="20"
@complete="handleReturnGuideComplete"
- @skip="handleReturnGuideComplete"
position-offset-y="85%"
/>