|
|
|
@@ -437,6 +437,10 @@ import DriverFactorBudget from '../components/DriverFactorBudget.vue'
|
|
|
|
|
const activeTab = ref('input')
|
|
|
|
|
const currentYear = new Date().getFullYear()
|
|
|
|
|
const currentMonth = new Date().getMonth() + 1
|
|
|
|
|
|
|
|
|
|
function getEntityId() {
|
|
|
|
|
return Number(localStorage.getItem('cma_entity_id') || 1)
|
|
|
|
|
}
|
|
|
|
|
const yearOptions = computed(() => { const y: number[] = []; for (let i = currentYear - 2; i <= currentYear + 2; i++) y.push(i); return y })
|
|
|
|
|
const filterYear = ref(currentYear); const filterMonth = ref(0); const searchKpi = ref('')
|
|
|
|
|
const loading = ref(false); const budgetList = ref<any[]>([]); const page = ref(1); const pageSize = ref(20); const total = ref(0); const noMap = ref(false)
|
|
|
|
@@ -494,7 +498,7 @@ const addForm = ref({ kpi_id: null as any, year: currentYear, month: new Date().
|
|
|
|
|
const addKpiOptions = ref<any[]>([]); const addKpiLoading = ref(false); const addSaving = ref(false)
|
|
|
|
|
async function searchKpiForAdd(query: string) {
|
|
|
|
|
addKpiLoading.value = true
|
|
|
|
|
try { const r: any = await kpiApi.list({ keyword: query, page_size: 20 }); const d = r.data || r || []; addKpiOptions.value = Array.isArray(d) ? d : (d.items || []) } catch { }
|
|
|
|
|
try { const r: any = await kpiApi.list({ keyword: query, page_size: 20, entity_id: getEntityId() }); const d = r.data || r || []; addKpiOptions.value = Array.isArray(d) ? d : (d.items || []) } catch { }
|
|
|
|
|
addKpiLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
async function doAddBudget() {
|
|
|
|
@@ -516,11 +520,11 @@ async function loadBudget() {
|
|
|
|
|
if (mapCodes.size === 0) { budgetList.value = []; total.value = 0; return }
|
|
|
|
|
noMap.value = false; loading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const params: any = { page: page.value, page_size: pageSize.value }; if (filterYear.value) params.year = filterYear.value; if (filterMonth.value > 0) params.budget_month = filterMonth.value; if (searchKpi.value) params.keyword = searchKpi.value
|
|
|
|
|
const params: any = { page: page.value, page_size: pageSize.value, entity_id: getEntityId() }; if (filterYear.value) params.year = filterYear.value; if (filterMonth.value > 0) params.budget_month = filterMonth.value; if (searchKpi.value) params.keyword = searchKpi.value
|
|
|
|
|
const r: any = await budgetApi.list(params); const d = r.data || r || []; const allPlans = (Array.isArray(d) ? d : (d.items || [])) as any[]
|
|
|
|
|
// 只保留选中地图关联KPI的预算记录
|
|
|
|
|
const plans = allPlans.filter((p: any) => mapCodes.has(p.kpi_code))
|
|
|
|
|
const kr: any = await kpiApi.list({ page_size: 100, keyword: searchKpi.value || undefined }); const kd = kr.data || kr || []; const ak = Array.isArray(kd) ? kd : (kd.items || [])
|
|
|
|
|
const kr: any = await kpiApi.list({ page_size: 100, keyword: searchKpi.value || undefined, entity_id: getEntityId() }); const kd = kr.data || kr || []; const ak = Array.isArray(kd) ? kd : (kd.items || [])
|
|
|
|
|
const mk = ak.filter((k: any) => mapCodes.has(k.kpi_code)); const pk = new Set(plans.map((p: any) => p.kpi_code))
|
|
|
|
|
const ek = mk.filter((k: any) => !pk.has(k.kpi_code)).map((k: any) => ({ kpi_id: k.id, kpi_code: k.kpi_code, kpi_name: k.kpi_name, dimension: k.dimension, unit: k.unit || '', period: `${filterYear.value || currentYear}-${String(filterMonth.value || 1).padStart(2, '0')}`, budget_value: null, version: 'v1.0', status: '', precision: 0, step: 1 }))
|
|
|
|
|
budgetList.value = [...plans.map((item: any) => initRow({ ...item, _empty: false })), ...ek.map(item => initRow({ ...item, _empty: true }))]
|
|
|
|
@@ -596,9 +600,9 @@ const strategyTree = ref<any[]>([]); const strategyFilterYear = ref(currentYear)
|
|
|
|
|
|
|
|
|
|
async function loadStrategyBudget() {
|
|
|
|
|
if (!selectedMap.value) return
|
|
|
|
|
try { const p: any = { budget_year: strategyFilterYear.value }; if (strategyFilterMonth.value > 0) p.budget_month = strategyFilterMonth.value; const r: any = await budgetApi.list(p); const d = r.data || r || []; const plans = (Array.isArray(d) ? d : (d.items || [])) as any[]; strategyAllBudgetPlans.value = plans
|
|
|
|
|
try { const p: any = { budget_year: strategyFilterYear.value, entity_id: getEntityId() }; if (strategyFilterMonth.value > 0) p.budget_month = strategyFilterMonth.value; const r: any = await budgetApi.list(p); const d = r.data || r || []; const plans = (Array.isArray(d) ? d : (d.items || [])) as any[]; strategyAllBudgetPlans.value = plans
|
|
|
|
|
let dims = selectedMap.value.dimensions; if (typeof dims === 'string') { try { dims = JSON.parse(dims) } catch { dims = [] } }; if (!Array.isArray(dims) || dims.length === 0) { strategyTree.value = []; return }
|
|
|
|
|
const kr: any = await kpiApi.list({ page_size: 100 }); const kd = kr.data || kr || []; const km = new Map<string, any>(); (Array.isArray(kd) ? kd : (kd.items || [])).forEach((k: any) => km.set(k.kpi_code, k))
|
|
|
|
|
const kr: any = await kpiApi.list({ page_size: 100, entity_id: getEntityId() }); const kd = kr.data || kr || []; const km = new Map<string, any>(); (Array.isArray(kd) ? kd : (kd.items || [])).forEach((k: any) => km.set(k.kpi_code, k))
|
|
|
|
|
const tree: any[] = []
|
|
|
|
|
for (const dim of dims) { const dt = { budget: 0, kpiCount: 0 }; const objs = (dim.objectives || []).map((obj: any) => { const kpis = (obj.kpis || []).map((code: string) => { const kpi = km.get(code); const plan = plans.find((p: any) => p.kpi_code === code); const bv = plan ? plan.budget_value : null; dt.kpiCount++; if (bv) dt.budget += bv; return { kpi_code: code, kpi_id: kpi?.id || null, kpi_name: kpi?.kpi_name || code, unit: kpi?.unit || '', precision: kpi?.precision || 0, step: kpi?.step || 1, dimension: dim.key, period: plan?.period || `${strategyFilterYear.value}-${String(strategyFilterMonth.value || 1).padStart(2, '0')}`, version: plan?.version || 'v1.0', budget_value: bv, plan_id: plan?.id || null, _editValue: bv ?? 0, _changed: false, _saving: false, _dimKey: dim.key } }).filter(Boolean); return { name: obj.name, kpis } }); tree.push({ key: dim.key, name: dim.name, icon: dim.icon, color: dim.color, kpiCount: dt.kpiCount, totalBudget: dt.budget, objectives: objs }) }
|
|
|
|
|
strategyTree.value = tree
|
|
|
|
@@ -625,20 +629,20 @@ async function approveVersion(row: any, action: string) { const l = action === '
|
|
|
|
|
async function resubmitVersion(row: any) { try { await ElMessageBox.confirm(`确认重新提交版本「${row.version}」?`, '确认'); await budgetApi.versionSubmit({ version: row.version }); ElMessage.success('已重新提交'); loadVersions() } catch { } }
|
|
|
|
|
|
|
|
|
|
const versionYear = ref(currentYear); const versionsList = ref<any[]>([]); const versionsLoading = ref(false); const selectedVersions = ref<any[]>([]); const showVersionDiff = ref(false); const diffLoading = ref(false); const diffData = ref<any>(null); const currentVersion = ref('')
|
|
|
|
|
async function loadVersions() { versionsLoading.value = true; try { const r: any = await budgetApi.versions({ year: versionYear.value }); const d = r.data || []; versionsList.value = d.map((v: any) => ({ ...v, _selected: false })); currentVersion.value = d[0]?.version || '' } catch (e) { ElMessage.error('加载版本列表失败') }; versionsLoading.value = false }
|
|
|
|
|
async function loadVersions() { versionsLoading.value = true; try { const r: any = await budgetApi.versions({ year: versionYear.value, entity_id: getEntityId() }); const d = r.data || []; versionsList.value = d.map((v: any) => ({ ...v, _selected: false })); currentVersion.value = d[0]?.version || '' } catch (e) { ElMessage.error('加载版本列表失败') }; versionsLoading.value = false }
|
|
|
|
|
function onVersionSelect(row: any) { selectedVersions.value = versionsList.value.filter((v: any) => v._selected); if (selectedVersions.value.length > 2) { row._selected = false; selectedVersions.value = versionsList.value.filter((v: any) => v._selected) } }
|
|
|
|
|
async function loadVersionDiff() { if (selectedVersions.value.length !== 2) return; diffLoading.value = true; try { const [a, b] = selectedVersions.value; const r: any = await budgetApi.versionDiff({ version_a: a.version, version_b: b.version, year: versionYear.value }); diffData.value = r; showVersionDiff.value = true } catch (e) { ElMessage.error('加载版本对比失败') }; diffLoading.value = false }
|
|
|
|
|
|
|
|
|
|
const execFilterYear = ref(currentYear); const execFilterMonth = ref(new Date().getMonth()); const execFilterDim = ref(''); const execLoading = ref(false); const execReport = ref<any[]>([]); const execSummaryCards = ref<any[]>([])
|
|
|
|
|
async function loadExecutionReport() {
|
|
|
|
|
execLoading.value = true
|
|
|
|
|
try { const r: any = await budgetApi.deviationReport({ year: execFilterYear.value, month: execFilterMonth.value, dimension: execFilterDim.value || undefined, hierarchical: true }); const items: any[] = r.items || []; const en = items.map((i: any) => ({ ...i, execution_rate: i.budget_value && i.budget_value > 0 ? Math.round((i.actual_value || 0) / i.budget_value * 100) : 0, deviation_value: (i.actual_value || 0) - (i.budget_value || 0) })); execReport.value = en; const hb = en.filter((i: any) => i.budget_value != null); const ob = en.filter((i: any) => i.is_over_budget); const ar = hb.length > 0 ? Math.round(hb.reduce((s, i) => s + (i.execution_rate || 0), 0) / hb.length) : 0; const tb = hb.reduce((s, i) => s + (i.budget_value || 0), 0); const ta = hb.reduce((s, i) => s + (i.actual_value || 0), 0); execSummaryCards.value = [{ label: '有预算KPI', value: `${hb.length}/${en.length}`, color: '#409eff' }, { label: '超预算KPI', value: ob.length.toString(), color: '#f56c6c' }, { label: '平均执行率', value: `${ar}%`, color: ar > 100 ? '#f56c6c' : '#67c23a' }, { label: '预算执行进度', value: tb > 0 ? `${Math.round(ta / tb * 100)}%` : '-', color: '#e6a23c' }] } catch (e) { ElMessage.error('加载执行报告失败') }
|
|
|
|
|
try { const r: any = await budgetApi.deviationReport({ year: execFilterYear.value, month: execFilterMonth.value, dimension: execFilterDim.value || undefined, hierarchical: true, entity_id: getEntityId() }); const items: any[] = r.items || []; const en = items.map((i: any) => ({ ...i, execution_rate: i.budget_value && i.budget_value > 0 ? Math.round((i.actual_value || 0) / i.budget_value * 100) : 0, deviation_value: (i.actual_value || 0) - (i.budget_value || 0) })); execReport.value = en; const hb = en.filter((i: any) => i.budget_value != null); const ob = en.filter((i: any) => i.is_over_budget); const ar = hb.length > 0 ? Math.round(hb.reduce((s, i) => s + (i.execution_rate || 0), 0) / hb.length) : 0; const tb = hb.reduce((s, i) => s + (i.budget_value || 0), 0); const ta = hb.reduce((s, i) => s + (i.actual_value || 0), 0); execSummaryCards.value = [{ label: '有预算KPI', value: `${hb.length}/${en.length}`, color: '#409eff' }, { label: '超预算KPI', value: ob.length.toString(), color: '#f56c6c' }, { label: '平均执行率', value: `${ar}%`, color: ar > 100 ? '#f56c6c' : '#67c23a' }, { label: '预算执行进度', value: tb > 0 ? `${Math.round(ta / tb * 100)}%` : '-', color: '#e6a23c' }] } catch (e) { ElMessage.error('加载执行报告失败') }
|
|
|
|
|
execLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function doDecompose() {
|
|
|
|
|
decomposing.value = true; decomposeResult.value = ''; decomposeDetails.value = []
|
|
|
|
|
try { const r: any = await budgetApi.autoDecompose({ year: decomposeForm.value.year, method: decomposeForm.value.method }); decomposeResult.value = r.message || '分解成功'; if (r.results) decomposeDetails.value = r.results.map((res: any) => ({ kpi_code: res.kpi_code, kpi_name: res.kpi_name, annual_budget: res.annual_budget, method: res.method === 'equal' ? '均分' : '加权', monthly_count: res.monthly.length })); ElMessage.success('年度预算分解完成'); loadBudget() } catch (e: any) { ElMessage.error(e.detail || e.message || '分解失败') }
|
|
|
|
|
try { const r: any = await budgetApi.autoDecompose({ year: decomposeForm.value.year, method: decomposeForm.value.method, entity_id: getEntityId() }); decomposeResult.value = r.message || '分解成功'; if (r.results) decomposeDetails.value = r.results.map((res: any) => ({ kpi_code: res.kpi_code, kpi_name: res.kpi_name, annual_budget: res.annual_budget, method: res.method === 'equal' ? '均分' : '加权', monthly_count: res.monthly.length })); ElMessage.success('年度预算分解完成'); loadBudget() } catch (e: any) { ElMessage.error(e.detail || e.message || '分解失败') }
|
|
|
|
|
decomposing.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -657,7 +661,7 @@ function loadMethodDetail(id: string) {
|
|
|
|
|
|
|
|
|
|
async function refreshMethodComparison() {
|
|
|
|
|
try {
|
|
|
|
|
const r: any = await budgetApi.methodComparison({ entity: 'hanke' })
|
|
|
|
|
const r: any = await budgetApi.methodComparison({ entity: 'hanke', entity_id: getEntityId() })
|
|
|
|
|
budgetMethods.value = r.methods || []
|
|
|
|
|
selectedMethod.value = r.recommended || 'zero_based'
|
|
|
|
|
loadMethodDetail(selectedMethod.value)
|
|
|
|
@@ -721,7 +725,7 @@ let comparisonChart: any = null
|
|
|
|
|
|
|
|
|
|
async function loadComparisonKpiOptions() {
|
|
|
|
|
try {
|
|
|
|
|
const r: any = await kpiApi.list({ page_size: 200 })
|
|
|
|
|
const r: any = await kpiApi.list({ page_size: 200, entity_id: getEntityId() })
|
|
|
|
|
const d = r.data || r || []
|
|
|
|
|
comparisonKpiOptions.value = Array.isArray(d) ? d : (d.items || [])
|
|
|
|
|
} catch { comparisonKpiOptions.value = [] }
|
|
|
|
@@ -730,7 +734,7 @@ async function loadComparisonKpiOptions() {
|
|
|
|
|
async function loadComparison() {
|
|
|
|
|
comparisonLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const params: any = { year: comparisonYear.value }
|
|
|
|
|
const params: any = { year: comparisonYear.value, entity_id: getEntityId() }
|
|
|
|
|
if (comparisonKpiId.value) params.kpi_id = comparisonKpiId.value
|
|
|
|
|
const r: any = await budgetApi.getComparison(params)
|
|
|
|
|
comparisonData.value = r.months_data || []
|
|
|
|
@@ -846,7 +850,7 @@ const alertPeriodOptions = computed(() => {
|
|
|
|
|
async function loadDeviationAlerts() {
|
|
|
|
|
alertLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const params: any = {}
|
|
|
|
|
const params: any = { entity_id: getEntityId() }
|
|
|
|
|
if (alertFilterPeriod.value) params.period = alertFilterPeriod.value
|
|
|
|
|
if (alertFilterLevel.value) params.alert_level = alertFilterLevel.value
|
|
|
|
|
if (alertFilterStatus.value) params.status = alertFilterStatus.value
|
|
|
|
@@ -863,6 +867,7 @@ async function doDeviationCheck() {
|
|
|
|
|
const r: any = await budgetApi.deviationCheck({
|
|
|
|
|
period: alertFilterPeriod.value,
|
|
|
|
|
threshold: 20,
|
|
|
|
|
entity_id: getEntityId(),
|
|
|
|
|
})
|
|
|
|
|
if (r.alerts_generated > 0) {
|
|
|
|
|
deviationAlertMessage.value = `偏差检查完成:生成了 ${r.alerts_generated} 条预警`
|
|
|
|
|