feat: 完成支出管理模块前端开发

- 创建 expense.js API 文件(完整的 CRUD 接口)
- 创建 expense.vue 管理页面
  - 多条件搜索(支出编号、支出类型、项目、状态)
  - 表格展示(支出编号、类型、金额、日期、事由、项目、付款方式、状态)
  - 新增/编辑表单(支持金额、日期、事由、项目、付款方式等管理)
  - 状态更新对话框(待付款/已付款/已完成/已作废)
  - 支出类型树形选择
  - 项目下拉选择
- 添加支出管理路由配置(/finance/expense)

支出管理模块全栈开发完成!
This commit is contained in:
zhangjf 2026-02-15 18:20:59 +08:00
parent bb101d9c5e
commit fe3230eb9c
3 changed files with 577 additions and 0 deletions

View File

@ -0,0 +1,65 @@
import request from '../utils/request'
/**
* 获取支出列表分页
*/
export const getExpenseList = (params) => {
return request({
url: '/proj/api/v1/expense/list',
method: 'get',
params
})
}
/**
* 获取支出详情
*/
export const getExpenseById = (expenseId) => {
return request({
url: `/proj/api/v1/expense/${expenseId}`,
method: 'get'
})
}
/**
* 创建支出
*/
export const createExpense = (data) => {
return request({
url: '/proj/api/v1/expense',
method: 'post',
data
})
}
/**
* 更新支出
*/
export const updateExpense = (expenseId, data) => {
return request({
url: `/proj/api/v1/expense/${expenseId}`,
method: 'put',
data
})
}
/**
* 删除支出
*/
export const deleteExpense = (expenseId) => {
return request({
url: `/proj/api/v1/expense/${expenseId}`,
method: 'delete'
})
}
/**
* 更新支出状态
*/
export const updateExpenseStatus = (expenseId, status) => {
return request({
url: `/proj/api/v1/expense/${expenseId}/status`,
method: 'put',
params: { status }
})
}

View File

@ -105,6 +105,12 @@ const routes = [
name: 'ExpenseType',
component: () => import('../views/finance/expenseType.vue'),
meta: { title: '支出类型' }
},
{
path: 'expense',
name: 'Expense',
component: () => import('../views/finance/expense.vue'),
meta: { title: '支出管理' }
}
]
}

View File

@ -0,0 +1,506 @@
<template>
<div class="expense-container">
<el-card>
<!-- 搜索栏 -->
<el-form :inline="true" :model="searchForm">
<el-form-item label="支出编号">
<el-input v-model="searchForm.expenseCode" placeholder="请输入支出编号" clearable />
</el-form-item>
<el-form-item label="支出类型">
<el-tree-select
v-model="searchForm.expenseTypeId"
:data="expenseTypeOptions"
:props="{ label: 'typeName', value: 'typeId', children: 'children' }"
placeholder="请选择支出类型"
clearable
/>
</el-form-item>
<el-form-item label="项目">
<el-select v-model="searchForm.projectId" placeholder="请选择项目" clearable>
<el-option
v-for="project in projectOptions"
:key="project.projectId"
:label="project.projectName"
:value="project.projectId"
/>
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
<el-option label="待付款" value="pending" />
<el-option label="已付款" value="paid" />
<el-option label="已完成" value="completed" />
<el-option label="已作废" value="cancelled" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作栏 -->
<el-row style="margin-bottom: 15px;">
<el-button type="primary" @click="handleAdd">新增支出</el-button>
</el-row>
<!-- 表格 -->
<el-table :data="tableData" border v-loading="loading">
<el-table-column prop="expenseCode" label="支出编号" width="150" />
<el-table-column prop="expenseTypeId" label="支出类型" width="120">
<template #default="{ row }">
{{ getExpenseTypeName(row.expenseTypeId) }}
</template>
</el-table-column>
<el-table-column prop="expenseAmount" label="支出金额" width="120">
<template #default="{ row }">
¥{{ row.expenseAmount }}
</template>
</el-table-column>
<el-table-column prop="expenseDate" label="支出日期" width="120" />
<el-table-column prop="expenseReason" label="支出事由" min-width="200" show-overflow-tooltip />
<el-table-column prop="projectId" label="所属项目" width="150">
<template #default="{ row }">
{{ getProjectName(row.projectId) }}
</template>
</el-table-column>
<el-table-column prop="paymentMethod" label="付款方式" width="100">
<template #default="{ row }">
{{ getPaymentMethodText(row.paymentMethod) }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag v-if="row.status === 'pending'" type="warning">待付款</el-tag>
<el-tag v-else-if="row.status === 'paid'" type="primary">已付款</el-tag>
<el-tag v-else-if="row.status === 'completed'" type="success">已完成</el-tag>
<el-tag v-else type="info">已作废</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdTime" label="创建时间" width="180" />
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
<el-button type="info" size="small" @click="handleUpdateStatus(row)">更新状态</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="page.current"
v-model:page-size="page.size"
:total="page.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchData"
@current-change="fetchData"
style="margin-top: 20px; justify-content: flex-end;"
/>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="form.expenseId ? '编辑支出' : '新增支出'"
width="700px"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="支出编号" prop="expenseCode">
<el-input v-model="form.expenseCode" placeholder="请输入支出编号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="支出类型" prop="expenseTypeId">
<el-tree-select
v-model="form.expenseTypeId"
:data="expenseTypeOptions"
:props="{ label: 'typeName', value: 'typeId', children: 'children' }"
placeholder="请选择支出类型"
style="width: 100%;"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="支出金额" prop="expenseAmount">
<el-input-number v-model="form.expenseAmount" :min="0" :precision="2" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="支出日期" prop="expenseDate">
<el-date-picker
v-model="form.expenseDate"
type="date"
placeholder="选择日期"
style="width: 100%;"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="支出事由" prop="expenseReason">
<el-input
v-model="form.expenseReason"
type="textarea"
:rows="3"
placeholder="请输入支出事由"
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="所属项目" prop="projectId">
<el-select v-model="form.projectId" placeholder="请选择项目" style="width: 100%;" clearable>
<el-option
v-for="project in projectOptions"
:key="project.projectId"
:label="project.projectName"
:value="project.projectId"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="付款方式" prop="paymentMethod">
<el-select v-model="form.paymentMethod" placeholder="请选择付款方式" style="width: 100%;">
<el-option label="转账" value="transfer" />
<el-option label="现金" value="cash" />
<el-option label="支票" value="check" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="付款账户" prop="paymentAccount">
<el-input v-model="form.paymentAccount" placeholder="请输入付款账户" />
</el-form-item>
<el-form-item label="备注说明" prop="remark">
<el-input
v-model="form.remark"
type="textarea"
:rows="2"
placeholder="请输入备注说明"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
<!-- 更新状态对话框 -->
<el-dialog v-model="statusDialogVisible" title="更新状态" width="400px">
<el-form>
<el-form-item label="状态">
<el-select v-model="statusForm.status" placeholder="请选择状态" style="width: 100%;">
<el-option label="待付款" value="pending" />
<el-option label="已付款" value="paid" />
<el-option label="已完成" value="completed" />
<el-option label="已作废" value="cancelled" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="statusDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmitStatus">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getExpenseList,
createExpense,
updateExpense,
deleteExpense,
updateExpenseStatus
} from '../../api/expense'
import { getExpenseTypeTree } from '../../api/expenseType'
import { getProjectList } from '../../api/project'
//
const searchForm = reactive({
expenseCode: '',
expenseTypeId: null,
projectId: null,
status: ''
})
//
const tableData = ref([])
const loading = ref(false)
//
const page = reactive({
current: 1,
size: 10,
total: 0
})
//
const expenseTypeOptions = ref([])
//
const projectOptions = ref([])
//
const dialogVisible = ref(false)
const formRef = ref(null)
//
const form = reactive({
expenseId: null,
expenseCode: '',
expenseTypeId: null,
expenseAmount: 0,
expenseDate: null,
expenseReason: '',
projectId: null,
paymentMethod: 'transfer',
paymentAccount: '',
remark: ''
})
//
const rules = {
expenseCode: [
{ required: true, message: '请输入支出编号', trigger: 'blur' }
],
expenseTypeId: [
{ required: true, message: '请选择支出类型', trigger: 'change' }
],
expenseAmount: [
{ required: true, message: '请输入支出金额', trigger: 'blur' }
]
}
//
const statusDialogVisible = ref(false)
const statusForm = reactive({
expenseId: null,
status: ''
})
//
const loadExpenseTypes = async () => {
try {
const res = await getExpenseTypeTree()
expenseTypeOptions.value = res
} catch (error) {
console.error('加载支出类型失败:', error)
}
}
//
const loadProjects = async () => {
try {
const res = await getProjectList({ current: 1, size: 1000 })
projectOptions.value = res.records || []
} catch (error) {
console.error('加载项目列表失败:', error)
}
}
//
const getExpenseTypeName = (typeId) => {
const findType = (types) => {
for (const type of types) {
if (type.typeId === typeId) return type.typeName
if (type.children) {
const found = findType(type.children)
if (found) return found
}
}
return null
}
return findType(expenseTypeOptions.value) || '-'
}
//
const getProjectName = (projectId) => {
const project = projectOptions.value.find(p => p.projectId === projectId)
return project ? project.projectName : '-'
}
//
const getPaymentMethodText = (method) => {
const map = {
transfer: '转账',
cash: '现金',
check: '支票',
other: '其他'
}
return map[method] || method
}
//
const fetchData = async () => {
loading.value = true
try {
const params = {
current: page.current,
size: page.size,
expenseCode: searchForm.expenseCode || undefined,
expenseTypeId: searchForm.expenseTypeId || undefined,
projectId: searchForm.projectId || undefined,
status: searchForm.status || undefined
}
const res = await getExpenseList(params)
tableData.value = res.records
page.total = res.total
} catch (error) {
console.error('加载数据失败:', error)
ElMessage.error(error.message || '加载数据失败')
} finally {
loading.value = false
}
}
//
const handleSearch = () => {
page.current = 1
fetchData()
}
//
const handleReset = () => {
searchForm.expenseCode = ''
searchForm.expenseTypeId = null
searchForm.projectId = null
searchForm.status = ''
page.current = 1
fetchData()
}
//
const handleAdd = () => {
Object.assign(form, {
expenseId: null,
expenseCode: '',
expenseTypeId: null,
expenseAmount: 0,
expenseDate: null,
expenseReason: '',
projectId: null,
paymentMethod: 'transfer',
paymentAccount: '',
remark: ''
})
dialogVisible.value = true
}
//
const handleEdit = (row) => {
Object.assign(form, {
expenseId: row.expenseId,
expenseCode: row.expenseCode,
expenseTypeId: row.expenseTypeId,
expenseAmount: row.expenseAmount,
expenseDate: row.expenseDate,
expenseReason: row.expenseReason,
projectId: row.projectId,
paymentMethod: row.paymentMethod,
paymentAccount: row.paymentAccount,
remark: row.remark
})
dialogVisible.value = true
}
//
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
} catch (error) {
ElMessage.warning('请检查表单填写是否完整')
return
}
try {
if (form.expenseId) {
await updateExpense(form.expenseId, form)
ElMessage.success('更新成功')
} else {
await createExpense(form)
ElMessage.success('创建成功')
}
dialogVisible.value = false
await fetchData()
} catch (error) {
console.error('保存失败:', error)
ElMessage.error(error.message || '操作失败')
}
}
//
const handleUpdateStatus = (row) => {
statusForm.expenseId = row.expenseId
statusForm.status = row.status
statusDialogVisible.value = true
}
//
const handleSubmitStatus = async () => {
try {
await updateExpenseStatus(statusForm.expenseId, statusForm.status)
ElMessage.success('状态更新成功')
statusDialogVisible.value = false
await fetchData()
} catch (error) {
console.error('状态更新失败:', error)
ElMessage.error(error.message || '状态更新失败')
}
}
//
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm('确定要删除该支出吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await deleteExpense(row.expenseId)
ElMessage.success('删除成功')
//
if (tableData.value.length === 1 && page.current > 1) {
page.current--
}
await fetchData()
} catch (error) {
if (error !== 'cancel') {
console.error('删除失败:', error)
ElMessage.error(error.message || '删除失败')
}
}
}
//
onMounted(() => {
loadExpenseTypes()
loadProjects()
fetchData()
})
</script>
<style scoped>
.expense-container {
padding: 20px;
}
</style>