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

一、环境准备

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?.();
}
相关推荐
Highcharts.js10 分钟前
缺失数据可视化图表开发实战|Highcharts创建人员出生统计面积图表示例
开发语言·前端·javascript·信息可视化·highcharts·图表开发
LaughingZhu7 小时前
Product Hunt 每日热榜 | 2026-05-21
前端·人工智能·经验分享·chatgpt·html
怕浪猫7 小时前
Electron 开发实战(一):从零入门核心基础与环境搭建
前端·electron·ai编程
小鹏linux8 小时前
Ubuntu 22.04 部署开源免费具有精美现代web页面的Casdoor账号管理系统
linux·前端·ubuntu·开源·堡垒机
前端若水9 小时前
会话管理:创建、切换、删除对话历史
前端·人工智能·python·react.js
Bigger9 小时前
mini-cc:一个轻量级 AI 编程助手的诞生
前端·ai编程·claude
涵涵(互关)9 小时前
Naive-ui树型选择器只显示根节点
前端·ui·vue
BY组态9 小时前
Ricon组态系统最佳实践:从零开始构建物联网监控平台
前端·物联网·iot·web组态·组态
BY组态9 小时前
Ricon组态系统vs传统组态软件:为什么选择新一代Web组态平台
前端·物联网·iot·web组态·组态
SoaringHeart9 小时前
Flutter进阶:OverlayEntry 插入图层管理器 NOverlayZIndexManager
前端·flutter