@@ -15,104 +15,6 @@
< / div >
< div v-else >
< el -tabs v-model = "activeTab" style="margin-top:16px;" >
< el -tab -pane label = "预算录入" name = "input" >
< div style = "margin-bottom:8px;font-size:12px;color:#909399;display:flex;align-items:center;gap:8px;flex-wrap:wrap;" >
选择战略地图
< el-select v-model = "selectedMapId" placeholder="选择已发布的战略地图" style="width:260px;" @change="onMapSelect" >
< el -option v-for = "m in publishedMaps" :key="m.id" :label="m.title" :value="m.id" / >
< / el-select >
< el-tag v-if = "selectedMapId" size="small" type="success" > 已选择 < / el -tag >
< span style = "color:#999;" > 共 { { total } } 条KPI < / span >
< / div >
< div style = "display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap;align-items:center;" >
< el-select v-model = "filterYear" placeholder="年份" style="width:100px;" @change="loadBudget"><el-option v-for="y in yearOptions" :key="y" :label="y" :value="y" / > < / el-select >
< el-select v-model = "filterVersion" placeholder="版本" style="width:180px;" @change="loadBudget" clearable >
< el-option v-for = "v in versionsList" :key="v.version" :label="v.version + (v.status === 'active' ? ' (当前)' : '')" :value="v.version" / >
< / el-select >
< el-select v-model = "filterMonth" placeholder="月份" style="width:90px;" @change="loadBudget"><el-option label="全年" :value="0" /><el-option v-for="m in 12" :key="m" :label="`${m}月`" :value="m" / > < / el-select >
< el-input v-model = "searchKpi" placeholder="搜索KPI名称" clearable style="width:200px;" @clear="loadBudget" @keyup.enter="loadBudget" / >
< el-button type = "primary" @click ="loadBudget" > 查询 < / el -button >
< el-button type = "success" @click ="showAddBudget = true" > + 新增预算 < / el -button >
< el-button @click ="showDecompose = true" :disabled = "!filterYear" > 年度分解 < / el-button >
< el-divider direction = "vertical" / >
< el-radio-group v-model = "budgetViewMode" size="small" @change="onViewModeChange"><el-radio-button value="list">KPI列表</el-radio-button><el-radio-button value="summary" > 维度汇总 < / el -radio -button > < / el-radio-group >
< el-switch v-model = "batchEditMode" active-text="批量编辑" inactive-text="逐行编辑" @change="onBatchEditToggle" / >
< el-button v-if = "batchEditMode" type="primary" @click="batchSaveAll" :loading="batchSavingAll" :disabled="changedRows.length === 0" > 批量保存 ( {{ changedRows.length }} ) < / el -button >
< el-button v-if = "batchEditMode" @click="batchCancelAll" > 取消 < / el -button >
< el-divider direction = "vertical" / >
< div style = "display:flex;align-items:center;gap:6px;font-size:12px;" >
< span style = "color:#909399;" > 预算模式 : < / span >
< el-tag size = "small" : type = "budgetMode === 'rolling' ? 'warning' : 'info'" effect = "plain" >
{ { budgetMode === 'rolling' ? '滚动预算(滚动12月)' : '固定预算(固定年度)' } }
< / el-tag >
< span style = "color:#c0c4cc;" > 系统级设置 , 影响全部账套 · 更改请到 【 系统设置 】 < / span >
< / div >
< el-button v-if = "budgetMode === 'rolling'" size="small" type="warning" @click="doRollForward" :loading="rollingForward" > 延展 < / el -button >
< / div >
< template v-if = "budgetViewMode === 'list'" >
< el -table :data = "budgetList" v-loading = "loading" border stripe size="small" style="width:100%;" :row-class-name="rowClass" :empty-text="filterYear ? '暂无数据,在行内编辑填值后保存即可创建' : '请先选择年份'" >
< el -table -column type = "index" label = "#" width = "40" / >
< el-table-column prop = "kpi_code" label = "KPI编码" width = "120" / >
< el-table-column prop = "kpi_name" label = "KPI名称" min -width = " 160 " / >
< el-table-column prop = "dimension" label = "维度" width = "80" > < template # default = "{ row }" > < el-tag size = "small" :type = "dimTag(row.dimension)" > { { dimLabel ( row . dimension ) } } < / el-tag > < / template > < / el-table-column >
< el-table-column prop = "period" label = "期间" width = "90" / >
< el-table-column prop = "budget_value" label = "预算值" width = "170" >
< template # default = "{ row }" > < el-input-number v-if = "row._editing" v-model="row._editValue" :min="0" :precision="row.precision || 0" :step="row.step || 1" controls-position="right" style="width:155px;" @change="onEditChange(row)" / > < span v-else > {{ formatNumber ( row.budget_value , row.unit ) }} < / span > < / template >
< / el -table -column >
< el-table-column prop = "unit" label = "单位" width = "60" / >
<!-- 行动方案列 ( 非财务维度显示 ) -- >
< el-table-column label = "行动方案" width = "120" >
< template # default = "{ row }" >
< span v-if = "row.dimension === 'finance'" style="color:#ccc;font-size:12px;" > — < / span >
< el -button v-else size = "small" link type = "primary" @click ="showActionPlans(row)" >
{{ row._actionCount ! = null ? ` $ { row._actionCount } 项 ` : ' 关联 ' }}
< / el -button >
< / template >
< / el-table-column >
< el-table-column label = "操作" width = "160" fixed = "right" >
< template # default = "{ row }" >
< el-button v-if = "!batchEditMode && !row._editing" size="small" @click="startEdit(row)" > 编辑 < / el -button >
< template v-else-if = "!batchEditMode"><el-button size="small" type="primary" @click="saveEdit(row)" :loading="row._saving">保存</el-button><el-button size="small" @click="cancelEdit(row)" > 取消 < / el -button > < / template >
< el-button v-if = "!batchEditMode && row.id" size="small" type="danger" plain @click="doDelete(row)" > 删除 < / el -button >
< el-tag v-else-if = "!batchEditMode" size="small" type="info" effect="plain" style="cursor:pointer" @click="startEdit(row)" > 待创建 < / el -tag >
< / template >
< / el-table-column >
< / el-table >
< / template >
< template v-if = "budgetViewMode === 'summary'" >
< el -row :gutter = "16" style = "margin-bottom:16px;" >
< el-col :span = "6" v-for = "card in summaryCards" :key="card.key" >
< el -card shadow = "hover" : body -style = " { borderLeft : ' 4px solid ' + card.color } " >
< div style = "text-align:center;" > < div style = "font-size:14px;margin-bottom:4px;" > { { card . icon } } { { card . name } } < / div > < div style = "font-size:24px;font-weight:600;color:#333;" > { { formatNumber ( card . totalBudget ) } } < / div > < div style = "font-size:11px;color:#999;margin-top:4px;" > { { card . kpiCount } } 个KPI · 占比 { { card . ratio } } % < / div > < / div >
< / el-card >
< / el-col >
< / el-row >
< el-table :data = "dimSummaryRows" border stripe size = "small" style = "width:100%;" >
< el-table-column prop = "dimension" label = "维度" width = "100" > < template # default = "{ row }" > < el-tag size = "small" :type = "dimTag(row.dimension)" > { { dimLabel ( row . dimension ) } } < / el-tag > < / template > < / el-table-column >
< el-table-column prop = "kpiCount" label = "KPI数" width = "80" / >
< el-table-column prop = "totalBudget" label = "预算总额" width = "140" > < template # default = "{ row }" > { { formatNumber ( row . totalBudget ) } } < / template > < / el-table-column >
< el-table-column prop = "ratio" label = "占比" width = "100" > < template # default = "{ row }" > < el-progress :percentage = "row.ratio" :stroke-width = "12" text -inside / > < / template > < / el-table-column >
< el-table-column prop = "avgPerKpi" label = "KPI均值" width = "140" > < template # default = "{ row }" > { { formatNumber ( row . avgPerKpi ) } } < / template > < / el-table-column >
< / el-table >
< / template >
< / el-tab-pane >
< el-tab-pane label = "年度分解" name = "decompose" >
< el-card >
< template # header > < div style = "display:flex;justify-content:space-between;align-items:center;" > < span > 年度预算 → 月度自动分解 < / span > < el-button type = "primary" @click ="doDecompose" :loading = "decomposing" > 执行分解 < / el-button > < / div > < / template >
< el-form :model = "decomposeForm" label -width = " 120px " style = "max-width:500px;" >
< el-form-item label = "年份" > < el-select v-model = "decomposeForm.year" style="width:150px;"><el-option v-for="y in yearOptions" :key="y" :label="y" :value="y" / > < / el-select > < / el-form-item >
< el-form-item label = "分解方式" > < el-radio-group v-model = "decomposeForm.method"><el-radio value="equal">均分(1/12) </el-radio><el-radio value="weighted" > 按历史权重 < / el -radio > < / el-radio-group > < / el-form-item >
< / el-form >
< el-alert v-if = "decomposeResult" :title="decomposeResult" type="success" show-icon :closable="false" style="margin-top:16px;" / >
< el-table v-if = "decomposeDetails.length > 0" :data="decomposeDetails" border stripe size="small" style="width:100%;margin-top:12px;" max-height="300" >
< el -table -column prop = "kpi_code" label = "KPI编码" width = "120" / > < el-table-column prop = "kpi_name" label = "KPI名称" min -width = " 150 " / >
< el-table-column prop = "annual_budget" label = "年度预算" width = "120" > < template # default = "{ row }" > { { formatNumber ( row . annual _budget ) } } < / template > < / el-table-column >
< el-table-column prop = "method" label = "方式" width = "80" / > < el-table-column prop = "monthly_count" label = "月数" width = "60" / >
< / el-table >
< / el-card >
< / el-tab-pane >
< el -tab -pane label = "战略预算编制" name = "strategy" >
< div v-if = "!selectedMap" style="padding:40px 0;text-align:center;color:#999;" > < p > 请先在预算录入中选择战略地图 < / p > < / div >
< template v-else >
@@ -152,108 +54,6 @@
</template>
</el-tab-pane>
<el-tab-pane label=" 版本管理 " name=" versions ">
<div style=" display : flex ; gap : 12 px ; margin - bottom : 16 px ; align - items : center ; ">
<el-select v-model=" versionYear " placeholder=" 年份 " style=" width : 120 px ; "><el-option v-for=" y in yearOptions " :key=" y " :label=" y " :value=" y " /></el-select>
<el-button type=" primary " @click=" loadVersions ">刷新</el-button>
</div>
<el-table :data=" versionsList " v-loading=" versionsLoading " border stripe size=" small " style=" width : 100 % ; ">
<el-table-column prop=" version " label=" 版本 " width=" 90 "><template #default=" { row } "><el-tag :type=" row . version === currentVersion ? 'success' : 'info' " size=" small ">{{ row.version }}</el-tag></template></el-table-column>
<el-table-column prop=" approval _status " label=" 状态 " width=" 90 ">
<template #default=" { row } "><el-tag v-if=" row . approval _status === 'approved' " type=" success " size=" small ">已批准</el-tag><el-tag v-else-if=" row . approval _status === 'submitted' " type=" warning " size=" small ">待审批</el-tag><el-tag v-else-if=" row . approval _status === 'rejected' " type=" danger " size=" small ">已驳回</el-tag><el-tag v-else type=" info " size=" small ">草稿</el-tag></template>
</el-table-column>
<el-table-column prop=" plan _count " label=" 预算条目数 " width=" 100 " />
<el-table-column prop=" total _budget " label=" 预算总额 " width=" 130 "><template #default=" { row } ">{{ formatNumber(row.total_budget) }}</template></el-table-column>
<el-table-column prop=" last _updated " label=" 最后更新 " min-width=" 150 " />
<el-table-column label=" 操作 " width=" 200 " fixed=" right ">
<template #default=" { row } ">
<el-button v-if=" row . approval _status === 'draft' || row . approval _status === 'active' " size=" small " @click=" submitVersion ( row ) ">提交审批</el-button>
<template v-if=" row . approval _status === 'submitted' "><el-button size=" small " type=" success " @click=" approveVersion ( row , 'approved' ) ">批准</el-button><el-button size=" small " type=" danger " @click=" approveVersion ( row , 'rejected' ) ">驳回</el-button></template>
<el-button v-if=" row . approval _status === 'rejected' " size=" small " @click=" resubmitVersion ( row ) ">重新提交</el-button>
<el-checkbox v-model=" row . _selected " @change=" onVersionSelect ( row ) " :disabled=" selectedVersions . length >= 2 && ! row . _selected " style=" margin - left : 8 px ; " />
</template>
</el-table-column>
</el-table>
<div v-if=" selectedVersions . length === 2 " style=" margin - top : 16 px ; "><el-button type=" primary " @click=" loadVersionDiff " :loading=" diffLoading ">对比 {{ selectedVersions[0].version }} vs {{ selectedVersions[1].version }}</el-button></div>
</el-tab-pane>
<el-tab-pane label=" 预算执行 " name=" execution ">
<div style=" display : flex ; gap : 12 px ; margin - bottom : 16 px ; flex - wrap : wrap ; align - items : center ; ">
<el-select v-model=" execFilterYear " placeholder=" 年份 " style=" width : 100 px ; " @change=" loadExecutionReport "><el-option v-for=" y in yearOptions " :key=" y " :label=" y " :value=" y " /></el-select>
<el-select v-model=" execFilterMonth " placeholder=" 截止月份 " style=" width : 110 px ; " @change=" loadExecutionReport "><el-option v-for=" m in 12 " :key=" m " :label=" ` 至 ${ m } 月 ` " :value=" m " /></el-select>
<el-select v-model=" execFilterDim " placeholder=" 维度 " clearable style=" width : 110 px ; " @change=" loadExecutionReport "><el-option label=" 财务 " value=" finance " /><el-option label=" 客户 " value=" customer " /><el-option label=" 内部流程 " value=" process " /><el-option label=" 学习成长 " value=" learning " /></el-select>
<el-button type=" primary " @click=" loadExecutionReport ">刷新</el-button>
<el-button type=" warning " :loading=" syncingCash " @click=" syncCashPlans ">⇄ 同步现金流计划</el-button>
</div>
<el-row :gutter=" 16 " style=" margin - bottom : 16 px ; ">
<el-col :span=" 6 " v-for=" card in execSummaryCards " :key=" card . label "><el-card shadow=" hover "><div style=" text - align : center ; "><div style=" font - size : 12 px ; color : # 999 ; ">{{ card.label }}</div><div style=" font - size : 22 px ; font - weight : 600 ; margin - top : 4 px ; " :style=" { color : card . color } ">{{ card.value }}</div></div></el-card></el-col>
</el-row>
<el-table :data=" execReport " v-loading=" execLoading " border stripe size=" small " style=" width : 100 % ; ">
<el-table-column prop=" kpi _code " label=" 编码 " width=" 110 " /><el-table-column prop=" kpi _name " label=" KPI名称 " min-width=" 140 " />
<el-table-column prop=" dimension " label=" 维度 " width=" 80 "><template #default=" { row } "><el-tag size=" small " :type=" dimTag ( row . dimension ) ">{{ dimLabel(row.dimension) }}</el-tag></template></el-table-column>
<el-table-column prop=" budget _value " label=" 预算值 " width=" 130 "><template #default=" { row } ">{{ formatNumber(row.budget_value, row.unit) }}</template></el-table-column>
<!-- 战略目标 vs 预算(2026-08-27: 允许不同, 差异可见可解释) -->
<el-table-column prop=" strategic _target " label=" 战略目标 " width=" 120 "><template #default=" { row } "><span :style=" { color : row . target _gap _level === 'high' ? '#f56c6c' : row . target _gap _level === 'medium' ? '#e6a23c' : '#606266' } ">{{ formatNumber(row.strategic_target, row.unit) }}</span></template></el-table-column>
<el-table-column label=" 目标差异 " width=" 110 "><template #default=" { row } ">
<span v-if=" row . target _gap _pct != null " :style=" { color : row . target _gap _level === 'high' ? '#f56c6c' : row . target _gap _level === 'medium' ? '#e6a23c' : '#67c23a' , fontWeight : 600 } ">{{ row.target_gap_pct > 0 ? '+' : '' }}{{ row.target_gap_pct }}%</span>
<el-tooltip v-if=" row . target _gap _level === 'high' " content=" 预算偏离战略目标超20 % , 需说明原因 " placement=" top "><span style=" margin - left : 4 px ; cursor : help ; ">⚠️</span></el-tooltip>
<span v-else style=" color : # c0c4cc ; ">-</span>
</template></el-table-column>
<el-table-column prop=" actual _value " label=" 实际值 " width=" 130 "><template #default=" { row } ">{{ formatNumber(row.actual_value, row.unit) }}</template></el-table-column>
<el-table-column prop=" deviation _value " label=" 偏差 " width=" 130 "><template #default=" { row } "><span :style=" { color : row . deviation _value > 0 ? '#f56c6c' : row . deviation _value < 0 ? '#67c23a' : '#999' } ">{{ row.deviation_value > 0 ? '+' : '' }}{{ formatNumber(row.deviation_value, row.unit) }}</span></template></el-table-column>
<el-table-column label=" 执行率 " width=" 100 "><template #default=" { row } "><el-progress v-if=" row . execution _rate != null " :percentage=" row . execution _rate " :status=" row . execution _rate > 100 ? 'exception' : row . execution _rate > 80 ? 'warning' : 'success' " :stroke-width=" 16 " :text-inside=" true " /><span v-else style=" color : # 909399 ">-</span></template></el-table-column>
<el-table-column label=" 预警 " width=" 80 "><template #default=" { row } "><el-tag v-if=" row . alert _level === 'red' " size=" small " type=" danger ">严重</el-tag><el-tag v-else-if=" row . alert _level === 'yellow' " size=" small " type=" warning ">关注</el-tag><el-tag v-else size=" small " type=" success ">正常</el-tag></template></el-table-column>
</el-table>
<!-- P2-⑥ 现金流分类规则 + 待分类队列 -->
<el-row :gutter=" 16 " style=" margin - top : 20 px ; ">
<el-col :span=" 12 ">
<el-card shadow=" never ">
<template #header>
<div style=" display : flex ; justify - content : space - between ; align - items : center ; ">
<span><strong>现金流分类规则</strong> <span style=" font - size : 12 px ; color : # 999 ; ">( KPI→receive/pay 可维护)</span></span>
<el-button type=" primary " size=" small " @click=" showClassifyRuleDialog = true ">+ 新建规则</el-button>
</div>
</template>
<el-table :data=" cashClassifyRules " border stripe size=" small " style=" width : 100 % ; " max-height=" 240 ">
<el-table-column prop=" kpi _code " label=" KPI编码 " width=" 110 "><template #default=" { row } ">{{ row.kpi_code || row.kpi_code_pattern || '--' }}</template></el-table-column>
<el-table-column prop=" kpi _name " label=" 名称 / 关键词 " min-width=" 110 "><template #default=" { row } ">{{ row.kpi_name || row.kpi_code_pattern || '--' }}</template></el-table-column>
<el-table-column label=" 类型 " width=" 80 ">
<template #default=" { row } "><el-tag size=" small " :type=" row . plan _type === 'receive' ? 'success' : 'danger' ">{{ row.plan_type === 'receive' ? '收' : '付' }}</el-tag></template>
</el-table-column>
<el-table-column prop=" priority " label=" 优先级 " width=" 70 " />
<el-table-column label=" 操作 " width=" 80 " fixed=" right ">
<template #default=" { row } "><el-button size=" small " link type=" danger " @click=" deleteClassifyRule ( row ) ">删除</el-button></template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<el-col :span=" 12 ">
<el-card shadow=" never ">
<template #header>
<div style=" display : flex ; justify - content : space - between ; align - items : center ; ">
<span><strong>待分类队列</strong> <span style=" font - size : 12 px ; color : # 999 ; ">(无法判别的KPI不再静默跳过)</span></span>
<el-badge :value=" unclassifiedCount " :hidden=" unclassifiedCount === 0 " type=" danger "><el-button size=" small " @click=" loadCashClassify ">刷新</el-button></el-badge>
</div>
</template>
<el-table :data=" unclassifiedRows " border stripe size=" small " style=" width : 100 % ; " max-height=" 240 ">
<el-table-column prop=" kpi _name " label=" KPI " min-width=" 120 " />
<el-table-column prop=" period " label=" 期间 " width=" 80 " />
<el-table-column prop=" budget _value " label=" 预算值 " width=" 90 "><template #default=" { row } ">{{ formatNumber(row.budget_value) }}</template></el-table-column>
<el-table-column label=" 操作 " width=" 140 " fixed=" right ">
<template #default=" { row } ">
<el-button size=" small " link type=" success " @click=" classifyUnclassified ( row , 'receive' ) ">收</el-button>
<el-button size=" small " link type=" danger " @click=" classifyUnclassified ( row , 'pay' ) ">付</el-button>
<el-button size=" small " link @click=" ignoreUnclassified ( row ) ">忽略</el-button>
</template>
</el-table-column>
</el-table>
<div v-if=" unclassifiedRows . length === 0 " style=" padding : 16 px 0 ; text - align : center ; color : # 999 ; font - size : 13 px ; ">暂无待分类KPI,同步现金流计划后未命中规则的KPI会出现在这里</div>
</el-card>
</el-col>
</el-row>
</el-tab-pane>
<el-tab-pane label=" 编制方法 " name=" method ">
<div>
<p style=" font - size : 14 px ; color : # 606266 ; margin - bottom : 16 px ; ">选择预算编制方法 — CMA P1 预算编制(增量预算 vs 零基预算 vs 弹性预算)</p>
@@ -380,94 +180,263 @@
<DriverFactorBudget />
</el-tab-pane>
<el-tab-pane label=" 实际值归集 " name=" collect ">
<el-tab-pane label=" 预算录入 " name=" input ">
<div style=" margin - bottom : 8 px ; font - size : 12 px ; color : # 909399 ; display : flex ; align - items : center ; gap : 8 px ; flex - wrap : wrap ; ">
选择战略地图
<el-select v-model=" selectedMapId " placeholder=" 选择已发布的战略地图 " style=" width : 260 px ; " @change=" onMapSelect ">
<el-option v-for=" m in publishedMaps " :key=" m . id " :label=" m . title " :value=" m . id " />
</el-select>
<el-tag v-if=" selectedMapId " size=" small " type=" success ">已选择</el-tag>
<span style=" color : # 999 ; ">共 {{ total }} 条KPI</span>
</div>
<div style=" display : flex ; gap : 12 px ; margin - bottom : 16 px ; flex - wrap : wrap ; align - items : center ; ">
<el-select v-model=" filterYear " placeholder=" 年份 " style=" width : 100 px ; " @change=" loadBudget "><el-option v-for=" y in yearOptions " :key=" y " :label=" y " :value=" y " /></el-select>
<el-select v-model=" filterVersion " placeholder=" 版本 " style=" width : 180 px ; " @change=" loadBudget " clearable>
<el-option v-for=" v in versionsList " :key=" v . version " :label=" v . version + ( v . status === 'active' ? ' (当前)' : '' ) " :value=" v . version " />
</el-select>
<el-select v-model=" filterMonth " placeholder=" 月份 " style=" width : 90 px ; " @change=" loadBudget "><el-option label=" 全年 " :value=" 0 " /><el-option v-for=" m in 12 " :key=" m " :label=" ` ${ m } 月 ` " :value=" m " /></el-select>
<el-input v-model=" searchKpi " placeholder=" 搜索KPI名称 " clearable style=" width : 200 px ; " @clear=" loadBudget " @keyup.enter=" loadBudget " />
<el-button type=" primary " @click=" loadBudget ">查询</el-button>
<el-button type=" success " @click=" showAddBudget = true ">+ 新增预算</el-button>
<el-button @click=" showDecompose = true " :disabled=" ! filterYear ">年度分解</el-button>
<el-divider direction=" vertical " />
<el-radio-group v-model=" budgetViewMode " size=" small " @change=" onViewModeChange "><el-radio-button value=" list ">KPI列表</el-radio-button><el-radio-button value=" summary ">维度汇总</el-radio-button></el-radio-group>
<el-switch v-model=" batchEditMode " active-text=" 批量编辑 " inactive-text=" 逐行编辑 " @change=" onBatchEditToggle " />
<el-button v-if=" batchEditMode " type=" primary " @click=" batchSaveAll " :loading=" batchSavingAll " :disabled=" changedRows . length === 0 ">批量保存 ({{ changedRows.length }})</el-button>
<el-button v-if=" batchEditMode " @click=" batchCancelAll ">取消</el-button>
<el-divider direction=" vertical " />
<div style=" display : flex ; align - items : center ; gap : 6 px ; font - size : 12 px ; ">
<span style=" color : # 909399 ; ">预算模式:</span>
<el-tag size=" small " :type=" budgetMode === 'rolling' ? 'warning' : 'info' " effect=" plain ">
{{ budgetMode === 'rolling' ? '滚动预算(滚动12月)' : '固定预算(固定年度)' }}
</el-tag>
<span style=" color : # c0c4cc ; ">系统级设置,影响全部账套 · 更改请到【系统设置】</span>
</div>
<el-button v-if=" budgetMode === 'rolling' " size=" small " type=" warning " @click=" doRollForward " :loading=" rollingForward ">延展</el-button>
</div>
<template v-if=" budgetViewMode === 'list' ">
<el-table :data=" budgetList " v-loading=" loading " border stripe size=" small " style=" width : 100 % ; " :row-class-name=" rowClass " :empty-text=" filterYear ? '暂无数据,在行内编辑填值后保存即可创建' : '请先选择年份' ">
<el-table-column type=" index " label=" # " width=" 40 " />
<el-table-column prop=" kpi _code " label=" KPI编码 " width=" 120 " />
<el-table-column prop=" kpi _name " label=" KPI名称 " min-width=" 160 " />
<el-table-column prop=" dimension " label=" 维度 " width=" 80 "><template #default=" { row } "><el-tag size=" small " :type=" dimTag ( row . dimension ) ">{{ dimLabel(row.dimension) }}</el-tag></template></el-table-column>
<el-table-column prop=" period " label=" 期间 " width=" 90 " />
<el-table-column prop=" budget _value " label=" 预算值 " width=" 170 ">
<template #default=" { row } "><el-input-number v-if=" row . _editing " v-model=" row . _editValue " :min=" 0 " :precision=" row . precision || 0 " :step=" row . step || 1 " controls-position=" right " style=" width : 155 px ; " @change=" onEditChange ( row ) " /><span v-else>{{ formatNumber(row.budget_value, row.unit) }}</span></template>
</el-table-column>
<el-table-column prop=" unit " label=" 单位 " width=" 60 " />
<!-- 行动方案列(非财务维度显示) -->
<el-table-column label=" 行动方案 " width=" 120 ">
<template #default=" { row } ">
<span v-if=" row . dimension === 'finance' " style=" color : # ccc ; font - size : 12 px ; ">—</span>
<el-button v-else size=" small " link type=" primary " @click=" showActionPlans ( row ) ">
{{ row._actionCount != null ? `${row._actionCount}项` : '关联' }}
</el-button>
</template>
</el-table-column>
<el-table-column label=" 操作 " width=" 160 " fixed=" right ">
<template #default=" { row } ">
<el-button v-if=" ! batchEditMode && ! row . _editing " size=" small " @click=" startEdit ( row ) ">编辑</el-button>
<template v-else-if=" ! batchEditMode "><el-button size=" small " type=" primary " @click=" saveEdit ( row ) " :loading=" row . _saving ">保存</el-button><el-button size=" small " @click=" cancelEdit ( row ) ">取消</el-button></template>
<el-button v-if=" ! batchEditMode && row . id " size=" small " type=" danger " plain @click=" doDelete ( row ) ">删除</el-button>
<el-tag v-else-if=" ! batchEditMode " size=" small " type=" info " effect=" plain " style=" cursor : pointer " @click=" startEdit ( row ) ">待创建</el-tag>
</template>
</el-table-column>
</el-table>
</template>
<template v-if=" budgetViewMode === 'summary' ">
<el-row :gutter=" 16 " style=" margin - bottom : 16 px ; ">
<el-col :span=" 6 " v-for=" card in summaryCards " :key=" card . key ">
<el-card shadow=" hover " :body-style=" { borderLeft : '4px solid ' + card . color } ">
<div style=" text - align : center ; "><div style=" font - size : 14 px ; margin - bottom : 4 px ; ">{{ card.icon }} {{ card.name }}</div><div style=" font - size : 24 px ; font - weight : 600 ; color : # 333 ; ">{{ formatNumber(card.totalBudget) }}</div><div style=" font - size : 11 px ; color : # 999 ; margin - top : 4 px ; ">{{ card.kpiCount }}个KPI · 占比{{ card.ratio }}%</div></div>
</el-card>
</el-col>
</el-row>
<el-table :data=" dimSummaryRows " border stripe size=" small " style=" width : 100 % ; ">
<el-table-column prop=" dimension " label=" 维度 " width=" 100 "><template #default=" { row } "><el-tag size=" small " :type=" dimTag ( row . dimension ) ">{{ dimLabel(row.dimension) }}</el-tag></template></el-table-column>
<el-table-column prop=" kpiCount " label=" KPI数 " width=" 80 " />
<el-table-column prop=" totalBudget " label=" 预算总额 " width=" 140 "><template #default=" { row } ">{{ formatNumber(row.totalBudget) }}</template></el-table-column>
<el-table-column prop=" ratio " label=" 占比 " width=" 100 "><template #default=" { row } "><el-progress :percentage=" row . ratio " :stroke-width=" 12 " text-inside /></template></el-table-column>
<el-table-column prop=" avgPerKpi " label=" KPI均值 " width=" 140 "><template #default=" { row } ">{{ formatNumber(row.avgPerKpi) }}</template></el-table-column>
</el-table>
</template>
</el-tab-pane>
<el-tab-pane label=" 年度分解 " name=" decompose ">
<el-card>
<template #header><div style=" display : flex ; justify - content : space - between ; align - items : center ; "><span>年度预算 → 月度自动分解</span><el-button type=" primary " @click=" doDecompose " :loading=" decomposing ">执行分解</el-button></div></template>
<el-form :model=" decomposeForm " label-width=" 120 px " style=" max - width : 500 px ; ">
<el-form-item label=" 年份 "><el-select v-model=" decomposeForm . year " style=" width : 150 px ; "><el-option v-for=" y in yearOptions " :key=" y " :label=" y " :value=" y " /></el-select></el-form-item>
<el-form-item label=" 分解方式 "><el-radio-group v-model=" decomposeForm . method "><el-radio value=" equal ">均分(1/12) </el-radio><el-radio value=" weighted ">按历史权重</el-radio></el-radio-group></el-form-item>
</el-form>
<el-alert v-if=" decomposeResult " :title=" decomposeResult " type=" success " show-icon :closable=" false " style=" margin - top : 16 px ; " />
<el-table v-if=" decomposeDetails . length > 0 " :data=" decomposeDetails " border stripe size=" small " style=" width : 100 % ; margin - top : 12 px ; " max-height=" 300 ">
<el-table-column prop=" kpi _code " label=" KPI编码 " width=" 120 " /><el-table-column prop=" kpi _name " label=" KPI名称 " min-width=" 150 " />
<el-table-column prop=" annual _budget " label=" 年度预算 " width=" 120 "><template #default=" { row } ">{{ formatNumber(row.annual_budget) }}</template></el-table-column>
<el-table-column prop=" method " label=" 方式 " width=" 80 " /><el-table-column prop=" monthly _count " label=" 月数 " width=" 60 " />
</el-table>
</el-card>
</el-tab-pane>
<el-tab-pane label=" 版本审批 " name=" versions ">
<div style=" display : flex ; gap : 12 px ; margin - bottom : 16 px ; align - items : center ; ">
<el-select v-model=" versionYear " placeholder=" 年份 " style=" width : 120 px ; "><el-option v-for=" y in yearOptions " :key=" y " :label=" y " :value=" y " /></el-select>
<el-button type=" primary " @click=" loadVersions ">刷新</el-button>
</div>
<el-table :data=" versionsList " v-loading=" versionsLoading " border stripe size=" small " style=" width : 100 % ; ">
<el-table-column prop=" version " label=" 版本 " width=" 90 "><template #default=" { row } "><el-tag :type=" row . version === currentVersion ? 'success' : 'info' " size=" small ">{{ row.version }}</el-tag></template></el-table-column>
<el-table-column prop=" approval _status " label=" 状态 " width=" 90 ">
<template #default=" { row } "><el-tag v-if=" row . approval _status === 'approved' " type=" success " size=" small ">已批准</el-tag><el-tag v-else-if=" row . approval _status === 'submitted' " type=" warning " size=" small ">待审批</el-tag><el-tag v-else-if=" row . approval _status === 'rejected' " type=" danger " size=" small ">已驳回</el-tag><el-tag v-else type=" info " size=" small ">草稿</el-tag></template>
</el-table-column>
<el-table-column prop=" plan _count " label=" 预算条目数 " width=" 100 " />
<el-table-column prop=" total _budget " label=" 预算总额 " width=" 130 "><template #default=" { row } ">{{ formatNumber(row.total_budget) }}</template></el-table-column>
<el-table-column prop=" last _updated " label=" 最后更新 " min-width=" 150 " />
<el-table-column label=" 操作 " width=" 200 " fixed=" right ">
<template #default=" { row } ">
<el-button v-if=" row . approval _status === 'draft' || row . approval _status === 'active' " size=" small " @click=" submitVersion ( row ) ">提交审批</el-button>
<template v-if=" row . approval _status === 'submitted' "><el-button size=" small " type=" success " @click=" approveVersion ( row , 'approved' ) ">批准</el-button><el-button size=" small " type=" danger " @click=" approveVersion ( row , 'rejected' ) ">驳回</el-button></template>
<el-button v-if=" row . approval _status === 'rejected' " size=" small " @click=" resubmitVersion ( row ) ">重新提交</el-button>
<el-checkbox v-model=" row . _selected " @change=" onVersionSelect ( row ) " :disabled=" selectedVersions . length >= 2 && ! row . _selected " style=" margin - left : 8 px ; " />
</template>
</el-table-column>
</el-table>
<div v-if=" selectedVersions . length === 2 " style=" margin - top : 16 px ; "><el-button type=" primary " @click=" loadVersionDiff " :loading=" diffLoading ">对比 {{ selectedVersions[0].version }} vs {{ selectedVersions[1].version }}</el-button></div>
</el-tab-pane>
<el-tab-pane label=" 下达 " name=" release ">
<div style=" display : flex ; gap : 12 px ; margin - bottom : 16 px ; align - items : center ; ">
<el-select v-model=" releaseYear " placeholder=" 年份 " style=" width : 120 px ; " @change=" loadReleaseVersions "><el-option v-for=" y in yearOptions " :key=" y " :label=" y " :value=" y " /></el-select>
<el-button type=" primary " @click=" loadReleaseVersions ">刷新</el-button>
</div>
<el-alert title=" 审批通过 ( approved ) 的版本可正式下达为生效执行版本 ( active ) " type=" info " :closable=" false " show-icon style=" margin - bottom : 12 px ; " />
<el-table :data=" releaseList " v-loading=" releaseLoading " border stripe size=" small " style=" width : 100 % ; ">
<el-table-column prop=" version " label=" 版本 " width=" 110 "><template #default=" { row } "><el-tag size=" small " :type=" row . status === 'active' ? 'success' : 'info' ">{{ row.version }}</el-tag></template></el-table-column>
<el-table-column prop=" status " label=" 状态 " width=" 100 "><template #default=" { row } "><el-tag v-if=" row . status === 'active' " type=" success " size=" small ">已下达</el-tag><el-tag v-else-if=" row . status === 'approved' " type=" warning " size=" small ">已批准</el-tag><el-tag v-else size=" small " type=" info ">{{ row.approval_status || row.status }}</el-tag></template></el-table-column>
<el-table-column prop=" kpi _count " label=" KPI数 " width=" 90 " />
<el-table-column prop=" total _budget " label=" 预算总额 " width=" 140 "><template #default=" { row } ">{{ formatNumber(row.total_budget) }}</template></el-table-column>
<el-table-column prop=" updated _at " label=" 更新时间 " min-width=" 150 " />
<el-table-column label=" 操作 " width=" 120 " fixed=" right ">
<template #default=" { row } "><el-button size=" small " type=" primary " :disabled=" row . status !== 'approved' " @click=" askRelease ( row ) ">下达</el-button></template>
</el-table-column>
</el-table>
<MyDialog v-model=" showReleaseConfirm " title=" 确认下达 " :width=" 460 ">
<div style=" font - size : 14 px ; ">确认将版本 <b>{{ releaseTarget?.version }}</b> 下达为生效执行版本?</div>
<div style=" font - size : 12 px ; color : # 909399 ; margin - top : 8 px ; ">下达后该版本状态由 approved 变为 active。</div>
<template #footer>
<el-button @click=" showReleaseConfirm = false ">取消</el-button>
<el-button type=" primary " :loading=" releasing " @click=" doRelease ">确认下达</el-button>
</template>
</MyDialog>
</el-tab-pane>
<el-tab-pane label=" 预算执行 " name=" execution ">
<el-card shadow=" never " style=" margin - bottom : 16 px ; ">
<template #header>
<div style=" display : flex ; justify - content : space - between ; align - items : center ; ">
<span><strong>预算控制状态</strong> <span style=" font - size : 12 px ; color : # 999 ; ">(生效预算 vs 实际值,阈值 {{ controlData?.threshold_pct ?? 20 }}%) </span></span>
<el-button size=" small " @click=" loadControlStatus ">刷新</el-button>
</div>
</template>
<div v-if=" controlData " style=" display : flex ; gap : 24 px ; margin - bottom : 12 px ; font - size : 13 px ; flex - wrap : wrap ; ">
<span>KPI总数:<b>{{ controlData.summary?.total ?? 0 }}</b></span>
<span style=" color : # 67 c23a ; ">正常:<b>{{ controlData.summary?.ok ?? 0 }}</b></span>
<span style=" color : # e6a23c ; ">关注:<b>{{ controlData.summary?.warn ?? 0 }}</b></span>
<span style=" color : # f56c6c ; ">超支:<b>{{ controlData.summary?.over ?? 0 }}</b></span>
</div>
<el-table :data=" controlItems " v-loading=" controlLoading " border stripe size=" small " style=" width : 100 % ; " max-height=" 240 ">
<el-table-column prop=" kpi _code " label=" KPI编码 " width=" 110 " />
<el-table-column prop=" kpi _name " label=" KPI名称 " min-width=" 140 " />
<el-table-column prop=" period " label=" 期间 " width=" 90 " />
<el-table-column prop=" budget _value " label=" 预算 " width=" 120 "><template #default=" { row } ">{{ formatNumber(row.budget_value, row.unit) }}</template></el-table-column>
<el-table-column prop=" actual _value " label=" 实际 " width=" 120 "><template #default=" { row } ">{{ formatNumber(row.actual_value, row.unit) }}</template></el-table-column>
<el-table-column label=" 执行率 " width=" 100 "><template #default=" { row } "><span v-if=" row . execution _rate != null ">{{ row.execution_rate }}%</span><span v-else style=" color : # ccc ; ">--</span></template></el-table-column>
<el-table-column label=" 控制级 " width=" 90 "><template #default=" { row } "><el-tag size=" small " :type=" row . control _level === 'over' ? 'danger' : row . control _level === 'warn' ? 'warning' : 'success' ">{{ row.control_level === 'over' ? '超支' : row.control_level === 'warn' ? '关注' : '正常' }}</el-tag></template></el-table-column>
</el-table>
<div v-if=" controlItems . length === 0 && ! controlLoading " style=" padding : 20 px 0 ; text - align : center ; color : # 999 ; font - size : 13 px ; ">暂无控制状态数据</div>
</el-card>
<div style=" display : flex ; gap : 12 px ; margin - bottom : 16 px ; flex - wrap : wrap ; align - items : center ; ">
<el-select v-model=" execFilterYear " placeholder=" 年份 " style=" width : 100 px ; " @change=" loadExecutionReport "><el-option v-for=" y in yearOptions " :key=" y " :label=" y " :value=" y " /></el-select>
<el-select v-model=" execFilterMonth " placeholder=" 截止月份 " style=" width : 110 px ; " @change=" loadExecutionReport "><el-option v-for=" m in 12 " :key=" m " :label=" ` 至 ${ m } 月 ` " :value=" m " /></el-select>
<el-select v-model=" execFilterDim " placeholder=" 维度 " clearable style=" width : 110 px ; " @change=" loadExecutionReport "><el-option label=" 财务 " value=" finance " /><el-option label=" 客户 " value=" customer " /><el-option label=" 内部流程 " value=" process " /><el-option label=" 学习成长 " value=" learning " /></el-select>
<el-button type=" primary " @click=" loadExecutionReport ">刷新</el-button>
</div>
<el-row :gutter=" 16 " style=" margin - bottom : 16 px ; ">
<el-col :span=" 6 " v-for=" c in collectCoverageCards " :key=" c . label ">
<el-card shadow=" hover "><div style=" text - align : center ; "><div style=" font - size : 12 px ; color : # 999 ; ">{{ c.label }}</div><div style=" font - size : 22 px ; font - weight : 600 ; margin - top : 4 px ; " :style=" { color : c . color } ">{{ c.value }}</div></div></el-card>
</el-col>
<el-col :span=" 6 " v-for=" card in execSummaryCards " :key=" card . label "><el-card shadow=" hover "><div style=" text - align : center ; "><div style=" font - size : 12 px ; color : # 999 ; ">{{ card.label }}</div><div style=" font - size : 22 px ; font - weight : 600 ; margin - top : 4 px ; " :style=" { color : card . color } ">{{ card.value }}</div></div></el-card></el-col>
</el-row>
<el-tabs v-model =" collectTab " type=" border - card " style=" margin - top : 4 px ; ">
<el-tab-pane label=" 取数映射 " name=" mappings " >
<div style=" display : flex ; gap : 8 px ; margin - bottom : 12 px ; align - items : center ; flex - wrap : wrap ; " >
<el-select v-model=" collectKpiId " placeholder=" 选择KPI ( 可选 ) " filterable clearable style=" width : 220 px ; " @change=" loadValueSources " >
<el-option v-for=" k in zbbKpiOptions " :key=" k . id " :label=" ` ${ k . kpi _code } - ${ k . kpi _name } ` " :value=" k . id " / >
</el-select >
<el-button type=" primary " @click=" showValueSourceDialog = true ">+ 新建映射</el-button >
<el-button type=" warning " :loading=" collectRunning " @click=" runValueCollect ">▶ 立即采集</el-butto n>
<el-button @click =" loadCollectData ">刷新</el-button >
</div >
<el-table :data=" valueSources " border stripe size=" small " style=" width : 100 % ; " >
<el-table-column prop=" kpi _code " label=" KPI编码 " width=" 100 " / >
<el-table-column prop=" kpi _name " label=" KPI名称 " min-width=" 120 " / >
<el-table-column prop=" source _table " label=" 源头表 " width=" 160 " >
<template #default=" { row } "><el-tag size=" small ">{{ row.source_table }} </el-tag></template>
</el-table-column >
<el-table-column prop=" source _field " label=" 金额字段 " width=" 110 " / >
<el-table-column prop=" aggregate " label=" 聚合 " width=" 70 " />
<el-table-column label=" 过滤规则 " width=" 140 ">
<template #default=" { row } "><span style=" font - size : 12 px ; ">{{ JSON.stringify(row.filter_rule || {}) }}</span></template>
</el-table-column>
<el-table-column prop=" unit _conversion " label=" 倍率 " width=" 60 " />
<el-table-column label=" 状态 " width=" 80 ">
<template #default=" { row } "><el-tag size=" small " :type=" row . status === 'active' ? 'success' : 'info' ">{{ row.status === 'active' ? '启用' : '停用' }}</el-tag></template>
</el-table-column>
<el-table-column label=" 操作 " width=" 160 " fixed=" right ">
<template #default=" { row } ">
<el-button size=" small " link type=" primary " @click=" testValueSource ( row ) ">试跑</el-button>
<el-button size=" small " link type=" warning " @click=" toggleValueSource ( row ) ">{{ row.status === 'active' ? '停用' : '启用' }}</el-button>
<el-button size=" small " link type=" danger " @click=" deleteValueSource ( row ) ">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-table :data=" execReport " v-loading ="execLoading " border stripe size=" small " style=" width : 100 % ; ">
<el-table-column prop=" kpi _code " label=" 编码 " width=" 110 " /><el-table-column prop=" kpi _name " label=" KPI名称 " min-width=" 140 " / >
<el-table-column prop=" dimension " label=" 维度 " width=" 80 "><template #default=" { row } "><el-tag size=" small " :type=" dimTag ( row . dimension ) ">{{ dimLabel(row.dimension) }}</el-tag></template></el-table-column >
<el-table-column prop=" budget _value " label=" 预算值 " width=" 130 "><template #default=" { row } ">{{ formatNumber(row.budget_value, row.unit) }}</template></el-table-column >
<!-- 战略目标 vs 预算(2026-08-27: 允许不同, 差异可见可解释) -- >
<el-table-column prop=" strategic _target " label=" 战略目标 " width=" 120 "><template #default=" { row } "><span :style=" { color : row . target _gap _level === 'high' ? '#f56c6c' : row . target _gap _level === 'medium' ? '#e6a23c' : '#606266' } ">{{ formatNumber(row.strategic_target, row.unit) }}</span></template></el-table-column >
<el-table-column label=" 目标差异 " width=" 110 "><template #default=" { row } " >
<span v-if=" row . target _gap _pct != null " :style=" { color : row . target _gap _level === 'high' ? '#f56c6c' : row . target _gap _level === 'medium' ? '#e6a23c' : '#67c23a' , fontWeight : 600 } ">{{ row.target_gap_pct > 0 ? '+' : '' }}{{ row.target_gap_pct }}%</spa n>
<el-tooltip v-if=" row . target _gap _level === 'high' " content=" 预算偏离战略目标超20 % , 需说明原因 " placement ="top "><span style=" margin - left : 4 px ; cursor : help ; ">⚠️</span></el-tooltip >
<span v-else style=" color : # c0c4cc ; ">-</span >
</template></el-table-column >
<el-table-column prop=" actual _value " label=" 实际值 " width=" 130 "><template #default=" { row } ">{{ formatNumber(row.actual_value, row.unit) }}</template></el-table-column >
<el-table-column prop=" deviation _value " label=" 偏差 " width=" 130 "><template #default=" { row } "><span :style=" { color : row . deviation _value > 0 ? '#f56c6c' : row . deviation _value < 0 ? '#67c23a' : '#999' } ">{{ row.deviation_value > 0 ? '+' : '' }}{{ formatNumber(row.deviation_value, row.unit) }}</span></template></el-table-column >
<el-table-column label=" 执行率 " width=" 100 "><template #default=" { row } "><el-progress v-if=" row . execution _rate != null " :percentage=" row . execution _rate " :status=" row . execution _rate > 100 ? 'exception' : row . execution _rate > 80 ? 'warning' : 'success' " :stroke-width=" 16 " :text-inside=" true " /><span v-else style=" color : # 909399 ">-</span></template></el-table-column >
<el-table-column label=" 预警 " width=" 80 "><template #default=" { row } "><el-tag v-if=" row . alert _level === 'red' " size=" small " type=" danger ">严重</el-tag><el-tag v-else-if=" row . alert _level === 'yellow' " size=" small " type=" warning ">关注</el-tag><el-tag v-else size=" small " type=" success ">正常 </el-tag></template></el-table-column>
</el-table>
</el-tab-pane >
<el-tab-pane label=" 实际值标签 " name=" values ">
<div style=" display : flex ; gap : 8 px ; margin - bottom : 12 px ; align - items : center ; ">
<el-radio-group v-mod el=" valueTagFilter " siz e=" small ">
<el-radio-button value=" auto _collect ">已自动归集</el-radio -button>
<el-radio-button value=" manual ">需人工确认</el-radio-button >
<el-radio-button value =" all ">全部</el-radio-button >
</el-radio-group >
<span style=" font - size : 12 px ; color : # 999 ; ">对账页将按数据来源分类显示,自动归集数据可追溯采集批次</span >
</div >
<el-table :data =" valueTagRows " border stripe size =" small " style=" width : 100 % ; " >
<el-table-column prop=" kpi _code " label=" KPI编码 " width=" 100 " / >
<el-table-column prop =" kpi _name " label=" KPI名称 " min-width ="120 " / >
<el-table-column prop=" period " label=" 期间 " width=" 90 " / >
<el-table-column prop=" actual _value " label=" 实际值 " width=" 110 "><template #default=" { row } ">{{ formatNumber(row.actual_value) }}</template></el-table-column >
<el-table-column label=" 来源 " width=" 130 " >
<template #default=" { row } ">
<el-tag v-if=" row . source _type === 'auto_collect' " size=" small " type=" success ">已自动归集</el-tag>
<el-tag v-else-if=" row . source _type === 'manual' || row . source _type === 'excel' " size=" small " type=" warning ">需人工确认</el-tag>
<el-tag v-else size=" small " type=" info ">{{ row.source_type || 'manual' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop=" source _batch " label=" 采集批次 " width=" 170 "><template #default=" { row } "><span style=" font - size : 12 px ; ">{{ row.source_batch || '--' }}</span></template></el-table-column>
<el-table-column prop=" remark " label=" 备注 " min-width=" 150 " />
</el-table>
</el-tab-pane>
<el-tab-pane label=" 差异分析 " name=" deviation ">
<div style=" display : flex ; gap : 12 px ; margin - bottom : 16 px ; align - items : center ; ">
<el-select v-model=" deviationYear " placeholder=" 年份 " style=" width : 120 px ; " @change=" loadDeviationReport "><el-option v-for=" y in yearOptions " :key=" y " :lab el="y " :valu e=" y " /></el-select >
<el-button type=" primary " @click=" loadDeviationReport ">刷新</el -button>
</div >
<el-table :data =" deviationItems " v-loading=" deviationLoading " border stripe size=" small " style=" width : 100 % ; " >
<el-table-column prop=" kpi _code " label=" KPI编码 " width=" 120 " / >
<el-table-column prop=" kpi _name " label=" KPI名称 " min-width=" 150 " / >
<el-table-column prop=" budget _value " label=" 预算 " width=" 130 "><template #default=" { row } ">{{ formatNumber(row.budget_value, row.unit) }}</template></el-table-column >
<el-table-column prop =" actual _value " label=" 实际 " width ="130 "><template #default=" { row } ">{{ formatNumber(row.actual_value, row.unit) }}</template></el-table-column >
<el-table-column label=" 偏差率 " width=" 110 "><template #default=" { row } "><el-tag v-if=" row . deviation _rate != null " :type=" Math . abs ( row . deviation _rate ) > 20 ? 'danger' : Math . abs ( row . deviation _rate ) > 10 ? 'warning' : 'success' " size=" small ">{{ row.deviation_rate > 0 ? '+' : '' }}{{ row.deviation_rate }}%</el-tag><span v-else style=" color : # ccc ; ">--</span></template></el-table-column >
<el-table-column label=" 状态 " width=" 90 "><template #default=" { row } "><el-tag v-if=" row . is _over _budget " type ="danger " size=" small ">超预算</el-tag><el-tag v-else-if=" row . deviation _rate != null && Math . abs ( row . deviation _rate ) > 10 " type=" warning " size=" small ">关注</el-tag><el-tag v-else size ="small " type=" success ">正常</el-tag></template></el-table-column >
</ el-table>
<div v-if=" deviationItems . length === 0 && ! deviationLoading " style=" padding : 40 px 0 ; text - align : center ; color : # 999 ; ">暂无差异数据</div >
</el-tab-pane >
<el-tab-pane label=" 采集日志 " name=" logs ">
<div style=" display : flex ; gap : 8 px ; margin - bottom : 12 px ; align - items : center ; ">
<el-select v-model=" collectLogStatus " placeholder=" 状态 " clearable style=" width : 110 px ; " size=" small " >
<el-option label=" 成功 " value=" success " /><el-option label=" 失败 " value=" failed " / >
<el-tab-pane label=" 预算调整 " name=" adjust ">
<el-card shadow=" never ">
<template #header><span>生成调整版本(复制基线版本 → 覆盖指定值 → 记录调整原因;原版本数值不变)</span></template >
<el-form :model=" adjustForm " label-width=" 110 px " style=" max - width : 560 px ; " >
<el-form-item label=" 基线版本 ">
<el-select v-model=" adjustForm . base _version " placeholder=" 选择生效版本 ( approved / active ) " style=" width : 100 % ; ">
<el-option v-for=" v in adjustBaseVersions " :key=" v . version " :label=" ` ${ v . version } ( ${ v . status } ) ` " :value=" v . version " />
</el-select>
<el-button size=" small " type=" primary " @click=" loadCollectLogs ">查询</el-button >
</el-form-item >
<el-form-item label=" 新版本号 "><el-input v-model=" adjustForm . new _version " placeholder=" 如 2026 - ADJ1 ( 不可与既有版本重名 ) " /></el-form-item>
<el-form-item label=" 调整原因 "><el-input v-model=" adjustForm . reason " type=" textarea " :rows=" 3 " placeholder=" 本次预算调整的原因 " /></el-form-item>
</el-form>
<el-button type=" primary " :loading=" adjusting " :disabled=" ! adjustForm . base _version || ! adjustForm . new _version " @click=" doAdjust ">生成调整版本</el-button>
<el-alert v-if=" adjustResult " :title=" adjustResult " type=" success " show-icon :closable=" false " style=" margin - top : 12 px ; " />
</el-card>
</el-tab-pane>
<el-tab-pane label=" 预算考核 " name=" assess ">
<div style=" display : flex ; gap : 12 px ; margin - bottom : 16 px ; align - items : center ; ">
<el-select v-model=" assessYear " placeholder=" 年份 " style=" width : 120 px ; " @change=" loadAssessment "><el-option v-for=" y in yearOptions " :key=" y " :label=" y " :value=" y " /></el-select>
<el-button type=" primary " @click=" loadAssessment ">刷新</el-button>
</div>
<div style=" display : flex ; gap : 12 px ; margin - bottom : 16 px ; flex - wrap : wrap ; ">
<el-card v-for=" c in assessCards " :key=" c . label " shadow=" hover " style=" flex : 1 ; min - width : 110 px ; ">
<div style=" text - align : center ; ">
<div style=" font - size : 12 px ; color : # 999 ; ">{{ c.label }}</div>
<div style=" font - size : 22 px ; font - weight : 600 ; margin - top : 4 px ; " :style=" { color : c . color } ">{{ c.value }}</div>
</div>
<el-table :data=" collectLogs " border stripe size=" small " style=" width : 100 % ; " >
<el-table-column prop=" kpi _code " label=" KPI编码 " width=" 100 " / >
<el-table-column prop =" kpi _name " label=" KPI名称 " min-width ="110 " / >
<el-table-column prop=" period " label=" 期间 " width=" 90 " />
<el-table-column prop=" source _table " label=" 源头表 " width="140 " />
<el-table-column prop=" collected _value " label=" 采集值 " width=" 100 "><template #default=" { row } ">{{ formatNumber(row.collected_value) }}</template></el-table-column >
<el-table-column label=" 状态 " width=" 80 " >
<template #default=" { row } "><el-tag size=" small " :type=" row . status === 'success' ? 'success' : 'danger' ">{{ row.status === 'success' ? '成功' : '失败' }}</el-tag></template >
</el-table-column>
<el-table-column prop=" message " label="说明 " min- width=" 180 " / >
<el-table-column prop=" collected _at " label=" 采集时间 " width=" 160 " / >
</el-table>
</el-tab-pane>
</el-tabs>
</el-card >
</div >
<el-table :data =" assessItems " v-loading=" assessLoading " border stripe size ="small " style=" width : 100 % ; " >
<el-table-column prop=" kpi _code " label=" KPI编码 " width=" 110 " />
<el-table-column prop=" kpi _name " label=" KPI名称 " min- width="140 " />
<el-table-column prop=" basis _version " label=" 基准版本 " width=" 110 " / >
<el-table-column prop=" budget _total " label="预算 " width=" 120 "><template #default=" { row } ">{{ formatNumber(row.budget_total, row.unit) }}</template></el-table-column >
<el-table-column prop=" actual _total " label=" 实际 " width=" 120 "><template #default=" { row } ">{{ formatNumber(row.actual_total, row.unit) }}</template></el-table-column >
<el-table-column label=" 达成率 " width=" 110 "><template #default=" { row } "><span v-if=" row . achieve _rate != null ">{{ row.achieve_rate }}%</span><span v-else style=" color : # ccc ; ">--</span></template> </el-table-column>
<el-table-column label=" 等级 " width=" 80 "><template #default=" { row } "><el-tag size=" small " :type=" row . grade === 'A' ? 'success' : row . grade === 'B' ? 'primary' : row . grade === 'C' ? 'warning' : row . grade === 'D' ? 'danger' : 'info' ">{{ row.grade }}</el-tag></template></el-table-column >
</ el-table>
</el-tab-pane>
<el-tab-pane label=" 持续规划 " name=" rolling ">
@@ -801,61 +770,6 @@
</el-form>
<template #footer><el-button @click=" showDerivationRuleDialog = false ">取消</el-button><el-button type=" primary " @click=" saveDerivationRule ">保存</el-button></template>
</MyDialog>
<!-- P1-④ 取数映射新建弹窗 -->
<MyDialog v-model=" showValueSourceDialog " title=" 新建取数映射 " :width=" 560 ">
<el-form :model=" valueSourceForm " label-width=" 100 px ">
<el-form-item label=" 目标KPI ">
<el-select v-model=" valueSourceForm . kpi _id " filterable style=" width : 100 % ; ">
<el-option v-for=" k in zbbKpiOptions " :key=" k . id " :label=" ` ${ k . kpi _code } - ${ k . kpi _name } ` " :value=" k . id " />
</el-select>
</el-form-item>
<el-form-item label=" 源头表 ">
<el-select v-model=" valueSourceForm . source _table " style=" width : 100 % ; ">
<el-option label=" 网银凭证明细 voucher _details " value=" voucher _details " />
<el-option label=" 库存汇总 product _inventory " value=" product _inventory " />
<el-option label=" 库存明细 product _inventory _detail " value=" product _inventory _detail " />
<el-option label=" 收付款计划 cash _plans " value=" cash _plans " />
</el-select>
</el-form-item>
<el-form-item label=" 金额字段 ">
<el-select v-model=" valueSourceForm . source _field " style=" width : 100 % ; ">
<el-option v-for=" f in [ 'credit_amount' , 'debit_amount' , 'amount' , 'qty' , 'out_amount' , 'in_amount' , 'end_amount' ] " :key=" f " :label=" f " :value=" f " />
</el-select>
</el-form-item>
<el-form-item label=" 聚合方式 ">
<el-select v-model=" valueSourceForm . aggregate " style=" width : 120 px ; "><el-option v-for=" a in [ 'sum' , 'avg' , 'count' , 'max' , 'min' ] " :key=" a " :label=" a " :value=" a " /></el-select>
</el-form-item>
<el-form-item label=" 方向过滤 ">
<el-select v-model=" valueSourceForm . direction " clearable placeholder=" 不限定 " style=" width : 150 px ; ">
<el-option label=" 贷方 ( 收入 ) " value=" credit " /><el-option label=" 借方 ( 支出 ) " value=" debit " />
</el-select>
</el-form-item>
<el-form-item label=" 科目过滤 "><el-input v-model=" valueSourceForm . subject _code " placeholder=" 如 : 6601 销售费用 " /></el-form-item>
<el-form-item label=" 期间字段 ">
<el-select v-model=" valueSourceForm . period _field " style=" width : 150 px ; "><el-option label=" period " value=" period " /><el-option label=" voucher _date " value=" voucher _date " /></el-select>
</el-form-item>
<el-form-item label=" 单位倍率 "><el-input-number v-model=" valueSourceForm . unit _conversion " :min=" 0.0001 " :precision=" 4 " style=" width : 150 px ; " /><span style=" font - size : 12 px ; color : # 999 ; margin - left : 6 px ; ">元→万元填 0.0001</span></el-form-item>
</el-form>
<template #footer><el-button @click=" showValueSourceDialog = false ">取消</el-button><el-button type=" primary " @click=" saveValueSource ">保存</el-button></template>
</MyDialog>
<!-- P2-⑥ 分类规则新建弹窗 -->
<MyDialog v-model=" showClassifyRuleDialog " title=" 新建现金流分类规则 " :width=" 500 ">
<el-form :model=" classifyRuleForm " label-width=" 110 px ">
<el-form-item label=" KPI ( 精确 ) ">
<el-select v-model=" classifyRuleForm . kpi _id " filterable clearable placeholder=" 或使用关键词 " style=" width : 100 % ; ">
<el-option v-for=" k in zbbKpiOptions " :key=" k . id " :label=" ` ${ k . kpi _code } - ${ k . kpi _name } ` " :value=" k . id " />
</el-select>
</el-form-item>
<el-form-item label=" 关键词 ( 兜底 ) "><el-input v-model=" classifyRuleForm . kpi _code _pattern " placeholder=" 如 : 保证金 " /></el-form-item>
<el-form-item label=" 收付类型 ">
<el-radio-group v-model=" classifyRuleForm . plan _type "><el-radio value=" receive ">收</el-radio><el-radio value=" pay ">付</el-radio></el-radio-group>
</el-form-item>
<el-form-item label=" 优先级 "><el-input-number v-model=" classifyRuleForm . priority " :min=" 1 " :max=" 100 " style=" width : 120 px ; " /></el-form-item>
</el-form>
<template #footer><el-button @click=" showClassifyRuleDialog = false ">取消</el-button><el-button type=" primary " @click=" saveClassifyRule ">保存</el-button></template>
</MyDialog>
</div>
</div>
</template>
@@ -878,21 +792,6 @@ const filterYear = ref(currentYear); const filterMonth = ref(0); const searchKpi
const loading = ref ( false ) ; const budgetList = ref < any [ ] > ( [ ] ) ; const page = ref ( 1 ) ; const pageSize = ref ( 20 ) ; const total = ref ( 0 ) ; const noMap = ref ( false )
const batchEditMode = ref ( false ) ; const batchSavingAll = ref ( false )
// ── 现金流联动(断点修复#1, 2026-08-27) ──
const syncingCash = ref(false)
async function syncCashPlans() {
syncingCash.value = true
try {
const r: any = await api.post('/budget/sync-cash-plans')
ElMessage.success(r.message || " 现金流计划已同步 ")
loadExecutionReport()
} catch (e: any) {
ElMessage.error(" 同步失败 : " + (e?.message || " " ) )
} finally {
syncingCash . value = false
}
}
// ── 战略地图选择 ──
const publishedMaps = ref < any [ ] > ( [ ] )
const selectedMapId = ref < number | null > ( null )
@@ -1501,210 +1400,147 @@ async function deleteDerivationRule(row: any) {
} catch { }
}
// ══════════════════════════════════════════════
// P1-④ 实际值自动归集
// 预算控制状态(GET /budget/control/status)
// ══════════════════════════════════════════════
const collectTab = ref ( 'mappings' )
const collectKpiId = ref < number | null > ( null )
const collectCoverageCards = ref < any [ ] > ( [ ] )
const valueSources = ref < any [ ] > ( [ ] )
const showValueSourceDialog = ref ( false )
const valueSourceForm = ref < any > ( { } )
const collectRunning = ref ( false )
const collectLogStatus = ref ( '' )
const collectLogs = ref < any [ ] > ( [ ] )
const valueTagFilter = ref ( 'auto_collect' )
const valueTagRows = ref < any [ ] > ( [ ] )
async function loadCollectData ( ) {
await Promise . all ( [ loadValueSources ( ) , loadCollectCoverage ( ) , loadValueTags ( ) , loadCollectLogs ( ) ] )
}
async function loadValueSources ( ) {
const controlLoading = ref ( false )
const controlData = ref < any > ( null )
const controlItems = ref < any [ ] > ( [ ] )
async function loadControlStatus ( ) {
controlLoading . value = true
try {
const params : any = { entity _id : getEntityId ( ) }
if ( collectKpiId . value ) params . kpi _id = collectKpiId . value
const r : any = await budgetApi . valueSources ( params )
valueSources . value = r . data || [ ]
} catch { valueSources . value = [ ] }
}
async function loadCollectCoverage ( ) {
try {
const r : any = await budgetApi . valueSourceCoverage ( { entity _id : getEntityId ( ) } )
const c = r . data || r || { }
collectCoverageCards . value = [
{ label : '已配映射KPI' , value : c . mapped _count ? ? 0 , color : '#409eff' } ,
{ label : '活跃KPI总数' , value : c . total _kpis ? ? 0 , color : '#606266' } ,
{ label : '覆盖率' , value : ` ${ c . coverage _pct ? ? 0 } % ` , color : '#67c23a' } ,
{ label : '未配置KPI' , value : c . unmapped _count ? ? 0 , color : '#e6a23c' } ,
]
} catch { collectCoverageCards . value = [ ] }
}
async function loadValueTags ( ) {
try {
const r : any = await kpiApi . list ( { page _size : 100 , entity _id : getEntityId ( ) } )
const d = r . data || r || [ ]
const kpis = Array . isArray ( d ) ? d : ( d . items || [ ] )
const rows : any [ ] = [ ]
for ( const k of kpis ) {
const vr : any = await kpiApi . values ( k . id , { entity _id : getEntityId ( ) } ) . catch ( ( ) => null )
const vals = vr ? . data || [ ]
for ( const v of ( Array . isArray ( vals ) ? vals : ( vals . items || [ ] ) ) ) {
rows . push ( { kpi _code : k . kpi _code , kpi _name : k . kpi _name , period : v . period , actual _value : v . actual _value , source _type : v . source _type || 'manual' , source _batch : v . source _batch || '' , remark : v . remark || '' } )
}
}
const recent = rows . filter ( ( r : any ) => r . actual _value != null ) . sort ( ( a : any , b : any ) => ( b . period || '' ) . localeCompare ( a . period || '' ) ) . slice ( 0 , 100 )
valueTagRows . value = valueTagFilter . value === 'all' ? recent : recent . filter ( ( r : any ) => ( valueTagFilter . value === 'auto_collect' ? r . source _type === 'auto_collect' : r . source _type !== 'auto_collect' ) )
} catch { valueTagRows . value = [ ] }
}
async function loadCollectLogs ( ) {
try {
const params : any = { entity _id : getEntityId ( ) }
if ( collectLogStatus . value ) params . status = collectLogStatus . value
const r : any = await budgetApi . valueCollectLogs ( params )
collectLogs . value = r . data || [ ]
} catch { collectLogs . value = [ ] }
}
function openValueSourceDialog ( ) {
valueSourceForm . value = { kpi _id : null , source _table : 'voucher_details' , source _field : 'credit_amount' , aggregate : 'sum' , direction : '' , subject _code : '' , period _field : 'period' , unit _conversion : 1 }
showValueSourceDialog . value = true
}
async function saveValueSource ( ) {
const f = valueSourceForm . value
if ( ! f . kpi _id ) { ElMessage . warning ( '请选择KPI' ) ; return }
const filter : any = { }
if ( f . direction ) filter . direction = f . direction
if ( f . subject _code ) filter . subject _code = f . subject _code
try {
await budgetApi . createValueSource ( {
kpi _id : f . kpi _id ,
source _table : f . source _table ,
source _field : f . source _field ,
aggregate : f . aggregate ,
filter _rule : Object . keys ( filter ) . length ? filter : null ,
period _field : f . period _field ,
unit _conversion : f . unit _conversion ,
entity _id : getEntityId ( ) ,
} )
ElMessage . success ( '映射已创建' )
showValueSourceDialog . value = false
loadValueSources ( )
loadCollectCoverage ( )
} catch ( e : any ) { ElMessage . error ( e ? . response ? . data ? . detail || '创建失败' ) }
}
async function deleteValueSource ( row : any ) {
try {
await ElMessageBox . confirm ( ` 确认删除映射( ${ row . kpi _name } ← ${ row . source _table } )? ` , '确认' )
await budgetApi . deleteValueSource ( row . id )
ElMessage . success ( '已删除' )
loadValueSources ( )
loadCollectCoverage ( )
} catch { }
}
async function toggleValueSource ( row : any ) {
try {
await budgetApi . updateValueSource ( row . id , { status : row . status === 'active' ? 'inactive' : 'active' } )
row . status = row . status === 'active' ? 'inactive' : 'active'
} catch { ElMessage . error ( '操作失败' ) }
}
async function testValueSource ( row : any ) {
try {
const r : any = await budgetApi . testValueSource ( {
kpi _id : row . kpi _id ,
source _table : row . source _table ,
source _field : row . source _field ,
aggregate : row . aggregate ,
filter _rule : row . filter _rule ,
period _field : row . period _field ,
unit _conversion : row . unit _conversion ,
period : zbbPeriod . value ,
} )
const d = r . data || r
if ( d . success ) ElMessage . success ( ` 试跑值: ${ formatNumber ( d . value ) } ( ${ d . message || '' } ) ` )
else ElMessage . error ( ` 试跑失败: ${ d . message || '' } ` )
} catch { ElMessage . error ( '试跑失败' ) }
}
async function runValueCollect ( ) {
collectRunning . value = true
try {
const r : any = await budgetApi . runValueCollect ( { period : zbbPeriod . value , entity _id : getEntityId ( ) } )
const d = r . data || r
ElMessage . success ( ` 采集完成: 成功 ${ d . collected ? ? 0 } 失败 ${ d . failed ? ? 0 } ` )
loadCollectData ( )
} catch { ElMessage . error ( '采集失败' ) }
collectRunning . value = false
const r : any = await budgetApi . controlStatus ( { year : execFilterYear . value , entity _id : getEntityId ( ) } )
const d = r . data || r || { }
controlData . value = d
controlItems . value = d . items || [ ]
} catch { controlData . value = null ; controlItems . value = [ ] }
controlLoading . value = false
}
// ══════════════════════════════════════════════
// P2-⑥ 现金流分类规则 + 待分类队列
// 预算下达(GET /budget/versions + POST /budget/versions/release)
// ══════════════════════════════════════════════
const cashClassifyRules = ref < any [ ] > ( [ ] )
const showClassifyRuleDialog = ref ( false )
const classifyRuleForm = ref < any > ( { } )
const unclassifiedRows = ref < any [ ] > ( [ ] )
const unclassifiedCount = ref ( 0 )
async function loadCashClassify ( ) {
const releaseYear = ref ( currentYear )
const releaseLoading = ref ( false )
const releaseList = ref < any [ ] > ( [ ] )
const showReleaseConfirm = ref ( false )
const releaseTarget = ref < any > ( null )
const releasing = ref ( false )
async function loadReleaseVersions ( ) {
releaseLoading . value = true
try {
const r : any = await budgetApi . cashClassifyRules ( { entity _id : getEntityId ( ) } )
cashClassifyRules . value = r . data || [ ]
} catch { cashClassifyRules . value = [ ] }
try {
const r : any = await budgetApi . cashUnclassified ( { status : 'pending' , entity _id : getEntityId ( ) } )
unclassifiedRows . value = r . data || [ ]
unclassifiedCount . value = ( r . data || [ ] ) . length
} catch { unclassifiedRows . value = [ ] ; unclassifiedCount . value = 0 }
const r : any = await budgetApi . versions ( { year : releaseYear . value , entity _id : getEntityId ( ) } )
const d = Array . isArray ( r ) ? r : ( r . data || [ ] )
releaseList . value = d
} catch { ElMessage . error ( '加载版本列表失败' ) }
releaseLoading . value = false
}
function openClassifyRuleDialog ( ) {
classifyRuleForm . value = { kpi _id : null , kpi _code _pattern : '' , plan _type : 'receive' , priority : 10 }
showClassifyRuleDialog . value = true
}
async function saveClassifyRule ( ) {
const f = classifyRuleForm . value
if ( ! f . kpi _id && ! f . kpi _code _pattern ) { ElMessage . warning ( '请选择KPI或填写关键词' ) ; return }
function askRelease ( row : any ) { releaseTarget . value = row ; showReleaseConfirm . value = true }
async function doRelease ( ) {
const row = releaseTarget . value
if ( ! row ) return
releasing . value = true
try {
await budgetApi . createCashClassifyRule ( {
kpi _id : f . kpi _id || null ,
kpi _code _pattern : f . kpi _code _pattern || null ,
plan _type : f . plan _type ,
priority : f . priority || 10 ,
entity _id : getEntityId ( ) ,
} )
ElMessage . success ( '规则已创建' )
showClassifyRuleDialog . value = false
loadCashClassify ( )
} catch ( e : any ) { ElMessage . error ( e ? . response ? . data ? . detail || '创建失败' ) }
const r : any = await budgetApi . versionRelease ( { version : row . version } )
ElMessage . success ( r . already ? ` 版本「 ${ row . version } 」已是下达态 ` : ` 版本「 ${ row . version } 」已下达( ${ r . count ? ? 0 } 条) ` )
showReleaseConfirm . value = false
loadReleaseVersions ( )
} catch ( e : any ) {
ElMessage . error ( e ? . response ? . data ? . detail || e ? . message || '下达失败' )
}
releasing . value = false
}
async function deleteClassifyRule ( row : any ) {
// ══════════════════════════════════════════════
// 差异分析(GET /budget/deviation-report)
// ══════════════════════════════════════════════
const deviationYear = ref ( currentYear )
const deviationLoading = ref ( false )
const deviationItems = ref < any [ ] > ( [ ] )
async function loadDeviationReport ( ) {
deviationLoading . value = true
try {
await ElMessageBox . confirm ( '确认删除该分类规则?' , '确认' )
await budgetApi . deleteCashClassifyRule ( row . id )
ElMessage . success ( '已删除' )
loadCashClassify ( )
} catch { }
const r : any = await budgetApi . deviationReport ( { year : deviationYear . value , entity _id : getEntityId ( ) } )
deviationItems . value = r . items || [ ]
} catch { deviationItems . value = [ ] }
deviationLoading . value = false
}
async function classifyUnclassified ( row : any , planType : string ) {
// ══════════════════════════════════════════════
// 预算调整(POST /budget/adjustments)
// ══════════════════════════════════════════════
const adjustForm = ref ( { base _version : '' , new _version : '' , reason : '' } )
const adjustBaseVersions = ref < any [ ] > ( [ ] )
const adjusting = ref ( false )
const adjustResult = ref ( '' )
async function loadAdjustBaseVersions ( ) {
try {
const r : any = await budgetApi . classifyCashUnclassified ( row . id , { plan _type : planType } )
ElMessage . success ( r . message || ` 已归类为 ${ planType === 'receive' ? '收' : '付' } ` )
loadCashClassify ( )
} catch ( e : any ) { ElMessage . error ( e ? . response ? . data ? . detail || '归类失败' ) }
const r : any = await budgetApi . versions ( { entity _id : getEntityId ( ) } )
const d = Array . isArray ( r ) ? r : ( r . data || [ ] )
adjustBaseVersions . value = d . filter ( ( v : any ) => v . status === 'approved' || v . status === 'active' )
} catch { adjustBaseVersions . value = [ ] }
}
async function ignoreUnclassified ( row : any ) {
async function doAdjust ( ) {
const f = adjustForm . value
if ( ! f . base _version || ! f . new _version ) { ElMessage . warning ( '请选择基线版本并填写新版本号' ) ; return }
adjusting . value = true
try {
await budgetApi . ignoreCashUnclassified ( row . id )
ElMessage . success ( '已忽略' )
loadCashClassify ( )
} catch { ElMessage . error ( '操作失败' ) }
const r : any = await budgetApi . adjustments ( { base _version : f . base _version , new _version : f . new _version , reason : f . reason , items : [ ] } )
adjustResult . value = ` 已生成调整版本 ${ r . new _version } (新建 ${ r . created ? ? 0 } 条 / 覆盖 ${ r . applied _overrides ? ? 0 } 条 ${ r . unmatched _overrides ? ` / 未匹配 ${ r . unmatched _overrides } 条 ` : '' } ) `
ElMessage . success ( '调整版本已生成' )
adjustForm . value = { base _version : '' , new _version : '' , reason : '' }
loadAdjustBaseVersions ( )
} catch ( e : any ) {
ElMessage . error ( e ? . response ? . data ? . detail || e ? . message || '生成调整版本失败' )
}
adjusting . value = false
}
// ══════════════════════════════════════════════
// 预算考核(GET /budget/assessment)
// ══════════════════════════════════════════════
const assessYear = ref ( currentYear )
const assessLoading = ref ( false )
const assessData = ref < any > ( null )
const assessItems = ref < any [ ] > ( [ ] )
const assessCards = computed ( ( ) => {
const s = assessData . value ? . summary || { }
return [
{ label : 'KPI数' , value : s . kpi _count ? ? 0 , color : '#409eff' } ,
{ label : 'A' , value : s . a ? ? 0 , color : '#67c23a' } ,
{ label : 'B' , value : s . b ? ? 0 , color : '#409eff' } ,
{ label : 'C' , value : s . c ? ? 0 , color : '#e6a23c' } ,
{ label : 'D' , value : s . d ? ? 0 , color : '#f56c6c' } ,
{ label : '总预算' , value : formatNumber ( s . total _budget ) , color : '#606266' } ,
{ label : '总实际' , value : formatNumber ( s . total _actual ) , color : '#606266' } ,
{ label : '达成率' , value : s . achieve _rate != null ? ` ${ s . achieve _rate } % ` : '--' , color : '#e6a23c' } ,
]
} )
async function loadAssessment ( ) {
assessLoading . value = true
try {
const r : any = await budgetApi . assessment ( { year : assessYear . value , entity _id : getEntityId ( ) } )
const d = r . data || r || { }
assessData . value = d
assessItems . value = d . items || [ ]
} catch { assessData . value = null ; assessItems . value = [ ] }
assessLoading . value = false
}
watch ( activeTab , ( tab ) => {
if ( tab === 'decompose' ) loadBudget ( )
else if ( tab === 'strategy' ) loadStrategyBudget ( )
else if ( tab === 'versions' ) loadVersions ( )
else if ( tab === 'execution' ) { loadExecutionReport ( ) ; loadCashClassify ( ) }
else if ( tab === 'execution' ) { loadExecutionReport ( ) ; loadControlStatus ( ) }
else if ( tab === 'method' ) { refreshMethodComparison ( ) ; loadDerivationRules ( ) }
else if ( tab === 'collect' ) loadCollectData ( )
else if ( tab === 'driver' ) {
// DriverFactorBudget handles its own loading on mount
}
else if ( tab === 'release' ) loadReleaseVersions ( )
else if ( tab === 'deviation' ) loadDeviationReport ( )
else if ( tab === 'adjust' ) loadAdjustBaseVersions ( )
else if ( tab === 'assess' ) loadAssessment ( )
else if ( tab === 'rolling' ) {
loadComparison ( )
loadDeviationAlerts ( )