Vue3 造轮子: Dialog组件

360 阅读1分钟

Dialog 组件

需求

  • 点击后弹出

  • 有遮罩层overlay

  • 有close按钮

  • 有标题

  • 有内容

  • 有yes/no按钮

API设计

  • Dialog组件怎么用
<Dialog
  :visible ="true"
  title="标题"
  @yes="fn1" @no="fn2"
></Dialog>

创建Dialog组件

创建Dialog.vue,在DialogDemo.vue里面引入Dialog.vue,在Dialog.vue里面创建两个div,一个是dialog-overlay,一个是dialog-wrapper

让Dialog支持visible属性

不要用show表示是否可见(是个错误的命令,show是动词,不能表示可见)

首先,在Dialog.vue 里面接受一个props,然后DialogDemo.vue里面声明一个变量,让:visible="x"

 props: {
    visible: {
      type: Boolean,
      default: false,
    },

image.png

这时visible没有任何的作用,看不见效果,使用:

template标签里面再写个template , <template><template>其他内容</template></template>

<template>
  <template v-if="visible">
    <div class="circle-dialog-overlay"></div> 
    <div class="circle-dialog-wrapper"> 
      <div class="circle-dialog">
        <header>
          标题
          <span class="gulu-dialog-close"></span> 
        </header>                              
        <main> 
          <p>第一行字</p><p>第二行字</p>
        </main> 
        <footer> 
          <Button level="main">OK</Button>
          <Button>Cancel</Button>
        </footer>
      </div>
    </div>
  </template>
</template>

<script lang="ts">
import Button from "./Button.vue";
export default {
  components: {
    Button,
  },
  props: {
    visible: {
      type: Boolean,
      default: false,
    },
  }
}
</script>

让Dialog可以点击关闭

注意不能修改 props

<Dialog :visible="x" @update:visible="x = $event"></Dialog>
//等价于
<Dialogv v-model:visible="x"></Dialog>

点击ok按钮关闭,点击cancel按钮关闭,点击右上方X关闭(closeOnClickOverlay)

让Dialog支持title和content

为了让title和content都可以支持自定义内容

使用插槽 slot

使用具名插槽

具名插槽

顾名思义,就是具有名字的插槽。就是将插槽后面加上name="..."

在Dialog.vue使用<slot name="title"/><slot name="content"/>

然后在DialogDemo.vue里面使用

<template v-slot:content>
  <strong>hi</strong>
  <div>hi2</div>
</template>
<template v-slot:title>
  <strong>加粗的标题</strong>
</template>

使用Teleport

把Dialog移到body下

防止Dialog被遮挡

新组件:Teleport

将两个div都放到Teleport里面,加上属性<Teleport to="body">...</Teleport>。可以理解为:Teleport相当于一个传送门,请把我传送到body的下面

为什么把Dialog移到body下面?

因为CSS存在层叠上下文的,导致Dialog被遮住,使用用Teleport。

一句话打开Dialog

动态挂载组件

创建showDialog.ts

showDialog.vue

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

DialogDemo.vue

<template>
  <div>Dialog 示例</div>
  <h1>示例1</h1>

  <Button @click="toggle">toggle</Button>
  <Dialog
    v-model:visible="x"
    :closeOnClickOverlay="false"
    :ok="f1"
    :cancel="f2"
  >
    <template v-slot:content>
      <strong>hi</strong>
      <div>hi2</div>
    </template>
    <template v-slot:title>
      <strong>加粗的标题</strong>
    </template>
  </Dialog>
  <h1>示例2</h1>
  <Button @click="showDialog">show</Button>
</template>
<script lang="ts">
import { ref, h } from "vue";
import Button from "../lib/Button.vue";
import Dialog from "../lib/Dialog.vue";
import { openDialog } from "../lib/openDialog";

export default {
  components: { Dialog, Button },
  setup() {
    const x = ref(false); //x 的参考值为false
    const toggle = () => {
      x.value = !x.value; //x 的值等于 x 的值取相反值
    };
    const f1 = () => {
      return false;
    };
    const f2 = () => {};
    const showDialog = () => {
      openDialog({
        title: h("strong", {}, "标题"),
        content: "你好",
        ok() {
          console.log("ok");
        },
        cancel() {
          console.log("cancel");
        },
      });
    };
    return {
      x,
      toggle,
      f1,
      f2,
      showDialog,
    };
  },
};
</script>

Dialog.vue

<template>
  <template v-if="visible">
    <Teleport to="body">
      <div class="circle-dialog-overlay" @click="onClickOverlay"></div>
      <div class="circle-dialog-wrapper">
        <div class="circle-dialog">
          <header>
            <slot name="title" />
            <span @click="close" class="circle-dialog-close"></span>
          </header>
          <main>
            <slot name="content" /> 
          </main>
          <footer>
            <Button level="main" @click="ok">OK</Button>
            <Button @click="cancel">Cancel</Button>
          </footer>
        </div>
      </div>
    </Teleport>
  </template>
</template>
<script lang="ts">
import Button from "./Button.vue";
export default {
  components: {
    Button,
  },
  props: {
    visible: {
      type: Boolean,
      default: false,
    },
    closeOnClickOverlay: {
      type: Boolean,
      default: true,
    }, //是否要做到遮盖层关闭,默认是true
    ok: {
      type: Function,
    },
    cancel: {
      type: Function,
    },
  },
  setup(props, context) {
    const close = () => {
      context.emit("update:visible", false);
    };
    const onClickOverlay = () => {
      if (props.closeOnClickOverlay) {
        close();
      }
      //如果开启这个功能就调用这个close,否则什么都不做
    };
    const ok = () => {
      if (props.ok && props.ok !== false) {
        close();
      }
      //如果ok 存在,且props.ok执行之后的返回值不等于false,就close
    };
    const cancel = () => {
      props.cancel && props.cancel();
      close();
    };

    return {
      close,
      onClickOverlay,
      ok,
      cancel,
    };
  },
};
</script>

<style lang="scss">
$radius: 4px;
$border-color: #d9d9d9;

.circle-dialog {
  background: white;
  border-radius: $radius;
  box-shadow: 0 0 3px fade-out(black, 0.5);
  min-width: 15em;
  max-width: 90%;

  &-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: fade-out(black, 0.5);
    z-index: 10;
  }

  &-wrapper {
    position: fixed;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    z-index: 11;
  }

  > header {
    padding: 12px 6px;
    border-bottom: 1px solid $border-color;
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-size: 20px;
  }

  > main {
    padding: 12px 16px;
  }
  > footer {
    border-top: 1px solid $border-color;
    padding: 12px 16px;
    text-align: right;
  }

  &-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);
    }
  }
}
</style>