yusm пре 1 дан
родитељ
комит
8c09023b68

+ 27 - 12
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/ReportController.java

@@ -3697,53 +3697,68 @@ public class ReportController {
 
 
     //项目工时排名前十
     //项目工时排名前十
     @RequestMapping("/getTop10ProjectReport")
     @RequestMapping("/getTop10ProjectReport")
-    public HttpRespMsg getTop10ProjectReport(String ymonth) {
+    public HttpRespMsg getTop10ProjectReport(String ymonth,
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
         User user = userMapper.selectById(request.getHeader("TOKEN"));
         User user = userMapper.selectById(request.getHeader("TOKEN"));
-        return reportService.getTop10ProjectReport(user.getCompanyId(), ymonth);
+        return reportService.getTop10ProjectReport(user.getCompanyId(), ymonth, startDate, endDate);
     }
     }
 
 
     //个人参与项目数前十
     //个人参与项目数前十
     @RequestMapping("/getUserProjectTop10")
     @RequestMapping("/getUserProjectTop10")
-    public HttpRespMsg getUserProjectTop10(String ymonth) {
+    public HttpRespMsg getUserProjectTop10(String ymonth,
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
         User user = userMapper.selectById(request.getHeader("TOKEN"));
         User user = userMapper.selectById(request.getHeader("TOKEN"));
-        return reportService.getUserProjectTop10(user.getCompanyId(), ymonth);
+        return reportService.getUserProjectTop10(user.getCompanyId(), ymonth, startDate, endDate);
     }
     }
 
 
     //项目工时排名前三部门分配表
     //项目工时排名前三部门分配表
     @RequestMapping("/getTop3ProjectReportGroupByDept")
     @RequestMapping("/getTop3ProjectReportGroupByDept")
     public HttpRespMsg getTop3ProjectReportGroupByDept(String ymonth,
     public HttpRespMsg getTop3ProjectReportGroupByDept(String ymonth,
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate,
             @RequestParam(required = false) String deptMode) {
             @RequestParam(required = false) String deptMode) {
         User user = userMapper.selectById(request.getHeader("TOKEN"));
         User user = userMapper.selectById(request.getHeader("TOKEN"));
-        return reportService.getTop3ProjectReportGroupByDept(user.getCompanyId(), ymonth, deptMode);
+        return reportService.getTop3ProjectReportGroupByDept(user.getCompanyId(), ymonth, startDate, endDate, deptMode);
     }
     }
 
 
     //各部门总工时和人均工时表
     //各部门总工时和人均工时表
     @RequestMapping("/getProjectReportGroupByDept")
     @RequestMapping("/getProjectReportGroupByDept")
     public HttpRespMsg getProjectReportGroupByDept(String ymonth,
     public HttpRespMsg getProjectReportGroupByDept(String ymonth,
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate,
             @RequestParam(required = false) String deptMode) {
             @RequestParam(required = false) String deptMode) {
         User user = userMapper.selectById(request.getHeader("TOKEN"));
         User user = userMapper.selectById(request.getHeader("TOKEN"));
-        return reportService.getProjectReportGroupByDept(user.getCompanyId(), ymonth, deptMode);
+        return reportService.getProjectReportGroupByDept(user.getCompanyId(), ymonth, startDate, endDate, deptMode);
     }
     }
 
 
     //各部门参与项目数量表
     //各部门参与项目数量表
     @RequestMapping("/getDeptProjectCount")
     @RequestMapping("/getDeptProjectCount")
-    public HttpRespMsg getDeptProjectCount(String ymonth) {
+    public HttpRespMsg getDeptProjectCount(String ymonth,
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
         User user = userMapper.selectById(request.getHeader("TOKEN"));
         User user = userMapper.selectById(request.getHeader("TOKEN"));
-        return reportService.getDeptProjectCount(user.getCompanyId(), ymonth);
+        return reportService.getDeptProjectCount(user.getCompanyId(), ymonth, startDate, endDate);
     }
     }
 
 
     //工时分析报告综合数据
     //工时分析报告综合数据
     @RequestMapping("/getDashboardAnalysisReport")
     @RequestMapping("/getDashboardAnalysisReport")
-    public HttpRespMsg getDashboardAnalysisReport(String ymonth) {
+    public HttpRespMsg getDashboardAnalysisReport(String ymonth,
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate) {
         User user = userMapper.selectById(request.getHeader("TOKEN"));
         User user = userMapper.selectById(request.getHeader("TOKEN"));
-        return reportService.getDashboardAnalysisReport(user.getCompanyId(), ymonth);
+        return reportService.getDashboardAnalysisReport(user.getCompanyId(), ymonth, startDate, endDate);
     }
     }
 
 
     //有工时投入项目明细,分页加载
     //有工时投入项目明细,分页加载
     @RequestMapping("/getWorkedProjectList")
     @RequestMapping("/getWorkedProjectList")
-    public HttpRespMsg getWorkedProjectList(String ymonth, Integer pageIndex, Integer pageSize) {
+    public HttpRespMsg getWorkedProjectList(String ymonth,
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate,
+            Integer pageIndex, Integer pageSize) {
         User user = userMapper.selectById(request.getHeader("TOKEN"));
         User user = userMapper.selectById(request.getHeader("TOKEN"));
-        return reportService.getWorkedProjectList(user.getCompanyId(), ymonth, pageIndex, pageSize);
+        return reportService.getWorkedProjectList(user.getCompanyId(), ymonth, startDate, endDate, pageIndex, pageSize);
     }
     }
 
 
     @RequestMapping("/getAllReport")
     @RequestMapping("/getAllReport")

+ 9 - 9
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/ReportService.java

@@ -231,23 +231,23 @@ public interface ReportService extends IService<Report> {
 
 
     HttpRespMsg exportExtraWorkHoursList(String year, Integer departmentId, String userId, HttpServletRequest request);
     HttpRespMsg exportExtraWorkHoursList(String year, Integer departmentId, String userId, HttpServletRequest request);
 
 
-    HttpRespMsg getTop10ProjectReport(Integer companyId, String ymonth);
+    HttpRespMsg getTop10ProjectReport(Integer companyId, String ymonth, String startDate, String endDate);
 
 
-    HttpRespMsg getUserProjectTop10(Integer companyId, String ymonth);
+    HttpRespMsg getUserProjectTop10(Integer companyId, String ymonth, String startDate, String endDate);
 
 
-    HttpRespMsg getTop3ProjectReportGroupByDept(Integer companyId, String ymonth);
+    HttpRespMsg getTop3ProjectReportGroupByDept(Integer companyId, String ymonth, String startDate, String endDate);
 
 
-    HttpRespMsg getTop3ProjectReportGroupByDept(Integer companyId, String ymonth, String deptMode);
+    HttpRespMsg getTop3ProjectReportGroupByDept(Integer companyId, String ymonth, String startDate, String endDate, String deptMode);
 
 
-    HttpRespMsg getProjectReportGroupByDept(Integer companyId, String ymonth);
+    HttpRespMsg getProjectReportGroupByDept(Integer companyId, String ymonth, String startDate, String endDate);
 
 
-    HttpRespMsg getProjectReportGroupByDept(Integer companyId, String ymonth, String deptMode);
+    HttpRespMsg getProjectReportGroupByDept(Integer companyId, String ymonth, String startDate, String endDate, String deptMode);
 
 
-    HttpRespMsg getDeptProjectCount(Integer companyId, String ymonth);
+    HttpRespMsg getDeptProjectCount(Integer companyId, String ymonth, String startDate, String endDate);
 
 
-    HttpRespMsg getDashboardAnalysisReport(Integer companyId, String ymonth);
+    HttpRespMsg getDashboardAnalysisReport(Integer companyId, String ymonth, String startDate, String endDate);
 
 
-    HttpRespMsg getWorkedProjectList(Integer companyId, String ymonth, Integer pageIndex, Integer pageSize);
+    HttpRespMsg getWorkedProjectList(Integer companyId, String ymonth, String startDate, String endDate, Integer pageIndex, Integer pageSize);
 
 
     HttpRespMsg getAllReportListByToken(String json);
     HttpRespMsg getAllReportListByToken(String json);
 
 

+ 188 - 90
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/impl/ReportServiceImpl.java

@@ -16108,15 +16108,73 @@ public class ReportServiceImpl extends ServiceImpl<ReportMapper, Report> impleme
         return msg;
         return msg;
     }
     }
 
 
+    private static final int MAX_DASHBOARD_RANGE_DAYS = 366;
+
+    private static class DashboardDateFilter {
+        private final String month;
+        private final LocalDate startDate;
+        private final LocalDate endDate;
+        private final boolean rangeMode;
+
+        private DashboardDateFilter(String month, LocalDate startDate, LocalDate endDate, boolean rangeMode) {
+            this.month = month;
+            this.startDate = startDate;
+            this.endDate = endDate;
+            this.rangeMode = rangeMode;
+        }
+
+        private void applyDateFilter(QueryWrapper<Report> wrapper) {
+            if (rangeMode) {
+                wrapper.apply("date(create_date) >= {0} and date(create_date) <= {1}",
+                        startDate.toString(), endDate.toString());
+            } else {
+                wrapper.apply("date_format(create_date, '%Y-%m') = {0}", month);
+            }
+        }
+    }
+
+    private DashboardDateFilter resolveDashboardDateFilter(String ymonth, String startDate, String endDate, HttpRespMsg msg) {
+        boolean hasStart = !StringUtils.isEmpty(startDate);
+        boolean hasEnd = !StringUtils.isEmpty(endDate);
+        if (hasStart && hasEnd) {
+            try {
+                LocalDate start = LocalDate.parse(startDate);
+                LocalDate end = LocalDate.parse(endDate);
+                if (end.isBefore(start)) {
+                    msg.setError("结束日期不能早于开始日期");
+                    return null;
+                }
+                long days = ChronoUnit.DAYS.between(start, end) + 1;
+                if (days > MAX_DASHBOARD_RANGE_DAYS) {
+                    msg.setError("查询时间区间最多不能超过一年");
+                    return null;
+                }
+                return new DashboardDateFilter(null, start, end, true);
+            } catch (DateTimeParseException e) {
+                msg.setError("日期格式错误,请使用yyyy-MM-dd");
+                return null;
+            }
+        }
+        if (hasStart || hasEnd) {
+            msg.setError("请同时选择开始日期和结束日期");
+            return null;
+        }
+        return new DashboardDateFilter(normalizeYearMonth(ymonth), null, null, false);
+    }
+
     @Override
     @Override
-    public HttpRespMsg getTop10ProjectReport(Integer companyId, String ymonth) {
+    public HttpRespMsg getTop10ProjectReport(Integer companyId, String ymonth, String startDate, String endDate) {
         HttpRespMsg msg = new HttpRespMsg();
         HttpRespMsg msg = new HttpRespMsg();
-        String month = normalizeYearMonth(ymonth);
-        List<Map<String, Object>> reportList = reportMapper.selectMaps(new QueryWrapper<Report>()
+        DashboardDateFilter dateFilter = resolveDashboardDateFilter(ymonth, startDate, endDate, msg);
+        if (dateFilter == null) {
+            return msg;
+        }
+        QueryWrapper<Report> queryWrapper = new QueryWrapper<Report>()
                 .select("project_id as projectId", "sum(working_time) as workingTime", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .select("project_id as projectId", "sum(working_time) as workingTime", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .eq("company_id", companyId)
                 .eq("company_id", companyId)
-                .eq("state", 1)
-                .apply("date_format(create_date, '%Y-%m') = {0}", month)
+                .eq("state", 1);
+        dateFilter.applyDateFilter(queryWrapper);
+        List<Map<String, Object>> reportList = reportMapper.selectMaps(queryWrapper
                 .groupBy("project_id")
                 .groupBy("project_id")
                 .orderByDesc("sum(working_time)")
                 .orderByDesc("sum(working_time)")
                 .last("limit 10"));
                 .last("limit 10"));
@@ -16152,14 +16210,18 @@ public class ReportServiceImpl extends ServiceImpl<ReportMapper, Report> impleme
     }
     }
 
 
     @Override
     @Override
-    public HttpRespMsg getUserProjectTop10(Integer companyId, String ymonth) {
+    public HttpRespMsg getUserProjectTop10(Integer companyId, String ymonth, String startDate, String endDate) {
         HttpRespMsg msg = new HttpRespMsg();
         HttpRespMsg msg = new HttpRespMsg();
-        String month = normalizeYearMonth(ymonth);
-        List<Map<String, Object>> userProjectList = reportMapper.selectMaps(new QueryWrapper<Report>()
+        DashboardDateFilter dateFilter = resolveDashboardDateFilter(ymonth, startDate, endDate, msg);
+        if (dateFilter == null) {
+            return msg;
+        }
+        QueryWrapper<Report> queryWrapper = new QueryWrapper<Report>()
                 .select("creator_id as userId", "count(distinct project_id) as projectCount")
                 .select("creator_id as userId", "count(distinct project_id) as projectCount")
                 .eq("company_id", companyId)
                 .eq("company_id", companyId)
-                .eq("state", 1)
-                .apply("date_format(create_date, '%Y-%m') = {0}", month)
+                .eq("state", 1);
+        dateFilter.applyDateFilter(queryWrapper);
+        List<Map<String, Object>> userProjectList = reportMapper.selectMaps(queryWrapper
                 .groupBy("creator_id")
                 .groupBy("creator_id")
                 .orderByDesc("count(distinct project_id)")
                 .orderByDesc("count(distinct project_id)")
                 .last("limit 10"));
                 .last("limit 10"));
@@ -16188,42 +16250,57 @@ public class ReportServiceImpl extends ServiceImpl<ReportMapper, Report> impleme
     }
     }
 
 
     @Override
     @Override
-    public HttpRespMsg getTop3ProjectReportGroupByDept(Integer companyId, String ymonth) {
-        return getTop3ProjectReportGroupByDept(companyId, ymonth, null);
+    public HttpRespMsg getTop3ProjectReportGroupByDept(Integer companyId, String ymonth, String startDate, String endDate) {
+        return getTop3ProjectReportGroupByDept(companyId, ymonth, startDate, endDate, null);
     }
     }
 
 
-    public HttpRespMsg getTop3ProjectReportGroupByDept(Integer companyId, String ymonth, String deptMode) {
+    @Override
+    public HttpRespMsg getTop3ProjectReportGroupByDept(Integer companyId, String ymonth, String startDate, String endDate, String deptMode) {
         HttpRespMsg msg = new HttpRespMsg();
         HttpRespMsg msg = new HttpRespMsg();
-        String month = normalizeYearMonth(ymonth);
+        DashboardDateFilter dateFilter = resolveDashboardDateFilter(ymonth, startDate, endDate, msg);
+        if (dateFilter == null) {
+            return msg;
+        }
+        msg.data = buildTop3ProjectReportGroupByDept(companyId, dateFilter, deptMode, false);
+        return msg;
+    }
+
+    private List<HashMap> buildTop3ProjectReportGroupByDept(Integer companyId, DashboardDateFilter dateFilter, String deptMode, boolean orderByOvertime) {
         boolean useTopLevel = "top".equals(deptMode);
         boolean useTopLevel = "top".equals(deptMode);
-        
-        List<Map<String, Object>> topProjectList = reportMapper.selectMaps(new QueryWrapper<Report>()
+
+        QueryWrapper<Report> topProjectQuery = new QueryWrapper<Report>()
                 .select("project_id as projectId", "sum(working_time) as workingTime")
                 .select("project_id as projectId", "sum(working_time) as workingTime")
                 .eq("company_id", companyId)
                 .eq("company_id", companyId)
-                .eq("state", 1)
-                .apply("date_format(create_date, '%Y-%m') = {0}", month)
-                .groupBy("project_id")
-                .orderByDesc("sum(working_time)")
-                .last("limit 3"));
+                .eq("state", 1);
+        dateFilter.applyDateFilter(topProjectQuery);
+        topProjectQuery.groupBy("project_id");
+        if (orderByOvertime) {
+            topProjectQuery.having("sum(ifnull(overtime_hours, 0)) > 0")
+                    .orderByDesc("sum(ifnull(overtime_hours, 0))");
+        } else {
+            topProjectQuery.orderByDesc("sum(working_time)");
+        }
+        topProjectQuery.last("limit 3");
+        List<Map<String, Object>> topProjectList = reportMapper.selectMaps(topProjectQuery);
         List<Integer> projectIds = topProjectList.stream()
         List<Integer> projectIds = topProjectList.stream()
                 .map(row -> toInteger(row.get("projectId")))
                 .map(row -> toInteger(row.get("projectId")))
                 .filter(Objects::nonNull)
                 .filter(Objects::nonNull)
                 .collect(Collectors.toList());
                 .collect(Collectors.toList());
         if (projectIds.isEmpty()) {
         if (projectIds.isEmpty()) {
-            msg.data = new ArrayList<>();
-            return msg;
+            return new ArrayList<>();
         }
         }
         Map<Integer, Integer> projectRankMap = new HashMap<>();
         Map<Integer, Integer> projectRankMap = new HashMap<>();
         for (int i = 0; i < projectIds.size(); i++) {
         for (int i = 0; i < projectIds.size(); i++) {
             projectRankMap.put(projectIds.get(i), i);
             projectRankMap.put(projectIds.get(i), i);
         }
         }
 
 
-        List<Map<String, Object>> projectDeptReportList = reportMapper.selectMaps(new QueryWrapper<Report>()
+        QueryWrapper<Report> projectDeptQuery = new QueryWrapper<Report>()
                 .select("dept_id as deptId", "project_id as projectId", "sum(working_time) as workingTime", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .select("dept_id as deptId", "project_id as projectId", "sum(working_time) as workingTime", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .eq("company_id", companyId)
                 .eq("company_id", companyId)
                 .eq("state", 1)
                 .eq("state", 1)
-                .apply("date_format(create_date, '%Y-%m') = {0}", month)
-                .in("project_id", projectIds)
+                .in("project_id", projectIds);
+        dateFilter.applyDateFilter(projectDeptQuery);
+        List<Map<String, Object>> projectDeptReportList = reportMapper.selectMaps(projectDeptQuery
                 .groupBy("dept_id", "project_id"));
                 .groupBy("dept_id", "project_id"));
         Map<Integer, Department> departmentMap = getDepartmentMap(companyId);
         Map<Integer, Department> departmentMap = getDepartmentMap(companyId);
         Map<String, HashMap<String, Object>> projectDeptMap = new LinkedHashMap<>();
         Map<String, HashMap<String, Object>> projectDeptMap = new LinkedHashMap<>();
@@ -16288,26 +16365,29 @@ public class ReportServiceImpl extends ServiceImpl<ReportMapper, Report> impleme
                     deptMap.put("overtimeHours", roundHours(toDouble(deptMap.get("overtimeHours")) + toDouble(item.get("overtimeHours"))));
                     deptMap.put("overtimeHours", roundHours(toDouble(deptMap.get("overtimeHours")) + toDouble(item.get("overtimeHours"))));
                     ((List<HashMap>) deptMap.get("items")).add(item);
                     ((List<HashMap>) deptMap.get("items")).add(item);
                 });
                 });
-        List<HashMap> resultList = new ArrayList<>(deptGroupMap.values());
-        msg.data = resultList;
-        return msg;
+        return new ArrayList<>(deptGroupMap.values());
     }
     }
+
     @Override
     @Override
-    public HttpRespMsg getProjectReportGroupByDept(Integer companyId, String ymonth) {
-        return getProjectReportGroupByDept(companyId, ymonth, null);
+    public HttpRespMsg getProjectReportGroupByDept(Integer companyId, String ymonth, String startDate, String endDate) {
+        return getProjectReportGroupByDept(companyId, ymonth, startDate, endDate, null);
     }
     }
 
 
     @Override
     @Override
-    public HttpRespMsg getProjectReportGroupByDept(Integer companyId, String ymonth, String deptMode) {
+    public HttpRespMsg getProjectReportGroupByDept(Integer companyId, String ymonth, String startDate, String endDate, String deptMode) {
         HttpRespMsg msg = new HttpRespMsg();
         HttpRespMsg msg = new HttpRespMsg();
-        String month = normalizeYearMonth(ymonth);
+        DashboardDateFilter dateFilter = resolveDashboardDateFilter(ymonth, startDate, endDate, msg);
+        if (dateFilter == null) {
+            return msg;
+        }
         boolean useTopLevel = "top".equals(deptMode);
         boolean useTopLevel = "top".equals(deptMode);
-        
-        List<Map<String, Object>> deptReportList = reportMapper.selectMaps(new QueryWrapper<Report>()
+
+        QueryWrapper<Report> deptReportQuery = new QueryWrapper<Report>()
                 .select("dept_id as deptId", "creator_id as userId", "sum(working_time) as workingTime", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .select("dept_id as deptId", "creator_id as userId", "sum(working_time) as workingTime", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .eq("company_id", companyId)
                 .eq("company_id", companyId)
-                .eq("state", 1)
-                .apply("date_format(create_date, '%Y-%m') = {0}", month)
+                .eq("state", 1);
+        dateFilter.applyDateFilter(deptReportQuery);
+        List<Map<String, Object>> deptReportList = reportMapper.selectMaps(deptReportQuery
                 .groupBy("dept_id", "creator_id")
                 .groupBy("dept_id", "creator_id")
                 .orderByDesc("sum(working_time)"));
                 .orderByDesc("sum(working_time)"));
         Map<Integer, Department> departmentMap = getDepartmentMap(companyId);
         Map<Integer, Department> departmentMap = getDepartmentMap(companyId);
@@ -16347,14 +16427,18 @@ public class ReportServiceImpl extends ServiceImpl<ReportMapper, Report> impleme
     }
     }
 
 
     @Override
     @Override
-    public HttpRespMsg getDeptProjectCount(Integer companyId, String ymonth) {
+    public HttpRespMsg getDeptProjectCount(Integer companyId, String ymonth, String startDate, String endDate) {
         HttpRespMsg msg = new HttpRespMsg();
         HttpRespMsg msg = new HttpRespMsg();
-        String month = normalizeYearMonth(ymonth);
-        List<Map<String, Object>> deptReportList = reportMapper.selectMaps(new QueryWrapper<Report>()
+        DashboardDateFilter dateFilter = resolveDashboardDateFilter(ymonth, startDate, endDate, msg);
+        if (dateFilter == null) {
+            return msg;
+        }
+        QueryWrapper<Report> deptReportQuery = new QueryWrapper<Report>()
                 .select("dept_id as deptId", "project_id as projectId", "creator_id as userId", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .select("dept_id as deptId", "project_id as projectId", "creator_id as userId", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .eq("company_id", companyId)
                 .eq("company_id", companyId)
-                .eq("state", 1)
-                .apply("date_format(create_date, '%Y-%m') = {0}", month)
+                .eq("state", 1);
+        dateFilter.applyDateFilter(deptReportQuery);
+        List<Map<String, Object>> deptReportList = reportMapper.selectMaps(deptReportQuery
                 .groupBy("dept_id", "project_id", "creator_id"));
                 .groupBy("dept_id", "project_id", "creator_id"));
         Map<Integer, Department> departmentMap = getDepartmentMap(companyId);
         Map<Integer, Department> departmentMap = getDepartmentMap(companyId);
         List<HashMap> resultList = new ArrayList<>();
         List<HashMap> resultList = new ArrayList<>();
@@ -16375,50 +16459,59 @@ public class ReportServiceImpl extends ServiceImpl<ReportMapper, Report> impleme
     }
     }
 
 
     @Override
     @Override
-    public HttpRespMsg getDashboardAnalysisReport(Integer companyId, String ymonth) {
+    public HttpRespMsg getDashboardAnalysisReport(Integer companyId, String ymonth, String startDate, String endDate) {
         HttpRespMsg msg = new HttpRespMsg();
         HttpRespMsg msg = new HttpRespMsg();
-        String month = normalizeYearMonth(ymonth);
+        DashboardDateFilter dateFilter = resolveDashboardDateFilter(ymonth, startDate, endDate, msg);
+        if (dateFilter == null) {
+            return msg;
+        }
         Map<Integer, Department> departmentMap = getDepartmentMap(companyId);
         Map<Integer, Department> departmentMap = getDepartmentMap(companyId);
         Map<String, Object> result = new HashMap<>();
         Map<String, Object> result = new HashMap<>();
-        result.put("projectSituation", getProjectSituation(companyId, month));
-        result.put("projectOvertimeTop5", getProjectRank(companyId, month, "sum(ifnull(overtime_hours, 0))", 5, true));
-        result.put("top3OvertimeProjectDept", getTopProjectDeptReport(companyId, month, "sum(working_time)", false));
-        List<Map<String, Object>> deptOvertimeSummaryList = reportMapper.selectMaps(new QueryWrapper<Report>()
+        result.put("projectSituation", getProjectSituation(companyId, dateFilter));
+        result.put("projectOvertimeTop5", getProjectRank(companyId, dateFilter, "sum(ifnull(overtime_hours, 0))", 5, true));
+        result.put("top3OvertimeProjectDept", buildTop3ProjectReportGroupByDept(companyId, dateFilter, "top", true));
+        QueryWrapper<Report> deptOvertimeQuery = new QueryWrapper<Report>()
                 .select("dept_id as deptId", "creator_id as userId", "sum(working_time) as workingTime", "sum(ifnull(overtime_hours, 0)) as overtimeHours", "max(ifnull(overtime_hours, 0)) as maxOvertimeHours")
                 .select("dept_id as deptId", "creator_id as userId", "sum(working_time) as workingTime", "sum(ifnull(overtime_hours, 0)) as overtimeHours", "max(ifnull(overtime_hours, 0)) as maxOvertimeHours")
                 .eq("company_id", companyId)
                 .eq("company_id", companyId)
-                .eq("state", 1)
-                .apply("date_format(create_date, '%Y-%m') = {0}", month)
+                .eq("state", 1);
+        dateFilter.applyDateFilter(deptOvertimeQuery);
+        List<Map<String, Object>> deptOvertimeSummaryList = reportMapper.selectMaps(deptOvertimeQuery
                 .groupBy("dept_id", "creator_id")
                 .groupBy("dept_id", "creator_id")
                 .orderByDesc("sum(ifnull(overtime_hours, 0))"));
                 .orderByDesc("sum(ifnull(overtime_hours, 0))"));
         result.put("deptOvertimeSummary", buildDeptReportResult(rollupDeptReportRows(deptOvertimeSummaryList, departmentMap), departmentMap, true));
         result.put("deptOvertimeSummary", buildDeptReportResult(rollupDeptReportRows(deptOvertimeSummaryList, departmentMap), departmentMap, true));
-        result.put("deptProjectTop3", getDeptProjectTopN(companyId, month, departmentMap, false));
-        result.put("deptOvertimeProjectTop3", getDeptProjectTopN(companyId, month, departmentMap, true));
-        result.put("deptUserOvertimeTop3", getDeptUserOvertimeTopN(companyId, month, departmentMap));
-        result.put("userOvertimeTop10", getUserOvertimeTopN(companyId, month, departmentMap));
+        result.put("deptProjectTop3", getDeptProjectTopN(companyId, dateFilter, departmentMap, false));
+        result.put("deptOvertimeProjectTop3", getDeptProjectTopN(companyId, dateFilter, departmentMap, true));
+        result.put("deptUserOvertimeTop3", getDeptUserOvertimeTopN(companyId, dateFilter, departmentMap));
+        result.put("userOvertimeTop10", getUserOvertimeTopN(companyId, dateFilter, departmentMap));
         msg.data = result;
         msg.data = result;
         return msg;
         return msg;
     }
     }
 
 
     @Override
     @Override
-    public HttpRespMsg getWorkedProjectList(Integer companyId, String ymonth, Integer pageIndex, Integer pageSize) {
+    public HttpRespMsg getWorkedProjectList(Integer companyId, String ymonth, String startDate, String endDate, Integer pageIndex, Integer pageSize) {
         HttpRespMsg msg = new HttpRespMsg();
         HttpRespMsg msg = new HttpRespMsg();
-        String month = normalizeYearMonth(ymonth);
+        DashboardDateFilter dateFilter = resolveDashboardDateFilter(ymonth, startDate, endDate, msg);
+        if (dateFilter == null) {
+            return msg;
+        }
         int currentPage = pageIndex == null || pageIndex < 1 ? 1 : pageIndex;
         int currentPage = pageIndex == null || pageIndex < 1 ? 1 : pageIndex;
         int size = pageSize == null || pageSize < 1 ? 30 : Math.min(pageSize, 100);
         int size = pageSize == null || pageSize < 1 ? 30 : Math.min(pageSize, 100);
         int offset = (currentPage - 1) * size;
         int offset = (currentPage - 1) * size;
 
 
-        List<Map<String, Object>> countList = reportMapper.selectMaps(new QueryWrapper<Report>()
+        QueryWrapper<Report> countQuery = new QueryWrapper<Report>()
                 .select("count(distinct project_id) as total")
                 .select("count(distinct project_id) as total")
                 .eq("company_id", companyId)
                 .eq("company_id", companyId)
-                .eq("state", 1)
-                .apply("date_format(create_date, '%Y-%m') = {0}", month));
+                .eq("state", 1);
+        dateFilter.applyDateFilter(countQuery);
+        List<Map<String, Object>> countList = reportMapper.selectMaps(countQuery);
         int total = countList.isEmpty() ? 0 : toInteger(countList.get(0).get("total"));
         int total = countList.isEmpty() ? 0 : toInteger(countList.get(0).get("total"));
 
 
-        List<Map<String, Object>> reportList = reportMapper.selectMaps(new QueryWrapper<Report>()
+        QueryWrapper<Report> reportQuery = new QueryWrapper<Report>()
                 .select("project_id as projectId", "sum(working_time) as workingTime", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .select("project_id as projectId", "sum(working_time) as workingTime", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .eq("company_id", companyId)
                 .eq("company_id", companyId)
-                .eq("state", 1)
-                .apply("date_format(create_date, '%Y-%m') = {0}", month)
+                .eq("state", 1);
+        dateFilter.applyDateFilter(reportQuery);
+        List<Map<String, Object>> reportList = reportMapper.selectMaps(reportQuery
                 .groupBy("project_id")
                 .groupBy("project_id")
                 .orderByDesc("sum(working_time)")
                 .orderByDesc("sum(working_time)")
                 .last("limit " + offset + "," + size));
                 .last("limit " + offset + "," + size));
@@ -16557,15 +16650,15 @@ public class ReportServiceImpl extends ServiceImpl<ReportMapper, Report> impleme
         return result;
         return result;
     }
     }
 
 
-    private HashMap<String, Object> getProjectSituation(Integer companyId, String month) {
+    private HashMap<String, Object> getProjectSituation(Integer companyId, DashboardDateFilter dateFilter) {
         long projectCount = projectMapper.selectCount(new QueryWrapper<Project>().eq("company_id", companyId));
         long projectCount = projectMapper.selectCount(new QueryWrapper<Project>().eq("company_id", companyId));
         long executingProjectCount = projectMapper.selectCount(new QueryWrapper<Project>().eq("company_id", companyId).eq("status", 1));
         long executingProjectCount = projectMapper.selectCount(new QueryWrapper<Project>().eq("company_id", companyId).eq("status", 1));
-        List<Map<String, Object>> workedProjectList = reportMapper.selectMaps(new QueryWrapper<Report>()
+        QueryWrapper<Report> workedProjectQuery = new QueryWrapper<Report>()
                 .select("project_id as projectId", "sum(working_time) as workingTime", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .select("project_id as projectId", "sum(working_time) as workingTime", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .eq("company_id", companyId)
                 .eq("company_id", companyId)
-                .eq("state", 1)
-                .apply("date_format(create_date, '%Y-%m') = {0}", month)
-                .groupBy("project_id"));
+                .eq("state", 1);
+        dateFilter.applyDateFilter(workedProjectQuery);
+        List<Map<String, Object>> workedProjectList = reportMapper.selectMaps(workedProjectQuery.groupBy("project_id"));
         double totalWorkingTime = workedProjectList.stream().mapToDouble(row -> toDouble(row.get("workingTime"))).sum();
         double totalWorkingTime = workedProjectList.stream().mapToDouble(row -> toDouble(row.get("workingTime"))).sum();
         double totalOvertimeHours = workedProjectList.stream().mapToDouble(row -> toDouble(row.get("overtimeHours"))).sum();
         double totalOvertimeHours = workedProjectList.stream().mapToDouble(row -> toDouble(row.get("overtimeHours"))).sum();
         HashMap<String, Object> map = new HashMap<>();
         HashMap<String, Object> map = new HashMap<>();
@@ -16578,13 +16671,13 @@ public class ReportServiceImpl extends ServiceImpl<ReportMapper, Report> impleme
         return map;
         return map;
     }
     }
 
 
-    private List<HashMap> getProjectRank(Integer companyId, String month, String orderExpression, int limit, boolean overtimeOnly) {
+    private List<HashMap> getProjectRank(Integer companyId, DashboardDateFilter dateFilter, String orderExpression, int limit, boolean overtimeOnly) {
         QueryWrapper<Report> queryWrapper = new QueryWrapper<Report>()
         QueryWrapper<Report> queryWrapper = new QueryWrapper<Report>()
                 .select("project_id as projectId", "sum(working_time) as workingTime", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .select("project_id as projectId", "sum(working_time) as workingTime", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .eq("company_id", companyId)
                 .eq("company_id", companyId)
-                .eq("state", 1)
-                .apply("date_format(create_date, '%Y-%m') = {0}", month)
-                .groupBy("project_id")
+                .eq("state", 1);
+        dateFilter.applyDateFilter(queryWrapper);
+        queryWrapper.groupBy("project_id")
                 .having(overtimeOnly, "sum(ifnull(overtime_hours, 0)) > 0")
                 .having(overtimeOnly, "sum(ifnull(overtime_hours, 0)) > 0")
                 .orderByDesc(orderExpression);
                 .orderByDesc(orderExpression);
         if (limit > 0 && limit < Integer.MAX_VALUE) {
         if (limit > 0 && limit < Integer.MAX_VALUE) {
@@ -16594,12 +16687,13 @@ public class ReportServiceImpl extends ServiceImpl<ReportMapper, Report> impleme
         return fillProjectInfo(companyId, reportList);
         return fillProjectInfo(companyId, reportList);
     }
     }
 
 
-    private List<HashMap> getTopProjectDeptReport(Integer companyId, String month, String orderExpression, boolean orderByOvertime) {
-        List<Map<String, Object>> topProjectList = reportMapper.selectMaps(new QueryWrapper<Report>()
+    private List<HashMap> getTopProjectDeptReport(Integer companyId, DashboardDateFilter dateFilter, String orderExpression, boolean orderByOvertime) {
+        QueryWrapper<Report> topProjectQuery = new QueryWrapper<Report>()
                 .select("project_id as projectId")
                 .select("project_id as projectId")
                 .eq("company_id", companyId)
                 .eq("company_id", companyId)
-                .eq("state", 1)
-                .apply("date_format(create_date, '%Y-%m') = {0}", month)
+                .eq("state", 1);
+        dateFilter.applyDateFilter(topProjectQuery);
+        List<Map<String, Object>> topProjectList = reportMapper.selectMaps(topProjectQuery
                 .groupBy("project_id")
                 .groupBy("project_id")
                 .having(orderByOvertime, "sum(ifnull(overtime_hours, 0)) > 0")
                 .having(orderByOvertime, "sum(ifnull(overtime_hours, 0)) > 0")
                 .orderByDesc(orderExpression)
                 .orderByDesc(orderExpression)
@@ -16611,23 +16705,25 @@ public class ReportServiceImpl extends ServiceImpl<ReportMapper, Report> impleme
         if (projectIds.isEmpty()) {
         if (projectIds.isEmpty()) {
             return new ArrayList<>();
             return new ArrayList<>();
         }
         }
-        List<Map<String, Object>> deptReportList = reportMapper.selectMaps(new QueryWrapper<Report>()
+        QueryWrapper<Report> deptReportQuery = new QueryWrapper<Report>()
                 .select("dept_id as deptId", "creator_id as userId", "sum(working_time) as workingTime", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .select("dept_id as deptId", "creator_id as userId", "sum(working_time) as workingTime", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .eq("company_id", companyId)
                 .eq("company_id", companyId)
                 .eq("state", 1)
                 .eq("state", 1)
-                .apply("date_format(create_date, '%Y-%m') = {0}", month)
-                .in("project_id", projectIds)
+                .in("project_id", projectIds);
+        dateFilter.applyDateFilter(deptReportQuery);
+        List<Map<String, Object>> deptReportList = reportMapper.selectMaps(deptReportQuery
                 .groupBy("dept_id", "creator_id")
                 .groupBy("dept_id", "creator_id")
                 .orderByDesc(orderByOvertime ? "sum(ifnull(overtime_hours, 0))" : "sum(working_time)"));
                 .orderByDesc(orderByOvertime ? "sum(ifnull(overtime_hours, 0))" : "sum(working_time)"));
         Map<Integer, Department> departmentMap = getDepartmentMap(companyId);
         Map<Integer, Department> departmentMap = getDepartmentMap(companyId);
         return buildDeptReportResult(rollupDeptReportRows(deptReportList, departmentMap), departmentMap, false);
         return buildDeptReportResult(rollupDeptReportRows(deptReportList, departmentMap), departmentMap, false);
     }
     }
-    private List<HashMap> getDeptProjectTopN(Integer companyId, String month, Map<Integer, Department> departmentMap, boolean overtimeOnly) {
-        List<Map<String, Object>> reportList = reportMapper.selectMaps(new QueryWrapper<Report>()
+    private List<HashMap> getDeptProjectTopN(Integer companyId, DashboardDateFilter dateFilter, Map<Integer, Department> departmentMap, boolean overtimeOnly) {
+        QueryWrapper<Report> reportQuery = new QueryWrapper<Report>()
                 .select("dept_id as deptId", "project_id as projectId", "sum(working_time) as workingTime", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .select("dept_id as deptId", "project_id as projectId", "sum(working_time) as workingTime", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .eq("company_id", companyId)
                 .eq("company_id", companyId)
-                .eq("state", 1)
-                .apply("date_format(create_date, '%Y-%m') = {0}", month)
+                .eq("state", 1);
+        dateFilter.applyDateFilter(reportQuery);
+        List<Map<String, Object>> reportList = reportMapper.selectMaps(reportQuery
                 .groupBy("dept_id", "project_id")
                 .groupBy("dept_id", "project_id")
                 .having(overtimeOnly, "sum(ifnull(overtime_hours, 0)) > 0"));
                 .having(overtimeOnly, "sum(ifnull(overtime_hours, 0)) > 0"));
         Map<String, HashMap<String, Object>> projectGroupMap = new LinkedHashMap<>();
         Map<String, HashMap<String, Object>> projectGroupMap = new LinkedHashMap<>();
@@ -16667,12 +16763,13 @@ public class ReportServiceImpl extends ServiceImpl<ReportMapper, Report> impleme
         return resultList;
         return resultList;
     }
     }
 
 
-    private List<HashMap> getDeptUserOvertimeTopN(Integer companyId, String month, Map<Integer, Department> departmentMap) {
-        List<Map<String, Object>> reportList = reportMapper.selectMaps(new QueryWrapper<Report>()
+    private List<HashMap> getDeptUserOvertimeTopN(Integer companyId, DashboardDateFilter dateFilter, Map<Integer, Department> departmentMap) {
+        QueryWrapper<Report> reportQuery = new QueryWrapper<Report>()
                 .select("dept_id as deptId", "creator_id as userId", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .select("dept_id as deptId", "creator_id as userId", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .eq("company_id", companyId)
                 .eq("company_id", companyId)
-                .eq("state", 1)
-                .apply("date_format(create_date, '%Y-%m') = {0}", month)
+                .eq("state", 1);
+        dateFilter.applyDateFilter(reportQuery);
+        List<Map<String, Object>> reportList = reportMapper.selectMaps(reportQuery
                 .groupBy("dept_id", "creator_id")
                 .groupBy("dept_id", "creator_id")
                 .having("sum(ifnull(overtime_hours, 0)) > 0"));
                 .having("sum(ifnull(overtime_hours, 0)) > 0"));
         List<String> userIds = reportList.stream().map(row -> String.valueOf(row.get("userId"))).filter(id -> !StringUtils.isEmpty(id)).collect(Collectors.toList());
         List<String> userIds = reportList.stream().map(row -> String.valueOf(row.get("userId"))).filter(id -> !StringUtils.isEmpty(id)).collect(Collectors.toList());
@@ -16716,12 +16813,13 @@ public class ReportServiceImpl extends ServiceImpl<ReportMapper, Report> impleme
         return resultList;
         return resultList;
     }
     }
 
 
-    private List<HashMap> getUserOvertimeTopN(Integer companyId, String month, Map<Integer, Department> departmentMap) {
-        List<Map<String, Object>> reportList = reportMapper.selectMaps(new QueryWrapper<Report>()
+    private List<HashMap> getUserOvertimeTopN(Integer companyId, DashboardDateFilter dateFilter, Map<Integer, Department> departmentMap) {
+        QueryWrapper<Report> reportQuery = new QueryWrapper<Report>()
                 .select("creator_id as userId", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .select("creator_id as userId", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
                 .eq("company_id", companyId)
                 .eq("company_id", companyId)
-                .eq("state", 1)
-                .apply("date_format(create_date, '%Y-%m') = {0}", month)
+                .eq("state", 1);
+        dateFilter.applyDateFilter(reportQuery);
+        List<Map<String, Object>> reportList = reportMapper.selectMaps(reportQuery
                 .groupBy("creator_id")
                 .groupBy("creator_id")
                 .having("sum(ifnull(overtime_hours, 0)) > 0"));
                 .having("sum(ifnull(overtime_hours, 0)) > 0"));
         List<String> userIds = reportList.stream()
         List<String> userIds = reportList.stream()

+ 2 - 2
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/task/DataCollectTask.java

@@ -214,8 +214,8 @@ public class DataCollectTask {
      */
      */
     @Scheduled(cron ="0 05 3 * * ?")
     @Scheduled(cron ="0 05 3 * * ?")
     public void prdOrderData(){
     public void prdOrderData(){
-//        if(isDev){return;}
-//        if(isPrivateDeploy) return;
+        if(isDev){return;}
+        if(isPrivateDeploy) return;
         LocalDate nowDate = LocalDate.now();
         LocalDate nowDate = LocalDate.now();
         RestTemplate restTemplate = new RestTemplate();
         RestTemplate restTemplate = new RestTemplate();
         String getUrl = PREFIX_URL+"/dataCollect/getProdOrderData";
         String getUrl = PREFIX_URL+"/dataCollect/getProdOrderData";

+ 576 - 70
fhKeeper/formulahousekeeper/timesheet/src/views/dashboard/index.vue

@@ -6,16 +6,63 @@
         <h2 class="dashboard-title">工时看板</h2>
         <h2 class="dashboard-title">工时看板</h2>
         <div class="dashboard-subtitle">项目、部门与加班数据概览</div>
         <div class="dashboard-subtitle">项目、部门与加班数据概览</div>
       </div>
       </div>
-      <el-date-picker
-        v-model="selectedMonth"
-        type="month"
-        placeholder="选择年月"
-        value-format="yyyy-MM"
-        :clearable="false"
-        @change="onMonthChange"
-        size="small"
-        style="width: 160px"
-      ></el-date-picker>
+      <div class="dashboard-filter">
+        <el-radio-group
+          v-model="filterMode"
+          size="small"
+          @change="onFilterModeChange"
+        >
+          <el-radio-button label="month">按月</el-radio-button>
+          <el-radio-button label="range">按区间</el-radio-button>
+        </el-radio-group>
+        <div
+          class="dashboard-date-picker"
+          :class="filterMode === 'month' ? 'is-month' : 'is-range'"
+        >
+          <div
+            v-show="filterMode === 'month'"
+            class="dashboard-date-picker-slot"
+          >
+            <el-date-picker
+              ref="monthDatePicker"
+              class="dashboard-date-picker-input"
+              v-model="selectedMonth"
+              type="month"
+              placeholder="选择年月"
+              value-format="yyyy-MM"
+              :clearable="false"
+              align="right"
+              :append-to-body="true"
+              popper-class="dashboard-date-picker-popper"
+              @focus="onDatePickerFocus"
+              @change="onMonthChange"
+              size="small"
+            ></el-date-picker>
+          </div>
+          <div
+            v-show="filterMode === 'range'"
+            class="dashboard-date-picker-slot"
+          >
+            <el-date-picker
+              ref="rangeDatePicker"
+              class="dashboard-date-picker-input"
+              v-model="dateRange"
+              type="daterange"
+              align="right"
+              range-separator="至"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              value-format="yyyy-MM-dd"
+              :clearable="false"
+              :append-to-body="true"
+              popper-class="dashboard-date-picker-popper"
+              @focus="onDatePickerFocus"
+              @change="onDateRangeChange"
+              size="small"
+            ></el-date-picker>
+          </div>
+        </div>
+      </div>
     </div>
     </div>
 
 
     <el-dialog
     <el-dialog
@@ -241,13 +288,13 @@
           <i class="el-icon-loading"></i> 加载中...
           <i class="el-icon-loading"></i> 加载中...
         </div>
         </div>
         <div
         <div
-          v-else-if="top3OvertimeProjectDept.length === 0"
+          v-else-if="!hasTop3OvertimeChartData()"
           class="chart-empty project-rank-loading"
           class="chart-empty project-rank-loading"
         >
         >
           暂无数据
           暂无数据
         </div>
         </div>
         <div
         <div
-          v-show="!loading5 && top3OvertimeProjectDept.length > 0"
+          v-show="!loading5 && hasTop3OvertimeChartData()"
           ref="chart6"
           ref="chart6"
           class="chart-body project-rank-chart"
           class="chart-body project-rank-chart"
         ></div>
         ></div>
@@ -381,9 +428,15 @@ export default {
     const now = new Date();
     const now = new Date();
     const year = now.getFullYear();
     const year = now.getFullYear();
     const month = String(now.getMonth() + 1).padStart(2, "0");
     const month = String(now.getMonth() + 1).padStart(2, "0");
+    const lastDay = new Date(year, now.getMonth() + 1, 0).getDate();
     return {
     return {
       user: JSON.parse(sessionStorage.getItem("user")),
       user: JSON.parse(sessionStorage.getItem("user")),
+      filterMode: "month",
       selectedMonth: `${year}-${month}`,
       selectedMonth: `${year}-${month}`,
+      dateRange: [
+        `${year}-${month}-01`,
+        `${year}-${month}-${String(lastDay).padStart(2, "0")}`,
+      ],
       loading1: false,
       loading1: false,
       loading2: false,
       loading2: false,
       loading3: false,
       loading3: false,
@@ -423,9 +476,19 @@ export default {
   mounted() {
   mounted() {
     this.loadAllCharts();
     this.loadAllCharts();
     window.addEventListener("resize", this.handleResize);
     window.addEventListener("resize", this.handleResize);
+    this.contentScroller = document.querySelector(".content-container");
+    if (this.contentScroller) {
+      this.handleContentScroll = () => this.updateOpenDatePickers();
+      this.contentScroller.addEventListener("scroll", this.handleContentScroll, {
+        passive: true,
+      });
+    }
   },
   },
   beforeDestroy() {
   beforeDestroy() {
     window.removeEventListener("resize", this.handleResize);
     window.removeEventListener("resize", this.handleResize);
+    if (this.contentScroller && this.handleContentScroll) {
+      this.contentScroller.removeEventListener("scroll", this.handleContentScroll);
+    }
     this.removeWorkedProjectScrollListener();
     this.removeWorkedProjectScrollListener();
     for (let i = 1; i <= 9; i++) {
     for (let i = 1; i <= 9; i++) {
       const chart = this[`chart${i}`];
       const chart = this[`chart${i}`];
@@ -518,9 +581,12 @@ export default {
         {
         {
           key: "top3OvertimeProjectDept",
           key: "top3OvertimeProjectDept",
           title: "加班前三项目部门参与情况",
           title: "加班前三项目部门参与情况",
-          data: this.analysisData.top3OvertimeProjectDept || [],
+          data: this.flattenDeptRows(this.analysisData.top3OvertimeProjectDept).map(
+            (item) => ({ ...item, projectText: this.projectText(item) }),
+          ),
           columns: [
           columns: [
             { prop: "departmentName", label: "部门" },
             { prop: "departmentName", label: "部门" },
+            { prop: "projectText", label: "项目" },
             { prop: "overtimeHours", label: "加班工时(h)" },
             { prop: "overtimeHours", label: "加班工时(h)" },
             { prop: "workingTime", label: "总工时(h)" },
             { prop: "workingTime", label: "总工时(h)" },
           ],
           ],
@@ -579,10 +645,144 @@ export default {
   },
   },
   methods: {
   methods: {
     // ========== 基础工具方法 ==========
     // ========== 基础工具方法 ==========
+    getQueryParams() {
+      if (this.filterMode === "range") {
+        return {
+          startDate: this.dateRange[0],
+          endDate: this.dateRange[1],
+        };
+      }
+      return { ymonth: this.selectedMonth };
+    },
+    getQueryToken() {
+      if (this.filterMode === "range") {
+        return `range_${this.dateRange[0]}_${this.dateRange[1]}`;
+      }
+      return `month_${this.selectedMonth}`;
+    },
+    validateDateRange() {
+      if (this.filterMode !== "range") return true;
+      if (!this.dateRange || this.dateRange.length !== 2) {
+        this.$message({ message: "请选择开始和结束日期", type: "warning" });
+        return false;
+      }
+      const start = new Date(this.dateRange[0]);
+      const end = new Date(this.dateRange[1]);
+      if (end < start) {
+        this.$message({ message: "结束日期不能早于开始日期", type: "warning" });
+        return false;
+      }
+      const days =
+        Math.floor((end.getTime() - start.getTime()) / (24 * 3600 * 1000)) + 1;
+      if (days > 366) {
+        this.$message({
+          message: "查询时间区间最多不能超过一年",
+          type: "warning",
+        });
+        return false;
+      }
+      return true;
+    },
+    onFilterModeChange() {
+      this.closeDashboardDatePickers();
+      this.resetWorkedProjectDialog();
+      if (this.validateDateRange()) {
+        this.loadAllCharts();
+      }
+    },
+    onDateRangeChange() {
+      if (!this.validateDateRange()) return;
+      this.resetWorkedProjectDialog();
+      this.loadAllCharts();
+    },
     onMonthChange() {
     onMonthChange() {
       this.resetWorkedProjectDialog();
       this.resetWorkedProjectDialog();
       this.loadAllCharts();
       this.loadAllCharts();
     },
     },
+    onDatePickerFocus() {
+      [0, 30, 80, 150].forEach((delay) => {
+        setTimeout(() => this.fixDatePickerPopperPosition(), delay);
+      });
+    },
+    getDatePickerReference(picker) {
+      if (!picker) return null;
+      const ref = picker.reference || picker.$refs.reference;
+      if (!ref) return null;
+      return ref.$el || ref;
+    },
+    getDatePickerPopperEl(picker) {
+      if (!picker) return null;
+      return picker.popperElm || (picker.picker && picker.picker.$el) || null;
+    },
+    fixDatePickerPopperPosition(picker) {
+      picker = picker || this.getActiveDatePickerRef();
+      if (!picker || !picker.pickerVisible) return;
+
+      const reference = this.getDatePickerReference(picker);
+      const popper = this.getDatePickerPopperEl(picker);
+      if (!reference || !popper) return;
+
+      if (popper.parentNode !== document.body) {
+        document.body.appendChild(popper);
+      }
+
+      if (picker.popperJS && picker.popperJS.destroy) {
+        picker.popperJS.destroy();
+        picker.popperJS = null;
+      }
+
+      const rect = reference.getBoundingClientRect();
+      if (!rect.width && !rect.height) return;
+
+      const gap = 6;
+      const margin = 8;
+      const popperWidth =
+        popper.offsetWidth || (picker.type === "daterange" ? 646 : 322);
+      const popperHeight = popper.offsetHeight || 320;
+
+      let left = rect.right - popperWidth;
+      let top = rect.bottom + gap;
+
+      if (left < margin) left = margin;
+      if (left + popperWidth > window.innerWidth - margin) {
+        left = window.innerWidth - popperWidth - margin;
+      }
+      if (top + popperHeight > window.innerHeight - margin) {
+        top = rect.top - popperHeight - gap;
+      }
+
+      popper.style.position = "fixed";
+      popper.style.top = `${Math.max(margin, top)}px`;
+      popper.style.left = `${left}px`;
+      popper.style.right = "auto";
+      popper.style.bottom = "auto";
+      popper.style.transform = "none";
+      popper.style.margin = "0";
+      popper.style.zIndex = "3000";
+    },
+    getActiveDatePickerRef() {
+      return this.filterMode === "month"
+        ? this.$refs.monthDatePicker
+        : this.$refs.rangeDatePicker;
+    },
+    closeDashboardDatePickers() {
+      ["monthDatePicker", "rangeDatePicker"].forEach((refName) => {
+        const picker = this.$refs[refName];
+        if (!picker) return;
+        picker.pickerVisible = false;
+        if (picker.picker) {
+          picker.picker.visible = false;
+        }
+        if (picker.popperJS && picker.popperJS.destroy) {
+          picker.popperJS.destroy();
+          picker.popperJS = null;
+        }
+        picker.referenceElm = null;
+      });
+    },
+    updateOpenDatePickers() {
+      this.fixDatePickerPopperPosition();
+    },
 
 
     openDataType(prop) {
     openDataType(prop) {
       return prop === "departmentName" ? "departmentName" : "userName";
       return prop === "departmentName" ? "departmentName" : "userName";
@@ -655,7 +855,7 @@ export default {
       this.http.post(
       this.http.post(
         "/report/getWorkedProjectList",
         "/report/getWorkedProjectList",
         {
         {
-          ymonth: this.selectedMonth,
+          ...this.getQueryParams(),
           pageIndex,
           pageIndex,
           pageSize: this.workedProjectPageSize,
           pageSize: this.workedProjectPageSize,
         },
         },
@@ -712,19 +912,25 @@ export default {
       );
       );
     },
     },
 
 
-    isCurrentMonthRequest(month) {
-      return month === this.selectedMonth;
+    hasTop3OvertimeChartData() {
+      return (this.top3OvertimeProjectDept || []).some((dept) =>
+        (dept.items || []).some((item) => Number(item.overtimeHours || 0) > 0),
+      );
+    },
+
+    isCurrentQueryRequest(token) {
+      return token === this.getQueryToken();
     },
     },
 
 
     loadTop10Project() {
     loadTop10Project() {
-      const requestMonth = this.selectedMonth;
+      const requestToken = this.getQueryToken();
       this.loading1 = true;
       this.loading1 = true;
       this.top10Data = [];
       this.top10Data = [];
       this.http.post(
       this.http.post(
         "/report/getTop10ProjectReport",
         "/report/getTop10ProjectReport",
-        { ymonth: requestMonth },
+        this.getQueryParams(),
         (res) => {
         (res) => {
-          if (!this.isCurrentMonthRequest(requestMonth)) return;
+          if (!this.isCurrentQueryRequest(requestToken)) return;
           this.loading1 = false;
           this.loading1 = false;
           if (res.code === "ok") {
           if (res.code === "ok") {
             this.top10Data = res.data || [];
             this.top10Data = res.data || [];
@@ -734,7 +940,7 @@ export default {
           }
           }
         },
         },
         (err) => {
         (err) => {
-          if (!this.isCurrentMonthRequest(requestMonth)) return;
+          if (!this.isCurrentQueryRequest(requestToken)) return;
           this.loading1 = false;
           this.loading1 = false;
           this.$message({ message: err, type: "error" });
           this.$message({ message: err, type: "error" });
         },
         },
@@ -746,7 +952,7 @@ export default {
     },
     },
 
 
     loadTop3ProjectDept() {
     loadTop3ProjectDept() {
-      const requestMonth = this.selectedMonth;
+      const requestToken = this.getQueryToken();
       this.loading2 = true;
       this.loading2 = true;
       this.top3DeptData = [];
       this.top3DeptData = [];
       this.top3DeptDataAll = [];
       this.top3DeptDataAll = [];
@@ -754,9 +960,9 @@ export default {
       const loadMode = (deptMode, storeKey, callback) => {
       const loadMode = (deptMode, storeKey, callback) => {
         this.http.post(
         this.http.post(
           "/report/getTop3ProjectReportGroupByDept",
           "/report/getTop3ProjectReportGroupByDept",
-          { ymonth: requestMonth, deptMode },
+          { ...this.getQueryParams(), deptMode },
           (res) => {
           (res) => {
-            if (!this.isCurrentMonthRequest(requestMonth)) return;
+            if (!this.isCurrentQueryRequest(requestToken)) return;
             if (res.code === "ok") {
             if (res.code === "ok") {
               const data = res.data || [];
               const data = res.data || [];
               const processData = (translated) => {
               const processData = (translated) => {
@@ -773,7 +979,7 @@ export default {
             }
             }
           },
           },
           (err) => {
           (err) => {
-            if (!this.isCurrentMonthRequest(requestMonth)) return;
+            if (!this.isCurrentQueryRequest(requestToken)) return;
             this.$message({ message: err, type: "error" });
             this.$message({ message: err, type: "error" });
           },
           },
         );
         );
@@ -808,7 +1014,7 @@ export default {
     },
     },
 
 
     loadDeptHours() {
     loadDeptHours() {
-      const requestMonth = this.selectedMonth;
+      const requestToken = this.getQueryToken();
       this.loading3 = true;
       this.loading3 = true;
       this.deptHoursData = [];
       this.deptHoursData = [];
       this.deptHoursDataAll = [];
       this.deptHoursDataAll = [];
@@ -816,9 +1022,9 @@ export default {
       const loadMode = (deptMode, storeKey, callback) => {
       const loadMode = (deptMode, storeKey, callback) => {
         this.http.post(
         this.http.post(
           "/report/getProjectReportGroupByDept",
           "/report/getProjectReportGroupByDept",
-          { ymonth: requestMonth, deptMode },
+          { ...this.getQueryParams(), deptMode },
           (res) => {
           (res) => {
-            if (!this.isCurrentMonthRequest(requestMonth)) return;
+            if (!this.isCurrentQueryRequest(requestToken)) return;
             if (res.code === "ok") {
             if (res.code === "ok") {
               const data = res.data || [];
               const data = res.data || [];
               const processData = (translated) => {
               const processData = (translated) => {
@@ -835,7 +1041,7 @@ export default {
             }
             }
           },
           },
           (err) => {
           (err) => {
-            if (!this.isCurrentMonthRequest(requestMonth)) return;
+            if (!this.isCurrentQueryRequest(requestToken)) return;
             this.$message({ message: err, type: "error" });
             this.$message({ message: err, type: "error" });
           },
           },
         );
         );
@@ -861,14 +1067,14 @@ export default {
     },
     },
 
 
     loadDeptProjectCount() {
     loadDeptProjectCount() {
-      const requestMonth = this.selectedMonth;
+      const requestToken = this.getQueryToken();
       this.loading4 = true;
       this.loading4 = true;
       this.deptProjectCountData = [];
       this.deptProjectCountData = [];
       this.http.post(
       this.http.post(
         "/report/getDeptProjectCount",
         "/report/getDeptProjectCount",
-        { ymonth: requestMonth },
+        this.getQueryParams(),
         (res) => {
         (res) => {
-          if (!this.isCurrentMonthRequest(requestMonth)) return;
+          if (!this.isCurrentQueryRequest(requestToken)) return;
           this.loading4 = false;
           this.loading4 = false;
           if (res.code === "ok") {
           if (res.code === "ok") {
             const data = res.data || [];
             const data = res.data || [];
@@ -886,7 +1092,7 @@ export default {
           }
           }
         },
         },
         (err) => {
         (err) => {
-          if (!this.isCurrentMonthRequest(requestMonth)) return;
+          if (!this.isCurrentQueryRequest(requestToken)) return;
           this.loading4 = false;
           this.loading4 = false;
           this.$message({ message: err, type: "error" });
           this.$message({ message: err, type: "error" });
         },
         },
@@ -894,14 +1100,14 @@ export default {
     },
     },
 
 
     loadDashboardAnalysis() {
     loadDashboardAnalysis() {
-      const requestMonth = this.selectedMonth;
+      const requestToken = this.getQueryToken();
       this.loading5 = true;
       this.loading5 = true;
       this.analysisData = {};
       this.analysisData = {};
       this.http.post(
       this.http.post(
         "/report/getDashboardAnalysisReport",
         "/report/getDashboardAnalysisReport",
-        { ymonth: requestMonth },
+        this.getQueryParams(),
         (res) => {
         (res) => {
-          if (!this.isCurrentMonthRequest(requestMonth)) return;
+          if (!this.isCurrentQueryRequest(requestToken)) return;
           this.loading5 = false;
           this.loading5 = false;
           if (res.code === "ok") {
           if (res.code === "ok") {
             this.analysisData = res.data || {};
             this.analysisData = res.data || {};
@@ -918,7 +1124,7 @@ export default {
           }
           }
         },
         },
         (err) => {
         (err) => {
-          if (!this.isCurrentMonthRequest(requestMonth)) return;
+          if (!this.isCurrentQueryRequest(requestToken)) return;
           this.loading5 = false;
           this.loading5 = false;
           this.$message({ message: err, type: "error" });
           this.$message({ message: err, type: "error" });
         },
         },
@@ -926,14 +1132,14 @@ export default {
     },
     },
 
 
     loadUserProjectTop10() {
     loadUserProjectTop10() {
-      const requestMonth = this.selectedMonth;
+      const requestToken = this.getQueryToken();
       this.loading9 = true;
       this.loading9 = true;
       this.userProjectTop10Data = [];
       this.userProjectTop10Data = [];
       this.http.post(
       this.http.post(
         "/report/getUserProjectTop10",
         "/report/getUserProjectTop10",
-        { ymonth: requestMonth },
+        this.getQueryParams(),
         (res) => {
         (res) => {
-          if (!this.isCurrentMonthRequest(requestMonth)) return;
+          if (!this.isCurrentQueryRequest(requestToken)) return;
           this.loading9 = false;
           this.loading9 = false;
           if (res.code === "ok") {
           if (res.code === "ok") {
             const data = res.data || [];
             const data = res.data || [];
@@ -951,7 +1157,7 @@ export default {
           }
           }
         },
         },
         (err) => {
         (err) => {
-          if (!this.isCurrentMonthRequest(requestMonth)) return;
+          if (!this.isCurrentQueryRequest(requestToken)) return;
           this.loading9 = false;
           this.loading9 = false;
           this.$message({ message: err, type: "error" });
           this.$message({ message: err, type: "error" });
         },
         },
@@ -1499,12 +1705,6 @@ export default {
       const sourceData = this.deptHoursTabMode === "top" ? this.deptHoursData : this.deptHoursDataAll;
       const sourceData = this.deptHoursTabMode === "top" ? this.deptHoursData : this.deptHoursDataAll;
       const chartData = (sourceData || []).filter((item) => Number(item.workingTime || 0) > 0);
       const chartData = (sourceData || []).filter((item) => Number(item.workingTime || 0) > 0);
 
 
-      const rowHeight = 28;
-      const minHeight = 300;
-      const height = Math.max(minHeight, chartData.length * rowHeight + 100);
-      el.style.height = height + "px";
-      this.chart3 = echarts.init(el, null, { width, height });
-
       const depts = chartData.map((item) => item.departmentName);
       const depts = chartData.map((item) => item.departmentName);
       const totalHours = chartData.map((item) =>
       const totalHours = chartData.map((item) =>
         Number((item.workingTime || 0).toFixed(2)),
         Number((item.workingTime || 0).toFixed(2)),
@@ -1518,6 +1718,140 @@ export default {
         deptOpenIdMap3[item.departmentName] = item._deptOpenId || null;
         deptOpenIdMap3[item.departmentName] = item._deptOpenId || null;
       });
       });
 
 
+      const rowHeight = 28;
+      const minHeight = 300;
+      const isGroupMode = this.deptHoursTabMode !== "top";
+
+      if (isGroupMode) {
+        el.innerHTML = "";
+        const height = Math.max(minHeight, chartData.length * rowHeight + 100);
+        el.style.height = height + "px";
+        this.chart3 = echarts.init(el, null, { width, height });
+
+        const barMax = totalHours.length ? Math.max(...totalHours) : 0;
+        const lineMax = avgHours.length ? Math.max(...avgHours) : 0;
+        const xAxis0Max = Math.ceil(barMax * 1.15) || 10;
+        const xAxis1Max = Math.ceil(lineMax * 1.15) || 10;
+        const needTranslate = this.needWxOpenData;
+        const deptNameHtml = this.deptNameHtml.bind(this);
+
+        const option = {
+          tooltip: {
+            trigger: "axis",
+            axisPointer: { type: "shadow" },
+            enterable: needTranslate,
+            formatter: (params) => {
+              const deptName = params[0].name;
+              const openId = deptOpenIdMap3[deptName] || null;
+              let result = deptNameHtml(deptName, openId) + "<br/>";
+              params.forEach((p) => {
+                result += p.marker + p.seriesName + ": " + p.value + " h<br/>";
+              });
+              return result;
+            },
+          },
+          legend: {
+            data: ["总时长", "人均工时"],
+            top: 10,
+            textStyle: { fontSize: 11 },
+          },
+          grid: {
+            left: 86,
+            right: 72,
+            top: 50,
+            bottom: 36,
+            containLabel: true,
+          },
+          xAxis: [
+            {
+              type: "value",
+              name: "总时长",
+              max: xAxis0Max,
+              axisLabel: { formatter: "{value} h" },
+            },
+            {
+              type: "value",
+              name: "人均工时",
+              max: xAxis1Max,
+              position: "top",
+              axisLabel: { formatter: "{value} h" },
+            },
+          ],
+          yAxis: {
+            type: "category",
+            data: depts,
+            inverse: true,
+            axisLabel: {
+              interval: 0,
+              fontSize: 11,
+              margin: 12,
+            },
+          },
+          series: [
+            {
+              name: "总时长",
+              type: "bar",
+              xAxisIndex: 0,
+              data: totalHours,
+              itemStyle: { color: "#5470C6" },
+              barMaxWidth: 18,
+              label: {
+                normal: {
+                  show: true,
+                  position: "right",
+                  distance: 4,
+                  formatter: (params) => {
+                    const val = Number(params.value || 0);
+                    return val === 0 ? "" : val + " h";
+                  },
+                  color: "#1f2d3d",
+                  fontSize: 10,
+                  fontWeight: "600",
+                  backgroundColor: "rgba(255,255,255,0.85)",
+                  borderColor: "#ddd",
+                  borderWidth: 1,
+                  borderRadius: 3,
+                  padding: [1, 3],
+                },
+              },
+            },
+            {
+              name: "人均工时",
+              type: "line",
+              xAxisIndex: 1,
+              data: avgHours,
+              itemStyle: { color: "#FF7F0E" },
+              lineStyle: { width: 2 },
+              symbol: "circle",
+              symbolSize: 6,
+              label: {
+                normal: {
+                  show: true,
+                  position: "right",
+                  distance: 6,
+                  formatter: (params) => {
+                    const val = Number(params.value || 0);
+                    return val === 0 ? "" : val + " h";
+                  },
+                  fontSize: 10,
+                  color: "#303133",
+                  backgroundColor: "rgba(255,255,255,0.78)",
+                  padding: [1, 3],
+                  borderRadius: 3,
+                },
+              },
+            },
+          ],
+        };
+        this.chart3.setOption(option, true);
+        this.chart3.resize({ width, height });
+        return;
+      }
+
+      const height = Math.max(minHeight, chartData.length * rowHeight + 100);
+      el.style.height = height + "px";
+      this.chart3 = echarts.init(el, null, { width, height });
+
       const option = this.buildBarLineOption({
       const option = this.buildBarLineOption({
         names: depts,
         names: depts,
         barSeries: [{ name: "总时长", data: totalHours, color: "#5470C6" }],
         barSeries: [{ name: "总时长", data: totalHours, color: "#5470C6" }],
@@ -1741,34 +2075,128 @@ export default {
         this.chart6 = null;
         this.chart6 = null;
       }
       }
       const el = this.$refs.chart6;
       const el = this.$refs.chart6;
+      el.innerHTML = "";
       const parentEl = el.parentElement;
       const parentEl = el.parentElement;
       const width = parentEl ? parentEl.clientWidth - 32 : el.clientWidth;
       const width = parentEl ? parentEl.clientWidth - 32 : el.clientWidth;
-      this.chart6 = echarts.init(el, null, { width, height: 380 });
-      const names = this.top3OvertimeProjectDept.map(
-        (item) => item.departmentName,
-      );
-      const overtimeHours = this.top3OvertimeProjectDept.map((item) =>
-        Number(item.overtimeHours || 0),
-      );
-      const workingTimes = this.top3OvertimeProjectDept.map((item) =>
-        Number(item.workingTime || 0),
+      const valueKey = "overtimeHours";
+      const chartData = (this.top3OvertimeProjectDept || []).filter((dept) =>
+        (dept.items || []).some((item) => Number(item[valueKey] || 0) > 0),
       );
       );
+      const rowHeight = 28;
+      const minHeight = 300;
+      const height = Math.max(minHeight, chartData.length * rowHeight + 100);
+      el.style.height = height + "px";
+      this.chart6 = echarts.init(el, null, { width, height });
+      const depts = chartData.map((item) => item.departmentName);
       const deptOpenIdMap6 = {};
       const deptOpenIdMap6 = {};
-      this.top3OvertimeProjectDept.forEach((item) => {
-        deptOpenIdMap6[item.departmentName] = item._deptOpenId || null;
+      chartData.forEach((item) => {
+        const openId = item._deptOpenId || this.getDeptOpenId(item);
+        deptOpenIdMap6[item.departmentName] = openId || null;
       });
       });
-      const option = this.buildBarLineOption({
-        names,
-        barSeries: [
-          { name: "加班总时长", data: overtimeHours, color: "#5470C6" },
-        ],
-        lineSeries: [{ name: "总工时", data: workingTimes, color: "#FF7F0E" }],
-        yAxisNames: ["加班总时长", "总工时"],
-        visibleSize: 8,
-        deptOpenIdMap: deptOpenIdMap6,
+      const projectMap = new Map();
+      chartData.forEach((dept) => {
+        (dept.items || []).forEach((project) => {
+          const projectId = project.projectId || project.projectCode || project.projectName;
+          if (!projectMap.has(projectId)) {
+            projectMap.set(projectId, {
+              id: projectId,
+              name: project.projectCode || project.projectName || "未关联项目",
+              rank: Number(project.projectRank || 0),
+            });
+          }
+        });
       });
       });
-      this.chart6.setOption(option);
-      this.chart6.resize({ width, height: 380 });
+      const projects = Array.from(projectMap.values()).sort(
+        (left, right) => left.rank - right.rank,
+      );
+      const maxValue = Math.max(
+        ...chartData.flatMap((dept) =>
+          (dept.items || []).map((project) => Number(project[valueKey] || 0)),
+        ),
+        0,
+      );
+      const xAxisMax = Math.ceil(maxValue * 1.15) || 10;
+      const needTranslate = this.needWxOpenData;
+      const deptNameHtml = this.deptNameHtml.bind(this);
+
+      const option = {
+        tooltip: {
+          trigger: "axis",
+          axisPointer: { type: "shadow" },
+          enterable: needTranslate,
+          formatter: (params) => {
+            const deptName = params[0].name;
+            const openId = deptOpenIdMap6[deptName] || null;
+            let result = deptNameHtml(deptName, openId) + "<br/>";
+            params.forEach((p) => {
+              const value = Number(p.value || 0);
+              if (!value) return;
+              result += p.marker + p.seriesName + ": " + value + " h<br/>";
+            });
+            return result;
+          },
+        },
+        legend: {
+          data: projects.map((project) => project.name),
+          bottom: 8,
+          textStyle: { fontSize: 11 },
+        },
+        grid: {
+          left: 86,
+          right: 112,
+          top: 36,
+          bottom: 52,
+          containLabel: true,
+        },
+        xAxis: {
+          type: "value",
+          max: xAxisMax,
+          axisLabel: { formatter: "{value} h" },
+        },
+        yAxis: {
+          type: "category",
+          data: depts,
+          inverse: true,
+          axisLabel: {
+            interval: 0,
+            fontSize: 11,
+            margin: 12,
+          },
+        },
+        series: projects.map((project) => ({
+          name: project.name,
+          type: "bar",
+          data: chartData.map((dept) => {
+            const matched = (dept.items || []).find(
+              (item) => (item.projectId || item.projectCode || item.projectName) === project.id,
+            );
+            return matched ? Number(matched[valueKey] || 0) : 0;
+          }),
+          barMaxWidth: 14,
+          label: {
+            normal: {
+              show: true,
+              position: "right",
+              distance: 4,
+              formatter: (params) => {
+                const val = Number(params.value);
+                if (val === 0) return "";
+                return val + " h";
+              },
+              color: "#1f2d3d",
+              fontSize: 10,
+              fontWeight: "600",
+              backgroundColor: "rgba(255,255,255,0.85)",
+              borderColor: "#ddd",
+              borderWidth: 1,
+              borderRadius: 3,
+              padding: [1, 3],
+            },
+          },
+        })),
+      };
+      this.chart6.setOption(option, true);
+      this.chart6.resize({ width, height });
     },
     },
 
 
     renderChart7() {
     renderChart7() {
@@ -1953,12 +2381,13 @@ export default {
     },
     },
 
 
     handleResize() {
     handleResize() {
+      this.updateOpenDatePickers();
       if (this.top10Data.length > 0) this.renderChart1();
       if (this.top10Data.length > 0) this.renderChart1();
       if (this.top3DeptData.length > 0) this.renderChart2();
       if (this.top3DeptData.length > 0) this.renderChart2();
       if (this.deptHoursData.length > 0) this.renderChart3();
       if (this.deptHoursData.length > 0) this.renderChart3();
       if (this.deptProjectCountData.length > 0) this.renderChart4();
       if (this.deptProjectCountData.length > 0) this.renderChart4();
       if (this.projectOvertimeTop5.length > 0) this.renderChart5();
       if (this.projectOvertimeTop5.length > 0) this.renderChart5();
-      if (this.top3OvertimeProjectDept.length > 0) this.renderChart6();
+      if (this.hasTop3OvertimeChartData()) this.renderChart6();
       if (this.deptOvertimeSummary.length > 0) this.renderChart7();
       if (this.deptOvertimeSummary.length > 0) this.renderChart7();
       if (this.userOvertimeTop10.length > 0) this.renderChart8();
       if (this.userOvertimeTop10.length > 0) this.renderChart8();
       if (this.userProjectTop10Data.length > 0) this.renderChart9();
       if (this.userProjectTop10Data.length > 0) this.renderChart9();
@@ -1996,7 +2425,8 @@ export default {
   box-shadow: 0 10px 28px rgba(31, 45, 61, 0.08);
   box-shadow: 0 10px 28px rgba(31, 45, 61, 0.08);
   box-sizing: border-box;
   box-sizing: border-box;
   position: relative;
   position: relative;
-  overflow: hidden;
+  overflow: visible;
+  gap: 16px;
 }
 }
 
 
 .dashboard-header::before {
 .dashboard-header::before {
@@ -2009,7 +2439,63 @@ export default {
   background: linear-gradient(180deg, #409eff, #67c23a);
   background: linear-gradient(180deg, #409eff, #67c23a);
 }
 }
 
 
+.dashboard-filter {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  flex-shrink: 0;
+  margin-left: auto;
+}
+
+.dashboard-date-picker {
+  flex-shrink: 0;
+  position: relative;
+  display: block;
+  box-sizing: border-box;
+}
+
+.dashboard-date-picker.is-month {
+  width: 220px;
+}
+
+.dashboard-date-picker.is-range {
+  width: 350px;
+}
+
+.dashboard-date-picker-slot {
+  width: 100%;
+}
+
+.dashboard-date-picker ::v-deep .dashboard-date-picker-input.el-date-editor {
+  width: 100% !important;
+  max-width: 100%;
+  box-sizing: border-box;
+  vertical-align: middle;
+}
+
+.dashboard-date-picker.is-month ::v-deep .el-input__inner {
+  padding-left: 30px;
+  padding-right: 10px;
+}
+
+.dashboard-date-picker.is-range ::v-deep .el-range-editor.el-input__inner {
+  padding: 3px 10px;
+}
+
+.dashboard-date-picker.is-range ::v-deep .el-range-input {
+  width: 39%;
+  font-size: 12px;
+}
+
+.dashboard-date-picker.is-range ::v-deep .el-range-separator {
+  width: auto;
+  padding: 0 4px;
+  line-height: 24px;
+}
+
 .dashboard-heading {
 .dashboard-heading {
+  flex: 1;
+  min-width: 0;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   gap: 5px;
   gap: 5px;
@@ -2346,6 +2832,19 @@ export default {
     gap: 12px;
     gap: 12px;
   }
   }
 
 
+  .dashboard-filter {
+    width: 100%;
+    margin-left: 0;
+    justify-content: flex-start;
+    flex-wrap: wrap;
+  }
+
+  .dashboard-date-picker.is-month,
+  .dashboard-date-picker.is-range {
+    width: 100%;
+    max-width: 350px;
+  }
+
   .charts-grid,
   .charts-grid,
   .analysis-grid {
   .analysis-grid {
     grid-template-columns: 1fr;
     grid-template-columns: 1fr;
@@ -2356,3 +2855,10 @@ export default {
   }
   }
 }
 }
 </style>
 </style>
+
+<style lang="scss">
+.dashboard-date-picker-popper {
+  z-index: 3000 !important;
+  margin: 0 !important;
+}
+</style>