聊聊Vue3+hook怎麼寫彈窗元件更快更高效

2022-12-28 22:00:16

為什麼會有這個想法

在管理後臺開發過程中,涉及到太多的彈窗業務彈窗,其中最多的就是「新增XX資料」,「編輯XX資料」,「檢視XX詳情資料」等彈窗型別最多。【相關推薦:、】

這些彈窗元件的程式碼,很多都是相同的,例如元件狀態,表單元件相關的方法...

於是,我簡單地對Dialog元件進行的二次封裝和hooks,減少了一些重複的程式碼

要封裝什麼

如果是普通彈窗使用的話,直接使用el-dialog元件已經足夠了

但我還是一個比較愛折騰的人,我們先看看官方dialog檔案有什麼可以新增的功能

...

大概看了一下,我打算封裝一下功能

  • 提供全螢幕操作按鈕(右上角)
  • 預設提供「確認」,「關閉」按鈕
  • 內部新增Loading效果

封裝Dialog

確定了要封裝的功能之後,先來一個簡單的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>
登入後複製

header

這裡使用到圖示庫@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>
登入後複製

我們看看現在頭部的效果(這裡沒傳入標題,預設為''

1.png

現在這個按鈕只有樣式效果,還沒有寫上對應的功能 ~

給他們先繫結上對應的事件和指令

<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>
登入後複製

再點選下全螢幕圖示看看效果怎麼樣

2.png

NICE 頭部功能也就完成了

Footer

接下來,再處理下底部內容,預設提供兩個按鈕,分別是「確定」和「關閉」,這個名稱也是可以通過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>
登入後複製

3.png

又搞定了一部分了,就剩下Content了 ~

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效果

4.gif

剩下一些細節處理

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元件的封裝就到這裡了

封裝hooks

利用Vue composition Api再封裝一下在使用el-dialog元件狀態的管理hook

useDialog

簡單處理顯示和載入態開關的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,
  };
}
登入後複製

useDialog Demo

5.gif

<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>
登入後複製

useDialogState 和 useDialogWithForm

useDialogState

針對開發管理後臺彈窗狀態封裝的一個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 };
}
登入後複製

useDialogWithForm

針對表單彈窗元件封裝的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 };
}
登入後複製

useDialogWithForm Demo

6.gif

<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地址

7 (1).gif

如果您覺得本文對您有幫助,請幫幫忙點個star

您的反饋 是我更新的動力!

(學習視訊分享:、)

以上就是聊聊Vue3+hook怎麼寫彈窗元件更快更高效的詳細內容,更多請關注TW511.COM其它相關文章!