feat: 表单保存交互规范P0 — 全局未保存离开守卫(useFormGuard)+MapCanvas接入

This commit is contained in:
Hermes CI Fix
2026-08-16 11:39:26 +08:00
parent acf0665010
commit 40d743fc96
6 changed files with 130 additions and 50 deletions
+81
View File
@@ -0,0 +1,81 @@
import { watch, isRef, onBeforeUnmount, type Ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessageBox } from 'element-plus'
/**
* 统一"未保存离开守卫"(CMA表单保存交互规范 P0
*
* 用法(页面 setup 中):
* const isDirty = ref(false)
* const { markDirty, markClean } = useFormGuard(isDirty)
* 表单数据变化时 markDirty();保存成功后 markClean()
*
* 行为:
* 1. 路由切换前检查:从 meta.editable 页面离开且 isDirty 时弹确认框
* 2. 浏览器关闭/刷新前检查:beforeunload 原生提示
* 组件卸载时自动注销守卫与事件监听,避免全局钩子累积。
*/
export function useFormGuard(isDirty: Ref<boolean>) {
const router = useRouter()
// 路由切换前检查(组件卸载时自动注销)
const removeGuard = router.beforeEach((to, from, next) => {
// 同页 query/hash 变化不算离开(如筛选条件更新URL),不触发守卫
if (to.path === from.path) {
next()
return
}
if (isDirty.value && from.meta.editable) {
ElMessageBox.confirm('有未保存的修改,确定离开吗?', '提示', {
confirmButtonText: '离开',
cancelButtonText: '继续编辑',
type: 'warning',
}).then(() => {
isDirty.value = false
next()
}).catch(() => {
next(false)
})
} else {
next()
}
})
// 浏览器关闭/刷新前检查
const onBeforeUnload = (e: BeforeUnloadEvent) => {
if (isDirty.value) {
e.preventDefault()
e.returnValue = ''
}
}
window.addEventListener('beforeunload', onBeforeUnload)
onBeforeUnmount(() => {
removeGuard()
window.removeEventListener('beforeunload', onBeforeUnload)
})
return {
markDirty: () => { isDirty.value = true },
markClean: () => { isDirty.value = false },
}
}
/**
* 弹窗表单脏追踪:弹窗打开时记录表单快照,内容变化后触发 onDirty,关闭时重置。
* 适用于"弹窗编辑"类页面(KPIList/OrgManage/UserManage/ExpenseManage/TaxCompliance/CashPlan/AlertList 等)。
* form 可为 ref 或 reactive 对象。
*
* 用法:
* trackDialogForm(showForm, form, markDirty)
*/
export function trackDialogForm(open: Ref<boolean>, form: any, onDirty: () => void) {
let snap = ''
const get = () => JSON.stringify(isRef(form) ? form.value : form)
watch(open, (v) => { if (!v) snap = '' })
watch(get, (v) => {
if (!open.value) return
if (!snap) snap = v
else if (v !== snap) onDirty()
})
}