Modal.vue
3.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
<script setup>
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
// Set sizes based on the size prop
const sizeClasses = {
sm: 'max-w-md',
md: 'max-w-lg',
lg: 'max-w-2xl',
xl: 'max-w-4xl',
full: 'max-w-full mx-4'
};
const props = defineProps({
isOpen: {
type: Boolean,
required: true
},
title: {
type: String,
required: true
},
size: {
type: String,
default: 'md'
},
closeOnClickOutside: {
type: Boolean,
default: true
},
showCloseButton: {
type: Boolean,
default: true
},
contentClassName: {
type: String,
default: ''
}
});
const emit = defineEmits(['close']);
const modalRef = ref(null);
const isMounted = ref(false);
const modalSize = computed(() => sizeClasses[props.size] || sizeClasses.md);
// Handle ESC key press
const handleKeyDown = (event) => {
if (event.key === 'Escape' && props.isOpen) {
emit('close');
}
};
// Handle click outside
const handleClickOutside = (event) => {
if (modalRef.value && !modalRef.value.contains(event.target) && props.closeOnClickOutside) {
emit('close');
}
};
// Handle mounting/unmounting and event listeners
onMounted(() => {
isMounted.value = true;
document.addEventListener('keydown', handleKeyDown);
});
onUnmounted(() => {
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('mousedown', handleClickOutside);
document.body.style.overflow = 'auto';
});
// Watch isOpen changes
watch(() => props.isOpen, (newValue) => {
if (newValue) {
document.body.style.overflow = 'hidden';
document.addEventListener('mousedown', handleClickOutside);
} else {
document.body.style.overflow = 'auto';
document.removeEventListener('mousedown', handleClickOutside);
}
});
</script>
<template>
<Teleport to="body">
<div v-if="isOpen && isMounted" class="fixed inset-0 z-50 overflow-y-auto">
<!-- Backdrop with semi-transparent background -->
<div class="fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm transition-opacity"></div>
<!-- Modal container -->
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center">
<div
:class="['relative inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle w-full', modalSize]"
ref="modalRef"
>
<!-- Modal header -->
<div class="bg-white px-4 py-4 border-b border-gray-200 sm:px-6">
<div class="flex items-center justify-between">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ title }}
</h3>
<button
v-if="showCloseButton"
type="button"
class="rounded-md text-gray-400 hover:text-gray-500 focus:outline-none"
@click="emit('close')"
>
<span class="sr-only">关闭</span>
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<!-- Modal content -->
<div :class="['bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4', contentClassName]">
<slot></slot>
</div>
<!-- Modal footer -->
<div v-if="$slots.footer" class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<slot name="footer"></slot>
</div>
</div>
</div>
</div>
</Teleport>
</template>