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
+9
View File
@@ -329,6 +329,7 @@ import { ref, reactive, computed, onMounted, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { expenseApi } from '../api/index'
import MyDialog from '../components/MyDialog.vue'
import { useFormGuard, trackDialogForm } from '../composables/useFormGuard'
const EXPENSE_TYPES: Record<string, string> = {
entertainment: '招待费',
@@ -399,6 +400,13 @@ const reviewTitle = computed(() => {
return m[review.mode]
})
// ── 未保存离开守卫 ──
const isDirty = ref(false)
const { markDirty, markClean } = useFormGuard(isDirty)
trackDialogForm(ruleDialogVisible, ruleForm, markDirty)
trackDialogForm(submitVisible, submitForm, markDirty)
trackDialogForm(reviewVisible, review, markDirty)
// ── 图表 ──
const typeChartRef = ref<HTMLElement | null>(null)
@@ -626,6 +634,7 @@ async function saveRule() {
}
ruleDialogVisible.value = false
ElMessage.success('规则已保存')
markClean()
loadRules()
} catch (e: any) {
ElMessage.error('保存失败: ' + (e?.response?.data?.detail || e.message))
+6 -33
View File
@@ -268,13 +268,14 @@
</template>
<script setup lang="ts">
import { ref, onMounted, computed, watch, onBeforeUnmount } from 'vue'
import { useRoute, onBeforeRouteLeave } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ref, onMounted, computed, watch } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import VChart from 'vue-echarts'
import 'echarts'
import { kpiApi, mapApi, kpiCausalityApi, orgApi, userApi } from '../api/index'
import api from '../api/index'
import { useFormGuard } from '../composables/useFormGuard'
const route = useRoute()
const kpi = ref<any>(null)
@@ -301,38 +302,13 @@ const fullNetworkNodes = ref<any[]>([])
const fullNetworkEdges = ref<any[]>([])
const fullNetworkLoading = ref(false)
// 编辑未保存离开确认
// 编辑未保存离开确认(统一守卫 composable
const isDirty = ref(false)
useFormGuard(isDirty)
let kpiSnapshot: string = ''
let watchInitialized = false
// 路由离开确认
onBeforeRouteLeave((to, from, next) => {
if (isDirty.value) {
ElMessageBox.confirm('有未保存的修改,确定离开吗?', '提示', {
confirmButtonText: '离开',
cancelButtonText: '取消',
type: 'warning',
}).then(() => next()).catch(() => next(false))
} else {
next()
}
})
// 页面关闭/刷新确认
function onBeforeUnloadHandler(e: BeforeUnloadEvent) {
if (isDirty.value) {
e.preventDefault()
e.returnValue = ''
}
}
// 组件卸载时清理事件
onBeforeUnmount(() => {
window.removeEventListener('beforeunload', onBeforeUnloadHandler)
})
// 监听 kpi 变化设置脏标记(跳过初始化赋值)
watch(() => kpi.value, () => {
if (!kpi.value) return
@@ -657,9 +633,6 @@ watch(showFullNetwork, (val) => {
})
onMounted(async () => {
// 注册页面关闭/刷新确认
window.addEventListener('beforeunload', onBeforeUnloadHandler)
const r: any = await kpiApi.get(Number(route.params.id))
kpi.value = r.data || r
// 保存初始快照用于脏检测
+9
View File
@@ -358,6 +358,7 @@ import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { kpiApi, templateApi, dashboardApi, entityApi, userApi, orgApi } from '../api/index'
import MyDialog from '../components/MyDialog.vue'
import { useFormGuard, trackDialogForm } from '../composables/useFormGuard'
// ── 数据 ──
const kpis = ref<any[]>([])
@@ -417,6 +418,11 @@ const showForm = ref(false)
const editMode = ref(false)
const form = ref<any>({})
// ── 未保存离开守卫 ──
const isDirty = ref(false)
const { markDirty, markClean } = useFormGuard(isDirty)
trackDialogForm(showForm, form, markDirty)
// ── 维度映射 ──
const DIM_MAP: Record<string, string> = { finance: '财务', customer: '客户', process: '内部流程', learning: '学习成长' }
const CAT_MAP: Record<string, string> = {
@@ -613,6 +619,7 @@ async function saveKPI() {
ElMessage.success('创建成功')
}
showForm.value = false
markClean()
loadKpis()
loadCategories()
} catch (e) {
@@ -639,6 +646,7 @@ const templateKeyword = ref('')
const templateDimFilter = ref('')
const selectedTemplate = ref<any>(null)
const instantiateForm = ref<any>({})
trackDialogForm(showTemplateSelect, instantiateForm, markDirty)
async function loadTemplates() {
templateLoading.value = true
@@ -680,6 +688,7 @@ async function doInstantiate() {
await templateApi.instantiate(selectedTemplate.value.id, data)
ElMessage.success(`已从模板「${selectedTemplate.value.kpi_name}」创建KPI`)
showTemplateSelect.value = false
markClean()
selectedTemplate.value = null
instantiateForm.value = {}
loadKpis()
+8
View File
@@ -165,6 +165,7 @@ import { mapApi, kpiApi, okrTemplateApi } from "../api/index"
import KnowledgePanel from '../components/KnowledgePanel.vue'
import api from "../api/index"
import { LAYER_CONFIG, LAYER_KEYS, getLayerList } from "../config/layers"
import { useFormGuard } from "../composables/useFormGuard"
import StrategyLayer from "../components/strategy-map/StrategyLayer.vue"
import ConnectionLines from "../components/strategy/ConnectionLines.vue"
import MapCanvasToolbar from "../components/map-canvas/MapCanvasToolbar.vue"
@@ -179,6 +180,8 @@ const orderedLayerConfigs = getLayerList()
const maps = ref<any[]>([])
const selectedMap = ref<number | null>(null)
const currentMap = ref<any>(null)
const isDirty = ref(false)
const { markDirty, markClean } = useFormGuard(isDirty)
const allKpis = ref<any[]>([])
const kpiNameMap = reactive<Record<string, string>>({})
@@ -222,6 +225,7 @@ function onChainFocus(nodes: string[] | null) { chainFocusNodes.value = nodes }
function onUpdateConnection(idx: number, patch: any) {
if (!connections.value[idx]) return
Object.assign(connections.value[idx], patch)
markDirty()
saveConnections()
}
@@ -247,6 +251,7 @@ const editingLayerKey = ref('')
const dialogRefreshKey = ref(0)
const dialogForm = reactive({ name: '', description: '', icon: 'target', targetValue: null, currentValue: null, unit: '%', owner: '', isLeading: false, kpis: [] as string[] })
function onSaveDialog(data: any) {
markDirty()
// 新格式:O+KR 结构(来自两步弹窗)
if (data.o_name) {
if (!data.o_name?.trim()) { ElMessage.warning('请输入目标名称'); return }
@@ -489,11 +494,13 @@ function completeConnection(fromKey: string, toKey: string) {
}).then(() => {
ElMessage.success("连线已添加")
connections.value.push({ from: fromKey, to: toKey, style: "solid", effect: "positive", label: "" })
markDirty()
cancelLink()
nextTick(() => { triggerRecalc(); saveConnections() })
}).catch((e: any) => {
if (e?.response?.status !== 400) {
connections.value.push({ from: fromKey, to: toKey, style: "solid", effect: "positive", label: "" })
markDirty()
ElMessage.success("连线已添加本地")
} else {
ElMessage.warning(e?.response?.data?.detail || "连线失败")
@@ -779,6 +786,7 @@ async function saveCanvas(silent?: boolean) {
canvas_data: { connections: connections.value },
version_num: currentMap.value?.version_num,
})
markClean()
if (!silent) ElMessage.success("已保存")
} catch (e: any) {
if (e?.response?.status === 409) {