fix: 战略地图P0修复 — KPI code统一+放开同层连线+数据迁移
This commit is contained in:
@@ -8,96 +8,231 @@
|
||||
<el-tag v-if="kpi.dimension" :type="dimTagType(kpi.dimension)" size="small" style="margin-left:4px;">{{ dimLabel(kpi.dimension) }}</el-tag>
|
||||
<el-tag v-if="kpi.category" type="info" size="small" style="margin-left:4px;">{{ catLabel(kpi.category) }}</el-tag>
|
||||
</template>
|
||||
<el-form :model="kpi" label-width="120px" size="small">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="KPI编码">{{ kpi.kpi_code }}</el-form-item>
|
||||
<el-form-item label="KPI名称"><el-input v-model="kpi.kpi_name" /></el-form-item>
|
||||
<el-form-item label="维度">
|
||||
<el-select v-model="kpi.dimension" style="width:100%">
|
||||
<el-option label="财务" value="finance" />
|
||||
<el-option label="客户" value="customer" />
|
||||
<el-option label="内部流程" value="process" />
|
||||
<el-option label="学习成长" value="learning" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="二级类别">
|
||||
<el-select v-model="kpi.category" style="width:100%" :disabled="!kpi.dimension">
|
||||
<el-option v-for="c in categoryOptions" :key="c.value" :label="c.label" :value="c.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标值"><el-input-number v-model="kpi.target_value" :min="0" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="单位"><el-input v-model="kpi.unit" /></el-form-item>
|
||||
<el-form-item label="频率">
|
||||
<el-select v-model="kpi.frequency" style="width:100%">
|
||||
<el-option label="月度" value="monthly" />
|
||||
<el-option label="季度" value="quarterly" />
|
||||
<el-option label="年度" value="yearly" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="负责部门"><el-input v-model="kpi.responsible_dept" /></el-form-item>
|
||||
<el-form-item label="负责人"><el-input v-model="kpi.responsible_user" /></el-form-item>
|
||||
<el-form-item label="数据源">
|
||||
<el-select v-model="kpi.data_source_type" style="width:100%">
|
||||
<el-option label="ERP" value="erp" />
|
||||
<el-option label="Excel" value="excel" />
|
||||
<el-option label="手工" value="manual" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属Epic">
|
||||
<el-select v-model="kpi.epic" style="width:100%">
|
||||
<el-option v-for="i in 10" :key="i" :label="'Epic ' + i" :value="'Epic' + i" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="绿灯阈值">
|
||||
<div style="display:flex;gap:8px;"><el-input v-model="kpi.threshold_green" /><el-button size="small" type="primary" plain @click="suggestThreshold">自动建议</el-button></div>
|
||||
</el-form-item>
|
||||
<el-form-item label="黄灯阈值"><el-input v-model="kpi.threshold_yellow" /></el-form-item>
|
||||
<el-form-item label="红灯阈值"><el-input v-model="kpi.threshold_red" /></el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="计算公式"><el-input v-model="kpi.formula" type="textarea" :rows="2" /></el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item><el-button type="primary" @click="save">保存</el-button></el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="基本信息" name="basic">
|
||||
<el-form :model="kpi" label-width="120px" size="small">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="KPI编码">{{ kpi.kpi_code }}</el-form-item>
|
||||
<el-form-item label="KPI名称"><el-input v-model="kpi.kpi_name" /></el-form-item>
|
||||
<el-form-item label="维度">
|
||||
<el-select v-model="kpi.dimension" style="width:100%">
|
||||
<el-option label="财务" value="finance" />
|
||||
<el-option label="客户" value="customer" />
|
||||
<el-option label="内部流程" value="process" />
|
||||
<el-option label="学习成长" value="learning" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="二级类别">
|
||||
<el-select v-model="kpi.category" style="width:100%" :disabled="!kpi.dimension">
|
||||
<el-option v-for="c in categoryOptions" :key="c.value" :label="c.label" :value="c.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标值"><el-input-number v-model="kpi.target_value" :min="0" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="单位"><el-input v-model="kpi.unit" /></el-form-item>
|
||||
<el-form-item label="频率">
|
||||
<el-select v-model="kpi.frequency" style="width:100%">
|
||||
<el-option label="月度" value="monthly" />
|
||||
<el-option label="季度" value="quarterly" />
|
||||
<el-option label="年度" value="yearly" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="负责部门"><el-input v-model="kpi.responsible_dept" /></el-form-item>
|
||||
<el-form-item label="负责人"><el-input v-model="kpi.responsible_user" /></el-form-item>
|
||||
<el-form-item label="数据源">
|
||||
<el-select v-model="kpi.data_source_type" style="width:100%">
|
||||
<el-option label="ERP" value="erp" />
|
||||
<el-option label="Excel" value="excel" />
|
||||
<el-option label="手工" value="manual" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属Epic">
|
||||
<el-select v-model="kpi.epic" style="width:100%">
|
||||
<el-option v-for="i in 10" :key="i" :label="'Epic ' + i" :value="'Epic' + i" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关联战略地图">
|
||||
<div style="display:flex;gap:8px;width:100%;">
|
||||
<el-select v-model="kpi.map_id" placeholder="选择战略地图" clearable style="flex:1;" @change="onMapChange">
|
||||
<el-option v-for="m in mapOptions" :key="m.id" :label="m.title" :value="m.id" />
|
||||
</el-select>
|
||||
<el-button size="small" type="primary" plain @click="associateMap">关联</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="绿灯阈值">
|
||||
<div style="display:flex;gap:8px;"><el-input v-model="kpi.threshold_green" /><el-button size="small" type="primary" plain @click="suggestThreshold">自动建议</el-button></div>
|
||||
</el-form-item>
|
||||
<el-form-item label="黄灯阈值"><el-input v-model="kpi.threshold_yellow" /></el-form-item>
|
||||
<el-form-item label="红灯阈值"><el-input v-model="kpi.threshold_red" /></el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="计算公式"><el-input v-model="kpi.formula" type="textarea" :rows="2" /></el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item><el-button type="primary" @click="save">保存</el-button></el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="历史数据" name="history">
|
||||
<v-chart :option="chartOption" autoresize style="height:300px;width:100%;" />
|
||||
<el-table :data="values" size="small" style="width:100%;margin-top:12px;">
|
||||
<el-table-column prop="period" label="期间" width="120" />
|
||||
<el-table-column prop="actual_value" label="实际值" width="120" />
|
||||
<el-table-column prop="source_type" label="来源" width="80" />
|
||||
<el-table-column prop="data_status" label="状态" width="80">
|
||||
<template #default="{ row }"><el-tag :type="row.data_status==='verified'?'success':'warning'" size="small">{{ row.data_status }}</el-tag></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="因果链" name="causality">
|
||||
<div v-loading="networkLoading">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never">
|
||||
<template #header><b>上游驱动</b>(影响本KPI的因子)</template>
|
||||
<div v-if="upstream.length === 0" style="color:#999;text-align:center;padding:20px;">无上游驱动</div>
|
||||
<el-timeline v-else>
|
||||
<el-timeline-item v-for="item in upstream" :key="item.kpi_id"
|
||||
:color="item.direction === 'positive' ? '#67c23a' : '#f56c6c'"
|
||||
:timestamp="'强度:' + item.strength + ' / 滞后期:' + item.lag_months + '月'">
|
||||
<div><b>{{ item.kpi_name }}</b> ({{ item.kpi_code }})</div>
|
||||
<div style="font-size:12px;color:#999;">
|
||||
{{ item.direction === 'positive' ? '正向驱动' : '负向抑制' }}
|
||||
<span v-if="item.formula"> · {{ item.formula }}</span>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never">
|
||||
<template #header><b>下游影响</b>(本KPI影响的指标)</template>
|
||||
<div v-if="downstream.length === 0" style="color:#999;text-align:center;padding:20px;">无下游影响</div>
|
||||
<el-timeline v-else>
|
||||
<el-timeline-item v-for="item in downstream" :key="item.kpi_id"
|
||||
:color="item.direction === 'positive' ? '#67c23a' : '#f56c6c'"
|
||||
:timestamp="'强度:' + item.strength + ' / 滞后期:' + item.lag_months + '月'">
|
||||
<div><b>{{ item.kpi_name }}</b> ({{ item.kpi_code }})</div>
|
||||
<div style="font-size:12px;color:#999;">
|
||||
{{ item.direction === 'positive' ? '正向推动' : '负向抑制' }}
|
||||
<span v-if="item.formula"> · {{ item.formula }}</span>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<div style="margin-bottom:12px;">
|
||||
<el-button type="primary" @click="showSimulate = true">模拟推演</el-button>
|
||||
<el-button @click="showFullNetwork = true">查看全局因果网络图</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 因果链力导向图 -->
|
||||
<el-card v-if="networkEdges.length > 0" shadow="never">
|
||||
<template #header><b>因果网络图</b></template>
|
||||
<v-chart :option="networkChartOption" autoresize style="height:450px;width:100%;" />
|
||||
</el-card>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
|
||||
<el-card style="margin-top:16px;">
|
||||
<template #header>趋势图</template>
|
||||
<v-chart :option="chartOption" autoresize style="height:300px;width:100%;" />
|
||||
</el-card>
|
||||
<!-- 模拟推演对话框 -->
|
||||
<el-dialog v-model="showSimulate" title="因果链模拟推演" :width="700" top="5vh">
|
||||
<div v-if="simulateResult">
|
||||
<el-alert :title="'修改 ' + simulateResult.source.kpi_name + ' 从 ' + simulateResult.source.current_value + ' 到 ' + simulateResult.source.new_value + '(变化率 ' + simulateResult.source.change_pct + '%)'"
|
||||
type="info" show-icon :closable="false" style="margin-bottom:12px;" />
|
||||
<el-table :data="simulateResult.impacts" size="small" max-height="400" border>
|
||||
<el-table-column prop="kpi_name" label="受影响KPI" min-width="150" />
|
||||
<el-table-column prop="kpi_code" label="编码" width="120" />
|
||||
<el-table-column prop="dimension" label="维度" width="80">
|
||||
<template #default="{ row }">{{ row.dimension === 'finance' ? '财务' : row.dimension === 'customer' ? '客户' : row.dimension === 'process' ? '流程' : '学习' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="current_value" label="当前值" width="90" />
|
||||
<el-table-column prop="predicted_value" label="预测值" width="90">
|
||||
<template #default="{ row }"><span style="color:#409eff;font-weight:600;">{{ row.predicted_value ?? '-' }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="change_pct" label="变化率" width="80">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ color: row.change_pct > 0 ? '#67c23a' : '#f56c6c' }">{{ row.change_pct > 0 ? '+' : '' }}{{ row.change_pct }}%</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="path_strength" label="传递强度" width="80" />
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-form label-width="120px">
|
||||
<el-form-item label="选择KPI">
|
||||
<el-select v-model="simulateKpiId" filterable style="width:100%" placeholder="选择要修改的KPI">
|
||||
<el-option v-for="k in allKpis" :key="k.id" :label="k.kpi_name + ' (' + k.kpi_code + ')'" :value="k.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="当前值">{{ simulateCurrentValue ?? '无数据' }}</el-form-item>
|
||||
<el-form-item label="修改为">
|
||||
<el-input-number v-model="simulateNewValue" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showSimulate = false">关闭</el-button>
|
||||
<el-button v-if="!simulateResult" type="primary" @click="doSimulate" :loading="simulating">执行推演</el-button>
|
||||
<el-button v-else @click="resetSimulate">重新推演</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-card style="margin-top:16px;">
|
||||
<template #header>历史数据</template>
|
||||
<el-table :data="values" size="small" style="width:100%">
|
||||
<el-table-column prop="period" label="期间" width="120" />
|
||||
<el-table-column prop="actual_value" label="实际值" width="120" />
|
||||
<el-table-column prop="source_type" label="来源" width="80" />
|
||||
<el-table-column prop="data_status" label="状态" width="80">
|
||||
<template #default="{ row }"><el-tag :type="row.data_status==='verified'?'success':'warning'" size="small">{{ row.data_status }}</el-tag></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
<!-- 全局因果网络图对话框 -->
|
||||
<el-dialog v-model="showFullNetwork" title="全局因果网络图" :width="900" top="5vh">
|
||||
<div v-loading="fullNetworkLoading" style="height:550px;">
|
||||
<v-chart v-if="fullNetworkNodes.length > 0" :option="fullNetworkChartOption" autoresize style="height:550px;width:100%;" />
|
||||
<div v-else style="text-align:center;padding:40px;color:#999;">加载中...</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showFullNetwork = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
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 } from '../api/index'
|
||||
import { kpiApi, mapApi, kpiCausalityApi } from '../api/index'
|
||||
import api from '../api/index'
|
||||
|
||||
const route = useRoute()
|
||||
const kpi = ref<any>(null)
|
||||
const values = ref<any[]>([])
|
||||
const mapOptions = ref<any[]>([])
|
||||
const activeTab = ref('basic')
|
||||
|
||||
// 因果链
|
||||
const upstream = ref<any[]>([])
|
||||
const downstream = ref<any[]>([])
|
||||
const networkEdges = ref<any[]>([])
|
||||
const networkLoading = ref(false)
|
||||
const showSimulate = ref(false)
|
||||
const showFullNetwork = ref(false)
|
||||
const simulateKpiId = ref<number | null>(null)
|
||||
const simulateNewValue = ref(0)
|
||||
const simulateCurrentValue = ref<number | null>(null)
|
||||
const simulateResult = ref<any>(null)
|
||||
const simulating = ref(false)
|
||||
const allKpis = ref<any[]>([])
|
||||
const fullNetworkNodes = ref<any[]>([])
|
||||
const fullNetworkEdges = ref<any[]>([])
|
||||
const fullNetworkLoading = ref(false)
|
||||
|
||||
const CAT_MAP: Record<string, string> = {
|
||||
revenue_growth: '收入增长', profitability: '盈利水平', cost_control: '成本费用',
|
||||
@@ -133,6 +268,148 @@ const chartOption = computed(() => ({
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
}))
|
||||
|
||||
// 因果网络ECharts力导向图
|
||||
const networkChartOption = computed(() => {
|
||||
const nodeMap: Record<number, any> = {}
|
||||
const nodes: any[] = []
|
||||
const edges: any[] = []
|
||||
|
||||
// 中心节点
|
||||
if (kpi.value) {
|
||||
nodeMap[kpi.value.id] = {
|
||||
id: String(kpi.value.id),
|
||||
name: kpi.value.kpi_code,
|
||||
symbolSize: 50,
|
||||
itemStyle: { color: '#409eff' },
|
||||
category: 0,
|
||||
label: { fontSize: 12, fontWeight: 'bold' },
|
||||
}
|
||||
nodes.push(nodeMap[kpi.value.id])
|
||||
}
|
||||
|
||||
// 上游节点
|
||||
for (const item of upstream.value) {
|
||||
if (!nodeMap[item.kpi_id]) {
|
||||
nodeMap[item.kpi_id] = {
|
||||
id: String(item.kpi_id),
|
||||
name: item.kpi_code,
|
||||
symbolSize: 35,
|
||||
itemStyle: { color: '#67c23a' },
|
||||
category: 1,
|
||||
}
|
||||
nodes.push(nodeMap[item.kpi_id])
|
||||
}
|
||||
edges.push({
|
||||
source: String(item.kpi_id),
|
||||
target: String(kpi.value?.id),
|
||||
value: item.strength,
|
||||
lineStyle: { color: item.direction === 'positive' ? '#67c23a' : '#f56c6c', width: item.strength * 5 },
|
||||
label: { show: true, formatter: item.strength, fontSize: 10 },
|
||||
})
|
||||
}
|
||||
|
||||
// 下游节点
|
||||
for (const item of downstream.value) {
|
||||
if (!nodeMap[item.kpi_id]) {
|
||||
nodeMap[item.kpi_id] = {
|
||||
id: String(item.kpi_id),
|
||||
name: item.kpi_code,
|
||||
symbolSize: 35,
|
||||
itemStyle: { color: '#e6a23c' },
|
||||
category: 2,
|
||||
}
|
||||
nodes.push(nodeMap[item.kpi_id])
|
||||
}
|
||||
edges.push({
|
||||
source: String(kpi.value?.id),
|
||||
target: String(item.kpi_id),
|
||||
value: item.strength,
|
||||
lineStyle: { color: item.direction === 'positive' ? '#67c23a' : '#f56c6c', width: item.strength * 5 },
|
||||
label: { show: true, formatter: item.strength, fontSize: 10 },
|
||||
})
|
||||
}
|
||||
|
||||
for (const e of networkEdges.value) {
|
||||
const srcStr = String(e.source_kpi_id)
|
||||
const tgtStr = String(e.target_kpi_id)
|
||||
const exists = edges.some((ex: any) => ex.source === srcStr && ex.target === tgtStr)
|
||||
if (!exists && nodeMap[e.source_kpi_id] && nodeMap[e.target_kpi_id]) {
|
||||
edges.push({
|
||||
source: srcStr,
|
||||
target: tgtStr,
|
||||
lineStyle: { color: '#ddd', width: 1, type: 'dashed' as const },
|
||||
label: { show: false },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tooltip: { trigger: 'item' as const, formatter: (p: any) => `${p.name}<br/>强度: ${p.value ?? '-'}` },
|
||||
series: [{
|
||||
type: 'graph',
|
||||
layout: 'force',
|
||||
force: { repulsion: 500, edgeLength: [100, 300], gravity: 0.1 },
|
||||
roam: true,
|
||||
draggable: true,
|
||||
data: nodes,
|
||||
edges: edges,
|
||||
categories: [
|
||||
{ name: '当前KPI', itemStyle: { color: '#409eff' } },
|
||||
{ name: '上游驱动', itemStyle: { color: '#67c23a' } },
|
||||
{ name: '下游影响', itemStyle: { color: '#e6a23c' } },
|
||||
],
|
||||
label: { show: true, position: 'right', fontSize: 10 },
|
||||
edgeLabel: { show: true, fontSize: 9, color: '#666' },
|
||||
lineStyle: { curveness: 0.2 },
|
||||
}],
|
||||
}
|
||||
})
|
||||
|
||||
// 全局因果网络力导向图
|
||||
const fullNetworkChartOption = computed(() => {
|
||||
const dimColors: Record<string, string> = {
|
||||
finance: '#409eff', customer: '#67c23a', process: '#e6a23c', learning: '#9b59b6',
|
||||
}
|
||||
const nodes = fullNetworkNodes.value.map(n => ({
|
||||
id: String(n.id),
|
||||
name: n.kpi_code,
|
||||
symbolSize: 25,
|
||||
itemStyle: { color: dimColors[n.dimension] || '#999' },
|
||||
category: n.dimension === 'finance' ? 0 : n.dimension === 'customer' ? 1 : n.dimension === 'process' ? 2 : 3,
|
||||
label: { fontSize: 9 },
|
||||
}))
|
||||
const edges = fullNetworkEdges.value.map(e => ({
|
||||
source: String(e.source),
|
||||
target: String(e.target),
|
||||
lineStyle: {
|
||||
color: e.direction === 'positive' ? 'rgba(103,194,58,0.5)' : 'rgba(245,108,108,0.5)',
|
||||
width: (e.strength || 0.3) * 4,
|
||||
},
|
||||
value: e.strength,
|
||||
}))
|
||||
|
||||
return {
|
||||
tooltip: { trigger: 'item' as const, formatter: (p: any) => `${p.name}<br/>${p.value ? '强度:' + p.value : ''}` },
|
||||
series: [{
|
||||
type: 'graph',
|
||||
layout: 'force',
|
||||
force: { repulsion: 400, edgeLength: [80, 200], gravity: 0.05, friction: 0.1 },
|
||||
roam: true,
|
||||
draggable: true,
|
||||
data: nodes,
|
||||
edges: edges,
|
||||
categories: [
|
||||
{ name: '财务', itemStyle: { color: '#409eff' } },
|
||||
{ name: '客户', itemStyle: { color: '#67c23a' } },
|
||||
{ name: '流程', itemStyle: { color: '#e6a23c' } },
|
||||
{ name: '学习', itemStyle: { color: '#9b59b6' } },
|
||||
],
|
||||
label: { show: true, position: 'right', fontSize: 9 },
|
||||
lineStyle: { curveness: 0.2 },
|
||||
}],
|
||||
}
|
||||
})
|
||||
|
||||
async function save() {
|
||||
try { await kpiApi.update(kpi.value.id, kpi.value); ElMessage.success('保存成功') }
|
||||
catch (e) { ElMessage.error('保存失败') }
|
||||
@@ -152,6 +429,72 @@ async function suggestThreshold() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCausality(kpiId: number) {
|
||||
networkLoading.value = true
|
||||
try {
|
||||
const [upRes, downRes, listRes]: any = await Promise.all([
|
||||
kpiCausalityApi.getNetwork(kpiId),
|
||||
kpiCausalityApi.getNetwork(kpiId),
|
||||
kpiCausalityApi.list({ source_kpi_id: kpiId }),
|
||||
])
|
||||
upstream.value = upRes.upstream || []
|
||||
downstream.value = downRes.downstream || []
|
||||
networkEdges.value = listRes.data || []
|
||||
} catch (e) {
|
||||
console.error('因果链加载失败', e)
|
||||
}
|
||||
networkLoading.value = false
|
||||
}
|
||||
|
||||
async function doSimulate() {
|
||||
if (!simulateKpiId.value) { ElMessage.warning('请选择KPI'); return }
|
||||
simulating.value = true
|
||||
try {
|
||||
const r: any = await kpiCausalityApi.simulate({
|
||||
kpi_id: simulateKpiId.value,
|
||||
new_value: simulateNewValue.value,
|
||||
})
|
||||
simulateResult.value = r
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.detail || '推演失败')
|
||||
}
|
||||
simulating.value = false
|
||||
}
|
||||
|
||||
function resetSimulate() {
|
||||
simulateResult.value = null
|
||||
}
|
||||
|
||||
watch(simulateKpiId, async (id) => {
|
||||
if (!id) return
|
||||
try {
|
||||
const r: any = await kpiApi.get(id)
|
||||
const kpiData = r.data || r
|
||||
simulateCurrentValue.value = kpiData.current_value ?? null
|
||||
simulateNewValue.value = kpiData.current_value ?? 0
|
||||
} catch (e) {
|
||||
simulateCurrentValue.value = null
|
||||
}
|
||||
})
|
||||
|
||||
async function loadFullNetwork() {
|
||||
fullNetworkLoading.value = true
|
||||
try {
|
||||
const r: any = await kpiCausalityApi.getFullNetwork()
|
||||
fullNetworkNodes.value = r.nodes || []
|
||||
fullNetworkEdges.value = r.edges || []
|
||||
} catch (e) {
|
||||
ElMessage.error('加载因果网络失败')
|
||||
}
|
||||
fullNetworkLoading.value = false
|
||||
}
|
||||
|
||||
watch(showFullNetwork, (val) => {
|
||||
if (val && fullNetworkNodes.value.length === 0) {
|
||||
loadFullNetwork()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const r: any = await kpiApi.get(Number(route.params.id))
|
||||
kpi.value = r.data || r
|
||||
@@ -160,5 +503,31 @@ onMounted(async () => {
|
||||
const kv: any = await api.get('/kpis/' + kpi.value.id)
|
||||
values.value = kv.values || []
|
||||
} catch(e) {}
|
||||
// 加载战略地图列表
|
||||
try {
|
||||
const mr: any = await mapApi.list()
|
||||
mapOptions.value = mr.data || []
|
||||
} catch(e) {}
|
||||
// 加载因果链
|
||||
await loadCausality(Number(route.params.id))
|
||||
// 加载KPI列表(用于模拟推演选择器)
|
||||
try {
|
||||
const kr: any = await kpiApi.list({ page_size: 100 })
|
||||
allKpis.value = kr.data || []
|
||||
} catch(e) {}
|
||||
})
|
||||
|
||||
async function associateMap() {
|
||||
if (!kpi.value) return
|
||||
try {
|
||||
await kpiApi.associateMap(kpi.value.id, { map_id: kpi.value.map_id || null })
|
||||
ElMessage.success('地图关联成功')
|
||||
const r: any = await kpiApi.get(Number(route.params.id))
|
||||
kpi.value = r.data || r
|
||||
} catch (e) {
|
||||
ElMessage.error('关联失败')
|
||||
}
|
||||
}
|
||||
|
||||
function onMapChange(val: any) {}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user