Modal.vue 3.65 KB
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';

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);

// 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 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>