实现效果

el-dialog进行封装,便于其他组件进行简单调用,同时简化代码

useDialogDemo221211

封装Dialog

过程

主要是参考了 Vue3这样子结合hook写弹窗组件更快更高效 这篇文章里面的内容,但是我的项目里面的脚本使用的是JavaScript,参考文章里面用的是typescript,因此进行了一些~~翻译(不是)~~修改。

首先记录一下一些踩坑实况(

问题

点击触发弹窗的按钮后,弹窗并没有弹出来

排查

修改一下openDialog,使其修改visible状态后在控制台log一下,发现状态是成功修改的,但是弹窗确实没有弹出来。

同时能够发现,浏览器的滚动条在点击按钮之前是有的,点击按钮后没有了,虽然也不知道为什么。(此处应有图)

于是我再整一个按钮,调用closeDialog,随便点点试试。

image-20240128115310307

然后发现,点一次open,再点一次close,浏览器的滚动条又出现了,状态也是对应的修改了true/false

然后重点来了!!!!

此时再点一次open,弹窗出来了。

ca0a96d9193d9a42122b662c7ef3eb4f9a8c190f880ed93d41a3a052da888bcd

然后我将openDialog改为先把visible改true,再false,再改true~~(搁这搁这呢~~

发现还是不行

此时我再翻了一下控制台,发现其实是有报错的~~,只是我之前都忽略了(~~

image-20240128120524588

这里其实就是我初始化的时候用了defineProps定义了hiddenFullBtn,后面直接就调用了,显示没有定义,到底怎么会事呢。

请看VCR:[学习Vue3的defineProps方法](https://zhuanlan.zhihu.com/p/680340457#:~:text=defineProps 方法是在 环境下使用的,这是Vue 3中推荐的语法糖,可以更紧凑地书写组件。 以下是 defineProps,<%2Fscript> 在上述代码中,我们通过 defineProps 定义了两个Props属性: prop1 和 prop2 。)

defineProps其实是有返回值的,我这里直接参考其他组件的写法,没有拿到返回值,因此这里的hiddenFullBtn会报错为没有定义。

改了一下,OK了~

附上代码

CommonDialog.vue

<script setup>
import { computed, ref, useAttrs, useSlots } from "vue";
import { FullScreen, Close } from "@element-plus/icons-vue";
const attrs = useAttrs();
const slots = useSlots();
const props=defineProps({
title:{
type:String
},
isDraggable:{
type:Boolean,
default:false
},
modelValue:{
type:Boolean,
default:false
},
hiddenFullBtn:{
type:Boolean,
default:false
},
loading:{
type:Boolean,
default:false
},
confirmText:{
type:String,
default:"确认"
},
cancelText:{
type:String,
default:"关闭"
},
})

const isFullscreen = ref(false);
// 是否显示全屏效果图标
const isFullScreenBtn = computed(() => {
if (props.hiddenFullBtn) return false;
return !attrs?.fullscreen;

});

const emits=defineEmits(["update:modelValue","confirm","close"])

// 开启、关闭全屏效果
const handleFullscreen = () => {
if (attrs?.fullscreen) return;
isFullscreen.value = !isFullscreen.value;
};
// 关闭弹窗时向外部发送close事件
const handleClose = () => {
// 获取attrs.['before-close']属性,如果类型是函数函数,先执行它
if (
Reflect.has(attrs, "before-close") &&
typeof attrs["before-close"] === "function"
) {
attrs["before-close"]();
}
emits("close");
};
const handleConfirm = () => {
emits("confirm");
};
</script>

<template>
<div class="">
<el-dialog
v-bind="attrs"
:model-value="props.modelValue"
:show-close="false"
:fullscreen="attrs?.fullscreen ?? isFullscreen"
:before-close="handleClose"
>
<template #header>
<div>
<span class="dialog-title">{{ props.title }}</span>
</div>
<div class="btns">
<el-icon v-if="isFullScreenBtn" @click="handleFullscreen"
><FullScreen
/></el-icon>
<el-icon @click="handleClose"><Close /></el-icon>
</div>
</template>
<div class="content" v-loading="props.loading">
<slot></slot>
</div>

<template #footer>
<!-- 如果没有提供其他footer插槽,就使用默认的 -->
<span v-if="!slots.footer" class="dialog-footer">
<el-button type="primary" @click="handleConfirm">{{
props.confirmText
}}</el-button>
<el-button @click="handleClose">{{ props.cancelText }}</el-button>
</span>
<!-- 使用传入进来的插槽 -->
<slot v-else name="footer"></slot>
</template>
</el-dialog>
</div>
</template>

<style lang="less" scoped>
:deep(.el-dialog__header) {
border-bottom: 1px solid #eee;
display: flex;
padding: 12px 16px;
align-items: center;
justify-content: space-between;
margin: 0;
}
.dialog-title {
line-height: 24px;
font-size: 18px;
color: #303133;
}
.btns {
display: flex;
align-items: center;
i {
margin-right: 8px;

font-size: 16px;
cursor: pointer;
}
i:last-child {
margin-right: 0;
}
}
</style>

useDialog.js

import {ref} from "vue";

export const useDialog=()=>{
const visible = ref(false);
const loading = ref(false);
const openDialog=()=>{
visible.value=true
}
const closeDialog = () => (visible.value = false);
const openLoading = () => (loading.value = true);
const closeLoading = () => (loading.value = false);
return {
visible,
loading,
openDialog,
closeDialog,
openLoading,
closeLoading
}
}

使用Dialog

script

import {useDialog} from "@/composables/useDialog.js";
import CommonDialog from "@/components/dialog/CommonDialog.vue";
import {ElMessage} from "element-plus";

const {
visible,
openDialog,
closeDialog,
}=useDialog()

const handleConfirm = () => {
ElMessage({
message: "你点击了确定按钮",
type: "success",
});
closeDialog();
};

const handleClose = () => {
ElMessage({
message: "关闭了窗口",
type: "info",
});
closeDialog();
};

template

<el-button @click="openDialog">普通弹窗</el-button>
<CommonDialog title="test111" :hidden-full-btn="true" v-model="visible"
@confirm="handleConfirm" @close="handleClose"></CommonDialog>

二次封装FormDialog

首先这里创建一个用于区分表格是添加/编辑/只读状态的js

dialogTypes.js

export const MODE = {
ADD:'add',
EDIT:'edit',
READONLY:'readonly',
}

然后创建一个管理表格弹窗状态的js以及useFormDialog.js

useDialogState.js

import {ref} from "vue";
import {MODE} from "./dialogTypes.js"

export const useDialogState=()=>{
const mode=ref(MODE.ADD)
const visible=ref(false)
const updateMode=(target)=>{
mode.value=target
}
return{
mode,
visible,
updateMode
}
}

useFormDialog.js

import {useDialogState} from "@/composables/useDialogState.js";

export const useFormDialog=(formInstance)=>{
const { visible, mode, updateMode } = useDialogState();

const closeDialog = () => {
formInstance.value?.resetFields();
visible.value = false;
};
const openDialog = (target) => {
updateMode(target);
visible.value = true;
formInstance.value?.resetFields();
};

const modeText = () => {
if (mode.value === 'add') return '添加';
if (mode.value === 'edit') return '编辑';
if (mode.value === 'readonly') return '详情';
}

return{
visible, mode, openDialog, closeDialog, modeText
}
}

其中useFormDialog传入的是表格对象,在关闭和打开弹窗时都要清空一下表格的内容。

FormDialog.vue

script

// 省略表格本身的定义等等
const { visible, mode, closeDialog, openDialog } = useFormDialog(formRef)

defineExpose({
openDialog
})
const confirm = () => {
if (!formRef.value) return;
formRef.value.validate((valid) => {
if (valid) {
ElMessage({
message: "提交成功",
type: "success",
})
console.log(form)
closeDialog()
}
})
}

const customClose = () => {
closeDialog()
}

这里使用了defineExpose,将useFormDialog中的openDialog导出供父组件使用。

template

<CommonDialog
:before-close="customClose"
@confirm="confirm"
v-model="visible"
title="标题标题"
:confirm-text="mode === MODE.ADD ? '添加' : '修改'"
>
<div class="addressWrapper">
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="100px"
status-icon>
<!-- 此处省略表格内容 -->
</el-form>
</div>
</CommonDialog>

使用FormDialog

<script setup>
import AddressFormDialog from "@/components/dialog/AddressFormDialog.vue";
import {MODE} from "@/composables/dialogTypes.js";
import {ref} from "vue";

const formDialogRef = ref(AddressFormDialog)

const openDialog=(mode)=>{
if (!formDialogRef.value) return
formDialogRef.value.openDialog(mode)
}
</script>

<template>
<el-button @click="openDialog(MODE.EDIT)">普通弹窗</el-button>
<AddressFormDialog ref="formDialogRef"/>
</template>

大功告成!

image-20240130155212539

参考链接

Vue3这样子结合hook写弹窗组件更快更高效 - 掘金 (juejin.cn)

[学习Vue3的defineProps方法 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/680340457#:~:text=defineProps 方法是在 环境下使用的,这是Vue 3中推荐的语法糖,可以更紧凑地书写组件。 以下是 defineProps,<%2Fscript> 在上述代码中,我们通过 defineProps 定义了两个Props属性: prop1 和 prop2 。)

关于Vue3的defineProps用法 - 掘金 (juejin.cn)