Vue2对话框(Dialog)
可自定义设置以下属性:
• 标题(title),类型:string | slot,默认 '提示'
• 内容(content),类型:string | slot,默认 ''
• 宽度(width),类型:number,单位px,默认 540px
• 高度(height),类型:number|string,默认 'auto',自适应内容高度
• 取消按钮文字(cancelText),类型:string,默认 '取消'
• 确定按钮文字(okText),类型:string,默认 '确认'
• 确定按钮类型(okType),类型:'primary' | 'danger',默认 'primary'
• 是否显示底部按钮(footer),类型:boolean | slot,默认 true
• 是否水平垂直居中(center),类型:boolean,默认 true,(false时是固定高度水平居中)
• 固定高度水平居中时,距顶部高度(top),仅当 center: false 时生效,单位px,类型:number,默认 100
• 是否允许切换全屏(switchFullscreen),允许后右上角会出现一个按钮,类型:boolean,默认 false
• 确定按钮加载中(loading),类型:boolean,默认 false
• 对话框 body 样式(bodyStyle),类型:CSSProperties,默认 {}
• 对话框是否可见(show),类型:boolean,默认 false
效果如下图:
①创建对话框组件Dialog.vue:
<script setup lang="ts">
import Button from '../button'
import { ref, computed, useSlots, watch, nextTick } from 'vue'
import type { CSSProperties } from 'vue'
interface Props {
title?: string // 标题 string | slot
content?: string // 内容 string | slot
width?: number // 宽度,单位p
height?: number | string // 高度,单位px,默认auto,自适应内容高度
cancelText?: string // 取消按钮文字
okText?: string // 确定按钮文字
okType?: 'primary' | 'danger' // 确定按钮类型
footer?: boolean // 是否显示底部按钮 boolean | slot
center?: boolean // 水平垂直居中:true 固定高度水平居中:false
top?: number // 固定高度水平居中时,距顶部高度,仅当 center: false 时生效
switchFullscreen?: boolean // 是否允许切换全屏,允许后右上角会出现一个按钮
loading?: boolean // 确定按钮 loading
bodyStyle?: CSSProperties // 对话框 body 样式
show?: boolean // 对话框是否可见
}
const props = withDefaults(defineProps<Props>(), {
title: '提示',
content: '',
width: 540,
height: 'auto',
cancelText: '取消',
okText: '确定',
okType: 'primary',
footer: true,
center: true,
top: 100,
switchFullscreen: false,
loading: false,
bodyStyle: () => ({}),
show: false
})
const fullScreen = ref(false)
const dialogHeight = computed(() => {
if (typeof props.height === 'number') {
return props.height + 'px'
} else {
return props.height
}
})
const dialogRef = ref() // DOM 引用
const slots = useSlots()
const showFooter = computed(() => {
const footerSlots = slots.footer?.()
return footerSlots
})
watch(
() => props.show,
(to) => {
if (to) {
nextTick(() => {
dialogRef.value.focus()
})
// 重置全屏显示
fullScreen.value = false
}
}
)
const emits = defineEmits(['update:show', 'cancel', 'ok'])
function onBlur() {
emits('update:show', false)
emits('cancel')
}
function onFullScreen() {
fullScreen.value = !fullScreen.value
}
function onClose() {
emits('update:show', false)
emits('cancel')
}
function onCancel() {
emits('update:show', false)
emits('cancel')
}
function onOk() {
emits('ok')
}
</script>
<template>
<div class="m-dialog-root">
<Transition name="fade">
<div v-show="show" class="m-dialog-mask"></div>
</Transition>
<Transition name="zoom">
<div v-show="show" class="m-dialog-wrap" @click.self="onBlur">
<div
ref="dialogRef"
tabindex="-1"
:class="['m-dialog', center ? 'relative-hv-center' : 'top-center']"
:style="`width: ${fullScreen ? '100%' : props.width + 'px'}; top: ${center ? '50%' : fullScreen ? 0 : top + 'px'};`"
@keydown.esc="onClose"
>
<div class="m-dialog-content" :style="`--height: ${fullScreen ? '100vh' : dialogHeight}`">
<div class="m-dialog-header">
<p class="u-head">
<slot name="title">{{ title }}</slot>
</p>
</div>
<span class="m-screen" @click="onFullScreen" v-if="switchFullscreen">
<svg
v-show="!fullScreen"
class="u-svg"
viewBox="64 64 896 896"
data-icon="fullscreen"
aria-hidden="true"
focusable="false"
>
<path
d="M290 236.4l43.9-43.9a8.01 8.01 0 0 0-4.7-13.6L169 160c-5.1-.6-9.5 3.7-8.9 8.9L179 329.1c.8 6.6 8.9 9.4 13.6 4.7l43.7-43.7L370 423.7c3.1 3.1 8.2 3.1 11.3 0l42.4-42.3c3.1-3.1 3.1-8.2 0-11.3L290 236.4zm352.7 187.3c3.1 3.1 8.2 3.1 11.3 0l133.7-133.6 43.7 43.7a8.01 8.01 0 0 0 13.6-4.7L863.9 169c.6-5.1-3.7-9.5-8.9-8.9L694.8 179c-6.6.8-9.4 8.9-4.7 13.6l43.9 43.9L600.3 370a8.03 8.03 0 0 0 0 11.3l42.4 42.4zM845 694.9c-.8-6.6-8.9-9.4-13.6-4.7l-43.7 43.7L654 600.3a8.03 8.03 0 0 0-11.3 0l-42.4 42.3a8.03 8.03 0 0 0 0 11.3L734 787.6l-43.9 43.9a8.01 8.01 0 0 0 4.7 13.6L855 864c5.1.6 9.5-3.7 8.9-8.9L845 694.9zm-463.7-94.6a8.03 8.03 0 0 0-11.3 0L236.3 733.9l-43.7-43.7a8.01 8.01 0 0 0-13.6 4.7L160.1 855c-.6 5.1 3.7 9.5 8.9 8.9L329.2 845c6.6-.8 9.4-8.9 4.7-13.6L290 787.6 423.7 654c3.1-3.1 3.1-8.2 0-11.3l-42.4-42.4z"
></path>
</svg>
<svg
v-show="fullScreen"
class="u-svg"
viewBox="64 64 896 896"
data-icon="fullscreen-exit"
aria-hidden="true"
focusable="false"
>
<path
d="M391 240.9c-.8-6.6-8.9-9.4-13.6-4.7l-43.7 43.7L200 146.3a8.03 8.03 0 0 0-11.3 0l-42.4 42.3a8.03 8.03 0 0 0 0 11.3L280 333.6l-43.9 43.9a8.01 8.01 0 0 0 4.7 13.6L401 410c5.1.6 9.5-3.7 8.9-8.9L391 240.9zm10.1 373.2L240.8 633c-6.6.8-9.4 8.9-4.7 13.6l43.9 43.9L146.3 824a8.03 8.03 0 0 0 0 11.3l42.4 42.3c3.1 3.1 8.2 3.1 11.3 0L333.7 744l43.7 43.7A8.01 8.01 0 0 0 391 783l18.9-160.1c.6-5.1-3.7-9.4-8.8-8.8zm221.8-204.2L783.2 391c6.6-.8 9.4-8.9 4.7-13.6L744 333.6 877.7 200c3.1-3.1 3.1-8.2 0-11.3l-42.4-42.3a8.03 8.03 0 0 0-11.3 0L690.3 279.9l-43.7-43.7a8.01 8.01 0 0 0-13.6 4.7L614.1 401c-.6 5.2 3.7 9.5 8.8 8.9zM744 690.4l43.9-43.9a8.01 8.01 0 0 0-4.7-13.6L623 614c-5.1-.6-9.5 3.7-8.9 8.9L633 783.1c.8 6.6 8.9 9.4 13.6 4.7l43.7-43.7L824 877.7c3.1 3.1 8.2 3.1 11.3 0l42.4-42.3c3.1-3.1 3.1-8.2 0-11.3L744 690.4z"
></path>
</svg>
</span>
<span class="m-close" @click="onClose">
<svg class="u-svg" viewBox="64 64 896 896" data-icon="close" aria-hidden="true" focusable="false">
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 0 0 203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
></path>
</svg>
</span>
<div class="m-dialog-body" :style="bodyStyle">
<slot>{{ content }}</slot>
</div>
<div class="m-dialog-footer" v-show="footer">
<slot name="footer"></slot>
<template v-if="!showFooter">
<Button class="mr8" @click="onCancel">{{ cancelText }}</Button>
<Button :type="okType" :loading="loading" @click="onOk">{{ okText }}</Button>
</template>
</div>
</div>
</div>
</div>
</Transition>
</div>
</template>
<style lang="less" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s linear;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.zoom-enter-active {
transition: all 0.3s;
}
.zoom-leave-active {
transition: all 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
}
.zoom-enter-from,
.zoom-leave-to {
opacity: 0;
transform: scale(0.2);
}
.flex-hv-center {
// 水平垂直居中方法①:弹性布局,随内容增大高度,并自适应水平垂直居中
display: flex;
justify-content: center;
align-items: center;
}
.relative-hv-center {
// 水平垂直居中方法②:相对定位,随内容增大高度,并自适应水平垂直居中
position: relative;
top: 50%;
transform: translateY(-50%);
}
.top-center {
// 相对定位,固定高度,始终距离视图顶端100px
position: relative;
// top: 100px;
}
.m-dialog-mask {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
background: rgba(0, 0, 0, 0.45);
}
.m-dialog-wrap {
position: fixed;
top: 0;
inset-inline-end: 0;
bottom: 0;
inset-inline-start: 0;
overflow: auto;
outline: 0;
inset: 0;
z-index: 1010;
.m-dialog {
margin: 0 auto;
outline: none;
.m-dialog-content {
display: flex;
flex-direction: column;
height: var(--height);
position: relative;
background-color: #fff;
border-radius: 8px;
box-shadow:
0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
padding: 20px 24px;
.m-dialog-header {
color: rgba(0, 0, 0, 0.88);
background: transparent;
border-radius: 8px 8px 0 0;
margin-bottom: 8px;
max-width: calc(100% - 54px);
.u-head {
margin: 0;
color: rgba(0, 0, 0, 0.88);
font-weight: 600;
font-size: 16px;
line-height: 1.5;
word-break: break-all;
}
}
.m-screen {
.m-close();
inset-inline-end: 48px;
}
.m-close {
position: absolute;
top: 17px;
inset-inline-end: 17px;
z-index: 1010;
font-weight: 600;
line-height: 1;
background: transparent;
border-radius: 4px;
width: 22px;
height: 22px;
cursor: pointer;
transition: background 0.2s;
display: flex;
align-items: center;
justify-content: center;
.u-svg {
display: inline-block;
width: 16px;
height: 16px;
line-height: 22px;
fill: rgba(0, 0, 0, 0.45);
cursor: pointer;
transition: fill 0.2s;
}
&:hover {
background: rgba(0, 0, 0, 0.06);
.u-svg {
fill: rgba(0, 0, 0, 0.88);
}
}
}
.m-dialog-body {
flex: 1;
font-size: 14px;
color: rgba(0, 0, 0, 0.88);
line-height: 1.5714285714285714;
word-break: break-all;
overflow: auto;
transition: all 0.25s;
}
.m-dialog-footer {
text-align: end;
background: transparent;
margin-top: 12px;
.mr8 {
margin-inline-end: 8px;
}
}
}
}
}
</style>
②在要使用的页面引入:
<script setup lang="ts">
import Dialog from './Dialog.vue'
import { ref } from 'vue'
const show1 = ref(false)
const show2 = ref(false)
const show3 = ref(false)
const show4 = ref(false)
const show5 = ref(false)
const show6 = ref(false)
const show7 = ref(false)
const show8 = ref(false)
const show9 = ref(false)
const loading = ref(false)
function onCancel() {
// 点击遮罩层或右上角叉或取消按钮的回调
console.log('cancel')
}
function onOk() {
// 点击确定回调
show1.value = false
show2.value = false
show3.value = false
show4.value = false
show5.value = false
show6.value = false
show7.value = false
show8.value = false
show9.value = false
}
function handleCancel() {
show4.value = false
}
function handleOk() {
loading.value = true
setTimeout(() => {
loading.value = false
show4.value = false
}, 2000)
}
function onLoadingOk() {
// 点击确定回调
loading.value = true // 开启加载状态
setTimeout(() => {
show8.value = false
loading.value = false
}, 2000)
}
</script>
<template>
<div>
<h1>{{ $route.name }} {{ $route.meta.title }}</h1>
<h2 class="mt30 mb10">基本使用</h2>
<Button type="primary" @click="show1 = true">Open Dialog</Button>
<Dialog v-model:show="show1" title="Title" @cancel="onCancel" @ok="onOk">
<p>Bla bla ...</p>
<p>Bla bla ...</p>
<p>Bla bla ...</p>
</Dialog>
<h2 class="mt30 mb10">自定义宽高</h2>
<Button type="primary" @click="show2 = true">Open Dialog</Button>
<Dialog v-model:show="show2" :width="480" :height="180" @ok="onOk">
<template #title>Title</template>
<p>Bla bla ...</p>
<p>Bla bla ...</p>
<p>Bla bla ...</p>
</Dialog>
<h2 class="mt30 mb10">自定义按钮文字 & 类型</h2>
<Button type="primary" @click="show3 = true">Open Dialog</Button>
<Dialog v-model:show="show3" title="Title" cancelText="cancel" okText="ok" okType="danger" @ok="onOk">
<p>Bla bla ...</p>
<p>Bla bla ...</p>
<p>Bla bla ...</p>
</Dialog>
<h2 class="mt30 mb10">自定义底部按钮</h2>
<Button type="primary" @click="show4 = true">Open Dialog</Button>
<Dialog v-model:show="show4" title="Title">
<p>Bla bla ...</p>
<p>Bla bla ...</p>
<p>Bla bla ...</p>
<template #footer>
<Button @click="handleCancel">Return</Button>
<Button type="primary" style="margin-left: 8px" :loading="loading" @click="handleOk">Submit</Button>
</template>
</Dialog>
<h2 class="mt30 mb10">隐藏底部按钮</h2>
<Button type="primary" @click="show5 = true">Open Dialog</Button>
<Dialog v-model:show="show5" title="Title" :footer="false" @ok="onOk">
<p>Bla bla ...</p>
<p>Bla bla ...</p>
<p>Bla bla ...</p>
</Dialog>
<h2 class="mt30 mb10">固定高度</h2>
<Button type="primary" @click="show6 = true">Open Dialog</Button>
<Dialog v-model:show="show6" title="Title" :center="false" :top="120" @ok="onOk">
<p>Bla bla ...</p>
<p>Bla bla ...</p>
<p>Bla bla ...</p>
</Dialog>
<h2 class="mt30 mb10">切换全屏</h2>
<Button type="primary" @click="show7 = true">Open Dialog</Button>
<Dialog v-model:show="show7" title="Title" switch-fullscreen @ok="onOk">
<p>Bla bla ...</p>
<p>Bla bla ...</p>
<p>Bla bla ...</p>
</Dialog>
<h2 class="mt30 mb10">提交 loading</h2>
<Button type="primary" @click="show8 = true">Open Dialog</Button>
<Dialog v-model:show="show8" title="Title" :loading="loading" @ok="onLoadingOk">
<p>Bla bla ...</p>
<p>Bla bla ...</p>
<p>Bla bla ...</p>
</Dialog>
<h2 class="mt30 mb10">body 样式自定义</h2>
<Button type="primary" @click="show9 = true">Open Dialog</Button>
<Dialog v-model:show="show9" title="Title" :body-style="{ fontSize: '20px', color: '#eb2f96' }" @ok="onOk">
<p>Bla bla ...</p>
<p>Bla bla ...</p>
<p>Bla bla ...</p>
</Dialog>
</div>
</template>