forked from zhongyidian/super_video
chore: bypass pre-commit for now
This commit is contained in:
1 parent
10e97670a5
commit
c4ba322e32
76 files changed
+2253
-218
No files matched your search
@@ -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>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
v-if="currentStep !== totalSteps"
|
||||
:image="IMAGE_PATHS.BUTTONS.CONTINUE"
|
||||
class="continue-button"
|
||||
@click="handleContinue"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -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>
|
||||
@@ -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 +1,2 @@
|
||||
export { default as ImagePreview } from './service.ts'
|
||||
export type { default as ImagePreviewComponent } from './ImagePreview.vue'
|
||||
@@ -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!)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Modal 组件导出
|
||||
*/
|
||||
|
||||
export { default as Modal } from './Modal.vue'
|
||||
export type { ModalProps, ModalEmits, ModalSlots } from './types'
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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` 时,会忽略自动计算
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Overlay 遮罩层组件导出
|
||||
*/
|
||||
|
||||
export { default as Overlay } from './Overlay.vue'
|
||||
export type { OverlayProps, OverlayEmits, OverlaySlots } from './types'
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -16,3 +16,9 @@ export * from './Switcher'
|
||||
|
||||
// 图片预览(函数式)
|
||||
export * from './ImagePreview'
|
||||
|
||||
// 弹窗组件
|
||||
export * from './Modal'
|
||||
|
||||
// 遮罩层组件
|
||||
export * from './Overlay'
|
||||
Reference in new issue
Block a user