init: 管理会计OS初始代码

包含前后端完整代码:
- 前端:Vue3+Vite+ElementPlus
- 后端:FastAPI+SQLAlchemy
- 模块:驾驶舱/KPI/战略地图/预警/预算/成本/预测/改善行动
- 当前版本:v1.0.0
This commit is contained in:
Hermes CI Fix
2026-05-28 17:32:22 +08:00
commit 3dddd36866
142 changed files with 18533 additions and 0 deletions
+311
View File
@@ -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>