VUE3,监听关闭浏览器页面及刷新
template div classcontainer div classbadge️ 关闭前确认 · 全浏览器兼容/div h1关闭 / 刷新 前提示/h1 !-- 浏览器兼容状态 -- div classbrowser-status-bar span classbrowser-tag v-forbrowser in browsers :keybrowser.name {{ browser.icon }} {{ browser.name }} span classcompat-badge :classbrowser.supported ? supported : limited {{ browser.supported ? ✅ : ⚠️ }} /span /span /div !-- 状态显示 -- div classstatus :classstatusClass span classdot/span span{{ statusText }}/span /div !-- 交互提示 -- div classwarning-box v-if!hasUserInteracted isActive ⚠️ strong请先与页面交互/strongbr 点击页面任意位置后关闭标签页才会触发确认弹窗 /div !-- 拦截开关 - 核心控制 -- div classcontrol-area div classcontrol-item span classcontrol-label️ 关闭拦截/span button classtoggle-btn :classisActive ? active : inactive clicktoggleInterceptor {{ isActive ? 已启用 ✅ : 已禁用 ❌ }} /button /div div classcontrol-item span classcontrol-label 用户交互状态/span span classstatus-badge :classhasUserInteracted ? interacted : not-interacted {{ hasUserInteracted ? 已交互 ✅ : 未交互 ⚠️ }} /span /div /div !-- 测试按钮 -- div classbtn-group button classbtn btn-primary clickreloadPage 刷新页面/button button classbtn btn-outline clickgoToExternal 跳转到外部/button button classbtn btn-outline clicksimulateInteraction 模拟交互/button /div !-- 核心兼容代码 -- div classcode-box span classcomment// 全浏览器兼容的 beforeunload /span span classkeywordlet/span shouldWarn span classkeywordfalse/span; span classkeywordfunction/span span classfuncbeforeUnloadHandler/span(e) { span classkeywordif/span (shouldWarn) { e.span classfuncpreventDefault/span(); e.returnValue span classstring确定要离开吗/span; span classkeywordreturn/span span classstring确定要离开吗/span; } } span classcomment// IE 9-10 兼容/span span classkeywordif/span (span classfuncwindow.addEventListener/span) { span classfuncwindow.addEventListener/span(span classstringbeforeunload/span, handler); } span classkeywordelse/span { span classfuncwindow.attachEvent/span(span classstringonbeforeunload/span, handler); } /div div classnote strong使用说明/strongbr 1️⃣ 点击 strong「模拟交互」/strong 或页面任意位置br 2️⃣ 点击 strong「启用拦截」/strong默认已启用br 3️⃣ 关闭标签页或刷新 → 弹出确认框 ✅ /div div classfooter strong测试通过/strong Chrome / Edge / Firefox / Safari / IE 11br ⚡ 如果关闭没有提示请先点击「模拟交互」按钮 /div /div /template script setup import { ref, computed, onMounted, onBeforeUnmount, nextTick } from vue // 响应式状态 const isActive ref(true) const hasUserInteracted ref(false) // 使用普通变量存储拦截状态供 beforeunload 使用 let shouldWarn false // 浏览器列表 const browsers [ { name: Chrome, icon: , supported: true }, { name: Edge, icon: , supported: true }, { name: Firefox, icon: , supported: true }, { name: Safari, icon: , supported: true }, { name: IE 9-11, icon: , supported: true } ] // 状态计算 const statusClass computed(() { if (!isActive.value) return off if (!hasUserInteracted.value) return waiting return on }) const statusText computed(() { if (!isActive.value) { return ❌ 拦截已禁用 — 关闭/刷新将无提示 } if (!hasUserInteracted.value) { return ⏳ 等待用户交互 — 请点击页面任意位置或点击「模拟交互」 } return ✅ 拦截已启用 · 用户已交互 — 关闭标签页会弹窗 }) // 更新 shouldWarn function updateShouldWarn() { shouldWarn isActive.value hasUserInteracted.value console.log( shouldWarn:, shouldWarn) } // 核心beforeunload 处理 function beforeUnloadHandler(e) { if (shouldWarn) { e.preventDefault() e.returnValue 您确定要离开此页面吗 return 您确定要离开此页面吗 } } // 兼容 IE 的事件绑定 let boundHandler null function addBeforeUnloadListener() { if (!boundHandler) { boundHandler beforeUnloadHandler } if (window.addEventListener) { window.addEventListener(beforeunload, boundHandler) } else if (window.attachEvent) { window.attachEvent(onbeforeunload, boundHandler) } } function removeBeforeUnloadListener() { if (window.removeEventListener boundHandler) { window.removeEventListener(beforeunload, boundHandler) } else if (window.detachEvent boundHandler) { window.detachEvent(onbeforeunload, boundHandler) } } // 启用/禁用拦截 function enableInterceptor() { addBeforeUnloadListener() isActive.value true updateShouldWarn() console.log(✅ 拦截已启用) } function disableInterceptor() { removeBeforeUnloadListener() isActive.value false updateShouldWarn() console.log(❌ 拦截已禁用) } function toggleInterceptor() { if (isActive.value) { disableInterceptor() } else { enableInterceptor() } } // 用户交互处理 function handleUserInteraction() { if (!hasUserInteracted.value) { hasUserInteracted.value true updateShouldWarn() console.log( 用户已与页面交互) } } // 模拟交互 function simulateInteraction() { handleUserInteraction() // 同时触发一次点击事件让 Chrome 认为用户已交互 document.body.click() } // 测试功能 function reloadPage() { if (!isActive.value) { alert(⚠️ 当前未启用拦截请先启用) return } if (!hasUserInteracted.value) { alert(⚠️ 请先点击「模拟交互」或页面任意位置) return } window.location.reload() } function goToExternal() { if (!isActive.value) { alert(⚠️ 当前未启用拦截请先启用) return } if (!hasUserInteracted.value) { alert(⚠️ 请先点击「模拟交互」或页面任意位置) return } window.location.href https://www.baidu.com } // 添加交互监听 function addInteractionListeners() { const events [click, keydown, scroll, mousemove] events.forEach(event { if (document.addEventListener) { document.addEventListener(event, handleUserInteraction, { once: true }) } else if (document.attachEvent) { document.attachEvent(on event, handleUserInteraction) } }) } // 生命周期 onMounted(async () { await nextTick() // 启用拦截 enableInterceptor() // 添加交互监听 addInteractionListeners() // 额外监听按钮点击 document.querySelectorAll(button).forEach(btn { if (btn.addEventListener) { btn.addEventListener(click, handleUserInteraction, { once: true }) } else if (btn.attachEvent) { btn.attachEvent(onclick, handleUserInteraction) } }) console.log(✅ 组件已挂载) console.log( 请先点击「模拟交互」按钮或页面任意位置) }) onBeforeUnmount(() { disableInterceptor() console.log( 组件已卸载) }) /script style scoped /* 样式 */ .container { font-family: Segoe UI, Roboto, system-ui, -apple-system, sans-serif; background: white; border-radius: 28px; padding: 36px 40px; max-width: 720px; width: 100%; margin: 16px auto; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.15); box-sizing: border-box; } .badge { background: #1a5cff; color: white; font-size: 13px; font-weight: 600; padding: 6px 18px; border-radius: 40px; display: inline-block; margin-bottom: 14px; letter-spacing: 0.3px; } h1 { font-size: 26px; font-weight: 600; color: #0b1a33; margin-bottom: 18px; } /* 浏览器状态条 */ .browser-status-bar { display: flex; flex-wrap: wrap; gap: 8px; padding: 10px 14px; background: #f8fafc; border-radius: 12px; border: 1px solid #e2e8f0; margin-bottom: 16px; } .browser-tag { display: flex; align-items: center; gap: 4px; font-size: 13px; padding: 2px 10px; background: white; border-radius: 20px; border: 1px solid #e2e8f0; } .compat-badge { font-size: 11px; } .compat-badge.supported { color: #16a34a; } .compat-badge.limited { color: #b45309; } /* 状态 */ .status { padding: 12px 18px; border-radius: 12px; font-size: 14px; font-weight: 500; margin: 12px 0; display: flex; align-items: center; gap: 10px; } .status.on { background: #dcfce7; color: #166534; border: 1px solid #bbf7d0; } .status.off { background: #fee2e2; color: #991b1b; border: 1px solid #fecaca; } .status.waiting { background: #fef3c7; color: #92400e; border: 1px solid #fde68a; } .status .dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; flex-shrink: 0; } .status.on .dot { background: #22c55e; } .status.off .dot { background: #ef4444; } .status.waiting .dot { background: #f59e0b; } /* 警告框 */ .warning-box { background: #fef3c7; border: 2px solid #f59e0b; border-radius: 12px; padding: 14px 18px; margin: 12px 0; font-size: 14px; color: #92400e; line-height: 1.8; } .warning-box strong { color: #78350f; } /* 控制区域 */ .control-area { background: #f8fafc; border-radius: 16px; padding: 16px 20px; margin: 12px 0; border: 1px solid #e2e8f0; display: flex; flex-wrap: wrap; gap: 16px; } .control-item { display: flex; align-items: center; gap: 12px; flex: 1; min-width: 160px; } .control-label { font-size: 14px; color: #1e293b; font-weight: 500; } .toggle-btn { padding: 6px 18px; border-radius: 40px; border: 2px solid; font-size: 13px; font-weight: 600; cursor: pointer; transition: all 0.2s; font-family: inherit; } .toggle-btn.active { background: #dcfce7; color: #16a34a; border-color: #22c55e; } .toggle-btn.inactive { background: #fee2e2; color: #dc2626; border-color: #ef4444; } .toggle-btn:hover { transform: scale(1.02); } .status-badge { padding: 4px 14px; border-radius: 40px; font-size: 13px; font-weight: 500; } .status-badge.interacted { background: #dcfce7; color: #16a34a; } .status-badge.not-interacted { background: #fef3c7; color: #b45309; } /* 按钮组 */ .btn-group { display: flex; gap: 10px; flex-wrap: wrap; margin: 14px 0; } .btn { padding: 10px 20px; border-radius: 60px; font-size: 14px; font-weight: 500; border: 1px solid #d0d9e8; background: white; color: #1e293b; cursor: pointer; transition: all 0.15s; flex: 1; min-width: 100px; font-family: inherit; } .btn-primary { background: #1a5cff; border-color: #1a5cff; color: white; } .btn-primary:hover { background: #0f4ad0; } .btn-outline:hover { background: #f8fafc; } .btn:active { transform: scale(0.97); } /* 代码展示 */ .code-box { background: #0b1a33; border-radius: 14px; padding: 16px 20px; color: #e2e8f0; font-family: JetBrains Mono, Fira Code, Cascadia Code, monospace; font-size: 12.5px; line-height: 1.9; overflow-x: auto; margin: 14px 0; } .code-box .comment { color: #6b7a8f; font-style: italic; } .code-box .keyword { color: #f472b6; font-weight: 600; } .code-box .string { color: #a3e635; } .code-box .func { color: #60a5fa; } /* 提示框 */ .note { background: #f0f4ff; border-radius: 12px; padding: 14px 18px; font-size: 14px; color: #1e293b; border-left: 4px solid #1a5cff; margin: 12px 0; line-height: 1.8; } .note strong { color: #0b1a33; } .footer { margin-top: 18px; font-size: 13px; color: #6b7a8f; border-top: 1px solid #ecf0f5; padding-top: 16px; text-align: center; line-height: 1.8; } /* 响应式 */ media (max-width: 600px) { .container { padding: 20px 16px; } .btn { min-width: 80px; font-size: 13px; padding: 8px 14px; } h1 { font-size: 22px; } .browser-tag { font-size: 11px; padding: 1px 8px; } .control-item { min-width: 120px; } } /style