chore: bypass pre-commit for now

This commit is contained in:
zhongyingmin committed 2025-09-26 00:11:15 +08:00
1 parent 10e97670a5
commit c4ba322e32
76 files changed
+2253 -218

No files matched your search

+6 -1
View File
@@ -4,7 +4,12 @@
-->
<template>
<img class="cursor-pointer" draggable="false" :src="getStaticImage(image)" @click="handleClick" />
<img
class="cursor-pointer transition-all duration-300 hover:scale-105 hover:brightness-110"
draggable="false"
:src="getStaticImage(image)"
@click="handleClick"
/>
</template>
<script lang="ts" setup>
-1
View File
@@ -5,7 +5,6 @@
v-if="currentStep !== totalSteps"
:image="IMAGE_PATHS.BUTTONS.CONTINUE"
class="continue-button"
@click="handleContinue"
/>
</span>
</template>
+430
View File
@@ -0,0 +1,430 @@
<template>
<!-- 抽屉遮罩层 -->
<div v-if="visible" class="drawer-overlay" @click="handleClose">
<!-- 抽屉内容 -->
<div
class="drawer-container"
:class="{
'drawer-container--visible': visible && !isClosing,
'drawer-container--closing': isClosing
}"
@click.stop
>
<!-- 右侧步骤面板 -->
<div class="steps-panel">
<!-- 关闭按钮 -->
<!-- <button class="close-button" @click="handleClose">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M12 4L4 12M4 4L12 12"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
/>
</svg>
</button> -->
<!-- 顶部导航箭头 -->
<div class="panel-header">
<button class="nav-arrow nav-arrow-up" @click="scrollUp" :disabled="!canScrollUp">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
<path
d="M6 9L3 6L6 3"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
<!-- 步骤列表 -->
<div class="steps-list" ref="stepsListRef">
<div
v-for="(step, index) in steps"
:key="step.id"
class="step-item"
:class="{ 'step-item--active': index === currentStepIndex }"
@click="handleStepClick(index)"
>
<!-- 播放图标(仅当前步骤显示) -->
<!-- <div v-if="index === currentStepIndex" class="play-icon">
<svg width="8" height="8" viewBox="0 0 8 8" fill="none">
<path d="M1.5 0.5L6.5 4L1.5 7.5V0.5Z" fill="currentColor" />
</svg>
</div> -->
<!-- 步骤标签 -->
<div class="step-label" :class="`step-label--${step.type}`">
{{ step.label }}
</div>
<!-- 步骤内容 -->
<div class="step-content">
{{ step.content }}
</div>
<!-- 步骤数字 -->
<div class="step-number" :class="{ 'step-number--active': index === currentStepIndex }">
{{ index + 1 }}
</div>
</div>
</div>
<!-- 底部导航 -->
<div class="panel-footer">
<button class="nav-arrow nav-arrow-down" @click="scrollDown" :disabled="!canScrollDown">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
<path
d="M6 3L9 6L6 9"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick, watch } from 'vue'
export interface StepItem {
id: string
label: string
content: string
type: 'shooting' | 'planning' | 'post' | 'other'
}
interface Props {
/** 是否显示抽屉 */
visible: boolean
/** 背景图片路径 */
backgroundImage: string
/** 步骤列表 */
steps: StepItem[]
/** 当前步骤索引 */
currentStepIndex?: number
/** 是否显示关闭按钮 */
showClose?: boolean
}
interface Emits {
(e: 'step-click', index: number, step: StepItem): void
(e: 'close'): void
}
const props = withDefaults(defineProps<Props>(), {
currentStepIndex: 0,
showClose: true
})
const emit = defineEmits<Emits>()
const stepsListRef = ref<HTMLElement>()
const scrollPosition = ref(0)
const maxScroll = ref(0)
const isClosing = ref(false)
// 计算是否可以滚动
const canScrollUp = computed(() => scrollPosition.value > 0)
const canScrollDown = computed(() => scrollPosition.value < maxScroll.value)
// 滚动到指定位置
const scrollTo = (position: number) => {
if (!stepsListRef.value) return
stepsListRef.value.scrollTop = position
scrollPosition.value = position
}
// 向上滚动
const scrollUp = () => {
if (!canScrollUp.value) return
const newPosition = Math.max(0, scrollPosition.value - 100)
scrollTo(newPosition)
}
// 向下滚动
const scrollDown = () => {
if (!canScrollDown.value) return
const newPosition = Math.min(maxScroll.value, scrollPosition.value + 100)
scrollTo(newPosition)
}
// 处理步骤点击
const handleStepClick = (index: number) => {
emit('step-click', index, props.steps[index])
}
// 处理关闭
const handleClose = () => {
isClosing.value = true
// 延迟关闭,让动画完成
setTimeout(() => {
emit('close')
isClosing.value = false
}, 300)
}
// 监听visible变化,重置关闭状态
watch(
() => props.visible,
newVal => {
if (newVal) {
isClosing.value = false
}
}
)
// 更新滚动状态
const updateScrollState = () => {
if (!stepsListRef.value) return
const { scrollTop, scrollHeight, clientHeight } = stepsListRef.value
scrollPosition.value = scrollTop
maxScroll.value = scrollHeight - clientHeight
}
// 监听滚动事件
const handleScroll = () => {
updateScrollState()
}
onMounted(() => {
nextTick(() => {
updateScrollState()
if (stepsListRef.value) {
stepsListRef.value.addEventListener('scroll', handleScroll)
}
})
})
</script>
<style scoped>
.drawer-overlay {
@apply fixed inset-0 z-50;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(8px);
animation: fadeIn 0.3s ease-out;
}
.drawer-container {
@apply absolute right-0 top-0 w-1/2 h-full flex justify-end;
transform: translateX(100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
max-width: 100vw;
}
.drawer-container--visible {
transform: translateX(0);
}
.drawer-container--closing {
transform: translateX(100%);
}
.steps-panel {
@apply h-full relative;
/* background: linear-gradient(
180deg,
rgba(20, 30, 45, 0.85) 0%,
rgba(10, 15, 25, 0.9) 30%,
rgba(15, 20, 30, 0.85) 70%,
rgba(20, 30, 45, 0.85) 100%
); */
width: 648px;
background-image: url('@/assets/images/vlog/专业剖析侧边.png');
backdrop-filter: blur(20px);
display: flex;
flex-direction: column;
/* border-left: 1px solid rgba(255, 255, 255, 0.1); */
border-radius: 12px 0 0 12px;
box-shadow: -10px 0 30px rgba(0, 0, 0, 0.3);
}
.close-button {
@apply absolute top-4 right-4 w-8 h-8 rounded-full flex items-center justify-center;
background: rgba(59, 130, 246, 0.9);
color: white;
border: none;
cursor: pointer;
transition: all 0.2s ease;
z-index: 10;
box-shadow: 0 0 20px rgba(59, 130, 246, 0.6);
}
.close-button:hover {
background: rgba(59, 130, 246, 1);
transform: scale(1.1);
box-shadow: 0 0 25px rgba(59, 130, 246, 0.8);
}
.panel-header {
@apply flex justify-center items-center;
padding: 3rem 0 1rem 0;
z-index: 2;
position: relative;
}
.nav-arrow {
@apply w-6 h-6 flex items-center justify-center;
background: transparent;
color: rgba(255, 255, 255, 0.7);
border: none;
cursor: pointer;
transition: all 0.2s ease;
}
.nav-arrow:hover:not(:disabled) {
color: white;
transform: scale(1.1);
}
.nav-arrow:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.steps-list {
@apply flex-1 px-6 py-4;
scrollbar-width: none;
-ms-overflow-style: none;
z-index: 2;
position: absolute;
top: 50%;
left: 60%;
transform: translate(-50%, -50%);
}
.steps-list::-webkit-scrollbar {
display: none;
}
.step-item {
@apply flex items-center py-4 px-8 mb-4 cursor-pointer transition-all duration-300;
position: relative;
width: 831px;
height: 122px;
/* min-height: 4rem; */
/* background: rgba(255, 255, 255, 0.05); */
background-image: url('@/assets/images/vlog/侧边-底板未选中.png');
/* border: 1px solid rgba(255, 255, 255, 0.1); */
/* border-radius: 20px; */
/* backdrop-filter: blur(10px); */
}
.step-item:hover {
/* background: rgba(255, 255, 255, 0.08); */
transform: translateX(4px);
}
.step-item--active {
width: 1034px;
height: 230px;
padding-left: 180px;
/* background: linear-gradient(90deg, rgba(59, 130, 246, 0.3) 0%, rgba(59, 130, 246, 0.1) 100%); */
/* border-color: rgba(59, 130, 246, 0.6);
box-shadow:
0 0 30px rgba(59, 130, 246, 0.5),
inset 0 0 20px rgba(59, 130, 246, 0.1); */
background-image: url('@/assets/images/vlog/侧边-选中底板.png');
transform: translateX(-23%);
}
.play-icon {
@apply w-5 h-5 flex items-center justify-center mr-3;
color: #60a5fa;
background: rgba(96, 165, 250, 0.2);
border-radius: 50%;
box-shadow: 0 0 10px rgba(96, 165, 250, 0.5);
}
.step-label {
@apply px-16 py-4 rounded-full text-2xl font-semibold mr-4 whitespace-nowrap;
color: white;
min-width: 5rem;
text-align: center;
}
.step-label--shooting {
background: linear-gradient(135deg, #22c55e, #16a34a);
box-shadow: 0 2px 8px rgba(34, 197, 94, 0.4);
}
.step-label--planning {
background: linear-gradient(135deg, #f97316, #ea580c);
box-shadow: 0 2px 8px rgba(249, 115, 22, 0.4);
}
.step-label--post {
background: linear-gradient(135deg, #0ea5e9, #0284c7);
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.4);
}
.step-label--other {
background: linear-gradient(135deg, #a855f7, #9333ea);
box-shadow: 0 2px 8px rgba(168, 85, 247, 0.4);
}
.step-content {
@apply flex-1 text-3xl font-bold;
line-height: 1.5;
margin-right: 1rem;
}
.step-number {
@apply flex items-center justify-center text-6xl font-semibold;
color: rgba(255, 255, 255, 0.3);
transition: all 0.3s ease;
position: absolute;
right: 15%;
z-index: 1;
}
.step-number--active {
margin-bottom: 80px;
font-size: 150px;
color: white;
text-shadow: 5px 10px 5px rgba(0, 0, 0, 0.1);
/* transform: scale(1.2);
background: rgba(59, 130, 246, 0.2);
box-shadow: 0 0 20px rgba(59, 130, 246, 0.4); */
}
.panel-footer {
@apply flex flex-col justify-center items-center;
padding: 1rem 0 2rem 0;
z-index: 2;
position: relative;
}
.guide-button {
@apply px-8 py-3 rounded-full text-sm font-semibold;
background: linear-gradient(135deg, #dc2626, #b91c1c);
color: white;
border: none;
cursor: pointer;
transition: all 0.2s ease;
margin-top: 1rem;
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.4);
}
.guide-button:hover {
background: linear-gradient(135deg, #ef4444, #dc2626);
transform: scale(1.05);
box-shadow: 0 6px 16px rgba(220, 38, 38, 0.5);
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>
+1
View File
@@ -3,6 +3,7 @@ export { default as GuideDragon } from './GuideDragon.vue'
export { default as GuideSteps } from './GuideSteps.vue'
export { default as GuideStepsItem } from './GuideStepsItem.vue'
export { default as SpotlightOverlay } from './SpotlightOverlay.vue'
export { default as StepNavigation } from './StepNavigation.vue'
// 类型导出
export type {
@@ -50,6 +50,8 @@ type Props = {
designWidth?: number
designHeight?: number
getScaleMetrics: () => { scale: number; offsetX: number; offsetY: number }
// 关闭回调函数
onClose?: () => void
}
const props = withDefaults(defineProps<Props>(), {
@@ -119,6 +121,10 @@ const imageStyle = computed(() => ({
function emitClose() {
visible.value = false
emit('close')
// 调用用户提供的关闭回调
if (props.onClose) {
props.onClose()
}
}
function onMaskClick() {
+1
View File
@@ -1 +1,2 @@
export { default as ImagePreview } from './service.ts'
export type { default as ImagePreviewComponent } from './ImagePreview.vue'
+7
View File
@@ -8,6 +8,8 @@ type PreviewOptions = {
zIndex?: number
// Supply scale metrics provider compatible with useScale
getScaleMetrics: () => { scale: number; offsetX: number; offsetY: number }
// 关闭回调函数
onClose?: () => void
}
let container: HTMLElement | null = null
@@ -24,6 +26,11 @@ export default function previewImage(options: PreviewOptions) {
...options,
onClose: () => {
state.visible = false
// 调用用户提供的关闭回调
if (options.onClose) {
options.onClose()
}
// 清理 DOM
render(null, container!)
}
})
+156
View File
@@ -0,0 +1,156 @@
<template>
<Overlay
:visible="visible"
:z-index="zIndex"
:clickable="maskClosable"
:custom-class="customClass"
:animation="animation"
@click="handleMaskClick"
@close="handleClose"
>
<div class="modal-container" :style="modalStyle">
<!-- 头部 -->
<div v-if="showHeader" class="modal-header">
<slot name="header">
<div class="modal-title">
<slot name="title">
{{ title }}
</slot>
</div>
</slot>
<button v-if="closable" class="modal-close" @click="handleClose">×</button>
</div>
<!-- 内容区域 -->
<div class="modal-body">
<slot name="body">
<slot />
</slot>
</div>
<!-- 底部 -->
<div v-if="$slots.footer" class="modal-footer">
<slot name="footer" />
</div>
</div>
</Overlay>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { Overlay } from '@/components/Overlay'
import type { ModalProps, ModalEmits } from './types'
interface Props extends ModalProps {}
const props = withDefaults(defineProps<Props>(), {
visible: false,
title: '',
width: '520px',
height: 'auto',
mask: true,
maskClosable: true,
closable: true,
zIndex: 1000,
customClass: '',
center: true,
animation: 'fade'
})
const emit = defineEmits<ModalEmits>()
// 计算属性
const showHeader = computed(() => {
return props.title
})
// 移除 animationName 计算属性,由 Overlay 组件处理
const modalStyle = computed(() => {
const style: Record<string, string> = {}
if (props.width) {
style.width = typeof props.width === 'number' ? `${props.width}px` : props.width
}
if (props.height && props.height !== 'auto') {
style.height = typeof props.height === 'number' ? `${props.height}px` : props.height
}
return style
})
// 事件处理
const handleClose = () => {
emit('close')
}
const handleMaskClick = (event: MouseEvent) => {
emit('mask-click')
}
</script>
<style scoped>
.modal-container {
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
max-width: 90vw;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
min-height: 60px;
}
.modal-title {
font-size: 18px;
font-weight: 600;
color: #333;
flex: 1;
}
.modal-close {
width: 32px;
height: 32px;
border: none;
background: none;
font-size: 24px;
color: #999;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
}
.modal-close:hover {
background-color: #f5f5f5;
color: #666;
}
.modal-body {
padding: 20px;
flex: 1;
overflow-y: auto;
}
.modal-footer {
padding: 16px 20px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 动画效果由 Overlay 组件处理 */
</style>
+135
View File
@@ -0,0 +1,135 @@
# Modal 弹窗组件
一个功能完整的 Vue 3 弹窗组件,支持多种动画效果和灵活的插槽系统。
## 基本用法
```vue
<template>
<Modal v-model:visible="showModal" title="弹窗标题" @close="handleClose">
<template #body>
<p>这是弹窗的主体内容</p>
</template>
</Modal>
</template>
<script setup>
import { ref } from 'vue'
import { Modal } from '@/components/Modal'
const showModal = ref(false)
const handleClose = () => {
showModal.value = false
}
</script>
```
## 插槽说明
### body 插槽(推荐)
直接传入弹窗的主体内容,这是最常用的插槽:
```vue
<template #body>
<div class="custom-content">
<img src="image.jpg" alt="图片" />
<p>自定义内容</p>
</div>
</template>
```
### 其他插槽
- `title`: 标题内容
- `header`: 整个头部区域
- `footer`: 底部区域
- `default`: 默认内容(会作为 body 的备用内容)
## 属性
| 属性 | 类型 | 默认值 | 说明 |
| ------------ | ------------- | ------- | ------------------------------ |
| visible | boolean | false | 是否显示弹窗 |
| title | string | '' | 弹窗标题 |
| width | string/number | '520px' | 弹窗宽度 |
| height | string/number | 'auto' | 弹窗高度 |
| animation | string | 'fade' | 动画类型:fade/slide/zoom/none |
| mask | boolean | true | 是否显示遮罩 |
| maskClosable | boolean | true | 是否可点击遮罩关闭 |
| closable | boolean | true | 是否显示关闭按钮 |
| center | boolean | true | 是否居中显示 |
| zIndex | number | 1000 | 弹窗层级 |
| customClass | string | '' | 自定义类名 |
## 事件
| 事件名 | 说明 | 参数 |
| ---------- | -------------- | ---- |
| close | 关闭弹窗时触发 | - |
| confirm | 确认时触发 | - |
| cancel | 取消时触发 | - |
| mask-click | 点击遮罩时触发 | - |
## 动画效果
支持四种动画效果:
- `fade`: 淡入淡出(默认)
- `slide`: 从上方滑入
- `zoom`: 缩放效果
- `none`: 无动画
## 完整示例
```vue
<template>
<div>
<button @click="showModal = true">打开弹窗</button>
<Modal
v-model:visible="showModal"
title="图片预览"
:width="800"
:height="600"
animation="zoom"
:closable="false"
:mask-closable="false"
@close="handleClose"
>
<template #body>
<div class="relative w-full h-full flex items-center justify-center">
<img :src="imageUrl" alt="预览图片" class="max-w-full max-h-full object-contain" />
</div>
<div class="absolute bottom-4 right-4 flex gap-4">
<button @click="handleConfirm">确认</button>
<button @click="handleCancel">取消</button>
</div>
</template>
</Modal>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Modal } from '@/components/Modal'
const showModal = ref(false)
const imageUrl = ref('path/to/image.jpg')
const handleClose = () => {
showModal.value = false
}
const handleConfirm = () => {
// 处理确认逻辑
showModal.value = false
}
const handleCancel = () => {
// 处理取消逻辑
showModal.value = false
}
</script>
```
+6
View File
@@ -0,0 +1,6 @@
/**
* Modal 组件导出
*/
export { default as Modal } from './Modal.vue'
export type { ModalProps, ModalEmits, ModalSlots } from './types'
+52
View File
@@ -0,0 +1,52 @@
/**
* Modal 组件类型定义
*/
export interface ModalProps {
/** 是否显示弹窗 */
visible: boolean
/** 弹窗标题 */
title?: string
/** 弹窗宽度 */
width?: string | number
/** 弹窗高度 */
height?: string | number
/** 是否显示遮罩层 */
mask?: boolean
/** 是否可点击遮罩关闭 */
maskClosable?: boolean
/** 是否显示关闭按钮 */
closable?: boolean
/** 弹窗层级 */
zIndex?: number
/** 自定义类名 */
customClass?: string
/** 是否居中显示 */
center?: boolean
/** 动画类型 */
animation?: 'fade' | 'slide' | 'zoom' | 'none'
}
export interface ModalEmits {
/** 关闭弹窗事件 */
close: []
/** 确认事件 */
confirm: []
/** 取消事件 */
cancel: []
/** 遮罩点击事件 */
'mask-click': []
}
export interface ModalSlots {
/** 默认内容插槽 */
default: () => any
/** 主体内容插槽 */
body: () => any
/** 标题插槽 */
title: () => any
/** 头部插槽 */
header: () => any
/** 底部插槽 */
footer: () => any
}
+142
View File
@@ -0,0 +1,142 @@
<template>
<Teleport to=".app-content">
<Transition :name="animationName" :duration="duration" appear>
<div
v-if="visible"
class="overlay"
:class="[customClass]"
:style="overlayStyle"
@click="handleOverlayClick"
>
<div class="overlay-content" :class="[contentClass]" @click.stop>
<slot />
</div>
</div>
</Transition>
</Teleport>
</template>
<script lang="ts" setup>
import { computed, inject, provide } from 'vue'
import type { OverlayProps, OverlayEmits } from './types'
interface Props extends OverlayProps {}
// 全局层级管理
const OVERLAY_LEVEL_KEY = Symbol('overlay-level')
const props = withDefaults(defineProps<Props>(), {
visible: false,
background: 'rgba(0, 0, 0, 0.5)',
zIndex: undefined,
clickable: true,
customClass: '',
contentClass: '',
customStyle: () => ({}),
animation: 'fade',
duration: 300,
level: 0,
baseZIndex: 1000
})
const emit = defineEmits<OverlayEmits>()
// 获取父级层级
const parentLevel = inject(OVERLAY_LEVEL_KEY, 0)
// 计算当前层级
const currentLevel = computed(() => {
return props.level !== undefined ? props.level : parentLevel + 1
})
// 向子组件提供当前层级
provide(OVERLAY_LEVEL_KEY, currentLevel.value)
// 计算属性
const animationName = computed(() => {
if (props.animation === 'none') return ''
return `overlay-${props.animation}`
})
const overlayStyle = computed(() => {
// 计算 z-index:如果手动指定了 zIndex 则使用,否则根据当前层级自动计算
const calculatedZIndex =
props.zIndex !== undefined ? props.zIndex : props.baseZIndex + currentLevel.value * 10
return {
zIndex: calculatedZIndex,
backgroundColor: props.background,
...props.customStyle
}
})
// 事件处理
const handleOverlayClick = (event: MouseEvent) => {
emit('click', event)
if (props.clickable) {
emit('close')
}
}
</script>
<style scoped>
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
/* padding: 20px; */
}
.overlay-content {
position: relative;
max-width: 100%;
max-height: 100%;
}
/* 动画效果 */
.overlay-fade-enter-active,
.overlay-fade-leave-active {
transition: opacity v-bind('`${duration}ms`') ease;
}
.overlay-fade-enter-from,
.overlay-fade-leave-to {
opacity: 0;
}
.overlay-slide-enter-active,
.overlay-slide-leave-active {
transition: all v-bind('`${duration}ms`') ease;
}
.overlay-slide-enter-from {
opacity: 0;
transform: translateY(-50px);
}
.overlay-slide-leave-to {
opacity: 0;
transform: translateY(-50px);
}
.overlay-zoom-enter-active,
.overlay-zoom-leave-active {
transition: all v-bind('`${duration}ms`') ease;
}
.overlay-zoom-enter-from {
opacity: 0;
transform: scale(0.8);
}
.overlay-zoom-leave-to {
opacity: 0;
transform: scale(0.8);
}
</style>
+92
View File
@@ -0,0 +1,92 @@
# Overlay 组件
遮罩层组件,支持多层嵌套。
## 基本用法
```vue
<template>
<Overlay :visible="showModal" @close="showModal = false">
<div>内容</div>
</Overlay>
</template>
```
## 多层嵌套
组件支持多层嵌套,会自动管理 z-index 层级:
```vue
<template>
<!-- 第一层 Overlay -->
<Overlay :visible="showFirstModal" @close="showFirstModal = false">
<div>
<h2>第一层弹窗</h2>
<button @click="showSecondModal = true">打开第二层</button>
<!-- 第二层 Overlay -->
<Overlay :visible="showSecondModal" @close="showSecondModal = false">
<div>
<h2>第二层弹窗</h2>
<button @click="showThirdModal = true">打开第三层</button>
<!-- 第三层 Overlay -->
<Overlay :visible="showThirdModal" @close="showThirdModal = false">
<div>
<h2>第三层弹窗</h2>
<p>这是最顶层的弹窗</p>
</div>
</Overlay>
</div>
</Overlay>
</div>
</Overlay>
</template>
<script setup>
import { ref } from 'vue'
const showFirstModal = ref(false)
const showSecondModal = ref(false)
const showThirdModal = ref(false)
</script>
```
## 手动指定层级
如果需要手动控制层级,可以使用 `level` 或 `zIndex` 属性:
```vue
<template>
<!-- 手动指定层级 -->
<Overlay :visible="showModal" :level="5" @close="showModal = false">
<div>内容</div>
</Overlay>
<!-- 或者直接指定 z-index -->
<Overlay :visible="showModal" :z-index="2000" @close="showModal = false">
<div>内容</div>
</Overlay>
</template>
```
## 属性说明
| 属性 | 类型 | 默认值 | 说明 |
| ---------- | ------- | -------------------- | ------------------------------ |
| visible | boolean | false | 是否显示遮罩 |
| background | string | 'rgba(0, 0, 0, 0.5)' | 遮罩背景色 |
| zIndex | number | undefined | 手动指定 z-index |
| level | number | 0 | 嵌套层级,用于自动计算 z-index |
| baseZIndex | number | 1000 | 基础 z-index 值 |
| clickable | boolean | true | 是否可点击遮罩关闭 |
| animation | string | 'fade' | 动画类型 |
| duration | number | 300 | 动画持续时间 |
## 层级计算规则
- 如果没有手动指定 `zIndex`,组件会根据 `level` 自动计算:
- 计算公式:`baseZIndex + level * 10`
- 例如:baseZIndex=1000, level=1 → z-index=1010
- 嵌套时,子组件的 level 会自动递增
- 手动指定 `zIndex` 时,会忽略自动计算
+6
View File
@@ -0,0 +1,6 @@
/**
* Overlay 遮罩层组件导出
*/
export { default as Overlay } from './Overlay.vue'
export type { OverlayProps, OverlayEmits, OverlaySlots } from './types'
+39
View File
@@ -0,0 +1,39 @@
/**
* Overlay 遮罩层组件类型定义
*/
export interface OverlayProps {
/** 是否显示遮罩 */
visible: boolean
/** 遮罩背景色 */
background?: string
/** 遮罩层级 */
zIndex?: number
/** 是否可点击遮罩关闭 */
clickable?: boolean
/** 自定义类名 */
customClass?: string
/** 自定义样式 */
customStyle?: Record<string, string>
contentClass?: string
/** 动画类型 */
animation?: 'fade' | 'slide' | 'zoom' | 'none'
/** 动画持续时间(毫秒) */
duration?: number
/** 嵌套层级,用于自动计算 z-index */
level?: number
/** 基础 z-index 值,默认为 1000 */
baseZIndex?: number
}
export interface OverlayEmits {
/** 点击遮罩事件 */
click: [event: MouseEvent]
/** 关闭事件 */
close: []
}
export interface OverlaySlots {
/** 默认内容插槽 */
default: () => void
}
+77 -41
View File
@@ -10,28 +10,58 @@
<!-- 中间步骤指示器 -->
<div class="steps-section" v-if="currentStep > 0">
<!-- 标题区域 -->
<div class="absolute top-0 left-40">
<div class="absolute top-0 left-40" v-if="currentStep > 0">
<img
:src="getStaticImage(IMAGE_PATHS.PROGRESS_BAR.TITLE)"
alt="制作变焦镜-我的志愿者手记"
class="title-image"
/>
</div>
<div
v-for="(step, index) in steps.slice(0, 3)"
:key="index"
class="step-item"
:class="getStepStatus(index)"
>
<span class="step-text">
<img
:src="
props.currentPage === index ? steps[index].nameSelect : steps[index].nameselectNot
"
alt=""
/>
<img class="step-ellipsis" src="@/assets/images/midterm/进度装饰(未完成.png" alt="" />
</span>
<div class="flex">
<div
v-for="(step, index) in steps.slice(0, 3)"
:key="index"
class="step-item"
:class="getStepStatus(index)"
>
<span class="relative flex items-center justify-center">
<!-- <img
:src="currentStep === index ? steps[index].nameSelect : steps[index].nameselectNot"
alt=""
/> -->
<div class="flex items-center">
<span
:class="[
'font-bold text-4xl ',
currentStep === index + 1 ? 'text-[#fbbf24]' : 'text-white'
]"
>{{ step.name }}</span
>
<img v-if="canComplete" src="@/assets/images/midterm/完成.png" alt="" />
<img
v-else
:class="['ml-2', currentStep === index + 1 && 'filter-blue']"
:src="step.icon"
alt=""
/>
</div>
<template v-if="index < 2">
<img
v-if="canComplete"
class="step-ellipsis"
src="@/assets/images/midterm/进度装饰(已完成.png"
alt=""
/>
<img
v-else
class="step-ellipsis"
src="@/assets/images/midterm/进度装饰(未完成.png"
alt=""
/>
</template>
</span>
</div>
</div>
</div>
<div class="absolute top-0 left-40" v-else>
@@ -71,11 +101,6 @@ interface Props {
/** 当前步骤索引 (0-3) */
currentStep?: number
}
//
interface Props {
/** 当前步骤索引 (0-3) */
currentPage?: number
}
// 定义事件
interface Emits {
/** 点击完成按钮时触发 */
@@ -83,14 +108,13 @@ interface Emits {
}
const props = withDefaults(defineProps<Props>(), {
currentStep: 0,
currentPage: 0
currentStep: 0
})
const emit = defineEmits<Emits>()
// 计算属性:是否可以完成(当前在最后一步)
const canComplete = computed(() => props.currentStep === 3)
const canComplete = computed(() => props.currentStep > steps.length)
// 处理完成按钮点击
const handleComplete = () => {
@@ -102,16 +126,22 @@ const handleComplete = () => {
// 步骤配置
const steps = [
{
nameSelect: '/src/assets/images/midterm/前期准备-选中.png',
nameselectNot: '/src/assets/images/midterm/前期准备-未选中.png'
name: '前期准备',
icon: getStaticImage('midterm/准备图标.png'),
nameSelect: getStaticImage('midterm/前期准备-选中.png'),
nameselectNot: getStaticImage('midterm/前期准备-未选中.png')
},
{
nameSelect: '/src/assets/images/midterm/中期拍摄-选中.png',
nameselectNot: '/src/assets/images/midterm/中期拍摄-未选中.png'
name: '中期拍摄',
icon: getStaticImage('midterm/拍摄图标.png'),
nameSelect: getStaticImage('midterm/中期拍摄-选中.png'),
nameselectNot: getStaticImage('midterm/中期拍摄-未选中.png')
},
{
nameSelect: '/src/assets/images/midterm/后期剪辑-选中.png',
nameselectNot: '/src/assets/images/midterm/后期剪辑-未选中.png'
name: '后期剪辑',
icon: getStaticImage('midterm/剪辑图标.png'),
nameSelect: getStaticImage('midterm/后期剪辑-选中.png'),
nameselectNot: getStaticImage('midterm/后期剪辑-未选中.png')
}
]
@@ -138,8 +168,9 @@ const getStepStatus = (stepIndex: number) => {
.progress-bar {
position: relative;
display: flex;
justify-content: center;
align-items: center;
padding: 15px 20px;
padding-right: 160px;
height: 292px;
background-image: url('@/assets/images/components/ProgressBar-bg.png');
background-size: cover;
@@ -153,22 +184,22 @@ const getStepStatus = (stepIndex: number) => {
}
.steps-section {
width: 1600px;
width: 1400px;
display: flex;
align-items: center;
justify-content: space-between;
justify-content: center;
margin: 30px 20px 0;
gap: 15px;
}
.step-item {
padding-left: 300px;
display: flex;
/* .step-item { */
/* padding-left: 300px; */
/* display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
flex: 1;
}
gap: 5px; */
/* flex: 1; */
/* } */
.step-text {
/* font-size: 40px; */
@@ -178,12 +209,12 @@ const getStepStatus = (stepIndex: number) => {
white-space: nowrap;
text-shadow: 0 0 8px rgba(255, 255, 255, 0.5);
display: flex;
justify-content: center;
justify-content: start;
align-items: center;
}
.step-ellipsis {
padding-left: 17px;
padding: 0 40px;
}
.step-dots {
@@ -256,6 +287,11 @@ button:disabled {
object-fit: contain;
}
.filter-blue {
filter: brightness(0) saturate(100%) invert(79%) sepia(29%) saturate(1190%) hue-rotate(330deg)
brightness(102%) contrast(98%);
}
/* 响应式设计 */
@media (max-width: 768px) {
.progress-bar {
+6
View File
@@ -16,3 +16,9 @@ export * from './Switcher'
// 图片预览(函数式)
export * from './ImagePreview'
// 弹窗组件
export * from './Modal'
// 遮罩层组件
export * from './Overlay'