feat: 表单保存交互规范P0 — 全局未保存离开守卫(useFormGuard)+MapCanvas接入
This commit is contained in:
@@ -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()
|
||||
})
|
||||
}
|
||||
@@ -6,46 +6,46 @@ const routes = [
|
||||
{ path: '/', component: () => import('@/layouts/MainLayout.vue'), redirect: '/my-dashboard',
|
||||
children: [
|
||||
{ path: 'dashboard', name: 'Dashboard', component: () => import('@/views/Dashboard.vue'), meta: { title: '经营看板', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
{ path: 'kpis', name: 'KPIs', component: () => import('@/views/KPIList.vue'), meta: { title: 'KPI字典', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
{ path: 'kpis/:id', name: 'KPIDetail', component: () => import('@/views/KPIDetail.vue'), meta: { title: 'KPI详情', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
{ path: 'maps', name: 'Maps', component: () => import('@/views/MapList.vue'), meta: { title: '战略地图', roles: ['ceo', 'finance'] } },
|
||||
{ path: 'kpis', name: 'KPIs', component: () => import('@/views/KPIList.vue'), meta: { title: 'KPI字典', roles: ['ceo', 'finance', 'business', 'it'], editable: true } },
|
||||
{ path: 'kpis/:id', name: 'KPIDetail', component: () => import('@/views/KPIDetail.vue'), meta: { title: 'KPI详情', roles: ['ceo', 'finance', 'business', 'it'], editable: true } },
|
||||
{ path: 'maps', name: 'Maps', component: () => import('@/views/MapList.vue'), meta: { title: '战略地图', roles: ['ceo', 'finance'], editable: true } },
|
||||
{ path: 'customer', name: 'CustomerDimension', component: () => import('@/views/CustomerDashboard.vue'), meta: { title: '客户维度', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
{ path: 'learning-dashboard', name: 'LearningDashboard', component: () => import('@/views/LearningDashboard.vue'), meta: { title: '学习成长看板', roles: ['ceo', 'finance', 'it'] } },
|
||||
{ path: 'maps-review', name: 'MapReviewList', component: () => import('@/views/MapReview.vue'), meta: { title: '战略回顾会', roles: ['ceo', 'finance'] } },
|
||||
{ path: 'maps/canvas/:id', name: 'MapCanvas', component: () => import('@/views/MapCanvas.vue'), meta: { title: '战略地图画布', roles: ['ceo', 'finance'] } },
|
||||
{ path: 'maps/canvas/:id', name: 'MapCanvas', component: () => import('@/views/MapCanvas.vue'), meta: { title: '战略地图画布', roles: ['ceo', 'finance'], editable: true } },
|
||||
{ path: 'maps/review/:id', name: 'MapReview', component: () => import('@/views/MapReview.vue'), meta: { title: '战略回顾会', roles: ['ceo', 'finance'] } },
|
||||
{ path: 'my-dashboard', name: 'MyDashboard', component: () => import('@/views/MyDashboard.vue'), meta: { title: '我的工作台', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
{ path: 'knowledge', name: 'CMAKnowledge', component: () => import('@/views/CMAKnowledge.vue'), meta: { title: 'CMA知识库', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
{ path: 'knowledge', name: 'CMAKnowledge', component: () => import('@/views/CMAKnowledge.vue'), meta: { title: 'CMA知识库', roles: ['ceo', 'finance', 'business', 'it'], editable: true } },
|
||||
{ path: 'guide', name: 'NewUserGuide', component: () => import('@/views/NewUserGuide.vue'), meta: { title: '新手引导', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
{ path: 'alerts', name: 'Alerts', component: () => import('@/views/AlertList.vue'), meta: { title: '预警中心', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
{ path: 'alerts', name: 'Alerts', component: () => import('@/views/AlertList.vue'), meta: { title: '预警中心', roles: ['ceo', 'finance', 'business', 'it'], editable: true } },
|
||||
{ path: 'data', name: 'Data', component: () => import('@/views/DataManage.vue'), meta: { title: '数据管理', roles: ['ceo', 'finance', 'it'] } },
|
||||
{ path: 'users', name: 'Users', component: () => import('@/views/UserManage.vue'), meta: { title: '用户管理', roles: ['ceo', 'it'] } },
|
||||
{ path: 'notifications', name: 'Notifications', component: () => import('@/views/NotificationManage.vue'), meta: { title: '通知配置', roles: ['ceo', 'it'] } },
|
||||
{ path: 'permissions', name: 'RolePermissions', component: () => import('@/views/RolePermissions.vue'), meta: { title: '系统设置', roles: ['ceo', 'it'] } },
|
||||
{ path: 'budget', name: 'BudgetManagement', component: () => import('@/views/BudgetManagement.vue'), meta: { title: '预算管理', roles: ['ceo', 'finance', 'it'] } },
|
||||
{ path: 'org', name: 'OrgManage', component: () => import('@/views/OrgManage.vue'), meta: { title: '组织管理', roles: ['ceo', 'it'] } },
|
||||
{ path: 'users', name: 'Users', component: () => import('@/views/UserManage.vue'), meta: { title: '用户管理', roles: ['ceo', 'it'], editable: true } },
|
||||
{ path: 'notifications', name: 'Notifications', component: () => import('@/views/NotificationManage.vue'), meta: { title: '通知配置', roles: ['ceo', 'it'], editable: true } },
|
||||
{ path: 'permissions', name: 'RolePermissions', component: () => import('@/views/RolePermissions.vue'), meta: { title: '系统设置', roles: ['ceo', 'it'], editable: true } },
|
||||
{ path: 'budget', name: 'BudgetManagement', component: () => import('@/views/BudgetManagement.vue'), meta: { title: '预算管理', roles: ['ceo', 'finance', 'it'], editable: true } },
|
||||
{ path: 'org', name: 'OrgManage', component: () => import('@/views/OrgManage.vue'), meta: { title: '组织管理', roles: ['ceo', 'it'], editable: true } },
|
||||
{ path: 'deviations', name: 'DeviationDashboard', component: () => import('@/views/DeviationDashboard.vue'), meta: { title: '差异分析', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
{ path: 'cost', name: 'CostDashboard', component: () => import('@/views/CostDashboard.vue'), meta: { title: '成本分析', roles: ['ceo', 'finance', 'it'] } },
|
||||
{ path: 'predict', name: 'PredictDashboard', component: () => import('@/views/PredictDashboard.vue'), meta: { title: '预测模拟', roles: ['ceo', 'finance', 'it'] } },
|
||||
{ path: 'predict/accuracy', name: 'PredictAccuracy', component: () => import('@/views/PredictAccuracy.vue'), meta: { title: '预测准确率', roles: ['ceo', 'finance', 'it'] } },
|
||||
{ path: 'real-options', name: 'RealOptions', component: () => import('@/views/RealOptions.vue'), meta: { title: '实物期权计算器', roles: ['ceo', 'finance'] } },
|
||||
{ path: 'action-plans', name: 'ActionPlans', component: () => import('@/views/ActionPlanLibrary.vue'), meta: { title: '改善行动', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
{ path: 'action-plans', name: 'ActionPlans', component: () => import('@/views/ActionPlanLibrary.vue'), meta: { title: '改善行动', roles: ['ceo', 'finance', 'business', 'it'], editable: true } },
|
||||
{ path: 'reports', name: 'ReportCenter', component: () => import('@/views/ReportCenter.vue'), meta: { title: '管理报表', roles: ['ceo', 'finance', 'business'] } },
|
||||
{ path: 'alignment', name: 'KPIAlignment', component: () => import('@/views/KPIAlignment.vue'), meta: { title: '战略执行看板', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
{ path: 'alignment', name: 'KPIAlignment', component: () => import('@/views/KPIAlignment.vue'), meta: { title: '战略执行看板', roles: ['ceo', 'finance', 'business', 'it'], editable: true } },
|
||||
{ path: 'dupont-analysis', name: 'DupontAnalysis', component: () => import('@/views/DupontAnalysis.vue'), meta: { title: '杜邦分析', roles: ['ceo', 'finance', 'it'] } },
|
||||
{ path: 'data-quality', name: 'DataQuality', component: () => import('@/views/DataQuality.vue'), meta: { title: '数据质量', roles: ['ceo', 'finance', 'it'] } },
|
||||
{ path: 'bi-reports', name: 'BiReports', component: () => import('@/views/BIReportCenter.vue'), meta: { title: 'BI报表', roles: ['ceo', 'finance', 'business'] } },
|
||||
{ path: 'okr-templates', name: 'OKRTemplates', component: () => import('@/views/OKRTemplates.vue'), meta: { title: 'OKR模板库', roles: ['ceo', 'finance', 'it'] } },
|
||||
{ path: 'okr-templates', name: 'OKRTemplates', component: () => import('@/views/OKRTemplates.vue'), meta: { title: 'OKR模板库', roles: ['ceo', 'finance', 'it'], editable: true } },
|
||||
{ path: 'okr/:id', name: 'OkrDetail', component: () => import('@/views/OkrDetail.vue'), meta: { title: 'OKR详情', roles: ['ceo', 'finance', 'it'] } },
|
||||
{ path: 'subjects', name: 'SubjectManage', component: () => import('@/views/SubjectManage.vue'), meta: { title: '科目打标', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
{ path: 'mpm-calculator', name: 'MpmCalculator', component: () => import('@/views/MpmCalculator.vue'), meta: { title: 'MPM计算器', roles: ['ceo', 'finance', 'business'] } },
|
||||
{ path: 'bot-kpis', name: 'BotKpis', component: () => import('@/views/BotKpiDashboard.vue'), meta: { title: 'Bot KPI看板', roles: ['ceo', 'finance', 'it'] } },
|
||||
{ path: 'analysis-confidence', name: 'AnalysisConfidence', component: () => import('@/views/AnalysisConfidence.vue'), meta: { title: '分析置信度', roles: ['ceo', 'finance', 'it'] } },
|
||||
{ path: 'expenses', name: 'ExpenseManage', component: () => import('@/views/ExpenseManage.vue'), meta: { title: '费用审核', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
{ path: 'cash-plan', name: 'CashPlan', component: () => import('@/views/CashPlan.vue'), meta: { title: '收付款计划', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
{ path: 'expenses', name: 'ExpenseManage', component: () => import('@/views/ExpenseManage.vue'), meta: { title: '费用审核', roles: ['ceo', 'finance', 'business', 'it'], editable: true } },
|
||||
{ path: 'cash-plan', name: 'CashPlan', component: () => import('@/views/CashPlan.vue'), meta: { title: '收付款计划', roles: ['ceo', 'finance', 'business', 'it'], editable: true } },
|
||||
{ path: 'receivables', name: 'Receivables', component: () => import('@/views/Receivables.vue'), meta: { title: '应收款催收', roles: ['ceo', 'finance', 'business'] } },
|
||||
{ path: 'growth-quality', name: 'GrowthQuality', component: () => import('@/views/GrowthQuality.vue'), meta: { title: '增长质量诊断', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
{ path: 'tax-compliance', name: 'TaxCompliance', component: () => import('@/views/TaxCompliance.vue'), meta: { title: '税务合规', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
{ path: 'tax-compliance', name: 'TaxCompliance', component: () => import('@/views/TaxCompliance.vue'), meta: { title: '税务合规', roles: ['ceo', 'finance', 'business', 'it'], editable: true } },
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
// 保存初始快照用于脏检测
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user