yusm 1 هفته پیش
والد
کامیت
bbef04e914

+ 62 - 6
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/impl/ReportServiceImpl.java

@@ -16191,7 +16191,7 @@ public class ReportServiceImpl extends ServiceImpl<ReportMapper, Report> impleme
         HttpRespMsg msg = new HttpRespMsg();
         String month = normalizeYearMonth(ymonth);
         List<Map<String, Object>> topProjectList = reportMapper.selectMaps(new QueryWrapper<Report>()
-                .select("project_id as projectId")
+                .select("project_id as projectId", "sum(working_time) as workingTime")
                 .eq("company_id", companyId)
                 .eq("state", 1)
                 .apply("date_format(create_date, '%Y-%m') = {0}", month)
@@ -16206,17 +16206,73 @@ public class ReportServiceImpl extends ServiceImpl<ReportMapper, Report> impleme
             msg.data = new ArrayList<>();
             return msg;
         }
+        Map<Integer, Integer> projectRankMap = new HashMap<>();
+        for (int i = 0; i < projectIds.size(); i++) {
+            projectRankMap.put(projectIds.get(i), i);
+        }
 
-        List<Map<String, Object>> deptReportList = reportMapper.selectMaps(new QueryWrapper<Report>()
-                .select("dept_id as deptId", "creator_id as userId", "sum(working_time) as workingTime", "sum(ifnull(overtime_hours, 0)) as overtimeHours")
+        List<Map<String, Object>> projectDeptReportList = reportMapper.selectMaps(new QueryWrapper<Report>()
+                .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("state", 1)
                 .apply("date_format(create_date, '%Y-%m') = {0}", month)
                 .in("project_id", projectIds)
-                .groupBy("dept_id", "creator_id")
-                .orderByDesc("sum(working_time)"));
+                .groupBy("dept_id", "project_id"));
         Map<Integer, Department> departmentMap = getDepartmentMap(companyId);
-        List<HashMap> resultList = buildDeptReportResult(rollupDeptReportRows(deptReportList, departmentMap), departmentMap, false);
+        Map<String, HashMap<String, Object>> projectDeptMap = new LinkedHashMap<>();
+        fillProjectInfo(companyId, projectDeptReportList).forEach(item -> {
+            Integer deptId = toInteger(item.get("deptId"));
+            Integer topDeptId = getTopDepartmentId(deptId, departmentMap);
+            Integer projectId = toInteger(item.get("projectId"));
+            String key = (topDeptId == null ? -1 : topDeptId) + "_" + projectId;
+            HashMap<String, Object> target = projectDeptMap.computeIfAbsent(key, groupKey -> {
+                HashMap<String, Object> map = new HashMap<>();
+                map.putAll(item);
+                map.put("deptId", topDeptId);
+                fillDepartmentInfo(map, topDeptId, departmentMap);
+                map.put("projectRank", projectRankMap.getOrDefault(projectId, Integer.MAX_VALUE));
+                map.put("workingTime", 0D);
+                map.put("overtimeHours", 0D);
+                return map;
+            });
+            target.put("workingTime", roundHours(toDouble(target.get("workingTime")) + toDouble(item.get("workingTime"))));
+            target.put("overtimeHours", roundHours(toDouble(target.get("overtimeHours")) + toDouble(item.get("overtimeHours"))));
+        });
+
+        Map<Integer, HashMap<String, Object>> deptGroupMap = new LinkedHashMap<>();
+        departmentMap.values().stream()
+                .filter(department -> department.getDepartmentId() != null)
+                .filter(department -> department.getDepartmentId().equals(getTopDepartmentId(department.getDepartmentId(), departmentMap)))
+                .filter(department -> !Integer.valueOf(1).equals(department.getCorpwxDeptid()))
+                .sorted(Comparator
+                        .comparing((Department department) -> department.getSeq(), Comparator.nullsLast(Integer::compareTo))
+                        .thenComparing(Department::getDepartmentId))
+                .forEach(department -> {
+                    HashMap<String, Object> map = new HashMap<>();
+                    fillDepartmentInfo(map, department.getDepartmentId(), departmentMap);
+                    map.put("workingTime", 0D);
+                    map.put("overtimeHours", 0D);
+                    map.put("items", new ArrayList<HashMap>());
+                    deptGroupMap.put(department.getDepartmentId(), map);
+                });
+        projectDeptMap.values().stream()
+                .sorted(Comparator.comparingInt(item -> toInteger(item.get("projectRank")) == null ? Integer.MAX_VALUE : toInteger(item.get("projectRank"))))
+                .forEach(item -> {
+                    Integer deptId = toInteger(item.get("departmentId"));
+                    Integer key = deptId == null ? -1 : deptId;
+                    HashMap<String, Object> deptMap = deptGroupMap.computeIfAbsent(key, groupKey -> {
+                        HashMap<String, Object> map = new HashMap<>();
+                        fillDepartmentInfo(map, key == -1 ? null : key, departmentMap);
+                        map.put("workingTime", 0D);
+                        map.put("overtimeHours", 0D);
+                        map.put("items", new ArrayList<HashMap>());
+                        return map;
+                    });
+                    deptMap.put("workingTime", roundHours(toDouble(deptMap.get("workingTime")) + toDouble(item.get("workingTime"))));
+                    deptMap.put("overtimeHours", roundHours(toDouble(deptMap.get("overtimeHours")) + toDouble(item.get("overtimeHours"))));
+                    ((List<HashMap>) deptMap.get("items")).add(item);
+                });
+        List<HashMap> resultList = new ArrayList<>(deptGroupMap.values());
         msg.data = resultList;
         return msg;
     }

+ 173 - 33
fhKeeper/formulahousekeeper/timesheet/src/views/dashboard/index.vue

@@ -1,4 +1,4 @@
-<template>
+<template>
   <div class="dashboard-container" ref="dashboardContainer">
     <!-- 顶部标题和日期选择 -->
     <div class="dashboard-header">
@@ -137,7 +137,7 @@
             <el-radio-group
               v-model="top3PieMode"
               size="mini"
-              :disabled="loading2 || top3DeptData.length === 0"
+              :disabled="loading2 || !hasTop3DeptChartData()"
               @change="renderChart2"
             >
               <el-radio-button label="working">总工时</el-radio-button>
@@ -149,7 +149,7 @@
           <i class="el-icon-loading"></i> 加载中...
         </div>
         <div
-          v-else-if="top3DeptData.length === 0"
+          v-else-if="!hasTop3DeptChartData()"
           class="chart-empty project-rank-loading"
         >
           暂无数据
@@ -661,6 +661,7 @@ export default {
 
     // ========== 数据加载 ==========
     loadAllCharts() {
+      this.disposeDashboardCharts();
       this.loadTop10Project();
       this.loadTop3ProjectDept();
       this.loadDeptHours();
@@ -669,13 +670,36 @@ export default {
       this.loadUserProjectTop10();
     },
 
+    disposeDashboardCharts() {
+      for (let i = 1; i <= 9; i++) {
+        const chart = this[`chart${i}`];
+        if (chart) {
+          chart.dispose();
+          this[`chart${i}`] = null;
+        }
+      }
+    },
+
+    hasTop3DeptChartData() {
+      const valueKey = this.top3PieMode === "overtime" ? "overtimeHours" : "workingTime";
+      return (this.top3DeptData || []).some((dept) =>
+        (dept.items || []).some((item) => Number(item[valueKey] || 0) > 0),
+      );
+    },
+
+    isCurrentMonthRequest(month) {
+      return month === this.selectedMonth;
+    },
+
     loadTop10Project() {
+      const requestMonth = this.selectedMonth;
       this.loading1 = true;
       this.top10Data = [];
       this.http.post(
         "/report/getTop10ProjectReport",
-        { ymonth: this.selectedMonth },
+        { ymonth: requestMonth },
         (res) => {
+          if (!this.isCurrentMonthRequest(requestMonth)) return;
           this.loading1 = false;
           if (res.code === "ok") {
             this.top10Data = res.data || [];
@@ -685,6 +709,7 @@ export default {
           }
         },
         (err) => {
+          if (!this.isCurrentMonthRequest(requestMonth)) return;
           this.loading1 = false;
           this.$message({ message: err, type: "error" });
         },
@@ -692,12 +717,14 @@ export default {
     },
 
     loadTop3ProjectDept() {
+      const requestMonth = this.selectedMonth;
       this.loading2 = true;
       this.top3DeptData = [];
       this.http.post(
         "/report/getTop3ProjectReportGroupByDept",
-        { ymonth: this.selectedMonth },
+        { ymonth: requestMonth },
         (res) => {
+          if (!this.isCurrentMonthRequest(requestMonth)) return;
           this.loading2 = false;
           if (res.code === "ok") {
             const data = res.data || [];
@@ -715,6 +742,7 @@ export default {
           }
         },
         (err) => {
+          if (!this.isCurrentMonthRequest(requestMonth)) return;
           this.loading2 = false;
           this.$message({ message: err, type: "error" });
         },
@@ -722,12 +750,14 @@ export default {
     },
 
     loadDeptHours() {
+      const requestMonth = this.selectedMonth;
       this.loading3 = true;
       this.deptHoursData = [];
       this.http.post(
         "/report/getProjectReportGroupByDept",
-        { ymonth: this.selectedMonth },
+        { ymonth: requestMonth },
         (res) => {
+          if (!this.isCurrentMonthRequest(requestMonth)) return;
           this.loading3 = false;
           if (res.code === "ok") {
             const data = res.data || [];
@@ -745,6 +775,7 @@ export default {
           }
         },
         (err) => {
+          if (!this.isCurrentMonthRequest(requestMonth)) return;
           this.loading3 = false;
           this.$message({ message: err, type: "error" });
         },
@@ -752,12 +783,14 @@ export default {
     },
 
     loadDeptProjectCount() {
+      const requestMonth = this.selectedMonth;
       this.loading4 = true;
       this.deptProjectCountData = [];
       this.http.post(
         "/report/getDeptProjectCount",
-        { ymonth: this.selectedMonth },
+        { ymonth: requestMonth },
         (res) => {
+          if (!this.isCurrentMonthRequest(requestMonth)) return;
           this.loading4 = false;
           if (res.code === "ok") {
             const data = res.data || [];
@@ -775,6 +808,7 @@ export default {
           }
         },
         (err) => {
+          if (!this.isCurrentMonthRequest(requestMonth)) return;
           this.loading4 = false;
           this.$message({ message: err, type: "error" });
         },
@@ -782,12 +816,14 @@ export default {
     },
 
     loadDashboardAnalysis() {
+      const requestMonth = this.selectedMonth;
       this.loading5 = true;
       this.analysisData = {};
       this.http.post(
         "/report/getDashboardAnalysisReport",
-        { ymonth: this.selectedMonth },
+        { ymonth: requestMonth },
         (res) => {
+          if (!this.isCurrentMonthRequest(requestMonth)) return;
           this.loading5 = false;
           if (res.code === "ok") {
             this.analysisData = res.data || {};
@@ -804,6 +840,7 @@ export default {
           }
         },
         (err) => {
+          if (!this.isCurrentMonthRequest(requestMonth)) return;
           this.loading5 = false;
           this.$message({ message: err, type: "error" });
         },
@@ -811,12 +848,14 @@ export default {
     },
 
     loadUserProjectTop10() {
+      const requestMonth = this.selectedMonth;
       this.loading9 = true;
       this.userProjectTop10Data = [];
       this.http.post(
         "/report/getUserProjectTop10",
-        { ymonth: this.selectedMonth },
+        { ymonth: requestMonth },
         (res) => {
+          if (!this.isCurrentMonthRequest(requestMonth)) return;
           this.loading9 = false;
           if (res.code === "ok") {
             const data = res.data || [];
@@ -834,6 +873,7 @@ export default {
           }
         },
         (err) => {
+          if (!this.isCurrentMonthRequest(requestMonth)) return;
           this.loading9 = false;
           this.$message({ message: err, type: "error" });
         },
@@ -998,7 +1038,7 @@ export default {
           position: "top",
           distance: 4,
           formatter: (params) => {
-            const val = Number(params.value);
+            const val = Number(params.value || 0);
             if (val === 0) return "";
             return val + " " + unit;
           },
@@ -1242,39 +1282,127 @@ export default {
         this.chart2 = null;
       }
       const el = this.$refs.chart2;
+      el.innerHTML = "";
       const parentEl = el.parentElement;
       const width = parentEl ? parentEl.clientWidth - 32 : el.clientWidth;
       this.chart2 = echarts.init(el, null, { width, height: 380 });
 
       const isOvertime = this.top3PieMode === "overtime";
-      const names = this.top3DeptData.map((item) => item.departmentName);
-      const workingTimes = this.top3DeptData.map((item) =>
-        Number(item.workingTime || 0)
-      );
-      const overtimeTimes = this.top3DeptData.map((item) =>
-        Number(item.overtimeHours || 0)
+      const valueKey = isOvertime ? "overtimeHours" : "workingTime";
+      const chartData = this.top3DeptData.filter((dept) =>
+        (dept.items || []).some((item) => Number(item[valueKey] || 0) > 0),
       );
-
+      const depts = chartData.map((item) => item.departmentName);
       const deptOpenIdMap2 = {};
-      this.top3DeptData.forEach((item) => {
-        deptOpenIdMap2[item.departmentName] = item._deptOpenId || null;
+      chartData.forEach((item) => {
+        const openId = item._deptOpenId || this.getDeptOpenId(item);
+        deptOpenIdMap2[item.departmentName] = openId || null;
       });
+      const rankSeries = [
+        { rank: 0, name: "项目TOP1" },
+        { rank: 1, name: "项目TOP2" },
+        { rank: 2, name: "项目TOP3" },
+      ];
+      const getDeptRankItem = (dept, rank) =>
+        (dept.items || [])
+          .slice()
+          .sort(
+            (left, right) =>
+              Number(left.projectRank || 0) - Number(right.projectRank || 0),
+          )[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 = this.buildBarLineOption({
-        names,
-        barSeries: [
-          {
-            name: isOvertime ? "加班工时" : "总工时",
-            data: isOvertime ? overtimeTimes : workingTimes,
-            color: isOvertime ? "#FF7F0E" : "#5470C6",
+      const option = {
+        tooltip: {
+          trigger: "axis",
+          axisPointer: { type: "shadow" },
+          enterable: needTranslate,
+          formatter: (params) => {
+            const deptName = params[0].name;
+            const openId = deptOpenIdMap2[deptName] || null;
+            let result = deptNameHtml(deptName, openId) + "<br/>";
+            params.forEach((p) => {
+              const item = p.data || {};
+              const value = Number(item.value || 0);
+              if (!value) return;
+              const projectText = item.projectCode || item.projectName || p.seriesName;
+              result += p.marker + projectText + ": " + value + " h<br/>";
+            });
+            return result;
           },
-        ],
-        lineSeries: [],
-        yAxisNames: [isOvertime ? "加班工时" : "总工时", ""],
-        visibleSize: Number.MAX_SAFE_INTEGER,
-        deptOpenIdMap: deptOpenIdMap2,
-      });
-      this.chart2.setOption(option);
+        },
+        legend: {
+          data: rankSeries.map((series) => series.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: rankSeries.map((rankConfig) => ({
+          name: rankConfig.name,
+          type: "bar",
+          data: chartData.map((dept) => {
+            const matched = getDeptRankItem(dept, rankConfig.rank);
+            return matched
+              ? {
+                  value: Number(matched[valueKey] || 0),
+                  projectCode: matched.projectCode,
+                  projectName: matched.projectName,
+                }
+              : { value: 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.chart2.setOption(option, true);
       this.chart2.resize({ width, height: 380 });
     },
 
@@ -1929,6 +2057,7 @@ export default {
   color: #8b9bad;
   font-size: 14px;
   height: 300px;
+  line-height: 24px;
   border-radius: 10px;
   background: repeating-linear-gradient(
     45deg,
@@ -1939,6 +2068,17 @@ export default {
   );
 }
 
+.chart-loading .el-icon-loading {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 18px;
+  height: 18px;
+  line-height: 18px;
+  margin-right: 6px;
+  overflow: visible;
+}
+
 .analysis-section {
   margin-top: 18px;
   background: rgba(255, 255, 255, 0.96);