| <template> | |
| <Teleport to="body"> | |
| <Transition name="modal-fade"> | |
| <div class="modal" ref="modalRef" v-show="visible" tabindex="-1" @keyup.esc="onEsc()"> | |
| <div class="mask" @click="onClickMask()"></div> | |
| <Transition name="modal-zoom" | |
| @afterLeave="contentVisible = false" | |
| @before-enter="contentVisible = true" | |
| > | |
| <div class="modal-content" v-show="visible" :style="contentStyle"> | |
| <span class="close-btn" v-if="closeButton" @click="close()"><IconClose /></span> | |
| <slot v-if="contentVisible"></slot> | |
| </div> | |
| </Transition> | |
| </div> | |
| </Transition> | |
| </Teleport> | |
| </template> | |
| <script lang="ts" setup> | |
| import { computed, nextTick, ref, watch, type CSSProperties } from 'vue' | |
| import { icons } from '@/plugins/icon' | |
| const { IconClose } = icons | |
| const props = withDefaults(defineProps<{ | |
| visible: boolean | |
| width?: number | |
| closeButton?: boolean | |
| closeOnClickMask?: boolean | |
| closeOnEsc?: boolean | |
| contentStyle?: CSSProperties | |
| }>(), { | |
| width: 480, | |
| closeButton: false, | |
| closeOnClickMask: true, | |
| closeOnEsc: true, | |
| }) | |
| const modalRef = ref<HTMLDivElement>() | |
| const emit = defineEmits<{ | |
| (event: 'update:visible', payload: boolean): void | |
| (event: 'closed'): void | |
| }>() | |
| const contentVisible = ref(false) | |
| const contentStyle = computed(() => { | |
| return { | |
| width: props.width + 'px', | |
| ...(props.contentStyle || {}) | |
| } | |
| }) | |
| watch(() => props.visible, () => { | |
| if (props.visible) { | |
| nextTick(() => modalRef.value!.focus()) | |
| } | |
| }) | |
| const close = () => { | |
| emit('update:visible', false) | |
| emit('closed') | |
| } | |
| const onEsc = () => { | |
| if (props.visible && props.closeOnEsc) close() | |
| } | |
| const onClickMask = () => { | |
| if (props.closeOnClickMask) close() | |
| } | |
| </script> | |
| <style lang="scss" scoped> | |
| .modal, .mask { | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 5000; | |
| } | |
| .modal { | |
| position: fixed; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| outline: 0; | |
| border: 0; | |
| } | |
| .mask { | |
| position: absolute; | |
| background: rgba(0, 0, 0, .25); | |
| } | |
| .modal-content { | |
| z-index: 5001; | |
| padding: 20px; | |
| background: #fff; | |
| border-radius: $borderRadius; | |
| overflow: hidden; | |
| box-shadow: 0 1px 3px rgba(0, 0, 0, .2); | |
| position: relative; | |
| } | |
| .close-btn { | |
| width: 20px; | |
| height: 20px; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| position: absolute; | |
| top: 16px; | |
| right: 16px; | |
| cursor: pointer; | |
| } | |
| .modal-fade-enter-active { | |
| animation: modal-fade-enter .25s both ease-in; | |
| } | |
| .modal-fade-leave-active { | |
| animation: modal-fade-leave .25s both ease-out; | |
| } | |
| .modal-zoom-enter-active { | |
| animation: modal-zoom-enter .25s both cubic-bezier(.4, 0, 0, 1.5); | |
| } | |
| .modal-zoom-leave-active { | |
| animation: modal-zoom-leave .25s both; | |
| } | |
| @keyframes modal-fade-enter { | |
| from { | |
| opacity: 0; | |
| } | |
| } | |
| @keyframes modal-fade-leave { | |
| to { | |
| opacity: 0; | |
| } | |
| } | |
| @keyframes modal-zoom-enter { | |
| from { | |
| transform: scale3d(.3, .3, .3); | |
| } | |
| } | |
| @keyframes modal-zoom-leave { | |
| to { | |
| transform: scale3d(.3, .3, .3); | |
| } | |
| } | |
| </style> |