feat: 完善模块引导与学习进度体验
统一引导提示的持久化控制,补充学习进度入口与模块资源,避免用户返回页面时重复触发提示。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
No files matched your search
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="app-container" :style="containerStyle">
|
||||
<div class="app-content" :style="scaleStyle">
|
||||
<div class="app-content overflow-hidden" :style="scaleStyle">
|
||||
<router-view v-slot="{ Component }">
|
||||
<KeepAlive :include="['Action', 'Skill']">
|
||||
<component :is="Component" />
|
||||
|
||||
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 365 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 336 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 403 KiB |
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 427 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 367 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 4.5 MiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 736 KiB |
@@ -0,0 +1,159 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { getStaticImage } from '@/utils'
|
||||
import type { AnchoredProgressBarProps, ProgressAnchor } from './types'
|
||||
|
||||
defineOptions({
|
||||
name: 'AnchoredProgressBar'
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<AnchoredProgressBarProps>(), {
|
||||
min: 0,
|
||||
max: 100,
|
||||
showPercent: true,
|
||||
nodeImage: '',
|
||||
trackClass: 'bg-black/20',
|
||||
fillClass: 'bg-[#31effa] shadow-[0_0_26px_rgba(49,239,250,0.95)]',
|
||||
markerClass: 'bg-white shadow-[0_0_12px_rgba(255,255,255,0.9)]',
|
||||
labelClass: 'text-white font-[1000] tracking-wider',
|
||||
childLabelClass: 'text-[#43f3f2] font-[1000] tracking-wider',
|
||||
percentClass: 'text-white font-[1000] italic',
|
||||
labelTop: '0%',
|
||||
childLabelTop: '26%',
|
||||
barTop: '56%',
|
||||
barHeight: '26%',
|
||||
markerTop: '48%',
|
||||
markerHeight: '42%',
|
||||
markerWidth: '0.55%',
|
||||
childMarkerTop: '56%',
|
||||
childMarkerHeight: '26%',
|
||||
childMarkerWidth: '0.22%',
|
||||
nodeTop: '39%',
|
||||
nodeHeight: '58%',
|
||||
percentTop: '76%',
|
||||
labelStyle: () => ({}),
|
||||
childLabelStyle: () => ({}),
|
||||
percentStyle: () => ({})
|
||||
})
|
||||
|
||||
const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max)
|
||||
|
||||
const toPercent = (value: number) => {
|
||||
const range = props.max - props.min
|
||||
if (range <= 0) return 0
|
||||
return clamp(((value - props.min) / range) * 100, 0, 100)
|
||||
}
|
||||
|
||||
const progressPercent = computed(() => toPercent(props.progress))
|
||||
const nodeSrc = computed(() => (props.nodeImage ? getStaticImage(props.nodeImage) : ''))
|
||||
const markerAnchors = computed(() => {
|
||||
const anchorsByValue = new Map<number, ProgressAnchor & { markerType: 'major' | 'minor' }>()
|
||||
|
||||
props.anchors.forEach(anchor => {
|
||||
anchorsByValue.set(toPercent(anchor.value), { ...anchor, markerType: 'major' })
|
||||
anchor.children?.forEach(child => {
|
||||
const left = toPercent(child.value)
|
||||
if (!anchorsByValue.has(left)) {
|
||||
anchorsByValue.set(left, { ...child, markerType: 'minor' })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
anchorsByValue.set(0, { id: 'start', label: '', value: props.min, markerType: 'major' })
|
||||
anchorsByValue.set(100, { id: 'end', label: '', value: props.max, markerType: 'major' })
|
||||
|
||||
return [...anchorsByValue.entries()]
|
||||
.sort(([leftA], [leftB]) => leftA - leftB)
|
||||
.map(([left, anchor]) => ({ ...anchor, left }))
|
||||
})
|
||||
|
||||
const anchorWithPercent = computed(() =>
|
||||
props.anchors.map(anchor => ({
|
||||
...anchor,
|
||||
left: toPercent(anchor.value),
|
||||
children: anchor.children?.map(child => ({ ...child, left: toPercent(child.value) }))
|
||||
}))
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative h-full w-full">
|
||||
<template v-for="anchor in anchorWithPercent" :key="anchor.id">
|
||||
<div
|
||||
v-if="anchor.label"
|
||||
class="absolute whitespace-nowrap font-[1000]"
|
||||
:class="[labelClass, anchor.left >= 98 ? '-translate-x-full' : '-translate-x-0']"
|
||||
:style="{
|
||||
...labelStyle,
|
||||
top: anchor.labelPosition === 'bottom' ? percentTop : labelTop,
|
||||
left: `${anchor.left}%`
|
||||
}"
|
||||
>
|
||||
{{ anchor.label }}
|
||||
</div>
|
||||
<div
|
||||
v-for="child in anchor.children"
|
||||
:key="child.id"
|
||||
class="absolute -translate-x-1/2 whitespace-nowrap font-bold leading-none"
|
||||
:class="childLabelClass"
|
||||
:style="{ ...childLabelStyle, top: childLabelTop, left: `${child.left}%` }"
|
||||
>
|
||||
{{ child.label }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="absolute left-0 w-full"
|
||||
:class="trackClass"
|
||||
:style="{ top: barTop, height: barHeight }"
|
||||
></div>
|
||||
<div
|
||||
class="absolute left-0 rounded-full transition-all duration-700 ease-out"
|
||||
:class="fillClass"
|
||||
:style="{ top: barTop, height: barHeight, width: `${progressPercent}%` }"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-for="anchor in markerAnchors"
|
||||
:key="anchor.id"
|
||||
class="absolute rounded-full"
|
||||
:class="markerClass"
|
||||
:style="{
|
||||
top: anchor.markerType === 'major' ? markerTop : childMarkerTop,
|
||||
height: anchor.markerType === 'major' ? markerHeight : childMarkerHeight,
|
||||
width: anchor.markerType === 'major' ? markerWidth : childMarkerWidth,
|
||||
left: `${anchor.left}%`
|
||||
}"
|
||||
></div>
|
||||
|
||||
<img
|
||||
v-if="nodeSrc"
|
||||
class="absolute w-auto -translate-x-1/2 transition-all duration-700 ease-out"
|
||||
:src="nodeSrc"
|
||||
:style="{ top: nodeTop, height: nodeHeight, left: `${progressPercent}%` }"
|
||||
alt=""
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="absolute aspect-square -translate-x-1/2 rounded-full border-[6px] border-[#31effa] bg-white/20 transition-all duration-700 ease-out"
|
||||
:style="{ top: nodeTop, height: nodeHeight, left: `${progressPercent}%` }"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="showPercent"
|
||||
class="absolute -translate-x-1/2 whitespace-nowrap"
|
||||
:class="percentClass"
|
||||
:style="{ ...percentStyle, top: percentTop, left: `${progressPercent}%` }"
|
||||
>
|
||||
{{ Math.round(progressPercent) }}%
|
||||
</div>
|
||||
<div
|
||||
v-if="showPercent"
|
||||
class="absolute right-0"
|
||||
:class="percentClass"
|
||||
:style="{ ...percentStyle, top: percentTop }"
|
||||
>
|
||||
100%
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as AnchoredProgressBar } from './AnchoredProgressBar.vue'
|
||||
export type * from './types'
|
||||
@@ -0,0 +1,38 @@
|
||||
export type ProgressAnchor = {
|
||||
id: string
|
||||
label: string
|
||||
value: number
|
||||
labelPosition?: 'top' | 'bottom'
|
||||
children?: ProgressAnchor[]
|
||||
}
|
||||
|
||||
export type AnchoredProgressBarProps = {
|
||||
progress: number
|
||||
anchors: ProgressAnchor[]
|
||||
min?: number
|
||||
max?: number
|
||||
showPercent?: boolean
|
||||
nodeImage?: string
|
||||
trackClass?: string
|
||||
fillClass?: string
|
||||
markerClass?: string
|
||||
labelClass?: string
|
||||
childLabelClass?: string
|
||||
percentClass?: string
|
||||
labelTop?: string
|
||||
childLabelTop?: string
|
||||
barTop?: string
|
||||
barHeight?: string
|
||||
markerTop?: string
|
||||
markerHeight?: string
|
||||
markerWidth?: string
|
||||
childMarkerTop?: string
|
||||
childMarkerHeight?: string
|
||||
childMarkerWidth?: string
|
||||
nodeTop?: string
|
||||
nodeHeight?: string
|
||||
percentTop?: string
|
||||
labelStyle?: Record<string, string>
|
||||
childLabelStyle?: Record<string, string>
|
||||
percentStyle?: Record<string, string>
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<img
|
||||
v-else-if="moduleName === 'interview'"
|
||||
:src="getStaticImage('midterm/中期拍摄')"
|
||||
:src="getStaticImage('interview/制作变焦镜-讲述我的育人故事')"
|
||||
alt="制作变焦镜-我的志愿者手记"
|
||||
class="title-image"
|
||||
/>
|
||||
|
||||
@@ -10,6 +10,7 @@ export * from './Guide'
|
||||
|
||||
// 进度条组件
|
||||
export * from './ProgressBar'
|
||||
export * from './AnchoredProgressBar'
|
||||
|
||||
// 切换组件
|
||||
export * from './Switcher'
|
||||
|
||||
@@ -97,7 +97,7 @@ const Interview: RouteRecordRaw[] = [
|
||||
},
|
||||
{
|
||||
path: '/interview/filmingLocation-b',
|
||||
name: 'InterviewFilmingLocationA',
|
||||
name: 'InterviewFilmingLocationB',
|
||||
component: () =>
|
||||
import('@/views/interview/midterm-shooting/shooting/filmingLocation-b/index.vue'),
|
||||
meta: {
|
||||
@@ -113,14 +113,6 @@ const Interview: RouteRecordRaw[] = [
|
||||
title: '剪辑'
|
||||
}
|
||||
},
|
||||
// {
|
||||
// path: '/interview/adjustmentColor',
|
||||
// name: 'InterviewAdjustmentColor',
|
||||
// component: () => import('@/views/interview/after-editing/adjustmentColor.vue'),
|
||||
// meta: {
|
||||
// title: '调色'
|
||||
// }
|
||||
// },
|
||||
{
|
||||
path: '/interview/color-practice',
|
||||
name: 'InterviewColorPractice',
|
||||
|
||||
@@ -159,14 +159,6 @@ const Vlog: RouteRecordRaw[] = [
|
||||
title: '后期剪辑-剪辑'
|
||||
}
|
||||
},
|
||||
// {
|
||||
// path: '/vlog/adjustmentColor',
|
||||
// name: 'VlogAdjustmentColor',
|
||||
// component: () => import('@/views/vlog/after-editing/adjustmentColor.vue'),
|
||||
// meta: {
|
||||
// title: '后期剪辑-调色'
|
||||
// }
|
||||
// },
|
||||
{
|
||||
path: '/vlog/color',
|
||||
name: 'VlogColor',
|
||||
@@ -175,6 +167,14 @@ const Vlog: RouteRecordRaw[] = [
|
||||
title: '后期剪辑-调色'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/vlog/color-practice',
|
||||
name: 'VlogColorPractice',
|
||||
component: () => import('@/views/vlog/after-editing/color-practice.vue'),
|
||||
meta: {
|
||||
title: '后期剪辑-调色实践'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/vlog/export-publish',
|
||||
name: 'VlogExportPublish',
|
||||
|
||||
@@ -9,6 +9,7 @@ interface BasicElementsSavePayload {
|
||||
isGuideBasicElementsTask?: boolean
|
||||
isBasicElementsUnlock?: boolean
|
||||
basicElementsUnlockProgress?: number
|
||||
guideShown?: Record<string, boolean>
|
||||
}
|
||||
|
||||
async function getYibanApi(): Promise<typeof import('@/utils/yiban') | null> {
|
||||
@@ -36,6 +37,7 @@ export const useBasicElementsStore = defineStore(
|
||||
const isBasicElementsUnlock = ref<boolean>(false)
|
||||
//basic-elements-发现与记录解锁进度
|
||||
const basicElementsUnlockProgress = ref<number>(0)
|
||||
const guideShown = ref<Record<string, boolean>>({})
|
||||
|
||||
//Actions
|
||||
function saveBasicElementsStore(): void {
|
||||
@@ -43,7 +45,8 @@ export const useBasicElementsStore = defineStore(
|
||||
isGuideNewUser: isGuideNewUser.value,
|
||||
isGuideBasicElementsTask: isGuideBasicElementsTask.value,
|
||||
isBasicElementsUnlock: isBasicElementsUnlock.value,
|
||||
basicElementsUnlockProgress: basicElementsUnlockProgress.value
|
||||
basicElementsUnlockProgress: basicElementsUnlockProgress.value,
|
||||
guideShown: guideShown.value
|
||||
}
|
||||
|
||||
getYibanApi()
|
||||
@@ -66,6 +69,7 @@ export const useBasicElementsStore = defineStore(
|
||||
isGuideBasicElementsTask.value = data.isGuideBasicElementsTask ?? true
|
||||
isBasicElementsUnlock.value = data.isBasicElementsUnlock ?? false
|
||||
basicElementsUnlockProgress.value = data.basicElementsUnlockProgress ?? 0
|
||||
guideShown.value = data.guideShown ?? {}
|
||||
} catch (error) {
|
||||
console.error('Failed to load Basic Elements store', error)
|
||||
}
|
||||
@@ -91,11 +95,18 @@ export const useBasicElementsStore = defineStore(
|
||||
saveBasicElementsStore()
|
||||
}
|
||||
|
||||
function markGuideShown(key: string): void {
|
||||
if (!key) return
|
||||
guideShown.value[key] = true
|
||||
saveBasicElementsStore()
|
||||
}
|
||||
|
||||
function resetBasicElementsState(): void {
|
||||
isGuideNewUser.value = true
|
||||
isGuideBasicElementsTask.value = true
|
||||
isBasicElementsUnlock.value = false
|
||||
basicElementsUnlockProgress.value = 0
|
||||
guideShown.value = {}
|
||||
saveBasicElementsStore()
|
||||
}
|
||||
|
||||
@@ -105,6 +116,7 @@ export const useBasicElementsStore = defineStore(
|
||||
setIsGuideBasicElementsTask,
|
||||
setIsBasicElementsUnlock,
|
||||
setBasicElementsUnlockProgress,
|
||||
markGuideShown,
|
||||
saveBasicElementsStore,
|
||||
loadBasicElementsStore,
|
||||
resetBasicElementsState,
|
||||
@@ -112,7 +124,8 @@ export const useBasicElementsStore = defineStore(
|
||||
isGuideNewUser,
|
||||
isGuideBasicElementsTask,
|
||||
isBasicElementsUnlock,
|
||||
basicElementsUnlockProgress
|
||||
basicElementsUnlockProgress,
|
||||
guideShown
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,11 +8,26 @@ export const FILM_KEY = 4
|
||||
interface FilmSavePayload {
|
||||
viewed?: Record<string, boolean>
|
||||
isGuideFilmTask?: boolean
|
||||
hasShownGuideFilmTask?: boolean
|
||||
isGuideFilmReturn?: boolean
|
||||
isGuideFilmStage2?: boolean
|
||||
isGuideFilmStage3?: boolean
|
||||
isFilmUnlock?: boolean
|
||||
FilmUnlockProgress?: number
|
||||
filmMenuVisible?: boolean
|
||||
editingVideosWatched?: Record<string, boolean>
|
||||
isGuideFilmEditingComplete?: boolean
|
||||
guideShown?: Record<string, boolean>
|
||||
}
|
||||
|
||||
export const FILM_EDITING_VIDEO_KEYS = [
|
||||
'start',
|
||||
'rhythm',
|
||||
'soundTransition',
|
||||
'cut',
|
||||
'association',
|
||||
'continuity'
|
||||
] as const
|
||||
|
||||
async function getYibanApi(): Promise<typeof import('@/utils/yiban') | null> {
|
||||
try {
|
||||
return await import('@/utils/yiban')
|
||||
@@ -38,20 +53,38 @@ export const useFilmStore = defineStore(
|
||||
|
||||
//Film-任务取景框引导
|
||||
const isGuideFilmTask = ref<boolean>(true)
|
||||
const hasShownGuideFilmTask = ref<boolean>(false)
|
||||
//Film-思路提点完成回到首页的一次性引导
|
||||
const isGuideFilmReturn = ref<boolean>(true)
|
||||
//Film-中期拍摄阶段一次性引导
|
||||
const isGuideFilmStage2 = ref<boolean>(true)
|
||||
//Film-后期剪辑阶段一次性引导
|
||||
const isGuideFilmStage3 = ref<boolean>(true)
|
||||
//Film-是否解锁完成前置(观看视频)
|
||||
const isFilmUnlock = ref<boolean>(false)
|
||||
//Film-发现与记录解锁进度
|
||||
const FilmUnlockProgress = ref<number>(0)
|
||||
//Film-菜单 Overlay 是否可见
|
||||
const filmMenuVisible = ref<boolean>(false)
|
||||
//Film-后期剪辑6个子视频已观看记录
|
||||
const editingVideosWatched = ref<Record<string, boolean>>({})
|
||||
//Film-后期剪辑6个视频学完后的一次性引导
|
||||
const isGuideFilmEditingComplete = ref<boolean>(true)
|
||||
const guideShown = ref<Record<string, boolean>>({})
|
||||
|
||||
//Actions
|
||||
function saveFilmStore(): void {
|
||||
const payload: FilmSavePayload = {
|
||||
viewed: viewed.value,
|
||||
isGuideFilmTask: isGuideFilmTask.value,
|
||||
hasShownGuideFilmTask: hasShownGuideFilmTask.value,
|
||||
isGuideFilmReturn: isGuideFilmReturn.value,
|
||||
isGuideFilmStage2: isGuideFilmStage2.value,
|
||||
isGuideFilmStage3: isGuideFilmStage3.value,
|
||||
isFilmUnlock: isFilmUnlock.value,
|
||||
FilmUnlockProgress: FilmUnlockProgress.value
|
||||
FilmUnlockProgress: FilmUnlockProgress.value,
|
||||
filmMenuVisible: filmMenuVisible.value,
|
||||
editingVideosWatched: editingVideosWatched.value,
|
||||
isGuideFilmEditingComplete: isGuideFilmEditingComplete.value,
|
||||
guideShown: guideShown.value
|
||||
}
|
||||
|
||||
getYibanApi()
|
||||
@@ -70,9 +103,15 @@ export const useFilmStore = defineStore(
|
||||
const data = JSON.parse(raw) as FilmSavePayload
|
||||
viewed.value = data.viewed ?? {}
|
||||
isGuideFilmTask.value = data.isGuideFilmTask ?? true
|
||||
hasShownGuideFilmTask.value = data.hasShownGuideFilmTask ?? false
|
||||
isGuideFilmReturn.value = data.isGuideFilmReturn ?? true
|
||||
isGuideFilmStage2.value = data.isGuideFilmStage2 ?? true
|
||||
isGuideFilmStage3.value = data.isGuideFilmStage3 ?? true
|
||||
isFilmUnlock.value = data.isFilmUnlock ?? false
|
||||
FilmUnlockProgress.value = data.FilmUnlockProgress ?? 0
|
||||
filmMenuVisible.value = data.filmMenuVisible ?? false
|
||||
editingVideosWatched.value = data.editingVideosWatched ?? {}
|
||||
isGuideFilmEditingComplete.value = data.isGuideFilmEditingComplete ?? true
|
||||
guideShown.value = data.guideShown ?? {}
|
||||
} catch (error) {
|
||||
console.error('Failed to load Film store', error)
|
||||
}
|
||||
@@ -104,11 +143,48 @@ export const useFilmStore = defineStore(
|
||||
saveFilmStore()
|
||||
}
|
||||
|
||||
function setHasShownGuideFilmTask(value: boolean): void {
|
||||
hasShownGuideFilmTask.value = value
|
||||
function setIsGuideFilmReturn(value: boolean): void {
|
||||
isGuideFilmReturn.value = value
|
||||
saveFilmStore()
|
||||
}
|
||||
|
||||
function setIsGuideFilmStage2(value: boolean): void {
|
||||
isGuideFilmStage2.value = value
|
||||
saveFilmStore()
|
||||
}
|
||||
|
||||
function setIsGuideFilmStage3(value: boolean): void {
|
||||
isGuideFilmStage3.value = value
|
||||
saveFilmStore()
|
||||
}
|
||||
|
||||
function setFilmMenuVisible(value: boolean): void {
|
||||
filmMenuVisible.value = value
|
||||
saveFilmStore()
|
||||
}
|
||||
|
||||
function markEditingVideoWatched(key: string): void {
|
||||
if (!key) return
|
||||
if (editingVideosWatched.value[key]) return
|
||||
editingVideosWatched.value[key] = true
|
||||
saveFilmStore()
|
||||
}
|
||||
|
||||
function setIsGuideFilmEditingComplete(value: boolean): void {
|
||||
isGuideFilmEditingComplete.value = value
|
||||
saveFilmStore()
|
||||
}
|
||||
|
||||
function markGuideShown(key: string): void {
|
||||
if (!key) return
|
||||
guideShown.value[key] = true
|
||||
saveFilmStore()
|
||||
}
|
||||
|
||||
const allEditingVideosWatched = computed<boolean>(() =>
|
||||
FILM_EDITING_VIDEO_KEYS.every(key => editingVideosWatched.value[key] === true)
|
||||
)
|
||||
|
||||
// 基于“子页面是否已查看”的大步骤完成度(1..3)
|
||||
// disabled 子项不计入(在这里直接不放进列表)
|
||||
const stepRouteNames = moduleStepMap.film
|
||||
@@ -135,9 +211,15 @@ export const useFilmStore = defineStore(
|
||||
function resetFilmState(): void {
|
||||
viewed.value = {}
|
||||
isGuideFilmTask.value = true
|
||||
hasShownGuideFilmTask.value = false
|
||||
isGuideFilmReturn.value = true
|
||||
isGuideFilmStage2.value = true
|
||||
isGuideFilmStage3.value = true
|
||||
isFilmUnlock.value = false
|
||||
FilmUnlockProgress.value = 0
|
||||
filmMenuVisible.value = false
|
||||
editingVideosWatched.value = {}
|
||||
isGuideFilmEditingComplete.value = true
|
||||
guideShown.value = {}
|
||||
saveFilmStore()
|
||||
}
|
||||
|
||||
@@ -147,7 +229,13 @@ export const useFilmStore = defineStore(
|
||||
setIsGuideFilmTask,
|
||||
setIsFilmUnlock,
|
||||
setFilmUnlockProgress,
|
||||
setHasShownGuideFilmTask,
|
||||
setIsGuideFilmReturn,
|
||||
setIsGuideFilmStage2,
|
||||
setIsGuideFilmStage3,
|
||||
setFilmMenuVisible,
|
||||
markEditingVideoWatched,
|
||||
setIsGuideFilmEditingComplete,
|
||||
markGuideShown,
|
||||
saveFilmStore,
|
||||
loadFilmStore,
|
||||
resetFilmState,
|
||||
@@ -158,7 +246,14 @@ export const useFilmStore = defineStore(
|
||||
isGuideFilmTask,
|
||||
isFilmUnlock,
|
||||
FilmUnlockProgress,
|
||||
hasShownGuideFilmTask
|
||||
isGuideFilmReturn,
|
||||
isGuideFilmStage2,
|
||||
isGuideFilmStage3,
|
||||
filmMenuVisible,
|
||||
editingVideosWatched,
|
||||
isGuideFilmEditingComplete,
|
||||
guideShown,
|
||||
allEditingVideosWatched
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,8 +8,13 @@ export const INTERVIEW_KEY = 3
|
||||
interface InterviewSavePayload {
|
||||
viewed?: Record<string, boolean>
|
||||
isGuideInterviewTask?: boolean
|
||||
isGuideInterviewReturn?: boolean
|
||||
isGuideInterviewStage2?: boolean
|
||||
isGuideInterviewStage3?: boolean
|
||||
isInterviewUnlock?: boolean
|
||||
InterviewUnlockProgress?: number
|
||||
interviewMenuVisible?: boolean
|
||||
guideShown?: Record<string, boolean>
|
||||
}
|
||||
|
||||
async function getYibanApi(): Promise<typeof import('@/utils/yiban') | null> {
|
||||
@@ -37,18 +42,32 @@ export const useInterviewStore = defineStore(
|
||||
|
||||
//Interview-任务取景框引导
|
||||
const isGuideInterviewTask = ref<boolean>(true)
|
||||
//Interview-思路提点完成回到首页的一次性引导
|
||||
const isGuideInterviewReturn = ref<boolean>(true)
|
||||
//Interview-中期拍摄阶段菜单一次性引导
|
||||
const isGuideInterviewStage2 = ref<boolean>(true)
|
||||
//Interview-后期剪辑阶段菜单一次性引导
|
||||
const isGuideInterviewStage3 = ref<boolean>(true)
|
||||
//Interview-是否解锁完成前置(观看视频)
|
||||
const isInterviewUnlock = ref<boolean>(false)
|
||||
//Interview-发现与记录解锁进度
|
||||
const InterviewUnlockProgress = ref<number>(0)
|
||||
//Interview-菜单 Overlay 是否可见
|
||||
const interviewMenuVisible = ref<boolean>(false)
|
||||
const guideShown = ref<Record<string, boolean>>({})
|
||||
|
||||
//Actions
|
||||
function saveInterviewStore(): void {
|
||||
const payload: InterviewSavePayload = {
|
||||
viewed: viewed.value,
|
||||
isGuideInterviewTask: isGuideInterviewTask.value,
|
||||
isGuideInterviewReturn: isGuideInterviewReturn.value,
|
||||
isGuideInterviewStage2: isGuideInterviewStage2.value,
|
||||
isGuideInterviewStage3: isGuideInterviewStage3.value,
|
||||
isInterviewUnlock: isInterviewUnlock.value,
|
||||
InterviewUnlockProgress: InterviewUnlockProgress.value
|
||||
InterviewUnlockProgress: InterviewUnlockProgress.value,
|
||||
interviewMenuVisible: interviewMenuVisible.value,
|
||||
guideShown: guideShown.value
|
||||
}
|
||||
|
||||
getYibanApi()
|
||||
@@ -69,8 +88,13 @@ export const useInterviewStore = defineStore(
|
||||
const data = JSON.parse(raw) as InterviewSavePayload
|
||||
viewed.value = data.viewed ?? {}
|
||||
isGuideInterviewTask.value = data.isGuideInterviewTask ?? true
|
||||
isGuideInterviewReturn.value = data.isGuideInterviewReturn ?? true
|
||||
isGuideInterviewStage2.value = data.isGuideInterviewStage2 ?? true
|
||||
isGuideInterviewStage3.value = data.isGuideInterviewStage3 ?? true
|
||||
isInterviewUnlock.value = data.isInterviewUnlock ?? false
|
||||
InterviewUnlockProgress.value = data.InterviewUnlockProgress ?? 0
|
||||
interviewMenuVisible.value = data.interviewMenuVisible ?? false
|
||||
guideShown.value = data.guideShown ?? {}
|
||||
} catch (error) {
|
||||
console.error('Failed to load Interview store', error)
|
||||
}
|
||||
@@ -112,6 +136,21 @@ export const useInterviewStore = defineStore(
|
||||
saveInterviewStore()
|
||||
}
|
||||
|
||||
function setIsGuideInterviewReturn(value: boolean): void {
|
||||
isGuideInterviewReturn.value = value
|
||||
saveInterviewStore()
|
||||
}
|
||||
|
||||
function setIsGuideInterviewStage2(value: boolean): void {
|
||||
isGuideInterviewStage2.value = value
|
||||
saveInterviewStore()
|
||||
}
|
||||
|
||||
function setIsGuideInterviewStage3(value: boolean): void {
|
||||
isGuideInterviewStage3.value = value
|
||||
saveInterviewStore()
|
||||
}
|
||||
|
||||
function setIsInterviewUnlock(value: boolean): void {
|
||||
isInterviewUnlock.value = value
|
||||
saveInterviewStore()
|
||||
@@ -123,11 +162,27 @@ export const useInterviewStore = defineStore(
|
||||
saveInterviewStore()
|
||||
}
|
||||
|
||||
function setInterviewMenuVisible(value: boolean): void {
|
||||
interviewMenuVisible.value = value
|
||||
saveInterviewStore()
|
||||
}
|
||||
|
||||
function markGuideShown(key: string): void {
|
||||
if (!key) return
|
||||
guideShown.value[key] = true
|
||||
saveInterviewStore()
|
||||
}
|
||||
|
||||
function resetInterviewState(): void {
|
||||
viewed.value = {}
|
||||
isGuideInterviewTask.value = true
|
||||
isGuideInterviewReturn.value = true
|
||||
isGuideInterviewStage2.value = true
|
||||
isGuideInterviewStage3.value = true
|
||||
isInterviewUnlock.value = false
|
||||
InterviewUnlockProgress.value = 0
|
||||
interviewMenuVisible.value = false
|
||||
guideShown.value = {}
|
||||
saveInterviewStore()
|
||||
}
|
||||
|
||||
@@ -135,8 +190,13 @@ export const useInterviewStore = defineStore(
|
||||
//State
|
||||
markViewed,
|
||||
setIsGuideInterviewTask,
|
||||
setIsGuideInterviewReturn,
|
||||
setIsGuideInterviewStage2,
|
||||
setIsGuideInterviewStage3,
|
||||
setIsInterviewUnlock,
|
||||
setInterviewUnlockProgress,
|
||||
setInterviewMenuVisible,
|
||||
markGuideShown,
|
||||
resetInterviewState,
|
||||
loadInterviewStore,
|
||||
saveInterviewStore,
|
||||
@@ -145,8 +205,13 @@ export const useInterviewStore = defineStore(
|
||||
progressPercent,
|
||||
viewed,
|
||||
isGuideInterviewTask,
|
||||
isGuideInterviewReturn,
|
||||
isGuideInterviewStage2,
|
||||
isGuideInterviewStage3,
|
||||
isInterviewUnlock,
|
||||
InterviewUnlockProgress
|
||||
InterviewUnlockProgress,
|
||||
interviewMenuVisible,
|
||||
guideShown
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ interface VlogSavePayload {
|
||||
isVlogUnlock?: boolean
|
||||
vlogUnlockProgress?: number
|
||||
vlogMenuVisible?: boolean
|
||||
guideShown?: Record<string, boolean>
|
||||
}
|
||||
|
||||
async function getYibanApi(): Promise<typeof import('@/utils/yiban') | null> {
|
||||
@@ -58,6 +59,7 @@ export const useVlogStore = defineStore(
|
||||
|
||||
//vlog-发现与记录解锁进度
|
||||
const vlogUnlockProgress = ref<number>(0)
|
||||
const guideShown = ref<Record<string, boolean>>({})
|
||||
|
||||
//Actions
|
||||
function saveVlogStore(): void {
|
||||
@@ -70,7 +72,8 @@ export const useVlogStore = defineStore(
|
||||
isGuideVlogStage3: isGuideVlogStage3.value,
|
||||
isVlogUnlock: isVlogUnlock.value,
|
||||
vlogUnlockProgress: vlogUnlockProgress.value,
|
||||
vlogMenuVisible: vlogMenuVisible.value
|
||||
vlogMenuVisible: vlogMenuVisible.value,
|
||||
guideShown: guideShown.value
|
||||
}
|
||||
|
||||
getYibanApi()
|
||||
@@ -96,6 +99,7 @@ export const useVlogStore = defineStore(
|
||||
isVlogUnlock.value = data.isVlogUnlock ?? false
|
||||
vlogUnlockProgress.value = data.vlogUnlockProgress ?? 0
|
||||
vlogMenuVisible.value = data.vlogMenuVisible ?? false
|
||||
guideShown.value = data.guideShown ?? {}
|
||||
} catch (error) {
|
||||
console.error('Failed to load Vlog store', error)
|
||||
}
|
||||
@@ -152,6 +156,12 @@ export const useVlogStore = defineStore(
|
||||
saveVlogStore()
|
||||
}
|
||||
|
||||
function markGuideShown(key: string): void {
|
||||
if (!key) return
|
||||
guideShown.value[key] = true
|
||||
saveVlogStore()
|
||||
}
|
||||
|
||||
// 基于“子页面是否已查看”的大步骤完成度(1..3)
|
||||
// disabled 子项不计入(在这里直接不放进列表)
|
||||
const stepRouteNames = moduleStepMap.vlog
|
||||
@@ -184,6 +194,7 @@ export const useVlogStore = defineStore(
|
||||
isGuideVlogStage3.value = true
|
||||
isVlogUnlock.value = false
|
||||
vlogMenuVisible.value = false
|
||||
guideShown.value = {}
|
||||
saveVlogStore()
|
||||
}
|
||||
|
||||
@@ -201,6 +212,7 @@ export const useVlogStore = defineStore(
|
||||
loadVlogStore,
|
||||
resetVlogState,
|
||||
setVlogUnlockProgress,
|
||||
markGuideShown,
|
||||
//Actions
|
||||
completedStep,
|
||||
progressPercent,
|
||||
@@ -212,7 +224,8 @@ export const useVlogStore = defineStore(
|
||||
isGuideVlogStage3,
|
||||
isVlogUnlock,
|
||||
vlogUnlockProgress,
|
||||
vlogMenuVisible
|
||||
vlogMenuVisible,
|
||||
guideShown
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { useUserStore } from '@/stores'
|
||||
|
||||
declare const $: {
|
||||
extend(value: Record<string, unknown>): void
|
||||
toJSON(value: unknown): string
|
||||
parseJSON(value: string): unknown
|
||||
getUrlVars?: () => string[] & Record<string, string | undefined>
|
||||
getUrlVar?: (name: string) => string | undefined
|
||||
}
|
||||
// 声明外部依赖(由运行环境提供)
|
||||
declare const $: any
|
||||
declare const iscallyb: boolean
|
||||
declare const emykey: string
|
||||
declare function videoAjax(): VideoAjax
|
||||
@@ -14,6 +9,11 @@ declare function getFutureDa(days: number): string
|
||||
declare function getCurrentDa(str: string): void
|
||||
declare function formatDateFromMilliseconds(date: Date): string
|
||||
|
||||
// 本地或非易班环境下,外部全局变量可能未注入;统一用安全访问
|
||||
const isYibanRuntime = (): boolean => {
|
||||
return typeof iscallyb !== 'undefined' && iscallyb === true
|
||||
}
|
||||
|
||||
interface UserDefineParam {
|
||||
lessonId: string
|
||||
key?: string | number
|
||||
@@ -46,44 +46,48 @@ interface VideoAjax {
|
||||
finishCourse(param: { order: number; co: string; process: number }): void
|
||||
}
|
||||
|
||||
// 扩展 jQuery,获取 URL 参数
|
||||
$.extend({
|
||||
getUrlVars(): string[] & Record<string, string | undefined> {
|
||||
const vars = [] as unknown as string[] & Record<string, string | undefined>
|
||||
const vars: any[] & Record<string, string | undefined> = [] as any
|
||||
let hash: string[]
|
||||
const queryIndex = window.location.href.indexOf('?')
|
||||
if (queryIndex === -1) return vars
|
||||
|
||||
const hashes = window.location.href
|
||||
.slice(queryIndex + 1)
|
||||
.split('&')
|
||||
.filter(Boolean)
|
||||
|
||||
for (const item of hashes) {
|
||||
const [keyRaw = '', valueRaw = ''] = item.split('=')
|
||||
for (let i = 0; i < hashes.length; i++) {
|
||||
const item = hashes[i]
|
||||
if (!item) continue
|
||||
hash = item.split('=')
|
||||
const keyRaw = hash[0] ?? ''
|
||||
const valueRaw = hash[1] ?? ''
|
||||
const key = decodeURIComponent(keyRaw)
|
||||
const value = decodeURIComponent(valueRaw)
|
||||
vars.push(key)
|
||||
vars[key] = value
|
||||
}
|
||||
|
||||
return vars
|
||||
},
|
||||
getUrlVar(name: string): string | undefined {
|
||||
return $.getUrlVars?.()[name]
|
||||
return ($ as any).getUrlVars()[name]
|
||||
}
|
||||
})
|
||||
|
||||
const LESSON_PREFIX = 'dsp'
|
||||
|
||||
const videoajax: VideoAjax = videoAjax()
|
||||
videoajax.setHost('/')
|
||||
const videoajax: VideoAjax | null = typeof videoAjax !== 'undefined' ? videoAjax() : null
|
||||
videoajax?.setHost('/')
|
||||
|
||||
function normalizeLessonId(lessonId: string | number): string {
|
||||
const raw = String(lessonId)
|
||||
return raw.startsWith(`${LESSON_PREFIX}_`) ? raw : `${LESSON_PREFIX}_${raw}`
|
||||
}
|
||||
|
||||
// 设置我的自定义数据(所有的自定义数据都设置在第一个课时ID上,以key来区分不同的类型)
|
||||
function setLtDefine(key: string | number, value: string): void {
|
||||
if (iscallyb) {
|
||||
if (isYibanRuntime()) {
|
||||
setUserDefine(1, key, value)
|
||||
} else {
|
||||
sessionStorage.setItem(`${LESSON_PREFIX}_${key}`, value)
|
||||
@@ -92,46 +96,57 @@ function setLtDefine(key: string | number, value: string): void {
|
||||
|
||||
let mycstr9 = ''
|
||||
|
||||
// 获取定义数据(由于底层是异步接口,这里返回 Promise)
|
||||
async function getLtDefine(key: string | number): Promise<string> {
|
||||
if (iscallyb) {
|
||||
if (isYibanRuntime()) {
|
||||
if (mycstr9 === '') {
|
||||
const all = await getUsersDefine(1, emykey)
|
||||
if (all === undefined || all === null || all === 'null' || all === '') {
|
||||
mycstr9 = getFutureDa(10)
|
||||
setLtDefine(emykey, mycstr9)
|
||||
} else {
|
||||
mycstr9 = all as string
|
||||
mycstr9 = all as string // 这里是 emykey,对应上面 getUsersDefine 的 string 分支
|
||||
}
|
||||
}
|
||||
|
||||
const str = mycstr9 === undefined || mycstr9 === null || mycstr9 === 'null' ? '' : mycstr9
|
||||
getCurrentDa(str)
|
||||
const objitem = await getUserDefine(1, key)
|
||||
return objitem === undefined || objitem === null || objitem === 'null' ? '' : objitem
|
||||
}
|
||||
|
||||
} else {
|
||||
const itemvalue = sessionStorage.getItem(`${LESSON_PREFIX}_${key}`)
|
||||
return itemvalue === undefined || itemvalue === null || itemvalue === 'null' ? '' : itemvalue
|
||||
}
|
||||
}
|
||||
|
||||
export function setCourseNotepad(collect: string[]): void {
|
||||
// 设置课程笔记10002
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function setCourseNotepad(collect: string[]): void {
|
||||
// 保持和原逻辑一致:序列化为 JSON 存储
|
||||
setLtDefine(10002, $.toJSON(collect))
|
||||
}
|
||||
|
||||
export async function getCourseNotepad(): Promise<string[]> {
|
||||
// 获取课程笔记10002(异步)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async function getCourseNotepad(): Promise<string[]> {
|
||||
const raw = await getLtDefine(10002)
|
||||
if (raw === undefined || raw === null || raw === '') {
|
||||
// 原逻辑:返回长度为 9 的空字符串数组
|
||||
return ['', '', '', '', '', '', '', '', '']
|
||||
}
|
||||
|
||||
return $.parseJSON(raw) as string[]
|
||||
}
|
||||
|
||||
// lessonId(课时唯一ID)
|
||||
export function setUserDefine(
|
||||
lessonId: string | number,
|
||||
key: string | number,
|
||||
value: string
|
||||
): void {
|
||||
if (!isYibanRuntime() || !videoajax) {
|
||||
sessionStorage.setItem(`${normalizeLessonId(lessonId)}_${key}`, value)
|
||||
return
|
||||
}
|
||||
|
||||
const param: UserDefineParam = {
|
||||
lessonId: normalizeLessonId(lessonId),
|
||||
key,
|
||||
@@ -140,22 +155,31 @@ export function setUserDefine(
|
||||
videoajax.addUserDefine(param)
|
||||
}
|
||||
|
||||
// 内部:将回调形式封装为 Promise
|
||||
function getUserDefineAsyncInternal(
|
||||
lessonId: string | number,
|
||||
key: string | number
|
||||
): Promise<string> {
|
||||
if (!isYibanRuntime() || !videoajax) {
|
||||
const value = sessionStorage.getItem(`${normalizeLessonId(lessonId)}_${key}`)
|
||||
return Promise.resolve(value === null || value === 'null' ? '' : value)
|
||||
}
|
||||
|
||||
const param: UserDefineParam = {
|
||||
lessonId: normalizeLessonId(lessonId),
|
||||
order: key
|
||||
}
|
||||
|
||||
const ajax = videoajax
|
||||
return new Promise(resolve => {
|
||||
let content = ''
|
||||
videoajax.getUserDefineInfoByUser(param, res => {
|
||||
ajax.getUserDefineInfoByUser(param, res => {
|
||||
// 保留原有 console 行为
|
||||
console.log(res)
|
||||
if (res && res.code === 200) {
|
||||
const json = (res.data.value || []) as VideoAjaxResponseItem[]
|
||||
content = json[0]?.content || ''
|
||||
const contents = json[0]?.content || ''
|
||||
content = contents
|
||||
} else {
|
||||
content = ''
|
||||
}
|
||||
@@ -164,6 +188,7 @@ function getUserDefineAsyncInternal(
|
||||
})
|
||||
}
|
||||
|
||||
// Promise 版本:获取用户自定义数据
|
||||
export async function getUserDefine(
|
||||
lessonId: string | number,
|
||||
key: string | number
|
||||
@@ -171,18 +196,24 @@ export async function getUserDefine(
|
||||
return getUserDefineAsyncInternal(lessonId, key)
|
||||
}
|
||||
|
||||
// 内部:获取【所有】用户自定义数据(Promise 封装)
|
||||
function getUsersDefineAsyncInternal(
|
||||
lessonId: string | number,
|
||||
key: string | number
|
||||
): Promise<string | VideoAjaxResponseItem[]> {
|
||||
if (!isYibanRuntime() || !videoajax) {
|
||||
return Promise.resolve('')
|
||||
}
|
||||
|
||||
const param: UserDefineParam = {
|
||||
lessonId: normalizeLessonId(lessonId),
|
||||
order: key
|
||||
}
|
||||
|
||||
const ajax = videoajax
|
||||
return new Promise(resolve => {
|
||||
let content: string | VideoAjaxResponseItem[] = ''
|
||||
videoajax.getUserDefineInfo(param, data => {
|
||||
ajax.getUserDefineInfo(param, data => {
|
||||
if (data && data.code === 200) {
|
||||
const json = (data.data.value || []) as VideoAjaxResponseItem[]
|
||||
if (json.length > 1) {
|
||||
@@ -208,6 +239,7 @@ function getUsersDefineAsyncInternal(
|
||||
})
|
||||
}
|
||||
|
||||
// Promise 版本:获取【所有】用户自定义数据
|
||||
export async function getUsersDefine(
|
||||
lessonId: string | number,
|
||||
key: string | number
|
||||
@@ -215,12 +247,22 @@ export async function getUsersDefine(
|
||||
return getUsersDefineAsyncInternal(lessonId, key)
|
||||
}
|
||||
|
||||
// 根据课时顺序号(所处课程的第几课时)更新对应易班平台上的每个环节进度(异步)
|
||||
export async function setCourseTimeProgress(order: number): Promise<void> {
|
||||
const courseid = useUserStore().courseId
|
||||
// var courseid = getCookie(lesson+"_course_id");//获取url链接的课程ID
|
||||
// const courseid = await getLtDefine('10000')
|
||||
console.info(`courseid:${courseid} order:${order}`)
|
||||
if (iscallyb && courseid !== undefined && courseid !== null && courseid !== '') {
|
||||
if (
|
||||
isYibanRuntime() &&
|
||||
videoajax &&
|
||||
courseid !== undefined &&
|
||||
courseid !== null &&
|
||||
courseid !== ''
|
||||
) {
|
||||
if (order > 0) {
|
||||
videoajax.finishCourse({ order, co: courseid, process: 100 })
|
||||
const param2 = { order, co: courseid, process: 100 }
|
||||
videoajax.finishCourse(param2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,21 +5,16 @@ import ImgButton from '@/components/Button/ImgButton.vue'
|
||||
import { StepNavigation, GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { Video, QuestionQuiz, type Question } from '@/components'
|
||||
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
// 视频导入
|
||||
// 手机摄像头介绍
|
||||
import video1 from '/video/new/xinshou_shoujishexiangtoujieshao.mp4'
|
||||
// 景别概念介绍 - 暂时用其他视频代替
|
||||
import video2 from '/video/new/xsdh_jbgnjs_batch.mp4'
|
||||
// 手机慢动作 - 暂时用其他视频代替
|
||||
import video3 from '/video/new/xsdh_sjmdz_batch.mp4'
|
||||
// 延时摄影 - 暂时用其他视频代替
|
||||
import video4 from '/video/new/xsdh_yssy_batch.mp4'
|
||||
// 神奇的微距摄影
|
||||
import video5 from '/video/new/xsdh_sqdwjsy_batch.mp4'
|
||||
// 夜景摄影 - 暂时用其他视频代替
|
||||
import video6 from '/video/new/xsdh_djmldyjsy_batch.mp4'
|
||||
import { useBasicElementsStore } from '@/stores/modules/basic-elements'
|
||||
|
||||
const router = useRouter()
|
||||
const basicElementsStore = useBasicElementsStore()
|
||||
const introGuideKey = 'basic-shooting-skills-intro'
|
||||
const showIntroGuide = ref(!basicElementsStore.guideShown[introGuideKey])
|
||||
|
||||
if (showIntroGuide.value) {
|
||||
basicElementsStore.markGuideShown(introGuideKey)
|
||||
}
|
||||
|
||||
// 受控播放开关(用于切换到下一个视频时自动播放)
|
||||
const playing = ref(false)
|
||||
@@ -32,36 +27,42 @@ const rawSteps = [
|
||||
id: 'step2',
|
||||
label: '摄影基础',
|
||||
content: '手机摄像头介绍',
|
||||
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
|
||||
},
|
||||
{
|
||||
id: 'step4',
|
||||
label: '摄影基础',
|
||||
content: '手机慢动作',
|
||||
videoUrl: getPublicPath('video/new/shooting-skills/xsdh_sjmdz_batch.mp4'),
|
||||
type: 'post' as const
|
||||
},
|
||||
{
|
||||
id: 'step5',
|
||||
label: '摄影基础',
|
||||
content: '延时摄影',
|
||||
videoUrl: getPublicPath('video/new/shooting-skills/xsdh_yssy_batch.mp4'),
|
||||
type: 'post' as const
|
||||
},
|
||||
{
|
||||
id: 'step6',
|
||||
label: '摄影基础',
|
||||
content: '神奇的微距摄影',
|
||||
videoUrl: getPublicPath('video/new/shooting-skills/xsdh_sqdwjsy_batch.mp4'),
|
||||
type: 'post' as const
|
||||
},
|
||||
{
|
||||
id: 'step7',
|
||||
label: '摄影基础',
|
||||
content: '独具魅力的夜景摄影',
|
||||
videoUrl: getPublicPath('video/new/shooting-skills/xsdh_djmldyjsy_batch.mp4'),
|
||||
type: 'post' as const
|
||||
}
|
||||
]
|
||||
@@ -73,9 +74,11 @@ const showStepNavigation = ref(false)
|
||||
const currentNavigationStep = ref(0) // 对应视频索引,从0开始
|
||||
|
||||
// 视频列表
|
||||
const videoList = [video1, video2, video3, video4, video5, video6]
|
||||
const currentVideoIndex = ref(0)
|
||||
|
||||
//按顺序播放
|
||||
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
|
||||
|
||||
// 题目数据:景别概念介绍(index 1)、手机慢动作(index 2)、独具魅力的夜景摄影(index 5)
|
||||
const questions = ref<Question[]>([])
|
||||
|
||||
@@ -172,9 +175,8 @@ const getQuestionsForVideo = (index: number): Question[] => {
|
||||
}
|
||||
|
||||
//按顺序播放
|
||||
const currentVideo = computed(() => videoList[currentVideoIndex.value])
|
||||
const handleVideoChange = () => {
|
||||
if (currentVideoIndex.value < videoList.length - 1) {
|
||||
if (currentVideoIndex.value < navigationSteps.value.length - 1) {
|
||||
currentVideoIndex.value++
|
||||
currentNavigationStep.value = currentVideoIndex.value
|
||||
} else {
|
||||
@@ -297,7 +299,11 @@ const handleQuestionsComplete = () => {
|
||||
@close="handleStepNavigationClose"
|
||||
/>
|
||||
|
||||
<GuideSteps :visible="true" position-offset-y="85%" @complete="handleIntroGuideComplete">
|
||||
<GuideSteps
|
||||
:visible="showIntroGuide"
|
||||
position-offset-y="85%"
|
||||
@complete="handleIntroGuideComplete"
|
||||
>
|
||||
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_018.MP3')">
|
||||
欢迎来到基础拍摄训练区!
|
||||
</GuideStepsItem>
|
||||
|
||||
@@ -4,14 +4,19 @@ import { useRouter } from 'vue-router'
|
||||
import ImgButton from '@/components/Button/ImgButton.vue'
|
||||
import { StepNavigation } from '@/components/Guide'
|
||||
import { Video } from '@/components'
|
||||
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
// 运镜初识 - 暂时用运镜的基本方式代替
|
||||
import video1 from '/video/new/xinshou_yunjingdejibenfangshi.mp4'
|
||||
import video2 from '/video/new/xinshou_zhengquedeshoujipaishe.mp4'
|
||||
import video3 from '/video/new/xinshou_yunjingdejibenfangshi.mp4'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { useBasicElementsStore } from '@/stores/modules/basic-elements'
|
||||
|
||||
const router = useRouter()
|
||||
const basicElementsStore = useBasicElementsStore()
|
||||
const introGuideKey = 'basic-camera-movement-intro'
|
||||
const showIntroGuide = ref(!basicElementsStore.guideShown[introGuideKey])
|
||||
|
||||
if (showIntroGuide.value) {
|
||||
basicElementsStore.markGuideShown(introGuideKey)
|
||||
}
|
||||
const props = defineProps<{
|
||||
label: string
|
||||
}>()
|
||||
@@ -43,18 +48,21 @@ const navigationSteps = ref([
|
||||
// id: 'step1',
|
||||
// label: '视觉素养',
|
||||
// content: '运镜初识',
|
||||
// videoUrl: getPublicPath('video/new/movement/xxx.mp4'),
|
||||
// type: 'planning' as const
|
||||
// },
|
||||
{
|
||||
id: 'step2',
|
||||
label: '视觉素养',
|
||||
content: '正确的手机拍摄持机姿势',
|
||||
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
|
||||
}
|
||||
])
|
||||
@@ -64,13 +72,12 @@ const showStepNavigation = ref(false)
|
||||
const currentNavigationStep = ref(0) // 对应视频索引,从0开始
|
||||
|
||||
// 视频列表
|
||||
const videoList = [video2, video3]
|
||||
const currentVideoIndex = ref(0)
|
||||
|
||||
//按顺序播放
|
||||
const currentVideo = computed(() => videoList[currentVideoIndex.value])
|
||||
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
|
||||
const handleVideoChange = () => {
|
||||
if (currentVideoIndex.value < videoList.length - 1) {
|
||||
if (currentVideoIndex.value < navigationSteps.value.length - 1) {
|
||||
currentVideoIndex.value++
|
||||
currentNavigationStep.value = currentVideoIndex.value
|
||||
} else {
|
||||
@@ -170,7 +177,11 @@ const handleStepNavigationClose = () => {
|
||||
@close="handleStepNavigationClose"
|
||||
/>
|
||||
|
||||
<GuideSteps :visible="true" position-offset-y="85%" @complete="handleIntroGuideComplete">
|
||||
<GuideSteps
|
||||
:visible="showIntroGuide"
|
||||
position-offset-y="85%"
|
||||
@complete="handleIntroGuideComplete"
|
||||
>
|
||||
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_014.MP3')">
|
||||
想拍出好的视频效果,画面稳定性是最基本的要求。
|
||||
</GuideStepsItem>
|
||||
|
||||
@@ -4,11 +4,17 @@ import { useRouter } from 'vue-router'
|
||||
import ImgButton from '@/components/Button/ImgButton.vue'
|
||||
import { StepNavigation } from '@/components/Guide'
|
||||
import { Video } from '@/components'
|
||||
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import video1 from '/video/new/xsdh_rhxzgt_batch.mp4'
|
||||
import video2 from '/video/new/xsdh_jzgtf_batch.mp4'
|
||||
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { useBasicElementsStore } from '@/stores/modules/basic-elements'
|
||||
const router = useRouter()
|
||||
const basicElementsStore = useBasicElementsStore()
|
||||
const introGuideKey = 'basic-composition-intro'
|
||||
const showIntroGuide = ref(!basicElementsStore.guideShown[introGuideKey])
|
||||
|
||||
if (showIntroGuide.value) {
|
||||
basicElementsStore.markGuideShown(introGuideKey)
|
||||
}
|
||||
const props = defineProps<{
|
||||
label: string
|
||||
}>()
|
||||
@@ -25,12 +31,14 @@ const navigationSteps = ref([
|
||||
id: 'step1',
|
||||
label: '视觉素养',
|
||||
content: '如何选择构图',
|
||||
videoUrl: getPublicPath('video/new/composition/xsdh_rhxzgt_batch.mp4'),
|
||||
type: 'planning' as const
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
label: '视觉素养',
|
||||
content: '九种经典构图方式',
|
||||
videoUrl: getPublicPath('video/new/composition/xsdh_jzgtf_batch.mp4'),
|
||||
type: 'planning' as const
|
||||
}
|
||||
])
|
||||
@@ -40,13 +48,12 @@ const showStepNavigation = ref(false)
|
||||
const currentNavigationStep = ref(0) // 对应视频索引,从0开始
|
||||
|
||||
// 视频列表
|
||||
const videoList = [video1, video2]
|
||||
const currentVideoIndex = ref(0)
|
||||
|
||||
//按顺序播放
|
||||
const currentVideo = computed(() => videoList[currentVideoIndex.value])
|
||||
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
|
||||
const handleVideoChange = () => {
|
||||
if (currentVideoIndex.value < videoList.length - 1) {
|
||||
if (currentVideoIndex.value < navigationSteps.value.length - 1) {
|
||||
currentVideoIndex.value++
|
||||
currentNavigationStep.value = currentVideoIndex.value
|
||||
} else {
|
||||
@@ -149,7 +156,7 @@ onMounted(() => {})
|
||||
/>
|
||||
|
||||
<GuideSteps
|
||||
:visible="true"
|
||||
:visible="showIntroGuide"
|
||||
:highlight-selector="guideStepIndex === 3 ? '.step-navigation-btn' : ''"
|
||||
position-offset-y="85%"
|
||||
@step-change="(_: any, index: number) => (guideStepIndex = index)"
|
||||
|
||||
@@ -6,20 +6,24 @@ import { StepNavigation } from '@/components/Guide'
|
||||
import { Video, QuestionQuiz, type Question } from '@/components'
|
||||
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { GuideSteps } from '@/components/Guide'
|
||||
// 光初识 - 暂时用光的基础认知代替
|
||||
import video1 from '/video/new/xinshou_guangdejichurenzhi.mp4'
|
||||
import video2 from '/video/new/xinshou_guangdesidashuxing.mp4'
|
||||
import video3 from '/video/new/xsdh_psdhjsj_batch.mp4'
|
||||
import GuideStepsItem from '@/components/Guide/GuideStepsItem.vue'
|
||||
import { useBasicElementsStore } from '@/stores/modules/basic-elements'
|
||||
|
||||
const router = useRouter()
|
||||
const basicElementsStore = useBasicElementsStore()
|
||||
const introGuideKey = 'basic-light-intro'
|
||||
const showIntroGuide = ref(!basicElementsStore.guideShown[introGuideKey])
|
||||
|
||||
if (showIntroGuide.value) {
|
||||
basicElementsStore.markGuideShown(introGuideKey)
|
||||
}
|
||||
|
||||
// 受控播放开关(用于切换到下一个视频时自动播放)
|
||||
const playing = ref(false)
|
||||
const status = ref(false)
|
||||
const showQuiz = ref(false) // 控制是否显示答题窗口
|
||||
|
||||
const isGuide = ref(true)
|
||||
const isGuide = showIntroGuide
|
||||
const currentGuideStep = ref(0)
|
||||
const showGoldenHourGuide = ref(false)
|
||||
const goldenHourGuideShown = ref(false)
|
||||
@@ -48,18 +52,21 @@ const navigationSteps = ref([
|
||||
id: 'step2',
|
||||
label: '视觉素养',
|
||||
content: '光的基础认知',
|
||||
videoUrl: getPublicPath('video/new/light/xinshou_guangdejichurenzhi.mp4'),
|
||||
type: 'planning' as const
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
label: '视觉素养',
|
||||
content: '光的四大属性',
|
||||
videoUrl: getPublicPath('video/new/light/xinshou_guangdesidashuxing.mp4'),
|
||||
type: 'planning' as const
|
||||
},
|
||||
{
|
||||
id: 'step4',
|
||||
label: '视觉素养',
|
||||
content: '拍摄的黄金时间',
|
||||
videoUrl: getPublicPath('video/new/light/xsdh_psdhjsj_batch.mp4'),
|
||||
type: 'planning' as const
|
||||
}
|
||||
])
|
||||
@@ -69,9 +76,11 @@ const showStepNavigation = ref(false)
|
||||
const currentNavigationStep = ref(0) // 对应视频索引,从0开始
|
||||
|
||||
// 视频列表
|
||||
const videoList = [video1, video2, video3]
|
||||
const currentVideoIndex = ref(0)
|
||||
|
||||
//按顺序播放
|
||||
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
|
||||
|
||||
// 题目数据:拍摄的黄金时间(index 2)
|
||||
const questions = ref<Question[]>([])
|
||||
|
||||
@@ -107,9 +116,8 @@ const getQuestionsForVideo = (index: number): Question[] => {
|
||||
}
|
||||
|
||||
//按顺序播放
|
||||
const currentVideo = computed(() => videoList[currentVideoIndex.value])
|
||||
const handleVideoChange = () => {
|
||||
if (currentVideoIndex.value < videoList.length - 1) {
|
||||
if (currentVideoIndex.value < navigationSteps.value.length - 1) {
|
||||
currentVideoIndex.value++
|
||||
currentNavigationStep.value = currentVideoIndex.value
|
||||
} else {
|
||||
|
||||
@@ -5,8 +5,16 @@ import ImgButton from '@/components/Button/ImgButton.vue'
|
||||
import { StepNavigation, GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { Video } from '@/components'
|
||||
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { useBasicElementsStore } from '@/stores/modules/basic-elements'
|
||||
|
||||
const router = useRouter()
|
||||
const basicElementsStore = useBasicElementsStore()
|
||||
const introGuideKey = 'basic-short-video-editing-skills-intro'
|
||||
const showIntroGuide = ref(!basicElementsStore.guideShown[introGuideKey])
|
||||
|
||||
if (showIntroGuide.value) {
|
||||
basicElementsStore.markGuideShown(introGuideKey)
|
||||
}
|
||||
|
||||
// 受控播放开关(用于切换到下一个视频时自动播放)
|
||||
const playing = ref(false)
|
||||
@@ -18,70 +26,70 @@ const rawSteps = [
|
||||
id: 'step2',
|
||||
label: '剪映基础操作',
|
||||
content: '初识剪映专业版界面和功能',
|
||||
videoUrl: getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
|
||||
videoUrl: getPublicPath('video/new/editing-skills/xsdh_csjyzybjmhgn_batch.mp4'),
|
||||
type: 'other' as const
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
label: '剪映基础操作',
|
||||
content: '时间线的用法详解',
|
||||
videoUrl: getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
|
||||
videoUrl: getPublicPath('video/new/editing-skills/xsdh_sjxdyfxj_batch.mp4'),
|
||||
type: 'other' as const
|
||||
},
|
||||
{
|
||||
id: 'step4',
|
||||
label: '剪映基础操作',
|
||||
content: '转场特效贴纸功能详解',
|
||||
videoUrl: getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
|
||||
videoUrl: getPublicPath('video/new/editing-skills/xsdh_zctxtzgnxj_batch.mp4'),
|
||||
type: 'other' as const
|
||||
},
|
||||
{
|
||||
id: 'step5',
|
||||
label: '剪映基础操作',
|
||||
content: '如何给视频快速上字幕',
|
||||
videoUrl: getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
|
||||
videoUrl: getPublicPath('video/new/editing-skills/xsdh_rhgspksszm_batch.mp4'),
|
||||
type: 'other' as const
|
||||
},
|
||||
{
|
||||
id: 'step6',
|
||||
label: '剪映基础操作',
|
||||
content: '音频的功能应用和处理',
|
||||
videoUrl: getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
|
||||
videoUrl: getPublicPath('video/new/editing-skills/xsdh_ypdgnyyhcl_batch.mp4'),
|
||||
type: 'other' as const
|
||||
},
|
||||
{
|
||||
id: 'step7',
|
||||
label: '剪映基础操作',
|
||||
content: '关键帧功能用法详解',
|
||||
videoUrl: getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
|
||||
videoUrl: getPublicPath('video/new/editing-skills/xsdh_gjzgnyfxj_batch.mp4'),
|
||||
type: 'other' as const
|
||||
},
|
||||
{
|
||||
id: 'step8',
|
||||
label: '剪映基础操作',
|
||||
content: '蒙版功能用法详解',
|
||||
videoUrl: getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
|
||||
videoUrl: getPublicPath('video/new/editing-skills/xsdh_mbgnyfxj_batch.mp4'),
|
||||
type: 'other' as const
|
||||
},
|
||||
{
|
||||
id: 'step9',
|
||||
label: '剪映基础操作',
|
||||
content: '混合模式功能用法详解',
|
||||
videoUrl: getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
|
||||
videoUrl: getPublicPath('video/new/editing-skills/xsdh_hhmsgnyfxj_batch.mp4'),
|
||||
type: 'other' as const
|
||||
},
|
||||
{
|
||||
id: 'step10',
|
||||
label: '剪映基础操作',
|
||||
content: '曲线变速功能用法详解',
|
||||
videoUrl: getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
|
||||
videoUrl: getPublicPath('video/new/editing-skills/xsdh_qxbsgnyfxj_batch.mp4'),
|
||||
type: 'other' as const
|
||||
},
|
||||
{
|
||||
id: 'step11',
|
||||
label: '剪映基础操作',
|
||||
content: '如何给画面调色',
|
||||
videoUrl: getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
|
||||
videoUrl: getPublicPath('video/new/editing-skills/xsdh_rhghmts_batch.mp4'),
|
||||
type: 'other' as const
|
||||
}
|
||||
]
|
||||
@@ -196,7 +204,11 @@ const handleIntroGuideComplete = () => {
|
||||
@close="handleStepNavigationClose"
|
||||
/>
|
||||
|
||||
<GuideSteps :visible="true" position-offset-y="85%" @complete="handleIntroGuideComplete">
|
||||
<GuideSteps
|
||||
:visible="showIntroGuide"
|
||||
position-offset-y="85%"
|
||||
@complete="handleIntroGuideComplete"
|
||||
>
|
||||
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_021.MP3')">
|
||||
拍摄工作完成以后还需要剪辑来赋予节奏与表达。
|
||||
</GuideStepsItem>
|
||||
|
||||
@@ -1,54 +1,311 @@
|
||||
<script lang="ts" setup>
|
||||
import { IMAGE_PATHS, getStaticImage } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Video } from '@/components'
|
||||
import { getPublicPath } from '@/utils'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Overlay } from '@/components/Overlay'
|
||||
import { ref } from 'vue'
|
||||
import ImageFilter from '@/components/ImageFilter/ImageFilter.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const back = () => {
|
||||
const selected = ref<number | undefined>(1)
|
||||
const btnState = ref(false)
|
||||
const screenshot = ref()
|
||||
const showModal = ref(false)
|
||||
const activeTab = ref<'color' | 'filter'>('color') // 'color' | 'filter'
|
||||
|
||||
const adjustmentList = ref([
|
||||
{ name: '亮度', value: 0, min: -100, max: 100 },
|
||||
{ name: '对比度', value: 0, min: -100, max: 100 },
|
||||
{ name: '饱和度', value: 0, min: -100, max: 100 },
|
||||
{ name: '曝光', value: 0, min: -100, max: 100 },
|
||||
{ name: '色调', value: 0, min: 0, max: 100 },
|
||||
{ name: '锐化', value: 0, min: 0, max: 100 },
|
||||
{ name: '模糊', value: 0, min: 0, max: 100 }
|
||||
])
|
||||
|
||||
const colorBarList = ref([
|
||||
{ id: 1, name: '原图', filter: 'none' },
|
||||
{ id: 2, name: '暖阳', filter: 'saturate(2.5) contrast(2.1)' },
|
||||
{ id: 3, name: '冷调', filter: 'hue-rotate(180deg) contrast(0.9)' },
|
||||
{ id: 4, name: '复古', filter: 'sepia(0.6) contrast(1.2) brightness(0.9)' },
|
||||
{ id: 5, name: '黑白', filter: 'grayscale(1)' },
|
||||
{ id: 6, name: '鲜艳', filter: 'sepia(0.1) saturate(2.5) contrast(2.1) brightness(1)' }
|
||||
])
|
||||
|
||||
// 获取指定参数的值
|
||||
// 获取指定参数的值
|
||||
const imageFilterRef = ref<InstanceType<typeof ImageFilter> | null>(null)
|
||||
|
||||
// 获取指定参数的值
|
||||
const getVal = (name: string) => {
|
||||
const val = adjustmentList.value.find(item => item.name === name)?.value || 0
|
||||
return Number(val)
|
||||
}
|
||||
|
||||
// 更新滤镜效果
|
||||
const updateFilter = () => {
|
||||
if (!imageFilterRef.value) return
|
||||
|
||||
// 如果开启了对比模式(显示原图),则不应用任何滤镜
|
||||
if (btnState.value) {
|
||||
imageFilterRef.value.setOptions({
|
||||
brightness: 0,
|
||||
contrast: 0,
|
||||
saturation: 0,
|
||||
exposure: 0,
|
||||
hue: 0,
|
||||
blur: 0,
|
||||
sharpen: 0,
|
||||
filter: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const selectedFilter = colorBarList.value.find(v => v.id === selected.value)?.filter || 'none'
|
||||
|
||||
imageFilterRef.value.setOptions({
|
||||
brightness: getVal('亮度'),
|
||||
contrast: getVal('对比度'),
|
||||
saturation: getVal('饱和度'),
|
||||
exposure: getVal('曝光'),
|
||||
hue: getVal('色调'),
|
||||
blur: getVal('模糊'),
|
||||
sharpen: getVal('锐化'),
|
||||
filter: selectedFilter
|
||||
})
|
||||
}
|
||||
|
||||
const changeOriginalImgae = () => {
|
||||
btnState.value = !btnState.value
|
||||
updateFilter()
|
||||
}
|
||||
|
||||
// Tab 切换处理
|
||||
const handleTabChange = (tab: 'color' | 'filter') => {
|
||||
activeTab.value = tab
|
||||
// 切换时重置所有参数
|
||||
handleReset()
|
||||
}
|
||||
|
||||
// 重置所有参数
|
||||
const handleReset = () => {
|
||||
adjustmentList.value.forEach(item => (item.value = 0))
|
||||
selected.value = 1
|
||||
updateFilter()
|
||||
}
|
||||
|
||||
// 生成图片
|
||||
const handleGenerate = async () => {
|
||||
if (!imageFilterRef.value) return
|
||||
|
||||
try {
|
||||
const dataUrl = await imageFilterRef.value.generate()
|
||||
screenshot.value = dataUrl
|
||||
showModal.value = true
|
||||
} catch (error) {
|
||||
console.error('生成图片失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalClose = () => {
|
||||
showModal.value = false
|
||||
screenshot.value = null
|
||||
}
|
||||
|
||||
const handleTryOther = () => {
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
const handleFinish = () => {
|
||||
router.push({
|
||||
name: 'Film'
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const changeColor = (item: { id: number; name: string }) => {
|
||||
selected.value = item.id
|
||||
updateFilter()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative w-full h-full overflow-hidden">
|
||||
<!-- 返回按钮 -->
|
||||
<div class="w-full h-full relative pt-44">
|
||||
<!-- 返回 -->
|
||||
<ImgButton
|
||||
class="absolute right-0 top-0 z-20"
|
||||
class="absolute right-0 top-0 z-10"
|
||||
:image="IMAGE_PATHS.COMPONENTS.BACK"
|
||||
@click="back"
|
||||
@click="handleClose"
|
||||
/>
|
||||
<!-- 背景 -->
|
||||
<img class="absolute inset-0" :src="getStaticImage('img/背景.png')" alt="" />
|
||||
|
||||
<!-- 标题 -->
|
||||
<img class="absolute top-5 z-10" :src="getStaticImage('img/调色-标题.png')" alt="" />
|
||||
<!-- 背景图片-center -->
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center bg-no-repeat -z-10"
|
||||
:style="{
|
||||
backgroundImage: getBackgroundImage('after-editing/color/底图')
|
||||
}"
|
||||
></div>
|
||||
|
||||
<!-- 内容 -->
|
||||
<div class="relative h-full pt-28 overflow-y-scroll">
|
||||
<img :src="getStaticImage('film/img/调色.png')" alt="" />
|
||||
|
||||
<div class="absolute top-[6250px] left-36 w-[1670px] h-[1000px] rounded-2xl overflow-hidden">
|
||||
<Video
|
||||
:src="getPublicPath('video/editing/color_jianji_batch.mp4')"
|
||||
playsinline
|
||||
controls
|
||||
:muted="false"
|
||||
:volume="1"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex h-[759px] justify-center space-x-12">
|
||||
<!-- 效果图 -->
|
||||
<div
|
||||
class="relative w-[1273px] h-[732px] bg-no-repeat bg-cover mt-10 p-2 pb-[26px] pr-[26px]"
|
||||
:style="{ backgroundImage: getBackgroundImage('after-editing/color/图片框.png') }"
|
||||
>
|
||||
<ImageFilter ref="imageFilterRef" :src="getStaticImage('after-editing/color/film_img')" />
|
||||
|
||||
<!-- 开启对比 -->
|
||||
<ImgButton
|
||||
class="absolute top-[7830px] left-[51.5%] -translate-x-1/2"
|
||||
image="after-editing/color/调色实践"
|
||||
@click="router.push({ name: 'FilmColor' })"
|
||||
/>
|
||||
class="absolute top-2 right-6 flex-[0_0_200px]"
|
||||
@click="changeOriginalImgae"
|
||||
:image="
|
||||
btnState
|
||||
? 'after-editing/color/开启对比 - 可开启'
|
||||
: 'after-editing/color/开启对比 - 不可开启'
|
||||
"
|
||||
></ImgButton>
|
||||
</div>
|
||||
|
||||
<GuideSteps :visible="true" position-offset-y="85%">
|
||||
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
<!-- 滤镜/参数配置 -->
|
||||
<div>
|
||||
<div
|
||||
class="w-[445px] h-[759px] bg-no-repeat bg-cover p-2 rounded-xl"
|
||||
:style="{ backgroundImage: getBackgroundImage('after-editing/color/小底板') }"
|
||||
>
|
||||
<!-- tab切换 -->
|
||||
<header class="flex h-[75px] items-center justify-center space-x-4">
|
||||
<div @click="handleTabChange('color')">
|
||||
<ImgButton
|
||||
:image="
|
||||
activeTab !== 'color'
|
||||
? 'after-editing/color/调色-暗'
|
||||
: 'after-editing/color/调色-亮'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div @click="handleTabChange('filter')">
|
||||
<ImgButton
|
||||
:image="
|
||||
activeTab !== 'filter'
|
||||
? 'after-editing/color/滤镜-暗'
|
||||
: 'after-editing/color/滤镜-亮'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<!-- tab-content -->
|
||||
<div class="px-4 h-[calc(100%-75px)]">
|
||||
<!-- 调色内容 -->
|
||||
<div
|
||||
v-show="activeTab === 'color'"
|
||||
class="space-y-6 h-full overflow-y-auto custom-scrollbar pt-4"
|
||||
>
|
||||
<div v-for="item in adjustmentList" :key="item.name" class="px-2">
|
||||
<div class="flex justify-between text-[#648ab2] text-2xl font-bold mb-2">
|
||||
<span>{{ item.name }}</span>
|
||||
<span>{{ item.value }}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
v-model.number="item.value"
|
||||
:min="item.min"
|
||||
:max="item.max"
|
||||
@change="updateFilter"
|
||||
class="w-full h-3 bg-[#b8cfe7] rounded-lg appearance-none cursor-pointer slider-thumb"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 滤镜内容 -->
|
||||
<div
|
||||
v-show="activeTab === 'filter'"
|
||||
class="space-y-10 h-full overflow-y-auto custom-scrollbar pt-10"
|
||||
>
|
||||
<div
|
||||
v-for="item in colorBarList"
|
||||
:key="item.id"
|
||||
:style="{
|
||||
backgroundImage:
|
||||
selected == item.id
|
||||
? getBackgroundImage('after-editing/color/滤镜-选中')
|
||||
: getBackgroundImage('after-editing/color/滤镜-未选中')
|
||||
}"
|
||||
:class="[
|
||||
'mx-auto w-[373px] h-[62px] text-white text-2xl font-bold flex items-center justify-center cursor-pointer bg-no-repeat bg-contain bg-center transition-all hover:scale-105',
|
||||
selected !== item.id && 'text-gray-700'
|
||||
]"
|
||||
@click="changeColor(item)"
|
||||
>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-around mt-4">
|
||||
<!-- 重置 -->
|
||||
<ImgButton image="after-editing/color/重置" @click="handleReset" />
|
||||
<!-- 生成 -->
|
||||
<ImgButton image="after-editing/color/生成" @click="handleGenerate" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- title -->
|
||||
<img class="absolute top-0 left-0" :src="getStaticImage('after-editing/color/标题-调色实践')" />
|
||||
|
||||
<!-- 回到顶部按钮 - 最后一页显示 -->
|
||||
<p class="absolute left-40 bottom-10 flex items-center">
|
||||
<img class="rotate-0 w-28 mr-4" src="@/assets/svg/deng.svg" />
|
||||
<span class="text-white text-2xl font-bold">
|
||||
推荐参数:亮度 90、对比度 100、饱和度 100、曝光 - 90、色调 0、锐化 100、模糊 0
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<!-- 保存图片弹窗 -->
|
||||
<Overlay :visible="showModal" @close="handleModalClose">
|
||||
<div class="relative w-[1000px] h-[600px] flex items-center justify-center">
|
||||
<!-- 截图内容 -->
|
||||
<div class="bg-white p-2 rounded-md -rotate-6">
|
||||
<img :src="screenshot" alt="调色结果" />
|
||||
</div>
|
||||
<!-- <img :src="screenshot" alt="调色结果" class="max-w-full max-h-full object-contain rotate-2" /> -->
|
||||
<!-- 操作按钮 -->
|
||||
<div class="absolute bottom-1/2 -left-1/3 translate-y-1/2">
|
||||
<ImgButton image="after-editing/color/按钮-试试其他调色" @click="handleTryOther" />
|
||||
</div>
|
||||
<div class="absolute bottom-1/2 -right-1/3 translate-y-1/2">
|
||||
<ImgButton image="after-editing/color/按钮-结束调色实践" @click="handleFinish" />
|
||||
</div>
|
||||
</div>
|
||||
</Overlay>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义滑块样式 */
|
||||
.slider-thumb::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
background: #92c6f2;
|
||||
border: 3px solid #ffffff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
box-shadow: 4px 4px 2px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.slider-thumb::-moz-range-thumb {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
background: #92c6f2;
|
||||
border: 3px solid #ffffff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
box-shadow: 4px 4px 2px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
</style>
|
||||
@@ -1,310 +1,49 @@
|
||||
<script lang="ts" setup>
|
||||
import router from '@/router'
|
||||
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { IMAGE_PATHS, getStaticImage } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Overlay } from '@/components/Overlay'
|
||||
import { ref } from 'vue'
|
||||
import ImageFilter from '@/components/ImageFilter/ImageFilter.vue'
|
||||
import { useFilmStore } from '@/stores'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Video } from '@/components'
|
||||
import { getPublicPath } from '@/utils'
|
||||
|
||||
const selected = ref<number | undefined>(1)
|
||||
const btnState = ref(false)
|
||||
const screenshot = ref()
|
||||
const showModal = ref(false)
|
||||
const activeTab = ref<'color' | 'filter'>('color') // 'color' | 'filter'
|
||||
const router = useRouter()
|
||||
|
||||
const adjustmentList = ref([
|
||||
{ name: '亮度', value: 0, min: -100, max: 100 },
|
||||
{ name: '对比度', value: 0, min: -100, max: 100 },
|
||||
{ name: '饱和度', value: 0, min: -100, max: 100 },
|
||||
{ name: '曝光', value: 0, min: -100, max: 100 },
|
||||
{ name: '色调', value: 0, min: 0, max: 100 },
|
||||
{ name: '锐化', value: 0, min: 0, max: 100 },
|
||||
{ name: '模糊', value: 0, min: 0, max: 100 }
|
||||
])
|
||||
|
||||
const colorBarList = ref([
|
||||
{ id: 1, name: '原图', filter: 'none' },
|
||||
{ id: 2, name: '暖阳', filter: 'saturate(2.5) contrast(2.1)' },
|
||||
{ id: 3, name: '冷调', filter: 'hue-rotate(180deg) contrast(0.9)' },
|
||||
{ id: 4, name: '复古', filter: 'sepia(0.6) contrast(1.2) brightness(0.9)' },
|
||||
{ id: 5, name: '黑白', filter: 'grayscale(1)' },
|
||||
{ id: 6, name: '鲜艳', filter: 'sepia(0.1) saturate(2.5) contrast(2.1) brightness(1)' }
|
||||
])
|
||||
|
||||
// 获取指定参数的值
|
||||
// 获取指定参数的值
|
||||
const imageFilterRef = ref<InstanceType<typeof ImageFilter> | null>(null)
|
||||
|
||||
// 获取指定参数的值
|
||||
const getVal = (name: string) => {
|
||||
const val = adjustmentList.value.find(item => item.name === name)?.value || 0
|
||||
return Number(val)
|
||||
}
|
||||
|
||||
// 更新滤镜效果
|
||||
const updateFilter = () => {
|
||||
if (!imageFilterRef.value) return
|
||||
|
||||
// 如果开启了对比模式(显示原图),则不应用任何滤镜
|
||||
if (btnState.value) {
|
||||
imageFilterRef.value.setOptions({
|
||||
brightness: 0,
|
||||
contrast: 0,
|
||||
saturation: 0,
|
||||
exposure: 0,
|
||||
hue: 0,
|
||||
blur: 0,
|
||||
sharpen: 0,
|
||||
filter: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const selectedFilter = colorBarList.value.find(v => v.id === selected.value)?.filter || 'none'
|
||||
|
||||
imageFilterRef.value.setOptions({
|
||||
brightness: getVal('亮度'),
|
||||
contrast: getVal('对比度'),
|
||||
saturation: getVal('饱和度'),
|
||||
exposure: getVal('曝光'),
|
||||
hue: getVal('色调'),
|
||||
blur: getVal('模糊'),
|
||||
sharpen: getVal('锐化'),
|
||||
filter: selectedFilter
|
||||
})
|
||||
}
|
||||
|
||||
const changeOriginalImgae = () => {
|
||||
btnState.value = !btnState.value
|
||||
updateFilter()
|
||||
}
|
||||
|
||||
// Tab 切换处理
|
||||
const handleTabChange = (tab: 'color' | 'filter') => {
|
||||
activeTab.value = tab
|
||||
// 切换时重置所有参数
|
||||
handleReset()
|
||||
}
|
||||
|
||||
// 重置所有参数
|
||||
const handleReset = () => {
|
||||
adjustmentList.value.forEach(item => (item.value = 0))
|
||||
selected.value = 1
|
||||
updateFilter()
|
||||
}
|
||||
|
||||
// 生成图片
|
||||
const handleGenerate = async () => {
|
||||
if (!imageFilterRef.value) return
|
||||
|
||||
try {
|
||||
const dataUrl = await imageFilterRef.value.generate()
|
||||
screenshot.value = dataUrl
|
||||
showModal.value = true
|
||||
} catch (error) {
|
||||
console.error('生成图片失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalClose = () => {
|
||||
showModal.value = false
|
||||
screenshot.value = null
|
||||
}
|
||||
|
||||
const handleTryOther = () => {
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
const handleFinish = () => {
|
||||
router.push({
|
||||
name: 'Film'
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
const back = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const changeColor = (item: { id: number; name: string }) => {
|
||||
selected.value = item.id
|
||||
updateFilter()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full relative pt-44">
|
||||
<!-- 返回 -->
|
||||
<div class="relative w-full h-full overflow-hidden">
|
||||
<!-- 返回按钮 -->
|
||||
<ImgButton
|
||||
class="absolute right-0 top-0 z-10"
|
||||
class="absolute right-0 top-0 z-20"
|
||||
:image="IMAGE_PATHS.COMPONENTS.BACK"
|
||||
@click="handleClose"
|
||||
@click="back"
|
||||
/>
|
||||
<!-- 背景 -->
|
||||
<img class="absolute inset-0" :src="getStaticImage('img/背景.png')" alt="" />
|
||||
|
||||
<!-- 背景图片-center -->
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center bg-no-repeat -z-10"
|
||||
:style="{
|
||||
backgroundImage: getBackgroundImage('after-editing/color/底图')
|
||||
}"
|
||||
></div>
|
||||
<!-- 标题 -->
|
||||
<img class="absolute top-5 z-10" :src="getStaticImage('img/调色-标题.png')" alt="" />
|
||||
|
||||
<div class="flex h-[759px] justify-center space-x-12">
|
||||
<!-- 效果图 -->
|
||||
<div
|
||||
class="relative w-[1273px] h-[732px] bg-no-repeat bg-cover mt-10 p-2 pb-[26px] pr-[26px]"
|
||||
:style="{ backgroundImage: getBackgroundImage('after-editing/color/图片框.png') }"
|
||||
>
|
||||
<ImageFilter ref="imageFilterRef" :src="getStaticImage('after-editing/color/film_img')" />
|
||||
<!-- 内容 -->
|
||||
<div class="relative h-full pt-28 overflow-y-scroll">
|
||||
<img :src="getStaticImage('film/img/调色.png')" alt="" />
|
||||
|
||||
<div class="absolute top-[6250px] left-36 w-[1670px] h-[1000px] rounded-2xl overflow-hidden">
|
||||
<Video
|
||||
:src="getPublicPath('video/film/editing/color_jianji_batch.mp4')"
|
||||
playsinline
|
||||
controls
|
||||
:muted="false"
|
||||
:volume="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 开启对比 -->
|
||||
<ImgButton
|
||||
class="absolute top-2 right-6 flex-[0_0_200px]"
|
||||
@click="changeOriginalImgae"
|
||||
:image="
|
||||
btnState
|
||||
? 'after-editing/color/开启对比 - 可开启'
|
||||
: 'after-editing/color/开启对比 - 不可开启'
|
||||
"
|
||||
></ImgButton>
|
||||
</div>
|
||||
|
||||
<!-- 滤镜/参数配置 -->
|
||||
<div>
|
||||
<div
|
||||
class="w-[445px] h-[759px] bg-no-repeat bg-cover p-2 rounded-xl"
|
||||
:style="{ backgroundImage: getBackgroundImage('after-editing/color/小底板') }"
|
||||
>
|
||||
<!-- tab切换 -->
|
||||
<header class="flex h-[75px] items-center justify-center space-x-4">
|
||||
<div @click="handleTabChange('color')">
|
||||
<ImgButton
|
||||
:image="
|
||||
activeTab !== 'color'
|
||||
? 'after-editing/color/调色-暗'
|
||||
: 'after-editing/color/调色-亮'
|
||||
"
|
||||
class="absolute top-[7830px] left-[51.5%] -translate-x-1/2"
|
||||
image="after-editing/color/调色实践"
|
||||
@click="router.push({ name: 'FilmColorPractice' })"
|
||||
/>
|
||||
</div>
|
||||
<div @click="handleTabChange('filter')">
|
||||
<ImgButton
|
||||
:image="
|
||||
activeTab !== 'filter'
|
||||
? 'after-editing/color/滤镜-暗'
|
||||
: 'after-editing/color/滤镜-亮'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<!-- tab-content -->
|
||||
<div class="px-4 h-[calc(100%-75px)]">
|
||||
<!-- 调色内容 -->
|
||||
<div
|
||||
v-show="activeTab === 'color'"
|
||||
class="space-y-6 h-full overflow-y-auto custom-scrollbar pt-4"
|
||||
>
|
||||
<div v-for="item in adjustmentList" :key="item.name" class="px-2">
|
||||
<div class="flex justify-between text-[#648ab2] text-2xl font-bold mb-2">
|
||||
<span>{{ item.name }}</span>
|
||||
<span>{{ item.value }}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
v-model.number="item.value"
|
||||
:min="item.min"
|
||||
:max="item.max"
|
||||
@change="updateFilter"
|
||||
class="w-full h-3 bg-[#b8cfe7] rounded-lg appearance-none cursor-pointer slider-thumb"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 滤镜内容 -->
|
||||
<div
|
||||
v-show="activeTab === 'filter'"
|
||||
class="space-y-10 h-full overflow-y-auto custom-scrollbar pt-10"
|
||||
>
|
||||
<div
|
||||
v-for="item in colorBarList"
|
||||
:key="item.id"
|
||||
:style="{
|
||||
backgroundImage:
|
||||
selected == item.id
|
||||
? getBackgroundImage('after-editing/color/滤镜-选中')
|
||||
: getBackgroundImage('after-editing/color/滤镜-未选中')
|
||||
}"
|
||||
:class="[
|
||||
'mx-auto w-[373px] h-[62px] text-white text-2xl font-bold flex items-center justify-center cursor-pointer bg-no-repeat bg-contain bg-center transition-all hover:scale-105',
|
||||
selected !== item.id && 'text-gray-700'
|
||||
]"
|
||||
@click="changeColor(item)"
|
||||
>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-around mt-4">
|
||||
<!-- 重置 -->
|
||||
<ImgButton image="after-editing/color/重置" @click="handleReset" />
|
||||
<!-- 生成 -->
|
||||
<ImgButton image="after-editing/color/生成" @click="handleGenerate" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- title -->
|
||||
<img class="absolute top-0 left-0" :src="getStaticImage('after-editing/color/标题-调色实践')" />
|
||||
|
||||
<!-- 回到顶部按钮 - 最后一页显示 -->
|
||||
<p class="absolute left-40 bottom-10 flex items-center">
|
||||
<img class="rotate-0 w-28 mr-4" src="@/assets/svg/deng.svg" />
|
||||
<span class="text-white text-2xl font-bold"
|
||||
>推荐参数:亮度 90、对比度 100、饱和度 100、曝光 - 90、色调 0、锐化 100、模糊 0</span
|
||||
>
|
||||
</p>
|
||||
|
||||
<!-- 保存图片弹窗 -->
|
||||
<Overlay :visible="showModal" @close="handleModalClose">
|
||||
<div class="relative w-[1000px] h-[600px] flex items-center justify-center">
|
||||
<!-- 截图内容 -->
|
||||
<div class="bg-white p-2 rounded-md -rotate-6">
|
||||
<img :src="screenshot" alt="调色结果" />
|
||||
</div>
|
||||
<!-- <img :src="screenshot" alt="调色结果" class="max-w-full max-h-full object-contain rotate-2" /> -->
|
||||
<!-- 操作按钮 -->
|
||||
<div class="absolute bottom-1/2 -left-1/3 translate-y-1/2">
|
||||
<ImgButton image="after-editing/color/按钮-试试其他调色" @click="handleTryOther" />
|
||||
</div>
|
||||
<div class="absolute bottom-1/2 -right-1/3 translate-y-1/2">
|
||||
<ImgButton image="after-editing/color/按钮-结束调色实践" @click="handleFinish" />
|
||||
</div>
|
||||
</div>
|
||||
</Overlay>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义滑块样式 */
|
||||
.slider-thumb::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
background: #92c6f2;
|
||||
border: 3px solid #ffffff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
box-shadow: 4px 4px 2px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.slider-thumb::-moz-range-thumb {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
background: #92c6f2;
|
||||
border: 3px solid #ffffff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
box-shadow: 4px 4px 2px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
</style>
|
||||
@@ -3,31 +3,30 @@ import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Video } from '@/components'
|
||||
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
import { getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
import { useFilmStore } from '@/stores'
|
||||
|
||||
const router = useRouter()
|
||||
const film = useFilmStore()
|
||||
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 关联剪辑视频 URL(暂时用其他视频代替)
|
||||
const associationVideoUrl = getPublicPath('video/on-site/dygmk_gljj_batch.mp4')
|
||||
const associationVideoUrl = getPublicPath('video/film/editing/dygmk_gljj_batch.mp4')
|
||||
|
||||
// 视频播放状态
|
||||
const videoPlaying = ref(false)
|
||||
|
||||
const handleVideoEnded = () => {
|
||||
film.markEditingVideoWatched('association')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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"
|
||||
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
|
||||
></div>
|
||||
|
||||
<div class="relative w-full h-full overflow-hidden">
|
||||
<!-- 视频播放区域 -->
|
||||
<div class="w-full h-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20">
|
||||
<Video
|
||||
class="w-full h-full"
|
||||
:src="associationVideoUrl"
|
||||
@@ -38,8 +37,8 @@ const videoPlaying = ref(false)
|
||||
:hideNativeControls="true"
|
||||
:autoplay="true"
|
||||
:show-center-play-icon="true"
|
||||
@ended="handleVideoEnded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<ImgButton
|
||||
|
||||
@@ -3,16 +3,18 @@ import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Video } from '@/components'
|
||||
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
import { getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
import { useFilmStore } from '@/stores'
|
||||
|
||||
const router = useRouter()
|
||||
const film = useFilmStore()
|
||||
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 连续剪辑视频 URL(暂时用其他视频代替)
|
||||
const continuityVideoUrl = getPublicPath('video/on-site/dygmk_lxjj_batch.mp4')
|
||||
const continuityVideoUrl = getPublicPath('video/film/editing/dygmk_lxjj_batch.mp4')
|
||||
|
||||
// 视频播放状态
|
||||
const videoEnded = ref(false)
|
||||
@@ -23,19 +25,13 @@ const showSummary = ref(false)
|
||||
const handleVideoEnded = () => {
|
||||
videoEnded.value = true
|
||||
showSummary.value = true
|
||||
film.markEditingVideoWatched('continuity')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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"
|
||||
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
|
||||
></div>
|
||||
|
||||
<div class="relative w-full h-full overflow-hidden">
|
||||
<!-- 视频播放区域 -->
|
||||
<div class="w-full h-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20">
|
||||
<Video
|
||||
class="w-full h-full"
|
||||
:src="continuityVideoUrl"
|
||||
@@ -47,9 +43,7 @@ const handleVideoEnded = () => {
|
||||
:autoplay="true"
|
||||
:show-center-play-icon="true"
|
||||
@ended="handleVideoEnded"
|
||||
>
|
||||
</Video>
|
||||
</div>
|
||||
></Video>
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<ImgButton
|
||||
|
||||
@@ -3,31 +3,30 @@ import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Video } from '@/components'
|
||||
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
import { getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
import { useFilmStore } from '@/stores'
|
||||
|
||||
const router = useRouter()
|
||||
const film = useFilmStore()
|
||||
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 切块视频 URL(暂时用其他视频代替)
|
||||
const cutVideoUrl = getPublicPath('video/on-site/dygmk_kq_batch.mp4')
|
||||
const cutVideoUrl = getPublicPath('video/film/editing/dygmk_kq_batch.mp4')
|
||||
|
||||
// 视频播放状态
|
||||
const videoPlaying = ref(false)
|
||||
|
||||
const handleVideoEnded = () => {
|
||||
film.markEditingVideoWatched('cut')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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"
|
||||
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
|
||||
></div>
|
||||
|
||||
<div class="relative w-full h-full overflow-hidden">
|
||||
<!-- 视频播放区域 -->
|
||||
<div class="w-full h-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20">
|
||||
<Video
|
||||
class="w-full h-full"
|
||||
:src="cutVideoUrl"
|
||||
@@ -38,8 +37,8 @@ const videoPlaying = ref(false)
|
||||
:hideNativeControls="true"
|
||||
:autoplay="true"
|
||||
:show-center-play-icon="true"
|
||||
@ended="handleVideoEnded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<ImgButton
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { GuideSteps, type GuideStep } from '@/components/Guide'
|
||||
import { Video } from '@/components'
|
||||
import { useFilmStore } from '@/stores'
|
||||
|
||||
const router = useRouter()
|
||||
const film = useFilmStore()
|
||||
|
||||
const handleClose = () => {
|
||||
router.back()
|
||||
@@ -47,6 +49,33 @@ const goAssociation = () => {
|
||||
const goContinuity = () => {
|
||||
router.push({ name: 'FilmEditingContinuity' })
|
||||
}
|
||||
|
||||
// 6 个剪辑子视频学习完成后的一次性引导
|
||||
const editingCompleteSteps: GuideStep[] = [
|
||||
{
|
||||
id: 'editing-complete-step1',
|
||||
content:
|
||||
'刚才我们提到的剪辑方式,并不是孤立存在的技巧,而是针对不同问题的解决方案。所以,理解剪辑逻辑才是最重要的!',
|
||||
audioSrc: getPublicPath('audio/py_4_018.MP3')
|
||||
}
|
||||
]
|
||||
|
||||
const showEditingCompleteGuide = ref(false)
|
||||
|
||||
watch(
|
||||
() => film.allEditingVideosWatched,
|
||||
watched => {
|
||||
if (watched && film.isGuideFilmEditingComplete) {
|
||||
showEditingCompleteGuide.value = true
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const handleEditingCompleteGuideDone = () => {
|
||||
showEditingCompleteGuide.value = false
|
||||
film.setIsGuideFilmEditingComplete(false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -157,9 +186,14 @@ const goContinuity = () => {
|
||||
@click="handleClose"
|
||||
/>
|
||||
|
||||
<GuideSteps :visible="true" position-offset-y="85%">
|
||||
<GuideStepsItem>选择任意图片按钮,进入对应拍摄场景!</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
<!-- 6 个剪辑视频学完后的一次性引导 -->
|
||||
<GuideSteps
|
||||
v-if="showEditingCompleteGuide"
|
||||
:steps="editingCompleteSteps"
|
||||
position-offset-y="85%"
|
||||
@complete="handleEditingCompleteGuideDone"
|
||||
@skip="handleEditingCompleteGuideDone"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -3,31 +3,30 @@ import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Video } from '@/components'
|
||||
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
import { getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
import { useFilmStore } from '@/stores'
|
||||
|
||||
const router = useRouter()
|
||||
const film = useFilmStore()
|
||||
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 节奏型剪辑视频 URL(暂时用其他视频代替)
|
||||
const rhythmVideoUrl = getPublicPath('video/on-site/dygmk_jzxjj_batch.mp4')
|
||||
const rhythmVideoUrl = getPublicPath('video/film/editing/dygmk_jzxjj_batch.mp4')
|
||||
|
||||
// 视频播放状态
|
||||
const videoPlaying = ref(false)
|
||||
|
||||
const handleVideoEnded = () => {
|
||||
film.markEditingVideoWatched('rhythm')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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"
|
||||
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
|
||||
></div>
|
||||
|
||||
<div class="relative w-full h-full overflow-hidden">
|
||||
<!-- 视频播放区域 -->
|
||||
<div class="w-full h-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20">
|
||||
<Video
|
||||
class="w-full h-full"
|
||||
:src="rhythmVideoUrl"
|
||||
@@ -38,8 +37,8 @@ const videoPlaying = ref(false)
|
||||
:hideNativeControls="true"
|
||||
:autoplay="true"
|
||||
:show-center-play-icon="true"
|
||||
@ended="handleVideoEnded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<ImgButton
|
||||
|
||||
@@ -4,8 +4,10 @@ import { useRouter } from 'vue-router'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Video, QuestionQuiz, type Question } from '@/components'
|
||||
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
import { useFilmStore } from '@/stores'
|
||||
|
||||
const router = useRouter()
|
||||
const film = useFilmStore()
|
||||
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
@@ -13,11 +15,11 @@ const handleBack = () => {
|
||||
|
||||
// 声音转场视频 URL(暂时用其他视频代替)
|
||||
const soundTransitionVideoUrl = getPublicPath(
|
||||
'video/on-site/xianchangpaishe_shengyingzhuanchang1.mp4'
|
||||
'video/film/editing/xianchangpaishe_shengyingzhuanchang1.mp4'
|
||||
)
|
||||
// const soundTransitionVideoUrl2 = getPublicPath('video/on-site/xianchangpaishe_yanshipaishe.mp4')
|
||||
const soundTransitionVideoUrl3 = getPublicPath(
|
||||
'video/on-site/xianchangpaishe_shengyingzhuanchang3.mp4'
|
||||
'video/film/editing/xianchangpaishe_shengyingzhuanchang3.mp4'
|
||||
)
|
||||
|
||||
// 视频播放状态
|
||||
@@ -63,6 +65,7 @@ const questions = ref<Question[]>([
|
||||
const handleQuestionsComplete = () => {
|
||||
showQuiz.value = false
|
||||
showVerification.value = true
|
||||
film.markEditingVideoWatched('soundTransition')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ import { useRouter } from 'vue-router'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Video } from '@/components'
|
||||
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
import { useFilmStore } from '@/stores'
|
||||
|
||||
const router = useRouter()
|
||||
const film = useFilmStore()
|
||||
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
@@ -21,6 +23,7 @@ const videoPlaying = ref(false)
|
||||
// 视频播放完成
|
||||
const handleVideoEnded = () => {
|
||||
videoEnded.value = true
|
||||
film.markEditingVideoWatched('start')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -45,8 +48,7 @@ const handleVideoEnded = () => {
|
||||
:autoplay="true"
|
||||
:show-center-play-icon="true"
|
||||
@ended="handleVideoEnded"
|
||||
>
|
||||
</Video>
|
||||
></Video>
|
||||
</div>
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
|
||||
@@ -1,39 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Video, GuideSteps, GuideStepsItem } from '@/components'
|
||||
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
import { getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
import { useFilmStore } from '@/stores/modules/film'
|
||||
|
||||
const router = useRouter()
|
||||
const filmStore = useFilmStore()
|
||||
const guideKey = 'film-export-publish-intro'
|
||||
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 创作随机性视频 URL(暂时用其他视频代替)
|
||||
const randomVideoUrl = getPublicPath('video/on-site/dygmk_zlsxhbj.mp4')
|
||||
const randomVideoUrl = getPublicPath('video/film/publish/dygmk_zlsxhbj.mp4')
|
||||
|
||||
// 视频播放状态
|
||||
const videoPlaying = ref(false)
|
||||
|
||||
const showGuide = ref(true)
|
||||
const showGuide = ref(!filmStore.guideShown[guideKey])
|
||||
|
||||
if (showGuide.value) {
|
||||
filmStore.markGuideShown(guideKey)
|
||||
}
|
||||
|
||||
const handleComplete = () => {
|
||||
showGuide.value = false
|
||||
nextTick(() => {
|
||||
videoPlaying.value = true
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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"
|
||||
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
|
||||
></div>
|
||||
|
||||
<div class="relative w-full h-full overflow-hidden">
|
||||
<!-- 视频播放区域 -->
|
||||
<div class="w-full h-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20">
|
||||
<Video
|
||||
class="w-full h-full"
|
||||
:src="randomVideoUrl"
|
||||
@@ -43,7 +46,6 @@ const handleComplete = () => {
|
||||
:controls="true"
|
||||
:hideNativeControls="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<ImgButton
|
||||
@@ -52,8 +54,15 @@ const handleComplete = () => {
|
||||
@click="handleBack"
|
||||
/>
|
||||
|
||||
<GuideSteps v-if="showGuide" position-offset-y="85%" @complete="handleComplete">
|
||||
<GuideStepsItem>现在一起来看看导出后的影片效果吧!</GuideStepsItem>
|
||||
<GuideSteps
|
||||
v-if="showGuide"
|
||||
position-offset-y="85%"
|
||||
@complete="handleComplete"
|
||||
@skip="handleComplete"
|
||||
>
|
||||
<GuideStepsItem :audio-src="getPublicPath('audio/py_4_019.MP3')">
|
||||
现在一起来看看导出后的影片效果吧!
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,7 +3,6 @@ import { IMAGE_PATHS, getPublicPath, getStaticImage } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Video } from '@/components'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -60,9 +59,5 @@ const back = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GuideSteps :visible="true" position-offset-y="85%">
|
||||
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</div>
|
||||
</template>
|
||||
@@ -4,7 +4,6 @@ import { ImgButton } from '@/components/Button'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useScale } from '@/composables/useScale'
|
||||
import { ImagePreview } from '@/components'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -46,8 +45,5 @@ const handleOpenImages = () => {
|
||||
@click="handleOpenImages"
|
||||
/>
|
||||
</div>
|
||||
<GuideSteps :visible="true" position-offset-y="85%">
|
||||
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,29 +1,111 @@
|
||||
<script lang="tsx" setup>
|
||||
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { getBackgroundImage, getStaticImage, getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, ref, watch, type Ref } from 'vue'
|
||||
import { ProgressBar, Overlay } from '@/components'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useFilmStore } from '@/stores'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { GuideSteps, GuideStepsItem, type GuideStep } from '@/components/Guide'
|
||||
import FilmMenu from './components/menu.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const film = useFilmStore()
|
||||
const visible = ref(false)
|
||||
|
||||
const status = ref(true)
|
||||
const visible = computed({
|
||||
get: () => film.filmMenuVisible,
|
||||
set: value => film.setFilmMenuVisible(value)
|
||||
})
|
||||
|
||||
// const status = ref(true)
|
||||
|
||||
const currentStep = computed(() => film.FilmUnlockProgress)
|
||||
|
||||
const progress = computed(() => film.progressPercent)
|
||||
|
||||
const guideIndex = ref<number>(0)
|
||||
const taskGuideKey = 'film-task-guide'
|
||||
const hasShownTaskGuide = computed(() => film.guideShown[taskGuideKey] === true)
|
||||
const showTaskGuide = computed(
|
||||
() => currentStep.value === 1 && guideIndex.value < 2 && film.isGuideFilmTask
|
||||
)
|
||||
|
||||
// 思路提点完成回到首页的一次性引导
|
||||
const showReturnGuide = ref(route.query.guide === 'return' && film.isGuideFilmReturn)
|
||||
|
||||
const returnSteps: GuideStep[] = [
|
||||
{
|
||||
id: 'return-step1',
|
||||
content: '现在,我们把镜头带到了徐汇滨江。',
|
||||
audioSrc: getPublicPath('audio/py_4_010.MP3')
|
||||
},
|
||||
{
|
||||
id: 'return-step2',
|
||||
content: '这里既保留着工业时代的痕迹,也承载着当代生活的节奏。',
|
||||
audioSrc: getPublicPath('audio/py_4_011.MP3')
|
||||
}
|
||||
]
|
||||
|
||||
const handleReturnGuideComplete = () => {
|
||||
showReturnGuide.value = false
|
||||
film.setIsGuideFilmReturn(false)
|
||||
router.replace({ name: 'Film' })
|
||||
}
|
||||
|
||||
// 阶段菜单的一次性引导(中期拍摄 / 后期剪辑)
|
||||
const stageGuideMap = {
|
||||
2: {
|
||||
flagKey: 'isGuideFilmStage2',
|
||||
setter: 'setIsGuideFilmStage2',
|
||||
steps: [
|
||||
{
|
||||
id: 'stage2-step1',
|
||||
content: '拍摄工具本身不分好坏,关键在于是否适合当下的拍摄内容和使用习惯。',
|
||||
audioSrc: getPublicPath('audio/py_4_012.MP3')
|
||||
},
|
||||
{
|
||||
id: 'stage2-step2',
|
||||
content: '我们一起看看在这个模块中会用到哪些常见、实用的拍摄工具。',
|
||||
audioSrc: getPublicPath('audio/py_4_013.MP3')
|
||||
}
|
||||
]
|
||||
},
|
||||
3: {
|
||||
flagKey: 'isGuideFilmStage3',
|
||||
setter: 'setIsGuideFilmStage3',
|
||||
steps: [
|
||||
{
|
||||
id: 'stage3-step1',
|
||||
content: '拍摄完成啦!接下来进入剪辑环节。',
|
||||
audioSrc: getPublicPath('audio/py_4_017.MP3')
|
||||
}
|
||||
]
|
||||
}
|
||||
} as const
|
||||
|
||||
type StageKey = keyof typeof stageGuideMap
|
||||
|
||||
const stageGuideStep = ref<StageKey | null>(null)
|
||||
const stageGuideSteps = computed(() =>
|
||||
stageGuideStep.value ? stageGuideMap[stageGuideStep.value].steps : []
|
||||
) as Ref<GuideStep[]>
|
||||
|
||||
const triggerStageGuide = (step: number) => {
|
||||
if (step !== 2 && step !== 3) return
|
||||
const stage = step as StageKey
|
||||
const config = stageGuideMap[stage]
|
||||
if (!(film as any)[config.flagKey]) return
|
||||
stageGuideStep.value = stage
|
||||
}
|
||||
|
||||
const handleStageGuideComplete = () => {
|
||||
if (!stageGuideStep.value) return
|
||||
const config = stageGuideMap[stageGuideStep.value]
|
||||
;(film as any)[config.setter](false)
|
||||
stageGuideStep.value = null
|
||||
}
|
||||
|
||||
const handleGuideClick = () => {
|
||||
// Mark guide as completed after the second click and hide permanently
|
||||
if (guideIndex.value >= 1) {
|
||||
@@ -55,21 +137,28 @@ const handleClose = () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (film.hasShownGuideFilmTask) {
|
||||
if (hasShownTaskGuide.value) {
|
||||
film.setIsGuideFilmTask(false)
|
||||
}
|
||||
|
||||
if (currentStep.value !== 0 && currentStep.value !== 4) {
|
||||
visible.value = true
|
||||
}
|
||||
})
|
||||
|
||||
// Cache the guide after it pops up once so it does not re-appear
|
||||
watch(
|
||||
showTaskGuide,
|
||||
val => {
|
||||
if (val && !film.hasShownGuideFilmTask) {
|
||||
film.setHasShownGuideFilmTask(true)
|
||||
if (val && !hasShownTaskGuide.value) {
|
||||
film.markGuideShown(taskGuideKey)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 阶段引导触发:当 Overlay 菜单打开且当前步骤变化时检查是否需要弹阶段引导
|
||||
watch(
|
||||
[visible, currentStep],
|
||||
([isVisible, step]) => {
|
||||
if (isVisible && step < 4) {
|
||||
triggerStageGuide(step)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -134,6 +223,7 @@ watch(
|
||||
<!-- 引导任务 -->
|
||||
<ImgButton
|
||||
v-if="currentStep !== 4"
|
||||
v-show="!showReturnGuide"
|
||||
class="absolute right-24 bottom-24"
|
||||
:image="IMAGE_PATHS.VLOG.TASK_PERSON"
|
||||
/>
|
||||
@@ -181,7 +271,25 @@ watch(
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 思路提点完成回首页的一次性引导 -->
|
||||
<GuideSteps
|
||||
v-if="showReturnGuide"
|
||||
:steps="returnSteps"
|
||||
position-offset-y="85%"
|
||||
@complete="handleReturnGuideComplete"
|
||||
@skip="handleReturnGuideComplete"
|
||||
/>
|
||||
|
||||
<!-- 阶段菜单一次性引导(中期拍摄 / 后期剪辑) -->
|
||||
<GuideSteps
|
||||
v-if="stageGuideStep"
|
||||
:steps="stageGuideSteps"
|
||||
position-offset-y="85%"
|
||||
@complete="handleStageGuideComplete"
|
||||
@skip="handleStageGuideComplete"
|
||||
/>
|
||||
|
||||
<!-- <GuideSteps
|
||||
:visible="status && currentStep === 0"
|
||||
highlight-selector="1"
|
||||
highlight-mode="gradient"
|
||||
@@ -195,6 +303,6 @@ watch(
|
||||
进入任务取景框开始学习!
|
||||
</div>
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</GuideSteps> -->
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="tsx" setup>
|
||||
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { GuideSteps } from '@/components/Guide'
|
||||
import { ref, computed } from 'vue'
|
||||
import { onBeforeUnmount, ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
|
||||
@@ -14,20 +14,15 @@ const back = () => {
|
||||
const steps = ref([
|
||||
{
|
||||
id: 'step1',
|
||||
content:
|
||||
'拍摄工具本身不分好坏,关键在于是否适合当下的拍摄内容和使用习惯。接下来,我们就一起看看在这个模块中会用到哪些常见、实用的拍摄工具。'
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
content: '点击右滑按钮,查看下一张拍摄工具',
|
||||
highlightSelector: '.equipment-rightbutton'
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
content: '滚动鼠标滚轮,查看拍摄工具详细介绍',
|
||||
highlightSelector: '.plate'
|
||||
content: '设备调试完毕,我们如约抵达徐汇滨江,一起开启拍摄之旅吧!',
|
||||
audioSrc: getPublicPath('audio/py_4_014.MP3')
|
||||
}
|
||||
])
|
||||
|
||||
const showGuide = ref(false)
|
||||
const guideTriggered = ref(false)
|
||||
const guideTimer = ref<ReturnType<typeof setTimeout> | null>(null)
|
||||
const viewedMenuKeys = ref<Set<string>>(new Set())
|
||||
const hiddenbar = ref(false)
|
||||
const currentIndex = ref(0) // 当前显示的图片索引
|
||||
const selectedMenuIndex = ref(0) // 当前选中的菜单项索引
|
||||
@@ -116,6 +111,31 @@ categories.value = categories.value.map(c => ({
|
||||
disabled: (c.products?.length ?? 0) === 0
|
||||
}))
|
||||
|
||||
// 默认选中的菜单也算作已查看
|
||||
if (categories.value[0] && !categories.value[0].disabled) {
|
||||
viewedMenuKeys.value.add(categories.value[0].id)
|
||||
}
|
||||
|
||||
const maybeTriggerGuide = () => {
|
||||
if (guideTriggered.value) return
|
||||
const enabledIds = categories.value.filter(c => !c.disabled).map(c => c.id)
|
||||
if (enabledIds.length === 0) return
|
||||
const allViewed = enabledIds.every(id => viewedMenuKeys.value.has(id))
|
||||
if (!allViewed) return
|
||||
guideTriggered.value = true
|
||||
guideTimer.value = setTimeout(() => {
|
||||
showGuide.value = true
|
||||
guideTimer.value = null
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (guideTimer.value) {
|
||||
clearTimeout(guideTimer.value)
|
||||
guideTimer.value = null
|
||||
}
|
||||
})
|
||||
|
||||
const currentCategory = computed(
|
||||
() => categories.value[selectedMenuIndex.value] ?? categories.value[0]
|
||||
)
|
||||
@@ -133,6 +153,8 @@ const selectMenuItem = (index: number, item: { id: string; name: string; disable
|
||||
}
|
||||
selectedMenuIndex.value = index
|
||||
currentIndex.value = 0
|
||||
viewedMenuKeys.value.add(item.id)
|
||||
maybeTriggerGuide()
|
||||
}
|
||||
|
||||
// 下一张图片
|
||||
@@ -245,7 +267,7 @@ const prevPhone = () => {
|
||||
|
||||
<!-- 引导步骤 -->
|
||||
<GuideSteps
|
||||
:visible="true"
|
||||
:visible="showGuide"
|
||||
:steps="steps"
|
||||
:highlight-padding="12"
|
||||
:highlight-radius="20"
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { useFilmStore } from '@/stores/modules/film'
|
||||
|
||||
const router = useRouter()
|
||||
const filmStore = useFilmStore()
|
||||
const guideKey = 'film-onsite-intro'
|
||||
const showGuide = ref(!filmStore.guideShown[guideKey])
|
||||
|
||||
if (showGuide.value) {
|
||||
filmStore.markGuideShown(guideKey)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
router.back()
|
||||
@@ -133,8 +142,10 @@ const goRandom = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<GuideSteps :visible="true" position-offset-y="85%">
|
||||
<GuideStepsItem>选择任意图片按钮,进入对应拍摄场景!</GuideStepsItem>
|
||||
<GuideSteps :visible="showGuide" position-offset-y="85%">
|
||||
<GuideStepsItem :audio-src="getPublicPath('audio/py_4_016.MP3')">
|
||||
接下来进入核心拍摄环节,我们依次来学习!
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ const questions = ref<Question[]>([
|
||||
{ id: 'd', label: '让画面信息量变得更多' }
|
||||
],
|
||||
correctOptionId: 'b',
|
||||
mediaSrc: getPublicPath('video/on-site/xianchangpaishe_dijiaodu.mp4')
|
||||
mediaSrc: getPublicPath('video/film/on-site/xianchangpaishe_dijiaodu.mp4')
|
||||
}
|
||||
])
|
||||
|
||||
@@ -101,7 +101,7 @@ const handleQuestionsComplete = () => {
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<ImgButton
|
||||
class="absolute right-0 top-0 z-20"
|
||||
class="absolute right-0 top-0 z-50"
|
||||
:image="IMAGE_PATHS.COMPONENTS.BACK"
|
||||
@click="handleBack"
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Video } from '@/components'
|
||||
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
import { getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -12,7 +12,7 @@ const handleBack = () => {
|
||||
}
|
||||
|
||||
// 创作随机性视频 URL(暂时用其他视频代替)
|
||||
const randomVideoUrl = getPublicPath('video/on-site/dygmk_czsjx_batch.mp4')
|
||||
const randomVideoUrl = getPublicPath('video/film/on-site/dygmk_czsjx_batch.mp4')
|
||||
|
||||
// 视频播放状态
|
||||
const videoPlaying = ref(false)
|
||||
@@ -20,14 +20,8 @@ const videoPlaying = ref(false)
|
||||
|
||||
<template>
|
||||
<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"
|
||||
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
|
||||
></div>
|
||||
|
||||
<!-- 视频播放区域 -->
|
||||
<div class="w-full h-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20">
|
||||
<div class="w-full h-full">
|
||||
<Video
|
||||
class="w-full h-full"
|
||||
:src="randomVideoUrl"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Video, QuestionQuiz, type Question } from '@/components'
|
||||
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
import { getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -12,7 +12,7 @@ const handleBack = () => {
|
||||
}
|
||||
|
||||
// 滑轨拍摄视频 URL
|
||||
const sliderVideoUrl = getPublicPath('video/on-site/dygmk_hgydjt_batch.mp4')
|
||||
const sliderVideoUrl = getPublicPath('video/film/on-site/dygmk_hgydjt_batch.mp4')
|
||||
|
||||
// 视频播放状态
|
||||
const videoEnded = ref(false)
|
||||
@@ -53,14 +53,7 @@ const handleQuestionsComplete = () => {
|
||||
|
||||
<template>
|
||||
<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"
|
||||
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
|
||||
></div>
|
||||
|
||||
<!-- 视频播放区域 -->
|
||||
<div class="w-full h-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20">
|
||||
<Video
|
||||
class="w-full h-full"
|
||||
:src="sliderVideoUrl"
|
||||
@@ -81,7 +74,6 @@ const handleQuestionsComplete = () => {
|
||||
/>
|
||||
</template>
|
||||
</Video>
|
||||
</div>
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<ImgButton
|
||||
|
||||
@@ -36,7 +36,7 @@ const questions = ref<Question[]>([
|
||||
{ id: 'c', label: '使用连拍模式' }
|
||||
],
|
||||
correctOptionId: 'b',
|
||||
mediaSrc: getPublicPath('video/on-site/xianchangpaishe_baitianmanmen.mp4')
|
||||
mediaSrc: getPublicPath('video/film/on-site/xianchangpaishe_baitianmanmen.mp4')
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
@@ -12,14 +12,14 @@ const handleBack = () => {
|
||||
}
|
||||
|
||||
// 慢门拍摄夜晚视频 URL(暂时用其他视频代替)
|
||||
const slowShutterNightVideoUrl = getPublicPath('video/on-site/dygmk_mmps_batch.mp4')
|
||||
const slowShutterNightVideoUrl = getPublicPath('video/film/on-site/dygmk_mmps_batch.mp4')
|
||||
|
||||
// 视频播放状态
|
||||
const videoPlaying = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative w-full h-full flex items-center justify-center overflow-hidden">
|
||||
<div class="relative w-full h-full overflow-hidden">
|
||||
<!-- 背景 -->
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center bg-no-repeat -z-10"
|
||||
@@ -27,7 +27,7 @@ const videoPlaying = ref(false)
|
||||
></div>
|
||||
|
||||
<!-- 视频播放区域 -->
|
||||
<div class="w-full h-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20">
|
||||
<div class="w-full h-full">
|
||||
<Video
|
||||
class="w-full h-full"
|
||||
:src="slowShutterNightVideoUrl"
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
const router = useRouter()
|
||||
|
||||
// 延时拍摄视频 URL(占位,后续可替换)
|
||||
const timeLapseVideoUrl = getPublicPath('video/on-site/xianchangpaishe_yanshipaishe.mp4')
|
||||
const timeLapseVideoUrl = getPublicPath('video/film/on-site/xianchangpaishe_yanshipaishe.mp4')
|
||||
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
@@ -81,15 +81,32 @@ const handleQuestionsComplete = () => {
|
||||
<h2 class="text-4xl font-bold mb-4 leading-relaxed">总结</h2>
|
||||
<p>在这个延时画面中,注意天空变化与光影移动的节奏,它们在短时间内被连续呈现出来。</p>
|
||||
|
||||
<p><span>延时拍摄的主要目的</span>,<span>是交代时间流逝与环境状态</span>。</p>
|
||||
<p>
|
||||
<span>这里的拍摄对象是江边的建筑群与天空</span
|
||||
>。<span>通过固定机位的连续拍摄</span>,<span>将原本需要较长时间才能观察到的</span
|
||||
><span>天空变化和光影移动</span>,<span>压缩到一个较短的镜头中呈现</span>。
|
||||
<span>延时拍摄的主要目的</span>
|
||||
,
|
||||
<span>是交代时间流逝与环境状态</span>
|
||||
。
|
||||
</p>
|
||||
<p>
|
||||
<span>这种处理方式</span
|
||||
>,<span>非常适合放置在视频片头阶段</span>,<span>用于快速建立时间感与空间环境</span>,<span>而不会占用过多时长</span>。
|
||||
<span>这里的拍摄对象是江边的建筑群与天空</span>
|
||||
。
|
||||
<span>通过固定机位的连续拍摄</span>
|
||||
,
|
||||
<span>将原本需要较长时间才能观察到的</span>
|
||||
<span>天空变化和光影移动</span>
|
||||
,
|
||||
<span>压缩到一个较短的镜头中呈现</span>
|
||||
。
|
||||
</p>
|
||||
<p>
|
||||
<span>这种处理方式</span>
|
||||
,
|
||||
<span>非常适合放置在视频片头阶段</span>
|
||||
,
|
||||
<span>用于快速建立时间感与空间环境</span>
|
||||
,
|
||||
<span>而不会占用过多时长</span>
|
||||
。
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { GuideSteps } from '@/components/Guide'
|
||||
import { Overlay } from '@/components/Overlay'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { getBackgroundImage, 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/xsdh_jzgtf_batch.mp4'
|
||||
import videoSrc from '/video/new/composition/xsdh_jzgtf_batch.mp4'
|
||||
import KnowledgeCard from '@/components/KnowledgeCard/FilmKnowledgeCard.vue'
|
||||
import { Video } from '@/components'
|
||||
import { useFilmStore } from '@/stores'
|
||||
|
||||
const film = useFilmStore()
|
||||
import { useFilmStore } from '@/stores/modules/film'
|
||||
|
||||
type Btn = {
|
||||
key: string
|
||||
@@ -22,22 +20,22 @@ type Btn = {
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const filmStore = useFilmStore()
|
||||
const introGuideKey = 'film-shooting-intro'
|
||||
const compositionGuideKey = 'film-shooting-composition'
|
||||
const showIntroGuide = ref(!filmStore.guideShown[introGuideKey])
|
||||
const showCompositionGuide = ref(!filmStore.guideShown[compositionGuideKey])
|
||||
|
||||
if (showIntroGuide.value) {
|
||||
filmStore.markGuideShown(introGuideKey)
|
||||
}
|
||||
|
||||
const steps = ref([
|
||||
{
|
||||
id: '1',
|
||||
audioSrc: getPublicPath('audio/py_4_015.MP3'),
|
||||
content: '哇,徐汇滨江真的超适合取景!一起看看可以运用哪些构图方法吧~',
|
||||
highlightSelector: '.btn-1'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
content: '跟随箭头指引,一键解锁取景地',
|
||||
highlightSelector: '.btn-2'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
content: '跟随箭头指引,一键解锁取景地',
|
||||
highlightSelector: '.btn-3'
|
||||
}
|
||||
])
|
||||
|
||||
@@ -98,6 +96,13 @@ const handleModalClose = () => {
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
const openCompositionGuide = () => {
|
||||
showModal.value = true
|
||||
if (showCompositionGuide.value) {
|
||||
filmStore.markGuideShown(compositionGuideKey)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClick = (item: any) => {
|
||||
currentItem.value = item
|
||||
showModal2.value = true
|
||||
@@ -142,12 +147,12 @@ const back = () => {
|
||||
<ImgButton class="btn-3" image="film/shooting/油罐艺术公园入口.png" @click="handleClickB" />
|
||||
</div>
|
||||
<div class="shooting-composition btn-1">
|
||||
<ImgButton image="shooting/构图指南.png" @click="showModal = true" />
|
||||
<ImgButton image="shooting/构图指南.png" @click="openCompositionGuide" />
|
||||
</div>
|
||||
|
||||
<!-- 引导步骤 -->
|
||||
<GuideSteps
|
||||
:visible="true"
|
||||
:visible="showIntroGuide"
|
||||
:steps="steps"
|
||||
:highlight-padding="12"
|
||||
:highlight-radius="20"
|
||||
@@ -196,7 +201,7 @@ const back = () => {
|
||||
</div>
|
||||
|
||||
<GuideSteps
|
||||
:visible="true"
|
||||
:visible="showCompositionGuide"
|
||||
:steps="guideSteps"
|
||||
:keyboard-navigation="true"
|
||||
:highlight-padding="12"
|
||||
|
||||
@@ -6,8 +6,10 @@ import { StarRating } from '@/components/Rating'
|
||||
import { useRouter } from 'vue-router'
|
||||
import BackToTopIcon from '@/assets/svg/back-to-top.svg'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { useFilmStore } from '@/stores/modules/film'
|
||||
|
||||
const router = useRouter()
|
||||
const filmStore = useFilmStore()
|
||||
|
||||
const TOTAL_PAGES = 3
|
||||
const PAGE_HEIGHT = 1080
|
||||
@@ -34,6 +36,7 @@ const showSittingPerson = computed<boolean>(() => {
|
||||
})
|
||||
|
||||
const visible = ref<boolean>(false)
|
||||
const guideKeyPrefix = 'film-practice-page'
|
||||
|
||||
//每一页的引导词
|
||||
const steps = ref([
|
||||
@@ -42,10 +45,18 @@ const steps = ref([
|
||||
'点击【查看】按钮,欣赏他人实战作品~'
|
||||
])
|
||||
|
||||
const currentGuideKey = computed(() => `${guideKeyPrefix}-${currentPage.value}`)
|
||||
|
||||
const contentSteps = computed(() => {
|
||||
return steps.value[currentPage.value]
|
||||
})
|
||||
|
||||
const showPageGuide = (): void => {
|
||||
if (filmStore.guideShown[currentGuideKey.value]) return
|
||||
visible.value = true
|
||||
filmStore.markGuideShown(currentGuideKey.value)
|
||||
}
|
||||
|
||||
/** 他人实战作品列表 */
|
||||
interface WorkItem {
|
||||
id: number
|
||||
@@ -62,7 +73,7 @@ const worksList = ref<WorkItem[]>([
|
||||
image: getStaticImage('film/practice/学生案例'),
|
||||
title: '学生案例',
|
||||
stars: 4,
|
||||
path: '/video/practice/film_anli1.mp4',
|
||||
path: '/video/film/practice/film_anli1.mp4',
|
||||
description:
|
||||
'作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介'
|
||||
}
|
||||
@@ -115,7 +126,7 @@ const smoothScrollTo = (toPage: number): void => {
|
||||
} else {
|
||||
isAnimating.value = false
|
||||
currentPage.value = toPage
|
||||
visible.value = true
|
||||
showPageGuide()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +169,7 @@ const handleWheel = (event: WheelEvent): void => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
visible.value = true
|
||||
showPageGuide()
|
||||
containerRef.value?.addEventListener('wheel', handleWheel, { passive: false })
|
||||
})
|
||||
|
||||
@@ -333,11 +344,15 @@ onUnmounted(() => {
|
||||
<style scoped>
|
||||
/* 隐藏滚动条但保持可滚动 - 兼容主流浏览器 */
|
||||
.task-panel {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
-ms-overflow-style: none;
|
||||
/* IE/Edge */
|
||||
}
|
||||
|
||||
.task-panel::-webkit-scrollbar {
|
||||
display: none; /* Chrome/Safari/Opera */
|
||||
display: none;
|
||||
/* Chrome/Safari/Opera */
|
||||
}
|
||||
|
||||
/* 第一页人物动画容器 */
|
||||
|
||||
@@ -7,7 +7,15 @@ import { IMAGE_PATHS, getBackgroundImage, getPublicPath } from '@/utils'
|
||||
import { Overlay } from '@/components/Overlay'
|
||||
import { ref } from 'vue'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { useFilmStore } from '@/stores/modules/film'
|
||||
const router = useRouter()
|
||||
const filmStore = useFilmStore()
|
||||
const guideKey = 'film-practice-video-rating'
|
||||
const showGuide = ref(!filmStore.guideShown[guideKey])
|
||||
|
||||
if (showGuide.value) {
|
||||
filmStore.markGuideShown(guideKey)
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
path: {
|
||||
@@ -125,7 +133,7 @@ const handleBack = (): void => {
|
||||
</div>
|
||||
</div>
|
||||
</Overlay>
|
||||
<GuideSteps :visible="true" :highlight-selector="'.btn-1'" position-offset-y="60%">
|
||||
<GuideSteps :visible="showGuide" :highlight-selector="'.btn-1'" position-offset-y="60%">
|
||||
<GuideStepsItem>点击这里可以给作品评分噢~</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { IMAGE_PATHS, getStaticImage } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -28,9 +27,5 @@ const back = () => {
|
||||
<div class="relative h-full pt-28 overflow-y-scroll">
|
||||
<img :src="getStaticImage('film/img/资源筹备.png')" alt="" />
|
||||
</div>
|
||||
|
||||
<GuideSteps :visible="true" position-offset-y="85%">
|
||||
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</div>
|
||||
</template>
|
||||
@@ -5,7 +5,6 @@ import { Video } from '@/components'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ImagePreview } from '@/components'
|
||||
import { useScale } from '@/composables/useScale'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -44,7 +43,7 @@ const handleOpenImages = () => {
|
||||
|
||||
<div class="absolute top-60 left-36 w-[1000px] h-[700px] rounded-2xl overflow-hidden">
|
||||
<Video
|
||||
:src="getPublicPath('video/jiaoben.mp4')"
|
||||
:src="getPublicPath('video/film/script-writing/jiaoben.mp4')"
|
||||
playsinline
|
||||
controls
|
||||
:volume="1"
|
||||
@@ -57,8 +56,5 @@ const handleOpenImages = () => {
|
||||
@click="handleOpenImages"
|
||||
/>
|
||||
</div>
|
||||
<GuideSteps :visible="true" position-offset-y="85%">
|
||||
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</div>
|
||||
</template>
|
||||
@@ -4,7 +4,7 @@ import { ImgButton } from '@/components/Button'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useScale } from '@/composables/useScale'
|
||||
import { ImagePreview } from '@/components'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
// import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
|
||||
const { scale, offsetX, offsetY } = useScale()
|
||||
|
||||
@@ -46,8 +46,8 @@ const handleOpenImages = () => {
|
||||
@click="handleOpenImages"
|
||||
/>
|
||||
</div>
|
||||
<GuideSteps :visible="true" position-offset-y="85%">
|
||||
<!-- <GuideSteps :visible="true" position-offset-y="85%">
|
||||
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</GuideSteps> -->
|
||||
</div>
|
||||
</template>
|
||||
@@ -25,48 +25,50 @@ const status = ref(false)
|
||||
const showQuiz = ref(false) // 控制是否显示答题窗口
|
||||
|
||||
//引导步骤
|
||||
const steps = ref([
|
||||
{
|
||||
id: 'step1',
|
||||
content: '试着用这些方法记录身边的小事,你会发现,原来平凡的日子里有这么多值得捕捉的瞬间。'
|
||||
}
|
||||
])
|
||||
const steps = ref<{ id: string; content: string; audioSrc?: string }[]>([])
|
||||
|
||||
// 步骤导航数据
|
||||
const navigationSteps = ref([
|
||||
{
|
||||
id: 'step1',
|
||||
label: '后期制作',
|
||||
videoUrl: getPublicPath('video/film/work/dygmk_zf_batch.mp4'),
|
||||
content: '遮幅'
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
label: '拍摄执行',
|
||||
videoUrl: getPublicPath('video/film/work/dygmk_gx_batch.mp4'),
|
||||
content: '光线'
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
label: '拍摄执行',
|
||||
videoUrl: getPublicPath('video/film/work/dygmk_xs_batch.mp4'),
|
||||
content: '虚实'
|
||||
},
|
||||
{
|
||||
id: 'step4',
|
||||
label: '拍摄执行',
|
||||
videoUrl: getPublicPath('video/film/work/dygmk_txjt_batch.mp4'),
|
||||
content: '特写镜头'
|
||||
},
|
||||
{
|
||||
id: 'step5',
|
||||
label: '拍摄执行',
|
||||
videoUrl: getPublicPath('video/film/work/dygmk_fjt_batch.mp4'),
|
||||
content: '分镜头'
|
||||
},
|
||||
{
|
||||
id: 'step5',
|
||||
label: '后期制作',
|
||||
videoUrl: getPublicPath('video/film/work/dygmk_lj_batch.mp4'),
|
||||
content: '滤镜'
|
||||
},
|
||||
{
|
||||
id: 'step5',
|
||||
label: '拍摄执行',
|
||||
videoUrl: getPublicPath('video/film/work/dygmk_tqs_batch.mp4'),
|
||||
content: '同期声'
|
||||
}
|
||||
])
|
||||
@@ -75,16 +77,6 @@ const navigationSteps = ref([
|
||||
const showStepNavigation = ref(false)
|
||||
const currentNavigationStep = ref(0) // 对应视频索引,从0开始
|
||||
|
||||
// 视频列表
|
||||
const videoList = [
|
||||
getPublicPath('video/film/dygmk_zf_batch.mp4'),
|
||||
getPublicPath('video/film/dygmk_gx_batch.mp4'),
|
||||
getPublicPath('video/film/dygmk_xs_batch.mp4'),
|
||||
getPublicPath('video/film/dygmk_txjt_batch.mp4'),
|
||||
getPublicPath('video/film/dygmk_fjt_batch.mp4'),
|
||||
getPublicPath('video/film/dygmk_lj_batch.mp4'),
|
||||
getPublicPath('video/film/dygmk_tqs_batch.mp4')
|
||||
]
|
||||
const currentVideoIndex = ref(0)
|
||||
|
||||
// 题目数据:虚实(index 2)和分镜头(index 4)
|
||||
@@ -141,14 +133,14 @@ const getQuestionsForVideo = (index: number): Question[] => {
|
||||
}
|
||||
|
||||
//按顺序播放
|
||||
const currentVideo = computed(() => videoList[currentVideoIndex.value])
|
||||
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
|
||||
const resolvedSrc = computed(() => {
|
||||
if (key.value === 'case') return getPublicPath('video/film/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 ''
|
||||
})
|
||||
const handleVideoChange = () => {
|
||||
if (currentVideoIndex.value < videoList.length - 1) {
|
||||
if (currentVideoIndex.value < navigationSteps.value.length - 1) {
|
||||
currentVideoIndex.value++
|
||||
currentNavigationStep.value = currentVideoIndex.value
|
||||
} else {
|
||||
@@ -171,7 +163,8 @@ const handleClose = () => {
|
||||
if (key.value === 'idea') {
|
||||
film.setFilmUnlockProgress(1)
|
||||
router.push({
|
||||
name: 'Film'
|
||||
name: 'Film',
|
||||
query: { guide: 'return' }
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -234,11 +227,15 @@ const handleQuestionsComplete = () => {
|
||||
handleVideoChange()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (key.value === 'idea') {
|
||||
status.value = true
|
||||
}
|
||||
// 专业剖析引导结束后自动播放视频
|
||||
const handleProfessionGuideComplete = () => {
|
||||
visibleGuide.value = false
|
||||
nextTick(() => {
|
||||
playing.value = true
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -316,14 +313,16 @@ onMounted(() => {
|
||||
v-if="visibleGuide && key === 'profession'"
|
||||
position-offset-y="85%"
|
||||
@step-change="(_: any, index: number) => (guideStepIndex = index)"
|
||||
@complete="handleProfessionGuideComplete"
|
||||
@skip="handleProfessionGuideComplete"
|
||||
>
|
||||
<GuideStepsItem>
|
||||
<GuideStepsItem :audio-src="getPublicPath('audio/py_4_004.MP3')">
|
||||
从刚才的案例可以看出,电影感画面自带一种独特的艺术魅力——或大气恢弘,扑朔迷离或充满故事感。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem>
|
||||
<GuideStepsItem :audio-src="getPublicPath('audio/py_4_005.MP3')">
|
||||
作为短视频创作者,我们可以借助这些方法,在拍摄与剪辑中打造更有质感的画面。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem>
|
||||
<GuideStepsItem :audio-src="getPublicPath('audio/py_4_006.MP3')">
|
||||
<div class="flex flex-wrap items-center justify-start">
|
||||
<div>接下来,让我们一起看看打造电影感的几种关键技巧吧。</div>
|
||||
<div class="text-yellow-400">
|
||||
@@ -354,13 +353,14 @@ onMounted(() => {
|
||||
position-offset-y="85%"
|
||||
@complete="handleComplete"
|
||||
>
|
||||
<GuideStepsItem>
|
||||
<GuideStepsItem :audio-src="getPublicPath('audio/py_4_007.MP3')">
|
||||
一个好镜头,既要好看,又要好懂 —— 既有视觉吸引力,又能把信息清晰地传达出来。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem>当这些条件被满足,一个镜头就有了“高级感”。</GuideStepsItem>
|
||||
<GuideStepsItem>而优秀的短视频,就是靠无数这样的好镜头累积而成的。</GuideStepsItem>
|
||||
<GuideStepsItem>
|
||||
接下来,我们就以徐汇滨江为例,一起动手练习如何拍出属于它的意义与故事吧!
|
||||
<GuideStepsItem :audio-src="getPublicPath('audio/py_4_008.MP3')">
|
||||
当这些条件被满足,一个镜头就有了“高级感”。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem :audio-src="getPublicPath('audio/py_4_009.MP3')">
|
||||
而优秀的短视频,就是靠无数这样的好镜头累积而成的。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="tsx" name="FilmWork" setup>
|
||||
defineOptions({ name: 'FilmWork' })
|
||||
import { ref, computed } from 'vue'
|
||||
import { getBackgroundImage, IMAGE_PATHS, getStaticImage } 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'
|
||||
@@ -50,9 +50,7 @@ const currentItem = computed(() => {
|
||||
return sideSwitcherItems.value.find(item => item.key === selected.value)
|
||||
})
|
||||
|
||||
const currentHighlight = computed(() =>
|
||||
stepIndex.value === 0 ? '.img-switch-item--case' : '.work-border'
|
||||
)
|
||||
const currentHighlight = computed(() => (stepIndex.value === 2 ? '.work-border' : null))
|
||||
|
||||
const handleStepChange = (_: any, index: number) => {
|
||||
stepIndex.value = index
|
||||
@@ -194,15 +192,14 @@ function openImages(urls: string[], startIndex = 0) {
|
||||
@skip="handleGuideComplete"
|
||||
position-offset-y="85%"
|
||||
>
|
||||
<GuideStepsItem>
|
||||
点击右侧【案例赏析】、【专业剖析】、【思路提点】,可以切换不同内容
|
||||
<GuideStepsItem :audio-src="getPublicPath('audio/py_4_001.MP3')">
|
||||
你是否也听过这样的评价——“这个镜头,看起来很高级。”
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem>
|
||||
<div class="flex items-center justify-start">
|
||||
请点击
|
||||
<img class="w-16" :src="getStaticImage('film/sx')" alt="" />
|
||||
,进入案例赏析!
|
||||
</div>
|
||||
<GuideStepsItem :audio-src="getPublicPath('audio/py_4_002.MP3')">
|
||||
但所谓高级感,到底来自哪里?
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem :audio-src="getPublicPath('audio/py_4_003.MP3')">
|
||||
带着这个问题,我们一起来看看“钟表博物馆”的案例,看看普通画面是如何被赋予质感与故事感的。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ defineOptions({
|
||||
import { getBackgroundImage, getPublicPath, IMAGE_PATHS, getStaticImage } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { ModuleSwitcher } from './components/ModuleSwitcher'
|
||||
import StudyProgressPopup from './components/StudyProgressPopup.vue'
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useRouter, type RouteRecordName } from 'vue-router'
|
||||
import {
|
||||
@@ -35,10 +36,9 @@ type GuideNode = {
|
||||
children?: GuideChildNode[]
|
||||
}
|
||||
|
||||
const homeGuideShow = ref(true)
|
||||
const unlockedHomeGuideShow = ref(false)
|
||||
const elementGuideShow = ref(false)
|
||||
const unlockedHomeGuideCompleted = ref(false)
|
||||
const actionGuideKey = 'home-action-guide'
|
||||
const activeActionGuide = ref<'home' | 'unlocked' | 'element' | null>(null)
|
||||
const studyProgressVisible = ref(false)
|
||||
|
||||
const homeGuideSteps: GuideStep[] = [
|
||||
{
|
||||
@@ -109,16 +109,16 @@ const elementGuideSteps: GuideStep[] = [
|
||||
]
|
||||
|
||||
const handleHomeGuideComplete = () => {
|
||||
homeGuideShow.value = false
|
||||
activeActionGuide.value = null
|
||||
}
|
||||
|
||||
const handleUnlockedHomeGuideComplete = () => {
|
||||
unlockedHomeGuideShow.value = false
|
||||
activeActionGuide.value = null
|
||||
}
|
||||
|
||||
const handleElementGuideComplete = () => {
|
||||
elementGuideShow.value = false
|
||||
unlockedHomeGuideCompleted.value = true
|
||||
activeActionGuide.value = null
|
||||
basicElements.markGuideShown(actionGuideKey)
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
@@ -142,6 +142,7 @@ const imageMap = {
|
||||
|
||||
//获取新手导航引导状态
|
||||
const isGuideNewUser = computed(() => basicElements.isGuideNewUser)
|
||||
const hasFinishedActionGuide = computed(() => basicElements.guideShown[actionGuideKey] === true)
|
||||
|
||||
// 预加载所有图片
|
||||
const preloadImages = () => {
|
||||
@@ -155,8 +156,8 @@ const preloadImages = () => {
|
||||
// 事件处理函数
|
||||
const handleOpenGuide = () => {
|
||||
guideShow.value = true
|
||||
if (!homeGuideShow.value && isGuideNewUser.value) {
|
||||
unlockedHomeGuideShow.value = true
|
||||
if (activeActionGuide.value !== 'home' && isGuideNewUser.value && !hasFinishedActionGuide.value) {
|
||||
activeActionGuide.value = 'unlocked'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,8 +194,11 @@ const selectedImage = computed(() => {
|
||||
return getBackgroundImage(imageMap[selectedModule.value])
|
||||
})
|
||||
|
||||
const hasFinishedAllHomeGuides = computed(() => unlockedHomeGuideCompleted.value)
|
||||
const isSkillDisabled = computed(() => !hasFinishedAllHomeGuides.value)
|
||||
const isSkillDisabled = computed(() => !hasFinishedActionGuide.value)
|
||||
|
||||
const handleOpenStudyProgress = () => {
|
||||
studyProgressVisible.value = true
|
||||
}
|
||||
|
||||
const handleSkill = () => {
|
||||
if (isSkillDisabled.value) return
|
||||
@@ -268,8 +272,8 @@ const handleNodeClick = (node: GuideNode, event?: MouseEvent) => {
|
||||
}
|
||||
|
||||
showChildrenNodeId.value = node.id
|
||||
if (!unlockedHomeGuideShow.value && !unlockedHomeGuideCompleted.value) {
|
||||
elementGuideShow.value = true
|
||||
if (activeActionGuide.value !== 'unlocked' && !hasFinishedActionGuide.value) {
|
||||
activeActionGuide.value = 'element'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,25 +291,28 @@ const handleClickOutside = () => {
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
preloadImages()
|
||||
if (isGuideNewUser.value && !hasFinishedActionGuide.value) {
|
||||
activeActionGuide.value = 'home'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full relative">
|
||||
<GuideSteps
|
||||
v-if="isGuideNewUser && homeGuideShow"
|
||||
v-if="activeActionGuide === 'home'"
|
||||
:steps="homeGuideSteps"
|
||||
position-offset-y="82%"
|
||||
@complete="handleHomeGuideComplete"
|
||||
/>
|
||||
<GuideSteps
|
||||
v-if="unlockedHomeGuideShow"
|
||||
v-if="activeActionGuide === 'unlocked'"
|
||||
:steps="unlockedHomeGuideSteps"
|
||||
position-offset-y="82%"
|
||||
@complete="handleUnlockedHomeGuideComplete"
|
||||
/>
|
||||
<GuideSteps
|
||||
v-if="elementGuideShow"
|
||||
v-if="activeActionGuide === 'element'"
|
||||
:steps="elementGuideSteps"
|
||||
position-offset-y="82%"
|
||||
@complete="handleElementGuideComplete"
|
||||
@@ -317,6 +324,13 @@ onMounted(() => {
|
||||
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.ACTION_BG) }"
|
||||
></div>
|
||||
|
||||
<!-- 学习进度 -->
|
||||
<div class="absolute top-0 left-0">
|
||||
<ImgButton image="home/action/学习进度入口" @click="handleOpenStudyProgress" />
|
||||
</div>
|
||||
|
||||
<StudyProgressPopup v-model="studyProgressVisible" />
|
||||
|
||||
<div
|
||||
class="absolute top-4 left-4 flex items-center text-white text-xl bg-black/10 px-4 py-2 rounded-xl"
|
||||
>
|
||||
@@ -355,8 +369,11 @@ onMounted(() => {
|
||||
|
||||
<!-- 新手指导遮罩 -->
|
||||
<div
|
||||
v-if="isGuideNewUser"
|
||||
:class="['absolute inset-0 z-10', homeGuideShow ? 'bg-black/0' : 'bg-black/50']"
|
||||
v-if="isGuideNewUser && !hasFinishedActionGuide"
|
||||
:class="[
|
||||
'absolute inset-0 z-10',
|
||||
activeActionGuide === 'home' ? 'bg-black/0' : 'bg-black/50'
|
||||
]"
|
||||
></div>
|
||||
|
||||
<!-- 新手指导 -->
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { getBackgroundImage } from '@/utils'
|
||||
import { useBasicElementsStore, useFilmStore, useInterviewStore, useVlogStore } from '@/stores'
|
||||
import { AnchoredProgressBar, ImgButton, type ProgressAnchor } from '@/components'
|
||||
import { studyModuleAnchors, type StudyProgressModuleKey } from './studyProgressConfig'
|
||||
import { getProductionModuleProgress } from './studyProgressUtils'
|
||||
|
||||
defineOptions({
|
||||
name: 'StudyProgressPopup'
|
||||
})
|
||||
|
||||
type StudyProgressItem = {
|
||||
key: StudyProgressModuleKey
|
||||
progress: number
|
||||
anchors: ProgressAnchor[]
|
||||
}
|
||||
|
||||
const visible = defineModel<boolean>({ default: false })
|
||||
|
||||
const basicElements = useBasicElementsStore()
|
||||
const vlog = useVlogStore()
|
||||
const interview = useInterviewStore()
|
||||
const film = useFilmStore()
|
||||
|
||||
const rowPositions = {
|
||||
basic: { height: 168 },
|
||||
vlog: { height: 148 },
|
||||
interview: { height: 146 },
|
||||
film: { height: 146 }
|
||||
} as const
|
||||
const labelTextShadow =
|
||||
'0 0 5px rgba(255,255,255,0.95), 0 0 10px rgba(54,111,255,0.75), 0 3px 0 rgba(0, 48, 151, 0.75)'
|
||||
const totalTextShadow = '4px 5px 0 rgba(40, 130, 253, 0.75)'
|
||||
const childTextShadow = '0 0 6px rgba(54,250,255,0.9), 0 2px 0 rgba(0, 68, 151, 0.7)'
|
||||
|
||||
const basicProgress = computed(() =>
|
||||
basicElements.isBasicElementsUnlock ? 100 : basicElements.basicElementsUnlockProgress
|
||||
)
|
||||
const totalProgress = computed(() => {
|
||||
const total =
|
||||
basicProgress.value + vlog.progressPercent + interview.progressPercent + film.progressPercent
|
||||
return Math.round(total / 4)
|
||||
})
|
||||
const progressItems = computed<StudyProgressItem[]>(() => [
|
||||
{
|
||||
key: 'basic',
|
||||
progress: basicProgress.value,
|
||||
anchors: studyModuleAnchors.basic
|
||||
},
|
||||
{
|
||||
key: 'vlog',
|
||||
progress: getProductionModuleProgress('vlog', vlog.viewed),
|
||||
anchors: studyModuleAnchors.vlog
|
||||
},
|
||||
{
|
||||
key: 'interview',
|
||||
progress: getProductionModuleProgress('interview', interview.viewed),
|
||||
anchors: studyModuleAnchors.interview
|
||||
},
|
||||
{
|
||||
key: 'film',
|
||||
progress: getProductionModuleProgress('film', film.viewed),
|
||||
anchors: studyModuleAnchors.film
|
||||
}
|
||||
])
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 scale-95 blur-[8px]"
|
||||
enter-to-class="opacity-100 scale-100 blur-none"
|
||||
leave-active-class="transition-all duration-300 ease-in"
|
||||
leave-from-class="opacity-100 scale-100 blur-none"
|
||||
leave-to-class="opacity-0 scale-95 blur-[8px]"
|
||||
>
|
||||
<div
|
||||
v-if="visible"
|
||||
class="fixed inset-0 z-[100] bg-cover bg-center bg-no-repeat"
|
||||
:style="{ backgroundImage: getBackgroundImage('home/stuy/学习进度底图') }"
|
||||
>
|
||||
<ImgButton
|
||||
class="absolute right-0 top-0 z-10"
|
||||
@click="handleClose"
|
||||
:image="'components/close'"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="absolute left-1/2 top-[14%] flex -translate-x-1/2 items-end text-white font-[1000] tracking-widest pr-10"
|
||||
:style="{ textShadow: totalTextShadow }"
|
||||
>
|
||||
<span class="text-[clamp(36px,4.5vw,72px)] leading-none">总进度</span>
|
||||
<span class="text-[clamp(36px,4.5vw,72px)] leading-none">{{ totalProgress }}%</span>
|
||||
</div>
|
||||
|
||||
<div class="w-[1580px] mx-auto mt-[215px] flex flex-col pr-16 pl-64 gap-2">
|
||||
<AnchoredProgressBar
|
||||
v-for="item in progressItems"
|
||||
:key="item.key"
|
||||
class="relative w-full"
|
||||
:style="{ height: `${rowPositions[item.key].height}px` }"
|
||||
:progress="item.progress"
|
||||
:anchors="item.anchors"
|
||||
node-image="home/stuy/节点圆圈"
|
||||
:label-class="
|
||||
item.key === 'basic'
|
||||
? 'text-[clamp(24px,2.2vw,42px)] text-white font-[1000] tracking-wider'
|
||||
: 'text-[clamp(24px,2.1vw,40px)] text-white font-[1000] tracking-wider'
|
||||
"
|
||||
child-label-class="text-[clamp(20px,1.75vw,34px)] text-[#43f3f2] font-[1000] tracking-wider"
|
||||
percent-class="text-[clamp(22px,2vw,38px)] text-white font-[1000] italic"
|
||||
label-top="2%"
|
||||
child-label-top="34%"
|
||||
bar-top="58%"
|
||||
bar-height="18%"
|
||||
marker-top="50%"
|
||||
marker-height="34%"
|
||||
marker-width="1%"
|
||||
child-marker-top="53%"
|
||||
child-marker-height="28%"
|
||||
child-marker-width="0.3%"
|
||||
node-top="41%"
|
||||
node-height="52%"
|
||||
percent-top="78%"
|
||||
:label-style="{ textShadow: labelTextShadow }"
|
||||
:child-label-style="{ textShadow: childTextShadow }"
|
||||
:percent-style="{ textShadow: labelTextShadow }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { ProgressAnchor } from '@/components'
|
||||
|
||||
export type StudyProgressModuleKey = 'basic' | 'vlog' | 'interview' | 'film'
|
||||
|
||||
const productionAnchors: ProgressAnchor[] = [
|
||||
{ id: 'task', label: '任务取景框', value: 0 },
|
||||
{
|
||||
id: 'zoom',
|
||||
label: '制作变焦镜',
|
||||
value: 20,
|
||||
children: [
|
||||
{ id: 'prepare', label: '前期准备', value: 20 },
|
||||
{ id: 'shooting', label: '中期拍摄', value: 42 },
|
||||
{ id: 'editing', label: '后期剪辑', value: 64 }
|
||||
]
|
||||
},
|
||||
{ id: 'practice', label: '实战操作台', value: 85 },
|
||||
{ id: 'done', label: '', value: 100 }
|
||||
]
|
||||
|
||||
export const studyModuleAnchors: Record<StudyProgressModuleKey, ProgressAnchor[]> = {
|
||||
basic: [
|
||||
{ id: 'three-elements', label: '三大基础要素', value: 0 },
|
||||
{ id: 'shooting-skills', label: '基础拍摄技巧', value: 33.33 },
|
||||
{ id: 'editing-skills', label: '短视频剪辑技巧', value: 66.66 },
|
||||
{ id: 'done', label: '', value: 100 }
|
||||
],
|
||||
vlog: productionAnchors,
|
||||
interview: productionAnchors,
|
||||
film: productionAnchors
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { moduleStepMap } from '@/config/moduleStepMap'
|
||||
|
||||
type ProductionModuleKey = 'vlog' | 'interview' | 'film'
|
||||
|
||||
type StepKey = 1 | 2 | 3
|
||||
|
||||
const countViewedRoutes = (routes: readonly string[], viewed: Record<string, boolean>) => {
|
||||
if (routes.length === 0) return 0
|
||||
return routes.filter(routeName => viewed[routeName]).length
|
||||
}
|
||||
|
||||
const getStepProgress = (routes: readonly string[], viewed: Record<string, boolean>) => {
|
||||
if (routes.length === 0) return 0
|
||||
return countViewedRoutes(routes, viewed) / routes.length
|
||||
}
|
||||
|
||||
const interpolate = (start: number, end: number, progress: number) =>
|
||||
start + (end - start) * progress
|
||||
|
||||
export const getProductionModuleProgress = (
|
||||
moduleKey: ProductionModuleKey,
|
||||
viewed: Record<string, boolean>
|
||||
) => {
|
||||
const stepRoutes = moduleStepMap[moduleKey]
|
||||
const hasAnyViewed = Object.values(stepRoutes).some(routes =>
|
||||
routes.some(routeName => viewed[routeName])
|
||||
)
|
||||
|
||||
if (!hasAnyViewed) return 0
|
||||
|
||||
const stepProgress = (step: StepKey) => getStepProgress(stepRoutes[step], viewed)
|
||||
const step1Progress = stepProgress(1)
|
||||
const step2Progress = stepProgress(2)
|
||||
const step3Progress = stepProgress(3)
|
||||
|
||||
if (step1Progress < 1) return Math.round(interpolate(20, 42, step1Progress))
|
||||
if (step2Progress < 1) return Math.round(interpolate(42, 64, step2Progress))
|
||||
if (step3Progress < 1) return Math.round(interpolate(64, 85, step3Progress))
|
||||
|
||||
return 100
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { GuideSteps, ImagePreview } from '@/components'
|
||||
import { useScale } from '@/composables/useScale'
|
||||
import { useBasicElementsStore } from '@/stores/modules/basic-elements'
|
||||
|
||||
const CourseFrameGuide = () => (
|
||||
<div class="flex items-center justify-start">
|
||||
@@ -64,11 +65,17 @@ export default defineComponent({
|
||||
positionOffsetY: '40%'
|
||||
}
|
||||
])
|
||||
const showGuide = ref(true)
|
||||
const basicElementsStore = useBasicElementsStore()
|
||||
const guideKey = 'home-intro'
|
||||
const showGuide = ref(!basicElementsStore.guideShown[guideKey])
|
||||
const { scale, offsetX, offsetY } = useScale()
|
||||
|
||||
if (showGuide.value) {
|
||||
basicElementsStore.markGuideShown(guideKey)
|
||||
}
|
||||
|
||||
const handleComplete = () => {
|
||||
console.log('handleComplete')
|
||||
showGuide.value = false
|
||||
}
|
||||
|
||||
const openMindMap = () => {
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import ImgButton from '@/components/Button/ImgButton.vue'
|
||||
// import { GuideSteps } from '@/components/Guide'
|
||||
import { IMAGE_PATHS } from '@/utils'
|
||||
const router = useRouter()
|
||||
|
||||
// 导入视频文件
|
||||
import videoadjustmentColor from '/video/vlog_renwuqujing_tiaose.mp4'
|
||||
const videoList = [videoadjustmentColor]
|
||||
|
||||
const videoRef = ref<HTMLVideoElement>()
|
||||
const currentVideoIndex = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
// 确保视频元素加载并自动播放
|
||||
if (videoRef.value) {
|
||||
videoRef.value.src = videoList[currentVideoIndex.value]
|
||||
videoRef.value.load()
|
||||
|
||||
// 尝试自动播放
|
||||
const playPromise = videoRef.value.play()
|
||||
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.catch(error => {
|
||||
// 自动播放失败,可能是浏览器策略阻止,需要用户交互
|
||||
console.log('自动播放被阻止:', error)
|
||||
})
|
||||
}
|
||||
|
||||
// 监听视频结束事件
|
||||
videoRef.value.addEventListener('ended', playNextVideo)
|
||||
}
|
||||
})
|
||||
|
||||
// 播放下一个视频
|
||||
const playNextVideo = () => {
|
||||
if (currentVideoIndex.value < videoList.length - 1) {
|
||||
currentVideoIndex.value++
|
||||
if (videoRef.value) {
|
||||
videoRef.value.src = videoList[currentVideoIndex.value]
|
||||
videoRef.value.load()
|
||||
|
||||
// 自动播放下一个视频
|
||||
const playPromise = videoRef.value.play()
|
||||
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.catch(error => {
|
||||
console.log('自动播放被阻止:', error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 监听当前视频索引变化
|
||||
watch(currentVideoIndex, newIndex => {
|
||||
if (videoRef.value) {
|
||||
videoRef.value.src = videoList[newIndex]
|
||||
videoRef.value.load()
|
||||
}
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!--使用动态绑定和ref-->
|
||||
<video
|
||||
ref="videoRef"
|
||||
:src="videoList[currentVideoIndex]"
|
||||
type="video/mp4"
|
||||
controls
|
||||
preload="auto"
|
||||
class="video-player"
|
||||
autoplay
|
||||
>
|
||||
<p>你的浏览器不支持video标签.</p>
|
||||
</video>
|
||||
<ImgButton
|
||||
class="absolute right-0 top-0 z-10"
|
||||
:image="IMAGE_PATHS.COMPONENTS.BACK"
|
||||
@click="handleClose"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.video-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
@@ -1,38 +1,311 @@
|
||||
<script lang="ts" setup>
|
||||
import { IMAGE_PATHS, getStaticImage } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Overlay } from '@/components/Overlay'
|
||||
import { ref } from 'vue'
|
||||
import ImageFilter from '@/components/ImageFilter/ImageFilter.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const back = () => {
|
||||
const selected = ref<number | undefined>(1)
|
||||
const btnState = ref(false)
|
||||
const screenshot = ref()
|
||||
const showModal = ref(false)
|
||||
const activeTab = ref<'color' | 'filter'>('color') // 'color' | 'filter'
|
||||
|
||||
const adjustmentList = ref([
|
||||
{ name: '亮度', value: 0, min: -100, max: 100 },
|
||||
{ name: '对比度', value: 0, min: -100, max: 100 },
|
||||
{ name: '饱和度', value: 0, min: -100, max: 100 },
|
||||
{ name: '曝光', value: 0, min: -100, max: 100 },
|
||||
{ name: '色调', value: 0, min: 0, max: 100 },
|
||||
{ name: '锐化', value: 0, min: 0, max: 100 },
|
||||
{ name: '模糊', value: 0, min: 0, max: 100 }
|
||||
])
|
||||
|
||||
const colorBarList = ref([
|
||||
{ id: 1, name: '原图', filter: 'none' },
|
||||
{ id: 2, name: '暖阳', filter: 'saturate(2.5) contrast(2.1)' },
|
||||
{ id: 3, name: '冷调', filter: 'hue-rotate(180deg) contrast(0.9)' },
|
||||
{ id: 4, name: '复古', filter: 'sepia(0.6) contrast(1.2) brightness(0.9)' },
|
||||
{ id: 5, name: '黑白', filter: 'grayscale(1)' },
|
||||
{ id: 6, name: '鲜艳', filter: 'sepia(0.1) saturate(2.5) contrast(2.1) brightness(1)' }
|
||||
])
|
||||
|
||||
// 获取指定参数的值
|
||||
// 获取指定参数的值
|
||||
const imageFilterRef = ref<InstanceType<typeof ImageFilter> | null>(null)
|
||||
|
||||
// 获取指定参数的值
|
||||
const getVal = (name: string) => {
|
||||
const val = adjustmentList.value.find(item => item.name === name)?.value || 0
|
||||
return Number(val)
|
||||
}
|
||||
|
||||
// 更新滤镜效果
|
||||
const updateFilter = () => {
|
||||
if (!imageFilterRef.value) return
|
||||
|
||||
// 如果开启了对比模式(显示原图),则不应用任何滤镜
|
||||
if (btnState.value) {
|
||||
imageFilterRef.value.setOptions({
|
||||
brightness: 0,
|
||||
contrast: 0,
|
||||
saturation: 0,
|
||||
exposure: 0,
|
||||
hue: 0,
|
||||
blur: 0,
|
||||
sharpen: 0,
|
||||
filter: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const selectedFilter = colorBarList.value.find(v => v.id === selected.value)?.filter || 'none'
|
||||
|
||||
imageFilterRef.value.setOptions({
|
||||
brightness: getVal('亮度'),
|
||||
contrast: getVal('对比度'),
|
||||
saturation: getVal('饱和度'),
|
||||
exposure: getVal('曝光'),
|
||||
hue: getVal('色调'),
|
||||
blur: getVal('模糊'),
|
||||
sharpen: getVal('锐化'),
|
||||
filter: selectedFilter
|
||||
})
|
||||
}
|
||||
|
||||
const changeOriginalImgae = () => {
|
||||
btnState.value = !btnState.value
|
||||
updateFilter()
|
||||
}
|
||||
|
||||
// Tab 切换处理
|
||||
const handleTabChange = (tab: 'color' | 'filter') => {
|
||||
activeTab.value = tab
|
||||
// 切换时重置所有参数
|
||||
handleReset()
|
||||
}
|
||||
|
||||
// 重置所有参数
|
||||
const handleReset = () => {
|
||||
adjustmentList.value.forEach(item => (item.value = 0))
|
||||
selected.value = 1
|
||||
updateFilter()
|
||||
}
|
||||
|
||||
// 生成图片
|
||||
const handleGenerate = async () => {
|
||||
if (!imageFilterRef.value) return
|
||||
|
||||
try {
|
||||
const dataUrl = await imageFilterRef.value.generate()
|
||||
screenshot.value = dataUrl
|
||||
showModal.value = true
|
||||
} catch (error) {
|
||||
console.error('生成图片失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalClose = () => {
|
||||
showModal.value = false
|
||||
screenshot.value = null
|
||||
}
|
||||
|
||||
const handleTryOther = () => {
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
const handleFinish = () => {
|
||||
router.push({
|
||||
name: 'Interview'
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const changeColor = (item: { id: number; name: string }) => {
|
||||
selected.value = item.id
|
||||
updateFilter()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative w-full h-full overflow-hidden">
|
||||
<div class="w-full h-full relative pt-44">
|
||||
<!-- 返回 -->
|
||||
<ImgButton
|
||||
class="absolute right-0 top-0 z-20"
|
||||
class="absolute right-0 top-0 z-10"
|
||||
:image="IMAGE_PATHS.COMPONENTS.BACK"
|
||||
@click="back"
|
||||
@click="handleClose"
|
||||
/>
|
||||
<img class="absolute inset-0" :src="getStaticImage('img/背景.png')" alt="" />
|
||||
|
||||
<img class="absolute top-5 z-10" :src="getStaticImage('img/调色-标题.png')" alt="" />
|
||||
<!-- 背景图片-center -->
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center bg-no-repeat -z-10"
|
||||
:style="{
|
||||
backgroundImage: getBackgroundImage('after-editing/color/底图')
|
||||
}"
|
||||
></div>
|
||||
|
||||
<div class="relative h-full pt-28 overflow-y-scroll">
|
||||
<img :src="getStaticImage('interview/img/调色实践.png')" alt="" />
|
||||
<div class="flex h-[759px] justify-center space-x-12">
|
||||
<!-- 效果图 -->
|
||||
<div
|
||||
class="relative w-[1273px] h-[732px] bg-no-repeat bg-cover mt-10 p-2 pb-[26px] pr-[26px]"
|
||||
:style="{ backgroundImage: getBackgroundImage('after-editing/color/图片框.png') }"
|
||||
>
|
||||
<ImageFilter ref="imageFilterRef" :src="getStaticImage('interview/color/访谈调色')" />
|
||||
|
||||
<!-- 开启对比 -->
|
||||
<ImgButton
|
||||
class="absolute top-[1180px] left-[51.5%] -translate-x-1/2"
|
||||
image="after-editing/color/调色实践"
|
||||
@click="router.push({ name: 'InterviewColor' })"
|
||||
/>
|
||||
class="absolute top-2 right-6 flex-[0_0_200px]"
|
||||
@click="changeOriginalImgae"
|
||||
:image="
|
||||
btnState
|
||||
? 'after-editing/color/开启对比 - 可开启'
|
||||
: 'after-editing/color/开启对比 - 不可开启'
|
||||
"
|
||||
></ImgButton>
|
||||
</div>
|
||||
|
||||
<GuideSteps :visible="true" position-offset-y="85%">
|
||||
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
<!-- 滤镜/参数配置 -->
|
||||
<div>
|
||||
<div
|
||||
class="w-[445px] h-[759px] bg-no-repeat bg-cover p-2 rounded-xl"
|
||||
:style="{ backgroundImage: getBackgroundImage('after-editing/color/小底板') }"
|
||||
>
|
||||
<!-- tab切换 -->
|
||||
<header class="flex h-[75px] items-center justify-center space-x-4">
|
||||
<div @click="handleTabChange('color')">
|
||||
<ImgButton
|
||||
:image="
|
||||
activeTab !== 'color'
|
||||
? 'after-editing/color/调色-暗'
|
||||
: 'after-editing/color/调色-亮'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div @click="handleTabChange('filter')">
|
||||
<ImgButton
|
||||
:image="
|
||||
activeTab !== 'filter'
|
||||
? 'after-editing/color/滤镜-暗'
|
||||
: 'after-editing/color/滤镜-亮'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<!-- tab-content -->
|
||||
<div class="px-4 h-[calc(100%-75px)]">
|
||||
<!-- 调色内容 -->
|
||||
<div
|
||||
v-show="activeTab === 'color'"
|
||||
class="space-y-6 h-full overflow-y-auto custom-scrollbar pt-4"
|
||||
>
|
||||
<div v-for="item in adjustmentList" :key="item.name" class="px-2">
|
||||
<div class="flex justify-between text-[#648ab2] text-2xl font-bold mb-2">
|
||||
<span>{{ item.name }}</span>
|
||||
<span>{{ item.value }}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
v-model.number="item.value"
|
||||
:min="item.min"
|
||||
:max="item.max"
|
||||
@change="updateFilter"
|
||||
class="w-full h-3 bg-[#b8cfe7] rounded-lg appearance-none cursor-pointer slider-thumb"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 滤镜内容 -->
|
||||
<div
|
||||
v-show="activeTab === 'filter'"
|
||||
class="space-y-10 h-full overflow-y-auto custom-scrollbar pt-10"
|
||||
>
|
||||
<div
|
||||
v-for="item in colorBarList"
|
||||
:key="item.id"
|
||||
:style="{
|
||||
backgroundImage:
|
||||
selected == item.id
|
||||
? getBackgroundImage('after-editing/color/滤镜-选中')
|
||||
: getBackgroundImage('after-editing/color/滤镜-未选中')
|
||||
}"
|
||||
:class="[
|
||||
'mx-auto w-[373px] h-[62px] text-white text-2xl font-bold flex items-center justify-center cursor-pointer bg-no-repeat bg-contain bg-center transition-all hover:scale-105',
|
||||
selected !== item.id && 'text-gray-700'
|
||||
]"
|
||||
@click="changeColor(item)"
|
||||
>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-around mt-4">
|
||||
<!-- 重置 -->
|
||||
<ImgButton image="after-editing/color/重置" @click="handleReset" />
|
||||
<!-- 生成 -->
|
||||
<ImgButton image="after-editing/color/生成" @click="handleGenerate" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- title -->
|
||||
<img class="absolute top-0 left-0" :src="getStaticImage('after-editing/color/标题-调色实践')" />
|
||||
|
||||
<!-- 回到顶部按钮 - 最后一页显示 -->
|
||||
<p class="absolute left-40 bottom-10 flex items-center">
|
||||
<img class="rotate-0 w-28 mr-4" src="@/assets/svg/deng.svg" />
|
||||
<span class="text-white text-2xl font-bold">
|
||||
推荐参数:亮度 90、对比度 100、饱和度 100、曝光 - 90、色调 0、锐化 100、模糊 0
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<!-- 保存图片弹窗 -->
|
||||
<Overlay :visible="showModal" @close="handleModalClose">
|
||||
<div class="relative w-[1000px] h-[600px] flex items-center justify-center">
|
||||
<!-- 截图内容 -->
|
||||
<div class="bg-white p-2 rounded-md -rotate-6">
|
||||
<img :src="screenshot" alt="调色结果" />
|
||||
</div>
|
||||
<!-- <img :src="screenshot" alt="调色结果" class="max-w-full max-h-full object-contain rotate-2" /> -->
|
||||
<!-- 操作按钮 -->
|
||||
<div class="absolute bottom-1/2 -left-1/3 translate-y-1/2">
|
||||
<ImgButton image="after-editing/color/按钮-试试其他调色" @click="handleTryOther" />
|
||||
</div>
|
||||
<div class="absolute bottom-1/2 -right-1/3 translate-y-1/2">
|
||||
<ImgButton image="after-editing/color/按钮-结束调色实践" @click="handleFinish" />
|
||||
</div>
|
||||
</div>
|
||||
</Overlay>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义滑块样式 */
|
||||
.slider-thumb::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
background: #92c6f2;
|
||||
border: 3px solid #ffffff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
box-shadow: 4px 4px 2px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.slider-thumb::-moz-range-thumb {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
background: #92c6f2;
|
||||
border: 3px solid #ffffff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
box-shadow: 4px 4px 2px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
</style>
|
||||
@@ -1,309 +1,38 @@
|
||||
<script lang="ts" setup>
|
||||
import router from '@/router'
|
||||
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { IMAGE_PATHS, getStaticImage } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Overlay } from '@/components/Overlay'
|
||||
import { ref } from 'vue'
|
||||
import ImageFilter from '@/components/ImageFilter/ImageFilter.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
// import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
|
||||
const selected = ref<number | undefined>(1)
|
||||
const btnState = ref(false)
|
||||
const screenshot = ref()
|
||||
const showModal = ref(false)
|
||||
const activeTab = ref<'color' | 'filter'>('color') // 'color' | 'filter'
|
||||
const router = useRouter()
|
||||
|
||||
const adjustmentList = ref([
|
||||
{ name: '亮度', value: 0, min: -100, max: 100 },
|
||||
{ name: '对比度', value: 0, min: -100, max: 100 },
|
||||
{ name: '饱和度', value: 0, min: -100, max: 100 },
|
||||
{ name: '曝光', value: 0, min: -100, max: 100 },
|
||||
{ name: '色调', value: 0, min: 0, max: 100 },
|
||||
{ name: '锐化', value: 0, min: 0, max: 100 },
|
||||
{ name: '模糊', value: 0, min: 0, max: 100 }
|
||||
])
|
||||
|
||||
const colorBarList = ref([
|
||||
{ id: 1, name: '原图', filter: 'none' },
|
||||
{ id: 2, name: '暖阳', filter: 'saturate(2.5) contrast(2.1)' },
|
||||
{ id: 3, name: '冷调', filter: 'hue-rotate(180deg) contrast(0.9)' },
|
||||
{ id: 4, name: '复古', filter: 'sepia(0.6) contrast(1.2) brightness(0.9)' },
|
||||
{ id: 5, name: '黑白', filter: 'grayscale(1)' },
|
||||
{ id: 6, name: '鲜艳', filter: 'sepia(0.1) saturate(2.5) contrast(2.1) brightness(1)' }
|
||||
])
|
||||
|
||||
// 获取指定参数的值
|
||||
// 获取指定参数的值
|
||||
const imageFilterRef = ref<InstanceType<typeof ImageFilter> | null>(null)
|
||||
|
||||
// 获取指定参数的值
|
||||
const getVal = (name: string) => {
|
||||
const val = adjustmentList.value.find(item => item.name === name)?.value || 0
|
||||
return Number(val)
|
||||
}
|
||||
|
||||
// 更新滤镜效果
|
||||
const updateFilter = () => {
|
||||
if (!imageFilterRef.value) return
|
||||
|
||||
// 如果开启了对比模式(显示原图),则不应用任何滤镜
|
||||
if (btnState.value) {
|
||||
imageFilterRef.value.setOptions({
|
||||
brightness: 0,
|
||||
contrast: 0,
|
||||
saturation: 0,
|
||||
exposure: 0,
|
||||
hue: 0,
|
||||
blur: 0,
|
||||
sharpen: 0,
|
||||
filter: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const selectedFilter = colorBarList.value.find(v => v.id === selected.value)?.filter || 'none'
|
||||
|
||||
imageFilterRef.value.setOptions({
|
||||
brightness: getVal('亮度'),
|
||||
contrast: getVal('对比度'),
|
||||
saturation: getVal('饱和度'),
|
||||
exposure: getVal('曝光'),
|
||||
hue: getVal('色调'),
|
||||
blur: getVal('模糊'),
|
||||
sharpen: getVal('锐化'),
|
||||
filter: selectedFilter
|
||||
})
|
||||
}
|
||||
|
||||
const changeOriginalImgae = () => {
|
||||
btnState.value = !btnState.value
|
||||
updateFilter()
|
||||
}
|
||||
|
||||
// Tab 切换处理
|
||||
const handleTabChange = (tab: 'color' | 'filter') => {
|
||||
activeTab.value = tab
|
||||
// 切换时重置所有参数
|
||||
handleReset()
|
||||
}
|
||||
|
||||
// 重置所有参数
|
||||
const handleReset = () => {
|
||||
adjustmentList.value.forEach(item => (item.value = 0))
|
||||
selected.value = 1
|
||||
updateFilter()
|
||||
}
|
||||
|
||||
// 生成图片
|
||||
const handleGenerate = async () => {
|
||||
if (!imageFilterRef.value) return
|
||||
|
||||
try {
|
||||
const dataUrl = await imageFilterRef.value.generate()
|
||||
screenshot.value = dataUrl
|
||||
showModal.value = true
|
||||
} catch (error) {
|
||||
console.error('生成图片失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalClose = () => {
|
||||
showModal.value = false
|
||||
screenshot.value = null
|
||||
}
|
||||
|
||||
const handleTryOther = () => {
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
const handleFinish = () => {
|
||||
router.push({
|
||||
name: 'Film'
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
const back = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const changeColor = (item: { id: number; name: string }) => {
|
||||
selected.value = item.id
|
||||
updateFilter()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full relative pt-44">
|
||||
<!-- 返回 -->
|
||||
<div class="relative w-full h-full overflow-hidden">
|
||||
<ImgButton
|
||||
class="absolute right-0 top-0 z-10"
|
||||
class="absolute right-0 top-0 z-20"
|
||||
:image="IMAGE_PATHS.COMPONENTS.BACK"
|
||||
@click="handleClose"
|
||||
@click="back"
|
||||
/>
|
||||
<img class="absolute inset-0" :src="getStaticImage('img/背景.png')" alt="" />
|
||||
|
||||
<!-- 背景图片-center -->
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center bg-no-repeat -z-10"
|
||||
:style="{
|
||||
backgroundImage: getBackgroundImage('after-editing/color/底图')
|
||||
}"
|
||||
></div>
|
||||
<img class="absolute top-5 z-10" :src="getStaticImage('img/调色-标题.png')" alt="" />
|
||||
|
||||
<div class="flex h-[759px] justify-center space-x-12">
|
||||
<!-- 效果图 -->
|
||||
<div
|
||||
class="relative w-[1273px] h-[732px] bg-no-repeat bg-cover mt-10 p-2 pb-[26px] pr-[26px]"
|
||||
:style="{ backgroundImage: getBackgroundImage('after-editing/color/图片框.png') }"
|
||||
>
|
||||
<ImageFilter ref="imageFilterRef" :src="getStaticImage('after-editing/color/film_img')" />
|
||||
|
||||
<!-- 开启对比 -->
|
||||
<div class="relative h-full pt-28 overflow-y-scroll">
|
||||
<img :src="getStaticImage('interview/img/调色实践.png')" alt="" />
|
||||
<ImgButton
|
||||
class="absolute top-2 right-6 flex-[0_0_200px]"
|
||||
@click="changeOriginalImgae"
|
||||
:image="
|
||||
btnState
|
||||
? 'after-editing/color/开启对比 - 可开启'
|
||||
: 'after-editing/color/开启对比 - 不可开启'
|
||||
"
|
||||
></ImgButton>
|
||||
</div>
|
||||
|
||||
<!-- 滤镜/参数配置 -->
|
||||
<div>
|
||||
<div
|
||||
class="w-[445px] h-[759px] bg-no-repeat bg-cover p-2 rounded-xl"
|
||||
:style="{ backgroundImage: getBackgroundImage('after-editing/color/小底板') }"
|
||||
>
|
||||
<!-- tab切换 -->
|
||||
<header class="flex h-[75px] items-center justify-center space-x-4">
|
||||
<div @click="handleTabChange('color')">
|
||||
<ImgButton
|
||||
:image="
|
||||
activeTab !== 'color'
|
||||
? 'after-editing/color/调色-暗'
|
||||
: 'after-editing/color/调色-亮'
|
||||
"
|
||||
class="absolute top-[1180px] left-[51.5%] -translate-x-1/2"
|
||||
image="after-editing/color/调色实践"
|
||||
@click="router.push({ name: 'InterviewColorPractice' })"
|
||||
/>
|
||||
</div>
|
||||
<div @click="handleTabChange('filter')">
|
||||
<ImgButton
|
||||
:image="
|
||||
activeTab !== 'filter'
|
||||
? 'after-editing/color/滤镜-暗'
|
||||
: 'after-editing/color/滤镜-亮'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<!-- tab-content -->
|
||||
<div class="px-4 h-[calc(100%-75px)]">
|
||||
<!-- 调色内容 -->
|
||||
<div
|
||||
v-show="activeTab === 'color'"
|
||||
class="space-y-6 h-full overflow-y-auto custom-scrollbar pt-4"
|
||||
>
|
||||
<div v-for="item in adjustmentList" :key="item.name" class="px-2">
|
||||
<div class="flex justify-between text-[#648ab2] text-2xl font-bold mb-2">
|
||||
<span>{{ item.name }}</span>
|
||||
<span>{{ item.value }}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
v-model.number="item.value"
|
||||
:min="item.min"
|
||||
:max="item.max"
|
||||
@change="updateFilter"
|
||||
class="w-full h-3 bg-[#b8cfe7] rounded-lg appearance-none cursor-pointer slider-thumb"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 滤镜内容 -->
|
||||
<div
|
||||
v-show="activeTab === 'filter'"
|
||||
class="space-y-10 h-full overflow-y-auto custom-scrollbar pt-10"
|
||||
>
|
||||
<div
|
||||
v-for="item in colorBarList"
|
||||
:key="item.id"
|
||||
:style="{
|
||||
backgroundImage:
|
||||
selected == item.id
|
||||
? getBackgroundImage('after-editing/color/滤镜-选中')
|
||||
: getBackgroundImage('after-editing/color/滤镜-未选中')
|
||||
}"
|
||||
:class="[
|
||||
'mx-auto w-[373px] h-[62px] text-white text-2xl font-bold flex items-center justify-center cursor-pointer bg-no-repeat bg-contain bg-center transition-all hover:scale-105',
|
||||
selected !== item.id && 'text-gray-700'
|
||||
]"
|
||||
@click="changeColor(item)"
|
||||
>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-around mt-4">
|
||||
<!-- 重置 -->
|
||||
<ImgButton image="after-editing/color/重置" @click="handleReset" />
|
||||
<!-- 生成 -->
|
||||
<ImgButton image="after-editing/color/生成" @click="handleGenerate" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- title -->
|
||||
<img class="absolute top-0 left-0" :src="getStaticImage('after-editing/color/标题-调色实践')" />
|
||||
|
||||
<!-- 回到顶部按钮 - 最后一页显示 -->
|
||||
<p class="absolute left-40 bottom-10 flex items-center">
|
||||
<img class="rotate-0 w-28 mr-4" src="@/assets/svg/deng.svg" />
|
||||
<span class="text-white text-2xl font-bold">
|
||||
推荐参数:亮度 90、对比度 100、饱和度 100、曝光 - 90、色调 0、锐化 100、模糊 0
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<!-- 保存图片弹窗 -->
|
||||
<Overlay :visible="showModal" @close="handleModalClose">
|
||||
<div class="relative w-[1000px] h-[600px] flex items-center justify-center">
|
||||
<!-- 截图内容 -->
|
||||
<div class="bg-white p-2 rounded-md -rotate-6">
|
||||
<img :src="screenshot" alt="调色结果" />
|
||||
</div>
|
||||
<!-- <img :src="screenshot" alt="调色结果" class="max-w-full max-h-full object-contain rotate-2" /> -->
|
||||
<!-- 操作按钮 -->
|
||||
<div class="absolute bottom-1/2 -left-1/3 translate-y-1/2">
|
||||
<ImgButton image="after-editing/color/按钮-试试其他调色" @click="handleTryOther" />
|
||||
</div>
|
||||
<div class="absolute bottom-1/2 -right-1/3 translate-y-1/2">
|
||||
<ImgButton image="after-editing/color/按钮-结束调色实践" @click="handleFinish" />
|
||||
</div>
|
||||
</div>
|
||||
</Overlay>
|
||||
<!-- <GuideSteps :visible="true" position-offset-y="85%">
|
||||
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
|
||||
</GuideSteps> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义滑块样式 */
|
||||
.slider-thumb::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
background: #92c6f2;
|
||||
border: 3px solid #ffffff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
box-shadow: 4px 4px 2px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.slider-thumb::-moz-range-thumb {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
background: #92c6f2;
|
||||
border: 3px solid #ffffff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
box-shadow: 4px 4px 2px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
</style>
|
||||
@@ -1,23 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Video } from '@/components'
|
||||
import { Video, GuideSteps, GuideStepsItem } from '@/components'
|
||||
import { IMAGE_PATHS, getPublicPath } from '@/utils'
|
||||
import { useInterviewStore } from '@/stores/modules/interview'
|
||||
|
||||
const router = useRouter()
|
||||
const interviewStore = useInterviewStore()
|
||||
const guideKey = 'interview-export-publish-intro'
|
||||
const playing = ref(false)
|
||||
const showGuide = ref(!interviewStore.guideShown[guideKey])
|
||||
|
||||
if (showGuide.value) {
|
||||
interviewStore.markGuideShown(guideKey)
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const handleComplete = () => {
|
||||
showGuide.value = false
|
||||
nextTick(() => {
|
||||
playing.value = true
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative w-full h-full overflow-hidden bg-black">
|
||||
<Video
|
||||
class="w-full h-full"
|
||||
:src="getPublicPath('video/on-site/dygmk_zlsxhbj.mp4')"
|
||||
:src="getPublicPath('video/interview/publish/ftmk_jsyrgs_.mp4')"
|
||||
v-model="playing"
|
||||
:muted="false"
|
||||
:volume="1"
|
||||
@@ -29,5 +44,16 @@ const handleBack = () => {
|
||||
:image="IMAGE_PATHS.COMPONENTS.BACK"
|
||||
@click="handleBack"
|
||||
/>
|
||||
|
||||
<GuideSteps
|
||||
v-if="showGuide"
|
||||
position-offset-y="85%"
|
||||
@complete="handleComplete"
|
||||
@skip="handleComplete"
|
||||
>
|
||||
<GuideStepsItem :audio-src="getPublicPath('audio/py_3_013.MP3')">
|
||||
现在一起来看看导出后的影片效果吧!
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</div>
|
||||
</template>
|
||||
@@ -86,7 +86,7 @@ const menuItems = ref([
|
||||
},
|
||||
{
|
||||
label: '调色',
|
||||
routeName: 'InterviewColorPractice',
|
||||
routeName: 'InterviewColor',
|
||||
parentValue: 3,
|
||||
disabled: false
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { IMAGE_PATHS, getStaticImage } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
// import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -26,8 +26,8 @@ const back = () => {
|
||||
<img :src="getStaticImage('interview/img/创意构想.png')" alt="" />
|
||||
</div>
|
||||
|
||||
<GuideSteps :visible="true" position-offset-y="85%">
|
||||
<!-- <GuideSteps :visible="true" position-offset-y="85%">
|
||||
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</GuideSteps> -->
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,17 +1,91 @@
|
||||
<script lang="tsx" name="Interview" setup>
|
||||
defineOptions({ name: 'Interview' })
|
||||
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
|
||||
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, ref, watch, type Ref } from 'vue'
|
||||
import { ProgressBar, Overlay } from '@/components'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { GuideSteps, type GuideStep } from '@/components/Guide'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useInterviewStore } from '@/stores'
|
||||
import InterviewMenu from './components/menu.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const interview = useInterviewStore()
|
||||
const visible = ref(false)
|
||||
|
||||
const showReturnGuide = ref(route.query.guide === 'return' && interview.isGuideInterviewReturn)
|
||||
|
||||
const returnSteps: GuideStep[] = [
|
||||
{
|
||||
id: 'return-step1',
|
||||
content: '接下来,我们将进入真实项目流程,体验一条人物访谈作品是如何一步步完成的。',
|
||||
audioSrc: getPublicPath('audio/py_3_007.MP3')
|
||||
}
|
||||
]
|
||||
|
||||
const handleReturnGuideComplete = () => {
|
||||
showReturnGuide.value = false
|
||||
interview.setIsGuideInterviewReturn(false)
|
||||
router.replace({ name: 'Interview' })
|
||||
}
|
||||
|
||||
const stageGuideMap = {
|
||||
2: {
|
||||
flagKey: 'isGuideInterviewStage2',
|
||||
setter: 'setIsGuideInterviewStage2',
|
||||
steps: [
|
||||
{
|
||||
id: 'stage2-step1',
|
||||
content: '前期准备阶段,我们已经完成了主题确认、脚本结构与镜头规划。',
|
||||
audioSrc: getPublicPath('audio/py_3_010.MP3')
|
||||
},
|
||||
{
|
||||
id: 'stage2-step2',
|
||||
content: '进入中期拍摄后就要按照既定方案,把后期需要用的素材尽可能拍完整。',
|
||||
audioSrc: getPublicPath('audio/py_3_011.MP3')
|
||||
}
|
||||
]
|
||||
},
|
||||
3: {
|
||||
flagKey: 'isGuideInterviewStage3',
|
||||
setter: 'setIsGuideInterviewStage3',
|
||||
steps: [
|
||||
{
|
||||
id: 'stage3-step1',
|
||||
content: '拍摄完成啦!现在我们正式进入剪辑环节。',
|
||||
audioSrc: getPublicPath('audio/py_3_012.MP3')
|
||||
}
|
||||
]
|
||||
}
|
||||
} as const
|
||||
|
||||
type StageKey = keyof typeof stageGuideMap
|
||||
|
||||
const stageGuideStep = ref<StageKey | null>(null)
|
||||
const stageGuideSteps = computed(() =>
|
||||
stageGuideStep.value ? stageGuideMap[stageGuideStep.value].steps : []
|
||||
) as Ref<GuideStep[]>
|
||||
|
||||
const triggerStageGuide = (step: number) => {
|
||||
if (step !== 2 && step !== 3) return
|
||||
const stage = step as StageKey
|
||||
const config = stageGuideMap[stage]
|
||||
if (!(interview as any)[config.flagKey]) return
|
||||
stageGuideStep.value = stage
|
||||
}
|
||||
|
||||
const handleStageGuideComplete = () => {
|
||||
if (!stageGuideStep.value) return
|
||||
const config = stageGuideMap[stageGuideStep.value]
|
||||
;(interview as any)[config.setter](false)
|
||||
stageGuideStep.value = null
|
||||
}
|
||||
|
||||
const visible = computed({
|
||||
get: () => interview.interviewMenuVisible,
|
||||
set: value => interview.setInterviewMenuVisible(value)
|
||||
})
|
||||
|
||||
const currentStep = computed(() => interview.InterviewUnlockProgress)
|
||||
const progress = computed(() => interview.progressPercent)
|
||||
@@ -37,11 +111,15 @@ const handleClose = () => {
|
||||
router.push({ name: 'Action' })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (currentStep.value !== 0 && currentStep.value !== 4) {
|
||||
visible.value = true
|
||||
watch(
|
||||
[visible, currentStep],
|
||||
([isVisible, step]) => {
|
||||
if (isVisible && step < 4) {
|
||||
triggerStageGuide(step)
|
||||
}
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -92,7 +170,11 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- 引导任务 -->
|
||||
<ImgButton class="absolute right-24 bottom-24" :image="IMAGE_PATHS.VLOG.TASK_PERSON" />
|
||||
<ImgButton
|
||||
v-show="!showReturnGuide"
|
||||
class="absolute right-24 bottom-24"
|
||||
:image="IMAGE_PATHS.VLOG.TASK_PERSON"
|
||||
/>
|
||||
|
||||
<Overlay content-class="w-full h-full" :visible="visible && currentStep < 4">
|
||||
<!-- 关闭按钮 -->
|
||||
@@ -125,5 +207,23 @@ onMounted(() => {
|
||||
@step-change="handleStepChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 思路提点完成回首页的一次性引导 -->
|
||||
<GuideSteps
|
||||
v-if="showReturnGuide"
|
||||
:steps="returnSteps"
|
||||
position-offset-y="85%"
|
||||
@complete="handleReturnGuideComplete"
|
||||
@skip="handleReturnGuideComplete"
|
||||
/>
|
||||
|
||||
<!-- 阶段菜单一次性引导 -->
|
||||
<GuideSteps
|
||||
v-if="stageGuideStep"
|
||||
:steps="stageGuideSteps"
|
||||
position-offset-y="85%"
|
||||
@complete="handleStageGuideComplete"
|
||||
@skip="handleStageGuideComplete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -188,18 +188,6 @@ const prevPhone = () => {
|
||||
<button @click="disableAllMenuItems" class="debug-btn">禁用全部</button>
|
||||
<button @click="toggleMenuItem('bluetooth-remote')" class="debug-btn">切换蓝牙遥控器</button>
|
||||
</div> -->
|
||||
|
||||
<!-- 引导步骤 -->
|
||||
<GuideSteps
|
||||
:steps="steps"
|
||||
:highlight-padding="12"
|
||||
:highlight-radius="20"
|
||||
position-offset-y="85%"
|
||||
>
|
||||
<GuideStepsItem>
|
||||
<img src="@/assets/images/equipment/设备准备引导1.png" alt="" />
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -10,17 +10,19 @@ const playing = ref(false)
|
||||
const showStepNavigation = ref(false)
|
||||
const currentVideoIndex = ref(0)
|
||||
|
||||
const navigationSteps = [
|
||||
const navigationSteps = ref([
|
||||
{
|
||||
id: 'q1-shoot-material',
|
||||
label: '中期拍摄',
|
||||
content: '素材怎么拍',
|
||||
videoUrl: getPublicPath('video/interview/on-site/ftmk_sczmp_batch.mp4'),
|
||||
type: 'shooting' as const
|
||||
},
|
||||
{
|
||||
id: 'q1-required-shots',
|
||||
label: '中期拍摄',
|
||||
content: '必拍镜头有哪些',
|
||||
videoUrl: getPublicPath('video/interview/on-site/ftmk_bpjtynx_batch.mp4'),
|
||||
type: 'shooting' as const
|
||||
},
|
||||
{
|
||||
@@ -35,16 +37,9 @@ const navigationSteps = [
|
||||
content: '特写镜头的作用',
|
||||
type: 'shooting' as const
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
const videoList = [
|
||||
getPublicPath('video/on-site/dygmk_mmps_batch.mp4'),
|
||||
getPublicPath('video/on-site/dygmk_hgydjt_batch.mp4'),
|
||||
getPublicPath('video/on-site/dygmk_jzxjj_batch.mp4'),
|
||||
getPublicPath('video/on-site/dygmk_czsjx_batch.mp4')
|
||||
]
|
||||
|
||||
const currentVideo = computed(() => videoList[currentVideoIndex.value])
|
||||
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
|
||||
|
||||
const back = () => {
|
||||
router.back()
|
||||
@@ -63,7 +58,7 @@ const handleStepNavigationClose = () => {
|
||||
}
|
||||
|
||||
const handleEnded = () => {
|
||||
if (currentVideoIndex.value >= videoList.length - 1) {
|
||||
if (currentVideoIndex.value >= navigationSteps.value.length - 1) {
|
||||
playing.value = false
|
||||
return
|
||||
}
|
||||
|
||||
@@ -156,9 +156,12 @@ 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('shooting/取景地模糊背景.png') }"
|
||||
:style="{ backgroundImage: getBackgroundImage('interview/shooting/操场-模糊.png') }"
|
||||
>
|
||||
<ImgButton image="shooting/标题-取景地a.png" class="title-button absolute left-0" />
|
||||
<ImgButton
|
||||
image="interview/shooting/操场-侧边标题.png"
|
||||
class="title-button absolute left-0"
|
||||
/>
|
||||
<ImgButton image="shooting/返回.png" class="back-button absolute right-0" @click="back" />
|
||||
|
||||
<StartOverlay v-if="!start" @start="start = true" />
|
||||
@@ -186,7 +189,7 @@ const capturePhoto = async () => {
|
||||
'cursor-grabbing': isDragging
|
||||
}"
|
||||
:style="{ transform: getTransform() }"
|
||||
src="@/assets/images/shooting/取景大图.png"
|
||||
src="@/assets/images/interview/shooting/操场-清晰.png"
|
||||
alt=""
|
||||
crossorigin="anonymous"
|
||||
@mousedown="startDrag"
|
||||
|
||||
@@ -101,12 +101,15 @@ const handleComplete = () => {
|
||||
class="relative w-full h-full bg-cover bg-center bg-no-repeat shooting-bg"
|
||||
:style="{
|
||||
backgroundImage: start
|
||||
? getBackgroundImage('shooting/取景大图.png')
|
||||
: getBackgroundImage('shooting/取景地模糊背景.png')
|
||||
? getBackgroundImage('interview/shooting/教学楼-清晰.png')
|
||||
: getBackgroundImage('interview/shooting/教学楼-模糊.png')
|
||||
}"
|
||||
>
|
||||
<!-- 标题 -->
|
||||
<ImgButton image="shooting/标题-取景地b.png" class="title-button absolute left-0" />
|
||||
<ImgButton
|
||||
image="interview/shooting/教学楼-侧边标题.png"
|
||||
class="title-button absolute left-0"
|
||||
/>
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<ImgButton
|
||||
@@ -123,7 +126,7 @@ const handleComplete = () => {
|
||||
<img
|
||||
ref="backgroundImage"
|
||||
class="absolute inset-0 w-full h-full object-cover pointer-events-none opacity-0"
|
||||
src="@/assets/images/shooting/取景大图.png"
|
||||
src="@/assets/images/interview/shooting/教学楼-清晰.png"
|
||||
alt=""
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
|
||||
@@ -4,10 +4,11 @@ import { useRouter } from 'vue-router'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { Overlay } from '@/components/Overlay'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
|
||||
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
import shootingtitle from '@/assets/images/shooting/标题-实地取景.png'
|
||||
import videoSrc from '/video/xinshou_jiuzhonggoutufa.mp4'
|
||||
import KnowledgeCard from '@/components/KnowledgeCard/KnowledgeCard.vue'
|
||||
import { useInterviewStore } from '@/stores/modules/interview'
|
||||
|
||||
type Btn = {
|
||||
key: string
|
||||
@@ -18,6 +19,13 @@ type Btn = {
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const interviewStore = useInterviewStore()
|
||||
const guideKey = 'interview-shooting-intro'
|
||||
const showIntroGuide = ref(!interviewStore.guideShown[guideKey])
|
||||
|
||||
if (showIntroGuide.value) {
|
||||
interviewStore.markGuideShown(guideKey)
|
||||
}
|
||||
|
||||
const showModal = ref(false)
|
||||
|
||||
@@ -87,7 +95,7 @@ const back = () => {
|
||||
<!-- 背景图片 -->
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center bg-no-repeat shooting-bg"
|
||||
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
|
||||
:style="{ backgroundImage: getBackgroundImage('interview/shooting/实地取景底图') }"
|
||||
>
|
||||
<div class="shooting-title">
|
||||
<img :src="shootingtitle" alt="" />
|
||||
@@ -95,25 +103,28 @@ const back = () => {
|
||||
<div class="shooting-return">
|
||||
<ImgButton image="shooting/返回.png" @click="back" />
|
||||
</div>
|
||||
<div class="shooting-left">
|
||||
<ImgButton image="shooting/按钮-取景地A.png" @click="handleClickA" />
|
||||
<div class="absolute left-[12%] top-1/2 -translate-y-[calc(50%-70px)]">
|
||||
<ImgButton image="interview/shooting/操场-入口.png" @click="handleClickA" />
|
||||
</div>
|
||||
<div class="shooting-right">
|
||||
<ImgButton image="shooting/按钮-取景地B.png" @click="handleClickB" />
|
||||
<div class="absolute right-[12%] top-1/2 -translate-y-[calc(50%-70px)]">
|
||||
<ImgButton image="interview/shooting/教学楼-入口.png" @click="handleClickB" />
|
||||
</div>
|
||||
<div class="shooting-composition">
|
||||
<ImgButton image="shooting/构图指南.png" @click="showModal = true" />
|
||||
<ImgButton class="mybutton" image="shooting/构图指南.png" @click="showModal = true" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 引导步骤 -->
|
||||
<GuideSteps
|
||||
v-if="showIntroGuide"
|
||||
:highlight-selector="'.mybutton'"
|
||||
:highlight-padding="12"
|
||||
:highlight-radius="20"
|
||||
position-offset-y="85%"
|
||||
position-offset-y="30%"
|
||||
>
|
||||
<GuideStepsItem> 在走进拍摄场景之前,我们先来查看一下构图指南。 </GuideStepsItem>
|
||||
<GuideStepsItem :audio-src="getPublicPath('audio/py_3_009.MP3')">
|
||||
同一个地点,站位不同、构图不同,画面质感就完全不同。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
<!-- 保存图片弹窗 -->
|
||||
|
||||
@@ -43,15 +43,15 @@ interface WorkItem {
|
||||
}
|
||||
|
||||
const worksList = ref<WorkItem[]>([
|
||||
// {
|
||||
// id: 1,
|
||||
// image: 'https://picsum.photos/400/300?random=1',
|
||||
// title: '作品名称',
|
||||
// stars: 4,
|
||||
// path: '/video/practice/vlog_anli1.mp4',
|
||||
// description:
|
||||
// '作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介'
|
||||
// },
|
||||
{
|
||||
id: 1,
|
||||
image: 'https://picsum.photos/400/300?random=1',
|
||||
title: '作品名称',
|
||||
stars: 4,
|
||||
path: '/video/interview/practice/fangtan_cace1_batch.mp4',
|
||||
description:
|
||||
'作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介'
|
||||
}
|
||||
// {
|
||||
// id: 2,
|
||||
// image: 'https://picsum.photos/400/300?random=2',
|
||||
@@ -321,11 +321,15 @@ onUnmounted(() => {
|
||||
<style scoped>
|
||||
/* 隐藏滚动条但保持可滚动 - 兼容主流浏览器 */
|
||||
.task-panel {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
-ms-overflow-style: none;
|
||||
/* IE/Edge */
|
||||
}
|
||||
|
||||
.task-panel::-webkit-scrollbar {
|
||||
display: none; /* Chrome/Safari/Opera */
|
||||
display: none;
|
||||
/* Chrome/Safari/Opera */
|
||||
}
|
||||
|
||||
/* 第一页人物动画容器 */
|
||||
|
||||
@@ -26,8 +26,8 @@ const back = () => {
|
||||
<img :src="getStaticImage('interview/img/资源筹备.png')" alt="" />
|
||||
</div>
|
||||
|
||||
<GuideSteps :visible="true" position-offset-y="85%">
|
||||
<!-- <GuideSteps :visible="true" position-offset-y="85%">
|
||||
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</GuideSteps> -->
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,14 +2,26 @@
|
||||
import { IMAGE_PATHS, getPublicPath, getStaticImage } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { Video } from '@/components'
|
||||
// import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { ImagePreview, Video } from '@/components'
|
||||
import { useScale } from '@/composables/useScale'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const back = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const { scale, offsetX, offsetY } = useScale()
|
||||
|
||||
const handleOpenImages = () => {
|
||||
ImagePreview({
|
||||
images: [getStaticImage('interview/我的育人故事_脚本')],
|
||||
startIndex: 0,
|
||||
maskClosable: true,
|
||||
getScaleMetrics: () => ({ scale: scale.value, offsetX: offsetX.value, offsetY: offsetY.value })
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -35,10 +47,15 @@ const back = () => {
|
||||
:volume="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="absolute top-[2290px] left-1/2 -translate-x-1/2 w-[240px] h-[80px] ml-6"
|
||||
@click="handleOpenImages"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<GuideSteps :visible="true" position-offset-y="85%">
|
||||
<!-- <GuideSteps :visible="true" position-offset-y="85%">
|
||||
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</GuideSteps> -->
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,13 +2,26 @@
|
||||
import { IMAGE_PATHS, getStaticImage } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
// import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { useScale } from '@/composables/useScale'
|
||||
import { ImagePreview } from '@/components'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const back = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const { scale, offsetX, offsetY } = useScale()
|
||||
|
||||
const handleOpenImages = () => {
|
||||
ImagePreview({
|
||||
images: [getStaticImage('interview/我的育人故事_分镜')],
|
||||
startIndex: 0,
|
||||
maskClosable: true,
|
||||
getScaleMetrics: () => ({ scale: scale.value, offsetX: offsetX.value, offsetY: offsetY.value })
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -24,10 +37,15 @@ const back = () => {
|
||||
|
||||
<div class="relative h-full pt-28 overflow-y-scroll">
|
||||
<img :src="getStaticImage('interview/img/分镜制作.png')" alt="" />
|
||||
|
||||
<button
|
||||
class="absolute top-[1590px] left-1/2 -translate-x-1/2 w-[480px] h-[80px] ml-6"
|
||||
@click="handleOpenImages"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<GuideSteps :visible="true" position-offset-y="85%">
|
||||
<!-- <GuideSteps :visible="true" position-offset-y="85%">
|
||||
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</GuideSteps> -->
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,14 +2,9 @@
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import ImgButton from '@/components/Button/ImgButton.vue'
|
||||
import { GuideSteps, StepNavigation } from '@/components/Guide'
|
||||
import { GuideSteps, StepNavigation, type GuideStep } from '@/components/Guide'
|
||||
import { Video } from '@/components'
|
||||
import { getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import second from '/video/vlog_renwuqujing_jichujingbieyunyong.mp4'
|
||||
import third from '/video/vlog_renwuqujing_huamiangoutu.mp4'
|
||||
import fourth from '/video/vlog_renwuqujing_jiaobencehua.mp4'
|
||||
import five from '/video/vlog_renwuqujing_zhuapai.mp4'
|
||||
import six from '/video/vlog_renwuqujing_jianjipingjie.mp4'
|
||||
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { useInterviewStore } from '@/stores'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -22,44 +17,78 @@ const playing = ref(false)
|
||||
const status = ref(false)
|
||||
|
||||
//引导步骤
|
||||
const steps = ref([
|
||||
const steps = ref<GuideStep[]>([
|
||||
{
|
||||
id: 'step1',
|
||||
content: '试着用这些方法记录身边的小事,你会发现,原来平凡的日子里有这么多值得捕捉的瞬间。'
|
||||
audioSrc: getPublicPath('audio/py_3_006.MP3'),
|
||||
content: '刚才我们学习了访谈作品的核心拍摄技能,现在通过这张思维导图来回顾一下吧!'
|
||||
}
|
||||
])
|
||||
|
||||
const introGuideSteps = computed<GuideStep[]>(() => {
|
||||
if (key.value === 'case') {
|
||||
return [
|
||||
{
|
||||
id: 'case-intro',
|
||||
audioSrc: getPublicPath('audio/py_3_003.MP3'),
|
||||
content: '现在,让我们通过一段访谈案例,去看看同龄人心中的职业蓝图吧!'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (key.value === 'profession') {
|
||||
return [
|
||||
{
|
||||
id: 'profession-intro1',
|
||||
audioSrc: getPublicPath('audio/py_3_004.MP3'),
|
||||
content: '看完作品后,你可能已经感受到了,一条访谈视频,好像并不是“坐着讲话”这么简单。'
|
||||
},
|
||||
{
|
||||
id: 'profession-intro2',
|
||||
audioSrc: getPublicPath('audio/py_3_005.MP3'),
|
||||
content: '那么,让我们一起来拆解访谈作品的核心技能吧!'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return []
|
||||
})
|
||||
|
||||
const showIntroGuide = computed(() => introGuideSteps.value.length > 0)
|
||||
|
||||
const handleIntroGuideDone = () => {
|
||||
playing.value = true
|
||||
}
|
||||
|
||||
// 步骤导航数据
|
||||
const navigationSteps = ref([
|
||||
{
|
||||
id: 'step1',
|
||||
label: '拍摄执行',
|
||||
content: '基础景别应用',
|
||||
content: '机位设置',
|
||||
videoUrl: getPublicPath('video/interview/work/ftmk_jwsz_batch.mp4'),
|
||||
type: 'shooting' as const
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
label: '拍摄执行',
|
||||
content: '画面构图',
|
||||
content: '轴线原则',
|
||||
videoUrl: getPublicPath('video/interview/work/ftmk_zxyz_batch.mp4'),
|
||||
type: 'shooting' as const
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
label: '内容策划',
|
||||
content: '简单脚本策划',
|
||||
content: '镜头组接逻辑',
|
||||
videoUrl: getPublicPath('video/interview/work/ftmk_jtzjlj_batch.mp4'),
|
||||
type: 'planning' as const
|
||||
},
|
||||
{
|
||||
id: 'step4',
|
||||
label: '拍摄执行',
|
||||
content: '动态事件跟拍与抓拍',
|
||||
content: '声音同步与收录',
|
||||
videoUrl: getPublicPath('video/interview/work/ftmk_sytbysl_batch.mp4'),
|
||||
type: 'shooting' as const
|
||||
},
|
||||
{
|
||||
id: 'step5',
|
||||
label: '后期制作',
|
||||
content: '素材导入、剪辑拼接',
|
||||
type: 'post' as const
|
||||
}
|
||||
])
|
||||
|
||||
@@ -68,28 +97,27 @@ const showStepNavigation = ref(false)
|
||||
const currentNavigationStep = ref(1) // 对应视频索引,从0开始
|
||||
|
||||
// 视频列表
|
||||
const videoList = [second, third, fourth, five, six]
|
||||
const currentVideoIndex = ref(0)
|
||||
|
||||
//按顺序播放
|
||||
const currentVideo = computed(() => videoList[currentVideoIndex.value])
|
||||
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
|
||||
const resolvedSrc = computed(() => {
|
||||
if (key.value === 'case') return '/video/anlishangxi_meishi_xiaoyuanzi.mp4'
|
||||
if (key.value === 'case') return '/video/interview/work/ft_zylt.mp4'
|
||||
if (key.value === 'profession') return currentVideo.value
|
||||
return ''
|
||||
})
|
||||
const handleVideoChange = () => {
|
||||
if (currentVideoIndex.value < videoList.length - 1) {
|
||||
if (currentVideoIndex.value < navigationSteps.value.length - 1) {
|
||||
currentVideoIndex.value++
|
||||
currentNavigationStep.value = currentVideoIndex.value
|
||||
} else {
|
||||
status.value = true
|
||||
steps.value = [
|
||||
{
|
||||
id: 'step1',
|
||||
content: '恭喜你,已完成专业剖析的学习,点击【查看脚本】回顾视频脚本吧!'
|
||||
}
|
||||
]
|
||||
// status.value = true
|
||||
// steps.value = [
|
||||
// {
|
||||
// id: 'step1',
|
||||
// content: '恭喜你,已完成专业剖析的学习,点击【查看脚本】回顾视频脚本吧!'
|
||||
// }
|
||||
// ]
|
||||
return
|
||||
}
|
||||
nextTick(() => {
|
||||
@@ -102,7 +130,8 @@ const handleClose = () => {
|
||||
if (key.value === 'idea') {
|
||||
interview.setInterviewUnlockProgress(1)
|
||||
router.push({
|
||||
name: 'Interview'
|
||||
name: 'Interview',
|
||||
query: { guide: 'return' }
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -202,6 +231,15 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进入案例赏析 / 专业剖析时显示引导 -->
|
||||
<GuideSteps
|
||||
v-if="showIntroGuide"
|
||||
:steps="introGuideSteps"
|
||||
position-offset-y="85%"
|
||||
@complete="handleIntroGuideDone"
|
||||
@skip="handleIntroGuideDone"
|
||||
/>
|
||||
|
||||
<!-- 引导与高亮 -->
|
||||
<GuideSteps v-if="status" :steps="steps" position-offset-y="85%" @complete="handleComplete" />
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="tsx" name="Work" setup>
|
||||
defineOptions({ name: 'Work' })
|
||||
import { ref, computed } from 'vue'
|
||||
import { getBackgroundImage, IMAGE_PATHS, getStaticImage } from '@/utils'
|
||||
import { getBackgroundImage, IMAGE_PATHS, getStaticImage, getPublicPath } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import WorkSideSwitcher from './components/WorkSideSwitcher.vue'
|
||||
import { GuideSteps } from '@/components/Guide'
|
||||
@@ -26,13 +26,14 @@ const isInterviewUnlock = computed(() => interview.isInterviewUnlock)
|
||||
const steps = [
|
||||
{
|
||||
id: 'step1',
|
||||
content:
|
||||
'现在我们要一起走进 Vlog 的世界。简单说,Vlog 就是用视频记录生活的一种方式 —— 它像一本会动的日记,能把我们身边的小事、瞬间的心情,通过镜头变得生动又具体。',
|
||||
audioSrc: getPublicPath('audio/py_3_001.MP3'),
|
||||
content: '我们常常会思考,未来自己想成为什么样的人呢?',
|
||||
highlightSelector: '.img-switch-item--case'
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
content: '接下来,我们就通过一个具体的案例,看看 Vlog 是怎么“捕捉美好” 的吧。',
|
||||
audioSrc: getPublicPath('audio/py_3_002.MP3'),
|
||||
content: '如果把这份思考勇敢地说出来,被镜头记录下来,你会如何表达呢?',
|
||||
highlightSelector: '.work-border'
|
||||
}
|
||||
]
|
||||
|
||||