Merge remote-tracking branch 'origin/dev' into dev

dev
lh 3 months ago
commit 09b39feca3

@ -0,0 +1,36 @@
package org.dromara.oa.erp.constant;
/**
*
*
* @author ruoyi
*/
public interface ProjectCategoryConstant {
/**
*
*/
String MARKET = "1";
/**
*
*/
String MARKET_PART = "2";
/**
*
*/
String RD = "3";
/**
*
*/
String PRE_PRODUCTION = "4";
/**
*
*/
String CONTRACT_ORDER = "9";
}

@ -13,6 +13,8 @@ import org.dromara.common.web.core.BaseController;
import org.dromara.oa.erp.domain.bo.ErpProjectInfoBo; import org.dromara.oa.erp.domain.bo.ErpProjectInfoBo;
import org.dromara.oa.erp.domain.vo.ErpProjectInfoVo; import org.dromara.oa.erp.domain.vo.ErpProjectInfoVo;
import org.dromara.oa.erp.service.IErpContractOrderService; import org.dromara.oa.erp.service.IErpContractOrderService;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -31,6 +33,15 @@ public class ErpContractOrderController extends BaseController {
private final IErpContractOrderService erpContractOrderService; private final IErpContractOrderService erpContractOrderService;
/**
*
*/
@SaCheckPermission("oa/erp:projectInfo:list")
@GetMapping("/list")
public TableDataInfo<ErpProjectInfoVo> list(ErpProjectInfoBo bo, PageQuery pageQuery) {
return erpContractOrderService.queryPageList(bo, pageQuery);
}
/** /**
* *
*/ */

@ -0,0 +1,335 @@
package org.dromara.oa.erp.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.oa.erp.domain.vo.ProjectManHourReportVo;
import org.dromara.oa.erp.service.IErpTimesheetReportService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import cn.idev.excel.FastExcel;
import cn.idev.excel.write.handler.CellWriteHandler;
import cn.idev.excel.write.metadata.holder.WriteSheetHolder;
import cn.idev.excel.write.metadata.holder.WriteTableHolder;
import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.Head;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.FillPatternType;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import org.dromara.oa.erp.domain.vo.ProjectPersonnelReportVo;
import org.springframework.web.bind.annotation.RequestParam;
/**
*
*
* @author Yangk
* @date 2025-12-26
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/erp/timesheetReport")
public class ErpTimesheetReportController {
private final IErpTimesheetReportService reportService;
/**
*
*/
@SaCheckPermission("oa:erp:timesheetReport:projectManHour:list")
@GetMapping("/projectManHourList")
public TableDataInfo<ProjectManHourReportVo> getProjectManHourList(ProjectManHourReportVo bo, String startTime,
String endTime) {
return reportService.queryProjectManHourList(bo, startTime, endTime);
}
/**
*
*/
@SaCheckPermission("oa:erp:timesheetReport:projectPersonnel:list")
@GetMapping("/projectPersonnelReport")
public R<ProjectPersonnelReportVo> getProjectPersonnelReport(@RequestParam(required = true) Long deptId,
String startTime, String endTime) {
if (deptId == null) {
return R.fail("部门ID不能为空");
}
return R.ok(reportService.queryProjectPersonnelReport(deptId, startTime, endTime));
}
/**
*
*/
@SaCheckPermission("oa/erp:timesheetInfo:export")
@Log(title = "项目工时统计报表", businessType = BusinessType.EXPORT)
@PostMapping("/exportProjectManHour")
public void exportProjectManHour(ProjectManHourReportVo bo, String startTime, String endTime,
HttpServletResponse response) {
try {
List<ProjectManHourReportVo> list = reportService.queryProjectManHourAll(bo, startTime, endTime);
// 处理数据:如果跨部门工时 等于 当月工时(说明没有跨部门协作),则置空
BigDecimal totalHoursSum = BigDecimal.ZERO;
if (list != null) {
for (ProjectManHourReportVo vo : list) {
// 累加总工时
if (vo.getTotalHours() != null) {
totalHoursSum = totalHoursSum.add(vo.getTotalHours());
}
if (vo.getTotalHours() != null && vo.getCrossDeptHours() != null
&& vo.getTotalHours().compareTo(vo.getCrossDeptHours()) == 0) {
vo.setCrossDeptHours(null);
}
}
// 添加“合计”行
ProjectManHourReportVo totalVo = new ProjectManHourReportVo();
totalVo.setDeptName("总计");
totalVo.setTotalHours(totalHoursSum);
list.add(totalVo);
}
// 构建动态表头
List<List<String>> heads = new ArrayList<>();
heads.add(Collections.singletonList("部门"));
heads.add(Collections.singletonList("项目经理"));
heads.add(Collections.singletonList("项目名称"));
heads.add(Collections.singletonList("项目编号"));
heads.add(Collections.singletonList("项目类别"));
String totalHoursTitle = "当月工时";
if (startTime != null && !startTime.isEmpty() && endTime != null && !endTime.isEmpty()) {
totalHoursTitle = totalHoursTitle + " (" + startTime + " 至 " + endTime + ")";
}
heads.add(Collections.singletonList(totalHoursTitle));
heads.add(Collections.singletonList("跨部门工时"));
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("项目工时统计报表", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
FastExcel.write(response.getOutputStream(), ProjectManHourReportVo.class)
.head(heads)
.sheet("项目工时统计报表")
.registerWriteHandler(new ProjectCellMergeStrategy(list))
.registerWriteHandler(new ProjectRowHighlightStrategy(list))
.doWrite(list);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("导出失败");
}
}
/**
*
*/
@SaCheckPermission("oa/erp:timesheetInfo:export")
@Log(title = "项目人员工时统计报表", businessType = BusinessType.EXPORT)
@PostMapping("/exportProjectPersonnel")
public void exportProjectPersonnel(Long deptId, String startTime, String endTime, HttpServletResponse response) {
try {
ProjectPersonnelReportVo result = reportService.queryProjectPersonnelReport(deptId, startTime, endTime);
List<Map<String, Object>> rows = result.getRows();
List<ProjectPersonnelReportVo.ReportColumn> columns = result.getColumns();
// 1. 构建表头
List<List<String>> heads = new ArrayList<>();
heads.add(Collections.singletonList("部门"));
heads.add(Collections.singletonList("项目名称"));
heads.add(Collections.singletonList("项目编号"));
heads.add(Collections.singletonList("汇总"));
// 动态人员列
List<String> userKeys = new ArrayList<>();
if (columns != null) {
for (ProjectPersonnelReportVo.ReportColumn col : columns) {
heads.add(Collections.singletonList(col.getLabel()));
userKeys.add(col.getProp());
}
}
heads.add(Collections.singletonList("备注"));
// 2. 构建数据行
List<List<Object>> dataList = new ArrayList<>();
if (rows != null) {
for (Map<String, Object> row : rows) {
List<Object> dataRow = new ArrayList<>();
dataRow.add(row.get("deptName"));
dataRow.add(row.get("projectName"));
dataRow.add(row.get("projectCode"));
dataRow.add(row.get("rowTotal")); // 对应“汇总”列
// 人员列数据
for (String key : userKeys) {
Object hours = row.get(key);
dataRow.add(hours != null ? hours : "");
}
dataRow.add(row.get("remark"));
dataList.add(dataRow);
}
}
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("项目人员工时统计报表", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
// 3. 导出 (注册自定义样式策略)
FastExcel.write(response.getOutputStream())
.head(heads)
.sheet("项目人员工时统计")
.registerWriteHandler(new MapRowHighlightStrategy(rows))
.doWrite(dataList);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("导出失败");
}
}
/**
* Map
*/
public static class MapRowHighlightStrategy implements CellWriteHandler {
private final List<Map<String, Object>> dataList;
public MapRowHighlightStrategy(List<Map<String, Object>> dataList) {
this.dataList = dataList;
}
@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
if (isHead || relativeRowIndex == null)
return;
if (dataList != null && relativeRowIndex < dataList.size()) {
Map<String, Object> rowData = dataList.get(relativeRowIndex);
Boolean isCrossDept = (Boolean) rowData.get("isCrossDept");
if (Boolean.TRUE.equals(isCrossDept)) {
Workbook workbook = writeSheetHolder.getSheet().getWorkbook();
CellStyle cellStyle = workbook.createCellStyle();
cellStyle.cloneStyleFrom(cell.getCellStyle());
cellStyle.setFillForegroundColor(IndexedColors.LIGHT_YELLOW.getIndex());
cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
cell.setCellStyle(cellStyle);
}
}
}
}
/**
* ID
*/
public static class ProjectCellMergeStrategy implements CellWriteHandler {
private final List<ProjectManHourReportVo> list;
private final Map<Integer, Integer> mergeMap = new HashMap<>();
public ProjectCellMergeStrategy(List<ProjectManHourReportVo> list) {
this.list = list;
calculateMerge();
}
private void calculateMerge() {
if (list == null || list.isEmpty())
return;
for (int i = 0; i < list.size();) {
Long currentId = list.get(i).getProjectId();
int count = 1;
for (int j = i + 1; j < list.size(); j++) {
Long nextId = list.get(j).getProjectId();
if (currentId != null && currentId.equals(nextId)) {
count++;
} else {
break;
}
}
if (count > 1) {
mergeMap.put(i, count);
}
i += count;
}
}
@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
if (isHead || relativeRowIndex == null)
return;
// 仅合并“跨部门工时”列
if (head.getHeadNameList().contains("跨部门工时")) {
if (mergeMap.containsKey(relativeRowIndex)) {
Integer rowspan = mergeMap.get(relativeRowIndex);
if (rowspan > 1) {
// 合并单元格: firstRow, lastRow, firstCol, lastCol
CellRangeAddress cellRangeAddress = new CellRangeAddress(
cell.getRowIndex(),
cell.getRowIndex() + rowspan - 1,
cell.getColumnIndex(),
cell.getColumnIndex());
writeSheetHolder.getSheet().addMergedRegionUnsafe(cellRangeAddress);
}
}
}
}
}
/**
*
*/
public static class ProjectRowHighlightStrategy implements CellWriteHandler {
private final List<ProjectManHourReportVo> list;
// 黄色背景样式
private final static short YELLOW_COLOR_INDEX = org.apache.poi.ss.usermodel.IndexedColors.LIGHT_YELLOW
.getIndex();
public ProjectRowHighlightStrategy(List<ProjectManHourReportVo> list) {
this.list = list;
}
@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
if (isHead || relativeRowIndex == null || relativeRowIndex >= list.size()) {
return;
}
ProjectManHourReportVo vo = list.get(relativeRowIndex);
// 如果 crossDeptHours 不为空(说明有跨部门工时),则整行标黄
// 注意:前面的逻辑保证了只有不相等时 crossDeptHours 才不为空
if (vo.getCrossDeptHours() != null) {
org.apache.poi.ss.usermodel.Workbook workbook = writeSheetHolder.getSheet().getWorkbook();
org.apache.poi.ss.usermodel.CellStyle cellStyle = workbook.createCellStyle();
cellStyle.cloneStyleFrom(cell.getCellStyle());
cellStyle.setFillForegroundColor(YELLOW_COLOR_INDEX);
cellStyle.setFillPattern(org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND);
cell.setCellStyle(cellStyle);
}
}
}
}

@ -49,7 +49,7 @@ public class ErpProjectInfo extends TenantEntity {
private String businessDirection; private String businessDirection;
/** /**
* 1 2 3 4 5 * 1 2 3 4 9
*/ */
private String projectCategory; private String projectCategory;

@ -54,7 +54,7 @@ public class ErpProjectInfoBo extends BaseEntity {
private String businessDirection; private String businessDirection;
/** /**
* 1 2 3 4 5 * 1 2 3 4 9
*/ */
@NotBlank(message = "项目类别", groups = { AddGroup.class, EditGroup.class }) @NotBlank(message = "项目类别", groups = { AddGroup.class, EditGroup.class })
private String projectCategory; private String projectCategory;

@ -2,6 +2,9 @@ package org.dromara.oa.erp.domain.bo;
import org.dromara.oa.erp.domain.ErpTimesheetDept; import org.dromara.oa.erp.domain.ErpTimesheetDept;
import org.dromara.common.mybatis.core.domain.BaseEntity; import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.math.BigDecimal;
import org.dromara.common.core.validate.AddGroup; import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup; import org.dromara.common.core.validate.EditGroup;
import io.github.linpeilie.annotations.AutoMapper; import io.github.linpeilie.annotations.AutoMapper;
@ -55,12 +58,11 @@ public class ErpTimesheetDeptBo extends BaseEntity {
/** /**
* *
*/ */
private Long hours; private BigDecimal hours;
/** /**
* ID * ID
*/ */
private Long approverId; private Long approverId;
} }

@ -2,6 +2,9 @@ package org.dromara.oa.erp.domain.bo;
import org.dromara.oa.erp.domain.ErpTimesheetProject; import org.dromara.oa.erp.domain.ErpTimesheetProject;
import org.dromara.common.mybatis.core.domain.BaseEntity; import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.math.BigDecimal;
import org.dromara.common.core.validate.AddGroup; import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup; import org.dromara.common.core.validate.EditGroup;
import io.github.linpeilie.annotations.AutoMapper; import io.github.linpeilie.annotations.AutoMapper;
@ -65,12 +68,11 @@ public class ErpTimesheetProjectBo extends BaseEntity {
/** /**
* *
*/ */
private Long hours; private BigDecimal hours;
/** /**
* ID * ID
*/ */
private Long approverId; private Long approverId;
} }

@ -62,7 +62,7 @@ public class ErpProjectInfoVo implements Serializable {
private String businessDirection; private String businessDirection;
/** /**
* 1 2 3 4 5 * 1 2 3 4 9
*/ */
@ExcelProperty(value = "项目类别", converter = ExcelDictConvert.class) @ExcelProperty(value = "项目类别", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "project_category") @ExcelDictFormat(dictType = "project_category")
@ -197,4 +197,16 @@ public class ErpProjectInfoVo implements Serializable {
@ExcelProperty(value = "分管副总名称") @ExcelProperty(value = "分管副总名称")
private String deputyName; private String deputyName;
/**
*
*/
@ExcelProperty(value = "合同编号")
private String contractCode;
/**
*
*/
@ExcelProperty(value = "合同名称")
private String contractName;
} }

@ -10,10 +10,9 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
/** /**
* erp_timesheet_dept * erp_timesheet_dept
* *
@ -68,12 +67,11 @@ public class ErpTimesheetDeptVo implements Serializable {
* *
*/ */
@ExcelProperty(value = "工时") @ExcelProperty(value = "工时")
private Long hours; private BigDecimal hours;
/** /**
* ID * ID
*/ */
private Long approverId; private Long approverId;
} }

@ -10,10 +10,9 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
/** /**
* erp_timesheet_project * erp_timesheet_project
* *
@ -80,12 +79,11 @@ public class ErpTimesheetProjectVo implements Serializable {
* *
*/ */
@ExcelProperty(value = "工时") @ExcelProperty(value = "工时")
private Long hours; private BigDecimal hours;
/** /**
* ID * ID
*/ */
private Long approverId; private Long approverId;
} }

@ -0,0 +1,75 @@
package org.dromara.oa.erp.domain.vo;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.convert.ExcelDictConvert;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* VO
*
* @author Yangk
* @date 2025-12-26
*/
@Data
@ColumnWidth(20)
@ExcelIgnoreUnannotated
public class ProjectManHourReportVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long projectId;
/**
*
*/
@ExcelProperty(value = "部门")
private String deptName;
/**
*
*/
@ExcelProperty(value = "项目经理")
private String managerName;
/**
*
*/
@ExcelProperty(value = "项目名称")
private String projectName;
/**
*
*/
@ExcelProperty(value = "项目编号")
private String projectCode;
/**
*
*/
@ExcelProperty(value = "项目类别", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "project_category")
private String projectCategory;
/**
* /
*/
@ExcelProperty(value = "当月工时")
private BigDecimal totalHours;
/**
*
*/
@ExcelProperty(value = "跨部门工时")
private BigDecimal crossDeptHours;
}

@ -0,0 +1,60 @@
package org.dromara.oa.erp.domain.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
/**
* VO
*
* @author Yangk
* @date 2025-12-30
*/
@Data
public class ProjectPersonnelReportVo implements Serializable {
/**
*
*/
private List<ReportColumn> columns;
/**
* (Map key column.prop)
*/
private List<Map<String, Object>> rows;
/**
*
*/
private ReportFooter footer;
@Data
public static class ReportColumn implements Serializable {
private String label;
private String prop;
// userId (可选,用于后续可能的操作)
private Long userId;
public ReportColumn(String label, String prop) {
this.label = label;
this.prop = prop;
}
public ReportColumn(String label, String prop, Long userId) {
this.label = label;
this.prop = prop;
this.userId = userId;
}
}
@Data
public static class ReportFooter implements Serializable {
// 当月天数
private Integer totalDays;
// 部门人数
private Integer deptHeadCount;
// 部门总人天
private Integer totalManDays;
}
}

@ -10,9 +10,9 @@ import lombok.Getter;
@Getter @Getter
public enum TechTypeEnum { public enum TechTypeEnum {
/** /**
* *
*/ */
TECH_CONSULT("1", "技术咨询开发"), TECH_CONSULT("1", "新产品设计费"),
/** /**
* - * -

@ -0,0 +1,51 @@
package org.dromara.oa.erp.enums;
import lombok.Getter;
/**
* @author xins
* @description erp_rd_budget_travel_cost
* @date 2025/12/11 13:58
*/
@Getter
public enum TripTypeEnum {
/**
*
*/
TRAVEL("1", "差旅费"),
/**
*
*/
TRANSPORTATION("2", "交通费");
/**
*
*/
private final String code;
/**
*
*/
private final String name;
TripTypeEnum(String code, String name) {
this.code = code;
this.name = name;
}
/**
*
*/
public static TripTypeEnum getByCode(String code) {
for (TripTypeEnum config : values()) {
if (config.getCode().equals(code)) {
return config;
}
}
return null;
}
}

@ -0,0 +1,47 @@
package org.dromara.oa.erp.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.dromara.oa.erp.domain.vo.ProjectManHourReportVo;
import java.util.List;
import java.util.Map;
/**
* Mapper
*
* @author Yangk
* @date 2025-12-26
*/
@Mapper
public interface ErpTimesheetReportMapper {
/**
*
*
* @param bo
* @param startTime
* @param endTime
* @return
*/
List<ProjectManHourReportVo> selectProjectManHourList(@Param("bo") ProjectManHourReportVo bo,
@Param("startTime") String startTime,
@Param("endTime") String endTime);
/**
* -
*/
List<Map<String, Object>> selectProjectPersonnelHours(@Param("deptId") Long deptId,
@Param("startTime") String startTime, @Param("endTime") String endTime);
/**
*
*/
List<Map<String, Object>> selectProjectTotalHours(@Param("deptId") Long deptId,
@Param("startTime") String startTime, @Param("endTime") String endTime);
/**
*
*/
List<Map<String, Object>> selectDeptUsers(@Param("deptId") Long deptId);
}

@ -2,6 +2,10 @@ package org.dromara.oa.erp.service;
import org.dromara.oa.erp.domain.vo.ErpProjectInfoVo; import org.dromara.oa.erp.domain.vo.ErpProjectInfoVo;
import org.dromara.oa.erp.domain.bo.ErpProjectInfoBo; import org.dromara.oa.erp.domain.bo.ErpProjectInfoBo;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import java.util.List;
/** /**
* Service * Service
@ -19,6 +23,23 @@ public interface IErpContractOrderService {
*/ */
ErpProjectInfoVo queryById(Long projectId); ErpProjectInfoVo queryById(Long projectId);
/**
*
*
* @param bo
* @param pageQuery
* @return
*/
TableDataInfo<ErpProjectInfoVo> queryPageList(ErpProjectInfoBo bo, PageQuery pageQuery);
/**
*
*
* @param bo
* @return
*/
List<ErpProjectInfoVo> queryList(ErpProjectInfoBo bo);
/** /**
* *
* *

@ -72,8 +72,14 @@ public interface IErpTimesheetInfoService {
/** /**
* *
*
* @param bo * @param bo
* @return * @return
*/ */
ErpTimesheetInfoVo submitAndFlowStart(ErpTimesheetInfoBo bo); ErpTimesheetInfoVo submitAndFlowStart(ErpTimesheetInfoBo bo);
/**
*
*/
ErpTimesheetInfoVo processSubmitAndFlowStart(ErpTimesheetInfoBo bo);
} }

@ -0,0 +1,38 @@
package org.dromara.oa.erp.service;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.oa.erp.domain.vo.ProjectManHourReportVo;
import java.util.List;
import org.dromara.oa.erp.domain.vo.ProjectPersonnelReportVo;
/**
* Service
*
* @author Yangk
* @date 2025-12-26
*/
public interface IErpTimesheetReportService {
/**
*
*/
TableDataInfo<ProjectManHourReportVo> queryProjectManHourList(ProjectManHourReportVo bo, String startTime,
String endTime);
/**
* @return
*/
List<ProjectManHourReportVo> queryProjectManHourAll(ProjectManHourReportVo bo, String startTime, String endTime);
/**
*
*
* @param deptId ID
* @param startTime
* @param endTime
* @return
*/
ProjectPersonnelReportVo queryProjectPersonnelReport(Long deptId, String startTime, String endTime);
}

@ -24,6 +24,12 @@ import org.dromara.oa.erp.mapper.ErpProjectInfoMapper;
import org.dromara.oa.erp.mapper.ErpProjectPlanMapper; import org.dromara.oa.erp.mapper.ErpProjectPlanMapper;
import org.dromara.oa.erp.mapper.ErpProjectPlanStageMapper; import org.dromara.oa.erp.mapper.ErpProjectPlanStageMapper;
import org.dromara.oa.erp.service.IErpContractOrderService; import org.dromara.oa.erp.service.IErpContractOrderService;
import org.dromara.oa.erp.constant.ProjectCategoryConstant;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.yulichang.toolkit.JoinWrappers;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import org.dromara.system.api.RemoteCodeRuleService; import org.dromara.system.api.RemoteCodeRuleService;
import org.dromara.workflow.api.RemoteWorkflowService; import org.dromara.workflow.api.RemoteWorkflowService;
import org.dromara.workflow.api.domain.RemoteStartProcess; import org.dromara.workflow.api.domain.RemoteStartProcess;
@ -70,11 +76,70 @@ public class ErpContractOrderServiceImpl implements IErpContractOrderService {
*/ */
@Override @Override
public ErpProjectInfoVo queryById(Long projectId) { public ErpProjectInfoVo queryById(Long projectId) {
ErpProjectInfo projectInfo = projectInfoMapper.selectById(projectId); MPJLambdaWrapper<ErpProjectInfo> lqw = JoinWrappers.lambda(ErpProjectInfo.class)
if (projectInfo == null) { .eq(ErpProjectInfo::getProjectId, projectId)
return null; .eq(ErpProjectInfo::getProjectCategory, ProjectCategoryConstant.CONTRACT_ORDER)
} .eq("t.del_flag", "0");
return MapstructUtils.convert(projectInfo, ErpProjectInfoVo.class); List<ErpProjectInfoVo> list = projectInfoMapper.selectCustomErpProjectInfoVoList(lqw);
return !list.isEmpty() ? list.get(0) : null;
}
/**
*
*
* @param bo
* @param pageQuery
* @return
*/
@Override
public TableDataInfo<ErpProjectInfoVo> queryPageList(ErpProjectInfoBo bo, PageQuery pageQuery) {
MPJLambdaWrapper<ErpProjectInfo> lqw = buildQueryWrapper(bo);
Page<ErpProjectInfoVo> result = projectInfoMapper.selectCustomErpProjectInfoVoList(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
*
*
* @param bo
* @return
*/
@Override
public List<ErpProjectInfoVo> queryList(ErpProjectInfoBo bo) {
MPJLambdaWrapper<ErpProjectInfo> lqw = buildQueryWrapper(bo);
return projectInfoMapper.selectCustomErpProjectInfoVoList(lqw);
}
/**
*
*
* @param bo
* @return
*/
private MPJLambdaWrapper<ErpProjectInfo> buildQueryWrapper(ErpProjectInfoBo bo) {
MPJLambdaWrapper<ErpProjectInfo> lqw = JoinWrappers.lambda(ErpProjectInfo.class)
.selectAll(ErpProjectInfo.class)
.eq(ErpProjectInfo::getProjectCategory, ProjectCategoryConstant.CONTRACT_ORDER)
.eq(StringUtils.isNotBlank(bo.getContractFlag()), ErpProjectInfo::getContractFlag, bo.getContractFlag())
.like(StringUtils.isNotBlank(bo.getProjectCode()), ErpProjectInfo::getProjectCode, bo.getProjectCode())
.like(StringUtils.isNotBlank(bo.getProjectName()), ErpProjectInfo::getProjectName, bo.getProjectName())
.eq(StringUtils.isNotBlank(bo.getBusinessDirection()), ErpProjectInfo::getBusinessDirection, bo.getBusinessDirection())
.eq(StringUtils.isNotBlank(bo.getSpareFlag()), ErpProjectInfo::getSpareFlag, bo.getSpareFlag())
.eq(bo.getProjectTypeId() != null, ErpProjectInfo::getProjectTypeId, bo.getProjectTypeId())
.eq(StringUtils.isNotBlank(bo.getPaymentMethod()), ErpProjectInfo::getPaymentMethod, bo.getPaymentMethod())
.eq(bo.getDeptId() != null, ErpProjectInfo::getDeptId, bo.getDeptId())
.eq(bo.getManagerId() != null, ErpProjectInfo::getManagerId, bo.getManagerId())
.eq(bo.getChargeId() != null, ErpProjectInfo::getChargeId, bo.getChargeId())
.eq(bo.getDeputyId() != null, ErpProjectInfo::getDeputyId, bo.getDeputyId())
.eq(StringUtils.isNotBlank(bo.getPeopleId()), ErpProjectInfo::getPeopleId, bo.getPeopleId())
.eq(bo.getAmount() != null, ErpProjectInfo::getAmount, bo.getAmount())
.eq(StringUtils.isNotBlank(bo.getProjectStatus()), ErpProjectInfo::getProjectStatus, bo.getProjectStatus())
.eq(StringUtils.isNotBlank(bo.getFlowStatus()), ErpProjectInfo::getFlowStatus, bo.getFlowStatus())
.eq(bo.getContractId() != null, ErpProjectInfo::getContractId, bo.getContractId())
.eq(ErpProjectInfo::getProjectCategory, ProjectCategoryConstant.CONTRACT_ORDER)
.eq("t.del_flag", "0")
.orderByDesc(ErpProjectInfo::getCreateTime);
return lqw;
} }
/** /**

@ -14,6 +14,7 @@ import com.github.yulichang.toolkit.JoinWrappers;
import com.github.yulichang.wrapper.MPJLambdaWrapper; import com.github.yulichang.wrapper.MPJLambdaWrapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.dromara.common.tenant.helper.TenantHelper; import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.oa.erp.constant.ProjectCategoryConstant;
import org.dromara.oa.erp.enums.ProjectCategoryEnum; import org.dromara.oa.erp.enums.ProjectCategoryEnum;
import org.dromara.workflow.api.event.ProcessEvent; import org.dromara.workflow.api.event.ProcessEvent;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
@ -121,6 +122,7 @@ public class ErpProjectInfoServiceImpl implements IErpProjectInfoService {
.eq(bo.getSortOrder() != null, ErpProjectInfo::getSortOrder, bo.getSortOrder()) .eq(bo.getSortOrder() != null, ErpProjectInfo::getSortOrder, bo.getSortOrder())
.eq(bo.getContractId() != null, ErpProjectInfo::getContractId, bo.getContractId()) .eq(bo.getContractId() != null, ErpProjectInfo::getContractId, bo.getContractId())
.eq(StringUtils.isNotBlank(bo.getActiveFlag()), ErpProjectInfo::getActiveFlag, bo.getActiveFlag()) .eq(StringUtils.isNotBlank(bo.getActiveFlag()), ErpProjectInfo::getActiveFlag, bo.getActiveFlag())
.ne(ErpProjectInfo::getProjectCategory, ProjectCategoryConstant.CONTRACT_ORDER)
.eq("t.del_flag", "0") .eq("t.del_flag", "0")
.orderByDesc(ErpProjectInfo::getCreateTime); .orderByDesc(ErpProjectInfo::getCreateTime);
return lqw; return lqw;
@ -318,6 +320,7 @@ public class ErpProjectInfoServiceImpl implements IErpProjectInfoService {
wrapper.like(StringUtils.isNotBlank(bo.getProjectCode()), ErpProjectInfo::getProjectCode, bo.getProjectCode()) wrapper.like(StringUtils.isNotBlank(bo.getProjectCode()), ErpProjectInfo::getProjectCode, bo.getProjectCode())
.like(StringUtils.isNotBlank(bo.getProjectName()), ErpProjectInfo::getProjectName, bo.getProjectName()) .like(StringUtils.isNotBlank(bo.getProjectName()), ErpProjectInfo::getProjectName, bo.getProjectName())
.eq(StringUtils.isNotBlank(bo.getProjectStatus()), ErpProjectInfo::getProjectStatus, bo.getProjectStatus()) .eq(StringUtils.isNotBlank(bo.getProjectStatus()), ErpProjectInfo::getProjectStatus, bo.getProjectStatus())
.ne(ErpProjectInfo::getProjectCategory, ProjectCategoryConstant.CONTRACT_ORDER)
.eq("t.del_flag", "0") .eq("t.del_flag", "0")
.and(w -> .and(w ->
{ {
@ -328,12 +331,8 @@ public class ErpProjectInfoServiceImpl implements IErpProjectInfoService {
w.eq(StringUtils.isNotBlank(bo.getProjectCategory()), ErpProjectInfo::getProjectCategory, ProjectCategoryEnum.RD.getCode()) w.eq(StringUtils.isNotBlank(bo.getProjectCategory()), ErpProjectInfo::getProjectCategory, ProjectCategoryEnum.RD.getCode())
.or().eq(StringUtils.isNotBlank(bo.getProjectCategory()), ErpProjectInfo::getProjectCategory, ProjectCategoryEnum.PRE_PRODUCTION.getCode()); .or().eq(StringUtils.isNotBlank(bo.getProjectCategory()), ErpProjectInfo::getProjectCategory, ProjectCategoryEnum.PRE_PRODUCTION.getCode());
} }
}).orderByDesc(ErpProjectInfo::getCreateTime); }).orderByDesc(ErpProjectInfo::getCreateTime);
;
return wrapper; return wrapper;
} }
} }

@ -20,6 +20,7 @@ import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.tenant.helper.TenantHelper; import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.oa.erp.constant.ProjectCategoryConstant;
import org.dromara.oa.erp.domain.ErpProjectPlan; import org.dromara.oa.erp.domain.ErpProjectPlan;
import org.dromara.oa.erp.domain.ErpProjectPlanStage; import org.dromara.oa.erp.domain.ErpProjectPlanStage;
import org.dromara.oa.erp.domain.bo.ErpProjectPlanBo; import org.dromara.oa.erp.domain.bo.ErpProjectPlanBo;
@ -129,6 +130,7 @@ public class ErpProjectPlanServiceImpl implements IErpProjectPlanService {
.eq(bo.getContractId() != null, ErpProjectPlan::getContractId, bo.getContractId()) .eq(bo.getContractId() != null, ErpProjectPlan::getContractId, bo.getContractId())
.eq(StringUtils.isNotBlank(bo.getOssId()), ErpProjectPlan::getOssId, bo.getOssId()) .eq(StringUtils.isNotBlank(bo.getOssId()), ErpProjectPlan::getOssId, bo.getOssId())
.eq(StringUtils.isNotBlank(bo.getActiveFlag()), ErpProjectPlan::getActiveFlag, bo.getActiveFlag()) .eq(StringUtils.isNotBlank(bo.getActiveFlag()), ErpProjectPlan::getActiveFlag, bo.getActiveFlag())
.ne("p.project_category", ProjectCategoryConstant.CONTRACT_ORDER)
.orderByDesc(ErpProjectPlan::getCreateTime); .orderByDesc(ErpProjectPlan::getCreateTime);
return lqw; return lqw;
} }

@ -29,6 +29,7 @@ import org.dromara.oa.erp.service.IErpTimesheetProjectService;
import org.dromara.system.api.RemoteCodeRuleService; import org.dromara.system.api.RemoteCodeRuleService;
import org.dromara.workflow.api.RemoteWorkflowService; import org.dromara.workflow.api.RemoteWorkflowService;
import org.dromara.workflow.api.domain.RemoteStartProcess; import org.dromara.workflow.api.domain.RemoteStartProcess;
import org.dromara.workflow.api.domain.RemoteStartProcessReturn;
import org.dromara.workflow.api.event.ProcessEvent; import org.dromara.workflow.api.event.ProcessEvent;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -64,6 +65,10 @@ public class ErpTimesheetInfoServiceImpl implements IErpTimesheetInfoService {
private final IErpTimesheetDeptService timesheetDeptService; private final IErpTimesheetDeptService timesheetDeptService;
private final IErpTimesheetProjectService timesheetProjectService; private final IErpTimesheetProjectService timesheetProjectService;
@jakarta.annotation.Resource
@org.springframework.context.annotation.Lazy
private IErpTimesheetInfoService self;
/** /**
* *
* *
@ -254,9 +259,23 @@ public class ErpTimesheetInfoServiceImpl implements IErpTimesheetInfoService {
* @param bo * @param bo
* @return * @return
*/ */
/**
*
*/
@Override
public ErpTimesheetInfoVo submitAndFlowStart(ErpTimesheetInfoBo bo) {
// 1. 如果是新增且没有编号,先生成编号
if (bo.getTimesheetId() == null && StringUtils.isBlank(bo.getTimesheetCode())) {
String timesheetCode = remoteCodeRuleService.selectCodeRuleCode("1015");
bo.setTimesheetCode(timesheetCode);
}
// 2. 调用实际处理方法 (开启全局事务)
return self.processSubmitAndFlowStart(bo);
}
@Override @Override
@GlobalTransactional(rollbackFor = Exception.class) // 开启全局事务 @GlobalTransactional(rollbackFor = Exception.class) // 开启全局事务
public ErpTimesheetInfoVo submitAndFlowStart(ErpTimesheetInfoBo bo) { public ErpTimesheetInfoVo processSubmitAndFlowStart(ErpTimesheetInfoBo bo) {
if (bo.getTimesheetId() == null) { if (bo.getTimesheetId() == null) {
this.insertByBo(bo); this.insertByBo(bo);
} else { } else {
@ -309,9 +328,27 @@ public class ErpTimesheetInfoServiceImpl implements IErpTimesheetInfoService {
bo.getBizExt().setBusinessId(startProcess.getBusinessId()); bo.getBizExt().setBusinessId(startProcess.getBusinessId());
} }
boolean flag = remoteWorkflowService.startCompleteTask(startProcess); RemoteStartProcessReturn result = remoteWorkflowService.startWorkFlow(startProcess);
if (!flag) { if (result == null) {
throw new ServiceException("流程发起异常"); throw new ServiceException("流程发起失败");
}
// 如果返回了 taskId说明流程停留在第一个节点需要我们手动完成它 (对应 "startCompleteTask" 的逻辑)
if (result.getTaskId() != null) {
org.dromara.workflow.api.domain.RemoteCompleteTask completeTask = new org.dromara.workflow.api.domain.RemoteCompleteTask();
completeTask.setTaskId(result.getTaskId());
// 审批意见,提交时默认为 "提交申请"
completeTask.setMessage("提交申请");
completeTask.setVariables(variables);
boolean completeFlag = remoteWorkflowService.completeTask(completeTask);
if (!completeFlag) {
// 这里可能需要警告,但既然流程已启动,可能不一定非要抛异常,视业务而定
log.warn("流程启动成功,但自动完成第一个任务失败: taskId={}", result.getTaskId());
}
} else {
// taskId 为空,说明流程启动后直接跳过/已结束/进入下一个节点(可能因为自动完成)
// 这种情况下,我们不需要做任何事,直接视为成功
log.info("流程启动成功,首节点已自动完成或跳过: instanceId={}", result.getProcessInstanceId());
} }
return MapstructUtils.convert(baseMapper.selectById(bo.getTimesheetId()), ErpTimesheetInfoVo.class); return MapstructUtils.convert(baseMapper.selectById(bo.getTimesheetId()), ErpTimesheetInfoVo.class);

@ -0,0 +1,164 @@
package org.dromara.oa.erp.service.impl;
import com.github.yulichang.toolkit.JoinWrappers;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import lombok.RequiredArgsConstructor;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.oa.erp.domain.vo.ProjectManHourReportVo;
import org.dromara.oa.erp.domain.vo.ProjectPersonnelReportVo;
import org.dromara.oa.erp.mapper.ErpTimesheetReportMapper;
import org.dromara.oa.erp.service.IErpTimesheetReportService;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
/**
* Service
*
* @author Yangk
* @date 2025-12-26
*/
@RequiredArgsConstructor
@Service
public class ErpTimesheetReportServiceImpl implements IErpTimesheetReportService {
private final ErpTimesheetReportMapper reportMapper;
@Override
public TableDataInfo<ProjectManHourReportVo> queryProjectManHourList(ProjectManHourReportVo bo, String startTime,
String endTime) {
List<ProjectManHourReportVo> list = reportMapper.selectProjectManHourList(bo, startTime, endTime);
return TableDataInfo.build(list);
}
@Override
public List<ProjectManHourReportVo> queryProjectManHourAll(ProjectManHourReportVo bo, String startTime,
String endTime) {
return reportMapper.selectProjectManHourList(bo, startTime, endTime);
}
@Override
public ProjectPersonnelReportVo queryProjectPersonnelReport(Long deptId, String startTime, String endTime) {
ProjectPersonnelReportVo result = new ProjectPersonnelReportVo();
// 1. 查询该部门所有人员 (作为所有的列)
List<Map<String, Object>> deptUsers = reportMapper.selectDeptUsers(deptId);
List<ProjectPersonnelReportVo.ReportColumn> columns = new ArrayList<>();
Set<Long> userIds = new HashSet<>();
// 记录部门名称 (随便取一个用户的,或者如果没人则为空)
String deptName = "";
if (deptUsers != null) {
for (Map<String, Object> u : deptUsers) {
Long uid = (Long) u.get("userId");
String nickName = (String) u.get("nickName");
if (deptName.isEmpty() && u.get("deptName") != null) {
deptName = (String) u.get("deptName");
}
if (uid != null && !userIds.contains(uid)) {
userIds.add(uid);
columns.add(new ProjectPersonnelReportVo.ReportColumn(nickName, "user_" + uid, uid));
}
}
}
// 按名字排序 (或者保持数据库查询顺序)
// columns.sort(Comparator.comparing(ProjectPersonnelReportVo.ReportColumn::getLabel));
result.setColumns(columns);
// 2. 查询工时明细数据 (projectId, userId, hours)
List<Map<String, Object>> rawData = reportMapper.selectProjectPersonnelHours(deptId, startTime, endTime);
// 3. 查询跨部门总工时 (projectId, totalHours)
List<Map<String, Object>> totalHoursData = reportMapper.selectProjectTotalHours(deptId, startTime, endTime);
Map<Long, BigDecimal> projectTotalHoursMap = new HashMap<>();
if (totalHoursData != null) {
for (Map<String, Object> map : totalHoursData) {
Long pid = (Long) map.get("projectId");
BigDecimal total = (BigDecimal) map.get("totalHours");
projectTotalHoursMap.put(pid, total);
}
}
// 4. 构建行 (项目)
Map<Long, Map<String, Object>> rowsMap = new LinkedHashMap<>();
if (rawData != null) {
for (Map<String, Object> map : rawData) {
Long pid = (Long) map.get("projectId");
Long uid = (Long) map.get("userId");
BigDecimal hours = (BigDecimal) map.get("hours");
Map<String, Object> row = rowsMap.computeIfAbsent(pid, k -> {
Map<String, Object> r = new HashMap<>();
r.put("projectId", pid);
r.put("projectDeptId", map.get("projectDeptId"));
r.put("projectName", map.get("projectName"));
r.put("projectCode", map.get("projectCode"));
r.put("rowTotal", BigDecimal.ZERO);
return r;
});
// 设置人员工时
row.put("user_" + uid, hours);
// 累加行汇总
BigDecimal currentTotal = (BigDecimal) row.get("rowTotal");
row.put("rowTotal", currentTotal.add(hours));
}
}
List<Map<String, Object>> rows = new ArrayList<>(rowsMap.values());
// 5. 跨部门逻辑 & 底部统计
int days = calculateDays(startTime, endTime);
int headCount = userIds.size();
for (Map<String, Object> row : rows) {
// 补充部门名称
row.put("deptName", deptName);
Long projectDeptId = (Long) row.get("projectDeptId");
// 如果项目所属部门 != 当前报表统计部门,则是跨部门项目
if (projectDeptId != null && !projectDeptId.equals(deptId)) {
row.put("isCrossDept", true);
row.put("remark", "跨部门"); // 备注列
} else {
row.put("isCrossDept", false);
row.put("remark", "");
}
}
result.setRows(rows);
// 6. 底部
ProjectPersonnelReportVo.ReportFooter footer = new ProjectPersonnelReportVo.ReportFooter();
footer.setTotalDays(days);
footer.setDeptHeadCount(headCount);
footer.setTotalManDays(days * headCount);
result.setFooter(footer);
return result;
}
private int calculateDays(String start, String end) {
if (start == null || end == null)
return 0;
try {
LocalDate d1 = LocalDate.parse(start, DateTimeFormatter.ISO_DATE);
LocalDate d2 = LocalDate.parse(end, DateTimeFormatter.ISO_DATE);
return (int) ChronoUnit.DAYS.between(d1, d2) + 1;
} catch (Exception e) {
return 0;
}
}
}

@ -10,6 +10,8 @@ import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -24,6 +26,10 @@ public abstract class BaseExcelExporter {
protected Workbook workbook; protected Workbook workbook;
//字体定义
protected Font yaheiNormal10Font;
protected Font yaheiBold10Font;
// 样式定义 // 样式定义
protected CellStyle titleStyle; protected CellStyle titleStyle;
protected CellStyle formLeftStyle; protected CellStyle formLeftStyle;
@ -40,9 +46,54 @@ public abstract class BaseExcelExporter {
protected CellStyle remarkStyle; protected CellStyle remarkStyle;
protected CellStyle leftMergeStyle; protected CellStyle leftMergeStyle;
protected CellStyle rdCoverTitleStyle;
protected CellStyle rdCoverSubtitleStyle;
protected CellStyle rdCoverFormKeyStyle;
protected CellStyle rdCoverFormValueStyle;
protected CellStyle rdTitleStyle;
protected CellStyle rdHeaderStyle;
protected CellStyle rdNoTopBorderHeaderStyle;
protected CellStyle rdNoBottomBorderHeaderStyle;
protected CellStyle rdLeftHeaderStyle;
protected CellStyle rdRightHeaderStyle;
protected CellStyle rdFooterStyle;
protected CellStyle rdLeftFooterStyle;
protected CellStyle rdRightFooterStyle;
protected CellStyle rdDataStyle;
protected CellStyle rdLeftDataStyle;
protected CellStyle rdRightDataStyle;
protected CellStyle rdLeftBottomDataStyle;
protected CellStyle rdRightBottomDataStyle;
protected CellStyle rdDataBoldStyle;
protected CellStyle rdLeftDataBoldStyle;
protected CellStyle rdRightDataBoldStyle;
protected CellStyle rdDataRedBoldStyle;
protected CellStyle rdLeftDataRedBoldStyle;
protected CellStyle rdRightDataRedBoldStyle;
protected CellStyle rdBottomDataRedBoldStyle;
protected CellStyle rdFormulaStyle;
protected CellStyle rdRightFormulaStyle;
protected CellStyle rdFooterFormulaStyle;
protected CellStyle rdFormulaPercentageStyle;
protected CellStyle rdFooterFormulaPercentageStyle;
protected CellStyle rdFooterRightFormulaStyle;
protected CellStyle rdInstructionStyle;
protected CellStyle rdFormDataStyle;
protected CellStyle rdFormStyle;
protected CellStyle rdFormFormulaStyle;
// 存储各sheet的总计行位置 // 存储各sheet的总计行位置
protected final Map<String, Integer> sheetTotalRowMap = new ConcurrentHashMap<>(); protected final Map<String, Integer> sheetTotalRowMap = new ConcurrentHashMap<>();
// 富文本样式缓存
protected final Map<String, Font> fontCache = new ConcurrentHashMap<>();
/** /**
* Excel * Excel
*/ */
@ -64,6 +115,9 @@ public abstract class BaseExcelExporter {
* *
*/ */
protected void createStyles() { protected void createStyles() {
//创建字体样式
createFont();
// 创建标题样式 // 创建标题样式
createTitleStyle(); createTitleStyle();
@ -78,8 +132,69 @@ public abstract class BaseExcelExporter {
// 创建其他样式 // 创建其他样式
createOtherStyles(); createOtherStyles();
// 创建研发预算封面样式
createRdCoverStyle();
} }
/**
*
*/
protected void createFont() {
yaheiNormal10Font = workbook.createFont();
yaheiNormal10Font.setFontName("微软雅黑");
yaheiNormal10Font.setFontHeightInPoints((short) 10);
yaheiBold10Font = workbook.createFont();
yaheiBold10Font.setBold(true);
yaheiBold10Font.setFontName("微软雅黑");
yaheiBold10Font.setFontHeightInPoints((short) 10);
initFontCache();
}
/**
*
*/
protected void initFontCache() {
fontCache.clear();
fontCache.put("normal-10", yaheiNormal10Font);
fontCache.put("bold-10", yaheiBold10Font);
}
/**
*
*/
protected Font getOrCreateFont(String fontKey, FontStyleConfig config) {
if (fontCache.containsKey(fontKey)) {
return fontCache.get(fontKey);
}
Font font = workbook.createFont();
font.setFontName(config.getFontName() != null ? config.getFontName() : "微软雅黑");
font.setFontHeightInPoints(config.getFontSize());
if (config.isBold()) {
font.setBold(true);
}
if (config.getColor() != null) {
font.setColor(config.getColor());
}
if (config.isItalic()) {
font.setItalic(true);
}
if (config.isUnderline()) {
font.setUnderline(Font.U_SINGLE);
}
fontCache.put(fontKey, font);
return font;
}
/** /**
* *
*/ */
@ -95,6 +210,275 @@ public abstract class BaseExcelExporter {
setBorder(titleStyle, BorderStyle.MEDIUM); setBorder(titleStyle, BorderStyle.MEDIUM);
} }
/**
*
*/
protected void createRdCoverStyle() {
rdCoverTitleStyle = workbook.createCellStyle();
Font titleFont = workbook.createFont();
titleFont.setBold(true);
titleFont.setFontName("宋体");
titleFont.setFontHeightInPoints((short) 26);
rdCoverTitleStyle.setFont(titleFont);
rdCoverTitleStyle.setAlignment(HorizontalAlignment.CENTER);
rdCoverTitleStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdCoverSubtitleStyle = workbook.createCellStyle();
Font rdSubtitleFont = workbook.createFont();
rdSubtitleFont.setBold(true);
rdSubtitleFont.setFontName("宋体");
rdSubtitleFont.setFontHeightInPoints((short) 48);
rdCoverSubtitleStyle.setFont(rdSubtitleFont);
rdCoverSubtitleStyle.setAlignment(HorizontalAlignment.CENTER);
rdCoverSubtitleStyle.setVerticalAlignment(VerticalAlignment.CENTER);
Font formFont = workbook.createFont();
formFont.setFontName("宋体");
formFont.setFontHeightInPoints((short) 18);
rdCoverFormKeyStyle = workbook.createCellStyle();
rdCoverFormKeyStyle.setFont(formFont);
rdCoverFormKeyStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdCoverFormKeyStyle.setAlignment(HorizontalAlignment.LEFT);
rdCoverFormValueStyle = workbook.createCellStyle();
rdCoverFormValueStyle.setFont(formFont);
rdCoverFormValueStyle.setBorderBottom(BorderStyle.THIN);
rdCoverFormValueStyle.setBottomBorderColor(IndexedColors.BLACK.getIndex());
rdCoverFormValueStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdCoverFormValueStyle.setAlignment(HorizontalAlignment.CENTER);
// 研发预算表头样式
rdTitleStyle = workbook.createCellStyle();
Font rdTitleFont = workbook.createFont();
rdTitleFont.setBold(true);
rdTitleFont.setFontName("微软雅黑");
rdTitleFont.setFontHeightInPoints((short) 18);
rdTitleStyle.setFont(rdTitleFont);
rdTitleStyle.setAlignment(HorizontalAlignment.CENTER);
rdTitleStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdHeaderStyle = workbook.createCellStyle();
rdHeaderStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
rdHeaderStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
Font dataBoldFont = workbook.createFont();
dataBoldFont.setFontName("微软雅黑");
dataBoldFont.setFontHeightInPoints((short) 11);
dataBoldFont.setBold(true);
rdHeaderStyle.setFont(dataBoldFont);
setBorder(rdHeaderStyle, BorderStyle.DOUBLE, BorderStyle.THIN, BorderStyle.THIN, BorderStyle.THIN);
rdHeaderStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdHeaderStyle.setAlignment(HorizontalAlignment.CENTER);
rdLeftHeaderStyle = workbook.createCellStyle();
rdLeftHeaderStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
rdLeftHeaderStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
rdLeftHeaderStyle.setFont(dataBoldFont);
setBorder(rdLeftHeaderStyle, BorderStyle.DOUBLE, BorderStyle.THIN, BorderStyle.DOUBLE, BorderStyle.THIN);
rdLeftHeaderStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdLeftHeaderStyle.setAlignment(HorizontalAlignment.CENTER);
rdRightHeaderStyle = workbook.createCellStyle();
rdRightHeaderStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
rdRightHeaderStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
rdRightHeaderStyle.setFont(dataBoldFont);
setBorder(rdRightHeaderStyle, BorderStyle.DOUBLE, BorderStyle.THIN, BorderStyle.THIN, BorderStyle.DOUBLE);
rdRightHeaderStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdRightHeaderStyle.setAlignment(HorizontalAlignment.CENTER);
rdNoTopBorderHeaderStyle = workbook.createCellStyle();
rdNoTopBorderHeaderStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
rdNoTopBorderHeaderStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
rdNoTopBorderHeaderStyle.setFont(dataBoldFont);
setBorder(rdNoTopBorderHeaderStyle, null, BorderStyle.THIN, BorderStyle.THIN, BorderStyle.THIN);
rdNoTopBorderHeaderStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdNoTopBorderHeaderStyle.setAlignment(HorizontalAlignment.CENTER);
rdNoBottomBorderHeaderStyle = workbook.createCellStyle();
rdNoBottomBorderHeaderStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
rdNoBottomBorderHeaderStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
rdNoBottomBorderHeaderStyle.setFont(dataBoldFont);
setBorder(rdNoBottomBorderHeaderStyle, BorderStyle.DOUBLE, null, BorderStyle.THIN, BorderStyle.THIN);
rdNoBottomBorderHeaderStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdNoBottomBorderHeaderStyle.setAlignment(HorizontalAlignment.CENTER);
Font dataFont = workbook.createFont();
dataFont.setFontName("微软雅黑");
dataFont.setFontHeightInPoints((short) 11);
rdDataStyle = workbook.createCellStyle();
rdDataStyle.setFont(dataFont);
setBorder(rdDataStyle, BorderStyle.THIN);
rdDataStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdDataStyle.setAlignment(HorizontalAlignment.CENTER);
rdDataStyle.setWrapText(true);
rdLeftDataStyle = workbook.createCellStyle();
rdLeftDataStyle.setFont(dataFont);
setBorder(rdLeftDataStyle, BorderStyle.THIN, BorderStyle.THIN, BorderStyle.DOUBLE, BorderStyle.THIN);
rdLeftDataStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdLeftDataStyle.setAlignment(HorizontalAlignment.CENTER);
rdRightDataStyle = workbook.createCellStyle();
rdRightDataStyle.setFont(dataFont);
setBorder(rdRightDataStyle, BorderStyle.THIN, BorderStyle.THIN, BorderStyle.THIN, BorderStyle.DOUBLE);
rdRightDataStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdRightDataStyle.setAlignment(HorizontalAlignment.CENTER);
rdRightDataStyle.setWrapText(true);
rdLeftBottomDataStyle = workbook.createCellStyle();
rdLeftBottomDataStyle.setFont(dataFont);
setBorder(rdLeftBottomDataStyle, BorderStyle.THIN, BorderStyle.DOUBLE, BorderStyle.DOUBLE, BorderStyle.THIN);
rdLeftBottomDataStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdLeftBottomDataStyle.setAlignment(HorizontalAlignment.CENTER);
rdRightBottomDataStyle = workbook.createCellStyle();
rdRightBottomDataStyle.setFont(dataFont);
setBorder(rdRightBottomDataStyle, BorderStyle.THIN, BorderStyle.DOUBLE, BorderStyle.THIN, BorderStyle.DOUBLE);
rdRightBottomDataStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdRightBottomDataStyle.setAlignment(HorizontalAlignment.CENTER);
rdRightBottomDataStyle.setWrapText(true);
rdDataBoldStyle = workbook.createCellStyle();
rdDataBoldStyle.setFont(dataBoldFont);
setBorder(rdDataBoldStyle, BorderStyle.THIN);
rdDataBoldStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdDataBoldStyle.setAlignment(HorizontalAlignment.CENTER);
rdDataBoldStyle.setWrapText(true);
rdLeftDataBoldStyle = workbook.createCellStyle();
rdLeftDataBoldStyle.setFont(dataBoldFont);
setBorder(rdLeftDataBoldStyle, BorderStyle.DOUBLE, BorderStyle.THIN, BorderStyle.THIN, BorderStyle.DOUBLE);
rdLeftDataBoldStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdLeftDataBoldStyle.setAlignment(HorizontalAlignment.CENTER);
rdRightDataBoldStyle = workbook.createCellStyle();
rdRightDataBoldStyle.setFont(dataBoldFont);
setBorder(rdRightDataBoldStyle, BorderStyle.DOUBLE, BorderStyle.THIN, BorderStyle.DOUBLE, BorderStyle.THIN);
rdRightDataBoldStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdRightDataBoldStyle.setAlignment(HorizontalAlignment.CENTER);
rdRightDataBoldStyle.setWrapText(true);
Font dataRedBoldFont = workbook.createFont();
dataRedBoldFont.setFontName("微软雅黑");
dataRedBoldFont.setFontHeightInPoints((short) 11);
dataRedBoldFont.setBold(true);
dataRedBoldFont.setColor(IndexedColors.RED.getIndex());
rdDataRedBoldStyle = workbook.createCellStyle();
rdDataRedBoldStyle.setFont(dataRedBoldFont);
setBorder(rdDataRedBoldStyle, BorderStyle.THIN, BorderStyle.THIN, BorderStyle.THIN, BorderStyle.THIN);
rdDataRedBoldStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdDataRedBoldStyle.setAlignment(HorizontalAlignment.CENTER);
rdLeftDataRedBoldStyle = workbook.createCellStyle();
rdLeftDataRedBoldStyle.setFont(dataRedBoldFont);
setBorder(rdLeftDataRedBoldStyle, BorderStyle.DOUBLE, BorderStyle.THIN, BorderStyle.DOUBLE, BorderStyle.THIN);
rdLeftDataRedBoldStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdLeftDataRedBoldStyle.setAlignment(HorizontalAlignment.CENTER);
rdRightDataRedBoldStyle = workbook.createCellStyle();
rdRightDataRedBoldStyle.setFont(dataRedBoldFont);
setBorder(rdRightDataRedBoldStyle, BorderStyle.DOUBLE, BorderStyle.THIN, BorderStyle.THIN, BorderStyle.DOUBLE);
rdRightDataRedBoldStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdRightDataRedBoldStyle.setAlignment(HorizontalAlignment.CENTER);
rdBottomDataRedBoldStyle = workbook.createCellStyle();
rdBottomDataRedBoldStyle.setFont(dataRedBoldFont);
setBorder(rdBottomDataRedBoldStyle, BorderStyle.THIN, BorderStyle.DOUBLE, BorderStyle.THIN, BorderStyle.THIN);
rdBottomDataRedBoldStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdBottomDataRedBoldStyle.setAlignment(HorizontalAlignment.CENTER);
rdFooterStyle = workbook.createCellStyle();
rdFooterStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
rdFooterStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
rdFooterStyle.setFont(dataBoldFont);
setBorder(rdFooterStyle, BorderStyle.THIN, BorderStyle.DOUBLE, BorderStyle.THIN, BorderStyle.THIN);
rdFooterStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdFooterStyle.setAlignment(HorizontalAlignment.CENTER);
rdLeftFooterStyle = workbook.createCellStyle();
rdLeftFooterStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
rdLeftFooterStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
rdLeftFooterStyle.setFont(dataBoldFont);
setBorder(rdLeftFooterStyle, BorderStyle.THIN, BorderStyle.DOUBLE, BorderStyle.DOUBLE, BorderStyle.THIN);
rdLeftFooterStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdLeftFooterStyle.setAlignment(HorizontalAlignment.CENTER);
rdRightFooterStyle = workbook.createCellStyle();
rdRightFooterStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
rdRightFooterStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
rdRightFooterStyle.setFont(dataBoldFont);
setBorder(rdRightFooterStyle, BorderStyle.THIN, BorderStyle.DOUBLE, BorderStyle.THIN, BorderStyle.DOUBLE);
rdRightFooterStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdRightFooterStyle.setAlignment(HorizontalAlignment.CENTER);
// footer表单公式样式
rdFooterFormulaStyle = workbook.createCellStyle();
rdFooterFormulaStyle.cloneStyleFrom(rdFooterStyle);
rdFooterFormulaStyle.setDataFormat(workbook.createDataFormat().getFormat("0.00"));
// 百分比样式
rdFormulaPercentageStyle = workbook.createCellStyle();
rdFormulaPercentageStyle.cloneStyleFrom(rdDataStyle);
rdFormulaPercentageStyle.setFillForegroundColor(IndexedColors.LIGHT_ORANGE.getIndex());
rdFormulaPercentageStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
rdFormulaPercentageStyle.setDataFormat(workbook.createDataFormat().getFormat("0.00%"));
rdFooterFormulaPercentageStyle = workbook.createCellStyle();
rdFooterFormulaPercentageStyle.cloneStyleFrom(rdFooterStyle);
rdFooterFormulaPercentageStyle.setDataFormat(workbook.createDataFormat().getFormat("0.00%"));
rdFooterRightFormulaStyle = workbook.createCellStyle();
rdFooterRightFormulaStyle.cloneStyleFrom(rdRightFooterStyle);
rdFooterRightFormulaStyle.setDataFormat(workbook.createDataFormat().getFormat("0.00"));
rdInstructionStyle = workbook.createCellStyle();
rdInstructionStyle.setFont(yaheiNormal10Font);
rdInstructionStyle.setWrapText(true);
rdFormulaStyle = workbook.createCellStyle();
rdFormulaStyle.cloneStyleFrom(rdDataStyle);
rdFormulaStyle.setFillForegroundColor(IndexedColors.LIGHT_ORANGE.getIndex());
rdFormulaStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
rdFormulaStyle.setDataFormat(workbook.createDataFormat().getFormat("0.00"));
rdRightFormulaStyle = workbook.createCellStyle();
rdRightFormulaStyle.cloneStyleFrom(rdRightDataStyle);
rdRightFormulaStyle.setFillForegroundColor(IndexedColors.LIGHT_ORANGE.getIndex());
rdRightFormulaStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
rdRightFormulaStyle.setDataFormat(workbook.createDataFormat().getFormat("0.00"));
rdFormDataStyle = workbook.createCellStyle();
Font rdFormDataFont = workbook.createFont();
rdFormDataFont.setFontName("微软雅黑");
rdFormDataFont.setFontHeightInPoints((short) 9);
rdFormDataStyle.setFont(rdFormDataFont);
rdFormDataStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdFormDataStyle.setAlignment(HorizontalAlignment.LEFT);
rdFormStyle = workbook.createCellStyle();
rdFormStyle.setFont(rdFormDataFont);
rdFormStyle.setVerticalAlignment(VerticalAlignment.CENTER);
rdFormStyle.setAlignment(HorizontalAlignment.LEFT);
// 公式样式
rdFormFormulaStyle = workbook.createCellStyle();
rdFormFormulaStyle.cloneStyleFrom(rdFormDataStyle);
rdFormFormulaStyle.setDataFormat(workbook.createDataFormat().getFormat("0.00"));
}
/** /**
* *
*/ */
@ -223,6 +607,7 @@ public abstract class BaseExcelExporter {
setBorder(leftMergeStyle, BorderStyle.MEDIUM); setBorder(leftMergeStyle, BorderStyle.MEDIUM);
} }
/** /**
* *
*/ */
@ -238,6 +623,29 @@ public abstract class BaseExcelExporter {
style.setRightBorderColor(IndexedColors.BLACK.getIndex()); style.setRightBorderColor(IndexedColors.BLACK.getIndex());
} }
/**
*
*/
protected void setBorder(CellStyle style, BorderStyle topBorderStyle, BorderStyle bottomBorderStyle,
BorderStyle leftBorderStyle, BorderStyle rightBorderStyle) {
if (topBorderStyle != null) {
style.setBorderTop(topBorderStyle);
style.setTopBorderColor(IndexedColors.BLACK.getIndex());
}
if (bottomBorderStyle != null) {
style.setBorderBottom(bottomBorderStyle);
style.setBottomBorderColor(IndexedColors.BLACK.getIndex());
}
style.setBorderLeft(leftBorderStyle);
style.setBorderRight(rightBorderStyle);
style.setLeftBorderColor(IndexedColors.BLACK.getIndex());
style.setRightBorderColor(IndexedColors.BLACK.getIndex());
}
/** /**
* *
*/ */
@ -457,9 +865,9 @@ public abstract class BaseExcelExporter {
// 1. 先横向合并前两列A列和B列 // 1. 先横向合并前两列A列和B列
// 在数据区域的第一行创建"人工费"单元格 // 在数据区域的第一行创建"人工费"单元格
Row firstDataRow = getOrCreateRow(sheet, dataStartRow); Row firstDataRow = getOrCreateRow(sheet, dataStartRow);
Cell leftMergeCell = firstDataRow.createCell(0); Cell leftMergeCell = firstDataRow.createCell(firstCol);
leftMergeCell.setCellValue(cellValue); leftMergeCell.setCellValue(cellValue);
leftMergeStyle.setWrapText(true);
// 创建左侧合并单元格的样式 // 创建左侧合并单元格的样式
leftMergeCell.setCellStyle(leftMergeStyle); leftMergeCell.setCellStyle(leftMergeStyle);
@ -544,7 +952,7 @@ public abstract class BaseExcelExporter {
/** /**
* *
*/ */
protected int fillEmptyRow(Sheet sheet, int rowNum, int startCol, int endCol, int fillRowNum,String formula, CellStyle style) { protected int fillEmptyRow(Sheet sheet, int rowNum, int startCol, int endCol, int fillRowNum, String formula, CellStyle style) {
while (rowNum < fillRowNum) { while (rowNum < fillRowNum) {
Row dataRow = sheet.getRow(rowNum); Row dataRow = sheet.getRow(rowNum);
if (dataRow == null) { if (dataRow == null) {
@ -676,4 +1084,225 @@ public abstract class BaseExcelExporter {
style.setLeftBorderColor(borderColor); style.setLeftBorderColor(borderColor);
style.setRightBorderColor(borderColor); style.setRightBorderColor(borderColor);
} }
/**
*
* @param sheet
* @param startRow
* @param endRow
* @param startCol
* @param endCol
* @param fullText
* @param boldSegments [start, end]
* @param baseStyle
* @param rowHeight
*/
protected void createMergedCellWithPartialBold(
Sheet sheet,
int startRow, int endRow,
int startCol, int endCol,
String fullText,
List<TextSegment> boldSegments,
CellStyle baseStyle,
float rowHeight) {
// 1. 创建合并区域
CellRangeAddress mergedRegion = new CellRangeAddress(
startRow, endRow, startCol, endCol);
sheet.addMergedRegion(mergedRegion);
// 2. 创建主单元格
Row mainRow = getOrCreateRow(sheet, startRow);
mainRow.setHeightInPoints(rowHeight);
Cell mainCell = mainRow.createCell(startCol);
// 3. 创建富文本
RichTextString richText = workbook.getCreationHelper()
.createRichTextString(fullText);
// 4. 默认使用基础字体的正常版本
Font normalFont = getNormalFont(baseStyle);
richText.applyFont(0, fullText.length(), normalFont);
// 5. 应用加粗段落
for (TextSegment segment : boldSegments) {
if (segment.isValid() && segment.isWithinBounds(fullText.length())) {
Font boldFont = getBoldFont(baseStyle, segment.getColor());
richText.applyFont(segment.getStart(), segment.getEnd(), boldFont);
}
}
mainCell.setCellValue(richText);
// 6. 设置样式(保留原有的边框、对齐等)
if (baseStyle != null) {
mainCell.setCellStyle(baseStyle);
}
// 7. 修复合并单元格的边框
fixMergedCellBorders(sheet, mergedRegion, baseStyle);
}
/**
*
*/
protected void createMergedCellWithBoldKeywords(
Sheet sheet,
int startRow, int endRow,
int startCol, int endCol,
String text,
List<String> keywords,
CellStyle baseStyle,
float rowHeight) {
List<TextSegment> boldSegments = new ArrayList<>();
for (String keyword : keywords) {
int index = 0;
while (index < text.length()) {
int foundIndex = text.indexOf(keyword, index);
if (foundIndex >= 0) {
boldSegments.add(new TextSegment(foundIndex, foundIndex + keyword.length()));
index = foundIndex + keyword.length();
} else {
break;
}
}
}
// 创建合并单元格
createMergedCellWithPartialBold(sheet, startRow, endRow,
startCol, endCol, text, boldSegments, baseStyle,rowHeight);
}
/**
*
*/
private Font getNormalFont(CellStyle style) {
if (style == null) {
return yaheiNormal10Font;
}
Font existingFont = workbook.getFontAt(style.getFontIndex());
Font normalFont = workbook.createFont();
// 复制所有属性,但取消加粗
normalFont.setFontName(existingFont.getFontName());
normalFont.setFontHeightInPoints(existingFont.getFontHeightInPoints());
normalFont.setColor(existingFont.getColor());
normalFont.setItalic(existingFont.getItalic());
normalFont.setUnderline(existingFont.getUnderline());
return normalFont;
}
/**
*
*/
private Font getBoldFont(CellStyle style, Short color) {
if (style == null) {
Font boldFont = workbook.createFont();
boldFont.setBold(true);
boldFont.setFontName("微软雅黑");
boldFont.setFontHeightInPoints((short) 10);
if (color != null) {
boldFont.setColor(color);
}
return boldFont;
}
Font existingFont = workbook.getFontAt(style.getFontIndex());
Font boldFont = workbook.createFont();
// 复制所有属性,并设置为加粗
boldFont.setFontName(existingFont.getFontName());
boldFont.setFontHeightInPoints(existingFont.getFontHeightInPoints());
boldFont.setBold(true);
if (color != null) {
boldFont.setColor(color);
} else {
boldFont.setColor(existingFont.getColor());
}
boldFont.setItalic(existingFont.getItalic());
boldFont.setUnderline(existingFont.getUnderline());
return boldFont;
}
/**
*
*/
protected static class TextSegment {
private int start; // 起始位置(包含)
private int end; // 结束位置(不包含)
private Short color; // 字体颜色
public TextSegment(int start, int end) {
this.start = start;
this.end = end;
}
public TextSegment(int start, int end, Short color) {
this.start = start;
this.end = end;
this.color = color;
}
public boolean isValid() {
return start >= 0 && end > start;
}
public boolean isWithinBounds(int textLength) {
return start < textLength && end <= textLength;
}
// Getters and Setters
public int getStart() { return start; }
public void setStart(int start) { this.start = start; }
public int getEnd() { return end; }
public void setEnd(int end) { this.end = end; }
public Short getColor() { return color; }
public void setColor(Short color) { this.color = color; }
}
/**
*
*/
protected static class FontStyleConfig {
private short fontSize = 10;
private boolean bold = false;
private Short color = null;
private String fontName = "微软雅黑";
private boolean italic = false;
private boolean underline = false;
// Getters and Setters
public short getFontSize() { return fontSize; }
public void setFontSize(short fontSize) { this.fontSize = fontSize; }
public boolean isBold() { return bold; }
public void setBold(boolean bold) { this.bold = bold; }
public Short getColor() { return color; }
public void setColor(Short color) { this.color = color; }
public String getFontName() { return fontName; }
public void setFontName(String fontName) { this.fontName = fontName; }
public boolean isItalic() { return italic; }
public void setItalic(boolean italic) { this.italic = italic; }
public boolean isUnderline() { return underline; }
public void setUnderline(boolean underline) { this.underline = underline; }
}
} }

@ -0,0 +1,309 @@
package org.dromara.oa.excel;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFFont;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
/**
* @Author xins
* @Date 2025/12/29 15:25
* @Description:Excel
* *
*/
public class ExcelStyleBuilder {
private final Workbook workbook;
private final Map<String, CellStyle> styleCache = new HashMap<>();
// 常用字体名称
private static final String FONT_SONG = "宋体";
private static final String FONT_YA_HEI = "微软雅黑";
public ExcelStyleBuilder(Workbook workbook) {
this.workbook = workbook;
}
/**
*
*/
public StyleBuilder base() {
return new StyleBuilder();
}
/**
*
*/
public CellStyle getStyle(String key) {
return styleCache.get(key);
}
/**
*
*/
public CellStyle getOrCreate(String key, Consumer<StyleBuilder> styleConfig) {
if (styleCache.containsKey(key)) {
return styleCache.get(key);
}
StyleBuilder builder = new StyleBuilder();
styleConfig.accept(builder);
CellStyle style = builder.build();
styleCache.put(key, style);
return style;
}
/**
*
*/
public Font createFont(Consumer<Font> fontConfig) {
Font font = workbook.createFont();
fontConfig.accept(font);
return font;
}
/**
*
*/
public class StyleBuilder {
private CellStyle style;
private Font font;
public StyleBuilder() {
this.style = workbook.createCellStyle();
}
/**
*
*/
public StyleBuilder font(String fontName, short fontSize, boolean bold) {
this.font = createFont(f -> {
f.setFontName(fontName);
f.setFontHeightInPoints(fontSize);
f.setBold(bold);
});
style.setFont(font);
return this;
}
/**
*
*/
public StyleBuilder song(short fontSize, boolean bold) {
return font(FONT_SONG, fontSize, bold);
}
/**
*
*/
public StyleBuilder yaHei(short fontSize, boolean bold) {
return font(FONT_YA_HEI, fontSize, bold);
}
/**
*
*/
public StyleBuilder fontColor(IndexedColors color) {
if (font == null) {
yaHei((short) 10, false);
}
font.setColor(color.getIndex());
return this;
}
/**
*
*/
public StyleBuilder bgColor(IndexedColors color) {
style.setFillForegroundColor(color.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
return this;
}
/**
*
*/
public StyleBuilder align(HorizontalAlignment horizontal, VerticalAlignment vertical) {
style.setAlignment(horizontal);
style.setVerticalAlignment(vertical);
return this;
}
/**
*
*/
public StyleBuilder center() {
return align(HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
/**
*
*/
public StyleBuilder left() {
return align(HorizontalAlignment.LEFT, VerticalAlignment.CENTER);
}
/**
*
*/
public StyleBuilder right() {
return align(HorizontalAlignment.RIGHT, VerticalAlignment.CENTER);
}
/**
*
*/
public StyleBuilder border(BorderStyle borderStyle) {
style.setBorderTop(borderStyle);
style.setBorderBottom(borderStyle);
style.setBorderLeft(borderStyle);
style.setBorderRight(borderStyle);
short color = IndexedColors.BLACK.getIndex();
style.setTopBorderColor(color);
style.setBottomBorderColor(color);
style.setLeftBorderColor(color);
style.setRightBorderColor(color);
return this;
}
/**
*
*/
public StyleBuilder border(BorderStyle top, BorderStyle bottom,
BorderStyle left, BorderStyle right) {
style.setBorderTop(top);
style.setBorderBottom(bottom);
style.setBorderLeft(left);
style.setBorderRight(right);
short color = IndexedColors.BLACK.getIndex();
style.setTopBorderColor(color);
style.setBottomBorderColor(color);
style.setLeftBorderColor(color);
style.setRightBorderColor(color);
return this;
}
/**
*
*/
public StyleBuilder bottomBorder(BorderStyle borderStyle) {
style.setBorderBottom(borderStyle);
style.setBottomBorderColor(IndexedColors.BLACK.getIndex());
return this;
}
/**
*
*/
public StyleBuilder dataFormat(String format) {
DataFormat dataFormat = workbook.createDataFormat();
style.setDataFormat(dataFormat.getFormat(format));
return this;
}
/**
*
*/
public StyleBuilder numberFormat() {
return dataFormat("0.00");
}
/**
*
*/
public StyleBuilder percentFormat() {
return dataFormat("0.00%");
}
/**
*
*/
public StyleBuilder moneyFormat() {
return dataFormat("#,##0.00");
}
/**
*
*/
public StyleBuilder wrapText(boolean wrap) {
style.setWrapText(wrap);
return this;
}
/**
*
*/
public StyleBuilder clone(CellStyle source, Consumer<StyleBuilder> customizer) {
this.style.cloneStyleFrom(source);
customizer.accept(this);
return this;
}
/**
*
*/
public CellStyle build() {
return style;
}
/**
*
*/
public CellStyle buildAndCache(String key) {
CellStyle builtStyle = build();
styleCache.put(key, builtStyle);
return builtStyle;
}
}
/**
*
*/
public static class Keys {
// 标题样式
public static final String TITLE = "title";
public static final String SUBTITLE = "subtitle";
// 表单样式
public static final String FORM_LEFT = "form_left";
public static final String FORM_RIGHT = "form_right";
public static final String FORM_FORMULA = "form_formula";
public static final String FORM_PERCENT = "form_percent";
// 表头样式
public static final String HEADER = "header";
public static final String MERGED_HEADER = "merged_header";
// 数据样式
public static final String DATA = "data";
public static final String DATA_LEFT = "data_left";
public static final String DATA_RIGHT = "data_right";
public static final String DATA_BOTTOM = "data_bottom";
public static final String DATA_CENTER = "data_center";
// 特殊格式
public static final String PERCENT = "percent";
public static final String FORMULA = "formula";
public static final String MONEY = "money";
public static final String FOOTER_FORMULA = "footer_formula";
// 备注样式
public static final String REMARK = "remark";
// 合并单元格样式
public static final String LEFT_MERGE = "left_merge";
// 研发预算相关
public static final String RD_COVER_TITLE = "rd_cover_title";
public static final String RD_COVER_SUBTITLE = "rd_cover_subtitle";
public static final String RD_HEADER = "rd_header";
public static final String RD_DATA = "rd_data";
public static final String RD_DATA_BOLD = "rd_data_bold";
public static final String RD_DATA_RED = "rd_data_red";
}
}

@ -676,7 +676,7 @@ public class MarketProjectBudgetExcelExporter extends BaseExcelExporter {
Double reducePeopleNumber = detail.getReducePeopleNumber() != null ? detail.getReducePeopleNumber().doubleValue() : null; Double reducePeopleNumber = detail.getReducePeopleNumber() != null ? detail.getReducePeopleNumber().doubleValue() : null;
Double reduceCumulativeTime = detail.getReduceCumulativeTime() != null ? detail.getReduceCumulativeTime().doubleValue() : null; Double reduceCumulativeTime = detail.getReduceCumulativeTime() != null ? detail.getReduceCumulativeTime().doubleValue() : null;
Double reduceArtificialStandard = detail.getReduceArtificialStandard() != null ? detail.getReduceArtificialStandard().doubleValue() : null; Double reduceArtificialStandard = detail.getReduceArtificialStandard() != null ? detail.getReduceArtificialStandard().doubleValue() : null;
createCell(dataRow, 8, detail.getPersonnelCategory(), dataStyle); createCell(dataRow, 8, detail.getReducePersonnelCategory(), dataStyle);
createNumericCell(dataRow, 9, reducePeopleNumber, dataStyle); createNumericCell(dataRow, 9, reducePeopleNumber, dataStyle);
createNumericCell(dataRow, 10, reduceCumulativeTime, dataStyle); createNumericCell(dataRow, 10, reduceCumulativeTime, dataStyle);
createNumericCell(dataRow, 11, detail.getReduceMonthRate() == null ? BigDecimal.ZERO : createNumericCell(dataRow, 11, detail.getReduceMonthRate() == null ? BigDecimal.ZERO :
@ -860,7 +860,7 @@ public class MarketProjectBudgetExcelExporter extends BaseExcelExporter {
Double reducePeopleNumber = detail.getReducePeopleNumber() != null ? detail.getReducePeopleNumber().doubleValue() : null; Double reducePeopleNumber = detail.getReducePeopleNumber() != null ? detail.getReducePeopleNumber().doubleValue() : null;
Double reduceCumulativeTime = detail.getReduceCumulativeTime() != null ? detail.getReduceCumulativeTime().doubleValue() : null; Double reduceCumulativeTime = detail.getReduceCumulativeTime() != null ? detail.getReduceCumulativeTime().doubleValue() : null;
Double reduceArtificialStandard = detail.getReduceArtificialStandard() != null ? detail.getReduceArtificialStandard().doubleValue() : null; Double reduceArtificialStandard = detail.getReduceArtificialStandard() != null ? detail.getReduceArtificialStandard().doubleValue() : null;
createCell(dataRow, 8, detail.getPersonnelCategory(), dataStyle); createCell(dataRow, 8, detail.getReducePersonnelCategory(), dataStyle);
createNumericCell(dataRow, 9, reducePeopleNumber, dataStyle); createNumericCell(dataRow, 9, reducePeopleNumber, dataStyle);
createNumericCell(dataRow, 10, reduceCumulativeTime, dataStyle); createNumericCell(dataRow, 10, reduceCumulativeTime, dataStyle);
createNumericCell(dataRow, 11, detail.getReduceMonthRate() == null ? BigDecimal.ZERO : createNumericCell(dataRow, 11, detail.getReduceMonthRate() == null ? BigDecimal.ZERO :

@ -0,0 +1,55 @@
package org.dromara.oa.excel;
/**
* @Author xins
* @Date 2025/12/30 17:21
* @Description:
*/
public enum RdBudgetItemEnums {
MATERIAL("材料费", "表2-材料费", "表2-材料费", "H"),
LABOR("人工费", "表3-人工费", "表3-人工费", "H"),
TRAVEL("差旅费", "表4-差旅费", "表4-差旅费", "M"),
TESTING("测试化验加工费", "表5-测试化验加工费","表5-测试化验加工费", "H"),
CONSULTATION("专家咨询费用", "表6-咨询费、设计费","专家费", "I"),
DESIGN("新产品设计费", "表6-咨询费、设计费", "新产品设计费", "H"),
OTHER("其他费用", "表7-其他费用", "表7-其他费用","D");
private final String name;
private final String sheetName;
private final String childSheetName;
private final String sheetPosition;
RdBudgetItemEnums(String name, String sheetName, String childSheetName, String sheetPosition) {
this.name = name;
this.sheetName = sheetName;
this.childSheetName = childSheetName;
this.sheetPosition = sheetPosition;
}
// 获取所有项目名称
public static String[] getAllItemNames() {
RdBudgetItemEnums[] items = values();
String[] names = new String[items.length];
for (int i = 0; i < items.length; i++) {
names[i] = items[i].getName();
}
return names;
}
// getter 方法
public String getName() {
return name;
}
public String getSheetName() {
return sheetName;
}
public String getChildSheetName() {
return childSheetName;
}
public String getSheetPosition() {
return sheetPosition;
}
}

@ -40,7 +40,9 @@
d.dept_name deptName, d.dept_name deptName,
u1.nick_name managerName, u1.nick_name managerName,
u2.nick_name chargeName, u2.nick_name chargeName,
u3.nick_name deputyName u3.nick_name deputyName,
c.contract_code contractCode,
c.contract_name contractName
from erp_project_info t from erp_project_info t
left join erp_project_type t1 on t1.project_type_id = t.project_type_id left join erp_project_type t1 on t1.project_type_id = t.project_type_id
left join erp_project_type t2 on t1.parent_id = t2.project_type_id left join erp_project_type t2 on t1.parent_id = t2.project_type_id
@ -48,6 +50,7 @@
left join sys_user u1 on u1.user_id = t.manager_id left join sys_user u1 on u1.user_id = t.manager_id
left join sys_user u2 on u2.user_id = t.charge_id left join sys_user u2 on u2.user_id = t.charge_id
left join sys_user u3 on u3.user_id = t.deputy_id left join sys_user u3 on u3.user_id = t.deputy_id
left join erp_contract_info c on t.contract_id = c.contract_id
${ew.getCustomSqlSegment} ${ew.getCustomSqlSegment}
</select> </select>

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.oa.erp.mapper.ErpTimesheetReportMapper">
<select id="selectProjectManHourList" resultType="org.dromara.oa.erp.domain.vo.ProjectManHourReportVo">
SELECT
t_dept.dept_name,
u.nick_name AS manager_name,
p.project_id,
p.project_name,
p.project_code,
p.project_category,
COALESCE(SUM(tp.hours), 0) AS total_hours,
<!-- 获取该项目在全公司的总工时(用于判断是否存在跨部门) -->
COALESCE(global_stats.total_hours, 0) AS cross_dept_hours
FROM
erp_project_info p
LEFT JOIN sys_user u ON p.manager_id = u.user_id
<!-- 关联工时明细 -->
LEFT JOIN erp_timesheet_project tp ON p.project_id = tp.project_id AND tp.del_flag = '0'
<!-- 关联工时主表以获取日期和人员(从而获取填报部门) -->
LEFT JOIN erp_timesheet_info ti ON tp.timesheet_id = ti.timesheet_id AND ti.del_flag = '0'
<!-- 关联填报部门 -->
LEFT JOIN sys_dept t_dept ON ti.dept_id = t_dept.dept_id
<!-- 关联项目所属部门,用于部门筛选 -->
LEFT JOIN sys_dept p_dept ON p.dept_id = p_dept.dept_id
<!-- 关联全局统计 (独立子查询,不受主查询部门条件影响) -->
LEFT JOIN (
SELECT
tp2.project_id,
SUM(tp2.hours) AS total_hours
FROM erp_timesheet_project tp2
JOIN erp_timesheet_info ti2 ON tp2.timesheet_id = ti2.timesheet_id AND ti2.del_flag = '0'
WHERE tp2.del_flag = '0'
AND ti2.timesheet_status = '3'
<if test="startTime != null and startTime != ''">
AND ti2.start_time >= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND ti2.end_time &lt;= #{endTime}
</if>
GROUP BY tp2.project_id
) global_stats ON p.project_id = global_stats.project_id
<where>
p.del_flag = '0'
<if test="bo.projectName != null and bo.projectName != ''">
AND p.project_name LIKE concat('%', #{bo.projectName}, '%')
</if>
<if test="bo.projectCode != null and bo.projectCode != ''">
AND p.project_code LIKE concat('%', #{bo.projectCode}, '%')
</if>
<if test="bo.projectCategory != null and bo.projectCategory != ''">
AND p.project_category = #{bo.projectCategory}
</if>
<if test="bo.deptName != null and bo.deptName != ''">
AND t_dept.dept_name LIKE concat('%', #{bo.deptName}, '%')
</if>
<!-- 日期范围筛选 -->
<if test="startTime != null and startTime != ''">
AND ti.start_time >= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND ti.end_time &lt;= #{endTime}
</if>
<!-- 仅统计已审批的工时 (3=已审批) -->
AND ti.timesheet_status = '3'
</where>
<!-- 按项目ID 和 填报部门ID 分组 -->
GROUP BY p.project_id, ti.dept_id, global_stats.total_hours
<!-- 仅显示有工时的记录 -->
HAVING SUM(tp.hours) > 0
ORDER BY p.project_code, t_dept.dept_id
</select>
<!-- 查询项目-人员工时分布 -->
<select id="selectProjectPersonnelHours" resultType="java.util.Map">
SELECT
p.project_id AS projectId,
p.dept_id AS projectDeptId,
p.project_name AS projectName,
p.project_code AS projectCode,
u.user_id AS userId,
u.nick_name AS nickName,
COALESCE(SUM(tp.hours), 0) AS hours
FROM erp_timesheet_project tp
JOIN erp_timesheet_info ti ON tp.timesheet_id = ti.timesheet_id AND ti.del_flag = '0'
JOIN erp_project_info p ON tp.project_id = p.project_id AND p.del_flag = '0'
JOIN sys_user u ON ti.user_id = u.user_id AND u.del_flag = '0'
WHERE ti.timesheet_status = '3'
AND tp.del_flag = '0'
<if test="deptId != null">
AND ti.dept_id = #{deptId}
</if>
<if test="startTime != null and startTime != ''">
AND ti.start_time >= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND ti.end_time &lt;= #{endTime}
</if>
GROUP BY p.project_id, u.user_id
</select>
<!-- 查询项目在全公司的总工时(用于跨部门判断) -->
<select id="selectProjectTotalHours" resultType="java.util.Map">
SELECT
tp.project_id AS projectId,
COALESCE(SUM(tp.hours), 0) AS totalHours
FROM erp_timesheet_project tp
JOIN erp_timesheet_info ti ON tp.timesheet_id = ti.timesheet_id AND ti.del_flag = '0'
WHERE ti.timesheet_status = '3'
AND tp.del_flag = '0'
<if test="startTime != null and startTime != ''">
AND ti.start_time >= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND ti.end_time &lt;= #{endTime}
</if>
<!-- 仅统计涉及到的项目 -->
AND tp.project_id IN
(
SELECT DISTINCT tpp.project_id
FROM erp_timesheet_project tpp
JOIN erp_timesheet_info tii ON tpp.timesheet_id = tii.timesheet_id
WHERE tpp.del_flag = '0'
AND tii.dept_id = #{deptId}
AND tii.timesheet_status = '3'
<if test="startTime != null and startTime != ''">
AND tii.start_time >= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND tii.end_time &lt;= #{endTime}
</if>
)
GROUP BY tp.project_id
</select>
<!-- 查询部门的所有人员 -->
<select id="selectDeptUsers" resultType="java.util.Map">
SELECT
u.user_id AS userId,
u.nick_name AS nickName,
d.dept_name AS deptName
FROM sys_user u
LEFT JOIN sys_dept d ON u.dept_id = d.dept_id
WHERE u.dept_id = #{deptId}
AND u.del_flag = '0'
AND u.status = '0'
ORDER BY u.user_id
</select>
</mapper>
Loading…
Cancel
Save