feat: 财务报表助手—对外法定报表(三表合一+Excel导出)

This commit is contained in:
Hermes CI Fix
2026-08-04 15:35:15 +08:00
parent 38b603f263
commit f33162b6c9
3 changed files with 894 additions and 1 deletions
+237
View File
@@ -360,6 +360,200 @@
</div>
</div>
</el-tab-pane>
<!-- 对外法定报表新30号准则三表合一 -->
<el-tab-pane label="📑 对外法定报表" name="statutory">
<div v-loading="loadingStatutory">
<div class="report-desc">对外法定报表新30号准则2027 利润表五板块+附注明细 / 资产负债表新准则科目分类 / 现金流量表三活动支持导出Excel</div>
<div class="statutory-toolbar">
<el-radio-group v-model="statutoryView" size="small">
<el-radio-button value="profit">利润表</el-radio-button>
<el-radio-button value="balance">资产负债表</el-radio-button>
<el-radio-button value="cashflow">现金流量表</el-radio-button>
</el-radio-group>
<el-button type="primary" size="small" :loading="exporting" @click="exportStatutory">📥 导出Excel三表合一</el-button>
<span v-if="statutoryDemoMode" class="new30-demo-hint"> 部分数据为示例数据导入凭证/打标后显示实际数据</span>
</div>
<!-- 利润表五板块+附注 -->
<div v-if="statutoryView === 'profit' && statutoryData.profit">
<div class="statutory-sub-title">利润表 新30号准则五板块{{ statutoryData.period }}</div>
<div class="new30-blocks">
<div v-for="block in statutoryData.profit.blocks || []" :key="block.key" class="new30-block">
<div class="new30-block-header" @click="block.expanded = !block.expanded">
<span class="new30-toggle">{{ block.expanded ? '▼' : '▶' }}</span>
<span class="new30-block-name">{{ block.name }}</span>
<span class="new30-block-subtotal" :class="{ negative: block.subtotal < 0 }">{{ formatMoney(block.subtotal) }}</span>
<span class="new30-block-label">{{ block.subtotal_name }}</span>
<span v-if="!block.has_real_data" class="new30-demo-tag">示例数据</span>
</div>
<div v-if="block.expanded" class="new30-block-body">
<div v-for="item in block.items" :key="item.code" class="new30-item">
<span class="new30-item-name">{{ item.name }}</span>
<span class="new30-item-amount" :class="{ negative: item.effective != null && item.effective < 0 }">
{{ item.effective != null ? formatMoney(item.effective) : '-' }}
</span>
</div>
</div>
</div>
</div>
<div class="new30-net-profit" v-if="statutoryData.profit.net_profit != null">
<div class="new30-profit-row">
<span class="new30-profit-label">净利润</span>
<span class="new30-profit-value" :class="{ negative: statutoryData.profit.net_profit < 0 }">{{ formatMoney(statutoryData.profit.net_profit) }}</span>
<span class="new30-profit-name">= 经营 + 投资 + 筹资 + 所得税 + 终止经营</span>
</div>
</div>
<!-- 附注明细 -->
<div v-if="statutoryData.profit.notes" class="statutory-notes">
<div class="statutory-notes-title">附注明细</div>
<div class="statutory-notes-grid">
<!-- 收入构成 -->
<div class="statutory-note-card">
<div class="statutory-note-head">附注一收入构成</div>
<div v-for="n in statutoryData.profit.notes.revenue_breakdown || []" :key="n.code" class="statutory-note-row">
<span>{{ n.name }}{{ n.code }}</span>
<span>{{ n.amount != null ? formatMoney(n.amount) : '-' }}</span>
</div>
<div class="statutory-note-row total">
<span>营业收入合计</span>
<span>{{ statutoryData.profit.notes.revenue_total != null ? formatMoney(statutoryData.profit.notes.revenue_total) : '-' }}</span>
</div>
</div>
<!-- 费用构成 -->
<div class="statutory-note-card">
<div class="statutory-note-head">附注二费用构成</div>
<div v-for="n in statutoryData.profit.notes.expense_breakdown || []" :key="n.code" class="statutory-note-row">
<span>{{ n.name }}{{ n.code }}</span>
<span>{{ n.amount != null ? formatMoney(n.amount) : '-' }}</span>
</div>
</div>
<!-- 财务费用拆解 -->
<div class="statutory-note-card">
<div class="statutory-note-head">附注三财务费用拆解</div>
<div v-for="n in statutoryData.profit.notes.finance_breakdown || []" :key="n.code" class="statutory-note-row">
<span>{{ n.name }}{{ n.code }}</span>
<span>{{ n.amount != null ? formatMoney(n.amount) : '-' }}</span>
</div>
</div>
<!-- 板块勾稽 + 关键比率 -->
<div class="statutory-note-card">
<div class="statutory-note-head">附注四板块勾稽</div>
<div v-for="n in statutoryData.profit.notes.block_reconciliation || []" :key="n.key" class="statutory-note-row">
<span>{{ n.name }}</span>
<span>{{ formatMoney(n.amount) }}</span>
</div>
<div class="statutory-note-row total">
<span>净利润</span>
<span>{{ formatMoney(statutoryData.profit.notes.net_profit) }}</span>
</div>
</div>
<div class="statutory-note-card">
<div class="statutory-note-head">附注五关键比率</div>
<div v-for="n in statutoryData.profit.notes.key_ratios || []" :key="n.name" class="statutory-note-row">
<span>{{ n.name }}</span>
<span>{{ n.value != null ? n.value.toFixed(2) + '%' : '-' }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 资产负债表新准则科目分类 -->
<div v-else-if="statutoryView === 'balance' && statutoryData.balance_sheet">
<div class="statutory-sub-title">资产负债表 新30号准则科目分类{{ statutoryData.period }}</div>
<el-table :data="balanceSheetRows" border stripe size="small" style="width:100%;margin-top:12px;" :row-class-name="statutoryBsRowClass">
<el-table-column label="项目" min-width="220">
<template #default="{ row }">
<span :style="{ fontWeight: row.is_section ? 700 : row.is_total ? 700 : 400, paddingLeft: row.is_section ? '0' : '16px' }">{{ row.name }}</span>
</template>
</el-table-column>
<el-table-column label="新准则分类" width="110">
<template #default="{ row }">
<el-tag v-if="row.ns_category_label && !row.is_section" size="small" :type="statutoryNsTagType(row.ns_category)">{{ row.ns_category_label }}</el-tag>
<span v-else-if="row.is_section" style="color:#999;">{{ row.category_label }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="期末余额" width="150" align="right">
<template #default="{ row }">
<span :style="{ fontWeight: row.is_section || row.is_total ? 700 : 400 }">{{ row.end_value != null ? formatMoney(row.end_value) : '-' }}</span>
</template>
</el-table-column>
<el-table-column label="期初余额" width="150" align="right">
<template #default="{ row }">
<span :style="{ fontWeight: row.is_section || row.is_total ? 700 : 400 }">{{ row.begin_value != null ? formatMoney(row.begin_value) : '-' }}</span>
</template>
</el-table-column>
<el-table-column label="数据" width="80">
<template #default="{ row }">
<span v-if="row.is_demo && !row.is_section" class="new30-demo-tag">示例</span>
<span v-else-if="row.is_section" style="color:#999;font-size:12px;">小计</span>
<span v-else style="color:#999;font-size:12px;">凭证</span>
</template>
</el-table-column>
</el-table>
<div class="statutory-totals">
<div class="statutory-total-card" :class="{ ok: statutoryData.balance_sheet.totals?.balanced }">
<div class="statutory-total-label">资产总计</div>
<div class="statutory-total-val">{{ formatMoney(statutoryData.balance_sheet.totals?.assets?.end) }}</div>
</div>
<div class="statutory-total-card">
<div class="statutory-total-label">负债合计</div>
<div class="statutory-total-val">{{ formatMoney(statutoryData.balance_sheet.totals?.liabilities?.end) }}</div>
</div>
<div class="statutory-total-card">
<div class="statutory-total-label">所有者权益合计</div>
<div class="statutory-total-val">{{ formatMoney(statutoryData.balance_sheet.totals?.equity?.end) }}</div>
</div>
<div class="statutory-total-card" :class="{ ok: statutoryData.balance_sheet.totals?.balanced }">
<div class="statutory-total-label">勾稽校验资产=负债+权益</div>
<div class="statutory-total-val" style="font-size:14px;">{{ statutoryData.balance_sheet.totals?.balanced ? '✅ 平衡' : '⚠️ 不平' }}</div>
</div>
</div>
</div>
<!-- 现金流量表三活动 -->
<div v-else-if="statutoryView === 'cashflow' && statutoryData.cash_flow">
<div class="statutory-sub-title">现金流量表 新30号准则三活动{{ statutoryData.period }}</div>
<div v-for="sec in statutoryData.cash_flow.sections || []" :key="sec.key" class="cf-section">
<div class="cf-section-head">
<span class="cf-section-name">{{ sec.name }}</span>
<span class="cf-section-net" :class="{ negative: sec.net < 0 }">{{ formatMoney(sec.net) }}</span>
</div>
<div v-for="line in sec.lines" :key="line.code" class="cf-line">
<span class="cf-line-name">{{ line.name }}</span>
<span class="cf-line-code">{{ line.code }}</span>
<span class="cf-line-value" :class="{ negative: line.value != null && line.value < 0 }">{{ line.value != null ? formatMoney(line.value) : '-' }}</span>
<span v-if="line.is_demo" class="new30-demo-tag">示例</span>
</div>
</div>
<div class="cf-section">
<div class="cf-section-head">
<span class="cf-section-name">{{ statutoryData.cash_flow.fx_effect?.name }}</span>
<span class="cf-section-net">{{ formatMoney(statutoryData.cash_flow.fx_effect?.value) }}</span>
</div>
</div>
<div class="cf-summary">
<div class="cf-summary-row">
<span>现金及现金等价物净增加额</span>
<span class="cf-summary-val">{{ formatMoney(statutoryData.cash_flow.summary?.net_increase) }}</span>
</div>
<div class="cf-summary-row">
<span>期初现金及现金等价物余额</span>
<span class="cf-summary-val">{{ formatMoney(statutoryData.cash_flow.summary?.begin_cash) }}</span>
</div>
<div class="cf-summary-row total">
<span>期末现金及现金等价物余额</span>
<span class="cf-summary-val">{{ formatMoney(statutoryData.cash_flow.summary?.end_cash) }}</span>
</div>
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
@@ -471,6 +665,48 @@ function openSubjectManage() {
window.open('/subjects', '_blank')
}
// ════ 对外法定报表(新30号准则三表合一)════
const statutoryData = ref<any>({})
const statutoryView = ref('profit')
const statutoryDemoMode = ref(false)
const statutoryLoading = ref(false)
const exporting = ref(false)
async function loadStatutory() {
statutoryLoading.value = true
try {
const r = await api.get('/reports/statutory', { params: { period: reportPeriod.value, entity_id: getEntityId() } })
const d = (r as any).data || {}
statutoryData.value = d
statutoryDemoMode.value = !d.cash_flow?.all_items_have_data
} catch (e) {
ElMessage.error('加载法定报表失败')
} finally {
statutoryLoading.value = false
}
}
async function exportStatutory() {
exporting.value = true
try {
const token = localStorage.getItem('cma_token') || ''
const url = `/api/cma/reports/statutory/export?period=${reportPeriod.value}&entity_id=${getEntityId()}`
const resp = await fetch(url, { headers: { 'Authorization': `Bearer ${token}` } })
if (!resp.ok) throw new Error('导出失败')
const blob = await resp.blob()
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = `对外法定报表_${reportPeriod.value}.xlsx`
a.click()
URL.revokeObjectURL(a.href)
ElMessage.success('导出成功')
} catch (e) {
ElMessage.error('导出失败')
} finally {
exporting.value = false
}
}
// ── 报表2:预算执行报告 ──
const budgetSummary = ref({ total: 0, with_budget: 0, over_budget: 0, under_budget: 0, normal: 0 })
const budgetItems = ref<any[]>([])
@@ -654,6 +890,7 @@ function onBudgetSort(sort: any) {
onMounted(() => {
loadKpiOptions()
loadAll()
loadStatutory()
})
</script>