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

一、环境准备

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?.();
}
相关推荐
努力奋斗12 分钟前
npm ERR! code CERT_HAS_EXPIRED:解决证书过期问题
前端·npm·node.js
༺๑Tobias๑༻18 分钟前
Linux下Redis常用命令
linux·前端·redis
寅时码1 小时前
我开源了一款 Canvas “瑞士军刀”,十几种“特效与工具”开箱即用
前端·开源·canvas
CF14年老兵1 小时前
🚀 React 面试 20 题精选:基础 + 实战 + 代码解析
前端·react.js·redux
CF14年老兵1 小时前
2025 年每个开发人员都应该知道的 6 个 VS Code AI 工具
前端·后端·trae
十五_在努力1 小时前
参透 JavaScript —— 彻底理解 new 操作符及手写实现
前端·javascript
拾光拾趣录2 小时前
🔥99%人答不全的安全链!第5问必翻车?💥
前端·面试
IH_LZH2 小时前
kotlin小记(1)
android·java·前端·kotlin
lwlcode2 小时前
前端大数据渲染性能优化 - 分时函数的封装
前端·javascript
Java技术小馆2 小时前
MCP是怎么和大模型交互
前端·面试·架构