Maison > Article > interface Web > Parlons de la façon d'écrire des composants contextuels plus rapidement et plus efficacement avec Vue3+hook
Dans le processus de développement du backend de gestion, trop de fenêtres contextuelles d'entreprise sont impliquées, dont les plus courantes sont "Ajouter des données XX", "Modifier des données XX ", "Afficher les données détaillées XX" " et d'autres types de fenêtres contextuelles sont les plus courants. [Recommandations associées : Tutoriel vidéo Vuejs, Développement frontal Web]
La plupart des codes de ces composants contextuels sont les mêmes, comme l'état des composants, les méthodes liées aux composants de formulaire...
Donc , je fais simplement L'encapsulation secondaire et les hooks
du composant Dialog
réduisent certains codes répétésDialog
组件进行的二次封装和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>
这里使用到图标库@element-plus/icons-vue
如没有安装,请执行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"></el-dialog>
组件吗?
不不不,还有另外一种方法,还记得刚刚在做全屏操作的时候使用到的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
el-dialog
🎜🎜Mais je suis toujours une personne qui aime déconner. Jetons un coup d'œil au Chargement
en internedialog
. 🎜🎜 Traitez la liaison bidirectionnelle afin que l'extérieur puisse contrôler directement la fenêtre contextuelle via v-model
. 🎜const handleClose = () => { if ( Reflect.has(attrs, "before-close") && typeof attrs["before-close"] === "function" ) { attrs["before-close"](); } emits("close"); };
🎜La bibliothèque d'icônes @element-plus/icons-vue est utilisée ici🎜
🎜Si elle n'est pas installée, veuillez exécutez npm install @element-plus/icons-vue🎜🎜Utilisez l'emplacement
header
fourni par el-dialog
pour placer le graphique en plein écran et fermer l'icône dans le coin supérieur droit. Passez l'attribut show-close
à el-dialog
pour fermer l'icône par défaut. 🎜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, }; }🎜Le contenu du texte du titre de la fenêtre contextuelle est transmis via
props
, et la valeur par défaut est vide (''
) 🎜<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>🎜Jetons un coup d'œil au effet de la tête actuelle (non transmis ici) Entrez le titre, la valeur par défaut est
''
)🎜🎜🎜🎜Maintenant, ce bouton n'a qu'un effet de style, et la fonction correspondante n'a pas encore été écrite~🎜🎜Lier les événements correspondants et des instructions pour eux d'abord🎜export enum MODE { ADD, EDIT, }🎜 Cliquez ensuite sur l'icône plein écran. Voyons comment cela fonctionne🎜🎜🎜🎜NICE La fonction d'en-tête est maintenant terminée🎜
props
. 🎜🎜Deux boutons lient les événements de clic et envoient différents événements vers l'extérieur. 🎜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 }; }🎜🎜🎜 Une autre partie est terminée, il ne reste que le contenu ~🎜
div externe Ajoutez le Balise <code>v-loading
à l'élément code> pour atteindre l'état de chargement. 🎜🎜Si vous souhaitez que la fenêtre contextuelle entière obtienne l'effet de chargement, veuillez déplacer v-loading vers l'élément le plus à l'extérieur. Notez qu'il ne peut pas être sur l'élément el-dialog, sinon il ne peut pas être implémenté. Il se peut qu'el-dialog utilise le composant de téléportation, ce qui empêche le v-loading de fonctionner correctement. Étudiez-le quand vous avez le temps ~🎜
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 }; }🎜Essayez de voir l'effet
loading
au milieu🎜🎜🎜el-dialog
Le composant fournit de nombreux attributs props
que les utilisateurs peuvent choisir, mais le composant dialog
que nous encapsulons actuellement n'utilise qu'une petite partie de la propriété props
. Que doit faire l'utilisateur lorsqu'il souhaite utiliser d'autres propriétés props
? 🎜🎜Par exemple, lors de l'utilisation de l'attribut width, devons-nous recevoir props.width
dans notre composant encapsulé, puis le transmettre à <el-dialog :width="props.width "></el-dialog> ?🎜🎜Non, non, il existe une autre méthode. Vous souvenez-vous de la fonction auxiliaire <code>useAttrs
que vous venez d'utiliser lors d'une opération en plein écran ?🎜🎜Elle peut obtenir le transfert de composants actuel entre dans les propriétés. Avec cette méthode, vous pouvez ensuite coopérer et transmettre les fonctions transmises de l'extérieur au composant el-dialog
🎜<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>
🎜Afin d'éviter que les accessoires transmis en interne ne soient écrasés, v-bind="attrs"
doit être placé au premier plan🎜
🎜Lors de son utilisation, une fonction peut être passée à l'attribut before-close
, mais elle sera utilisée plus tard. La méthode interne handleClose
est écrasée. 🎜解决方案是在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>
如果您觉得本文对您有帮助,请帮帮忙点个star
您的反馈 是我更新的动力!
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!