在开发管理后台时,我们常遇到需要快速调用确认框的场景。Element Plus默认的ElDialog采用声明式写法,需要维护v-model状态,而Arco Design Vue的命令式调用方式(如Modal.open())明显更高效
由于公司的项目是Element Plus,在开发过程中,为了提高开发效率和维护,封装了一个仿Arco Design Vue的Modal命令弹窗,实现函数调用
Arco Design Vue的Modal命令式弹窗个人觉得设计很不错,下面展示下Arco组件库Modal的一些使用示例:
虽然目前Element Plus也可以使用ElMessageBox来实现命令式弹窗,但是无法自定义底部插槽,也无法全屏等等,最好办法是基于el-dialog实现命令式弹窗
下面是基于Element Plus的el-dialog封装的命令式弹窗组件
index.vue
Dialog.tsx
使用示例
演示效果
源码 index.vue
<template>
<el-dialog v-bind="dialogProps" :fullscreen="fullscreen" v-model="visible">
<template #header>
<el-row>
<div class="el-dialog__title cecw-dialog__title">
<slot name="title">{{ props.title }}</slot>
</div>
<el-space size="default">
<button type="button" class="el-dialog__headerbtn" @click="fullscreen = !fullscreen">
<el-icon class="el-dialog__close"><FullScreen /></el-icon>
</button>
<button type="button" class="el-dialog__headerbtn" @click="visible = false">
<el-icon class="el-dialog__close"><Close /></el-icon>
</button>
</el-space>
</el-row>
</template>
<slot>
<template v-if="typeof props.content === 'string'">
<p>{{ props.content }}</p>
</template>
<template v-if="typeof props.content === 'function'">
<component :is="props?.content?.()"></component>
</template>
</slot>
<template v-if="props.footer" #footer>
<slot name="footer">
<template v-if="typeof props.footer === 'boolean'">
<el-button v-bind="props.cancelButtonProps" @click="handleCancel">{{ props.cancelText }}</el-button>
<el-button type="primary" v-bind="props.okButtonProps" :loading="okLoading" @click="handleOk">{{
props.okText
}}</el-button>
</template>
<template v-else>
<component :is="props.footer()"></component>
</template>
</slot>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { defineProps, defineSlots, computed, ref, type VNode } from 'vue';
import type { DialogProps, ButtonProps } from 'element-plus';
import { Close, FullScreen } from '@element-plus/icons-vue';
defineOptions({ name: 'CecwDialog' });
interface Props extends Partial<DialogProps> {
content?: string | (() => VNode);
footer?: boolean | (() => VNode);
okText?: string;
cancelText?: string;
okButtonProps?: Partial<ButtonProps>;
cancelButtonProps?: Partial<ButtonProps>;
onOk?: () => void;
onBeforeOk?: () => Promise<boolean>;
onCancel?: () => void;
}
const props = withDefaults(defineProps<Props>(), {
closeOnClickModal: true,
footer: true,
okText: '确认',
cancelText: '取消'
});
defineSlots<{
title: () => VNode;
footer: () => VNode;
default: () => VNode;
}>();
const visible = defineModel('modelValue', {
type: Boolean,
default: false
});
const dialogProps = computed(() => {
return {
...props,
content: undefined,
footer: undefined,
okText: undefined,
cancelText: undefined,
okButtonProps: undefined,
cancelButtonProps: undefined,
onOk: undefined,
onBeforeOk: undefined,
onCancel: undefined
};
});
const okLoading = ref(false);
const fullscreen = ref(false);
const handleCancel = () => {
props.onCancel?.();
visible.value = false;
};
const handleOk = async () => {
if (props.onBeforeOk) {
try {
okLoading.value = true;
const flag = await props.onBeforeOk();
if (flag) {
okLoading.value = false;
visible.value = false;
}
} catch (error) {
okLoading.value = false;
}
} else {
props.onOk?.();
visible.value = false;
}
};
</script>
<style lang="scss" scoped>
:deep(.el-dialog__headerbtn) {
position: static;
width: auto;
height: auto;
}
.cecw-dialog__title {
flex: 1;
}
</style>
Dialog.tsx
import { createApp, h, ref, AppContext } from 'vue';
import type { DialogInstance as CecwDialogInstance } from './index';
import ElementPlus from 'element-plus';
import CecwDialog from './index.vue';
type DialogOptions = Partial<CecwDialogInstance['$props']>;
export interface DialogInstance {
close: () => void;
update: (newProps?: Record<string, any>) => void;
}
const defaultOptions: DialogOptions = {
width: '600px',
center: false,
footer: true,
closeOnClickModal: true
};
export function createDialog(appContext?: AppContext) {
const dialog = {
// 核心创建方法
create(options: DialogOptions): DialogInstance {
const mergedOptions = { ...defaultOptions, ...options };
let context = null;
// 创建容器
const container = document.createElement('div');
document.body.appendChild(container);
// 状态管理
const visible = ref(true);
const dialogOptions = ref(mergedOptions || {});
// 创建弹窗应用
const dialogApp = createApp({
setup() {
// context = appContext || getCurrentInstance()?.appContext;
// console.log('getCurrentInstance', getCurrentInstance());
// 关闭处理
const closed = () => {
dialogApp.unmount();
container.remove();
};
return () =>
h(CecwDialog, {
...dialogOptions.value,
modelValue: visible.value,
'onUpdate:modelValue': (val: boolean) => (visible.value = val),
onClosed: () => closed()
});
}
});
dialogApp.use(ElementPlus);
// 继承上下文
if (context) {
dialogApp._context = Object.assign({}, context);
// dialogApp.config.globalProperties = context.config.globalProperties;
}
// 挂载
dialogApp.mount(container);
return {
/** 关闭对话框 */
close: () => {
visible.value = false;
setTimeout(() => {
dialogApp.unmount();
container.remove();
}, 300);
},
/** 更新对话框 */
update: (newProps?: Record<string, any>) => {
dialogOptions.value = { ...dialogOptions.value, ...newProps };
}
};
},
/** 对话框-打开 */
open(options: DialogOptions) {
return this.create(options);
}
};
return dialog;
}
// 默认导出实例
export const Dialog = createDialog();