全局交互革命:uniapp 统一弹窗体系

45 阅读4分钟

全局交互革命:统一弹窗体系

在现代移动应用中,弹窗(Modal)是最基础也是最常用的交互组件。然而,传统的弹窗实现往往存在耦合度高、调用繁琐、扩展性差等问题。

本文将详细介绍本项目如何通过 Pinia Store + 全局挂载 + Hook 封装 的组合拳,打造一个既支持基础 Alert/Confirm,又支持多层级、可翻页公告的强大全局弹窗体系。


一、 传统痛点 vs 创新方案

1. 传统痛点

  • 组件地狱:需要在每个页面重复引入 <modal> 组件,模板代码冗余。
  • 调用受限:只能在 Vue 组件内部通过 this.$refs.modal.open() 调用,无法在纯 JS 文件(如 Axios 拦截器、Router 守卫)中直接唤起。
  • 功能单一:通常只支持简单的标题+内容,面对复杂的“多条公告翻页”、“富文本展示”等需求无能为力。

2. 创新方案:HjModal

我们构建了一个全局单例的弹窗系统,它像操作系统的系统级弹窗一样,可以在任何时间、任何地点被唤起。


二、 核心架构设计

1. 视图层:全局挂载

App.ku.vue 或 Layout 根节点中,我们只放置唯一一个 <hj-modal-global> 组件。

<!-- src/components/hjModal/index.vue -->
<template>
  <hj-modal v-model="store.show" v-bind="store.state" @confirm="store.handleConfirm" @cancel="store.handleCancel">
    <slot />
  </hj-modal>
</template>

<script setup lang="ts">
  import { useModalStore } from '@/store/hjModal/index';
  // 这个组件直接与 Pinia Store 绑定
  const store = useModalStore();
</script>

2. 逻辑层:Store 驱动

整个弹窗的状态(显示/隐藏、标题、内容、回调函数)完全由 Pinia Store 管理。这使得我们可以通过操作 Store 来驱动视图。

核心 Store 实现 (src/store/hjModal/index.ts):

export const useModalStore = defineStore('modal', () => {
    // 弹窗队列(支持连续弹出)
    const queue = ref<ModalOptions[]>([]);
    const show = ref(false);

    // 扁平化状态,方便直接 v-bind 到组件
    const state = reactive({
        title: '提示',
        content: '',
        typeCode: 'alert', // 支持 alert, confirm, notice
        // ...
    });

    // 核心方法:打开弹窗
    const openModal = (options: ModalOptions) => {
        queue.value.push(options);
        processQueue(); // 触发队列处理
    };

    // 支持 Promise 调用的 Confirm
    const handleConfirm = async () => {
        if (onConfirmCallback.value) {
            await onConfirmCallback.value();
        }
        closeModal();
    };

    return { openModal, state, show, ... };
});

3. 能力层:Hook 封装

为了应对复杂的业务逻辑(如公告的翻页、接口请求),我们将核心交互逻辑抽离到了 Hook 中。

src/components/hjModal/useHjModal.ts:

export function useHjModal(props, emit) {
    // 内部维护公告列表
    const internalNoticeList = ref([]);

    // 自动加载远程公告
    const initNoticeList = async () => {
        if (props.noticeApi) {
            const res = await request(props.noticeApi, ...);
            internalNoticeList.value = res.list;
        }
    };

    // 翻页逻辑
    const nextNotice = () => { ... };

    return { internalNoticeList, nextNotice, ... };
}

三、 实战场景

场景 1:最简单的命令式调用

在任何 JS 文件中(例如拦截器捕获到 401 错误):

import { useModalStore } from '@/store/hjModal';

// 无需引入组件,直接调用
useModalStore().openModal({
  title: '登录过期',
  content: '您的会话已失效,请重新登录',
  onConfirm: () => {
    router.push('/login');
  }
});

场景 2:多层级公告(复杂交互)

系统需要弹出一个包含多条更新日志的公告,支持翻页查看,甚至在点击某条公告详情时叠加新的弹窗层级。

useModalStore().openModal({
  typeCode: 'notice', // 切换为公告模式
  title: '系统更新',
  noticeApi: '/api/system/notices', // 告诉弹窗去哪里拉取数据
  noticeParams: { version: '1.0.0' }
});

此时,全局弹窗组件会自动:

  1. 显示 Loading 状态。
  2. 调用 noticeApi 获取数据(支持多条数据)。
  3. 渲染出带有“上一页/下一页”按钮的富文本内容区域,实现翻页效果。
  4. 多层级支持:如果当前已有弹窗,新的 openModal 调用会将新弹窗推入 queue 队列或根据策略直接叠加覆盖,实现层级递进体验。
  5. 用户看完点击关闭后,自动销毁当前层,展示下一层或彻底关闭。

四、 方案优势

  1. 解耦:视图与逻辑完全分离,业务代码中不再混杂弹窗的 HTML 结构。
  2. 全域调用:打破了组件层级的限制,任何模块(Store、Router、Utils)皆可唤起 UI 交互。
  3. 高扩展性:通过 typeCode 和 Hook 机制,我们可以轻松扩展出“输入框弹窗”、“图片广告弹窗”等多种形态,而无需修改调用方的代码。
  4. 队列管理:内置队列机制,确保多个弹窗指令同时发出时,能按顺序依次展示,不会重叠冲突。

这套体系将弹窗从一个“UI 组件”升级为了一种“系统服务”,极大地提升了交互开发的灵活性和效率。