309 lines
6.3 KiB
Vue
Raw Normal View History

<template>
<Teleport to="body">
<Transition name="popup-fade">
<div
v-if="visible"
class="stretchable-modal__overlay"
@click="handleOverlayClick"
>
<div
class="stretchable-modal__container"
:style="containerStyle"
@click.stop
>
<!-- 头部大弹窗top切图.png -->
<div class="stretchable-modal__header">
<slot name="header">
<h3 class="stretchable-modal__title">{{ title }}</h3>
</slot>
<button
v-if="showClose"
class="stretchable-modal__close"
@click="handleClose"
aria-label="关闭弹窗"
>
<img
src="../../assets/images/modal/closeIcon.png"
alt="关闭"
class="close-icon"
/>
</button>
</div>
<!-- 中间大弹窗mid切图.png -->
<div class="stretchable-modal__body">
<slot></slot>
</div>
<!-- 底部大弹窗bottom切图.png -->
<div v-if="$slots.footer" class="stretchable-modal__footer">
<slot name="footer"></slot>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup>
import { computed, watch, onMounted, onUnmounted } from 'vue';
/**
* StretchableModal - 三段式可拉伸弹窗组件
*
* 特性:
* - 使用三张切图实现可拉伸的弹窗背景头部中间底部
* - 头部和底部仅支持横向拉伸中间部分支持全方向拉伸
* - 标题居中显示
* - 支持多种交互方式关闭点击遮罩ESC键关闭按钮
* - 支持插槽自定义内容
*/
const props = defineProps({
/**
* 控制弹窗显示/隐藏
*/
visible: {
type: Boolean,
default: false
},
/**
* 弹窗标题
*/
title: {
type: String,
default: ''
},
/**
* 弹窗宽度支持 CSS 单位或响应式值
* @example '800px' | 'clamp(778px, 90vw, 1200px)'
*/
width: {
type: String,
default: 'clamp(900px, 85vw, 1400px)'
},
/**
* 弹窗高度支持 CSS 单位或响应式值
* @example '600px' | 'clamp(500px, 80vh, 800px)'
*/
height: {
type: String,
default: 'clamp(600px, 85vh, 900px)'
},
/**
* 点击遮罩层是否关闭弹窗
*/
closeOnClickModal: {
type: Boolean,
default: true
},
/**
* ESC 键是否关闭弹窗
*/
closeOnPressEscape: {
type: Boolean,
default: true
},
/**
* 是否显示关闭按钮
*/
showClose: {
type: Boolean,
default: true
}
});
const emit = defineEmits(['update:visible', 'close', 'open']);
/**
* 计算弹窗容器的样式
*/
const containerStyle = computed(() => ({
width: props.width,
height: props.height
}));
/**
* 处理遮罩层点击事件
*/
const handleOverlayClick = () => {
if (props.closeOnClickModal) {
handleClose();
}
};
/**
* 关闭弹窗
*/
const handleClose = () => {
emit('update:visible', false);
emit('close');
};
/**
* 处理 ESC 键按下事件
*/
const handleKeydown = (event) => {
if (props.visible && props.closeOnPressEscape && event.key === 'Escape') {
handleClose();
}
};
// 监听 visible 变化,触发相应事件
watch(() => props.visible, (newVal) => {
if (newVal) {
emit('open');
}
});
// 挂载和卸载时添加/移除键盘事件监听
onMounted(() => {
document.addEventListener('keydown', handleKeydown);
});
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown);
});
</script>
<style scoped lang="scss">
@use "@/styles/mixins.scss" as *;
.stretchable-modal {
&__overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(vw(5));
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: vw(20);
}
&__container {
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
}
&__header {
position: relative;
height: vh(66);
min-height: vh(66);
background: url(../../assets/images/modal/大弹窗top切图.png) repeat-x;
background-size: 100% vh(66);
display: flex;
align-items: center;
justify-content: center;
padding: 0 vw(60);
}
&__title {
color: var(--text-white);
font-size: fs(20);
font-family: SourceHanSansCN-Bold, sans-serif;
font-weight: bold;
text-align: center;
margin: 0;
}
&__close {
position: absolute;
top: 34px;
right: 34px;
transform: translateY(-50%);
width: vw(32);
height: vw(32);
background: transparent;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
z-index: 10;
padding: 0;
&:hover {
opacity: 0.8;
transform: translateY(-50%) scale(1.1);
}
&:active {
transform: translateY(-50%) scale(0.95);
}
.close-icon {
width: 100%;
height: 100%;
object-fit: contain;
}
}
&__body {
flex: 1;
background: url(../../assets/images/modal/大弹窗mid切图.png) repeat;
background-size: 100% vh(66);
overflow-y: auto;
overflow-x: hidden;
padding: vw(20) vw(30);
// 自定义滚动条样式
&::-webkit-scrollbar {
width: vw(6);
}
&::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: vw(3);
}
&::-webkit-scrollbar-thumb {
background: var(--primary-color);
border-radius: vw(3);
&:hover {
background: var(--primary-light);
}
}
}
&__footer {
height: vh(66);
min-height: vh(66);
background: url(../../assets/images/modal/大弹窗bottom切图.png) repeat-x;
background-size: 100% vh(66);
display: flex;
align-items: center;
justify-content: center;
padding: 0 vw(30);
}
}
// 弹窗淡入淡出动画
.popup-fade-enter-active,
.popup-fade-leave-active {
transition: opacity 0.3s ease;
.stretchable-modal__container {
transition: transform 0.3s ease, opacity 0.3s ease;
}
}
.popup-fade-enter-from,
.popup-fade-leave-to {
opacity: 0;
.stretchable-modal__container {
transform: scale(0.9);
opacity: 0;
}
}
</style>