Files
fad_oa/ruoyi-ui/src/views/rm/budget/index.vue
2026-06-12 13:54:43 +08:00

326 lines
11 KiB
Vue

<template>
<div class="app-container">
<el-card shadow="never" class="module-panel">
<div slot="header" class="module-header">
<span>💰 项目预算管理</span>
<div class="header-actions">
<el-button size="small" icon="el-icon-upload2" @click="handleArchive">💾 存档当前预算</el-button>
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAdd">+ 新增预算项</el-button>
</div>
</div>
<!-- Summary Cards -->
<div class="summary-row">
<div class="stat-card">
<div class="label">预算总额</div>
<div class="value">¥{{ totalBudget }}</div>
</div>
<div class="stat-card orange">
<div class="label">实际支出</div>
<div class="value">¥{{ totalSpent }}</div>
</div>
<div class="stat-card" :class="budgetBalanceClass">
<div class="label">预算余量</div>
<div class="value">¥{{ totalBalance }}</div>
</div>
</div>
<!-- Table -->
<el-table :data="budgetList" v-loading="loading" stripe border highlight-current-row>
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="category" label="类别" width="120" />
<el-table-column prop="item" label="项目名称" min-width="180" />
<el-table-column prop="budgetAmount" label="预算金额(¥)" width="140" align="right">
<template slot-scope="scope">{{ formatMoney(scope.row.budgetAmount) }}</template>
</el-table-column>
<el-table-column prop="spentAmount" label="实际金额(¥)" width="140" align="right">
<template slot-scope="scope">{{ formatMoney(scope.row.spentAmount) }}</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="110" align="center">
<template slot-scope="scope">
<el-tag :type="statusTag(scope.row.status)" size="small">
{{ statusLabel(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination :current-page="query.pageNum" :page-sizes="[10, 20, 50, 100]" :page-size="query.pageSize"
:total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
@current-change="handlePageChange" />
</div>
</el-card>
<!-- Add/Edit Dialog -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="550px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px" size="small">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="类别" prop="category">
<el-select v-model="form.category" placeholder="请选择类别" style="width:100%">
<el-option label="机械设备" value="机械设备" />
<el-option label="电气设备" value="电气设备" />
<el-option label="液压设备" value="液压设备" />
<el-option label="流体设备" value="流体设备" />
<el-option label="能源介质" value="能源介质" />
<el-option label="安装费用" value="安装费用" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目名称" prop="item">
<el-input v-model="form.item" placeholder="如:主轧机牌坊" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="预算金额(¥)" prop="budgetAmount">
<el-input v-model="form.budgetAmount" type="number" placeholder="预算金额" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实际金额(¥)" prop="spentAmount">
<el-input v-model="form.spentAmount" type="number" placeholder="实际金额" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态" style="width:100%">
<el-option label="草稿" value="draft" />
<el-option label="审核中" value="review" />
<el-option label="已批准" value="approved" />
<el-option label="已驳回" value="rejected" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注信息" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listBudget, getBudget, addBudget, updateBudget, delBudget } from '@/api/rm/budget'
import { listProject } from '@/api/rm/project'
export default {
name: 'RmBudget',
data() {
return {
loading: false,
budgetList: [],
total: 0,
currentProjectId: null,
query: {
pageNum: 1,
pageSize: 10,
projectId: undefined
},
dialogVisible: false,
dialogTitle: '',
submitting: false,
form: {},
rules: {
category: [{ required: true, message: '请选择类别', trigger: 'change' }],
item: [{ required: true, message: '请填写项目名称', trigger: 'blur' }],
budgetAmount: [{ required: true, message: '请填写预算金额', trigger: 'blur' }],
projectId: [{ required: true, message: '缺少项目ID', trigger: 'blur' }]
},
}
},
computed: {
totalBudget() {
return this.formatMoney(this.budgetList.reduce((s, b) => s + (Number(b.budgetAmount) || 0), 0))
},
totalSpent() {
return this.formatMoney(this.budgetList.reduce((s, b) => s + (Number(b.spentAmount) || 0), 0))
},
totalBalance() {
const budget = this.budgetList.reduce((s, b) => s + (Number(b.budgetAmount) || 0), 0)
const spent = this.budgetList.reduce((s, b) => s + (Number(b.spentAmount) || 0), 0)
return this.formatMoney(budget - spent)
},
budgetBalanceClass() {
const balance = this.budgetList.reduce((s, b) => s + (Number(b.budgetAmount) || 0), 0)
- this.budgetList.reduce((s, b) => s + (Number(b.spentAmount) || 0), 0)
return balance < 0 ? 'red' : 'green'
}
},
created() {
this.loadCurrentProject()
},
methods: {
loadCurrentProject() {
listProject({ pageNum: 1, pageSize: 1 }).then(res => {
const rows = res.rows || []
if (rows.length > 0) {
this.currentProjectId = rows[0].projectId
this.query.projectId = rows[0].projectId
this.loadList()
}
})
},
loadList() {
this.loading = true
listBudget(this.query).then(res => {
this.budgetList = res.rows || []
this.total = res.total || 0
}).finally(() => {
this.loading = false
})
},
handleSizeChange(val) {
this.query.pageSize = val
this.loadList()
},
handlePageChange(val) {
this.query.pageNum = val
this.loadList()
},
handleAdd() {
if (!this.currentProjectId) {
this.$message.warning('请先在项目总览中创建项目')
return
}
this.dialogTitle = '新增预算项'
this.form = {
projectId: this.currentProjectId,
category: '',
item: '',
budgetAmount: '',
spentAmount: '',
status: 'draft',
remark: ''
}
this.dialogVisible = true
this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
},
handleEdit(row) {
this.dialogTitle = '编辑预算项'
getBudget(row.budgetId).then(res => {
this.form = res.data
this.dialogVisible = true
this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
})
},
handleDelete(row) {
this.$confirm(`确认删除预算项 "${row.item}" 吗?`, '提示', { type: 'warning' }).then(() => {
delBudget(row.budgetId).then(() => {
this.$message.success('删除成功')
this.loadList()
})
})
},
handleSubmit() {
this.$refs.formRef.validate(valid => {
if (!valid) return
this.submitting = true
const api = this.form.budgetId ? updateBudget : addBudget
api(this.form).then(() => {
this.$message.success(this.form.budgetId ? '更新成功' : '新增成功')
this.dialogVisible = false
if (!this.form.budgetId) {
this.query.pageNum = 1
}
this.loadList()
}).finally(() => {
this.submitting = false
})
})
},
handleArchive() {
this.$message.info('存档功能待实现')
},
formatMoney(val) {
if (val === null || val === undefined || val === '') return '0.00'
return Number(val).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
},
statusTag(status) {
const map = { draft: 'info', review: 'warning', approved: 'success', rejected: 'danger' }
return map[status] || 'info'
},
statusLabel(status) {
const map = { draft: '草稿', review: '审核中', approved: '已批准', rejected: '已驳回' }
return map[status] || status
}
}
}
</script>
<style scoped>
.module-panel {
border: none;
border-radius: 4px;
}
.module-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.header-actions {
display: flex;
gap: 8px;
}
.summary-row {
display: flex;
gap: 10px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.stat-card {
flex: 1;
min-width: 140px;
background: #fff;
border-radius: 5px;
padding: 10px 14px;
border: 1px solid #d0d7de;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
.stat-card .label {
font-size: 11px;
color: #666;
margin-bottom: 4px;
}
.stat-card .value {
font-size: 20px;
font-weight: 700;
color: #2176ae;
}
.stat-card.green .value { color: #27ae60; }
.stat-card.orange .value { color: #f39c12; }
.stat-card.red .value { color: #e74c3c; }
.pagination-wrapper {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
.dialog-footer {
text-align: right;
}
</style>