队列在弹窗管理中的应用

1,904 阅读2分钟

之前开发H5活动页时,产品同学往页面加入了十几种弹窗,而且弹窗还存在优先级关系,管理弹窗弹出的顺序是个大难题。为了方便管理弹窗,我通过弹窗队列管理弹窗,弹窗可以一个接一个地弹出,用户体验非常友好!

1.数据结构-队列

在介绍弹窗队列之前,我们必须了解数据结构-队列。队列是一种特殊的线性表,特殊之处在于它只允许在表的前端进行删除操作,而在表的后端进行插入操作。所以队列遵循先进先出(FIFO)原则。

回到弹窗的需求,弹窗是逐个弹出的,先添加的弹出的弹出优先级更高,所以很适合使用队列管理。下面是一个弹窗队列的原理图。

2.基于vue3+vuex实现全局弹窗队列

源码传送门

2.1.使用vuex管理队列

因为弹窗顺序存在优先级,所以采用优先队列管理弹窗。往队列插入弹窗时,会检查优先级,往合适的位置插入弹窗。

对外暴露两个方法:

  1. addModal:往队列添加弹窗组件实例
  2. nextModal:弹出队列头部的弹窗
import { createStore } from 'vuex'

const store = createStore({
  state: {
    queue: [], // 弹窗队列
    visible: false, // 是否已经有弹窗展示
    order: ['modal2', 'modal3' ,'modal1'], // 权重
  },
  mutations: {
    /**
     * 新增要弹出的弹窗
     * @param {Object} info 组件的实例、类型等信息
     * @param {Object} info.instance 组件的实例
     * @param {String} info.type 组件的权重名,对应order
     * @param {Boolean} info.show 是否立即展示
     */
    add(state, info) {
      const { queue, order } = state;
      const orderIndex = order.findIndex(ceil => ceil === info.type);
      // 找到合适的位置插入
      for (let i = 0; i < queue.length; i++) {
        const curModal = queue[i];
        const curOrderIndex = order.findIndex(ceil => ceil === curModal.type);
        if (curOrderIndex > orderIndex) {
          queue.splice(i - 1, 0, info);
          return;
        } else if (curModal.type === info.type) {
          queue.splice(i, 0, info);
          return;
        }
      }
      queue.push(info);
    },

    // 弹出第一个弹窗
    pop(state) {
      const queue = state.queue;
      if (queue.length > 0) {
        const { instance } = queue.shift();
        instance.show();
        state.visible = true;
      } else {
        state.visible = false;
      }
    }
  },
  actions: {
    /**
     * 往队列添加弹窗,并弹出队列的第一个弹窗
     * @param {Object} info 组件的实例、类型等信息
     * @param {Object} info.instance 组件的实例
     * @param {String} info.type 组件的权重名,对应order
     * @param {Boolean} info.show 是否立即展示
     */
    addModal(ctx, info = {}) {
      ctx.commit('add', info);
      if (!ctx.state.visible && info.show) {
        ctx.commit('pop');
      }
    },
    // 下一个弹窗
    nextModal(ctx) {
      ctx.commit('pop');
    }
  }
})

export default store

2.2.弹窗组件

2.2.1弹窗蒙层

不同的弹窗有公共的蒙层和关闭按钮,可以抽取为一个公共组件mask.vue。点击关闭按钮时,弹出下一个弹窗。

<template>
  <div class="mask" v-if="visible">
      <div class="content animate__bounceIn">
        <img src="../assets/关闭.png" class="close" alt="" @click="close">
        <slot></slot>
      </div>
  </div>
</template>

<script setup>
import { useStore } from "vuex";
defineProps(['visible'])
const emit = defineEmits(['update:visible'])
const store = useStore();

// 关闭弹窗
function close() {
  // 关闭当前弹窗
  emit('update:visible', false)
  // 弹出下一个弹窗
  store.dispatch('nextModal')
}
</script>

<style lang="scss" scoped>
.mask {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba($color: #000000, $alpha: 0.7);
  display: flex;
  justify-content: center;
  align-items: center;

  .content {
    padding: 20px;
    background-color: #fff;
    border-radius: 12px;
    position: relative;

    .close {
      position: absolute;
      right: 0;
      top: -40px;
      width: 30px;
      height: 30px;
    }
  }
}

.animate__bounceIn {
  animation-duration: calc(1s * 0.75);
  animation-name: bounceIn;
}
@keyframes bounceIn {
  from, 20%, 40%, 60%, 80%, to {
    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
  }

  0% {
    opacity: 0;
    transform: scale3d(0.3, 0.3, 0.3);
  }

  20% {
    transform: scale3d(1.1, 1.1, 1.1);
  }

  40% {
    transform: scale3d(0.9, 0.9, 0.9);
  }

  60% {
    opacity: 1;
    transform: scale3d(1.03, 1.03, 1.03);
  }

  80% {
    transform: scale3d(0.97, 0.97, 0.97);
  }

  to {
    opacity: 1;
    transform: scale3d(1, 1, 1);
  }
}
</style>

2.2.2公共的展示弹窗方法

所有弹窗通过show方法展示弹窗,因此抽取作为组合式函数modalvisible.js

import { ref } from 'vue'

export function useModalVisible() {
  const visible = ref(false)
  // 显示弹窗
  function show() {
    visible.value = true
  }
  return { visible, show }
}

2.2.3弹窗组件

<template>
  <Mask v-model:visible="visible">
    <div class="container">
      我是弹窗1
    </div>
  </Mask>
</template>

<script setup>
import Mask from './mask.vue'
import { useModalVisible } from '../common/modalvisible'
const { visible, show } = useModalVisible()
defineExpose({
  show
})

</script>

<style lang="scss" scoped>
.container {
  width: 200px;
  height: 200px;
  display: flex;
  justify-content: center;
  align-items: center;
}
</style>

2.2.4弹出弹窗

使用addModal方法往弹窗队列添加弹窗的实例,即可实现弹出弹窗

<template>
  <button @click="testShow" style="margin-right: 10px;">展示弹窗</button>
  <button @click="testOrder">测试排序</button>
  <Modal1 ref="modal1"></Modal1>
  <Modal2 ref="modal2"></Modal2>
  <Modal3 ref="modal3"></Modal3>
</template>

<script setup>
import { useStore } from "vuex";
import { ref } from 'vue'
import Modal1 from './components/modal1.vue';
import Modal2 from './components/modal2.vue';
import Modal3 from './components/modal3.vue';

const store = useStore();
const modal1 = ref(null)
const modal2 = ref(null)
const modal3 = ref(null)

function testShow() {
  store.dispatch('addModal', {
    instance: modal1,
    type: 'modal1',
    show: true
  })
  setTimeout(() => {
    store.dispatch('addModal', {
      instance: modal2,
      type: 'modal2',
      show: true
    })
  }, 500);
  setTimeout(() => {
    store.dispatch('addModal', {
      instance: modal3,
      type: 'modal3',
      show: true
    })
  }, 1000);
}

function testOrder() {
  store.dispatch('addModal', {
    instance: modal3,
    type: 'modal3',
    show: false
  })

  store.dispatch('addModal', {
    instance: modal2,
    type: 'modal2',
    show: false
  })

  setTimeout(() => {
    store.dispatch('addModal', {
      instance: modal1,
      type: 'modal1',
      show: true
    })
  }, 300);
}
</script>

<style scoped>
</style>