feat: 支出和应收款模块新增Excel导出功能

后端:
- fund-exp: 新增ExpenseExcel导出实体、导出接口和listExpenses方法
- fund-receipt: 新增ReceivableExcel导出实体、导出接口和listReceivables方法

前端:
- fund-admin: 支出管理页面新增导出按钮
- fund-admin: 应收款管理页面新增导出按钮
- fund-admin: 新增exportExpense和exportReceivable API
This commit is contained in:
zhangjf 2026-02-20 08:36:20 +08:00
parent ad4176ae8a
commit 06efab9596
48 changed files with 496 additions and 10 deletions

View File

@ -62,3 +62,39 @@ export function rejectExpense(id: number, comment: string) {
export function confirmPayExpense(id: number, payChannel: string, payVoucher?: string) {
return request.put(`/exp/api/v1/exp/expense/${id}/confirm-pay?payChannel=${payChannel}&payVoucher=${payVoucher || ''}`)
}
// 导出支出明细
export function exportExpense(params?: { title?: string; expenseType?: number; approvalStatus?: number; payStatus?: number }) {
const baseUrl = import.meta.env.VITE_API_URL || ''
const token = localStorage.getItem('token')
const tenantId = localStorage.getItem('tenantId') || '1'
const queryParams = new URLSearchParams()
if (params?.title) queryParams.append('title', params.title)
if (params?.expenseType) queryParams.append('expenseType', String(params.expenseType))
if (params?.approvalStatus !== undefined) queryParams.append('approvalStatus', String(params.approvalStatus))
if (params?.payStatus !== undefined) queryParams.append('payStatus', String(params.payStatus))
const queryString = queryParams.toString()
const url = `${baseUrl}/exp/api/v1/exp/expense/export${queryString ? '?' + queryString : ''}`
return fetch(url, {
headers: {
Authorization: `Bearer ${token}`,
'X-Tenant-Id': tenantId
}
}).then(response => {
if (!response.ok) throw new Error('导出失败')
const contentDisposition = response.headers.get('Content-Disposition')
let filename = '支出明细.xlsx'
if (contentDisposition) {
const match = contentDisposition.match(/filename\*=utf-8''(.+)/i)
if (match && match[1]) {
filename = decodeURIComponent(match[1])
}
}
return response.blob().then(blob => ({ blob, filename }))
})
}

View File

@ -49,3 +49,39 @@ export function getReceiptById(id: number) {
export function createReceipt(data: any) {
return request.post('/receipt/api/v1/receipt/receipt', data)
}
// 导出应收款明细
export function exportReceivable(params?: { projectId?: number; customerId?: number; status?: string; confirmStatus?: number }) {
const baseUrl = import.meta.env.VITE_API_URL || ''
const token = localStorage.getItem('token')
const tenantId = localStorage.getItem('tenantId') || '1'
const queryParams = new URLSearchParams()
if (params?.projectId) queryParams.append('projectId', String(params.projectId))
if (params?.customerId) queryParams.append('customerId', String(params.customerId))
if (params?.status) queryParams.append('status', params.status)
if (params?.confirmStatus !== undefined) queryParams.append('confirmStatus', String(params.confirmStatus))
const queryString = queryParams.toString()
const url = `${baseUrl}/receipt/api/v1/receipt/receivable/export${queryString ? '?' + queryString : ''}`
return fetch(url, {
headers: {
Authorization: `Bearer ${token}`,
'X-Tenant-Id': tenantId
}
}).then(response => {
if (!response.ok) throw new Error('导出失败')
const contentDisposition = response.headers.get('Content-Disposition')
let filename = '应收款明细.xlsx'
if (contentDisposition) {
const match = contentDisposition.match(/filename\*=utf-8''(.+)/i)
if (match && match[1]) {
filename = decodeURIComponent(match[1])
}
}
return response.blob().then(blob => ({ blob, filename }))
})
}

View File

@ -40,6 +40,7 @@
<el-card shadow="never" style="margin-top: 10px">
<div style="margin-bottom: 15px">
<el-button type="primary" :icon="Plus" @click="handleAdd">新增支出</el-button>
<el-button type="success" :icon="Download" @click="handleExport" :loading="exporting">导出Excel</el-button>
</div>
<el-table :data="tableData" v-loading="loading" border stripe>
@ -262,7 +263,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Search, Refresh, Plus, Edit, Delete, View, Check, Document } from '@element-plus/icons-vue'
import { Search, Refresh, Plus, Edit, Delete, View, Check, Document, Download } from '@element-plus/icons-vue'
import {
getExpenseList,
createExpense,
@ -270,7 +271,8 @@ import {
deleteExpense,
submitExpense,
approveExpense,
rejectExpense
rejectExpense,
exportExpense
} from '@/api/expense'
import { getExpenseTypeTree } from '@/api/expense'
import { getProjectList } from '@/api/project'
@ -285,6 +287,7 @@ const fileList = ref<any[]>([])
const loading = ref(false)
const submitLoading = ref(false)
const exporting = ref(false)
const tableData = ref<any[]>([])
const total = ref(0)
const expenseTypeList = ref<any[]>([])
@ -558,6 +561,34 @@ const resetForm = () => {
formRef.value?.clearValidate()
}
// Excel
const handleExport = async () => {
exporting.value = true
try {
const params: any = {}
if (queryParams.title) params.title = queryParams.title
if (queryParams.expenseTypeId) params.expenseType = queryParams.expenseTypeId
if (queryParams.approvalStatus) params.approvalStatus = parseInt(queryParams.approvalStatus)
if (queryParams.payStatus) params.payStatus = parseInt(queryParams.payStatus)
const { blob, filename } = await exportExpense(params)
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('导出成功')
} catch (error) {
console.error('导出失败:', error)
ElMessage.error('导出失败')
} finally {
exporting.value = false
}
}
onMounted(() => {
fetchData()
fetchExpenseTypes()

View File

@ -27,6 +27,7 @@
<div style="margin-bottom: 15px">
<el-button type="primary" :icon="Plus" @click="handleAdd">新增应收款</el-button>
<el-button type="success" :icon="Money">批量收款</el-button>
<el-button type="success" :icon="Download" @click="handleExport" :loading="exporting">导出Excel</el-button>
</div>
<el-table :data="tableData" v-loading="loading" border stripe>
@ -262,14 +263,15 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Search, Refresh, Plus, Edit, Delete, View, Money } from '@element-plus/icons-vue'
import { Search, Refresh, Plus, Edit, Delete, View, Money, Download } from '@element-plus/icons-vue'
import {
getReceivableList,
createReceivable,
updateReceivable,
deleteReceivable,
recordReceipt,
getReceiptRecords
getReceiptRecords,
exportReceivable
} from '@/api/receivable'
import { getProjectList } from '@/api/project'
import { getCustomerList } from '@/api/customer'
@ -277,6 +279,7 @@ import { getCustomerList } from '@/api/customer'
const loading = ref(false)
const submitLoading = ref(false)
const receiptLoading = ref(false)
const exporting = ref(false)
const tableData = ref<any[]>([])
const total = ref(0)
const projectList = ref<any[]>([])
@ -500,6 +503,31 @@ const resetForm = () => {
formRef.value?.clearValidate()
}
// Excel
const handleExport = async () => {
exporting.value = true
try {
const params: any = {}
if (queryParams.receiptStatus) params.status = queryParams.receiptStatus
const { blob, filename } = await exportReceivable(params)
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('导出成功')
} catch (error) {
console.error('导出失败:', error)
ElMessage.error('导出失败')
} finally {
exporting.value = false
}
}
onMounted(() => {
fetchData()
fetchProjects()

View File

@ -2,12 +2,18 @@ package com.fundplatform.exp.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fundplatform.common.core.Result;
import com.fundplatform.common.util.ExcelUtil;
import com.fundplatform.exp.dto.ExpenseExcel;
import com.fundplatform.exp.dto.FundExpenseDTO;
import com.fundplatform.exp.service.FundExpenseService;
import com.fundplatform.exp.vo.FundExpenseVO;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* 支出管理Controller
*
@ -164,4 +170,32 @@ public class FundExpenseController {
public Result<java.util.List<java.util.Map<String, Object>>> getTypeDistribution() {
return Result.success(expenseService.getTypeDistribution());
}
/**
* 导出支出明细Excel
*/
@GetMapping("/export")
public void exportExcel(
@RequestParam(required = false) String title,
@RequestParam(required = false) Long expenseType,
@RequestParam(required = false) Integer payStatus,
@RequestParam(required = false) Integer approvalStatus,
HttpServletResponse response) {
List<FundExpenseVO> list = expenseService.listExpenses(title, expenseType, payStatus, approvalStatus);
List<ExpenseExcel> excelData = list.stream().map(vo -> {
ExpenseExcel excel = new ExpenseExcel();
excel.setExpenseNo(vo.getExpenseNo());
excel.setTitle(vo.getTitle());
excel.setExpenseTypeName(vo.getExpenseTypeName());
excel.setAmount(vo.getAmount());
excel.setExpenseDate(vo.getExpenseDate());
excel.setPayeeName(vo.getPayeeName());
excel.setApprovalStatus(vo.getApprovalStatus());
excel.setPayStatus(vo.getPayStatus());
excel.setPurpose(vo.getPurpose());
excel.setCreatedTime(vo.getCreatedTime());
return excel;
}).collect(Collectors.toList());
ExcelUtil.exportExcel(excelData, "支出明细", "支出明细", ExpenseExcel.class, response, "支出明细.xlsx");
}
}

View File

@ -0,0 +1,122 @@
package com.fundplatform.exp.dto;
import cn.afterturn.easypoi.excel.annotation.Excel;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 支出明细Excel导出实体
*/
public class ExpenseExcel {
@Excel(name = "支出编号", width = 15)
private String expenseNo;
@Excel(name = "支出标题", width = 25)
private String title;
@Excel(name = "支出类型", width = 12)
private String expenseTypeName;
@Excel(name = "支出金额", width = 12, type = 10)
private BigDecimal amount;
@Excel(name = "支出日期", width = 12, format = "yyyy-MM-dd")
private LocalDateTime expenseDate;
@Excel(name = "收款人", width = 12)
private String payeeName;
@Excel(name = "审批状态", width = 10, replace = {"草稿_0", "待审批_1", "已通过_2", "已拒绝_3", "已撤回_4"})
private Integer approvalStatus;
@Excel(name = "支付状态", width = 10, replace = {"未支付_0", "已支付_1", "支付失败_2"})
private Integer payStatus;
@Excel(name = "用途说明", width = 30)
private String purpose;
@Excel(name = "创建时间", width = 18, format = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdTime;
public String getExpenseNo() {
return expenseNo;
}
public void setExpenseNo(String expenseNo) {
this.expenseNo = expenseNo;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getExpenseTypeName() {
return expenseTypeName;
}
public void setExpenseTypeName(String expenseTypeName) {
this.expenseTypeName = expenseTypeName;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public LocalDateTime getExpenseDate() {
return expenseDate;
}
public void setExpenseDate(LocalDateTime expenseDate) {
this.expenseDate = expenseDate;
}
public String getPayeeName() {
return payeeName;
}
public void setPayeeName(String payeeName) {
this.payeeName = payeeName;
}
public Integer getApprovalStatus() {
return approvalStatus;
}
public void setApprovalStatus(Integer approvalStatus) {
this.approvalStatus = approvalStatus;
}
public Integer getPayStatus() {
return payStatus;
}
public void setPayStatus(Integer payStatus) {
this.payStatus = payStatus;
}
public String getPurpose() {
return purpose;
}
public void setPurpose(String purpose) {
this.purpose = purpose;
}
public LocalDateTime getCreatedTime() {
return createdTime;
}
public void setCreatedTime(LocalDateTime createdTime) {
this.createdTime = createdTime;
}
}

View File

@ -14,6 +14,11 @@ public interface FundExpenseService {
Page<FundExpenseVO> pageExpenses(int pageNum, int pageSize, String title, Long expenseType, Integer payStatus, Integer approvalStatus);
/**
* 查询支出列表不分页用于导出
*/
java.util.List<FundExpenseVO> listExpenses(String title, Long expenseType, Integer payStatus, Integer approvalStatus);
boolean deleteExpense(Long id);
/**

View File

@ -135,6 +135,19 @@ public class FundExpenseServiceImpl implements FundExpenseService {
return voPage;
}
@Override
public List<FundExpenseVO> listExpenses(String title, Long expenseType, Integer payStatus, Integer approvalStatus) {
LambdaQueryWrapper<FundExpense> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(FundExpense::getDeleted, 0);
if (StringUtils.hasText(title)) wrapper.like(FundExpense::getTitle, title);
if (expenseType != null) wrapper.eq(FundExpense::getExpenseType, expenseType);
if (payStatus != null) wrapper.eq(FundExpense::getPayStatus, payStatus);
if (approvalStatus != null) wrapper.eq(FundExpense::getApprovalStatus, approvalStatus);
wrapper.orderByDesc(FundExpense::getCreatedTime);
List<FundExpense> list = expenseDataService.list(wrapper);
return list.stream().map(this::convertToVO).toList();
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteExpense(Long id) {

View File

@ -12,6 +12,7 @@
/home/along/MyCode/wanjiabuluo/fundplatform/fund-exp/src/main/java/com/fundplatform/exp/ExpApplication.java
/home/along/MyCode/wanjiabuluo/fundplatform/fund-exp/src/main/java/com/fundplatform/exp/controller/ExpenseTypeController.java
/home/along/MyCode/wanjiabuluo/fundplatform/fund-exp/src/main/java/com/fundplatform/exp/vo/FundExpenseVO.java
/home/along/MyCode/wanjiabuluo/fundplatform/fund-exp/src/main/java/com/fundplatform/exp/dto/ExpenseExcel.java
/home/along/MyCode/wanjiabuluo/fundplatform/fund-exp/src/main/java/com/fundplatform/exp/controller/HealthController.java
/home/along/MyCode/wanjiabuluo/fundplatform/fund-exp/src/main/java/com/fundplatform/exp/vo/ExpenseTypeVO.java
/home/along/MyCode/wanjiabuluo/fundplatform/fund-exp/src/main/java/com/fundplatform/exp/dto/ExpenseTypeDTO.java

View File

@ -2,15 +2,19 @@ package com.fundplatform.receipt.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fundplatform.common.core.Result;
import com.fundplatform.common.util.ExcelUtil;
import com.fundplatform.receipt.dto.ReceivableDTO;
import com.fundplatform.receipt.dto.ReceivableExcel;
import com.fundplatform.receipt.service.ReceivableService;
import com.fundplatform.receipt.vo.FundReceiptVO;
import com.fundplatform.receipt.vo.ReceivableVO;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;
/**
* 应收款管理Controller
@ -146,4 +150,33 @@ public class ReceivableController {
public Result<Integer> getOverdueCount() {
return Result.success(receivableService.getOverdueCount());
}
/**
* 导出应收款明细Excel
*/
@GetMapping("/export")
public void exportExcel(
@RequestParam(required = false) Long projectId,
@RequestParam(required = false) Long customerId,
@RequestParam(required = false) String status,
@RequestParam(required = false) Integer confirmStatus,
HttpServletResponse response) {
List<ReceivableVO> list = receivableService.listReceivables(projectId, customerId, status, confirmStatus);
List<ReceivableExcel> excelData = list.stream().map(vo -> {
ReceivableExcel excel = new ReceivableExcel();
excel.setReceivableCode(vo.getReceivableCode());
excel.setProjectName(vo.getProjectName());
excel.setCustomerName(vo.getCustomerName());
excel.setTotalAmount(vo.getTotalAmount());
excel.setReceivedAmount(vo.getReceivedAmount());
excel.setRemainingAmount(vo.getRemainingAmount());
excel.setDueDate(vo.getDueDate());
excel.setReceiptStatus(vo.getReceiptStatus());
excel.setConfirmStatus(vo.getConfirmStatus());
excel.setRemark(vo.getRemark());
excel.setCreatedTime(vo.getCreatedTime());
return excel;
}).collect(Collectors.toList());
ExcelUtil.exportExcel(excelData, "应收款明细", "应收款明细", ReceivableExcel.class, response, "应收款明细.xlsx");
}
}

View File

@ -0,0 +1,134 @@
package com.fundplatform.receipt.dto;
import cn.afterturn.easypoi.excel.annotation.Excel;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 应收款明细Excel导出实体
*/
public class ReceivableExcel {
@Excel(name = "应收款编号", width = 15)
private String receivableCode;
@Excel(name = "项目名称", width = 20)
private String projectName;
@Excel(name = "客户名称", width = 15)
private String customerName;
@Excel(name = "应收金额", width = 12, type = 10)
private BigDecimal totalAmount;
@Excel(name = "已收金额", width = 12, type = 10)
private BigDecimal receivedAmount;
@Excel(name = "未收金额", width = 12, type = 10)
private BigDecimal remainingAmount;
@Excel(name = "应收日期", width = 12, format = "yyyy-MM-dd")
private LocalDate dueDate;
@Excel(name = "收款状态", width = 10, replace = {"未收款_UNPAID", "部分收款_PARTIAL", "已收款_PAID", "已逾期_OVERDUE"})
private String receiptStatus;
@Excel(name = "确认状态", width = 10, replace = {"待确认_0", "已确认_1"})
private Integer confirmStatus;
@Excel(name = "备注", width = 25)
private String remark;
@Excel(name = "创建时间", width = 18, format = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdTime;
public String getReceivableCode() {
return receivableCode;
}
public void setReceivableCode(String receivableCode) {
this.receivableCode = receivableCode;
}
public String getProjectName() {
return projectName;
}
public void setProjectName(String projectName) {
this.projectName = projectName;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public BigDecimal getTotalAmount() {
return totalAmount;
}
public void setTotalAmount(BigDecimal totalAmount) {
this.totalAmount = totalAmount;
}
public BigDecimal getReceivedAmount() {
return receivedAmount;
}
public void setReceivedAmount(BigDecimal receivedAmount) {
this.receivedAmount = receivedAmount;
}
public BigDecimal getRemainingAmount() {
return remainingAmount;
}
public void setRemainingAmount(BigDecimal remainingAmount) {
this.remainingAmount = remainingAmount;
}
public LocalDate getDueDate() {
return dueDate;
}
public void setDueDate(LocalDate dueDate) {
this.dueDate = dueDate;
}
public String getReceiptStatus() {
return receiptStatus;
}
public void setReceiptStatus(String receiptStatus) {
this.receiptStatus = receiptStatus;
}
public Integer getConfirmStatus() {
return confirmStatus;
}
public void setConfirmStatus(Integer confirmStatus) {
this.confirmStatus = confirmStatus;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public LocalDateTime getCreatedTime() {
return createdTime;
}
public void setCreatedTime(LocalDateTime createdTime) {
this.createdTime = createdTime;
}
}

View File

@ -33,6 +33,11 @@ public interface ReceivableService {
*/
Page<ReceivableVO> pageReceivables(int pageNum, int pageSize, Long projectId, Long customerId, String status, Integer confirmStatus);
/**
* 查询应收款列表不分页用于导出
*/
List<ReceivableVO> listReceivables(Long projectId, Long customerId, String status, Integer confirmStatus);
/**
* 确认应收款
*/

View File

@ -137,6 +137,19 @@ public class ReceivableServiceImpl implements ReceivableService {
return voPage;
}
@Override
public List<ReceivableVO> listReceivables(Long projectId, Long customerId, String status, Integer confirmStatus) {
LambdaQueryWrapper<Receivable> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Receivable::getDeleted, 0);
if (projectId != null) wrapper.eq(Receivable::getProjectId, projectId);
if (customerId != null) wrapper.eq(Receivable::getCustomerId, customerId);
if (status != null && !status.isEmpty()) wrapper.eq(Receivable::getStatus, status);
if (confirmStatus != null) wrapper.eq(Receivable::getConfirmStatus, confirmStatus);
wrapper.orderByDesc(Receivable::getCreatedTime);
List<Receivable> list = receivableDataService.list(wrapper);
return list.stream().map(this::convertToVO).toList();
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean confirmReceivable(Long id, Long confirmBy) {

View File

@ -1,19 +1,13 @@
com/fundplatform/receipt/data/entity/Receivable.class
com/fundplatform/receipt/dto/FundReceiptDTO.class
com/fundplatform/receipt/controller/FundReceiptController.class
com/fundplatform/receipt/data/mapper/ReceivableMapper.class
com/fundplatform/receipt/service/impl/FundReceiptServiceImpl.class
com/fundplatform/receipt/vo/ReceivableVO.class
com/fundplatform/receipt/vo/FundReceiptVO.class
com/fundplatform/receipt/data/service/FundReceiptDataService.class
com/fundplatform/receipt/controller/HealthController.class
com/fundplatform/receipt/data/service/ReceivableDataService.class
com/fundplatform/receipt/data/mapper/FundReceiptMapper.class
com/fundplatform/receipt/service/impl/ReceivableServiceImpl.class
com/fundplatform/receipt/data/entity/FundReceipt.class
com/fundplatform/receipt/ReceiptApplication.class
com/fundplatform/receipt/service/FundReceiptService.class
com/fundplatform/receipt/aop/ApiLogAspect.class
com/fundplatform/receipt/dto/ReceivableDTO.class
com/fundplatform/receipt/controller/ReceivableController.class
com/fundplatform/receipt/service/ReceivableService.class

View File

@ -4,6 +4,7 @@
/home/along/MyCode/wanjiabuluo/fundplatform/fund-receipt/src/main/java/com/fundplatform/receipt/ReceiptApplication.java
/home/along/MyCode/wanjiabuluo/fundplatform/fund-receipt/src/main/java/com/fundplatform/receipt/data/service/FundReceiptDataService.java
/home/along/MyCode/wanjiabuluo/fundplatform/fund-receipt/src/main/java/com/fundplatform/receipt/dto/ReceivableDTO.java
/home/along/MyCode/wanjiabuluo/fundplatform/fund-receipt/src/main/java/com/fundplatform/receipt/dto/ReceivableExcel.java
/home/along/MyCode/wanjiabuluo/fundplatform/fund-receipt/src/main/java/com/fundplatform/receipt/data/entity/FundReceipt.java
/home/along/MyCode/wanjiabuluo/fundplatform/fund-receipt/src/main/java/com/fundplatform/receipt/service/ReceivableService.java
/home/along/MyCode/wanjiabuluo/fundplatform/fund-receipt/src/main/java/com/fundplatform/receipt/vo/ReceivableVO.java