在管理后臺開發過程中,涉及到太多的彈窗業務彈窗,其中最多的就是“添加XX數據”,“編輯XX數據”,“查看XX詳情數據”等彈窗類型最多?!鞠嚓P推薦:vuejs視頻教程、web前端開發】
這些彈窗組件的代碼,很多都是相同的,例如組件狀態,表單組件相關的方法...
(資料圖片僅供參考)
于是,我簡單地對Dialog
組件進行的二次封裝和hooks
,減少了一些重復的代碼
如果是普通彈窗使用的話,直接使用el-dialog
組件已經足夠了
但我還是一個比較愛折騰的人,我們先看看官方dialog
文檔有什么可以添加的功能
...
大概看了一下,我打算封裝一下功能
提供全屏操作按鈕(右上角)默認提供“確認”,“關閉”按鈕內部添加Loading
效果確定了要封裝的功能之后,先來一個簡單的dialog
組件。
把雙向綁定處理一下,這樣外部就可以直接通過v-model
直接控制彈窗了。
<template> <el-dialog :model-value="props.modelValue"></el-dialog></template><script setup>interface PropsType { modelValue?: boolean;}const props = withDefaults(defineProps<PropsType>(), { modelValue: false,});const emits = defineEmits<{ (e: "update:modelValue"): void;}>();</script>
如沒有安裝,請執行npm install @element-plus/icons-vue
使用el-dialog
提供的header
插槽,將全屏圖表和關閉圖標放置到右上角中。給el-dialog
傳遞show-close
屬性關閉默認圖標。
<template> <el-dialog :model-value="props.modelValue" :show-close="false"> <template #header> <div> <span>{{ props.title }}</span> </div> <div> <el-icon><FullScreen /></el-icon> <el-icon><Close /></el-icon> </div> </template> </el-dialog></template><script setup>import { FullScreen, Close } from "@element-plus/icons-vue";</script><style 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>
彈窗的標題文字內容通過props
進行傳遞,默認為空(""
)
<script lang="ts" setup>interface PropsType { // 忽略之前的代碼 title?: string;}const props = withDefaults(defineProps<PropsType>(), { title: "",});</script>
我們看看現在頭部的效果(這里沒傳入標題,默認為""
)
現在這個按鈕只有樣式效果,還沒有寫上對應的功能 ~
給他們先綁定上對應的事件和指令
<template> <el-dialog :model-value="props.modelValue" :show-close="false" :fullscreen="attrs?.fullscreen ?? isFullscreen" > <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> </el-dialog></template><script setup lang="ts">import { FullScreen, Close } from "@element-plus/icons-vue";interface PropsType { title?: string; modelValue?: boolean; hiddenFullBtn?: boolean;}const props = withDefaults(defineProps<PropsType>(), { title: "", modelValue: false, hiddenFullBtn: false,});const emits = defineEmits<{ (e: "update:modelValue"): void; (e: "close"): void;}>();// 當前是否處于全屏狀態const isFullscreen = ref(false);// 是否顯示全屏效果圖標const isFullScreenBtn = computed(() => { if (props.hiddenFullBtn) return false; if (attrs?.fullscreen) return false; return true;});// 開啟、關閉全屏效果const handleFullscreen = () => { if (attrs?.fullscreen) return; isFullscreen.value = !isFullscreen.value;};// 關閉彈窗時向外部發送close事件const handleClose = () => { emits("close");};</script>
再點擊下全屏圖標看看效果怎么樣
NICE 頭部功能也就完成了
接下來,再處理下底部內容,默認提供兩個按鈕,分別是“確定”和“關閉”,這個名稱也是可以通過props
屬性修改的。
兩個按鈕綁定點擊事件,向外發送不同的事件。
<template> <div class=""> <el-dialog v-bind="attrs" :model-value="props.modelValue" :show-close="false" :fullscreen="attrs?.fullscreen ?? isFullscreen" > <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><script setup lang="ts">import { useSlots } from "vue";// 獲取插槽const slots = useSlots();interface PropsType { title?: string; width?: string | number; isDraggable?: boolean; modelValue?: boolean; hiddenFullBtn?: boolean; confirmText?: string; cancelText?: string;}const props = withDefaults(defineProps<PropsType>(), { title: "", isDraggable: false, modelValue: false, hiddenFullBtn: false, confirmText: "確認", cancelText: "關閉",});const handleClose = () => { emits("close");};const handleConfirm = () => { emits("confirm");};</script>
又搞定了一部分了,就剩下Content了 ~
彈窗內容通過默認插槽的方式傳入進來,在外層的div
元素上添加v-loading
標簽,實現加載態。
如果你想整個彈窗實現loading效果,請把v-loading移到最外層元素即可。注意不能是el-dialog元素上,否則無法實現可能是el-dialog使用了teleport組件,導致v-loading無法正常工作。等有空研究一下 ~
<template> <div class=""> <el-dialog v-bind="attrs" :model-value="props.modelValue" :show-close="false" :fullscreen="attrs?.fullscreen ?? isFullscreen" > <div class="content" v-loading="props.loading"> <slot></slot> </div> </el-dialog> </div></template><script lang="ts" setup>interface PropsType { loading?: boolean;}const props = withDefaults(defineProps<PropsType>(), { loading: false,});</script>
試試看中間的loading
效果
在el-dialog
組件提供了很多個props
屬性供用戶選擇,但我們現在封裝的dialog
組件只使用到了一小部分props
屬性。當用戶想要使用其他的props
屬性時該怎么辦?
例如使用width屬性時,難道要在我們封裝的組件中接收props.width
再傳遞給<el-dialog :width="props.width" />
組件嗎?
不不不,還有另外一種方法,還記得剛剛在做全屏操作的時候使用到的useAttrs
輔助函數嗎
它可以獲取當前組件傳遞進來的屬性。有了這個方法之后,再配合并即可將外部傳遞進來的函數再傳遞到el-dialog
組件上面啦
<el-dialog v-bind="attrs" :model-value="props.modelValue" :show-close="false" :fullscreen="attrs?.fullscreen ?? isFullscreen" :before-close="handleClose"> <!-- 忽略其他代碼 --></el-dialog>
為了避免內部傳遞的props被覆蓋掉,v-bind="attrs"
需要放在最前面
在使用時,可能會給before-close
屬性傳遞一個函數,但到了后面被內部的handleClose
方法給覆蓋掉了。
解決方案是在handleClose
函數中,獲取attrs.["before-close"]
屬性,如果類型是函數函數,先執行它。
const handleClose = () => { if ( Reflect.has(attrs, "before-close") && typeof attrs["before-close"] === "function" ) { attrs["before-close"](); } emits("close");};
有關于el-dialog
組件的封裝就到這里了
利用Vue composition Api
再封裝一下在使用el-dialog
組件狀態的管理hook
簡單處理顯示和加載態開關的hook
import { ref } from "vue";export default function 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, };}
<template><el-button @click="openDialog1">普通彈窗</el-button><DialogCmp title="DialogCmp1" :hiddenFullBtn="true" v-model="visible1" @confirm="handleConfirm" @close="handleClose"> <h3>DialogCmp1</h3></DialogCmp></template><script setup lang="ts">import useDialog from "./components/useDialog";import DialogCmp from "./components/Dialog.vue";const { visible: visible1, openDialog: openDialog1, closeDialog: closeDialog1,} = useDialog();</script>
針對開發管理后臺彈窗狀態封裝的一個hook
,搭配下面的useDialogWithForm
使用。
export enum MODE { ADD, EDIT,}
import { ref } from "vue";import { MODE } from "./types";export default function useDialogState() { const mode = ref<MODE>(MODE.ADD); const visible = ref(false); const updateMode = (target: MODE) => { mode.value = target; }; return { mode, visible, updateMode };}
針對表單彈窗組件封裝的hooks
,接收一個formRef
實例,負責控制彈窗內標題及清空表單中的校驗結果,減少多余的代碼 ~
import { FormInstance } from "element-plus";import { Ref, ref } from "vue";import { MODE } from "./types";import useDialogState from "./useDialogState";export default function useDialogFn( formInstance: Ref<FormInstance>) { const { visible, mode, updateMode } = useDialogState(); const closeDialog = () => { formInstance.value.resetFields(); visible.value = false; }; const openDialog = (target: MODE) => { updateMode(target); visible.value = true; }; return { visible, mode, openDialog, closeDialog };}
<template> <Dialog :before-close="customClose" @confirm="confirm" v-model="visible" :title="mode == MODE.ADD ? "添加數據" : "編輯信息"" :confirm-text="mode == MODE.ADD ? "添加" : "修改"" > <el-form label-width="100px" :model="formData" ref="formDataRef" style="max-width: 460px" :rules="rules" > <el-form-item label="姓名" prop="name"> <el-input v-model="formData.name" /> </el-form-item> <el-form-item label="年齡" prop="age"> <el-input v-model="formData.age" /> </el-form-item> <el-form-item label="手機號碼" prop="mobile"> <el-input v-model="formData.mobile" /> </el-form-item> </el-form> </Dialog></template><script setup>import { ElMessage, FormInstance } from "element-plus";import { Ref, ref } from "vue";import Dialog from "./Dialog.vue";import { MODE } from "./types";import useDialogWithForm from "./useDialogWithForm";const rules = { name: { type: "string", required: true, pattern: /^[a-z]+$/, trigger: "change", message: "只能是英文名稱哦", transform(value: string) { return value.trim(); }, }, age: { type: "string", required: true, pattern: /^[0-9]+$/, trigger: "change", message: "年齡只能是數字哦", transform(value: string) { return value.trim(); }, }, mobile: { type: "string", required: true, pattern: /^(?:(?:\+|00)86)?1(?:(?:3[\d])|(?:4[5-79])|(?:5[0-35-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\d])|(?:9[189]))\d{8}$/, trigger: "change", message: "請輸入正確的手機號碼", transform(value: string) { return value.trim(); }, },};interface FromDataType { name: string; age: string; mobile: string;}const formDataRef = ref<FormInstance | null>(null);let formData = ref<FromDataType>({ name: "", age: "", mobile: "",});const { visible, closeDialog, openDialog, mode } = useDialogWithForm( formDataRef as Ref<FormInstance>);const confirm = () => { if (!formDataRef.value) return; formDataRef.value.validate((valid) => { if (valid) { console.log("confirm"); ElMessage({ message: "提交成功", type: "success", }); closeDialog(); } });};const customClose = () => { ElMessage({ message: "取消提交", type: "info", }); closeDialog();};defineExpose({ closeDialog, openDialog,});</script><style scoped></style>
useDialog
在線demo地址
如果您覺得本文對您有幫助,請幫幫忙點個star
您的反饋 是我更新的動力!
(學習視頻分享:vuejs入門教程、編程基礎視頻)
以上就是聊聊Vue3+hook怎么寫彈窗組件更快更高效的詳細內容,更多請關注php中文網其它相關文章!
關鍵詞: vue3