feat: 完善课程引导与实战评价体验
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
No files matched your search
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 239 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 453 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 456 KiB |
|
After Width: | Height: | Size: 217 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 2.7 MiB After Width: | Height: | Size: 348 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 276 KiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 384 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 376 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 337 KiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 333 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 312 KiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 373 KiB |
|
Before Width: | Height: | Size: 653 KiB After Width: | Height: | Size: 339 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 345 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 271 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 287 KiB |
|
After Width: | Height: | Size: 281 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 259 KiB |
|
After Width: | Height: | Size: 288 KiB |
|
After Width: | Height: | Size: 253 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 455 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 234 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 3.6 MiB |
|
After Width: | Height: | Size: 910 KiB |
|
After Width: | Height: | Size: 241 KiB |
|
After Width: | Height: | Size: 269 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 458 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 354 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 3.7 MiB After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 53 KiB |
@@ -1,14 +1,20 @@
|
||||
<template>
|
||||
<teleport to="body">
|
||||
<div class="guide-steps-container" v-if="show">
|
||||
<div
|
||||
class="guide-steps-container"
|
||||
v-if="show"
|
||||
>
|
||||
<SpotlightOverlay
|
||||
:visible="Boolean(currentHighlightSelector)"
|
||||
:visible="show"
|
||||
:target-selector="currentHighlightSelector"
|
||||
:padding="currentHighlightPadding"
|
||||
:border-radius="currentHighlightRadius"
|
||||
:highlight-mode="currentHighlightMode"
|
||||
/>
|
||||
<div class="guide-align" :style="alignStyle">
|
||||
<div
|
||||
class="guide-align"
|
||||
:style="alignStyle"
|
||||
>
|
||||
<GuideDragon
|
||||
:show-skip="currentStep < totalSteps"
|
||||
:current-step="currentStep"
|
||||
@@ -44,9 +50,15 @@
|
||||
@continue="handleNext"
|
||||
>
|
||||
<!-- 支持组件形式的 content -->
|
||||
<component v-if="isCurrentStepComponent" :is="currentStepContentComponent" />
|
||||
<component
|
||||
v-if="isCurrentStepComponent"
|
||||
:is="currentStepContentComponent"
|
||||
/>
|
||||
<!-- 支持字符串形式的 content -->
|
||||
<div v-else v-html="currentStepContent"></div>
|
||||
<div
|
||||
v-else
|
||||
v-html="currentStepContent"
|
||||
></div>
|
||||
</GuideStepsItem>
|
||||
</template>
|
||||
</GuideDragon>
|
||||
@@ -101,7 +113,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
highlightRadius: 16,
|
||||
highlightMode: 'border',
|
||||
positionOffsetY: '50%',
|
||||
visible: true
|
||||
visible: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
@@ -126,7 +138,7 @@ const alignStyle = reactive<{
|
||||
top: '0px',
|
||||
transform: 'scale(1)',
|
||||
width: '1920px',
|
||||
height: '1080px'
|
||||
height: '1080px',
|
||||
})
|
||||
const normalizeSelector = (selector?: string | null) => {
|
||||
if (!selector) return null
|
||||
@@ -157,21 +169,25 @@ const currentStepData = computed(() => {
|
||||
return props.steps[currentStepIndex.value] || null
|
||||
})
|
||||
|
||||
const currentStepConfig = computed(() => props.steps[currentStepIndex.value] || null)
|
||||
const currentStepConfig = computed(
|
||||
() => props.steps[currentStepIndex.value] || null,
|
||||
)
|
||||
const currentHighlightSelector = computed(() => {
|
||||
return normalizeSelector(currentStepConfig.value?.highlightSelector ?? props.highlightSelector)
|
||||
return normalizeSelector(
|
||||
currentStepConfig.value?.highlightSelector ?? props.highlightSelector,
|
||||
)
|
||||
})
|
||||
const currentHighlightPadding = computed(
|
||||
() => currentStepConfig.value?.highlightPadding ?? props.highlightPadding
|
||||
() => currentStepConfig.value?.highlightPadding ?? props.highlightPadding,
|
||||
)
|
||||
const currentHighlightRadius = computed(
|
||||
() => currentStepConfig.value?.highlightRadius ?? props.highlightRadius
|
||||
() => currentStepConfig.value?.highlightRadius ?? props.highlightRadius,
|
||||
)
|
||||
const currentHighlightMode = computed(
|
||||
() => currentStepConfig.value?.highlightMode ?? props.highlightMode
|
||||
() => currentStepConfig.value?.highlightMode ?? props.highlightMode,
|
||||
)
|
||||
const currentPositionOffsetY = computed(
|
||||
() => currentStepConfig.value?.positionOffsetY ?? props.positionOffsetY
|
||||
() => currentStepConfig.value?.positionOffsetY ?? props.positionOffsetY,
|
||||
)
|
||||
|
||||
const currentStepComponent = computed(() => {
|
||||
@@ -210,7 +226,9 @@ const currentStepContentComponent = computed(() => {
|
||||
})
|
||||
|
||||
const handleNext = () => {
|
||||
const maxSteps = useSlotMode.value ? slotSteps.value.length : props.steps.length
|
||||
const maxSteps = useSlotMode.value
|
||||
? slotSteps.value.length
|
||||
: props.steps.length
|
||||
if (maxSteps === 0) {
|
||||
handleComplete()
|
||||
return
|
||||
@@ -234,7 +252,9 @@ const handleComplete = () => {
|
||||
}
|
||||
|
||||
const goToStep = (index: number) => {
|
||||
const maxSteps = useSlotMode.value ? slotSteps.value.length : props.steps.length
|
||||
const maxSteps = useSlotMode.value
|
||||
? slotSteps.value.length
|
||||
: props.steps.length
|
||||
if (maxSteps === 0) {
|
||||
console.warn('GuideSteps goToStep: No steps available')
|
||||
return
|
||||
@@ -244,7 +264,12 @@ const goToStep = (index: number) => {
|
||||
currentStepIndex.value = index
|
||||
emit('step-change', currentStepData.value, currentStepIndex.value)
|
||||
} else {
|
||||
console.warn('GuideSteps goToStep: Invalid step index', index, 'max:', maxSteps - 1)
|
||||
console.warn(
|
||||
'GuideSteps goToStep: Invalid step index',
|
||||
index,
|
||||
'max:',
|
||||
maxSteps - 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,7 +308,9 @@ let autoPlayTimer: number | null = null
|
||||
const startAutoPlay = () => {
|
||||
if (props.autoPlayInterval > 0) {
|
||||
autoPlayTimer = window.setInterval(() => {
|
||||
const maxSteps = useSlotMode.value ? slotSteps.value.length : props.steps.length
|
||||
const maxSteps = useSlotMode.value
|
||||
? slotSteps.value.length
|
||||
: props.steps.length
|
||||
if (currentStepIndex.value < maxSteps - 1) {
|
||||
handleNext()
|
||||
} else {
|
||||
@@ -317,7 +344,7 @@ onMounted(() => {
|
||||
if (transform && transform !== 'none') {
|
||||
const match = transform.match(/matrix\(([^)]+)\)/)
|
||||
if (match) {
|
||||
const parts = match[1].split(',').map(s => parseFloat(s.trim()))
|
||||
const parts = match[1].split(',').map((s) => parseFloat(s.trim()))
|
||||
if (!Number.isNaN(parts[0])) scale = parts[0]
|
||||
}
|
||||
}
|
||||
@@ -343,7 +370,7 @@ defineExpose({
|
||||
currentStep,
|
||||
totalSteps,
|
||||
startAutoPlay,
|
||||
stopAutoPlay
|
||||
stopAutoPlay,
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
<template>
|
||||
<div v-if="visible" class="spotlight-overlay">
|
||||
<div
|
||||
v-if="visible"
|
||||
class="spotlight-overlay"
|
||||
>
|
||||
<div
|
||||
v-if="!hasHighlightTarget"
|
||||
class="spotlight-dim"
|
||||
></div>
|
||||
<!-- 渐变遮罩层 -->
|
||||
<div v-if="props.highlightMode === 'gradient'" class="spotlight-mask" :style="maskStyle"></div>
|
||||
<div
|
||||
v-else-if="props.highlightMode === 'gradient'"
|
||||
class="spotlight-mask"
|
||||
:style="maskStyle"
|
||||
></div>
|
||||
<!-- 画框模式:使用 box-shadow 遮罩 + 边框 -->
|
||||
<div
|
||||
v-if="props.highlightMode === 'border' && rect.width > 0 && rect.height > 0"
|
||||
v-else-if="props.highlightMode === 'border'"
|
||||
class="spotlight-hole"
|
||||
:style="holeStyle"
|
||||
></div>
|
||||
@@ -35,11 +46,14 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
padding: 8,
|
||||
borderRadius: 16,
|
||||
gradientRange: 100,
|
||||
highlightMode: 'border'
|
||||
highlightMode: 'border',
|
||||
})
|
||||
|
||||
const rect = reactive({ top: 0, left: 0, width: 0, height: 0 })
|
||||
const isElementReady = ref(false)
|
||||
const hasHighlightTarget = computed(
|
||||
() => isElementReady.value && rect.width > 0 && rect.height > 0,
|
||||
)
|
||||
|
||||
// 重试相关变量
|
||||
let retryCount = 0
|
||||
@@ -148,7 +162,7 @@ const startRetry = (selector: string) => {
|
||||
mutationObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: false
|
||||
attributes: false,
|
||||
})
|
||||
|
||||
// 同时使用 requestAnimationFrame 轮询检查(作为备用机制)
|
||||
@@ -210,7 +224,7 @@ watch(
|
||||
stopRetry() // 停止之前的重试
|
||||
requestAnimationFrame(measure)
|
||||
},
|
||||
{ immediate: true }
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const onResize = () => measure()
|
||||
@@ -233,7 +247,7 @@ const maskStyle = computed(() => {
|
||||
if (!props.visible || rect.width === 0 || rect.height === 0) {
|
||||
return {
|
||||
maskImage: 'none',
|
||||
WebkitMaskImage: 'none'
|
||||
WebkitMaskImage: 'none',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,7 +295,7 @@ const maskStyle = computed(() => {
|
||||
|
||||
return {
|
||||
maskImage: gradient,
|
||||
WebkitMaskImage: gradient
|
||||
WebkitMaskImage: gradient,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -297,7 +311,7 @@ const holeStyle = computed(() => {
|
||||
width: rect.width + 'px',
|
||||
height: rect.height + 'px',
|
||||
borderRadius: props.borderRadius + 'px',
|
||||
boxShadow: `0 0 0 9999px rgba(0,0,0,0.6)`
|
||||
boxShadow: `0 0 0 9999px rgba(0,0,0,0.6)`,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -310,16 +324,20 @@ const holeStyle = computed(() => {
|
||||
z-index: 100000;
|
||||
}
|
||||
|
||||
.spotlight-dim,
|
||||
.spotlight-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.spotlight-mask {
|
||||
mask-image: radial-gradient(ellipse, transparent 0%, black 100%);
|
||||
-webkit-mask-image: radial-gradient(ellipse, transparent 0%, black 100%);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.spotlight-hole {
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { Overlay } from '@/components/Overlay'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
title: string
|
||||
desc: string
|
||||
images: string[]
|
||||
compositionKey: '1' | '2' | '3'
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
|
||||
const imageMap = {
|
||||
'1': 'interview/composition/示意图-前景',
|
||||
'2': 'interview/composition/示意图-留白',
|
||||
'3': 'interview/composition/示意图-三角',
|
||||
}
|
||||
|
||||
const getImage = computed(() => {
|
||||
return getBackgroundImage(imageMap[props.compositionKey])
|
||||
})
|
||||
|
||||
const getOverlayImage = (index: number) => {
|
||||
const overlayMap: Record<string, string[]> = {
|
||||
'1': [
|
||||
'interview/composition/前景1-辅助线',
|
||||
'interview/composition/前景2-辅助线',
|
||||
],
|
||||
'2': [
|
||||
'interview/composition/留白1-辅助线',
|
||||
'interview/composition/留白2-辅助线',
|
||||
],
|
||||
'3': [
|
||||
'interview/composition/三角1-辅助线',
|
||||
'interview/composition/三角2-辅助线',
|
||||
],
|
||||
}
|
||||
const images = overlayMap[props.compositionKey] || []
|
||||
return images[index] || ''
|
||||
}
|
||||
|
||||
const showBoxLineMap = ref<Record<number, boolean>>({})
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(v) => {
|
||||
if (v) {
|
||||
showBoxLineMap.value = {}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const handleToggle = (index: number) => {
|
||||
showBoxLineMap.value[index] = !showBoxLineMap.value[index]
|
||||
}
|
||||
|
||||
const show = computed(() => props.visible)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Overlay
|
||||
:visible="show"
|
||||
:clickable="false"
|
||||
>
|
||||
<div
|
||||
class="relative w-[1752px] h-[932px]"
|
||||
:style="{ backgroundImage: getBackgroundImage('shooting/知识卡片底板') }"
|
||||
>
|
||||
<div
|
||||
class="absolute top-4 left-1/2 -translate-x-1/2 text-[45px] text-white font-bold tracking-[2px] drop-shadow-[2px_2px_4px_rgba(0,0,139,0.8)]"
|
||||
>
|
||||
{{ props.title }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute w-full h-[38px] top-32 left-1/2 -translate-x-1/2 bg-no-repeat bg-center"
|
||||
:style="{ backgroundImage: getBackgroundImage('shooting/文案1') }"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="absolute w-[191px] h-[131px] top-[30px] left-[120px] bg-no-repeat bg-center"
|
||||
:style="{ backgroundImage: getImage }"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="absolute top-[calc(50%-30px)] -translate-y-1/2"
|
||||
v-for="(image, index) in props.images"
|
||||
:key="index"
|
||||
:style="{ left: index === 0 ? '15%' : '55%' }"
|
||||
>
|
||||
<ImgButton
|
||||
class="absolute right-6 top-6 z-10"
|
||||
:image="
|
||||
showBoxLineMap[index] ? 'shooting/辅助线开' : 'shooting/辅助线关'
|
||||
"
|
||||
@click="handleToggle(index)"
|
||||
/>
|
||||
<div class="relative">
|
||||
<img
|
||||
class="w-[560px]"
|
||||
:src="getStaticImage(image)"
|
||||
alt=""
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="showBoxLineMap[index]"
|
||||
class="absolute inset-0 pointer-events-none w-[560px] bg-cover bg-center bg-no-repeat"
|
||||
:style="{
|
||||
backgroundImage: getBackgroundImage(getOverlayImage(index)),
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute w-full px-20 bottom-32 left-1/2 -translate-x-1/2 text-[#395E66] text-3xl font-bold indent-14 leading-[50px]"
|
||||
>
|
||||
{{ props.desc }}
|
||||
</div>
|
||||
|
||||
<ImgButton
|
||||
class="absolute right-2 top-4 translate-x-1/2 -translate-y-1/2 z-10"
|
||||
:image="IMAGE_PATHS.COMPONENTS.CLOSE"
|
||||
@click="emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</Overlay>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from './index.vue'
|
||||
export type { PracticeRatingItem } from './types'
|
||||
@@ -0,0 +1,117 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Overlay } from '@/components/Overlay'
|
||||
import { StarRating } from '@/components/Rating'
|
||||
import { IMAGE_PATHS, getBackgroundImage } from '@/utils'
|
||||
import type { PracticeRatingItem } from './types'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
items: PracticeRatingItem[]
|
||||
backgroundImage: string
|
||||
topTip?: string
|
||||
bottomTip?: string
|
||||
itemBackground?: string
|
||||
saveButtonImage?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
itemBackground: 'vlog/practice/评分条',
|
||||
saveButtonImage: 'vlog/practice/保存',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
const hasTips = computed<boolean>(() =>
|
||||
Boolean(props.topTip || props.bottomTip),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Overlay
|
||||
:visible="visible"
|
||||
:clickable="false"
|
||||
>
|
||||
<div
|
||||
class="w-[1795px] h-[987px] bg-no-repeat bg-cover p-10 pt-[100px]"
|
||||
:style="{ backgroundImage: getBackgroundImage(backgroundImage) }"
|
||||
>
|
||||
<ImgButton
|
||||
class="absolute -top-8 -right-14 z-10"
|
||||
:image="IMAGE_PATHS.COMPONENTS.CLOSE"
|
||||
@click="emit('close')"
|
||||
/>
|
||||
|
||||
<div class="relative h-full overflow-y-scroll px-8">
|
||||
<div class="pl-10 space-y-10 mt-10">
|
||||
<p
|
||||
v-if="topTip"
|
||||
class="glow-text"
|
||||
>
|
||||
{{ topTip }}
|
||||
</p>
|
||||
<div
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
class="relative w-full h-[182px] bg-no-repeat bg-cover px-12 py-3 flex justify-between"
|
||||
:style="{ backgroundImage: getBackgroundImage(itemBackground) }"
|
||||
>
|
||||
<div class="pr-8 space-y-10">
|
||||
<h4 class="text-3xl font-bold text-white mb-3">
|
||||
{{ `0${index + 1}` }} {{ item.title }}
|
||||
</h4>
|
||||
<p class="text-3xl text-gray-800 font-bold leading-relaxed">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
</div>
|
||||
<StarRating
|
||||
class="absolute -bottom-5 right-10"
|
||||
:score="item.score"
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
v-if="bottomTip"
|
||||
class="glow-text"
|
||||
>
|
||||
{{ bottomTip }}
|
||||
</p>
|
||||
<div
|
||||
class="w-full flex justify-center"
|
||||
:class="{ 'mt-16': !hasTips }"
|
||||
>
|
||||
<ImgButton
|
||||
:image="saveButtonImage"
|
||||
@click="emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Overlay>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.glow-text {
|
||||
color: white;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
text-shadow:
|
||||
-1px -1px 0 #0066ff,
|
||||
1px -1px 0 #0066ff,
|
||||
-1px 1px 0 #0066ff,
|
||||
1px 1px 0 #0066ff,
|
||||
-2px -2px 0 #0066ff,
|
||||
2px -2px 0 #0066ff,
|
||||
-2px 2px 0 #0066ff,
|
||||
2px 2px 0 #0066ff,
|
||||
0 0 10px #0066ff,
|
||||
0 0 20px #0066ff,
|
||||
0 0 30px #0066ff,
|
||||
0 0 40px #0066ff,
|
||||
0 0 50px #0066ff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface PracticeRatingItem {
|
||||
title: string
|
||||
description: string
|
||||
score: number
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { ComponentOptionsMixin, DefineComponent, PublicProps, SlotsType } from 'vue'
|
||||
import type {
|
||||
ComponentOptionsMixin,
|
||||
DefineComponent,
|
||||
PublicProps,
|
||||
SlotsType,
|
||||
} from 'vue'
|
||||
import VideoSfc from './Video.vue'
|
||||
|
||||
type TimeUnit = number
|
||||
@@ -34,6 +39,7 @@ interface VideoProps {
|
||||
playbackRate?: number
|
||||
fit?: 'contain' | 'cover' | 'fill'
|
||||
anchors?: VideoAnchor[]
|
||||
onEnded?: () => void
|
||||
}
|
||||
|
||||
interface VideoSlotProps {
|
||||
@@ -53,7 +59,11 @@ interface VideoSlotProps {
|
||||
|
||||
type VideoSlots = SlotsType<{
|
||||
default?: () => any
|
||||
anchor?: (props: { anchor: VideoAnchor; resume: () => void; unmute: () => void }) => any
|
||||
anchor?: (props: {
|
||||
anchor: VideoAnchor
|
||||
resume: () => void
|
||||
unmute: () => void
|
||||
}) => any
|
||||
overlay?: (props: VideoSlotProps) => any
|
||||
controls?: (props: VideoSlotProps) => any
|
||||
'controls-left'?: (props: VideoSlotProps) => any
|
||||
|
||||
@@ -8,26 +8,46 @@ export type StepIndex = 1 | 2 | 3
|
||||
* - 用于“停留≥阈值后自动计为已查看(viewed)”以及计算 completedStep。
|
||||
* - 按需求:菜单中 disabled=true 的子项不计入完成条件,因此这里不包含 disabled 子项。
|
||||
*/
|
||||
export const moduleStepMap: Record<ModuleName, Record<StepIndex, readonly string[]>> = {
|
||||
export const moduleStepMap: Record<
|
||||
ModuleName,
|
||||
Record<StepIndex, readonly string[]>
|
||||
> = {
|
||||
film: {
|
||||
1: ['FilmResource', 'FilmCreative', 'FilmScriptWriting', 'FilmStoryboard'],
|
||||
2: ['FilmMidtermEquipment', 'FilmMidtermShooting', 'FilmOnsiteShooting'],
|
||||
3: ['FilmEditing', 'FilmColorPractice', 'FilmSoundEffect', 'FilmExportPublish']
|
||||
3: [
|
||||
'FilmEditing',
|
||||
'FilmColorPractice',
|
||||
'FilmSoundEffect',
|
||||
'FilmExportPublish',
|
||||
],
|
||||
},
|
||||
vlog: {
|
||||
1: ['VlogResource', 'VlogCreative', 'VlogScriptWriting', 'VlogStoryboard'],
|
||||
2: ['VlogMidtermEquipment', 'VlogMidtermShooting', 'VlogShooting'],
|
||||
3: ['VlogEditing', 'VlogColor', 'VlogExportPublish']
|
||||
3: ['VlogEditing', 'VlogColor', 'VlogExportPublish'],
|
||||
},
|
||||
interview: {
|
||||
1: ['InterviewResource', 'InterviewCreative', 'InterviewScriptWriting', 'InterviewStoryboard'],
|
||||
1: [
|
||||
'InterviewResource',
|
||||
'InterviewCreative',
|
||||
'InterviewScriptWriting',
|
||||
'InterviewStoryboard',
|
||||
],
|
||||
2: ['InterviewEquipment', 'InterviewShooting', 'InterviewOnsiteShooting'],
|
||||
3: ['InterviewEditing', 'InterviewColorPractice', 'InterviewAudio', 'InterviewExportPublish']
|
||||
}
|
||||
3: [
|
||||
'InterviewEditing',
|
||||
'InterviewColor',
|
||||
'InterviewAudio',
|
||||
'InterviewExportPublish',
|
||||
],
|
||||
},
|
||||
} as const
|
||||
|
||||
export const allTrackedRouteNames = new Set<string>(
|
||||
Object.values(moduleStepMap).flatMap(stepMap => Object.values(stepMap).flatMap(names => names))
|
||||
Object.values(moduleStepMap).flatMap((stepMap) =>
|
||||
Object.values(stepMap).flatMap((names) => names),
|
||||
),
|
||||
)
|
||||
|
||||
export function getModuleNameByRouteName(routeName: string): ModuleName | null {
|
||||
|
||||
@@ -8,8 +8,8 @@ const Interview: RouteRecordRaw[] = [
|
||||
name: 'Interview',
|
||||
component: () => import('@/views/interview/index.vue'),
|
||||
meta: {
|
||||
title: '发现与记录'
|
||||
}
|
||||
title: '发现与记录',
|
||||
},
|
||||
},
|
||||
// interview 发现与记录
|
||||
{
|
||||
@@ -17,16 +17,16 @@ const Interview: RouteRecordRaw[] = [
|
||||
name: 'InterviewWork',
|
||||
component: () => import('@/views/interview/work/index.vue'),
|
||||
meta: {
|
||||
title: '发现与记录'
|
||||
}
|
||||
title: '发现与记录',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/interview/video',
|
||||
name: 'InterviewVideo',
|
||||
component: () => import('@/views/interview/video/index.vue'),
|
||||
meta: {
|
||||
title: '发现与记录-视频'
|
||||
}
|
||||
title: '发现与记录-视频',
|
||||
},
|
||||
},
|
||||
//前期准备
|
||||
{
|
||||
@@ -34,75 +34,82 @@ const Interview: RouteRecordRaw[] = [
|
||||
name: 'InterviewResource',
|
||||
component: () => import('@/views/interview/resource/index.vue'),
|
||||
meta: {
|
||||
title: '资源筹备'
|
||||
}
|
||||
title: '资源筹备',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/interview/creative',
|
||||
name: 'InterviewCreative',
|
||||
component: () => import('@/views/interview/creative/index.vue'),
|
||||
meta: {
|
||||
title: '创意构想'
|
||||
}
|
||||
title: '创意构想',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/interview/script-writing',
|
||||
name: 'InterviewScriptWriting',
|
||||
component: () => import('@/views/interview/script-writing/index.vue'),
|
||||
meta: {
|
||||
title: '脚本撰写'
|
||||
}
|
||||
title: '脚本撰写',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/interview/storyboard',
|
||||
name: 'InterviewStoryboard',
|
||||
component: () => import('@/views/interview/storyboard/index.vue'),
|
||||
meta: {
|
||||
title: '分镜制作'
|
||||
}
|
||||
title: '分镜制作',
|
||||
},
|
||||
},
|
||||
//中期拍摄
|
||||
{
|
||||
path: '/interview/equipment',
|
||||
name: 'InterviewEquipment',
|
||||
component: () => import('@/views/interview/midterm-shooting/equipment/index.vue'),
|
||||
component: () =>
|
||||
import('@/views/interview/midterm-shooting/equipment/index.vue'),
|
||||
meta: {
|
||||
title: '设备准备'
|
||||
}
|
||||
title: '设备准备',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/interview/shooting',
|
||||
name: 'InterviewShooting',
|
||||
component: () => import('@/views/interview/midterm-shooting/shooting/index.vue'),
|
||||
component: () =>
|
||||
import('@/views/interview/midterm-shooting/shooting/index.vue'),
|
||||
meta: {
|
||||
title: '实地取景'
|
||||
}
|
||||
title: '实地取景',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/interview/on-site-shooting',
|
||||
name: 'InterviewOnsiteShooting',
|
||||
component: () => import('@/views/interview/midterm-shooting/on-site/index.vue'),
|
||||
component: () =>
|
||||
import('@/views/interview/midterm-shooting/on-site/index.vue'),
|
||||
meta: {
|
||||
title: '现场拍摄'
|
||||
}
|
||||
title: '现场拍摄',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/interview/filmingLocation-a',
|
||||
name: 'InterviewFilmingLocationA',
|
||||
component: () =>
|
||||
import('@/views/interview/midterm-shooting/shooting/filmingLocation-a/index.vue'),
|
||||
import(
|
||||
'@/views/interview/midterm-shooting/shooting/filmingLocation-a/index.vue'
|
||||
),
|
||||
meta: {
|
||||
title: '实地取景-取景地A'
|
||||
}
|
||||
title: '实地取景-取景地A',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/interview/filmingLocation-b',
|
||||
name: 'InterviewFilmingLocationB',
|
||||
component: () =>
|
||||
import('@/views/interview/midterm-shooting/shooting/filmingLocation-b/index.vue'),
|
||||
import(
|
||||
'@/views/interview/midterm-shooting/shooting/filmingLocation-b/index.vue'
|
||||
),
|
||||
meta: {
|
||||
title: '实地取景-取景地B'
|
||||
}
|
||||
title: '实地取景-取景地B',
|
||||
},
|
||||
},
|
||||
// 后期剪辑
|
||||
{
|
||||
@@ -110,40 +117,42 @@ const Interview: RouteRecordRaw[] = [
|
||||
name: 'InterviewEditing',
|
||||
component: () => import('@/views/interview/after-editing/editing.vue'),
|
||||
meta: {
|
||||
title: '剪辑'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/interview/color-practice',
|
||||
name: 'InterviewColorPractice',
|
||||
component: () => import('@/views/interview/after-editing/color-practice.vue'),
|
||||
meta: {
|
||||
title: '后期剪辑-调色实践'
|
||||
}
|
||||
title: '剪辑',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/interview/color',
|
||||
name: 'InterviewColor',
|
||||
component: () => import('@/views/interview/after-editing/color.vue'),
|
||||
meta: {
|
||||
title: '调色'
|
||||
}
|
||||
title: '调色',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/interview/color-practice',
|
||||
name: 'InterviewColorPractice',
|
||||
component: () =>
|
||||
import('@/views/interview/after-editing/color-practice.vue'),
|
||||
meta: {
|
||||
title: '后期剪辑-调色实践',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/interview/audio',
|
||||
name: 'InterviewAudio',
|
||||
component: () => import('@/views/interview/after-editing/audio/index.vue'),
|
||||
meta: {
|
||||
title: '音频'
|
||||
}
|
||||
title: '音频',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/interview/export-publish',
|
||||
name: 'InterviewExportPublish',
|
||||
component: () => import('@/views/interview/after-editing/export-publish/index.vue'),
|
||||
component: () =>
|
||||
import('@/views/interview/after-editing/export-publish/index.vue'),
|
||||
meta: {
|
||||
title: '导出发布'
|
||||
}
|
||||
title: '导出发布',
|
||||
},
|
||||
},
|
||||
// 实战操作台
|
||||
{
|
||||
@@ -151,8 +160,8 @@ const Interview: RouteRecordRaw[] = [
|
||||
name: 'InterviewPractice',
|
||||
component: () => import('@/views/interview/practice/index.vue'),
|
||||
meta: {
|
||||
title: '实战操作台'
|
||||
}
|
||||
title: '实战操作台',
|
||||
},
|
||||
},
|
||||
// 我的创作笔记
|
||||
{
|
||||
@@ -160,8 +169,18 @@ const Interview: RouteRecordRaw[] = [
|
||||
name: 'InterviewMyNotes',
|
||||
component: () => import('@/views/interview/practice/my-notes/index.vue'),
|
||||
meta: {
|
||||
title: '我的创作笔记'
|
||||
}
|
||||
title: '我的创作笔记',
|
||||
},
|
||||
},
|
||||
// 教师点评
|
||||
{
|
||||
path: '/interview/practice/teacher-comment',
|
||||
name: 'InterviewTeacherComment',
|
||||
component: () =>
|
||||
import('@/views/interview/practice/teacher-comment/index.vue'),
|
||||
meta: {
|
||||
title: '教师点评',
|
||||
},
|
||||
},
|
||||
// 他人实战作品
|
||||
{
|
||||
@@ -170,9 +189,9 @@ const Interview: RouteRecordRaw[] = [
|
||||
props: true,
|
||||
component: () => import('@/views/interview/practice/video/index.vue'),
|
||||
meta: {
|
||||
title: '他人实战作品'
|
||||
}
|
||||
}
|
||||
title: '他人实战作品',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default Interview
|
||||
@@ -8,8 +8,8 @@ const Vlog: RouteRecordRaw[] = [
|
||||
name: 'Vlog',
|
||||
component: () => import('@/views/vlog/index.vue'),
|
||||
meta: {
|
||||
title: '发现与记录'
|
||||
}
|
||||
title: '发现与记录',
|
||||
},
|
||||
},
|
||||
// vlog 任务取景框
|
||||
{
|
||||
@@ -17,16 +17,16 @@ const Vlog: RouteRecordRaw[] = [
|
||||
name: 'VlogWork',
|
||||
component: () => import('@/views/vlog/work/index.vue'),
|
||||
meta: {
|
||||
title: '发现与记录'
|
||||
}
|
||||
title: '发现与记录',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/vlog/video',
|
||||
name: 'VlogVideo',
|
||||
component: () => import('@/views/vlog/video/index.vue'),
|
||||
meta: {
|
||||
title: '发现与记录-视频'
|
||||
}
|
||||
title: '发现与记录-视频',
|
||||
},
|
||||
},
|
||||
//前期准备
|
||||
{
|
||||
@@ -34,57 +34,58 @@ const Vlog: RouteRecordRaw[] = [
|
||||
name: 'VlogResource',
|
||||
component: () => import('@/views/vlog/resource/index.vue'),
|
||||
meta: {
|
||||
title: '资源筹备'
|
||||
}
|
||||
title: '资源筹备',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/vlog/creative',
|
||||
name: 'VlogCreative',
|
||||
component: () => import('@/views/vlog/creative/index.vue'),
|
||||
meta: {
|
||||
title: '创意构想'
|
||||
}
|
||||
title: '创意构想',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/vlog/script-writing',
|
||||
name: 'VlogScriptWriting',
|
||||
component: () => import('@/views/vlog/script-writing/index.vue'),
|
||||
meta: {
|
||||
title: '脚本撰写'
|
||||
}
|
||||
title: '脚本撰写',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/vlog/storyboard',
|
||||
name: 'VlogStoryboard',
|
||||
component: () => import('@/views/vlog/storyboard/index.vue'),
|
||||
meta: {
|
||||
title: '分镜制作'
|
||||
}
|
||||
title: '分镜制作',
|
||||
},
|
||||
},
|
||||
//中期拍摄
|
||||
{
|
||||
path: '/vlog/equipment',
|
||||
name: 'VlogMidtermEquipment',
|
||||
component: () => import('@/views/vlog/midterm-shooting/equipment/index.vue'),
|
||||
component: () =>
|
||||
import('@/views/vlog/midterm-shooting/equipment/index.vue'),
|
||||
meta: {
|
||||
title: '设备准备'
|
||||
}
|
||||
title: '设备准备',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/vlog/shooting',
|
||||
name: 'VlogMidtermShooting',
|
||||
component: () => import('@/views/vlog/midterm-shooting/shooting/index.vue'),
|
||||
meta: {
|
||||
title: '实地取景'
|
||||
}
|
||||
title: '实地取景',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/vlog/on-site-shooting',
|
||||
name: 'VlogShooting',
|
||||
component: () => import('@/views/vlog/midterm-shooting/on-site/index.vue'),
|
||||
meta: {
|
||||
title: '现场拍摄'
|
||||
}
|
||||
title: '现场拍摄',
|
||||
},
|
||||
},
|
||||
// {
|
||||
// path: '/vlog/on-site-shooting/time-lapse',
|
||||
@@ -137,18 +138,24 @@ const Vlog: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/vlog/filmingLocation-a',
|
||||
name: 'VlogFilmingLocationA',
|
||||
component: () => import('@/views/vlog/midterm-shooting/shooting/filmingLocation-a/index.vue'),
|
||||
component: () =>
|
||||
import(
|
||||
'@/views/vlog/midterm-shooting/shooting/filmingLocation-a/index.vue'
|
||||
),
|
||||
meta: {
|
||||
title: '实地取景-取景地A'
|
||||
}
|
||||
title: '实地取景-取景地A',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/vlog/filmingLocation-b',
|
||||
name: 'VlogFilmingLocationB',
|
||||
component: () => import('@/views/vlog/midterm-shooting/shooting/filmingLocation-b/index.vue'),
|
||||
component: () =>
|
||||
import(
|
||||
'@/views/vlog/midterm-shooting/shooting/filmingLocation-b/index.vue'
|
||||
),
|
||||
meta: {
|
||||
title: '实地取景-取景地B'
|
||||
}
|
||||
title: '实地取景-取景地B',
|
||||
},
|
||||
},
|
||||
// 后期剪辑
|
||||
{
|
||||
@@ -156,32 +163,33 @@ const Vlog: RouteRecordRaw[] = [
|
||||
name: 'VlogEditing',
|
||||
component: () => import('@/views/vlog/after-editing/editing.vue'),
|
||||
meta: {
|
||||
title: '后期剪辑-剪辑'
|
||||
}
|
||||
title: '后期剪辑-剪辑',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/vlog/color',
|
||||
name: 'VlogColor',
|
||||
component: () => import('@/views/vlog/after-editing/color.vue'),
|
||||
meta: {
|
||||
title: '后期剪辑-调色'
|
||||
}
|
||||
title: '后期剪辑-调色',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/vlog/color-practice',
|
||||
name: 'VlogColorPractice',
|
||||
component: () => import('@/views/vlog/after-editing/color-practice.vue'),
|
||||
meta: {
|
||||
title: '后期剪辑-调色实践'
|
||||
}
|
||||
title: '后期剪辑-调色实践',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/vlog/export-publish',
|
||||
name: 'VlogExportPublish',
|
||||
component: () => import('@/views/vlog/after-editing/export-publish/index.vue'),
|
||||
component: () =>
|
||||
import('@/views/vlog/after-editing/export-publish/index.vue'),
|
||||
meta: {
|
||||
title: '后期剪辑-导出发布'
|
||||
}
|
||||
title: '后期剪辑-导出发布',
|
||||
},
|
||||
},
|
||||
// 实战操作台
|
||||
{
|
||||
@@ -189,8 +197,8 @@ const Vlog: RouteRecordRaw[] = [
|
||||
name: 'VlogPractice',
|
||||
component: () => import('@/views/vlog/practice/index.vue'),
|
||||
meta: {
|
||||
title: '实战操作台'
|
||||
}
|
||||
title: '实战操作台',
|
||||
},
|
||||
},
|
||||
// 我的创作笔记
|
||||
{
|
||||
@@ -198,8 +206,17 @@ const Vlog: RouteRecordRaw[] = [
|
||||
name: 'VlogMyNotes',
|
||||
component: () => import('@/views/vlog/practice/my-notes/index.vue'),
|
||||
meta: {
|
||||
title: '我的创作笔记'
|
||||
}
|
||||
title: '我的创作笔记',
|
||||
},
|
||||
},
|
||||
// 教师点评
|
||||
{
|
||||
path: '/vlog/practice/teacher-comment',
|
||||
name: 'VlogTeacherComment',
|
||||
component: () => import('@/views/vlog/practice/teacher-comment/index.vue'),
|
||||
meta: {
|
||||
title: '教师点评',
|
||||
},
|
||||
},
|
||||
// 他人实战作品
|
||||
{
|
||||
@@ -208,9 +225,9 @@ const Vlog: RouteRecordRaw[] = [
|
||||
props: true,
|
||||
component: () => import('@/views/vlog/practice/video/index.vue'),
|
||||
meta: {
|
||||
title: '他人实战作品'
|
||||
}
|
||||
}
|
||||
title: '他人实战作品',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default Vlog
|
||||
@@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { moduleStepMap } from '@/config/moduleStepMap'
|
||||
|
||||
export const FILM_LESSON_ID = 'film_rwqjk'
|
||||
export const FILM_LESSON_ID = 'dianying_rwqjk'
|
||||
export const FILM_KEY = 4
|
||||
|
||||
interface FilmSavePayload {
|
||||
@@ -17,6 +17,7 @@ interface FilmSavePayload {
|
||||
editingVideosWatched?: Record<string, boolean>
|
||||
isGuideFilmEditingComplete?: boolean
|
||||
guideShown?: Record<string, boolean>
|
||||
practiceScrollPage?: number
|
||||
}
|
||||
|
||||
export const FILM_EDITING_VIDEO_KEYS = [
|
||||
@@ -25,7 +26,7 @@ export const FILM_EDITING_VIDEO_KEYS = [
|
||||
'soundTransition',
|
||||
'cut',
|
||||
'association',
|
||||
'continuity'
|
||||
'continuity',
|
||||
] as const
|
||||
|
||||
async function getYibanApi(): Promise<typeof import('@/utils/yiban') | null> {
|
||||
@@ -70,6 +71,7 @@ export const useFilmStore = defineStore(
|
||||
//Film-后期剪辑6个视频学完后的一次性引导
|
||||
const isGuideFilmEditingComplete = ref<boolean>(true)
|
||||
const guideShown = ref<Record<string, boolean>>({})
|
||||
const practiceScrollPage = ref<number>(0)
|
||||
|
||||
//Actions
|
||||
function saveFilmStore(): void {
|
||||
@@ -84,12 +86,15 @@ export const useFilmStore = defineStore(
|
||||
filmMenuVisible: filmMenuVisible.value,
|
||||
editingVideosWatched: editingVideosWatched.value,
|
||||
isGuideFilmEditingComplete: isGuideFilmEditingComplete.value,
|
||||
guideShown: guideShown.value
|
||||
guideShown: guideShown.value,
|
||||
practiceScrollPage: practiceScrollPage.value,
|
||||
}
|
||||
|
||||
getYibanApi()
|
||||
.then(api => api?.setUserDefine(FILM_LESSON_ID, FILM_KEY, JSON.stringify(payload)))
|
||||
.catch(error => console.error('Failed to save Film store', error))
|
||||
.then((api) =>
|
||||
api?.setUserDefine(FILM_LESSON_ID, FILM_KEY, JSON.stringify(payload)),
|
||||
)
|
||||
.catch((error) => console.error('Failed to save Film store', error))
|
||||
}
|
||||
|
||||
async function loadFilmStore(): Promise<void> {
|
||||
@@ -110,8 +115,10 @@ export const useFilmStore = defineStore(
|
||||
FilmUnlockProgress.value = data.FilmUnlockProgress ?? 0
|
||||
filmMenuVisible.value = data.filmMenuVisible ?? false
|
||||
editingVideosWatched.value = data.editingVideosWatched ?? {}
|
||||
isGuideFilmEditingComplete.value = data.isGuideFilmEditingComplete ?? true
|
||||
isGuideFilmEditingComplete.value =
|
||||
data.isGuideFilmEditingComplete ?? true
|
||||
guideShown.value = data.guideShown ?? {}
|
||||
practiceScrollPage.value = data.practiceScrollPage ?? 0
|
||||
} catch (error) {
|
||||
console.error('Failed to load Film store', error)
|
||||
}
|
||||
@@ -122,7 +129,7 @@ export const useFilmStore = defineStore(
|
||||
viewed.value[key] = true
|
||||
FilmUnlockProgress.value = Math.max(
|
||||
FilmUnlockProgress.value,
|
||||
Math.min(completedStep.value + 1, 4)
|
||||
Math.min(completedStep.value + 1, 4),
|
||||
)
|
||||
saveFilmStore()
|
||||
}
|
||||
@@ -181,8 +188,15 @@ export const useFilmStore = defineStore(
|
||||
saveFilmStore()
|
||||
}
|
||||
|
||||
function setPracticeScrollPage(value: number): void {
|
||||
practiceScrollPage.value = value
|
||||
saveFilmStore()
|
||||
}
|
||||
|
||||
const allEditingVideosWatched = computed<boolean>(() =>
|
||||
FILM_EDITING_VIDEO_KEYS.every(key => editingVideosWatched.value[key] === true)
|
||||
FILM_EDITING_VIDEO_KEYS.every(
|
||||
(key) => editingVideosWatched.value[key] === true,
|
||||
),
|
||||
)
|
||||
|
||||
// 基于“子页面是否已查看”的大步骤完成度(1..3)
|
||||
@@ -191,7 +205,7 @@ export const useFilmStore = defineStore(
|
||||
|
||||
const completedStep = computed<number>(() => {
|
||||
const isDone = (step: 1 | 2 | 3) =>
|
||||
stepRouteNames[step].every(name => viewed.value[name] === true)
|
||||
stepRouteNames[step].every((name) => viewed.value[name] === true)
|
||||
|
||||
if (isDone(1) && isDone(2) && isDone(3)) return 3
|
||||
if (isDone(1) && isDone(2)) return 2
|
||||
@@ -203,7 +217,7 @@ export const useFilmStore = defineStore(
|
||||
const progressPercent = computed<number>(() => {
|
||||
const allRoutes = (Object.values(stepRouteNames) as string[][]).flat()
|
||||
if (allRoutes.length === 0) return 0
|
||||
const viewedCount = allRoutes.filter(name => viewed.value[name]).length
|
||||
const viewedCount = allRoutes.filter((name) => viewed.value[name]).length
|
||||
return Math.round((viewedCount / allRoutes.length) * 100)
|
||||
})
|
||||
|
||||
@@ -220,6 +234,7 @@ export const useFilmStore = defineStore(
|
||||
editingVideosWatched.value = {}
|
||||
isGuideFilmEditingComplete.value = true
|
||||
guideShown.value = {}
|
||||
practiceScrollPage.value = 0
|
||||
saveFilmStore()
|
||||
}
|
||||
|
||||
@@ -236,6 +251,7 @@ export const useFilmStore = defineStore(
|
||||
markEditingVideoWatched,
|
||||
setIsGuideFilmEditingComplete,
|
||||
markGuideShown,
|
||||
setPracticeScrollPage,
|
||||
saveFilmStore,
|
||||
loadFilmStore,
|
||||
resetFilmState,
|
||||
@@ -253,10 +269,11 @@ export const useFilmStore = defineStore(
|
||||
editingVideosWatched,
|
||||
isGuideFilmEditingComplete,
|
||||
guideShown,
|
||||
allEditingVideosWatched
|
||||
practiceScrollPage,
|
||||
allEditingVideosWatched,
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: true
|
||||
}
|
||||
persist: true,
|
||||
},
|
||||
)
|
||||
@@ -15,6 +15,7 @@ interface InterviewSavePayload {
|
||||
InterviewUnlockProgress?: number
|
||||
interviewMenuVisible?: boolean
|
||||
guideShown?: Record<string, boolean>
|
||||
practiceScrollPage?: number
|
||||
}
|
||||
|
||||
async function getYibanApi(): Promise<typeof import('@/utils/yiban') | null> {
|
||||
@@ -55,6 +56,7 @@ export const useInterviewStore = defineStore(
|
||||
//Interview-菜单 Overlay 是否可见
|
||||
const interviewMenuVisible = ref<boolean>(false)
|
||||
const guideShown = ref<Record<string, boolean>>({})
|
||||
const practiceScrollPage = ref<number>(0)
|
||||
|
||||
//Actions
|
||||
function saveInterviewStore(): void {
|
||||
@@ -67,14 +69,21 @@ export const useInterviewStore = defineStore(
|
||||
isInterviewUnlock: isInterviewUnlock.value,
|
||||
InterviewUnlockProgress: InterviewUnlockProgress.value,
|
||||
interviewMenuVisible: interviewMenuVisible.value,
|
||||
guideShown: guideShown.value
|
||||
guideShown: guideShown.value,
|
||||
practiceScrollPage: practiceScrollPage.value,
|
||||
}
|
||||
|
||||
getYibanApi()
|
||||
.then(api =>
|
||||
api?.setUserDefine(INTERVIEW_LESSON_ID, INTERVIEW_KEY, JSON.stringify(payload))
|
||||
.then((api) =>
|
||||
api?.setUserDefine(
|
||||
INTERVIEW_LESSON_ID,
|
||||
INTERVIEW_KEY,
|
||||
JSON.stringify(payload),
|
||||
),
|
||||
)
|
||||
.catch((error) =>
|
||||
console.error('Failed to save Interview store', error),
|
||||
)
|
||||
.catch(error => console.error('Failed to save Interview store', error))
|
||||
}
|
||||
|
||||
async function loadInterviewStore(): Promise<void> {
|
||||
@@ -95,6 +104,7 @@ export const useInterviewStore = defineStore(
|
||||
InterviewUnlockProgress.value = data.InterviewUnlockProgress ?? 0
|
||||
interviewMenuVisible.value = data.interviewMenuVisible ?? false
|
||||
guideShown.value = data.guideShown ?? {}
|
||||
practiceScrollPage.value = data.practiceScrollPage ?? 0
|
||||
} catch (error) {
|
||||
console.error('Failed to load Interview store', error)
|
||||
}
|
||||
@@ -105,7 +115,7 @@ export const useInterviewStore = defineStore(
|
||||
viewed.value[key] = true
|
||||
InterviewUnlockProgress.value = Math.max(
|
||||
InterviewUnlockProgress.value,
|
||||
Math.min(completedStep.value + 1, 4)
|
||||
Math.min(completedStep.value + 1, 4),
|
||||
)
|
||||
saveInterviewStore()
|
||||
}
|
||||
@@ -116,7 +126,7 @@ export const useInterviewStore = defineStore(
|
||||
|
||||
const completedStep = computed<number>(() => {
|
||||
const isDone = (step: 1 | 2 | 3) =>
|
||||
stepRouteNames[step].every(name => viewed.value[name] === true)
|
||||
stepRouteNames[step].every((name) => viewed.value[name] === true)
|
||||
|
||||
if (isDone(1) && isDone(2) && isDone(3)) return 3
|
||||
if (isDone(1) && isDone(2)) return 2
|
||||
@@ -127,7 +137,7 @@ export const useInterviewStore = defineStore(
|
||||
const progressPercent = computed<number>(() => {
|
||||
const allRoutes = (Object.values(stepRouteNames) as string[][]).flat()
|
||||
if (allRoutes.length === 0) return 0
|
||||
const viewedCount = allRoutes.filter(name => viewed.value[name]).length
|
||||
const viewedCount = allRoutes.filter((name) => viewed.value[name]).length
|
||||
return Math.round((viewedCount / allRoutes.length) * 100)
|
||||
})
|
||||
|
||||
@@ -173,6 +183,11 @@ export const useInterviewStore = defineStore(
|
||||
saveInterviewStore()
|
||||
}
|
||||
|
||||
function setPracticeScrollPage(value: number): void {
|
||||
practiceScrollPage.value = value
|
||||
saveInterviewStore()
|
||||
}
|
||||
|
||||
function resetInterviewState(): void {
|
||||
viewed.value = {}
|
||||
isGuideInterviewTask.value = true
|
||||
@@ -183,6 +198,7 @@ export const useInterviewStore = defineStore(
|
||||
InterviewUnlockProgress.value = 0
|
||||
interviewMenuVisible.value = false
|
||||
guideShown.value = {}
|
||||
practiceScrollPage.value = 0
|
||||
saveInterviewStore()
|
||||
}
|
||||
|
||||
@@ -197,6 +213,7 @@ export const useInterviewStore = defineStore(
|
||||
setInterviewUnlockProgress,
|
||||
setInterviewMenuVisible,
|
||||
markGuideShown,
|
||||
setPracticeScrollPage,
|
||||
resetInterviewState,
|
||||
loadInterviewStore,
|
||||
saveInterviewStore,
|
||||
@@ -211,10 +228,11 @@ export const useInterviewStore = defineStore(
|
||||
isInterviewUnlock,
|
||||
InterviewUnlockProgress,
|
||||
interviewMenuVisible,
|
||||
guideShown
|
||||
guideShown,
|
||||
practiceScrollPage,
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: true
|
||||
}
|
||||
persist: true,
|
||||
},
|
||||
)
|
||||
@@ -16,6 +16,7 @@ interface VlogSavePayload {
|
||||
vlogUnlockProgress?: number
|
||||
vlogMenuVisible?: boolean
|
||||
guideShown?: Record<string, boolean>
|
||||
practiceScrollPage?: number
|
||||
}
|
||||
|
||||
async function getYibanApi(): Promise<typeof import('@/utils/yiban') | null> {
|
||||
@@ -60,6 +61,7 @@ export const useVlogStore = defineStore(
|
||||
//vlog-发现与记录解锁进度
|
||||
const vlogUnlockProgress = ref<number>(0)
|
||||
const guideShown = ref<Record<string, boolean>>({})
|
||||
const practiceScrollPage = ref<number>(0)
|
||||
|
||||
//Actions
|
||||
function saveVlogStore(): void {
|
||||
@@ -73,12 +75,15 @@ export const useVlogStore = defineStore(
|
||||
isVlogUnlock: isVlogUnlock.value,
|
||||
vlogUnlockProgress: vlogUnlockProgress.value,
|
||||
vlogMenuVisible: vlogMenuVisible.value,
|
||||
guideShown: guideShown.value
|
||||
guideShown: guideShown.value,
|
||||
practiceScrollPage: practiceScrollPage.value,
|
||||
}
|
||||
|
||||
getYibanApi()
|
||||
.then(api => api?.setUserDefine(VLOG_LESSON_ID, VLOG_KEY, JSON.stringify(payload)))
|
||||
.catch(error => console.error('Failed to save Vlog store', error))
|
||||
.then((api) =>
|
||||
api?.setUserDefine(VLOG_LESSON_ID, VLOG_KEY, JSON.stringify(payload)),
|
||||
)
|
||||
.catch((error) => console.error('Failed to save Vlog store', error))
|
||||
}
|
||||
|
||||
async function loadVlogStore(): Promise<void> {
|
||||
@@ -100,6 +105,7 @@ export const useVlogStore = defineStore(
|
||||
vlogUnlockProgress.value = data.vlogUnlockProgress ?? 0
|
||||
vlogMenuVisible.value = data.vlogMenuVisible ?? false
|
||||
guideShown.value = data.guideShown ?? {}
|
||||
practiceScrollPage.value = data.practiceScrollPage ?? 0
|
||||
} catch (error) {
|
||||
console.error('Failed to load Vlog store', error)
|
||||
}
|
||||
@@ -110,7 +116,7 @@ export const useVlogStore = defineStore(
|
||||
viewed.value[key] = true
|
||||
vlogUnlockProgress.value = Math.max(
|
||||
vlogUnlockProgress.value,
|
||||
Math.min(completedStep.value + 1, 4)
|
||||
Math.min(completedStep.value + 1, 4),
|
||||
)
|
||||
saveVlogStore()
|
||||
}
|
||||
@@ -162,13 +168,18 @@ export const useVlogStore = defineStore(
|
||||
saveVlogStore()
|
||||
}
|
||||
|
||||
function setPracticeScrollPage(value: number): void {
|
||||
practiceScrollPage.value = value
|
||||
saveVlogStore()
|
||||
}
|
||||
|
||||
// 基于“子页面是否已查看”的大步骤完成度(1..3)
|
||||
// disabled 子项不计入(在这里直接不放进列表)
|
||||
const stepRouteNames = moduleStepMap.vlog
|
||||
|
||||
const completedStep = computed<number>(() => {
|
||||
const isDone = (step: 1 | 2 | 3) =>
|
||||
stepRouteNames[step].every(name => viewed.value[name] === true)
|
||||
stepRouteNames[step].every((name) => viewed.value[name] === true)
|
||||
|
||||
if (isDone(1) && isDone(2) && isDone(3)) return 3
|
||||
if (isDone(1) && isDone(2)) return 2
|
||||
@@ -180,7 +191,7 @@ export const useVlogStore = defineStore(
|
||||
const progressPercent = computed<number>(() => {
|
||||
const allRoutes = (Object.values(stepRouteNames) as string[][]).flat()
|
||||
if (allRoutes.length === 0) return 0
|
||||
const viewedCount = allRoutes.filter(name => viewed.value[name]).length
|
||||
const viewedCount = allRoutes.filter((name) => viewed.value[name]).length
|
||||
return Math.round((viewedCount / allRoutes.length) * 100)
|
||||
})
|
||||
|
||||
@@ -195,6 +206,7 @@ export const useVlogStore = defineStore(
|
||||
isVlogUnlock.value = false
|
||||
vlogMenuVisible.value = false
|
||||
guideShown.value = {}
|
||||
practiceScrollPage.value = 0
|
||||
saveVlogStore()
|
||||
}
|
||||
|
||||
@@ -213,6 +225,7 @@ export const useVlogStore = defineStore(
|
||||
resetVlogState,
|
||||
setVlogUnlockProgress,
|
||||
markGuideShown,
|
||||
setPracticeScrollPage,
|
||||
//Actions
|
||||
completedStep,
|
||||
progressPercent,
|
||||
@@ -225,10 +238,11 @@ export const useVlogStore = defineStore(
|
||||
isVlogUnlock,
|
||||
vlogUnlockProgress,
|
||||
vlogMenuVisible,
|
||||
guideShown
|
||||
guideShown,
|
||||
practiceScrollPage,
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: true
|
||||
}
|
||||
persist: true,
|
||||
},
|
||||
)
|
||||
@@ -2,8 +2,12 @@
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import ImgButton from '@/components/Button/ImgButton.vue'
|
||||
import { StepNavigation, GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { Video, QuestionQuiz, type Question } from '@/components'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import StepNavigation from '@/components/Guide/StepNavigation.vue'
|
||||
import { Video } from '@/components'
|
||||
import QuestionQuiz, {
|
||||
type Question,
|
||||
} from '@/components/QuestionQuiz/QuestionQuiz.vue'
|
||||
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { useBasicElementsStore } from '@/stores/modules/basic-elements'
|
||||
|
||||
@@ -27,44 +31,48 @@ const rawSteps = [
|
||||
id: 'step2',
|
||||
label: '摄影基础',
|
||||
content: '手机摄像头介绍',
|
||||
videoUrl: getPublicPath('video/new/shooting-skills/xinshou_shoujishexiangtoujieshao.mp4'),
|
||||
type: 'post' as const
|
||||
videoUrl: getPublicPath(
|
||||
'video/new/shooting-skills/xinshou_shoujishexiangtoujieshao.mp4',
|
||||
),
|
||||
type: 'post' as const,
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
label: '摄影基础',
|
||||
content: '景别概念介绍',
|
||||
videoUrl: getPublicPath('video/new/shooting-skills/xsdh_jbgnjs_batch.mp4'),
|
||||
type: 'post' as const
|
||||
type: 'post' as const,
|
||||
},
|
||||
{
|
||||
id: 'step4',
|
||||
label: '摄影基础',
|
||||
content: '手机慢动作',
|
||||
videoUrl: getPublicPath('video/new/shooting-skills/xsdh_sjmdz_batch.mp4'),
|
||||
type: 'post' as const
|
||||
type: 'post' as const,
|
||||
},
|
||||
{
|
||||
id: 'step5',
|
||||
label: '摄影基础',
|
||||
content: '延时摄影',
|
||||
videoUrl: getPublicPath('video/new/shooting-skills/xsdh_yssy_batch.mp4'),
|
||||
type: 'post' as const
|
||||
type: 'post' as const,
|
||||
},
|
||||
{
|
||||
id: 'step6',
|
||||
label: '摄影基础',
|
||||
content: '神奇的微距摄影',
|
||||
videoUrl: getPublicPath('video/new/shooting-skills/xsdh_sqdwjsy_batch.mp4'),
|
||||
type: 'post' as const
|
||||
type: 'post' as const,
|
||||
},
|
||||
{
|
||||
id: 'step7',
|
||||
label: '摄影基础',
|
||||
content: '独具魅力的夜景摄影',
|
||||
videoUrl: getPublicPath('video/new/shooting-skills/xsdh_djmldyjsy_batch.mp4'),
|
||||
type: 'post' as const
|
||||
}
|
||||
videoUrl: getPublicPath(
|
||||
'video/new/shooting-skills/xsdh_djmldyjsy_batch.mp4',
|
||||
),
|
||||
type: 'post' as const,
|
||||
},
|
||||
]
|
||||
|
||||
const navigationSteps = ref(rawSteps)
|
||||
@@ -77,7 +85,9 @@ const currentNavigationStep = ref(0) // 对应视频索引,从0开始
|
||||
const currentVideoIndex = ref(0)
|
||||
|
||||
//按顺序播放
|
||||
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
|
||||
const currentVideo = computed(
|
||||
() => navigationSteps.value[currentVideoIndex.value].videoUrl,
|
||||
)
|
||||
|
||||
// 题目数据:景别概念介绍(index 1)、手机慢动作(index 2)、独具魅力的夜景摄影(index 5)
|
||||
const questions = ref<Question[]>([])
|
||||
@@ -90,24 +100,25 @@ const getQuestionsForVideo = (index: number): Question[] => {
|
||||
{
|
||||
id: 1,
|
||||
title: '',
|
||||
description: '(单选)如果你想让观众“看清环境、理解空间关系”,更适合用哪种镜头?',
|
||||
description:
|
||||
'(单选)如果你想让观众“看清环境、理解空间关系”,更适合用哪种镜头?',
|
||||
options: [
|
||||
{
|
||||
id: 'a',
|
||||
label: '面部特写视频'
|
||||
label: '面部特写视频',
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
label: '中景人物视频'
|
||||
label: '中景人物视频',
|
||||
},
|
||||
{
|
||||
id: 'c',
|
||||
label: '全景环境视频'
|
||||
}
|
||||
label: '全景环境视频',
|
||||
},
|
||||
],
|
||||
correctOptionId: 'c',
|
||||
explanation: '全景更适合交代故事发生在哪里。'
|
||||
}
|
||||
explanation: '全景更适合交代故事发生在哪里。',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -117,30 +128,32 @@ const getQuestionsForVideo = (index: number): Question[] => {
|
||||
{
|
||||
id: 2,
|
||||
title: '',
|
||||
description: '(多选题)下面这些拍摄场景中,哪些更适合使用"慢动作"来表现?',
|
||||
description:
|
||||
'(多选题)下面这些拍摄场景中,哪些更适合使用"慢动作"来表现?',
|
||||
options: [
|
||||
{
|
||||
id: 'a',
|
||||
label: '城市街道上车水马龙,车辆快速穿行'
|
||||
label: '城市街道上车水马龙,车辆快速穿行',
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
label: '甩头发、转身、跳跃等人物动作瞬间'
|
||||
label: '甩头发、转身、跳跃等人物动作瞬间',
|
||||
},
|
||||
{
|
||||
id: 'c',
|
||||
label: '水花飞溅、雨滴落地、浪花拍岸'
|
||||
label: '水花飞溅、雨滴落地、浪花拍岸',
|
||||
},
|
||||
{
|
||||
id: 'd',
|
||||
label: '风吹动衣摆、窗帘或树叶缓慢摆动'
|
||||
}
|
||||
label: '风吹动衣摆、窗帘或树叶缓慢摆动',
|
||||
},
|
||||
],
|
||||
isMultiple: true,
|
||||
correctOptionIds: ['b', 'c', 'd'],
|
||||
correctOptionId: '', // 多选题时此字段不使用
|
||||
explanation: '更适合用慢动作表现的是 B、C、D,它们都包含了"瞬间细节"和"节奏变化"。'
|
||||
}
|
||||
explanation:
|
||||
'更适合用慢动作表现的是 B、C、D,它们都包含了"瞬间细节"和"节奏变化"。',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -154,20 +167,20 @@ const getQuestionsForVideo = (index: number): Question[] => {
|
||||
options: [
|
||||
{
|
||||
id: 'a',
|
||||
label: '哪里好看就拍哪里'
|
||||
label: '哪里好看就拍哪里',
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
label: '设备越贵,画面越好'
|
||||
label: '设备越贵,画面越好',
|
||||
},
|
||||
{
|
||||
id: 'c',
|
||||
label: '在合适的时间,用合适的方式拍'
|
||||
}
|
||||
label: '在合适的时间,用合适的方式拍',
|
||||
},
|
||||
],
|
||||
correctOptionId: 'c',
|
||||
explanation: '拍摄要学会判断时间、光线和方式。'
|
||||
}
|
||||
explanation: '拍摄要学会判断时间、光线和方式。',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -252,7 +265,12 @@ const handleQuestionsComplete = () => {
|
||||
@click="showStepNavigation = true"
|
||||
title="步骤导航"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M3 4H17M3 8H17M3 12H17M3 16H17"
|
||||
stroke="currentColor"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import ImgButton from '@/components/Button/ImgButton.vue'
|
||||
import { StepNavigation } from '@/components/Guide'
|
||||
import StepNavigation from '@/components/Guide/StepNavigation.vue'
|
||||
import { Video } from '@/components'
|
||||
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
// 运镜初识 - 暂时用运镜的基本方式代替
|
||||
@@ -55,16 +55,20 @@ const navigationSteps = ref([
|
||||
id: 'step2',
|
||||
label: '视觉素养',
|
||||
content: '正确的手机拍摄持机姿势',
|
||||
videoUrl: getPublicPath('video/new/movement/xinshou_zhengquedeshoujipaishe.mp4'),
|
||||
type: 'planning' as const
|
||||
videoUrl: getPublicPath(
|
||||
'video/new/movement/xinshou_zhengquedeshoujipaishe.mp4',
|
||||
),
|
||||
type: 'planning' as const,
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
label: '视觉素养',
|
||||
content: '运镜的基本方式',
|
||||
videoUrl: getPublicPath('video/new/movement/xinshou_yunjingdejibenfangshi.mp4'),
|
||||
type: 'planning' as const
|
||||
}
|
||||
videoUrl: getPublicPath(
|
||||
'video/new/movement/xinshou_yunjingdejibenfangshi.mp4',
|
||||
),
|
||||
type: 'planning' as const,
|
||||
},
|
||||
])
|
||||
|
||||
// 步骤导航相关状态
|
||||
@@ -75,7 +79,9 @@ const currentNavigationStep = ref(0) // 对应视频索引,从0开始
|
||||
const currentVideoIndex = ref(0)
|
||||
|
||||
//按顺序播放
|
||||
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
|
||||
const currentVideo = computed(
|
||||
() => navigationSteps.value[currentVideoIndex.value].videoUrl,
|
||||
)
|
||||
const handleVideoChange = () => {
|
||||
if (currentVideoIndex.value < navigationSteps.value.length - 1) {
|
||||
currentVideoIndex.value++
|
||||
@@ -137,7 +143,12 @@ const handleStepNavigationClose = () => {
|
||||
@click="showStepNavigation = true"
|
||||
title="步骤导航"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M3 4H17M3 8H17M3 12H17M3 16H17"
|
||||
stroke="currentColor"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import ImgButton from '@/components/Button/ImgButton.vue'
|
||||
import { StepNavigation } from '@/components/Guide'
|
||||
import StepNavigation from '@/components/Guide/StepNavigation.vue'
|
||||
import { Video } from '@/components'
|
||||
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
@@ -32,15 +32,15 @@ const navigationSteps = ref([
|
||||
label: '视觉素养',
|
||||
content: '如何选择构图',
|
||||
videoUrl: getPublicPath('video/new/composition/xsdh_rhxzgt_batch.mp4'),
|
||||
type: 'planning' as const
|
||||
type: 'planning' as const,
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
label: '视觉素养',
|
||||
content: '九种经典构图方式',
|
||||
videoUrl: getPublicPath('video/new/composition/xsdh_jzgtf_batch.mp4'),
|
||||
type: 'planning' as const
|
||||
}
|
||||
type: 'planning' as const,
|
||||
},
|
||||
])
|
||||
|
||||
// 步骤导航相关状态
|
||||
@@ -51,7 +51,9 @@ const currentNavigationStep = ref(0) // 对应视频索引,从0开始
|
||||
const currentVideoIndex = ref(0)
|
||||
|
||||
//按顺序播放
|
||||
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
|
||||
const currentVideo = computed(
|
||||
() => navigationSteps.value[currentVideoIndex.value].videoUrl,
|
||||
)
|
||||
const handleVideoChange = () => {
|
||||
if (currentVideoIndex.value < navigationSteps.value.length - 1) {
|
||||
currentVideoIndex.value++
|
||||
@@ -115,7 +117,12 @@ onMounted(() => {})
|
||||
@click="showStepNavigation = true"
|
||||
title="步骤导航"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M3 4H17M3 8H17M3 12H17M3 16H17"
|
||||
stroke="currentColor"
|
||||
@@ -180,7 +187,12 @@ onMounted(() => {})
|
||||
class="w-10 h-10 bg-blue-500/80 hover:bg-blue-500 rounded-full inline-flex items-center justify-center text-white transition-all duration-200"
|
||||
title="步骤导航"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M3 4H17M3 8H17M3 12H17M3 16H17"
|
||||
stroke="currentColor"
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import ImgButton from '@/components/Button/ImgButton.vue'
|
||||
import { StepNavigation } from '@/components/Guide'
|
||||
import { Video, QuestionQuiz, type Question } from '@/components'
|
||||
import StepNavigation from '@/components/Guide/StepNavigation.vue'
|
||||
import { Video } from '@/components'
|
||||
import QuestionQuiz, {
|
||||
type Question,
|
||||
} from '@/components/QuestionQuiz/QuestionQuiz.vue'
|
||||
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { GuideSteps } from '@/components/Guide'
|
||||
import GuideStepsItem from '@/components/Guide/GuideStepsItem.vue'
|
||||
@@ -53,22 +56,22 @@ const navigationSteps = ref([
|
||||
label: '视觉素养',
|
||||
content: '光的基础认知',
|
||||
videoUrl: getPublicPath('video/new/light/xinshou_guangdejichurenzhi.mp4'),
|
||||
type: 'planning' as const
|
||||
type: 'planning' as const,
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
label: '视觉素养',
|
||||
content: '光的四大属性',
|
||||
videoUrl: getPublicPath('video/new/light/xinshou_guangdesidashuxing.mp4'),
|
||||
type: 'planning' as const
|
||||
type: 'planning' as const,
|
||||
},
|
||||
{
|
||||
id: 'step4',
|
||||
label: '视觉素养',
|
||||
content: '拍摄的黄金时间',
|
||||
videoUrl: getPublicPath('video/new/light/xsdh_psdhjsj_batch.mp4'),
|
||||
type: 'planning' as const
|
||||
}
|
||||
type: 'planning' as const,
|
||||
},
|
||||
])
|
||||
|
||||
// 步骤导航相关状态
|
||||
@@ -79,7 +82,9 @@ const currentNavigationStep = ref(0) // 对应视频索引,从0开始
|
||||
const currentVideoIndex = ref(0)
|
||||
|
||||
//按顺序播放
|
||||
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
|
||||
const currentVideo = computed(
|
||||
() => navigationSteps.value[currentVideoIndex.value].videoUrl,
|
||||
)
|
||||
|
||||
// 题目数据:拍摄的黄金时间(index 2)
|
||||
const questions = ref<Question[]>([])
|
||||
@@ -92,24 +97,25 @@ const getQuestionsForVideo = (index: number): Question[] => {
|
||||
{
|
||||
id: 1,
|
||||
title: '',
|
||||
description: '(单选)同一条街道,在不同时间拍摄,哪一张画面更适合表现"温暖、舒服的日常感"?',
|
||||
description:
|
||||
'(单选)同一条街道,在不同时间拍摄,哪一张画面更适合表现"温暖、舒服的日常感"?',
|
||||
options: [
|
||||
{
|
||||
id: 'a',
|
||||
label: '正午直射光,阴影很硬,路面反光明显'
|
||||
label: '正午直射光,阴影很硬,路面反光明显',
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
label: '傍晚低角度阳光,墙面被染成金色'
|
||||
label: '傍晚低角度阳光,墙面被染成金色',
|
||||
},
|
||||
{
|
||||
id: 'c',
|
||||
label: '夜晚路灯照明,光源复杂、色温混杂'
|
||||
}
|
||||
label: '夜晚路灯照明,光源复杂、色温混杂',
|
||||
},
|
||||
],
|
||||
correctOptionId: 'b',
|
||||
explanation: '光线角度越低,画面柔和,情绪放大'
|
||||
}
|
||||
explanation: '光线角度越低,画面柔和,情绪放大',
|
||||
},
|
||||
]
|
||||
}
|
||||
return []
|
||||
@@ -195,7 +201,12 @@ const handleQuestionsComplete = () => {
|
||||
@click="showStepNavigation = true"
|
||||
title="步骤导航"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M3 4H17M3 8H17M3 12H17M3 16H17"
|
||||
stroke="currentColor"
|
||||
@@ -243,9 +254,13 @@ const handleQuestionsComplete = () => {
|
||||
|
||||
<!-- 引导与高亮 -->
|
||||
|
||||
<GuideSteps v-if="isGuide" position-offset-y="85%" @complete="handleComplete">
|
||||
<GuideSteps
|
||||
v-if="isGuide"
|
||||
position-offset-y="85%"
|
||||
@complete="handleComplete"
|
||||
>
|
||||
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_009.MP3')">
|
||||
光是摄影的灵魂,摄影高手一定是用光的高手
|
||||
光是摄影的灵魂,摄影高手一定是用光的高手。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_010.MP3')">
|
||||
现在, 让我们一起去认识光、读懂光。
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import ImgButton from '@/components/Button/ImgButton.vue'
|
||||
import { StepNavigation, GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import StepNavigation from '@/components/Guide/StepNavigation.vue'
|
||||
import { Video } from '@/components'
|
||||
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { useBasicElementsStore } from '@/stores/modules/basic-elements'
|
||||
@@ -26,72 +27,86 @@ const rawSteps = [
|
||||
id: 'step2',
|
||||
label: '剪映基础操作',
|
||||
content: '初识剪映专业版界面和功能',
|
||||
videoUrl: getPublicPath('video/new/editing-skills/xsdh_csjyzybjmhgn_batch.mp4'),
|
||||
type: 'other' as const
|
||||
videoUrl: getPublicPath(
|
||||
'video/new/editing-skills/xsdh_csjyzybjmhgn_batch.mp4',
|
||||
),
|
||||
type: 'other' as const,
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
label: '剪映基础操作',
|
||||
content: '时间线的用法详解',
|
||||
videoUrl: getPublicPath('video/new/editing-skills/xsdh_sjxdyfxj_batch.mp4'),
|
||||
type: 'other' as const
|
||||
type: 'other' as const,
|
||||
},
|
||||
{
|
||||
id: 'step4',
|
||||
label: '剪映基础操作',
|
||||
content: '转场特效贴纸功能详解',
|
||||
videoUrl: getPublicPath('video/new/editing-skills/xsdh_zctxtzgnxj_batch.mp4'),
|
||||
type: 'other' as const
|
||||
videoUrl: getPublicPath(
|
||||
'video/new/editing-skills/xsdh_zctxtzgnxj_batch.mp4',
|
||||
),
|
||||
type: 'other' as const,
|
||||
},
|
||||
{
|
||||
id: 'step5',
|
||||
label: '剪映基础操作',
|
||||
content: '如何给视频快速上字幕',
|
||||
videoUrl: getPublicPath('video/new/editing-skills/xsdh_rhgspksszm_batch.mp4'),
|
||||
type: 'other' as const
|
||||
videoUrl: getPublicPath(
|
||||
'video/new/editing-skills/xsdh_rhgspksszm_batch.mp4',
|
||||
),
|
||||
type: 'other' as const,
|
||||
},
|
||||
{
|
||||
id: 'step6',
|
||||
label: '剪映基础操作',
|
||||
content: '音频的功能应用和处理',
|
||||
videoUrl: getPublicPath('video/new/editing-skills/xsdh_ypdgnyyhcl_batch.mp4'),
|
||||
type: 'other' as const
|
||||
videoUrl: getPublicPath(
|
||||
'video/new/editing-skills/xsdh_ypdgnyyhcl_batch.mp4',
|
||||
),
|
||||
type: 'other' as const,
|
||||
},
|
||||
{
|
||||
id: 'step7',
|
||||
label: '剪映基础操作',
|
||||
content: '关键帧功能用法详解',
|
||||
videoUrl: getPublicPath('video/new/editing-skills/xsdh_gjzgnyfxj_batch.mp4'),
|
||||
type: 'other' as const
|
||||
videoUrl: getPublicPath(
|
||||
'video/new/editing-skills/xsdh_gjzgnyfxj_batch.mp4',
|
||||
),
|
||||
type: 'other' as const,
|
||||
},
|
||||
{
|
||||
id: 'step8',
|
||||
label: '剪映基础操作',
|
||||
content: '蒙版功能用法详解',
|
||||
videoUrl: getPublicPath('video/new/editing-skills/xsdh_mbgnyfxj_batch.mp4'),
|
||||
type: 'other' as const
|
||||
type: 'other' as const,
|
||||
},
|
||||
{
|
||||
id: 'step9',
|
||||
label: '剪映基础操作',
|
||||
content: '混合模式功能用法详解',
|
||||
videoUrl: getPublicPath('video/new/editing-skills/xsdh_hhmsgnyfxj_batch.mp4'),
|
||||
type: 'other' as const
|
||||
videoUrl: getPublicPath(
|
||||
'video/new/editing-skills/xsdh_hhmsgnyfxj_batch.mp4',
|
||||
),
|
||||
type: 'other' as const,
|
||||
},
|
||||
{
|
||||
id: 'step10',
|
||||
label: '剪映基础操作',
|
||||
content: '曲线变速功能用法详解',
|
||||
videoUrl: getPublicPath('video/new/editing-skills/xsdh_qxbsgnyfxj_batch.mp4'),
|
||||
type: 'other' as const
|
||||
videoUrl: getPublicPath(
|
||||
'video/new/editing-skills/xsdh_qxbsgnyfxj_batch.mp4',
|
||||
),
|
||||
type: 'other' as const,
|
||||
},
|
||||
{
|
||||
id: 'step11',
|
||||
label: '剪映基础操作',
|
||||
content: '如何给画面调色',
|
||||
videoUrl: getPublicPath('video/new/editing-skills/xsdh_rhghmts_batch.mp4'),
|
||||
type: 'other' as const
|
||||
}
|
||||
type: 'other' as const,
|
||||
},
|
||||
]
|
||||
|
||||
const navigationSteps = ref(rawSteps)
|
||||
@@ -163,7 +178,12 @@ const handleIntroGuideComplete = () => {
|
||||
@click="showStepNavigation = true"
|
||||
title="步骤导航"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M3 4H17M3 8H17M3 12H17M3 16H17"
|
||||
stroke="currentColor"
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Video, QuestionQuiz, type Question } from '@/components'
|
||||
import { Video } from '@/components'
|
||||
import QuestionQuiz, {
|
||||
type Question,
|
||||
} from '@/components/QuestionQuiz/QuestionQuiz.vue'
|
||||
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
import { useFilmStore } from '@/stores'
|
||||
|
||||
@@ -15,11 +18,11 @@ const handleBack = () => {
|
||||
|
||||
// 声音转场视频 URL(暂时用其他视频代替)
|
||||
const soundTransitionVideoUrl = getPublicPath(
|
||||
'video/film/editing/xianchangpaishe_shengyingzhuanchang1.mp4'
|
||||
'video/film/editing/xianchangpaishe_shengyingzhuanchang1.mp4',
|
||||
)
|
||||
// const soundTransitionVideoUrl2 = getPublicPath('video/on-site/xianchangpaishe_yanshipaishe.mp4')
|
||||
const soundTransitionVideoUrl3 = getPublicPath(
|
||||
'video/film/editing/xianchangpaishe_shengyingzhuanchang3.mp4'
|
||||
'video/film/editing/xianchangpaishe_shengyingzhuanchang3.mp4',
|
||||
)
|
||||
|
||||
// 视频播放状态
|
||||
@@ -39,13 +42,14 @@ const questions = ref<Question[]>([
|
||||
{
|
||||
id: 1,
|
||||
title: '画面感受判断',
|
||||
description: '播放视频,听听看,你觉得这个声音,像是用来结束一段画面,还是开启一段新的内容?',
|
||||
description:
|
||||
'播放视频,听听看,你觉得这个声音,像是用来结束一段画面,还是开启一段新的内容?',
|
||||
options: [
|
||||
{ id: 'a', label: '开始' },
|
||||
{ id: 'b', label: '结束' }
|
||||
{ id: 'b', label: '结束' },
|
||||
],
|
||||
correctOptionId: '', // 引导题,无对错
|
||||
mediaSrc: soundTransitionVideoUrl
|
||||
mediaSrc: soundTransitionVideoUrl,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -54,11 +58,11 @@ const questions = ref<Question[]>([
|
||||
options: [
|
||||
{ id: 'a', label: '直接切画面' },
|
||||
{ id: 'b', label: '用相似声音连接' },
|
||||
{ id: 'c', label: '用字幕说明' }
|
||||
{ id: 'c', label: '用字幕说明' },
|
||||
],
|
||||
correctOptionId: 'b',
|
||||
mediaSrc: 'film/on-site/声音转场jh_2.png'
|
||||
}
|
||||
mediaSrc: 'film/on-site/声音转场jh_2.png',
|
||||
},
|
||||
])
|
||||
|
||||
// 答题完成
|
||||
@@ -70,7 +74,9 @@ const handleQuestionsComplete = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative w-full h-full flex items-center justify-center overflow-hidden">
|
||||
<div
|
||||
class="relative w-full h-full flex items-center justify-center overflow-hidden"
|
||||
>
|
||||
<!-- 背景 -->
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center bg-no-repeat -z-10"
|
||||
@@ -113,7 +119,9 @@ const handleQuestionsComplete = () => {
|
||||
class="max-w-[1100px] w-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20 p-16 space-y-10 text-white pointer-events-auto"
|
||||
>
|
||||
<h2 class="text-4xl font-bold mb-4 leading-relaxed">结果验证</h2>
|
||||
<div class="rounded-2xl overflow-hidden border border-white/20 bg-black/60 min-h-[500px]">
|
||||
<div
|
||||
class="rounded-2xl overflow-hidden border border-white/20 bg-black/60 min-h-[500px]"
|
||||
>
|
||||
<Video
|
||||
class="w-full h-full"
|
||||
:src="soundTransitionVideoUrl3"
|
||||
@@ -123,7 +131,9 @@ const handleQuestionsComplete = () => {
|
||||
:volume="1"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-6 p-6 rounded-xl bg-amber-500/20 border border-amber-400/30">
|
||||
<div
|
||||
class="mt-6 p-6 rounded-xl bg-amber-500/20 border border-amber-400/30"
|
||||
>
|
||||
<div class="text-xl font-semibold text-amber-300 mb-2">提示语:</div>
|
||||
<div class="text-lg text-amber-100 leading-relaxed">
|
||||
<p>相似的声音,</p>
|
||||
|
||||
@@ -2,8 +2,15 @@
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { QuestionQuiz, type Question } from '@/components'
|
||||
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import QuestionQuiz, {
|
||||
type Question,
|
||||
} from '@/components/QuestionQuiz/QuestionQuiz.vue'
|
||||
import {
|
||||
getBackgroundImage,
|
||||
getPublicPath,
|
||||
getStaticImage,
|
||||
IMAGE_PATHS,
|
||||
} from '@/utils'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -16,14 +23,15 @@ const questions = ref<Question[]>([
|
||||
{
|
||||
id: 1,
|
||||
title: '画面感受判断',
|
||||
description: '如果你身处滨江岸边,看到一位正在打太极的老人,你会选择哪个角度拍摄?',
|
||||
description:
|
||||
'如果你身处滨江岸边,看到一位正在打太极的老人,你会选择哪个角度拍摄?',
|
||||
options: [
|
||||
{ id: 'a', label: '平视拍摄' },
|
||||
{ id: 'b', label: '俯拍' },
|
||||
{ id: 'c', label: '低角度仰拍' }
|
||||
{ id: 'c', label: '低角度仰拍' },
|
||||
],
|
||||
correctOptionId: '', // 引导题,没有正确答案,任意选择都可进入下一题
|
||||
mediaSrc: 'film/on-site/低角度拍摄jh_1.png'
|
||||
mediaSrc: 'film/on-site/低角度拍摄jh_1.png',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -33,11 +41,11 @@ const questions = ref<Question[]>([
|
||||
{ id: 'a', label: '放大了人物的真实身高' },
|
||||
{ id: 'b', label: '强化了人物动作的力量感与气势' },
|
||||
{ id: 'c', label: '让背景元素显得更加丰富' },
|
||||
{ id: 'd', label: '让画面信息量变得更多' }
|
||||
{ id: 'd', label: '让画面信息量变得更多' },
|
||||
],
|
||||
correctOptionId: 'b',
|
||||
mediaSrc: getPublicPath('video/film/on-site/xianchangpaishe_dijiaodu.mp4')
|
||||
}
|
||||
mediaSrc: getPublicPath('video/film/on-site/xianchangpaishe_dijiaodu.mp4'),
|
||||
},
|
||||
])
|
||||
|
||||
const showSummaryStep = ref(false)
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Video, QuestionQuiz, type Question } from '@/components'
|
||||
import { Video } from '@/components'
|
||||
import QuestionQuiz, {
|
||||
type Question,
|
||||
} from '@/components/QuestionQuiz/QuestionQuiz.vue'
|
||||
import { getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -12,7 +15,9 @@ const handleBack = () => {
|
||||
}
|
||||
|
||||
// 滑轨拍摄视频 URL
|
||||
const sliderVideoUrl = getPublicPath('video/film/on-site/dygmk_hgydjt_batch.mp4')
|
||||
const sliderVideoUrl = getPublicPath(
|
||||
'video/film/on-site/dygmk_hgydjt_batch.mp4',
|
||||
)
|
||||
|
||||
// 视频播放状态
|
||||
const videoEnded = ref(false)
|
||||
@@ -34,15 +39,16 @@ const questions = ref<Question[]>([
|
||||
options: [
|
||||
{
|
||||
id: 'a',
|
||||
label: '人物正对镜头说话,背景简单,镜头静止人物正对镜头说话,背景简单,镜头静止'
|
||||
label:
|
||||
'人物正对镜头说话,背景简单,镜头静止人物正对镜头说话,背景简单,镜头静止',
|
||||
},
|
||||
{ id: 'b', label: '人物站在窗边,前景有栏杆或墙体,镜头缓慢横向移动' },
|
||||
{ id: 'c', label: '人物快速奔跑,摄影师跟着移动拍摄' }
|
||||
{ id: 'c', label: '人物快速奔跑,摄影师跟着移动拍摄' },
|
||||
],
|
||||
correctOptionId: 'b',
|
||||
explanation:
|
||||
'滑轨镜头的价值,不在"动",而在稳定、克制地交代空间关系。当前景—人物—背景同时存在时,缓慢的横向移动,会让画面产生层次和叙事感,而不是分散注意力。'
|
||||
}
|
||||
'滑轨镜头的价值,不在"动",而在稳定、克制地交代空间关系。当前景—人物—背景同时存在时,缓慢的横向移动,会让画面产生层次和叙事感,而不是分散注意力。',
|
||||
},
|
||||
])
|
||||
|
||||
// 答题完成
|
||||
@@ -52,7 +58,9 @@ const handleQuestionsComplete = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative w-full h-full flex items-center justify-center overflow-hidden">
|
||||
<div
|
||||
class="relative w-full h-full flex items-center justify-center overflow-hidden"
|
||||
>
|
||||
<!-- 视频播放区域 -->
|
||||
<Video
|
||||
class="w-full h-full"
|
||||
|
||||
@@ -2,8 +2,15 @@
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { QuestionQuiz, type Question } from '@/components'
|
||||
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import QuestionQuiz, {
|
||||
type Question,
|
||||
} from '@/components/QuestionQuiz/QuestionQuiz.vue'
|
||||
import {
|
||||
getBackgroundImage,
|
||||
getPublicPath,
|
||||
getStaticImage,
|
||||
IMAGE_PATHS,
|
||||
} from '@/utils'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -19,11 +26,12 @@ const questions = ref<Question[]>([
|
||||
description: '在这个白天场景中,画面里的运动变化,是否容易被直接观察到?',
|
||||
options: [
|
||||
{ id: 'a', label: '是,变化本身已经很清楚' },
|
||||
{ id: 'b', label: '否,变化存在,但不容易被完整感知' }
|
||||
{ id: 'b', label: '否,变化存在,但不容易被完整感知' },
|
||||
],
|
||||
correctOptionId: 'b',
|
||||
mediaSrc: 'film/on-site/白天慢门jh_1.png',
|
||||
explanation: '白天光照条件下,许多运动变化存在于连续时间中,并不容易被单帧画面完整呈现。'
|
||||
explanation:
|
||||
'白天光照条件下,许多运动变化存在于连续时间中,并不容易被单帧画面完整呈现。',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -33,11 +41,13 @@ const questions = ref<Question[]>([
|
||||
options: [
|
||||
{ id: 'a', label: '提高 ISO' },
|
||||
{ id: 'b', label: '加装 ND 镜片,降低快门速度' },
|
||||
{ id: 'c', label: '使用连拍模式' }
|
||||
{ id: 'c', label: '使用连拍模式' },
|
||||
],
|
||||
correctOptionId: 'b',
|
||||
mediaSrc: getPublicPath('video/film/on-site/xianchangpaishe_baitianmanmen.mp4')
|
||||
}
|
||||
mediaSrc: getPublicPath(
|
||||
'video/film/on-site/xianchangpaishe_baitianmanmen.mp4',
|
||||
),
|
||||
},
|
||||
])
|
||||
|
||||
const showSummaryStep = ref(false)
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Video, QuestionQuiz, type Question } from '@/components'
|
||||
import { Video } from '@/components'
|
||||
import QuestionQuiz, {
|
||||
type Question,
|
||||
} from '@/components/QuestionQuiz/QuestionQuiz.vue'
|
||||
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 延时拍摄视频 URL(占位,后续可替换)
|
||||
const timeLapseVideoUrl = getPublicPath('video/film/on-site/xianchangpaishe_yanshipaishe.mp4')
|
||||
const timeLapseVideoUrl = getPublicPath(
|
||||
'video/film/on-site/xianchangpaishe_yanshipaishe.mp4',
|
||||
)
|
||||
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
@@ -19,29 +24,31 @@ const questions = ref<Question[]>([
|
||||
{
|
||||
id: 1,
|
||||
title: '观察判断',
|
||||
description: '如果你只用一条普通视频来拍这个场景,你觉得画面最难被感知的是什么?',
|
||||
description:
|
||||
'如果你只用一条普通视频来拍这个场景,你觉得画面最难被感知的是什么?',
|
||||
options: [
|
||||
{ id: 'a', label: '建筑的外形结构' },
|
||||
{ id: 'b', label: '天空的颜色' },
|
||||
{ id: 'c', label: '时间的变化' },
|
||||
{ id: 'd', label: '画面的清晰度' }
|
||||
{ id: 'd', label: '画面的清晰度' },
|
||||
],
|
||||
correctOptionId: 'c',
|
||||
mediaSrc: 'film/on-site/延时拍摄jh_1.png' // 占位:请按需替换为真实图片
|
||||
mediaSrc: 'film/on-site/延时拍摄jh_1.png', // 占位:请按需替换为真实图片
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '方法选择',
|
||||
description: '如果希望在很短时间内,让观众明显感知“时间正在流动”,你会选择哪种拍摄方式?',
|
||||
description:
|
||||
'如果希望在很短时间内,让观众明显感知“时间正在流动”,你会选择哪种拍摄方式?',
|
||||
options: [
|
||||
{ id: 'a', label: '手持拍摄' },
|
||||
{ id: 'b', label: '慢动作拍摄' },
|
||||
{ id: 'c', label: '延时拍摄' },
|
||||
{ id: 'd', label: '推拉镜头' }
|
||||
{ id: 'd', label: '推拉镜头' },
|
||||
],
|
||||
correctOptionId: 'c',
|
||||
mediaSrc: 'film/on-site/延时拍摄jh_1.png' // 占位:请按需替换为真实图片
|
||||
}
|
||||
mediaSrc: 'film/on-site/延时拍摄jh_1.png', // 占位:请按需替换为真实图片
|
||||
},
|
||||
])
|
||||
|
||||
const showObserveStep = ref(false)
|
||||
@@ -79,7 +86,9 @@ const handleQuestionsComplete = () => {
|
||||
<!-- 总结文本内容 -->
|
||||
<div class="w-2/6 text-2xl leading-10 opacity-90 space-y-3">
|
||||
<h2 class="text-4xl font-bold mb-4 leading-relaxed">总结</h2>
|
||||
<p>在这个延时画面中,注意天空变化与光影移动的节奏,它们在短时间内被连续呈现出来。</p>
|
||||
<p>
|
||||
在这个延时画面中,注意天空变化与光影移动的节奏,它们在短时间内被连续呈现出来。
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<span>延时拍摄的主要目的</span>
|
||||
|
||||
@@ -4,7 +4,12 @@ import { useRouter } from 'vue-router'
|
||||
import { GuideSteps } from '@/components/Guide'
|
||||
import { Overlay } from '@/components/Overlay'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import {
|
||||
getBackgroundImage,
|
||||
getPublicPath,
|
||||
getStaticImage,
|
||||
IMAGE_PATHS,
|
||||
} from '@/utils'
|
||||
import shootingtitle from '@/assets/images/shooting/标题-实地取景.png'
|
||||
import videoSrc from '/video/new/composition/xsdh_jzgtf_batch.mp4'
|
||||
import KnowledgeCard from '@/components/KnowledgeCard/FilmKnowledgeCard.vue'
|
||||
@@ -35,26 +40,26 @@ const steps = ref([
|
||||
id: '1',
|
||||
audioSrc: getPublicPath('audio/py_4_015.MP3'),
|
||||
content: '哇,徐汇滨江真的超适合取景!一起看看可以运用哪些构图方法吧~',
|
||||
highlightSelector: '.btn-1'
|
||||
}
|
||||
highlightSelector: '.btn-1',
|
||||
},
|
||||
])
|
||||
|
||||
const guideSteps = ref([
|
||||
{
|
||||
id: '1',
|
||||
content: '点击框架构图,查看构图方法',
|
||||
highlightSelector: '.btn-item-1'
|
||||
highlightSelector: '.btn-item-1',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
content: '点击对称构图,查看构图方法',
|
||||
highlightSelector: '.btn-item-2'
|
||||
highlightSelector: '.btn-item-2',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
content: '点击引导线构图,查看构图方法',
|
||||
highlightSelector: '.btn-item-3'
|
||||
}
|
||||
highlightSelector: '.btn-item-3',
|
||||
},
|
||||
])
|
||||
const showModal = ref(false)
|
||||
|
||||
@@ -65,7 +70,7 @@ const currentItem = ref<Btn>({
|
||||
name: '',
|
||||
btnImage: '',
|
||||
images: [],
|
||||
desc: ''
|
||||
desc: '',
|
||||
})
|
||||
|
||||
const btnList = ref<Btn[]>([
|
||||
@@ -74,22 +79,25 @@ const btnList = ref<Btn[]>([
|
||||
name: '框架构图',
|
||||
btnImage: 'film/shooting/框架构图',
|
||||
desc: '框架构图是拍摄时用“框架”把主体框起来,可以让画面更加有空间感及层次感,并且使用也并不复杂,比如镜子、门窗、山洞等,也可以说是光影框等。',
|
||||
images: ['film/shooting/01框架构图_底图', 'film/shooting/02框架构图_底图']
|
||||
images: ['film/shooting/01框架构图_底图', 'film/shooting/02框架构图_底图'],
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
name: '对称构图',
|
||||
btnImage: 'film/shooting/对称构图',
|
||||
desc: '对称构图是通过画面中轴线(水平 / 垂直)两侧元素镜像分布的构图方式,属于平衡类构图。它借对称的视觉均衡感,让画面显规整、稳定,同时强化主体秩序与美感。拍摄可找天然对称景物(如建筑、倒影),或调整角度让元素对称;略有不对称可后期微调,适合展现建筑、对称静物等规整场景。',
|
||||
images: ['film/shooting/03对称构图_底图', 'film/shooting/04对称构图_底图']
|
||||
images: ['film/shooting/03对称构图_底图', 'film/shooting/04对称构图_底图'],
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
name: '引导线构图',
|
||||
btnImage: 'film/shooting/引导线构图',
|
||||
desc: '引导线构图的关键在于引导线,借助环境中的引导线,把观者注意力集中到引导线上,营造画面纵深效果,我们日常拍摄中常见的引导线有:马路及其延伸(马路线、栏杆、走廊)',
|
||||
images: ['film/shooting/05引导线构图_底图', 'film/shooting/06引导线构图_底图']
|
||||
}
|
||||
desc: '引导线构图的关键在于引导线,借助环境中的引导线,把观者注意力集中到引导线上,营造画面纵深效果,我们日常拍摄中常见的引导线有:马路及其延伸(马路线、栏杆、走廊)。',
|
||||
images: [
|
||||
'film/shooting/05引导线构图_底图',
|
||||
'film/shooting/06引导线构图_底图',
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
const handleModalClose = () => {
|
||||
@@ -135,19 +143,36 @@ const back = () => {
|
||||
></div>
|
||||
|
||||
<div class="shooting-title">
|
||||
<img :src="shootingtitle" alt="" />
|
||||
<img
|
||||
:src="shootingtitle"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="shooting-return">
|
||||
<ImgButton image="shooting/返回.png" @click="back" />
|
||||
<ImgButton
|
||||
image="shooting/返回.png"
|
||||
@click="back"
|
||||
/>
|
||||
</div>
|
||||
<div class="absolute left-[12%] top-1/2 -translate-y-[calc(50%-70px)]">
|
||||
<ImgButton class="btn-2" image="film/shooting/龙美术馆入口.png" @click="handleClickA" />
|
||||
<ImgButton
|
||||
class="btn-2"
|
||||
image="film/shooting/龙美术馆入口.png"
|
||||
@click="handleClickA"
|
||||
/>
|
||||
</div>
|
||||
<div class="absolute right-[12%] top-1/2 -translate-y-[calc(50%-70px)]">
|
||||
<ImgButton class="btn-3" image="film/shooting/油罐艺术公园入口.png" @click="handleClickB" />
|
||||
<ImgButton
|
||||
class="btn-3"
|
||||
image="film/shooting/油罐艺术公园入口.png"
|
||||
@click="handleClickB"
|
||||
/>
|
||||
</div>
|
||||
<div class="shooting-composition btn-1">
|
||||
<ImgButton image="shooting/构图指南.png" @click="openCompositionGuide" />
|
||||
<ImgButton
|
||||
image="shooting/构图指南.png"
|
||||
@click="openCompositionGuide"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 引导步骤 -->
|
||||
@@ -164,7 +189,7 @@ const back = () => {
|
||||
:visible="showModal"
|
||||
:clickable="false"
|
||||
:customStyle="{
|
||||
backgroundImage: getBackgroundImage('film/shooting/构图指南底图')
|
||||
backgroundImage: getBackgroundImage('film/shooting/构图指南底图'),
|
||||
}"
|
||||
customClass="inset-0 bg-cover bg-center bg-no-repeat"
|
||||
contentClass="relative w-full h-full"
|
||||
@@ -177,15 +202,25 @@ const back = () => {
|
||||
/>
|
||||
|
||||
<!-- 构图指南 -->
|
||||
<ImgButton class="absolute left-0 top-0 z-10" image="shooting/标题-构图指南" />
|
||||
<ImgButton
|
||||
class="absolute left-0 top-0 z-10"
|
||||
image="shooting/标题-构图指南"
|
||||
/>
|
||||
|
||||
<!-- 新手导航视频 -->
|
||||
<div class="absolute left-28 top-[calc(50%-40px)] -translate-y-1/2">
|
||||
<ImgButton image="shooting/新手导航-视频回顾" @click="showModal3 = true" />
|
||||
<ImgButton
|
||||
image="shooting/新手导航-视频回顾"
|
||||
@click="showModal3 = true"
|
||||
/>
|
||||
<div
|
||||
class="absolute left-[40%] top-[42%] -translate-x-1/2 -translate-y-1/2 z-10 animate-bounce pointer-events-none"
|
||||
>
|
||||
<img class="w-32" :src="getStaticImage('work/小手')" alt="" />
|
||||
<img
|
||||
class="w-32"
|
||||
:src="getStaticImage('work/小手')"
|
||||
alt=""
|
||||
/>
|
||||
<span class="text-white text-2xl font-bold ml-5">点击进入</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -220,8 +255,16 @@ const back = () => {
|
||||
@close="showModal2 = false"
|
||||
/>
|
||||
<!-- 视频 -->
|
||||
<Overlay :visible="showModal3" :clickable="false">
|
||||
<Video :src="videoSrc" controls :volume="1" :muted="false"></Video>
|
||||
<Overlay
|
||||
:visible="showModal3"
|
||||
:clickable="false"
|
||||
>
|
||||
<Video
|
||||
:src="videoSrc"
|
||||
controls
|
||||
:volume="1"
|
||||
:muted="false"
|
||||
></Video>
|
||||
<!-- 返回 -->
|
||||
<ImgButton
|
||||
class="absolute right-0 top-0 z-10"
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { StarRating } from '@/components/Rating'
|
||||
import { useRouter } from 'vue-router'
|
||||
import BackToTopIcon from '@/assets/svg/back-to-top.svg'
|
||||
import BackToTopIcon from '@/assets/images/components/置顶.png'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { useFilmStore } from '@/stores/modules/film'
|
||||
|
||||
@@ -26,7 +26,9 @@ const isLastPage = computed<boolean>(() => currentPage.value >= TOTAL_PAGES - 1)
|
||||
|
||||
/** 是否正在从第二页回到第一页 */
|
||||
const isScrollingBackToFirst = computed<boolean>(() => {
|
||||
return isAnimating.value && currentPage.value === 1 && animatingToPage.value === 0
|
||||
return (
|
||||
isAnimating.value && currentPage.value === 1 && animatingToPage.value === 0
|
||||
)
|
||||
})
|
||||
|
||||
/** 第一页坐着的人物是否显示 */
|
||||
@@ -42,7 +44,7 @@ const guideKeyPrefix = 'film-practice-page'
|
||||
const steps = ref([
|
||||
'欢迎来到实战操作台! 这里的内容很丰富,滚动鼠标就能浏览到全部内容啦~',
|
||||
'点击【我的创作笔记】,编写创作思路~',
|
||||
'点击【查看】按钮,欣赏他人实战作品~'
|
||||
'点击【查看】按钮,欣赏他人实战作品~',
|
||||
])
|
||||
|
||||
const currentGuideKey = computed(() => `${guideKeyPrefix}-${currentPage.value}`)
|
||||
@@ -75,8 +77,8 @@ const worksList = ref<WorkItem[]>([
|
||||
stars: 4,
|
||||
path: '/video/film/practice/film_anli1.mp4',
|
||||
description:
|
||||
'作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介'
|
||||
}
|
||||
'作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介',
|
||||
},
|
||||
])
|
||||
|
||||
/** 左滑动作品列表 */
|
||||
@@ -168,18 +170,44 @@ const handleWheel = (event: WheelEvent): void => {
|
||||
}
|
||||
}
|
||||
|
||||
const restoreScrollPage = (): void => {
|
||||
const savedPage = filmStore.practiceScrollPage
|
||||
const targetPage = Math.max(0, Math.min(savedPage, TOTAL_PAGES - 1))
|
||||
|
||||
currentPage.value = targetPage
|
||||
animatingToPage.value = targetPage
|
||||
requestAnimationFrame(() => {
|
||||
if (containerRef.value) {
|
||||
containerRef.value.scrollTop = targetPage * PAGE_HEIGHT
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const saveScrollPage = (): void => {
|
||||
const page = containerRef.value
|
||||
? Math.round(containerRef.value.scrollTop / PAGE_HEIGHT)
|
||||
: currentPage.value
|
||||
const targetPage = Math.max(0, Math.min(page, TOTAL_PAGES - 1))
|
||||
filmStore.setPracticeScrollPage(targetPage)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
restoreScrollPage()
|
||||
showPageGuide()
|
||||
containerRef.value?.addEventListener('wheel', handleWheel, { passive: false })
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
saveScrollPage()
|
||||
containerRef.value?.removeEventListener('wheel', handleWheel)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" class="w-full h-full overflow-auto">
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="w-full h-full overflow-auto"
|
||||
>
|
||||
<div class="relative min-h-full overflow-hidden">
|
||||
<!-- 关闭 -->
|
||||
<ImgButton
|
||||
@@ -188,16 +216,19 @@ onUnmounted(() => {
|
||||
@click="handleClose"
|
||||
/>
|
||||
<!-- 实战操作台 -->
|
||||
<div
|
||||
class="w-full h-[1080px] bg-cover bg-no-repeat relative"
|
||||
:style="{ backgroundImage: getBackgroundImage('film/practice/实战操作台首页- 电影感') }"
|
||||
>
|
||||
<div class="w-full h-[1080px] bg-[#F5EFE6] relative">
|
||||
<ImgButton
|
||||
class="absolute left-40 bottom-28 z-10"
|
||||
class="absolute left-40 bottom-14 z-10"
|
||||
image="vlog/practice/查看任务要求"
|
||||
@click="scrollToNextPage"
|
||||
/>
|
||||
<!-- img -->
|
||||
<img
|
||||
class="absolute left-0 top-0 z-10"
|
||||
draggable="false"
|
||||
:src="getStaticImage('film/practice/电影')"
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
class="absolute right-0"
|
||||
draggable="false"
|
||||
@@ -212,12 +243,18 @@ onUnmounted(() => {
|
||||
>
|
||||
<ImgButton
|
||||
class="absolute right-20 bottom-0 z-99"
|
||||
image="vlog/practice/我的创作笔记"
|
||||
image="vlog/practice/我的创作笔记btn"
|
||||
path="/film/practice/my-notes"
|
||||
/>
|
||||
|
||||
<div class="task-panel w-[757px] h-full ml-10 overflow-y-auto" @wheel.stop>
|
||||
<img :src="getStaticImage('film/practice/滑动任务面板-电影感')" alt="" />
|
||||
<div
|
||||
class="task-panel w-[757px] h-full ml-10 overflow-y-auto"
|
||||
@wheel.stop
|
||||
>
|
||||
<img
|
||||
:src="getStaticImage('film/practice/任务要求')"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
<img
|
||||
@@ -239,7 +276,9 @@ onUnmounted(() => {
|
||||
<!-- 他人实战作品 -->
|
||||
<div
|
||||
class="w-full h-[1080px] bg-cover bg-no-repeat pt-44 px-16"
|
||||
:style="{ backgroundImage: getBackgroundImage('vlog/practice/他人实战作品') }"
|
||||
:style="{
|
||||
backgroundImage: getBackgroundImage('vlog/practice/他人实战作品'),
|
||||
}"
|
||||
>
|
||||
<!-- 作品列表 -->
|
||||
<div class="w-full overflow-hidden">
|
||||
@@ -254,15 +293,27 @@ onUnmounted(() => {
|
||||
>
|
||||
<!-- 作品图片 -->
|
||||
<div class="w-full h-[280px] rounded-2xl overflow-hidden">
|
||||
<img class="w-full h-full object-cover" :src="work.image" :alt="work.title" />
|
||||
<img
|
||||
class="w-full h-full object-cover"
|
||||
:src="work.image"
|
||||
:alt="work.title"
|
||||
/>
|
||||
</div>
|
||||
<!-- 作品信息 -->
|
||||
<div class="flex flex-col items-center pt-8 space-y-4">
|
||||
<h3 class="text-5xl font-bold text-gray-800 mb-2">{{ work.title }}</h3>
|
||||
<h3 class="text-5xl font-bold text-gray-800 mb-2">
|
||||
{{ work.title }}
|
||||
</h3>
|
||||
<!-- 星星评分 -->
|
||||
<StarRating :score="work.stars" size="lg" readonly />
|
||||
<StarRating
|
||||
:score="work.stars"
|
||||
size="lg"
|
||||
readonly
|
||||
/>
|
||||
<!-- 简介 -->
|
||||
<p class="h-[110px] text-2xl text-gray-600 mb-3 line-clamp-3 leading-relaxed">
|
||||
<p
|
||||
class="h-[110px] text-2xl text-gray-600 mb-3 line-clamp-3 leading-relaxed"
|
||||
>
|
||||
{{ work.description }}
|
||||
</p>
|
||||
<!-- 查看按钮 -->
|
||||
@@ -275,7 +326,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 指示按钮 -->
|
||||
<!-- 指示按钮,暂时不要
|
||||
<div class="absolute bottom-5 left-1/2 -translate-x-1/2 flex gap-16">
|
||||
<img
|
||||
class="cursor-pointer hover:scale-110 transition-transform"
|
||||
@@ -286,19 +337,23 @@ onUnmounted(() => {
|
||||
/>
|
||||
<img
|
||||
class="cursor-pointer hover:scale-110 transition-transform rotate-180"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': worksScrollIndex >= worksList.length - 3 }"
|
||||
:class="{
|
||||
'opacity-50 cursor-not-allowed':
|
||||
worksScrollIndex >= worksList.length - 3,
|
||||
}"
|
||||
:src="getStaticImage('vlog/practice/滑动左')"
|
||||
alt="向右"
|
||||
@click="scrollWorksRight"
|
||||
/>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下滑按钮 - 最后一页隐藏 -->
|
||||
<ImgButton
|
||||
v-show="!isLastPage"
|
||||
class="absolute bottom-0 left-1/2 -translate-x-1/2"
|
||||
class="absolute bottom-0 left-1/2 down-scroll-hint"
|
||||
:image="'vlog/practice/下滑'"
|
||||
@click="scrollToNextPage"
|
||||
/>
|
||||
@@ -306,13 +361,16 @@ onUnmounted(() => {
|
||||
<!-- 回到顶部按钮 - 最后一页显示 -->
|
||||
<img
|
||||
v-show="isLastPage"
|
||||
class="absolute bottom-10 right-10 cursor-pointer transition-all duration-300 hover:scale-105"
|
||||
class="absolute bottom-10 left-1/2 -translate-x-1/2 cursor-pointer transition-all duration-300 hover:scale-105"
|
||||
:src="BackToTopIcon"
|
||||
@click="scrollToTop"
|
||||
/>
|
||||
|
||||
<!-- 跨页人物动画 -->
|
||||
<div v-show="showSittingPerson || isScrollingBackToFirst" class="character-container">
|
||||
<div
|
||||
v-show="showSittingPerson || isScrollingBackToFirst"
|
||||
class="character-container"
|
||||
>
|
||||
<!-- 坐着的人物 - 第一页 -->
|
||||
<Transition name="fade">
|
||||
<img
|
||||
@@ -335,7 +393,11 @@ onUnmounted(() => {
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<GuideSteps v-if="visible" position-offset-y="70%" @complete="visible = false">
|
||||
<GuideSteps
|
||||
v-if="visible"
|
||||
position-offset-y="70%"
|
||||
@complete="visible = false"
|
||||
>
|
||||
<GuideStepsItem>{{ contentSteps }}</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</div>
|
||||
@@ -355,6 +417,22 @@ onUnmounted(() => {
|
||||
/* Chrome/Safari/Opera */
|
||||
}
|
||||
|
||||
.down-scroll-hint {
|
||||
transform: translateX(-50%);
|
||||
animation: down-scroll-hint 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes down-scroll-hint {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translate(-50%, 12px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 第一页人物动画容器 */
|
||||
.character-container {
|
||||
position: fixed;
|
||||
|
||||
@@ -3,8 +3,9 @@ import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Overlay } from '@/components/Overlay'
|
||||
import { StarRating } from '@/components/Rating'
|
||||
import PracticeRatingModal, {
|
||||
type PracticeRatingItem,
|
||||
} from '@/components/PracticeRatingModal'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -21,42 +22,33 @@ const handleBack = (): void => {
|
||||
|
||||
const disabled = computed<boolean>(() => noteContent.value.trim() === '')
|
||||
|
||||
interface EvaluationItem {
|
||||
/** 标题 */
|
||||
title: string
|
||||
/** 描述 */
|
||||
description: string
|
||||
/** 评分 */
|
||||
score: number
|
||||
}
|
||||
|
||||
/** 评估项目列表 */
|
||||
const evaluationItems = ref<EvaluationItem[]>([
|
||||
const evaluationItems = ref<PracticeRatingItem[]>([
|
||||
{
|
||||
title: '画面氛围',
|
||||
description: '画面具有明确情绪或空间气质,整体风格统一',
|
||||
score: 4
|
||||
score: 4,
|
||||
},
|
||||
{
|
||||
title: '拍摄方法',
|
||||
description: '拍摄方式选择合理,镜头语言服务于画面表达',
|
||||
score: 2
|
||||
score: 2,
|
||||
},
|
||||
{
|
||||
title: '剪辑逻辑',
|
||||
description: '剪辑节奏清晰,镜头衔接自然,有整体感',
|
||||
score: 5
|
||||
score: 5,
|
||||
},
|
||||
{
|
||||
title: '创作意识',
|
||||
description: '能体现主动构思,而非随拍拼接',
|
||||
score: 0
|
||||
score: 0,
|
||||
},
|
||||
{
|
||||
title: '综合观感',
|
||||
description: '作品具有“电影感”,完成度高',
|
||||
score: 5
|
||||
}
|
||||
score: 5,
|
||||
},
|
||||
])
|
||||
const handleSave = (): void => {
|
||||
console.log('保存笔记')
|
||||
@@ -67,7 +59,9 @@ const handleSave = (): void => {
|
||||
<template>
|
||||
<div
|
||||
class="w-full h-full bg-no-repeat bg-cover px-8 pt-[400px]"
|
||||
:style="{ backgroundImage: getBackgroundImage('film/practice/创作笔记底图-电影感') }"
|
||||
:style="{
|
||||
backgroundImage: getBackgroundImage('film/practice/我的创作笔记'),
|
||||
}"
|
||||
>
|
||||
<ImgButton
|
||||
class="absolute top-0 right-0"
|
||||
@@ -83,53 +77,25 @@ const handleSave = (): void => {
|
||||
></textarea>
|
||||
|
||||
<div class="absolute bottom-24 left-1/2 -translate-x-1/2 flex gap-60">
|
||||
<ImgButton image="vlog/practice/保存" @click="handleSave" />
|
||||
<ImgButton image="vlog/practice/自评" :disabled="disabled" @click="showModal = true" />
|
||||
<ImgButton
|
||||
image="vlog/practice/保存"
|
||||
@click="handleSave"
|
||||
/>
|
||||
<ImgButton
|
||||
image="vlog/practice/自评"
|
||||
:disabled="disabled"
|
||||
@click="showModal = true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 保存图片弹窗 -->
|
||||
<Overlay :visible="showModal" :clickable="false">
|
||||
<div
|
||||
class="w-[1795px] h-[987px] bg-no-repeat bg-cover p-10 pt-[100px]"
|
||||
:style="{ backgroundImage: getBackgroundImage('film/practice/评分底板-电影感') }"
|
||||
>
|
||||
<ImgButton
|
||||
class="absolute -top-8 -right-14 z-10"
|
||||
:image="IMAGE_PATHS.COMPONENTS.CLOSE"
|
||||
@click="showModal = false"
|
||||
/>
|
||||
|
||||
<div class="relative h-full overflow-y-scroll px-8">
|
||||
<div class="pl-10 space-y-10 mt-10">
|
||||
<p class="glow-text">
|
||||
评分时要关注5个维度哦~每个维度满分5分,能打1-5分,记得对照作品表现来打分呀~
|
||||
</p>
|
||||
<div
|
||||
v-for="(item, index) in evaluationItems"
|
||||
:key="index"
|
||||
class="relative w-full h-[182px] bg-no-repeat bg-cover px-12 py-3 flex justify-between"
|
||||
:style="{ backgroundImage: getBackgroundImage('vlog/practice/评分条') }"
|
||||
>
|
||||
<div class="pr-8 space-y-10">
|
||||
<h4 class="text-3xl font-bold text-white mb-3">
|
||||
{{ `0${index + 1}` }} {{ item.title }}
|
||||
</h4>
|
||||
<p class="text-3xl text-gray-800 font-bold leading-relaxed">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
</div>
|
||||
<StarRating class="absolute -bottom-5 right-10" :score="item.score" size="lg" />
|
||||
</div>
|
||||
<p class="glow-text">
|
||||
己评价的实战作品,从封面入口就能直接看到具体评分!打了多少分一目了然,不用再点进详情页翻找啦~
|
||||
</p>
|
||||
<div class="w-full flex justify-center">
|
||||
<ImgButton image="vlog/practice/保存" @click="showModal = false" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Overlay>
|
||||
<PracticeRatingModal
|
||||
:visible="showModal"
|
||||
:items="evaluationItems"
|
||||
background-image="film/practice/电影感短视频创作-作品评分表"
|
||||
top-tip="评分时要关注5个维度哦~分别是画面氛围、抽摄方法、剪辑逻辑、创作意识、综合观感,每个维度满分5分,能打1-5分,记得对照作品表现来打分呀~"
|
||||
bottom-tip="己评价的实战作品,从封面入口就能直接看到具体评分!打了多少分一目了然,不用再点进详情页翻找啦~"
|
||||
@close="showModal = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -146,25 +112,4 @@ textarea::placeholder {
|
||||
color: #5d5b5b;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.glow-text {
|
||||
color: white;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
text-shadow:
|
||||
/* 描边效果 - 多层阴影模拟描边 */
|
||||
-1px -1px 0 #0066ff,
|
||||
1px -1px 0 #0066ff,
|
||||
-1px 1px 0 #0066ff,
|
||||
1px 1px 0 #0066ff,
|
||||
-2px -2px 0 #0066ff,
|
||||
2px -2px 0 #0066ff,
|
||||
-2px 2px 0 #0066ff,
|
||||
2px 2px 0 #0066ff,
|
||||
/* 发光效果 - 蓝色光晕 */ 0 0 10px #0066ff,
|
||||
0 0 20px #0066ff,
|
||||
0 0 30px #0066ff,
|
||||
0 0 40px #0066ff,
|
||||
0 0 50px #0066ff;
|
||||
}
|
||||
</style>
|
||||
@@ -2,9 +2,10 @@
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Video } from '@/components'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { StarRating } from '@/components/Rating'
|
||||
import { IMAGE_PATHS, getBackgroundImage, getPublicPath } from '@/utils'
|
||||
import { Overlay } from '@/components/Overlay'
|
||||
import PracticeRatingModal, {
|
||||
type PracticeRatingItem,
|
||||
} from '@/components/PracticeRatingModal'
|
||||
import { IMAGE_PATHS, getPublicPath } from '@/utils'
|
||||
import { ref } from 'vue'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { useFilmStore } from '@/stores/modules/film'
|
||||
@@ -20,46 +21,37 @@ if (showGuide.value) {
|
||||
const props = defineProps({
|
||||
path: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
interface EvaluationItem {
|
||||
/** 标题 */
|
||||
title: string
|
||||
/** 描述 */
|
||||
description: string
|
||||
/** 评分 */
|
||||
score: number
|
||||
}
|
||||
|
||||
/** 评估项目列表 */
|
||||
const evaluationItems = ref<EvaluationItem[]>([
|
||||
const evaluationItems = ref<PracticeRatingItem[]>([
|
||||
{
|
||||
title: '主题与内容',
|
||||
description: '主题明确聚焦"身边美好",内容真实、有感染力,能让观众感受到日常闪光',
|
||||
score: 3
|
||||
title: '画面氛围',
|
||||
description: '画面具有明确情绪或空间气质,整体风格统一',
|
||||
score: 4,
|
||||
},
|
||||
{
|
||||
title: '拍摄技巧',
|
||||
description: '运镜自然、光线运用合理、画面清晰饱和度、能辅助主题表达(至少1种运镜+1种光线)',
|
||||
score: 4
|
||||
title: '拍摄方法',
|
||||
description: '拍摄方式选择合理,镜头语言服务于画面表达',
|
||||
score: 2,
|
||||
},
|
||||
{
|
||||
title: '剪辑与包装',
|
||||
description: '素材拼接流畅,调色/滤镜适配、字幕清晰、音乐不盖人声、整体风格统一',
|
||||
score: 5
|
||||
title: '剪辑逻辑',
|
||||
description: '剪辑节奏清晰,镜头衔接自然,有整体感',
|
||||
score: 5,
|
||||
},
|
||||
{
|
||||
title: '创意与独特性',
|
||||
description: '视角新颖、有个人观察,或用特殊细节/情节展现“美好”,避免平淡复刻',
|
||||
score: 4
|
||||
title: '创作意识',
|
||||
description: '能体现主动构思,而非随拍拼接',
|
||||
score: 0,
|
||||
},
|
||||
{
|
||||
title: '综合感受',
|
||||
description: '视角新颖、有个人观察,或用特殊细节/情节展现“美好”避免平淡复刻',
|
||||
score: 5
|
||||
}
|
||||
title: '综合观感',
|
||||
description: '作品具有“电影感”,完成度高',
|
||||
score: 5,
|
||||
},
|
||||
])
|
||||
|
||||
/** 保存弹窗 */
|
||||
@@ -91,49 +83,19 @@ const handleBack = (): void => {
|
||||
:loop="false"
|
||||
/>
|
||||
|
||||
<!-- 保存图片弹窗 -->
|
||||
<Overlay :visible="showModal" :clickable="false">
|
||||
<div
|
||||
class="w-[1795px] h-[987px] bg-no-repeat bg-cover p-10 pt-[80px]"
|
||||
:style="{ backgroundImage: getBackgroundImage('vlog/practice/评分底板') }"
|
||||
>
|
||||
<ImgButton
|
||||
class="absolute -top-8 -right-14 z-10"
|
||||
:image="IMAGE_PATHS.COMPONENTS.CLOSE"
|
||||
@click="showModal = false"
|
||||
/>
|
||||
|
||||
<div class="relative h-full overflow-y-scroll px-8">
|
||||
<div
|
||||
class="w-full bg-no-repeat bg-cover bg-left py-44"
|
||||
:style="{ backgroundImage: getBackgroundImage('vlog/practice/滑动表内文字') }"
|
||||
>
|
||||
<div class="pl-10 space-y-16">
|
||||
<div
|
||||
v-for="(item, index) in evaluationItems"
|
||||
:key="index"
|
||||
class="relative w-full h-[182px] bg-no-repeat bg-cover px-12 py-3 flex justify-between"
|
||||
:style="{ backgroundImage: getBackgroundImage('vlog/practice/评分条') }"
|
||||
>
|
||||
<div class="pr-8 space-y-10">
|
||||
<h4 class="text-3xl font-bold text-white mb-3">
|
||||
{{ `0${index + 1}` }} {{ item.title }}
|
||||
</h4>
|
||||
<p class="text-3xl text-gray-800 font-bold leading-relaxed">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
</div>
|
||||
<StarRating class="absolute -bottom-5 right-10" :score="item.score" size="lg" />
|
||||
</div>
|
||||
<div class="w-full flex justify-center">
|
||||
<ImgButton image="vlog/practice/保存" @click="showModal = false" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Overlay>
|
||||
<GuideSteps :visible="showGuide" :highlight-selector="'.btn-1'" position-offset-y="60%">
|
||||
<PracticeRatingModal
|
||||
:visible="showModal"
|
||||
:items="evaluationItems"
|
||||
background-image="film/practice/电影感短视频创作-作品评分表"
|
||||
top-tip="评分时要关注5个维度哦~分别是画面氛围、抽摄方法、剪辑逻辑、创作意识、综合观感,每个维度满分5分,能打1-5分,记得对照作品表现来打分呀~"
|
||||
bottom-tip="己评价的实战作品,从封面入口就能直接看到具体评分!打了多少分一目了然,不用再点进详情页翻找啦~"
|
||||
@close="showModal = false"
|
||||
/>
|
||||
<GuideSteps
|
||||
:visible="showGuide"
|
||||
:highlight-selector="'.btn-1'"
|
||||
position-offset-y="60%"
|
||||
>
|
||||
<GuideStepsItem>点击这里可以给作品评分噢~</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import ImgButton from '@/components/Button/ImgButton.vue'
|
||||
import { GuideSteps, GuideStepsItem, StepNavigation } from '@/components/Guide'
|
||||
import { Video, QuestionQuiz, type Question } from '@/components'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import StepNavigation from '@/components/Guide/StepNavigation.vue'
|
||||
import { Video } from '@/components'
|
||||
import QuestionQuiz, {
|
||||
type Question,
|
||||
} from '@/components/QuestionQuiz/QuestionQuiz.vue'
|
||||
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { useFilmStore } from '@/stores'
|
||||
|
||||
@@ -33,44 +37,44 @@ const navigationSteps = ref([
|
||||
id: 'step1',
|
||||
label: '后期制作',
|
||||
videoUrl: getPublicPath('video/film/work/dygmk_zf_batch.mp4'),
|
||||
content: '遮幅'
|
||||
content: '遮幅',
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
label: '拍摄执行',
|
||||
videoUrl: getPublicPath('video/film/work/dygmk_gx_batch.mp4'),
|
||||
content: '光线'
|
||||
content: '光线',
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
label: '拍摄执行',
|
||||
videoUrl: getPublicPath('video/film/work/dygmk_xs_batch.mp4'),
|
||||
content: '虚实'
|
||||
content: '虚实',
|
||||
},
|
||||
{
|
||||
id: 'step4',
|
||||
label: '拍摄执行',
|
||||
videoUrl: getPublicPath('video/film/work/dygmk_txjt_batch.mp4'),
|
||||
content: '特写镜头'
|
||||
content: '特写镜头',
|
||||
},
|
||||
{
|
||||
id: 'step5',
|
||||
label: '拍摄执行',
|
||||
videoUrl: getPublicPath('video/film/work/dygmk_fjt_batch.mp4'),
|
||||
content: '分镜头'
|
||||
content: '分镜头',
|
||||
},
|
||||
{
|
||||
id: 'step5',
|
||||
label: '后期制作',
|
||||
videoUrl: getPublicPath('video/film/work/dygmk_lj_batch.mp4'),
|
||||
content: '滤镜'
|
||||
content: '滤镜',
|
||||
},
|
||||
{
|
||||
id: 'step5',
|
||||
label: '拍摄执行',
|
||||
videoUrl: getPublicPath('video/film/work/dygmk_tqs_batch.mp4'),
|
||||
content: '同期声'
|
||||
}
|
||||
content: '同期声',
|
||||
},
|
||||
])
|
||||
|
||||
// 步骤导航相关状态
|
||||
@@ -94,17 +98,17 @@ const getQuestionsForVideo = (index: number): Question[] => {
|
||||
options: [
|
||||
{
|
||||
id: 'a',
|
||||
label: '前景、人物、背景全部清晰'
|
||||
label: '前景、人物、背景全部清晰',
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
label: '前景虚化,人物清晰,背景有层次'
|
||||
}
|
||||
label: '前景虚化,人物清晰,背景有层次',
|
||||
},
|
||||
],
|
||||
correctOptionId: 'b',
|
||||
explanation:
|
||||
'电影感的画面,往往会主动模糊一部分信息。这是为了把观众的注意力,留给正在发生的情绪。'
|
||||
}
|
||||
'电影感的画面,往往会主动模糊一部分信息。这是为了把观众的注意力,留给正在发生的情绪。',
|
||||
},
|
||||
]
|
||||
}
|
||||
// 分镜头(index 4)
|
||||
@@ -117,25 +121,28 @@ const getQuestionsForVideo = (index: number): Question[] => {
|
||||
options: [
|
||||
{
|
||||
id: 'a',
|
||||
label: '特写→中景→ 全景'
|
||||
label: '特写→中景→ 全景',
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
label: '全景→ 中景→ 特写'
|
||||
}
|
||||
label: '全景→ 中景→ 特写',
|
||||
},
|
||||
],
|
||||
correctOptionId: 'b',
|
||||
explanation: '电影里的镜头, 往往是从环境进入人物,再进入情绪。'
|
||||
}
|
||||
explanation: '电影里的镜头, 往往是从环境进入人物,再进入情绪。',
|
||||
},
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
//按顺序播放
|
||||
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
|
||||
const currentVideo = computed(
|
||||
() => navigationSteps.value[currentVideoIndex.value].videoUrl,
|
||||
)
|
||||
const resolvedSrc = computed(() => {
|
||||
if (key.value === 'case') return getPublicPath('video/film/work/dygmk_zbbwg_batch.mp4')
|
||||
if (key.value === 'case')
|
||||
return getPublicPath('video/film/work/dygmk_zbbwg_batch.mp4')
|
||||
if (key.value === 'profession') return currentVideo.value
|
||||
return ''
|
||||
})
|
||||
@@ -148,8 +155,9 @@ const handleVideoChange = () => {
|
||||
steps.value = [
|
||||
{
|
||||
id: 'step1',
|
||||
content: '恭喜你,已完成专业剖析的学习,点击【查看脚本】回顾视频脚本吧!'
|
||||
}
|
||||
content:
|
||||
'恭喜你,已完成专业剖析的学习,点击【查看脚本】回顾视频脚本吧!',
|
||||
},
|
||||
]
|
||||
return
|
||||
}
|
||||
@@ -164,7 +172,7 @@ const handleClose = () => {
|
||||
film.setFilmUnlockProgress(1)
|
||||
router.push({
|
||||
name: 'Film',
|
||||
query: { guide: 'return' }
|
||||
query: { guide: 'return' },
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -200,7 +208,7 @@ const handleComplete = () => {
|
||||
if (key.value === 'profession') {
|
||||
if (!film.isFilmUnlock) film.setIsFilmUnlock(true)
|
||||
router.push({
|
||||
name: 'FilmWork'
|
||||
name: 'FilmWork',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -239,7 +247,10 @@ onMounted(() => {})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full relative bg-white/10" @click="handleClick">
|
||||
<div
|
||||
class="w-full h-full relative bg-white/10"
|
||||
@click="handleClick"
|
||||
>
|
||||
<template v-if="key !== 'case' && key !== 'profession'">
|
||||
<ImgButton
|
||||
class="absolute right-0 top-0 z-10"
|
||||
@@ -254,7 +265,12 @@ onMounted(() => {})
|
||||
@click="showStepNavigation = true"
|
||||
title="步骤导航"
|
||||
>
|
||||
<svg width="35" height="35" viewBox="0 0 20 20" fill="none">
|
||||
<svg
|
||||
width="35"
|
||||
height="35"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M3 4H17M3 8H17M3 12H17M3 16H17"
|
||||
stroke="currentColor"
|
||||
@@ -289,13 +305,24 @@ onMounted(() => {})
|
||||
/>
|
||||
</template>
|
||||
</Video>
|
||||
<div v-else-if="key === 'idea'" :src="image">
|
||||
<img :src="getStaticImage('film/思路提点全屏')" alt="" />
|
||||
<div
|
||||
v-else-if="key === 'idea'"
|
||||
:src="image"
|
||||
>
|
||||
<img
|
||||
:src="getStaticImage('film/思路提点全屏')"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 引导与高亮 -->
|
||||
<GuideSteps v-if="status" :steps="steps" position-offset-y="85%" @complete="handleComplete" />
|
||||
<GuideSteps
|
||||
v-if="status"
|
||||
:steps="steps"
|
||||
position-offset-y="85%"
|
||||
@complete="handleComplete"
|
||||
/>
|
||||
|
||||
<!-- 步骤导航抽屉 -->
|
||||
<StepNavigation
|
||||
@@ -331,7 +358,12 @@ onMounted(() => {})
|
||||
class="w-10 h-10 bg-blue-500/80 hover:bg-blue-500 rounded-full inline-flex items-center justify-center text-white transition-all duration-200"
|
||||
title="步骤导航"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M3 4H17M3 8H17M3 12H17M3 16H17"
|
||||
stroke="currentColor"
|
||||
@@ -354,7 +386,8 @@ onMounted(() => {})
|
||||
@complete="handleComplete"
|
||||
>
|
||||
<GuideStepsItem :audio-src="getPublicPath('audio/py_4_007.MP3')">
|
||||
一个好镜头,既要好看,又要好懂 —— 既有视觉吸引力,又能把信息清晰地传达出来。
|
||||
一个好镜头,既要好看,又要好懂 ——
|
||||
既有视觉吸引力,又能把信息清晰地传达出来。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem :audio-src="getPublicPath('audio/py_4_008.MP3')">
|
||||
当这些条件被满足,一个镜头就有了“高级感”。
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<script lang="tsx" name="FilmWork" setup>
|
||||
defineOptions({ name: 'FilmWork' })
|
||||
import { ref, computed } from 'vue'
|
||||
import { getBackgroundImage, IMAGE_PATHS, getStaticImage, getPublicPath } from '@/utils'
|
||||
import {
|
||||
getBackgroundImage,
|
||||
IMAGE_PATHS,
|
||||
getStaticImage,
|
||||
getPublicPath,
|
||||
} from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import WorkSideSwitcher from './components/WorkSideSwitcher.vue'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
@@ -29,28 +34,30 @@ const sideSwitcherItems = ref([
|
||||
key: 'case',
|
||||
name: '案例赏析',
|
||||
description: '物件春秋-上海钟表文化科普馆',
|
||||
image: 'film/案件赏析小屏'
|
||||
image: 'film/案件赏析小屏',
|
||||
},
|
||||
{
|
||||
key: 'profession',
|
||||
name: '专业剖析',
|
||||
description: '物件春秋-上海钟表文化科普馆',
|
||||
image: 'film/专业剖析'
|
||||
image: 'film/专业剖析',
|
||||
},
|
||||
{
|
||||
key: 'idea',
|
||||
name: '思路提点',
|
||||
description: '物件春秋-上海钟表文化科普馆',
|
||||
image: 'film/思路提点小屏'
|
||||
}
|
||||
image: 'film/思路提点小屏',
|
||||
},
|
||||
])
|
||||
|
||||
//当前选中的item
|
||||
const currentItem = computed(() => {
|
||||
return sideSwitcherItems.value.find(item => item.key === selected.value)
|
||||
return sideSwitcherItems.value.find((item) => item.key === selected.value)
|
||||
})
|
||||
|
||||
const currentHighlight = computed(() => (stepIndex.value === 2 ? '.work-border' : null))
|
||||
const currentHighlight = computed(() =>
|
||||
stepIndex.value === 2 ? '.work-border' : null,
|
||||
)
|
||||
|
||||
const handleStepChange = (_: any, index: number) => {
|
||||
stepIndex.value = index
|
||||
@@ -61,14 +68,16 @@ const handleSideSwitcherChange = (value: WorkTab) => {
|
||||
}
|
||||
|
||||
const handleImageClick = () => {
|
||||
const currentItem = workSideSwitcherRef.value?.items.find(item => item.key === selected.value)
|
||||
const currentItem = workSideSwitcherRef.value?.items.find(
|
||||
(item) => item.key === selected.value,
|
||||
)
|
||||
router.push({
|
||||
name: 'FilmVideo',
|
||||
query: {
|
||||
image: currentItem?.activeImage || '',
|
||||
key: selected.value,
|
||||
label: currentItem?.label || ''
|
||||
}
|
||||
label: currentItem?.label || '',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -96,7 +105,11 @@ function openImages(urls: string[], startIndex = 0) {
|
||||
images: urls,
|
||||
startIndex,
|
||||
maskClosable: true,
|
||||
getScaleMetrics: () => ({ scale: scale.value, offsetX: offsetX.value, offsetY: offsetY.value })
|
||||
getScaleMetrics: () => ({
|
||||
scale: scale.value,
|
||||
offsetX: offsetX.value,
|
||||
offsetY: offsetY.value,
|
||||
}),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -131,10 +144,14 @@ function openImages(urls: string[], startIndex = 0) {
|
||||
/>
|
||||
<!-- 手 -->
|
||||
<div
|
||||
class="absolute left-[46%] top-[40%] -translate-x-1/2 -translate-y-1/2 z-10 animate-bounce pointer-events-none"
|
||||
class="absolute left-[46%] top-[45%] -translate-x-1/2 -translate-y-1/2 z-10 animate-bounce pointer-events-none"
|
||||
>
|
||||
<img class="w-40" :src="getStaticImage('work/小手')" alt="" />
|
||||
<span class="text-white text-2xl font-bold ml-5">点击进入</span>
|
||||
<img
|
||||
class="w-40"
|
||||
:src="getStaticImage('work/小手')"
|
||||
alt=""
|
||||
/>
|
||||
<!-- <span class="text-white text-2xl font-bold ml-5">点击进入</span> -->
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -177,7 +194,9 @@ function openImages(urls: string[], startIndex = 0) {
|
||||
<!-- 任务取景按钮 -->
|
||||
<ImgButton
|
||||
class="absolute right-14 bottom-10"
|
||||
:image="isFilmUnlock ? IMAGE_PATHS.WORK.TASK_BTN_OK : IMAGE_PATHS.WORK.TASK_BTN"
|
||||
:image="
|
||||
isFilmUnlock ? IMAGE_PATHS.WORK.TASK_BTN_OK : IMAGE_PATHS.WORK.TASK_BTN
|
||||
"
|
||||
@click="handleWorkClick"
|
||||
/>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -2,7 +2,7 @@
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import ImgButton from '@/components/Button/ImgButton.vue'
|
||||
import { StepNavigation } from '@/components/Guide'
|
||||
import StepNavigation from '@/components/Guide/StepNavigation.vue'
|
||||
import { IMAGE_PATHS } from '@/utils'
|
||||
import { Video } from '@/components'
|
||||
|
||||
@@ -19,20 +19,20 @@ const navigationSteps = [
|
||||
id: 'step1',
|
||||
label: '后期制作',
|
||||
content: '声画剪辑流程',
|
||||
type: 'post' as const
|
||||
type: 'post' as const,
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
label: '后期制作',
|
||||
content: 'J-cut/L-cut运用',
|
||||
type: 'post' as const
|
||||
type: 'post' as const,
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
label: '后期制作',
|
||||
content: '【Q3】节奏剪辑技巧',
|
||||
type: 'post' as const
|
||||
}
|
||||
type: 'post' as const,
|
||||
},
|
||||
]
|
||||
|
||||
const currentVideoIndex = ref(0)
|
||||
@@ -83,7 +83,12 @@ const handleClose = () => {
|
||||
title="步骤导航"
|
||||
@click="showStepNavigation = true"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M3 4H17M3 8H17M3 12H17M3 16H17"
|
||||
stroke="currentColor"
|
||||
|
||||
@@ -16,61 +16,62 @@ const category = ref([
|
||||
{
|
||||
label: '前期准备',
|
||||
value: 1,
|
||||
icon: getStaticImage('midterm/准备图标.png')
|
||||
icon: getStaticImage('midterm/准备图标.png'),
|
||||
},
|
||||
{
|
||||
label: '中期拍摄',
|
||||
value: 2,
|
||||
icon: getStaticImage('midterm/拍摄图标.png')
|
||||
icon: getStaticImage('midterm/拍摄图标.png'),
|
||||
},
|
||||
{
|
||||
label: '后期剪辑',
|
||||
value: 3,
|
||||
icon: getStaticImage('midterm/剪辑图标.png')
|
||||
}
|
||||
icon: getStaticImage('midterm/剪辑图标.png'),
|
||||
},
|
||||
])
|
||||
|
||||
const menuItems = ref([
|
||||
{
|
||||
label: '资源筹备',
|
||||
routeName: 'InterviewResource',
|
||||
parentValue: 1,
|
||||
disabled: false
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: '创意构想',
|
||||
routeName: 'InterviewCreative',
|
||||
parentValue: 1,
|
||||
disabled: false
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: '脚本撰写',
|
||||
routeName: 'InterviewScriptWriting',
|
||||
parentValue: 1,
|
||||
disabled: false
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: '分镜制作',
|
||||
routeName: 'InterviewStoryboard',
|
||||
parentValue: 1,
|
||||
disabled: false
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: '设备准备',
|
||||
routeName: 'InterviewEquipment',
|
||||
parentValue: 2,
|
||||
disabled: false
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: '实地取景',
|
||||
routeName: 'InterviewShooting',
|
||||
parentValue: 2,
|
||||
disabled: false
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: '现场拍摄',
|
||||
routeName: 'InterviewOnsiteShooting',
|
||||
parentValue: 2,
|
||||
disabled: false
|
||||
disabled: false,
|
||||
},
|
||||
// {
|
||||
// label: '素材管理',
|
||||
@@ -82,26 +83,26 @@ const menuItems = ref([
|
||||
label: '剪辑',
|
||||
routeName: 'InterviewEditing',
|
||||
parentValue: 3,
|
||||
disabled: false
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: '调色',
|
||||
routeName: 'InterviewColor',
|
||||
parentValue: 3,
|
||||
disabled: false
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: '音频',
|
||||
routeName: 'InterviewAudio',
|
||||
parentValue: 3,
|
||||
disabled: false
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: '导出发布',
|
||||
routeName: 'InterviewExportPublish',
|
||||
parentValue: 3,
|
||||
disabled: false
|
||||
}
|
||||
disabled: false,
|
||||
},
|
||||
])
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -119,28 +120,36 @@ const currentParentValue = computed<number>(() => {
|
||||
return props.currentStep
|
||||
}) // 当前选中的父级菜单项
|
||||
|
||||
const currentItemValue = computed<string>(() => {
|
||||
return router.currentRoute.value.name?.toString() ?? ''
|
||||
})
|
||||
const currentItemValue = ref<string>('') // 当前选中的子级菜单项
|
||||
|
||||
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 }) => {
|
||||
if (item.disabled) return
|
||||
currentItemValue.value = item.routeName
|
||||
router.push({ name: item.routeName })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute left-20 top-0 bottom-0 w-[520px] pt-28" v-if="currentStep">
|
||||
<div
|
||||
class="absolute left-20 top-0 bottom-0 w-[520px] pt-28"
|
||||
v-if="currentStep"
|
||||
>
|
||||
<!-- 分类名称 -->
|
||||
<div class="absolute top-3 left-24 flex items-center space-x-2">
|
||||
<span class="text-4xl text-[#fbbf24] font-black">
|
||||
{{ category[currentParentValue - 1].label }}
|
||||
</span>
|
||||
<img class="filter-blue" :src="category[currentParentValue - 1].icon" alt="" />
|
||||
<img
|
||||
class="filter-blue"
|
||||
:src="category[currentParentValue - 1].icon"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<!-- 背景图片 -->
|
||||
<div
|
||||
@@ -148,12 +157,17 @@ const handleItemClick = (item: { routeName: string; disabled: boolean }) => {
|
||||
:style="{ backgroundImage: getBackgroundImage('components/menu-bg.png') }"
|
||||
></div>
|
||||
<div class="flex items-center flex-col pl-[60px]">
|
||||
<template v-for="(item, index) in getMenuItems" :key="item.routeName">
|
||||
<template
|
||||
v-for="(item, index) in getMenuItems"
|
||||
:key="item.routeName"
|
||||
>
|
||||
<div
|
||||
class="relative w-[373px] h-[143px] pl-14 flex items-center justify-center group text-4xl font-black transition-colors"
|
||||
:class="[
|
||||
currentItemValue === item.routeName ? 'text-blue-400' : 'text-white',
|
||||
item.disabled ? 'opacity-20 cursor-not-allowed' : 'cursor-pointer'
|
||||
currentItemValue === item.routeName
|
||||
? 'text-blue-400'
|
||||
: 'text-white',
|
||||
item.disabled ? 'opacity-20 cursor-not-allowed' : 'cursor-pointer',
|
||||
]"
|
||||
:style="{ marginLeft: -index * 40 + 'px' }"
|
||||
@click="handleItemClick(item)"
|
||||
@@ -161,13 +175,22 @@ const handleItemClick = (item: { routeName: string; disabled: boolean }) => {
|
||||
<div
|
||||
v-if="!item.disabled"
|
||||
class="absolute inset-0 bg-cover bg-center bg-no-repeat opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
||||
:style="{ backgroundImage: getBackgroundImage('components/menu-hover.png') }"
|
||||
:style="{
|
||||
backgroundImage: getBackgroundImage('components/menu-hover.png'),
|
||||
}"
|
||||
></div>
|
||||
<span class="flex items-center gap-2" :class="[!item.disabled ? 'menu-glow-text' : '']">
|
||||
<span
|
||||
class="flex items-center gap-2"
|
||||
:class="[!item.disabled ? 'menu-glow-text' : '']"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
<img
|
||||
:class="['w-10', isViewed(item.routeName) ? 'brightness-90' : 'brightness-50']"
|
||||
src="@/assets/images/midterm/完成.png"
|
||||
class="w-10"
|
||||
:src="
|
||||
isViewed(item.routeName)
|
||||
? '/src/assets/images/midterm/完成.png'
|
||||
: '/src/assets/images/midterm/未完成.png'
|
||||
"
|
||||
alt=""
|
||||
/>
|
||||
</span>
|
||||
@@ -179,8 +202,8 @@ const handleItemClick = (item: { routeName: string; disabled: boolean }) => {
|
||||
|
||||
<style scoped>
|
||||
.filter-blue {
|
||||
filter: brightness(0) saturate(100%) invert(79%) sepia(29%) saturate(1190%) hue-rotate(330deg)
|
||||
brightness(102%) contrast(98%);
|
||||
filter: brightness(0) saturate(100%) invert(79%) sepia(29%) saturate(1190%)
|
||||
hue-rotate(330deg) brightness(102%) contrast(98%);
|
||||
}
|
||||
|
||||
.menu-glow-text {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ImgButton, Video } from '@/components'
|
||||
import { StepNavigation } from '@/components/Guide'
|
||||
import StepNavigation from '@/components/Guide/StepNavigation.vue'
|
||||
import { IMAGE_PATHS, getPublicPath, getStaticImage } from '@/utils'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -16,30 +16,32 @@ const navigationSteps = ref([
|
||||
label: '中期拍摄',
|
||||
content: '素材怎么拍',
|
||||
videoUrl: getPublicPath('video/interview/on-site/ftmk_sczmp_batch.mp4'),
|
||||
type: 'shooting' as const
|
||||
type: 'shooting' as const,
|
||||
},
|
||||
{
|
||||
id: 'q1-required-shots',
|
||||
label: '中期拍摄',
|
||||
content: '必拍镜头有哪些',
|
||||
videoUrl: getPublicPath('video/interview/on-site/ftmk_bpjtynx_batch.mp4'),
|
||||
type: 'shooting' as const
|
||||
type: 'shooting' as const,
|
||||
},
|
||||
{
|
||||
id: 'q2-moving-shot',
|
||||
label: '中期拍摄',
|
||||
content: '移动镜头的作用',
|
||||
type: 'shooting' as const
|
||||
type: 'shooting' as const,
|
||||
},
|
||||
{
|
||||
id: 'q2-close-up',
|
||||
label: '中期拍摄',
|
||||
content: '特写镜头的作用',
|
||||
type: 'shooting' as const
|
||||
}
|
||||
type: 'shooting' as const,
|
||||
},
|
||||
])
|
||||
|
||||
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
|
||||
const currentVideo = computed(
|
||||
() => navigationSteps.value[currentVideoIndex.value].videoUrl,
|
||||
)
|
||||
|
||||
const back = () => {
|
||||
router.back()
|
||||
@@ -92,7 +94,12 @@ const handleEnded = () => {
|
||||
title="资源列表"
|
||||
@click="showStepNavigation = true"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M3 4H17M3 8H17M3 12H17M3 16H17"
|
||||
stroke="currentColor"
|
||||
|
||||
@@ -1,7 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { getStaticImage } from '@/utils'
|
||||
|
||||
defineEmits<{
|
||||
start: []
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
scene: 'a' | 'b'
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute bottom-0">
|
||||
<div class="relative">
|
||||
<img src="@/assets/images/shooting/开始取景.png" alt="" />
|
||||
<img
|
||||
:src="
|
||||
getStaticImage(
|
||||
props.scene === 'a'
|
||||
? 'interview/shooting/开始拍摄-操场.png'
|
||||
: 'interview/shooting/开始拍摄-教学楼.png',
|
||||
)
|
||||
"
|
||||
alt=""
|
||||
/>
|
||||
<ImgButton
|
||||
class="absolute bottom-[46%] left-[49%] -translate-x-1/2"
|
||||
image="shooting/按钮-开始取景.png"
|
||||
@@ -10,11 +32,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ImgButton } from '@/components/Button'
|
||||
|
||||
defineEmits<{
|
||||
start: []
|
||||
}>()
|
||||
</script>
|
||||
@@ -35,7 +35,7 @@ const {
|
||||
getTransform,
|
||||
currentOffsetX,
|
||||
currentOffsetY,
|
||||
handleWheel
|
||||
handleWheel,
|
||||
} = useViewfinderA()
|
||||
|
||||
// 刻度盘配置常量
|
||||
@@ -88,7 +88,7 @@ const dialTicks = computed<DialTick[]>(() => {
|
||||
angle,
|
||||
isMajor,
|
||||
isInteger,
|
||||
label: isInteger ? zoom + 'x' : ''
|
||||
label: isInteger ? zoom + 'x' : '',
|
||||
})
|
||||
}
|
||||
return ticks
|
||||
@@ -116,7 +116,7 @@ const capturePhoto = async () => {
|
||||
if (isLoading.value) return
|
||||
|
||||
isCapturing.value = true
|
||||
await new Promise(resolve => setTimeout(resolve, FLASH_DURATION))
|
||||
await new Promise((resolve) => setTimeout(resolve, FLASH_DURATION))
|
||||
|
||||
await capture({
|
||||
width: CANVAS_WIDTH,
|
||||
@@ -124,7 +124,7 @@ const capturePhoto = async () => {
|
||||
scale: CANVAS_SCALE,
|
||||
delay: UI_UPDATE_DELAY,
|
||||
resultDelay: RESULT_DISPLAY_DELAY,
|
||||
onDraw: ctx => {
|
||||
onDraw: (ctx) => {
|
||||
if (!photoImage.value) {
|
||||
throw new Error('Photo image not found')
|
||||
}
|
||||
@@ -142,12 +142,12 @@ const capturePhoto = async () => {
|
||||
-CANVAS_WIDTH / 2,
|
||||
-CANVAS_HEIGHT / 2,
|
||||
CANVAS_WIDTH,
|
||||
CANVAS_HEIGHT
|
||||
CANVAS_HEIGHT,
|
||||
)
|
||||
},
|
||||
onComplete: () => {
|
||||
isCapturing.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -156,18 +156,31 @@ const capturePhoto = async () => {
|
||||
<div class="w-full h-full">
|
||||
<div
|
||||
class="relative w-full h-full bg-cover bg-center bg-no-repeat shooting-bg"
|
||||
:style="{ backgroundImage: getBackgroundImage('interview/shooting/操场-模糊.png') }"
|
||||
:style="{
|
||||
backgroundImage: getBackgroundImage('interview/shooting/操场-模糊.png'),
|
||||
}"
|
||||
>
|
||||
<ImgButton
|
||||
image="interview/shooting/操场-侧边标题.png"
|
||||
class="title-button absolute left-0"
|
||||
/>
|
||||
<ImgButton image="shooting/返回.png" class="back-button absolute right-0" @click="back" />
|
||||
<ImgButton
|
||||
image="shooting/返回.png"
|
||||
class="back-button absolute right-0"
|
||||
@click="back"
|
||||
/>
|
||||
|
||||
<StartOverlay v-if="!start" @start="start = true" />
|
||||
<StartOverlay
|
||||
scene="a"
|
||||
v-if="!start"
|
||||
@start="start = true"
|
||||
/>
|
||||
|
||||
<!-- 手机拍照界面 -->
|
||||
<div class="absolute bottom-0 w-full h-[766px]" v-else>
|
||||
<div
|
||||
class="absolute bottom-0 w-full h-[766px]"
|
||||
v-else
|
||||
>
|
||||
<img
|
||||
class="phone-frame absolute w-full h-[766px] z-10 pointer-events-none"
|
||||
src="@/assets/images/shooting/shoot.png"
|
||||
@@ -186,7 +199,7 @@ const capturePhoto = async () => {
|
||||
:class="{
|
||||
'transition-transform duration-300': !isDragging,
|
||||
'cursor-grab': !isDragging,
|
||||
'cursor-grabbing': isDragging
|
||||
'cursor-grabbing': isDragging,
|
||||
}"
|
||||
:style="{ transform: getTransform() }"
|
||||
src="@/assets/images/interview/shooting/操场-清晰.png"
|
||||
@@ -213,7 +226,9 @@ const capturePhoto = async () => {
|
||||
|
||||
<div
|
||||
class="shooting-decor relative pointer-events-none top-[19.2%] left-[48.3%] -translate-x-1/2 w-[871px] h-[420px] bg-cover bg-center bg-no-repeat"
|
||||
:style="{ backgroundImage: getBackgroundImage('shooting/拍摄装饰.png') }"
|
||||
:style="{
|
||||
backgroundImage: getBackgroundImage('shooting/拍摄装饰.png'),
|
||||
}"
|
||||
>
|
||||
<!-- 仿苹果半圆盘缩放控件 -->
|
||||
<div
|
||||
@@ -237,13 +252,17 @@ const capturePhoto = async () => {
|
||||
<!-- 圆心在容器右侧外部,形成左侧弧形 -->
|
||||
<div
|
||||
class="dial-wheel absolute top-1/2 right-[-480px] w-[600px] h-[600px] rounded-full bg-black/30 backdrop-blur-md border-[5px] border-white/20 transition-transform duration-100 ease-out origin-center cursor-grab active:cursor-grabbing"
|
||||
:style="{ transform: `translateY(-50%) rotate(${dialRotation}deg)` }"
|
||||
:style="{
|
||||
transform: `translateY(-50%) rotate(${dialRotation}deg)`,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="tick in dialTicks"
|
||||
:key="tick.zoom"
|
||||
class="absolute top-1/2 left-0 w-full h-0 flex justify-start items-center origin-center pointer-events-auto"
|
||||
:style="{ transform: `translate(0, -50%) rotate(${tick.angle}deg)` }"
|
||||
:style="{
|
||||
transform: `translate(0, -50%) rotate(${tick.angle}deg)`,
|
||||
}"
|
||||
@click.stop="setZoom(tick.zoom)"
|
||||
>
|
||||
<!-- 刻度线本体 (位于圆盘左边缘) -->
|
||||
@@ -252,17 +271,21 @@ const capturePhoto = async () => {
|
||||
:class="{
|
||||
'w-10 h-1 opacity-90': tick.isInteger,
|
||||
'w-6 h-0.5 opacity-60': !tick.isInteger && tick.isMajor,
|
||||
'w-3 h-[1px] opacity-40': !tick.isMajor
|
||||
'w-3 h-[1px] opacity-40': !tick.isMajor,
|
||||
}"
|
||||
></div>
|
||||
<!-- 刻度文字 -->
|
||||
<div
|
||||
v-if="tick.isInteger"
|
||||
class="absolute left-[52px] text-white/90 font-bold text-lg drop-shadow-md"
|
||||
:style="{ transform: `rotate(${-tick.angle - dialRotation}deg)` }"
|
||||
:style="{
|
||||
transform: `rotate(${-tick.angle - dialRotation}deg)`,
|
||||
}"
|
||||
>
|
||||
<span
|
||||
:class="{ 'text-yellow-400 scale-110': currentZoom === tick.zoom }"
|
||||
:class="{
|
||||
'text-yellow-400 scale-110': currentZoom === tick.zoom,
|
||||
}"
|
||||
class="block transition-all duration-200"
|
||||
>
|
||||
{{ tick.label }}
|
||||
@@ -282,22 +305,39 @@ const capturePhoto = async () => {
|
||||
</div>
|
||||
|
||||
<!-- 结果弹窗 -->
|
||||
<Overlay :visible="showModal" @close="handleModalClose">
|
||||
<div class="relative w-[1000px] h-[600px] flex items-center justify-center">
|
||||
<Overlay
|
||||
:visible="showModal"
|
||||
@close="handleModalClose"
|
||||
>
|
||||
<div
|
||||
class="relative w-[1000px] h-[600px] flex items-center justify-center"
|
||||
>
|
||||
<!-- 截图内容 -->
|
||||
<div v-if="capturedImageUrl" class="bg-white p-2 rounded-md -rotate-6">
|
||||
<img :src="capturedImageUrl" alt="拍摄结果" />
|
||||
<div
|
||||
v-if="capturedImageUrl"
|
||||
class="bg-white p-2 rounded-md -rotate-6"
|
||||
>
|
||||
<img
|
||||
:src="capturedImageUrl"
|
||||
alt="拍摄结果"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="absolute bottom-1/2 -right-1/3 translate-y-1/2">
|
||||
<ImgButton image="shooting/按钮-前往下一个取景地" @click="handleFinish" />
|
||||
<ImgButton
|
||||
image="shooting/按钮-前往下一个取景地"
|
||||
@click="handleFinish"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Overlay>
|
||||
</div>
|
||||
|
||||
<LoadingOverlay :visible="isLoading" text="正在出片中..." />
|
||||
<LoadingOverlay
|
||||
:visible="isLoading"
|
||||
text="正在出片中..."
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -321,7 +361,19 @@ const capturePhoto = async () => {
|
||||
}
|
||||
|
||||
.mask-gradient {
|
||||
mask-image: linear-gradient(to bottom, transparent, black 15%, black 85%, transparent);
|
||||
-webkit-mask-image: linear-gradient(to bottom, transparent, black 15%, black 85%, transparent);
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
black 15%,
|
||||
black 85%,
|
||||
transparent
|
||||
);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
black 15%,
|
||||
black 85%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
</style>
|
||||