|
|
<template> |
|
|
<Teleport to="body"> |
|
|
<Transition name="modal" appear> |
|
|
<div v-if="visible" class="modal-overlay" @click="handleOverlayClick"> |
|
|
<div |
|
|
class="modal" |
|
|
:class="size" |
|
|
@click.stop |
|
|
> |
|
|
|
|
|
<div class="modal-header" v-if="title || closable"> |
|
|
<h3 class="modal-title" v-if="title">{{ title }}</h3> |
|
|
<button |
|
|
v-if="closable" |
|
|
class="modal-close-btn" |
|
|
@click="handleClose" |
|
|
> |
|
|
<i class="fas fa-times"></i> |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="modal-body"> |
|
|
<slot></slot> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="modal-footer" v-if="$slots.footer"> |
|
|
<slot name="footer"></slot> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</Transition> |
|
|
</Teleport> |
|
|
</template> |
|
|
|
|
|
<script setup> |
|
|
import { ref, onMounted, onUnmounted } from 'vue' |
|
|
|
|
|
const props = defineProps({ |
|
|
|
|
|
title: { |
|
|
type: String, |
|
|
default: '' |
|
|
}, |
|
|
|
|
|
|
|
|
size: { |
|
|
type: String, |
|
|
default: 'medium', |
|
|
validator: (value) => ['small', 'medium', 'large'].includes(value) |
|
|
}, |
|
|
|
|
|
|
|
|
closable: { |
|
|
type: Boolean, |
|
|
default: true |
|
|
}, |
|
|
|
|
|
|
|
|
maskClosable: { |
|
|
type: Boolean, |
|
|
default: true |
|
|
}, |
|
|
|
|
|
|
|
|
lockScroll: { |
|
|
type: Boolean, |
|
|
default: true |
|
|
} |
|
|
}) |
|
|
|
|
|
const emit = defineEmits(['close', 'open']) |
|
|
|
|
|
const visible = ref(false) |
|
|
|
|
|
|
|
|
const handleOverlayClick = () => { |
|
|
if (props.maskClosable) { |
|
|
handleClose() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const handleClose = () => { |
|
|
close() |
|
|
} |
|
|
|
|
|
|
|
|
const handleKeyDown = (event) => { |
|
|
if (event.key === 'Escape' && props.closable) { |
|
|
handleClose() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const toggleScrollLock = (lock) => { |
|
|
if (!props.lockScroll) return |
|
|
|
|
|
if (lock) { |
|
|
document.body.style.overflow = 'hidden' |
|
|
} else { |
|
|
document.body.style.overflow = '' |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const open = () => { |
|
|
visible.value = true |
|
|
toggleScrollLock(true) |
|
|
document.addEventListener('keydown', handleKeyDown) |
|
|
emit('open') |
|
|
} |
|
|
|
|
|
|
|
|
const close = () => { |
|
|
visible.value = false |
|
|
toggleScrollLock(false) |
|
|
document.removeEventListener('keydown', handleKeyDown) |
|
|
emit('close') |
|
|
} |
|
|
|
|
|
|
|
|
onMounted(() => { |
|
|
|
|
|
}) |
|
|
|
|
|
onUnmounted(() => { |
|
|
toggleScrollLock(false) |
|
|
document.removeEventListener('keydown', handleKeyDown) |
|
|
}) |
|
|
|
|
|
|
|
|
defineExpose({ |
|
|
open, |
|
|
close |
|
|
}) |
|
|
</script> |
|
|
|
|
|
<style scoped> |
|
|
.modal-overlay { |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
right: 0; |
|
|
bottom: 0; |
|
|
background: rgba(0, 0, 0, 0.6); |
|
|
backdrop-filter: blur(4px); |
|
|
z-index: 2000; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
padding: 20px; |
|
|
} |
|
|
|
|
|
.modal { |
|
|
background: var(--bg-secondary); |
|
|
border-radius: 16px; |
|
|
border: 1px solid rgba(255, 255, 255, 0.1); |
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); |
|
|
max-height: calc(100vh - 40px); |
|
|
overflow: hidden; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
} |
|
|
|
|
|
.modal.small { |
|
|
max-width: 400px; |
|
|
width: 90%; |
|
|
} |
|
|
|
|
|
.modal.medium { |
|
|
max-width: 600px; |
|
|
width: 90%; |
|
|
} |
|
|
|
|
|
.modal.large { |
|
|
max-width: 800px; |
|
|
width: 95%; |
|
|
} |
|
|
|
|
|
.modal-header { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: space-between; |
|
|
padding: 20px 24px 16px; |
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1); |
|
|
flex-shrink: 0; |
|
|
} |
|
|
|
|
|
.modal-title { |
|
|
font-size: 18px; |
|
|
font-weight: 600; |
|
|
color: var(--text-primary); |
|
|
margin: 0; |
|
|
} |
|
|
|
|
|
.modal-close-btn { |
|
|
width: 32px; |
|
|
height: 32px; |
|
|
border: none; |
|
|
background: rgba(255, 255, 255, 0.1); |
|
|
color: var(--text-secondary); |
|
|
border-radius: 50%; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
cursor: pointer; |
|
|
transition: var(--transition-fast); |
|
|
} |
|
|
|
|
|
.modal-close-btn:hover { |
|
|
background: rgba(255, 255, 255, 0.2); |
|
|
color: var(--text-primary); |
|
|
} |
|
|
|
|
|
.modal-body { |
|
|
flex: 1; |
|
|
padding: 20px 24px; |
|
|
overflow-y: auto; |
|
|
} |
|
|
|
|
|
.modal-footer { |
|
|
padding: 16px 24px 20px; |
|
|
border-top: 1px solid rgba(255, 255, 255, 0.1); |
|
|
flex-shrink: 0; |
|
|
} |
|
|
|
|
|
|
|
|
.modal-enter-active, |
|
|
.modal-leave-active { |
|
|
transition: all 0.3s ease-out; |
|
|
} |
|
|
|
|
|
.modal-enter-from, |
|
|
.modal-leave-to { |
|
|
opacity: 0; |
|
|
} |
|
|
|
|
|
.modal-enter-from .modal, |
|
|
.modal-leave-to .modal { |
|
|
transform: scale(0.9) translateY(-20px); |
|
|
} |
|
|
|
|
|
|
|
|
@media (max-width: 375px) { |
|
|
.modal-overlay { |
|
|
padding: 16px; |
|
|
} |
|
|
|
|
|
.modal { |
|
|
border-radius: 12px; |
|
|
} |
|
|
|
|
|
.modal.small, |
|
|
.modal.medium, |
|
|
.modal.large { |
|
|
width: 100%; |
|
|
max-width: none; |
|
|
} |
|
|
|
|
|
.modal-header { |
|
|
padding: 16px 20px 12px; |
|
|
} |
|
|
|
|
|
.modal-title { |
|
|
font-size: 16px; |
|
|
} |
|
|
|
|
|
.modal-close-btn { |
|
|
width: 28px; |
|
|
height: 28px; |
|
|
} |
|
|
|
|
|
.modal-body { |
|
|
padding: 16px 20px; |
|
|
} |
|
|
|
|
|
.modal-footer { |
|
|
padding: 12px 20px 16px; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.modal-body::-webkit-scrollbar { |
|
|
width: 6px; |
|
|
} |
|
|
|
|
|
.modal-body::-webkit-scrollbar-track { |
|
|
background: transparent; |
|
|
} |
|
|
|
|
|
.modal-body::-webkit-scrollbar-thumb { |
|
|
background: rgba(255, 255, 255, 0.2); |
|
|
border-radius: 3px; |
|
|
} |
|
|
|
|
|
.modal-body::-webkit-scrollbar-thumb:hover { |
|
|
background: rgba(255, 255, 255, 0.3); |
|
|
} |
|
|
</style> |