06 - Vue3 UI Framework - Dialog 元件

Jeremy.Wu發表於2021-12-14

做完按鈕之後,我們應該瞭解了遮罩層的概念,接下來我們來做 Dialog 元件!

返回閱讀列表點選 這裡

需求分析

  1. 預設是不可見的,在使用者觸發某個動作後變為可見
  2. 自帶白板卡片,分為上中下三個區域,分別放置標題、內容、操作
  3. 有兩個基本操作:確定、取消
  4. 卡片後應放置淡黑色遮罩層,遮住原本網頁內容
  5. 可以自定義是否允許取消
  6. 右上角提供小叉叉來允許關閉
  7. 允許通過點選遮罩層來關閉

所以,我們能夠得出如下的參數列格

引數 含義 型別 可選值 預設值
visible 是否可見 boolean false / true false
title 標題 string 任意字串 必填
ok 確定回撥 ()=>boolean 返回 boolean 的函式 ()=>true
cancel 取消回撥 ()=>boolean 返回 boolean 的函式 ()=>true

注意:可以通過設定返回值為 true 來允許事件發生,反之不允許。可以通過設定返回 false 來取消事件

骨架

我們複用之前做好的 Button 元件

一般情況下,我們不希望對話方塊彈窗在 DOM 樹上的位置,而希望是 body 的直接子元素,那麼我們可以使用 vue3teleport 元件。

程式碼如下:

<template>
  <template v-if="visible">
    <teleport to="body">
      <div class="jeremy-dialog-overlay" @click="close"></div>
      <div class="jeremy-dialog">
        <header class="jeremy-dialog-header">
          {{ title }}
          <span class="jeremy-dialog-close" @click="close"></span>
        </header>
        <div class="jeremy-dialog-divider" />
        <main class="jeremy-dialog-main">
          <slot></slot>
        </main>
        <div class="jeremy-dialog-divider" />
        <footer class="jeremy-dialog-footer">
          <jeremy-button level="plain" @click="close">取消</jeremy-button>
          &nbsp;&nbsp;&nbsp;&nbsp;
          <jeremy-button @click="task" :loading="loading">確定</jeremy-button>
        </footer>
      </div>
    </teleport>
  </template>
</template>

這樣,在渲染時,teleport 內部的內容就會出現在 body 的子級上。

功能

現在 ts 中宣告引數:

declare const props: {
  visible: boolean;
  title: string;
  ok: () => boolean;
  cancel: () => boolean;
};
declare const context: SetupContext;

然後在 export default 中,寫入我們的引數:

export default {
  install: function (Vue) {
    Vue.component(this.name, this);
  },
  name: "JeremyDialog",
  props: {
    visible: {
      type: Boolean,
      default: false,
    },
    title: {
      type: String,
      required: true,
    },
    ok: {
      type: Function,
      default: () => {
        return true;
      },
    },
    cancel: {
      type: Function,
      default: () => {
        return true;
      },
    },
  },
  components: {
    JeremyButton,
  },
};

再補全 setup 方法,此處選用 Promise 製造提交等待響應的感覺

  setup(props, context) {
    const loading = ref(false);
    const close = () => {
      if (loading.value) {
        return;
      }
      new Promise((resolve, reject) => {
        resolve(props.cancel());
      }).then((result) => {
        if (result !== false) {
          context.emit("update:visible", false);
        }
      });
    };
    const task = () => {
      new Promise((resolve, reject) => {
        loading.value = true;
        resolve(props.ok());
      }).then((result) => {
        if (result === true) {
          loading.value = false;
          context.emit("update:visible", false);
        }
      });
    };
    return { loading, close, task };
  },

樣式表

最後再補全樣式表:

<style lang="scss">
.jeremy-dialog-overlay {
  z-index: 20;
  position: fixed;
  left: 0;
  top: 0;
  background: fade-out($color: #000000, $amount: 0.7);
  width: 100vw;
  height: 100vh;
}
.jeremy-dialog {
  z-index: 20;
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  min-width: 300px;
  min-height: 200px;
  border-radius: 8px;
  background: white;
  display: flex;
  flex-direction: column;
  > * {
    padding: 8px;
  }
  > .jeremy-dialog-divider {
    border: 1px solid #8c6fef;
    padding: 0;
  }
  > .jeremy-dialog-header {
    display: flex;
    justify-content: space-between;
    > .jeremy-dialog-close {
      position: relative;
      display: inline-block;
      width: 16px;
      height: 16px;
      cursor: pointer;
      &::before,
      &::after {
        content: "";
        position: absolute;
        height: 1px;
        background: black;
        width: 100%;
        top: 50%;
        left: 50%;
      }
      &::before {
        transform: translate(-50%, -50%) rotate(-45deg);
      }
      &::after {
        transform: translate(-50%, -50%) rotate(45deg);
      }
    }
  }
  > .jeremy-dialog-main {
    flex-grow: 1;
    background: white;
  }
  > .jeremy-dialog-footer {
    display: flex;
    justify-content: flex-end;
  }
}
</style>

一行程式碼開啟

多數時候我們是不希望使用元件式的,而是直接用函式生成一個彈窗。那麼,我們只要使用 vue3 提供的 createApph 函式就可以做到了。

我們再建立一個 ts 檔案,即 createDialog.ts ,程式碼如下:

import { createApp, h } from 'vue'
import JeremyDialog from './Dialog.vue'
export const createDialog = options => {
  const { title, content, ok, cancel } = options
  const div = document.createElement('div')
  document.body.appendChild(div)
  const close = () => {
    app.unmount(div)
    div.remove()
  }
  const app = createApp({
    render() {
      return h(JeremyDialog, {
        visible: true,
        'onUpdate:visible': newVisible => {
          if (newVisible === false) {
            close();
          }
        },
        title,
        ok, cancel
      }, { default() { return content } })
    }
  })
  app.mount(div)
}

然後再需要使用的地方匯入即可:

import {createDialog} from './createDialog.ts'

注意:該函式要求傳入一個 options 物件,該物件包含 title, content, ok, cancel 等 4 個部分,content 指代元件式中的插槽,其餘含義見需求分析

然後使用 h 函式渲染新 app 中的內容,並作為引數傳入 createApp 函式用以建立新的 app,最後掛載到 DOM 樹上。

執行效果

接下來,我們將元件引入到頁面中,看一下實際執行效果

dialog

感謝閱讀 ☕

相關文章