feat: 财务报表助手—对外法定报表(三表合一+Excel导出)
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user