状态未保存,拦截页面跳转通用方法

一、环境准备

vue + ant-design-vue + ts + mitt

具体效果:如果监听的值发生变化,且未保存时,点击目录或者更改url地址等进行路由跳转的操作,会出现一个弹窗进行确认

二、代码用例

  1. 事件总线
ts 复制代码
// utils/eventBus.ts
import mitt from 'mitt'; 
export default mitt();
  1. 拦截通用方法
ts 复制代码
// useRouteLeaveGuard.ts
import { computed, onBeforeUnmount, onMounted, ref, Ref, watch } from 'vue';
import { useRouter, RouteLocationNormalized, NavigationGuardNext } from 'vue-router';
import { Modal } from 'ant-design-vue';
import { cloneDeep, debounce } from 'lodash-es';

import mitt from '/@/utils/eventBus';
// 定义事件类型
const RouteLeaveEventKey = Symbol();

interface RouteLeaveGuard {
  hasUnsavedChange?: Ref<boolean>;
  hasUnsavedContent?: Ref<any>;
  message?: string;
}
// 定义全局状态类型
export interface GuardStatus {
  allowJump: boolean;
  removeGuard?: () => void;
}

// 监听路由守卫
export function useRouteLeaveGuard({
  hasUnsavedChange,
  hasUnsavedContent,
  message = '您有未保存的更改,确定要离开吗?',
}: RouteLeaveGuard) {
  const router = useRouter();
  // 路由守卫
  let currentGuard;
  // 保存传入hasUnsavedContent初始值
  const initialData = ref(cloneDeep(hasUnsavedContent?.value));
  // 判断传入hasUnsavedContent是否发生了更改
  const isDirty = ref<boolean>(false);

  // 暴露重置脏状态的方法
  const resetDirtyState = () => {
    initialData.value = cloneDeep(hasUnsavedContent?.value);
    isDirty.value = false;
  };

  // 当hasUnsavedChange 和 isDirty 均为false时 允许跳转
  const allowJump = computed(() => {
    return !hasUnsavedChange?.value && !isDirty.value;
  });

  const handleRouteChange = async (_, from: RouteLocationNormalized, next: NavigationGuardNext) => {
    if (allowJump.value) {
      next();
      return;
    }
    try {
      const confirmed = await new Promise<boolean>((resolve) => {
        Modal.confirm({
          title: '确认离开',
          content: message,
          onOk: () => resolve(true),
          onCancel: () => resolve(false),
        });
      });

      if (confirmed) {
        next();
      } else {
        next(false);
        // 处理浏览器后退按钮的特殊情况
        if (window.history.state && window.history.state.forward === from.path) {
          window.history.pushState(null, '', from.fullPath);
        }
      }
    } catch (error) {
      console.error(error);
    }
  };

  // 处理页面刷新/关闭
  const handleBeforeUnload = (e: BeforeUnloadEvent) => {
    if (!allowJump.value) {
      e.preventDefault();
      e.returnValue = message;
      return message;
    }
  };

  // 注册路由守卫
  const setupGuard = () => {
    currentGuard = router.beforeEach(handleRouteChange);
  };

  // 清除路由守卫
  const removeGuard = () => {
    if (currentGuard) currentGuard();
  };

  onMounted(() => {
    setupGuard();
    window.addEventListener('beforeunload', handleBeforeUnload);
  });

  onBeforeUnmount(() => {
    removeGuard();
    mitt.emit(RouteLeaveEventKey, {
      allowJump: true,
    });
    window.removeEventListener('beforeunload', handleBeforeUnload);
  });

  // 监听传入的hasUnsavedContent是否发生了改变
  if (hasUnsavedContent) {
    watch(
      () => hasUnsavedContent.value,
      debounce((newVal) => {
        if (initialData.value === undefined) {
          initialData.value = cloneDeep(newVal); // 初始化
        } else {
          isDirty.value = JSON.stringify(newVal) !== JSON.stringify(initialData);
        }
      }, 300),
      { deep: true, immediate: true }
    );
  }

  // 状态变化时触发全局事件
  watch(
    allowJump,
    (newVal) => {
      mitt.emit(RouteLeaveEventKey, {
        allowJump: newVal,
        removeGuard,
      });
    },
    { immediate: true }
  );

  // 返回 resetDirtyState 供外部调用(关键新增)
  return { resetDirtyState };
}

// 监听状态变化的函数
export function useRouteLeaveListener(callback: (allowed: GuardStatus) => void) {
  // 注册监听回调
  const handler = (status: GuardStatus) => callback(status);

  mitt.on(RouteLeaveEventKey, handler);

  onBeforeUnmount(() => {
    mitt.off(RouteLeaveEventKey, handler);
  });
}

三、通用方法在 vue中具体使用

ts 复制代码
import { useRouteLeaveGuard } from 'useRouteLeaveGuard';
 
const formData = ref({ name: '' });
const hasUnsavedChange = ref(false);

const { resetDirtyState } = useRouteLeaveGuard({
  hasUnsavedChange,
  hasUnsavedContent: formData, // 传入需要检测的内容
});

// 保存操作
const handleSave = async () => {
  await api.saveData(formData.value);
  hasUnsavedChange.value = false; // 标记已保存
  resetDirtyState(); // 重置脏状态
};

// 如果场景复杂:可通过hasUnsavedChange一个字段进行判断

四、事件总线使用(特殊场景情况使用)

在另一处代码,获取当前使用拦截功能页面的保存状态

ts 复制代码
import { useRouteLeaveListener, GuardStatus } from 'useRouteLeaveGuard';

const leaveStatus = ref<GuardStatus>({
  allowJump: true,
});
// 状态发生变化时回调
useRouteLeaveListener((status: GuardStatus) => {
  leaveStatus.value = status;
});

// 获取具体的状态值,进行判断
if(leaveStatus.value?.allowJump){
    // 可以跳转
} else {
    // 如果允许跳转
    leaveStatus.value?.removeGuard?.();
}
相关推荐
正在学习前端的---小方同学2 小时前
vue-easy-tree树状结构
前端·javascript·vue.js
键盘不能没有CV键5 小时前
【图片处理】✈️HTML转图片字体异常处理
前端·javascript·html
yantuguiguziPGJ6 小时前
WPF 联合 Web 开发调试流程梳理(基于 Microsoft.Web.WebView2)
前端·microsoft·wpf
大飞记Python6 小时前
部门管理|“编辑部门”功能实现(Django5零基础Web平台)
前端·数据库·python·django
tsumikistep7 小时前
【前端】前端运行环境的结构
前端
你的人类朋友7 小时前
【Node】认识multer库
前端·javascript·后端
Aitter7 小时前
PDF和Word文件转换为Markdown的技术实现
前端·ai编程
mapbar_front8 小时前
面试问题—上家公司的离职原因
前端·面试
昔人'9 小时前
css使用 :where() 来简化大型 CSS 选择器列表
前端·css
昔人'9 小时前
css `dorp-shadow`
前端·css