feat(proforma): 预编报表(预算版三张报表)P2 — 预算vs实际vs差异,独立/proforma端点,复用budget_plans+报表模板,无预算行显式标注
This commit is contained in:
@@ -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>
|
||||
|
||||
<!-- ════ 报表3:KPI趋势报告 ════ -->
|
||||
<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; }
|
||||
|
||||
Reference in New Issue
Block a user