背景
在生产中我们经常会遇到一些基于UI库二次封装的场景,我认为二次封装对于老手来说没有什么难点,只不过是业务上的变化,但是对于新手或者其他框架的开发者,不免有些抓耳挠腮,我呢又恰巧有机会和时间,就留一些文章在这里供有需要的人互相参考和大家一起讨论。
需求描述
如上图所示,表格,抽屉,确认框,在用户意外关闭的时候进行提示,正常提交的时候不需要提示
实现步骤
第一,我们要整理关键线索,从element-plus文档中可以看到before-close
点击mask关闭时会触发,@close
弹框关闭时会触发,有了以上线索我们就可以进行二次封装了,那么为了实用方便,我们要尽量把二次封装做的像普通drawer使用。
下面跟着我的思路来实现一下
新建Drawer.vue文件
<script lang="ts" setup>
import { ElMessageBox } from 'element-plus'
// 是否显示弹框
const showModal = ref(false)
const props = defineProps({
// 控制是否提示
closeConfirm: propTypes.bool.def(false)
})
const emit = defineEmits(['update:modelValue'])
// 封装确认弹框
const showConfirm = async () => {
if (!props.closeConfirm) return true
try {
await ElMessageBox.confirm('确定要关闭吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
return true
} catch {
return false
}
}
/** 是否是关闭拦截 */
const isBeforeClose = ref(false)
// 关闭拦截
const handleBeforeClose = async (done: () => void) => {
const shouldClose = await showConfirm()
if (shouldClose) {
// 设置拦截标志
isBeforeClose.value = true
// 放开拦截
done()
// 关闭弹窗
handleShowModal(false)
}
}
// 点击Icon 关闭
const handleClose = async () => {
const shouldClose = await showConfirm()
if (shouldClose) {
// 设置拦截标志
isBeforeClose.value = true
// 关闭弹窗
handleShowModal(false)
}
}
// 监听 modelValue 的变化
watch(
() => props.modelValue,
async (newVal) => {
if (newVal) {
// 打开表格
handleShowModal(true)
return
}
// 当外部绑定变量设置关闭,并且拦截标志为false,则通过监听拦截
if (!newVal && !isBeforeClose.value) {
isBeforeClose.value = true
// 等待弹框验证
const shouldClose = await showConfirm()
// 验证为确定,模态框关闭
handleShowModal(!shouldClose)
}
// 每次状态变化都还原拦截默认值
isBeforeClose.value = false
}
)
// 打开弹窗方法,同步外部响应式变量
const handleShowModal = (value: boolean) => {
// 打开表格
showModal.value = value
// 同步外部变量
emit('update:modelValue', value)
}
</script>
<template>
<ElDrawer
:model-value="showModal"
:close-on-click-modal="true"
destroy-on-close
lock-scroll
:show-close="false"
:before-close="handleBeforeClose"
>
<template #header>
<div class="relative h-54px flex items-center justify-between pl-15px pr-15px">
<slot name="title">
{{ title }}
</slot>
<div
class="absolute right-15px top-[50%] h-54px flex translate-y-[-50%] items-center justify-between"
>
<Icon
class="is-hover cursor-pointer"
icon="ep:close"
hover-color="var(--el-color-primary)"
color="var(--el-color-info)"
@click="handleClose"
/>
</div>
</div>
</template>
<ElScrollbar v-if="scroll" :style="dialogStyle">
<slot></slot>
</ElScrollbar>
<slot v-else></slot>
<template v-if="slots.footer" #footer>
<slot name="footer"></slot>
</template>
</ElDrawer>
</template>
上面就完成了二次封装的组件,下面去使用
在使用之前,创建一个hooks组件来应付一般场景,创建useDrawer.ts文件
// 抽屉控制变量显示
export const useDrawerFlag = (title: string = "") => {
// 弹框标题
const dialogTitle = ref(title)
// 抽屉显示控制
const dialogVisible = ref(false)
// 关闭时是否检查确认框
const closeConfirm = ref(true)
// 确认抽屉关闭并取消提示
const ConfirmDrawerVisible = () =>{
closeConfirm.value = false
dialogVisible.value = false
// 异步还原弹框检测默认值
setTimeout(()=>{
closeConfirm.value = true
},500)
}
return {
dialogVisible,
dialogTitle,
closeConfirm,
ConfirmDrawerVisible
}
}
有了这个文件,我们就可以统一使用二次封装好的组件,和必要的参数方法,为什么封装了ConfirmDrawerVisible方法?
qaq: 因为我们的drawer里面可能会放表单,一般表单会需要验证,通过后直接关闭,不需要二次确认,所以有了这个方法的封装,那接下来看下使用的代码
使用代码
<template>
<Drawer :title="dialogTitle" v-model="dialogVisible" :closeConfirm="closeConfirm">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="父分类id" prop="parentId">
<el-tree-select
v-model="formData.parentId"
:data="categoryTree"
:props="defaultProps"
check-strictly
default-expand-all
placeholder="请选择父分类id"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Drawer>
</template>
<script setup lang="ts">
import { useDrawerFlag } from '@/hooks/web/useDrawer'
// 使用hooks封装
const { closeConfirm, dialogVisible, dialogTitle, ConfirmDrawerVisible } = useDrawerFlag()
/** IOT产品分类 表单 */
defineOptions({ name: 'CategoryForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({
id: undefined,
parentId: undefined,
name: undefined,
sort: undefined,
status: undefined,
imgUrl: undefined
// isSys: undefined
})
const formRules = reactive({
parentId: [{ required: true, message: '父分类id不能为空', trigger: 'blur' }],
name: [{ required: true, message: '分类名称不能为空', trigger: 'blur' }],
status: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }]
// isSys: [{ required: true, message: '是否系统通用不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const categoryTree = ref() // 树形结构
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await CategoryApi.getCategory(id)
} finally {
formLoading.value = false
}
}
await getCategoryTree()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as CategoryVO
if (formType.value === 'create') {
await CategoryApi.createCategory(data)
message.success(t('common.createSuccess'))
} else {
await CategoryApi.updateCategory(data)
message.success(t('common.updateSuccess'))
}
// 关闭弹框
ConfirmDrawerVisible()
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
parentId: undefined,
name: undefined,
sort: undefined,
status: undefined,
imgUrl: undefined
// isSys: undefined
}
formRef.value?.resetFields()
}
</script>
总结
看这样就可以到处去用了,虽然是一个小功能,但是里面也需要花时间协调逻辑,如果你觉得有用,请三连~~