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'
+import {
+ useBasicElementsStore,
+ useFilmStore,
+ useInterviewStore,
+ useVlogStore,
+} from '@/stores'
+import {
+ AnchoredProgressBar,
+ ImgButton,
+ type ProgressAnchor,
+} from '@/components'
+import {
+ studyModuleAnchors,
+ type StudyProgressModuleKey,
+} from './studyProgressConfig'
+import {
+ getBasicElementsProgress,
+ getOverallStudyProgress,
+ getProductionModuleProgress,
+} from './studyProgressUtils'
defineOptions({
- name: 'StudyProgressPopup'
+ name: 'StudyProgressPopup',
})
type StudyProgressItem = {
@@ -27,42 +43,68 @@ const rowPositions = {
basic: { height: 168 },
vlog: { height: 148 },
interview: { height: 146 },
- film: { 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 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
+ getBasicElementsProgress(
+ basicElements.basicElementsUnlockProgress,
+ basicElements.isBasicElementsUnlock,
+ basicElements.learningVideosWatched,
+ ),
+)
+const vlogProgress = computed(() =>
+ getProductionModuleProgress('vlog', vlog.viewed, vlog.workVideosWatched),
+)
+const interviewProgress = computed(() =>
+ getProductionModuleProgress(
+ 'interview',
+ interview.viewed,
+ interview.workVideosWatched,
+ ),
+)
+const filmViewed = computed(() => ({
+ ...film.viewed,
+ FilmOnsiteShooting: film.allOnsiteScenesViewed,
+ FilmEditing: film.allEditingVideosWatched,
+}))
+const filmProgress = computed(() =>
+ getProductionModuleProgress('film', filmViewed.value, film.workVideosWatched),
+)
+const totalProgress = computed(() =>
+ getOverallStudyProgress([
+ basicProgress.value,
+ vlogProgress.value,
+ interviewProgress.value,
+ filmProgress.value,
+ ]),
)
-const totalProgress = computed(() => {
- const total =
- basicProgress.value + vlog.progressPercent + interview.progressPercent + film.progressPercent
- return Math.round(total / 4)
-})
const progressItems = computed(() => [
{
key: 'basic',
progress: basicProgress.value,
- anchors: studyModuleAnchors.basic
+ anchors: studyModuleAnchors.basic,
},
{
key: 'vlog',
- progress: getProductionModuleProgress('vlog', vlog.viewed),
- anchors: studyModuleAnchors.vlog
+ progress: vlogProgress.value,
+ anchors: studyModuleAnchors.vlog,
},
{
key: 'interview',
- progress: getProductionModuleProgress('interview', interview.viewed),
- anchors: studyModuleAnchors.interview
+ progress: interviewProgress.value,
+ anchors: studyModuleAnchors.interview,
},
{
key: 'film',
- progress: getProductionModuleProgress('film', film.viewed),
- anchors: studyModuleAnchors.film
- }
+ progress: filmProgress.value,
+ anchors: studyModuleAnchors.film,
+ },
])
const handleClose = () => {
@@ -95,10 +137,14 @@ const handleClose = () => {
:style="{ textShadow: totalTextShadow }"
>
总进度
- {{ totalProgress }}%
+
+ {{ totalProgress }}%
+
-
+
) => {
+const countViewedRoutes = (
+ routes: readonly string[],
+ viewed: Record,
+) => {
if (routes.length === 0) return 0
- return routes.filter(routeName => viewed[routeName]).length
+ return routes.filter((routeName) => viewed[routeName]).length
}
-const getStepProgress = (routes: readonly string[], viewed: Record) => {
+const getStepProgress = (
+ routes: readonly string[],
+ viewed: Record,
+) => {
if (routes.length === 0) return 0
return countViewedRoutes(routes, viewed) / routes.length
}
-const interpolate = (start: number, end: number, progress: number) =>
- start + (end - start) * progress
+const WORK_COMPLETION_KEYS = ['case', 'profession', 'idea'] as const
+
+const getWorkProgress = (workVideosWatched: Record) =>
+ countViewedRoutes(WORK_COMPLETION_KEYS, workVideosWatched) /
+ WORK_COMPLETION_KEYS.length
export const getProductionModuleProgress = (
moduleKey: ProductionModuleKey,
- viewed: Record
+ viewed: Record,
+ workVideosWatched: Record = {},
) => {
const stepRoutes = moduleStepMap[moduleKey]
- const hasAnyViewed = Object.values(stepRoutes).some(routes =>
- routes.some(routeName => viewed[routeName])
- )
+ const taskProgress = getWorkProgress(workVideosWatched) * 20
+ const stepProgress = (step: StepKey) =>
+ getStepProgress(stepRoutes[step], viewed)
+ const step1Progress = stepProgress(1) * 22
+ const step2Progress = stepProgress(2) * 22
+ const step3Progress = stepProgress(3) * 21
+ const progress = taskProgress + step1Progress + step2Progress + step3Progress
- if (!hasAnyViewed) return 0
+ if (
+ progress >= 85 &&
+ step1Progress === 22 &&
+ step2Progress === 22 &&
+ step3Progress === 21
+ ) {
+ return 100
+ }
- 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
+ return Math.round(progress)
+}
+
+const BASIC_ELEMENTS_VIDEO_KEYS = [
+ 'basic-composition:step1',
+ 'basic-composition:step2',
+ 'basic-light:step2',
+ 'basic-light:step3',
+ 'basic-light:step4',
+ 'basic-camera-movement:step2',
+ 'basic-camera-movement:step3',
+ 'basic-shooting-skills:step2',
+ 'basic-shooting-skills:step3',
+ 'basic-shooting-skills:step4',
+ 'basic-shooting-skills:step5',
+ 'basic-shooting-skills:step6',
+ 'basic-shooting-skills:step7',
+ 'basic-short-video-editing-skills:step2',
+ 'basic-short-video-editing-skills:step3',
+ 'basic-short-video-editing-skills:step4',
+ 'basic-short-video-editing-skills:step5',
+ 'basic-short-video-editing-skills:step6',
+ 'basic-short-video-editing-skills:step7',
+ 'basic-short-video-editing-skills:step8',
+ 'basic-short-video-editing-skills:step9',
+ 'basic-short-video-editing-skills:step10',
+ 'basic-short-video-editing-skills:step11',
+] as const
+
+export const getBasicElementsProgress = (
+ unlockProgress: number,
+ isUnlocked: boolean,
+ learningVideosWatched: Record = {},
+) => {
+ if (isUnlocked) return 100
+
+ const watchedProgress =
+ (countViewedRoutes(BASIC_ELEMENTS_VIDEO_KEYS, learningVideosWatched) /
+ BASIC_ELEMENTS_VIDEO_KEYS.length) *
+ 100
+
+ return Math.round(
+ Math.max(0, Math.min(100, Math.max(unlockProgress, watchedProgress))),
+ )
+}
+
+export const getOverallStudyProgress = (progresses: readonly number[]) => {
+ if (progresses.length === 0) return 0
+ const total = progresses.reduce((sum, progress) => sum + progress, 0)
+ return Math.round(total / progresses.length)
}
diff --git a/src/views/interview/after-editing/editing.vue b/src/views/interview/after-editing/editing.vue
index 0bc0e50..325840d 100644
--- a/src/views/interview/after-editing/editing.vue
+++ b/src/views/interview/after-editing/editing.vue
@@ -1,46 +1,121 @@
-
-
+
+
+
+
+
+
+
diff --git a/src/views/interview/after-editing/audio/index.vue b/src/views/interview/after-editing/sound-effect/index.vue
similarity index 100%
rename from src/views/interview/after-editing/audio/index.vue
rename to src/views/interview/after-editing/sound-effect/index.vue
diff --git a/src/views/interview/components/menu.vue b/src/views/interview/components/menu.vue
index d77075f..30532bf 100644
--- a/src/views/interview/components/menu.vue
+++ b/src/views/interview/components/menu.vue
@@ -92,8 +92,8 @@ const menuItems = ref([
disabled: false,
},
{
- label: '音频',
- routeName: 'InterviewAudio',
+ label: '音效',
+ routeName: 'InterviewSoundEffect',
parentValue: 3,
disabled: false,
},
@@ -188,8 +188,8 @@ const handleItemClick = (item: { routeName: string; disabled: boolean }) => {
class="w-10"
:src="
isViewed(item.routeName)
- ? '/src/assets/images/midterm/完成.png'
- : '/src/assets/images/midterm/未完成.png'
+ ? getStaticImage('midterm/完成.png')
+ : getStaticImage('midterm/未完成.png')
"
alt=""
/>
diff --git a/src/views/interview/index.vue b/src/views/interview/index.vue
index 15daf6c..7d0fe17 100644
--- a/src/views/interview/index.vue
+++ b/src/views/interview/index.vue
@@ -5,7 +5,7 @@ import { ImgButton } from '@/components/Button'
import { computed, ref, watch, type Ref } from 'vue'
import { ProgressBar, Overlay } from '@/components'
-import { GuideSteps, type GuideStep } from '@/components/Guide'
+import { GuideSteps, GuideStepsItem, type GuideStep } from '@/components/Guide'
import { useRoute, useRouter } from 'vue-router'
import { useInterviewStore } from '@/stores'
import InterviewMenu from './components/menu.vue'
@@ -14,14 +14,26 @@ const route = useRoute()
const router = useRouter()
const interview = useInterviewStore()
-const showReturnGuide = ref(route.query.guide === 'return' && interview.isGuideInterviewReturn)
+const showReturnGuide = ref(
+ route.query.guide === 'return' && interview.isGuideInterviewReturn,
+)
+
+// 实战操作台一次性引导(currentStep === 4 时解锁)
+const practiceGuideKey = 'interview-practice-guide'
+const hasShownPracticeGuide = computed(
+ () => interview.guideShown[practiceGuideKey] === true,
+)
+const showPracticeGuide = computed(
+ () => currentStep.value === 4 && !hasShownPracticeGuide.value,
+)
const returnSteps: GuideStep[] = [
{
id: 'return-step1',
- content: '接下来,我们将进入真实项目流程,体验一条人物访谈作品是如何一步步完成的。',
- audioSrc: getPublicPath('audio/py_3_007.MP3')
- }
+ content:
+ '接下来,我们将进入真实项目流程,体验一条人物访谈作品是如何一步步完成的。',
+ audioSrc: getPublicPath('audio/py_3_007.MP3'),
+ },
]
const handleReturnGuideComplete = () => {
@@ -30,6 +42,10 @@ const handleReturnGuideComplete = () => {
router.replace({ name: 'Interview' })
}
+const handlePracticeGuideComplete = () => {
+ interview.markGuideShown(practiceGuideKey)
+}
+
const stageGuideMap = {
2: {
flagKey: 'isGuideInterviewStage2',
@@ -38,14 +54,15 @@ const stageGuideMap = {
{
id: 'stage2-step1',
content: '前期准备阶段,我们已经完成了主题确认、脚本结构与镜头规划。',
- audioSrc: getPublicPath('audio/py_3_010.MP3')
+ audioSrc: getPublicPath('audio/py_3_010.MP3'),
},
{
id: 'stage2-step2',
- content: '进入中期拍摄后就要按照既定方案,把后期需要用的素材尽可能拍完整。',
- audioSrc: getPublicPath('audio/py_3_011.MP3')
- }
- ]
+ content:
+ '进入中期拍摄后就要按照既定方案,把后期需要用的素材尽可能拍完整。',
+ audioSrc: getPublicPath('audio/py_3_011.MP3'),
+ },
+ ],
},
3: {
flagKey: 'isGuideInterviewStage3',
@@ -54,17 +71,17 @@ const stageGuideMap = {
{
id: 'stage3-step1',
content: '拍摄完成啦!现在我们正式进入剪辑环节。',
- audioSrc: getPublicPath('audio/py_3_012.MP3')
- }
- ]
- }
+ audioSrc: getPublicPath('audio/py_3_012.MP3'),
+ },
+ ],
+ },
} as const
type StageKey = keyof typeof stageGuideMap
const stageGuideStep = ref(null)
const stageGuideSteps = computed(() =>
- stageGuideStep.value ? stageGuideMap[stageGuideStep.value].steps : []
+ stageGuideStep.value ? stageGuideMap[stageGuideStep.value].steps : [],
) as Ref
const triggerStageGuide = (step: number) => {
@@ -84,7 +101,7 @@ const handleStageGuideComplete = () => {
const visible = computed({
get: () => interview.interviewMenuVisible,
- set: value => interview.setInterviewMenuVisible(value)
+ set: (value) => interview.setInterviewMenuVisible(value),
})
const currentStep = computed(() => interview.InterviewUnlockProgress)
@@ -111,6 +128,16 @@ const handleClose = () => {
router.push({ name: 'Action' })
}
+watch(
+ showPracticeGuide,
+ (val) => {
+ if (val) {
+ interview.markGuideShown(practiceGuideKey)
+ }
+ },
+ { immediate: true },
+)
+
watch(
[visible, currentStep],
([isVisible, step]) => {
@@ -118,7 +145,7 @@ watch(
triggerStageGuide(step)
}
},
- { immediate: true }
+ { immediate: true },
)
@@ -161,22 +188,41 @@ watch(
+
+ 点击【实战操作台】进入实战操作!
+
+
-
+
import { computed, nextTick, ref } from 'vue'
-import { useRouter } from 'vue-router'
+import { onBeforeRouteLeave, useRouter } from 'vue-router'
import { ImgButton, Video } from '@/components'
+import QuestionQuiz, {
+ type Question,
+} from '@/components/QuestionQuiz/QuestionQuiz.vue'
import StepNavigation from '@/components/Guide/StepNavigation.vue'
+import UnfinishedLearningPrompt from '@/components/UnfinishedLearningPrompt'
import { IMAGE_PATHS, getPublicPath, getStaticImage } from '@/utils'
+import { useInterviewStore } from '@/stores'
const router = useRouter()
+const interview = useInterviewStore()
const playing = ref(false)
+const showQuiz = ref(false)
+const questions = ref([])
+
+// 只给“移动镜头的作用”(index 2)出题
+const getQuestionsForVideo = (): Question[] => {
+ return [
+ {
+ id: 1,
+ title: '',
+ description: '【单选题】下面哪一种拍摄情境,最适合使用滑轨移动镜头?',
+ options: [
+ { id: 'a', label: '人物正对镜头说话,背景简单,镜头静止' },
+ { id: 'b', label: '人物站在窗边,前景有栏杆或墙体,镜头缓慢横向移动' },
+ { id: 'c', label: '人物快速奔跑,摄影师跟着移动拍摄' },
+ ],
+ correctOptionId: 'b',
+ explanation:
+ '滑轨镜头的作用在于稳定、克制地交代空间关系。当前景—人物—背景同时存在时,缓慢的横向移动,会让画面产生层次和叙事感。',
+ },
+ ]
+}
+
const showStepNavigation = ref(false)
+const showUnfinishedPrompt = ref(false)
+const pendingLeavePath = ref(null)
+const allowRouteLeave = ref(false)
const currentVideoIndex = ref(0)
const navigationSteps = ref([
@@ -29,16 +60,41 @@ const navigationSteps = ref([
id: 'q2-moving-shot',
label: '中期拍摄',
content: '移动镜头的作用',
+ videoUrl: getPublicPath('video/interview/on-site/ftmk_ydjtdzy_batch.mp4'),
type: 'shooting' as const,
},
{
id: 'q2-close-up',
label: '中期拍摄',
content: '特写镜头的作用',
+ videoUrl: getPublicPath('video/interview/on-site/ftmk_txjtdzy_batch.mp4'),
type: 'shooting' as const,
},
])
+const onsiteVideoKeys = [
+ 'interview-onsite-shoot-material',
+ 'interview-onsite-required-shots',
+ 'interview-onsite-moving-shot',
+ 'interview-onsite-close-up',
+] as const
+
+const completedNavigationSteps = computed(() =>
+ navigationSteps.value.map(
+ (_, index) =>
+ interview.professionVideosWatched[onsiteVideoKeys[index]] === true,
+ ),
+)
+
+const hasUnfinishedLearningVideos = computed(() =>
+ completedNavigationSteps.value.some((completed) => !completed),
+)
+
+const markCurrentOnsiteVideoWatched = () => {
+ const videoKey = onsiteVideoKeys[currentVideoIndex.value]
+ if (videoKey) interview.markOnsiteVideoWatched(videoKey)
+}
+
const currentVideo = computed(
() => navigationSteps.value[currentVideoIndex.value].videoUrl,
)
@@ -48,8 +104,8 @@ const back = () => {
}
const handleStepClick = (index: number) => {
+ showQuiz.value = false
currentVideoIndex.value = index
- // showStepNavigation.value = false
nextTick(() => {
playing.value = true
})
@@ -59,7 +115,26 @@ const handleStepNavigationClose = () => {
showStepNavigation.value = false
}
+const handleQuestionsComplete = () => {
+ markCurrentOnsiteVideoWatched()
+ showQuiz.value = false
+ currentVideoIndex.value++
+ nextTick(() => {
+ playing.value = true
+ })
+}
+
const handleEnded = () => {
+ // “移动镜头的作用”(index 2)播完后先出题,答完再算完成并切下一个
+ if (currentVideoIndex.value === 2) {
+ playing.value = false
+ questions.value = getQuestionsForVideo()
+ showQuiz.value = true
+ return
+ }
+
+ markCurrentOnsiteVideoWatched()
+
if (currentVideoIndex.value >= navigationSteps.value.length - 1) {
playing.value = false
return
@@ -70,6 +145,25 @@ const handleEnded = () => {
playing.value = true
})
}
+
+const handleContinueLearning = () => {
+ showUnfinishedPrompt.value = false
+ pendingLeavePath.value = null
+}
+
+const handleConfirmLeave = () => {
+ if (!pendingLeavePath.value) return
+ allowRouteLeave.value = true
+ showUnfinishedPrompt.value = false
+ router.push(pendingLeavePath.value)
+}
+
+onBeforeRouteLeave((to) => {
+ if (allowRouteLeave.value || !hasUnfinishedLearningVideos.value) return true
+ pendingLeavePath.value = to.fullPath
+ showUnfinishedPrompt.value = true
+ return false
+})
@@ -108,6 +202,12 @@ const handleEnded = () => {
/>
+
@@ -116,8 +216,15 @@ const handleEnded = () => {
:background-image="getStaticImage('work/vlog背景全屏待定')"
:steps="navigationSteps"
:current-step-index="currentVideoIndex"
+ :completed-steps="completedNavigationSteps"
@step-click="handleStepClick"
@close="handleStepNavigationClose"
/>
+
+
diff --git a/src/views/interview/midterm-shooting/shooting/index.vue b/src/views/interview/midterm-shooting/shooting/index.vue
index 1a21337..9398c50 100644
--- a/src/views/interview/midterm-shooting/shooting/index.vue
+++ b/src/views/interview/midterm-shooting/shooting/index.vue
@@ -1,23 +1,15 @@
@@ -99,13 +64,13 @@ const back = () => {
backgroundImage: getBackgroundImage('interview/shooting/实地取景底图'),
}"
>
-
+
-
+
{
@click="handleClickB"
/>
-
+
{
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ bg-image="interview/shooting/访谈-构图指南底图"
+ :btn-list="btnList"
+ :knowledge-card-component="KnowledgeCard"
+ :video-src="videoSrc"
+ @close="showModal = false"
+ />
-
diff --git a/src/views/interview/practice/index.vue b/src/views/interview/practice/index.vue
index 4cdd39c..d7a0854 100644
--- a/src/views/interview/practice/index.vue
+++ b/src/views/interview/practice/index.vue
@@ -67,6 +67,17 @@ const worksList = ref
([
// }
])
+const getCachedWorkScore = (work: WorkItem): number => {
+ const scores = interviewStore.practiceWorkRatings[work.path]
+ const values = scores ? Object.values(scores) : []
+
+ if (values.length === 0) return work.stars
+
+ return Math.round(
+ values.reduce((total, score) => total + score, 0) / values.length,
+ )
+}
+
/** 左滑动作品列表 */
const scrollWorksLeft = (): void => {
if (worksScrollIndex.value > 0) {
@@ -290,7 +301,7 @@ onUnmounted(() => {
diff --git a/src/views/interview/practice/my-notes/index.vue b/src/views/interview/practice/my-notes/index.vue
index 23e5d68..69d6538 100644
--- a/src/views/interview/practice/my-notes/index.vue
+++ b/src/views/interview/practice/my-notes/index.vue
@@ -1,19 +1,37 @@
@@ -81,6 +116,13 @@ const handleSave = (): void => {
placeholder="点击输入创作笔记"
>
+
+ 提交成功
+
+
diff --git a/src/views/interview/practice/video/index.vue b/src/views/interview/practice/video/index.vue
index a502367..788ddbb 100644
--- a/src/views/interview/practice/video/index.vue
+++ b/src/views/interview/practice/video/index.vue
@@ -6,9 +6,11 @@ import PracticeRatingModal, {
type PracticeRatingItem,
} from '@/components/PracticeRatingModal'
import { IMAGE_PATHS, getPublicPath } from '@/utils'
-import { ref } from 'vue'
+import { useInterviewStore } from '@/stores/modules/interview'
+import { computed, ref } from 'vue'
const router = useRouter()
+const store = useInterviewStore()
const props = defineProps({
path: {
@@ -17,8 +19,24 @@ const props = defineProps({
},
})
+type PracticeRatingScores = Record
+
+const workKey = computed(() => decodeURIComponent(props.path))
+
+const applyScores = (
+ items: PracticeRatingItem[],
+ scores: PracticeRatingScores,
+): PracticeRatingItem[] =>
+ items.map((item) => ({
+ ...item,
+ score: scores[item.title] ?? item.score,
+ }))
+
+const toScores = (items: PracticeRatingItem[]): PracticeRatingScores =>
+ Object.fromEntries(items.map((item) => [item.title, item.score]))
+
/** 评估项目列表 */
-const evaluationItems = ref([
+const defaultEvaluationItems: PracticeRatingItem[] = [
{
title: '人物表达',
description: '能清晰介绍人物身份、特点或故事,让观众认识人物',
@@ -49,7 +67,18 @@ const evaluationItems = ref([
description: '作品完整,有真实感与感染力,具备观看吸引力',
score: 5,
},
-])
+]
+
+const evaluationItems = ref(
+ applyScores(
+ defaultEvaluationItems,
+ store.getPracticeWorkRating(workKey.value),
+ ),
+)
+
+const handleSaveEvaluation = (items: PracticeRatingItem[]): void => {
+ store.setPracticeWorkRating(workKey.value, toScores(items))
+}
/** 保存弹窗 */
const showModal = ref(false)
@@ -82,10 +111,11 @@ const handleBack = (): void => {
diff --git a/src/views/interview/video/index.vue b/src/views/interview/video/index.vue
index cf0b16d..4697e4f 100644
--- a/src/views/interview/video/index.vue
+++ b/src/views/interview/video/index.vue
@@ -1,10 +1,14 @@
@@ -131,10 +171,17 @@ const handleEndingGuideComplete = () => {
:background-image="''"
:steps="navigationSteps"
:current-step-index="currentVideoIndex"
+ :completed-steps="completedNavigationSteps"
@step-click="handleStepClick"
@close="showStepNavigation = false"
/>
+
+
(() => {
const currentItemValue = ref('') // 当前选中的子级菜单项
const getMenuItems = computed(() => {
- return menuItems.value.filter(item => item.parentValue === currentParentValue.value)
+ return menuItems.value.filter(
+ (item) => item.parentValue === currentParentValue.value,
+ )
})
const handleItemClick = (item: { routeName: string; disabled: boolean }) => {
@@ -128,13 +130,20 @@ const handleItemClick = (item: { routeName: string; disabled: boolean }) => {
-
+
{{ category[currentParentValue - 1].label }}
-
![]()
+
{
:style="{ backgroundImage: getBackgroundImage('components/menu-bg.png') }"
>
-
+
{
-
+
{{ item.label }}
@@ -177,8 +196,8 @@ const handleItemClick = (item: { routeName: string; disabled: boolean }) => {
diff --git a/src/views/vlog/practice/index.vue b/src/views/vlog/practice/index.vue
index ca3df74..c57e6a6 100644
--- a/src/views/vlog/practice/index.vue
+++ b/src/views/vlog/practice/index.vue
@@ -68,6 +68,17 @@ const worksList = ref([
},
])
+const getCachedWorkScore = (work: WorkItem): number => {
+ const scores = vlogStore.practiceWorkRatings[work.path]
+ const values = scores ? Object.values(scores) : []
+
+ if (values.length === 0) return work.stars
+
+ return Math.round(
+ values.reduce((total, score) => total + score, 0) / values.length,
+ )
+}
+
/** 左滑动作品列表 */
const scrollWorksLeft = (): void => {
if (worksScrollIndex.value > 0) {
@@ -291,7 +302,7 @@ onUnmounted(() => {
diff --git a/src/views/vlog/practice/my-notes/index.vue b/src/views/vlog/practice/my-notes/index.vue
index 5b00dab..7fcb3df 100644
--- a/src/views/vlog/practice/my-notes/index.vue
+++ b/src/views/vlog/practice/my-notes/index.vue
@@ -1,19 +1,37 @@
@@ -82,6 +117,13 @@ const handleSave = (): void => {
placeholder="点击输入创作笔记"
>
+
+ 提交成功
+
+
diff --git a/src/views/vlog/practice/video/index.vue b/src/views/vlog/practice/video/index.vue
index b5c8bc3..3b4f879 100644
--- a/src/views/vlog/practice/video/index.vue
+++ b/src/views/vlog/practice/video/index.vue
@@ -6,9 +6,11 @@ import PracticeRatingModal, {
type PracticeRatingItem,
} from '@/components/PracticeRatingModal'
import { IMAGE_PATHS, getPublicPath } from '@/utils'
-import { ref } from 'vue'
+import { useVlogStore } from '@/stores/modules/vlog'
+import { computed, ref } from 'vue'
const router = useRouter()
+const store = useVlogStore()
const props = defineProps({
path: {
@@ -17,8 +19,24 @@ const props = defineProps({
},
})
+type PracticeRatingScores = Record
+
+const workKey = computed(() => decodeURIComponent(props.path))
+
+const applyScores = (
+ items: PracticeRatingItem[],
+ scores: PracticeRatingScores,
+): PracticeRatingItem[] =>
+ items.map((item) => ({
+ ...item,
+ score: scores[item.title] ?? item.score,
+ }))
+
+const toScores = (items: PracticeRatingItem[]): PracticeRatingScores =>
+ Object.fromEntries(items.map((item) => [item.title, item.score]))
+
/** 评估项目列表 */
-const evaluationItems = ref([
+const defaultEvaluationItems: PracticeRatingItem[] = [
{
title: '开场建立',
description: '能清晰交代时间、地点与事件开端,观众易于进入情境',
@@ -50,7 +68,18 @@ const evaluationItems = ref([
description: '视频完整、有连贯观看体验,能表达出一次真实记录或个人感受',
score: 5,
},
-])
+]
+
+const evaluationItems = ref(
+ applyScores(
+ defaultEvaluationItems,
+ store.getPracticeWorkRating(workKey.value),
+ ),
+)
+
+const handleSaveEvaluation = (items: PracticeRatingItem[]): void => {
+ store.setPracticeWorkRating(workKey.value, toScores(items))
+}
/** 保存弹窗 */
const showModal = ref(false)
@@ -83,10 +112,11 @@ const handleBack = (): void => {
diff --git a/src/views/vlog/video/index.vue b/src/views/vlog/video/index.vue
index 38ac3cb..e0728ae 100644
--- a/src/views/vlog/video/index.vue
+++ b/src/views/vlog/video/index.vue
@@ -1,9 +1,10 @@