feat: 新30号准则P2 — MPM计算器+旧格式双列对比
This commit is contained in:
+238
-2
@@ -9,6 +9,7 @@ CMA管理报表中心 — 管理会计OS
|
|||||||
4. 四维度绩效评分卡 — BSC健康度雷达图
|
4. 四维度绩效评分卡 — BSC健康度雷达图
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -571,10 +572,10 @@ def _get_subject_amount(db: Session, code: str, period: str) -> Optional[float]:
|
|||||||
@router.get("/profit-statement")
|
@router.get("/profit-statement")
|
||||||
def get_profit_statement(
|
def get_profit_statement(
|
||||||
period: str = Query(None, description="格式 YYYY-MM"),
|
period: str = Query(None, description="格式 YYYY-MM"),
|
||||||
format: str = Query("old", description="old/new"),
|
format: str = Query("old", description="old/new/dual"),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""利润表 — 支持旧格式和新30号准则五板块格式"""
|
"""利润表 — 支持旧格式、新30号准则五板块格式、双列对比"""
|
||||||
if period is None:
|
if period is None:
|
||||||
period = datetime.now().strftime("%Y-%m")
|
period = datetime.now().strftime("%Y-%m")
|
||||||
|
|
||||||
@@ -582,7 +583,24 @@ def get_profit_statement(
|
|||||||
# 旧30号准则格式(保留兼容)
|
# 旧30号准则格式(保留兼容)
|
||||||
return get_profit_summary(period=period, db=db)
|
return get_profit_summary(period=period, db=db)
|
||||||
|
|
||||||
|
if format == "dual":
|
||||||
|
# 双列对比:旧准则 vs 新准则
|
||||||
|
old_data = get_profit_summary(period=period, db=db)
|
||||||
|
new_data = _build_new_format_profit(db, period)
|
||||||
|
return {
|
||||||
|
"period": period,
|
||||||
|
"format": "dual",
|
||||||
|
"title": f"利润表双列对比({period})",
|
||||||
|
"old_format": old_data,
|
||||||
|
"new_format": new_data,
|
||||||
|
}
|
||||||
|
|
||||||
# === 新30号准则:五板块结构 ===
|
# === 新30号准则:五板块结构 ===
|
||||||
|
return _build_new_format_profit(db, period)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_new_format_profit(db: Session, period: str) -> dict:
|
||||||
|
"""构建新30号准则五板块利润表"""
|
||||||
blocks = []
|
blocks = []
|
||||||
total_net_profit = 0
|
total_net_profit = 0
|
||||||
all_items_have_data = True
|
all_items_have_data = True
|
||||||
@@ -638,6 +656,224 @@ def get_profit_statement(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# MPM管理层指标计算器 (P2)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# MPM指标类型定义
|
||||||
|
MPM_INDICATOR_TYPES = {
|
||||||
|
"ebitda": {
|
||||||
|
"name": "EBITDA",
|
||||||
|
"description": "息税折旧摊销前利润",
|
||||||
|
"base_label": "净利润",
|
||||||
|
"default_adjustments": [
|
||||||
|
{"code": "tax", "name": "加:所得税费用", "sign": 1, "checked": True},
|
||||||
|
{"code": "interest", "name": "加:利息支出", "sign": 1, "checked": True},
|
||||||
|
{"code": "depreciation", "name": "加:折旧与摊销", "sign": 1, "checked": True},
|
||||||
|
{"code": "impairment", "name": "加:资产减值损失", "sign": 1, "checked": False},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"adjusted_net_profit": {
|
||||||
|
"name": "调整后净利润",
|
||||||
|
"description": "剔除非经常性项目后的可持续净利润",
|
||||||
|
"base_label": "净利润(准则)",
|
||||||
|
"default_adjustments": [
|
||||||
|
{"code": "impairment", "name": "加:资产减值损失", "sign": 1, "checked": True},
|
||||||
|
{"code": "equity_incentive", "name": "加:股权激励费用", "sign": 1, "checked": True},
|
||||||
|
{"code": "ma_cost", "name": "加:并购相关费用", "sign": 1, "checked": False},
|
||||||
|
{"code": "nonrecurring_income", "name": "减:非经常性投资收益", "sign": -1, "checked": True},
|
||||||
|
{"code": "asset_disposal", "name": "减:资产处置收益", "sign": -1, "checked": False},
|
||||||
|
{"code": "government_grant", "name": "减:政府补助", "sign": -1, "checked": False},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"free_cash_flow": {
|
||||||
|
"name": "自由现金流",
|
||||||
|
"description": "经营现金流扣除资本支出后的可自由支配现金流",
|
||||||
|
"base_label": "经营现金流",
|
||||||
|
"default_adjustments": [
|
||||||
|
{"code": "capex", "name": "减:资本支出", "sign": -1, "checked": True},
|
||||||
|
{"code": "working_capital", "name": "减:营运资本增加", "sign": -1, "checked": True},
|
||||||
|
{"code": "maintenance_capex", "name": "减:维护性资本支出", "sign": -1, "checked": False},
|
||||||
|
{"code": "dividend", "name": "加:股息收入", "sign": 1, "checked": False},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"name": "自定义指标",
|
||||||
|
"description": "自定义管理层指标",
|
||||||
|
"base_label": "净利润",
|
||||||
|
"default_adjustments": [
|
||||||
|
{"code": "adjustment_1", "name": "调整项目1", "sign": 1, "checked": False, "amount": None},
|
||||||
|
{"code": "adjustment_2", "name": "调整项目2", "sign": -1, "checked": False, "amount": None},
|
||||||
|
{"code": "adjustment_3", "name": "调整项目3", "sign": 1, "checked": False, "amount": None},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# 调整项默认金额(从利润表自动取值映射)
|
||||||
|
ADJUSTMENT_VALUE_MAP = {
|
||||||
|
"tax": {"code": "6801", "sign_inverse": True}, # 所得税费用,加回需取绝对值
|
||||||
|
"interest": {"code": "660301", "sign_inverse": True},
|
||||||
|
"impairment": {"code": "6701", "sign_inverse": False}, # 资产减值损失本身是费用
|
||||||
|
"equity_incentive": None, # 无映射,需用户输入
|
||||||
|
"ma_cost": None,
|
||||||
|
"nonrecurring_income": {"code": "6111", "sign_inverse": False},
|
||||||
|
"asset_disposal": None,
|
||||||
|
"government_grant": None,
|
||||||
|
"capex": None,
|
||||||
|
"working_capital": None,
|
||||||
|
"maintenance_capex": None,
|
||||||
|
"dividend": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MpmCalculateRequest(BaseModel):
|
||||||
|
indicator_type: str = "adjusted_net_profit"
|
||||||
|
period: str = None
|
||||||
|
adjustments: list[dict] = None # [{code, name, sign, checked, amount}]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/mpm-calculate")
|
||||||
|
def mpm_calculate(
|
||||||
|
req: MpmCalculateRequest,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""MPM管理层指标计算器 — 生成合规调节表"""
|
||||||
|
if req.period is None:
|
||||||
|
req.period = datetime.now().strftime("%Y-%m")
|
||||||
|
|
||||||
|
indicator_cfg = MPM_INDICATOR_TYPES.get(req.indicator_type)
|
||||||
|
if not indicator_cfg:
|
||||||
|
raise HTTPException(status_code=400, detail=f"不支持的指标类型: {req.indicator_type}")
|
||||||
|
|
||||||
|
# 获取基准值:净利润
|
||||||
|
net_profit = _calc_new_net_profit(db, req.period)
|
||||||
|
if net_profit is None:
|
||||||
|
net_profit = 0
|
||||||
|
|
||||||
|
# 经营现金流(自由现金流的基准)
|
||||||
|
operating_cf = _get_kpi_val(db, "F_OPERATING_CF", req.period)
|
||||||
|
|
||||||
|
# 确定基准值
|
||||||
|
if req.indicator_type == "free_cash_flow":
|
||||||
|
base_value = operating_cf or net_profit # fallback
|
||||||
|
base_label = "经营现金流"
|
||||||
|
else:
|
||||||
|
base_value = net_profit
|
||||||
|
base_label = indicator_cfg["base_label"]
|
||||||
|
|
||||||
|
# 获取调整项(来自请求或默认)
|
||||||
|
adjustments = req.adjustments if req.adjustments else indicator_cfg["default_adjustments"]
|
||||||
|
|
||||||
|
# 自动填充调整项金额
|
||||||
|
reconciliation_items = []
|
||||||
|
running_total = base_value
|
||||||
|
|
||||||
|
# 第一步:基准值
|
||||||
|
reconciliation_items.append({
|
||||||
|
"step": 0,
|
||||||
|
"code": "_base",
|
||||||
|
"name": base_label,
|
||||||
|
"sign": 1,
|
||||||
|
"amount": round(base_value, 2),
|
||||||
|
"effective": round(base_value, 2),
|
||||||
|
"is_base": True,
|
||||||
|
"running_total": round(base_value, 2),
|
||||||
|
})
|
||||||
|
|
||||||
|
for adj in adjustments:
|
||||||
|
code = adj.get("code", "")
|
||||||
|
checked = adj.get("checked", False)
|
||||||
|
sign = adj.get("sign", 1)
|
||||||
|
name = adj.get("name", "")
|
||||||
|
amount = adj.get("amount")
|
||||||
|
|
||||||
|
# 尝试自动取值
|
||||||
|
if amount is None and checked:
|
||||||
|
amount = _get_adjustment_value(db, code, req.period)
|
||||||
|
|
||||||
|
effective = round(amount * sign, 2) if amount is not None else None
|
||||||
|
|
||||||
|
item = {
|
||||||
|
"step": len(reconciliation_items),
|
||||||
|
"code": code,
|
||||||
|
"name": name,
|
||||||
|
"sign": sign,
|
||||||
|
"amount": round(amount, 2) if amount is not None else None,
|
||||||
|
"effective": effective,
|
||||||
|
"checked": checked,
|
||||||
|
"is_base": False,
|
||||||
|
"running_total": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
if checked and effective is not None:
|
||||||
|
running_total += effective
|
||||||
|
item["running_total"] = round(running_total, 2)
|
||||||
|
|
||||||
|
reconciliation_items.append(item)
|
||||||
|
|
||||||
|
# 最终结果
|
||||||
|
final_value = round(running_total, 2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"indicator_type": req.indicator_type,
|
||||||
|
"indicator_name": indicator_cfg["name"],
|
||||||
|
"indicator_desc": indicator_cfg["description"],
|
||||||
|
"period": req.period,
|
||||||
|
"base_value": round(base_value, 2),
|
||||||
|
"base_label": base_label,
|
||||||
|
"final_value": final_value,
|
||||||
|
"final_label": indicator_cfg["name"],
|
||||||
|
"adjustment_count": sum(1 for a in adjustments if a.get("checked", False)),
|
||||||
|
"total_adjustments": len(adjustments),
|
||||||
|
"reconciliation_items": reconciliation_items,
|
||||||
|
"has_real_data": net_profit != 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_kpi_val(db: Session, code: str, period: str) -> Optional[float]:
|
||||||
|
"""从KPI定义+值获取数值"""
|
||||||
|
kpi = db.query(KPIDefinition).filter(KPIDefinition.kpi_code == code).first()
|
||||||
|
if not kpi:
|
||||||
|
return None
|
||||||
|
v = db.query(KPIValue).filter(
|
||||||
|
KPIValue.kpi_id == kpi.id, KPIValue.period == period
|
||||||
|
).order_by(KPIValue.id.desc()).first()
|
||||||
|
return float(v.actual_value) if v and v.actual_value is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def _calc_new_net_profit(db: Session, period: str) -> Optional[float]:
|
||||||
|
"""计算新30号准则下的净利润"""
|
||||||
|
total = 0
|
||||||
|
has_data = False
|
||||||
|
for block_key in ["operating", "investing", "financing", "tax", "discontinued"]:
|
||||||
|
block_cfg = BLOCK_INFO[block_key]
|
||||||
|
for item_cfg in block_cfg["items"]:
|
||||||
|
amount = _get_subject_amount(db, item_cfg["code"], period)
|
||||||
|
if amount is not None:
|
||||||
|
total += amount * item_cfg["sign"]
|
||||||
|
has_data = True
|
||||||
|
if not has_data:
|
||||||
|
return None
|
||||||
|
return round(total, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_adjustment_value(db: Session, adj_code: str, period: str) -> Optional[float]:
|
||||||
|
"""获取调整项的自动取值"""
|
||||||
|
mapping = ADJUSTMENT_VALUE_MAP.get(adj_code)
|
||||||
|
if mapping is None:
|
||||||
|
return None # 需要用户输入
|
||||||
|
|
||||||
|
code = mapping["code"]
|
||||||
|
amount = _get_subject_amount(db, code, period)
|
||||||
|
if amount is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# sign_inverse: 如果调整项是"加回"费用,费用本身的sign是负的(在利润表中是减项)
|
||||||
|
# 但在MPM调节中,加回费用取绝对值
|
||||||
|
return abs(amount) if mapping.get("sign_inverse", False) else amount
|
||||||
|
|
||||||
|
|
||||||
def _get_demo_block_total(block_key: str) -> float:
|
def _get_demo_block_total(block_key: str) -> float:
|
||||||
"""PRD示例数据 fallback"""
|
"""PRD示例数据 fallback"""
|
||||||
demo = {
|
demo = {
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
|
|
||||||
// 各角色可访问的路由列表
|
// 各角色可访问的路由列表
|
||||||
export const ROLE_ROUTES: Record<string, string[]> = {
|
export const ROLE_ROUTES: Record<string, string[]> = {
|
||||||
ceo: ['/my-dashboard', '/dashboard', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/notifications', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/okr-templates'],
|
ceo: ['/my-dashboard', '/dashboard', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/notifications', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/okr-templates', '/mpm-calculator'],
|
||||||
finance: ['/my-dashboard', '/dashboard', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/data', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/okr-templates'],
|
finance: ['/my-dashboard', '/dashboard', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/data', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/okr-templates', '/mpm-calculator'],
|
||||||
business: ['/my-dashboard', '/dashboard', '/kpis', '/alerts', '/deviations', '/budget', '/action-plans', '/knowledge', '/guide', '/customer', '/reports', '/alignment'],
|
business: ['/my-dashboard', '/dashboard', '/kpis', '/alerts', '/deviations', '/budget', '/action-plans', '/knowledge', '/guide', '/customer', '/reports', '/alignment', '/mpm-calculator'],
|
||||||
it: ['/my-dashboard', '/dashboard', '/kpis', '/alerts', '/data', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/okr-templates'],
|
it: ['/my-dashboard', '/dashboard', '/kpis', '/alerts', '/data', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/okr-templates'],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +62,7 @@ export const MENU_ITEMS: MenuItem[] = [
|
|||||||
{ path: '/maps-review', label: '战略回顾会', icon: 'TrendCharts', roles: ['ceo', 'finance'], group: '🔴 A 复盘与改进' },
|
{ path: '/maps-review', label: '战略回顾会', icon: 'TrendCharts', roles: ['ceo', 'finance'], group: '🔴 A 复盘与改进' },
|
||||||
{ path: '/predict', label: '预测模拟', icon: 'DataLine', roles: ['ceo', 'finance', 'it'], group: '🔴 A 复盘与改进' },
|
{ path: '/predict', label: '预测模拟', icon: 'DataLine', roles: ['ceo', 'finance', 'it'], group: '🔴 A 复盘与改进' },
|
||||||
{ path: '/real-options', label: '实物期权计算器', icon: 'TrendCharts', roles: ['ceo', 'finance'], group: '🔴 A 复盘与改进' },
|
{ path: '/real-options', label: '实物期权计算器', icon: 'TrendCharts', roles: ['ceo', 'finance'], group: '🔴 A 复盘与改进' },
|
||||||
|
{ path: '/mpm-calculator', label: 'MPM计算器', icon: 'Money', roles: ['ceo', 'finance', 'business'], group: '🔴 A 复盘与改进' },
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
// GROUP 5: 系统与支持(Infra)
|
// GROUP 5: 系统与支持(Infra)
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const routes = [
|
|||||||
{ path: 'bi-reports', name: 'BiReports', component: () => import('@/views/BIReportCenter.vue'), meta: { title: 'BI报表', roles: ['ceo', 'finance', 'business'] } },
|
{ path: 'bi-reports', name: 'BiReports', component: () => import('@/views/BIReportCenter.vue'), meta: { title: 'BI报表', roles: ['ceo', 'finance', 'business'] } },
|
||||||
{ path: 'okr-templates', name: 'OKRTemplates', component: () => import('@/views/OKRTemplates.vue'), meta: { title: 'OKR模板库', roles: ['ceo', 'finance', 'it'] } },
|
{ path: 'okr-templates', name: 'OKRTemplates', component: () => import('@/views/OKRTemplates.vue'), meta: { title: 'OKR模板库', roles: ['ceo', 'finance', 'it'] } },
|
||||||
{ path: 'subjects', name: 'SubjectManage', component: () => import('@/views/SubjectManage.vue'), meta: { title: '科目打标', roles: ['ceo', 'finance', 'business', 'it'] } },
|
{ path: 'subjects', name: 'SubjectManage', component: () => import('@/views/SubjectManage.vue'), meta: { title: '科目打标', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||||
|
{ path: 'mpm-calculator', name: 'MpmCalculator', component: () => import('@/views/MpmCalculator.vue'), meta: { title: 'MPM计算器', roles: ['ceo', 'finance', 'business'] } },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,370 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mpm-page">
|
||||||
|
<div class="mpm-header">
|
||||||
|
<h3 class="page-title">MPM管理层指标计算器</h3>
|
||||||
|
<div class="header-right">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="period"
|
||||||
|
type="month"
|
||||||
|
placeholder="选择月份"
|
||||||
|
value-format="YYYY-MM"
|
||||||
|
size="small"
|
||||||
|
style="width:140px"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" size="small" :loading="calculating" @click="calculate">计算</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<!-- 左侧:参数面板 -->
|
||||||
|
<el-col :span="10">
|
||||||
|
<el-card class="param-card">
|
||||||
|
<template #header>
|
||||||
|
<span>参数设置</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="param-section">
|
||||||
|
<label class="param-label">选择指标类型</label>
|
||||||
|
<el-radio-group v-model="indicatorType" class="indicator-types">
|
||||||
|
<el-radio
|
||||||
|
v-for="opt in indicatorOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
:value="opt.value"
|
||||||
|
class="indicator-radio"
|
||||||
|
>
|
||||||
|
<div class="indicator-opt">
|
||||||
|
<span class="indicator-name">{{ opt.label }}</span>
|
||||||
|
<span class="indicator-desc">{{ opt.desc }}</span>
|
||||||
|
</div>
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<div class="param-section">
|
||||||
|
<label class="param-label">调整项目
|
||||||
|
<span class="param-hint">勾选需要调整的项目,未勾选的项目不计入调节</span>
|
||||||
|
</label>
|
||||||
|
<div class="adjustment-list">
|
||||||
|
<div v-for="(adj, idx) in adjustments" :key="adj.code" class="adjustment-row">
|
||||||
|
<el-checkbox v-model="adj.checked" :disabled="adjusting" @change="onAdjustmentChange">
|
||||||
|
<span :class="{ 'adj-sign-positive': adj.sign > 0, 'adj-sign-negative': adj.sign < 0 }">
|
||||||
|
{{ adj.sign > 0 ? '加回' : '减' }}
|
||||||
|
</span>
|
||||||
|
{{ adj.name }}
|
||||||
|
</el-checkbox>
|
||||||
|
<el-input-number
|
||||||
|
v-model="adj.amount"
|
||||||
|
:disabled="!adj.checked || adjusting"
|
||||||
|
size="small"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
controls-position="right"
|
||||||
|
placeholder="0.00"
|
||||||
|
style="width:150px"
|
||||||
|
@change="onAdjustmentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="adjustments.length === 0" class="empty-hint">该指标类型暂无可选调整项</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<div style="text-align:right;">
|
||||||
|
<el-button size="small" @click="resetDefaults">重置默认</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<!-- 右侧:调节表结果 -->
|
||||||
|
<el-col :span="14">
|
||||||
|
<el-card class="result-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="result-header">
|
||||||
|
<span>调节表(附注披露用)</span>
|
||||||
|
<el-tag v-if="indicatorType === 'ebitda'" type="warning">EBITDA</el-tag>
|
||||||
|
<el-tag v-else-if="indicatorType === 'adjusted_net_profit'" type="success">调整后净利润</el-tag>
|
||||||
|
<el-tag v-else-if="indicatorType === 'free_cash_flow'" type="primary">自由现金流</el-tag>
|
||||||
|
<el-tag v-else type="info">自定义</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-loading="calculating">
|
||||||
|
<div v-if="result" class="reconciliation-table">
|
||||||
|
<div
|
||||||
|
v-for="item in result.reconciliation_items"
|
||||||
|
:key="item.step"
|
||||||
|
:class="['rec-item', { 'rec-base': item.is_base, 'rec-checked': item.checked && !item.is_base, 'rec-unchecked': !item.checked && !item.is_base }]"
|
||||||
|
>
|
||||||
|
<div class="rec-step">
|
||||||
|
<span v-if="item.is_base" class="rec-dot-base"></span>
|
||||||
|
<span v-else-if="item.checked" class="rec-dot-checked"></span>
|
||||||
|
<span v-else class="rec-dot-unchecked"></span>
|
||||||
|
<span class="rec-name">{{ item.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="rec-amount" :class="{ 'rec-negative': item.effective != null && item.effective < 0 }">
|
||||||
|
<template v-if="item.effective != null">
|
||||||
|
{{ item.sign > 0 && !item.is_base ? '+ ' : '' }}{{ item.sign < 0 && !item.is_base ? '- ' : '' }}¥ {{ Math.abs(item.effective).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
|
||||||
|
</template>
|
||||||
|
<span v-else class="rec-no-data">无数据</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.running_total != null" class="rec-running">
|
||||||
|
= ¥ {{ item.running_total.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rec-divider"></div>
|
||||||
|
|
||||||
|
<div class="rec-item rec-final">
|
||||||
|
<div class="rec-step">
|
||||||
|
<span class="rec-dot-final"></span>
|
||||||
|
<span class="rec-name rec-final-label">{{ result.final_label }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="rec-amount rec-final-amount" :class="{ 'rec-negative': result.final_value < 0 }">
|
||||||
|
¥ {{ Math.abs(result.final_value).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rec-summary">
|
||||||
|
<span class="rec-summary-item">基准值:¥ {{ result.base_value.toLocaleString() }}</span>
|
||||||
|
<span class="rec-summary-item">调整项:{{ result.adjustment_count }}/{{ result.total_adjustments }}</span>
|
||||||
|
<span class="rec-summary-item">最终值:¥ {{ result.final_value.toLocaleString() }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-empty v-else-if="!calculating" description="选择指标类型,点击「计算」生成合规调节表" />
|
||||||
|
|
||||||
|
<div v-if="!result?.has_real_data && result" class="demo-hint">
|
||||||
|
⚠️ 当前部分数据为示例数据,实际数据将在科目打标后显示
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 底部操作 -->
|
||||||
|
<div class="mpm-actions" v-if="result">
|
||||||
|
<el-button type="primary" @click="exportDetail">导出附注明细</el-button>
|
||||||
|
<el-button @click="saveTemplate">保存为模板</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const api = axios.create({ baseURL: '/api/cma', timeout: 30000 })
|
||||||
|
api.interceptors.request.use((config: any) => {
|
||||||
|
const token = localStorage.getItem('cma_token')
|
||||||
|
if (token) config.headers.Authorization = `Bearer ${token}`
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── 状态 ──
|
||||||
|
const period = ref('2026-06')
|
||||||
|
const indicatorType = ref('adjusted_net_profit')
|
||||||
|
const calculating = ref(false)
|
||||||
|
const result = ref<any>(null)
|
||||||
|
const adjusting = ref(false)
|
||||||
|
|
||||||
|
const indicatorOptions = [
|
||||||
|
{ value: 'ebitda', label: 'EBITDA', desc: '息税折旧摊销前利润' },
|
||||||
|
{ value: 'adjusted_net_profit', label: '调整后净利润', desc: '剔除非经常性项目' },
|
||||||
|
{ value: 'free_cash_flow', label: '自由现金流', desc: '经营现金流减资本支出' },
|
||||||
|
{ value: 'custom', label: '自定义', desc: '自定义管理层指标' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// ── 调整项 ──
|
||||||
|
const adjustments = ref<any[]>([])
|
||||||
|
|
||||||
|
// 默认调整项配置
|
||||||
|
const DEFAULT_ADJUSTMENTS: Record<string, any[]> = {
|
||||||
|
ebitda: [
|
||||||
|
{ code: 'tax', name: '加:所得税费用', sign: 1, checked: true, amount: null },
|
||||||
|
{ code: 'interest', name: '加:利息支出', sign: 1, checked: true, amount: null },
|
||||||
|
{ code: 'depreciation', name: '加:折旧与摊销', sign: 1, checked: true, amount: null },
|
||||||
|
{ code: 'impairment', name: '加:资产减值损失', sign: 1, checked: false, amount: null },
|
||||||
|
],
|
||||||
|
adjusted_net_profit: [
|
||||||
|
{ code: 'impairment', name: '加:资产减值损失', sign: 1, checked: true, amount: null },
|
||||||
|
{ code: 'equity_incentive', name: '加:股权激励费用', sign: 1, checked: true, amount: null },
|
||||||
|
{ code: 'ma_cost', name: '加:并购相关费用', sign: 1, checked: false, amount: null },
|
||||||
|
{ code: 'nonrecurring_income', name: '减:非经常性投资收益', sign: -1, checked: true, amount: null },
|
||||||
|
{ code: 'asset_disposal', name: '减:资产处置收益', sign: -1, checked: false, amount: null },
|
||||||
|
{ code: 'government_grant', name: '减:政府补助', sign: -1, checked: false, amount: null },
|
||||||
|
],
|
||||||
|
free_cash_flow: [
|
||||||
|
{ code: 'capex', name: '减:资本支出', sign: -1, checked: true, amount: null },
|
||||||
|
{ code: 'working_capital', name: '减:营运资本增加', sign: -1, checked: true, amount: null },
|
||||||
|
{ code: 'maintenance_capex', name: '减:维护性资本支出', sign: -1, checked: false, amount: null },
|
||||||
|
{ code: 'dividend', name: '加:股息收入', sign: 1, checked: false, amount: null },
|
||||||
|
],
|
||||||
|
custom: [
|
||||||
|
{ code: 'adjustment_1', name: '调整项目1', sign: 1, checked: true, amount: null },
|
||||||
|
{ code: 'adjustment_2', name: '调整项目2', sign: -1, checked: true, amount: null },
|
||||||
|
{ code: 'adjustment_3', name: '调整项目3', sign: 1, checked: false, amount: null },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换指标类型时更新调整项
|
||||||
|
watch(indicatorType, (val) => {
|
||||||
|
const defaults = DEFAULT_ADJUSTMENTS[val] || DEFAULT_ADJUSTMENTS.adjusted_net_profit
|
||||||
|
adjustments.value = JSON.parse(JSON.stringify(defaults))
|
||||||
|
result.value = null
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
function resetDefaults() {
|
||||||
|
const defaults = DEFAULT_ADJUSTMENTS[indicatorType.value] || DEFAULT_ADJUSTMENTS.adjusted_net_profit
|
||||||
|
adjustments.value = JSON.parse(JSON.stringify(defaults))
|
||||||
|
calculate()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAdjustmentChange() {
|
||||||
|
// 当用户勾选/取消或改金额时自动重新计算
|
||||||
|
calculate()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function calculate() {
|
||||||
|
calculating.value = true
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
indicator_type: indicatorType.value,
|
||||||
|
period: period.value,
|
||||||
|
adjustments: adjustments.value.map(a => ({
|
||||||
|
code: a.code,
|
||||||
|
name: a.name,
|
||||||
|
sign: a.sign,
|
||||||
|
checked: a.checked,
|
||||||
|
amount: a.amount,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
const r = await api.post('/reports/mpm-calculate', payload)
|
||||||
|
result.value = (r as any).data || {}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('计算失败,请检查参数')
|
||||||
|
result.value = null
|
||||||
|
} finally {
|
||||||
|
calculating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportDetail() {
|
||||||
|
if (!result.value) return
|
||||||
|
const items = result.value.reconciliation_items || []
|
||||||
|
let text = `MPM管理层指标调节表\n`
|
||||||
|
text += `指标类型:${result.value.indicator_name}\n`
|
||||||
|
text += `期间:${result.value.period}\n`
|
||||||
|
text += `基准值(${result.value.base_label}):¥ ${result.value.base_value.toLocaleString()}\n`
|
||||||
|
text += `\n调节过程:\n`
|
||||||
|
text += `──────────────────────────\n`
|
||||||
|
items.forEach((item: any) => {
|
||||||
|
if (item.is_base) {
|
||||||
|
text += `${item.name}\t¥ ${item.amount?.toLocaleString() || '-'}\n`
|
||||||
|
} else if (item.checked && item.effective != null) {
|
||||||
|
const signStr = item.sign > 0 ? '+' : '-'
|
||||||
|
text += ` ${signStr} ${item.name}\t¥ ${Math.abs(item.effective).toLocaleString()}\t余额:¥ ${item.running_total?.toLocaleString() || ''}\n`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
text += `──────────────────────────\n`
|
||||||
|
text += `${result.value.final_label}\t¥ ${result.value.final_value.toLocaleString()}\n`
|
||||||
|
|
||||||
|
// 下载文本文件
|
||||||
|
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `MPM调节表_${result.value.period}_${result.value.indicator_name}.txt`
|
||||||
|
a.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
ElMessage.success('调节表明细已导出')
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveTemplate() {
|
||||||
|
const payload = {
|
||||||
|
indicator_type: indicatorType.value,
|
||||||
|
adjustments: adjustments.value,
|
||||||
|
period: period.value,
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const saved = JSON.parse(localStorage.getItem('mpm_templates') || '[]')
|
||||||
|
const name = `${indicatorType.value}_${period.value}_${Date.now()}`
|
||||||
|
saved.push({ name, data: payload, savedAt: new Date().toISOString() })
|
||||||
|
localStorage.setItem('mpm_templates', JSON.stringify(saved))
|
||||||
|
ElMessage.success(`模板已保存:${name}`)
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('保存模板失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mpm-page { max-width: 1400px; margin: 0 auto; }
|
||||||
|
.mpm-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||||||
|
.header-right { display: flex; gap: 8px; align-items: center; }
|
||||||
|
|
||||||
|
.param-card { min-height: 400px; }
|
||||||
|
.result-card { min-height: 400px; }
|
||||||
|
.result-header { display: flex; align-items: center; gap: 8px; }
|
||||||
|
|
||||||
|
.param-section { margin-bottom: 8px; }
|
||||||
|
.param-label { font-size: 14px; font-weight: 600; color: #1a1a2e; display: block; margin-bottom: 8px; }
|
||||||
|
.param-hint { font-size: 11px; color: #999; font-weight: 400; margin-left: 8px; }
|
||||||
|
|
||||||
|
.indicator-types { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.indicator-radio { display: flex; align-items: center; padding: 6px 0; }
|
||||||
|
.indicator-opt { display: flex; flex-direction: column; }
|
||||||
|
.indicator-name { font-weight: 600; font-size: 13px; }
|
||||||
|
.indicator-desc { font-size: 11px; color: #999; }
|
||||||
|
|
||||||
|
.adjustment-list { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.adjustment-row {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: 4px 8px; border-radius: 4px; transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.adjustment-row:hover { background: #f5f7fa; }
|
||||||
|
.adj-sign-positive { color: #67c23a; font-weight: 600; }
|
||||||
|
.adj-sign-negative { color: #f56c6c; font-weight: 600; }
|
||||||
|
.empty-hint { color: #ccc; font-size: 12px; text-align: center; padding: 20px; }
|
||||||
|
|
||||||
|
/* 调节表样式 */
|
||||||
|
.reconciliation-table { padding: 4px 0; }
|
||||||
|
.rec-item {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
padding: 8px 12px; border-radius: 4px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.rec-base { background: #f0f9eb; border-left: 3px solid #67c23a; }
|
||||||
|
.rec-checked { background: #f5f7fa; border-left: 3px solid #409eff; }
|
||||||
|
.rec-unchecked { opacity: 0.4; border-left: 3px solid #ddd; }
|
||||||
|
.rec-final { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; border-radius: 6px; margin-top: 4px; }
|
||||||
|
.rec-step { flex: 1; display: flex; align-items: center; gap: 6px; }
|
||||||
|
.rec-dot-base { width: 8px; height: 8px; border-radius: 50%; background: #67c23a; flex-shrink: 0; }
|
||||||
|
.rec-dot-checked { width: 8px; height: 8px; border-radius: 50%; background: #409eff; flex-shrink: 0; }
|
||||||
|
.rec-dot-unchecked { width: 8px; height: 8px; border-radius: 50%; background: #ddd; flex-shrink: 0; }
|
||||||
|
.rec-dot-final { width: 8px; height: 8px; border-radius: 50%; background: #fff; flex-shrink: 0; }
|
||||||
|
.rec-name { font-size: 13px; }
|
||||||
|
.rec-amount { font-size: 13px; font-weight: 500; min-width: 160px; text-align: right; font-variant-numeric: tabular-nums; }
|
||||||
|
.rec-negative { color: #f56c6c; }
|
||||||
|
.rec-final .rec-amount { font-size: 16px; font-weight: 700; }
|
||||||
|
.rec-final .rec-name { font-weight: 700; }
|
||||||
|
.rec-no-data { color: #999; font-size: 11px; }
|
||||||
|
.rec-running { font-size: 12px; color: #999; min-width: 160px; text-align: right; font-variant-numeric: tabular-nums; }
|
||||||
|
.rec-final .rec-running { color: rgba(255,255,255,0.7); }
|
||||||
|
|
||||||
|
.rec-divider { height: 1px; background: #ebeef5; margin: 8px 12px; }
|
||||||
|
|
||||||
|
.rec-summary {
|
||||||
|
display: flex; gap: 16px; padding: 8px 12px; margin-top: 8px;
|
||||||
|
background: #fafafa; border-radius: 4px; font-size: 12px; color: #666;
|
||||||
|
}
|
||||||
|
.rec-summary-item { flex: 1; }
|
||||||
|
|
||||||
|
.demo-hint { font-size: 11px; color: #d46b08; margin-top: 8px; padding: 4px 12px; background: #fff7e6; border-radius: 4px; }
|
||||||
|
|
||||||
|
.mpm-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
|
||||||
|
</style>
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="new30-net-profit">
|
<div class="new30-net-profit" v-if="new30Format !== 'dual'">
|
||||||
<div class="new30-profit-row">
|
<div class="new30-profit-row">
|
||||||
<span class="new30-profit-label">合计:</span>
|
<span class="new30-profit-label">合计:</span>
|
||||||
<span class="new30-profit-value" :class="{ negative: new30NetProfit < 0 }">
|
<span class="new30-profit-value" :class="{ negative: new30NetProfit < 0 }">
|
||||||
@@ -102,9 +102,57 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 双列对比模式 -->
|
||||||
|
<div v-if="new30Format === 'dual' && dualComparisonData" class="dual-comparison">
|
||||||
|
<div class="dual-desc">新旧准则双列对比 — 旧准则(一行到底) vs 新准则(五板块)</div>
|
||||||
|
|
||||||
|
<div class="dual-grid">
|
||||||
|
<!-- 旧准则列 -->
|
||||||
|
<div class="dual-col">
|
||||||
|
<div class="dual-col-header old-col">旧准则格式</div>
|
||||||
|
<el-table :data="dualComparisonData.old_format?.items || []" border stripe size="small" style="width:100%;">
|
||||||
|
<el-table-column prop="name" label="项目" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :style="{ fontWeight: row.is_total ? 700 : row.is_subtotal ? 600 : 400 }">{{ row.name }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="金额" width="140" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :style="{ fontWeight: row.is_total ? 700 : 400 }">
|
||||||
|
{{ row.value != null ? '¥ ' + row.value.toLocaleString() : '-' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新准则列 -->
|
||||||
|
<div class="dual-col">
|
||||||
|
<div class="dual-col-header new-col">新30号准则格式</div>
|
||||||
|
<div class="dual-blocks">
|
||||||
|
<div v-for="block in dualComparisonData.new_format?.blocks || []" :key="block.key" class="dual-block">
|
||||||
|
<div class="dual-block-header">
|
||||||
|
<span class="dual-block-name">{{ block.short_name }}</span>
|
||||||
|
<span class="dual-block-subtotal">{{ formatMoney(block.subtotal) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-for="item in block.items" :key="item.code" class="dual-block-item">
|
||||||
|
<span class="dual-item-name">{{ item.name }}</span>
|
||||||
|
<span class="dual-item-amount">{{ item.effective != null ? formatMoney(item.effective) : '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dual-net-profit">
|
||||||
|
<span>净利润:</span>
|
||||||
|
<span class="dual-net-val">{{ formatMoney(dualComparisonData.new_format?.net_profit) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="new30-footer">
|
<div class="new30-footer">
|
||||||
<el-button size="small" :type="new30Format === 'new' ? 'primary' : ''" @click="switchNew30Format('new')">新准则</el-button>
|
<el-button size="small" :type="new30Format === 'new' ? 'primary' : ''" @click="switchNew30Format('new')">新准则</el-button>
|
||||||
<el-button size="small" :type="new30Format === 'old' ? 'primary' : ''" @click="switchNew30Format('old')">旧准则对比</el-button>
|
<el-button size="small" :type="new30Format === 'old' ? 'primary' : ''" @click="switchNew30Format('old')">旧准则</el-button>
|
||||||
|
<el-button size="small" :type="new30Format === 'dual' ? 'primary' : ''" @click="switchNew30Format('dual')">双列对比</el-button>
|
||||||
<el-button size="small" @click="exportNew30">导出</el-button>
|
<el-button size="small" @click="exportNew30">导出</el-button>
|
||||||
<span v-if="new30DemoMode" class="new30-demo-hint">⚠️ 当前显示示例数据,科目打标后显示实际数据</span>
|
<span v-if="new30DemoMode" class="new30-demo-hint">⚠️ 当前显示示例数据,科目打标后显示实际数据</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -357,20 +405,36 @@ const new30Blocks = ref<any[]>([])
|
|||||||
const new30NetProfit = ref(0)
|
const new30NetProfit = ref(0)
|
||||||
const new30Format = ref('new')
|
const new30Format = ref('new')
|
||||||
const new30DemoMode = ref(false)
|
const new30DemoMode = ref(false)
|
||||||
|
const dualComparisonData = ref<any>(null)
|
||||||
|
|
||||||
async function loadNew30() {
|
async function loadNew30() {
|
||||||
loadingNew30.value = true
|
loadingNew30.value = true
|
||||||
|
dualComparisonData.value = null
|
||||||
try {
|
try {
|
||||||
const r = await api.get('/reports/profit-statement', {
|
const params: any = { period: reportPeriod.value }
|
||||||
params: { period: reportPeriod.value, format: 'new' }
|
if (new30Format.value === 'dual') {
|
||||||
})
|
params.format = 'dual'
|
||||||
|
} else {
|
||||||
|
params.format = 'new'
|
||||||
|
}
|
||||||
|
const r = await api.get('/reports/profit-statement', { params })
|
||||||
const d = (r as any).data || {}
|
const d = (r as any).data || {}
|
||||||
new30Blocks.value = d.blocks || []
|
|
||||||
new30NetProfit.value = d.net_profit ?? 0
|
if (new30Format.value === 'dual') {
|
||||||
new30DemoMode.value = !d.all_items_have_data
|
dualComparisonData.value = d
|
||||||
|
new30Blocks.value = d.new_format?.blocks || []
|
||||||
|
new30NetProfit.value = d.new_format?.net_profit ?? 0
|
||||||
|
new30DemoMode.value = !d.new_format?.all_items_have_data
|
||||||
|
} else {
|
||||||
|
dualComparisonData.value = null
|
||||||
|
new30Blocks.value = d.blocks || []
|
||||||
|
new30NetProfit.value = d.net_profit ?? 0
|
||||||
|
new30DemoMode.value = !d.all_items_have_data
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
new30Blocks.value = []
|
new30Blocks.value = []
|
||||||
new30NetProfit.value = 0
|
new30NetProfit.value = 0
|
||||||
|
dualComparisonData.value = null
|
||||||
ElMessage.error('加载新30号准则利润表失败')
|
ElMessage.error('加载新30号准则利润表失败')
|
||||||
} finally {
|
} finally {
|
||||||
loadingNew30.value = false
|
loadingNew30.value = false
|
||||||
@@ -387,7 +451,7 @@ function formatMoney(val: number): string {
|
|||||||
|
|
||||||
function switchNew30Format(fmt: string) {
|
function switchNew30Format(fmt: string) {
|
||||||
new30Format.value = fmt
|
new30Format.value = fmt
|
||||||
if (fmt === 'new') {
|
if (fmt === 'new' || fmt === 'dual') {
|
||||||
loadNew30()
|
loadNew30()
|
||||||
} else {
|
} else {
|
||||||
// 切换到旧准则tab
|
// 切换到旧准则tab
|
||||||
@@ -710,4 +774,23 @@ onMounted(() => {
|
|||||||
.restate-dot-red { background: #cf1322; }
|
.restate-dot-red { background: #cf1322; }
|
||||||
.restate-dot-green { background: #389e0d; }
|
.restate-dot-green { background: #389e0d; }
|
||||||
.restate-dot-orange { background: #d46b08; }
|
.restate-dot-orange { background: #d46b08; }
|
||||||
|
|
||||||
|
/* 双列对比样式 */
|
||||||
|
.dual-comparison { margin-top: 12px; }
|
||||||
|
.dual-desc { font-size: 13px; color: #888; margin-bottom: 12px; }
|
||||||
|
.dual-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||||
|
@media screen and (max-width: 1000px) { .dual-grid { grid-template-columns: 1fr; } }
|
||||||
|
.dual-col { background: #fff; border-radius: 8px; border: 1px solid #ebeef5; overflow: hidden; }
|
||||||
|
.dual-col-header { padding: 10px 14px; font-weight: 600; font-size: 14px; color: #fff; }
|
||||||
|
.dual-col-header.old-col { background: #909399; }
|
||||||
|
.dual-col-header.new-col { background: #409eff; }
|
||||||
|
.dual-blocks { padding: 8px; }
|
||||||
|
.dual-block { margin-bottom: 8px; border: 1px solid #ebeef5; border-radius: 4px; overflow: hidden; }
|
||||||
|
.dual-block-header { display: flex; justify-content: space-between; padding: 6px 10px; background: #f9fafc; font-size: 13px; font-weight: 600; }
|
||||||
|
.dual-block-subtotal { color: #1a1a2e; }
|
||||||
|
.dual-block-item { display: flex; justify-content: space-between; padding: 4px 10px 4px 16px; font-size: 12px; color: #555; border-top: 1px dashed #f0f0f0; }
|
||||||
|
.dual-item-name { flex: 1; }
|
||||||
|
.dual-item-amount { font-weight: 500; }
|
||||||
|
.dual-net-profit { padding: 10px 14px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; border-radius: 4px; margin: 8px; display: flex; align-items: center; gap: 8px; font-size: 14px; }
|
||||||
|
.dual-net-val { font-size: 18px; font-weight: 700; flex: 1; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user