forked from zhongyidian/super_video
feat: 完善模块引导与学习进度体验
统一引导提示的持久化控制,补充学习进度入口与模块资源,避免用户返回页面时重复触发提示。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
fc9633f371
commit
6f8252472f
114 files changed
+2758
-1798
No files matched your search
@@ -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="" />
|
||||
<ImgButton
|
||||
class="absolute top-[1180px] left-[51.5%] -translate-x-1/2"
|
||||
image="after-editing/color/调色实践"
|
||||
@click="router.push({ name: 'InterviewColor' })"
|
||||
/>
|
||||
<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-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/调色-亮'
|
||||
"
|
||||
/>
|
||||
</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>
|
||||
|
||||
<GuideSteps :visible="true" position-offset-y="85%">
|
||||
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
<!-- 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')" />
|
||||
|
||||
<!-- 开启对比 -->
|
||||
<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/调色-亮'
|
||||
"
|
||||
/>
|
||||
</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 class="relative h-full pt-28 overflow-y-scroll">
|
||||
<img :src="getStaticImage('interview/img/调色实践.png')" alt="" />
|
||||
<ImgButton
|
||||
class="absolute top-[1180px] left-[51.5%] -translate-x-1/2"
|
||||
image="after-editing/color/调色实践"
|
||||
@click="router.push({ name: 'InterviewColorPractice' })"
|
||||
/>
|
||||
</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>
|
||||
+110
-10
@@ -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'
|
||||
}
|
||||
]
|
||||
|
||||
Reference in new issue
Block a user