Vue3 弹窗组件封装

阅读 20

06-18 09:00

在 Vue3 中封装一个弹窗组件是一个常见的需求,下面我将提供一个完整的弹窗组件封装方案,包括组件实现、使用方法和最佳实践。

基础弹窗组件实现

首先创建一个基础的弹窗组件 BaseModal.vue

<template>
  <transition name="modal">
    <div v-if="isVisible" class="modal-overlay" @click.self="close">
      <div class="modal-container">
        <div class="modal-header">
          <h3>{{ title }}</h3>
          <button class="close-button" @click="close">×</button>
        </div>
        <div class="modal-body">
          <slot></slot>
        </div>
        <div class="modal-footer" v-if="showFooter">
          <slot name="footer">
            <button @click="close">关闭</button>
            <button @click="confirm" v-if="showConfirm">确认</button>
          </slot>
        </div>
      </div>
    </div>
  </transition>
</template>

<script setup>
import { defineProps, defineEmits } from 'vue';

const props = defineProps({
  isVisible: {
    type: Boolean,
    required: true
  },
  title: {
    type: String,
    default: '提示'
  },
  showFooter: {
    type: Boolean,
    default: true
  },
  showConfirm: {
    type: Boolean,
    default: true
  }
});

const emit = defineEmits(['close', 'confirm']);

const close = () => {
  emit('close');
};

const confirm = () => {
  emit('confirm');
};
</script>

<style scoped>
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}

.modal-container {
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
  width: 80%;
  max-width: 500px;
  max-height: 80vh;
  display: flex;
  flex-direction: column;
}

.modal-header {
  padding: 16px;
  border-bottom: 1px solid #eee;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.modal-body {
  padding: 16px;
  overflow-y: auto;
  flex-grow: 1;
}

.modal-footer {
  padding: 16px;
  border-top: 1px solid #eee;
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

.close-button {
  background: none;
  border: none;
  font-size: 1.5rem;
  cursor: pointer;
}

.modal-enter-active,
.modal-leave-active {
  transition: opacity 0.3s ease;
}

.modal-enter-from,
.modal-leave-to {
  opacity: 0;
}
</style>

使用示例

在父组件中使用这个弹窗:

<template>
  <button @click="showModal = true">打开弹窗</button>
  
  <BaseModal 
    :isVisible="showModal" 
    title="自定义标题"
    @close="showModal = false"
    @confirm="handleConfirm"
  >
    <p>这里是弹窗内容</p>
    
    <template #footer>
      <button @click="showModal = false">取消</button>
      <button @click="handleSubmit">提交</button>
    </template>
  </BaseModal>
</template>

<script setup>
import { ref } from 'vue';
import BaseModal from './BaseModal.vue';

const showModal = ref(false);

const handleConfirm = () => {
  console.log('确认操作');
  showModal.value = false;
};

const handleSubmit = () => {
  console.log('提交操作');
  showModal.value = false;
};
</script>

高级封装 - 使用 provide/inject 实现全局弹窗

如果你想实现一个全局弹窗,可以通过 provide/inject 来实现:

  1. 首先创建一个弹窗管理工具 modalManager.js:

import { ref, provide, inject } from 'vue';

const modalSymbol = Symbol();

export function provideModal() {
  const modals = ref({});

  const openModal = (name) => {
    modals.value[name] = true;
  };

  const closeModal = (name) => {
    modals.value[name] = false;
  };

  provide(modalSymbol, {
    modals,
    openModal,
    closeModal
  });

  return { modals };
}

export function useModal() {
  const modalContext = inject(modalSymbol);

  if (!modalContext) {
    throw new Error('useModal() called without provider');
  }

  return modalContext;
}

  1. 在 App.vue 中提供模态框状态:

<script setup>
import { provideModal } from './modalManager';
import BaseModal from './BaseModal.vue';

const { modals } = provideModal();
</script>

<template>
  <BaseModal 
    :isVisible="modals.exampleModal" 
    @close="closeModal('exampleModal')"
  >
    <!-- 内容 -->
  </BaseModal>
  
  <router-view />
</template>

  1. 在任何子组件中使用:

<script setup>
import { useModal } from './modalManager';

const { openModal, closeModal } = useModal();

const showExampleModal = () => {
  openModal('exampleModal');
};
</script>

更高级的弹窗服务

如果你想实现类似 Vue 的 $message 这样的服务式调用,可以创建一个弹窗插件:

// modalPlugin.js
import { createApp, h, ref } from 'vue';

const ModalComponent = {
  props: ['content', 'title'],
  setup(props, { emit }) {
    const isVisible = ref(true);
    
    const close = () => {
      isVisible.value = false;
      emit('close');
    };
    
    return { isVisible, close };
  },
  render() {
    return h(BaseModal, {
      isVisible: this.isVisible,
      title: this.title,
      onClose: this.close
    }, {
      default: () => this.content
    });
  }
};

export default {
  install(app) {
    app.config.globalProperties.$modal = {
      show(options) {
        const container = document.createElement('div');
        document.body.appendChild(container);
        
        const instance = createApp(ModalComponent, {
          content: options.content,
          title: options.title
        });
        
        const unmount = () => {
          instance.unmount();
          document.body.removeChild(container);
        };
        
        instance.mount(container);
        
        return {
          close: unmount
        };
      }
    };
  }
};

然后在 main.js 中使用:

import modalPlugin from './modalPlugin';

const app = createApp(App);
app.use(modalPlugin);
app.mount('#app');

使用方式:

this.$modal.show({
  title: '提示',
  content: '这是一条消息'
});

最佳实践

  1. 可访问性:确保弹窗对屏幕阅读器友好,添加适当的 ARIA 属性
  2. 焦点管理:打开弹窗时将焦点移到弹窗内,关闭时返回原焦点
  3. Esc 关闭:监听 Esc 键关闭弹窗
  4. 滚动锁定:弹窗打开时锁定背景滚动
  5. 动画效果:添加适当的过渡动画提升用户体验
  6. 响应式设计:确保弹窗在不同屏幕尺寸下表现良好

完整示例扩展

这里是一个更完整的弹窗组件示例,包含上述最佳实践:

<template>
  <transition name="modal">
    <div 
      v-if="isVisible" 
      class="modal-overlay" 
      @click.self="close"
      role="dialog"
      aria-modal="true"
      :aria-labelledby="'modal-title-' + _uid"
    >
      <div class="modal-container" ref="modalRef">
        <div class="modal-header">
          <h3 :id="'modal-title-' + _uid">{{ title }}</h3>
          <button 
            class="close-button" 
            @click="close"
            aria-label="关闭弹窗"
          >×</button>
        </div>
        <div class="modal-body">
          <slot></slot>
        </div>
        <div class="modal-footer" v-if="showFooter">
          <slot name="footer">
            <button @click="close">关闭</button>
            <button @click="confirm" v-if="showConfirm">确认</button>
          </slot>
        </div>
      </div>
    </div>
  </transition>
</template>

<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue';

const props = defineProps({
  isVisible: {
    type: Boolean,
    required: true
  },
  title: {
    type: String,
    default: '提示'
  },
  showFooter: {
    type: Boolean,
    default: true
  },
  showConfirm: {
    type: Boolean,
    default: true
  },
  closeOnEsc: {
    type: Boolean,
    default: true
  },
  closeOnClickOutside: {
    type: Boolean,
    default: true
  }
});

const emit = defineEmits(['close', 'confirm']);

const modalRef = ref(null);
let lastFocusedElement = null;

const close = () => {
  if (props.closeOnClickOutside) {
    emit('close');
  }
};

const confirm = () => {
  emit('confirm');
};

const handleKeydown = (e) => {
  if (props.closeOnEsc && e.key === 'Escape') {
    emit('close');
  }
};

onMounted(() => {
  if (props.isVisible) {
    lastFocusedElement = document.activeElement;
    nextTick(() => {
      modalRef.value?.focus();
    });
    document.body.style.overflow = 'hidden';
    document.addEventListener('keydown', handleKeydown);
  }
});

onUnmounted(() => {
  document.body.style.overflow = '';
  document.removeEventListener('keydown', handleKeydown);
  lastFocusedElement?.focus();
});

watch(() => props.isVisible, (val) => {
  if (val) {
    lastFocusedElement = document.activeElement;
    nextTick(() => {
      modalRef.value?.focus();
    });
    document.body.style.overflow = 'hidden';
    document.addEventListener('keydown', handleKeydown);
  } else {
    document.body.style.overflow = '';
    document.removeEventListener('keydown', handleKeydown);
    lastFocusedElement?.focus();
  }
});
</script>

这个组件提供了完整的可访问性支持、焦点管理、键盘交互和滚动锁定功能。

希望这个 Vue3 弹窗组件封装指南对你有所帮助!根据你的具体需求,你可以进一步扩展或修改这些示例。

精彩评论(0)

0 0 举报