feat(proforma): 预编报表(预算版三张报表)P2 — 预算vs实际vs差异,独立/proforma端点,复用budget_plans+报表模板,无预算行显式标注

This commit is contained in:
Hermes CI Fix
2026-08-30 23:13:32 +08:00
parent db0f7aa591
commit 27d8269667
3 changed files with 711 additions and 3 deletions
+128 -1
View File
@@ -219,6 +219,62 @@
</div>
</el-tab-pane>
<!-- 预编报表预算版三张报表 -->
<el-tab-pane label="📋 预编报表" name="proforma">
<div v-loading="loadingProforma">
<div class="report-desc">预算版三张报表预算 vs 实际 vs 差异口径与预算执行报告一致无预算映射的行显式标注不填示例数据</div>
<div style="display:flex;gap:8px;margin:12px 0;align-items:center;">
<el-radio-group v-model="proformaType" size="small" @change="loadProforma">
<el-radio-button value="profit">预算版利润表</el-radio-button>
<el-radio-button value="balance">预算版资产负债表</el-radio-button>
<el-radio-button value="cash">预算版现金流量表</el-radio-button>
</el-radio-group>
<el-tag v-if="proformaBudgetVersion" size="small" type="info">预算版本: {{ proformaBudgetVersion }}</el-tag>
</div>
<el-table :data="proformaRows" border stripe size="small" style="width:100%;" :row-class-name="proformaRowClass">
<el-table-column prop="name" label="项目" min-width="180" />
<el-table-column label="预算值" width="130" align="right">
<template #default="{ row }">{{ row.budget_value != null ? row.budget_value.toLocaleString() : (row.has_budget ? '-' : '—') }}</template>
</el-table-column>
<el-table-column label="实际值" width="130" align="right">
<template #default="{ row }">{{ row.actual_value != null ? row.actual_value.toLocaleString() : '-' }}</template>
</el-table-column>
<el-table-column label="差异" width="140" align="right">
<template #default="{ row }">
<span v-if="row.subtotal" style="color:#909399;">小计</span>
<span v-else-if="row.ratio_kpi" style="color:#909399;">比率型</span>
<span v-else-if="!row.has_budget" style="color:#bbb;">无预算</span>
<span v-else :style="{ color: row.deviation_amount > 0 ? '#f56c6c' : row.deviation_amount < 0 ? '#67c23a' : '#999' }">
{{ row.deviation_amount != null ? (row.deviation_amount > 0 ? '+' : '') + row.deviation_amount.toLocaleString() : '-' }}
</span>
</template>
</el-table-column>
<el-table-column label="差异率" width="110" align="right">
<template #default="{ row }">
<span v-if="row.subtotal" style="color:#909399;"></span>
<el-tag v-else-if="row.ratio_kpi" type="info" size="small">比率型</el-tag>
<span v-else-if="!row.has_budget" style="color:#bbb;">无预算</span>
<el-tag v-else-if="row.deviation_rate != null" :type="proformaRateType(row.deviation_rate)" size="small">
{{ row.deviation_rate > 0 ? '+' : '' }}{{ row.deviation_rate.toFixed(1) }}%
</el-tag>
<span v-else style="color:#ccc;">-</span>
</template>
</el-table-column>
<el-table-column label="预算来源" width="110">
<template #default="{ row }">
<el-tag v-if="row.has_budget && !row.subtotal" :type="row.budget_source === 'budget_plan' ? 'success' : 'warning'" size="small">
{{ row.budget_source === 'budget_plan' ? '预算方案' : row.budget_source === 'target_split' ? '目标分摊' : '' }}
</el-tag>
<span v-else style="color:#bbb;"></span>
</template>
</el-table-column>
<el-table-column prop="note" label="备注" min-width="200" show-overflow-tooltip>
<template #default="{ row }"><span style="color:#909399;">{{ row.note || '' }}</span></template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
<!-- 报表3KPI趋势报告 -->
<el-tab-pane label="📈 KPI趋势报告" name="trends">
<div v-loading="loading">
@@ -858,6 +914,73 @@ async function loadBudget() {
} catch { budgetItems.value = [] }
}
// ── 预编报表(预算版三张报表)──
const loadingProforma = ref(false)
const proformaType = ref('profit')
const proformaData = ref<any>(null)
const proformaBudgetVersion = computed(() => {
const v = proformaData.value?.budget_version
return Array.isArray(v) ? v.join(', ') : (v || '')
})
const proformaRows = computed(() => {
const d = proformaData.value
if (!d) return []
const rows: any[] = []
if (proformaType.value === 'profit') {
for (const b of (d.blocks || [])) {
rows.push({ name: b.name, subtotal: true, actual_value: b.subtotal_actual, budget_value: b.subtotal_budget, has_budget: b.has_budget })
for (const it of (b.items || [])) rows.push({ ...it, name: it.name })
}
rows.push({ name: ' 净利润', subtotal: true, actual_value: d.net_profit_actual, budget_value: d.net_profit_budget, has_budget: true })
} else if (proformaType.value === 'balance') {
for (const s of (d.sections || [])) {
rows.push({ name: s.name, subtotal: true, actual_value: s.subtotal_actual, budget_value: s.subtotal_budget, has_budget: s.has_budget })
for (const ln of (s.lines || [])) rows.push({ ...ln, name: ln.name })
}
} else {
for (const s of (d.sections || [])) {
rows.push({ name: s.name, subtotal: true, actual_value: s.net_actual, budget_value: s.net_budget, has_budget: s.has_budget })
for (const ln of (s.lines || [])) rows.push({ ...ln, name: ln.name })
}
const sm = d.summary || {}
rows.push({ name: ' 现金净增加额', subtotal: true, actual_value: sm.net_increase_actual, budget_value: sm.net_increase_budget, has_budget: true })
}
return rows
})
async function loadProforma() {
loadingProforma.value = true
try {
const map: Record<string, string> = {
profit: '/reports/proforma/profit-statement',
balance: '/reports/proforma/balance-sheet',
cash: '/reports/proforma/cash-flow',
}
const r = await api.get(map[proformaType.value], { params: { period: reportPeriod.value, entity_id: getEntityId() } })
proformaData.value = (r as any).data || {}
} catch (e) {
proformaData.value = null
ElMessage.error('加载预编报表失败')
} finally {
loadingProforma.value = false
}
}
function proformaRowClass({ row }: any) {
if (row.subtotal) return 'proforma-subtotal'
if (!row.has_budget) return 'proforma-no-budget'
return ''
}
function proformaRateType(rate: number): string {
const abs = Math.abs(rate)
if (abs > 20) return 'danger'
if (abs > 10) return 'warning'
return 'success'
}
async function loadTrends() {
try {
const params: any = { months: trendMonths.value, entity_id: getEntityId() }
@@ -885,7 +1008,7 @@ async function loadKpiOptions() {
function loadAll() {
loading.value = true
Promise.all([loadProfit(), loadNew30(), loadBudget(), loadTrends(), loadBsc(), loadRestatement()]).finally(() => { loading.value = false })
Promise.all([loadProfit(), loadNew30(), loadBudget(), loadProforma(), loadTrends(), loadBsc(), loadRestatement()]).finally(() => { loading.value = false })
}
function openDupont() {
@@ -954,6 +1077,10 @@ onMounted(() => {
.report-tabs { margin-bottom: 20px; }
.report-desc { font-size: 13px; color: #888; padding: 8px 0; }
/* 预编报表(预算版) */
:deep(.proforma-no-budget) { opacity: 0.55; }
:deep(.proforma-subtotal) { background: #f0f7ff !important; font-weight: 600; color: #1f2d3d; }
/* 统计卡片 */
.summary-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; }
.stat-card { background: #fff; border-radius: 8px; padding: 12px 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); border-top: 3px solid #ccc; }