QuYueTing 1 день назад
Родитель
Сommit
ff647969d3

+ 5 - 0
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/ProjectController.java

@@ -606,6 +606,11 @@ public class ProjectController {
         return projectService.getGanttDataNew(type, startDate, endDate, userId, projectId, groupName,taskType, request);
     }
 
+    @RequestMapping("/exportGanttDataNew")
+    public HttpRespMsg exportGanttDataNew(@RequestParam(required = false, defaultValue = "0") Integer type, String startDate, String endDate, String userId, Integer projectId, String groupName,Integer taskType, HttpServletRequest request) {
+        return projectService.exportGanttDataNew(type, startDate, endDate, userId, projectId, groupName,taskType, request);
+    }
+
     /**
      *
      * @return

+ 2 - 0
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/ProjectService.java

@@ -354,4 +354,6 @@ public interface ProjectService extends IService<Project> {
     HttpRespMsg getTimeCostForTableByDept(String startDate, String endDate, Integer projectId, Integer departmentId, HttpServletRequest request);
 
     HttpRespMsg exportTimeCostForTableByDept(String startDate, String endDate, Integer projectId, Integer departmentId, HttpServletRequest request);
+
+    HttpRespMsg exportGanttDataNew(Integer type, String startDate, String endDate, String userId, Integer projectId, String groupName, Integer taskType, HttpServletRequest request);
 }

+ 182 - 68
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/impl/ProjectServiceImpl.java

@@ -1447,34 +1447,6 @@ public class ProjectServiceImpl extends ServiceImpl<ProjectMapper, Project> impl
             } else {
                 queryWrapper = new QueryWrapper<Project>().eq("company_id", companyId);
             }
-            if (!StringUtils.isEmpty(keyword)) {
-                if (searchField == 1) {
-                    //按照项目名称模糊匹配
-                    queryWrapper.like("project_name", keyword);
-                } else if (searchField == 2){
-                    queryWrapper.like("project_code", keyword);
-                }
-            }
-            if(!StringUtils.isEmpty(inchagerId)){
-                queryWrapper.eq("incharger_id",inchagerId);
-            }
-            if(!StringUtils.isEmpty(participation)){
-                List<Participation> participationList = participationMapper.selectList(new QueryWrapper<Participation>().eq("user_id", participation).select("project_id"));
-                List<Integer> collect;
-                if(participationList.size()>0){
-                    collect = participationList.stream().map(Participation::getProjectId).collect(Collectors.toList());
-                }else {
-                    collect=new ArrayList<>();
-                    collect.add(-1);
-                }
-                queryWrapper.in("id",collect);
-            }
-            if (status != null && status != 0) {
-                queryWrapper.eq("status", status);
-            }
-            if (category != null) {
-                queryWrapper.eq("category", category);
-            }
             if (projectMainId != null) {
                 queryWrapper.eq("project_main_id", projectMainId);
             }
@@ -2047,46 +2019,6 @@ public class ProjectServiceImpl extends ServiceImpl<ProjectMapper, Project> impl
                                     httpRespMsg.setError("项目人天不能小于已经填报的工时:"+df.format(report.getWorkingTime()/timeType.getAllday())+"人天");
                                     return httpRespMsg;
                                 }
-                            } else {
-                                //同步的外部项目
-                                if(manDay!=null && manDay > 0){
-                                    if(manDay*timeType.getAllday()<report.getWorkingTime()){
-                                        httpRespMsg.setError("项目人天不能小于已经填报的工时:"+df.format(report.getWorkingTime()/timeType.getAllday())+"人天");
-                                        return httpRespMsg;
-                                    }
-                                }
-                            }
-                        } else if (estimateTimeSetting.getProjectManDayFillMode() == 2) {
-                            //都必填
-                            if(manDay!=null && manDay > 0){
-                                if(manDay*timeType.getAllday()<report.getWorkingTime()){
-                                    httpRespMsg.setError("项目人天不能小于已经填报的工时:"+df.format(report.getWorkingTime()/timeType.getAllday())+"人天");
-                                    return httpRespMsg;
-                                }
-                            } else {
-                                httpRespMsg.setError("项目人天不能小于已经填报的工时:"+df.format(report.getWorkingTime()/timeType.getAllday())+"人天");
-                                return httpRespMsg;
-                            }
-                        } else if (estimateTimeSetting.getProjectManDayFillMode() == 3) {
-                            //外部项目必填
-                            if (oldProject.getFromOutside() == 1) {
-                                if(manDay!=null && manDay > 0){
-                                    if(manDay*timeType.getAllday()<report.getWorkingTime()){
-                                        httpRespMsg.setError("项目人天不能小于已经填报的工时:"+df.format(report.getWorkingTime()/timeType.getAllday())+"人天");
-                                        return httpRespMsg;
-                                    }
-                                } else {
-                                    httpRespMsg.setError("项目人天不能小于已经填报的工时:"+df.format(report.getWorkingTime()/timeType.getAllday())+"人天");
-                                    return httpRespMsg;
-                                }
-                            } else {
-                                //工时系统创建的项目,只检查不能少于已填报
-                                if(manDay!=null && manDay > 0){
-                                    if(manDay*timeType.getAllday()<report.getWorkingTime()){
-                                        httpRespMsg.setError("项目人天不能小于已经填报的工时:"+df.format(report.getWorkingTime()/timeType.getAllday())+"人天");
-                                        return httpRespMsg;
-                                    }
-                                }
                             }
                         }
                     }
@@ -3744,6 +3676,11 @@ public class ProjectServiceImpl extends ServiceImpl<ProjectMapper, Project> impl
                 stringBuilder.append(userMapper.selectById(uid).getName()).append(",");
             }
         }
+        if (stringBuilder.length() == 0) {
+            HttpRespMsg msg = new HttpRespMsg();
+            msg.setError("选中的参与人已存在");
+            return msg;
+        }
         //生成操作记录
         User user = userMapper.selectById(request.getHeader("token"));
         Project project = getById(id);
@@ -16759,4 +16696,181 @@ public class ProjectServiceImpl extends ServiceImpl<ProjectMapper, Project> impl
         }
         return msg;
     }
+
+    @Override
+    public HttpRespMsg exportGanttDataNew(Integer type, String startDate, String endDate, String targetUserId, Integer targetProjectId, String groupName, Integer taskType, HttpServletRequest request) {
+        HttpRespMsg httpRespMsg = new HttpRespMsg();
+        try {
+            String token = request.getHeader("TOKEN");
+            User user = userMapper.selectById(token);
+            Integer companyId = user.getCompanyId();
+            WxCorpInfo wxCorpInfo = wxCorpInfoMapper.selectOne(new QueryWrapper<WxCorpInfo>().eq("company_id", companyId));
+            CompanyDingding dingding = companyDingdingMapper.selectOne(new LambdaQueryWrapper<CompanyDingding>().eq(CompanyDingding::getCompanyId, companyId));
+
+            // 权限控制逻辑(与getGanttDataNew保持一致)
+            List<String> userIds = new ArrayList<>();
+            List<SysRichFunction> functionList = sysFunctionMapper.getRoleFunctions(user.getRoleId(), "查看全部项目");
+            if (functionList.size() == 0) {
+                if (user.getManageDeptId() != null && user.getManageDeptId() != 0) {
+                    List<User> userList = userMapper.selectList(new QueryWrapper<User>().eq("department_id", user.getManageDeptId()));
+                    userIds = userList.stream().map(User::getId).collect(Collectors.toList());
+                } else {
+                    List<Project> projectList = projectMapper.selectList(new QueryWrapper<Project>().eq("incharger_id", user.getId()));
+                    if (projectList.size() > 0) {
+                        List<Integer> collect = projectList.stream().map(Project::getId).collect(Collectors.toList());
+                        List<Participation> participationList = participationMapper.selectList(new QueryWrapper<Participation>().in("project_id", collect));
+                        userIds = participationList.stream().map(Participation::getUserId).collect(Collectors.toList());
+                    }
+                    if (!userIds.contains(user.getId())) {
+                        userIds.add(user.getId());
+                    }
+                }
+            } else {
+                List<User> userList = userMapper.selectList(new QueryWrapper<User>().eq("company_id", companyId));
+                userIds = userList.stream().map(User::getId).collect(Collectors.toList());
+            }
+            if (!StringUtils.isEmpty(targetUserId)) {
+                userIds = userIds.stream().filter(u -> u.equals(targetUserId)).collect(Collectors.toList());
+            }
+
+            List<Integer> projectIds = null;
+            if (targetProjectId != null) {
+                projectIds = new ArrayList<>();
+                projectIds.add(targetProjectId);
+            } else {
+                if (functionList.size() == 0) {
+                    if (userIds.isEmpty()) {
+                        userIds.add("-1");
+                    }
+                    List<Participation> participationList = participationMapper.selectList(new QueryWrapper<Participation>().in("user_id", userIds));
+                    projectIds = participationList.stream().map(Participation::getProjectId).collect(Collectors.toList());
+                } else {
+                    projectIds = null;
+                }
+            }
+
+            // 处理groupName过滤
+            if (!StringUtils.isEmpty(groupName) && projectIds == null) {
+                List<Project> allProjects = projectMapper.selectList(new QueryWrapper<Project>().eq("company_id", companyId));
+                List<Integer> allPids = allProjects.stream().map(Project::getId).collect(Collectors.toList());
+                List<TaskGroup> taskGroups = taskGroupMapper.selectList(new QueryWrapper<TaskGroup>().eq("name", groupName).in("project_id", allPids));
+                projectIds = taskGroups.stream().map(TaskGroup::getProjectId).collect(Collectors.toList());
+                if (projectIds.isEmpty()) projectIds.add(-1);
+            } else if (!StringUtils.isEmpty(groupName) && projectIds != null) {
+                List<TaskGroup> taskGroups = taskGroupMapper.selectList(new QueryWrapper<TaskGroup>().eq("name", groupName).in("project_id", projectIds));
+                projectIds = taskGroups.stream().map(TaskGroup::getProjectId).collect(Collectors.toList());
+                if (projectIds.isEmpty()) projectIds.add(-1);
+            }
+
+            // 处理taskType过滤
+            if (taskType != null) {
+                List<Project> allProjects = projectMapper.selectList(new QueryWrapper<Project>().eq("company_id", companyId));
+                List<Integer> allPids = allProjects.stream().map(Project::getId).collect(Collectors.toList());
+                List<Task> tasks = taskMapper.selectList(new QueryWrapper<Task>().eq("task_type", taskType).in("project_id", allPids));
+                List<Integer> filterPids = tasks.stream().map(Task::getProjectId).collect(Collectors.toList());
+                if (projectIds == null) {
+                    projectIds = filterPids;
+                    if (projectIds.isEmpty()) projectIds.add(-1);
+                }
+            }
+
+            // 查询甘特图数据
+            List<Map> ganttData = new ArrayList<>();
+            if (!userIds.isEmpty()) {
+                if (type == 0) {
+                    // 按人员视图
+                    ganttData = projectMapper.getTaskPlanByMemb(userIds, startDate, endDate, companyId, user.getId(), null);
+                } else {
+                    // 按项目视图
+                    ganttData = projectMapper.getTaskPlanByProject(projectIds, startDate, endDate, companyId, groupName, taskType, user.getId(), null);
+                }
+            }
+
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+            List<List<String>> exportList = new ArrayList<>();
+
+            if (type == 0) {
+                // 按人员导出:序号、人员姓名、项目名称、任务名称、开始时间、结束时间、计划工时(h)
+                List<String> header = new ArrayList<>();
+                header.add("序号");
+                header.add("人员姓名");
+                header.add("项目名称");
+                header.add("任务名称");
+                header.add("开始时间");
+                header.add("结束时间");
+                header.add("计划工时(h)");
+                exportList.add(header);
+
+                int seq = 1;
+                for (Map map : ganttData) {
+                    List<String> row = new ArrayList<>();
+                    row.add(String.valueOf(seq++));
+                    String userName = map.get("name") == null ? "" : map.get("name").toString();
+                    if (wxCorpInfo != null && wxCorpInfo.getSaasSyncContact() == 1) {
+                        row.add("$userName=" + (map.get("user_id") == null ? "" : map.get("user_id").toString()) + "$");
+                    } else if (dingding != null && dingding.getContactNeedTranslate() == 1) {
+                        row.add("$userName=" + userName + "$");
+                    } else {
+                        row.add(userName);
+                    }
+                    row.add(map.get("project_name") == null ? "" : map.get("project_name").toString());
+                    row.add(map.get("task_name") == null ? "" : map.get("task_name").toString());
+                    row.add(map.get("start_date") == null ? "" : sdf.format((java.util.Date) map.get("start_date")));
+                    row.add(map.get("end_date") == null ? "" : sdf.format((java.util.Date) map.get("end_date")));
+                    row.add(map.get("duration") == null ? "" : map.get("duration").toString());
+                    exportList.add(row);
+                }
+            } else {
+                // 按项目导出甘特图:序号、项目、开始时间、结束时间 + 日期列(每天一列,用色块显示项目时间范围)
+                // 收集每个项目的唯一记录(去重,按项目id)
+                List<Map<String, String>> projectRows = new ArrayList<>();
+                Set<String> seenProjectIds = new LinkedHashSet<>();
+                for (Map map : ganttData) {
+                    String curProjectId = map.get("id") == null ? "" : map.get("id").toString();
+                    if (seenProjectIds.add(curProjectId)) {
+                        // 计算该项目的最早开始时间和最晚结束时间
+                        List<Map> projectTasks = ganttData.stream()
+                                .filter(m -> m.get("id") != null && m.get("id").toString().equals(curProjectId))
+                                .collect(Collectors.toList());
+                        String projStart = projectTasks.stream()
+                                .filter(m -> m.get("start_date") != null)
+                                .map(m -> sdf.format((java.util.Date) m.get("start_date")))
+                                .min(String::compareTo).orElse("");
+                        String projEnd = projectTasks.stream()
+                                .filter(m -> m.get("end_date") != null)
+                                .map(m -> sdf.format((java.util.Date) m.get("end_date")))
+                                .max(String::compareTo).orElse("");
+                        // 计算该项目的计划工时总和
+                        double totalPlanHours = projectTasks.stream()
+                                .mapToDouble(m -> m.get("duration") == null ? 0 : Double.parseDouble(m.get("duration").toString()))
+                                .sum();
+                        Map<String, String> projectRow = new java.util.LinkedHashMap<>();
+                        projectRow.put("projectName", map.get("project_name") == null ? "" : map.get("project_name").toString());
+                        projectRow.put("startDate", projStart);
+                        projectRow.put("endDate", projEnd);
+                        projectRow.put("planHours", totalPlanHours > 0 ? String.valueOf(totalPlanHours) : "");
+                        projectRows.add(projectRow);
+                    }
+                }
+                // 如果没有数据,也要保证startDate/endDate有值
+                String rangeStart = startDate != null ? startDate : java.time.LocalDate.now().toString();
+                String rangeEnd = endDate != null ? endDate : java.time.LocalDate.now().toString();
+                String fileName = "甘特图导出_" + System.currentTimeMillis();
+                String fileUrl = ExcelUtil.exportGanttChartByProject(fileName, projectRows, rangeStart, rangeEnd, path);
+                httpRespMsg.data = fileUrl;
+                return httpRespMsg;
+            }
+
+            String fileName = "甘特图导出_" + System.currentTimeMillis();
+            return excelExportService.exportGeneralExcelByTitleAndList(wxCorpInfo, dingding, fileName, exportList, path);
+        } catch (NullPointerException e) {
+            e.printStackTrace();
+            httpRespMsg.setError(MessageUtils.message("access.verificationError"));
+            return httpRespMsg;
+        } catch (Exception e) {
+            e.printStackTrace();
+            httpRespMsg.setError(e.getMessage());
+            return httpRespMsg;
+        }
+    }
 }

+ 654 - 0
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/util/ExcelUtil.java

@@ -2001,4 +2001,658 @@ public class ExcelUtil {
         }
         return "/upload/"+fileName;
     }
+
+    /**
+     * 导出按项目的甘特图Excel,带有水平色块显示项目时间范围
+     *
+     * @param projectDataList 项目数据列表,每个Map包含:
+     *                        projectName(项目名称), startDate(开始日期yyyy-MM-dd), endDate(结束日期yyyy-MM-dd)
+     * @param rangeStart      导出日期范围开始(yyyy-MM-dd)
+     * @param rangeEnd        导出日期范围结束(yyyy-MM-dd)
+     * @param path            文件保存路径
+     * @param fileName        文件名(不含扩展名)
+     * @return 文件相对路径
+     */
+    public static String exportGanttChartByProject(List<Map<String, Object>> projectDataList,
+                                             String rangeStart, String rangeEnd,
+                                             String path, String fileName) {
+        String result = "";
+        try {
+            java.time.LocalDate startLocalDate = java.time.LocalDate.parse(rangeStart);
+            java.time.LocalDate endLocalDate = java.time.LocalDate.parse(rangeEnd);
+
+            // 计算日期范围内的所有日期
+            List<java.time.LocalDate> dateList = new ArrayList<>();
+            java.time.LocalDate cur = startLocalDate;
+            while (!cur.isAfter(endLocalDate)) {
+                dateList.add(cur);
+                cur = cur.plusDays(1);
+            }
+
+            org.apache.poi.xssf.usermodel.XSSFWorkbook workbook = new org.apache.poi.xssf.usermodel.XSSFWorkbook();
+            org.apache.poi.xssf.usermodel.XSSFSheet sheet = workbook.createSheet("甘特图");
+
+            // 设置列宽
+            sheet.setColumnWidth(0, 1800);  // 序号
+            sheet.setColumnWidth(1, 6000);  // 项目名称
+            sheet.setColumnWidth(2, 3200);  // 开始时间
+            sheet.setColumnWidth(3, 3200);  // 结束时间
+            for (int i = 0; i < dateList.size(); i++) {
+                sheet.setColumnWidth(4 + i, 1000); // 每天一列,较窄
+            }
+
+            // 创建样式:表头样式
+            org.apache.poi.xssf.usermodel.XSSFCellStyle headerStyle = workbook.createCellStyle();
+            headerStyle.setFillForegroundColor(new XSSFColor(new byte[]{(byte)68, (byte)114, (byte)196}, null));
+            headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+            org.apache.poi.xssf.usermodel.XSSFFont headerFont = workbook.createFont();
+            headerFont.setColor(new XSSFColor(new byte[]{(byte)255, (byte)255, (byte)255}, null));
+            headerFont.setBold(true);
+            headerStyle.setFont(headerFont);
+            headerStyle.setAlignment(HorizontalAlignment.CENTER);
+            headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+            headerStyle.setBorderBottom(BorderStyle.THIN);
+            headerStyle.setBorderLeft(BorderStyle.THIN);
+            headerStyle.setBorderRight(BorderStyle.THIN);
+            headerStyle.setBorderTop(BorderStyle.THIN);
+
+            // 创建样式:普通单元格样式
+            org.apache.poi.xssf.usermodel.XSSFCellStyle normalStyle = workbook.createCellStyle();
+            normalStyle.setAlignment(HorizontalAlignment.CENTER);
+            normalStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+            normalStyle.setBorderBottom(BorderStyle.THIN);
+            normalStyle.setBorderLeft(BorderStyle.THIN);
+            normalStyle.setBorderRight(BorderStyle.THIN);
+            normalStyle.setBorderTop(BorderStyle.THIN);
+
+            // 创建样式:甘特色块样式(蓝色)
+            org.apache.poi.xssf.usermodel.XSSFCellStyle ganttStyle = workbook.createCellStyle();
+            ganttStyle.setFillForegroundColor(new XSSFColor(new byte[]{(byte)68, (byte)114, (byte)196}, null));
+            ganttStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+            ganttStyle.setBorderBottom(BorderStyle.THIN);
+            ganttStyle.setBorderLeft(BorderStyle.THIN);
+            ganttStyle.setBorderRight(BorderStyle.THIN);
+            ganttStyle.setBorderTop(BorderStyle.THIN);
+
+            // 创建样式:空白日期单元格(浅灰色背景)
+            org.apache.poi.xssf.usermodel.XSSFCellStyle emptyDateStyle = workbook.createCellStyle();
+            emptyDateStyle.setFillForegroundColor(new XSSFColor(new byte[]{(byte)242, (byte)242, (byte)242}, null));
+            emptyDateStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+            emptyDateStyle.setBorderBottom(BorderStyle.THIN);
+            emptyDateStyle.setBorderLeft(BorderStyle.THIN);
+            emptyDateStyle.setBorderRight(BorderStyle.THIN);
+            emptyDateStyle.setBorderTop(BorderStyle.THIN);
+
+            // 创建样式:日期表头(周末用不同颜色)
+            org.apache.poi.xssf.usermodel.XSSFCellStyle weekendHeaderStyle = workbook.createCellStyle();
+            weekendHeaderStyle.setFillForegroundColor(new XSSFColor(new byte[]{(byte)255, (byte)192, (byte)0}, null));
+            weekendHeaderStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+            org.apache.poi.xssf.usermodel.XSSFFont weekendFont = workbook.createFont();
+            weekendFont.setBold(true);
+            weekendHeaderStyle.setFont(weekendFont);
+            weekendHeaderStyle.setAlignment(HorizontalAlignment.CENTER);
+            weekendHeaderStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+            weekendHeaderStyle.setBorderBottom(BorderStyle.THIN);
+            weekendHeaderStyle.setBorderLeft(BorderStyle.THIN);
+            weekendHeaderStyle.setBorderRight(BorderStyle.THIN);
+            weekendHeaderStyle.setBorderTop(BorderStyle.THIN);
+
+            // 创建样式:周末空白单元格(浅黄色)
+            org.apache.poi.xssf.usermodel.XSSFCellStyle weekendEmptyStyle = workbook.createCellStyle();
+            weekendEmptyStyle.setFillForegroundColor(new XSSFColor(new byte[]{(byte)255, (byte)235, (byte)156}, null));
+            weekendEmptyStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+            weekendEmptyStyle.setBorderBottom(BorderStyle.THIN);
+            weekendEmptyStyle.setBorderLeft(BorderStyle.THIN);
+            weekendEmptyStyle.setBorderRight(BorderStyle.THIN);
+            weekendEmptyStyle.setBorderTop(BorderStyle.THIN);
+
+            // 第0行:表头
+            org.apache.poi.xssf.usermodel.XSSFRow headerRow = sheet.createRow(0);
+            headerRow.setHeightInPoints(20);
+
+            org.apache.poi.xssf.usermodel.XSSFCell seqCell = headerRow.createCell(0);
+            seqCell.setCellValue("序号");
+            seqCell.setCellStyle(headerStyle);
+
+            org.apache.poi.xssf.usermodel.XSSFCell nameCell = headerRow.createCell(1);
+            nameCell.setCellValue("项目");
+            nameCell.setCellStyle(headerStyle);
+
+            org.apache.poi.xssf.usermodel.XSSFCell startCell = headerRow.createCell(2);
+            startCell.setCellValue("开始时间");
+            startCell.setCellStyle(headerStyle);
+
+            org.apache.poi.xssf.usermodel.XSSFCell endCell = headerRow.createCell(3);
+            endCell.setCellValue("结束时间");
+            endCell.setCellStyle(headerStyle);
+
+            // 日期列表头
+            java.time.format.DateTimeFormatter dayFormatter = java.time.format.DateTimeFormatter.ofPattern("MM/dd");
+            for (int i = 0; i < dateList.size(); i++) {
+                java.time.LocalDate d = dateList.get(i);
+                org.apache.poi.xssf.usermodel.XSSFCell dateCell = headerRow.createCell(4 + i);
+                dateCell.setCellValue(d.format(dayFormatter));
+                // 周末用不同颜色
+                java.time.DayOfWeek dow = d.getDayOfWeek();
+                if (dow == java.time.DayOfWeek.SATURDAY || dow == java.time.DayOfWeek.SUNDAY) {
+                    dateCell.setCellStyle(weekendHeaderStyle);
+                } else {
+                    dateCell.setCellStyle(headerStyle);
+                }
+            }
+
+            // 数据行
+            int seq = 1;
+            // 用于区分不同项目的颜色(多种蓝色系)
+            byte[][] ganttColors = {
+                {(byte)68,  (byte)114, (byte)196},
+                {(byte)70,  (byte)130, (byte)180},
+                {(byte)30,  (byte)144, (byte)255},
+                {(byte)0,   (byte)120, (byte)215},
+                {(byte)0,   (byte)176, (byte)240},
+                {(byte)0,   (byte)112, (byte)192},
+                {(byte)91,  (byte)155, (byte)213},
+                {(byte)31,  (byte)73,  (byte)125},
+            };
+
+            // 去重:按项目名称+开始时间+结束时间去重
+            List<Map<String, Object>> uniqueProjects = new ArrayList<>();
+            Set<String> seen = new LinkedHashSet<>();
+            for (Map<String, Object> item : projectDataList) {
+                String key = (item.get("projectName") == null ? "" : item.get("projectName").toString())
+                        + "|" + (item.get("startDate") == null ? "" : item.get("startDate").toString())
+                        + "|" + (item.get("endDate") == null ? "" : item.get("endDate").toString());
+                if (seen.add(key)) {
+                    uniqueProjects.add(item);
+                }
+            }
+
+            for (int rowIdx = 0; rowIdx < uniqueProjects.size(); rowIdx++) {
+                Map<String, Object> projectData = uniqueProjects.get(rowIdx);
+                String projectName = projectData.get("projectName") == null ? "" : projectData.get("projectName").toString();
+                String projStart = projectData.get("startDate") == null ? "" : projectData.get("startDate").toString();
+                String projEnd = projectData.get("endDate") == null ? "" : projectData.get("endDate").toString();
+
+                org.apache.poi.xssf.usermodel.XSSFRow dataRow = sheet.createRow(rowIdx + 1);
+                dataRow.setHeightInPoints(18);
+
+                // 序号
+                org.apache.poi.xssf.usermodel.XSSFCell seqDataCell = dataRow.createCell(0);
+                seqDataCell.setCellValue(seq++);
+                seqDataCell.setCellStyle(normalStyle);
+
+                // 项目名称
+                org.apache.poi.xssf.usermodel.XSSFCell nameDataCell = dataRow.createCell(1);
+                nameDataCell.setCellValue(projectName);
+                nameDataCell.setCellStyle(normalStyle);
+
+                // 开始时间
+                org.apache.poi.xssf.usermodel.XSSFCell startDataCell = dataRow.createCell(2);
+                startDataCell.setCellValue(projStart);
+                startDataCell.setCellStyle(normalStyle);
+
+                // 结束时间
+                org.apache.poi.xssf.usermodel.XSSFCell endDataCell = dataRow.createCell(3);
+                endDataCell.setCellValue(projEnd);
+                endDataCell.setCellStyle(normalStyle);
+
+                // 解析项目的开始和结束日期
+                java.time.LocalDate projStartDate = null;
+                java.time.LocalDate projEndDate = null;
+                try {
+                    if (!projStart.isEmpty()) projStartDate = java.time.LocalDate.parse(projStart);
+                    if (!projEnd.isEmpty()) projEndDate = java.time.LocalDate.parse(projEnd);
+                } catch (Exception ex) {
+                    // 日期解析失败,跳过色块
+                }
+
+                // 选择颜色(循环使用)
+                byte[] colorBytes = ganttColors[rowIdx % ganttColors.length];
+                org.apache.poi.xssf.usermodel.XSSFCellStyle thisGanttStyle = workbook.createCellStyle();
+                thisGanttStyle.setFillForegroundColor(new XSSFColor(colorBytes, null));
+                thisGanttStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+                thisGanttStyle.setBorderBottom(BorderStyle.THIN);
+                thisGanttStyle.setBorderLeft(BorderStyle.THIN);
+                thisGanttStyle.setBorderRight(BorderStyle.THIN);
+                thisGanttStyle.setBorderTop(BorderStyle.THIN);
+
+                // 填充日期列
+                for (int i = 0; i < dateList.size(); i++) {
+                    java.time.LocalDate d = dateList.get(i);
+                    org.apache.poi.xssf.usermodel.XSSFCell dateDataCell = dataRow.createCell(4 + i);
+                    dateDataCell.setCellValue("");
+
+                    boolean inRange = projStartDate != null && projEndDate != null
+                            && !d.isBefore(projStartDate) && !d.isAfter(projEndDate);
+
+                    java.time.DayOfWeek dow = d.getDayOfWeek();
+                    boolean isWeekend = (dow == java.time.DayOfWeek.SATURDAY || dow == java.time.DayOfWeek.SUNDAY);
+
+                    if (inRange) {
+                        dateDataCell.setCellStyle(thisGanttStyle);
+                    } else if (isWeekend) {
+                        dateDataCell.setCellStyle(weekendEmptyStyle);
+                    } else {
+                        dateDataCell.setCellStyle(emptyDateStyle);
+                    }
+                }
+            }
+
+            // 保存文件
+            fileName = fileName + ".xlsx";
+            File dir = new File(path);
+            if (!dir.exists()) {
+                dir.mkdirs();
+            }
+            FileOutputStream os = new FileOutputStream(path + fileName);
+            workbook.write(os);
+            os.flush();
+            os.close();
+            workbook.close();
+            result = "/upload/" + fileName;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return result;
+    }
+
+    /**
+     * 导出按项目的甘特图Excel
+     * 列结构:序号 | 项目 | 开始时间 | 结束时间 | 日期1 | 日期2 | ... | 日期N
+     * 每个项目行在对应日期列上用彩色背景色块表示项目时间范围
+     *
+     * @param fileName   文件名(不含扩展名)
+     * @param projectRows 项目数据列表,每个Map包含:projectName, startDate(yyyy-MM-dd), endDate(yyyy-MM-dd)
+     * @param rangeStart 日期范围开始(yyyy-MM-dd)
+     * @param rangeEnd   日期范围结束(yyyy-MM-dd)
+     * @param path       文件保存路径
+     * @return 文件访问路径
+     */
+    public static String exportGanttChartByProject(String fileName,
+                                                    List<Map<String, String>> projectRows,
+                                                    String rangeStart,
+                                                    String rangeEnd,
+                                                    String path) {
+        String result = null;
+        org.apache.poi.xssf.usermodel.XSSFWorkbook workbook = new org.apache.poi.xssf.usermodel.XSSFWorkbook();
+        try {
+            org.apache.poi.xssf.usermodel.XSSFSheet sheet = workbook.createSheet("甘特图");
+
+            java.time.LocalDate startLocalDate = java.time.LocalDate.parse(rangeStart);
+            java.time.LocalDate endLocalDate = java.time.LocalDate.parse(rangeEnd);
+
+            // 计算日期列表
+            List<java.time.LocalDate> dateList = new ArrayList<>();
+            java.time.LocalDate cur = startLocalDate;
+            while (!cur.isAfter(endLocalDate)) {
+                dateList.add(cur);
+                cur = cur.plusDays(1);
+            }
+
+            int totalDays = dateList.size();
+            // 固定列:序号(0), 项目(1), 开始时间(2), 结束时间(3)
+            // 日期列从第4列开始,最后一列为计划工时
+            int dateColStart = 4;
+
+            // ---- 创建样式 ----
+            // 表头样式
+            org.apache.poi.xssf.usermodel.XSSFCellStyle headerStyle = workbook.createCellStyle();
+            headerStyle.setFillForegroundColor(new XSSFColor(new byte[]{(byte)68, (byte)114, (byte)196}, null));
+            headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+            headerStyle.setAlignment(HorizontalAlignment.CENTER);
+            headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+            headerStyle.setBorderBottom(BorderStyle.THIN);
+            headerStyle.setBorderTop(BorderStyle.THIN);
+            headerStyle.setBorderLeft(BorderStyle.THIN);
+            headerStyle.setBorderRight(BorderStyle.THIN);
+            org.apache.poi.xssf.usermodel.XSSFFont headerFont = workbook.createFont();
+            headerFont.setBold(true);
+            headerFont.setColor(new XSSFColor(new byte[]{(byte)255, (byte)255, (byte)255}, null));
+            headerStyle.setFont(headerFont);
+
+            // 普通单元格样式
+            org.apache.poi.xssf.usermodel.XSSFCellStyle normalStyle = workbook.createCellStyle();
+            normalStyle.setAlignment(HorizontalAlignment.CENTER);
+            normalStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+            normalStyle.setBorderBottom(BorderStyle.THIN);
+            normalStyle.setBorderTop(BorderStyle.THIN);
+            normalStyle.setBorderLeft(BorderStyle.THIN);
+            normalStyle.setBorderRight(BorderStyle.THIN);
+
+            // 甘特图色块样式(蓝绿色)
+            org.apache.poi.xssf.usermodel.XSSFCellStyle ganttStyle = workbook.createCellStyle();
+            ganttStyle.setFillForegroundColor(new XSSFColor(new byte[]{(byte)70, (byte)130, (byte)180}, null));
+            ganttStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+            ganttStyle.setBorderBottom(BorderStyle.THIN);
+            ganttStyle.setBorderTop(BorderStyle.THIN);
+            ganttStyle.setBorderLeft(BorderStyle.NONE);
+            ganttStyle.setBorderRight(BorderStyle.NONE);
+
+            // 空白日期单元格样式(浅灰色背景)
+            org.apache.poi.xssf.usermodel.XSSFCellStyle emptyDateStyle = workbook.createCellStyle();
+            emptyDateStyle.setFillForegroundColor(new XSSFColor(new byte[]{(byte)242, (byte)242, (byte)242}, null));
+            emptyDateStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+            emptyDateStyle.setBorderBottom(BorderStyle.THIN);
+            emptyDateStyle.setBorderTop(BorderStyle.THIN);
+            emptyDateStyle.setBorderLeft(BorderStyle.THIN);
+            emptyDateStyle.setBorderRight(BorderStyle.THIN);
+
+            // ---- 创建表头行 ----
+            org.apache.poi.xssf.usermodel.XSSFRow headerRow = sheet.createRow(0);
+            headerRow.setHeightInPoints(25);
+
+            String[] fixedHeaders = {"序号", "项目", "开始时间", "结束时间"};
+            for (int i = 0; i < fixedHeaders.length; i++) {
+                org.apache.poi.xssf.usermodel.XSSFCell cell = headerRow.createCell(i);
+                cell.setCellValue(fixedHeaders[i]);
+                cell.setCellStyle(headerStyle);
+            }
+
+            // 日期列表头
+            java.time.format.DateTimeFormatter dayFmt = java.time.format.DateTimeFormatter.ofPattern("MM/dd");
+            for (int i = 0; i < totalDays; i++) {
+                org.apache.poi.xssf.usermodel.XSSFCell cell = headerRow.createCell(dateColStart + i);
+                cell.setCellValue(dateList.get(i).format(dayFmt));
+                cell.setCellStyle(headerStyle);
+            }
+
+            // 计划工时列表头(最后一列)
+            int planHoursColIdx = dateColStart + totalDays;
+            org.apache.poi.xssf.usermodel.XSSFCell planHoursHeaderCell = headerRow.createCell(planHoursColIdx);
+            planHoursHeaderCell.setCellValue("计划工时(h)");
+            planHoursHeaderCell.setCellStyle(headerStyle);
+
+            // ---- 设置列宽 ----
+            sheet.setColumnWidth(0, 8 * 256);   // 序号
+            sheet.setColumnWidth(1, 30 * 256);  // 项目
+            sheet.setColumnWidth(2, 14 * 256);  // 开始时间
+            sheet.setColumnWidth(3, 14 * 256);  // 结束时间
+            for (int i = 0; i < totalDays; i++) {
+                sheet.setColumnWidth(dateColStart + i, 5 * 256); // 日期列较窄
+            }
+            sheet.setColumnWidth(planHoursColIdx, 14 * 256); // 计划工时列
+
+            // ---- 创建数据行 ----
+            java.time.format.DateTimeFormatter dateFmt = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd");
+            for (int rowIdx = 0; rowIdx < projectRows.size(); rowIdx++) {
+                Map<String, String> projectData = projectRows.get(rowIdx);
+                org.apache.poi.xssf.usermodel.XSSFRow dataRow = sheet.createRow(rowIdx + 1);
+                dataRow.setHeightInPoints(22);
+
+                String projectName = projectData.getOrDefault("projectName", "");
+                String projStartStr = projectData.getOrDefault("startDate", "");
+                String projEndStr = projectData.getOrDefault("endDate", "");
+
+                // 序号
+                org.apache.poi.xssf.usermodel.XSSFCell seqCell = dataRow.createCell(0);
+                seqCell.setCellValue(rowIdx + 1);
+                seqCell.setCellStyle(normalStyle);
+
+                // 项目名称
+                org.apache.poi.xssf.usermodel.XSSFCell nameCell = dataRow.createCell(1);
+                nameCell.setCellValue(projectName);
+                nameCell.setCellStyle(normalStyle);
+
+                // 开始时间
+                org.apache.poi.xssf.usermodel.XSSFCell startCell = dataRow.createCell(2);
+                startCell.setCellValue(projStartStr);
+                startCell.setCellStyle(normalStyle);
+
+                // 结束时间
+                org.apache.poi.xssf.usermodel.XSSFCell endCell = dataRow.createCell(3);
+                endCell.setCellValue(projEndStr);
+                endCell.setCellStyle(normalStyle);
+
+                // 解析项目的开始和结束日期
+                java.time.LocalDate projStart = null;
+                java.time.LocalDate projEnd = null;
+                try {
+                    if (!projStartStr.isEmpty()) projStart = java.time.LocalDate.parse(projStartStr, dateFmt);
+                    if (!projEndStr.isEmpty()) projEnd = java.time.LocalDate.parse(projEndStr, dateFmt);
+                } catch (Exception ignored) {}
+
+                // 为每个日期列设置样式
+                for (int dayIdx = 0; dayIdx < totalDays; dayIdx++) {
+                    java.time.LocalDate colDate = dateList.get(dayIdx);
+                    org.apache.poi.xssf.usermodel.XSSFCell dayCell = dataRow.createCell(dateColStart + dayIdx);
+                    dayCell.setCellValue("");
+
+                    boolean inRange = projStart != null && projEnd != null
+                            && !colDate.isBefore(projStart)
+                            && !colDate.isAfter(projEnd);
+
+                    if (inRange) {
+                        dayCell.setCellStyle(ganttStyle);
+                    } else {
+                        dayCell.setCellStyle(emptyDateStyle);
+                    }
+                }
+
+                // 计划工时列
+                org.apache.poi.xssf.usermodel.XSSFCell planHoursCell = dataRow.createCell(planHoursColIdx);
+                String planHoursVal = projectData.getOrDefault("planHours", "");
+                planHoursCell.setCellValue(planHoursVal);
+                planHoursCell.setCellStyle(normalStyle);
+            }
+
+            // ---- 冻结前4列 ----
+            sheet.createFreezePane(dateColStart, 1);
+
+            // ---- 保存文件 ----
+            fileName = fileName + ".xlsx";
+            File dir = new File(path);
+            if (!dir.exists()) {
+                dir.mkdirs();
+            }
+            FileOutputStream os = new FileOutputStream(path + fileName);
+            workbook.write(os);
+            os.flush();
+            os.close();
+            workbook.close();
+            result = "/upload/" + fileName;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return result;
+    }
+
+    /**
+     * 按人员导出甘特图(Excel)
+     * 列结构:序号 | 人员姓名 | 开始时间 | 结束时间 | [日期列...] | 计划工时(h)
+     * 每行代表一条任务记录,在对应日期列用色块显示任务时间范围。
+     *
+     * @param fileName   文件名(不含扩展名)
+     * @param personRows 每行数据,包含 personName/startDate/endDate/planHours 字段
+     * @param rangeStart 日期范围开始(yyyy-MM-dd)
+     * @param rangeEnd   日期范围结束(yyyy-MM-dd)
+     * @param path       文件保存路径
+     * @return 文件访问路径
+     */
+    public static String exportGanttChartByPerson(String fileName,
+                                                   List<Map<String, String>> personRows,
+                                                   String rangeStart,
+                                                   String rangeEnd,
+                                                   String path) {
+        String result = null;
+        org.apache.poi.xssf.usermodel.XSSFWorkbook workbook = new org.apache.poi.xssf.usermodel.XSSFWorkbook();
+        try {
+            org.apache.poi.xssf.usermodel.XSSFSheet sheet = workbook.createSheet("甘特图");
+
+            java.time.LocalDate startLocalDate = java.time.LocalDate.parse(rangeStart);
+            java.time.LocalDate endLocalDate = java.time.LocalDate.parse(rangeEnd);
+
+            // 计算日期列表
+            List<java.time.LocalDate> dateList = new ArrayList<>();
+            java.time.LocalDate cur = startLocalDate;
+            while (!cur.isAfter(endLocalDate)) {
+                dateList.add(cur);
+                cur = cur.plusDays(1);
+            }
+
+            int totalDays = dateList.size();
+            // 固定列:序号(0), 人员姓名(1), 开始时间(2), 结束时间(3)
+            // 日期列从第4列开始,最后一列为计划工时
+            int dateColStart = 4;
+
+            // ---- 创建样式 ----
+            // 表头样式
+            org.apache.poi.xssf.usermodel.XSSFCellStyle headerStyle = workbook.createCellStyle();
+            headerStyle.setFillForegroundColor(new XSSFColor(new byte[]{(byte)68, (byte)114, (byte)196}, null));
+            headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+            headerStyle.setAlignment(HorizontalAlignment.CENTER);
+            headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+            headerStyle.setBorderBottom(BorderStyle.THIN);
+            headerStyle.setBorderTop(BorderStyle.THIN);
+            headerStyle.setBorderLeft(BorderStyle.THIN);
+            headerStyle.setBorderRight(BorderStyle.THIN);
+            org.apache.poi.xssf.usermodel.XSSFFont headerFont = workbook.createFont();
+            headerFont.setBold(true);
+            headerFont.setColor(new XSSFColor(new byte[]{(byte)255, (byte)255, (byte)255}, null));
+            headerStyle.setFont(headerFont);
+
+            // 普通单元格样式
+            org.apache.poi.xssf.usermodel.XSSFCellStyle normalStyle = workbook.createCellStyle();
+            normalStyle.setAlignment(HorizontalAlignment.CENTER);
+            normalStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+            normalStyle.setBorderBottom(BorderStyle.THIN);
+            normalStyle.setBorderTop(BorderStyle.THIN);
+            normalStyle.setBorderLeft(BorderStyle.THIN);
+            normalStyle.setBorderRight(BorderStyle.THIN);
+
+            // 甘特图色块样式(橙色,区别于项目视图的蓝色)
+            org.apache.poi.xssf.usermodel.XSSFCellStyle ganttStyle = workbook.createCellStyle();
+            ganttStyle.setFillForegroundColor(new XSSFColor(new byte[]{(byte)255, (byte)153, (byte)51}, null));
+            ganttStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+            ganttStyle.setBorderBottom(BorderStyle.THIN);
+            ganttStyle.setBorderTop(BorderStyle.THIN);
+            ganttStyle.setBorderLeft(BorderStyle.NONE);
+            ganttStyle.setBorderRight(BorderStyle.NONE);
+
+            // 空白日期单元格样式(浅灰色背景)
+            org.apache.poi.xssf.usermodel.XSSFCellStyle emptyDateStyle = workbook.createCellStyle();
+            emptyDateStyle.setFillForegroundColor(new XSSFColor(new byte[]{(byte)242, (byte)242, (byte)242}, null));
+            emptyDateStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+            emptyDateStyle.setBorderBottom(BorderStyle.THIN);
+            emptyDateStyle.setBorderTop(BorderStyle.THIN);
+            emptyDateStyle.setBorderLeft(BorderStyle.THIN);
+            emptyDateStyle.setBorderRight(BorderStyle.THIN);
+
+            // ---- 创建表头行 ----
+            org.apache.poi.xssf.usermodel.XSSFRow headerRow = sheet.createRow(0);
+            headerRow.setHeightInPoints(25);
+
+            String[] fixedHeaders = {"序号", "人员姓名", "开始时间", "结束时间"};
+            for (int i = 0; i < fixedHeaders.length; i++) {
+                org.apache.poi.xssf.usermodel.XSSFCell cell = headerRow.createCell(i);
+                cell.setCellValue(fixedHeaders[i]);
+                cell.setCellStyle(headerStyle);
+            }
+
+            // 日期列表头
+            java.time.format.DateTimeFormatter dayFmt = java.time.format.DateTimeFormatter.ofPattern("MM/dd");
+            for (int i = 0; i < totalDays; i++) {
+                org.apache.poi.xssf.usermodel.XSSFCell cell = headerRow.createCell(dateColStart + i);
+                cell.setCellValue(dateList.get(i).format(dayFmt));
+                cell.setCellStyle(headerStyle);
+            }
+
+            // 计划工时列表头(最后一列)
+            int planHoursColIdx = dateColStart + totalDays;
+            org.apache.poi.xssf.usermodel.XSSFCell planHoursHeaderCell = headerRow.createCell(planHoursColIdx);
+            planHoursHeaderCell.setCellValue("计划工时(h)");
+            planHoursHeaderCell.setCellStyle(headerStyle);
+
+            // ---- 设置列宽 ----
+            sheet.setColumnWidth(0, 8 * 256);   // 序号
+            sheet.setColumnWidth(1, 25 * 256);  // 人员姓名
+            sheet.setColumnWidth(2, 14 * 256);  // 开始时间
+            sheet.setColumnWidth(3, 14 * 256);  // 结束时间
+            for (int i = 0; i < totalDays; i++) {
+                sheet.setColumnWidth(dateColStart + i, 5 * 256); // 日期列较窄
+            }
+            sheet.setColumnWidth(planHoursColIdx, 14 * 256); // 计划工时列
+
+            // ---- 创建数据行 ----
+            java.time.format.DateTimeFormatter dateFmt = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd");
+            for (int rowIdx = 0; rowIdx < personRows.size(); rowIdx++) {
+                Map<String, String> personData = personRows.get(rowIdx);
+                org.apache.poi.xssf.usermodel.XSSFRow dataRow = sheet.createRow(rowIdx + 1);
+                dataRow.setHeightInPoints(22);
+
+                String personName = personData.getOrDefault("personName", "");
+                String taskStartStr = personData.getOrDefault("startDate", "");
+                String taskEndStr = personData.getOrDefault("endDate", "");
+
+                // 序号
+                org.apache.poi.xssf.usermodel.XSSFCell seqCell = dataRow.createCell(0);
+                seqCell.setCellValue(rowIdx + 1);
+                seqCell.setCellStyle(normalStyle);
+
+                // 人员姓名
+                org.apache.poi.xssf.usermodel.XSSFCell nameCell = dataRow.createCell(1);
+                nameCell.setCellValue(personName);
+                nameCell.setCellStyle(normalStyle);
+
+                // 开始时间
+                org.apache.poi.xssf.usermodel.XSSFCell startCell = dataRow.createCell(2);
+                startCell.setCellValue(taskStartStr);
+                startCell.setCellStyle(normalStyle);
+
+                // 结束时间
+                org.apache.poi.xssf.usermodel.XSSFCell endCell = dataRow.createCell(3);
+                endCell.setCellValue(taskEndStr);
+                endCell.setCellStyle(normalStyle);
+
+                // 解析任务的开始和结束日期
+                java.time.LocalDate taskStart = null;
+                java.time.LocalDate taskEnd = null;
+                try {
+                    if (!taskStartStr.isEmpty()) taskStart = java.time.LocalDate.parse(taskStartStr, dateFmt);
+                    if (!taskEndStr.isEmpty()) taskEnd = java.time.LocalDate.parse(taskEndStr, dateFmt);
+                } catch (Exception ignored) {}
+
+                // 为每个日期列设置样式
+                for (int dayIdx = 0; dayIdx < totalDays; dayIdx++) {
+                    java.time.LocalDate colDate = dateList.get(dayIdx);
+                    org.apache.poi.xssf.usermodel.XSSFCell dayCell = dataRow.createCell(dateColStart + dayIdx);
+                    dayCell.setCellValue("");
+
+                    boolean inRange = taskStart != null && taskEnd != null
+                            && !colDate.isBefore(taskStart)
+                            && !colDate.isAfter(taskEnd);
+
+                    if (inRange) {
+                        dayCell.setCellStyle(ganttStyle);
+                    } else {
+                        dayCell.setCellStyle(emptyDateStyle);
+                    }
+                }
+
+                // 计划工时列
+                org.apache.poi.xssf.usermodel.XSSFCell planHoursCell = dataRow.createCell(planHoursColIdx);
+                String planHoursVal = personData.getOrDefault("planHours", "");
+                planHoursCell.setCellValue(planHoursVal);
+                planHoursCell.setCellStyle(normalStyle);
+            }
+
+            // ---- 冻结前4列 ----
+            sheet.createFreezePane(dateColStart, 1);
+
+            // ---- 保存文件 ----
+            fileName = fileName + ".xlsx";
+            File dir = new File(path);
+            if (!dir.exists()) {
+                dir.mkdirs();
+            }
+            FileOutputStream os = new FileOutputStream(path + fileName);
+            workbook.write(os);
+            os.flush();
+            os.close();
+            workbook.close();
+            result = "/upload/" + fileName;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return result;
+    }
 }

+ 48 - 1
fhKeeper/formulahousekeeper/timesheet/src/views/project/project_gantt.vue

@@ -58,7 +58,11 @@
             <el-option v-for="item in screenList" :key="item.id" :label="item.name" :value="item.id">
             </el-option>
           </el-select>
-          <selectCat v-if="!reqpar1 && user.userNameNeedTranslate == '1'" style="margin-left:9px;" :size="'small'" :widthStr="'153'" :subject="screenList" :subjectId="valuex" :distinction="'1'" @selectCal="selectCal"></selectCat>
+              <selectCat v-if="!reqpar1 && user.userNameNeedTranslate == '1'" style="margin-left:9px;" :size="'small'" :widthStr="'153'" :subject="screenList" :subjectId="valuex" :distinction="'1'" @selectCal="selectCal"></selectCat>
+      </div>
+      <!-- 导出 -->
+      <div class="head_files">
+        <el-button size="small" type="primary" @click="exportGantt" v-loading="exportingGantt">导出甘特图</el-button>
       </div>
     </div>
 
@@ -198,6 +202,7 @@ export default {
       demandList: [],
       pageIndex: 1,
       pageSize: 20,
+      exportingGantt: false, 
 
       demandEditDialog: false,
       editParameter: {},
@@ -554,6 +559,48 @@ export default {
             })
         })
       },
+      // 导出甘特图数据
+      exportGantt() {
+        let params = {type : this.reqpar1 , startDate : this.reqpar2[0] , endDate : this.reqpar2[1]}
+        if(this.reqpar1) {
+          if(this.valuex != ''){
+            params.projectId = this.valuex
+          }
+          if(this.valuex2 != ''){
+            params.groupName = this.valuex2
+          }
+          params.taskType = this.taskType
+        }else {
+          if(this.valuex != ''){
+            params.userId = this.valuex
+          }
+        }
+        if(this.justWaitForMe) {
+          params.justWaitForMe = 1
+        }
+        this.exportingGantt = true;
+        this.http.post(
+          '/project/exportGanttDataNew',
+          params,
+          (res) => {
+            this.exportingGantt = false;
+            if (res.code == "ok") {
+              var filePath = res.data;
+              const a = document.createElement("a"); // 创建a标签
+              a.setAttribute("download", "项目甘特图.xlsx"); // download属性
+              a.setAttribute("href", filePath); // href链接
+              a.click(); //自执行点击事件
+              a.remove();
+            }
+          },
+          (error) => {
+            this.$message({
+              message: error,
+              type: "error",
+            });
+          },
+        );
+      },
   },
 };
 </script>

Разница между файлами не показана из-за своего большого размера
+ 1192 - 1142
fhKeeper/formulahousekeeper/timesheet/src/views/settings/timetype.vue