在 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 来实现:
- 首先创建一个弹窗管理工具
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;
}
- 在 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>
- 在任何子组件中使用:
<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: '这是一条消息'
});
最佳实践
- 可访问性:确保弹窗对屏幕阅读器友好,添加适当的 ARIA 属性
- 焦点管理:打开弹窗时将焦点移到弹窗内,关闭时返回原焦点
- Esc 关闭:监听 Esc 键关闭弹窗
- 滚动锁定:弹窗打开时锁定背景滚动
- 动画效果:添加适当的过渡动画提升用户体验
- 响应式设计:确保弹窗在不同屏幕尺寸下表现良好
完整示例扩展
这里是一个更完整的弹窗组件示例,包含上述最佳实践:
<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 弹窗组件封装指南对你有所帮助!根据你的具体需求,你可以进一步扩展或修改这些示例。