init: 管理会计OS初始代码
包含前后端完整代码: - 前端:Vue3+Vite+ElementPlus - 后端:FastAPI+SQLAlchemy - 模块:驾驶舱/KPI/战略地图/预警/预算/成本/预测/改善行动 - 当前版本:v1.0.0
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3>预算管理</h3>
|
||||
|
||||
<el-tabs v-model="activeTab" style="margin-top:16px;">
|
||||
<!-- Tab 1: 预算录入 -->
|
||||
<el-tab-pane label="预算录入" name="input">
|
||||
<!-- 筛选栏 -->
|
||||
<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="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 @click="showBatchForm = true">批量录入</el-button>
|
||||
<el-button @click="showDecompose = true" :disabled="!filterYear">年度分解</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 预算表格 -->
|
||||
<el-table :data="budgetList" v-loading="loading" border stripe size="small" style="width:100%;">
|
||||
<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="140">
|
||||
<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:130px;" />
|
||||
<span v-else>{{ formatNumber(row.budget_value) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="version" label="版本" width="70" />
|
||||
<el-table-column prop="status" label="状态" width="70">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'active' ? 'success' : 'info'" size="small">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="130" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="!row.editing" size="small" @click="startEdit(row)">编辑</el-button>
|
||||
<template v-else>
|
||||
<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 size="small" type="danger" @click="doDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="margin-top:16px;display:flex;justify-content:flex-end;">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="prev, pager, next, total"
|
||||
@current-change="loadBudget"
|
||||
/>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab 2: 年度自动分解 -->
|
||||
<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-card>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- 批量录入弹窗 -->
|
||||
<MyDialog v-model="showBatchForm" title="批量录入预算" :width="600">
|
||||
<div style="margin-bottom:12px;">
|
||||
<p style="color:#666;font-size:13px;">从KPI字典中选择需要设置预算的KPI,统一填写预算值</p>
|
||||
<el-select v-model="batchYear" placeholder="年份" style="width:100px;margin-right:8px;">
|
||||
<el-option v-for="y in yearOptions" :key="y" :label="y" :value="y" />
|
||||
</el-select>
|
||||
<el-select v-model="batchMonth" placeholder="月份" style="width:90px;">
|
||||
<el-option v-for="m in 12" :key="m" :label="`${m}月`" :value="m" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-table :data="batchKpis" v-loading="batchLoading" border size="small" max-height="400" style="width:100%;" @selection-change="onBatchSelect">
|
||||
<el-table-column type="selection" width="40" />
|
||||
<el-table-column prop="kpi_code" label="编码" width="110" />
|
||||
<el-table-column prop="kpi_name" label="名称" min-width="150" />
|
||||
<el-table-column prop="dimension" label="维度" width="70">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="dimTag(row.dimension)" style="font-size:11px;">{{ dimLabel(row.dimension) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预算值" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.batchValue" :min="0" :precision="0" controls-position="right" style="width:110px;" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="margin-top:12px;display:flex;gap:12px;align-items:center;">
|
||||
<span style="font-size:13px;color:#666;">已选 {{ batchSelected.length }} 条</span>
|
||||
<el-button size="small" @click="batchSetSame">设为相同值</el-button>
|
||||
<el-input-number v-model="batchSameValue" :min="0" controls-position="right" style="width:130px;" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showBatchForm = false">取消</el-button>
|
||||
<el-button type="primary" @click="doBatchCreate" :loading="batchSaving">批量创建</el-button>
|
||||
</template>
|
||||
</MyDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { budgetApi, kpiApi } from '../api/index'
|
||||
import MyDialog from '../components/MyDialog.vue'
|
||||
|
||||
const activeTab = ref('input')
|
||||
|
||||
// ── 年份/月份选项 ──
|
||||
const currentYear = new Date().getFullYear()
|
||||
const yearOptions = computed(() => {
|
||||
const years: number[] = []
|
||||
for (let y = currentYear - 2; y <= currentYear + 2; y++) years.push(y)
|
||||
return years
|
||||
})
|
||||
|
||||
const filterYear = ref(currentYear)
|
||||
const filterMonth = ref(0)
|
||||
const searchKpi = ref('')
|
||||
const loading = ref(false)
|
||||
const budgetList = ref<any[]>([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
|
||||
// ── 维度标签 ──
|
||||
const dimMap: Record<string, string> = {
|
||||
finance: '财务', customer: '客户', process: '内部流程', learning: '学习成长'
|
||||
}
|
||||
const dimTagMap: Record<string, string> = {
|
||||
finance: 'danger', customer: 'warning', process: 'primary', learning: 'success'
|
||||
}
|
||||
function dimLabel(d: string) { return dimMap[d] || d }
|
||||
function dimTag(d: string) { return dimTagMap[d] || 'info' }
|
||||
|
||||
function formatNumber(v: any) {
|
||||
if (v === null || v === undefined) return '--'
|
||||
return Number(v).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// ── 加载预算列表 ──
|
||||
async function loadBudget() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = { page: page.value, page_size: pageSize.value }
|
||||
if (filterYear.value) params.budget_year = filterYear.value
|
||||
if (filterMonth.value > 0) params.budget_month = filterMonth.value
|
||||
if (searchKpi.value) params.keyword = searchKpi.value
|
||||
const r: any = await budgetApi.list(params)
|
||||
const data = r.data || r || []
|
||||
// 如果后端返回分页格式
|
||||
if (Array.isArray(data)) {
|
||||
budgetList.value = data.map((item: any) => ({ ...item, editing: false, editValue: item.budget_value, saving: false }))
|
||||
total.value = data.length
|
||||
} else if (data.items) {
|
||||
budgetList.value = (data.items || []).map((item: any) => ({ ...item, editing: false, editValue: item.budget_value, saving: false }))
|
||||
total.value = data.total || data.items.length
|
||||
} else {
|
||||
budgetList.value = []
|
||||
total.value = 0
|
||||
}
|
||||
} catch (e) { ElMessage.error('加载预算数据失败') }
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// ── 编辑操作 ──
|
||||
function startEdit(row: any) {
|
||||
row.editing = true
|
||||
row.editValue = row.budget_value
|
||||
}
|
||||
|
||||
function cancelEdit(row: any) {
|
||||
row.editing = false
|
||||
row.editValue = row.budget_value
|
||||
}
|
||||
|
||||
async function saveEdit(row: any) {
|
||||
row.saving = true
|
||||
try {
|
||||
await budgetApi.update(row.id, { budget_value: row.editValue })
|
||||
ElMessage.success('已更新')
|
||||
row.budget_value = row.editValue
|
||||
row.editing = false
|
||||
} catch (e) { ElMessage.error('更新失败') }
|
||||
row.saving = false
|
||||
}
|
||||
|
||||
async function doDelete(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除「${row.kpi_name || row.kpi_code}」的预算?`, '确认')
|
||||
await budgetApi.delete(row.id)
|
||||
ElMessage.success('已删除')
|
||||
loadBudget()
|
||||
} catch (e: any) {
|
||||
if (e !== 'cancel') ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// ── 批量录入 ──
|
||||
const showBatchForm = ref(false)
|
||||
const batchYear = ref(currentYear)
|
||||
const batchMonth = ref(new Date().getMonth() + 1)
|
||||
const batchLoading = ref(false)
|
||||
const batchKpis = ref<any[]>([])
|
||||
const batchSelected = ref<any[]>([])
|
||||
const batchSameValue = ref(0)
|
||||
const batchSaving = ref(false)
|
||||
|
||||
async function loadBatchKpis() {
|
||||
batchLoading.value = true
|
||||
try {
|
||||
const r: any = await kpiApi.list({ page_size: 500 })
|
||||
const data = r.data || r || []
|
||||
const items = Array.isArray(data) ? data : (data.items || [])
|
||||
batchKpis.value = items.map((k: any) => ({ ...k, batchValue: 0 }))
|
||||
} catch (e) { ElMessage.error('加载KPI列表失败') }
|
||||
batchLoading.value = false
|
||||
}
|
||||
|
||||
function onBatchSelect(sel: any[]) {
|
||||
batchSelected.value = sel
|
||||
}
|
||||
|
||||
function batchSetSame() {
|
||||
batchSelected.value.forEach(item => { item.batchValue = batchSameValue.value })
|
||||
}
|
||||
|
||||
async function doBatchCreate() {
|
||||
if (batchSelected.value.length === 0) { ElMessage.warning('请先选择KPI'); return }
|
||||
batchSaving.value = true
|
||||
let success = 0
|
||||
for (const kpi of batchSelected.value) {
|
||||
try {
|
||||
await budgetApi.create({
|
||||
kpi_id: kpi.id,
|
||||
period: `${batchYear.value}-${String(batchMonth.value).padStart(2, '0')}`,
|
||||
budget_value: kpi.batchValue,
|
||||
budget_year: batchYear.value,
|
||||
budget_month: batchMonth.value,
|
||||
})
|
||||
success++
|
||||
} catch (e) { /* skip */ }
|
||||
}
|
||||
ElMessage.success(`批量创建完成:${success}/${batchSelected.value.length} 条成功`)
|
||||
batchSaving.value = false
|
||||
showBatchForm.value = false
|
||||
loadBudget()
|
||||
}
|
||||
|
||||
// ── 年度分解 ──
|
||||
const showDecompose = ref(false)
|
||||
const decomposeForm = ref({ year: currentYear, method: 'equal' })
|
||||
const decomposing = ref(false)
|
||||
const decomposeResult = ref('')
|
||||
|
||||
async function doDecompose() {
|
||||
decomposing.value = true
|
||||
decomposeResult.value = ''
|
||||
try {
|
||||
const r: any = await budgetApi.autoDecompose({
|
||||
year: decomposeForm.value.year,
|
||||
method: decomposeForm.value.method,
|
||||
})
|
||||
decomposeResult.value = r.message || '分解成功'
|
||||
ElMessage.success('年度预算分解完成')
|
||||
loadBudget()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '分解失败')
|
||||
}
|
||||
decomposing.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadBudget()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user