feat(new30-p1): 追溯调整数据准备+KPI口径更新
- 后端新增 GET /api/cma/reports/restatement 端点 - 旧口径vs新口径双列对比 - 自动标记需调整项(管理费用剥离研发费用、财务费用拆解) - 返回净利润对比汇总 - 前端报表中心新增"追溯调整"Tab - 双列表格展示(旧准则/新准则) - 差异自动高亮标记 - 调整原因列、汇总统计 - KPI口径更新(entity_id=1) - F_COST_RATIO 费用率: (销售费用+管理费用-研发费用)/营业收入×100%, target=15% - 新增 F_OP_PROFIT_MARGIN 经营利润率: 经营利润/营业收入×100%, target=5% - 构建部署验证通过
This commit is contained in:
@@ -235,6 +235,83 @@
|
||||
<el-empty v-else description="暂无已发布的战略地图,请先在战略地图中发布" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ════ 追溯调整:2026数据按新30号准则重述 ════ -->
|
||||
<el-tab-pane label="🔄 追溯调整" name="restatement">
|
||||
<div v-loading="loadingRestatement">
|
||||
<div class="report-desc">2026年数据按新30号准则重述 — 旧口径vs新口径双列对比,差异自动高亮。</div>
|
||||
|
||||
<!-- 净利润对比 -->
|
||||
<div v-if="restatementNetProfit" class="restate-net-box">
|
||||
<div class="restate-net-row">
|
||||
<span class="restate-net-label">旧口径净利润:</span>
|
||||
<span class="restate-net-val">{{ formatMoney(restatementNetProfit.old_net_profit) }}</span>
|
||||
<span class="restate-net-arrow">→</span>
|
||||
<span class="restate-net-label">新口径净利润:</span>
|
||||
<span class="restate-net-val" :class="{ negative: restatementNetProfit.new_net_profit < 0 }">{{ formatMoney(restatementNetProfit.new_net_profit) }}</span>
|
||||
<span class="restate-net-diff" :class="{ up: restatementNetProfit.difference > 0, down: restatementNetProfit.difference < 0 }">
|
||||
差异:{{ restatementNetProfit.difference > 0 ? '+' : '' }}{{ formatMoney(restatementNetProfit.difference) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 汇总统计 -->
|
||||
<div v-if="restatementSummary" class="restate-summary-row">
|
||||
<div class="restate-stat green"><span class="restate-stat-val">{{ restatementSummary.total_items }}</span> 总项目</div>
|
||||
<div class="restate-stat red"><span class="restate-stat-val">{{ restatementSummary.adjusted_items }}</span> 需调整</div>
|
||||
<div class="restate-stat blue"><span class="restate-stat-val">{{ restatementSummary.new_items }}</span> 新增</div>
|
||||
<div class="restate-stat gray"><span class="restate-stat-val">{{ restatementSummary.unchanged_items }}</span> 不变</div>
|
||||
</div>
|
||||
|
||||
<!-- 对比表格 -->
|
||||
<el-table :data="restatementItems" border stripe size="small" style="width:100%;margin-top:12px;" :row-class-name="restatementRowClass">
|
||||
<el-table-column prop="item_name" label="项目" width="240">
|
||||
<template #default="{ row }">
|
||||
<div class="restate-item-name">
|
||||
<el-tag v-if="row.needs_adjustment" type="danger" size="small" effect="plain" class="restate-tag">调整</el-tag>
|
||||
<el-tag v-else size="small" effect="plain" type="info" class="restate-tag">不变</el-tag>
|
||||
<span :class="{ 'restate-breakdown-item': row.category === '_breakdown' }">{{ row.item_name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="旧准则" width="160" align="right">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'restate-highlight-red': row.needs_adjustment && row.old_value != null }">
|
||||
{{ row.old_value != null ? formatMoney(row.old_value) : '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="新准则" width="160" align="right">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'restate-highlight-green': row.needs_adjustment && row.new_value != null }">
|
||||
{{ row.new_value != null ? formatMoney(row.new_value) : '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="差异" width="140" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.difference != null" :class="restateDiffClass(row.difference)">
|
||||
{{ row.difference > 0 ? '+' : '' }}{{ formatMoney(row.difference) }}
|
||||
</span>
|
||||
<span v-else-if="row.needs_adjustment" class="restate-new-badge">新</span>
|
||||
<span v-else style="color:#ccc;">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="调整原因" width="260">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.adjustment_reason" class="restate-reason">{{ row.adjustment_reason }}</span>
|
||||
<span v-else style="color:#ccc;">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="restate-legend">
|
||||
<span class="restate-legend-item"><span class="restate-dot restate-dot-red"></span> 旧口径</span>
|
||||
<span class="restate-legend-item"><span class="restate-dot restate-dot-green"></span> 新口径</span>
|
||||
<span class="restate-legend-item"><span class="restate-dot restate-dot-orange"></span> 需调整项</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
@@ -347,6 +424,41 @@ function setTrendChartRef(id: number, el: any) {
|
||||
// ── 报表4:BSC评分卡 ──
|
||||
const bscData = ref<any>({})
|
||||
|
||||
// ── 追溯调整:2026年数据按新30号准则重述 ──
|
||||
const loadingRestatement = ref(false)
|
||||
const restatementItems = ref<any[]>([])
|
||||
const restatementSummary = ref<any>(null)
|
||||
const restatementNetProfit = ref<any>(null)
|
||||
|
||||
async function loadRestatement() {
|
||||
loadingRestatement.value = true
|
||||
try {
|
||||
const r = await api.get('/reports/restatement', { params: { period: reportPeriod.value } })
|
||||
const d = (r as any).data || {}
|
||||
restatementItems.value = d.items || []
|
||||
restatementSummary.value = d.summary || null
|
||||
restatementNetProfit.value = d.net_profit_comparison || null
|
||||
} catch (e) {
|
||||
restatementItems.value = []
|
||||
restatementSummary.value = null
|
||||
restatementNetProfit.value = null
|
||||
ElMessage.error('加载追溯调整数据失败')
|
||||
} finally {
|
||||
loadingRestatement.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function restatementRowClass({ row }: any) {
|
||||
if (row.needs_adjustment) return 'restate-row-adjusted'
|
||||
return ''
|
||||
}
|
||||
|
||||
function restatementDiffClass(diff: number) {
|
||||
if (diff > 0) return 'restate-diff-up'
|
||||
if (diff < 0) return 'restate-diff-down'
|
||||
return 'restate-diff-zero'
|
||||
}
|
||||
|
||||
// ── 公共 ──
|
||||
const dimensions = [
|
||||
{ value: 'finance', label: '财务维度' },
|
||||
@@ -417,7 +529,7 @@ async function loadKpiOptions() {
|
||||
|
||||
function loadAll() {
|
||||
loading.value = true
|
||||
Promise.all([loadProfit(), loadNew30(), loadBudget(), loadTrends(), loadBsc()]).finally(() => { loading.value = false })
|
||||
Promise.all([loadProfit(), loadNew30(), loadBudget(), loadTrends(), loadBsc(), loadRestatement()]).finally(() => { loading.value = false })
|
||||
}
|
||||
|
||||
function openDupont() {
|
||||
@@ -564,4 +676,38 @@ onMounted(() => {
|
||||
.new30-profit-name { font-size: 13px; opacity: 0.8; }
|
||||
.new30-footer { margin-top: 12px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.new30-demo-hint { font-size: 11px; color: #d46b08; margin-left: 8px; }
|
||||
|
||||
/* 追溯调整样式 */
|
||||
.restate-net-box { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 8px; padding: 14px 18px; color: #fff; margin-top: 12px; }
|
||||
.restate-net-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||
.restate-net-label { font-size: 13px; opacity: 0.9; }
|
||||
.restate-net-val { font-size: 18px; font-weight: 700; }
|
||||
.restate-net-val.negative { color: #ffccc7; }
|
||||
.restate-net-arrow { font-size: 18px; opacity: 0.7; }
|
||||
.restate-net-diff { font-size: 13px; padding: 2px 10px; border-radius: 4px; }
|
||||
.restate-net-diff.up { background: rgba(255,255,255,0.2); color: #b7eb8f; }
|
||||
.restate-net-diff.down { background: rgba(255,255,255,0.2); color: #ffccc7; }
|
||||
.restate-summary-row { display: flex; gap: 12px; margin-top: 12px; }
|
||||
.restate-stat { padding: 8px 14px; border-radius: 6px; font-size: 13px; color: #555; border-left: 3px solid #ccc; background: #fafafa; }
|
||||
.restate-stat.green { border-left-color: #67c23a; }
|
||||
.restate-stat.red { border-left-color: #f56c6c; }
|
||||
.restate-stat.blue { border-left-color: #409eff; }
|
||||
.restate-stat.gray { border-left-color: #909399; }
|
||||
.restate-stat-val { font-weight: 700; font-size: 16px; margin-right: 4px; }
|
||||
.restate-item-name { display: flex; align-items: center; gap: 6px; }
|
||||
.restate-tag { flex-shrink: 0; }
|
||||
.restate-breakdown-item { padding-left: 16px; color: #666; font-size: 12px; }
|
||||
.restate-highlight-red { background: #fff1f0; padding: 1px 4px; border-radius: 2px; font-weight: 600; color: #cf1322; }
|
||||
.restate-highlight-green { background: #f6ffed; padding: 1px 4px; border-radius: 2px; font-weight: 600; color: #389e0d; }
|
||||
.restate-diff-up { color: #f56c6c; font-weight: 600; }
|
||||
.restate-diff-down { color: #67c23a; font-weight: 600; }
|
||||
.restate-diff-zero { color: #999; }
|
||||
.restate-new-badge { background: #409eff; color: #fff; padding: 1px 6px; border-radius: 3px; font-size: 11px; }
|
||||
.restate-reason { font-size: 12px; color: #d46b08; }
|
||||
.restate-legend { display: flex; gap: 16px; margin-top: 12px; padding: 8px 12px; background: #fafafa; border-radius: 6px; font-size: 12px; color: #666; }
|
||||
.restate-legend-item { display: flex; align-items: center; gap: 4px; }
|
||||
.restate-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
||||
.restate-dot-red { background: #cf1322; }
|
||||
.restate-dot-green { background: #389e0d; }
|
||||
.restate-dot-orange { background: #d46b08; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user