项目实训 - Scene-UI - 编写dialog

对话框

对话框的实现主要依靠vue3的两个高级内置组件:

  • <Teleport>:使我们可以将一个组件的一部分模板“传送”到该组件的 DOM 层次结构之外的 DOM 节点中。为 <Teleport> 指定的目标 to 期望接收一个 CSS 选择器字符串或者一个真实的 DOM 节点。

    实现对话框,可以利用该组件去传送对话框的模板片段body 标签下。

  • <Transition> 会在一个元素或组件进入和离开 DOM 时应用动画。利用该内置组件,可以设置对话框进入离开的过度动画。

详情:Transition·过渡 | Vue.js (vuejs.org)Teleport·传送门 | Vue.js (vuejs.org)

::: tip

编写代码时注意:不能在onMount里进行DOM操作,因为dialog组件挂载完成时有一部分的v-if=false,此时是没有渲染的,无法进行模板引用。因此我们需要使用watch函数监听带有v-if的那一块模板,当模板存在时再进行DOM操作。

:::

如何实现控制遮罩层的显现?

在watch函数中监听 container模板的变化,当模板存在时,如果props.modal=false,设置遮罩层的div背景为透明。

如何实现滚动锁定?

在watch函数中监听 container模板的变化,当模板存在时,给window(浏览器)添加一个禁止函数,禁止wheel事件触发,当关闭时使用removeEventListener删除这个监听器。

源码:

<template>
  <Teleport to="body">
    <Transition name="scene-dialog"
      @before-enter="onBeforeEnter"
      @after-enter="onAfterEnter"
      @before-leave="onBeforeLeave"
      @after-leave="onAfterLeave"
    >
      <!-- 对话框的遮罩层 -->
      <div v-if="modelValue" ref="mask" class="scene-dialog-mask" tabindex="0" 
        @click.self="closeDialog(closeOnClickModal)"
      >
          <div ref="container" class="scene-dialog-container">
            <!-- 对话框的头部 -->
              <div v-if="$slots.title || title" class="scene-dialog-title">
                  <slot name="title">
                    <div class="scene-dialog-default-title">
                      <h3>{{title}}</h3><s-icon color="#aa00ff" name="x-circle-fill" @click="closeDialog(true)"></s-icon>
                    </div>
                  </slot>
              </div>
            <!-- 对话框的主体 -->
              <div class="scene-dialog-body">
                  <slot>a short message</slot>
              </div>
            <!-- 对话框的尾部  -->
              <div class="scene-dialog-footer">
                  <slot name="footer">
                    <div class="scene-dialog-default-footer">
                      <s-button size="small" @click="closeDialog(true)">OK</s-button>
                    </div>
                  </slot>
              </div>
          </div>
      </div>
    </Transition>
  </Teleport>
</template>

<script lang='ts'>
import {defineComponent, onMounted, ref, watch} from 'vue'
import {dialogProps, dialogEmits} from './dialog'
import sButton from '../button/button.vue'
import sIcon from '../icon/icon.vue'
import '../../styles/global.css'

export default defineComponent({
  name:'scene-dialog',
  props:dialogProps,
  emits:dialogEmits,
  components:{sButton,sIcon},
  setup(props,{emit,attrs,slots,expose}){
    const mask = ref<HTMLElement>()
    const container = ref<HTMLElement>()

    /**
     * 阻止默认事件进行的函数
     */
    function preventDefaultFunc(e: Event) { 
      let evt = e;
      evt.preventDefault();
    }

    /**
     *  关闭对话框,isTrigger控制该函数是否触发
     */
    function closeDialog(isTrigger: boolean){
      if(isTrigger)
        emit('update:modelValue',false)
    }

    /**
     * dialog的hook函数
     */
    function onBeforeEnter(){// 在元素被插入到 DOM 之前被调用
      emit('open','open')
    }
    function onAfterEnter() {// 当过度完成时调用
      emit('opened','opened')
    }
    function onBeforeLeave() {// 在leave之前调用
      emit('close','close')
    }
    function onAfterLeave() {// 在离开过渡完成、且元素已从 DOM 中移除时调用
      emit('closed','closed')
    }

    // 监听 container模板的变化,当模板存在时,修改其宽度
    // 为什么不在onMount里进行DOM操作? dialog在组件挂载完成时v-if=false,此时是没有渲染的,无法进行模板引用
    watch(mask,(newMask,oldMask)=>{
      if(newMask){// 打开对话框
        mask.value!.style.background = 'rgba(0, 0, 0, 0.5)';
        // 设置宽度
        container.value!.style.minWidth = (props.width + 40) + 'px'
        // 设置滚动锁定
        if(props.lockScroll)
          window.addEventListener("wheel", preventDefaultFunc, {passive:false});
        if(!props.modal){
          mask.value!.style.background = 'transparent'
        }
      }else{// 关闭对话框
        // 解除滚动锁定 
        if(props.lockScroll)
          window.removeEventListener('wheel',preventDefaultFunc)
      }
    })

    return{
      mask,
      container,
      closeDialog,
      onBeforeEnter,
      onBeforeLeave,
      onAfterEnter,
      onAfterLeave
    }
  }
})
</script>

<style scoped>
.scene-dialog-mask {
  position: fixed;
  z-index: var(--scene-index-advanced)+1;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: var(--scene-color-mask);

  display: flex;
  justify-content: center;
  align-items: center;
  transition: opacity 0.3s ease;
}
.scene-dialog-container {
  position: relative;
  z-index: var(--scene-index-superlative);
  margin: 0px auto;
  background-color: var(--scene-color-white);
  border-radius: var(--scene-border-radius);
  box-shadow: var(--scene-box-shadow);
  transition: all 0.3s ease;
}

.scene-dialog-title{
  padding: 20px 20px 10px;
}

.scene-dialog-body {
  padding: 10px 20px 10px;
}

.scene-dialog-footer{
  padding: 10px 20px 20px;
}

.scene-dialog-default-title{
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.scene-dialog-default-title h3{
  margin: 0;
  font-family: var(--scene-font-family);
  letter-spacing: var(--scene-letter-spacing);
  color: var(--scene-theme-color-main);
}

.scene-dialog-default-footer{
  display: flex;
  justify-content: flex-end;
  align-items: center;
}
/*
 * The following styles are auto-applied to elements with
 * transition="scene-dialog" when their visibility is toggled
 * by Vue.js.
 *
 * You can easily play with the scene-dialog transition by editing
 * these styles.
 */
.scene-dialog-enter-from,
.scene-dialog-leave-to {
  opacity: 0;
}
.scene-dialog-enter-from .scene-dialog-container,
.scene-dialog-leave-to .scene-dialog-container {
  -webkit-transform: scale(1.1);
  transform: scale(1.1);
}
</style>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sunburst7

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值