feat: 添加 SpotlightOverlay 组件,更新引导步骤组件以支持高亮功能,调整样式和路由配置
No files matched your search
@@ -0,0 +1,54 @@
|
|||||||
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Home',
|
||||||
|
component: () => import('@/views/home/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '首页'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/guide',
|
||||||
|
name: 'Guide',
|
||||||
|
component: () => import('@/views/guide/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '使用引导'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/action',
|
||||||
|
name: 'Action',
|
||||||
|
component: () => import('@/views/home/action.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '开始拍摄'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// vlog 发现与记录
|
||||||
|
{
|
||||||
|
path: '/vlog',
|
||||||
|
name: 'Vlog',
|
||||||
|
component: () => import('@/views/vlog/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '发现与记录'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHashHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
// 路由守卫
|
||||||
|
router.beforeEach((to, _from, next) => {
|
||||||
|
// 设置页面标题
|
||||||
|
if (to.meta?.title) {
|
||||||
|
document.title = `${to.meta.title} - 短视频达人`
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Home',
|
||||||
|
component: () => import('@/views/home/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '首页'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/guide',
|
||||||
|
name: 'Guide',
|
||||||
|
component: () => import('@/views/guide/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '使用引导'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/action',
|
||||||
|
name: 'Action',
|
||||||
|
component: () => import('@/views/home/action.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '开始拍摄'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// vlog 发现与记录
|
||||||
|
{
|
||||||
|
path: '/vlog',
|
||||||
|
name: 'Vlog',
|
||||||
|
component: () => import('@/views/vlog/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '发现与记录'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// vlog 发现与记录
|
||||||
|
{
|
||||||
|
path: '/work',
|
||||||
|
name: 'Work',
|
||||||
|
component: () => import('@/views/vlog/work/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '发现与记录'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHashHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
// 路由守卫
|
||||||
|
router.beforeEach((to, _from, next) => {
|
||||||
|
// 设置页面标题
|
||||||
|
if (to.meta?.title) {
|
||||||
|
document.title = `${to.meta.title} - 短视频达人`
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<script lang="tsx" setup>
|
||||||
|
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
|
||||||
|
import { ImgButton } from '@/components/Button'
|
||||||
|
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { ProgressBar } from '@/components'
|
||||||
|
|
||||||
|
const currentStep = ref(0)
|
||||||
|
|
||||||
|
const handleComplete = () => {
|
||||||
|
console.log('完成前置条件')
|
||||||
|
// 处理完成逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStepChange = (step: number) => {
|
||||||
|
console.log('步骤变化:', step)
|
||||||
|
// 处理步骤变化逻辑
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full relative">
|
||||||
|
<!-- 背景图片-center -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.BG_CENTER) }"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- 背景图片 -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.BG) }"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<div class="absolute top-2 left-1/2 -translate-x-1/2">
|
||||||
|
<ImgButton :image="IMAGE_PATHS.VLOG.TITLE" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 任务取景按钮 -->
|
||||||
|
<div
|
||||||
|
class="absolute left-24 top-24"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.TASK_CAMERA) }"
|
||||||
|
>
|
||||||
|
<ImgButton :image="IMAGE_PATHS.VLOG.TASK_BTN" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 引导任务 -->
|
||||||
|
<ImgButton class="absolute right-24 bottom-24" :image="IMAGE_PATHS.VLOG.TASK_PERSON" />
|
||||||
|
|
||||||
|
<div class="absolute bottom-0 w-full">
|
||||||
|
<ProgressBar
|
||||||
|
:current-step="currentStep"
|
||||||
|
@complete="handleComplete"
|
||||||
|
@step-change="handleStepChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<script lang="tsx" setup>
|
||||||
|
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
|
||||||
|
import { ImgButton } from '@/components/Button'
|
||||||
|
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { ProgressBar } from '@/components'
|
||||||
|
|
||||||
|
const currentStep = ref(0)
|
||||||
|
|
||||||
|
const handleComplete = () => {
|
||||||
|
console.log('完成前置条件')
|
||||||
|
// 处理完成逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStepChange = (step: number) => {
|
||||||
|
console.log('步骤变化:', step)
|
||||||
|
// 处理步骤变化逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleWorkClick = () => {
|
||||||
|
console.log('任务取景按钮点击')
|
||||||
|
// 处理任务取景按钮点击逻辑
|
||||||
|
router.push({ name: 'Work' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full relative">
|
||||||
|
<!-- 背景图片-center -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.BG_CENTER) }"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- 背景图片 -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.BG) }"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<div class="absolute top-2 left-1/2 -translate-x-1/2">
|
||||||
|
<ImgButton :image="IMAGE_PATHS.VLOG.TITLE" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 任务取景按钮 -->
|
||||||
|
<div
|
||||||
|
class="absolute left-24 top-24"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.TASK_CAMERA) }"
|
||||||
|
>
|
||||||
|
<ImgButton :image="IMAGE_PATHS.VLOG.TASK_BTN" @click="handleWorkClick" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 引导任务 -->
|
||||||
|
<ImgButton class="absolute right-24 bottom-24" :image="IMAGE_PATHS.VLOG.TASK_PERSON" />
|
||||||
|
|
||||||
|
<div class="absolute bottom-0 w-full">
|
||||||
|
<ProgressBar
|
||||||
|
:current-step="currentStep"
|
||||||
|
@complete="handleComplete"
|
||||||
|
@step-change="handleStepChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<script lang="tsx" setup>
|
||||||
|
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
|
||||||
|
import { ImgButton } from '@/components/Button'
|
||||||
|
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { ProgressBar } from '@/components'
|
||||||
|
|
||||||
|
const currentStep = ref(0)
|
||||||
|
|
||||||
|
const handleComplete = () => {
|
||||||
|
console.log('完成前置条件')
|
||||||
|
// 处理完成逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStepChange = (step: number) => {
|
||||||
|
console.log('步骤变化:', step)
|
||||||
|
// 处理步骤变化逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleWorkClick = () => {
|
||||||
|
console.log('任务取景按钮点击')
|
||||||
|
// 处理任务取景按钮点击逻辑
|
||||||
|
router.push({ name: 'Work' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full relative">
|
||||||
|
<!-- 背景图片-center -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.BG_CENTER) }"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- 背景图片 -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.BG) }"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<div class="absolute top-2 left-1/2 -translate-x-1/2">
|
||||||
|
<ImgButton :image="IMAGE_PATHS.VLOG.TITLE" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 任务取景按钮 -->
|
||||||
|
<div
|
||||||
|
class="absolute left-24 top-24"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.TASK_CAMERA) }"
|
||||||
|
>
|
||||||
|
<ImgButton :image="IMAGE_PATHS.VLOG.TASK_BTN" @click="handleWorkClick" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 引导任务 -->
|
||||||
|
<ImgButton class="absolute right-24 bottom-24" :image="IMAGE_PATHS.VLOG.TASK_PERSON" />
|
||||||
|
|
||||||
|
<div class="absolute bottom-0 w-full">
|
||||||
|
<ProgressBar
|
||||||
|
:current-step="currentStep"
|
||||||
|
@complete="handleComplete"
|
||||||
|
@step-change="handleStepChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<script lang="tsx" setup>
|
||||||
|
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
|
||||||
|
import { ImgButton } from '@/components/Button'
|
||||||
|
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { ProgressBar } from '@/components'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const currentStep = ref(0)
|
||||||
|
|
||||||
|
const handleComplete = () => {
|
||||||
|
console.log('完成前置条件')
|
||||||
|
// 处理完成逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStepChange = (step: number) => {
|
||||||
|
console.log('步骤变化:', step)
|
||||||
|
// 处理步骤变化逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleWorkClick = () => {
|
||||||
|
console.log('任务取景按钮点击')
|
||||||
|
// 处理任务取景按钮点击逻辑
|
||||||
|
router.push({ name: 'Work' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full relative">
|
||||||
|
<!-- 背景图片-center -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.BG_CENTER) }"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- 背景图片 -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.BG) }"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<div class="absolute top-2 left-1/2 -translate-x-1/2">
|
||||||
|
<ImgButton :image="IMAGE_PATHS.VLOG.TITLE" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 任务取景按钮 -->
|
||||||
|
<div
|
||||||
|
class="absolute left-24 top-24"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.TASK_CAMERA) }"
|
||||||
|
>
|
||||||
|
<ImgButton :image="IMAGE_PATHS.VLOG.TASK_BTN" @click="handleWorkClick" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 引导任务 -->
|
||||||
|
<ImgButton class="absolute right-24 bottom-24" :image="IMAGE_PATHS.VLOG.TASK_PERSON" />
|
||||||
|
|
||||||
|
<div class="absolute bottom-0 w-full">
|
||||||
|
<ProgressBar
|
||||||
|
:current-step="currentStep"
|
||||||
|
@complete="handleComplete"
|
||||||
|
@step-change="handleStepChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="tsx" setup>
|
||||||
|
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
|
||||||
|
import { ImgButton } from '@/components/Button'
|
||||||
|
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { ProgressBar } from '@/components'
|
||||||
|
|
||||||
|
const currentStep = ref(0)
|
||||||
|
|
||||||
|
const handleComplete = () => {
|
||||||
|
console.log('完成前置条件')
|
||||||
|
// 处理完成逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStepChange = (step: number) => {
|
||||||
|
console.log('步骤变化:', step)
|
||||||
|
// 处理步骤变化逻辑
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full relative">
|
||||||
|
<!-- 背景图片-center -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.BG_CENTER) }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="tsx" setup>
|
||||||
|
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full relative">
|
||||||
|
<!-- 背景图片-center -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.BG_CENTER) }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="tsx" setup>
|
||||||
|
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full relative">
|
||||||
|
<!-- 背景图片-center -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.BG_CENTER) }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
@@ -104,11 +104,14 @@ const handleContinue = () => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.guide-dragon-container {
|
.guide-dragon-container {
|
||||||
@apply w-full h-full relative overflow-hidden;
|
@apply w-full h-full relative overflow-hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100002; /* 高于遮罩 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-ui {
|
.guide-ui {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-panel {
|
.guide-panel {
|
||||||
@@ -189,6 +192,7 @@ const handleContinue = () => {
|
|||||||
width: 250px;
|
width: 250px;
|
||||||
height: 350px;
|
height: 350px;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dragon-container {
|
.dragon-container {
|
||||||
|
|||||||
@@ -1,33 +1,44 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="guide-steps-container" v-if="show">
|
<teleport to="body">
|
||||||
<GuideDragon
|
<div class="guide-steps-container" v-if="show">
|
||||||
:show-skip="currentStep < totalSteps"
|
<SpotlightOverlay
|
||||||
:current-step="currentStep"
|
:visible="Boolean(highlightSelector)"
|
||||||
:total-steps="totalSteps"
|
:target-selector="highlightSelector"
|
||||||
@next="handleNext"
|
:padding="highlightPadding"
|
||||||
@skip="handleSkip"
|
:border-radius="highlightRadius"
|
||||||
@complete="handleComplete"
|
/>
|
||||||
>
|
<div class="guide-align" :style="alignStyle">
|
||||||
<template #default>
|
<GuideDragon
|
||||||
<!-- 支持插槽形式的步骤内容 -->
|
:show-skip="currentStep < totalSteps"
|
||||||
<div v-if="useSlotMode">
|
:current-step="currentStep"
|
||||||
<component
|
:total-steps="totalSteps"
|
||||||
:is="currentStepComponent"
|
@next="handleNext"
|
||||||
@continue="handleNext"
|
@skip="handleSkip"
|
||||||
:is-last-step="currentStep >= totalSteps"
|
@complete="handleComplete"
|
||||||
:current-step="currentStep"
|
>
|
||||||
:total-steps="totalSteps"
|
<template #default>
|
||||||
/>
|
<!-- 支持插槽形式的步骤内容 -->
|
||||||
</div>
|
<div v-if="useSlotMode">
|
||||||
<!-- 兼容原有的配置形式 -->
|
<component
|
||||||
<div v-else v-html="currentStepContent"></div>
|
:is="currentStepComponent"
|
||||||
</template>
|
@continue="handleNext"
|
||||||
</GuideDragon>
|
:is-last-step="currentStep >= totalSteps"
|
||||||
</div>
|
:current-step="currentStep"
|
||||||
|
:total-steps="totalSteps"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- 兼容原有的配置形式 -->
|
||||||
|
<div v-else v-html="currentStepContent"></div>
|
||||||
|
</template>
|
||||||
|
</GuideDragon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, useSlots, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, useSlots, onMounted, onUnmounted, reactive } from 'vue'
|
||||||
|
import SpotlightOverlay from './SpotlightOverlay.vue'
|
||||||
import GuideDragon from './GuideDragon.vue'
|
import GuideDragon from './GuideDragon.vue'
|
||||||
|
|
||||||
interface GuideStep {
|
interface GuideStep {
|
||||||
@@ -46,6 +57,12 @@ interface Props {
|
|||||||
keyboardNavigation?: boolean
|
keyboardNavigation?: boolean
|
||||||
/** 自动播放间隔时间(毫秒),0表示不自动播放 */
|
/** 自动播放间隔时间(毫秒),0表示不自动播放 */
|
||||||
autoPlayInterval?: number
|
autoPlayInterval?: number
|
||||||
|
/** 当前步骤高亮目标的选择器(CSS选择器),为空则不高亮 */
|
||||||
|
highlightSelector?: string | null
|
||||||
|
/** 高亮内边距,默认8 */
|
||||||
|
highlightPadding?: number
|
||||||
|
/** 高亮圆角,默认16 */
|
||||||
|
highlightRadius?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
@@ -58,7 +75,10 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
steps: () => [],
|
steps: () => [],
|
||||||
autoStart: true,
|
autoStart: true,
|
||||||
keyboardNavigation: true,
|
keyboardNavigation: true,
|
||||||
autoPlayInterval: 0
|
autoPlayInterval: 0,
|
||||||
|
highlightSelector: null,
|
||||||
|
highlightPadding: 8,
|
||||||
|
highlightRadius: 16
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<Emits>()
|
const emit = defineEmits<Emits>()
|
||||||
@@ -66,6 +86,22 @@ const emit = defineEmits<Emits>()
|
|||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
const show = ref(true)
|
const show = ref(true)
|
||||||
const currentStepIndex = ref(0)
|
const currentStepIndex = ref(0)
|
||||||
|
const alignStyle = reactive<{
|
||||||
|
left: string
|
||||||
|
top: string
|
||||||
|
transform: string
|
||||||
|
width: string
|
||||||
|
height: string
|
||||||
|
}>({
|
||||||
|
left: '0px',
|
||||||
|
top: '0px',
|
||||||
|
transform: 'scale(1)',
|
||||||
|
width: '1920px',
|
||||||
|
height: '1080px'
|
||||||
|
})
|
||||||
|
const highlightSelector = computed(() => props.highlightSelector || null)
|
||||||
|
const highlightPadding = computed(() => props.highlightPadding || 0)
|
||||||
|
const highlightRadius = computed(() => props.highlightRadius || 0)
|
||||||
|
|
||||||
const useSlotMode = computed(() => {
|
const useSlotMode = computed(() => {
|
||||||
return slots.default && slots.default().length > 0
|
return slots.default && slots.default().length > 0
|
||||||
@@ -209,11 +245,34 @@ onMounted(() => {
|
|||||||
if (props.autoStart && props.autoPlayInterval > 0) {
|
if (props.autoStart && props.autoPlayInterval > 0) {
|
||||||
startAutoPlay()
|
startAutoPlay()
|
||||||
}
|
}
|
||||||
|
// 同步到 .app-content 的缩放与偏移,使引导面板与页面对齐
|
||||||
|
const syncAlign = () => {
|
||||||
|
const el = document.querySelector('.app-content') as HTMLElement | null
|
||||||
|
if (!el) return
|
||||||
|
const style = window.getComputedStyle(el)
|
||||||
|
const transform = style.transform
|
||||||
|
let scale = 1
|
||||||
|
if (transform && transform !== 'none') {
|
||||||
|
const match = transform.match(/matrix\(([^)]+)\)/)
|
||||||
|
if (match) {
|
||||||
|
const parts = match[1].split(',').map(s => parseFloat(s.trim()))
|
||||||
|
if (!Number.isNaN(parts[0])) scale = parts[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
alignStyle.transform = `scale(${scale})`
|
||||||
|
alignStyle.left = style.left || '0px'
|
||||||
|
alignStyle.top = style.top || '0px'
|
||||||
|
alignStyle.width = style.width || '1920px'
|
||||||
|
alignStyle.height = style.height || '1080px'
|
||||||
|
}
|
||||||
|
syncAlign()
|
||||||
|
window.addEventListener('resize', syncAlign)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener('keydown', handleKeydown)
|
document.removeEventListener('keydown', handleKeydown)
|
||||||
stopAutoPlay()
|
stopAutoPlay()
|
||||||
|
window.removeEventListener('resize', () => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
@@ -228,6 +287,17 @@ defineExpose({
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.guide-steps-container {
|
.guide-steps-container {
|
||||||
@apply w-full h-full relative;
|
@apply w-full h-full;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 10001;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-align {
|
||||||
|
position: absolute;
|
||||||
|
transform-origin: top left;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100002;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="visible" class="spotlight-overlay">
|
||||||
|
<div
|
||||||
|
class="spotlight-hole"
|
||||||
|
:style="{
|
||||||
|
top: rect.top + 'px',
|
||||||
|
left: rect.left + 'px',
|
||||||
|
width: rect.width + 'px',
|
||||||
|
height: rect.height + 'px',
|
||||||
|
borderRadius: borderRadius + 'px',
|
||||||
|
boxShadow: `0 0 0 9999px rgba(0,0,0,0.6)`
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 是否可见 */
|
||||||
|
visible?: boolean
|
||||||
|
/** 目标元素选择器 */
|
||||||
|
targetSelector?: string | null
|
||||||
|
/** 额外内边距 */
|
||||||
|
padding?: number
|
||||||
|
/** 圆角 */
|
||||||
|
borderRadius?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
visible: false,
|
||||||
|
targetSelector: null,
|
||||||
|
padding: 8,
|
||||||
|
borderRadius: 16
|
||||||
|
})
|
||||||
|
|
||||||
|
const rect = reactive({ top: 0, left: 0, width: 0, height: 0 })
|
||||||
|
|
||||||
|
const measure = () => {
|
||||||
|
if (!props.visible || !props.targetSelector) return
|
||||||
|
const el = document.querySelector(props.targetSelector) as HTMLElement | null
|
||||||
|
if (!el) return
|
||||||
|
const r = el.getBoundingClientRect()
|
||||||
|
const pad = Math.max(0, props.padding)
|
||||||
|
// 使用视口坐标(fixed层),不叠加滚动偏移
|
||||||
|
rect.top = r.top - pad
|
||||||
|
rect.left = r.left - pad
|
||||||
|
rect.width = r.width + pad * 2
|
||||||
|
rect.height = r.height + pad * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.visible, props.targetSelector, props.padding],
|
||||||
|
() => {
|
||||||
|
requestAnimationFrame(measure)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const onResize = () => measure()
|
||||||
|
const onScroll = () => measure()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('resize', onResize)
|
||||||
|
window.addEventListener('scroll', onScroll, { passive: true })
|
||||||
|
measure()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', onResize)
|
||||||
|
window.removeEventListener('scroll', onScroll)
|
||||||
|
})
|
||||||
|
|
||||||
|
const borderRadius = props.borderRadius
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.spotlight-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spotlight-hole {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
export { default as GuideDragon } from './GuideDragon.vue'
|
export { default as GuideDragon } from './GuideDragon.vue'
|
||||||
export { default as GuideSteps } from './GuideSteps.vue'
|
export { default as GuideSteps } from './GuideSteps.vue'
|
||||||
export { default as GuideStepsItem } from './GuideStepsItem.vue'
|
export { default as GuideStepsItem } from './GuideStepsItem.vue'
|
||||||
|
export { default as SpotlightOverlay } from './SpotlightOverlay.vue'
|
||||||
|
|
||||||
// 类型导出
|
// 类型导出
|
||||||
export type {
|
export type {
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="['img-switch-list', directionClass]">
|
||||||
|
<div
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.key"
|
||||||
|
:class="['img-switch-item', itemClass, `img-switch-item--${item.key}`]"
|
||||||
|
:data-key="item.key"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage(getItemImage(item)) }"
|
||||||
|
@click="onSelect(item.key)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { getBackgroundImage } from '@/utils'
|
||||||
|
|
||||||
|
interface SwitchItem {
|
||||||
|
key: string
|
||||||
|
activeImage: string
|
||||||
|
inactiveImage: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
items: SwitchItem[]
|
||||||
|
modelValue?: string
|
||||||
|
direction?: 'vertical' | 'horizontal'
|
||||||
|
itemClass?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value: string): void
|
||||||
|
(e: 'change', value: string): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: '',
|
||||||
|
direction: 'vertical',
|
||||||
|
itemClass: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
const directionClass = computed(() => (props.direction === 'vertical' ? 'vertical' : 'horizontal'))
|
||||||
|
|
||||||
|
const getItemImage = (item: SwitchItem) => {
|
||||||
|
return item.key === props.modelValue ? item.activeImage : item.inactiveImage
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSelect = (key: string) => {
|
||||||
|
if (key === props.modelValue) return
|
||||||
|
emit('update:modelValue', key)
|
||||||
|
emit('change', key)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.img-switch-list {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-switch-list.vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-switch-list.horizontal {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-switch-item {
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .img-switch-item:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
} */
|
||||||
|
</style>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as ImageSwitchList } from './ImageSwitchList.vue'
|
||||||
@@ -10,3 +10,6 @@ export * from './Guide'
|
|||||||
|
|
||||||
// 进度条组件
|
// 进度条组件
|
||||||
export * from './ProgressBar'
|
export * from './ProgressBar'
|
||||||
|
|
||||||
|
// 切换组件
|
||||||
|
export * from './Switcher'
|
||||||
@@ -34,6 +34,15 @@ const routes: RouteRecordRaw[] = [
|
|||||||
meta: {
|
meta: {
|
||||||
title: '发现与记录'
|
title: '发现与记录'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
// vlog 发现与记录
|
||||||
|
{
|
||||||
|
path: '/work',
|
||||||
|
name: 'Work',
|
||||||
|
component: () => import('@/views/vlog/work/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '发现与记录'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,18 @@ export const IMAGE_PATHS = {
|
|||||||
TASK_BTN: 'vlog/task-btn',
|
TASK_BTN: 'vlog/task-btn',
|
||||||
TASK_PERSON: 'vlog/person'
|
TASK_PERSON: 'vlog/person'
|
||||||
},
|
},
|
||||||
|
//Work
|
||||||
|
WORK: {
|
||||||
|
BG: 'work/bg',
|
||||||
|
BORDER: 'work/border',
|
||||||
|
BG_CENTER: 'work/bg-center',
|
||||||
|
TITLE: 'work/title',
|
||||||
|
IMAGE: 'work/image1',
|
||||||
|
TASK_CAMERA: 'work/task-camera',
|
||||||
|
TASK_BTN_OK: 'work/task-btn-ok',
|
||||||
|
TASK_BTN: 'work/task-btn',
|
||||||
|
TASK_PERSON: 'work/person'
|
||||||
|
},
|
||||||
|
|
||||||
//按钮
|
//按钮
|
||||||
BUTTONS: {
|
BUTTONS: {
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import { ImgButton } from '@/components/Button'
|
|||||||
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { ProgressBar } from '@/components'
|
import { ProgressBar } from '@/components'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const currentStep = ref(0)
|
const currentStep = ref(0)
|
||||||
|
|
||||||
@@ -16,6 +19,12 @@ const handleStepChange = (step: number) => {
|
|||||||
console.log('步骤变化:', step)
|
console.log('步骤变化:', step)
|
||||||
// 处理步骤变化逻辑
|
// 处理步骤变化逻辑
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleWorkClick = () => {
|
||||||
|
console.log('任务取景按钮点击')
|
||||||
|
// 处理任务取景按钮点击逻辑
|
||||||
|
router.push({ name: 'Work' })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -42,7 +51,7 @@ const handleStepChange = (step: number) => {
|
|||||||
class="absolute left-24 top-24"
|
class="absolute left-24 top-24"
|
||||||
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.TASK_CAMERA) }"
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.TASK_CAMERA) }"
|
||||||
>
|
>
|
||||||
<ImgButton :image="IMAGE_PATHS.VLOG.TASK_BTN" />
|
<ImgButton :image="IMAGE_PATHS.VLOG.TASK_BTN" @click="handleWorkClick" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 引导任务 -->
|
<!-- 引导任务 -->
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<ImageSwitchList
|
||||||
|
v-model="innerValue"
|
||||||
|
:items="items"
|
||||||
|
direction="vertical"
|
||||||
|
:item-class="itemClass"
|
||||||
|
@change="onChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ImageSwitchList } from '@/components/Switcher'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
type WorkTab = 'vlog' | 'case' | 'profession' | 'idea'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue?: WorkTab
|
||||||
|
itemClass?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value: WorkTab): void
|
||||||
|
(e: 'change', value: WorkTab): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: 'vlog',
|
||||||
|
itemClass: 'w-[260px] h-[64px] rounded-[18px]'
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
const innerValue = computed({
|
||||||
|
get: () => props.modelValue as WorkTab,
|
||||||
|
set: (val: WorkTab) => emit('update:modelValue', val)
|
||||||
|
})
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ key: 'vlog', activeImage: 'work/vlog初识-当前', inactiveImage: 'work/vlog初识-未选中' },
|
||||||
|
{ key: 'case', activeImage: 'work/案例赏析-当前', inactiveImage: 'work/案例赏析-未选中' },
|
||||||
|
{ key: 'profession', activeImage: 'work/专业剖析-当前', inactiveImage: 'work/专业剖析-未选中' },
|
||||||
|
{ key: 'idea', activeImage: 'work/思路提点-当前', inactiveImage: 'work/思路提点-未选中' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const onChange = (val: string) => emit('change', val as WorkTab)
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<script lang="tsx" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
|
||||||
|
import { ImgButton } from '@/components/Button'
|
||||||
|
import WorkSideSwitcher from './components/WorkSideSwitcher.vue'
|
||||||
|
import { GuideSteps } from '@/components/Guide'
|
||||||
|
|
||||||
|
type WorkTab = 'vlog' | 'case' | 'profession' | 'idea'
|
||||||
|
|
||||||
|
const selected = ref<WorkTab>('vlog')
|
||||||
|
const stepIndex = ref(0)
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{ id: 'step1', content: '先认识 Vlog:点击右侧「1.vlog初识」继续' },
|
||||||
|
{ id: 'step2', content: '这是主要内容区域的边框' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const currentHighlight = computed(() =>
|
||||||
|
stepIndex.value === 0 ? '.img-switch-item--vlog' : '.work-border'
|
||||||
|
)
|
||||||
|
|
||||||
|
interface SimpleStep {
|
||||||
|
id: string
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
const handleStepChange = (_step: SimpleStep | null, index: number) => {
|
||||||
|
stepIndex.value = index
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full relative">
|
||||||
|
<!-- 背景图片-center -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.WORK.BG) }"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- border图片 -->
|
||||||
|
<div
|
||||||
|
class="work-border absolute top-[140px] left-24 inset-0 bg-cover bg-center bg-no-repeat z-10 w-[1360px] h-[800px] p-10"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.WORK.BORDER) }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center pl-16 w-[1357px] h-[265px] absolute -bottom-16 left-0 bg-cover bg-center bg-no-repeat z-20"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.WORK.TITLE) }"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mt-24 text-white font-bold text-6xl [text-shadow:0_2px_4px_rgba(0,0,0,0.2),0_0_6px_rgba(0,207,255,0.9),0_0_14px_rgba(0,207,255,0.7)]"
|
||||||
|
>
|
||||||
|
记录身边的美好</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<img class="rounded-[62px]" src="@/assets/images/work/image1.png" alt="" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 左侧选项切换 -->
|
||||||
|
<div class="absolute right-4 top-32 z-20">
|
||||||
|
<WorkSideSwitcher
|
||||||
|
v-model="selected"
|
||||||
|
@change="() => {}"
|
||||||
|
:item-class="'w-[462px] h-[142px] '"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 任务取景按钮 -->
|
||||||
|
<ImgButton class="absolute right-14 bottom-10" :image="IMAGE_PATHS.WORK.TASK_BTN" />
|
||||||
|
|
||||||
|
<!-- 引导与高亮 -->
|
||||||
|
<GuideSteps
|
||||||
|
:steps="steps"
|
||||||
|
:highlight-selector="currentHighlight"
|
||||||
|
:highlight-padding="12"
|
||||||
|
:highlight-radius="20"
|
||||||
|
@step-change="handleStepChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||