包含前后端完整代码: - 前端:Vue3+Vite+ElementPlus - 后端:FastAPI+SQLAlchemy - 模块:驾驶舱/KPI/战略地图/预警/预算/成本/预测/改善行动 - 当前版本:v1.0.0
165 lines
7.8 KiB
Vue
165 lines
7.8 KiB
Vue
<template>
|
|
<div>
|
|
<el-button text @click="$router.back()">< 返回KPI字典</el-button>
|
|
<el-card v-if="kpi" style="margin-top:16px;">
|
|
<template #header>
|
|
<span style="font-size:16px;font-weight:600;">{{ kpi.kpi_name }}</span>
|
|
<el-tag size="small" style="margin-left:8px;">{{ kpi.kpi_code }}</el-tag>
|
|
<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-card>
|
|
|
|
<el-card style="margin-top:16px;">
|
|
<template #header>趋势图</template>
|
|
<v-chart :option="chartOption" autoresize style="height:300px;width:100%;" />
|
|
</el-card>
|
|
|
|
<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>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, computed } 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 api from '../api/index'
|
|
|
|
const route = useRoute()
|
|
const kpi = ref<any>(null)
|
|
const values = ref<any[]>([])
|
|
|
|
const CAT_MAP: Record<string, string> = {
|
|
revenue_growth: '收入增长', profitability: '盈利水平', cost_control: '成本费用',
|
|
asset_efficiency: '资产效率', cash_risk: '现金流风控',
|
|
customer_scale: '客户规模', customer_concentration: '客户集中度', customer_satisfaction: '客户满意',
|
|
supply_chain: '供应链效率', delivery_quality: '交付质量',
|
|
talent_pipeline: '人才梯队', employee_engagement: '员工敬业', innovation: '创新改善',
|
|
}
|
|
|
|
const categoryOptions = computed(() => {
|
|
const dim = kpi.value?.dimension
|
|
if (!dim) return []
|
|
return Object.entries(CAT_MAP)
|
|
.filter(([key]) => {
|
|
if (dim === 'finance') return ['revenue_growth','profitability','cost_control','asset_efficiency','cash_risk'].includes(key)
|
|
if (dim === 'customer') return ['customer_scale','customer_concentration','customer_satisfaction'].includes(key)
|
|
if (dim === 'process') return ['supply_chain','delivery_quality'].includes(key)
|
|
if (dim === 'learning') return ['talent_pipeline','employee_engagement','innovation'].includes(key)
|
|
return false
|
|
})
|
|
.map(([value, label]) => ({ value, label }))
|
|
})
|
|
|
|
function dimLabel(d: string) { return ({ finance: '财务', customer: '客户', process: '流程', learning: '学习' } as any)[d] || d }
|
|
function catLabel(c: string) { return CAT_MAP[c] || c }
|
|
function dimTagType(d: string) { return ({ finance: '', customer: 'success', process: 'warning', learning: 'info' } as any)[d] || '' }
|
|
|
|
const chartOption = computed(() => ({
|
|
tooltip: { trigger: 'axis' },
|
|
xAxis: { type: 'category', data: values.value.map(v => v.period) },
|
|
yAxis: { type: 'value' },
|
|
series: [{ type: 'line', data: values.value.map(v => v.actual_value), smooth: true, lineStyle: { width: 2 }, itemStyle: { color: '#409eff' }, areaStyle: { color: 'rgba(64,158,255,0.1)' } }],
|
|
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
|
}))
|
|
|
|
async function save() {
|
|
try { await kpiApi.update(kpi.value.id, kpi.value); ElMessage.success('保存成功') }
|
|
catch (e) { ElMessage.error('保存失败') }
|
|
}
|
|
|
|
async function suggestThreshold() {
|
|
try {
|
|
const r: any = await api.get('/thresholds/suggest/' + kpi.value.id)
|
|
const s = r.suggestion
|
|
if (!s) { ElMessage.warning('暂无建议数据'); return }
|
|
kpi.value.threshold_green = s.green?.label || ''
|
|
kpi.value.threshold_yellow = s.yellow?.label || ''
|
|
kpi.value.threshold_red = s.red?.label || ''
|
|
ElMessage.success('已自动填入推荐阈值')
|
|
} catch (e) {
|
|
ElMessage.error('获取阈值建议失败')
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
const r: any = await kpiApi.get(Number(route.params.id))
|
|
kpi.value = r.data || r
|
|
// 获取历史值
|
|
try {
|
|
const kv: any = await api.get('/kpis/' + kpi.value.id)
|
|
values.value = kv.values || []
|
|
} catch(e) {}
|
|
})
|
|
</script>
|