Appearance
Modal 弹窗组件
Modal 组件是一个功能强大的弹窗组件,用于展示重要信息、表单输入或进行用户交互确认。它支持多种尺寸、位置、动画效果和自定义内容,提供组件式和函数式两种调用方式。
基础用法
Modal 组件的基础用法非常简单,通过控制 open 属性来显示或隐藏弹窗。
基础用法
通过控制 open 属性来显示或隐藏弹窗
vue
<template>
<div class="modal-demo">
<div class="demo-header">
<h2>基础用法</h2>
<p>通过控制 open 属性来显示或隐藏弹窗</p>
</div>
<div class="demo-content">
<Button @click="openModal">打开弹窗</Button>
<Modal
v-model:open="isOpen"
title="基础弹窗"
@ok="handleOk"
@cancel="handleCancel"
:mask="true"
>
<div class="modal-body-content">
<p>这是一个基础的弹窗示例</p>
<p>您可以在这里放置任何内容,如表单、信息展示等</p>
</div>
</Modal>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const isOpen = ref(false);
const openModal = () => {
isOpen.value = true;
};
const handleOk = () => {
console.log('用户点击了确定');
isOpen.value = false;
};
const handleCancel = () => {
console.log('用户点击了取消');
isOpen.value = false;
};
</script>
<style scoped>
.modal-demo {
padding: 20px;
background-color: var(--bg-card);
border-radius: var(--radius-md);
}
.demo-header {
margin-bottom: 24px;
}
.demo-header h2 {
margin: 0 0 8px 0;
font-size: var(--font-size-xl);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
}
.demo-header p {
margin: 0;
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.modal-body-content {
padding: 8px 0;
}
.modal-body-content p {
margin: 12px 0;
line-height: 1.6;
}
</style>不同尺寸
Modal 组件支持多种预设尺寸,可以根据内容需求选择合适的大小。
不同尺寸
Modal 组件支持多种预设尺寸和自定义尺寸,可以根据内容需求选择合适的大小
自定义弹窗大小
vue
<template>
<div class="modal-demo">
<div class="demo-header">
<h2>不同尺寸</h2>
<p>
Modal 组件支持多种预设尺寸和自定义尺寸,可以根据内容需求选择合适的大小
</p>
</div>
<div class="demo-content">
<div class="button-group">
<Button @click="openModal('sm')">小尺寸弹窗</Button>
<Button @click="openModal('md')">中等弹窗</Button>
<Button @click="openModal('lg')">大尺寸弹窗</Button>
<Button @click="openModal('xl')">超大弹窗</Button>
<Button @click="openModal('fullscreen')">全屏弹窗</Button>
</div>
<div class="custom-size-section">
<h3>自定义弹窗大小</h3>
<div class="size-inputs">
<div class="input-group">
<label for="width-input">宽度:</label>
<input
id="width-input"
v-model="customWidth"
type="text"
placeholder="如: 600px 或 80%"
/>
</div>
<div class="input-group">
<label for="height-input">高度:</label>
<input
id="height-input"
v-model="customHeight"
type="text"
placeholder="如: 400px 或 60%"
/>
</div>
</div>
<Button @click="openCustomSizeModal" type="primary"
>自定义尺寸弹窗</Button
>
</div>
<Modal
v-model:open="isOpen"
:size="currentSize"
title="不同尺寸的弹窗"
:width="currentWidth"
:height="currentHeight"
@ok="handleOk"
@cancel="handleCancel"
>
<div class="modal-body-content">
<p>当前尺寸: {{ currentSize }}</p>
<p v-if="currentSize === 'custom'">
当前自定义大小: 宽度 {{ currentWidth }}, 高度 {{ currentHeight }}
</p>
<p v-if="currentSize === 'sm'">
这是一个小尺寸弹窗,适合简单提示信息。
</p>
<p v-else-if="currentSize === 'md'">
这是一个中等尺寸弹窗,是最常用的默认尺寸。
</p>
<p v-else-if="currentSize === 'lg'">
这是一个大尺寸弹窗,适合展示较多内容。
</p>
<p v-else-if="currentSize === 'xl'">
这是一个超大尺寸弹窗,适合复杂表单或大量数据展示。
</p>
<p v-else-if="currentSize === 'fullscreen'">
这是一个全屏弹窗,适合需要用户全神贯注完成的任务。
</p>
<div v-if="currentSize !== 'fullscreen'">
<h3>尺寸说明:</h3>
<ul>
<li>sm - 300px 宽度</li>
<li>md - 500px 宽度(默认)</li>
<li>lg - 700px 宽度</li>
<li>xl - 900px 宽度</li>
<li>fullscreen - 全屏显示</li>
<li>自定义 - 可自由设置宽度和高度</li>
</ul>
</div>
</div>
</Modal>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const isOpen = ref(false);
const currentSize = ref('md');
const currentWidth = ref(undefined);
const currentHeight = ref(undefined);
const customWidth = ref('600px');
const customHeight = ref('400px');
const openModal = size => {
currentSize.value = size;
currentWidth.value = undefined;
currentHeight.value = undefined;
isOpen.value = true;
};
const openCustomSizeModal = () => {
currentSize.value = 'custom';
currentWidth.value = customWidth.value || undefined;
currentHeight.value = customHeight.value || undefined;
isOpen.value = true;
};
const handleOk = () => {
isOpen.value = false;
};
const handleCancel = () => {
isOpen.value = false;
};
</script>
<style scoped>
.modal-demo {
padding: 20px;
background-color: var(--bg-card);
border-radius: var(--radius-md);
}
.demo-header {
margin-bottom: 24px;
}
.demo-header h2 {
margin: 0 0 8px 0;
font-size: var(--font-size-xl);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
}
.demo-header p {
margin: 0;
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.button-group {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.custom-size-section {
margin-top: 20px;
padding: 20px;
background-color: rgba(0, 0, 0, 0.02);
border-radius: var(--radius-md);
}
.custom-size-section h3 {
margin: 0 0 16px 0;
font-size: var(--font-size-lg);
font-weight: var(--font-weight-medium);
}
.size-inputs {
display: flex;
gap: 20px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.input-group {
display: flex;
align-items: center;
gap: 8px;
}
.input-group label {
font-size: var(--font-size-sm);
color: var(--text-secondary);
white-space: nowrap;
}
.input-group input {
padding: 8px 12px;
border: 1px solid var(--color-border-1);
border-radius: var(--radius-md);
font-size: var(--font-size-sm);
width: 180px;
}
.modal-body-content {
padding: 8px 0;
}
.modal-body-content p {
margin: 12px 0;
line-height: 1.6;
}
.modal-body-content h3 {
margin: 16px 0 8px 0;
font-size: var(--font-size-lg);
font-weight: var(--font-weight-medium);
}
.modal-body-content ul {
margin: 8px 0 16px 0;
padding-left: 24px;
}
.modal-body-content li {
margin: 4px 0;
line-height: 1.5;
}
</style>不同位置
Modal 组件可以显示在屏幕的不同位置,包括居中、顶部、底部、左侧和右侧。
不同位置
Modal 组件可以显示在屏幕的不同位置,包括居中、顶部、底部、左侧和右侧
vue
<template>
<div class="modal-demo">
<div class="demo-header">
<h2>不同位置</h2>
<p>Modal 组件可以显示在屏幕的不同位置,包括居中、顶部、底部、左侧和右侧</p>
</div>
<div class="demo-content">
<div class="button-group">
<Button @click="openModal('center')">居中显示</Button>
<Button @click="openModal('top')">顶部显示</Button>
<Button @click="openModal('bottom')">底部显示</Button>
<Button @click="openModal('left')">左侧显示</Button>
<Button @click="openModal('right')">右侧显示</Button>
</div>
<Modal
v-model:open="isOpen"
:position="currentPosition"
:size="getSizeByPosition(currentPosition)"
:title="getTitleByPosition(currentPosition)"
:closable="true"
:maskClosable="true"
@ok="handleOk"
@cancel="handleCancel"
>
<div class="modal-body-content">
<p>当前位置: {{ currentPosition }}</p>
<div v-if="currentPosition === 'center'">
<p>居中弹窗是最常用的模式,适合需要用户重点关注的内容。</p>
<p>可以设置为不同的尺寸,默认中等大小。</p>
</div>
<div v-else-if="currentPosition === 'top'">
<p>顶部弹窗从屏幕上方滑入,常用于通知、轻量级确认等场景。</p>
</div>
<div v-else-if="currentPosition === 'bottom'">
<p>底部弹窗从屏幕下方滑入,常用于操作菜单、选项选择等场景。</p>
<p>这种模式在移动端应用中尤为常见。</p>
</div>
<div v-else-if="currentPosition === 'left'">
<p>左侧弹窗从屏幕左侧滑入,常用于展示辅助信息、导航菜单等。</p>
</div>
<div v-else-if="currentPosition === 'right'">
<p>右侧弹窗从屏幕右侧滑入,常用于展示详情信息、设置面板等。</p>
</div>
<div class="responsive-note">
<strong>响应式说明:</strong>
<p>在移动设备上,侧边弹窗(left/right)会自动调整为全屏模式,以确保良好的用户体验。</p>
</div>
</div>
</Modal>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const isOpen = ref(false);
const currentPosition = ref('center');
const openModal = (position) => {
currentPosition.value = position;
isOpen.value = true;
};
const getSizeByPosition = (position) => {
if (position === 'left' || position === 'right') {
return 'md'; // 侧边弹窗的默认大小
}
return 'md'; // 其他位置使用默认大小
};
const getTitleByPosition = (position) => {
const positionTitles = {
center: '居中弹窗',
top: '顶部弹窗',
bottom: '底部弹窗',
left: '左侧弹窗',
right: '右侧弹窗'
};
return positionTitles[position] || '弹窗示例';
};
const handleOk = () => {
isOpen.value = false;
};
const handleCancel = () => {
isOpen.value = false;
};
</script>
<style scoped>
.modal-demo {
padding: 20px;
background-color: var(--bg-card);
border-radius: var(--radius-md);
}
.demo-header {
margin-bottom: 24px;
}
.demo-header h2 {
margin: 0 0 8px 0;
font-size: var(--font-size-xl);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
}
.demo-header p {
margin: 0;
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.button-group {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.modal-body-content {
padding: 8px 0;
}
.modal-body-content p {
margin: 12px 0;
line-height: 1.6;
}
.responsive-note {
margin-top: 20px;
padding: 12px;
background-color: var(--bg-secondary);
border-radius: var(--radius-sm);
font-size: var(--font-size-sm);
}
.responsive-note strong {
display: block;
margin-bottom: 8px;
color: var(--text-primary);
}
.responsive-note p {
margin: 0;
color: var(--text-secondary);
}
</style>自定义位置
Modal 组件支持通过设置 position="absolute" 并配合 top、right、bottom、left 属性来自定义弹窗位置,支持像素值和百分比值。
自定义位置
Modal 支持通过自定义位置属性精确定位到屏幕任意位置
像素值定位
百分比定位
动态定位
vue
<template>
<div id="modal-demo-6" class="modal-demo-section">
<h2 class="demo-title">自定义位置</h2>
<p class="demo-description">
Modal 支持通过自定义位置属性精确定位到屏幕任意位置
</p>
<div class="demo-container">
<!-- 自定义像素位置 -->
<div class="demo-item">
<h3 class="demo-subtitle">像素值定位</h3>
<Button @click="showPixelPositionModal">显示左上角弹窗</Button>
<Modal
v-model:open="pixelModalOpen"
title="左上角弹窗"
size="sm"
position="absolute"
top="50px"
left="50px"
@ok="pixelModalOpen = false"
@cancel="pixelModalOpen = false"
>
<div>这个弹窗使用像素值定位在屏幕左上角 (top: 50px, left: 50px)</div>
</Modal>
</div>
<!-- 自定义百分比位置 -->
<div class="demo-item">
<h3 class="demo-subtitle">百分比定位</h3>
<Button @click="showPercentPositionModal">显示右下角弹窗</Button>
<Modal
v-model:open="percentModalOpen"
title="右下角弹窗"
size="md"
position="absolute"
bottom="10%"
right="10%"
@ok="percentModalOpen = false"
@cancel="percentModalOpen = false"
>
<div>
这个弹窗使用百分比定位在屏幕右下角 (bottom: 10%, right: 10%)
</div>
</Modal>
</div>
<!-- 动态定位 -->
<div class="demo-item">
<h3 class="demo-subtitle">动态定位</h3>
<div class="demo-controls">
<div class="control-group">
<label>Top: {{ dynamicTop }}px</label>
<input type="range" min="0" max="500" v-model.number="dynamicTop" />
</div>
<div class="control-group">
<label>Left: {{ dynamicLeft }}px</label>
<input
type="range"
min="0"
max="500"
v-model.number="dynamicLeft"
/>
</div>
</div>
<Button @click="showDynamicPositionModal">显示动态定位弹窗</Button>
<Modal
v-model:open="dynamicModalOpen"
title="动态定位弹窗"
width="300px"
position="absolute"
:top="`${dynamicTop}px`"
:left="`${dynamicLeft}px`"
@ok="dynamicModalOpen = false"
@cancel="dynamicModalOpen = false"
>
<div>当前位置: top {{ dynamicTop }}px, left {{ dynamicLeft }}px</div>
</Modal>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
// 像素值定位
const pixelModalOpen = ref(false);
const showPixelPositionModal = () => {
pixelModalOpen.value = true;
};
// 百分比定位
const percentModalOpen = ref(false);
const showPercentPositionModal = () => {
percentModalOpen.value = true;
};
// 混合定位
const mixedModalOpen = ref(false);
const showMixedPositionModal = () => {
mixedModalOpen.value = true;
};
// 动态定位
const dynamicModalOpen = ref(false);
const dynamicTop = ref(100);
const dynamicLeft = ref(100);
const showDynamicPositionModal = () => {
dynamicModalOpen.value = true;
};
</script>
<style scoped>
.modal-demo-section {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.demo-title {
font-size: 24px;
margin-bottom: 10px;
color: var(--color-text-1);
}
.demo-description {
font-size: 14px;
color: var(--color-text-2);
margin-bottom: 30px;
}
.demo-container {
display: flex;
flex-direction: column;
gap: 30px;
}
.demo-item {
background-color: var(--color-background);
padding: 20px;
border-radius: var(--border-radius);
border: 1px solid var(--color-border-1);
}
.demo-subtitle {
font-size: 18px;
margin-bottom: 15px;
color: var(--color-text-1);
}
.demo-controls {
display: flex;
gap: 20px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.control-group {
display: flex;
flex-direction: column;
gap: 5px;
width: 200px;
}
.control-group label {
font-size: 14px;
color: var(--color-text-2);
}
.control-group input[type='range'] {
width: 100%;
height: 4px;
background: var(--color-border-1);
border-radius: 2px;
outline: none;
-webkit-appearance: none;
}
.control-group input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
background: var(--color-primary);
border-radius: 50%;
cursor: pointer;
}
.control-group input[type='range']::-moz-range-thumb {
width: 16px;
height: 16px;
background: var(--color-primary);
border-radius: 50%;
cursor: pointer;
border: none;
}
</style>动画效果
Modal 组件提供了多种动画效果,可以通过 animation 属性进行选择。
不同动画效果
vue
<template>
<div>
<h3>不同动画效果</h3>
<div class="demo-buttons">
<Button @click="openModal('zoom')">缩放动画 (zoom)</Button>
<Button @click="openModal('slide')">滑动动画 (slide)</Button>
<Button @click="openModal('fade')">淡入淡出 (fade)</Button>
<Button @click="openModal('bounce')">弹跳动画 (bounce)</Button>
</div>
<!-- 缩放动画 -->
<Modal
v-model:open="modalStates.zoom"
title="缩放动画示例"
animation="zoom"
size="md"
>
<div class="demo-content">
<p>这是一个使用缩放动画的弹窗。</p>
<p>弹窗出现时会从中心逐渐放大,关闭时会逐渐缩小消失。</p>
</div>
</Modal>
<!-- 滑动动画 -->
<Modal
v-model:open="modalStates.slide"
title="滑动动画示例"
animation="slide"
size="md"
position="bottom"
>
<div class="demo-content">
<p>这是一个使用滑动动画的弹窗。</p>
<p>根据弹窗位置不同,滑动方向也会不同:</p>
<ul>
<li>顶部位置:从上方滑入</li>
<li>底部位置:从下方滑入</li>
<li>左侧位置:从左侧滑入</li>
<li>右侧位置:从右侧滑入</li>
</ul>
</div>
</Modal>
<!-- 淡入淡出动画 -->
<Modal
v-model:open="modalStates.fade"
title="淡入淡出示例"
animation="fade"
size="md"
>
<div class="demo-content">
<p>这是一个使用淡入淡出动画的弹窗。</p>
<p>弹窗出现和消失时只有透明度的变化,没有其他变换效果。</p>
</div>
</Modal>
<!-- 弹跳动画 -->
<Modal
v-model:open="modalStates.bounce"
title="弹跳动画示例"
animation="bounce"
size="md"
>
<div class="demo-content">
<p>这是一个使用弹跳动画的弹窗。</p>
<p>弹窗出现时会有弹性缩放效果,关闭时也会有弹性缩小消失的效果。</p>
</div>
</Modal>
</div>
</template>
<script setup lang="ts">
import { reactive } from 'vue';
// 弹窗状态
const modalStates = reactive({
zoom: false,
slide: false,
fade: false,
bounce: false,
});
// 打开弹窗
const openModal = (animation: 'zoom' | 'slide' | 'fade' | 'bounce') => {
modalStates[animation] = true;
};
</script>
<style scoped>
.demo-buttons {
display: flex;
gap: 12px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.demo-content {
padding: 20px 0;
}
.demo-content p {
margin-bottom: 12px;
line-height: 1.6;
}
.demo-content ul {
margin-top: 12px;
padding-left: 20px;
}
.demo-content li {
margin-bottom: 8px;
line-height: 1.6;
}
</style>遮罩控制
Modal 组件支持控制遮罩层的显示/隐藏,以及自定义遮罩样式。
Mask 测试
测试1: mask=false
测试2: mask=true (默认)
测试3: 自定义遮罩样式
vue
<template>
<div class="modal-mask-test">
<h3>Mask 测试</h3>
<!-- 测试1: mask=false 应该隐藏遮罩 -->
<div class="test-section">
<h4>测试1: mask=false</h4>
<Button type="primary" @click="openModal1">打开无遮罩弹窗</Button>
<Modal
v-model:open="modal1Open"
title="无遮罩弹窗"
:mask="false"
position="center"
size="md"
>
<div>这个弹窗没有遮罩层,点击页面其他区域不会关闭。</div>
</Modal>
</div>
<!-- 测试2: mask=true 应该显示默认遮罩 -->
<div class="test-section">
<h4>测试2: mask=true (默认)</h4>
<Button type="primary" @click="openModal2">打开有遮罩弹窗</Button>
<Modal
v-model:open="modal2Open"
title="有遮罩弹窗"
:mask="true"
mask-closable
position="center"
size="md"
>
<div>这个弹窗有默认遮罩层,点击遮罩区域可以关闭。</div>
</Modal>
</div>
<!-- 测试3: 自定义遮罩样式 -->
<div class="test-section">
<h4>测试3: 自定义遮罩样式</h4>
<Button type="primary" @click="openModal3">打开自定义遮罩弹窗</Button>
<Modal
v-model:open="modal3Open"
title="自定义遮罩弹窗"
:mask="true"
:mask-style="customMaskStyle"
mask-closable
position="center"
size="md"
>
<div>这个弹窗有自定义样式的遮罩层,模糊背景效果</div>
</Modal>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
// 测试1: mask=false
const modal1Open = ref(false);
const openModal1 = () => {
modal1Open.value = true;
};
// 测试2: mask=true
const modal2Open = ref(false);
const openModal2 = () => {
modal2Open.value = true;
};
// 测试3: 自定义遮罩样式
const modal3Open = ref(false);
const customMaskStyle = {
backgroundColor: 'rgba(255, 250,255, 0.1)',
backdropFilter: 'blur(15px)', // 模糊效果
};
const openModal3 = () => {
modal3Open.value = true;
};
</script>
<style scoped>
.modal-mask-test {
padding: 20px;
}
.test-section {
margin-bottom: 20px;
}
.test-section h4 {
margin-bottom: 10px;
font-size: 16px;
color: #333;
}
</style>自定义内容
Modal 组件支持自定义头部、内容和底部,可以通过插槽来定制弹窗的各个部分。
自定义内容
Modal 组件支持自定义头部、内容和底部,可以通过插槽来定制弹窗的各个部分, 也可以控制是否显示头部和底部
vue
<template>
<div class="modal-demo">
<div class="demo-header">
<h2>自定义内容</h2>
<p>
Modal 组件支持自定义头部、内容和底部,可以通过插槽来定制弹窗的各个部分,
也可以控制是否显示头部和底部
</p>
</div>
<div class="demo-content">
<!-- 按钮组 -->
<div class="demo-buttons">
<Button @click="openModal('default')">默认弹窗</Button>
<Button @click="openModal('noHeader')">无头部弹窗</Button>
<Button @click="openModal('noFooter')">无底部弹窗</Button>
<Button @click="openModal('noHeaderFooter')">无头部和底部弹窗</Button>
<Button @click="openModal('customStyle')">自定义样式弹窗</Button>
</div>
<!-- 默认弹窗 -->
<Modal
v-model:open="modalStates.default"
size="lg"
:closable="true"
:maskClosable="true"
@cancel="handleCancel('default')"
>
<!-- 自定义头部 -->
<template #header>
<div class="custom-header">
<div class="header-icon">📝</div>
<div class="header-content">
<h3>自定义表单</h3>
<p>请填写以下表单信息</p>
</div>
</div>
</template>
<!-- 自定义内容 -->
<div class="custom-content">
<div class="form-group">
<label for="name">姓名</label>
<Input id="name" v-model="formData.name" placeholder="请输入姓名" />
</div>
<div class="form-group">
<label for="email">邮箱</label>
<Input
id="email"
v-model="formData.email"
placeholder="请输入邮箱"
type="email"
/>
</div>
<div class="form-group">
<label for="message">留言</label>
<Textarea
id="message"
v-model="formData.message"
placeholder="请输入留言内容"
rows="4"
/>
</div>
<div class="form-group">
<Checkbox v-model="formData.agree">我同意隐私政策</Checkbox>
</div>
</div>
<!-- 自定义底部 -->
<template #footer>
<div class="custom-footer">
<Button @click="resetForm" type="default">重置</Button>
<div class="action-buttons">
<Button @click="handleCancel('default')" type="default"
>取消</Button
>
<Button
@click="handleSubmit('default')"
type="primary"
:disabled="!formData.agree"
>提交</Button
>
</div>
</div>
</template>
</Modal>
<!-- 无头部弹窗 -->
<Modal
v-model:open="modalStates.noHeader"
size="lg"
:closable="true"
:header="false"
:maskClosable="true"
@cancel="handleCancel('noHeader')"
>
<!-- 自定义内容 -->
<div class="custom-content">
<div class="form-group">
<label for="name2">姓名</label>
<Input
id="name2"
v-model="formData.name"
placeholder="请输入姓名"
/>
</div>
<div class="form-group">
<label for="email2">邮箱</label>
<Input
id="email2"
v-model="formData.email"
placeholder="请输入邮箱"
type="email"
/>
</div>
<div class="form-group">
<label for="message2">留言</label>
<Textarea
id="message2"
v-model="formData.message"
placeholder="请输入留言内容"
rows="4"
/>
</div>
</div>
<!-- 自定义底部 -->
<template #footer>
<div class="custom-footer">
<Button @click="resetForm" type="default">重置</Button>
<div class="action-buttons">
<Button @click="handleCancel('noHeader')" type="default"
>取消</Button
>
<Button
@click="handleSubmit('noHeader')"
type="primary"
:disabled="!formData.agree"
>提交</Button
>
</div>
</div>
</template>
</Modal>
<!-- 无底部弹窗 -->
<Modal
v-model:open="modalStates.noFooter"
size="lg"
:closable="true"
:footer="false"
:maskClosable="true"
@cancel="handleCancel('noFooter')"
>
<!-- 自定义头部 -->
<template #header>
<div class="custom-header">
<div class="header-icon">📝</div>
<div class="header-content">
<h3>查看信息</h3>
<p>这是一个没有底部的弹窗</p>
</div>
</div>
</template>
<!-- 自定义内容 -->
<div class="custom-content">
<div class="info-item">
<strong>姓名:</strong> {{ formData.name || '未填写' }}
</div>
<div class="info-item">
<strong>邮箱:</strong> {{ formData.email || '未填写' }}
</div>
<div class="info-item">
<strong>留言:</strong> {{ formData.message || '未填写' }}
</div>
<div class="info-item">
<strong>隐私政策:</strong>
{{ formData.agree ? '已同意' : '未同意' }}
</div>
<div class="mt-4">
<Button @click="handleCancel('noFooter')" type="primary"
>关闭</Button
>
</div>
</div>
</Modal>
<!-- 无头部和底部弹窗 -->
<Modal
v-model:open="modalStates.noHeaderFooter"
size="md"
:closable="true"
:header="false"
:footer="false"
:maskClosable="true"
@cancel="handleCancel('noHeaderFooter')"
>
<!-- 自定义内容 -->
<div class="custom-content text-center">
<div class="warning-icon">⚠️</div>
<h3 class="warning-title">确认操作</h3>
<p class="warning-message">您确定要执行此操作吗?</p>
<div class="mt-4">
<Button
@click="handleCancel('noHeaderFooter')"
type="default"
class="mr-2"
>取消</Button
>
<Button
@click="handleConfirm('noHeaderFooter')"
type="primary"
danger
>确认</Button
>
</div>
</div>
</Modal>
<!-- 自定义样式弹窗 -->
<Modal
v-model:open="modalStates.customStyle"
size="lg"
:closable="true"
:maskClosable="true"
class="custom-style-modal"
:content-style="computedCustomStyles"
@cancel="handleCancel('customStyle')"
>
<!-- 自定义头部 -->
<template #header>
<div class="custom-header fancy-header">
<div class="header-icon">✨</div>
<div class="header-content">
<h3>自定义样式弹窗</h3>
<p>实时调整弹窗的样式</p>
</div>
</div>
</template>
<!-- 自定义内容 -->
<div class="custom-content fancy-content">
<!-- 样式自定义表单 -->
<div class="style-customizer">
<div class="form-row">
<div class="form-group-half">
<label>背景颜色</label>
<div class="color-input-group">
<Input
v-model="customStyles.backgroundColor"
placeholder="#b0dafe"
/>
</div>
</div>
<div class="form-group-half">
<label>文字颜色</label>
<div class="color-input-group">
<Input v-model="customStyles.color" placeholder="#333333" />
</div>
</div>
</div>
<div class="form-row">
<div class="form-group-half">
<label>边框颜色</label>
<div class="color-input-group">
<Input
v-model="customStyles.borderColor"
placeholder="#3b82f6"
/>
</div>
</div>
<div class="form-group-half">
<label>边框宽度</label>
<Input v-model="customStyles.borderWidth" placeholder="2px" />
</div>
</div>
<div class="form-row">
<div class="form-group-half">
<label>边框半径</label>
<Input v-model="customStyles.borderRadius" placeholder="12px" />
</div>
<div class="form-group-half">
<label>阴影效果</label>
<Input
v-model="customStyles.boxShadow"
placeholder="0 10px 30px rgba(0, 0, 0, 0.15)"
/>
</div>
</div>
</div>
</div>
<!-- 自定义底部 -->
<template #footer>
<div class="custom-footer fancy-footer">
<div class="action-buttons">
<Button @click="resetCustomStyles" type="default"
>重置样式</Button
>
<Button @click="handleCancel('customStyle')" type="default"
>关闭</Button
>
<Button @click="applyCustomStyles" type="primary"
>应用样式</Button
>
</div>
</div>
</template>
</Modal>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue';
// 控制不同弹窗的显示状态
const modalStates = reactive({
default: false,
noHeader: false,
noFooter: false,
noHeaderFooter: false,
customStyle: false,
});
// 表单数据
const formData = reactive({
name: '',
email: '',
message: '',
agree: false,
});
// 自定义样式数据
const customStyles = reactive({
backgroundColor: '#b0dafe',
color: '#333333',
borderColor: '#3b82f6',
borderRadius: '12px',
borderWidth: '2px',
boxShadow: '0 10px 30px rgba(0, 0, 0, 0.15)',
});
// 计算自定义弹窗样式(使用contentStyle属性)
const computedCustomStyles = computed(() => {
return {
backgroundColor: customStyles.backgroundColor,
color: customStyles.color,
borderColor: customStyles.borderColor,
borderRadius: customStyles.borderRadius,
borderWidth: customStyles.borderWidth,
boxShadow: customStyles.boxShadow,
};
});
// 打开弹窗
const openModal = type => {
modalStates[type] = true;
};
// 重置表单
const resetForm = () => {
formData.name = '';
formData.email = '';
formData.message = '';
formData.agree = false;
};
// 提交表单
const handleSubmit = type => {
if (type !== 'customStyle' && !formData.agree) return;
console.log('表单提交:', formData);
alert('操作成功!');
modalStates[type] = false;
};
// 确认操作
const handleConfirm = type => {
alert('确认操作已执行!');
modalStates[type] = false;
};
// 取消操作
const handleCancel = type => {
modalStates[type] = false;
};
// 重置自定义样式
const resetCustomStyles = () => {
Object.assign(customStyles, {
backgroundColor: '#b0dafe',
color: '#333333',
borderColor: '#3b82f6',
borderRadius: '12px',
borderWidth: '2px',
boxShadow: '0 10px 30px rgba(0, 0, 0, 0.15)',
});
};
// 应用自定义样式
const applyCustomStyles = () => {
console.log('应用自定义样式:', customStyles);
alert('样式已应用!');
};
</script>
<style scoped>
.modal-demo {
padding: 20px;
background-color: var(--bg-card);
border-radius: var(--radius-md);
}
.demo-header {
margin-bottom: 24px;
}
.demo-header h2 {
margin: 0 0 8px 0;
font-size: var(--font-size-xl);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
}
.demo-header p {
margin: 0;
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 16px;
}
/* 按钮组样式 */
.demo-buttons {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 20px;
}
/* 自定义头部样式 */
.custom-header {
display: flex;
align-items: center;
width: 100%;
gap: 12px;
padding: 0;
}
.header-icon {
font-size: 24px;
line-height: 1;
}
.header-content h3 {
margin: 0 0 4px 0;
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
}
.header-content p {
margin: 0;
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
/* 自定义内容样式 */
.custom-content {
padding: 8px 0;
}
/* 表单组样式 */
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: var(--font-weight-medium);
color: var(--text-primary);
font-size: var(--font-size-sm);
}
.form-group :deep(.Input) {
width: 100%;
}
.form-group :deep(.Textarea) {
width: 100%;
}
/* 信息项样式 */
.info-item {
margin-bottom: 12px;
padding: 8px 0;
border-bottom: 1px solid var(--border-color);
}
.info-item:last-child {
border-bottom: none;
}
/* 文本居中样式 */
.text-center {
text-align: center;
}
/* 警告图标样式 */
.warning-icon {
font-size: 48px;
margin-bottom: 16px;
}
/* 警告标题样式 */
.warning-title {
margin: 0 0 8px 0;
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
}
/* 警告消息样式 */
.warning-message {
margin: 0 0 16px 0;
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
/* 自定义底部样式 */
.custom-footer {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.action-buttons {
display: flex;
gap: 8px;
}
/* 自定义样式弹窗 */
:deep(.custom-style-modal .z-modal__content) {
/* 自定义样式将通过contentStyle属性直接应用 */
}
/* 样式自定义器 */
.style-customizer {
margin-bottom: 24px;
}
.form-row {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
.form-group-half {
flex: 1;
}
.color-input-group {
display: flex;
gap: 8px;
align-items: center;
}
.color-picker {
width: 40px;
height: 40px;
/* border: 1px solid var(--border-color); */
border-radius: var(--radius-sm);
cursor: pointer;
padding: 2px;
}
/* 样式预览 */
.style-preview {
margin-top: 24px;
padding: 20px;
background-color: rgba(255, 255, 255, 0.8);
border-radius: var(--radius-md);
border: 1px solid var(--border-color);
}
.style-preview h4 {
margin: 0 0 12px 0;
font-size: var(--font-size-md);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
}
.preview-box {
padding: 16px;
border-radius: var(--radius-sm);
border: 1px dashed var(--border-color);
background-color: var(--custom-bg-color, rgb(176, 218, 255));
color: var(--custom-text-color, #333333);
min-height: 100px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 12px;
}
.preview-text {
margin: 0;
font-size: var(--font-size-sm);
}
.preview-button {
margin-top: 8px;
}
/* 自定义样式头部 */
.fancy-header {
padding: 16px 0;
border-bottom: 2px solid var(--primary-light);
}
/* 自定义样式内容 */
.fancy-content {
padding: 24px 0;
}
/* 特性项样式 */
.feature-item {
display: flex;
align-items: flex-start;
gap: 16px;
margin-bottom: 20px;
padding: 16px;
background-color: var(--bg-hover);
border-radius: var(--radius-md);
border-left: 4px solid var(--primary);
}
.feature-item:last-child {
margin-bottom: 0;
}
.feature-icon {
font-size: 32px;
flex-shrink: 0;
}
.feature-content h4 {
margin: 0 0 4px 0;
font-size: var(--font-size-md);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
}
.feature-content p {
margin: 0;
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
/* 自定义样式底部 */
.fancy-footer {
padding: 16px 0;
border-top: 1px solid var(--border-color);
justify-content: flex-end;
}
/* 间距辅助类 */
.mt-4 {
margin-top: 16px;
}
.mr-2 {
margin-right: 8px;
}
</style>API 调用方式
除了组件方式,Modal 还提供了函数式 API,可以直接通过 JavaScript/TypeScript 调用。
函数式 API 说明
通过 showModal(options) 函数可以方便地在任何地方打开弹窗,无需在模板中预先声明组件。
该函数返回一个包含 close() 方法的对象,用于手动关闭弹窗。
vue
<template>
<div class="modal-demo">
<div class="demo-content">
<div class="button-group">
<Button @click="showBasicModal">基础弹窗</Button>
<Button @click="showCustomizedModal">自定义按钮</Button>
<Button @click="showAsyncModal">异步关闭</Button>
<Button @click="showMultipleModal">多弹窗嵌套</Button>
<Button @click="showNoMaskModal">无遮罩弹窗</Button>
<Button @click="showCustomMaskModal">自定义遮罩</Button>
</div>
<div class="info-box">
<h3>函数式 API 说明</h3>
<p>
通过
<code>showModal(options)</code>
函数可以方便地在任何地方打开弹窗,无需在模板中预先声明组件。
</p>
<p>
该函数返回一个包含 <code>close()</code> 方法的对象,用于手动关闭弹窗。
</p>
</div>
</div>
</div>
</template>
<script setup>
// 基础弹窗示例
const showBasicModal = () => {
showModal({
title: '基础函数式弹窗',
content: '这是一个通过函数式 API 打开的基础弹窗示例。',
size: 'md',
onOk: () => {
console.log('点击了确定按钮');
// 可以在这里执行确认操作
},
onCancel: () => {
console.log('点击了取消按钮');
},
});
};
// 自定义按钮示例
const showCustomizedModal = () => {
const modal = showModal({
title: '自定义按钮',
content: '这个弹窗使用了自定义的底部按钮。',
size: 'sm',
footer: () => {
// 创建自定义底部
const footer = document.createElement('div');
footer.className = 'custom-footer';
footer.style.display = 'flex';
footer.style.justifyContent = 'flex-end';
footer.style.gap = '8px';
footer.style.width = '100px';
// 创建删除按钮
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '删除';
deleteBtn.className = 'Button Button--default';
deleteBtn.style.backgroundColor = 'var(--color-danger)';
deleteBtn.style.color = 'white';
deleteBtn.style.padding = '4px 8px';
deleteBtn.style.borderRadius = '4px';
deleteBtn.onclick = () => {
alert('删除操作执行');
modal.close();
};
// 创建取消按钮
const cancelBtn = document.createElement('button');
cancelBtn.textContent = '取消';
cancelBtn.className = 'Button Button--default';
cancelBtn.onclick = () => {
modal.close();
};
footer.appendChild(cancelBtn);
footer.appendChild(deleteBtn);
return footer;
},
onClose: () => {
console.log('弹窗已关闭');
},
});
};
// 异步关闭示例
const showAsyncModal = () => {
let modal;
modal = showModal({
title: '异步关闭',
content: '点击确定后将模拟异步操作,操作完成后自动关闭弹窗。',
size: 'md',
onOk: () => {
// 禁用确定按钮,防止重复点击
modal.options.confirmLoading = true;
// 模拟异步操作
setTimeout(() => {
console.log('异步操作完成');
modal.close();
}, 2000);
// 返回 false 阻止自动关闭
return false;
},
onCancel: () => {
// 如果有正在进行的异步操作,可以在这里处理
console.log('取消操作');
},
});
};
// 多弹窗嵌套示例
const showMultipleModal = () => {
const outerModal = showModal({
title: '外层弹窗',
content: '这是一个外层弹窗,点击按钮可以打开内层弹窗。',
size: 'lg',
footer: () => {
const footer = document.createElement('div');
footer.className = 'custom-footer';
footer.style.display = 'flex';
footer.style.justifyContent = 'flex-end';
footer.style.gap = '8px';
// 创建打开内层弹窗按钮
const openInnerBtn = document.createElement('button');
openInnerBtn.textContent = '打开内层弹窗';
openInnerBtn.className = 'z-button z-button--primary';
openInnerBtn.onclick = () => {
console.log('点击了打开内层弹窗按钮');
// 打开内层弹窗
const innerModal = showModal({
title: '内层弹窗',
content: '这是一个嵌套在内层的弹窗。',
size: 'md',
onOk: () => {
console.log('内层弹窗确定');
},
onCancel: () => {
console.log('内层弹窗取消');
},
});
console.log('内层弹窗已创建:', innerModal);
};
// 创建关闭按钮
const closeBtn = document.createElement('button');
closeBtn.textContent = '关闭';
closeBtn.className = 'z-button z-button--default';
closeBtn.onclick = () => {
outerModal.close();
};
footer.appendChild(closeBtn);
footer.appendChild(openInnerBtn);
return footer;
},
onClose: () => {
console.log('外层弹窗已关闭');
},
});
};
// 无遮罩弹窗示例
const showNoMaskModal = () => {
showModal({
title: '无遮罩弹窗',
content: '这是一个没有遮罩层的弹窗,点击页面其他区域不会关闭。',
size: 'md',
mask: false,
onOk: () => {
console.log('点击了确定按钮');
},
});
};
// 自定义遮罩弹窗示例
const showCustomMaskModal = () => {
showModal({
title: '自定义遮罩弹窗',
content: '这个弹窗使用了自定义的遮罩样式,背景为半透明绿色并带有模糊效果。',
size: 'md',
mask: true,
maskStyle: {
backgroundColor: 'rgba(0, 255, 0, 0.3)',
backdropFilter: 'blur(5px)',
},
onOk: () => {
console.log('点击了确定按钮');
},
});
};
</script>
<style scoped>
.modal-demo {
padding: 20px;
background-color: var(--bg-card);
border-radius: var(--radius-md);
}
.demo-header {
margin-bottom: 24px;
}
.demo-header h2 {
margin: 0 0 8px 0;
font-size: var(--font-size-xl);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
}
.demo-header p {
margin: 0;
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.demo-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.button-group {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.info-box {
padding: 16px;
background-color: var(--bg-secondary);
border-radius: var(--radius-md);
border-left: 4px solid var(--color-primary);
}
.info-box h3 {
margin: 0 0 12px 0;
font-size: var(--font-size-base);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
}
.info-box p {
margin: 8px 0;
line-height: 1.6;
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.info-box code {
background-color: var(--bg-muted);
padding: 2px 6px;
border-radius: var(--radius-sm);
font-family: var(--font-family-mono);
font-size: var(--font-size-xs);
color: var(--text-primary);
}
</style>Modal Props
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| id | string | - | 弹窗的唯一标识符 |
| open | boolean | - | 控制弹窗的显示和隐藏 |
| defaultOpen | boolean | false | 弹窗的默认显示状态 |
| title | string | - | 弹窗标题 |
| content | string | HTMLElement | (() => HTMLElement) | - | 弹窗内容(函数式API时使用) |
| size | 'sm' | 'md' | 'lg' | 'xl' | 'fullscreen' | 'md' | 弹窗尺寸 |
| position | 'center' | 'top' | 'bottom' | 'left' | 'right' | 'absolute' | 'center' | 弹窗位置,设置为 'absolute' 时启用自定义位置 |
| closable | boolean | true | 是否显示关闭按钮 |
| mask | boolean | true | 是否显示遮罩层 |
| maskClosable | boolean | true | 点击遮罩层是否可以关闭弹窗 |
| escClosable | boolean | true | 按 ESC 键是否可以关闭弹窗 |
| footer | boolean | HTMLElement | (() => HTMLElement) | true | 底部按钮配置 |
| width | string | number | - | 自定义宽度 |
| height | string | number | - | 自定义高度 |
| top | string | number | - | 自定义顶部定位(支持像素值和百分比值),需配合 position="absolute" 使用 |
| right | string | number | - | 自定义右侧定位(支持像素值和百分比值),需配合 position="absolute" 使用 |
| bottom | string | number | - | 自定义底部定位(支持像素值和百分比值),需配合 position="absolute" 使用 |
| left | string | number | - | 自定义左侧定位(支持像素值和百分比值),需配合 position="absolute" 使用 |
| transitionDuration | number | 300 | 弹窗动画时长(毫秒) |
| maskTransitionDuration | number | 200 | 遮罩层动画时长(毫秒) |
| animation | 'zoom' | 'slide' | 'fade' | 'bounce' | 'zoom' | 弹窗动画类型 |
| contentStyle | Record<string, string | number> | {} | 自定义弹窗样式 |
| maskStyle | Record<string, string | number> | {} | 自定义遮罩样式 |
事件
| 事件名 | 参数 | 说明 |
|---|---|---|
| open | value: boolean | 弹窗打开时触发 |
| close | value: boolean | 弹窗关闭时触发 |
| ok | - | 点击确定按钮时触发 |
| cancel | - | 点击取消按钮或关闭弹窗时触发 |
插槽
| 插槽名 | 说明 |
|---|---|
| default | 弹窗内容 |
| footer | 底部自定义内容 |
函数式 API
Modal 组件提供了 showModal 函数,可以通过编程方式打开弹窗:
showModal(options)
参数:
options:ModalOptions类型,包含以下属性:title:string- 弹窗标题content:string \| HTMLElement \| (() => HTMLElement)- 弹窗内容size:ModalSize- 弹窗尺寸position:ModalPosition- 弹窗位置animation:'zoom' \| 'slide' \| 'fade' \| 'bounce'- 弹窗动画类型closable:boolean- 是否显示关闭按钮mask:boolean- 是否显示遮罩层maskClosable:boolean- 点击遮罩是否可以关闭escClosable:boolean- 按 ESC 键是否可以关闭footer:boolean \| HTMLElement \| (() => HTMLElement)- 底部配置contentStyle:Record<string, string \| number>- 自定义弹窗样式maskStyle:Record<string, string \| number>- 自定义遮罩样式onOk:() => void- 确定按钮回调onCancel:() => void- 取消按钮回调onClose:() => void- 关闭回调
返回值:
- 包含
close()方法的对象,用于手动关闭弹窗
使用示例
javascript
import { showModal } from '@/components/Modal';
// 基础用法
const modal = showModal({
title: '确认操作',
content: '确定要执行此操作吗?',
size: 'sm',
onOk: () => {
console.log('用户确认操作');
},
onCancel: () => {
console.log('用户取消操作');
},
});
// 自定义遮罩样式示例
showModal({
title: '自定义遮罩',
content: '这个弹窗使用了自定义的遮罩样式',
mask: true,
maskStyle: {
backgroundColor: 'rgba(0, 255, 0, 0.3)',
backdropFilter: 'blur(5px)',
},
});
// 无遮罩示例
showModal({
title: '无遮罩弹窗',
content: '这个弹窗没有遮罩层',
mask: false,
});
// 手动关闭
// modal.close();响应式设计
Modal 组件在移动设备上会自动调整大小和位置,确保良好的用户体验。对于侧边弹窗(left/right),在小屏幕上会自动调整为全屏模式。
无障碍支持
Modal 组件支持无障碍功能,包括 ARIA 属性和键盘导航。默认情况下,弹窗会自动管理焦点,并支持通过 ESC 键关闭。
暗黑模式
Modal 组件完全支持暗黑模式,会根据当前主题自动调整样式。