Appearance
封装命令式弹窗组件(v2/v3)
命令式弹窗组件封装时跟一般的组件封装不一样,在现有的UI组件库中已经很常见了,但是命令式组件封装起来还是有点难度的。
一般的组件:封装较容易,使用比较难;在使用时需要大量的代码,导致代码冗余
命令式组件:封装较难,使用比较容易;使用时需要简单的调用就行,方便高效
效果:
vue2版
编写组件的结构components/MessageBox.vue
弹窗组件
已折叠 点击查看组件代码
vue
<template>
<transition name="message-box-fade">
<div class="message-box-mask" v-if="visible" @click.self="handleMaskClick">
<transition name="message-box-bounce">
<div class="message-box" v-if="visible">
<div class="message-box-header">
<span class="title">{{ title }}</span>
<span class="close" @click="handleCancel">×</span>
</div>
<div class="message-box-content">
<div class="message-box-message">{{ message }}</div>
</div>
<div class="message-box-btns">
<button class="btn cancel" @click="handleCancel">{{ cancelButtonText }}</button>
<button class="btn confirm" :class="type" @click="handleConfirm">{{ confirmButtonText }}</button>
</div>
</div>
</transition>
</div>
</transition>
</template>
<script>
export default {
name: 'MessageBox',
data() {
return {
visible: false,
title: '',
message: '',
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'primary',
resolve: null,
reject: null
}
},
methods: {
handleConfirm() {
this.visible = false
this.resolve('confirm')
},
handleCancel() {
this.visible = false
this.reject('cancel')
},
handleMaskClick() {
this.handleCancel()
},
showMessage(message, title, options = {}) {
this.visible = true
this.message = message
this.title = title
if (options.confirmButtonText) {
this.confirmButtonText = options.confirmButtonText
}
if (options.cancelButtonText) {
this.cancelButtonText = options.cancelButtonText
}
if (options.type) {
this.type = options.type
}
return new Promise((resolve, reject) => {
this.resolve = resolve
this.reject = reject
})
}
}
}
</script>
<style scoped lang="less">
.message-box-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
}
.message-box {
background: #fff;
border-radius: 4px;
width: 420px;
padding: 20px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
}
.message-box-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.title {
font-size: 18px;
font-weight: 500;
color: #303133;
}
.close {
font-size: 20px;
color: #909399;
cursor: pointer;
&:hover {
color: #409EFF;
}
}
}
.message-box-content {
padding: 10px 0;
color: #606266;
font-size: 14px;
}
.message-box-btns {
display: flex;
justify-content: flex-end;
margin-top: 20px;
gap: 10px;
.btn {
padding: 8px 20px;
border-radius: 4px;
border: 1px solid #dcdfe6;
font-size: 14px;
cursor: pointer;
&.cancel {
background: #fff;
color: #606266;
&:hover {
color: #409EFF;
border-color: #c6e2ff;
background-color: #ecf5ff;
}
}
&.confirm {
color: #fff;
border-color: #409EFF;
background-color: #409EFF;
&:hover {
background: #66b1ff;
border-color: #66b1ff;
}
&.warning {
background-color: #E6A23C;
border-color: #E6A23C;
&:hover {
background: #ebb563;
border-color: #ebb563;
}
}
}
}
}
// 遮罩层淡入淡出
.message-box-fade-enter-active,
.message-box-fade-leave-active {
transition: opacity 0.3s;
}
.message-box-fade-enter,
.message-box-fade-leave-to {
opacity: 0;
}
// 弹窗弹跳效果
.message-box-bounce-enter-active {
animation: bounce-in 0.3s;
}
.message-box-bounce-leave-active {
animation: bounce-in 0.3s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0.8);
opacity: 0;
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
opacity: 1;
}
}
</style>
<template>
<transition name="message-box-fade">
<div class="message-box-mask" v-if="visible" @click.self="handleMaskClick">
<transition name="message-box-bounce">
<div class="message-box" v-if="visible">
<div class="message-box-header">
<span class="title">{{ title }}</span>
<span class="close" @click="handleCancel">×</span>
</div>
<div class="message-box-content">
<div class="message-box-message">{{ message }}</div>
</div>
<div class="message-box-btns">
<button class="btn cancel" @click="handleCancel">{{ cancelButtonText }}</button>
<button class="btn confirm" :class="type" @click="handleConfirm">{{ confirmButtonText }}</button>
</div>
</div>
</transition>
</div>
</transition>
</template>
<script>
export default {
name: 'MessageBox',
data() {
return {
visible: false,
title: '',
message: '',
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'primary',
resolve: null,
reject: null
}
},
methods: {
handleConfirm() {
this.visible = false
this.resolve('confirm')
},
handleCancel() {
this.visible = false
this.reject('cancel')
},
handleMaskClick() {
this.handleCancel()
},
showMessage(message, title, options = {}) {
this.visible = true
this.message = message
this.title = title
if (options.confirmButtonText) {
this.confirmButtonText = options.confirmButtonText
}
if (options.cancelButtonText) {
this.cancelButtonText = options.cancelButtonText
}
if (options.type) {
this.type = options.type
}
return new Promise((resolve, reject) => {
this.resolve = resolve
this.reject = reject
})
}
}
}
</script>
<style scoped lang="less">
.message-box-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
}
.message-box {
background: #fff;
border-radius: 4px;
width: 420px;
padding: 20px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
}
.message-box-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.title {
font-size: 18px;
font-weight: 500;
color: #303133;
}
.close {
font-size: 20px;
color: #909399;
cursor: pointer;
&:hover {
color: #409EFF;
}
}
}
.message-box-content {
padding: 10px 0;
color: #606266;
font-size: 14px;
}
.message-box-btns {
display: flex;
justify-content: flex-end;
margin-top: 20px;
gap: 10px;
.btn {
padding: 8px 20px;
border-radius: 4px;
border: 1px solid #dcdfe6;
font-size: 14px;
cursor: pointer;
&.cancel {
background: #fff;
color: #606266;
&:hover {
color: #409EFF;
border-color: #c6e2ff;
background-color: #ecf5ff;
}
}
&.confirm {
color: #fff;
border-color: #409EFF;
background-color: #409EFF;
&:hover {
background: #66b1ff;
border-color: #66b1ff;
}
&.warning {
background-color: #E6A23C;
border-color: #E6A23C;
&:hover {
background: #ebb563;
border-color: #ebb563;
}
}
}
}
}
// 遮罩层淡入淡出
.message-box-fade-enter-active,
.message-box-fade-leave-active {
transition: opacity 0.3s;
}
.message-box-fade-enter,
.message-box-fade-leave-to {
opacity: 0;
}
// 弹窗弹跳效果
.message-box-bounce-enter-active {
animation: bounce-in 0.3s;
}
.message-box-bounce-leave-active {
animation: bounce-in 0.3s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0.8);
opacity: 0;
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
opacity: 1;
}
}
</style>
plugins/messageBox.js
挂载组件
js
import Vue from 'vue'
import MessageBox from '../components/MessageBox.vue'
const MessageBoxConstructor = Vue.extend(MessageBox)
const messageBoxInstance = new MessageBoxConstructor()
messageBoxInstance.$mount(document.createElement('div'))
document.body.appendChild(messageBoxInstance.$el)
export default {
install(Vue) {
Vue.prototype.$confirm = (message, title, options) => {
return messageBoxInstance.showMessage(message, title, options)
}
}
}
import Vue from 'vue'
import MessageBox from '../components/MessageBox.vue'
const MessageBoxConstructor = Vue.extend(MessageBox)
const messageBoxInstance = new MessageBoxConstructor()
messageBoxInstance.$mount(document.createElement('div'))
document.body.appendChild(messageBoxInstance.$el)
export default {
install(Vue) {
Vue.prototype.$confirm = (message, title, options) => {
return messageBoxInstance.showMessage(message, title, options)
}
}
}
main.js
注册组件
js
import MessageBox from './plugins/messageBox'
Vue.use(MessageBox)
import MessageBox from './plugins/messageBox'
Vue.use(MessageBox)
调用命令式组件
vue
<template>
<div class="hello">
<button class="test-btn" @click="handleDelete">删除文件</button>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
},
methods: {
handleDelete() {
this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
alert("删除成功")
}).catch(() => {
alert("删除失败")
})
}
}
}
</script>
<style scoped lang="less">
.test-btn {
padding: 8px 20px;
color: #409EFF;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
background: #fff;
margin-top: 20px;
&:hover {
color: #66b1ff;
border-color: #c6e2ff;
background-color: #ecf5ff;
}
}
</style>
<template>
<div class="hello">
<button class="test-btn" @click="handleDelete">删除文件</button>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
},
methods: {
handleDelete() {
this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
alert("删除成功")
}).catch(() => {
alert("删除失败")
})
}
}
}
</script>
<style scoped lang="less">
.test-btn {
padding: 8px 20px;
color: #409EFF;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
background: #fff;
margin-top: 20px;
&:hover {
color: #66b1ff;
border-color: #c6e2ff;
background-color: #ecf5ff;
}
}
</style>
vue3版
编写组件的结构components/MessageBox.vue
弹窗组件
已折叠 点击查看组件代码
vue
<template>
<Transition name="fade" @after-leave="handleAfterLeave">
<div v-if="visible" class="message-box-overlay" @click.self="handleCancel">
<Transition name="zoom">
<div v-if="visible" class="message-box">
<div class="message-box-header">
<span class="title">{{ title }}</span>
<button class="close-btn" @click="handleCancel">×</button>
</div>
<div class="message-box-content">
{{ message }}
</div>
<div class="message-box-footer">
<button class="btn cancel" @click="handleCancel">{{ cancelText }}</button>
<button class="btn confirm" @click="handleConfirm">{{ confirmText }}</button>
</div>
</div>
</Transition>
</div>
</Transition>
</template>
<script setup>
import { ref } from 'vue'
const visible = ref(false)
const title = ref('')
const message = ref('')
const confirmText = ref('确定')
const cancelText = ref('取消')
let resolvePromise = null
let rejectPromise = null
const handleConfirm = () => {
visible.value = false
resolvePromise && resolvePromise(true)
}
const handleCancel = () => {
visible.value = false
rejectPromise && rejectPromise(false)
}
const confirm = (msg, ttl = '提示', config = {}) => {
visible.value = true
message.value = msg
title.value = ttl
if (config.confirmText) confirmText.value = config.confirmText
if (config.cancelText) cancelText.value = config.cancelText
return new Promise((resolve, reject) => {
resolvePromise = resolve
rejectPromise = reject
})
}
// 添加动画完成后的处理函数
const handleAfterLeave = () => {
// 可以在这里处理一些动画结束后的清理工作
message.value = ''
title.value = ''
confirmText.value = '确定'
cancelText.value = '取消'
}
defineExpose({
confirm,
})
</script>
<style scoped>
.message-box-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.message-box {
background: white;
border-radius: 8px;
width: 420px;
padding: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
position: relative;
}
.message-box-header {
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.title {
font-size: 18px;
font-weight: bold;
color: #303133;
}
.close-btn {
border: none;
background: none;
font-size: 20px;
color: #909399;
cursor: pointer;
padding: 0;
line-height: 1;
transition: color 0.3s;
}
.close-btn:hover {
color: #303133;
}
.message-box-content {
margin-bottom: 30px;
color: #606266;
font-size: 14px;
}
.message-box-footer {
text-align: right;
}
.btn {
padding: 8px 20px;
margin-left: 10px;
border-radius: 4px;
border: none;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.cancel {
background: #ffffff;
border: 1px solid #dcdfe6;
color: #606266;
}
.cancel:hover {
color: #409eff;
border-color: #c6e2ff;
background-color: #ecf5ff;
}
.confirm {
background: #409eff;
color: white;
border: 1px solid #409eff;
}
.confirm:hover {
background: #66b1ff;
border-color: #66b1ff;
}
/* 优化动画时序 */
.fade-enter-active {
transition: opacity 0.3s ease;
}
.fade-leave-active {
transition: opacity 0.2s ease;
}
.zoom-enter-active {
transition: all 0.3s cubic-bezier(0.4, 0, 0, 1.5);
}
.zoom-leave-active {
transition: all 0.2s ease;
}
</style>
<template>
<Transition name="fade" @after-leave="handleAfterLeave">
<div v-if="visible" class="message-box-overlay" @click.self="handleCancel">
<Transition name="zoom">
<div v-if="visible" class="message-box">
<div class="message-box-header">
<span class="title">{{ title }}</span>
<button class="close-btn" @click="handleCancel">×</button>
</div>
<div class="message-box-content">
{{ message }}
</div>
<div class="message-box-footer">
<button class="btn cancel" @click="handleCancel">{{ cancelText }}</button>
<button class="btn confirm" @click="handleConfirm">{{ confirmText }}</button>
</div>
</div>
</Transition>
</div>
</Transition>
</template>
<script setup>
import { ref } from 'vue'
const visible = ref(false)
const title = ref('')
const message = ref('')
const confirmText = ref('确定')
const cancelText = ref('取消')
let resolvePromise = null
let rejectPromise = null
const handleConfirm = () => {
visible.value = false
resolvePromise && resolvePromise(true)
}
const handleCancel = () => {
visible.value = false
rejectPromise && rejectPromise(false)
}
const confirm = (msg, ttl = '提示', config = {}) => {
visible.value = true
message.value = msg
title.value = ttl
if (config.confirmText) confirmText.value = config.confirmText
if (config.cancelText) cancelText.value = config.cancelText
return new Promise((resolve, reject) => {
resolvePromise = resolve
rejectPromise = reject
})
}
// 添加动画完成后的处理函数
const handleAfterLeave = () => {
// 可以在这里处理一些动画结束后的清理工作
message.value = ''
title.value = ''
confirmText.value = '确定'
cancelText.value = '取消'
}
defineExpose({
confirm,
})
</script>
<style scoped>
.message-box-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.message-box {
background: white;
border-radius: 8px;
width: 420px;
padding: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
position: relative;
}
.message-box-header {
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.title {
font-size: 18px;
font-weight: bold;
color: #303133;
}
.close-btn {
border: none;
background: none;
font-size: 20px;
color: #909399;
cursor: pointer;
padding: 0;
line-height: 1;
transition: color 0.3s;
}
.close-btn:hover {
color: #303133;
}
.message-box-content {
margin-bottom: 30px;
color: #606266;
font-size: 14px;
}
.message-box-footer {
text-align: right;
}
.btn {
padding: 8px 20px;
margin-left: 10px;
border-radius: 4px;
border: none;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.cancel {
background: #ffffff;
border: 1px solid #dcdfe6;
color: #606266;
}
.cancel:hover {
color: #409eff;
border-color: #c6e2ff;
background-color: #ecf5ff;
}
.confirm {
background: #409eff;
color: white;
border: 1px solid #409eff;
}
.confirm:hover {
background: #66b1ff;
border-color: #66b1ff;
}
/* 优化动画时序 */
.fade-enter-active {
transition: opacity 0.3s ease;
}
.fade-leave-active {
transition: opacity 0.2s ease;
}
.zoom-enter-active {
transition: all 0.3s cubic-bezier(0.4, 0, 0, 1.5);
}
.zoom-leave-active {
transition: all 0.2s ease;
}
</style>
plugins/messageBox.js
挂载组件
js
import { createApp } from 'vue'
import MessageBox from '../components/MessageBox.vue'
export const showMessageBox = (message, title, config = {}) => {
return new Promise((resolve, reject) => {
const mountNode = document.createElement('div')
document.body.appendChild(mountNode)
const app = createApp(MessageBox)
const instance = app.mount(mountNode)
instance
.confirm(message, title, config)
.then((res) => {
resolve(res)
app.unmount()
document.body.removeChild(mountNode)
})
.catch((err) => {
reject(err)
app.unmount()
document.body.removeChild(mountNode)
})
})
}
import { createApp } from 'vue'
import MessageBox from '../components/MessageBox.vue'
export const showMessageBox = (message, title, config = {}) => {
return new Promise((resolve, reject) => {
const mountNode = document.createElement('div')
document.body.appendChild(mountNode)
const app = createApp(MessageBox)
const instance = app.mount(mountNode)
instance
.confirm(message, title, config)
.then((res) => {
resolve(res)
app.unmount()
document.body.removeChild(mountNode)
})
.catch((err) => {
reject(err)
app.unmount()
document.body.removeChild(mountNode)
})
})
}
main.js
注册组件
js
import { showMessageBox } from './plugins/messageBox.js'
app.config.globalProperties.$messageBox = showMessageBox
import { showMessageBox } from './plugins/messageBox.js'
app.config.globalProperties.$messageBox = showMessageBox
页面调用命令式组件
vue
<template>
<div class="home">
<button @click="showConfirmDialog" class="custom-button">打开确认框</button>
</div>
</template>
<script setup>
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
const messageBox = instance.appContext.config.globalProperties.$messageBox
const showConfirmDialog = async () => {
messageBox('此操作将永久删除该文件, 是否继续?', '提示', {
confirmText: '确定',
cancelText: '取消',
}).then(() => {
alert('确定')
}).catch(() => {
alert('取消')
})
}
</script>
<style scoped>
.custom-button {
padding: 10px 20px;
background: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.custom-button:hover {
background: #66b1ff;
}
</style>
<template>
<div class="home">
<button @click="showConfirmDialog" class="custom-button">打开确认框</button>
</div>
</template>
<script setup>
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
const messageBox = instance.appContext.config.globalProperties.$messageBox
const showConfirmDialog = async () => {
messageBox('此操作将永久删除该文件, 是否继续?', '提示', {
confirmText: '确定',
cancelText: '取消',
}).then(() => {
alert('确定')
}).catch(() => {
alert('取消')
})
}
</script>
<style scoped>
.custom-button {
padding: 10px 20px;
background: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.custom-button:hover {
background: #66b1ff;
}
</style>
补一个vue3局部注册使用
直接页面调用命令式组件
vue
<template>
<div class="home">
<button @click="showConfirmDialog" class="custom-button">打开确认框</button>
</div>
</template>
<script setup>
import { showMessageBox } from '../utils/messageBox'
const showConfirmDialog = async () => {
showMessageBox('此操作将永久删除该文件, 是否继续?', '提示', {
confirmText: '确定',
cancelText: '取消',
}).then(() => {
alert('确定')
}).catch(() => {
alert('取消')
})
}
</script>
<style scoped>
.custom-button {
padding: 10px 20px;
background: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.custom-button:hover {
background: #66b1ff;
}
</style>
<template>
<div class="home">
<button @click="showConfirmDialog" class="custom-button">打开确认框</button>
</div>
</template>
<script setup>
import { showMessageBox } from '../utils/messageBox'
const showConfirmDialog = async () => {
showMessageBox('此操作将永久删除该文件, 是否继续?', '提示', {
confirmText: '确定',
cancelText: '取消',
}).then(() => {
alert('确定')
}).catch(() => {
alert('取消')
})
}
</script>
<style scoped>
.custom-button {
padding: 10px 20px;
background: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.custom-button:hover {
background: #66b1ff;
}
</style>